github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/pkg/datastore/test/caveat.go (about) 1 package test 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "testing" 8 "time" 9 10 "github.com/authzed/spicedb/internal/datastore/common" 11 "github.com/authzed/spicedb/internal/testfixtures" 12 "github.com/authzed/spicedb/pkg/caveats" 13 caveattypes "github.com/authzed/spicedb/pkg/caveats/types" 14 "github.com/authzed/spicedb/pkg/datastore" 15 core "github.com/authzed/spicedb/pkg/proto/core/v1" 16 "github.com/authzed/spicedb/pkg/tuple" 17 18 "github.com/google/go-cmp/cmp" 19 "github.com/google/uuid" 20 "github.com/stretchr/testify/require" 21 "google.golang.org/protobuf/testing/protocmp" 22 "google.golang.org/protobuf/types/known/structpb" 23 ) 24 25 // CaveatNotFound tests to ensure that an unknown caveat returns the expected 26 // error. 27 func CaveatNotFoundTest(t *testing.T, tester DatastoreTester) { 28 require := require.New(t) 29 30 ds, err := tester.New(0, veryLargeGCInterval, veryLargeGCWindow, 1) 31 require.NoError(err) 32 33 ctx := context.Background() 34 35 startRevision, err := ds.HeadRevision(ctx) 36 require.NoError(err) 37 38 _, _, err = ds.SnapshotReader(startRevision).ReadCaveatByName(ctx, "unknown") 39 require.True(errors.As(err, &datastore.ErrCaveatNameNotFound{})) 40 } 41 42 func WriteReadDeleteCaveatTest(t *testing.T, tester DatastoreTester) { 43 req := require.New(t) 44 ds, err := tester.New(0*time.Second, veryLargeGCInterval, veryLargeGCWindow, 1) 45 req.NoError(err) 46 47 skipIfNotCaveatStorer(t, ds) 48 49 ctx := context.Background() 50 // Don't fail on writing empty caveat list 51 _, err = writeCaveats(ctx, ds) 52 req.NoError(err) 53 54 // Dupes in same transaction fail to be written 55 coreCaveat := createCoreCaveat(t) 56 coreCaveat.Name = "a" 57 _, err = writeCaveats(ctx, ds, coreCaveat, coreCaveat) 58 req.Error(err) 59 60 // Succeeds writing various caveats 61 anotherCoreCaveat := createCoreCaveat(t) 62 anotherCoreCaveat.Name = "b" 63 rev, err := writeCaveats(ctx, ds, coreCaveat, anotherCoreCaveat) 64 req.NoError(err) 65 66 // The caveat can be looked up by name 67 cr := ds.SnapshotReader(rev) 68 cv, _, err := cr.ReadCaveatByName(ctx, coreCaveat.Name) 69 req.NoError(err) 70 71 foundDiff := cmp.Diff(coreCaveat, cv, protocmp.Transform()) 72 req.Empty(foundDiff) 73 74 // All caveats can be listed when no arg is provided 75 // Manually check the caveat's contents. 76 req.Equal(coreCaveat.Name, cv.Name) 77 req.Equal(2, len(cv.ParameterTypes)) 78 req.Equal("int", cv.ParameterTypes["foo"].TypeName) 79 req.Equal("map", cv.ParameterTypes["bar"].TypeName) 80 req.Equal("bytes", cv.ParameterTypes["bar"].ChildTypes[0].TypeName) 81 82 // All caveats can be listed 83 cvs, err := cr.ListAllCaveats(ctx) 84 req.NoError(err) 85 req.Len(cvs, 2) 86 87 foundDiff = cmp.Diff(coreCaveat, cvs[0].Definition, protocmp.Transform()) 88 req.Empty(foundDiff) 89 foundDiff = cmp.Diff(anotherCoreCaveat, cvs[1].Definition, protocmp.Transform()) 90 req.Empty(foundDiff) 91 92 // Caveats can be found by names 93 cvs, err = cr.LookupCaveatsWithNames(ctx, []string{coreCaveat.Name}) 94 req.NoError(err) 95 req.Len(cvs, 1) 96 97 foundDiff = cmp.Diff(coreCaveat, cvs[0].Definition, protocmp.Transform()) 98 req.Empty(foundDiff) 99 100 // Non-existing names returns no caveat 101 cvs, err = cr.LookupCaveatsWithNames(ctx, []string{"doesnotexist"}) 102 req.NoError(err) 103 req.Empty(cvs) 104 105 // Empty lookup returns no values. 106 cvs, err = cr.LookupCaveatsWithNames(ctx, []string{}) 107 req.NoError(err) 108 req.Len(cvs, 0) 109 110 // nil lookup returns no values. 111 cvs, err = cr.LookupCaveatsWithNames(ctx, nil) 112 req.NoError(err) 113 req.Len(cvs, 0) 114 115 // Delete Caveat 116 rev, err = ds.ReadWriteTx(ctx, func(ctx context.Context, tx datastore.ReadWriteTransaction) error { 117 return tx.DeleteCaveats(ctx, []string{coreCaveat.Name}) 118 }) 119 req.NoError(err) 120 cr = ds.SnapshotReader(rev) 121 _, _, err = cr.ReadCaveatByName(ctx, coreCaveat.Name) 122 req.ErrorAs(err, &datastore.ErrCaveatNameNotFound{}) 123 124 // Returns an error if caveat name or ID does not exist 125 _, _, err = cr.ReadCaveatByName(ctx, "doesnotexist") 126 req.ErrorAs(err, &datastore.ErrCaveatNameNotFound{}) 127 } 128 129 func WriteCaveatedRelationshipTest(t *testing.T, tester DatastoreTester) { 130 req := require.New(t) 131 ds, err := tester.New(0*time.Second, veryLargeGCInterval, veryLargeGCWindow, 1) 132 req.NoError(err) 133 134 skipIfNotCaveatStorer(t, ds) 135 136 req.NoError(err) 137 sds, _ := testfixtures.StandardDatastoreWithSchema(ds, req) 138 139 // Store caveat, write caveated tuple and read back same value 140 coreCaveat := createCoreCaveat(t) 141 anotherCoreCaveat := createCoreCaveat(t) 142 ctx := context.Background() 143 _, err = writeCaveats(ctx, ds, coreCaveat, anotherCoreCaveat) 144 req.NoError(err) 145 146 tpl := createTestCaveatedTuple(t, "document:companyplan#somerelation@folder:company#...", coreCaveat.Name) 147 rev, err := common.WriteTuples(ctx, sds, core.RelationTupleUpdate_CREATE, tpl) 148 req.NoError(err) 149 assertTupleCorrectlyStored(req, ds, rev, tpl) 150 151 // RelationTupleUpdate_CREATE of the same tuple and different caveat context will fail 152 _, err = common.WriteTuples(ctx, sds, core.RelationTupleUpdate_CREATE, tpl) 153 req.ErrorAs(err, &common.CreateRelationshipExistsError{}) 154 155 // RelationTupleUpdate_TOUCH does update the caveat context for a caveated relationship that already exists 156 currentMap := tpl.Caveat.Context.AsMap() 157 delete(currentMap, "b") 158 st, err := structpb.NewStruct(currentMap) 159 require.NoError(t, err) 160 161 tpl.Caveat.Context = st 162 rev, err = common.WriteTuples(ctx, sds, core.RelationTupleUpdate_TOUCH, tpl) 163 req.NoError(err) 164 assertTupleCorrectlyStored(req, ds, rev, tpl) 165 166 // RelationTupleUpdate_TOUCH does update the caveat name for a caveated relationship that already exists 167 tpl.Caveat.CaveatName = anotherCoreCaveat.Name 168 rev, err = common.WriteTuples(ctx, sds, core.RelationTupleUpdate_TOUCH, tpl) 169 req.NoError(err) 170 assertTupleCorrectlyStored(req, ds, rev, tpl) 171 172 // TOUCH can remove caveat from relationship 173 caveatContext := tpl.Caveat 174 tpl.Caveat = nil 175 rev, err = common.WriteTuples(ctx, sds, core.RelationTupleUpdate_TOUCH, tpl) 176 req.NoError(err) 177 assertTupleCorrectlyStored(req, ds, rev, tpl) 178 179 // TOUCH can store caveat in relationship with no caveat 180 tpl.Caveat = caveatContext 181 rev, err = common.WriteTuples(ctx, sds, core.RelationTupleUpdate_TOUCH, tpl) 182 req.NoError(err) 183 assertTupleCorrectlyStored(req, ds, rev, tpl) 184 185 // RelationTupleUpdate_DELETE ignores caveat part of the request 186 tpl.Caveat.CaveatName = "rando" 187 rev, err = common.WriteTuples(ctx, sds, core.RelationTupleUpdate_DELETE, tpl) 188 req.NoError(err) 189 iter, err := ds.SnapshotReader(rev).QueryRelationships(context.Background(), datastore.RelationshipsFilter{ 190 OptionalResourceType: tpl.ResourceAndRelation.Namespace, 191 }) 192 req.NoError(err) 193 defer iter.Close() 194 req.Nil(iter.Next()) 195 196 // Caveated tuple can reference non-existing caveat - controller layer is responsible for validation 197 tpl = createTestCaveatedTuple(t, "document:rando#somerelation@folder:company#...", "rando") 198 _, err = common.WriteTuples(ctx, sds, core.RelationTupleUpdate_CREATE, tpl) 199 req.NoError(err) 200 } 201 202 func CaveatedRelationshipFilterTest(t *testing.T, tester DatastoreTester) { 203 req := require.New(t) 204 ds, err := tester.New(0*time.Second, veryLargeGCInterval, veryLargeGCWindow, 1) 205 req.NoError(err) 206 207 skipIfNotCaveatStorer(t, ds) 208 209 req.NoError(err) 210 sds, _ := testfixtures.StandardDatastoreWithSchema(ds, req) 211 212 // Store caveat, write caveated tuple and read back same value 213 coreCaveat := createCoreCaveat(t) 214 anotherCoreCaveat := createCoreCaveat(t) 215 ctx := context.Background() 216 _, err = writeCaveats(ctx, ds, coreCaveat, anotherCoreCaveat) 217 req.NoError(err) 218 219 tpl := createTestCaveatedTuple(t, "document:companyplan#parent@folder:company#...", coreCaveat.Name) 220 anotherTpl := createTestCaveatedTuple(t, "document:anothercompanyplan#parent@folder:company#...", anotherCoreCaveat.Name) 221 nonCaveatedTpl := tuple.MustParse("document:yetanothercompanyplan#parent@folder:company#...") 222 rev, err := common.WriteTuples(ctx, sds, core.RelationTupleUpdate_CREATE, tpl, anotherTpl, nonCaveatedTpl) 223 req.NoError(err) 224 225 // filter by first caveat 226 iter, err := ds.SnapshotReader(rev).QueryRelationships(ctx, datastore.RelationshipsFilter{ 227 OptionalResourceType: tpl.ResourceAndRelation.Namespace, 228 OptionalCaveatName: coreCaveat.Name, 229 }) 230 req.NoError(err) 231 232 expectTuple(req, iter, tpl) 233 234 // filter by second caveat 235 iter, err = ds.SnapshotReader(rev).QueryRelationships(ctx, datastore.RelationshipsFilter{ 236 OptionalResourceType: anotherTpl.ResourceAndRelation.Namespace, 237 OptionalCaveatName: anotherCoreCaveat.Name, 238 }) 239 req.NoError(err) 240 241 expectTuple(req, iter, anotherTpl) 242 } 243 244 func CaveatSnapshotReadsTest(t *testing.T, tester DatastoreTester) { 245 req := require.New(t) 246 ds, err := tester.New(0*time.Second, veryLargeGCInterval, veryLargeGCWindow, 1) 247 req.NoError(err) 248 249 skipIfNotCaveatStorer(t, ds) 250 251 // Write an initial caveat 252 coreCaveat := createCoreCaveat(t) 253 ctx := context.Background() 254 oldRev, err := writeCaveat(ctx, ds, coreCaveat) 255 req.NoError(err) 256 257 // Modify caveat and update 258 oldExpression := coreCaveat.SerializedExpression 259 newExpression := []byte{0x0a} 260 coreCaveat.SerializedExpression = newExpression 261 newRev, err := writeCaveat(ctx, ds, coreCaveat) 262 req.NoError(err) 263 264 // check most recent revision 265 cr := ds.SnapshotReader(newRev) 266 cv, _, err := cr.ReadCaveatByName(ctx, coreCaveat.Name) 267 req.NoError(err) 268 req.Equal(newExpression, cv.SerializedExpression) 269 270 // check previous revision 271 cr = ds.SnapshotReader(oldRev) 272 cv, _, err = cr.ReadCaveatByName(ctx, coreCaveat.Name) 273 req.NoError(err) 274 req.Equal(oldExpression, cv.SerializedExpression) 275 } 276 277 func CaveatedRelationshipWatchTest(t *testing.T, tester DatastoreTester) { 278 req := require.New(t) 279 ds, err := tester.New(0*time.Second, veryLargeGCInterval, veryLargeGCWindow, 16) 280 req.NoError(err) 281 282 skipIfNotCaveatStorer(t, ds) 283 ctx, cancel := context.WithCancel(context.Background()) 284 defer cancel() 285 286 // Write caveat and caveated relationship 287 // TODO bug(postgres): Watch API won't send updates if revision used is the first revision, so write something first 288 coreCaveat := createCoreCaveat(t) 289 _, err = writeCaveat(ctx, ds, coreCaveat) 290 req.NoError(err) 291 292 // test relationship with caveat and context 293 tupleWithContext := createTestCaveatedTuple(t, "document:a#parent@folder:company#...", coreCaveat.Name) 294 295 revBeforeWrite, err := ds.HeadRevision(ctx) 296 require.NoError(t, err) 297 298 writeRev, err := common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, tupleWithContext) 299 require.NoError(t, err) 300 require.NotEqual(t, revBeforeWrite, writeRev, "found same transaction IDs: %v and %v", revBeforeWrite, writeRev) 301 302 expectTupleChange(t, ds, revBeforeWrite, tupleWithContext) 303 304 // test relationship with caveat and empty context 305 tupleWithEmptyContext := createTestCaveatedTuple(t, "document:b#parent@folder:company#...", coreCaveat.Name) 306 strct, err := structpb.NewStruct(nil) 307 308 req.NoError(err) 309 tupleWithEmptyContext.Caveat.Context = strct 310 311 secondRevBeforeWrite, err := ds.HeadRevision(ctx) 312 require.NoError(t, err) 313 314 secondWriteRev, err := common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, tupleWithEmptyContext) 315 require.NoError(t, err) 316 require.NotEqual(t, secondRevBeforeWrite, secondWriteRev) 317 318 expectTupleChange(t, ds, secondRevBeforeWrite, tupleWithEmptyContext) 319 320 // test relationship with caveat and empty context 321 tupleWithNilContext := createTestCaveatedTuple(t, "document:c#parent@folder:company#...", coreCaveat.Name) 322 tupleWithNilContext.Caveat.Context = nil 323 324 thirdRevBeforeWrite, err := ds.HeadRevision(ctx) 325 require.NoError(t, err) 326 327 thirdWriteRev, err := common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, tupleWithNilContext) 328 req.NoError(err) 329 require.NotEqual(t, thirdRevBeforeWrite, thirdWriteRev) 330 331 tupleWithNilContext.Caveat.Context = &structpb.Struct{} // nil struct comes back as zero-value struct 332 expectTupleChange(t, ds, thirdRevBeforeWrite, tupleWithNilContext) 333 } 334 335 func expectTupleChange(t *testing.T, ds datastore.Datastore, revBeforeWrite datastore.Revision, expectedTuple *core.RelationTuple) { 336 t.Helper() 337 338 ctx, cancel := context.WithCancel(context.Background()) 339 defer cancel() 340 341 chanRevisionChanges, chanErr := ds.Watch(ctx, revBeforeWrite, datastore.WatchJustRelationships()) 342 require.Zero(t, len(chanErr)) 343 344 changeWait := time.NewTimer(waitForChangesTimeout) 345 select { 346 case change, ok := <-chanRevisionChanges: 347 require.True(t, ok) 348 349 // do not check length of change, may contain duplicates 350 foundDiff := cmp.Diff(expectedTuple, change.RelationshipChanges[0].Tuple, protocmp.Transform()) 351 require.Empty(t, foundDiff) 352 case <-changeWait.C: 353 require.Fail(t, "timed out waiting for relationship update via Watch API") 354 } 355 } 356 357 func expectTuple(req *require.Assertions, iter datastore.RelationshipIterator, tpl *core.RelationTuple) { 358 defer iter.Close() 359 readTpl := iter.Next() 360 foundDiff := cmp.Diff(tpl, readTpl, protocmp.Transform()) 361 req.Empty(foundDiff) 362 req.Nil(iter.Next()) 363 } 364 365 func assertTupleCorrectlyStored(req *require.Assertions, ds datastore.Datastore, rev datastore.Revision, expected *core.RelationTuple) { 366 iter, err := ds.SnapshotReader(rev).QueryRelationships(context.Background(), datastore.RelationshipsFilter{ 367 OptionalResourceType: expected.ResourceAndRelation.Namespace, 368 }) 369 req.NoError(err) 370 371 defer iter.Close() 372 readTpl := iter.Next() 373 foundDiff := cmp.Diff(expected, readTpl, protocmp.Transform()) 374 req.Empty(foundDiff) 375 } 376 377 func skipIfNotCaveatStorer(t *testing.T, ds datastore.Datastore) { 378 ctx := context.Background() 379 _, _ = ds.ReadWriteTx(ctx, func(ctx context.Context, transaction datastore.ReadWriteTransaction) error { // nolint: errcheck 380 _, _, err := transaction.ReadCaveatByName(ctx, uuid.NewString()) 381 if !errors.As(err, &datastore.ErrCaveatNameNotFound{}) { 382 t.Skip("datastore does not implement CaveatStorer interface") 383 } 384 return fmt.Errorf("force rollback of unnecesary tx") 385 }) 386 } 387 388 func createTestCaveatedTuple(t *testing.T, tplString string, caveatName string) *core.RelationTuple { 389 tpl := tuple.MustParse(tplString) 390 st, err := structpb.NewStruct(map[string]interface{}{"a": 1, "b": "test"}) 391 require.NoError(t, err) 392 393 tpl.Caveat = &core.ContextualizedCaveat{ 394 CaveatName: caveatName, 395 Context: st, 396 } 397 return tpl 398 } 399 400 func writeCaveats(ctx context.Context, ds datastore.Datastore, coreCaveat ...*core.CaveatDefinition) (datastore.Revision, error) { 401 rev, err := ds.ReadWriteTx(ctx, func(ctx context.Context, tx datastore.ReadWriteTransaction) error { 402 return tx.WriteCaveats(ctx, coreCaveat) 403 }) 404 if err != nil { 405 return datastore.NoRevision, err 406 } 407 return rev, err 408 } 409 410 func writeCaveat(ctx context.Context, ds datastore.Datastore, coreCaveat *core.CaveatDefinition) (datastore.Revision, error) { 411 rev, err := writeCaveats(ctx, ds, coreCaveat) 412 if err != nil { 413 return datastore.NoRevision, err 414 } 415 return rev, nil 416 } 417 418 func createCoreCaveat(t *testing.T) *core.CaveatDefinition { 419 t.Helper() 420 c := createCompiledCaveat(t) 421 cBytes, err := c.Serialize() 422 require.NoError(t, err) 423 424 env := caveats.NewEnvironment() 425 426 err = env.AddVariable("foo", caveattypes.IntType) 427 require.NoError(t, err) 428 429 err = env.AddVariable("bar", caveattypes.MustMapType(caveattypes.BytesType)) 430 require.NoError(t, err) 431 432 coreCaveat := &core.CaveatDefinition{ 433 Name: c.Name(), 434 SerializedExpression: cBytes, 435 ParameterTypes: env.EncodedParametersTypes(), 436 } 437 require.NoError(t, err) 438 439 return coreCaveat 440 } 441 442 func createCompiledCaveat(t *testing.T) *caveats.CompiledCaveat { 443 t.Helper() 444 env, err := caveats.EnvForVariables(map[string]caveattypes.VariableType{ 445 "a": caveattypes.IntType, 446 "b": caveattypes.IntType, 447 }) 448 require.NoError(t, err) 449 450 c, err := caveats.CompileCaveatWithName(env, "a == b", uuid.New().String()) 451 require.NoError(t, err) 452 453 return c 454 }