summaryrefslogtreecommitdiff
path: root/vendor/github.com/authzed/spicedb/pkg/tuple/parsing.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/authzed/spicedb/pkg/tuple/parsing.go')
-rw-r--r--vendor/github.com/authzed/spicedb/pkg/tuple/parsing.go228
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,
+ }
+}