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  }