github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/services/integrationtesting/consistency_test.go (about) 1 //go:build !skipintegrationtests 2 // +build !skipintegrationtests 3 4 package integrationtesting_test 5 6 import ( 7 "context" 8 "fmt" 9 "path" 10 "sort" 11 "testing" 12 "time" 13 14 "github.com/jzelinskie/stringz" 15 "github.com/stretchr/testify/require" 16 "golang.org/x/exp/maps" 17 "google.golang.org/protobuf/types/known/structpb" 18 yamlv2 "gopkg.in/yaml.v2" 19 20 v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" 21 22 "github.com/authzed/spicedb/internal/developmentmembership" 23 "github.com/authzed/spicedb/internal/dispatch" 24 "github.com/authzed/spicedb/internal/graph" 25 "github.com/authzed/spicedb/internal/services/integrationtesting/consistencytestutil" 26 "github.com/authzed/spicedb/pkg/datastore" 27 "github.com/authzed/spicedb/pkg/development" 28 "github.com/authzed/spicedb/pkg/genutil/mapz" 29 core "github.com/authzed/spicedb/pkg/proto/core/v1" 30 devinterface "github.com/authzed/spicedb/pkg/proto/developer/v1" 31 dispatchv1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" 32 "github.com/authzed/spicedb/pkg/tuple" 33 "github.com/authzed/spicedb/pkg/typesystem" 34 "github.com/authzed/spicedb/pkg/validationfile" 35 "github.com/authzed/spicedb/pkg/validationfile/blocks" 36 ) 37 38 const testTimedelta = 1 * time.Second 39 40 // TestConsistency is a system-wide consistency test suite that reads in various 41 // validation files in the testconfigs directory, and executes a full set of APIs 42 // against the data within, ensuring that all results of the various APIs are 43 // consistent with one another. 44 // 45 // This test suite acts as essentially a full integration test for the API, 46 // dispatching, caching, computation and datastore layers. It should reflect 47 // both real-world schemas, as well as the full set of hand-constructed corner 48 // cases so that the system can be fully exercised. 49 func TestConsistency(t *testing.T) { 50 // Set dispatch sizes for testing. 51 graph.SetDispatchChunkSizesForTesting(t, []uint16{5, 10}) 52 53 // List all the defined consistency test files. 54 consistencyTestFiles, err := consistencytestutil.ListTestConfigs() 55 require.NoError(t, err) 56 57 for _, filePath := range consistencyTestFiles { 58 filePath := filePath 59 60 t.Run(path.Base(filePath), func(t *testing.T) { 61 for _, dispatcherKind := range []string{"local", "caching"} { 62 dispatcherKind := dispatcherKind 63 64 t.Run(dispatcherKind, func(t *testing.T) { 65 t.Parallel() 66 runConsistencyTestSuiteForFile(t, filePath, dispatcherKind == "caching") 67 }) 68 } 69 }) 70 } 71 } 72 73 func runConsistencyTestSuiteForFile(t *testing.T, filePath string, useCachingDispatcher bool) { 74 cad := consistencytestutil.LoadDataAndCreateClusterForTesting(t, filePath, testTimedelta) 75 76 // Validate the type system for each namespace. 77 headRevision, err := cad.DataStore.HeadRevision(cad.Ctx) 78 require.NoError(t, err) 79 80 for _, nsDef := range cad.Populated.NamespaceDefinitions { 81 _, ts, err := typesystem.ReadNamespaceAndTypes( 82 cad.Ctx, 83 nsDef.Name, 84 cad.DataStore.SnapshotReader(headRevision), 85 ) 86 require.NoError(t, err) 87 88 _, err = ts.Validate(cad.Ctx) 89 require.NoError(t, err) 90 } 91 92 // Run consistency tests. 93 testers := consistencytestutil.ServiceTesters(cad.Conn) 94 for _, tester := range testers { 95 tester := tester 96 97 t.Run(tester.Name(), func(t *testing.T) { 98 runConsistencyTestsWithServiceTester(t, cad, tester, headRevision, useCachingDispatcher) 99 }) 100 } 101 } 102 103 type validationContext struct { 104 clusterAndData consistencytestutil.ConsistencyClusterAndData 105 accessibilitySet *consistencytestutil.AccessibilitySet 106 serviceTester consistencytestutil.ServiceTester 107 revision datastore.Revision 108 dispatcher dispatch.Dispatcher 109 } 110 111 func runConsistencyTestsWithServiceTester( 112 t *testing.T, 113 cad consistencytestutil.ConsistencyClusterAndData, 114 tester consistencytestutil.ServiceTester, 115 revision datastore.Revision, 116 useCachingDispatcher bool, 117 ) { 118 // Build an accessibility set. 119 accessibilitySet := consistencytestutil.BuildAccessibilitySet(t, cad) 120 121 dispatcher := consistencytestutil.CreateDispatcherForTesting(t, useCachingDispatcher) 122 123 vctx := validationContext{ 124 clusterAndData: cad, 125 accessibilitySet: accessibilitySet, 126 serviceTester: tester, 127 revision: revision, 128 dispatcher: dispatcher, 129 } 130 131 // Call a write on each relationship to make sure it type checks. 132 ensureRelationshipWrites(t, vctx) 133 134 // Call a read on each relationship resource type and ensure it finds all expected relationships. 135 validateRelationshipReads(t, vctx) 136 137 // Run the assertions defined in the file. 138 runAssertions(t, vctx) 139 140 // Run basic expansion on each relation and ensure no errors are raised. 141 ensureNoExpansionErrors(t, vctx) 142 143 // Run a fully recursive expand on each relation and ensure all terminal subjects are reached. 144 validateExpansionSubjects(t, vctx) 145 146 // For each relation in each namespace, for each subject, collect the resources accessible 147 // to that subject and then verify the lookup resources returns the same set of subjects. 148 validateLookupResources(t, vctx) 149 150 // For each object accessible, validate that the subjects that can access it are found. 151 validateLookupSubjects(t, vctx) 152 153 // Run the development system over the full set of context and ensure they also return the expected information. 154 validateDevelopment(t, vctx) 155 } 156 157 // testForEachRelationship runs a subtest for each relationship defined. 158 func testForEachRelationship( 159 t *testing.T, 160 vctx validationContext, 161 prefix string, 162 handler func(t *testing.T, relationship *core.RelationTuple), 163 ) { 164 t.Helper() 165 166 for _, relationship := range vctx.clusterAndData.Populated.Tuples { 167 relationship := relationship 168 t.Run(fmt.Sprintf("%s_%s", prefix, tuple.MustString(relationship)), 169 func(t *testing.T) { 170 handler(t, relationship) 171 }) 172 } 173 } 174 175 // testForEachResource runs a subtest for each possible resource+relation in the schema. 176 func testForEachResource( 177 t *testing.T, 178 vctx validationContext, 179 prefix string, 180 handler func(t *testing.T, resource *core.ObjectAndRelation), 181 ) { 182 t.Helper() 183 184 encountered := mapz.NewSet[string]() 185 for _, resourceType := range vctx.clusterAndData.Populated.NamespaceDefinitions { 186 resources, ok := vctx.accessibilitySet.ResourcesByNamespace.Get(resourceType.Name) 187 if !ok { 188 continue 189 } 190 191 resourceType := resourceType 192 for _, relation := range resourceType.Relation { 193 relation := relation 194 for _, resource := range resources { 195 resource := resource 196 onr := &core.ObjectAndRelation{ 197 Namespace: resourceType.Name, 198 ObjectId: resource.ObjectId, 199 Relation: relation.Name, 200 } 201 key := tuple.StringONR(onr) 202 if !encountered.Add(key) { 203 continue 204 } 205 206 t.Run(fmt.Sprintf("%s_%s_%s_%s", prefix, resourceType.Name, resource.ObjectId, relation.Name), 207 func(t *testing.T) { 208 handler(t, onr) 209 }) 210 } 211 } 212 } 213 } 214 215 // testForEachResourceType runs a subtest for each possible resource type+relation in the schema. 216 func testForEachResourceType( 217 t *testing.T, 218 vctx validationContext, 219 prefix string, 220 handler func(t *testing.T, resourceType *core.RelationReference), 221 ) { 222 for _, resourceType := range vctx.clusterAndData.Populated.NamespaceDefinitions { 223 resourceType := resourceType 224 for _, relation := range resourceType.Relation { 225 relation := relation 226 t.Run(fmt.Sprintf("%s_%s_%s_", prefix, resourceType.Name, relation.Name), 227 func(t *testing.T) { 228 handler(t, &core.RelationReference{ 229 Namespace: resourceType.Name, 230 Relation: relation.Name, 231 }) 232 }) 233 } 234 } 235 } 236 237 // ensureRelationshipWrites ensures that all relationships can be written via the API. 238 func ensureRelationshipWrites(t *testing.T, vctx validationContext) { 239 for _, nsDef := range vctx.clusterAndData.Populated.NamespaceDefinitions { 240 relationships, ok := vctx.accessibilitySet.RelationshipsByResourceNamespace.Get(nsDef.Name) 241 if !ok { 242 continue 243 } 244 245 for _, relationship := range relationships { 246 err := vctx.serviceTester.Write(context.Background(), relationship) 247 require.NoError(t, err, "failed to write %s", tuple.MustString(relationship)) 248 } 249 } 250 } 251 252 // validateRelationshipReads ensures that all defined relationships are returned by the Read API. 253 func validateRelationshipReads(t *testing.T, vctx validationContext) { 254 testForEachRelationship(t, vctx, "read", func(t *testing.T, relationship *core.RelationTuple) { 255 foundRelationships, err := vctx.serviceTester.Read(context.Background(), 256 relationship.ResourceAndRelation.Namespace, 257 vctx.revision, 258 ) 259 require.NoError(t, err) 260 261 foundRelationshipsSet := mapz.NewSet[string]() 262 for _, rel := range foundRelationships { 263 foundRelationshipsSet.Insert(tuple.MustString(rel)) 264 } 265 266 require.True(t, foundRelationshipsSet.Has(tuple.MustString(relationship)), "missing expected relationship %s in read results: %s", tuple.MustString(relationship), foundRelationshipsSet.AsSlice()) 267 }) 268 } 269 270 // ensureNoExpansionErrors runs basic expansion on each relation and ensures no errors are raised. 271 func ensureNoExpansionErrors(t *testing.T, vctx validationContext) { 272 testForEachResource(t, vctx, "run_expand", 273 func(t *testing.T, resource *core.ObjectAndRelation) { 274 _, err := vctx.serviceTester.Expand(context.Background(), 275 resource, 276 vctx.revision, 277 ) 278 require.NoError(t, err) 279 }) 280 } 281 282 // validateExpansionSubjects runs a fully recursive expand on each relation and ensures that all expected terminal subjects are reached. 283 func validateExpansionSubjects(t *testing.T, vctx validationContext) { 284 testForEachResource(t, vctx, "validate_expand", 285 func(t *testing.T, resource *core.ObjectAndRelation) { 286 // Run a *recursive* expansion to collect all the reachable subjects. 287 resp, err := vctx.dispatcher.DispatchExpand( 288 vctx.clusterAndData.Ctx, 289 &dispatchv1.DispatchExpandRequest{ 290 ResourceAndRelation: resource, 291 Metadata: &dispatchv1.ResolverMeta{ 292 AtRevision: vctx.revision.String(), 293 DepthRemaining: 100, 294 TraversalBloom: dispatchv1.MustNewTraversalBloomFilter(100), 295 }, 296 ExpansionMode: dispatchv1.DispatchExpandRequest_RECURSIVE, 297 }) 298 require.NoError(t, err) 299 300 // Build an accessible subject set from the expansion tree. 301 subjectsFoundSet, err := developmentmembership.AccessibleExpansionSubjects(resp.TreeNode) 302 require.NoError(t, err) 303 304 // Ensure all non-wildcard terminal subjects that were found in the expansion are accessible. 305 for _, foundSubject := range subjectsFoundSet.ToSlice() { 306 if foundSubject.GetSubjectId() != tuple.PublicWildcard { 307 accessiblity, permissionship, ok := vctx.accessibilitySet.AccessibiliyAndPermissionshipFor(resource, foundSubject.Subject()) 308 require.True(t, ok, "missing accessibility for resource %s and subject %s", tuple.StringONR(resource), tuple.StringONR(foundSubject.Subject())) 309 310 // NOTE: an expanded subject must either be accessible directly (e.g. not via a wildcard) 311 // OR must be removed due to a static caveat context removing it from the set. 312 require.True(t, 313 accessiblity == consistencytestutil.AccessibleDirectly || 314 accessiblity == consistencytestutil.NotAccessibleDueToPrespecifiedCaveat, 315 "mismatch between expand and accessibility for resource %s and subject %s. accessibility: %v, permissionship: %v", 316 tuple.StringONR(resource), 317 tuple.StringONR(foundSubject.Subject()), 318 accessiblity, 319 permissionship, 320 ) 321 } 322 } 323 324 // Ensure all terminal subjects are found in the expansion. 325 for _, expectedSubject := range vctx.accessibilitySet.DirectlyAccessibleDefinedSubjects(resource) { 326 found := subjectsFoundSet.Contains(expectedSubject) 327 require.True(t, found, "missing expected subject %s in expand for resource %s", tuple.StringONR(expectedSubject), tuple.StringONR(resource)) 328 } 329 }) 330 } 331 332 func requireSameSets(t *testing.T, expected []string, found []string) { 333 expectedSet := mapz.NewSet(expected...) 334 foundSet := mapz.NewSet(found...) 335 336 orderedExpected := expectedSet.AsSlice() 337 orderedFound := foundSet.AsSlice() 338 339 sort.Strings(orderedExpected) 340 sort.Strings(orderedFound) 341 342 require.Equal(t, orderedExpected, orderedFound) 343 } 344 345 func requireSubsetOf(t *testing.T, found []string, expected []string) { 346 if len(expected) == 0 { 347 return 348 } 349 350 foundSet := mapz.NewSet(found...) 351 for _, expectedObjectID := range expected { 352 require.True(t, foundSet.Has(expectedObjectID), "missing expected object ID %s", expectedObjectID) 353 } 354 } 355 356 // validateLookupResources ensures that a lookup resources call returns the expected objects and 357 // only those expected. 358 func validateLookupResources(t *testing.T, vctx validationContext) { 359 testForEachResourceType(t, vctx, "validate_lookup_resources", 360 func(t *testing.T, resourceRelation *core.RelationReference) { 361 for _, subject := range vctx.accessibilitySet.AllSubjectsNoWildcards() { 362 subject := subject 363 t.Run(tuple.StringONR(subject), func(t *testing.T) { 364 for _, pageSize := range []uint32{0, 2} { 365 pageSize := pageSize 366 t.Run(fmt.Sprintf("pagesize-%d", pageSize), func(t *testing.T) { 367 accessibleResources := vctx.accessibilitySet.LookupAccessibleResources(resourceRelation, subject) 368 369 // Perform a lookup call and ensure it returns the at least the same set of object IDs. 370 // Loop until all resources have been found or we've hit max iterations. 371 var currentCursor *v1.Cursor 372 resolvedResources := map[string]*v1.LookupResourcesResponse{} 373 for i := 0; i < 100; i++ { 374 foundResources, lastCursor, err := vctx.serviceTester.LookupResources(context.Background(), resourceRelation, subject, vctx.revision, currentCursor, pageSize) 375 require.NoError(t, err) 376 377 if pageSize > 0 { 378 require.LessOrEqual(t, len(foundResources), int(pageSize)+1) // +1 for the wildcard 379 } 380 381 currentCursor = lastCursor 382 383 for _, resource := range foundResources { 384 resolvedResources[resource.ResourceObjectId] = resource 385 } 386 387 if pageSize == 0 || len(foundResources) < int(pageSize) { 388 break 389 } 390 } 391 392 requireSameSets(t, maps.Keys(accessibleResources), maps.Keys(resolvedResources)) 393 394 // Ensure that every returned concrete object Checks directly. 395 checkBulkItems := make([]*v1.CheckBulkPermissionsRequestItem, 0, len(resolvedResources)) 396 expectedBulkPermissions := map[string]v1.CheckPermissionResponse_Permissionship{} 397 398 for _, resolvedResource := range resolvedResources { 399 permissionship, err := vctx.serviceTester.Check(context.Background(), 400 &core.ObjectAndRelation{ 401 Namespace: resourceRelation.Namespace, 402 Relation: resourceRelation.Relation, 403 ObjectId: resolvedResource.ResourceObjectId, 404 }, 405 subject, 406 vctx.revision, 407 nil, 408 ) 409 410 expectedPermissionship := v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION 411 if resolvedResource.Permissionship == v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_CONDITIONAL_PERMISSION { 412 expectedPermissionship = v1.CheckPermissionResponse_PERMISSIONSHIP_CONDITIONAL_PERMISSION 413 } 414 415 expectedBulkPermissions[resolvedResource.ResourceObjectId] = expectedPermissionship 416 417 require.NoError(t, err) 418 require.Equal(t, 419 expectedPermissionship, 420 permissionship, 421 "Found Check failure for relation %s:%s#%s and subject %s in lookup resources; expected %v, found %v", 422 resourceRelation.Namespace, 423 resolvedResource.ResourceObjectId, 424 resourceRelation.Relation, 425 tuple.StringONR(subject), 426 expectedPermissionship, 427 permissionship, 428 ) 429 430 checkBulkItems = append(checkBulkItems, &v1.CheckBulkPermissionsRequestItem{ 431 Resource: &v1.ObjectReference{ 432 ObjectType: resourceRelation.Namespace, 433 ObjectId: resolvedResource.ResourceObjectId, 434 }, 435 Permission: resourceRelation.Relation, 436 Subject: &v1.SubjectReference{ 437 Object: &v1.ObjectReference{ 438 ObjectType: subject.Namespace, 439 ObjectId: subject.ObjectId, 440 }, 441 OptionalRelation: stringz.Default(subject.Relation, "", tuple.Ellipsis), 442 }, 443 }) 444 } 445 446 // Ensure they are all found via bulk check as well. 447 results, err := vctx.serviceTester.CheckBulk(context.Background(), 448 checkBulkItems, 449 vctx.revision, 450 ) 451 require.NoError(t, err) 452 for _, result := range results { 453 require.Equal(t, expectedBulkPermissions[result.Request.Resource.ObjectId], result.GetItem().Permissionship) 454 } 455 }) 456 } 457 }) 458 } 459 }) 460 } 461 462 // validateLookupSubjects validates that the subjects that can access it are those expected. 463 func validateLookupSubjects(t *testing.T, vctx validationContext) { 464 testForEachResource(t, vctx, "validate_lookup_subjects", 465 func(t *testing.T, resource *core.ObjectAndRelation) { 466 for _, subjectType := range vctx.accessibilitySet.SubjectTypes() { 467 subjectType := subjectType 468 t.Run(fmt.Sprintf("%s#%s", subjectType.Namespace, subjectType.Relation), 469 func(t *testing.T) { 470 for _, pageSize := range []uint32{0, 2} { 471 pageSize := pageSize 472 t.Run(fmt.Sprintf("pagesize-%d", pageSize), func(t *testing.T) { 473 // Loop until all subjects have been found or we've hit max iterations. 474 var currentCursor *v1.Cursor 475 resolvedSubjects := map[string]*v1.LookupSubjectsResponse{} 476 for i := 0; i < 100; i++ { 477 foundSubjects, lastCursor, err := vctx.serviceTester.LookupSubjects(context.Background(), resource, subjectType, vctx.revision, nil, currentCursor, pageSize) 478 require.NoError(t, err) 479 480 if pageSize > 0 { 481 require.LessOrEqual(t, len(foundSubjects), int(pageSize)+1) // +1 for possible wildcard 482 } 483 484 currentCursor = lastCursor 485 486 for _, subject := range foundSubjects { 487 resolvedSubjects[subject.Subject.SubjectObjectId] = subject 488 } 489 490 if pageSize == 0 || len(foundSubjects) < int(pageSize) { 491 break 492 } 493 } 494 495 // Ensure the subjects found include those defined as expected. Since the 496 // accessibility set does not include "inferred" subjects (e.g. those with 497 // permissions as their subject relation, or wildcards), this should be a 498 // subset. 499 expectedDefinedSubjects := vctx.accessibilitySet.DirectlyAccessibleDefinedSubjectsOfType(resource, subjectType) 500 requireSubsetOf(t, maps.Keys(resolvedSubjects), maps.Keys(expectedDefinedSubjects)) 501 502 // Ensure all subjects in true and caveated assertions for the subject type are found 503 // in the LookupSubject result, except those added via wildcard. 504 for _, parsedFile := range vctx.clusterAndData.Populated.ParsedFiles { 505 for _, entry := range []struct { 506 assertions []blocks.Assertion 507 requiresPermission bool 508 }{ 509 { 510 assertions: parsedFile.Assertions.AssertTrue, 511 requiresPermission: true, 512 }, 513 { 514 assertions: parsedFile.Assertions.AssertCaveated, 515 requiresPermission: false, 516 }, 517 } { 518 for _, assertion := range entry.assertions { 519 assertionRel := tuple.MustFromRelationship[*v1.ObjectReference, *v1.SubjectReference, *v1.ContextualizedCaveat](assertion.Relationship) 520 if !assertionRel.ResourceAndRelation.EqualVT(resource) { 521 continue 522 } 523 524 if assertionRel.Subject.Namespace != subjectType.Namespace || 525 assertionRel.Subject.Relation != subjectType.Relation { 526 continue 527 } 528 529 // For subjects found solely via wildcard, check that a wildcard instead exists in 530 // the result and that the subject is not excluded. 531 accessibility, _, ok := vctx.accessibilitySet.AccessibiliyAndPermissionshipFor(resource, assertionRel.Subject) 532 if !ok || accessibility == consistencytestutil.AccessibleViaWildcardOnly { 533 resolvedSubjectsToCheck := resolvedSubjects 534 535 // If the assertion has caveat context, rerun LookupSubjects with the context to ensure the returned subject 536 // matches the context given. 537 if len(assertion.CaveatContext) > 0 { 538 resolvedSubjectsWithContext, _, err := vctx.serviceTester.LookupSubjects(context.Background(), resource, subjectType, vctx.revision, assertion.CaveatContext, nil, 0) 539 require.NoError(t, err) 540 541 resolvedSubjectsToCheck = resolvedSubjectsWithContext 542 } 543 544 resolvedSubject, ok := resolvedSubjectsToCheck[tuple.PublicWildcard] 545 require.True(t, ok, "expected wildcard in lookupsubjects response for assertion `%s`", assertion.RelationshipWithContextString) 546 547 if entry.requiresPermission { 548 require.Equal(t, v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_HAS_PERMISSION, resolvedSubject.Subject.Permissionship) 549 } 550 551 // Ensure that the subject is not excluded. If a caveated assertion, then the exclusion 552 // can be caveated. 553 for _, excludedSubject := range resolvedSubject.ExcludedSubjects { 554 if entry.requiresPermission { 555 require.NotEqual(t, excludedSubject.SubjectObjectId, assertionRel.Subject.ObjectId, "wildcard excludes the asserted subject ID: %s", assertionRel.Subject.ObjectId) 556 } else if excludedSubject.SubjectObjectId == assertionRel.Subject.ObjectId { 557 require.NotEqual(t, v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_HAS_PERMISSION, excludedSubject.Permissionship, "wildcard concretely excludes the asserted subject ID: %s", assertionRel.Subject.ObjectId) 558 } 559 } 560 continue 561 } 562 563 _, ok = resolvedSubjects[assertionRel.Subject.ObjectId] 564 require.True(t, ok, "missing expected subject %s from assertion %s", assertionRel.Subject.ObjectId, assertion.RelationshipWithContextString) 565 } 566 } 567 } 568 569 // Ensure that all excluded subjects from wildcards do not have access. 570 for _, resolvedSubject := range resolvedSubjects { 571 if resolvedSubject.Subject.SubjectObjectId != tuple.PublicWildcard { 572 continue 573 } 574 575 for _, excludedSubject := range resolvedSubject.ExcludedSubjects { 576 permissionship, err := vctx.serviceTester.Check(context.Background(), 577 resource, 578 &core.ObjectAndRelation{ 579 Namespace: subjectType.Namespace, 580 ObjectId: excludedSubject.SubjectObjectId, 581 Relation: subjectType.Relation, 582 }, 583 vctx.revision, 584 nil, 585 ) 586 require.NoError(t, err) 587 588 expectedPermissionship := v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION 589 if resolvedSubject.Subject.Permissionship == v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_CONDITIONAL_PERMISSION { 590 expectedPermissionship = v1.CheckPermissionResponse_PERMISSIONSHIP_CONDITIONAL_PERMISSION 591 } 592 if excludedSubject.Permissionship == v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_CONDITIONAL_PERMISSION { 593 expectedPermissionship = v1.CheckPermissionResponse_PERMISSIONSHIP_CONDITIONAL_PERMISSION 594 } 595 596 require.Equal(t, 597 expectedPermissionship, 598 permissionship, 599 "Found Check failure for resource %s and excluded subject %s in lookup subjects", 600 tuple.StringONR(resource), 601 excludedSubject.SubjectObjectId, 602 ) 603 } 604 } 605 606 // Ensure that every returned defined, non-wildcard subject found checks as expected. 607 for _, resolvedSubject := range resolvedSubjects { 608 if resolvedSubject.Subject.SubjectObjectId == tuple.PublicWildcard { 609 continue 610 } 611 612 subject := &core.ObjectAndRelation{ 613 Namespace: subjectType.Namespace, 614 ObjectId: resolvedSubject.Subject.SubjectObjectId, 615 Relation: subjectType.Relation, 616 } 617 618 permissionship, err := vctx.serviceTester.Check(context.Background(), 619 resource, 620 subject, 621 vctx.revision, 622 nil, 623 ) 624 require.NoError(t, err) 625 626 expectedPermissionship := v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION 627 if resolvedSubject.Subject.Permissionship == v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_CONDITIONAL_PERMISSION { 628 expectedPermissionship = v1.CheckPermissionResponse_PERMISSIONSHIP_CONDITIONAL_PERMISSION 629 } 630 631 require.Equal(t, 632 expectedPermissionship, 633 permissionship, 634 "Found Check failure for resource %s and subject %s in lookup subjects", 635 tuple.StringONR(resource), 636 tuple.StringONR(subject), 637 ) 638 } 639 }) 640 } 641 }) 642 } 643 }) 644 } 645 646 // runAssertions runs all assertions defined in the validation files and ensures they 647 // return the expected results. 648 func runAssertions(t *testing.T, vctx validationContext) { 649 t.Run("assertions", func(t *testing.T) { 650 for _, parsedFile := range vctx.clusterAndData.Populated.ParsedFiles { 651 for _, entry := range []struct { 652 name string 653 assertions []blocks.Assertion 654 expectedPermissionship v1.CheckPermissionResponse_Permissionship 655 }{ 656 { 657 "true", 658 parsedFile.Assertions.AssertTrue, 659 v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 660 }, 661 { 662 "caveated", 663 parsedFile.Assertions.AssertCaveated, 664 v1.CheckPermissionResponse_PERMISSIONSHIP_CONDITIONAL_PERMISSION, 665 }, 666 { 667 "false", 668 parsedFile.Assertions.AssertFalse, 669 v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, 670 }, 671 } { 672 entry := entry 673 t.Run(entry.name, func(t *testing.T) { 674 bulkCheckItems := make([]*v1.BulkCheckPermissionRequestItem, 0, len(entry.assertions)) 675 676 for _, assertion := range entry.assertions { 677 var caveatContext *structpb.Struct 678 if assertion.CaveatContext != nil { 679 built, err := structpb.NewStruct(assertion.CaveatContext) 680 require.NoError(t, err) 681 caveatContext = built 682 } 683 684 bulkCheckItems = append(bulkCheckItems, &v1.BulkCheckPermissionRequestItem{ 685 Resource: assertion.Relationship.Resource, 686 Permission: assertion.Relationship.Relation, 687 Subject: assertion.Relationship.Subject, 688 Context: caveatContext, 689 }) 690 691 // Run each individual assertion. 692 assertion := assertion 693 t.Run(assertion.RelationshipWithContextString, func(t *testing.T) { 694 rel := tuple.MustFromRelationship[*v1.ObjectReference, *v1.SubjectReference, *v1.ContextualizedCaveat](assertion.Relationship) 695 permissionship, err := vctx.serviceTester.Check(context.Background(), rel.ResourceAndRelation, rel.Subject, vctx.revision, assertion.CaveatContext) 696 require.NoError(t, err) 697 require.Equal(t, entry.expectedPermissionship, permissionship, "Assertion `%s` returned %s; expected %s", tuple.MustString(rel), permissionship, entry.expectedPermissionship) 698 699 // Ensure the assertion passes LookupResources. 700 resolvedResources, _, err := vctx.serviceTester.LookupResources(context.Background(), &core.RelationReference{ 701 Namespace: rel.ResourceAndRelation.Namespace, 702 Relation: rel.ResourceAndRelation.Relation, 703 }, rel.Subject, vctx.revision, nil, 0) 704 require.NoError(t, err) 705 706 resolvedResourcesMap := map[string]*v1.LookupResourcesResponse{} 707 for _, resource := range resolvedResources { 708 resolvedResourcesMap[resource.ResourceObjectId] = resource 709 } 710 711 resolvedResourceIds := maps.Keys(resolvedResourcesMap) 712 accessibility, _, _ := vctx.accessibilitySet.AccessibiliyAndPermissionshipFor(rel.ResourceAndRelation, rel.Subject) 713 714 switch permissionship { 715 case v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION: 716 // If the caveat context given is empty, then the lookup result must not exist at all. 717 // Otherwise, it *could* be caveated or not exist, depending on the context given. 718 if len(assertion.CaveatContext) == 0 { 719 require.NotContains(t, resolvedResourceIds, rel.ResourceAndRelation.ObjectId, "Found unexpected object %s in lookup for assertion %s", rel.ResourceAndRelation, rel) 720 } else if accessibility == consistencytestutil.NotAccessible { 721 found, ok := resolvedResourcesMap[rel.ResourceAndRelation.ObjectId] 722 require.True(t, !ok || found.Permissionship != v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_HAS_PERMISSION) // LookupResources can be caveated, since we didn't rerun LookupResources with the context 723 } else if accessibility != consistencytestutil.NotAccessibleDueToPrespecifiedCaveat { 724 require.Equal(t, v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_CONDITIONAL_PERMISSION, resolvedResourcesMap[rel.ResourceAndRelation.ObjectId].Permissionship) 725 } 726 727 case v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION: 728 require.Contains(t, resolvedResourceIds, rel.ResourceAndRelation.ObjectId, "Missing object %s in lookup for assertion %s", rel.ResourceAndRelation, rel) 729 // If the caveat context given is empty, then the lookup result must be fully permissioned. 730 // Otherwise, it *could* be caveated or fully permissioned, depending on the context given. 731 if len(assertion.CaveatContext) == 0 { 732 require.Equal(t, v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_HAS_PERMISSION, resolvedResourcesMap[rel.ResourceAndRelation.ObjectId].Permissionship) 733 } 734 735 case v1.CheckPermissionResponse_PERMISSIONSHIP_CONDITIONAL_PERMISSION: 736 require.Contains(t, resolvedResourceIds, rel.ResourceAndRelation.ObjectId, "Missing object %s in lookup for assertion %s", rel.ResourceAndRelation, rel) 737 require.Equal(t, v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_CONDITIONAL_PERMISSION, resolvedResourcesMap[rel.ResourceAndRelation.ObjectId].Permissionship) 738 739 default: 740 panic("unknown permissionship") 741 } 742 }) 743 744 // Run all assertions under bulk check and ensure they match as well. 745 results, err := vctx.serviceTester.BulkCheck(context.Background(), bulkCheckItems, vctx.revision) 746 require.NoError(t, err) 747 748 for _, result := range results { 749 require.Equal(t, entry.expectedPermissionship, result.GetItem().Permissionship, "Bulk check for assertion request `%s` returned %s; expected %s", result.GetRequest(), result.GetItem().Permissionship, entry.expectedPermissionship) 750 } 751 } 752 }) 753 } 754 } 755 }) 756 } 757 758 // validateDevelopment runs the development package against the validation context and 759 // ensures its output matches that expected. 760 func validateDevelopment(t *testing.T, vctx validationContext) { 761 reqContext := &devinterface.RequestContext{ 762 Schema: vctx.clusterAndData.Populated.Schema, 763 Relationships: vctx.clusterAndData.Populated.Tuples, 764 } 765 766 devContext, _, err := development.NewDevContext(context.Background(), reqContext) 767 require.NoError(t, err) 768 769 // Validate checks. 770 validateDevelopmentChecks(t, devContext, vctx) 771 772 // Validate assertions. 773 validateDevelopmentAssertions(t, devContext, vctx) 774 775 // Validate expected relationships. 776 validateDevelopmentExpectedRels(t, devContext, vctx) 777 } 778 779 // validateDevelopmentChecks validates that the Check operation in the development package 780 // returns the expected permissionship. 781 func validateDevelopmentChecks(t *testing.T, devContext *development.DevContext, vctx validationContext) { 782 testForEachResource(t, vctx, "validate_check_watch", 783 func(t *testing.T, resource *core.ObjectAndRelation) { 784 for _, subject := range vctx.accessibilitySet.AllSubjectsNoWildcards() { 785 subject := subject 786 t.Run(tuple.StringONR(subject), func(t *testing.T) { 787 cr, err := development.RunCheck(devContext, resource, subject, nil) 788 require.NoError(t, err, "Got unexpected error from development check") 789 790 _, permissionship, ok := vctx.accessibilitySet.AccessibiliyAndPermissionshipFor(resource, subject) 791 require.True(t, ok) 792 require.Equal(t, permissionship, cr.Permissionship, 793 "Found unexpected membership difference for %s@%s. Expected %v, Found: %v", 794 tuple.StringONR(resource), 795 tuple.StringONR(subject), 796 permissionship, 797 cr.Permissionship) 798 }) 799 } 800 }) 801 } 802 803 // validateDevelopmentAssertions validates that Assertions in the development package return 804 // the expected results. 805 func validateDevelopmentAssertions(t *testing.T, devContext *development.DevContext, vctx validationContext) { 806 // Build the assertions YAML. 807 var trueAssertions []string 808 var caveatedAssertions []string 809 var falseAssertions []string 810 811 for relString, permissionship := range vctx.accessibilitySet.PermissionshipByRelationship { 812 switch permissionship { 813 case dispatchv1.ResourceCheckResult_MEMBER: 814 trueAssertions = append(trueAssertions, relString) 815 case dispatchv1.ResourceCheckResult_CAVEATED_MEMBER: 816 caveatedAssertions = append(caveatedAssertions, relString) 817 case dispatchv1.ResourceCheckResult_NOT_MEMBER: 818 falseAssertions = append(falseAssertions, relString) 819 default: 820 require.Fail(t, "unknown permissionship") 821 } 822 } 823 824 assertionsMap := map[string]interface{}{ 825 "assertTrue": trueAssertions, 826 "assertCaveated": caveatedAssertions, 827 "assertFalse": falseAssertions, 828 } 829 assertions, err := yamlv2.Marshal(assertionsMap) 830 require.NoError(t, err, "Could not marshal assertions map") 831 832 // Run validation with the assertions and the updated YAML. 833 parsedAssertions, devErr := development.ParseAssertionsYAML(string(assertions)) 834 require.NoError(t, err, "Got unexpected error from assertions") 835 require.Nil(t, devErr, "Got unexpected request error from assertions: %v", devErr) 836 837 devErrs, err := development.RunAllAssertions(devContext, parsedAssertions) 838 require.NoError(t, err, "Got unexpected error from assertions") 839 require.Equal(t, 0, len(devErrs), "Got unexpected errors from validation: %v", devErrs) 840 } 841 842 // validateDevelopmentExpectedRels validates that the generated expected relationships matches 843 // that expected. 844 func validateDevelopmentExpectedRels(t *testing.T, devContext *development.DevContext, vctx validationContext) { 845 // Build the Expected Relations (inputs only). 846 expectedMap := map[string]interface{}{} 847 for relString, permissionship := range vctx.accessibilitySet.PermissionshipByRelationship { 848 if permissionship == dispatchv1.ResourceCheckResult_NOT_MEMBER { 849 continue 850 } 851 852 relationship := tuple.MustParse(relString) 853 expectedMap[tuple.StringONR(relationship.ResourceAndRelation)] = []string{} 854 } 855 856 expectedRelations, err := yamlv2.Marshal(expectedMap) 857 require.NoError(t, err, "Could not marshal expected relations map") 858 859 expectedRelationsMap, devErr := development.ParseExpectedRelationsYAML(string(expectedRelations)) 860 require.Nil(t, devErr) 861 862 // NOTE: We are using this to generate, so we ignore any errors. 863 membershipSet, _, err := development.RunValidation(devContext, expectedRelationsMap) 864 require.NoError(t, err, "Got unexpected error from validation") 865 866 // Parse the full validation YAML, and ensure every referenced subject is, in fact, allowed. 867 updatedValidationYaml, gerr := development.GenerateValidation(membershipSet) 868 require.NoError(t, gerr) 869 870 validationMap, err := validationfile.ParseExpectedRelationsBlock([]byte(updatedValidationYaml)) 871 require.NoError(t, err) 872 873 for resourceKey, expectedSubjects := range validationMap.ValidationMap { 874 for _, expectedSubject := range expectedSubjects { 875 resourceAndRelation := resourceKey.ObjectAndRelation 876 subjectWithExceptions := expectedSubject.SubjectWithExceptions 877 require.NotNil(t, subjectWithExceptions, "Found expected relation without subject: %s", expectedSubject.ValidationString) 878 879 // For non-wildcard subjects, ensure they are accessible. 880 if subjectWithExceptions.Subject.Subject.ObjectId != tuple.PublicWildcard { 881 accessibility, permissionship, ok := vctx.accessibilitySet.AccessibiliyAndPermissionshipFor(resourceAndRelation, subjectWithExceptions.Subject.Subject) 882 require.True(t, ok, "missing expected subject %s in accessibility set", tuple.StringONR(subjectWithExceptions.Subject.Subject)) 883 884 switch permissionship { 885 case dispatchv1.ResourceCheckResult_MEMBER: 886 // May be caveated or uncaveated, so check the uncomputed permissionship. 887 uncomputed, ok := vctx.accessibilitySet.UncomputedPermissionshipFor(resourceAndRelation, subjectWithExceptions.Subject.Subject) 888 require.True(t, ok, "missing expected subject in accessibility set") 889 require.True(t, subjectWithExceptions.Subject.IsCaveated == (uncomputed == dispatchv1.ResourceCheckResult_CAVEATED_MEMBER), "found mismatch in uncomputed permissionship") 890 891 case dispatchv1.ResourceCheckResult_CAVEATED_MEMBER: 892 require.True(t, subjectWithExceptions.Subject.IsCaveated, "found uncaveated expected subject for caveated subject") 893 894 case dispatchv1.ResourceCheckResult_NOT_MEMBER: 895 // May be caveated or uncaveated, so check the accessibility. 896 if accessibility == consistencytestutil.NotAccessibleDueToPrespecifiedCaveat { 897 require.True(t, subjectWithExceptions.Subject.IsCaveated, "found uncaveated expected subject for caveated subject") 898 } else { 899 require.Failf(t, "found unexpected subject", "%s", tuple.StringONR(subjectWithExceptions.Subject.Subject)) 900 } 901 902 default: 903 require.Fail(t, "expected valid permissionship") 904 } 905 } 906 } 907 } 908 }