github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/services/integrationtesting/dispatch_test.go (about)

     1  //go:build ci && docker && !skipintegrationtests
     2  // +build ci,docker,!skipintegrationtests
     3  
     4  package integrationtesting_test
     5  
     6  import (
     7  	"context"
     8  	"slices"
     9  	"testing"
    10  	"time"
    11  
    12  	v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
    13  	"github.com/stretchr/testify/require"
    14  
    15  	"github.com/authzed/spicedb/internal/datastore/spanner"
    16  	"github.com/authzed/spicedb/internal/testserver"
    17  	testdatastore "github.com/authzed/spicedb/internal/testserver/datastore"
    18  	"github.com/authzed/spicedb/internal/testserver/datastore/config"
    19  	dsconfig "github.com/authzed/spicedb/pkg/cmd/datastore"
    20  	"github.com/authzed/spicedb/pkg/datastore"
    21  	"github.com/authzed/spicedb/pkg/tuple"
    22  )
    23  
    24  type testCase struct {
    25  	name   string
    26  	schema string
    27  	runOp  func(t *testing.T, client v1.PermissionsServiceClient)
    28  }
    29  
    30  func TestDispatchIntegration(t *testing.T) {
    31  	blacklist := []string{
    32  		spanner.Engine, // spanner emulator doesn't support parallel transactions
    33  	}
    34  
    35  	testCases := []testCase{
    36  		{
    37  			"basic dispatched permissions checks",
    38  			`definition user {}
    39  			
    40  			definition resource {
    41  				relation parent: resource
    42  				relation viewer: user
    43  				permission view = viewer + parent->view
    44  			}`,
    45  			func(t *testing.T, client v1.PermissionsServiceClient) {
    46  				resp, err := client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{
    47  					Updates: []*v1.RelationshipUpdate{
    48  						{
    49  							Operation:    v1.RelationshipUpdate_OPERATION_CREATE,
    50  							Relationship: tuple.MustToRelationship(tuple.MustParse("resource:foo#viewer@user:tom")),
    51  						},
    52  						{
    53  							Operation:    v1.RelationshipUpdate_OPERATION_CREATE,
    54  							Relationship: tuple.MustToRelationship(tuple.MustParse("resource:foo#parent@resource:bar")),
    55  						},
    56  						{
    57  							Operation:    v1.RelationshipUpdate_OPERATION_CREATE,
    58  							Relationship: tuple.MustToRelationship(tuple.MustParse("resource:bar#viewer@user:jill")),
    59  						},
    60  					},
    61  				})
    62  				require.NoError(t, err)
    63  
    64  				cresp, err := client.CheckPermission(context.Background(), &v1.CheckPermissionRequest{
    65  					Consistency: &v1.Consistency{
    66  						Requirement: &v1.Consistency_AtLeastAsFresh{
    67  							AtLeastAsFresh: resp.WrittenAt,
    68  						},
    69  					},
    70  					Resource: &v1.ObjectReference{
    71  						ObjectType: "resource",
    72  						ObjectId:   "foo",
    73  					},
    74  					Permission: "view",
    75  					Subject: &v1.SubjectReference{
    76  						Object: &v1.ObjectReference{
    77  							ObjectType: "user",
    78  							ObjectId:   "tom",
    79  						},
    80  					},
    81  				})
    82  				require.NoError(t, err)
    83  				require.Equal(t, v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, cresp.Permissionship)
    84  
    85  				cresp2, err := client.CheckPermission(context.Background(), &v1.CheckPermissionRequest{
    86  					Consistency: &v1.Consistency{
    87  						Requirement: &v1.Consistency_AtLeastAsFresh{
    88  							AtLeastAsFresh: resp.WrittenAt,
    89  						},
    90  					},
    91  					Resource: &v1.ObjectReference{
    92  						ObjectType: "resource",
    93  						ObjectId:   "foo",
    94  					},
    95  					Permission: "view",
    96  					Subject: &v1.SubjectReference{
    97  						Object: &v1.ObjectReference{
    98  							ObjectType: "user",
    99  							ObjectId:   "jill",
   100  						},
   101  					},
   102  				})
   103  				require.NoError(t, err)
   104  				require.Equal(t, v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, cresp2.Permissionship)
   105  			},
   106  		},
   107  		{
   108  			"unknown parent relation test",
   109  			`definition user {}
   110  			
   111  			definition someothertype {}
   112  
   113  			definition resource {
   114  				relation parent: someothertype
   115  				relation viewer: user
   116  				permission view = viewer + parent->unknown
   117  			}`,
   118  			func(t *testing.T, client v1.PermissionsServiceClient) {
   119  				resp, err := client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{
   120  					Updates: []*v1.RelationshipUpdate{
   121  						{
   122  							Operation:    v1.RelationshipUpdate_OPERATION_CREATE,
   123  							Relationship: tuple.MustToRelationship(tuple.MustParse("resource:foo#parent@someothertype:bar")),
   124  						},
   125  					},
   126  				})
   127  				require.NoError(t, err)
   128  
   129  				cresp, err := client.CheckPermission(context.Background(), &v1.CheckPermissionRequest{
   130  					Consistency: &v1.Consistency{
   131  						Requirement: &v1.Consistency_AtLeastAsFresh{
   132  							AtLeastAsFresh: resp.WrittenAt,
   133  						},
   134  					},
   135  					Resource: &v1.ObjectReference{
   136  						ObjectType: "resource",
   137  						ObjectId:   "foo",
   138  					},
   139  					Permission: "view",
   140  					Subject: &v1.SubjectReference{
   141  						Object: &v1.ObjectReference{
   142  							ObjectType: "user",
   143  							ObjectId:   "tom",
   144  						},
   145  					},
   146  				})
   147  				require.NoError(t, err)
   148  				require.Equal(t, v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, cresp.Permissionship)
   149  			},
   150  		},
   151  		{
   152  			"unknown relation test",
   153  			`definition user {}
   154  
   155  			definition resource {
   156  				relation viewer: user
   157  				permission view = viewer
   158  			}`,
   159  			func(t *testing.T, client v1.PermissionsServiceClient) {
   160  				resp, err := client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{
   161  					Updates: []*v1.RelationshipUpdate{
   162  						{
   163  							Operation:    v1.RelationshipUpdate_OPERATION_CREATE,
   164  							Relationship: tuple.MustToRelationship(tuple.MustParse("resource:foo#viewer@user:someuser")),
   165  						},
   166  					},
   167  				})
   168  				require.NoError(t, err)
   169  
   170  				_, cerr := client.CheckPermission(context.Background(), &v1.CheckPermissionRequest{
   171  					Consistency: &v1.Consistency{
   172  						Requirement: &v1.Consistency_AtLeastAsFresh{
   173  							AtLeastAsFresh: resp.WrittenAt,
   174  						},
   175  					},
   176  					Resource: &v1.ObjectReference{
   177  						ObjectType: "resource",
   178  						ObjectId:   "foo",
   179  					},
   180  					Permission: "unknown",
   181  					Subject: &v1.SubjectReference{
   182  						Object: &v1.ObjectReference{
   183  							ObjectType: "user",
   184  							ObjectId:   "tom",
   185  						},
   186  					},
   187  				})
   188  				require.Error(t, cerr)
   189  			},
   190  		},
   191  		{
   192  			"delete preconditions test",
   193  			`definition user {}
   194  
   195  			definition resource {
   196  				relation viewer: user
   197  				permission view = viewer
   198  			}`,
   199  			func(t *testing.T, client v1.PermissionsServiceClient) {
   200  				// Ensure the delete fails on the precondition.
   201  				_, derr := client.DeleteRelationships(context.Background(), &v1.DeleteRelationshipsRequest{
   202  					OptionalPreconditions: []*v1.Precondition{
   203  						{
   204  							Operation: v1.Precondition_OPERATION_MUST_MATCH,
   205  							Filter: &v1.RelationshipFilter{
   206  								ResourceType:       "resource",
   207  								OptionalResourceId: "someresource",
   208  								OptionalRelation:   "viewer",
   209  								OptionalSubjectFilter: &v1.SubjectFilter{
   210  									SubjectType:       "user",
   211  									OptionalSubjectId: "sarah",
   212  								},
   213  							},
   214  						},
   215  					},
   216  					RelationshipFilter: &v1.RelationshipFilter{
   217  						ResourceType: "resource",
   218  					},
   219  				})
   220  				require.Error(t, derr)
   221  				require.Contains(t, derr.Error(), "unable to satisfy write precondition")
   222  
   223  				// Write a relationship, but not the one we want.
   224  				_, werr := client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{
   225  					Updates: []*v1.RelationshipUpdate{
   226  						{
   227  							Operation:    v1.RelationshipUpdate_OPERATION_CREATE,
   228  							Relationship: tuple.MustToRelationship(tuple.MustParse("resource:someresource#viewer@user:someuser")),
   229  						},
   230  					},
   231  				})
   232  				require.NoError(t, werr)
   233  
   234  				// Ensure the delete still fails on the precondition.
   235  				_, derr = client.DeleteRelationships(context.Background(), &v1.DeleteRelationshipsRequest{
   236  					OptionalPreconditions: []*v1.Precondition{
   237  						{
   238  							Operation: v1.Precondition_OPERATION_MUST_MATCH,
   239  							Filter: &v1.RelationshipFilter{
   240  								ResourceType:       "resource",
   241  								OptionalResourceId: "someresource",
   242  								OptionalRelation:   "viewer",
   243  								OptionalSubjectFilter: &v1.SubjectFilter{
   244  									SubjectType:       "user",
   245  									OptionalSubjectId: "sarah",
   246  								},
   247  							},
   248  						},
   249  					},
   250  					RelationshipFilter: &v1.RelationshipFilter{
   251  						ResourceType: "resource",
   252  					},
   253  				})
   254  				require.Error(t, derr)
   255  				require.Contains(t, derr.Error(), "unable to satisfy write precondition")
   256  
   257  				// Write the relationship needed.
   258  				_, werr = client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{
   259  					Updates: []*v1.RelationshipUpdate{
   260  						{
   261  							Operation:    v1.RelationshipUpdate_OPERATION_CREATE,
   262  							Relationship: tuple.MustToRelationship(tuple.MustParse("resource:someresource#viewer@user:sarah")),
   263  						},
   264  					},
   265  				})
   266  				require.NoError(t, werr)
   267  
   268  				// Ensure a delete with an inverse precondition now fails.
   269  				_, derr = client.DeleteRelationships(context.Background(), &v1.DeleteRelationshipsRequest{
   270  					OptionalPreconditions: []*v1.Precondition{
   271  						{
   272  							Operation: v1.Precondition_OPERATION_MUST_NOT_MATCH,
   273  							Filter: &v1.RelationshipFilter{
   274  								ResourceType:       "resource",
   275  								OptionalResourceId: "someresource",
   276  								OptionalRelation:   "viewer",
   277  								OptionalSubjectFilter: &v1.SubjectFilter{
   278  									SubjectType:       "user",
   279  									OptionalSubjectId: "sarah",
   280  								},
   281  							},
   282  						},
   283  					},
   284  					RelationshipFilter: &v1.RelationshipFilter{
   285  						ResourceType: "resource",
   286  					},
   287  				})
   288  				require.Error(t, derr)
   289  				require.Contains(t, derr.Error(), "unable to satisfy write precondition")
   290  
   291  				// Ensure the delete with MUST_MATCH now works.
   292  				resp, derr := client.DeleteRelationships(context.Background(), &v1.DeleteRelationshipsRequest{
   293  					OptionalPreconditions: []*v1.Precondition{
   294  						{
   295  							Operation: v1.Precondition_OPERATION_MUST_MATCH,
   296  							Filter: &v1.RelationshipFilter{
   297  								ResourceType:       "resource",
   298  								OptionalResourceId: "someresource",
   299  								OptionalRelation:   "viewer",
   300  								OptionalSubjectFilter: &v1.SubjectFilter{
   301  									SubjectType:       "user",
   302  									OptionalSubjectId: "sarah",
   303  								},
   304  							},
   305  						},
   306  					},
   307  					RelationshipFilter: &v1.RelationshipFilter{
   308  						ResourceType: "resource",
   309  					},
   310  				})
   311  				require.NoError(t, derr)
   312  				require.NotNil(t, resp.DeletedAt)
   313  			},
   314  		},
   315  	}
   316  
   317  	for _, engine := range datastore.Engines {
   318  		if slices.Contains(blacklist, engine) {
   319  			continue
   320  		}
   321  		b := testdatastore.RunDatastoreEngine(t, engine)
   322  		t.Run(engine, func(t *testing.T) {
   323  			for _, tc := range testCases {
   324  				t.Run(tc.name, func(t *testing.T) {
   325  					ds := b.NewDatastore(t, config.DatastoreConfigInitFunc(t,
   326  						dsconfig.WithWatchBufferLength(0),
   327  						dsconfig.WithGCWindow(time.Duration(90_000_000_000_000)),
   328  						dsconfig.WithRevisionQuantization(10),
   329  						dsconfig.WithMaxRetries(50),
   330  						dsconfig.WithRequestHedgingEnabled(false)))
   331  
   332  					conns, cleanup := testserver.TestClusterWithDispatch(t, 1, ds)
   333  					t.Cleanup(cleanup)
   334  
   335  					schemaClient := v1.NewSchemaServiceClient(conns[0])
   336  					_, err := schemaClient.WriteSchema(context.Background(), &v1.WriteSchemaRequest{
   337  						Schema: tc.schema,
   338  					})
   339  					require.NoError(t, err)
   340  
   341  					client := v1.NewPermissionsServiceClient(conns[0])
   342  					tc.runOp(t, client)
   343  				})
   344  			}
   345  		})
   346  	}
   347  }