diff options
Diffstat (limited to 'vendor/github.com/authzed/spicedb/pkg/validationfile')
8 files changed, 840 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 +} diff --git a/vendor/github.com/authzed/spicedb/pkg/validationfile/doc.go b/vendor/github.com/authzed/spicedb/pkg/validationfile/doc.go new file mode 100644 index 0000000..eee5323 --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/validationfile/doc.go @@ -0,0 +1,2 @@ +// Package validationfile contains code to manipulate files accepted by the `zed validate` CLI. +package validationfile diff --git a/vendor/github.com/authzed/spicedb/pkg/validationfile/fileformat.go b/vendor/github.com/authzed/spicedb/pkg/validationfile/fileformat.go new file mode 100644 index 0000000..8f965a2 --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/validationfile/fileformat.go @@ -0,0 +1,55 @@ +package validationfile + +import ( + yamlv3 "gopkg.in/yaml.v3" + + "github.com/authzed/spicedb/pkg/validationfile/blocks" +) + +// DecodeValidationFile decodes the validation file as found in the contents bytes +// and returns it. +func DecodeValidationFile(contents []byte) (*ValidationFile, error) { + p := ValidationFile{} + err := yamlv3.Unmarshal(contents, &p) + if err != nil { + return nil, err + } + return &p, nil +} + +// ValidationFile is a structural representation of the validation file format. +type ValidationFile struct { + // Schema is the schema. + Schema blocks.ParsedSchema `yaml:"schema"` + + // Relationships are the relationships specified in the validation file. + Relationships blocks.ParsedRelationships `yaml:"relationships"` + + // Assertions are the assertions defined in the validation file. May be nil + // if no assertions are defined. + Assertions blocks.Assertions `yaml:"assertions"` + + // ExpectedRelations is the map of expected relations. + ExpectedRelations blocks.ParsedExpectedRelations `yaml:"validation"` + + // NamespaceConfigs are the namespace configuration protos, in text format. + // Deprecated: only for internal use. Use `schema`. + NamespaceConfigs []string `yaml:"namespace_configs"` + + // ValidationTuples are the validation tuples, in tuple string syntax. + // Deprecated: only for internal use. Use `relationships`. + ValidationTuples []string `yaml:"validation_tuples"` + + // Schema file represents the path specified for the schema file + SchemaFile string `yaml:"schemaFile"` +} + +// ParseAssertionsBlock parses the given contents as an assertions block. +func ParseAssertionsBlock(contents []byte) (*blocks.Assertions, error) { + return blocks.ParseAssertionsBlock(contents) +} + +// ParseExpectedRelationsBlock parses the given contents as an expected relations block. +func ParseExpectedRelationsBlock(contents []byte) (*blocks.ParsedExpectedRelations, error) { + return blocks.ParseExpectedRelationsBlock(contents) +} diff --git a/vendor/github.com/authzed/spicedb/pkg/validationfile/loader.go b/vendor/github.com/authzed/spicedb/pkg/validationfile/loader.go new file mode 100644 index 0000000..d3322b6 --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/validationfile/loader.go @@ -0,0 +1,172 @@ +package validationfile + +import ( + "context" + "fmt" + "os" + + log "github.com/authzed/spicedb/internal/logging" + dsctx "github.com/authzed/spicedb/internal/middleware/datastore" + "github.com/authzed/spicedb/internal/namespace" + "github.com/authzed/spicedb/internal/relationships" + caveattypes "github.com/authzed/spicedb/pkg/caveats/types" + "github.com/authzed/spicedb/pkg/datastore" + "github.com/authzed/spicedb/pkg/genutil/slicez" + core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/schema" + "github.com/authzed/spicedb/pkg/tuple" +) + +// PopulatedValidationFile contains the fully parsed information from a validation file. +type PopulatedValidationFile struct { + // Schema is the entered schema text, if any. + Schema string + + // NamespaceDefinitions are the namespaces defined in the validation file, in either + // direct or compiled from schema form. + NamespaceDefinitions []*core.NamespaceDefinition + + // CaveatDefinitions are the caveats defined in the validation file, in either + // direct or compiled from schema form. + CaveatDefinitions []*core.CaveatDefinition + + // Relationships are the relationships defined in the validation file, either directly + // or in the relationships block. + Relationships []tuple.Relationship + + // ParsedFiles are the underlying parsed validation files. + ParsedFiles []ValidationFile +} + +// PopulateFromFiles populates the given datastore with the namespaces and tuples found in +// the validation file(s) specified. +func PopulateFromFiles(ctx context.Context, ds datastore.Datastore, caveatTypeSet *caveattypes.TypeSet, filePaths []string) (*PopulatedValidationFile, datastore.Revision, error) { + contents := map[string][]byte{} + + for _, filePath := range filePaths { + fileContents, err := os.ReadFile(filePath) + if err != nil { + return nil, datastore.NoRevision, err + } + + contents[filePath] = fileContents + } + + return PopulateFromFilesContents(ctx, ds, caveatTypeSet, contents) +} + +// PopulateFromFilesContents populates the given datastore with the namespaces and tuples found in +// the validation file(s) contents specified. +func PopulateFromFilesContents(ctx context.Context, ds datastore.Datastore, caveatTypeSet *caveattypes.TypeSet, filesContents map[string][]byte) (*PopulatedValidationFile, datastore.Revision, error) { + var schemaStr string + var objectDefs []*core.NamespaceDefinition + var caveatDefs []*core.CaveatDefinition + var rels []tuple.Relationship + var updates []tuple.RelationshipUpdate + + var revision datastore.Revision + + files := make([]ValidationFile, 0, len(filesContents)) + + // Parse each file into definitions and relationship updates. + for filePath, fileContents := range filesContents { + // Decode the validation file. + parsed, err := DecodeValidationFile(fileContents) + if err != nil { + return nil, datastore.NoRevision, fmt.Errorf("error when parsing config file %s: %w", filePath, err) + } + + files = append(files, *parsed) + + // Disallow legacy sections. + if len(parsed.NamespaceConfigs) > 0 { + return nil, revision, fmt.Errorf("definitions must be specified in `schema`") + } + + if len(parsed.ValidationTuples) > 0 { + return nil, revision, fmt.Errorf("relationships must be specified in `relationships`") + } + + // Add schema definitions. + if parsed.Schema.CompiledSchema != nil { + defs := parsed.Schema.CompiledSchema.ObjectDefinitions + if len(defs) > 0 { + schemaStr += parsed.Schema.Schema + "\n\n" + } + + log.Ctx(ctx).Info().Str("filePath", filePath). + Int("definitionCount", len(defs)). + Int("caveatDefinitionCount", len(parsed.Schema.CompiledSchema.CaveatDefinitions)). + Int("schemaDefinitionCount", len(parsed.Schema.CompiledSchema.OrderedDefinitions)). + Msg("adding schema definitions") + + objectDefs = append(objectDefs, defs...) + caveatDefs = append(caveatDefs, parsed.Schema.CompiledSchema.CaveatDefinitions...) + } + + // Parse relationships for updates. + for _, rel := range parsed.Relationships.Relationships { + updates = append(updates, tuple.Touch(rel)) + rels = append(rels, rel) + } + } + + // Load the definitions and relationships into the datastore. + revision, err := ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { + // Write the caveat definitions. + err := rwt.WriteCaveats(ctx, caveatDefs) + if err != nil { + return err + } + + res := schema.ResolverForDatastoreReader(rwt).WithPredefinedElements(schema.PredefinedElements{ + Definitions: objectDefs, + Caveats: caveatDefs, + }) + ts := schema.NewTypeSystem(res) + // Validate and write the object definitions. + for _, objectDef := range objectDefs { + ctx := dsctx.ContextWithDatastore(ctx, ds) + vts, terr := ts.GetValidatedDefinition(ctx, objectDef.GetName()) + if terr != nil { + return terr + } + + aerr := namespace.AnnotateNamespace(vts) + if aerr != nil { + return aerr + } + + if err := rwt.WriteNamespaces(ctx, objectDef); err != nil { + return fmt.Errorf("error when loading object definition %s: %w", objectDef.Name, err) + } + } + + return err + }) + + slicez.ForEachChunk(updates, 500, func(chunked []tuple.RelationshipUpdate) { + if err != nil { + return + } + + chunkedRels := make([]tuple.Relationship, 0, len(chunked)) + for _, update := range chunked { + chunkedRels = append(chunkedRels, update.Relationship) + } + revision, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { + err = relationships.ValidateRelationshipsForCreateOrTouch(ctx, rwt, caveatTypeSet, chunkedRels...) + if err != nil { + return err + } + + return rwt.WriteRelationships(ctx, chunked) + }) + }) + + if err != nil { + return nil, nil, err + } + + return &PopulatedValidationFile{schemaStr, objectDefs, caveatDefs, rels, files}, revision, err +} |
