diff options
Diffstat (limited to 'vendor/github.com/authzed/spicedb/pkg/validationfile/blocks')
5 files changed, 611 insertions, 0 deletions
diff --git a/vendor/github.com/authzed/spicedb/pkg/validationfile/blocks/assertions.go b/vendor/github.com/authzed/spicedb/pkg/validationfile/blocks/assertions.go new file mode 100644 index 0000000..56c4e6c --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/validationfile/blocks/assertions.go @@ -0,0 +1,147 @@ +package blocks + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/ccoveille/go-safecast" + yamlv3 "gopkg.in/yaml.v3" + + "github.com/authzed/spicedb/pkg/spiceerrors" + "github.com/authzed/spicedb/pkg/tuple" +) + +// Assertions represents assertions defined in the validation file. +type Assertions struct { + // AssertTrue is the set of relationships to assert true. + AssertTrue []Assertion `yaml:"assertTrue"` + + // AssertCaveated is the set of relationships to assert that are caveated. + AssertCaveated []Assertion `yaml:"assertCaveated"` + + // AssertFalse is the set of relationships to assert false. + AssertFalse []Assertion `yaml:"assertFalse"` + + // SourcePosition is the position of the assertions in the file. + SourcePosition spiceerrors.SourcePosition +} + +// Assertion is a parsed assertion. +type Assertion struct { + // RelationshipWithContextString is the string form of the assertion, including optional context. + // Forms: + // `document:firstdoc#view@user:tom` + // `document:seconddoc#view@user:sarah with {"some":"contexthere"}` + RelationshipWithContextString string + + // Relationship is the parsed relationship on which the assertion is being + // run. + Relationship tuple.Relationship + + // CaveatContext is the caveat context for the assertion, if any. + CaveatContext map[string]any + + // SourcePosition is the position of the assertion in the file. + SourcePosition spiceerrors.SourcePosition +} + +type internalAssertions struct { + // AssertTrue is the set of relationships to assert true. + AssertTrue []Assertion `yaml:"assertTrue"` + + // AssertCaveated is the set of relationships to assert that are caveated. + AssertCaveated []Assertion `yaml:"assertCaveated"` + + // AssertFalse is the set of relationships to assert false. + AssertFalse []Assertion `yaml:"assertFalse"` +} + +// UnmarshalYAML is a custom unmarshaller. +func (a *Assertions) UnmarshalYAML(node *yamlv3.Node) error { + ia := internalAssertions{} + if err := node.Decode(&ia); err != nil { + return convertYamlError(err) + } + + a.AssertTrue = ia.AssertTrue + a.AssertFalse = ia.AssertFalse + a.AssertCaveated = ia.AssertCaveated + a.SourcePosition = spiceerrors.SourcePosition{LineNumber: node.Line, ColumnPosition: node.Column} + return nil +} + +// UnmarshalYAML is a custom unmarshaller. +func (a *Assertion) UnmarshalYAML(node *yamlv3.Node) error { + relationshipWithContextString := "" + + if err := node.Decode(&relationshipWithContextString); err != nil { + return convertYamlError(err) + } + + trimmed := strings.TrimSpace(relationshipWithContextString) + + line, err := safecast.ToUint64(node.Line) + if err != nil { + return err + } + column, err := safecast.ToUint64(node.Column) + if err != nil { + return err + } + + // Check for caveat context. + parts := strings.SplitN(trimmed, " with ", 2) + if len(parts) == 0 { + return spiceerrors.NewWithSourceError( + fmt.Errorf("error parsing assertion `%s`", trimmed), + trimmed, + line, + column, + ) + } + + relationship, err := tuple.Parse(strings.TrimSpace(parts[0])) + if err != nil { + return spiceerrors.NewWithSourceError( + fmt.Errorf("error parsing relationship in assertion `%s`: %w", trimmed, err), + trimmed, + line, + column, + ) + } + + a.Relationship = relationship + + if len(parts) == 2 { + caveatContextMap := make(map[string]any, 0) + err := json.Unmarshal([]byte(parts[1]), &caveatContextMap) + if err != nil { + return spiceerrors.NewWithSourceError( + fmt.Errorf("error parsing caveat context in assertion `%s`: %w", trimmed, err), + trimmed, + line, + column, + ) + } + + a.CaveatContext = caveatContextMap + } + + a.RelationshipWithContextString = relationshipWithContextString + a.SourcePosition = spiceerrors.SourcePosition{LineNumber: node.Line, ColumnPosition: node.Column} + return nil +} + +// ParseAssertionsBlock parses the given contents as an assertions block. +func ParseAssertionsBlock(contents []byte) (*Assertions, error) { + a := internalAssertions{} + if err := yamlv3.Unmarshal(contents, &a); err != nil { + return nil, convertYamlError(err) + } + return &Assertions{ + AssertTrue: a.AssertTrue, + AssertCaveated: a.AssertCaveated, + AssertFalse: a.AssertFalse, + }, nil +} diff --git a/vendor/github.com/authzed/spicedb/pkg/validationfile/blocks/errors.go b/vendor/github.com/authzed/spicedb/pkg/validationfile/blocks/errors.go new file mode 100644 index 0000000..b30ef7a --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/validationfile/blocks/errors.go @@ -0,0 +1,47 @@ +package blocks + +import ( + "errors" + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/authzed/spicedb/pkg/spiceerrors" +) + +var ( + yamlLineRegex = regexp.MustCompile(`line ([0-9]+): (.+)`) + yamlUnmarshalRegex = regexp.MustCompile("cannot unmarshal !!str `([^`]+)...`") +) + +func convertYamlError(err error) error { + linePieces := yamlLineRegex.FindStringSubmatch(err.Error()) + if len(linePieces) == 3 { + lineNumber, parseErr := strconv.ParseUint(linePieces[1], 10, 32) + if parseErr != nil { + lineNumber = 0 + } + + message := linePieces[2] + source := "" + unmarshalPieces := yamlUnmarshalRegex.FindStringSubmatch(message) + if len(unmarshalPieces) == 2 { + source = unmarshalPieces[1] + if strings.Contains(source, " ") { + source, _, _ = strings.Cut(source, " ") + } + + message = fmt.Sprintf("unexpected value `%s`", source) + } + + return spiceerrors.NewWithSourceError( + errors.New(message), + source, + lineNumber, + 0, + ) + } + + return err +} diff --git a/vendor/github.com/authzed/spicedb/pkg/validationfile/blocks/expectedrelations.go b/vendor/github.com/authzed/spicedb/pkg/validationfile/blocks/expectedrelations.go new file mode 100644 index 0000000..44f3b88 --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/validationfile/blocks/expectedrelations.go @@ -0,0 +1,264 @@ +package blocks + +import ( + "fmt" + "regexp" + "slices" + "strings" + + yamlv3 "gopkg.in/yaml.v3" + + "github.com/ccoveille/go-safecast" + + "github.com/authzed/spicedb/pkg/spiceerrors" + "github.com/authzed/spicedb/pkg/tuple" +) + +// ParsedExpectedRelations represents the expected relations defined in the validation +// file. +type ParsedExpectedRelations struct { + // ValidationMap is the parsed expected relations validation map. + ValidationMap ValidationMap + + // SourcePosition is the position of the expected relations in the file. + SourcePosition spiceerrors.SourcePosition +} + +// UnmarshalYAML is a custom unmarshaller. +func (per *ParsedExpectedRelations) UnmarshalYAML(node *yamlv3.Node) error { + err := node.Decode(&per.ValidationMap) + if err != nil { + return convertYamlError(err) + } + + per.SourcePosition = spiceerrors.SourcePosition{LineNumber: node.Line, ColumnPosition: node.Column} + return nil +} + +// ValidationMap is a map from an Object Relation (as a Relationship) to the +// validation strings containing the Subjects for that Object Relation. +type ValidationMap map[ObjectRelation][]ExpectedSubject + +// ObjectRelation represents an ONR defined as a string in the key for +// the ValidationMap. +type ObjectRelation struct { + // ObjectRelationString is the string form of the object relation. + ObjectRelationString string + + // ObjectAndRelation is the parsed object and relation. + ObjectAndRelation tuple.ObjectAndRelation + + // SourcePosition is the position of the expected relations in the file. + SourcePosition spiceerrors.SourcePosition +} + +// UnmarshalYAML is a custom unmarshaller. +func (ors *ObjectRelation) UnmarshalYAML(node *yamlv3.Node) error { + err := node.Decode(&ors.ObjectRelationString) + if err != nil { + return convertYamlError(err) + } + + line, err := safecast.ToUint64(node.Line) + if err != nil { + return err + } + column, err := safecast.ToUint64(node.Column) + if err != nil { + return err + } + + parsed, err := tuple.ParseONR(ors.ObjectRelationString) + if err != nil { + return spiceerrors.NewWithSourceError( + fmt.Errorf("could not parse %s: %w", ors.ObjectRelationString, err), + ors.ObjectRelationString, + line, + column, + ) + } + + ors.ObjectAndRelation = parsed + ors.SourcePosition = spiceerrors.SourcePosition{LineNumber: node.Line, ColumnPosition: node.Column} + return nil +} + +var ( + vsSubjectRegex = regexp.MustCompile(`(.*?)\[(?P<user_str>.*)](.*?)`) + vsObjectAndRelationRegex = regexp.MustCompile(`(.*?)<(?P<onr_str>[^>]+)>(.*?)`) + vsSubjectWithExceptionsOrCaveatRegex = regexp.MustCompile(`^(?P<subject_onr>[^]\s]+)(?P<caveat>\[\.\.\.])?(\s+-\s+\{(?P<exceptions>[^}]+)})?$`) +) + +// ExpectedSubject is a subject expected for the ObjectAndRelation. +type ExpectedSubject struct { + // ValidationString holds a validation string containing a Subject and one or + // more Relations to the parent Object. + // Example: `[tenant/user:someuser#...] is <tenant/document:example#viewer>` + ValidationString ValidationString + + // Subject is the subject expected. May be nil if not defined in the line. + SubjectWithExceptions *SubjectWithExceptions + + // Resources are the resources under which the subject is found. + Resources []tuple.ObjectAndRelation + + // SourcePosition is the position of the expected subject in the file. + SourcePosition spiceerrors.SourcePosition +} + +// SubjectAndCaveat returns a subject and whether it is caveated. +type SubjectAndCaveat struct { + // Subject is the subject found. + Subject tuple.ObjectAndRelation + + // IsCaveated indicates whether the subject is caveated. + IsCaveated bool +} + +// SubjectWithExceptions returns the subject found in a validation string, along with any exceptions. +type SubjectWithExceptions struct { + // Subject is the subject found. + Subject SubjectAndCaveat + + // Exceptions are those subjects removed from the subject, if it is a wildcard. + Exceptions []SubjectAndCaveat +} + +// UnmarshalYAML is a custom unmarshaller. +func (es *ExpectedSubject) UnmarshalYAML(node *yamlv3.Node) error { + err := node.Decode(&es.ValidationString) + if err != nil { + return convertYamlError(err) + } + + line, err := safecast.ToUint64(node.Line) + if err != nil { + return err + } + column, err := safecast.ToUint64(node.Column) + if err != nil { + return err + } + + subjectWithExceptions, subErr := es.ValidationString.Subject() + if subErr != nil { + return spiceerrors.NewWithSourceError( + subErr, + subErr.SourceCodeString, + line+subErr.LineNumber, + column+subErr.ColumnPosition, + ) + } + + onrs, onrErr := es.ValidationString.ONRS() + if onrErr != nil { + return spiceerrors.NewWithSourceError( + onrErr, + onrErr.SourceCodeString, + line+onrErr.LineNumber, + column+onrErr.ColumnPosition, + ) + } + + es.SubjectWithExceptions = subjectWithExceptions + es.SourcePosition = spiceerrors.SourcePosition{LineNumber: node.Line, ColumnPosition: node.Column} + es.Resources = onrs + return nil +} + +// ValidationString holds a validation string containing a Subject and one or +// more Relations to the parent Object. +// Example: `[tenant/user:someuser#...] is <tenant/document:example#viewer>` +type ValidationString string + +// SubjectString returns the subject contained in the ValidationString, if any. +func (vs ValidationString) SubjectString() (string, bool) { + result := vsSubjectRegex.FindStringSubmatch(string(vs)) + if len(result) != 4 { + return "", false + } + + return result[2], true +} + +// Subject returns the subject contained in the ValidationString, if any. If +// none, returns nil. +func (vs ValidationString) Subject() (*SubjectWithExceptions, *spiceerrors.WithSourceError) { + subjectStr, ok := vs.SubjectString() + if !ok { + return nil, nil + } + + subjectStr = strings.TrimSpace(subjectStr) + groups := vsSubjectWithExceptionsOrCaveatRegex.FindStringSubmatch(subjectStr) + if len(groups) == 0 { + bracketedSubjectString := "[" + subjectStr + "]" + return nil, spiceerrors.NewWithSourceError(fmt.Errorf("invalid subject: `%s`", subjectStr), bracketedSubjectString, 0, 0) + } + + subjectONRString := groups[slices.Index(vsSubjectWithExceptionsOrCaveatRegex.SubexpNames(), "subject_onr")] + subjectONR, err := tuple.ParseSubjectONR(subjectONRString) + if err != nil { + return nil, spiceerrors.NewWithSourceError(fmt.Errorf("invalid subject: `%s`: %w", subjectONRString, err), subjectONRString, 0, 0) + } + + exceptionsString := strings.TrimSpace(groups[slices.Index(vsSubjectWithExceptionsOrCaveatRegex.SubexpNames(), "exceptions")]) + var exceptions []SubjectAndCaveat + + if len(exceptionsString) > 0 { + exceptionsStringsSlice := strings.Split(exceptionsString, ",") + exceptions = make([]SubjectAndCaveat, 0, len(exceptionsStringsSlice)) + for _, exceptionString := range exceptionsStringsSlice { + isCaveated := false + if strings.HasSuffix(exceptionString, "[...]") { + exceptionString = strings.TrimSuffix(exceptionString, "[...]") + isCaveated = true + } + + exceptionONR, err := tuple.ParseSubjectONR(strings.TrimSpace(exceptionString)) + if err != nil { + return nil, spiceerrors.NewWithSourceError(fmt.Errorf("invalid subject: `%s`: %w", exceptionString, err), exceptionString, 0, 0) + } + + exceptions = append(exceptions, SubjectAndCaveat{exceptionONR, isCaveated}) + } + } + + isCaveated := len(strings.TrimSpace(groups[slices.Index(vsSubjectWithExceptionsOrCaveatRegex.SubexpNames(), "caveat")])) > 0 + return &SubjectWithExceptions{SubjectAndCaveat{subjectONR, isCaveated}, exceptions}, nil +} + +// ONRStrings returns the ONRs contained in the ValidationString, if any. +func (vs ValidationString) ONRStrings() []string { + results := vsObjectAndRelationRegex.FindAllStringSubmatch(string(vs), -1) + onrStrings := []string{} + for _, result := range results { + onrStrings = append(onrStrings, result[2]) + } + return onrStrings +} + +// ONRS returns the subject ONRs in the ValidationString, if any. +func (vs ValidationString) ONRS() ([]tuple.ObjectAndRelation, *spiceerrors.WithSourceError) { + onrStrings := vs.ONRStrings() + onrs := []tuple.ObjectAndRelation{} + for _, onrString := range onrStrings { + found, err := tuple.ParseONR(onrString) + if err != nil { + return nil, spiceerrors.NewWithSourceError(fmt.Errorf("invalid resource and relation: `%s`: %w", onrString, err), onrString, 0, 0) + } + + onrs = append(onrs, found) + } + return onrs, nil +} + +// ParseExpectedRelationsBlock parses the given contents as an expected relations block. +func ParseExpectedRelationsBlock(contents []byte) (*ParsedExpectedRelations, error) { + per := ParsedExpectedRelations{} + err := yamlv3.Unmarshal(contents, &per) + if err != nil { + return nil, convertYamlError(err) + } + return &per, nil +} diff --git a/vendor/github.com/authzed/spicedb/pkg/validationfile/blocks/relationships.go b/vendor/github.com/authzed/spicedb/pkg/validationfile/blocks/relationships.go new file mode 100644 index 0000000..278c7a8 --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/validationfile/blocks/relationships.go @@ -0,0 +1,83 @@ +package blocks + +import ( + "fmt" + "strings" + + "github.com/ccoveille/go-safecast" + yamlv3 "gopkg.in/yaml.v3" + + "github.com/authzed/spicedb/pkg/spiceerrors" + "github.com/authzed/spicedb/pkg/tuple" +) + +// ParsedRelationships is the parsed relationships in a validationfile. +type ParsedRelationships struct { + // RelationshipsString is the found string of newline-separated relationships. + RelationshipsString string + + // SourcePosition is the position of the schema in the file. + SourcePosition spiceerrors.SourcePosition + + // Relationships are the fully parsed relationships. + Relationships []tuple.Relationship +} + +// UnmarshalYAML is a custom unmarshaller. +func (pr *ParsedRelationships) UnmarshalYAML(node *yamlv3.Node) error { + err := node.Decode(&pr.RelationshipsString) + if err != nil { + return convertYamlError(err) + } + + relationshipsString := pr.RelationshipsString + if relationshipsString == "" { + return nil + } + + seenTuples := map[string]bool{} + lines := strings.Split(relationshipsString, "\n") + relationships := make([]tuple.Relationship, 0, len(lines)) + for index, line := range lines { + trimmed := strings.TrimSpace(line) + if len(trimmed) == 0 || strings.HasPrefix(trimmed, "//") { + continue + } + + // +1 for the key, and *2 for newlines in YAML + errorLine, err := safecast.ToUint64(node.Line + 1 + (index * 2)) + if err != nil { + return err + } + column, err := safecast.ToUint64(node.Column) + if err != nil { + return err + } + + rel, err := tuple.Parse(trimmed) + if err != nil { + return spiceerrors.NewWithSourceError( + fmt.Errorf("error parsing relationship `%s`: %w", trimmed, err), + trimmed, + errorLine, + column, + ) + } + + _, ok := seenTuples[tuple.StringWithoutCaveatOrExpiration(rel)] + if ok { + return spiceerrors.NewWithSourceError( + fmt.Errorf("found repeated relationship `%s`", trimmed), + trimmed, + errorLine, + column, + ) + } + seenTuples[tuple.StringWithoutCaveatOrExpiration(rel)] = true + relationships = append(relationships, rel) + } + + pr.Relationships = relationships + pr.SourcePosition = spiceerrors.SourcePosition{LineNumber: node.Line, ColumnPosition: node.Column} + return nil +} diff --git a/vendor/github.com/authzed/spicedb/pkg/validationfile/blocks/schema.go b/vendor/github.com/authzed/spicedb/pkg/validationfile/blocks/schema.go new file mode 100644 index 0000000..814eebc --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/validationfile/blocks/schema.go @@ -0,0 +1,70 @@ +package blocks + +import ( + "errors" + "fmt" + + yamlv3 "gopkg.in/yaml.v3" + + "github.com/ccoveille/go-safecast" + + "github.com/authzed/spicedb/pkg/schemadsl/compiler" + "github.com/authzed/spicedb/pkg/schemadsl/input" + "github.com/authzed/spicedb/pkg/spiceerrors" +) + +// ParsedSchema is the parsed schema in a validationfile. +type ParsedSchema struct { + // Schema is the schema found. + Schema string + + // SourcePosition is the position of the schema in the file. + SourcePosition spiceerrors.SourcePosition + + // CompiledSchema is the compiled schema. + CompiledSchema *compiler.CompiledSchema +} + +// UnmarshalYAML is a custom unmarshaller. +func (ps *ParsedSchema) UnmarshalYAML(node *yamlv3.Node) error { + err := node.Decode(&ps.Schema) + if err != nil { + return convertYamlError(err) + } + + compiled, err := compiler.Compile(compiler.InputSchema{ + Source: input.Source("schema"), + SchemaString: ps.Schema, + }, compiler.AllowUnprefixedObjectType()) + if err != nil { + var errWithContext compiler.WithContextError + if errors.As(err, &errWithContext) { + line, col, lerr := errWithContext.SourceRange.Start().LineAndColumn() + if lerr != nil { + return lerr + } + + uintLine, err := safecast.ToUint64(line) + if err != nil { + return err + } + uintCol, err := safecast.ToUint64(col) + if err != nil { + return err + } + + return spiceerrors.NewWithSourceError( + fmt.Errorf("error when parsing schema: %s", errWithContext.BaseMessage), + errWithContext.ErrorSourceCode, + uintLine+1, // source line is 0-indexed + uintCol+1, // source col is 0-indexed + ) + } + + return fmt.Errorf("error when parsing schema: %w", err) + } + + ps.CompiledSchema = compiled + ps.SourcePosition = spiceerrors.SourcePosition{LineNumber: node.Line, ColumnPosition: node.Column} + return nil +} |
