github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/pkg/development/validation.go (about)

     1  package development
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strings"
     7  
     8  	"github.com/google/go-cmp/cmp"
     9  	yaml "gopkg.in/yaml.v2"
    10  
    11  	"github.com/authzed/spicedb/internal/developmentmembership"
    12  	devinterface "github.com/authzed/spicedb/pkg/proto/developer/v1"
    13  	v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1"
    14  	"github.com/authzed/spicedb/pkg/tuple"
    15  	"github.com/authzed/spicedb/pkg/validationfile/blocks"
    16  )
    17  
    18  // RunValidation runs the parsed validation block against the data in the dev context.
    19  func RunValidation(devContext *DevContext, validation *blocks.ParsedExpectedRelations) (*developmentmembership.Set, []*devinterface.DeveloperError, error) {
    20  	var failures []*devinterface.DeveloperError
    21  	membershipSet := developmentmembership.NewMembershipSet()
    22  	ctx := devContext.Ctx
    23  
    24  	for onrKey, expectedSubjects := range validation.ValidationMap {
    25  		if onrKey.ObjectAndRelation == nil {
    26  			return nil, nil, fmt.Errorf("got nil ObjectAndRelation for key %s", onrKey.ObjectRelationString)
    27  		}
    28  
    29  		// Run a full recursive expansion over the ONR.
    30  		er, derr := devContext.Dispatcher.DispatchExpand(ctx, &v1.DispatchExpandRequest{
    31  			ResourceAndRelation: onrKey.ObjectAndRelation,
    32  			Metadata: &v1.ResolverMeta{
    33  				AtRevision:     devContext.Revision.String(),
    34  				DepthRemaining: maxDispatchDepth,
    35  				TraversalBloom: v1.MustNewTraversalBloomFilter(uint(maxDispatchDepth)),
    36  			},
    37  			ExpansionMode: v1.DispatchExpandRequest_RECURSIVE,
    38  		})
    39  		if derr != nil {
    40  			devErr, wireErr := DistinguishGraphError(devContext, derr, devinterface.DeveloperError_VALIDATION_YAML, 0, 0, onrKey.ObjectRelationString)
    41  			if wireErr != nil {
    42  				return nil, nil, wireErr
    43  			}
    44  
    45  			failures = append(failures, devErr)
    46  			continue
    47  		}
    48  
    49  		// Add the ONR and its expansion to the membership set.
    50  		foundSubjects, _, aerr := membershipSet.AddExpansion(onrKey.ObjectAndRelation, er.TreeNode)
    51  		if aerr != nil {
    52  			devErr, wireErr := DistinguishGraphError(devContext, aerr, devinterface.DeveloperError_VALIDATION_YAML, 0, 0, onrKey.ObjectRelationString)
    53  			if wireErr != nil {
    54  				return nil, nil, wireErr
    55  			}
    56  
    57  			failures = append(failures, devErr)
    58  			continue
    59  		}
    60  
    61  		// Compare the terminal subjects found to those specified.
    62  		errs := validateSubjects(onrKey, foundSubjects, expectedSubjects)
    63  		failures = append(failures, errs...)
    64  	}
    65  
    66  	if len(failures) > 0 {
    67  		return membershipSet, failures, nil
    68  	}
    69  
    70  	return membershipSet, nil, nil
    71  }
    72  
    73  func wrapRelationships(onrStrings []string) []string {
    74  	wrapped := make([]string, 0, len(onrStrings))
    75  	for _, str := range onrStrings {
    76  		wrapped = append(wrapped, "<"+str+">")
    77  	}
    78  
    79  	// Sort to ensure stability.
    80  	sort.Strings(wrapped)
    81  	return wrapped
    82  }
    83  
    84  func validateSubjects(onrKey blocks.ObjectRelation, fs developmentmembership.FoundSubjects, expectedSubjects []blocks.ExpectedSubject) []*devinterface.DeveloperError {
    85  	onr := onrKey.ObjectAndRelation
    86  
    87  	var failures []*devinterface.DeveloperError
    88  
    89  	// Verify that every referenced subject is found in the membership.
    90  	encounteredSubjects := map[string]struct{}{}
    91  	for _, expectedSubject := range expectedSubjects {
    92  		subjectWithExceptions := expectedSubject.SubjectWithExceptions
    93  		if subjectWithExceptions == nil {
    94  			failures = append(failures, &devinterface.DeveloperError{
    95  				Message: fmt.Sprintf("For object and permission/relation `%s`, no expected subject specified in `%s`", tuple.StringONR(onr), expectedSubject.ValidationString),
    96  				Source:  devinterface.DeveloperError_VALIDATION_YAML,
    97  				Kind:    devinterface.DeveloperError_MISSING_EXPECTED_RELATIONSHIP,
    98  				Context: string(expectedSubject.ValidationString),
    99  				Line:    uint32(expectedSubject.SourcePosition.LineNumber),
   100  				Column:  uint32(expectedSubject.SourcePosition.ColumnPosition),
   101  			})
   102  			continue
   103  		}
   104  
   105  		encounteredSubjects[tuple.StringONR(subjectWithExceptions.Subject.Subject)] = struct{}{}
   106  
   107  		subject, ok := fs.LookupSubject(subjectWithExceptions.Subject.Subject)
   108  		if !ok {
   109  			failures = append(failures, &devinterface.DeveloperError{
   110  				Message: fmt.Sprintf("For object and permission/relation `%s`, missing expected subject `%s`", tuple.StringONR(onr), tuple.StringONR(subjectWithExceptions.Subject.Subject)),
   111  				Source:  devinterface.DeveloperError_VALIDATION_YAML,
   112  				Kind:    devinterface.DeveloperError_MISSING_EXPECTED_RELATIONSHIP,
   113  				Context: string(expectedSubject.ValidationString),
   114  				Line:    uint32(expectedSubject.SourcePosition.LineNumber),
   115  				Column:  uint32(expectedSubject.SourcePosition.ColumnPosition),
   116  			})
   117  			continue
   118  		}
   119  
   120  		foundRelationships := subject.Relationships()
   121  
   122  		// Verify that the relationships are the same.
   123  		expectedONRStrings := tuple.StringsONRs(expectedSubject.Resources)
   124  		foundONRStrings := tuple.StringsONRs(foundRelationships)
   125  		if !cmp.Equal(expectedONRStrings, foundONRStrings) {
   126  			failures = append(failures, &devinterface.DeveloperError{
   127  				Message: fmt.Sprintf("For object and permission/relation `%s`, found different relationships for subject `%s`: Specified: `%s`, Computed: `%s`",
   128  					tuple.StringONR(onr),
   129  					tuple.StringONR(subjectWithExceptions.Subject.Subject),
   130  					strings.Join(wrapRelationships(expectedONRStrings), "/"),
   131  					strings.Join(wrapRelationships(foundONRStrings), "/"),
   132  				),
   133  				Source:  devinterface.DeveloperError_VALIDATION_YAML,
   134  				Kind:    devinterface.DeveloperError_MISSING_EXPECTED_RELATIONSHIP,
   135  				Context: string(expectedSubject.ValidationString),
   136  				Line:    uint32(expectedSubject.SourcePosition.LineNumber),
   137  				Column:  uint32(expectedSubject.SourcePosition.ColumnPosition),
   138  			})
   139  		}
   140  
   141  		// Verify exclusions are the same, if any.
   142  		foundExcludedSubjects, isWildcard := subject.ExcludedSubjectsFromWildcard()
   143  		expectedExcludedSubjects := subjectWithExceptions.Exceptions
   144  		if isWildcard {
   145  			expectedExcludedStrings := toExpectedRelationshipsStrings(expectedExcludedSubjects)
   146  			foundExcludedONRStrings := toFoundRelationshipsStrings(foundExcludedSubjects)
   147  			if !cmp.Equal(expectedExcludedStrings, foundExcludedONRStrings) {
   148  				failures = append(failures, &devinterface.DeveloperError{
   149  					Message: fmt.Sprintf("For object and permission/relation `%s`, found different excluded subjects for subject `%s`: Specified: `%s`, Computed: `%s`",
   150  						tuple.StringONR(onr),
   151  						tuple.StringONR(subjectWithExceptions.Subject.Subject),
   152  						strings.Join(wrapRelationships(expectedExcludedStrings), ", "),
   153  						strings.Join(wrapRelationships(foundExcludedONRStrings), ", "),
   154  					),
   155  					Source:  devinterface.DeveloperError_VALIDATION_YAML,
   156  					Kind:    devinterface.DeveloperError_MISSING_EXPECTED_RELATIONSHIP,
   157  					Context: string(expectedSubject.ValidationString),
   158  					Line:    uint32(expectedSubject.SourcePosition.LineNumber),
   159  					Column:  uint32(expectedSubject.SourcePosition.ColumnPosition),
   160  				})
   161  			}
   162  		} else {
   163  			if len(expectedExcludedSubjects) > 0 {
   164  				failures = append(failures, &devinterface.DeveloperError{
   165  					Message: fmt.Sprintf("For object and permission/relation `%s`, found unexpected excluded subjects",
   166  						tuple.StringONR(onr),
   167  					),
   168  					Source:  devinterface.DeveloperError_VALIDATION_YAML,
   169  					Kind:    devinterface.DeveloperError_EXTRA_RELATIONSHIP_FOUND,
   170  					Context: string(expectedSubject.ValidationString),
   171  					Line:    uint32(expectedSubject.SourcePosition.LineNumber),
   172  					Column:  uint32(expectedSubject.SourcePosition.ColumnPosition),
   173  				})
   174  			}
   175  		}
   176  
   177  		// Verify caveats.
   178  		if (subject.GetCaveatExpression() != nil) != subjectWithExceptions.Subject.IsCaveated {
   179  			failures = append(failures, &devinterface.DeveloperError{
   180  				Message: fmt.Sprintf("For object and permission/relation `%s`, found caveat mismatch",
   181  					tuple.StringONR(onr),
   182  				),
   183  				Source:  devinterface.DeveloperError_VALIDATION_YAML,
   184  				Kind:    devinterface.DeveloperError_MISSING_EXPECTED_RELATIONSHIP,
   185  				Context: string(expectedSubject.ValidationString),
   186  				Line:    uint32(expectedSubject.SourcePosition.LineNumber),
   187  				Column:  uint32(expectedSubject.SourcePosition.ColumnPosition),
   188  			})
   189  		}
   190  	}
   191  
   192  	// Verify that every subject found was referenced.
   193  	for _, foundSubject := range fs.ListFound() {
   194  		_, ok := encounteredSubjects[tuple.StringONR(foundSubject.Subject())]
   195  		if !ok {
   196  			failures = append(failures, &devinterface.DeveloperError{
   197  				Message: fmt.Sprintf("For object and permission/relation `%s`, subject `%s` found but missing from specified",
   198  					tuple.StringONR(onr),
   199  					tuple.StringONR(foundSubject.Subject()),
   200  				),
   201  				Source:  devinterface.DeveloperError_VALIDATION_YAML,
   202  				Kind:    devinterface.DeveloperError_EXTRA_RELATIONSHIP_FOUND,
   203  				Context: tuple.StringONR(onr),
   204  				Line:    uint32(onrKey.SourcePosition.LineNumber),
   205  				Column:  uint32(onrKey.SourcePosition.ColumnPosition),
   206  			})
   207  		}
   208  	}
   209  
   210  	return failures
   211  }
   212  
   213  // GenerateValidation generates the validation block based on a membership set.
   214  func GenerateValidation(membershipSet *developmentmembership.Set) (string, error) {
   215  	validationMap := map[string][]string{}
   216  	subjectsByONR := membershipSet.SubjectsByONR()
   217  
   218  	onrStrings := make([]string, 0, len(subjectsByONR))
   219  	for onrString := range subjectsByONR {
   220  		onrStrings = append(onrStrings, onrString)
   221  	}
   222  
   223  	// Sort to ensure stability of output.
   224  	sort.Strings(onrStrings)
   225  
   226  	for _, onrString := range onrStrings {
   227  		foundSubjects := subjectsByONR[onrString]
   228  		var strs []string
   229  		for _, fs := range foundSubjects.ListFound() {
   230  			strs = append(strs,
   231  				fmt.Sprintf("[%s] is %s",
   232  					fs.ToValidationString(),
   233  					strings.Join(wrapRelationships(tuple.StringsONRs(fs.Relationships())), "/"),
   234  				))
   235  		}
   236  
   237  		// Sort to ensure stability of output.
   238  		sort.Strings(strs)
   239  		validationMap[onrString] = strs
   240  	}
   241  
   242  	contents, err := yaml.Marshal(validationMap)
   243  	if err != nil {
   244  		return "", err
   245  	}
   246  
   247  	return string(contents), nil
   248  }
   249  
   250  func toExpectedRelationshipsStrings(subs []blocks.SubjectAndCaveat) []string {
   251  	mapped := make([]string, 0, len(subs))
   252  	for _, sub := range subs {
   253  		if sub.IsCaveated {
   254  			mapped = append(mapped, tuple.StringONR(sub.Subject)+"[...]")
   255  		} else {
   256  			mapped = append(mapped, tuple.StringONR(sub.Subject))
   257  		}
   258  	}
   259  	return mapped
   260  }
   261  
   262  func toFoundRelationshipsStrings(subs []developmentmembership.FoundSubject) []string {
   263  	mapped := make([]string, 0, len(subs))
   264  	for _, sub := range subs {
   265  		mapped = append(mapped, sub.ToValidationString())
   266  	}
   267  	return mapped
   268  }