diff options
Diffstat (limited to 'vendor/github.com/authzed/spicedb/pkg/tuple/parsing.go')
| -rw-r--r-- | vendor/github.com/authzed/spicedb/pkg/tuple/parsing.go | 228 |
1 files changed, 228 insertions, 0 deletions
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, + } +} |
