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 }