diff options
Diffstat (limited to 'vendor/github.com/authzed/spicedb/pkg/tuple')
| -rw-r--r-- | vendor/github.com/authzed/spicedb/pkg/tuple/comparison.go | 48 | ||||
| -rw-r--r-- | vendor/github.com/authzed/spicedb/pkg/tuple/core.go | 132 | ||||
| -rw-r--r-- | vendor/github.com/authzed/spicedb/pkg/tuple/doc.go | 2 | ||||
| -rw-r--r-- | vendor/github.com/authzed/spicedb/pkg/tuple/hashing.go | 108 | ||||
| -rw-r--r-- | vendor/github.com/authzed/spicedb/pkg/tuple/onr.go | 75 | ||||
| -rw-r--r-- | vendor/github.com/authzed/spicedb/pkg/tuple/parsing.go | 228 | ||||
| -rw-r--r-- | vendor/github.com/authzed/spicedb/pkg/tuple/strings.go | 165 | ||||
| -rw-r--r-- | vendor/github.com/authzed/spicedb/pkg/tuple/structs.go | 219 | ||||
| -rw-r--r-- | vendor/github.com/authzed/spicedb/pkg/tuple/updates.go | 22 | ||||
| -rw-r--r-- | vendor/github.com/authzed/spicedb/pkg/tuple/v1.go | 340 |
10 files changed, 1339 insertions, 0 deletions
diff --git a/vendor/github.com/authzed/spicedb/pkg/tuple/comparison.go b/vendor/github.com/authzed/spicedb/pkg/tuple/comparison.go new file mode 100644 index 0000000..159edd9 --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/tuple/comparison.go @@ -0,0 +1,48 @@ +package tuple + +import ( + "time" + + "google.golang.org/protobuf/proto" + + core "github.com/authzed/spicedb/pkg/proto/core/v1" +) + +// ONREqual checks if two ObjectAndRelation instances are equal. +func ONREqual(lhs, rhs ObjectAndRelation) bool { + return lhs == rhs +} + +// ONREqualOrWildcard checks if an ObjectAndRelation matches another ObjectAndRelation or is a wildcard. +func ONREqualOrWildcard(onr, target ObjectAndRelation) bool { + return ONREqual(onr, target) || (onr.ObjectID == PublicWildcard && onr.ObjectType == target.ObjectType) +} + +// Equal returns true if the two relationships are exactly the same. +func Equal(lhs, rhs Relationship) bool { + return ONREqual(lhs.Resource, rhs.Resource) && ONREqual(lhs.Subject, rhs.Subject) && caveatEqual(lhs.OptionalCaveat, rhs.OptionalCaveat) && expirationEqual(lhs.OptionalExpiration, rhs.OptionalExpiration) +} + +func expirationEqual(lhs, rhs *time.Time) bool { + if lhs == nil && rhs == nil { + return true + } + + if lhs == nil || rhs == nil { + return false + } + + return lhs.Equal(*rhs) +} + +func caveatEqual(lhs, rhs *core.ContextualizedCaveat) bool { + if lhs == nil && rhs == nil { + return true + } + + if lhs == nil || rhs == nil { + return false + } + + return lhs.CaveatName == rhs.CaveatName && proto.Equal(lhs.Context, rhs.Context) +} diff --git a/vendor/github.com/authzed/spicedb/pkg/tuple/core.go b/vendor/github.com/authzed/spicedb/pkg/tuple/core.go new file mode 100644 index 0000000..d71a016 --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/tuple/core.go @@ -0,0 +1,132 @@ +package tuple + +import ( + "time" + + core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/spiceerrors" +) + +// ONRStringToCore creates an ONR from string pieces. +func ONRStringToCore(ns, oid, rel string) *core.ObjectAndRelation { + spiceerrors.DebugAssert(func() bool { + return ns != "" && oid != "" && rel != "" + }, "namespace, object ID, and relation must not be empty") + + return &core.ObjectAndRelation{ + Namespace: ns, + ObjectId: oid, + Relation: rel, + } +} + +// CoreRelationToStringWithoutCaveatOrExpiration creates a string from a core.RelationTuple without stringifying the caveat. +func CoreRelationToStringWithoutCaveatOrExpiration(rel *core.RelationTuple) string { + if rel.Subject.Relation == Ellipsis { + return rel.ResourceAndRelation.Namespace + ":" + rel.ResourceAndRelation.ObjectId + "@" + rel.Subject.Namespace + ":" + rel.Subject.ObjectId + } + + return rel.ResourceAndRelation.Namespace + ":" + rel.ResourceAndRelation.ObjectId + "@" + rel.Subject.Namespace + ":" + rel.Subject.ObjectId + "#" + rel.ResourceAndRelation.Relation +} + +// CoreRelationToString creates a string from a core.RelationTuple. +func CoreRelationToString(rel *core.RelationTuple) (string, error) { + return String(FromCoreRelationTuple(rel)) +} + +// MustCoreRelationToString creates a string from a core.RelationTuple and panics if it can't. +func MustCoreRelationToString(rel *core.RelationTuple) string { + return MustString(FromCoreRelationTuple(rel)) +} + +// RRStringToCore creates a RelationReference from the string pieces. +func RRStringToCore(namespaceName string, relationName string) *core.RelationReference { + spiceerrors.DebugAssert(func() bool { + return namespaceName != "" && relationName != "" + }, "namespace and relation must not be empty") + + return &core.RelationReference{ + Namespace: namespaceName, + Relation: relationName, + } +} + +// FromCoreRelationTuple creates a Relationship from a core.RelationTuple. +func FromCoreRelationTuple(rt *core.RelationTuple) Relationship { + spiceerrors.DebugAssert(func() bool { + return rt.Validate() == nil + }, "relation tuple must be valid") + + var expiration *time.Time + if rt.OptionalExpirationTime != nil { + t := rt.OptionalExpirationTime.AsTime() + expiration = &t + } + + return Relationship{ + RelationshipReference: RelationshipReference{ + Resource: ObjectAndRelation{ + ObjectType: rt.ResourceAndRelation.Namespace, + ObjectID: rt.ResourceAndRelation.ObjectId, + Relation: rt.ResourceAndRelation.Relation, + }, + Subject: ObjectAndRelation{ + ObjectType: rt.Subject.Namespace, + ObjectID: rt.Subject.ObjectId, + Relation: rt.Subject.Relation, + }, + }, + OptionalCaveat: rt.Caveat, + OptionalExpiration: expiration, + } +} + +// FromCoreObjectAndRelation creates an ObjectAndRelation from a core.ObjectAndRelation. +func FromCoreObjectAndRelation(oar *core.ObjectAndRelation) ObjectAndRelation { + spiceerrors.DebugAssert(func() bool { + return oar.Validate() == nil + }, "object and relation must be valid") + + return ObjectAndRelation{ + ObjectType: oar.Namespace, + ObjectID: oar.ObjectId, + Relation: oar.Relation, + } +} + +// CoreONR creates a core ObjectAndRelation from the string pieces. +func CoreONR(namespace, objectID, relation string) *core.ObjectAndRelation { + spiceerrors.DebugAssert(func() bool { + return namespace != "" && objectID != "" && relation != "" + }, "namespace, object ID, and relation must not be empty") + + return &core.ObjectAndRelation{ + Namespace: namespace, + ObjectId: objectID, + Relation: relation, + } +} + +// CoreRR creates a core RelationReference from the string pieces. +func CoreRR(namespace, relation string) *core.RelationReference { + spiceerrors.DebugAssert(func() bool { + return namespace != "" && relation != "" + }, "namespace and relation must not be empty") + + return &core.RelationReference{ + Namespace: namespace, + Relation: relation, + } +} + +// FromCoreRelationReference creates a RelationReference from a core.RelationReference. +func FromCoreRelationReference(rr *core.RelationReference) RelationReference { + spiceerrors.DebugAssert(func() bool { + return rr.Validate() == nil + }, "relation reference must be valid") + + return RelationReference{ + ObjectType: rr.Namespace, + Relation: rr.Relation, + } +} diff --git a/vendor/github.com/authzed/spicedb/pkg/tuple/doc.go b/vendor/github.com/authzed/spicedb/pkg/tuple/doc.go new file mode 100644 index 0000000..b86f0d2 --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/tuple/doc.go @@ -0,0 +1,2 @@ +// Package tuple provides ways to convert to and from proto structs to Go structs that can extend the core functionality. +package tuple diff --git a/vendor/github.com/authzed/spicedb/pkg/tuple/hashing.go b/vendor/github.com/authzed/spicedb/pkg/tuple/hashing.go new file mode 100644 index 0000000..1f733d4 --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/tuple/hashing.go @@ -0,0 +1,108 @@ +package tuple + +import ( + "bytes" + "fmt" + "sort" + "time" + + "google.golang.org/protobuf/types/known/structpb" + + "github.com/authzed/spicedb/pkg/spiceerrors" +) + +// CanonicalBytes converts a tuple to a canonical set of bytes. +// Can be used for hashing purposes. +func CanonicalBytes(rel Relationship) ([]byte, error) { + spiceerrors.DebugAssert(rel.ValidateNotEmpty, "relationship must not be empty") + + var sb bytes.Buffer + sb.WriteString(rel.Resource.ObjectType) + sb.WriteString(":") + sb.WriteString(rel.Resource.ObjectID) + sb.WriteString("#") + sb.WriteString(rel.Resource.Relation) + sb.WriteString("@") + sb.WriteString(rel.Subject.ObjectType) + sb.WriteString(":") + sb.WriteString(rel.Subject.ObjectID) + sb.WriteString("#") + sb.WriteString(rel.Subject.Relation) + + if rel.OptionalCaveat != nil && rel.OptionalCaveat.CaveatName != "" { + sb.WriteString(" with ") + sb.WriteString(rel.OptionalCaveat.CaveatName) + + if rel.OptionalCaveat.Context != nil && len(rel.OptionalCaveat.Context.Fields) > 0 { + sb.WriteString(":") + if err := writeCanonicalContext(&sb, rel.OptionalCaveat.Context); err != nil { + return nil, err + } + } + } + + if rel.OptionalExpiration != nil { + sb.WriteString(" with $expiration:") + truncated := rel.OptionalExpiration.UTC().Truncate(time.Second) + sb.WriteString(truncated.Format(expirationFormat)) + } + + return sb.Bytes(), nil +} + +func writeCanonicalContext(sb *bytes.Buffer, context *structpb.Struct) error { + sb.WriteString("{") + for i, key := range sortedContextKeys(context.Fields) { + if i > 0 { + sb.WriteString(",") + } + sb.WriteString(key) + sb.WriteString(":") + if err := writeCanonicalContextValue(sb, context.Fields[key]); err != nil { + return err + } + } + sb.WriteString("}") + return nil +} + +func writeCanonicalContextValue(sb *bytes.Buffer, value *structpb.Value) error { + switch value.Kind.(type) { + case *structpb.Value_NullValue: + sb.WriteString("null") + case *structpb.Value_NumberValue: + sb.WriteString(fmt.Sprintf("%f", value.GetNumberValue())) + case *structpb.Value_StringValue: + sb.WriteString(value.GetStringValue()) + case *structpb.Value_BoolValue: + sb.WriteString(fmt.Sprintf("%t", value.GetBoolValue())) + case *structpb.Value_StructValue: + if err := writeCanonicalContext(sb, value.GetStructValue()); err != nil { + return err + } + case *structpb.Value_ListValue: + sb.WriteString("[") + for i, elem := range value.GetListValue().Values { + if i > 0 { + sb.WriteString(",") + } + if err := writeCanonicalContextValue(sb, elem); err != nil { + return err + } + } + sb.WriteString("]") + default: + return spiceerrors.MustBugf("unknown structpb.Value type: %T", value.Kind) + } + + return nil +} + +func sortedContextKeys(fields map[string]*structpb.Value) []string { + keys := make([]string, 0, len(fields)) + for key := range fields { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} diff --git a/vendor/github.com/authzed/spicedb/pkg/tuple/onr.go b/vendor/github.com/authzed/spicedb/pkg/tuple/onr.go new file mode 100644 index 0000000..83ad512 --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/tuple/onr.go @@ -0,0 +1,75 @@ +package tuple + +import ( + "fmt" + "regexp" + "slices" +) + +var ( + onrRegex = regexp.MustCompile(fmt.Sprintf("^%s$", onrExpr)) + subjectRegex = regexp.MustCompile(fmt.Sprintf("^%s$", subjectExpr)) +) + +var ( + onrSubjectRelIndex = slices.Index(subjectRegex.SubexpNames(), "subjectRel") + onrSubjectTypeIndex = slices.Index(subjectRegex.SubexpNames(), "subjectType") + onrSubjectIDIndex = slices.Index(subjectRegex.SubexpNames(), "subjectID") + onrResourceTypeIndex = slices.Index(onrRegex.SubexpNames(), "resourceType") + onrResourceIDIndex = slices.Index(onrRegex.SubexpNames(), "resourceID") + onrResourceRelIndex = slices.Index(onrRegex.SubexpNames(), "resourceRel") +) + +// ParseSubjectONR converts a string representation of a Subject ONR to an ObjectAndRelation. Unlike +// ParseONR, this method allows for objects without relations. If an object without a relation +// is given, the relation will be set to ellipsis. +func ParseSubjectONR(subjectOnr string) (ObjectAndRelation, error) { + groups := subjectRegex.FindStringSubmatch(subjectOnr) + if len(groups) == 0 { + return ObjectAndRelation{}, fmt.Errorf("invalid subject ONR: %s", subjectOnr) + } + + relation := Ellipsis + if len(groups[onrSubjectRelIndex]) > 0 { + relation = groups[onrSubjectRelIndex] + } + + return ObjectAndRelation{ + ObjectType: groups[onrSubjectTypeIndex], + ObjectID: groups[onrSubjectIDIndex], + Relation: relation, + }, nil +} + +// MustParseSubjectONR converts a string representation of a Subject ONR to an ObjectAndRelation. +// Panics on error. +func MustParseSubjectONR(subjectOnr string) ObjectAndRelation { + parsed, err := ParseSubjectONR(subjectOnr) + if err != nil { + panic(err) + } + return parsed +} + +// ParseONR converts a string representation of an ONR to an ObjectAndRelation object. +func ParseONR(onr string) (ObjectAndRelation, error) { + groups := onrRegex.FindStringSubmatch(onr) + if len(groups) == 0 { + return ObjectAndRelation{}, fmt.Errorf("invalid ONR: %s", onr) + } + + return ObjectAndRelation{ + ObjectType: groups[onrResourceTypeIndex], + ObjectID: groups[onrResourceIDIndex], + Relation: groups[onrResourceRelIndex], + }, nil +} + +// MustParseONR converts a string representation of an ONR to an ObjectAndRelation object. Panics on error. +func MustParseONR(onr string) ObjectAndRelation { + parsed, err := ParseONR(onr) + if err != nil { + panic(err) + } + return parsed +} diff --git a/vendor/github.com/authzed/spicedb/pkg/tuple/parsing.go b/vendor/github.com/authzed/spicedb/pkg/tuple/parsing.go new file mode 100644 index 0000000..f2ed7d2 --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/tuple/parsing.go @@ -0,0 +1,228 @@ +package tuple + +import ( + "encoding/json" + "fmt" + "maps" + "regexp" + "slices" + "time" + + "google.golang.org/protobuf/types/known/structpb" + + "github.com/jzelinskie/stringz" + + core "github.com/authzed/spicedb/pkg/proto/core/v1" +) + +const ( + namespaceNameExpr = "([a-z][a-z0-9_]{1,61}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]" + resourceIDExpr = "([a-zA-Z0-9/_|\\-=+]{1,})" + subjectIDExpr = "([a-zA-Z0-9/_|\\-=+]{1,})|\\*" + relationExpr = "[a-z][a-z0-9_]{1,62}[a-z0-9]" + caveatNameExpr = "([a-z][a-z0-9_]{1,61}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]" +) + +var onrExpr = fmt.Sprintf( + `(?P<resourceType>(%s)):(?P<resourceID>%s)#(?P<resourceRel>%s)`, + namespaceNameExpr, + resourceIDExpr, + relationExpr, +) + +var subjectExpr = fmt.Sprintf( + `(?P<subjectType>(%s)):(?P<subjectID>%s)(#(?P<subjectRel>%s|\.\.\.))?`, + namespaceNameExpr, + subjectIDExpr, + relationExpr, +) + +var ( + caveatExpr = fmt.Sprintf(`\[(?P<caveatName>(%s))(:(?P<caveatContext>(\{(.+)\})))?\]`, caveatNameExpr) + expirationExpr = `\[expiration:(?P<expirationDateTime>([\d\-\.:TZ]+))\]` +) + +var ( + resourceIDRegex = regexp.MustCompile(fmt.Sprintf("^%s$", resourceIDExpr)) + subjectIDRegex = regexp.MustCompile(fmt.Sprintf("^%s$", subjectIDExpr)) +) + +var parserRegex = regexp.MustCompile( + fmt.Sprintf( + `^%s@%s(%s)?(%s)?$`, + onrExpr, + subjectExpr, + caveatExpr, + expirationExpr, + ), +) + +// ValidateResourceID ensures that the given resource ID is valid. Returns an error if not. +func ValidateResourceID(objectID string) error { + if !resourceIDRegex.MatchString(objectID) { + return fmt.Errorf("invalid resource id; must match %s", resourceIDExpr) + } + if len(objectID) > 1024 { + return fmt.Errorf("invalid resource id; must be <= 1024 characters") + } + + return nil +} + +// ValidateSubjectID ensures that the given object ID (under a subject reference) is valid. Returns an error if not. +func ValidateSubjectID(subjectID string) error { + if !subjectIDRegex.MatchString(subjectID) { + return fmt.Errorf("invalid subject id; must be alphanumeric and between 1 and 127 characters or a star for public") + } + if len(subjectID) > 1024 { + return fmt.Errorf("invalid resource id; must be <= 1024 characters") + } + + return nil +} + +// MustParse wraps Parse such that any failures panic rather than returning an error. +func MustParse(relString string) Relationship { + parsed, err := Parse(relString) + if err != nil { + panic(err) + } + return parsed +} + +var ( + subjectRelIndex = slices.Index(parserRegex.SubexpNames(), "subjectRel") + caveatNameIndex = slices.Index(parserRegex.SubexpNames(), "caveatName") + caveatContextIndex = slices.Index(parserRegex.SubexpNames(), "caveatContext") + resourceIDIndex = slices.Index(parserRegex.SubexpNames(), "resourceID") + subjectIDIndex = slices.Index(parserRegex.SubexpNames(), "subjectID") + resourceTypeIndex = slices.Index(parserRegex.SubexpNames(), "resourceType") + resourceRelIndex = slices.Index(parserRegex.SubexpNames(), "resourceRel") + subjectTypeIndex = slices.Index(parserRegex.SubexpNames(), "subjectType") + expirationDateTimeIndex = slices.Index(parserRegex.SubexpNames(), "expirationDateTime") +) + +// Parse unmarshals the string form of a Tuple and returns an error on failure, +// +// This function treats both missing and Ellipsis relations equally. +func Parse(relString string) (Relationship, error) { + groups := parserRegex.FindStringSubmatch(relString) + if len(groups) == 0 { + return Relationship{}, fmt.Errorf("invalid relationship string") + } + + subjectRelation := Ellipsis + if len(groups[subjectRelIndex]) > 0 { + subjectRelation = stringz.DefaultEmpty(groups[subjectRelIndex], Ellipsis) + } + + caveatName := groups[caveatNameIndex] + var optionalCaveat *core.ContextualizedCaveat + if caveatName != "" { + optionalCaveat = &core.ContextualizedCaveat{ + CaveatName: caveatName, + } + + caveatContextString := groups[caveatContextIndex] + if len(caveatContextString) > 0 { + contextMap := make(map[string]any, 1) + err := json.Unmarshal([]byte(caveatContextString), &contextMap) + if err != nil { + return Relationship{}, fmt.Errorf("invalid caveat context JSON: %w", err) + } + + caveatContext, err := structpb.NewStruct(contextMap) + if err != nil { + return Relationship{}, fmt.Errorf("invalid caveat context: %w", err) + } + + optionalCaveat.Context = caveatContext + } + } + + expirationTimeStr := groups[expirationDateTimeIndex] + var optionalExpiration *time.Time + if len(expirationTimeStr) > 0 { + expirationTime, err := time.Parse(expirationFormat, expirationTimeStr) + if err != nil { + return Relationship{}, fmt.Errorf("invalid expiration time: %w", err) + } + + optionalExpiration = &expirationTime + } + + resourceID := groups[resourceIDIndex] + if err := ValidateResourceID(resourceID); err != nil { + return Relationship{}, fmt.Errorf("invalid resource id: %w", err) + } + + subjectID := groups[subjectIDIndex] + if err := ValidateSubjectID(subjectID); err != nil { + return Relationship{}, fmt.Errorf("invalid subject id: %w", err) + } + + return Relationship{ + RelationshipReference: RelationshipReference{ + Resource: ObjectAndRelation{ + ObjectType: groups[resourceTypeIndex], + ObjectID: resourceID, + Relation: groups[resourceRelIndex], + }, + Subject: ObjectAndRelation{ + ObjectType: groups[subjectTypeIndex], + ObjectID: subjectID, + Relation: subjectRelation, + }, + }, + OptionalCaveat: optionalCaveat, + OptionalExpiration: optionalExpiration, + }, nil +} + +// MustWithExpiration adds the given expiration to the relationship. This is for testing only. +func MustWithExpiration(rel Relationship, expiration time.Time) Relationship { + rel.OptionalExpiration = &expiration + return rel +} + +// MustWithCaveat adds the given caveat name to the relationship. This is for testing only. +func MustWithCaveat(rel Relationship, caveatName string, contexts ...map[string]any) Relationship { + wc, err := WithCaveat(rel, caveatName, contexts...) + if err != nil { + panic(err) + } + return wc +} + +// WithCaveat adds the given caveat name to the relationship. This is for testing only. +func WithCaveat(rel Relationship, caveatName string, contexts ...map[string]any) (Relationship, error) { + var context *structpb.Struct + + if len(contexts) > 0 { + combined := map[string]any{} + for _, current := range contexts { + maps.Copy(combined, current) + } + + contextStruct, err := structpb.NewStruct(combined) + if err != nil { + return Relationship{}, err + } + context = contextStruct + } + + rel.OptionalCaveat = &core.ContextualizedCaveat{ + CaveatName: caveatName, + Context: context, + } + return rel, nil +} + +// StringToONR creates an ONR from string pieces. +func StringToONR(ns, oid, rel string) ObjectAndRelation { + return ObjectAndRelation{ + ObjectType: ns, + ObjectID: oid, + Relation: rel, + } +} diff --git a/vendor/github.com/authzed/spicedb/pkg/tuple/strings.go b/vendor/github.com/authzed/spicedb/pkg/tuple/strings.go new file mode 100644 index 0000000..7b5a378 --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/tuple/strings.go @@ -0,0 +1,165 @@ +package tuple + +import ( + "sort" + "strings" + "time" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/structpb" + + core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/spiceerrors" +) + +var expirationFormat = time.RFC3339Nano + +// JoinRelRef joins the namespace and relation together into the same +// format as `StringRR()`. +func JoinRelRef(namespace, relation string) string { return namespace + "#" + relation } + +// MustSplitRelRef splits a string produced by `JoinRelRef()` and panics if +// it fails. +func MustSplitRelRef(relRef string) (namespace, relation string) { + var ok bool + namespace, relation, ok = strings.Cut(relRef, "#") + if !ok { + panic("improperly formatted relation reference") + } + return +} + +// StringRR converts a RR object to a string. +func StringRR(rr RelationReference) string { + return JoinRelRef(rr.ObjectType, rr.Relation) +} + +// StringONR converts an ONR object to a string. +func StringONR(onr ObjectAndRelation) string { + return StringONRStrings(onr.ObjectType, onr.ObjectID, onr.Relation) +} + +func StringCoreRR(rr *core.RelationReference) string { + if rr == nil { + return "" + } + + return JoinRelRef(rr.Namespace, rr.Relation) +} + +// StringCoreONR converts a core ONR object to a string. +func StringCoreONR(onr *core.ObjectAndRelation) string { + if onr == nil { + return "" + } + + return StringONRStrings(onr.Namespace, onr.ObjectId, onr.Relation) +} + +// StringONRStrings converts ONR strings to a string. +func StringONRStrings(namespace, objectID, relation string) string { + if relation == Ellipsis { + return JoinObjectRef(namespace, objectID) + } + return JoinRelRef(JoinObjectRef(namespace, objectID), relation) +} + +// StringsONRs converts ONR objects to a string slice, sorted. +func StringsONRs(onrs []ObjectAndRelation) []string { + onrstrings := make([]string, 0, len(onrs)) + for _, onr := range onrs { + onrstrings = append(onrstrings, StringONR(onr)) + } + + sort.Strings(onrstrings) + return onrstrings +} + +// MustString converts a relationship to a string. +func MustString(rel Relationship) string { + tplString, err := String(rel) + if err != nil { + panic(err) + } + return tplString +} + +// String converts a relationship to a string. +func String(rel Relationship) (string, error) { + spiceerrors.DebugAssert(rel.ValidateNotEmpty, "relationship must not be empty") + + caveatString, err := StringCaveat(rel.OptionalCaveat) + if err != nil { + return "", err + } + + expirationString, err := StringExpiration(rel.OptionalExpiration) + if err != nil { + return "", err + } + + return StringONR(rel.Resource) + "@" + StringONR(rel.Subject) + caveatString + expirationString, nil +} + +func StringExpiration(expiration *time.Time) (string, error) { + if expiration == nil { + return "", nil + } + + return "[expiration:" + expiration.Format(expirationFormat) + "]", nil +} + +// StringWithoutCaveatOrExpiration converts a relationship to a string, without its caveat or expiration included. +func StringWithoutCaveatOrExpiration(rel Relationship) string { + spiceerrors.DebugAssert(rel.ValidateNotEmpty, "relationship must not be empty") + + return StringONR(rel.Resource) + "@" + StringONR(rel.Subject) +} + +func MustStringCaveat(caveat *core.ContextualizedCaveat) string { + caveatString, err := StringCaveat(caveat) + if err != nil { + panic(err) + } + return caveatString +} + +// StringCaveat converts a contextualized caveat to a string. If the caveat is nil or empty, returns empty string. +func StringCaveat(caveat *core.ContextualizedCaveat) (string, error) { + if caveat == nil || caveat.CaveatName == "" { + return "", nil + } + + contextString, err := StringCaveatContext(caveat.Context) + if err != nil { + return "", err + } + + if len(contextString) > 0 { + contextString = ":" + contextString + } + + return "[" + caveat.CaveatName + contextString + "]", nil +} + +// StringCaveatContext converts the context of a caveat to a string. If the context is nil or empty, returns an empty string. +func StringCaveatContext(context *structpb.Struct) (string, error) { + if context == nil || len(context.Fields) == 0 { + return "", nil + } + + contextBytes, err := protojson.MarshalOptions{ + Multiline: false, + Indent: "", + }.Marshal(context) + if err != nil { + return "", err + } + return string(contextBytes), nil +} + +// JoinObjectRef joins the namespace and the objectId together into the standard +// format. +// +// This function assumes that the provided values have already been validated. +func JoinObjectRef(namespace, objectID string) string { return namespace + ":" + objectID } diff --git a/vendor/github.com/authzed/spicedb/pkg/tuple/structs.go b/vendor/github.com/authzed/spicedb/pkg/tuple/structs.go new file mode 100644 index 0000000..e6cd256 --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/tuple/structs.go @@ -0,0 +1,219 @@ +package tuple + +import ( + "errors" + "fmt" + "time" + + "google.golang.org/protobuf/types/known/timestamppb" + + core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/spiceerrors" +) + +const ( + // Ellipsis is the Ellipsis relation in v0 style subjects. + Ellipsis = "..." + + // PublicWildcard is the wildcard value for subject object IDs that indicates public access + // for the subject type. + PublicWildcard = "*" +) + +// ObjectAndRelation represents an object and its relation. +type ObjectAndRelation struct { + ObjectID string + ObjectType string + Relation string +} + +const onrStructSize = 48 /* size of the struct itself */ + +func (onr ObjectAndRelation) SizeVT() int { + return len(onr.ObjectID) + len(onr.ObjectType) + len(onr.Relation) + onrStructSize +} + +// WithRelation returns a copy of the object and relation with the given relation. +func (onr ObjectAndRelation) WithRelation(relation string) ObjectAndRelation { + onr.Relation = relation + return onr +} + +// RelationReference returns a RelationReference for the object and relation. +func (onr ObjectAndRelation) RelationReference() RelationReference { + return RelationReference{ + ObjectType: onr.ObjectType, + Relation: onr.Relation, + } +} + +// ToCoreONR converts the ObjectAndRelation to a core.ObjectAndRelation. +func (onr ObjectAndRelation) ToCoreONR() *core.ObjectAndRelation { + return &core.ObjectAndRelation{ + Namespace: onr.ObjectType, + ObjectId: onr.ObjectID, + Relation: onr.Relation, + } +} + +func (onr ObjectAndRelation) String() string { + return fmt.Sprintf("%s:%s#%s", onr.ObjectType, onr.ObjectID, onr.Relation) +} + +// RelationshipReference represents a reference to a relationship, i.e. those portions +// of a relationship that are not the integrity or caveat and thus form the unique +// identifier of the relationship. +type RelationshipReference struct { + Resource ObjectAndRelation + Subject ObjectAndRelation +} + +// Relationship represents a relationship between two objects. +type Relationship struct { + OptionalCaveat *core.ContextualizedCaveat + OptionalExpiration *time.Time + OptionalIntegrity *core.RelationshipIntegrity + + RelationshipReference +} + +// ToCoreTuple converts the Relationship to a core.RelationTuple. +func (r Relationship) ToCoreTuple() *core.RelationTuple { + var expirationTime *timestamppb.Timestamp + if r.OptionalExpiration != nil { + expirationTime = timestamppb.New(*r.OptionalExpiration) + } + + return &core.RelationTuple{ + ResourceAndRelation: r.Resource.ToCoreONR(), + Subject: r.Subject.ToCoreONR(), + Caveat: r.OptionalCaveat, + Integrity: r.OptionalIntegrity, + OptionalExpirationTime: expirationTime, + } +} + +const relStructSize = 120 /* size of the struct itself */ + +func (r Relationship) SizeVT() int { + size := r.Resource.SizeVT() + r.Subject.SizeVT() + relStructSize + if r.OptionalCaveat != nil { + size += r.OptionalCaveat.SizeVT() + } + return size +} + +// ValidateNotEmpty returns true if the relationship is not empty. +func (r Relationship) ValidateNotEmpty() bool { + return r.Resource.ObjectType != "" && r.Resource.ObjectID != "" && r.Subject.ObjectType != "" && r.Subject.ObjectID != "" && r.Resource.Relation != "" && r.Subject.Relation != "" +} + +// Validate returns an error if the relationship is invalid. +func (r Relationship) Validate() error { + if !r.ValidateNotEmpty() { + return errors.New("object and relation must not be empty") + } + + if r.RelationshipReference.Resource.ObjectID == PublicWildcard { + return errors.New("invalid resource id") + } + + return nil +} + +// WithoutIntegrity returns a copy of the relationship without its integrity. +func (r Relationship) WithoutIntegrity() Relationship { + r.OptionalIntegrity = nil + return r +} + +// WithCaveat returns a copy of the relationship with the given caveat. +func (r Relationship) WithCaveat(caveat *core.ContextualizedCaveat) Relationship { + r.OptionalCaveat = caveat + return r +} + +// UpdateOperation represents the type of update to a relationship. +type UpdateOperation int + +const ( + UpdateOperationTouch UpdateOperation = iota + UpdateOperationCreate + UpdateOperationDelete +) + +// RelationshipUpdate represents an update to a relationship. +type RelationshipUpdate struct { + Relationship Relationship + Operation UpdateOperation +} + +func (ru RelationshipUpdate) OperationString() string { + switch ru.Operation { + case UpdateOperationTouch: + return "TOUCH" + case UpdateOperationCreate: + return "CREATE" + case UpdateOperationDelete: + return "DELETE" + default: + return "unknown" + } +} + +func (ru RelationshipUpdate) DebugString() string { + return fmt.Sprintf("%s(%s)", ru.OperationString(), StringWithoutCaveatOrExpiration(ru.Relationship)) +} + +// RelationReference represents a reference to a relation. +type RelationReference struct { + ObjectType string + Relation string +} + +// ToCoreRR converts the RelationReference to a core.RelationReference. +func (rr RelationReference) ToCoreRR() *core.RelationReference { + return &core.RelationReference{ + Namespace: rr.ObjectType, + Relation: rr.Relation, + } +} + +func (rr RelationReference) RefString() string { + return JoinRelRef(rr.ObjectType, rr.Relation) +} + +func (rr RelationReference) String() string { + return rr.RefString() +} + +// ONR creates an ObjectAndRelation. +func ONR(namespace, objectID, relation string) ObjectAndRelation { + spiceerrors.DebugAssert(func() bool { + return namespace != "" && objectID != "" && relation != "" + }, "invalid ONR: %s %s %s", namespace, objectID, relation) + + return ObjectAndRelation{ + ObjectType: namespace, + ObjectID: objectID, + Relation: relation, + } +} + +// ONRRef creates an ObjectAndRelation reference. +func ONRRef(namespace, objectID, relation string) *ObjectAndRelation { + onr := ONR(namespace, objectID, relation) + return &onr +} + +// RR creates a RelationReference. +func RR(namespace, relation string) RelationReference { + spiceerrors.DebugAssert(func() bool { + return namespace != "" && relation != "" + }, "invalid RR: %s %s", namespace, relation) + + return RelationReference{ + ObjectType: namespace, + Relation: relation, + } +} diff --git a/vendor/github.com/authzed/spicedb/pkg/tuple/updates.go b/vendor/github.com/authzed/spicedb/pkg/tuple/updates.go new file mode 100644 index 0000000..73eafe9 --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/tuple/updates.go @@ -0,0 +1,22 @@ +package tuple + +func Create(rel Relationship) RelationshipUpdate { + return RelationshipUpdate{ + Operation: UpdateOperationCreate, + Relationship: rel, + } +} + +func Touch(rel Relationship) RelationshipUpdate { + return RelationshipUpdate{ + Operation: UpdateOperationTouch, + Relationship: rel, + } +} + +func Delete(rel Relationship) RelationshipUpdate { + return RelationshipUpdate{ + Operation: UpdateOperationDelete, + Relationship: rel, + } +} diff --git a/vendor/github.com/authzed/spicedb/pkg/tuple/v1.go b/vendor/github.com/authzed/spicedb/pkg/tuple/v1.go new file mode 100644 index 0000000..5a3705f --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/tuple/v1.go @@ -0,0 +1,340 @@ +package tuple + +import ( + "fmt" + "time" + + v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" + "github.com/jzelinskie/stringz" + "google.golang.org/protobuf/types/known/timestamppb" + + core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/spiceerrors" +) + +// ParseV1Rel parses a string representation of a relationship into a v1.Relationship object. +func ParseV1Rel(relString string) (*v1.Relationship, error) { + parsed, err := Parse(relString) + if err != nil { + return nil, err + } + + return ToV1Relationship(parsed), nil +} + +// Same as above, but panics if it cannot be parsed. Should only be used in tests. +func MustParseV1Rel(relString string) *v1.Relationship { + parsed, err := Parse(relString) + if err != nil { + panic(fmt.Sprintf("could not parse relationship string: %s %s", relString, err)) + } + + return ToV1Relationship(parsed) +} + +// MustV1RelString converts a relationship into a string. Will panic if +// the Relationship does not validate. +func MustV1RelString(rel *v1.Relationship) string { + if err := rel.Validate(); err != nil { + panic(fmt.Sprintf("invalid relationship: %#v %s", rel, err)) + } + return MustV1StringRelationship(rel) +} + +// StringObjectRef marshals a *v1.ObjectReference into a string. +// +// This function assumes that the provided values have already been validated. +func V1StringObjectRef(ref *v1.ObjectReference) string { + return JoinObjectRef(ref.ObjectType, ref.ObjectId) +} + +// StringSubjectRef marshals a *v1.SubjectReference into a string. +// +// This function assumes that the provided values have already been validated. +func V1StringSubjectRef(ref *v1.SubjectReference) string { + if ref.OptionalRelation == "" { + return V1StringObjectRef(ref.Object) + } + return JoinRelRef(V1StringObjectRef(ref.Object), ref.OptionalRelation) +} + +// MustV1StringRelationship converts a v1.Relationship to a string. +func MustV1StringRelationship(rel *v1.Relationship) string { + relString, err := V1StringRelationship(rel) + if err != nil { + panic(err) + } + return relString +} + +// V1StringRelationship converts a v1.Relationship to a string. +func V1StringRelationship(rel *v1.Relationship) (string, error) { + if rel == nil || rel.Resource == nil || rel.Subject == nil { + return "", nil + } + + caveatString, err := V1StringCaveatRef(rel.OptionalCaveat) + if err != nil { + return "", err + } + + expirationString, err := V1StringExpiration(rel.OptionalExpiresAt) + if err != nil { + return "", err + } + + return V1StringRelationshipWithoutCaveatOrExpiration(rel) + caveatString + expirationString, nil +} + +func V1StringExpiration(expiration *timestamppb.Timestamp) (string, error) { + if expiration == nil { + return "", nil + } + + return "[expiration:" + expiration.AsTime().Format(expirationFormat) + "]", nil +} + +// V1StringRelationshipWithoutCaveatOrExpiration converts a v1.Relationship to a string, excluding any caveat. +func V1StringRelationshipWithoutCaveatOrExpiration(rel *v1.Relationship) string { + if rel == nil || rel.Resource == nil || rel.Subject == nil { + return "" + } + + return V1StringObjectRef(rel.Resource) + "#" + rel.Relation + "@" + V1StringSubjectRef(rel.Subject) +} + +// V1StringCaveatRef converts a v1.ContextualizedCaveat to a string. +func V1StringCaveatRef(caveat *v1.ContextualizedCaveat) (string, error) { + if caveat == nil || caveat.CaveatName == "" { + return "", nil + } + + contextString, err := StringCaveatContext(caveat.Context) + if err != nil { + return "", err + } + + if len(contextString) > 0 { + contextString = ":" + contextString + } + + return "[" + caveat.CaveatName + contextString + "]", nil +} + +// UpdateToV1RelationshipUpdate converts a RelationshipUpdate into a +// v1.RelationshipUpdate. +func UpdateToV1RelationshipUpdate(update RelationshipUpdate) (*v1.RelationshipUpdate, error) { + var op v1.RelationshipUpdate_Operation + switch update.Operation { + case UpdateOperationCreate: + op = v1.RelationshipUpdate_OPERATION_CREATE + case UpdateOperationDelete: + op = v1.RelationshipUpdate_OPERATION_DELETE + case UpdateOperationTouch: + op = v1.RelationshipUpdate_OPERATION_TOUCH + default: + return nil, spiceerrors.MustBugf("unknown update operation: %v", update.Operation) + } + + return &v1.RelationshipUpdate{ + Operation: op, + Relationship: ToV1Relationship(update.Relationship), + }, nil +} + +// MustUpdateToV1RelationshipUpdate converts a RelationshipUpdate into a +// v1.RelationshipUpdate. Panics on error. +func MustUpdateToV1RelationshipUpdate(update RelationshipUpdate) *v1.RelationshipUpdate { + v1rel, err := UpdateToV1RelationshipUpdate(update) + if err != nil { + panic(err) + } + + return v1rel +} + +// UpdateFromV1RelationshipUpdate converts a RelationshipUpdate into a +// RelationTupleUpdate. +func UpdateFromV1RelationshipUpdate(update *v1.RelationshipUpdate) (RelationshipUpdate, error) { + var op UpdateOperation + switch update.Operation { + case v1.RelationshipUpdate_OPERATION_CREATE: + op = UpdateOperationCreate + case v1.RelationshipUpdate_OPERATION_DELETE: + op = UpdateOperationDelete + case v1.RelationshipUpdate_OPERATION_TOUCH: + op = UpdateOperationTouch + default: + return RelationshipUpdate{}, spiceerrors.MustBugf("unknown update operation: %v", update.Operation) + } + + return RelationshipUpdate{ + Operation: op, + Relationship: FromV1Relationship(update.Relationship), + }, nil +} + +// FromV1Relationship converts a v1.Relationship into a Relationship. +func FromV1Relationship(rel *v1.Relationship) Relationship { + var caveat *core.ContextualizedCaveat + if rel.OptionalCaveat != nil { + caveat = &core.ContextualizedCaveat{ + CaveatName: rel.OptionalCaveat.CaveatName, + Context: rel.OptionalCaveat.Context, + } + } + + var expiration *time.Time + if rel.OptionalExpiresAt != nil { + t := rel.OptionalExpiresAt.AsTime() + expiration = &t + } + + return Relationship{ + RelationshipReference: RelationshipReference{ + Resource: ObjectAndRelation{ + ObjectID: rel.Resource.ObjectId, + ObjectType: rel.Resource.ObjectType, + Relation: rel.Relation, + }, + Subject: ObjectAndRelation{ + ObjectID: rel.Subject.Object.ObjectId, + ObjectType: rel.Subject.Object.ObjectType, + Relation: stringz.Default(rel.Subject.OptionalRelation, Ellipsis, ""), + }, + }, + OptionalCaveat: caveat, + OptionalExpiration: expiration, + } +} + +// ToV1Relationship converts a Relationship into a v1.Relationship. +func ToV1Relationship(rel Relationship) *v1.Relationship { + var caveat *v1.ContextualizedCaveat + if rel.OptionalCaveat != nil { + caveat = &v1.ContextualizedCaveat{ + CaveatName: rel.OptionalCaveat.CaveatName, + Context: rel.OptionalCaveat.Context, + } + } + + var expiration *timestamppb.Timestamp + if rel.OptionalExpiration != nil { + expiration = timestamppb.New(*rel.OptionalExpiration) + } + + return &v1.Relationship{ + Resource: &v1.ObjectReference{ + ObjectType: rel.Resource.ObjectType, + ObjectId: rel.Resource.ObjectID, + }, + Relation: rel.Resource.Relation, + Subject: &v1.SubjectReference{ + Object: &v1.ObjectReference{ + ObjectType: rel.Subject.ObjectType, + ObjectId: rel.Subject.ObjectID, + }, + OptionalRelation: stringz.Default(rel.Subject.Relation, "", Ellipsis), + }, + OptionalCaveat: caveat, + OptionalExpiresAt: expiration, + } +} + +// CopyToV1Relationship copies a Relationship into a v1.Relationship. +func CopyToV1Relationship(rel Relationship, v1rel *v1.Relationship) { + v1rel.Resource.ObjectType = rel.Resource.ObjectType + v1rel.Resource.ObjectId = rel.Resource.ObjectID + v1rel.Relation = rel.Resource.Relation + v1rel.Subject.Object.ObjectType = rel.Subject.ObjectType + v1rel.Subject.Object.ObjectId = rel.Subject.ObjectID + v1rel.Subject.OptionalRelation = stringz.Default(rel.Subject.Relation, "", Ellipsis) + + if rel.OptionalCaveat != nil { + if v1rel.OptionalCaveat == nil { + v1rel.OptionalCaveat = &v1.ContextualizedCaveat{} + } + + v1rel.OptionalCaveat.CaveatName = rel.OptionalCaveat.CaveatName + v1rel.OptionalCaveat.Context = rel.OptionalCaveat.Context + } else { + v1rel.OptionalCaveat = nil + } + + if rel.OptionalExpiration != nil { + v1rel.OptionalExpiresAt = timestamppb.New(*rel.OptionalExpiration) + } else { + v1rel.OptionalExpiresAt = nil + } +} + +// UpdatesToV1RelationshipUpdates converts a slice of RelationshipUpdate into a +// slice of v1.RelationshipUpdate. +func UpdatesToV1RelationshipUpdates(updates []RelationshipUpdate) ([]*v1.RelationshipUpdate, error) { + relationshipUpdates := make([]*v1.RelationshipUpdate, 0, len(updates)) + + for _, update := range updates { + converted, err := UpdateToV1RelationshipUpdate(update) + if err != nil { + return nil, err + } + + relationshipUpdates = append(relationshipUpdates, converted) + } + + return relationshipUpdates, nil +} + +// UpdatesFromV1RelationshipUpdates converts a slice of v1.RelationshipUpdate into a +// slice of RelationshipUpdate. +func UpdatesFromV1RelationshipUpdates(updates []*v1.RelationshipUpdate) ([]RelationshipUpdate, error) { + relationshipUpdates := make([]RelationshipUpdate, 0, len(updates)) + + for _, update := range updates { + converted, err := UpdateFromV1RelationshipUpdate(update) + if err != nil { + return nil, err + } + + relationshipUpdates = append(relationshipUpdates, converted) + } + + return relationshipUpdates, nil +} + +// ToV1Filter converts a RelationTuple into a RelationshipFilter. +func ToV1Filter(rel Relationship) *v1.RelationshipFilter { + return &v1.RelationshipFilter{ + ResourceType: rel.Resource.ObjectType, + OptionalResourceId: rel.Resource.ObjectID, + OptionalRelation: rel.Resource.Relation, + OptionalSubjectFilter: SubjectONRToSubjectFilter(rel.Subject), + } +} + +// SubjectONRToSubjectFilter converts a userset to the equivalent exact SubjectFilter. +func SubjectONRToSubjectFilter(subject ObjectAndRelation) *v1.SubjectFilter { + return &v1.SubjectFilter{ + SubjectType: subject.ObjectType, + OptionalSubjectId: subject.ObjectID, + OptionalRelation: &v1.SubjectFilter_RelationFilter{ + Relation: stringz.Default(subject.Relation, "", Ellipsis), + }, + } +} + +// RelToFilter converts a Relationship into a RelationshipFilter. +func RelToFilter(rel *v1.Relationship) *v1.RelationshipFilter { + return &v1.RelationshipFilter{ + ResourceType: rel.Resource.ObjectType, + OptionalResourceId: rel.Resource.ObjectId, + OptionalRelation: rel.Relation, + OptionalSubjectFilter: &v1.SubjectFilter{ + SubjectType: rel.Subject.Object.ObjectType, + OptionalSubjectId: rel.Subject.Object.ObjectId, + OptionalRelation: &v1.SubjectFilter_RelationFilter{ + Relation: rel.Subject.OptionalRelation, + }, + }, + } +} |
