github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/services/v1/relationships_test.go (about) 1 package v1_test 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "maps" 9 "testing" 10 "time" 11 12 v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" 13 "github.com/authzed/grpcutil" 14 "github.com/stretchr/testify/require" 15 "golang.org/x/sync/errgroup" 16 "google.golang.org/grpc/codes" 17 "google.golang.org/grpc/status" 18 "google.golang.org/protobuf/proto" 19 "google.golang.org/protobuf/types/known/structpb" 20 21 "github.com/authzed/spicedb/internal/datastore/memdb" 22 tf "github.com/authzed/spicedb/internal/testfixtures" 23 "github.com/authzed/spicedb/internal/testserver" 24 "github.com/authzed/spicedb/pkg/datastore" 25 core "github.com/authzed/spicedb/pkg/proto/core/v1" 26 "github.com/authzed/spicedb/pkg/spiceerrors" 27 "github.com/authzed/spicedb/pkg/tuple" 28 "github.com/authzed/spicedb/pkg/zedtoken" 29 ) 30 31 func TestReadRelationships(t *testing.T) { 32 testCases := []struct { 33 name string 34 filter *v1.RelationshipFilter 35 expectedCode codes.Code 36 expected map[string]struct{} 37 }{ 38 { 39 "namespace only", 40 &v1.RelationshipFilter{ResourceType: tf.DocumentNS.Name}, 41 codes.OK, 42 map[string]struct{}{ 43 "document:ownerplan#viewer@user:owner": {}, 44 "document:companyplan#parent@folder:company": {}, 45 "document:masterplan#parent@folder:strategy": {}, 46 "document:masterplan#owner@user:product_manager": {}, 47 "document:masterplan#viewer@user:eng_lead": {}, 48 "document:masterplan#parent@folder:plans": {}, 49 "document:healthplan#parent@folder:plans": {}, 50 "document:specialplan#editor@user:multiroleguy": {}, 51 "document:specialplan#viewer_and_editor@user:multiroleguy": {}, 52 "document:specialplan#viewer_and_editor@user:missingrolegal": {}, 53 "document:base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==#owner@user:base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==": {}, 54 "document:veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryverylong#owner@user:veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryverylong": {}, 55 }, 56 }, 57 { 58 "namespace and object id", 59 &v1.RelationshipFilter{ 60 ResourceType: tf.DocumentNS.Name, 61 OptionalResourceId: "healthplan", 62 }, 63 codes.OK, 64 map[string]struct{}{ 65 "document:healthplan#parent@folder:plans": {}, 66 }, 67 }, 68 { 69 "namespace and relation", 70 &v1.RelationshipFilter{ 71 ResourceType: tf.DocumentNS.Name, 72 OptionalRelation: "parent", 73 }, 74 codes.OK, 75 map[string]struct{}{ 76 "document:companyplan#parent@folder:company": {}, 77 "document:masterplan#parent@folder:strategy": {}, 78 "document:masterplan#parent@folder:plans": {}, 79 "document:healthplan#parent@folder:plans": {}, 80 }, 81 }, 82 { 83 "resource id prefix and resource id", 84 &v1.RelationshipFilter{ 85 OptionalResourceId: "master", 86 OptionalResourceIdPrefix: "master", 87 OptionalRelation: "parent", 88 }, 89 codes.InvalidArgument, 90 nil, 91 }, 92 { 93 "just relation", 94 &v1.RelationshipFilter{ 95 OptionalRelation: "parent", 96 }, 97 codes.OK, 98 map[string]struct{}{ 99 "document:companyplan#parent@folder:company": {}, 100 "document:masterplan#parent@folder:strategy": {}, 101 "document:masterplan#parent@folder:plans": {}, 102 "document:healthplan#parent@folder:plans": {}, 103 "folder:strategy#parent@folder:company": {}, 104 }, 105 }, 106 { 107 "just resource ID", 108 &v1.RelationshipFilter{ 109 OptionalResourceId: "masterplan", 110 }, 111 codes.OK, 112 map[string]struct{}{ 113 "document:masterplan#parent@folder:strategy": {}, 114 "document:masterplan#parent@folder:plans": {}, 115 "document:masterplan#owner@user:product_manager": {}, 116 "document:masterplan#viewer@user:eng_lead": {}, 117 }, 118 }, 119 { 120 "just resource ID prefix", 121 &v1.RelationshipFilter{ 122 OptionalResourceIdPrefix: "masterpl", 123 }, 124 codes.OK, 125 map[string]struct{}{ 126 "document:masterplan#parent@folder:strategy": {}, 127 "document:masterplan#parent@folder:plans": {}, 128 "document:masterplan#owner@user:product_manager": {}, 129 "document:masterplan#viewer@user:eng_lead": {}, 130 }, 131 }, 132 { 133 "relation and resource ID prefix", 134 &v1.RelationshipFilter{ 135 OptionalRelation: "parent", 136 OptionalResourceIdPrefix: "masterpl", 137 }, 138 codes.OK, 139 map[string]struct{}{ 140 "document:masterplan#parent@folder:strategy": {}, 141 "document:masterplan#parent@folder:plans": {}, 142 }, 143 }, 144 { 145 "namespace and userset", 146 &v1.RelationshipFilter{ 147 ResourceType: tf.DocumentNS.Name, 148 OptionalSubjectFilter: &v1.SubjectFilter{ 149 SubjectType: "folder", 150 OptionalSubjectId: "plans", 151 }, 152 }, 153 codes.OK, 154 map[string]struct{}{ 155 "document:masterplan#parent@folder:plans": {}, 156 "document:healthplan#parent@folder:plans": {}, 157 }, 158 }, 159 { 160 "multiple filters", 161 &v1.RelationshipFilter{ 162 ResourceType: tf.DocumentNS.Name, 163 OptionalResourceId: "masterplan", 164 OptionalSubjectFilter: &v1.SubjectFilter{ 165 SubjectType: "folder", 166 OptionalSubjectId: "plans", 167 }, 168 }, 169 codes.OK, 170 map[string]struct{}{ 171 "document:masterplan#parent@folder:plans": {}, 172 }, 173 }, 174 { 175 "bad objectId", 176 &v1.RelationshipFilter{ 177 ResourceType: tf.DocumentNS.Name, 178 OptionalResourceId: "🍣", 179 OptionalSubjectFilter: &v1.SubjectFilter{ 180 SubjectType: "folder", 181 OptionalSubjectId: "plans", 182 }, 183 }, 184 codes.InvalidArgument, 185 nil, 186 }, 187 { 188 "bad object relation", 189 &v1.RelationshipFilter{ 190 ResourceType: tf.DocumentNS.Name, 191 OptionalRelation: "ad", 192 }, 193 codes.InvalidArgument, 194 nil, 195 }, 196 { 197 "bad subject filter", 198 &v1.RelationshipFilter{ 199 ResourceType: tf.DocumentNS.Name, 200 OptionalResourceId: "ma", 201 OptionalSubjectFilter: &v1.SubjectFilter{ 202 SubjectType: "doesnotexist", 203 }, 204 }, 205 codes.FailedPrecondition, 206 nil, 207 }, 208 { 209 "empty argument for required filter value", 210 &v1.RelationshipFilter{ 211 ResourceType: tf.DocumentNS.Name, 212 OptionalSubjectFilter: &v1.SubjectFilter{}, 213 }, 214 codes.InvalidArgument, 215 nil, 216 }, 217 { 218 "bad relation filter", 219 &v1.RelationshipFilter{ 220 ResourceType: tf.DocumentNS.Name, 221 OptionalSubjectFilter: &v1.SubjectFilter{ 222 SubjectType: "folder", 223 OptionalRelation: &v1.SubjectFilter_RelationFilter{ 224 Relation: "...", 225 }, 226 }, 227 }, 228 codes.InvalidArgument, 229 nil, 230 }, 231 { 232 "missing namespace", 233 &v1.RelationshipFilter{ 234 ResourceType: "doesnotexist", 235 }, 236 codes.FailedPrecondition, 237 nil, 238 }, 239 { 240 "missing relation", 241 &v1.RelationshipFilter{ 242 ResourceType: tf.DocumentNS.Name, 243 OptionalRelation: "invalidrelation", 244 }, 245 codes.FailedPrecondition, 246 nil, 247 }, 248 { 249 "missing subject relation", 250 &v1.RelationshipFilter{ 251 ResourceType: tf.DocumentNS.Name, 252 OptionalSubjectFilter: &v1.SubjectFilter{ 253 SubjectType: "folder", 254 OptionalRelation: &v1.SubjectFilter_RelationFilter{ 255 Relation: "doesnotexist", 256 }, 257 }, 258 }, 259 codes.FailedPrecondition, 260 nil, 261 }, 262 { 263 "invalid filter", 264 &v1.RelationshipFilter{ 265 OptionalResourceId: "auditors", 266 OptionalResourceIdPrefix: "aud", 267 }, 268 codes.InvalidArgument, 269 nil, 270 }, 271 } 272 273 for _, pageSize := range []int{0, 1, 5, 10} { 274 pageSize := pageSize 275 t.Run(fmt.Sprintf("page%d_", pageSize), func(t *testing.T) { 276 for _, delta := range testTimedeltas { 277 delta := delta 278 t.Run(fmt.Sprintf("fuzz%d", delta/time.Millisecond), func(t *testing.T) { 279 for _, tc := range testCases { 280 tc := tc 281 t.Run(tc.name, func(t *testing.T) { 282 require := require.New(t) 283 conn, cleanup, _, revision := testserver.NewTestServer(require, delta, memdb.DisableGC, true, tf.StandardDatastoreWithData) 284 client := v1.NewPermissionsServiceClient(conn) 285 t.Cleanup(cleanup) 286 287 var currentCursor *v1.Cursor 288 289 // Make a copy of the expected map 290 testExpected := make(map[string]struct{}, len(tc.expected)) 291 for k := range tc.expected { 292 testExpected[k] = struct{}{} 293 } 294 295 for i := 0; i < 20; i++ { 296 stream, err := client.ReadRelationships(context.Background(), &v1.ReadRelationshipsRequest{ 297 Consistency: &v1.Consistency{ 298 Requirement: &v1.Consistency_AtLeastAsFresh{ 299 AtLeastAsFresh: zedtoken.MustNewFromRevision(revision), 300 }, 301 }, 302 RelationshipFilter: tc.filter, 303 OptionalLimit: uint32(pageSize), 304 OptionalCursor: currentCursor, 305 }) 306 require.NoError(err) 307 308 if tc.expectedCode != codes.OK { 309 _, err := stream.Recv() 310 grpcutil.RequireStatus(t, tc.expectedCode, err) 311 return 312 } 313 314 foundCount := 0 315 for { 316 rel, err := stream.Recv() 317 if errors.Is(err, io.EOF) { 318 break 319 } 320 321 require.NoError(err) 322 323 dsFilter, err := datastore.RelationshipsFilterFromPublicFilter(tc.filter) 324 require.NoError(err) 325 326 require.True(dsFilter.Test(tuple.MustFromRelationship(rel.Relationship)), "relationship did not match filter: %v", rel.Relationship) 327 328 relString := tuple.MustRelString(rel.Relationship) 329 _, found := tc.expected[relString] 330 require.True(found, "relationship was not expected: %s", relString) 331 332 _, notFoundTwice := testExpected[relString] 333 require.True(notFoundTwice, "relationship was received from service twice: %s", relString) 334 335 delete(testExpected, relString) 336 currentCursor = rel.AfterResultCursor 337 foundCount++ 338 } 339 340 if pageSize == 0 { 341 break 342 } 343 344 require.LessOrEqual(foundCount, pageSize) 345 if foundCount < pageSize { 346 break 347 } 348 } 349 350 require.Empty(testExpected, "expected relationships were not received: %v", testExpected) 351 }) 352 } 353 }) 354 } 355 }) 356 } 357 } 358 359 func TestWriteRelationships(t *testing.T) { 360 require := require.New(t) 361 362 conn, cleanup, _, _ := testserver.NewTestServer(require, 0, memdb.DisableGC, true, tf.StandardDatastoreWithData) 363 client := v1.NewPermissionsServiceClient(conn) 364 t.Cleanup(cleanup) 365 366 toWrite := []*core.RelationTuple{ 367 tuple.MustParse("document:totallynew#parent@folder:plans"), 368 tuple.MustParse("document:--base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==#owner@user:--base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK=="), 369 tuple.MustParse("document:veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryincrediblysuuperlong#owner@user:veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryincrediblysuuperlong"), 370 } 371 372 // Write with a failing precondition 373 resp, err := client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ 374 Updates: []*v1.RelationshipUpdate{{ 375 Operation: v1.RelationshipUpdate_OPERATION_CREATE, 376 Relationship: tuple.MustToRelationship(toWrite[0]), 377 }}, 378 OptionalPreconditions: []*v1.Precondition{{ 379 Operation: v1.Precondition_OPERATION_MUST_MATCH, 380 Filter: tuple.MustToFilter(toWrite[0]), 381 }}, 382 }) 383 require.Nil(resp) 384 grpcutil.RequireStatus(t, codes.FailedPrecondition, err) 385 spiceerrors.RequireReason(t, v1.ErrorReason_ERROR_REASON_WRITE_OR_DELETE_PRECONDITION_FAILURE, err, 386 "precondition_operation", 387 "precondition_relation", 388 "precondition_resource_id", 389 "precondition_resource_type", 390 "precondition_subject_id", 391 "precondition_subject_relation", 392 "precondition_subject_type", 393 ) 394 395 existing := tuple.Parse(tf.StandardTuples[0]) 396 require.NotNil(existing) 397 398 // Write with a succeeding precondition 399 toWriteUpdates := make([]*v1.RelationshipUpdate, 0, len(toWrite)) 400 for _, tpl := range toWrite { 401 toWriteUpdates = append(toWriteUpdates, &v1.RelationshipUpdate{ 402 Operation: v1.RelationshipUpdate_OPERATION_CREATE, 403 Relationship: tuple.MustToRelationship(tpl), 404 }) 405 } 406 resp, err = client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ 407 Updates: toWriteUpdates, 408 OptionalPreconditions: []*v1.Precondition{{ 409 Operation: v1.Precondition_OPERATION_MUST_MATCH, 410 Filter: tuple.MustToFilter(existing), 411 }}, 412 }) 413 require.NoError(err) 414 require.NotNil(resp.WrittenAt) 415 require.NotZero(resp.WrittenAt.Token) 416 417 // Ensure the written relationships exist 418 for _, tpl := range toWrite { 419 findWritten := &v1.RelationshipFilter{ 420 ResourceType: tpl.ResourceAndRelation.Namespace, 421 OptionalResourceId: tpl.ResourceAndRelation.ObjectId, 422 } 423 424 stream, err := client.ReadRelationships(context.Background(), &v1.ReadRelationshipsRequest{ 425 RelationshipFilter: findWritten, 426 }) 427 require.NoError(err) 428 rel, err := stream.Recv() 429 require.NoError(err) 430 relStr, err := tuple.StringRelationship(rel.Relationship) 431 require.NoError(err) 432 require.Equal(tuple.MustString(tpl), relStr) 433 434 _, err = stream.Recv() 435 require.ErrorIs(err, io.EOF) 436 437 // Delete the written relationship 438 deleted, err := client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ 439 Updates: []*v1.RelationshipUpdate{{ 440 Operation: v1.RelationshipUpdate_OPERATION_DELETE, 441 Relationship: tuple.MustToRelationship(tpl), 442 }}, 443 }) 444 require.NoError(err) 445 446 // Ensure the relationship was deleted 447 stream, err = client.ReadRelationships(context.Background(), &v1.ReadRelationshipsRequest{ 448 Consistency: &v1.Consistency{ 449 Requirement: &v1.Consistency_AtLeastAsFresh{AtLeastAsFresh: deleted.WrittenAt}, 450 }, 451 RelationshipFilter: findWritten, 452 }) 453 require.NoError(err) 454 _, err = stream.Recv() 455 require.ErrorIs(err, io.EOF) 456 } 457 } 458 459 func TestDeleteRelationshipViaWriteNoop(t *testing.T) { 460 require := require.New(t) 461 462 conn, cleanup, _, _ := testserver.NewTestServer(require, 0, memdb.DisableGC, true, tf.StandardDatastoreWithData) 463 client := v1.NewPermissionsServiceClient(conn) 464 t.Cleanup(cleanup) 465 466 toDelete := tuple.MustParse("document:totallynew#parent@folder:plans") 467 468 // Delete the non-existent relationship 469 _, err := client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ 470 Updates: []*v1.RelationshipUpdate{{ 471 Operation: v1.RelationshipUpdate_OPERATION_DELETE, 472 Relationship: tuple.MustToRelationship(toDelete), 473 }}, 474 }) 475 require.NoError(err) 476 } 477 478 func TestWriteCaveatedRelationships(t *testing.T) { 479 for _, deleteWithCaveat := range []bool{true, false} { 480 t.Run(fmt.Sprintf("with-caveat-%v", deleteWithCaveat), func(t *testing.T) { 481 req := require.New(t) 482 483 conn, cleanup, _, _ := testserver.NewTestServer(req, 0, memdb.DisableGC, true, tf.StandardDatastoreWithData) 484 client := v1.NewPermissionsServiceClient(conn) 485 t.Cleanup(cleanup) 486 487 toWrite := tuple.MustParse("document:companyplan#caveated_viewer@user:johndoe#...") 488 caveatCtx, err := structpb.NewStruct(map[string]any{"expectedSecret": "hi"}) 489 req.NoError(err) 490 491 toWrite.Caveat = &core.ContextualizedCaveat{ 492 CaveatName: "doesnotexist", 493 Context: caveatCtx, 494 } 495 toWrite.Caveat.Context = caveatCtx 496 relWritten := tuple.MustToRelationship(toWrite) 497 writeReq := &v1.WriteRelationshipsRequest{ 498 Updates: []*v1.RelationshipUpdate{{ 499 Operation: v1.RelationshipUpdate_OPERATION_CREATE, 500 Relationship: relWritten, 501 }}, 502 } 503 504 // Should fail due to non-existing caveat 505 ctx := context.Background() 506 _, err = client.WriteRelationships(ctx, writeReq) 507 grpcutil.RequireStatus(t, codes.InvalidArgument, err) 508 509 req.Contains(err.Error(), "subjects of type `user with doesnotexist` are not allowed on relation `document#caveated_viewer`") 510 511 // should succeed 512 relWritten.OptionalCaveat.CaveatName = "test" 513 resp, err := client.WriteRelationships(context.Background(), writeReq) 514 req.NoError(err) 515 516 // read relationship back 517 relRead := readFirst(req, client, resp.WrittenAt, relWritten) 518 req.True(proto.Equal(relWritten, relRead)) 519 520 // issue the deletion 521 relToDelete := tuple.MustToRelationship(tuple.MustParse("document:companyplan#caveated_viewer@user:johndoe#...")) 522 if deleteWithCaveat { 523 relToDelete = tuple.MustToRelationship(tuple.MustParse("document:companyplan#caveated_viewer@user:johndoe#...[test]")) 524 } 525 526 deleteReq := &v1.WriteRelationshipsRequest{ 527 Updates: []*v1.RelationshipUpdate{{ 528 Operation: v1.RelationshipUpdate_OPERATION_DELETE, 529 Relationship: relToDelete, 530 }}, 531 } 532 533 resp, err = client.WriteRelationships(context.Background(), deleteReq) 534 req.NoError(err) 535 536 // ensure the relationship is no longer present. 537 stream, err := client.ReadRelationships(context.Background(), &v1.ReadRelationshipsRequest{ 538 Consistency: &v1.Consistency{ 539 Requirement: &v1.Consistency_AtExactSnapshot{ 540 AtExactSnapshot: resp.WrittenAt, 541 }, 542 }, 543 RelationshipFilter: tuple.RelToFilter(relWritten), 544 }) 545 require.NoError(t, err) 546 547 _, err = stream.Recv() 548 require.True(t, errors.Is(err, io.EOF)) 549 }) 550 } 551 } 552 553 func readFirst(require *require.Assertions, client v1.PermissionsServiceClient, token *v1.ZedToken, rel *v1.Relationship) *v1.Relationship { 554 stream, err := client.ReadRelationships(context.Background(), &v1.ReadRelationshipsRequest{ 555 Consistency: &v1.Consistency{ 556 Requirement: &v1.Consistency_AtExactSnapshot{ 557 AtExactSnapshot: token, 558 }, 559 }, 560 RelationshipFilter: tuple.RelToFilter(rel), 561 }) 562 require.NoError(err) 563 564 result, err := stream.Recv() 565 require.NoError(err) 566 return result.Relationship 567 } 568 569 func precondFilter(resType, resID, relation, subType, subID string, subRel *string) *v1.RelationshipFilter { 570 var optionalRel *v1.SubjectFilter_RelationFilter 571 if subRel != nil { 572 optionalRel = &v1.SubjectFilter_RelationFilter{ 573 Relation: *subRel, 574 } 575 } 576 577 return &v1.RelationshipFilter{ 578 ResourceType: resType, 579 OptionalResourceId: resID, 580 OptionalRelation: relation, 581 OptionalSubjectFilter: &v1.SubjectFilter{ 582 SubjectType: subType, 583 OptionalSubjectId: subID, 584 OptionalRelation: optionalRel, 585 }, 586 } 587 } 588 589 func rel(resType, resID, relation, subType, subID, subRel string) *v1.Relationship { 590 return &v1.Relationship{ 591 Resource: &v1.ObjectReference{ 592 ObjectType: resType, 593 ObjectId: resID, 594 }, 595 Relation: relation, 596 Subject: &v1.SubjectReference{ 597 Object: &v1.ObjectReference{ 598 ObjectType: subType, 599 ObjectId: subID, 600 }, 601 OptionalRelation: subRel, 602 }, 603 } 604 } 605 606 func relWithCaveat(resType, resID, relation, subType, subID, subRel, caveatName string) *v1.Relationship { 607 return &v1.Relationship{ 608 Resource: &v1.ObjectReference{ 609 ObjectType: resType, 610 ObjectId: resID, 611 }, 612 Relation: relation, 613 Subject: &v1.SubjectReference{ 614 Object: &v1.ObjectReference{ 615 ObjectType: subType, 616 ObjectId: subID, 617 }, 618 OptionalRelation: subRel, 619 }, 620 OptionalCaveat: &v1.ContextualizedCaveat{ 621 CaveatName: caveatName, 622 }, 623 } 624 } 625 626 func TestInvalidWriteRelationship(t *testing.T) { 627 testCases := []struct { 628 name string 629 preconditions []*v1.RelationshipFilter 630 relationships []*v1.Relationship 631 expectedCode codes.Code 632 errorContains string 633 }{ 634 { 635 "empty relationship", 636 nil, 637 []*v1.Relationship{{}}, 638 codes.InvalidArgument, 639 "value is required", 640 }, 641 { 642 "empty precondition", 643 []*v1.RelationshipFilter{{}}, 644 nil, 645 codes.InvalidArgument, 646 "the relationship filter provided is not valid", 647 }, 648 { 649 "good precondition, invalid update", 650 []*v1.RelationshipFilter{precondFilter("document", "newdoc", "parent", "folder", "afolder", nil)}, 651 []*v1.Relationship{rel("document", "🍣", "parent", "folder", "afolder", "")}, 652 codes.InvalidArgument, 653 "caused by: invalid ObjectReference.ObjectId: value does not match regex pattern", 654 }, 655 { 656 "invalid precondition, good write", 657 []*v1.RelationshipFilter{precondFilter("document", "🍣", "parent", "folder", "afolder", nil)}, 658 []*v1.Relationship{rel("document", "newdoc", "parent", "folder", "afolder", "")}, 659 codes.InvalidArgument, 660 "caused by: invalid RelationshipFilter.OptionalResourceId: value does not match regex pattern", 661 }, 662 { 663 "write permission", 664 nil, 665 []*v1.Relationship{rel("document", "newdoc", "view", "user", "tom", "")}, 666 codes.InvalidArgument, 667 "cannot write a relationship to permission `view`", 668 }, 669 { 670 "write non-existing resource namespace", 671 nil, 672 []*v1.Relationship{rel("notdocument", "newdoc", "parent", "folder", "afolder", "")}, 673 codes.FailedPrecondition, 674 "`notdocument` not found", 675 }, 676 { 677 "write non-existing relation", 678 nil, 679 []*v1.Relationship{rel("document", "newdoc", "notparent", "folder", "afolder", "")}, 680 codes.FailedPrecondition, 681 "`notparent` not found", 682 }, 683 { 684 "write non-existing subject type", 685 nil, 686 []*v1.Relationship{rel("document", "newdoc", "parent", "notfolder", "afolder", "")}, 687 codes.FailedPrecondition, 688 "`notfolder` not found", 689 }, 690 { 691 "write non-existing subject relation", 692 nil, 693 []*v1.Relationship{rel("document", "newdoc", "parent", "folder", "afolder", "none")}, 694 codes.FailedPrecondition, 695 "`none` not found", 696 }, 697 { 698 "bad write wrong relation type", 699 nil, 700 []*v1.Relationship{rel("document", "newdoc", "parent", "user", "someuser", "")}, 701 codes.InvalidArgument, 702 "user", 703 }, 704 { 705 "bad write wildcard object", 706 nil, 707 []*v1.Relationship{rel("document", "*", "parent", "user", "someuser", "")}, 708 codes.InvalidArgument, 709 "alphanumeric", 710 }, 711 { 712 "disallowed wildcard subject", 713 nil, 714 []*v1.Relationship{rel("document", "somedoc", "parent", "user", "*", "")}, 715 codes.InvalidArgument, 716 "user:*", 717 }, 718 { 719 "duplicate relationship", 720 nil, 721 []*v1.Relationship{ 722 rel("document", "somedoc", "parent", "user", "tom", ""), 723 rel("document", "somedoc", "parent", "user", "tom", ""), 724 }, 725 codes.InvalidArgument, 726 "found more than one update", 727 }, 728 { 729 "disallowed caveat", 730 nil, 731 []*v1.Relationship{relWithCaveat("document", "somedoc", "viewer", "user", "tom", "", "somecaveat")}, 732 codes.InvalidArgument, 733 "user with somecaveat", 734 }, 735 { 736 "wildcard disallowed caveat", 737 nil, 738 []*v1.Relationship{relWithCaveat("document", "somedoc", "viewer", "user", "*", "", "somecaveat")}, 739 codes.InvalidArgument, 740 "user:* with somecaveat", 741 }, 742 { 743 "disallowed relation caveat", 744 nil, 745 []*v1.Relationship{relWithCaveat("document", "somedoc", "viewer", "folder", "foo", "owner", "somecaveat")}, 746 codes.InvalidArgument, 747 "folder#owner with somecaveat", 748 }, 749 { 750 "caveated and uncaveated versions of the same relationship", 751 nil, 752 []*v1.Relationship{ 753 rel("document", "somedoc", "viewer", "user", "tom", ""), 754 relWithCaveat("document", "somedoc", "viewer", "user", "tom", "", "somecaveat"), 755 }, 756 codes.InvalidArgument, 757 "found more than one update with relationship", 758 }, 759 } 760 761 for _, delta := range testTimedeltas { 762 delta := delta 763 t.Run(fmt.Sprintf("fuzz%d", delta/time.Millisecond), func(t *testing.T) { 764 for _, tc := range testCases { 765 tc := tc 766 t.Run(tc.name, func(t *testing.T) { 767 require := require.New(t) 768 conn, cleanup, _, _ := testserver.NewTestServer(require, 0, memdb.DisableGC, true, tf.StandardDatastoreWithData) 769 client := v1.NewPermissionsServiceClient(conn) 770 t.Cleanup(cleanup) 771 772 var preconditions []*v1.Precondition 773 for _, filter := range tc.preconditions { 774 preconditions = append(preconditions, &v1.Precondition{ 775 Operation: v1.Precondition_OPERATION_MUST_MATCH, 776 Filter: filter, 777 }) 778 } 779 780 var mutations []*v1.RelationshipUpdate 781 for _, rel := range tc.relationships { 782 mutations = append(mutations, &v1.RelationshipUpdate{ 783 Operation: v1.RelationshipUpdate_OPERATION_TOUCH, 784 Relationship: rel, 785 }) 786 } 787 788 _, err := client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ 789 Updates: mutations, 790 OptionalPreconditions: preconditions, 791 }) 792 grpcutil.RequireStatus(t, tc.expectedCode, err) 793 errStatus, ok := status.FromError(err) 794 if !ok { 795 panic("failed to find error in status") 796 } 797 require.Contains(errStatus.Message(), tc.errorContains, "found unexpected error message: %s", errStatus.Message()) 798 }) 799 } 800 }) 801 } 802 } 803 804 func TestDeleteRelationships(t *testing.T) { 805 testCases := []struct { 806 name string 807 req *v1.DeleteRelationshipsRequest 808 deleted map[string]struct{} 809 expectedCode codes.Code 810 errorContains string 811 }{ 812 { 813 name: "delete fully specified", 814 req: &v1.DeleteRelationshipsRequest{ 815 RelationshipFilter: &v1.RelationshipFilter{ 816 ResourceType: "folder", 817 OptionalResourceId: "auditors", 818 OptionalRelation: "viewer", 819 OptionalSubjectFilter: &v1.SubjectFilter{ 820 SubjectType: "user", 821 OptionalSubjectId: "auditor", 822 }, 823 }, 824 }, 825 deleted: map[string]struct{}{ 826 "folder:auditors#viewer@user:auditor": {}, 827 }, 828 }, 829 { 830 name: "delete by resource ID", 831 req: &v1.DeleteRelationshipsRequest{ 832 RelationshipFilter: &v1.RelationshipFilter{ 833 OptionalResourceId: "auditors", 834 }, 835 }, 836 deleted: map[string]struct{}{ 837 "folder:auditors#viewer@user:auditor": {}, 838 }, 839 }, 840 { 841 name: "delete by resource ID prefix", 842 req: &v1.DeleteRelationshipsRequest{ 843 RelationshipFilter: &v1.RelationshipFilter{ 844 OptionalResourceIdPrefix: "a", 845 }, 846 }, 847 deleted: map[string]struct{}{ 848 "folder:auditors#viewer@user:auditor": {}, 849 }, 850 }, 851 { 852 name: "delete by relation and resource ID prefix", 853 req: &v1.DeleteRelationshipsRequest{ 854 RelationshipFilter: &v1.RelationshipFilter{ 855 OptionalResourceIdPrefix: "s", 856 OptionalRelation: "editor", 857 }, 858 }, 859 deleted: map[string]struct{}{ 860 "document:specialplan#editor@user:multiroleguy": {}, 861 }, 862 }, 863 { 864 name: "delete resource + relation + subject type", 865 req: &v1.DeleteRelationshipsRequest{ 866 RelationshipFilter: &v1.RelationshipFilter{ 867 ResourceType: "document", 868 OptionalResourceId: "masterplan", 869 OptionalRelation: "parent", 870 OptionalSubjectFilter: &v1.SubjectFilter{ 871 SubjectType: "folder", 872 }, 873 }, 874 }, 875 deleted: map[string]struct{}{ 876 "document:masterplan#parent@folder:strategy": {}, 877 "document:masterplan#parent@folder:plans": {}, 878 }, 879 }, 880 { 881 name: "delete resource + relation", 882 req: &v1.DeleteRelationshipsRequest{ 883 RelationshipFilter: &v1.RelationshipFilter{ 884 ResourceType: "document", 885 OptionalResourceId: "specialplan", 886 OptionalRelation: "viewer_and_editor", 887 }, 888 }, 889 deleted: map[string]struct{}{ 890 "document:specialplan#viewer_and_editor@user:multiroleguy": {}, 891 "document:specialplan#viewer_and_editor@user:missingrolegal": {}, 892 }, 893 }, 894 { 895 name: "delete resource", 896 req: &v1.DeleteRelationshipsRequest{ 897 RelationshipFilter: &v1.RelationshipFilter{ 898 ResourceType: "document", 899 OptionalResourceId: "specialplan", 900 }, 901 }, 902 deleted: map[string]struct{}{ 903 "document:specialplan#viewer_and_editor@user:multiroleguy": {}, 904 "document:specialplan#editor@user:multiroleguy": {}, 905 "document:specialplan#viewer_and_editor@user:missingrolegal": {}, 906 }, 907 }, 908 { 909 name: "delete resource type", 910 req: &v1.DeleteRelationshipsRequest{ 911 RelationshipFilter: &v1.RelationshipFilter{ 912 ResourceType: "document", 913 }, 914 }, 915 deleted: map[string]struct{}{ 916 "document:ownerplan#viewer@user:owner": {}, 917 "document:companyplan#parent@folder:company": {}, 918 "document:masterplan#parent@folder:strategy": {}, 919 "document:masterplan#owner@user:product_manager": {}, 920 "document:masterplan#viewer@user:eng_lead": {}, 921 "document:masterplan#parent@folder:plans": {}, 922 "document:healthplan#parent@folder:plans": {}, 923 "document:specialplan#viewer_and_editor@user:multiroleguy": {}, 924 "document:specialplan#editor@user:multiroleguy": {}, 925 "document:specialplan#viewer_and_editor@user:missingrolegal": {}, 926 "document:base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==#owner@user:base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==": {}, 927 "document:veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryverylong#owner@user:veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryverylong": {}, 928 }, 929 }, 930 { 931 name: "delete relation", 932 req: &v1.DeleteRelationshipsRequest{ 933 RelationshipFilter: &v1.RelationshipFilter{ 934 ResourceType: "document", 935 OptionalRelation: "parent", 936 }, 937 }, 938 deleted: map[string]struct{}{ 939 "document:companyplan#parent@folder:company": {}, 940 "document:masterplan#parent@folder:strategy": {}, 941 "document:masterplan#parent@folder:plans": {}, 942 "document:healthplan#parent@folder:plans": {}, 943 }, 944 }, 945 { 946 name: "delete relation + subject type", 947 req: &v1.DeleteRelationshipsRequest{ 948 RelationshipFilter: &v1.RelationshipFilter{ 949 ResourceType: "folder", 950 OptionalRelation: "parent", 951 OptionalSubjectFilter: &v1.SubjectFilter{ 952 SubjectType: "folder", 953 }, 954 }, 955 }, 956 deleted: map[string]struct{}{ 957 "folder:strategy#parent@folder:company": {}, 958 }, 959 }, 960 { 961 name: "delete relation + subject type + subject", 962 req: &v1.DeleteRelationshipsRequest{ 963 RelationshipFilter: &v1.RelationshipFilter{ 964 ResourceType: "document", 965 OptionalRelation: "parent", 966 OptionalSubjectFilter: &v1.SubjectFilter{ 967 SubjectType: "folder", 968 OptionalSubjectId: "plans", 969 }, 970 }, 971 }, 972 deleted: map[string]struct{}{ 973 "document:masterplan#parent@folder:plans": {}, 974 "document:healthplan#parent@folder:plans": {}, 975 }, 976 }, 977 { 978 name: "delete relation + subject type + subject + relation", 979 req: &v1.DeleteRelationshipsRequest{ 980 RelationshipFilter: &v1.RelationshipFilter{ 981 ResourceType: "folder", 982 OptionalRelation: "viewer", 983 OptionalSubjectFilter: &v1.SubjectFilter{ 984 SubjectType: "folder", 985 OptionalSubjectId: "auditors", 986 OptionalRelation: &v1.SubjectFilter_RelationFilter{ 987 Relation: "viewer", 988 }, 989 }, 990 }, 991 }, 992 deleted: map[string]struct{}{ 993 "folder:company#viewer@folder:auditors#viewer": {}, 994 }, 995 }, 996 { 997 name: "delete unknown relation", 998 req: &v1.DeleteRelationshipsRequest{ 999 RelationshipFilter: &v1.RelationshipFilter{ 1000 ResourceType: "folder", 1001 OptionalRelation: "spotter", 1002 }, 1003 }, 1004 expectedCode: codes.FailedPrecondition, 1005 errorContains: "relation/permission `spotter` not found under definition `folder`", 1006 }, 1007 { 1008 name: "delete unknown subject type", 1009 req: &v1.DeleteRelationshipsRequest{ 1010 RelationshipFilter: &v1.RelationshipFilter{ 1011 ResourceType: "folder", 1012 OptionalRelation: "viewer", 1013 OptionalSubjectFilter: &v1.SubjectFilter{ 1014 SubjectType: "patron", 1015 }, 1016 }, 1017 }, 1018 expectedCode: codes.FailedPrecondition, 1019 errorContains: "object definition `patron` not found", 1020 }, 1021 { 1022 name: "delete unknown subject", 1023 req: &v1.DeleteRelationshipsRequest{ 1024 RelationshipFilter: &v1.RelationshipFilter{ 1025 ResourceType: "folder", 1026 OptionalRelation: "viewer", 1027 OptionalSubjectFilter: &v1.SubjectFilter{ 1028 SubjectType: "folder", 1029 OptionalSubjectId: "nonexistent", 1030 }, 1031 }, 1032 }, 1033 expectedCode: codes.OK, 1034 }, 1035 { 1036 name: "delete unknown subject relation", 1037 req: &v1.DeleteRelationshipsRequest{ 1038 RelationshipFilter: &v1.RelationshipFilter{ 1039 ResourceType: "folder", 1040 OptionalRelation: "viewer", 1041 OptionalSubjectFilter: &v1.SubjectFilter{ 1042 SubjectType: "folder", 1043 OptionalRelation: &v1.SubjectFilter_RelationFilter{ 1044 Relation: "nonexistent", 1045 }, 1046 }, 1047 }, 1048 }, 1049 expectedCode: codes.FailedPrecondition, 1050 errorContains: "relation/permission `nonexistent` not found under definition `folder`", 1051 }, 1052 { 1053 name: "delete no resource type", 1054 req: &v1.DeleteRelationshipsRequest{ 1055 RelationshipFilter: &v1.RelationshipFilter{ 1056 OptionalResourceId: "someunknownid", 1057 }, 1058 }, 1059 expectedCode: codes.OK, 1060 deleted: map[string]struct{}{}, 1061 }, 1062 { 1063 name: "delete unknown resource type", 1064 req: &v1.DeleteRelationshipsRequest{ 1065 RelationshipFilter: &v1.RelationshipFilter{ 1066 ResourceType: "blah", 1067 }, 1068 }, 1069 expectedCode: codes.FailedPrecondition, 1070 errorContains: "object definition `blah` not found", 1071 }, 1072 { 1073 name: "preconditions met", 1074 req: &v1.DeleteRelationshipsRequest{ 1075 RelationshipFilter: &v1.RelationshipFilter{ 1076 ResourceType: "folder", 1077 OptionalResourceId: "auditors", 1078 OptionalRelation: "viewer", 1079 OptionalSubjectFilter: &v1.SubjectFilter{ 1080 SubjectType: "user", 1081 OptionalSubjectId: "auditor", 1082 }, 1083 }, 1084 OptionalPreconditions: []*v1.Precondition{{ 1085 Operation: v1.Precondition_OPERATION_MUST_MATCH, 1086 Filter: &v1.RelationshipFilter{ResourceType: "document"}, 1087 }}, 1088 }, 1089 deleted: map[string]struct{}{ 1090 "folder:auditors#viewer@user:auditor": {}, 1091 }, 1092 }, 1093 { 1094 name: "preconditions not met", 1095 req: &v1.DeleteRelationshipsRequest{ 1096 RelationshipFilter: &v1.RelationshipFilter{ 1097 ResourceType: "folder", 1098 OptionalResourceId: "auditors", 1099 OptionalRelation: "viewer", 1100 OptionalSubjectFilter: &v1.SubjectFilter{ 1101 SubjectType: "user", 1102 OptionalSubjectId: "auditor", 1103 }, 1104 }, 1105 OptionalPreconditions: []*v1.Precondition{{ 1106 Operation: v1.Precondition_OPERATION_MUST_MATCH, 1107 Filter: &v1.RelationshipFilter{ 1108 ResourceType: "folder", 1109 OptionalResourceId: "auditors", 1110 OptionalRelation: "viewer", 1111 OptionalSubjectFilter: &v1.SubjectFilter{ 1112 SubjectType: "user", 1113 OptionalSubjectId: "jeshk", 1114 }, 1115 }, 1116 }}, 1117 }, 1118 expectedCode: codes.FailedPrecondition, 1119 errorContains: "unable to satisfy write precondition", 1120 }, 1121 { 1122 name: "invalid filter", 1123 req: &v1.DeleteRelationshipsRequest{ 1124 RelationshipFilter: &v1.RelationshipFilter{ 1125 OptionalResourceId: "auditors", 1126 OptionalResourceIdPrefix: "aud", 1127 }, 1128 }, 1129 expectedCode: codes.InvalidArgument, 1130 errorContains: "resource_id and resource_id_prefix cannot be set at the same time", 1131 }, 1132 } 1133 for _, delta := range testTimedeltas { 1134 delta := delta 1135 for _, tc := range testCases { 1136 tc := tc 1137 t.Run(fmt.Sprintf("fuzz%d/%s", delta/time.Millisecond, tc.name), func(t *testing.T) { 1138 require := require.New(t) 1139 conn, cleanup, ds, revision := testserver.NewTestServer(require, delta, memdb.DisableGC, true, tf.StandardDatastoreWithData) 1140 client := v1.NewPermissionsServiceClient(conn) 1141 t.Cleanup(cleanup) 1142 1143 resp, err := client.DeleteRelationships(context.Background(), tc.req) 1144 1145 if tc.expectedCode != codes.OK { 1146 grpcutil.RequireStatus(t, tc.expectedCode, err) 1147 errStatus, ok := status.FromError(err) 1148 require.True(ok) 1149 require.Contains(errStatus.Message(), tc.errorContains) 1150 return 1151 } 1152 require.NoError(err) 1153 require.NotNil(resp.DeletedAt) 1154 rev, err := zedtoken.DecodeRevision(resp.DeletedAt, ds) 1155 require.NoError(err) 1156 require.True(rev.GreaterThan(revision)) 1157 require.EqualValues(standardTuplesWithout(tc.deleted), readAll(require, client, resp.DeletedAt)) 1158 }) 1159 } 1160 } 1161 } 1162 1163 func TestDeleteRelationshipsBeyondLimit(t *testing.T) { 1164 require := require.New(t) 1165 conn, cleanup, _, _ := testserver.NewTestServer(require, 0, memdb.DisableGC, true, tf.StandardDatastoreWithData) 1166 client := v1.NewPermissionsServiceClient(conn) 1167 t.Cleanup(cleanup) 1168 1169 _, err := client.DeleteRelationships(context.Background(), &v1.DeleteRelationshipsRequest{ 1170 RelationshipFilter: &v1.RelationshipFilter{ 1171 ResourceType: "document", 1172 }, 1173 OptionalLimit: 5, 1174 OptionalAllowPartialDeletions: false, 1175 }) 1176 require.Error(err) 1177 require.Contains(err.Error(), "found more than 5 relationships to be deleted and partial deletion was not requested") 1178 } 1179 1180 func TestDeleteRelationshipsBeyondAllowedLimit(t *testing.T) { 1181 require := require.New(t) 1182 conn, cleanup, _, _ := testserver.NewTestServer(require, 0, memdb.DisableGC, true, tf.StandardDatastoreWithData) 1183 client := v1.NewPermissionsServiceClient(conn) 1184 t.Cleanup(cleanup) 1185 1186 _, err := client.DeleteRelationships(context.Background(), &v1.DeleteRelationshipsRequest{ 1187 RelationshipFilter: &v1.RelationshipFilter{ 1188 ResourceType: "document", 1189 }, 1190 OptionalLimit: 1005, 1191 OptionalAllowPartialDeletions: false, 1192 }) 1193 require.Error(err) 1194 require.Contains(err.Error(), "provided limit 1005 is greater than maximum allowed of 1000") 1195 } 1196 1197 func TestReadRelationshipsBeyondAllowedLimit(t *testing.T) { 1198 require := require.New(t) 1199 conn, cleanup, _, _ := testserver.NewTestServer(require, 0, memdb.DisableGC, true, tf.StandardDatastoreWithData) 1200 client := v1.NewPermissionsServiceClient(conn) 1201 t.Cleanup(cleanup) 1202 1203 resp, err := client.ReadRelationships(context.Background(), &v1.ReadRelationshipsRequest{ 1204 RelationshipFilter: &v1.RelationshipFilter{ 1205 ResourceType: "document", 1206 }, 1207 OptionalLimit: 1005, 1208 }) 1209 require.NoError(err) 1210 1211 _, err = resp.Recv() 1212 require.Error(err) 1213 require.Contains(err.Error(), "provided limit 1005 is greater than maximum allowed of 1000") 1214 } 1215 1216 func TestDeleteRelationshipsBeyondLimitPartial(t *testing.T) { 1217 expected := map[string]struct{}{ 1218 "document:ownerplan#viewer@user:owner": {}, 1219 "document:companyplan#parent@folder:company": {}, 1220 "document:masterplan#parent@folder:strategy": {}, 1221 "document:masterplan#owner@user:product_manager": {}, 1222 "document:masterplan#viewer@user:eng_lead": {}, 1223 "document:masterplan#parent@folder:plans": {}, 1224 "document:healthplan#parent@folder:plans": {}, 1225 "document:specialplan#viewer_and_editor@user:multiroleguy": {}, 1226 "document:specialplan#editor@user:multiroleguy": {}, 1227 "document:specialplan#viewer_and_editor@user:missingrolegal": {}, 1228 "document:base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==#owner@user:base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==": {}, 1229 "document:veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryverylong#owner@user:veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryverylong": {}, 1230 } 1231 1232 for _, batchSize := range []int{5, 6, 7, 10} { 1233 batchSize := batchSize 1234 require.Greater(t, len(expected), batchSize) 1235 1236 t.Run(fmt.Sprintf("batchsize-%d", batchSize), func(t *testing.T) { 1237 require := require.New(t) 1238 conn, cleanup, ds, revision := testserver.NewTestServer(require, 0, memdb.DisableGC, true, tf.StandardDatastoreWithData) 1239 client := v1.NewPermissionsServiceClient(conn) 1240 t.Cleanup(cleanup) 1241 1242 iterations := 0 1243 for i := 0; i < 10; i++ { 1244 iterations++ 1245 1246 headRev, err := ds.HeadRevision(context.Background()) 1247 require.NoError(err) 1248 1249 beforeDelete := readOfType(require, "document", client, zedtoken.MustNewFromRevision(headRev)) 1250 1251 resp, err := client.DeleteRelationships(context.Background(), &v1.DeleteRelationshipsRequest{ 1252 RelationshipFilter: &v1.RelationshipFilter{ 1253 ResourceType: "document", 1254 }, 1255 OptionalLimit: uint32(batchSize), 1256 OptionalAllowPartialDeletions: true, 1257 }) 1258 require.NoError(err) 1259 1260 afterDelete := readOfType(require, "document", client, resp.DeletedAt) 1261 require.LessOrEqual(len(beforeDelete)-len(afterDelete), batchSize) 1262 1263 if i == 0 { 1264 require.Equal(v1.DeleteRelationshipsResponse_DELETION_PROGRESS_PARTIAL, resp.DeletionProgress) 1265 } 1266 1267 if resp.DeletionProgress == v1.DeleteRelationshipsResponse_DELETION_PROGRESS_COMPLETE { 1268 require.NoError(err) 1269 require.NotNil(resp.DeletedAt) 1270 1271 rev, err := zedtoken.DecodeRevision(resp.DeletedAt, ds) 1272 require.NoError(err) 1273 require.True(rev.GreaterThan(revision)) 1274 require.EqualValues(standardTuplesWithout(expected), readAll(require, client, resp.DeletedAt)) 1275 break 1276 } 1277 } 1278 1279 require.LessOrEqual(iterations, (len(expected)/batchSize)+1) 1280 }) 1281 } 1282 } 1283 1284 func TestDeleteRelationshipsPreconditionsOverLimit(t *testing.T) { 1285 require := require.New(t) 1286 conn, cleanup, _, _ := testserver.NewTestServerWithConfig( 1287 require, 1288 testTimedeltas[0], 1289 memdb.DisableGC, 1290 true, 1291 testserver.ServerConfig{ 1292 MaxPreconditionsCount: 1, 1293 MaxUpdatesPerWrite: 1, 1294 }, 1295 tf.StandardDatastoreWithData, 1296 ) 1297 client := v1.NewPermissionsServiceClient(conn) 1298 t.Cleanup(cleanup) 1299 1300 _, err := client.DeleteRelationships(context.Background(), &v1.DeleteRelationshipsRequest{ 1301 RelationshipFilter: &v1.RelationshipFilter{ 1302 ResourceType: "folder", 1303 OptionalResourceId: "auditors", 1304 OptionalRelation: "viewer", 1305 OptionalSubjectFilter: &v1.SubjectFilter{ 1306 SubjectType: "user", 1307 OptionalSubjectId: "auditor", 1308 }, 1309 }, 1310 OptionalPreconditions: []*v1.Precondition{ 1311 { 1312 Operation: v1.Precondition_OPERATION_MUST_MATCH, 1313 Filter: &v1.RelationshipFilter{ 1314 ResourceType: "folder", 1315 OptionalResourceId: "auditors", 1316 OptionalRelation: "viewer", 1317 OptionalSubjectFilter: &v1.SubjectFilter{ 1318 SubjectType: "user", 1319 OptionalSubjectId: "jeshk", 1320 }, 1321 }, 1322 }, 1323 { 1324 Operation: v1.Precondition_OPERATION_MUST_MATCH, 1325 Filter: &v1.RelationshipFilter{ 1326 ResourceType: "folder", 1327 OptionalResourceId: "auditors", 1328 OptionalRelation: "viewer", 1329 OptionalSubjectFilter: &v1.SubjectFilter{ 1330 SubjectType: "user", 1331 OptionalSubjectId: "jeshk", 1332 }, 1333 }, 1334 }, 1335 }, 1336 }) 1337 1338 require.Error(err) 1339 require.Contains(err.Error(), "precondition count of 2 is greater than maximum allowed of 1") 1340 } 1341 1342 func TestWriteRelationshipsPreconditionsOverLimit(t *testing.T) { 1343 require := require.New(t) 1344 conn, cleanup, _, _ := testserver.NewTestServerWithConfig( 1345 require, 1346 testTimedeltas[0], 1347 memdb.DisableGC, 1348 true, 1349 testserver.ServerConfig{ 1350 MaxPreconditionsCount: 1, 1351 MaxUpdatesPerWrite: 1, 1352 }, 1353 tf.StandardDatastoreWithData, 1354 ) 1355 client := v1.NewPermissionsServiceClient(conn) 1356 t.Cleanup(cleanup) 1357 1358 _, err := client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ 1359 OptionalPreconditions: []*v1.Precondition{ 1360 { 1361 Operation: v1.Precondition_OPERATION_MUST_MATCH, 1362 Filter: &v1.RelationshipFilter{ 1363 ResourceType: "folder", 1364 OptionalResourceId: "auditors", 1365 OptionalRelation: "viewer", 1366 OptionalSubjectFilter: &v1.SubjectFilter{ 1367 SubjectType: "user", 1368 OptionalSubjectId: "jeshk", 1369 }, 1370 }, 1371 }, 1372 { 1373 Operation: v1.Precondition_OPERATION_MUST_MATCH, 1374 Filter: &v1.RelationshipFilter{ 1375 ResourceType: "folder", 1376 OptionalResourceId: "auditors", 1377 OptionalRelation: "viewer", 1378 OptionalSubjectFilter: &v1.SubjectFilter{ 1379 SubjectType: "user", 1380 OptionalSubjectId: "jeshk", 1381 }, 1382 }, 1383 }, 1384 }, 1385 }) 1386 1387 require.Error(err) 1388 require.Contains(err.Error(), "precondition count of 2 is greater than maximum allowed of 1") 1389 } 1390 1391 func TestWriteRelationshipsUpdatesOverLimit(t *testing.T) { 1392 require := require.New(t) 1393 conn, cleanup, _, _ := testserver.NewTestServerWithConfig( 1394 require, 1395 testTimedeltas[0], 1396 memdb.DisableGC, 1397 true, 1398 testserver.ServerConfig{ 1399 MaxPreconditionsCount: 1, 1400 MaxUpdatesPerWrite: 1, 1401 }, 1402 tf.StandardDatastoreWithData, 1403 ) 1404 client := v1.NewPermissionsServiceClient(conn) 1405 t.Cleanup(cleanup) 1406 1407 _, err := client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ 1408 Updates: []*v1.RelationshipUpdate{ 1409 { 1410 Operation: v1.RelationshipUpdate_OPERATION_TOUCH, 1411 Relationship: rel("document", "newdoc", "parent", "folder", "afolder", ""), 1412 }, 1413 { 1414 Operation: v1.RelationshipUpdate_OPERATION_TOUCH, 1415 Relationship: rel("document", "newdoc", "parent", "folder", "afolder", ""), 1416 }, 1417 }, 1418 }) 1419 1420 require.Error(err) 1421 require.Contains(err.Error(), "update count of 2 is greater than maximum allowed of 1") 1422 } 1423 1424 func TestWriteRelationshipsCaveatExceedsMaxSize(t *testing.T) { 1425 require := require.New(t) 1426 conn, cleanup, _, _ := testserver.NewTestServerWithConfig( 1427 require, 1428 testTimedeltas[0], 1429 memdb.DisableGC, 1430 true, 1431 testserver.ServerConfig{ 1432 MaxRelationshipContextSize: 1, 1433 }, 1434 tf.StandardDatastoreWithCaveatedData, 1435 ) 1436 client := v1.NewPermissionsServiceClient(conn) 1437 t.Cleanup(cleanup) 1438 1439 rel := relWithCaveat("document", "newdoc", "parent", "folder", "afolder", "", "test") 1440 strct, err := structpb.NewStruct(map[string]any{"key": "value"}) 1441 require.NoError(err) 1442 rel.OptionalCaveat.Context = strct 1443 1444 _, err = client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ 1445 Updates: []*v1.RelationshipUpdate{ 1446 { 1447 Operation: v1.RelationshipUpdate_OPERATION_TOUCH, 1448 Relationship: rel, 1449 }, 1450 }, 1451 }) 1452 1453 require.Error(err) 1454 grpcutil.RequireStatus(t, codes.InvalidArgument, err) 1455 require.ErrorContains(err, "exceeded maximum allowed caveat size of 1") 1456 } 1457 1458 func TestReadRelationshipsWithTimeout(t *testing.T) { 1459 require := require.New(t) 1460 1461 conn, cleanup, _, _ := testserver.NewTestServerWithConfig( 1462 require, 1463 0, 1464 memdb.DisableGC, 1465 false, 1466 testserver.ServerConfig{ 1467 MaxUpdatesPerWrite: 1000, 1468 MaxPreconditionsCount: 1000, 1469 StreamingAPITimeout: 1, 1470 }, 1471 tf.StandardDatastoreWithData, 1472 ) 1473 client := v1.NewPermissionsServiceClient(conn) 1474 t.Cleanup(cleanup) 1475 1476 // Write additional test data. 1477 counter := 0 1478 for i := 0; i < 10; i++ { 1479 updates := make([]*v1.RelationshipUpdate, 0, 100) 1480 for j := 0; j < 1000; j++ { 1481 counter++ 1482 updates = append(updates, &v1.RelationshipUpdate{ 1483 Operation: v1.RelationshipUpdate_OPERATION_CREATE, 1484 Relationship: tuple.MustToRelationship(tuple.Parse(fmt.Sprintf("document:doc%d#viewer@user:someguy", counter))), 1485 }) 1486 } 1487 1488 _, err := client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ 1489 Updates: updates, 1490 }) 1491 require.NoError(err) 1492 } 1493 1494 retryCount := 5 1495 for i := 0; i < retryCount; i++ { 1496 // Perform a read and ensures it times out. 1497 stream, err := client.ReadRelationships(context.Background(), &v1.ReadRelationshipsRequest{ 1498 Consistency: &v1.Consistency{ 1499 Requirement: &v1.Consistency_FullyConsistent{FullyConsistent: true}, 1500 }, 1501 RelationshipFilter: &v1.RelationshipFilter{ 1502 ResourceType: "document", 1503 }, 1504 }) 1505 require.NoError(err) 1506 1507 // Ensure the recv fails with a context cancelation. 1508 _, err = stream.Recv() 1509 if err == nil { 1510 continue 1511 } 1512 1513 require.ErrorContains(err, "operation took longer than allowed 1ns to complete") 1514 grpcutil.RequireStatus(t, codes.DeadlineExceeded, err) 1515 } 1516 } 1517 1518 func TestReadRelationshipsInvalidCursor(t *testing.T) { 1519 require := require.New(t) 1520 1521 conn, cleanup, _, revision := testserver.NewTestServer(require, 0, memdb.DisableGC, true, tf.StandardDatastoreWithData) 1522 client := v1.NewPermissionsServiceClient(conn) 1523 t.Cleanup(cleanup) 1524 1525 stream, err := client.ReadRelationships(context.Background(), &v1.ReadRelationshipsRequest{ 1526 Consistency: &v1.Consistency{ 1527 Requirement: &v1.Consistency_AtLeastAsFresh{ 1528 AtLeastAsFresh: zedtoken.MustNewFromRevision(revision), 1529 }, 1530 }, 1531 RelationshipFilter: &v1.RelationshipFilter{ 1532 ResourceType: "folder", 1533 OptionalResourceId: "auditors", 1534 OptionalRelation: "viewer", 1535 OptionalSubjectFilter: &v1.SubjectFilter{ 1536 SubjectType: "user", 1537 OptionalSubjectId: "jeshk", 1538 }, 1539 }, 1540 OptionalLimit: 42, 1541 OptionalCursor: &v1.Cursor{Token: "someinvalidtoken"}, 1542 }) 1543 require.NoError(err) 1544 1545 _, err = stream.Recv() 1546 require.Error(err) 1547 require.ErrorContains(err, "error decoding cursor") 1548 grpcutil.RequireStatus(t, codes.InvalidArgument, err) 1549 } 1550 1551 func readOfType(require *require.Assertions, resourceType string, client v1.PermissionsServiceClient, token *v1.ZedToken) map[string]struct{} { 1552 got := make(map[string]struct{}) 1553 stream, err := client.ReadRelationships(context.Background(), &v1.ReadRelationshipsRequest{ 1554 Consistency: &v1.Consistency{ 1555 Requirement: &v1.Consistency_AtExactSnapshot{ 1556 AtExactSnapshot: token, 1557 }, 1558 }, 1559 RelationshipFilter: &v1.RelationshipFilter{ 1560 ResourceType: resourceType, 1561 }, 1562 }) 1563 require.NoError(err) 1564 1565 for { 1566 rel, err := stream.Recv() 1567 if errors.Is(err, io.EOF) { 1568 break 1569 } 1570 require.NoError(err) 1571 1572 got[tuple.MustRelString(rel.Relationship)] = struct{}{} 1573 } 1574 return got 1575 } 1576 1577 func readAll(require *require.Assertions, client v1.PermissionsServiceClient, token *v1.ZedToken) map[string]struct{} { 1578 got := make(map[string]struct{}) 1579 namespaces := []string{"document", "folder"} 1580 for _, n := range namespaces { 1581 found := readOfType(require, n, client, token) 1582 maps.Copy(got, found) 1583 } 1584 return got 1585 } 1586 1587 func standardTuplesWithout(without map[string]struct{}) map[string]struct{} { 1588 out := make(map[string]struct{}, len(tf.StandardTuples)-len(without)) 1589 for _, t := range tf.StandardTuples { 1590 t = tuple.MustString(tuple.MustParse(t)) 1591 if _, ok := without[t]; ok { 1592 continue 1593 } 1594 out[t] = struct{}{} 1595 } 1596 return out 1597 } 1598 1599 func TestManyConcurrentWriteRelationshipsReturnsSerializationErrorOnMemdb(t *testing.T) { 1600 require := require.New(t) 1601 1602 conn, cleanup, _, _ := testserver.NewTestServer(require, 0, memdb.DisableGC, true, tf.StandardDatastoreWithData) 1603 client := v1.NewPermissionsServiceClient(conn) 1604 t.Cleanup(cleanup) 1605 1606 // Kick off a number of writes to ensure at least one hits an error, as memdb's write throughput 1607 // is limited. 1608 g := errgroup.Group{} 1609 1610 for i := 0; i < 50; i++ { 1611 i := i 1612 g.Go(func() error { 1613 updates := []*v1.RelationshipUpdate{} 1614 for j := 0; j < 500; j++ { 1615 updates = append(updates, &v1.RelationshipUpdate{ 1616 Operation: v1.RelationshipUpdate_OPERATION_CREATE, 1617 Relationship: tuple.MustToRelationship(tuple.MustParse(fmt.Sprintf("document:doc-%d-%d#viewer@user:tom", i, j))), 1618 }) 1619 } 1620 1621 _, err := client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ 1622 Updates: updates, 1623 }) 1624 return err 1625 }) 1626 } 1627 1628 werr := g.Wait() 1629 require.Error(werr) 1630 require.ErrorContains(werr, "serialization max retries exceeded") 1631 grpcutil.RequireStatus(t, codes.DeadlineExceeded, werr) 1632 }