summaryrefslogtreecommitdiff
path: root/vendor/github.com/authzed/spicedb/pkg/development/validation.go
diff options
context:
space:
mode:
authormo khan <mo@mokhan.ca>2025-07-24 17:58:01 -0600
committermo khan <mo@mokhan.ca>2025-07-24 17:58:01 -0600
commit72296119fc9755774719f8f625ad03e0e0ec457a (patch)
treeed236ddee12a20fb55b7cfecf13f62d3a000dcb5 /vendor/github.com/authzed/spicedb/pkg/development/validation.go
parenta920a8cfe415858bb2777371a77018599ffed23f (diff)
parenteaa1bd3b8e12934aed06413d75e7482ac58d805a (diff)
Merge branch 'the-spice-must-flow' into 'main'
Add SpiceDB Authorization See merge request gitlab-org/software-supply-chain-security/authorization/sparkled!19
Diffstat (limited to 'vendor/github.com/authzed/spicedb/pkg/development/validation.go')
-rw-r--r--vendor/github.com/authzed/spicedb/pkg/development/validation.go286
1 files changed, 286 insertions, 0 deletions
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
+}