From 20ef0d92694465ac86b550df139e8366a0a2b4fa Mon Sep 17 00:00:00 2001 From: mo khan Date: Tue, 22 Jul 2025 17:35:49 -0600 Subject: feat: connect to spicedb --- .../authzed/spicedb/pkg/development/validation.go | 286 +++++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 vendor/github.com/authzed/spicedb/pkg/development/validation.go (limited to 'vendor/github.com/authzed/spicedb/pkg/development/validation.go') diff --git a/vendor/github.com/authzed/spicedb/pkg/development/validation.go b/vendor/github.com/authzed/spicedb/pkg/development/validation.go new file mode 100644 index 0000000..b3da6cf --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/development/validation.go @@ -0,0 +1,286 @@ +package development + +import ( + "fmt" + "sort" + "strings" + + "github.com/ccoveille/go-safecast" + "github.com/google/go-cmp/cmp" + yaml "gopkg.in/yaml.v2" + + "github.com/authzed/spicedb/internal/developmentmembership" + log "github.com/authzed/spicedb/internal/logging" + devinterface "github.com/authzed/spicedb/pkg/proto/developer/v1" + v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" + "github.com/authzed/spicedb/pkg/tuple" + "github.com/authzed/spicedb/pkg/validationfile/blocks" +) + +// RunValidation runs the parsed validation block against the data in the dev context. +func RunValidation(devContext *DevContext, validation *blocks.ParsedExpectedRelations) (*developmentmembership.Set, []*devinterface.DeveloperError, error) { + var failures []*devinterface.DeveloperError + membershipSet := developmentmembership.NewMembershipSet() + ctx := devContext.Ctx + + for onrKey, expectedSubjects := range validation.ValidationMap { + // Run a full recursive expansion over the ONR. + er, derr := devContext.Dispatcher.DispatchExpand(ctx, &v1.DispatchExpandRequest{ + ResourceAndRelation: onrKey.ObjectAndRelation.ToCoreONR(), + Metadata: &v1.ResolverMeta{ + AtRevision: devContext.Revision.String(), + DepthRemaining: maxDispatchDepth, + TraversalBloom: v1.MustNewTraversalBloomFilter(uint(maxDispatchDepth)), + }, + ExpansionMode: v1.DispatchExpandRequest_RECURSIVE, + }) + if derr != nil { + devErr, wireErr := DistinguishGraphError(devContext, derr, devinterface.DeveloperError_VALIDATION_YAML, 0, 0, onrKey.ObjectRelationString) + if wireErr != nil { + return nil, nil, wireErr + } + + failures = append(failures, devErr) + continue + } + + // Add the ONR and its expansion to the membership set. + foundSubjects, _, aerr := membershipSet.AddExpansion(onrKey.ObjectAndRelation, er.TreeNode) + if aerr != nil { + devErr, wireErr := DistinguishGraphError(devContext, aerr, devinterface.DeveloperError_VALIDATION_YAML, 0, 0, onrKey.ObjectRelationString) + if wireErr != nil { + return nil, nil, wireErr + } + + failures = append(failures, devErr) + continue + } + + // Compare the terminal subjects found to those specified. + errs := validateSubjects(onrKey, foundSubjects, expectedSubjects) + failures = append(failures, errs...) + } + + if len(failures) > 0 { + return membershipSet, failures, nil + } + + return membershipSet, nil, nil +} + +func wrapResources(onrStrings []string) []string { + wrapped := make([]string, 0, len(onrStrings)) + for _, str := range onrStrings { + wrapped = append(wrapped, "<"+str+">") + } + + // Sort to ensure stability. + sort.Strings(wrapped) + return wrapped +} + +func validateSubjects(onrKey blocks.ObjectRelation, fs developmentmembership.FoundSubjects, expectedSubjects []blocks.ExpectedSubject) []*devinterface.DeveloperError { + onr := onrKey.ObjectAndRelation + + var failures []*devinterface.DeveloperError + + // Verify that every referenced subject is found in the membership. + encounteredSubjects := map[string]struct{}{} + for _, expectedSubject := range expectedSubjects { + subjectWithExceptions := expectedSubject.SubjectWithExceptions + // NOTE: zeroes are fine here on failure. + lineNumber, err := safecast.ToUint32(expectedSubject.SourcePosition.LineNumber) + if err != nil { + log.Err(err).Msg("could not cast lineNumber to uint32") + } + columnPosition, err := safecast.ToUint32(expectedSubject.SourcePosition.ColumnPosition) + if err != nil { + log.Err(err).Msg("could not cast columnPosition to uint32") + } + if subjectWithExceptions == nil { + failures = append(failures, &devinterface.DeveloperError{ + Message: fmt.Sprintf("For object and permission/relation `%s`, no expected subject specified in `%s`", tuple.StringONR(onr), expectedSubject.ValidationString), + Source: devinterface.DeveloperError_VALIDATION_YAML, + Kind: devinterface.DeveloperError_MISSING_EXPECTED_RELATIONSHIP, + Context: string(expectedSubject.ValidationString), + Line: lineNumber, + Column: columnPosition, + }) + continue + } + + encounteredSubjects[tuple.StringONR(subjectWithExceptions.Subject.Subject)] = struct{}{} + + subject, ok := fs.LookupSubject(subjectWithExceptions.Subject.Subject) + if !ok { + failures = append(failures, &devinterface.DeveloperError{ + Message: fmt.Sprintf("For object and permission/relation `%s`, missing expected subject `%s`", tuple.StringONR(onr), tuple.StringONR(subjectWithExceptions.Subject.Subject)), + Source: devinterface.DeveloperError_VALIDATION_YAML, + Kind: devinterface.DeveloperError_MISSING_EXPECTED_RELATIONSHIP, + Context: string(expectedSubject.ValidationString), + Line: lineNumber, + Column: columnPosition, + }) + continue + } + + // Verify that the relationships are the same. + foundParentResources := subject.ParentResources() + expectedONRStrings := tuple.StringsONRs(expectedSubject.Resources) + foundONRStrings := tuple.StringsONRs(foundParentResources) + if !cmp.Equal(expectedONRStrings, foundONRStrings) { + failures = append(failures, &devinterface.DeveloperError{ + Message: fmt.Sprintf("For object and permission/relation `%s`, found different relationships for subject `%s`: Specified: `%s`, Computed: `%s`", + tuple.StringONR(onr), + tuple.StringONR(subjectWithExceptions.Subject.Subject), + strings.Join(wrapResources(expectedONRStrings), "/"), + strings.Join(wrapResources(foundONRStrings), "/"), + ), + Source: devinterface.DeveloperError_VALIDATION_YAML, + Kind: devinterface.DeveloperError_MISSING_EXPECTED_RELATIONSHIP, + Context: string(expectedSubject.ValidationString), + Line: lineNumber, + Column: columnPosition, + }) + } + + // Verify exclusions are the same, if any. + foundExcludedSubjects, isWildcard := subject.ExcludedSubjectsFromWildcard() + expectedExcludedSubjects := subjectWithExceptions.Exceptions + if isWildcard { + expectedExcludedStrings := toExpectedRelationshipsStrings(expectedExcludedSubjects) + foundExcludedONRStrings := toFoundRelationshipsStrings(foundExcludedSubjects) + + sort.Strings(expectedExcludedStrings) + sort.Strings(foundExcludedONRStrings) + + if !cmp.Equal(expectedExcludedStrings, foundExcludedONRStrings) { + failures = append(failures, &devinterface.DeveloperError{ + Message: fmt.Sprintf("For object and permission/relation `%s`, found different excluded subjects for subject `%s`: Specified: `%s`, Computed: `%s`", + tuple.StringONR(onr), + tuple.StringONR(subjectWithExceptions.Subject.Subject), + strings.Join(wrapResources(expectedExcludedStrings), ", "), + strings.Join(wrapResources(foundExcludedONRStrings), ", "), + ), + Source: devinterface.DeveloperError_VALIDATION_YAML, + Kind: devinterface.DeveloperError_MISSING_EXPECTED_RELATIONSHIP, + Context: string(expectedSubject.ValidationString), + Line: lineNumber, + Column: columnPosition, + }) + } + } else { + if len(expectedExcludedSubjects) > 0 { + failures = append(failures, &devinterface.DeveloperError{ + Message: fmt.Sprintf("For object and permission/relation `%s`, found unexpected excluded subjects", + tuple.StringONR(onr), + ), + Source: devinterface.DeveloperError_VALIDATION_YAML, + Kind: devinterface.DeveloperError_EXTRA_RELATIONSHIP_FOUND, + Context: string(expectedSubject.ValidationString), + Line: lineNumber, + Column: columnPosition, + }) + } + } + + // Verify caveats. + if (subject.GetCaveatExpression() != nil) != subjectWithExceptions.Subject.IsCaveated { + failures = append(failures, &devinterface.DeveloperError{ + Message: fmt.Sprintf("For object and permission/relation `%s`, found caveat mismatch", + tuple.StringONR(onr), + ), + Source: devinterface.DeveloperError_VALIDATION_YAML, + Kind: devinterface.DeveloperError_MISSING_EXPECTED_RELATIONSHIP, + Context: string(expectedSubject.ValidationString), + Line: lineNumber, + Column: columnPosition, + }) + } + } + + // Verify that every subject found was referenced. + for _, foundSubject := range fs.ListFound() { + _, ok := encounteredSubjects[tuple.StringONR(foundSubject.Subject())] + if !ok { + onrLineNumber, err := safecast.ToUint32(onrKey.SourcePosition.LineNumber) + if err != nil { + log.Err(err).Msg("could not cast lineNumber to uint32") + } + onrColumnPosition, err := safecast.ToUint32(onrKey.SourcePosition.ColumnPosition) + if err != nil { + log.Err(err).Msg("could not cast columnPosition to uint32") + } + failures = append(failures, &devinterface.DeveloperError{ + Message: fmt.Sprintf("For object and permission/relation `%s`, subject `%s` found but not listed in expected subjects", + tuple.StringONR(onr), + tuple.StringONR(foundSubject.Subject()), + ), + Source: devinterface.DeveloperError_VALIDATION_YAML, + Kind: devinterface.DeveloperError_EXTRA_RELATIONSHIP_FOUND, + Context: tuple.StringONR(onr), + Line: onrLineNumber, + Column: onrColumnPosition, + }) + } + } + + return failures +} + +// GenerateValidation generates the validation block based on a membership set. +func GenerateValidation(membershipSet *developmentmembership.Set) (string, error) { + validationMap := map[string][]string{} + subjectsByONR := membershipSet.SubjectsByONR() + + onrStrings := make([]string, 0, len(subjectsByONR)) + for onrString := range subjectsByONR { + onrStrings = append(onrStrings, onrString) + } + + // Sort to ensure stability of output. + sort.Strings(onrStrings) + + for _, onrString := range onrStrings { + foundSubjects := subjectsByONR[onrString] + var strs []string + for _, fs := range foundSubjects.ListFound() { + strs = append(strs, + fmt.Sprintf("[%s] is %s", + fs.ToValidationString(), + strings.Join(wrapResources(tuple.StringsONRs(fs.ParentResources())), "/"), + )) + } + + // Sort to ensure stability of output. + sort.Strings(strs) + validationMap[onrString] = strs + } + + contents, err := yaml.Marshal(validationMap) + if err != nil { + return "", err + } + + return string(contents), nil +} + +func toExpectedRelationshipsStrings(subs []blocks.SubjectAndCaveat) []string { + mapped := make([]string, 0, len(subs)) + for _, sub := range subs { + if sub.IsCaveated { + mapped = append(mapped, tuple.StringONR(sub.Subject)+"[...]") + } else { + mapped = append(mapped, tuple.StringONR(sub.Subject)) + } + } + return mapped +} + +func toFoundRelationshipsStrings(subs []developmentmembership.FoundSubject) []string { + mapped := make([]string, 0, len(subs)) + for _, sub := range subs { + mapped = append(mapped, sub.ToValidationString()) + } + return mapped +} -- cgit v1.2.3