diff --git a/cmd/spicedb/servetesting_integration_test.go b/cmd/spicedb/servetesting_integration_test.go index 3def2ec31c..669b02aeeb 100644 --- a/cmd/spicedb/servetesting_integration_test.go +++ b/cmd/spicedb/servetesting_integration_test.go @@ -82,7 +82,7 @@ func TestTestServer(t *testing.T) { // Try writing a simple relationship against readonly and ensure it fails. _, err = rov1client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ Updates: []*v1.RelationshipUpdate{ - tuple.UpdateToRelationshipUpdate(tuple.Create(relationship)), + tuple.MustUpdateToV1RelationshipUpdate(tuple.Create(relationship)), }, }) require.Equal("rpc error: code = Unavailable desc = service read-only", err.Error()) @@ -90,7 +90,7 @@ func TestTestServer(t *testing.T) { // Write a simple relationship. _, err = v1client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ Updates: []*v1.RelationshipUpdate{ - tuple.UpdateToRelationshipUpdate(tuple.Create(relationship)), + tuple.MustUpdateToV1RelationshipUpdate(tuple.Create(relationship)), }, }) require.NoError(err) diff --git a/e2e/go.mod b/e2e/go.mod index 2ccc06fa2d..a434b5ce0d 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -1,6 +1,6 @@ module github.com/authzed/spicedb/e2e -go 1.22.7 +go 1.23.1 require ( github.com/authzed/authzed-go v0.16.1-0.20241001202507-27cc182a7b92 diff --git a/go.mod b/go.mod index 9408e3b806..b4aefe0f0d 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/authzed/spicedb -go 1.22.7 +go 1.23.1 require ( buf.build/gen/go/prometheus/prometheus/protocolbuffers/go v1.34.2-20240802094132-5b212ab78fb7.2 diff --git a/internal/datasets/subjectsetbyresourceid.go b/internal/datasets/subjectsetbyresourceid.go index 5385d6c305..5b1ba13934 100644 --- a/internal/datasets/subjectsetbyresourceid.go +++ b/internal/datasets/subjectsetbyresourceid.go @@ -3,8 +3,8 @@ package datasets import ( "fmt" - core "github.com/authzed/spicedb/pkg/proto/core/v1" v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" + "github.com/authzed/spicedb/pkg/tuple" ) // NewSubjectSetByResourceID creates and returns a map of subject sets, indexed by resource ID. @@ -34,10 +34,10 @@ func (ssr SubjectSetByResourceID) add(resourceID string, subject *v1.FoundSubjec // AddFromRelationship adds the subject found in the given relationship to this map, indexed at // the resource ID specified in the relationship. -func (ssr SubjectSetByResourceID) AddFromRelationship(relationship *core.RelationTuple) error { - return ssr.add(relationship.ResourceAndRelation.ObjectId, &v1.FoundSubject{ - SubjectId: relationship.Subject.ObjectId, - CaveatExpression: wrapCaveat(relationship.Caveat), +func (ssr SubjectSetByResourceID) AddFromRelationship(relationship tuple.Relationship) error { + return ssr.add(relationship.Resource.ObjectID, &v1.FoundSubject{ + SubjectId: relationship.Subject.ObjectID, + CaveatExpression: wrapCaveat(relationship.OptionalCaveat), }) } diff --git a/internal/datasets/subjectsetbytype.go b/internal/datasets/subjectsetbytype.go index 0ba66cd432..8882a2e435 100644 --- a/internal/datasets/subjectsetbytype.go +++ b/internal/datasets/subjectsetbytype.go @@ -19,24 +19,24 @@ func NewSubjectByTypeSet() *SubjectByTypeSet { } // AddSubjectOf adds the subject found in the given relationship, along with its caveat. -func (s *SubjectByTypeSet) AddSubjectOf(relationship *core.RelationTuple) error { - return s.AddSubject(relationship.Subject, relationship.Caveat) +func (s *SubjectByTypeSet) AddSubjectOf(relationship tuple.Relationship) error { + return s.AddSubject(relationship.Subject, relationship.OptionalCaveat) } // AddConcreteSubject adds a non-caveated subject to the set. -func (s *SubjectByTypeSet) AddConcreteSubject(subject *core.ObjectAndRelation) error { +func (s *SubjectByTypeSet) AddConcreteSubject(subject tuple.ObjectAndRelation) error { return s.AddSubject(subject, nil) } // AddSubject adds the specified subject to the set. -func (s *SubjectByTypeSet) AddSubject(subject *core.ObjectAndRelation, caveat *core.ContextualizedCaveat) error { - key := tuple.JoinRelRef(subject.Namespace, subject.Relation) +func (s *SubjectByTypeSet) AddSubject(subject tuple.ObjectAndRelation, caveat *core.ContextualizedCaveat) error { + key := tuple.JoinRelRef(subject.ObjectType, subject.Relation) if _, ok := s.byType[key]; !ok { s.byType[key] = NewSubjectSet() } return s.byType[key].Add(&v1.FoundSubject{ - SubjectId: subject.ObjectId, + SubjectId: subject.ObjectID, CaveatExpression: wrapCaveat(caveat), }) } diff --git a/internal/datasets/subjectsetbytype_test.go b/internal/datasets/subjectsetbytype_test.go index e3a909a535..a6ccc499c3 100644 --- a/internal/datasets/subjectsetbytype_test.go +++ b/internal/datasets/subjectsetbytype_test.go @@ -41,19 +41,19 @@ func TestSubjectByTypeSet(t *testing.T) { require.True(t, set.IsEmpty()) // Add some concrete subjects. - err := set.AddConcreteSubject(tuple.ParseONR("document:foo#viewer")) + err := set.AddConcreteSubject(tuple.MustParseONR("document:foo#viewer")) require.NoError(t, err) - err = set.AddConcreteSubject(tuple.ParseONR("document:bar#viewer")) + err = set.AddConcreteSubject(tuple.MustParseONR("document:bar#viewer")) require.NoError(t, err) - err = set.AddConcreteSubject(tuple.ParseONR("team:something#member")) + err = set.AddConcreteSubject(tuple.MustParseONR("team:something#member")) require.NoError(t, err) - err = set.AddConcreteSubject(tuple.ParseONR("team:other#member")) + err = set.AddConcreteSubject(tuple.MustParseONR("team:other#member")) require.NoError(t, err) - err = set.AddConcreteSubject(tuple.ParseONR("team:other#manager")) + err = set.AddConcreteSubject(tuple.MustParseONR("team:other#manager")) require.NoError(t, err) // Add a caveated subject. diff --git a/internal/datastore/benchmark/driver_bench_test.go b/internal/datastore/benchmark/driver_bench_test.go index 2988009ef4..b483d853b0 100644 --- a/internal/datastore/benchmark/driver_bench_test.go +++ b/internal/datastore/benchmark/driver_bench_test.go @@ -24,7 +24,6 @@ import ( dsconfig "github.com/authzed/spicedb/pkg/cmd/datastore" "github.com/authzed/spicedb/pkg/datastore" "github.com/authzed/spicedb/pkg/datastore/options" - core "github.com/authzed/spicedb/pkg/proto/core/v1" "github.com/authzed/spicedb/pkg/tuple" ) @@ -80,12 +79,9 @@ func BenchmarkDatastoreDriver(b *testing.B) { // Write a fair amount of data, much more than a functional test for docNum := 0; docNum < numDocuments; docNum++ { _, err := ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { - var updates []*core.RelationTupleUpdate + var updates []tuple.RelationshipUpdate for userNum := 0; userNum < usersPerDoc; userNum++ { - updates = append(updates, &core.RelationTupleUpdate{ - Operation: core.RelationTupleUpdate_CREATE, - Tuple: docViewer(strconv.Itoa(docNum), strconv.Itoa(userNum)), - }) + updates = append(updates, tuple.Create(docViewer(strconv.Itoa(docNum), strconv.Itoa(userNum)))) } return rwt.WriteRelationships(ctx, updates) @@ -110,14 +106,26 @@ func BenchmarkDatastoreDriver(b *testing.B) { }) require.NoError(b, err) var count int - for rel := iter.Next(); rel != nil; rel = iter.Next() { + for _, err := range iter { + require.NoError(b, err) count++ } - require.NoError(b, iter.Err()) - iter.Close() require.Equal(b, usersPerDoc, count) } }) + b.Run("SnapshotReadOnlyNamespace", func(b *testing.B) { + for n := 0; n < b.N; n++ { + iter, err := ds.SnapshotReader(headRev).QueryRelationships(ctx, datastore.RelationshipsFilter{ + OptionalResourceType: testfixtures.DocumentNS.Name, + }) + require.NoError(b, err) + var count int + for _, err := range iter { + require.NoError(b, err) + count++ + } + } + }) b.Run("SortedSnapshotReadOnlyNamespace", func(b *testing.B) { for orderName, order := range sortOrders { order := order @@ -128,11 +136,10 @@ func BenchmarkDatastoreDriver(b *testing.B) { }, options.WithSort(order)) require.NoError(b, err) var count int - for rel := iter.Next(); rel != nil; rel = iter.Next() { + for _, err := range iter { + require.NoError(b, err) count++ } - require.NoError(b, iter.Err()) - iter.Close() } }) } @@ -148,11 +155,10 @@ func BenchmarkDatastoreDriver(b *testing.B) { }, options.WithSort(order)) require.NoError(b, err) var count int - for rel := iter.Next(); rel != nil; rel = iter.Next() { + for _, err := range iter { + require.NoError(b, err) count++ } - require.NoError(b, iter.Err()) - iter.Close() } }) } @@ -170,11 +176,10 @@ func BenchmarkDatastoreDriver(b *testing.B) { }, options.WithSort(order)) require.NoError(b, err) var count int - for rel := iter.Next(); rel != nil; rel = iter.Next() { + for _, err := range iter { + require.NoError(b, err) count++ } - require.NoError(b, iter.Err()) - iter.Close() } }) } @@ -186,15 +191,14 @@ func BenchmarkDatastoreDriver(b *testing.B) { }, options.WithSortForReverse(options.ByResource)) require.NoError(b, err) var count int - for rel := iter.Next(); rel != nil; rel = iter.Next() { + for _, err := range iter { + require.NoError(b, err) count++ } - require.NoError(b, iter.Err()) - iter.Close() } }) - b.Run("Touch", buildTupleTest(ctx, ds, core.RelationTupleUpdate_TOUCH)) - b.Run("Create", buildTupleTest(ctx, ds, core.RelationTupleUpdate_CREATE)) + b.Run("Touch", buildRelTest(ctx, ds, tuple.UpdateOperationTouch)) + b.Run("Create", buildRelTest(ctx, ds, tuple.UpdateOperationCreate)) b.Run("CreateAndTouch", func(b *testing.B) { const totalRelationships = 1000 for _, portionCreate := range []float64{0, 0.10, 0.25, 0.50, 1} { @@ -202,16 +206,16 @@ func BenchmarkDatastoreDriver(b *testing.B) { b.Run(fmt.Sprintf("%v_", portionCreate), func(b *testing.B) { for n := 0; n < b.N; n++ { portionCreateIndex := int(math.Floor(portionCreate * totalRelationships)) - mutations := make([]*core.RelationTupleUpdate, 0, totalRelationships) + mutations := make([]tuple.RelationshipUpdate, 0, totalRelationships) for index := 0; index < totalRelationships; index++ { if index >= portionCreateIndex { stableID := fmt.Sprintf("id-%d", index) - tpl := docViewer(stableID, stableID) - mutations = append(mutations, tuple.Touch(tpl)) + rel := docViewer(stableID, stableID) + mutations = append(mutations, tuple.Touch(rel)) } else { randomID := testfixtures.RandomObjectID(32) - tpl := docViewer(randomID, randomID) - mutations = append(mutations, tuple.Create(tpl)) + rel := docViewer(randomID, randomID) + mutations = append(mutations, tuple.Create(rel)) } } @@ -244,15 +248,15 @@ func TestAllDriversBenchmarkedOrSkipped(t *testing.T) { require.Empty(t, notBenchmarked) } -func buildTupleTest(ctx context.Context, ds datastore.Datastore, op core.RelationTupleUpdate_Operation) func(b *testing.B) { +func buildRelTest(ctx context.Context, ds datastore.Datastore, op tuple.UpdateOperation) func(b *testing.B) { return func(b *testing.B) { for n := 0; n < b.N; n++ { _, err := ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { randomID := testfixtures.RandomObjectID(32) - return rwt.WriteRelationships(ctx, []*core.RelationTupleUpdate{ + return rwt.WriteRelationships(ctx, []tuple.RelationshipUpdate{ { - Operation: op, - Tuple: docViewer(randomID, randomID), + Operation: op, + Relationship: docViewer(randomID, randomID), }, }) }) @@ -261,17 +265,19 @@ func buildTupleTest(ctx context.Context, ds datastore.Datastore, op core.Relatio } } -func docViewer(documentID, userID string) *core.RelationTuple { - return &core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: testfixtures.DocumentNS.Name, - ObjectId: documentID, - Relation: "viewer", - }, - Subject: &core.ObjectAndRelation{ - Namespace: testfixtures.UserNS.Name, - ObjectId: userID, - Relation: datastore.Ellipsis, +func docViewer(documentID, userID string) tuple.Relationship { + return tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.ObjectAndRelation{ + ObjectType: testfixtures.DocumentNS.Name, + ObjectID: documentID, + Relation: "viewer", + }, + Subject: tuple.ObjectAndRelation{ + ObjectType: testfixtures.UserNS.Name, + ObjectID: userID, + Relation: datastore.Ellipsis, + }, }, } } diff --git a/internal/datastore/common/changes.go b/internal/datastore/common/changes.go index 1cc18663fb..be6e34206b 100644 --- a/internal/datastore/common/changes.go +++ b/internal/datastore/common/changes.go @@ -33,8 +33,8 @@ type Changes[R datastore.Revision, K comparable] struct { type changeRecord[R datastore.Revision] struct { rev R - tupleTouches map[string]*core.RelationTuple - tupleDeletes map[string]*core.RelationTuple + relTouches map[string]tuple.Relationship + relDeletes map[string]tuple.Relationship definitionsChanged map[string]datastore.SchemaDefinition namespacesDeleted map[string]struct{} caveatsDeleted map[string]struct{} @@ -61,8 +61,8 @@ func (ch *Changes[R, K]) IsEmpty() bool { func (ch *Changes[R, K]) AddRelationshipChange( ctx context.Context, rev R, - tpl *core.RelationTuple, - op core.RelationTupleUpdate_Operation, + rel tuple.Relationship, + op tuple.UpdateOperation, ) error { if ch.content&datastore.WatchRelationships != datastore.WatchRelationships { return nil @@ -73,37 +73,37 @@ func (ch *Changes[R, K]) AddRelationshipChange( return err } - tplKey := tuple.StringWithoutCaveat(tpl) + key := tuple.StringWithoutCaveat(rel) switch op { - case core.RelationTupleUpdate_TOUCH: + case tuple.UpdateOperationTouch: // If there was a delete for the same tuple at the same revision, drop it - existing, ok := record.tupleDeletes[tplKey] + existing, ok := record.relDeletes[key] if ok { - delete(record.tupleDeletes, tplKey) + delete(record.relDeletes, key) if err := ch.adjustByteSize(existing, -1); err != nil { return err } } - record.tupleTouches[tplKey] = tpl - if err := ch.adjustByteSize(tpl, 1); err != nil { + record.relTouches[key] = rel + if err := ch.adjustByteSize(rel, 1); err != nil { return err } - case core.RelationTupleUpdate_DELETE: - _, alreadyTouched := record.tupleTouches[tplKey] + case tuple.UpdateOperationDelete: + _, alreadyTouched := record.relTouches[key] if !alreadyTouched { - record.tupleDeletes[tplKey] = tpl - if err := ch.adjustByteSize(tpl, 1); err != nil { + record.relDeletes[key] = rel + if err := ch.adjustByteSize(rel, 1); err != nil { return err } } default: - log.Ctx(ctx).Warn().Stringer("operation", op).Msg("unknown change operation") return spiceerrors.MustBugf("unknown change operation") } + return nil } @@ -159,8 +159,8 @@ func (ch *Changes[R, K]) recordForRevision(rev R) (changeRecord[R], error) { if !ok { revisionChanges = changeRecord[R]{ rev, - make(map[string]*core.RelationTuple), - make(map[string]*core.RelationTuple), + make(map[string]tuple.Relationship), + make(map[string]tuple.Relationship), make(map[string]datastore.SchemaDefinition), make(map[string]struct{}), make(map[string]struct{}), @@ -310,17 +310,11 @@ func (ch *Changes[R, K]) revisionChanges(lessThanFunc func(lhs, rhs K) bool, bou for i, k := range revisionsWithChanges { revisionChangeRecord := ch.records[k] changes[i].Revision = revisionChangeRecord.rev - for _, tpl := range revisionChangeRecord.tupleTouches { - changes[i].RelationshipChanges = append(changes[i].RelationshipChanges, &core.RelationTupleUpdate{ - Operation: core.RelationTupleUpdate_TOUCH, - Tuple: tpl, - }) + for _, rel := range revisionChangeRecord.relTouches { + changes[i].RelationshipChanges = append(changes[i].RelationshipChanges, tuple.Touch(rel)) } - for _, tpl := range revisionChangeRecord.tupleDeletes { - changes[i].RelationshipChanges = append(changes[i].RelationshipChanges, &core.RelationTupleUpdate{ - Operation: core.RelationTupleUpdate_DELETE, - Tuple: tpl, - }) + for _, rel := range revisionChangeRecord.relDeletes { + changes[i].RelationshipChanges = append(changes[i].RelationshipChanges, tuple.Delete(rel)) } changes[i].ChangedDefinitions = maps.Values(revisionChangeRecord.definitionsChanged) changes[i].DeletedNamespaces = maps.Keys(revisionChangeRecord.namespacesDeleted) diff --git a/internal/datastore/common/changes_test.go b/internal/datastore/common/changes_test.go index 7b16ab3e46..8cbc09548a 100644 --- a/internal/datastore/common/changes_test.go +++ b/internal/datastore/common/changes_test.go @@ -32,7 +32,7 @@ func TestChanges(t *testing.T) { type changeEntry struct { revision uint64 relationship string - op core.RelationTupleUpdate_Operation + op tuple.UpdateOperation deletedNamespaces []string deletedCaveats []string changedDefinitions []datastore.SchemaDefinition @@ -82,108 +82,108 @@ func TestChanges(t *testing.T) { { "create", []changeEntry{ - {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, + {1, tuple1, tuple.UpdateOperationTouch, nil, nil, nil}, }, []datastore.RevisionChanges{ - {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1)}}, + {Revision: rev1, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1)}}, }, }, { "delete", []changeEntry{ - {1, tuple1, core.RelationTupleUpdate_DELETE, nil, nil, nil}, + {1, tuple1, tuple.UpdateOperationDelete, nil, nil, nil}, }, []datastore.RevisionChanges{ - {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{del(tuple1)}}, + {Revision: rev1, RelationshipChanges: []tuple.RelationshipUpdate{del(tuple1)}}, }, }, { "in-order touch", []changeEntry{ - {1, tuple1, core.RelationTupleUpdate_DELETE, nil, nil, nil}, - {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, + {1, tuple1, tuple.UpdateOperationDelete, nil, nil, nil}, + {1, tuple1, tuple.UpdateOperationTouch, nil, nil, nil}, }, []datastore.RevisionChanges{ - {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1)}}, + {Revision: rev1, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1)}}, }, }, { "reverse-order touch", []changeEntry{ - {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, - {1, tuple1, core.RelationTupleUpdate_DELETE, nil, nil, nil}, + {1, tuple1, tuple.UpdateOperationTouch, nil, nil, nil}, + {1, tuple1, tuple.UpdateOperationDelete, nil, nil, nil}, }, []datastore.RevisionChanges{ - {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1)}}, + {Revision: rev1, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1)}}, }, }, { "create and delete", []changeEntry{ - {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, - {1, tuple2, core.RelationTupleUpdate_DELETE, nil, nil, nil}, + {1, tuple1, tuple.UpdateOperationTouch, nil, nil, nil}, + {1, tuple2, tuple.UpdateOperationDelete, nil, nil, nil}, }, []datastore.RevisionChanges{ - {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), del(tuple2)}}, + {Revision: rev1, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1), del(tuple2)}}, }, }, { "multiple creates", []changeEntry{ - {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, - {1, tuple2, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, + {1, tuple1, tuple.UpdateOperationTouch, nil, nil, nil}, + {1, tuple2, tuple.UpdateOperationTouch, nil, nil, nil}, }, []datastore.RevisionChanges{ - {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), touch(tuple2)}}, + {Revision: rev1, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1), touch(tuple2)}}, }, }, { "duplicates", []changeEntry{ - {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, - {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, - {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, - {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, - {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, + {1, tuple1, tuple.UpdateOperationTouch, nil, nil, nil}, + {1, tuple1, tuple.UpdateOperationTouch, nil, nil, nil}, + {1, tuple1, tuple.UpdateOperationTouch, nil, nil, nil}, + {1, tuple1, tuple.UpdateOperationTouch, nil, nil, nil}, + {1, tuple1, tuple.UpdateOperationTouch, nil, nil, nil}, }, []datastore.RevisionChanges{ - {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1)}}, + {Revision: rev1, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1)}}, }, }, { "create then touch", []changeEntry{ - {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, - {2, tuple1, core.RelationTupleUpdate_DELETE, nil, nil, nil}, - {2, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, + {1, tuple1, tuple.UpdateOperationTouch, nil, nil, nil}, + {2, tuple1, tuple.UpdateOperationDelete, nil, nil, nil}, + {2, tuple1, tuple.UpdateOperationTouch, nil, nil, nil}, }, []datastore.RevisionChanges{ - {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1)}}, - {Revision: rev2, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1)}}, + {Revision: rev1, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1)}}, + {Revision: rev2, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1)}}, }, }, { "big revision gap", []changeEntry{ - {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, - {1_000_000, tuple1, core.RelationTupleUpdate_DELETE, nil, nil, nil}, - {1_000_000, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, + {1, tuple1, tuple.UpdateOperationTouch, nil, nil, nil}, + {1_000_000, tuple1, tuple.UpdateOperationDelete, nil, nil, nil}, + {1_000_000, tuple1, tuple.UpdateOperationTouch, nil, nil, nil}, }, []datastore.RevisionChanges{ - {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1)}}, - {Revision: revOneMillion, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1)}}, + {Revision: rev1, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1)}}, + {Revision: revOneMillion, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1)}}, }, }, { "out of order", []changeEntry{ - {1_000_000, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, - {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, - {1_000_000, tuple1, core.RelationTupleUpdate_DELETE, nil, nil, nil}, + {1_000_000, tuple1, tuple.UpdateOperationTouch, nil, nil, nil}, + {1, tuple1, tuple.UpdateOperationTouch, nil, nil, nil}, + {1_000_000, tuple1, tuple.UpdateOperationDelete, nil, nil, nil}, }, []datastore.RevisionChanges{ - {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1)}}, - {Revision: revOneMillion, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1)}}, + {Revision: rev1, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1)}}, + {Revision: revOneMillion, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1)}}, }, }, { @@ -255,46 +255,46 @@ func TestChanges(t *testing.T) { { "kitchen sink relationships", []changeEntry{ - {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, - {2, tuple1, core.RelationTupleUpdate_DELETE, nil, nil, nil}, - {1_000_000, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, + {1, tuple1, tuple.UpdateOperationTouch, nil, nil, nil}, + {2, tuple1, tuple.UpdateOperationDelete, nil, nil, nil}, + {1_000_000, tuple1, tuple.UpdateOperationTouch, nil, nil, nil}, - {1, tuple2, core.RelationTupleUpdate_DELETE, nil, nil, nil}, - {2, tuple2, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, - {1_000_000, tuple2, core.RelationTupleUpdate_DELETE, nil, nil, nil}, - {1_000_000, tuple2, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, + {1, tuple2, tuple.UpdateOperationDelete, nil, nil, nil}, + {2, tuple2, tuple.UpdateOperationTouch, nil, nil, nil}, + {1_000_000, tuple2, tuple.UpdateOperationDelete, nil, nil, nil}, + {1_000_000, tuple2, tuple.UpdateOperationTouch, nil, nil, nil}, }, []datastore.RevisionChanges{ - {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), del(tuple2)}}, - {Revision: rev2, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple2), del(tuple1)}}, - {Revision: revOneMillion, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), touch(tuple2)}}, + {Revision: rev1, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1), del(tuple2)}}, + {Revision: rev2, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple2), del(tuple1)}}, + {Revision: revOneMillion, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1), touch(tuple2)}}, }, }, { "kitchen sink", []changeEntry{ - {1, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, - {2, tuple1, core.RelationTupleUpdate_DELETE, nil, nil, nil}, - {1_000_000, tuple1, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, + {1, tuple1, tuple.UpdateOperationTouch, nil, nil, nil}, + {2, tuple1, tuple.UpdateOperationDelete, nil, nil, nil}, + {1_000_000, tuple1, tuple.UpdateOperationTouch, nil, nil, nil}, {1_000_001, "", 0, []string{"deletednamespace"}, nil, nil}, {3, "", 0, nil, nil, []datastore.SchemaDefinition{ &core.NamespaceDefinition{Name: "midns"}, }}, - {1, tuple2, core.RelationTupleUpdate_DELETE, nil, nil, nil}, - {2, tuple2, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, - {1_000_000, tuple2, core.RelationTupleUpdate_DELETE, nil, nil, nil}, - {1_000_000, tuple2, core.RelationTupleUpdate_TOUCH, nil, nil, nil}, + {1, tuple2, tuple.UpdateOperationDelete, nil, nil, nil}, + {2, tuple2, tuple.UpdateOperationTouch, nil, nil, nil}, + {1_000_000, tuple2, tuple.UpdateOperationDelete, nil, nil, nil}, + {1_000_000, tuple2, tuple.UpdateOperationTouch, nil, nil, nil}, {1_000_001, "", 0, nil, []string{"deletedcaveat"}, nil}, }, []datastore.RevisionChanges{ - {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), del(tuple2)}}, - {Revision: rev2, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple2), del(tuple1)}}, + {Revision: rev1, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1), del(tuple2)}}, + {Revision: rev2, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple2), del(tuple1)}}, {Revision: rev3, ChangedDefinitions: []datastore.SchemaDefinition{ &core.NamespaceDefinition{Name: "midns"}, }}, - {Revision: revOneMillion, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), touch(tuple2)}}, + {Revision: revOneMillion, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1), touch(tuple2)}}, {Revision: revOneMillionOne, DeletedNamespaces: []string{"deletednamespace"}, DeletedCaveats: []string{"deletedcaveat"}}, }, }, @@ -346,7 +346,7 @@ func TestFilteredSchemaChanges(t *testing.T) { ch := NewChanges(revisions.TransactionIDKeyFunc, datastore.WatchSchema, 0) require.True(t, ch.IsEmpty()) - require.NoError(t, ch.AddRelationshipChange(ctx, rev1, tuple.MustParse("document:firstdoc#viewer@user:tom"), core.RelationTupleUpdate_TOUCH)) + require.NoError(t, ch.AddRelationshipChange(ctx, rev1, tuple.MustParse("document:firstdoc#viewer@user:tom"), tuple.UpdateOperationTouch)) require.True(t, ch.IsEmpty()) } @@ -450,10 +450,10 @@ func TestHLCOrdering(t *testing.T) { rev0, err := revisions.HLCRevisionFromString("1") require.NoError(t, err) - err = ch.AddRelationshipChange(ctx, rev1, tuple.MustParse("document:foo#viewer@user:tom"), core.RelationTupleUpdate_DELETE) + err = ch.AddRelationshipChange(ctx, rev1, tuple.MustParse("document:foo#viewer@user:tom"), tuple.UpdateOperationDelete) require.NoError(t, err) - err = ch.AddRelationshipChange(ctx, rev0, tuple.MustParse("document:foo#viewer@user:tom"), core.RelationTupleUpdate_TOUCH) + err = ch.AddRelationshipChange(ctx, rev0, tuple.MustParse("document:foo#viewer@user:tom"), tuple.UpdateOperationTouch) require.NoError(t, err) remaining, err := ch.AsRevisionChanges(revisions.HLCKeyLessThanFunc) @@ -463,7 +463,7 @@ func TestHLCOrdering(t *testing.T) { require.Equal(t, []datastore.RevisionChanges{ { Revision: rev0, - RelationshipChanges: []*core.RelationTupleUpdate{ + RelationshipChanges: []tuple.RelationshipUpdate{ tuple.Touch(tuple.MustParse("document:foo#viewer@user:tom")), }, DeletedNamespaces: []string{}, @@ -472,7 +472,7 @@ func TestHLCOrdering(t *testing.T) { }, { Revision: rev1, - RelationshipChanges: []*core.RelationTupleUpdate{ + RelationshipChanges: []tuple.RelationshipUpdate{ tuple.Delete(tuple.MustParse("document:foo#viewer@user:tom")), }, DeletedNamespaces: []string{}, @@ -494,29 +494,29 @@ func TestHLCSameRevision(t *testing.T) { rev0again, err := revisions.HLCRevisionFromString("1") require.NoError(t, err) - err = ch.AddRelationshipChange(ctx, rev0, tuple.MustParse("document:foo#viewer@user:tom"), core.RelationTupleUpdate_TOUCH) + err = ch.AddRelationshipChange(ctx, rev0, tuple.MustParse("document:foo#viewer@user:tom"), tuple.UpdateOperationTouch) require.NoError(t, err) - err = ch.AddRelationshipChange(ctx, rev0again, tuple.MustParse("document:foo#viewer@user:sarah"), core.RelationTupleUpdate_TOUCH) + err = ch.AddRelationshipChange(ctx, rev0again, tuple.MustParse("document:foo#viewer@user:sarah"), tuple.UpdateOperationTouch) require.NoError(t, err) remaining, err := ch.AsRevisionChanges(revisions.HLCKeyLessThanFunc) require.NoError(t, err) require.Equal(t, 1, len(remaining)) - expected := []*core.RelationTupleUpdate{ + expected := []tuple.RelationshipUpdate{ tuple.Touch(tuple.MustParse("document:foo#viewer@user:tom")), tuple.Touch(tuple.MustParse("document:foo#viewer@user:sarah")), } - slices.SortFunc(expected, func(i, j *core.RelationTupleUpdate) int { - iStr := tuple.StringWithoutCaveat(i.Tuple) - jStr := tuple.StringWithoutCaveat(j.Tuple) + slices.SortFunc(expected, func(i, j tuple.RelationshipUpdate) int { + iStr := tuple.StringWithoutCaveat(i.Relationship) + jStr := tuple.StringWithoutCaveat(j.Relationship) return strings.Compare(iStr, jStr) }) - slices.SortFunc(remaining[0].RelationshipChanges, func(i, j *core.RelationTupleUpdate) int { - iStr := tuple.StringWithoutCaveat(i.Tuple) - jStr := tuple.StringWithoutCaveat(j.Tuple) + slices.SortFunc(remaining[0].RelationshipChanges, func(i, j tuple.RelationshipUpdate) int { + iStr := tuple.StringWithoutCaveat(i.Relationship) + jStr := tuple.StringWithoutCaveat(j.Relationship) return strings.Compare(iStr, jStr) }) @@ -534,7 +534,7 @@ func TestHLCSameRevision(t *testing.T) { func TestMaximumSize(t *testing.T) { ctx := context.Background() - ch := NewChanges(revisions.HLCKeyFunc, datastore.WatchRelationships|datastore.WatchSchema, 150) + ch := NewChanges(revisions.HLCKeyFunc, datastore.WatchRelationships|datastore.WatchSchema, 939) require.True(t, ch.IsEmpty()) rev0, err := revisions.HLCRevisionFromString("1") @@ -549,36 +549,36 @@ func TestMaximumSize(t *testing.T) { rev3, err := revisions.HLCRevisionFromString("4") require.NoError(t, err) - err = ch.AddRelationshipChange(ctx, rev0, tuple.MustParse("document:foo#viewer@user:tom"), core.RelationTupleUpdate_TOUCH) + err = ch.AddRelationshipChange(ctx, rev0, tuple.MustParse("document:foo#viewer@user:tom"), tuple.UpdateOperationTouch) require.NoError(t, err) - err = ch.AddRelationshipChange(ctx, rev1, tuple.MustParse("document:foo#viewer@user:tom"), core.RelationTupleUpdate_TOUCH) + err = ch.AddRelationshipChange(ctx, rev1, tuple.MustParse("document:foo#viewer@user:tom"), tuple.UpdateOperationTouch) require.NoError(t, err) - err = ch.AddRelationshipChange(ctx, rev2, tuple.MustParse("document:foo#viewer@user:tom"), core.RelationTupleUpdate_TOUCH) + err = ch.AddRelationshipChange(ctx, rev2, tuple.MustParse("document:foo#viewer@user:tom"), tuple.UpdateOperationTouch) require.NoError(t, err) - err = ch.AddRelationshipChange(ctx, rev3, tuple.MustParse("document:foo#viewer@user:tom"), core.RelationTupleUpdate_TOUCH) + err = ch.AddRelationshipChange(ctx, rev3, tuple.MustParse("document:foo#viewer@user:tom"), tuple.UpdateOperationTouch) require.Error(t, err) - require.ErrorContains(t, err, "maximum changes byte size of 150 exceeded") + require.ErrorContains(t, err, "maximum changes byte size of 939 exceeded") } func TestMaximumSizeReplacement(t *testing.T) { ctx := context.Background() - ch := NewChanges(revisions.HLCKeyFunc, datastore.WatchRelationships|datastore.WatchSchema, 43) + ch := NewChanges(revisions.HLCKeyFunc, datastore.WatchRelationships|datastore.WatchSchema, 235) require.True(t, ch.IsEmpty()) rev0, err := revisions.HLCRevisionFromString("1") require.NoError(t, err) - err = ch.AddRelationshipChange(ctx, rev0, tuple.MustParse("document:foo#viewer@user:tom"), core.RelationTupleUpdate_TOUCH) + err = ch.AddRelationshipChange(ctx, rev0, tuple.MustParse("document:foo#viewer@user:tom"), tuple.UpdateOperationTouch) require.NoError(t, err) - require.Equal(t, int64(43), ch.currentByteSize) + require.Equal(t, int64(235), ch.currentByteSize) - err = ch.AddRelationshipChange(ctx, rev0, tuple.MustParse("document:foo#viewer@user:tom"), core.RelationTupleUpdate_DELETE) + err = ch.AddRelationshipChange(ctx, rev0, tuple.MustParse("document:foo#viewer@user:tom"), tuple.UpdateOperationDelete) require.NoError(t, err) - require.Equal(t, int64(43), ch.currentByteSize) + require.Equal(t, int64(235), ch.currentByteSize) } func TestCanonicalize(t *testing.T) { @@ -594,63 +594,63 @@ func TestCanonicalize(t *testing.T) { { "single entries", []datastore.RevisionChanges{ - {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1)}}, + {Revision: rev1, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1)}}, }, []datastore.RevisionChanges{ - {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1)}}, + {Revision: rev1, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1)}}, }, }, { "tuples out of order", []datastore.RevisionChanges{ - {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{del(tuple2), touch(tuple1)}}, + {Revision: rev1, RelationshipChanges: []tuple.RelationshipUpdate{del(tuple2), touch(tuple1)}}, }, []datastore.RevisionChanges{ - {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), del(tuple2)}}, + {Revision: rev1, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1), del(tuple2)}}, }, }, { "operations out of order", []datastore.RevisionChanges{ - {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{del(tuple1), touch(tuple1)}}, + {Revision: rev1, RelationshipChanges: []tuple.RelationshipUpdate{del(tuple1), touch(tuple1)}}, }, []datastore.RevisionChanges{ - {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), del(tuple1)}}, + {Revision: rev1, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1), del(tuple1)}}, }, }, { "equal entries", []datastore.RevisionChanges{ - {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), touch(tuple1)}}, + {Revision: rev1, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1), touch(tuple1)}}, }, []datastore.RevisionChanges{ - {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), touch(tuple1)}}, + {Revision: rev1, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1), touch(tuple1)}}, }, }, { "already canonical", []datastore.RevisionChanges{ - {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), del(tuple2)}}, - {Revision: rev2, RelationshipChanges: []*core.RelationTupleUpdate{del(tuple1), touch(tuple2)}}, - {Revision: revOneMillion, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), touch(tuple2)}}, + {Revision: rev1, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1), del(tuple2)}}, + {Revision: rev2, RelationshipChanges: []tuple.RelationshipUpdate{del(tuple1), touch(tuple2)}}, + {Revision: revOneMillion, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1), touch(tuple2)}}, }, []datastore.RevisionChanges{ - {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), del(tuple2)}}, - {Revision: rev2, RelationshipChanges: []*core.RelationTupleUpdate{del(tuple1), touch(tuple2)}}, - {Revision: revOneMillion, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), touch(tuple2)}}, + {Revision: rev1, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1), del(tuple2)}}, + {Revision: rev2, RelationshipChanges: []tuple.RelationshipUpdate{del(tuple1), touch(tuple2)}}, + {Revision: revOneMillion, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1), touch(tuple2)}}, }, }, { "revisions allowed out of order", []datastore.RevisionChanges{ - {Revision: revOneMillion, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), touch(tuple2)}}, - {Revision: rev2, RelationshipChanges: []*core.RelationTupleUpdate{del(tuple1), touch(tuple2)}}, - {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), del(tuple2)}}, + {Revision: revOneMillion, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1), touch(tuple2)}}, + {Revision: rev2, RelationshipChanges: []tuple.RelationshipUpdate{del(tuple1), touch(tuple2)}}, + {Revision: rev1, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1), del(tuple2)}}, }, []datastore.RevisionChanges{ - {Revision: revOneMillion, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), touch(tuple2)}}, - {Revision: rev2, RelationshipChanges: []*core.RelationTupleUpdate{del(tuple1), touch(tuple2)}}, - {Revision: rev1, RelationshipChanges: []*core.RelationTupleUpdate{touch(tuple1), del(tuple2)}}, + {Revision: revOneMillion, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1), touch(tuple2)}}, + {Revision: rev2, RelationshipChanges: []tuple.RelationshipUpdate{del(tuple1), touch(tuple2)}}, + {Revision: rev1, RelationshipChanges: []tuple.RelationshipUpdate{touch(tuple1), del(tuple2)}}, }, }, } @@ -664,31 +664,25 @@ func TestCanonicalize(t *testing.T) { } } -func touch(relationship string) *core.RelationTupleUpdate { - return &core.RelationTupleUpdate{ - Operation: core.RelationTupleUpdate_TOUCH, - Tuple: tuple.MustParse(relationship), - } +func touch(relationship string) tuple.RelationshipUpdate { + return tuple.Touch(tuple.MustParse(relationship)) } -func del(relationship string) *core.RelationTupleUpdate { - return &core.RelationTupleUpdate{ - Operation: core.RelationTupleUpdate_DELETE, - Tuple: tuple.MustParse(relationship), - } +func del(relationship string) tuple.RelationshipUpdate { + return tuple.Delete(tuple.MustParse(relationship)) } func canonicalize(in []datastore.RevisionChanges) []datastore.RevisionChanges { out := make([]datastore.RevisionChanges, 0, len(in)) for _, rev := range in { - outChanges := make([]*core.RelationTupleUpdate, 0, len(rev.RelationshipChanges)) + outChanges := make([]tuple.RelationshipUpdate, 0, len(rev.RelationshipChanges)) outChanges = append(outChanges, rev.RelationshipChanges...) sort.Slice(outChanges, func(i, j int) bool { // Return if i < j left, right := outChanges[i], outChanges[j] - tupleCompareResult := strings.Compare(tuple.StringWithoutCaveat(left.Tuple), tuple.StringWithoutCaveat(right.Tuple)) + tupleCompareResult := strings.Compare(tuple.StringWithoutCaveat(left.Relationship), tuple.StringWithoutCaveat(right.Relationship)) if tupleCompareResult < 0 { return true } diff --git a/internal/datastore/common/errors.go b/internal/datastore/common/errors.go index 5182fa7a58..dd47fb1f52 100644 --- a/internal/datastore/common/errors.go +++ b/internal/datastore/common/errors.go @@ -13,7 +13,6 @@ import ( v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" log "github.com/authzed/spicedb/internal/logging" - core "github.com/authzed/spicedb/pkg/proto/core/v1" "github.com/authzed/spicedb/pkg/spiceerrors" "github.com/authzed/spicedb/pkg/tuple" ) @@ -75,7 +74,7 @@ type CreateRelationshipExistsError struct { error // Relationship is the relationship that caused the error. May be nil, depending on the datastore. - Relationship *core.RelationTuple + Relationship *tuple.Relationship } // GRPCStatus implements retrieving the gRPC status for the error. @@ -91,14 +90,14 @@ func (err CreateRelationshipExistsError) GRPCStatus() *status.Status { ) } - relationship := tuple.ToRelationship(err.Relationship) + relationship := tuple.ToV1Relationship(*err.Relationship) return spiceerrors.WithCodeAndDetails( err, codes.AlreadyExists, spiceerrors.ForReason( v1.ErrorReason_ERROR_REASON_ATTEMPT_TO_RECREATE_RELATIONSHIP, map[string]string{ - "relationship": tuple.StringRelationshipWithoutCaveat(relationship), + "relationship": tuple.V1StringRelationshipWithoutCaveat(relationship), "resource_type": relationship.Resource.ObjectType, "resource_object_id": relationship.Resource.ObjectId, "resource_relation": relationship.Relation, @@ -111,10 +110,10 @@ func (err CreateRelationshipExistsError) GRPCStatus() *status.Status { } // NewCreateRelationshipExistsError creates a new CreateRelationshipExistsError. -func NewCreateRelationshipExistsError(relationship *core.RelationTuple) error { +func NewCreateRelationshipExistsError(relationship *tuple.Relationship) error { msg := "could not CREATE one or more relationships, as they already existed. If this is persistent, please switch to TOUCH operations or specify a precondition" if relationship != nil { - msg = fmt.Sprintf("could not CREATE relationship `%s`, as it already existed. If this is persistent, please switch to TOUCH operations or specify a precondition", tuple.StringWithoutCaveat(relationship)) + msg = fmt.Sprintf("could not CREATE relationship `%s`, as it already existed. If this is persistent, please switch to TOUCH operations or specify a precondition", tuple.StringWithoutCaveat(*relationship)) } return CreateRelationshipExistsError{ diff --git a/internal/datastore/common/helpers.go b/internal/datastore/common/helpers.go index 0d8c87001f..8f341340a1 100644 --- a/internal/datastore/common/helpers.go +++ b/internal/datastore/common/helpers.go @@ -8,23 +8,24 @@ import ( "github.com/authzed/spicedb/pkg/datastore" core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/tuple" ) -// WriteTuples is a convenience method to perform the same update operation on a set of tuples -func WriteTuples(ctx context.Context, ds datastore.Datastore, op core.RelationTupleUpdate_Operation, tuples ...*core.RelationTuple) (datastore.Revision, error) { - updates := make([]*core.RelationTupleUpdate, 0, len(tuples)) - for _, tpl := range tuples { - rtu := &core.RelationTupleUpdate{ - Operation: op, - Tuple: tpl, +// WriteRelationships is a convenience method to perform the same update operation on a set of relationships +func WriteRelationships(ctx context.Context, ds datastore.Datastore, op tuple.UpdateOperation, rels ...tuple.Relationship) (datastore.Revision, error) { + updates := make([]tuple.RelationshipUpdate, 0, len(rels)) + for _, rel := range rels { + ru := tuple.RelationshipUpdate{ + Operation: op, + Relationship: rel, } - updates = append(updates, rtu) + updates = append(updates, ru) } - return UpdateTuplesInDatastore(ctx, ds, updates...) + return UpdateRelationshipsInDatastore(ctx, ds, updates...) } -// UpdateTuplesInDatastore is a convenience method to perform multiple relation update operations on a Datastore -func UpdateTuplesInDatastore(ctx context.Context, ds datastore.Datastore, updates ...*core.RelationTupleUpdate) (datastore.Revision, error) { +// UpdateRelationshipsInDatastore is a convenience method to perform multiple relation update operations on a Datastore +func UpdateRelationshipsInDatastore(ctx context.Context, ds datastore.Datastore, updates ...tuple.RelationshipUpdate) (datastore.Revision, error) { return ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { return rwt.WriteRelationships(ctx, updates) }) diff --git a/internal/datastore/common/sql.go b/internal/datastore/common/sql.go index f762999c05..536de9385f 100644 --- a/internal/datastore/common/sql.go +++ b/internal/datastore/common/sql.go @@ -14,7 +14,6 @@ import ( log "github.com/authzed/spicedb/internal/logging" "github.com/authzed/spicedb/pkg/datastore" "github.com/authzed/spicedb/pkg/datastore/options" - core "github.com/authzed/spicedb/pkg/proto/core/v1" "github.com/authzed/spicedb/pkg/spiceerrors" ) @@ -156,24 +155,26 @@ type nameAndValue struct { value string } -func (sqf SchemaQueryFilterer) After(cursor *core.RelationTuple, order options.SortOrder) SchemaQueryFilterer { +func (sqf SchemaQueryFilterer) After(cursor options.Cursor, order options.SortOrder) SchemaQueryFilterer { + spiceerrors.DebugAssertNotNil(cursor, "cursor cannot be nil") + // NOTE: The ordering of these columns can affect query performance, be aware when changing. columnsAndValues := map[options.SortOrder][]nameAndValue{ options.ByResource: { { - sqf.schema.colNamespace, cursor.ResourceAndRelation.Namespace, + sqf.schema.colNamespace, cursor.Resource.ObjectType, }, { - sqf.schema.colObjectID, cursor.ResourceAndRelation.ObjectId, + sqf.schema.colObjectID, cursor.Resource.ObjectID, }, { - sqf.schema.colRelation, cursor.ResourceAndRelation.Relation, + sqf.schema.colRelation, cursor.Resource.Relation, }, { - sqf.schema.colUsersetNamespace, cursor.Subject.Namespace, + sqf.schema.colUsersetNamespace, cursor.Subject.ObjectType, }, { - sqf.schema.colUsersetObjectID, cursor.Subject.ObjectId, + sqf.schema.colUsersetObjectID, cursor.Subject.ObjectID, }, { sqf.schema.colUsersetRelation, cursor.Subject.Relation, @@ -181,19 +182,19 @@ func (sqf SchemaQueryFilterer) After(cursor *core.RelationTuple, order options.S }, options.BySubject: { { - sqf.schema.colUsersetNamespace, cursor.Subject.Namespace, + sqf.schema.colUsersetNamespace, cursor.Subject.ObjectType, }, { - sqf.schema.colUsersetObjectID, cursor.Subject.ObjectId, + sqf.schema.colUsersetObjectID, cursor.Subject.ObjectID, }, { - sqf.schema.colNamespace, cursor.ResourceAndRelation.Namespace, + sqf.schema.colNamespace, cursor.Resource.ObjectType, }, { - sqf.schema.colObjectID, cursor.ResourceAndRelation.ObjectId, + sqf.schema.colObjectID, cursor.Resource.ObjectID, }, { - sqf.schema.colRelation, cursor.ResourceAndRelation.Relation, + sqf.schema.colRelation, cursor.Resource.Relation, }, { sqf.schema.colUsersetRelation, cursor.Subject.Relation, @@ -567,21 +568,11 @@ func (tqs QueryExecutor) ExecuteQuery( return nil, err } - queryTuples, err := tqs.Executor(ctx, sql, args) - if err != nil { - return nil, err - } - - lenQueryTuples := uint64(len(queryTuples)) - if lenQueryTuples > limit { - queryTuples = queryTuples[:limit] - } - - return NewSliceRelationshipIterator(queryTuples, queryOpts.Sort), nil + return tqs.Executor(ctx, sql, args) } // ExecuteQueryFunc is a function that can be used to execute a single rendered SQL query. -type ExecuteQueryFunc func(ctx context.Context, sql string, args []any) ([]*core.RelationTuple, error) +type ExecuteQueryFunc func(ctx context.Context, sql string, args []any) (datastore.RelationshipIterator, error) // TxCleanupFunc is a function that should be executed when the caller of // TransactionFactory is done with the transaction. diff --git a/internal/datastore/common/sql_test.go b/internal/datastore/common/sql_test.go index 8edf06b9d8..e61339f2f6 100644 --- a/internal/datastore/common/sql_test.go +++ b/internal/datastore/common/sql_test.go @@ -16,6 +16,8 @@ import ( "github.com/authzed/spicedb/pkg/datastore" ) +var toCursor = options.ToCursor + func TestSchemaQueryFilterer(t *testing.T) { tests := []struct { name string @@ -473,7 +475,7 @@ func TestSchemaQueryFilterer(t *testing.T) { datastore.RelationshipsFilter{ OptionalResourceType: "someresourcetype", }, - ).After(tuple.MustParse("someresourcetype:foo#viewer@user:bar"), options.ByResource) + ).After(toCursor(tuple.MustParse("someresourcetype:foo#viewer@user:bar")), options.ByResource) }, "SELECT * WHERE ns = ? AND (object_id,relation,subject_ns,subject_object_id,subject_relation) > (?,?,?,?,?)", []any{"someresourcetype", "foo", "viewer", "user", "bar", "..."}, @@ -488,7 +490,7 @@ func TestSchemaQueryFilterer(t *testing.T) { datastore.RelationshipsFilter{ OptionalResourceRelation: "somerelation", }, - ).After(tuple.MustParse("someresourcetype:foo#viewer@user:bar"), options.ByResource) + ).After(toCursor(tuple.MustParse("someresourcetype:foo#viewer@user:bar")), options.ByResource) }, "SELECT * WHERE relation = ? AND (ns,object_id,subject_ns,subject_object_id,subject_relation) > (?,?,?,?,?)", []any{"somerelation", "someresourcetype", "foo", "user", "bar", "..."}, @@ -504,7 +506,7 @@ func TestSchemaQueryFilterer(t *testing.T) { OptionalResourceType: "someresourcetype", OptionalResourceIds: []string{"one"}, }, - ).After(tuple.MustParse("someresourcetype:foo#viewer@user:bar"), options.ByResource) + ).After(toCursor(tuple.MustParse("someresourcetype:foo#viewer@user:bar")), options.ByResource) }, "SELECT * WHERE ns = ? AND object_id IN (?) AND (relation,subject_ns,subject_object_id,subject_relation) > (?,?,?,?)", []any{"someresourcetype", "one", "viewer", "user", "bar", "..."}, @@ -520,7 +522,7 @@ func TestSchemaQueryFilterer(t *testing.T) { datastore.RelationshipsFilter{ OptionalResourceIds: []string{"one"}, }, - ).After(tuple.MustParse("someresourcetype:foo#viewer@user:bar"), options.ByResource) + ).After(toCursor(tuple.MustParse("someresourcetype:foo#viewer@user:bar")), options.ByResource) }, "SELECT * WHERE object_id IN (?) AND (ns,relation,subject_ns,subject_object_id,subject_relation) > (?,?,?,?,?)", []any{"one", "someresourcetype", "viewer", "user", "bar", "..."}, @@ -536,7 +538,7 @@ func TestSchemaQueryFilterer(t *testing.T) { OptionalResourceType: "someresourcetype", OptionalResourceIds: []string{"one", "two"}, }, - ).After(tuple.MustParse("someresourcetype:foo#viewer@user:bar"), options.ByResource) + ).After(toCursor(tuple.MustParse("someresourcetype:foo#viewer@user:bar")), options.ByResource) }, "SELECT * WHERE ns = ? AND object_id IN (?,?) AND (object_id,relation,subject_ns,subject_object_id,subject_relation) > (?,?,?,?,?)", []any{"someresourcetype", "one", "two", "foo", "viewer", "user", "bar", "..."}, @@ -553,7 +555,7 @@ func TestSchemaQueryFilterer(t *testing.T) { OptionalResourceType: "someresourcetype", OptionalResourceRelation: "somerelation", }, - ).After(tuple.MustParse("someresourcetype:foo#viewer@user:bar"), options.ByResource) + ).After(toCursor(tuple.MustParse("someresourcetype:foo#viewer@user:bar")), options.ByResource) }, "SELECT * WHERE ns = ? AND relation = ? AND (object_id,subject_ns,subject_object_id,subject_relation) > (?,?,?,?)", []any{"someresourcetype", "somerelation", "foo", "user", "bar", "..."}, @@ -567,7 +569,7 @@ func TestSchemaQueryFilterer(t *testing.T) { func(filterer SchemaQueryFilterer) SchemaQueryFilterer { return filterer.MustFilterWithSubjectsSelectors(datastore.SubjectsSelector{ OptionalSubjectType: "somesubjectype", - }).After(tuple.MustParse("someresourcetype:foo#viewer@user:bar"), options.ByResource) + }).After(toCursor(tuple.MustParse("someresourcetype:foo#viewer@user:bar")), options.ByResource) }, "SELECT * WHERE ((subject_ns = ?)) AND (ns,object_id,relation,subject_object_id,subject_relation) > (?,?,?,?,?)", []any{"somesubjectype", "someresourcetype", "foo", "viewer", "bar", "..."}, @@ -584,7 +586,7 @@ func TestSchemaQueryFilterer(t *testing.T) { OptionalSubjectType: "somesubjectype", }).MustFilterWithSubjectsSelectors(datastore.SubjectsSelector{ OptionalSubjectType: "anothersubjectype", - }).After(tuple.MustParse("someresourcetype:foo#viewer@user:bar"), options.ByResource) + }).After(toCursor(tuple.MustParse("someresourcetype:foo#viewer@user:bar")), options.ByResource) }, "SELECT * WHERE ((subject_ns = ?)) AND ((subject_ns = ?)) AND (ns,object_id,relation,subject_ns,subject_object_id,subject_relation) > (?,?,?,?,?,?)", []any{"somesubjectype", "anothersubjectype", "someresourcetype", "foo", "viewer", "user", "bar", "..."}, @@ -595,7 +597,7 @@ func TestSchemaQueryFilterer(t *testing.T) { { "after with resource ID prefix", func(filterer SchemaQueryFilterer) SchemaQueryFilterer { - return filterer.MustFilterWithResourceIDPrefix("someprefix").After(tuple.MustParse("someresourcetype:foo#viewer@user:bar"), options.ByResource) + return filterer.MustFilterWithResourceIDPrefix("someprefix").After(toCursor(tuple.MustParse("someresourcetype:foo#viewer@user:bar")), options.ByResource) }, "SELECT * WHERE object_id LIKE ? AND (ns,object_id,relation,subject_ns,subject_object_id,subject_relation) > (?,?,?,?,?,?)", []any{"someprefix%", "someresourcetype", "foo", "viewer", "user", "bar", "..."}, @@ -621,7 +623,7 @@ func TestSchemaQueryFilterer(t *testing.T) { func(filterer SchemaQueryFilterer) SchemaQueryFilterer { return filterer.MustFilterWithSubjectsSelectors(datastore.SubjectsSelector{ OptionalSubjectType: "somesubjectype", - }).After(tuple.MustParse("someresourcetype:foo#viewer@user:bar"), options.BySubject) + }).After(toCursor(tuple.MustParse("someresourcetype:foo#viewer@user:bar")), options.BySubject) }, "SELECT * WHERE ((subject_ns = ?)) AND (subject_object_id,ns,object_id,relation,subject_relation) > (?,?,?,?,?)", []any{"somesubjectype", "bar", "someresourcetype", "foo", "viewer", "..."}, @@ -635,7 +637,7 @@ func TestSchemaQueryFilterer(t *testing.T) { return filterer.MustFilterWithSubjectsSelectors(datastore.SubjectsSelector{ OptionalSubjectType: "somesubjectype", OptionalSubjectIds: []string{"foo"}, - }).After(tuple.MustParse("someresourcetype:someresource#viewer@user:bar"), options.BySubject) + }).After(toCursor(tuple.MustParse("someresourcetype:someresource#viewer@user:bar")), options.BySubject) }, "SELECT * WHERE ((subject_ns = ? AND subject_object_id IN (?))) AND (ns,object_id,relation,subject_relation) > (?,?,?,?)", []any{"somesubjectype", "foo", "someresourcetype", "someresource", "viewer", "..."}, @@ -647,7 +649,7 @@ func TestSchemaQueryFilterer(t *testing.T) { return filterer.MustFilterWithSubjectsSelectors(datastore.SubjectsSelector{ OptionalSubjectType: "somesubjectype", OptionalSubjectIds: []string{"foo", "bar"}, - }).After(tuple.MustParse("someresourcetype:someresource#viewer@user:next"), options.BySubject) + }).After(toCursor(tuple.MustParse("someresourcetype:someresource#viewer@user:next")), options.BySubject) }, "SELECT * WHERE ((subject_ns = ? AND subject_object_id IN (?,?))) AND (subject_object_id,ns,object_id,relation,subject_relation) > (?,?,?,?,?)", []any{"somesubjectype", "foo", "bar", "next", "someresourcetype", "someresource", "viewer", "..."}, diff --git a/internal/datastore/common/tuple.go b/internal/datastore/common/tuple.go index 492c14ce09..49727001c7 100644 --- a/internal/datastore/common/tuple.go +++ b/internal/datastore/common/tuple.go @@ -2,75 +2,16 @@ package common import ( "github.com/authzed/spicedb/pkg/datastore" - "github.com/authzed/spicedb/pkg/datastore/options" - core "github.com/authzed/spicedb/pkg/proto/core/v1" - "github.com/authzed/spicedb/pkg/spiceerrors" + "github.com/authzed/spicedb/pkg/tuple" ) -// NewSliceRelationshipIterator creates a datastore.TupleIterator instance from a materialized slice of tuples. -func NewSliceRelationshipIterator(tuples []*core.RelationTuple, order options.SortOrder) datastore.RelationshipIterator { - iter := &sliceRelationshipIterator{tuples: tuples, order: order} - spiceerrors.SetFinalizerForDebugging(iter, MustIteratorBeClosed) - return iter -} - -type sliceRelationshipIterator struct { - tuples []*core.RelationTuple - order options.SortOrder - last *core.RelationTuple - closed bool - err error -} - -// Next implements TupleIterator -func (sti *sliceRelationshipIterator) Next() *core.RelationTuple { - if sti.closed { - sti.err = datastore.ErrClosedIterator - return nil - } - - if len(sti.tuples) > 0 { - first := sti.tuples[0] - sti.tuples = sti.tuples[1:] - sti.last = first - return first - } - - return nil -} - -func (sti *sliceRelationshipIterator) Cursor() (options.Cursor, error) { - switch { - case sti.closed: - return nil, datastore.ErrClosedIterator - case sti.order == options.Unsorted: - return nil, datastore.ErrCursorsWithoutSorting - case sti.last == nil: - return nil, datastore.ErrCursorEmpty - default: - return sti.last, nil - } -} - -// Err implements TupleIterator -func (sti *sliceRelationshipIterator) Err() error { - return sti.err -} - -// Close implements TupleIterator -func (sti *sliceRelationshipIterator) Close() { - if sti.closed { - return - } - - sti.tuples = nil - sti.closed = true -} - -// MustIteratorBeClosed is a function which can be used as a finalizer to make sure that -// tuples are getting closed before they are garbage collected. -func MustIteratorBeClosed(iter *sliceRelationshipIterator) { - if !iter.closed { - panic("Tuple iterator garbage collected before Close() was called") +// NewSliceRelationshipIterator creates a datastore.RelationshipIterator instance from a materialized slice of tuples. +func NewSliceRelationshipIterator(rels []tuple.Relationship) datastore.RelationshipIterator { + return func(yield func(tuple.Relationship, error) bool) { + for _, rel := range rels { + if !yield(rel, nil) { + break + } + } } } diff --git a/internal/datastore/crdb/crdb_test.go b/internal/datastore/crdb/crdb_test.go index d90e91e25b..634d25691b 100644 --- a/internal/datastore/crdb/crdb_test.go +++ b/internal/datastore/crdb/crdb_test.go @@ -444,12 +444,12 @@ func RelationshipIntegrityInfoTest(t *testing.T, tester test.DatastoreTester) { _, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { tpl := tuple.MustParse("document:foo#viewer@user:tom") - tpl.Integrity = &core.RelationshipIntegrity{ + tpl.OptionalIntegrity = &core.RelationshipIntegrity{ KeyId: "key1", Hash: []byte("hash1"), HashedAt: timestamppb.New(timestamp), } - return rwt.WriteRelationships(ctx, []*core.RelationTupleUpdate{ + return rwt.WriteRelationships(ctx, []tuple.RelationshipUpdate{ tuple.Create(tpl), }) }) @@ -466,31 +466,30 @@ func RelationshipIntegrityInfoTest(t *testing.T, tester test.DatastoreTester) { OptionalResourceRelation: "viewer", }) require.NoError(err) - t.Cleanup(iter.Close) - tpl := iter.Next() - require.NotNil(tpl) + slice, err := datastore.IteratorToSlice(iter) + require.NoError(err) - require.NotNil(tpl.Integrity) - require.Equal("key1", tpl.Integrity.KeyId) - require.Equal([]byte("hash1"), tpl.Integrity.Hash) + rel := slice[0] - require.LessOrEqual(math.Abs(float64(timestamp.Sub(tpl.Integrity.HashedAt.AsTime()).Milliseconds())), 1000.0) + require.NotNil(rel.OptionalIntegrity) + require.Equal("key1", rel.OptionalIntegrity.KeyId) + require.Equal([]byte("hash1"), rel.OptionalIntegrity.Hash) - iter.Close() + require.LessOrEqual(math.Abs(float64(timestamp.Sub(rel.OptionalIntegrity.HashedAt.AsTime()).Milliseconds())), 1000.0) } type fakeSource struct { - tpl *core.RelationTuple + rel *tuple.Relationship } -func (f *fakeSource) Next(ctx context.Context) (*core.RelationTuple, error) { - if f.tpl == nil { +func (f *fakeSource) Next(ctx context.Context) (*tuple.Relationship, error) { + if f.rel == nil { return nil, nil } - tpl := f.tpl - f.tpl = nil + tpl := f.rel + f.rel = nil return tpl, nil } @@ -507,14 +506,14 @@ func BulkRelationshipIntegrityInfoTest(t *testing.T, tester test.DatastoreTester timestamp := time.Now().UTC() _, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { - tpl := tuple.MustParse("document:foo#viewer@user:tom") - tpl.Integrity = &core.RelationshipIntegrity{ + rel := tuple.MustParse("document:foo#viewer@user:tom") + rel.OptionalIntegrity = &core.RelationshipIntegrity{ KeyId: "key1", Hash: []byte("hash1"), HashedAt: timestamppb.New(timestamp), } - _, err := rwt.BulkLoad(ctx, &fakeSource{tpl}) + _, err := rwt.BulkLoad(ctx, &fakeSource{&rel}) return err }) require.NoError(err) @@ -530,18 +529,17 @@ func BulkRelationshipIntegrityInfoTest(t *testing.T, tester test.DatastoreTester OptionalResourceRelation: "viewer", }) require.NoError(err) - t.Cleanup(iter.Close) - tpl := iter.Next() - require.NotNil(tpl) + slice, err := datastore.IteratorToSlice(iter) + require.NoError(err) - require.NotNil(tpl.Integrity) - require.Equal("key1", tpl.Integrity.KeyId) - require.Equal([]byte("hash1"), tpl.Integrity.Hash) + rel := slice[0] - require.LessOrEqual(math.Abs(float64(timestamp.Sub(tpl.Integrity.HashedAt.AsTime()).Milliseconds())), 1000.0) + require.NotNil(rel.OptionalIntegrity) + require.Equal("key1", rel.OptionalIntegrity.KeyId) + require.Equal([]byte("hash1"), rel.OptionalIntegrity.Hash) - iter.Close() + require.LessOrEqual(math.Abs(float64(timestamp.Sub(rel.OptionalIntegrity.HashedAt.AsTime()).Milliseconds())), 1000.0) } func RelationshipIntegrityWatchTest(t *testing.T, tester test.DatastoreTester) { @@ -557,14 +555,14 @@ func RelationshipIntegrityWatchTest(t *testing.T, tester test.DatastoreTester) { timestamp := time.Now().UTC() _, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { - tpl := tuple.MustParse("document:foo#viewer@user:tom") - tpl.Integrity = &core.RelationshipIntegrity{ + rel := tuple.MustParse("document:foo#viewer@user:tom") + rel.OptionalIntegrity = &core.RelationshipIntegrity{ KeyId: "key1", Hash: []byte("hash1"), HashedAt: timestamppb.New(timestamp), } - return rwt.WriteRelationships(ctx, []*core.RelationTupleUpdate{ - tuple.Create(tpl), + return rwt.WriteRelationships(ctx, []tuple.RelationshipUpdate{ + tuple.Create(rel), }) }) require.NoError(err) @@ -583,12 +581,12 @@ func RelationshipIntegrityWatchTest(t *testing.T, tester test.DatastoreTester) { require.Fail("Timed out waiting for ErrWatchDisconnected") } - tpl := change.RelationshipChanges[0].Tuple - require.NotNil(tpl.Integrity) - require.Equal("key1", tpl.Integrity.KeyId) - require.Equal([]byte("hash1"), tpl.Integrity.Hash) + rel := change.RelationshipChanges[0].Relationship + require.NotNil(rel.OptionalIntegrity) + require.Equal("key1", rel.OptionalIntegrity.KeyId) + require.Equal([]byte("hash1"), rel.OptionalIntegrity.Hash) - require.LessOrEqual(math.Abs(float64(timestamp.Sub(tpl.Integrity.HashedAt.AsTime()).Milliseconds())), 1000.0) + require.LessOrEqual(math.Abs(float64(timestamp.Sub(rel.OptionalIntegrity.HashedAt.AsTime()).Milliseconds())), 1000.0) case err := <-errchan: require.Failf("Failed waiting for changes with error", "error: %v", err) case <-time.NewTimer(10 * time.Second).C: diff --git a/internal/datastore/crdb/reader.go b/internal/datastore/crdb/reader.go index 41e3c91fe0..0df1fc3e70 100644 --- a/internal/datastore/crdb/reader.go +++ b/internal/datastore/crdb/reader.go @@ -27,7 +27,7 @@ const ( var ( queryReadNamespace = psql.Select(colConfig, colTimestamp) - countTuples = psql.Select("count(*)") + countRels = psql.Select("count(*)") schema = common.NewSchemaInformation( colNamespace, @@ -74,7 +74,7 @@ func (cr *crdbReader) CountRelationships(ctx context.Context, name string) (int, return 0, err } - query := cr.fromBuilder(countTuples, cr.tupleTableName) + query := cr.fromBuilder(countRels, cr.tupleTableName) builder, err := common.NewSchemaQueryFilterer(schema, query, cr.filterMaximumIDCount).FilterWithRelationshipsFilter(relFilter) if err != nil { return 0, err diff --git a/internal/datastore/crdb/readwrite.go b/internal/datastore/crdb/readwrite.go index 626f5e8153..1aafec83c4 100644 --- a/internal/datastore/crdb/readwrite.go +++ b/internal/datastore/crdb/readwrite.go @@ -19,6 +19,7 @@ import ( "github.com/authzed/spicedb/pkg/datastore/options" core "github.com/authzed/spicedb/pkg/proto/core/v1" "github.com/authzed/spicedb/pkg/spiceerrors" + "github.com/authzed/spicedb/pkg/tuple" ) const ( @@ -253,7 +254,7 @@ func (rwt *crdbReadWriteTXN) StoreCounterValue(ctx context.Context, name string, return nil } -func (rwt *crdbReadWriteTXN) WriteRelationships(ctx context.Context, mutations []*core.RelationTupleUpdate) error { +func (rwt *crdbReadWriteTXN) WriteRelationships(ctx context.Context, mutations []tuple.RelationshipUpdate) error { bulkWrite := rwt.queryWriteTuple() var bulkWriteCount int64 @@ -266,35 +267,35 @@ func (rwt *crdbReadWriteTXN) WriteRelationships(ctx context.Context, mutations [ // Process the actual updates for _, mutation := range mutations { - rel := mutation.Tuple + rel := mutation.Relationship var caveatContext map[string]any var caveatName string - if rel.Caveat != nil { - caveatName = rel.Caveat.CaveatName - caveatContext = rel.Caveat.Context.AsMap() + if rel.OptionalCaveat != nil { + caveatName = rel.OptionalCaveat.CaveatName + caveatContext = rel.OptionalCaveat.Context.AsMap() } var integrityKeyID *string var integrityHash []byte - if rel.Integrity != nil { + if rel.OptionalIntegrity != nil { if !rwt.withIntegrity { return spiceerrors.MustBugf("attempted to write a relationship with integrity, but the datastore does not support integrity") } - integrityKeyID = &rel.Integrity.KeyId - integrityHash = rel.Integrity.Hash + integrityKeyID = &rel.OptionalIntegrity.KeyId + integrityHash = rel.OptionalIntegrity.Hash } else if rwt.withIntegrity { return spiceerrors.MustBugf("attempted to write a relationship without integrity, but the datastore requires integrity") } values := []any{ - rel.ResourceAndRelation.Namespace, - rel.ResourceAndRelation.ObjectId, - rel.ResourceAndRelation.Relation, - rel.Subject.Namespace, - rel.Subject.ObjectId, + rel.Resource.ObjectType, + rel.Resource.ObjectID, + rel.Resource.Relation, + rel.Subject.ObjectType, + rel.Subject.ObjectID, rel.Subject.Relation, caveatName, caveatContext, @@ -304,28 +305,28 @@ func (rwt *crdbReadWriteTXN) WriteRelationships(ctx context.Context, mutations [ values = append(values, integrityKeyID, integrityHash) } - rwt.addOverlapKey(rel.ResourceAndRelation.Namespace) - rwt.addOverlapKey(rel.Subject.Namespace) + rwt.addOverlapKey(rel.Resource.ObjectType) + rwt.addOverlapKey(rel.Subject.ObjectType) switch mutation.Operation { - case core.RelationTupleUpdate_TOUCH: + case tuple.UpdateOperationTouch: rwt.relCountChange++ bulkTouch = bulkTouch.Values(values...) bulkTouchCount++ - case core.RelationTupleUpdate_CREATE: + case tuple.UpdateOperationCreate: rwt.relCountChange++ bulkWrite = bulkWrite.Values(values...) bulkWriteCount++ - case core.RelationTupleUpdate_DELETE: + case tuple.UpdateOperationDelete: rwt.relCountChange-- bulkDeleteOr = append(bulkDeleteOr, exactRelationshipClause(rel)) bulkDeleteCount++ default: - log.Ctx(ctx).Error().Stringer("operation", mutation.Operation).Msg("unknown operation type") - return fmt.Errorf("unknown mutation operation: %s", mutation.Operation) + log.Ctx(ctx).Error().Msg("unknown operation type") + return fmt.Errorf("unknown mutation operation: %v", mutation.Operation) } } @@ -363,13 +364,13 @@ func (rwt *crdbReadWriteTXN) WriteRelationships(ctx context.Context, mutations [ return nil } -func exactRelationshipClause(r *core.RelationTuple) sq.Eq { +func exactRelationshipClause(r tuple.Relationship) sq.Eq { return sq.Eq{ - colNamespace: r.ResourceAndRelation.Namespace, - colObjectID: r.ResourceAndRelation.ObjectId, - colRelation: r.ResourceAndRelation.Relation, - colUsersetNamespace: r.Subject.Namespace, - colUsersetObjectID: r.Subject.ObjectId, + colNamespace: r.Resource.ObjectType, + colObjectID: r.Resource.ObjectID, + colRelation: r.Resource.Relation, + colUsersetNamespace: r.Subject.ObjectType, + colUsersetObjectID: r.Subject.ObjectID, colUsersetRelation: r.Subject.Relation, } } diff --git a/internal/datastore/crdb/watch.go b/internal/datastore/crdb/watch.go index d03989c6fe..883d14978a 100644 --- a/internal/datastore/crdb/watch.go +++ b/internal/datastore/crdb/watch.go @@ -20,6 +20,7 @@ import ( "github.com/authzed/spicedb/pkg/datastore" core "github.com/authzed/spicedb/pkg/proto/core/v1" "github.com/authzed/spicedb/pkg/spiceerrors" + "github.com/authzed/spicedb/pkg/tuple" ) const ( @@ -291,19 +292,21 @@ func (cds *crdbDatastore) watch( } } - tuple := &core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: pkValues[0], - ObjectId: pkValues[1], - Relation: pkValues[2], + relationship := tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.ObjectAndRelation{ + ObjectType: pkValues[0], + ObjectID: pkValues[1], + Relation: pkValues[2], + }, + Subject: tuple.ObjectAndRelation{ + ObjectType: pkValues[3], + ObjectID: pkValues[4], + Relation: pkValues[5], + }, }, - Subject: &core.ObjectAndRelation{ - Namespace: pkValues[3], - ObjectId: pkValues[4], - Relation: pkValues[5], - }, - Caveat: ctxCaveat, - Integrity: integrity, + OptionalCaveat: ctxCaveat, + OptionalIntegrity: integrity, } rev, err := revisions.HLCRevisionFromString(details.Updated) @@ -313,12 +316,12 @@ func (cds *crdbDatastore) watch( } if details.After == nil { - if err := tracked.AddRelationshipChange(ctx, rev, tuple, core.RelationTupleUpdate_DELETE); err != nil { + if err := tracked.AddRelationshipChange(ctx, rev, relationship, tuple.UpdateOperationDelete); err != nil { sendError(err) return } } else { - if err := tracked.AddRelationshipChange(ctx, rev, tuple, core.RelationTupleUpdate_TOUCH); err != nil { + if err := tracked.AddRelationshipChange(ctx, rev, relationship, tuple.UpdateOperationTouch); err != nil { sendError(err) return } diff --git a/internal/datastore/memdb/memdb.go b/internal/datastore/memdb/memdb.go index 40354e80d7..9df7eb7aaf 100644 --- a/internal/datastore/memdb/memdb.go +++ b/internal/datastore/memdb/memdb.go @@ -11,6 +11,7 @@ import ( "github.com/authzed/spicedb/internal/datastore/common" "github.com/authzed/spicedb/pkg/spiceerrors" + "github.com/authzed/spicedb/pkg/tuple" "github.com/google/uuid" "github.com/hashicorp/go-memdb" @@ -214,21 +215,21 @@ func (mdb *memdbDatastore) ReadWriteTx( switch change.Table { case tableRelationship: if change.After != nil { - rt, err := change.After.(*relationship).RelationTuple() + rt, err := change.After.(*relationship).Relationship() if err != nil { return datastore.NoRevision, err } - if err := tracked.AddRelationshipChange(ctx, newRevision, rt, corev1.RelationTupleUpdate_TOUCH); err != nil { + if err := tracked.AddRelationshipChange(ctx, newRevision, rt, tuple.UpdateOperationTouch); err != nil { return datastore.NoRevision, err } } else if change.After == nil && change.Before != nil { - rt, err := change.Before.(*relationship).RelationTuple() + rt, err := change.Before.(*relationship).Relationship() if err != nil { return datastore.NoRevision, err } - if err := tracked.AddRelationshipChange(ctx, newRevision, rt, corev1.RelationTupleUpdate_DELETE); err != nil { + if err := tracked.AddRelationshipChange(ctx, newRevision, rt, tuple.UpdateOperationDelete); err != nil { return datastore.NoRevision, err } } else { diff --git a/internal/datastore/memdb/memdb_test.go b/internal/datastore/memdb/memdb_test.go index 43b14d0880..9fa483ee9e 100644 --- a/internal/datastore/memdb/memdb_test.go +++ b/internal/datastore/memdb/memdb_test.go @@ -98,12 +98,9 @@ func TestConcurrentWriteRelsError(t *testing.T) { i := i g.Go(func() error { _, err := ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { - updates := []*corev1.RelationTupleUpdate{} + updates := []tuple.RelationshipUpdate{} for j := 0; j < 500; j++ { - updates = append(updates, &corev1.RelationTupleUpdate{ - Operation: corev1.RelationTupleUpdate_TOUCH, - Tuple: tuple.MustParse(fmt.Sprintf("document:doc-%d-%d#viewer@user:tom", i, j)), - }) + updates = append(updates, tuple.Touch(tuple.MustParse(fmt.Sprintf("document:doc-%d-%d#viewer@user:tom", i, j)))) } return rwt.WriteRelationships(ctx, updates) @@ -116,3 +113,35 @@ func TestConcurrentWriteRelsError(t *testing.T) { require.Error(werr) require.ErrorContains(werr, "serialization max retries exceeded") } + +func BenchmarkQueryRelationships(b *testing.B) { + require := require.New(b) + + ds, err := NewMemdbDatastore(0, 1*time.Hour, 1*time.Hour) + require.NoError(err) + + // Write a bunch of relationships. + ctx := context.Background() + rev, err := ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { + updates := []tuple.RelationshipUpdate{} + for i := 0; i < 1000; i++ { + updates = append(updates, tuple.Touch(tuple.MustParse(fmt.Sprintf("document:doc-%d#viewer@user:tom", i)))) + } + + return rwt.WriteRelationships(ctx, updates) + }) + require.NoError(err) + + reader := ds.SnapshotReader(rev) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + iter, err := reader.QueryRelationships(ctx, datastore.RelationshipsFilter{ + OptionalResourceType: "document", + }) + require.NoError(err) + for _, err := range iter { + require.NoError(err) + } + } +} diff --git a/internal/datastore/memdb/readonly.go b/internal/datastore/memdb/readonly.go index 76790f62e8..e3734d3184 100644 --- a/internal/datastore/memdb/readonly.go +++ b/internal/datastore/memdb/readonly.go @@ -14,6 +14,7 @@ import ( "github.com/authzed/spicedb/pkg/datastore/options" core "github.com/authzed/spicedb/pkg/proto/core/v1" "github.com/authzed/spicedb/pkg/spiceerrors" + "github.com/authzed/spicedb/pkg/tuple" ) type txFactory func() (*memdb.Txn, error) @@ -51,18 +52,15 @@ func (r *memdbReader) CountRelationships(ctx context.Context, name string) (int, if err != nil { return 0, err } - defer iter.Close() count := 0 - for iter.Next() != nil { - if iter.Err() != nil { - return 0, iter.Err() + for _, err := range iter { + if err != nil { + return 0, err } count++ } - iter.Close() - return count, nil } @@ -150,7 +148,7 @@ func (r *memdbReader) QueryRelationships( fallthrough case options.ByResource: - iter := newMemdbTupleIterator(filteredIterator, queryOpts.Limit, queryOpts.Sort) + iter := newMemdbTupleIterator(filteredIterator, queryOpts.Limit) return iter, nil case options.BySubject: @@ -161,12 +159,6 @@ func (r *memdbReader) QueryRelationships( } } -func mustHaveBeenClosed(iter *memdbTupleIterator) { - if !iter.closed { - panic("Tuple iterator garbage collected before Close() was called") - } -} - // ReverseQueryRelationships reads relationships starting from the subject. func (r *memdbReader) ReverseQueryRelationships( _ context.Context, @@ -218,7 +210,7 @@ func (r *memdbReader) ReverseQueryRelationships( fallthrough case options.ByResource: - iter := newMemdbTupleIterator(filteredIterator, queryOpts.LimitForReverse, queryOpts.SortForReverse) + iter := newMemdbTupleIterator(filteredIterator, queryOpts.LimitForReverse) return iter, nil case options.BySubject: @@ -453,13 +445,13 @@ func filterFuncForFilters( } } -func makeCursorFilterFn(after *core.RelationTuple, order options.SortOrder) func(tpl *relationship) bool { +func makeCursorFilterFn(after options.Cursor, order options.SortOrder) func(tpl *relationship) bool { if after != nil { switch order { case options.ByResource: return func(tpl *relationship) bool { - return less(tpl.namespace, tpl.resourceID, tpl.relation, after.ResourceAndRelation) || - (eq(tpl.namespace, tpl.resourceID, tpl.relation, after.ResourceAndRelation) && + return less(tpl.namespace, tpl.resourceID, tpl.relation, after.Resource) || + (eq(tpl.namespace, tpl.resourceID, tpl.relation, after.Resource) && (less(tpl.subjectNamespace, tpl.subjectObjectID, tpl.subjectRelation, after.Subject) || eq(tpl.subjectNamespace, tpl.subjectObjectID, tpl.subjectRelation, after.Subject))) } @@ -467,8 +459,8 @@ func makeCursorFilterFn(after *core.RelationTuple, order options.SortOrder) func return func(tpl *relationship) bool { return less(tpl.subjectNamespace, tpl.subjectObjectID, tpl.subjectRelation, after.Subject) || (eq(tpl.subjectNamespace, tpl.subjectObjectID, tpl.subjectRelation, after.Subject) && - (less(tpl.namespace, tpl.resourceID, tpl.relation, after.ResourceAndRelation) || - eq(tpl.namespace, tpl.resourceID, tpl.relation, after.ResourceAndRelation))) + (less(tpl.namespace, tpl.resourceID, tpl.relation, after.Resource) || + eq(tpl.namespace, tpl.resourceID, tpl.relation, after.Resource))) } } } @@ -476,11 +468,11 @@ func makeCursorFilterFn(after *core.RelationTuple, order options.SortOrder) func } func newSubjectSortedIterator(it memdb.ResultIterator, limit *uint64) (datastore.RelationshipIterator, error) { - results := make([]*core.RelationTuple, 0) + results := make([]tuple.Relationship, 0) // Coalesce all of the results into memory for foundRaw := it.Next(); foundRaw != nil; foundRaw = it.Next() { - rt, err := foundRaw.(*relationship).RelationTuple() + rt, err := foundRaw.(*relationship).Relationship() if err != nil { return nil, err } @@ -490,13 +482,13 @@ func newSubjectSortedIterator(it memdb.ResultIterator, limit *uint64) (datastore // Sort them by subject sort.Slice(results, func(i, j int) bool { - lhsRes := results[i].ResourceAndRelation + lhsRes := results[i].Resource lhsSub := results[i].Subject - rhsRes := results[j].ResourceAndRelation + rhsRes := results[j].Resource rhsSub := results[j].Subject - return less(lhsSub.Namespace, lhsSub.ObjectId, lhsSub.Relation, rhsSub) || - (eq(lhsSub.Namespace, lhsSub.ObjectId, lhsSub.Relation, rhsSub) && - (less(lhsRes.Namespace, lhsRes.ObjectId, lhsRes.Relation, rhsRes))) + return less(lhsSub.ObjectType, lhsSub.ObjectID, lhsSub.Relation, rhsSub) || + (eq(lhsSub.ObjectType, lhsSub.ObjectID, lhsSub.Relation, rhsSub) && + (less(lhsRes.ObjectType, lhsRes.ObjectID, lhsRes.Relation, rhsRes))) }) // Limit them if requested @@ -504,86 +496,45 @@ func newSubjectSortedIterator(it memdb.ResultIterator, limit *uint64) (datastore results = results[0:*limit] } - return common.NewSliceRelationshipIterator(results, options.BySubject), nil + return common.NewSliceRelationshipIterator(results), nil } func noopCursorFilter(_ *relationship) bool { return false } -func less(lhsNamespace, lhsObjectID, lhsRelation string, rhs *core.ObjectAndRelation) bool { - return lhsNamespace < rhs.Namespace || - (lhsNamespace == rhs.Namespace && lhsObjectID < rhs.ObjectId) || - (lhsNamespace == rhs.Namespace && lhsObjectID == rhs.ObjectId && lhsRelation < rhs.Relation) -} - -func eq(lhsNamespace, lhsObjectID, lhsRelation string, rhs *core.ObjectAndRelation) bool { - return lhsNamespace == rhs.Namespace && lhsObjectID == rhs.ObjectId && lhsRelation == rhs.Relation +func less(lhsNamespace, lhsObjectID, lhsRelation string, rhs tuple.ObjectAndRelation) bool { + return lhsNamespace < rhs.ObjectType || + (lhsNamespace == rhs.ObjectType && lhsObjectID < rhs.ObjectID) || + (lhsNamespace == rhs.ObjectType && lhsObjectID == rhs.ObjectID && lhsRelation < rhs.Relation) } -func newMemdbTupleIterator(it memdb.ResultIterator, limit *uint64, order options.SortOrder) *memdbTupleIterator { - iter := &memdbTupleIterator{it: it, limit: limit, order: order} - spiceerrors.SetFinalizerForDebugging(iter, mustHaveBeenClosed) - return iter +func eq(lhsNamespace, lhsObjectID, lhsRelation string, rhs tuple.ObjectAndRelation) bool { + return lhsNamespace == rhs.ObjectType && lhsObjectID == rhs.ObjectID && lhsRelation == rhs.Relation } -type memdbTupleIterator struct { - closed bool - it memdb.ResultIterator - limit *uint64 - count uint64 - err error - order options.SortOrder - last *core.RelationTuple -} - -func (mti *memdbTupleIterator) Next() *core.RelationTuple { - if mti.closed { - return nil - } - - foundRaw := mti.it.Next() - if foundRaw == nil { - return nil - } - - if mti.limit != nil && mti.count >= *mti.limit { - return nil - } - mti.count++ - - rt, err := foundRaw.(*relationship).RelationTuple() - if err != nil { - mti.err = err - return nil - } +func newMemdbTupleIterator(it memdb.ResultIterator, limit *uint64) datastore.RelationshipIterator { + var count uint64 + return func(yield func(tuple.Relationship, error) bool) { + for { + foundRaw := it.Next() + if foundRaw == nil { + return + } - mti.last = rt - return rt -} + if limit != nil && count >= *limit { + return + } -func (mti *memdbTupleIterator) Cursor() (options.Cursor, error) { - switch { - case mti.closed: - return nil, datastore.ErrClosedIterator - case mti.order == options.Unsorted: - return nil, datastore.ErrCursorsWithoutSorting - case mti.last == nil: - return nil, datastore.ErrCursorEmpty - default: - return mti.last, nil + rt, err := foundRaw.(*relationship).Relationship() + if !yield(rt, err) { + return + } + count++ + } } } -func (mti *memdbTupleIterator) Err() error { - return mti.err -} - -func (mti *memdbTupleIterator) Close() { - mti.closed = true - mti.err = datastore.ErrClosedIterator -} - var _ datastore.Reader = &memdbReader{} type TryLocker interface { diff --git a/internal/datastore/memdb/readwrite.go b/internal/datastore/memdb/readwrite.go index 55eebded02..2f97c92766 100644 --- a/internal/datastore/memdb/readwrite.go +++ b/internal/datastore/memdb/readwrite.go @@ -13,6 +13,7 @@ import ( "github.com/authzed/spicedb/pkg/datastore" "github.com/authzed/spicedb/pkg/datastore/options" core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/spiceerrors" "github.com/authzed/spicedb/pkg/tuple" ) @@ -21,7 +22,7 @@ type memdbReadWriteTx struct { newRevision datastore.Revision } -func (rwt *memdbReadWriteTx) WriteRelationships(_ context.Context, mutations []*core.RelationTupleUpdate) error { +func (rwt *memdbReadWriteTx) WriteRelationships(_ context.Context, mutations []tuple.RelationshipUpdate) error { rwt.mustLock() defer rwt.Unlock() @@ -33,29 +34,29 @@ func (rwt *memdbReadWriteTx) WriteRelationships(_ context.Context, mutations []* return rwt.write(tx, mutations...) } -func (rwt *memdbReadWriteTx) toIntegrity(mutation *core.RelationTupleUpdate) *relationshipIntegrity { +func (rwt *memdbReadWriteTx) toIntegrity(mutation tuple.RelationshipUpdate) *relationshipIntegrity { var ri *relationshipIntegrity - if mutation.Tuple.Integrity != nil { + if mutation.Relationship.OptionalIntegrity != nil { ri = &relationshipIntegrity{ - keyID: mutation.Tuple.Integrity.KeyId, - hash: mutation.Tuple.Integrity.Hash, - timestamp: mutation.Tuple.Integrity.HashedAt.AsTime(), + keyID: mutation.Relationship.OptionalIntegrity.KeyId, + hash: mutation.Relationship.OptionalIntegrity.Hash, + timestamp: mutation.Relationship.OptionalIntegrity.HashedAt.AsTime(), } } return ri } // Caller must already hold the concurrent access lock! -func (rwt *memdbReadWriteTx) write(tx *memdb.Txn, mutations ...*core.RelationTupleUpdate) error { +func (rwt *memdbReadWriteTx) write(tx *memdb.Txn, mutations ...tuple.RelationshipUpdate) error { // Apply the mutations for _, mutation := range mutations { rel := &relationship{ - mutation.Tuple.ResourceAndRelation.Namespace, - mutation.Tuple.ResourceAndRelation.ObjectId, - mutation.Tuple.ResourceAndRelation.Relation, - mutation.Tuple.Subject.Namespace, - mutation.Tuple.Subject.ObjectId, - mutation.Tuple.Subject.Relation, + mutation.Relationship.Resource.ObjectType, + mutation.Relationship.Resource.ObjectID, + mutation.Relationship.Resource.Relation, + mutation.Relationship.Subject.ObjectType, + mutation.Relationship.Subject.ObjectID, + mutation.Relationship.Subject.Relation, rwt.toCaveatReference(mutation), rwt.toIntegrity(mutation), } @@ -80,25 +81,25 @@ func (rwt *memdbReadWriteTx) write(tx *memdb.Txn, mutations ...*core.RelationTup } switch mutation.Operation { - case core.RelationTupleUpdate_CREATE: + case tuple.UpdateOperationCreate: if existing != nil { - rt, err := existing.RelationTuple() + rt, err := existing.Relationship() if err != nil { return err } - return common.NewCreateRelationshipExistsError(rt) + return common.NewCreateRelationshipExistsError(&rt) } if err := tx.Insert(tableRelationship, rel); err != nil { return fmt.Errorf("error inserting relationship: %w", err) } - case core.RelationTupleUpdate_TOUCH: + case tuple.UpdateOperationTouch: if existing != nil { - rt, err := existing.RelationTuple() + rt, err := existing.Relationship() if err != nil { return err } - if tuple.MustString(rt) == tuple.MustString(mutation.Tuple) { + if tuple.MustString(rt) == tuple.MustString(mutation.Relationship) { continue } } @@ -106,26 +107,27 @@ func (rwt *memdbReadWriteTx) write(tx *memdb.Txn, mutations ...*core.RelationTup if err := tx.Insert(tableRelationship, rel); err != nil { return fmt.Errorf("error inserting relationship: %w", err) } - case core.RelationTupleUpdate_DELETE: + + case tuple.UpdateOperationDelete: if existing != nil { if err := tx.Delete(tableRelationship, existing); err != nil { return fmt.Errorf("error deleting relationship: %w", err) } } default: - return fmt.Errorf("unknown tuple mutation operation type: %s", mutation.Operation) + return spiceerrors.MustBugf("unknown tuple mutation operation type: %v", mutation.Operation) } } return nil } -func (rwt *memdbReadWriteTx) toCaveatReference(mutation *core.RelationTupleUpdate) *contextualizedCaveat { +func (rwt *memdbReadWriteTx) toCaveatReference(mutation tuple.RelationshipUpdate) *contextualizedCaveat { var cr *contextualizedCaveat - if mutation.Tuple.Caveat != nil { + if mutation.Relationship.OptionalCaveat != nil { cr = &contextualizedCaveat{ - caveatName: mutation.Tuple.Caveat.CaveatName, - context: mutation.Tuple.Caveat.Context.AsMap(), + caveatName: mutation.Relationship.OptionalCaveat.CaveatName, + context: mutation.Relationship.OptionalCaveat.Context.AsMap(), } } return cr @@ -164,12 +166,12 @@ func (rwt *memdbReadWriteTx) deleteWithLock(tx *memdb.Txn, filter *v1.Relationsh filteredIter := memdb.NewFilterIterator(bestIter, relationshipFilterFilterFunc(filter)) // Collect the tuples into a slice of mutations for the changelog - var mutations []*core.RelationTupleUpdate + var mutations []tuple.RelationshipUpdate var counter uint64 metLimit := false for row := filteredIter.Next(); row != nil; row = filteredIter.Next() { - rt, err := row.(*relationship).RelationTuple() + rt, err := row.(*relationship).Relationship() if err != nil { return false, err } @@ -328,15 +330,16 @@ func (rwt *memdbReadWriteTx) DeleteNamespaces(_ context.Context, nsNames ...stri } func (rwt *memdbReadWriteTx) BulkLoad(ctx context.Context, iter datastore.BulkWriteRelationshipSource) (uint64, error) { - updates := []*core.RelationTupleUpdate{{ - Operation: core.RelationTupleUpdate_CREATE, - }} - var numCopied uint64 - var next *core.RelationTuple + var next *tuple.Relationship var err error + + updates := []tuple.RelationshipUpdate{{ + Operation: tuple.UpdateOperationCreate, + }} + for next, err = iter.Next(ctx); next != nil && err == nil; next, err = iter.Next(ctx) { - updates[0].Tuple = next + updates[0].Relationship = *next if err := rwt.WriteRelationships(ctx, updates); err != nil { return 0, err } diff --git a/internal/datastore/memdb/schema.go b/internal/datastore/memdb/schema.go index 4ef504f23c..2caf054a0a 100644 --- a/internal/datastore/memdb/schema.go +++ b/internal/datastore/memdb/schema.go @@ -12,6 +12,7 @@ import ( "github.com/authzed/spicedb/pkg/datastore" core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/tuple" ) const ( @@ -107,7 +108,7 @@ func (r relationship) MarshalZerologObject(e *zerolog.Event) { e.Str("rel", r.String()) } -func (r relationship) Relationship() *v1.Relationship { +func (r relationship) V1Relationship() *v1.Relationship { return &v1.Relationship{ Resource: &v1.ObjectReference{ ObjectType: r.namespace, @@ -124,10 +125,10 @@ func (r relationship) Relationship() *v1.Relationship { } } -func (r relationship) RelationTuple() (*core.RelationTuple, error) { +func (r relationship) Relationship() (tuple.Relationship, error) { cr, err := r.caveat.ContextualizedCaveat() if err != nil { - return nil, err + return tuple.Relationship{}, err } var ig *core.RelationshipIntegrity @@ -135,19 +136,21 @@ func (r relationship) RelationTuple() (*core.RelationTuple, error) { ig = r.integrity.RelationshipIntegrity() } - return &core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: r.namespace, - ObjectId: r.resourceID, - Relation: r.relation, - }, - Subject: &core.ObjectAndRelation{ - Namespace: r.subjectNamespace, - ObjectId: r.subjectObjectID, - Relation: r.subjectRelation, + return tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.ObjectAndRelation{ + ObjectType: r.namespace, + ObjectID: r.resourceID, + Relation: r.relation, + }, + Subject: tuple.ObjectAndRelation{ + ObjectType: r.subjectNamespace, + ObjectID: r.subjectObjectID, + Relation: r.subjectRelation, + }, }, - Caveat: cr, - Integrity: ig, + OptionalCaveat: cr, + OptionalIntegrity: ig, }, nil } diff --git a/internal/datastore/mysql/datastore.go b/internal/datastore/mysql/datastore.go index 3309c1b468..9d6d33827f 100644 --- a/internal/datastore/mysql/datastore.go +++ b/internal/datastore/mysql/datastore.go @@ -29,7 +29,7 @@ import ( log "github.com/authzed/spicedb/internal/logging" "github.com/authzed/spicedb/pkg/datastore" "github.com/authzed/spicedb/pkg/datastore/options" - core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/tuple" ) const ( @@ -426,52 +426,79 @@ func newMySQLExecutor(tx querier) common.ExecuteQueryFunc { // // Prepared statements are also not used given they perform poorly on environments where connections have // short lifetime (e.g. to gracefully handle load-balancer connection drain) - return func(ctx context.Context, sqlQuery string, args []interface{}) ([]*core.RelationTuple, error) { - span := trace.SpanFromContext(ctx) + return func(ctx context.Context, sqlQuery string, args []interface{}) (datastore.RelationshipIterator, error) { + return func(yield func(tuple.Relationship, error) bool) { + span := trace.SpanFromContext(ctx) - rows, err := tx.QueryContext(ctx, sqlQuery, args...) - if err != nil { - return nil, fmt.Errorf(errUnableToQueryTuples, err) - } - defer common.LogOnError(ctx, rows.Close) - - span.AddEvent("Query issued to database") - - var tuples []*core.RelationTuple - for rows.Next() { - nextTuple := &core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{}, - Subject: &core.ObjectAndRelation{}, - } - - var caveatName string - var caveatContext structpbWrapper - err := rows.Scan( - &nextTuple.ResourceAndRelation.Namespace, - &nextTuple.ResourceAndRelation.ObjectId, - &nextTuple.ResourceAndRelation.Relation, - &nextTuple.Subject.Namespace, - &nextTuple.Subject.ObjectId, - &nextTuple.Subject.Relation, - &caveatName, - &caveatContext, - ) + rows, err := tx.QueryContext(ctx, sqlQuery, args...) if err != nil { - return nil, fmt.Errorf(errUnableToQueryTuples, err) + yield(tuple.Relationship{}, fmt.Errorf(errUnableToQueryTuples, err)) + return } - - nextTuple.Caveat, err = common.ContextualizedCaveatFrom(caveatName, caveatContext) - if err != nil { - return nil, fmt.Errorf(errUnableToQueryTuples, err) + defer common.LogOnError(ctx, rows.Close) + + span.AddEvent("Query issued to database") + + relCount := 0 + + defer func() { + span.AddEvent("Relationships loaded", trace.WithAttributes(attribute.Int("relCount", relCount))) + }() + + for rows.Next() { + var resourceObjectType string + var resourceObjectID string + var relation string + var subjectObjectType string + var subjectObjectID string + var subjectRelation string + var caveatName string + var caveatContext structpbWrapper + err := rows.Scan( + &resourceObjectType, + &resourceObjectID, + &relation, + &subjectObjectType, + &subjectObjectID, + &subjectRelation, + &caveatName, + &caveatContext, + ) + if err != nil { + yield(tuple.Relationship{}, fmt.Errorf(errUnableToQueryTuples, err)) + return + } + + caveat, err := common.ContextualizedCaveatFrom(caveatName, caveatContext) + if err != nil { + yield(tuple.Relationship{}, fmt.Errorf(errUnableToQueryTuples, err)) + return + } + + relCount++ + if !yield(tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.ObjectAndRelation{ + ObjectType: resourceObjectType, + ObjectID: resourceObjectID, + Relation: relation, + }, + Subject: tuple.ObjectAndRelation{ + ObjectType: subjectObjectType, + ObjectID: subjectObjectID, + Relation: subjectRelation, + }, + }, + OptionalCaveat: caveat, + }, nil) { + return + } } - - tuples = append(tuples, nextTuple) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf(errUnableToQueryTuples, err) - } - span.AddEvent("Tuples loaded", trace.WithAttributes(attribute.Int("tupleCount", len(tuples)))) - return tuples, nil + if err := rows.Err(); err != nil { + yield(tuple.Relationship{}, fmt.Errorf(errUnableToQueryTuples, err)) + return + } + }, nil } } diff --git a/internal/datastore/mysql/datastore_test.go b/internal/datastore/mysql/datastore_test.go index 671749f595..81c6b7389c 100644 --- a/internal/datastore/mysql/datastore_test.go +++ b/internal/datastore/mysql/datastore_test.go @@ -24,7 +24,6 @@ import ( "github.com/authzed/spicedb/pkg/datastore/test" "github.com/authzed/spicedb/pkg/migrate" "github.com/authzed/spicedb/pkg/namespace" - corev1 "github.com/authzed/spicedb/pkg/proto/core/v1" "github.com/authzed/spicedb/pkg/tuple" ) @@ -215,8 +214,8 @@ func GarbageCollectionTest(t *testing.T, ds datastore.Datastore) { req.Equal(int64(2), removed.Namespaces) // Write a relationship. - tpl := tuple.Parse("resource:someresource#reader@user:someuser#...") - relWrittenAt, err := common.WriteTuples(ctx, ds, corev1.RelationTupleUpdate_CREATE, tpl) + rel := tuple.MustParse("resource:someresource#reader@user:someuser#...") + relWrittenAt, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, rel) req.NoError(err) // Run GC at the transaction and ensure no relationships are removed, but 1 transaction (the previous write namespace) is. @@ -234,12 +233,12 @@ func GarbageCollectionTest(t *testing.T, ds datastore.Datastore) { req.Zero(removed.Namespaces) // Ensure the relationship is still present. - tRequire := testfixtures.TupleChecker{Require: req, DS: ds} - tRequire.TupleExists(ctx, tpl, relWrittenAt) + tRequire := testfixtures.RelationshipChecker{Require: req, DS: ds} + tRequire.RelationshipExists(ctx, rel, relWrittenAt) // Overwrite the relationship. - ctpl := tuple.MustWithCaveat(tpl, "somecaveat") - relOverwrittenAt, err := common.WriteTuples(ctx, ds, corev1.RelationTupleUpdate_TOUCH, ctpl) + crel := tuple.MustWithCaveat(rel, "somecaveat") + relOverwrittenAt, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationTouch, crel) req.NoError(err) // Run GC at the transaction and ensure the (older copy of the) relationship is removed, as well as 1 transaction (the write). @@ -257,14 +256,14 @@ func GarbageCollectionTest(t *testing.T, ds datastore.Datastore) { req.Zero(removed.Namespaces) // Ensure the relationship is still present. - tRequire.TupleExists(ctx, ctpl, relOverwrittenAt) + tRequire.RelationshipExists(ctx, crel, relOverwrittenAt) // Delete the relationship. - relDeletedAt, err := common.WriteTuples(ctx, ds, corev1.RelationTupleUpdate_DELETE, ctpl) + relDeletedAt, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationDelete, crel) req.NoError(err) // Ensure the relationship is gone. - tRequire.NoTupleExists(ctx, ctpl, relDeletedAt) + tRequire.NoRelationshipExists(ctx, crel, relDeletedAt) // Run GC at the transaction and ensure the relationship is removed, as well as 1 transaction (the overwrite). removed, err = mds.DeleteBeforeTx(ctx, relDeletedAt) @@ -281,16 +280,16 @@ func GarbageCollectionTest(t *testing.T, ds datastore.Datastore) { req.Zero(removed.Namespaces) // Write the relationship a few times. - ctpl1 := tuple.MustWithCaveat(tpl, "somecaveat1") - ctpl2 := tuple.MustWithCaveat(tpl, "somecaveat2") - ctpl3 := tuple.MustWithCaveat(tpl, "somecaveat3") - _, err = common.WriteTuples(ctx, ds, corev1.RelationTupleUpdate_TOUCH, ctpl1) + crel1 := tuple.MustWithCaveat(rel, "somecaveat1") + crel2 := tuple.MustWithCaveat(rel, "somecaveat2") + crel3 := tuple.MustWithCaveat(rel, "somecaveat3") + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationTouch, crel1) req.NoError(err) - _, err = common.WriteTuples(ctx, ds, corev1.RelationTupleUpdate_TOUCH, ctpl2) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationTouch, crel2) req.NoError(err) - relLastWriteAt, err := common.WriteTuples(ctx, ds, corev1.RelationTupleUpdate_TOUCH, ctpl3) + relLastWriteAt, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationTouch, crel3) req.NoError(err) // Run GC at the transaction and ensure the older copies of the relationships are removed, @@ -302,7 +301,7 @@ func GarbageCollectionTest(t *testing.T, ds datastore.Datastore) { req.Zero(removed.Namespaces) // Ensure the relationship is still present. - tRequire.TupleExists(ctx, ctpl3, relLastWriteAt) + tRequire.RelationshipExists(ctx, crel3, relLastWriteAt) } func GarbageCollectionByTimeTest(t *testing.T, ds datastore.Datastore) { @@ -332,9 +331,9 @@ func GarbageCollectionByTimeTest(t *testing.T, ds datastore.Datastore) { time.Sleep(1 * time.Millisecond) // Write a relationship. - tpl := tuple.Parse("resource:someresource#reader@user:someuser#...") + rel := tuple.MustParse("resource:someresource#reader@user:someuser#...") - relLastWriteAt, err := common.WriteTuples(ctx, ds, corev1.RelationTupleUpdate_CREATE, tpl) + relLastWriteAt, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, rel) req.NoError(err) // Run GC and ensure only transactions were removed. @@ -351,14 +350,14 @@ func GarbageCollectionByTimeTest(t *testing.T, ds datastore.Datastore) { req.Zero(removed.Namespaces) // Ensure the relationship is still present. - tRequire := testfixtures.TupleChecker{Require: req, DS: ds} - tRequire.TupleExists(ctx, tpl, relLastWriteAt) + tRequire := testfixtures.RelationshipChecker{Require: req, DS: ds} + tRequire.RelationshipExists(ctx, rel, relLastWriteAt) // Sleep 1ms to ensure GC will delete the previous write. time.Sleep(1 * time.Millisecond) // Delete the relationship. - relDeletedAt, err := common.WriteTuples(ctx, ds, corev1.RelationTupleUpdate_DELETE, tpl) + relDeletedAt, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationDelete, rel) req.NoError(err) // Run GC and ensure the relationship is removed. @@ -375,7 +374,7 @@ func GarbageCollectionByTimeTest(t *testing.T, ds datastore.Datastore) { req.Zero(removed.Namespaces) // Ensure the relationship is still not present. - tRequire.NoTupleExists(ctx, tpl, relDeletedAt) + tRequire.NoRelationshipExists(ctx, rel, relDeletedAt) } func EmptyGarbageCollectionTest(t *testing.T, ds datastore.Datastore) { @@ -463,20 +462,20 @@ func ChunkedGarbageCollectionTest(t *testing.T, ds datastore.Datastore) { mds := ds.(*Datastore) // Prepare relationships to write. - var tuples []*corev1.RelationTuple + var rels []tuple.Relationship for i := 0; i < chunkRelationshipCount; i++ { - tpl := tuple.Parse(fmt.Sprintf("resource:resource-%d#reader@user:someuser#...", i)) - tuples = append(tuples, tpl) + rel := tuple.MustParse(fmt.Sprintf("resource:resource-%d#reader@user:someuser#...", i)) + rels = append(rels, rel) } // Write a large number of relationships. - writtenAt, err := common.WriteTuples(ctx, ds, corev1.RelationTupleUpdate_CREATE, tuples...) + writtenAt, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, rels...) req.NoError(err) // Ensure the relationships were written. - tRequire := testfixtures.TupleChecker{Require: req, DS: ds} - for _, tpl := range tuples { - tRequire.TupleExists(ctx, tpl, writtenAt) + tRequire := testfixtures.RelationshipChecker{Require: req, DS: ds} + for _, rel := range rels { + tRequire.RelationshipExists(ctx, rel, writtenAt) } // Run GC and ensure only transactions were removed. @@ -496,12 +495,12 @@ func ChunkedGarbageCollectionTest(t *testing.T, ds datastore.Datastore) { time.Sleep(1 * time.Millisecond) // Delete all the relationships. - deletedAt, err := common.WriteTuples(ctx, ds, corev1.RelationTupleUpdate_DELETE, tuples...) + deletedAt, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationDelete, rels...) req.NoError(err) // Ensure the relationships were deleted. - for _, tpl := range tuples { - tRequire.NoTupleExists(ctx, tpl, deletedAt) + for _, rel := range rels { + tRequire.NoRelationshipExists(ctx, rel, deletedAt) } // Sleep to ensure GC. diff --git a/internal/datastore/mysql/query_builder.go b/internal/datastore/mysql/query_builder.go index 2356dc605a..32a21c6830 100644 --- a/internal/datastore/mysql/query_builder.go +++ b/internal/datastore/mysql/query_builder.go @@ -12,23 +12,23 @@ type QueryBuilder struct { GetLastRevision sq.SelectBuilder LoadRevisionRange sq.SelectBuilder - WriteNamespaceQuery sq.InsertBuilder - ReadNamespaceQuery sq.SelectBuilder - DeleteNamespaceQuery sq.UpdateBuilder - DeleteNamespaceTuplesQuery sq.UpdateBuilder + WriteNamespaceQuery sq.InsertBuilder + ReadNamespaceQuery sq.SelectBuilder + DeleteNamespaceQuery sq.UpdateBuilder + DeleteNamespaceRelationshipsQuery sq.UpdateBuilder ReadCounterQuery sq.SelectBuilder InsertCounterQuery sq.InsertBuilder DeleteCounterQuery sq.UpdateBuilder UpdateCounterQuery sq.UpdateBuilder - QueryTuplesWithIdsQuery sq.SelectBuilder - QueryTuplesQuery sq.SelectBuilder - DeleteTupleQuery sq.UpdateBuilder - QueryTupleExistsQuery sq.SelectBuilder - WriteTupleQuery sq.InsertBuilder - QueryChangedQuery sq.SelectBuilder - CountTupleQuery sq.SelectBuilder + QueryRelsWithIdsQuery sq.SelectBuilder + QueryRelsQuery sq.SelectBuilder + DeleteRelsQuery sq.UpdateBuilder + QueryRelationshipExistsQuery sq.SelectBuilder + WriteRelsQuery sq.InsertBuilder + QueryChangedQuery sq.SelectBuilder + CountRelsQuery sq.SelectBuilder WriteCaveatQuery sq.InsertBuilder ReadCaveatQuery sq.SelectBuilder @@ -57,14 +57,14 @@ func NewQueryBuilder(driver *migrations.MySQLDriver) *QueryBuilder { builder.UpdateCounterQuery = updateCounter(driver.RelationshipCounters()) // tuple builders - builder.QueryTuplesWithIdsQuery = queryTuplesWithIds(driver.RelationTuple()) - builder.DeleteNamespaceTuplesQuery = deleteNamespaceTuples(driver.RelationTuple()) - builder.QueryTuplesQuery = queryTuples(driver.RelationTuple()) - builder.DeleteTupleQuery = deleteTuple(driver.RelationTuple()) - builder.QueryTupleExistsQuery = queryTupleExists(driver.RelationTuple()) - builder.WriteTupleQuery = writeTuple(driver.RelationTuple()) + builder.QueryRelsWithIdsQuery = queryRelationshipsWithIds(driver.RelationTuple()) + builder.DeleteNamespaceRelationshipsQuery = deleteNamespaceRelationships(driver.RelationTuple()) + builder.QueryRelsQuery = queryRelationships(driver.RelationTuple()) + builder.DeleteRelsQuery = deleteRelationship(driver.RelationTuple()) + builder.QueryRelationshipExistsQuery = queryRelationshipExists(driver.RelationTuple()) + builder.WriteRelsQuery = writeRelationship(driver.RelationTuple()) builder.QueryChangedQuery = queryChanged(driver.RelationTuple()) - builder.CountTupleQuery = countTuples(driver.RelationTuple()) + builder.CountRelsQuery = countRels(driver.RelationTuple()) // caveat builders builder.ReadCaveatQuery = readCaveat(driver.Caveat()) @@ -146,11 +146,11 @@ func deleteNamespace(tableNamespace string) sq.UpdateBuilder { return sb.Update(tableNamespace).Where(sq.Eq{colDeletedTxn: liveDeletedTxnID}) } -func deleteNamespaceTuples(tableTuple string) sq.UpdateBuilder { +func deleteNamespaceRelationships(tableTuple string) sq.UpdateBuilder { return sb.Update(tableTuple).Where(sq.Eq{colDeletedTxn: liveDeletedTxnID}) } -func queryTuplesWithIds(tableTuple string) sq.SelectBuilder { +func queryRelationshipsWithIds(tableTuple string) sq.SelectBuilder { return sb.Select( colID, colNamespace, @@ -164,7 +164,7 @@ func queryTuplesWithIds(tableTuple string) sq.SelectBuilder { ).From(tableTuple) } -func queryTuples(tableTuple string) sq.SelectBuilder { +func queryRelationships(tableTuple string) sq.SelectBuilder { return sb.Select( colNamespace, colObjectID, @@ -177,21 +177,21 @@ func queryTuples(tableTuple string) sq.SelectBuilder { ).From(tableTuple) } -func countTuples(tableTuple string) sq.SelectBuilder { +func countRels(tableTuple string) sq.SelectBuilder { return sb.Select( "count(*)", ).From(tableTuple) } -func deleteTuple(tableTuple string) sq.UpdateBuilder { +func deleteRelationship(tableTuple string) sq.UpdateBuilder { return sb.Update(tableTuple).Where(sq.Eq{colDeletedTxn: liveDeletedTxnID}) } -func queryTupleExists(tableTuple string) sq.SelectBuilder { +func queryRelationshipExists(tableTuple string) sq.SelectBuilder { return sb.Select(colID).From(tableTuple) } -func writeTuple(tableTuple string) sq.InsertBuilder { +func writeRelationship(tableTuple string) sq.InsertBuilder { return sb.Insert(tableTuple).Columns( colNamespace, colObjectID, diff --git a/internal/datastore/mysql/reader.go b/internal/datastore/mysql/reader.go index ce1ee33a3f..8d523f09e5 100644 --- a/internal/datastore/mysql/reader.go +++ b/internal/datastore/mysql/reader.go @@ -66,7 +66,7 @@ func (mr *mysqlReader) CountRelationships(ctx context.Context, name string) (int return 0, err } - qBuilder, err := common.NewSchemaQueryFilterer(schema, mr.filterer(mr.CountTupleQuery), mr.filterMaximumIDCount).FilterWithRelationshipsFilter(relFilter) + qBuilder, err := common.NewSchemaQueryFilterer(schema, mr.filterer(mr.CountRelsQuery), mr.filterMaximumIDCount).FilterWithRelationshipsFilter(relFilter) if err != nil { return 0, err } @@ -175,7 +175,7 @@ func (mr *mysqlReader) QueryRelationships( filter datastore.RelationshipsFilter, opts ...options.QueryOptionsOption, ) (iter datastore.RelationshipIterator, err error) { - qBuilder, err := common.NewSchemaQueryFilterer(schema, mr.filterer(mr.QueryTuplesQuery), mr.filterMaximumIDCount).FilterWithRelationshipsFilter(filter) + qBuilder, err := common.NewSchemaQueryFilterer(schema, mr.filterer(mr.QueryRelsQuery), mr.filterMaximumIDCount).FilterWithRelationshipsFilter(filter) if err != nil { return nil, err } @@ -188,7 +188,7 @@ func (mr *mysqlReader) ReverseQueryRelationships( subjectsFilter datastore.SubjectsFilter, opts ...options.ReverseQueryOptionsOption, ) (iter datastore.RelationshipIterator, err error) { - qBuilder, err := common.NewSchemaQueryFilterer(schema, mr.filterer(mr.QueryTuplesQuery), mr.filterMaximumIDCount). + qBuilder, err := common.NewSchemaQueryFilterer(schema, mr.filterer(mr.QueryRelsQuery), mr.filterMaximumIDCount). FilterWithSubjectsSelectors(subjectsFilter.AsSelector()) if err != nil { return nil, err diff --git a/internal/datastore/mysql/readwrite.go b/internal/datastore/mysql/readwrite.go index 6f536a7754..3ad0893760 100644 --- a/internal/datastore/mysql/readwrite.go +++ b/internal/datastore/mysql/readwrite.go @@ -168,32 +168,32 @@ func (rwt *mysqlReadWriteTXN) StoreCounterValue(ctx context.Context, name string // WriteRelationships takes a list of existing relationships that must exist, and a list of // tuple mutations and applies it to the datastore for the specified namespace. -func (rwt *mysqlReadWriteTXN) WriteRelationships(ctx context.Context, mutations []*core.RelationTupleUpdate) error { +func (rwt *mysqlReadWriteTXN) WriteRelationships(ctx context.Context, mutations []tuple.RelationshipUpdate) error { // TODO(jschorr): Determine if we can do this in a more efficient manner using ON CONFLICT UPDATE // rather than SELECT FOR UPDATE as we've been doing. - bulkWrite := rwt.WriteTupleQuery + bulkWrite := rwt.WriteRelsQuery bulkWriteHasValues := false - selectForUpdateQuery := rwt.QueryTuplesWithIdsQuery + selectForUpdateQuery := rwt.QueryRelsWithIdsQuery clauses := sq.Or{} - createAndTouchMutationsByTuple := make(map[string]*core.RelationTupleUpdate, len(mutations)) + createAndTouchMutationsByRel := make(map[string]tuple.RelationshipUpdate, len(mutations)) // Collect all TOUCH and DELETE operations. CREATE is handled below. for _, mut := range mutations { - tpl := mut.Tuple - tplString := tuple.StringWithoutCaveat(tpl) + rel := mut.Relationship + relString := tuple.StringWithoutCaveat(rel) switch mut.Operation { - case core.RelationTupleUpdate_CREATE: - createAndTouchMutationsByTuple[tplString] = mut + case tuple.UpdateOperationCreate: + createAndTouchMutationsByRel[relString] = mut - case core.RelationTupleUpdate_TOUCH: - createAndTouchMutationsByTuple[tplString] = mut - clauses = append(clauses, exactRelationshipClause(tpl)) + case tuple.UpdateOperationTouch: + createAndTouchMutationsByRel[relString] = mut + clauses = append(clauses, exactRelationshipClause(rel)) - case core.RelationTupleUpdate_DELETE: - clauses = append(clauses, exactRelationshipClause(tpl)) + case tuple.UpdateOperationDelete: + clauses = append(clauses, exactRelationshipClause(rel)) default: return spiceerrors.MustBugf("unknown mutation operation") @@ -215,58 +215,74 @@ func (rwt *mysqlReadWriteTXN) WriteRelationships(ctx context.Context, mutations } defer common.LogOnError(ctx, rows.Close) - foundTpl := &core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{}, - Subject: &core.ObjectAndRelation{}, - } - + var resourceObjectType string + var resourceObjectID string + var relation string + var subjectObjectType string + var subjectObjectID string + var subjectRelation string var caveatName string var caveatContext structpbWrapper - tupleIdsToDelete := make([]int64, 0, len(clauses)) + relIdsToDelete := make([]int64, 0, len(clauses)) for rows.Next() { - var tupleID int64 + var relationshipID int64 if err := rows.Scan( - &tupleID, - &foundTpl.ResourceAndRelation.Namespace, - &foundTpl.ResourceAndRelation.ObjectId, - &foundTpl.ResourceAndRelation.Relation, - &foundTpl.Subject.Namespace, - &foundTpl.Subject.ObjectId, - &foundTpl.Subject.Relation, + &relationshipID, + &resourceObjectType, + &resourceObjectID, + &relation, + &subjectObjectType, + &subjectObjectID, + &subjectRelation, &caveatName, &caveatContext, ); err != nil { return fmt.Errorf(errUnableToWriteRelationships, err) } + foundRel := tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.ObjectAndRelation{ + ObjectType: resourceObjectType, + ObjectID: resourceObjectID, + Relation: relation, + }, + Subject: tuple.ObjectAndRelation{ + ObjectType: subjectObjectType, + ObjectID: subjectObjectID, + Relation: subjectRelation, + }, + }, + } + // if the relationship to be deleted is for a TOUCH operation and the caveat // name or context has not changed, then remove it from delete and create. - tplString := tuple.StringWithoutCaveat(foundTpl) - if mut, ok := createAndTouchMutationsByTuple[tplString]; ok { - foundTpl.Caveat, err = common.ContextualizedCaveatFrom(caveatName, caveatContext) + tplString := tuple.StringWithoutCaveat(foundRel) + if mut, ok := createAndTouchMutationsByRel[tplString]; ok { + foundRel.OptionalCaveat, err = common.ContextualizedCaveatFrom(caveatName, caveatContext) if err != nil { return fmt.Errorf(errUnableToQueryTuples, err) } // Ensure the tuples are the same. - if tuple.Equal(mut.Tuple, foundTpl) { - delete(createAndTouchMutationsByTuple, tplString) + if tuple.Equal(mut.Relationship, foundRel) { + delete(createAndTouchMutationsByRel, tplString) continue } } - tupleIdsToDelete = append(tupleIdsToDelete, tupleID) + relIdsToDelete = append(relIdsToDelete, relationshipID) } if rows.Err() != nil { return fmt.Errorf(errUnableToWriteRelationships, rows.Err()) } - if len(tupleIdsToDelete) > 0 { + if len(relIdsToDelete) > 0 { query, args, err := rwt. - DeleteTupleQuery. - Where(sq.Eq{colID: tupleIdsToDelete}). + DeleteRelsQuery. + Where(sq.Eq{colID: relIdsToDelete}). Set(colDeletedTxn, rwt.newTxnID). ToSql() if err != nil { @@ -278,22 +294,22 @@ func (rwt *mysqlReadWriteTXN) WriteRelationships(ctx context.Context, mutations } } - for _, mut := range createAndTouchMutationsByTuple { - tpl := mut.Tuple + for _, mut := range createAndTouchMutationsByRel { + rel := mut.Relationship var caveatName string var caveatContext structpbWrapper - if tpl.Caveat != nil { - caveatName = tpl.Caveat.CaveatName - caveatContext = tpl.Caveat.Context.AsMap() + if rel.OptionalCaveat != nil { + caveatName = rel.OptionalCaveat.CaveatName + caveatContext = rel.OptionalCaveat.Context.AsMap() } bulkWrite = bulkWrite.Values( - tpl.ResourceAndRelation.Namespace, - tpl.ResourceAndRelation.ObjectId, - tpl.ResourceAndRelation.Relation, - tpl.Subject.Namespace, - tpl.Subject.ObjectId, - tpl.Subject.Relation, + rel.Resource.ObjectType, + rel.Resource.ObjectID, + rel.Resource.Relation, + rel.Subject.ObjectType, + rel.Subject.ObjectID, + rel.Subject.Relation, caveatName, &caveatContext, rwt.newTxnID, @@ -318,7 +334,7 @@ func (rwt *mysqlReadWriteTXN) WriteRelationships(ctx context.Context, mutations func (rwt *mysqlReadWriteTXN) DeleteRelationships(ctx context.Context, filter *v1.RelationshipFilter, opts ...options.DeleteOptionsOption) (bool, error) { // Add clauses for the ResourceFilter - query := rwt.DeleteTupleQuery + query := rwt.DeleteRelsQuery if filter.ResourceType != "" { query = query.Where(sq.Eq{colNamespace: filter.ResourceType}) } @@ -461,7 +477,7 @@ func (rwt *mysqlReadWriteTXN) DeleteNamespaces(ctx context.Context, nsNames ...s return fmt.Errorf(errUnableToDeleteConfig, err) } - deleteTupleSQL, deleteTupleArgs, err := rwt.DeleteNamespaceTuplesQuery. + deleteTupleSQL, deleteTupleArgs, err := rwt.DeleteNamespaceRelationshipsQuery. Set(colDeletedTxn, rwt.newTxnID). Where(sq.Or(tplClauses)). ToSql() @@ -480,41 +496,41 @@ func (rwt *mysqlReadWriteTXN) DeleteNamespaces(ctx context.Context, nsNames ...s func (rwt *mysqlReadWriteTXN) BulkLoad(ctx context.Context, iter datastore.BulkWriteRelationshipSource) (uint64, error) { var sqlStmt bytes.Buffer - sql, _, err := rwt.WriteTupleQuery.Values(1, 2, 3, 4, 5, 6, 7, 8, 9).ToSql() + sql, _, err := rwt.WriteRelsQuery.Values(1, 2, 3, 4, 5, 6, 7, 8, 9).ToSql() if err != nil { return 0, err } var numWritten uint64 - var tpl *core.RelationTuple + var rel *tuple.Relationship // Bootstrap the loop - tpl, err = iter.Next(ctx) + rel, err = iter.Next(ctx) - for tpl != nil && err == nil { + for rel != nil && err == nil { sqlStmt.Reset() sqlStmt.WriteString(sql) var args []interface{} var batchLen uint64 - for ; tpl != nil && err == nil && batchLen < bulkInsertRowsLimit; tpl, err = iter.Next(ctx) { + for ; rel != nil && err == nil && batchLen < bulkInsertRowsLimit; rel, err = iter.Next(ctx) { if batchLen != 0 { sqlStmt.WriteString(",(?,?,?,?,?,?,?,?,?)") } var caveatName string var caveatContext structpbWrapper - if tpl.Caveat != nil { - caveatName = tpl.Caveat.CaveatName - caveatContext = tpl.Caveat.Context.AsMap() + if rel.OptionalCaveat != nil { + caveatName = rel.OptionalCaveat.CaveatName + caveatContext = rel.OptionalCaveat.Context.AsMap() } args = append(args, - tpl.ResourceAndRelation.Namespace, - tpl.ResourceAndRelation.ObjectId, - tpl.ResourceAndRelation.Relation, - tpl.Subject.Namespace, - tpl.Subject.ObjectId, - tpl.Subject.Relation, + rel.Resource.ObjectType, + rel.Resource.ObjectID, + rel.Resource.Relation, + rel.Subject.ObjectType, + rel.Subject.ObjectID, + rel.Subject.Relation, caveatName, &caveatContext, rwt.newTxnID, @@ -548,16 +564,18 @@ func convertToWriteConstraintError(err error) error { if found != nil { parts := strings.Split(found[1], "-") if len(parts) == 7 { - return common.NewCreateRelationshipExistsError(&core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: parts[0], - ObjectId: parts[1], - Relation: parts[2], - }, - Subject: &core.ObjectAndRelation{ - Namespace: parts[3], - ObjectId: parts[4], - Relation: parts[5], + return common.NewCreateRelationshipExistsError(&tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.ObjectAndRelation{ + ObjectType: parts[0], + ObjectID: parts[1], + Relation: parts[2], + }, + Subject: tuple.ObjectAndRelation{ + ObjectType: parts[3], + ObjectID: parts[4], + Relation: parts[5], + }, }, }) } @@ -567,16 +585,18 @@ func convertToWriteConstraintError(err error) error { if found != nil { parts := strings.Split(found[1], "-") if len(parts) == 7 { - return common.NewCreateRelationshipExistsError(&core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: parts[0], - ObjectId: parts[1], - Relation: parts[2], - }, - Subject: &core.ObjectAndRelation{ - Namespace: parts[3], - ObjectId: parts[4], - Relation: parts[5], + return common.NewCreateRelationshipExistsError(&tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.ObjectAndRelation{ + ObjectType: parts[0], + ObjectID: parts[1], + Relation: parts[2], + }, + Subject: tuple.ObjectAndRelation{ + ObjectType: parts[3], + ObjectID: parts[4], + Relation: parts[5], + }, }, }) } @@ -587,13 +607,13 @@ func convertToWriteConstraintError(err error) error { return nil } -func exactRelationshipClause(r *core.RelationTuple) sq.Eq { +func exactRelationshipClause(r tuple.Relationship) sq.Eq { return sq.Eq{ - colNamespace: r.ResourceAndRelation.Namespace, - colObjectID: r.ResourceAndRelation.ObjectId, - colRelation: r.ResourceAndRelation.Relation, - colUsersetNamespace: r.Subject.Namespace, - colUsersetObjectID: r.Subject.ObjectId, + colNamespace: r.Resource.ObjectType, + colObjectID: r.Resource.ObjectID, + colRelation: r.Resource.Relation, + colUsersetNamespace: r.Subject.ObjectType, + colUsersetObjectID: r.Subject.ObjectID, colUsersetRelation: r.Subject.Relation, } } diff --git a/internal/datastore/mysql/stats.go b/internal/datastore/mysql/stats.go index 340e943c55..b6841eb2d2 100644 --- a/internal/datastore/mysql/stats.go +++ b/internal/datastore/mysql/stats.go @@ -53,7 +53,7 @@ func (mds *Datastore) Statistics(ctx context.Context) (datastore.Stats, error) { if !count.Valid || count.Int64 == 0 { // If we get a count of zero, its possible the information schema table has not yet // been updated, so we use a slower count(*) call. - query, args, err := mds.QueryBuilder.CountTupleQuery.ToSql() + query, args, err := mds.QueryBuilder.CountRelsQuery.ToSql() if err != nil { return datastore.Stats{}, err } diff --git a/internal/datastore/mysql/watch.go b/internal/datastore/mysql/watch.go index 14e356796f..a8f8b19ce6 100644 --- a/internal/datastore/mysql/watch.go +++ b/internal/datastore/mysql/watch.go @@ -8,7 +8,7 @@ import ( "github.com/authzed/spicedb/internal/datastore/common" "github.com/authzed/spicedb/internal/datastore/revisions" "github.com/authzed/spicedb/pkg/datastore" - core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/tuple" sq "github.com/Masterminds/squirrel" ) @@ -194,22 +194,23 @@ func (mds *Datastore) loadChanges( defer common.LogOnError(ctx, rows.Close) for rows.Next() { - nextTuple := &core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{}, - Subject: &core.ObjectAndRelation{}, - } - + var resourceObjectType string + var resourceObjectID string + var relation string + var subjectObjectType string + var subjectObjectID string + var subjectRelation string var createdTxn uint64 var deletedTxn uint64 var caveatName string var caveatContext structpbWrapper err = rows.Scan( - &nextTuple.ResourceAndRelation.Namespace, - &nextTuple.ResourceAndRelation.ObjectId, - &nextTuple.ResourceAndRelation.Relation, - &nextTuple.Subject.Namespace, - &nextTuple.Subject.ObjectId, - &nextTuple.Subject.Relation, + &resourceObjectType, + &resourceObjectID, + &relation, + &subjectObjectType, + &subjectObjectID, + &subjectRelation, &caveatName, &caveatContext, &createdTxn, @@ -218,19 +219,35 @@ func (mds *Datastore) loadChanges( if err != nil { return } - nextTuple.Caveat, err = common.ContextualizedCaveatFrom(caveatName, caveatContext) + + relationship := tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.ObjectAndRelation{ + ObjectType: resourceObjectType, + ObjectID: resourceObjectID, + Relation: relation, + }, + Subject: tuple.ObjectAndRelation{ + ObjectType: subjectObjectType, + ObjectID: subjectObjectID, + Relation: subjectRelation, + }, + }, + } + + relationship.OptionalCaveat, err = common.ContextualizedCaveatFrom(caveatName, caveatContext) if err != nil { return } if createdTxn > afterRevision && createdTxn <= newRevision { - if err = stagedChanges.AddRelationshipChange(ctx, revisions.NewForTransactionID(createdTxn), nextTuple, core.RelationTupleUpdate_TOUCH); err != nil { + if err = stagedChanges.AddRelationshipChange(ctx, revisions.NewForTransactionID(createdTxn), relationship, tuple.UpdateOperationTouch); err != nil { return } } if deletedTxn > afterRevision && deletedTxn <= newRevision { - if err = stagedChanges.AddRelationshipChange(ctx, revisions.NewForTransactionID(deletedTxn), nextTuple, core.RelationTupleUpdate_DELETE); err != nil { + if err = stagedChanges.AddRelationshipChange(ctx, revisions.NewForTransactionID(deletedTxn), relationship, tuple.UpdateOperationDelete); err != nil { return } } diff --git a/internal/datastore/postgres/common/bulk.go b/internal/datastore/postgres/common/bulk.go index 1d12e7bd9c..1360fe288a 100644 --- a/internal/datastore/postgres/common/bulk.go +++ b/internal/datastore/postgres/common/bulk.go @@ -7,15 +7,15 @@ import ( "github.com/jackc/pgx/v5" "github.com/authzed/spicedb/pkg/datastore" - core "github.com/authzed/spicedb/pkg/proto/core/v1" "github.com/authzed/spicedb/pkg/spiceerrors" + "github.com/authzed/spicedb/pkg/tuple" ) type tupleSourceAdapter struct { source datastore.BulkWriteRelationshipSource ctx context.Context - current *core.RelationTuple + current *tuple.Relationship err error valuesBuffer []any colNames []string @@ -33,24 +33,24 @@ func (tg *tupleSourceAdapter) Next() bool { func (tg *tupleSourceAdapter) Values() ([]any, error) { var caveatName string var caveatContext map[string]any - if tg.current.Caveat != nil { - caveatName = tg.current.Caveat.CaveatName - caveatContext = tg.current.Caveat.Context.AsMap() + if tg.current.OptionalCaveat != nil { + caveatName = tg.current.OptionalCaveat.CaveatName + caveatContext = tg.current.OptionalCaveat.Context.AsMap() } - tg.valuesBuffer[0] = tg.current.ResourceAndRelation.Namespace - tg.valuesBuffer[1] = tg.current.ResourceAndRelation.ObjectId - tg.valuesBuffer[2] = tg.current.ResourceAndRelation.Relation - tg.valuesBuffer[3] = tg.current.Subject.Namespace - tg.valuesBuffer[4] = tg.current.Subject.ObjectId + tg.valuesBuffer[0] = tg.current.Resource.ObjectType + tg.valuesBuffer[1] = tg.current.Resource.ObjectID + tg.valuesBuffer[2] = tg.current.Resource.Relation + tg.valuesBuffer[3] = tg.current.Subject.ObjectType + tg.valuesBuffer[4] = tg.current.Subject.ObjectID tg.valuesBuffer[5] = tg.current.Subject.Relation tg.valuesBuffer[6] = caveatName tg.valuesBuffer[7] = caveatContext - if len(tg.colNames) > 8 && tg.current.Integrity != nil { - tg.valuesBuffer[8] = tg.current.Integrity.KeyId - tg.valuesBuffer[9] = tg.current.Integrity.Hash - tg.valuesBuffer[10] = tg.current.Integrity.HashedAt.AsTime() + if len(tg.colNames) > 8 && tg.current.OptionalIntegrity != nil { + tg.valuesBuffer[8] = tg.current.OptionalIntegrity.KeyId + tg.valuesBuffer[9] = tg.current.OptionalIntegrity.Hash + tg.valuesBuffer[10] = tg.current.OptionalIntegrity.HashedAt.AsTime() } return tg.valuesBuffer, nil diff --git a/internal/datastore/postgres/common/errors.go b/internal/datastore/postgres/common/errors.go index e71b7c8eb3..d94c482500 100644 --- a/internal/datastore/postgres/common/errors.go +++ b/internal/datastore/postgres/common/errors.go @@ -9,7 +9,7 @@ import ( "github.com/jackc/pgx/v5/pgconn" dscommon "github.com/authzed/spicedb/internal/datastore/common" - core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/tuple" ) const ( @@ -46,32 +46,36 @@ func ConvertToWriteConstraintError(livingTupleConstraints []string, err error) e if errors.As(err, &pgerr) && pgerr.Code == pgUniqueConstraintViolation && slices.Contains(livingTupleConstraints, pgerr.ConstraintName) { found := createConflictDetailsRegex.FindStringSubmatch(pgerr.Detail) if found != nil { - return dscommon.NewCreateRelationshipExistsError(&core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: strings.TrimSpace(found[2]), - ObjectId: strings.TrimSpace(found[3]), - Relation: strings.TrimSpace(found[4]), - }, - Subject: &core.ObjectAndRelation{ - Namespace: strings.TrimSpace(found[5]), - ObjectId: strings.TrimSpace(found[6]), - Relation: strings.TrimSpace(found[7]), + return dscommon.NewCreateRelationshipExistsError(&tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.ObjectAndRelation{ + ObjectType: strings.TrimSpace(found[2]), + ObjectID: strings.TrimSpace(found[3]), + Relation: strings.TrimSpace(found[4]), + }, + Subject: tuple.ObjectAndRelation{ + ObjectType: strings.TrimSpace(found[5]), + ObjectID: strings.TrimSpace(found[6]), + Relation: strings.TrimSpace(found[7]), + }, }, }) } found = createConflictDetailsRegexWithoutCaveat.FindStringSubmatch(pgerr.Detail) if found != nil { - return dscommon.NewCreateRelationshipExistsError(&core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: strings.TrimSpace(found[2]), - ObjectId: strings.TrimSpace(found[3]), - Relation: strings.TrimSpace(found[4]), - }, - Subject: &core.ObjectAndRelation{ - Namespace: strings.TrimSpace(found[5]), - ObjectId: strings.TrimSpace(found[6]), - Relation: strings.TrimSpace(found[7]), + return dscommon.NewCreateRelationshipExistsError(&tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.ObjectAndRelation{ + ObjectType: strings.TrimSpace(found[2]), + ObjectID: strings.TrimSpace(found[3]), + Relation: strings.TrimSpace(found[4]), + }, + Subject: tuple.ObjectAndRelation{ + ObjectType: strings.TrimSpace(found[5]), + ObjectID: strings.TrimSpace(found[6]), + Relation: strings.TrimSpace(found[7]), + }, }, }) } diff --git a/internal/datastore/postgres/common/pgx.go b/internal/datastore/postgres/common/pgx.go index 5eca685998..546fe98945 100644 --- a/internal/datastore/postgres/common/pgx.go +++ b/internal/datastore/postgres/common/pgx.go @@ -22,100 +22,131 @@ import ( "github.com/authzed/spicedb/internal/datastore/common" log "github.com/authzed/spicedb/internal/logging" + "github.com/authzed/spicedb/pkg/datastore" corev1 "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/tuple" ) const errUnableToQueryTuples = "unable to query tuples: %w" // NewPGXExecutor creates an executor that uses the pgx library to make the specified queries. func NewPGXExecutor(querier DBFuncQuerier) common.ExecuteQueryFunc { - return func(ctx context.Context, sql string, args []any) ([]*corev1.RelationTuple, error) { + return func(ctx context.Context, sql string, args []any) (datastore.RelationshipIterator, error) { span := trace.SpanFromContext(ctx) - return queryTuples(ctx, sql, args, span, querier, false) + return queryRels(ctx, sql, args, span, querier, false) } } func NewPGXExecutorWithIntegrityOption(querier DBFuncQuerier, withIntegrity bool) common.ExecuteQueryFunc { - return func(ctx context.Context, sql string, args []any) ([]*corev1.RelationTuple, error) { + return func(ctx context.Context, sql string, args []any) (datastore.RelationshipIterator, error) { span := trace.SpanFromContext(ctx) - return queryTuples(ctx, sql, args, span, querier, withIntegrity) + return queryRels(ctx, sql, args, span, querier, withIntegrity) } } -// queryTuples queries tuples for the given query and transaction. -func queryTuples(ctx context.Context, sqlStatement string, args []any, span trace.Span, tx DBFuncQuerier, withIntegrity bool) ([]*corev1.RelationTuple, error) { - var tuples []*corev1.RelationTuple - err := tx.QueryFunc(ctx, func(ctx context.Context, rows pgx.Rows) error { - span.AddEvent("Query issued to database") - - for rows.Next() { - nextTuple := &corev1.RelationTuple{ - ResourceAndRelation: &corev1.ObjectAndRelation{}, - Subject: &corev1.ObjectAndRelation{}, - } +// queryRels queries relationships for the given query and transaction. +func queryRels(ctx context.Context, sqlStatement string, args []any, span trace.Span, tx DBFuncQuerier, withIntegrity bool) (datastore.RelationshipIterator, error) { + return func(yield func(tuple.Relationship, error) bool) { + err := tx.QueryFunc(ctx, func(ctx context.Context, rows pgx.Rows) error { + span.AddEvent("Query issued to database") + + var resourceObjectType string + var resourceObjectID string + var resourceRelation string + var subjectObjectType string + var subjectObjectID string + var subjectRelation string var caveatName sql.NullString var caveatCtx map[string]any - if withIntegrity { - var integrityKeyID string - var integrityHash []byte - var timestamp time.Time - - if err := rows.Scan( - &nextTuple.ResourceAndRelation.Namespace, - &nextTuple.ResourceAndRelation.ObjectId, - &nextTuple.ResourceAndRelation.Relation, - &nextTuple.Subject.Namespace, - &nextTuple.Subject.ObjectId, - &nextTuple.Subject.Relation, - &caveatName, - &caveatCtx, - &integrityKeyID, - &integrityHash, - ×tamp, - ); err != nil { - return fmt.Errorf(errUnableToQueryTuples, fmt.Errorf("scan err: %w", err)) + relCount := 0 + for rows.Next() { + var integrity *corev1.RelationshipIntegrity + + if withIntegrity { + var integrityKeyID string + var integrityHash []byte + var timestamp time.Time + + if err := rows.Scan( + &resourceObjectType, + &resourceObjectID, + &resourceRelation, + &subjectObjectType, + &subjectObjectID, + &subjectRelation, + &caveatName, + &caveatCtx, + &integrityKeyID, + &integrityHash, + ×tamp, + ); err != nil { + return fmt.Errorf(errUnableToQueryTuples, fmt.Errorf("scan err: %w", err)) + } + + integrity = &corev1.RelationshipIntegrity{ + KeyId: integrityKeyID, + Hash: integrityHash, + HashedAt: timestamppb.New(timestamp), + } + } else { + if err := rows.Scan( + &resourceObjectType, + &resourceObjectID, + &resourceRelation, + &subjectObjectType, + &subjectObjectID, + &subjectRelation, + &caveatName, + &caveatCtx, + ); err != nil { + return fmt.Errorf(errUnableToQueryTuples, fmt.Errorf("scan err: %w", err)) + } } - nextTuple.Integrity = &corev1.RelationshipIntegrity{ - KeyId: integrityKeyID, - Hash: integrityHash, - HashedAt: timestamppb.New(timestamp), + var caveat *corev1.ContextualizedCaveat + if caveatName.Valid { + var err error + caveat, err = common.ContextualizedCaveatFrom(caveatName.String, caveatCtx) + if err != nil { + return fmt.Errorf(errUnableToQueryTuples, fmt.Errorf("unable to fetch caveat context: %w", err)) + } } - } else { - if err := rows.Scan( - &nextTuple.ResourceAndRelation.Namespace, - &nextTuple.ResourceAndRelation.ObjectId, - &nextTuple.ResourceAndRelation.Relation, - &nextTuple.Subject.Namespace, - &nextTuple.Subject.ObjectId, - &nextTuple.Subject.Relation, - &caveatName, - &caveatCtx, - ); err != nil { - return fmt.Errorf(errUnableToQueryTuples, fmt.Errorf("scan err: %w", err)) + + relCount++ + if !yield(tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.ObjectAndRelation{ + ObjectType: resourceObjectType, + ObjectID: resourceObjectID, + Relation: resourceRelation, + }, + Subject: tuple.ObjectAndRelation{ + ObjectType: subjectObjectType, + ObjectID: subjectObjectID, + Relation: subjectRelation, + }, + }, + OptionalCaveat: caveat, + OptionalIntegrity: integrity, + }, nil) { + return nil } } - caveat, err := common.ContextualizedCaveatFrom(caveatName.String, caveatCtx) - if err != nil { - return fmt.Errorf(errUnableToQueryTuples, fmt.Errorf("unable to fetch caveat context: %w", err)) + if err := rows.Err(); err != nil { + return fmt.Errorf(errUnableToQueryTuples, fmt.Errorf("rows err: %w", err)) } - nextTuple.Caveat = caveat - tuples = append(tuples, nextTuple) - } - if err := rows.Err(); err != nil { - return fmt.Errorf(errUnableToQueryTuples, fmt.Errorf("rows err: %w", err)) - } - - span.AddEvent("Tuples loaded", trace.WithAttributes(attribute.Int("tupleCount", len(tuples)))) - return nil - }, sqlStatement, args...) - if err != nil { - return nil, err - } - return tuples, nil + span.AddEvent("Rels loaded", trace.WithAttributes(attribute.Int("relCount", relCount))) + return nil + }, sqlStatement, args...) + if err != nil { + if !yield(tuple.Relationship{}, err) { + return + } + } + }, nil } // ParseConfigWithInstrumentation returns a pgx.ConnConfig that has been instrumented for observability diff --git a/internal/datastore/postgres/postgres_shared_test.go b/internal/datastore/postgres/postgres_shared_test.go index ee8df58b92..b63e5574f6 100644 --- a/internal/datastore/postgres/postgres_shared_test.go +++ b/internal/datastore/postgres/postgres_shared_test.go @@ -33,7 +33,6 @@ import ( "github.com/authzed/spicedb/pkg/datastore/test" "github.com/authzed/spicedb/pkg/migrate" "github.com/authzed/spicedb/pkg/namespace" - core "github.com/authzed/spicedb/pkg/proto/core/v1" "github.com/authzed/spicedb/pkg/tuple" ) @@ -318,9 +317,9 @@ func GarbageCollectionTest(t *testing.T, ds datastore.Datastore) { require.Equal(int64(2), removed.Namespaces) // resource, user // Write a relationship. - tpl := tuple.Parse("resource:someresource#reader@user:someuser#...") + rel := tuple.MustParse("resource:someresource#reader@user:someuser#...") - wroteOneRelationship, err := common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, tpl) + wroteOneRelationship, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, rel) require.NoError(err) // Run GC at the transaction and ensure no relationships are removed, but 1 transaction (the previous write namespace) is. @@ -338,12 +337,12 @@ func GarbageCollectionTest(t *testing.T, ds datastore.Datastore) { require.Zero(removed.Namespaces) // Ensure the relationship is still present. - tRequire := testfixtures.TupleChecker{Require: require, DS: ds} - tRequire.TupleExists(ctx, tpl, wroteOneRelationship) + tRequire := testfixtures.RelationshipChecker{Require: require, DS: ds} + tRequire.RelationshipExists(ctx, rel, wroteOneRelationship) // Overwrite the relationship by changing its caveat. - tpl = tuple.MustWithCaveat(tpl, "somecaveat") - relOverwrittenAt, err := common.WriteTuples(ctx, ds, core.RelationTupleUpdate_TOUCH, tpl) + rel = tuple.MustWithCaveat(rel, "somecaveat") + relOverwrittenAt, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationTouch, rel) require.NoError(err) // Run GC, which won't clean anything because we're dropping the write transaction only @@ -361,16 +360,16 @@ func GarbageCollectionTest(t *testing.T, ds datastore.Datastore) { require.Zero(removed.Namespaces) // Ensure the relationship is still present. - tRequire.TupleExists(ctx, tpl, relOverwrittenAt) + tRequire.RelationshipExists(ctx, rel, relOverwrittenAt) // Delete the relationship. - relDeletedAt, err := common.WriteTuples(ctx, ds, core.RelationTupleUpdate_DELETE, tpl) + relDeletedAt, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationDelete, rel) require.NoError(err) // Ensure the relationship is gone. - tRequire.NoTupleExists(ctx, tpl, relDeletedAt) + tRequire.NoRelationshipExists(ctx, rel, relDeletedAt) - // Run GC, which will now drop the overwrite transaction only and the first tpl revision + // Run GC, which will now drop the overwrite transaction only and the first rel revision removed, err = pds.DeleteBeforeTx(ctx, relDeletedAt) require.NoError(err) require.Equal(int64(1), removed.Relationships) @@ -387,10 +386,10 @@ func GarbageCollectionTest(t *testing.T, ds datastore.Datastore) { // Write a the relationship a few times. var relLastWriteAt datastore.Revision for i := 0; i < 3; i++ { - tpl = tuple.MustWithCaveat(tpl, fmt.Sprintf("somecaveat%d", i)) + rel = tuple.MustWithCaveat(rel, fmt.Sprintf("somecaveat%d", i)) var err error - relLastWriteAt, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_TOUCH, tpl) + relLastWriteAt, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationTouch, rel) require.NoError(err) } @@ -403,7 +402,7 @@ func GarbageCollectionTest(t *testing.T, ds datastore.Datastore) { require.Zero(removed.Namespaces) // Ensure the relationship is still present. - tRequire.TupleExists(ctx, tpl, relLastWriteAt) + tRequire.RelationshipExists(ctx, rel, relLastWriteAt) // Inject a transaction to clean up the last write lastRev, err := pds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { @@ -479,8 +478,8 @@ func GarbageCollectionByTimeTest(t *testing.T, ds datastore.Datastore) { time.Sleep(1 * time.Millisecond) // Write a relationship. - tpl := tuple.Parse("resource:someresource#reader@user:someuser#...") - relLastWriteAt, err := common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, tpl) + rel := tuple.MustParse("resource:someresource#reader@user:someuser#...") + relLastWriteAt, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, rel) require.NoError(err) // Run GC and ensure only transactions were removed. @@ -497,14 +496,14 @@ func GarbageCollectionByTimeTest(t *testing.T, ds datastore.Datastore) { require.Zero(removed.Namespaces) // Ensure the relationship is still present. - tRequire := testfixtures.TupleChecker{Require: require, DS: ds} - tRequire.TupleExists(ctx, tpl, relLastWriteAt) + tRequire := testfixtures.RelationshipChecker{Require: require, DS: ds} + tRequire.RelationshipExists(ctx, rel, relLastWriteAt) // Sleep 1ms to ensure GC will delete the previous write. time.Sleep(1 * time.Millisecond) // Delete the relationship. - relDeletedAt, err := common.WriteTuples(ctx, ds, core.RelationTupleUpdate_DELETE, tpl) + relDeletedAt, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationDelete, rel) require.NoError(err) // Inject a revision to sweep up the last revision @@ -527,7 +526,7 @@ func GarbageCollectionByTimeTest(t *testing.T, ds datastore.Datastore) { require.Zero(removed.Namespaces) // Ensure the relationship is still not present. - tRequire.NoTupleExists(ctx, tpl, relDeletedAt) + tRequire.NoRelationshipExists(ctx, rel, relDeletedAt) } const chunkRelationshipCount = 2000 @@ -551,20 +550,20 @@ func ChunkedGarbageCollectionTest(t *testing.T, ds datastore.Datastore) { pds := ds.(*pgDatastore) // Prepare relationships to write. - var tpls []*core.RelationTuple + var rels []tuple.Relationship for i := 0; i < chunkRelationshipCount; i++ { - tpl := tuple.Parse(fmt.Sprintf("resource:resource-%d#reader@user:someuser#...", i)) - tpls = append(tpls, tpl) + rel := tuple.MustParse(fmt.Sprintf("resource:resource-%d#reader@user:someuser#...", i)) + rels = append(rels, rel) } // Write a large number of relationships. - writtenAt, err := common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, tpls...) + writtenAt, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, rels...) require.NoError(err) // Ensure the relationships were written. - tRequire := testfixtures.TupleChecker{Require: require, DS: ds} - for _, tpl := range tpls { - tRequire.TupleExists(ctx, tpl, writtenAt) + tRequire := testfixtures.RelationshipChecker{Require: require, DS: ds} + for _, rel := range rels { + tRequire.RelationshipExists(ctx, rel, writtenAt) } // Run GC and ensure only transactions were removed. @@ -584,7 +583,7 @@ func ChunkedGarbageCollectionTest(t *testing.T, ds datastore.Datastore) { time.Sleep(1 * time.Millisecond) // Delete all the relationships. - deletedAt, err := common.WriteTuples(ctx, ds, core.RelationTupleUpdate_DELETE, tpls...) + deletedAt, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationDelete, rels...) require.NoError(err) // Inject a revision to sweep up the last revision @@ -594,8 +593,8 @@ func ChunkedGarbageCollectionTest(t *testing.T, ds datastore.Datastore) { require.NoError(err) // Ensure the relationships were deleted. - for _, tpl := range tpls { - tRequire.NoTupleExists(ctx, tpl, deletedAt) + for _, rel := range rels { + tRequire.NoRelationshipExists(ctx, rel, deletedAt) } // Sleep to ensure GC. @@ -761,19 +760,8 @@ func ConcurrentRevisionHeadTest(t *testing.T, ds datastore.Datastore) { g.Go(func() error { var err error commitLastRev, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { - rtu := tuple.Touch(&core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: "resource", - ObjectId: "123", - Relation: "reader", - }, - Subject: &core.ObjectAndRelation{ - Namespace: "user", - ObjectId: "456", - Relation: "...", - }, - }) - err = rwt.WriteRelationships(ctx, []*core.RelationTupleUpdate{rtu}) + rtu := tuple.Touch(tuple.MustParse("resource:123#reader@user:456")) + err = rwt.WriteRelationships(ctx, []tuple.RelationshipUpdate{rtu}) require.NoError(err) close(waitToStart) @@ -788,19 +776,8 @@ func ConcurrentRevisionHeadTest(t *testing.T, ds datastore.Datastore) { <-waitToStart commitFirstRev, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { - rtu := tuple.Touch(&core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: "resource", - ObjectId: "789", - Relation: "reader", - }, - Subject: &core.ObjectAndRelation{ - Namespace: "user", - ObjectId: "456", - Relation: "...", - }, - }) - return rwt.WriteRelationships(ctx, []*core.RelationTupleUpdate{rtu}) + rtu := tuple.Touch(tuple.MustParse("resource:789#reader@user:456")) + return rwt.WriteRelationships(ctx, []tuple.RelationshipUpdate{rtu}) }) close(waitToFinish) @@ -820,14 +797,9 @@ func ConcurrentRevisionHeadTest(t *testing.T, ds datastore.Datastore) { OptionalResourceType: "resource", }) require.NoError(err) - defer it.Close() - - found := []*core.RelationTuple{} - for tpl := it.Next(); tpl != nil; tpl = it.Next() { - require.NoError(it.Err()) - found = append(found, tpl) - } + found, err := datastore.IteratorToSlice(it) + require.NoError(err) require.Equal(2, len(found), "missing relationships in %v", found) } @@ -894,7 +866,7 @@ func ConcurrentRevisionWatchTest(t *testing.T, ds datastore.Datastore) { g.Go(func() error { var err error commitLastRev, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { - err = rwt.WriteRelationships(ctx, []*core.RelationTupleUpdate{ + err = rwt.WriteRelationships(ctx, []tuple.RelationshipUpdate{ tuple.Touch(tuple.MustParse("something:001#viewer@user:123")), tuple.Touch(tuple.MustParse("something:002#viewer@user:123")), tuple.Touch(tuple.MustParse("something:003#viewer@user:123")), @@ -913,7 +885,7 @@ func ConcurrentRevisionWatchTest(t *testing.T, ds datastore.Datastore) { <-waitToStart commitFirstRev, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { - return rwt.WriteRelationships(ctx, []*core.RelationTupleUpdate{ + return rwt.WriteRelationships(ctx, []tuple.RelationshipUpdate{ tuple.Touch(tuple.MustParse("resource:1001#reader@user:456")), tuple.Touch(tuple.MustParse("resource:1002#reader@user:456")), tuple.Touch(tuple.MustParse("resource:1003#reader@user:456")), @@ -931,19 +903,8 @@ func ConcurrentRevisionWatchTest(t *testing.T, ds datastore.Datastore) { // Write another revision. afterRev, err := ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { - rtu := tuple.Touch(&core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: "resource", - ObjectId: "2345", - Relation: "reader", - }, - Subject: &core.ObjectAndRelation{ - Namespace: "user", - ObjectId: "456", - Relation: "...", - }, - }) - return rwt.WriteRelationships(ctx, []*core.RelationTupleUpdate{rtu}) + rtu := tuple.Touch(tuple.MustParse("resource:2345#reader@user:456")) + return rwt.WriteRelationships(ctx, []tuple.RelationshipUpdate{rtu}) }) require.NoError(err) require.True(afterRev.GreaterThan(commitFirstRev)) @@ -1088,19 +1049,8 @@ func RevisionInversionTest(t *testing.T, ds datastore.Datastore) { g.Go(func() error { var err error commitLastRev, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { - rtu := tuple.Touch(&core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: "resource", - ObjectId: "123", - Relation: "reader", - }, - Subject: &core.ObjectAndRelation{ - Namespace: "user", - ObjectId: "456", - Relation: "...", - }, - }) - err = rwt.WriteRelationships(ctx, []*core.RelationTupleUpdate{rtu}) + rtu := tuple.Touch(tuple.MustParse("resource:123#reader@user:456")) + err = rwt.WriteRelationships(ctx, []tuple.RelationshipUpdate{rtu}) require.NoError(err) close(waitToStart) @@ -1115,19 +1065,8 @@ func RevisionInversionTest(t *testing.T, ds datastore.Datastore) { <-waitToStart commitFirstRev, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { - rtu := tuple.Touch(&core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: "resource", - ObjectId: "789", - Relation: "reader", - }, - Subject: &core.ObjectAndRelation{ - Namespace: "user", - ObjectId: "ten", - Relation: "...", - }, - }) - return rwt.WriteRelationships(ctx, []*core.RelationTupleUpdate{rtu}) + rtu := tuple.Touch(tuple.MustParse("resource:789#reader@user:ten")) + return rwt.WriteRelationships(ctx, []tuple.RelationshipUpdate{rtu}) }) close(waitToFinish) @@ -1219,13 +1158,10 @@ func BenchmarkPostgresQuery(b *testing.B) { OptionalResourceType: testfixtures.DocumentNS.Name, }) require.NoError(err) - - defer iter.Close() - - for tpl := iter.Next(); tpl != nil; tpl = iter.Next() { - require.Equal(testfixtures.DocumentNS.Name, tpl.ResourceAndRelation.Namespace) + for rel, err := range iter { + require.NoError(err) + require.Equal(testfixtures.DocumentNS.Name, rel.Resource.ObjectType) } - require.NoError(iter.Err()) } }) } @@ -1263,46 +1199,52 @@ func datastoreWithInterceptorAndTestData(t *testing.T, interceptor pgcommon.Quer } // Write some relationships. - rtu := tuple.Touch(&core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: testfixtures.DocumentNS.Name, - ObjectId: fmt.Sprintf("doc%d", i), - Relation: "reader", - }, - Subject: &core.ObjectAndRelation{ - Namespace: "user", - ObjectId: "456", - Relation: "...", + rtu := tuple.Touch(tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.ObjectAndRelation{ + ObjectType: testfixtures.DocumentNS.Name, + ObjectID: fmt.Sprintf("doc%d", i), + Relation: "reader", + }, + Subject: tuple.ObjectAndRelation{ + ObjectType: "user", + ObjectID: "456", + Relation: "...", + }, }, }) - rtu2 := tuple.Touch(&core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: fmt.Sprintf("resource%d", i), - ObjectId: "123", - Relation: "reader", - }, - Subject: &core.ObjectAndRelation{ - Namespace: "user", - ObjectId: "456", - Relation: "...", + rtu2 := tuple.Touch(tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.ObjectAndRelation{ + ObjectType: fmt.Sprintf("resource%d", i), + ObjectID: "123", + Relation: "reader", + }, + Subject: tuple.ObjectAndRelation{ + ObjectType: "user", + ObjectID: "456", + Relation: "...", + }, }, }) - rtu3 := tuple.Touch(&core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: fmt.Sprintf("resource%d", i), - ObjectId: "123", - Relation: "writer", - }, - Subject: &core.ObjectAndRelation{ - Namespace: "user", - ObjectId: "456", - Relation: "...", + rtu3 := tuple.Touch(tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.ObjectAndRelation{ + ObjectType: fmt.Sprintf("resource%d", i), + ObjectID: "123", + Relation: "writer", + }, + Subject: tuple.ObjectAndRelation{ + ObjectType: "user", + ObjectID: "456", + Relation: "...", + }, }, }) - return rwt.WriteRelationships(ctx, []*core.RelationTupleUpdate{rtu, rtu2, rtu3}) + return rwt.WriteRelationships(ctx, []tuple.RelationshipUpdate{rtu, rtu2, rtu3}) }) require.NoError(err) } @@ -1310,32 +1252,36 @@ func datastoreWithInterceptorAndTestData(t *testing.T, interceptor pgcommon.Quer // Delete some relationships. for i := 990; i < 1000; i++ { _, err := ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { - rtu := tuple.Delete(&core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: testfixtures.DocumentNS.Name, - ObjectId: fmt.Sprintf("doc%d", i), - Relation: "reader", - }, - Subject: &core.ObjectAndRelation{ - Namespace: "user", - ObjectId: "456", - Relation: "...", + rtu := tuple.Delete(tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.ObjectAndRelation{ + ObjectType: testfixtures.DocumentNS.Name, + ObjectID: fmt.Sprintf("doc%d", i), + Relation: "reader", + }, + Subject: tuple.ObjectAndRelation{ + ObjectType: "user", + ObjectID: "456", + Relation: "...", + }, }, }) - rtu2 := tuple.Delete(&core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: fmt.Sprintf("resource%d", i), - ObjectId: "123", - Relation: "reader", - }, - Subject: &core.ObjectAndRelation{ - Namespace: "user", - ObjectId: "456", - Relation: "...", + rtu2 := tuple.Delete(tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.ObjectAndRelation{ + ObjectType: fmt.Sprintf("resource%d", i), + ObjectID: "123", + Relation: "reader", + }, + Subject: tuple.ObjectAndRelation{ + ObjectType: "user", + ObjectID: "456", + Relation: "...", + }, }, }) - return rwt.WriteRelationships(ctx, []*core.RelationTupleUpdate{rtu, rtu2}) + return rwt.WriteRelationships(ctx, []tuple.RelationshipUpdate{rtu, rtu2}) }) require.NoError(err) } @@ -1344,32 +1290,36 @@ func datastoreWithInterceptorAndTestData(t *testing.T, interceptor pgcommon.Quer for i := 1000; i < 1100; i++ { _, err := ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { // Write some relationships. - rtu := tuple.Touch(&core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: testfixtures.DocumentNS.Name, - ObjectId: fmt.Sprintf("doc%d", i), - Relation: "reader", - }, - Subject: &core.ObjectAndRelation{ - Namespace: "user", - ObjectId: "456", - Relation: "...", + rtu := tuple.Touch(tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.ObjectAndRelation{ + ObjectType: testfixtures.DocumentNS.Name, + ObjectID: fmt.Sprintf("doc%d", i), + Relation: "reader", + }, + Subject: tuple.ObjectAndRelation{ + ObjectType: "user", + ObjectID: "456", + Relation: "...", + }, }, }) - rtu2 := tuple.Touch(&core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: fmt.Sprintf("resource%d", i), - ObjectId: "123", - Relation: "reader", - }, - Subject: &core.ObjectAndRelation{ - Namespace: "user", - ObjectId: "456", - Relation: "...", + rtu2 := tuple.Touch(tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.ObjectAndRelation{ + ObjectType: fmt.Sprintf("resource%d", i), + ObjectID: "123", + Relation: "reader", + }, + Subject: tuple.ObjectAndRelation{ + ObjectType: "user", + ObjectID: "456", + Relation: "...", + }, }, }) - return rwt.WriteRelationships(ctx, []*core.RelationTupleUpdate{rtu, rtu2}) + return rwt.WriteRelationships(ctx, []tuple.RelationshipUpdate{rtu, rtu2}) }) require.NoError(err) } @@ -1461,7 +1411,9 @@ func StrictReadModeTest(t *testing.T, ds datastore.Datastore) { OptionalResourceType: "resource", }) require.NoError(err) - it.Close() + + _, err = datastore.IteratorToSlice(it) + require.NoError(err) // Perform a read at a manually constructed revision beyond head, which should fail. badRev := postgresRevision{ @@ -1473,9 +1425,12 @@ func StrictReadModeTest(t *testing.T, ds datastore.Datastore) { }, } - _, err = ds.SnapshotReader(badRev).QueryRelationships(ctx, datastore.RelationshipsFilter{ + it, err = ds.SnapshotReader(badRev).QueryRelationships(ctx, datastore.RelationshipsFilter{ OptionalResourceType: "resource", }) + require.NoError(err) + + _, err = datastore.IteratorToSlice(it) require.Error(err) require.ErrorContains(err, "is not available on the replica") require.ErrorAs(err, &common.RevisionUnavailableError{}) @@ -1524,9 +1479,9 @@ func NullCaveatWatchTest(t *testing.T, ds datastore.Datastore) { require.NoError(err) // Verify the relationship create was tracked by the watch. - test.VerifyUpdates(require, [][]*core.RelationTupleUpdate{ + test.VerifyUpdates(require, [][]tuple.RelationshipUpdate{ { - tuple.Touch(tuple.Parse("resource:someresourceid#somerelation@subject:somesubject")), + tuple.Touch(tuple.MustParse("resource:someresourceid#somerelation@subject:somesubject")), }, }, changes, @@ -1535,14 +1490,14 @@ func NullCaveatWatchTest(t *testing.T, ds datastore.Datastore) { ) // Delete the relationship and ensure it does not raise an error in watch. - deleteUpdate := tuple.Delete(tuple.Parse("resource:someresourceid#somerelation@subject:somesubject")) - _, err = common.UpdateTuplesInDatastore(ctx, ds, deleteUpdate) + deleteUpdate := tuple.Delete(tuple.MustParse("resource:someresourceid#somerelation@subject:somesubject")) + _, err = common.UpdateRelationshipsInDatastore(ctx, ds, deleteUpdate) require.NoError(err) // Verify the delete. - test.VerifyUpdates(require, [][]*core.RelationTupleUpdate{ + test.VerifyUpdates(require, [][]tuple.RelationshipUpdate{ { - tuple.Delete(tuple.Parse("resource:someresourceid#somerelation@subject:somesubject")), + tuple.Delete(tuple.MustParse("resource:someresourceid#somerelation@subject:somesubject")), }, }, changes, @@ -1568,7 +1523,7 @@ func RevisionTimestampAndTransactionIDTest(t *testing.T, ds datastore.Datastore) pds := ds.(*pgDatastore) _, err = pds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { - return rwt.WriteRelationships(ctx, []*core.RelationTupleUpdate{ + return rwt.WriteRelationships(ctx, []tuple.RelationshipUpdate{ tuple.Touch(tuple.MustParse("something:001#viewer@user:123")), }) }) diff --git a/internal/datastore/postgres/reader.go b/internal/datastore/postgres/reader.go index 8297bbaa64..b8c0448ebb 100644 --- a/internal/datastore/postgres/reader.go +++ b/internal/datastore/postgres/reader.go @@ -36,7 +36,7 @@ var ( colCaveatContext, ).From(tableTuple) - countTuples = psql.Select("COUNT(*)").From(tableTuple) + countRels = psql.Select("COUNT(*)").From(tableTuple) schema = common.NewSchemaInformation( colNamespace, @@ -82,7 +82,7 @@ func (r *pgReader) CountRelationships(ctx context.Context, name string) (int, er return 0, err } - qBuilder, err := common.NewSchemaQueryFilterer(schema, r.filterer(countTuples), r.filterMaximumIDCount).FilterWithRelationshipsFilter(relFilter) + qBuilder, err := common.NewSchemaQueryFilterer(schema, r.filterer(countRels), r.filterMaximumIDCount).FilterWithRelationshipsFilter(relFilter) if err != nil { return 0, err } diff --git a/internal/datastore/postgres/readwrite.go b/internal/datastore/postgres/readwrite.go index 6e182236b2..48b93c7471 100644 --- a/internal/datastore/postgres/readwrite.go +++ b/internal/datastore/postgres/readwrite.go @@ -83,20 +83,20 @@ type pgReadWriteTXN struct { newXID xid8 } -func appendForInsertion(builder sq.InsertBuilder, tpl *core.RelationTuple) sq.InsertBuilder { +func appendForInsertion(builder sq.InsertBuilder, tpl tuple.Relationship) sq.InsertBuilder { var caveatName string var caveatContext map[string]any - if tpl.Caveat != nil { - caveatName = tpl.Caveat.CaveatName - caveatContext = tpl.Caveat.Context.AsMap() + if tpl.OptionalCaveat != nil { + caveatName = tpl.OptionalCaveat.CaveatName + caveatContext = tpl.OptionalCaveat.Context.AsMap() } valuesToWrite := []interface{}{ - tpl.ResourceAndRelation.Namespace, - tpl.ResourceAndRelation.ObjectId, - tpl.ResourceAndRelation.Relation, - tpl.Subject.Namespace, - tpl.Subject.ObjectId, + tpl.Resource.ObjectType, + tpl.Resource.ObjectID, + tpl.Resource.Relation, + tpl.Subject.ObjectType, + tpl.Subject.ObjectID, tpl.Subject.Relation, caveatName, caveatContext, // PGX driver serializes map[string]any to JSONB type columns @@ -105,12 +105,12 @@ func appendForInsertion(builder sq.InsertBuilder, tpl *core.RelationTuple) sq.In return builder.Values(valuesToWrite...) } -func (rwt *pgReadWriteTXN) collectSimplifiedTouchTypes(ctx context.Context, mutations []*core.RelationTupleUpdate) (*mapz.Set[string], error) { +func (rwt *pgReadWriteTXN) collectSimplifiedTouchTypes(ctx context.Context, mutations []tuple.RelationshipUpdate) (*mapz.Set[string], error) { // Collect the list of namespaces used for resources for relationships being TOUCHed. touchedResourceNamespaces := mapz.NewSet[string]() for _, mut := range mutations { - if mut.Operation == core.RelationTupleUpdate_TOUCH { - touchedResourceNamespaces.Add(mut.Tuple.ResourceAndRelation.Namespace) + if mut.Operation == tuple.UpdateOperationTouch { + touchedResourceNamespaces.Add(mut.Relationship.Resource.ObjectType) } } @@ -136,13 +136,12 @@ func (rwt *pgReadWriteTXN) collectSimplifiedTouchTypes(ctx context.Context, muta } for _, mut := range mutations { - tpl := mut.Tuple - - if mut.Operation != core.RelationTupleUpdate_TOUCH { + rel := mut.Relationship + if mut.Operation != tuple.UpdateOperationTouch { continue } - nsDef, ok := nsDefByName[tpl.ResourceAndRelation.Namespace] + nsDef, ok := nsDefByName[rel.Resource.ObjectType] if !ok { continue } @@ -152,14 +151,14 @@ func (rwt *pgReadWriteTXN) collectSimplifiedTouchTypes(ctx context.Context, muta return nil, fmt.Errorf(errUnableToWriteRelationships, err) } - notAllowed, err := vts.RelationDoesNotAllowCaveatsForSubject(tpl.ResourceAndRelation.Relation, tpl.Subject.Namespace) + notAllowed, err := vts.RelationDoesNotAllowCaveatsForSubject(rel.Resource.Relation, rel.Subject.ObjectType) if err != nil { // Ignore errors and just fallback to the less efficient path. continue } if notAllowed { - relationSupportSimplifiedTouch.Add(nsDef.Name + "#" + tpl.ResourceAndRelation.Relation + "@" + tpl.Subject.Namespace) + relationSupportSimplifiedTouch.Add(nsDef.Name + "#" + rel.Resource.Relation + "@" + rel.Subject.ObjectType) continue } } @@ -167,8 +166,8 @@ func (rwt *pgReadWriteTXN) collectSimplifiedTouchTypes(ctx context.Context, muta return relationSupportSimplifiedTouch, nil } -func (rwt *pgReadWriteTXN) WriteRelationships(ctx context.Context, mutations []*core.RelationTupleUpdate) error { - touchMutationsByNonCaveat := make(map[string]*core.RelationTupleUpdate, len(mutations)) +func (rwt *pgReadWriteTXN) WriteRelationships(ctx context.Context, mutations []tuple.RelationshipUpdate) error { + touchMutationsByNonCaveat := make(map[string]tuple.RelationshipUpdate, len(mutations)) hasCreateInserts := false createInserts := writeTuple @@ -189,19 +188,19 @@ func (rwt *pgReadWriteTXN) WriteRelationships(ctx context.Context, mutations []* // Parse the updates, building inserts for CREATE/TOUCH and deletes for DELETE. for _, mut := range mutations { - tpl := mut.Tuple + rel := mut.Relationship switch mut.Operation { - case core.RelationTupleUpdate_CREATE: - createInserts = appendForInsertion(createInserts, tpl) + case tuple.UpdateOperationCreate: + createInserts = appendForInsertion(createInserts, rel) hasCreateInserts = true - case core.RelationTupleUpdate_TOUCH: - touchInserts = appendForInsertion(touchInserts, tpl) - touchMutationsByNonCaveat[tuple.StringWithoutCaveat(tpl)] = mut + case tuple.UpdateOperationTouch: + touchInserts = appendForInsertion(touchInserts, rel) + touchMutationsByNonCaveat[tuple.StringWithoutCaveat(rel)] = mut - case core.RelationTupleUpdate_DELETE: - deleteClauses = append(deleteClauses, exactRelationshipClause(tpl)) + case tuple.UpdateOperationDelete: + deleteClauses = append(deleteClauses, exactRelationshipClause(rel)) default: return spiceerrors.MustBugf("unknown tuple mutation: %v", mut) @@ -247,25 +246,42 @@ func (rwt *pgReadWriteTXN) WriteRelationships(ctx context.Context, mutations []* // Remove from the TOUCH map of operations each row that was successfully inserted. // This will cover any TOUCH that created an entirely new relationship, acting like // a CREATE. - tpl := &core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{}, - Subject: &core.ObjectAndRelation{}, - } - for rows.Next() { + var resourceObjectType string + var resourceObjectID string + var relation string + var subjectObjectType string + var subjectObjectID string + var subjectRelation string + err := rows.Scan( - &tpl.ResourceAndRelation.Namespace, - &tpl.ResourceAndRelation.ObjectId, - &tpl.ResourceAndRelation.Relation, - &tpl.Subject.Namespace, - &tpl.Subject.ObjectId, - &tpl.Subject.Relation, + &resourceObjectType, + &resourceObjectID, + &relation, + &subjectObjectType, + &subjectObjectID, + &subjectRelation, ) if err != nil { return fmt.Errorf(errUnableToWriteRelationships, err) } - tplString := tuple.StringWithoutCaveat(tpl) + rel := tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.ObjectAndRelation{ + ObjectType: resourceObjectType, + ObjectID: resourceObjectID, + Relation: relation, + }, + Subject: tuple.ObjectAndRelation{ + ObjectType: subjectObjectType, + ObjectID: subjectObjectID, + Relation: subjectRelation, + }, + }, + } + + tplString := tuple.StringWithoutCaveat(rel) _, ok := touchMutationsByNonCaveat[tplString] if !ok { return spiceerrors.MustBugf("missing expected completed TOUCH mutation") @@ -281,11 +297,11 @@ func (rwt *pgReadWriteTXN) WriteRelationships(ctx context.Context, mutations []* for _, mut := range touchMutationsByNonCaveat { // If the relation support a simplified TOUCH operation, then skip the DELETE operation, as it is unnecessary // because the relation does not support a caveat for a subject of this type. - if relationSupportSimplifiedTouch.Has(mut.Tuple.ResourceAndRelation.Namespace + "#" + mut.Tuple.ResourceAndRelation.Relation + "@" + mut.Tuple.Subject.Namespace) { + if relationSupportSimplifiedTouch.Has(mut.Relationship.Resource.ObjectType + "#" + mut.Relationship.Resource.Relation + "@" + mut.Relationship.Subject.ObjectType) { continue } - deleteClauses = append(deleteClauses, exactRelationshipDifferentCaveatClause(mut.Tuple)) + deleteClauses = append(deleteClauses, exactRelationshipDifferentCaveatClause(mut.Relationship)) } } @@ -326,24 +342,41 @@ func (rwt *pgReadWriteTXN) WriteRelationships(ctx context.Context, mutations []* touchWrite := writeTuple touchWriteHasValues := false - deletedTpl := &core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{}, - Subject: &core.ObjectAndRelation{}, - } - for rows.Next() { + var resourceObjectType string + var resourceObjectID string + var relation string + var subjectObjectType string + var subjectObjectID string + var subjectRelation string + err := rows.Scan( - &deletedTpl.ResourceAndRelation.Namespace, - &deletedTpl.ResourceAndRelation.ObjectId, - &deletedTpl.ResourceAndRelation.Relation, - &deletedTpl.Subject.Namespace, - &deletedTpl.Subject.ObjectId, - &deletedTpl.Subject.Relation, + &resourceObjectType, + &resourceObjectID, + &relation, + &subjectObjectType, + &subjectObjectID, + &subjectRelation, ) if err != nil { return fmt.Errorf(errUnableToWriteRelationships, err) } + deletedTpl := tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.ObjectAndRelation{ + ObjectType: resourceObjectType, + ObjectID: resourceObjectID, + Relation: relation, + }, + Subject: tuple.ObjectAndRelation{ + ObjectType: subjectObjectType, + ObjectID: subjectObjectID, + Relation: subjectRelation, + }, + }, + } + tplString := tuple.StringWithoutCaveat(deletedTpl) mutation, ok := touchMutationsByNonCaveat[tplString] if !ok { @@ -351,7 +384,7 @@ func (rwt *pgReadWriteTXN) WriteRelationships(ctx context.Context, mutations []* continue } - touchWrite = appendForInsertion(touchWrite, mutation.Tuple) + touchWrite = appendForInsertion(touchWrite, mutation.Relationship) touchWriteHasValues = true } rows.Close() @@ -703,32 +736,32 @@ func (rwt *pgReadWriteTXN) BulkLoad(ctx context.Context, iter datastore.BulkWrit return pgxcommon.BulkLoad(ctx, rwt.tx, tableTuple, copyCols, iter) } -func exactRelationshipClause(r *core.RelationTuple) sq.Eq { +func exactRelationshipClause(r tuple.Relationship) sq.Eq { return sq.Eq{ - colNamespace: r.ResourceAndRelation.Namespace, - colObjectID: r.ResourceAndRelation.ObjectId, - colRelation: r.ResourceAndRelation.Relation, - colUsersetNamespace: r.Subject.Namespace, - colUsersetObjectID: r.Subject.ObjectId, + colNamespace: r.Resource.ObjectType, + colObjectID: r.Resource.ObjectID, + colRelation: r.Resource.Relation, + colUsersetNamespace: r.Subject.ObjectType, + colUsersetObjectID: r.Subject.ObjectID, colUsersetRelation: r.Subject.Relation, } } -func exactRelationshipDifferentCaveatClause(r *core.RelationTuple) sq.And { +func exactRelationshipDifferentCaveatClause(r tuple.Relationship) sq.And { var caveatName string var caveatContext map[string]any - if r.Caveat != nil { - caveatName = r.Caveat.CaveatName - caveatContext = r.Caveat.Context.AsMap() + if r.OptionalCaveat != nil { + caveatName = r.OptionalCaveat.CaveatName + caveatContext = r.OptionalCaveat.Context.AsMap() } return sq.And{ sq.Eq{ - colNamespace: r.ResourceAndRelation.Namespace, - colObjectID: r.ResourceAndRelation.ObjectId, - colRelation: r.ResourceAndRelation.Relation, - colUsersetNamespace: r.Subject.Namespace, - colUsersetObjectID: r.Subject.ObjectId, + colNamespace: r.Resource.ObjectType, + colObjectID: r.Resource.ObjectID, + colRelation: r.Resource.Relation, + colUsersetNamespace: r.Subject.ObjectType, + colUsersetObjectID: r.Subject.ObjectID, colUsersetRelation: r.Subject.Relation, }, sq.Or{ diff --git a/internal/datastore/postgres/watch.go b/internal/datastore/postgres/watch.go index d28b0c0c9d..aac9873d4f 100644 --- a/internal/datastore/postgres/watch.go +++ b/internal/datastore/postgres/watch.go @@ -16,6 +16,7 @@ import ( "github.com/authzed/spicedb/pkg/datastore" core "github.com/authzed/spicedb/pkg/proto/core/v1" "github.com/authzed/spicedb/pkg/spiceerrors" + "github.com/authzed/spicedb/pkg/tuple" ) const ( @@ -308,21 +309,23 @@ func (pgd *pgDatastore) loadRelationshipChanges(ctx context.Context, xmin uint64 defer changes.Close() for changes.Next() { - nextTuple := &core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{}, - Subject: &core.ObjectAndRelation{}, - } + var resourceObjectType string + var resourceObjectID string + var relation string + var subjectObjectType string + var subjectObjectID string + var subjectRelation string var createdXID, deletedXID xid8 var caveatName *string var caveatContext map[string]any if err := changes.Scan( - &nextTuple.ResourceAndRelation.Namespace, - &nextTuple.ResourceAndRelation.ObjectId, - &nextTuple.ResourceAndRelation.Relation, - &nextTuple.Subject.Namespace, - &nextTuple.Subject.ObjectId, - &nextTuple.Subject.Relation, + &resourceObjectType, + &resourceObjectID, + &relation, + &subjectObjectType, + &subjectObjectID, + &subjectRelation, &caveatName, &caveatContext, &createdXID, @@ -331,24 +334,39 @@ func (pgd *pgDatastore) loadRelationshipChanges(ctx context.Context, xmin uint64 return fmt.Errorf("unable to parse changed tuple: %w", err) } + relationship := tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.ObjectAndRelation{ + ObjectType: resourceObjectType, + ObjectID: resourceObjectID, + Relation: relation, + }, + Subject: tuple.ObjectAndRelation{ + ObjectType: subjectObjectType, + ObjectID: subjectObjectID, + Relation: subjectRelation, + }, + }, + } + if caveatName != nil && *caveatName != "" { contextStruct, err := structpb.NewStruct(caveatContext) if err != nil { return fmt.Errorf("failed to read caveat context from update: %w", err) } - nextTuple.Caveat = &core.ContextualizedCaveat{ + relationship.OptionalCaveat = &core.ContextualizedCaveat{ CaveatName: *caveatName, Context: contextStruct, } } if _, found := filter[createdXID.Uint64]; found { - if err := tracked.AddRelationshipChange(ctx, txidToRevision[createdXID.Uint64], nextTuple, core.RelationTupleUpdate_TOUCH); err != nil { + if err := tracked.AddRelationshipChange(ctx, txidToRevision[createdXID.Uint64], relationship, tuple.UpdateOperationTouch); err != nil { return err } } if _, found := filter[deletedXID.Uint64]; found { - if err := tracked.AddRelationshipChange(ctx, txidToRevision[deletedXID.Uint64], nextTuple, core.RelationTupleUpdate_DELETE); err != nil { + if err := tracked.AddRelationshipChange(ctx, txidToRevision[deletedXID.Uint64], relationship, tuple.UpdateOperationDelete); err != nil { return err } } @@ -416,15 +434,15 @@ func (pgd *pgDatastore) loadNamespaceChanges(ctx context.Context, xmin uint64, x return nil } -func (pgd *pgDatastore) loadCaveatChanges(ctx context.Context, minimum uint64, maximum uint64, txidToRevision map[uint64]postgresRevision, filter map[uint64]int, tracked *common.Changes[postgresRevision, uint64]) error { +func (pgd *pgDatastore) loadCaveatChanges(ctx context.Context, xmin uint64, xmax uint64, txidToRevision map[uint64]postgresRevision, filter map[uint64]int, tracked *common.Changes[postgresRevision, uint64]) error { sql, args, err := queryChangedCaveats.Where(sq.Or{ sq.And{ - sq.LtOrEq{colCreatedXid: maximum}, - sq.GtOrEq{colCreatedXid: minimum}, + sq.LtOrEq{colCreatedXid: xmax}, + sq.GtOrEq{colCreatedXid: xmin}, }, sq.And{ - sq.LtOrEq{colDeletedXid: maximum}, - sq.GtOrEq{colDeletedXid: minimum}, + sq.LtOrEq{colDeletedXid: xmax}, + sq.GtOrEq{colDeletedXid: xmin}, }, }).ToSql() if err != nil { diff --git a/internal/datastore/proxy/hedging.go b/internal/datastore/proxy/hedging.go index 05d5cab594..57e6f75952 100644 --- a/internal/datastore/proxy/hedging.go +++ b/internal/datastore/proxy/hedging.go @@ -275,7 +275,9 @@ func (hp hedgingReader) executeQuery( // only the first call to once.Do will run the function, so whichever // hedged request is slower will have resultsUsed = false if !resultsUsed && tempErr == nil { - tempIterator.Close() + for range tempIterator { + break + } } responseReady <- struct{}{} } diff --git a/internal/datastore/proxy/hedging_test.go b/internal/datastore/proxy/hedging_test.go index db28441429..506c271575 100644 --- a/internal/datastore/proxy/hedging_test.go +++ b/internal/datastore/proxy/hedging_test.go @@ -16,8 +16,8 @@ import ( "github.com/authzed/spicedb/internal/datastore/proxy/proxy_test" "github.com/authzed/spicedb/internal/datastore/revisions" "github.com/authzed/spicedb/pkg/datastore" - "github.com/authzed/spicedb/pkg/datastore/options" core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/tuple" ) var ( @@ -31,7 +31,7 @@ var ( revisionKnown = revisions.NewForTransactionID(1) anotherRevisionKnown = revisions.NewForTransactionID(2) - emptyIterator = common.NewSliceRelationshipIterator(nil, options.Unsorted) + emptyIterator = common.NewSliceRelationshipIterator(nil) ) type testFunc func(t *testing.T, proxy datastore.Datastore, expectFirst bool) @@ -289,48 +289,35 @@ func TestDatastoreE2E(t *testing.T) { ) require.NoError(err) - expectedTuples := []*core.RelationTuple{ - { - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: "test", - ObjectId: "test", - Relation: "test", - }, - Subject: &core.ObjectAndRelation{ - Namespace: "test", - ObjectId: "test", - Relation: "test", - }, - }, + expectedRels := []tuple.Relationship{ + tuple.MustParse("document:first#viewer@user:bob"), + tuple.MustParse("document:second#viewer@user:alice"), } delegateDatastore.On("SnapshotReader", mock.Anything).Return(delegateReader) delegateReader. On("QueryRelationships", mock.Anything, mock.Anything). - Return(common.NewSliceRelationshipIterator(expectedTuples, options.Unsorted), nil). + Return(common.NewSliceRelationshipIterator(expectedRels), nil). WaitUntil(mockTime.After(2 * slowQueryTime)). Once() delegateReader. On("QueryRelationships", mock.Anything, mock.Anything). - Return(common.NewSliceRelationshipIterator(expectedTuples, options.Unsorted), nil). + Return(common.NewSliceRelationshipIterator(expectedRels), nil). Once() autoAdvance(mockTime, slowQueryTime/2, 2*slowQueryTime) it, err := proxy.SnapshotReader(revisionKnown).QueryRelationships( context.Background(), datastore.RelationshipsFilter{ - OptionalResourceType: "test", + OptionalResourceType: "document", }, ) require.NoError(err) - defer it.Close() - - only := it.Next() - require.Equal(expectedTuples[0], only) - require.Nil(it.Next()) - require.NoError(it.Err()) + slice, err := datastore.IteratorToSlice(it) + require.NoError(err) + require.Equal(expectedRels, slice) delegateDatastore.AssertExpectations(t) delegateReader.AssertExpectations(t) diff --git a/internal/datastore/proxy/observable.go b/internal/datastore/proxy/observable.go index d1a1ebe952..3f92beb71e 100644 --- a/internal/datastore/proxy/observable.go +++ b/internal/datastore/proxy/observable.go @@ -14,6 +14,7 @@ import ( "github.com/authzed/spicedb/pkg/datastore" "github.com/authzed/spicedb/pkg/datastore/options" core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/tuple" ) var ( @@ -223,40 +224,41 @@ func (r *observableReader) QueryRelationships(ctx context.Context, filter datast if err != nil { return iterator, err } - return &observableRelationshipIterator{closer, iterator, 0}, nil -} - -type observableRelationshipIterator struct { - closer func() - delegate datastore.RelationshipIterator - count uint32 -} -func (i *observableRelationshipIterator) Next() *core.RelationTuple { - if next := i.delegate.Next(); next != nil { - i.count++ - return next - } - return nil + return func(yield func(tuple.Relationship, error) bool) { + var count uint64 + for rel, err := range iterator { + count++ + if !yield(rel, err) { + break + } + } + loadedRelationshipCount.Observe(float64(count)) + closer() + }, nil } -func (i *observableRelationshipIterator) Err() error { return i.delegate.Err() } - -func (i *observableRelationshipIterator) Cursor() (options.Cursor, error) { return i.delegate.Cursor() } - -func (i *observableRelationshipIterator) Close() { - loadedRelationshipCount.Observe(float64(i.count)) - i.closer() - i.delegate.Close() -} +func (r *observableReader) ReverseQueryRelationships(ctx context.Context, subjectsFilter datastore.SubjectsFilter, options ...options.ReverseQueryOptionsOption) (datastore.RelationshipIterator, error) { + ctx, closer := observe(ctx, "ReverseQueryRelationships", trace.WithAttributes( + attribute.String("subjectType", subjectsFilter.SubjectType), + )) -func (r *observableReader) ReverseQueryRelationships(ctx context.Context, subjectFilter datastore.SubjectsFilter, options ...options.ReverseQueryOptionsOption) (datastore.RelationshipIterator, error) { - ctx, closer := observe(ctx, "ReverseQueryRelationships") - iterator, err := r.delegate.ReverseQueryRelationships(ctx, subjectFilter, options...) + iterator, err := r.delegate.ReverseQueryRelationships(ctx, subjectsFilter, options...) if err != nil { return iterator, err } - return &observableRelationshipIterator{closer, iterator, 0}, nil + + return func(yield func(tuple.Relationship, error) bool) { + var count uint64 + for rel, err := range iterator { + count++ + if !yield(rel, err) { + break + } + } + loadedRelationshipCount.Observe(float64(count)) + closer() + }, nil } type observableRWT struct { @@ -316,7 +318,7 @@ func (rwt *observableRWT) DeleteCaveats(ctx context.Context, names []string) err return rwt.delegate.DeleteCaveats(ctx, names) } -func (rwt *observableRWT) WriteRelationships(ctx context.Context, mutations []*core.RelationTupleUpdate) error { +func (rwt *observableRWT) WriteRelationships(ctx context.Context, mutations []tuple.RelationshipUpdate) error { ctx, closer := observe(ctx, "WriteRelationships", trace.WithAttributes( attribute.Int("mutations", len(mutations)), )) @@ -384,5 +386,4 @@ var ( _ datastore.Datastore = (*observableProxy)(nil) _ datastore.Reader = (*observableReader)(nil) _ datastore.ReadWriteTransaction = (*observableRWT)(nil) - _ datastore.RelationshipIterator = (*observableRelationshipIterator)(nil) ) diff --git a/internal/datastore/proxy/proxy_test/mock.go b/internal/datastore/proxy/proxy_test/mock.go index 3b109c39de..dc59c780ff 100644 --- a/internal/datastore/proxy/proxy_test/mock.go +++ b/internal/datastore/proxy/proxy_test/mock.go @@ -10,6 +10,7 @@ import ( "github.com/authzed/spicedb/pkg/datastore" "github.com/authzed/spicedb/pkg/datastore/options" core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/tuple" ) type MockDatastore struct { @@ -263,7 +264,7 @@ func (dm *MockReadWriteTransaction) LookupNamespacesWithNames(_ context.Context, return args.Get(0).([]datastore.RevisionedNamespace), args.Error(1) } -func (dm *MockReadWriteTransaction) WriteRelationships(_ context.Context, mutations []*core.RelationTupleUpdate) error { +func (dm *MockReadWriteTransaction) WriteRelationships(_ context.Context, mutations []tuple.RelationshipUpdate) error { args := dm.Called(mutations) return args.Error(0) } diff --git a/internal/datastore/proxy/readonly_test.go b/internal/datastore/proxy/readonly_test.go index fd73df7129..dbc2b108eb 100644 --- a/internal/datastore/proxy/readonly_test.go +++ b/internal/datastore/proxy/readonly_test.go @@ -45,7 +45,7 @@ func TestRWOperationErrors(t *testing.T) { require.ErrorAs(err, &datastore.ErrReadOnly{}) require.Equal(datastore.NoRevision, rev) - rev, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, tuple.Parse("user:test#boss@user:boss")) + rev, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, tuple.MustParse("user:test#boss@user:boss")) require.ErrorAs(err, &datastore.ErrReadOnly{}) require.Equal(datastore.NoRevision, rev) } diff --git a/internal/datastore/proxy/relationshipintegrity.go b/internal/datastore/proxy/relationshipintegrity.go index 6a09ab669b..4b129b14d1 100644 --- a/internal/datastore/proxy/relationshipintegrity.go +++ b/internal/datastore/proxy/relationshipintegrity.go @@ -140,8 +140,8 @@ func (r *relationshipIntegrityProxy) lookupKey(keyID string) (*hmacConfig, error } // computeRelationshipHash computes the HMAC hash of a relationship tuple. -func computeRelationshipHash(tpl *corev1.RelationTuple, key *hmacConfig) ([]byte, error) { - bytes, err := tuple.CanonicalBytes(tpl) +func computeRelationshipHash(rel tuple.Relationship, key *hmacConfig) ([]byte, error) { + bytes, err := tuple.CanonicalBytes(rel) if err != nil { return nil, err } @@ -209,10 +209,10 @@ func (r *relationshipIntegrityProxy) Statistics(ctx context.Context) (datastore. return r.ds.Statistics(ctx) } -func (r *relationshipIntegrityProxy) validateRelationTuple(tpl *corev1.RelationTuple) error { +func (r *relationshipIntegrityProxy) validateRelationTuple(rel tuple.Relationship) error { // Ensure the relationship has integrity data. - if tpl.Integrity == nil || len(tpl.Integrity.Hash) == 0 || tpl.Integrity.KeyId == "" { - str, err := tuple.String(tpl) + if rel.OptionalIntegrity == nil || len(rel.OptionalIntegrity.Hash) == 0 || rel.OptionalIntegrity.KeyId == "" { + str, err := tuple.String(rel) if err != nil { return err } @@ -220,28 +220,28 @@ func (r *relationshipIntegrityProxy) validateRelationTuple(tpl *corev1.RelationT return fmt.Errorf("relationship %s is missing required integrity data", str) } - hashWithoutByte := tpl.Integrity.Hash[1:] - if tpl.Integrity.Hash[0] != versionByte || len(hashWithoutByte) != hashLength { - return fmt.Errorf("relationship %s has invalid integrity data", tpl) + hashWithoutByte := rel.OptionalIntegrity.Hash[1:] + if rel.OptionalIntegrity.Hash[0] != versionByte || len(hashWithoutByte) != hashLength { + return fmt.Errorf("relationship %v has invalid integrity data", rel) } // Validate the integrity of the relationship. - key, err := r.lookupKey(tpl.Integrity.KeyId) + key, err := r.lookupKey(rel.OptionalIntegrity.KeyId) if err != nil { return err } - if key.expiredAt != nil && key.expiredAt.Before(tpl.Integrity.HashedAt.AsTime()) { - return fmt.Errorf("relationship %s is signed by an expired key", tpl) + if key.expiredAt != nil && key.expiredAt.Before(rel.OptionalIntegrity.HashedAt.AsTime()) { + return fmt.Errorf("relationship %s is signed by an expired key", rel) } - computedHash, err := computeRelationshipHash(tpl, key) + computedHash, err := computeRelationshipHash(rel, key) if err != nil { return err } if !hmac.Equal(computedHash, hashWithoutByte) { - str, err := tuple.String(tpl) + str, err := tuple.String(rel) if err != nil { return err } @@ -249,8 +249,6 @@ func (r *relationshipIntegrityProxy) validateRelationTuple(tpl *corev1.RelationT return fmt.Errorf("relationship %s has invalid integrity hash", str) } - // NOTE: The caller expects the integrity to be nil, so the proxy sets it to nil here. - tpl.Integrity = nil return nil } @@ -267,8 +265,8 @@ func (r *relationshipIntegrityProxy) Watch(ctx context.Context, afterRevision da select { case result := <-resultsChan: for _, rel := range result.RelationshipChanges { - if rel.Operation != corev1.RelationTupleUpdate_DELETE { - err := r.validateRelationTuple(rel.Tuple) + if rel.Operation != tuple.UpdateOperationDelete { + err := r.validateRelationTuple(rel.Relationship) if err != nil { checkedErrChan <- err return @@ -302,9 +300,22 @@ func (r relationshipIntegrityReader) QueryRelationships(ctx context.Context, fil return nil, err } - return &relationshipIntegrityIterator{ - parent: r, - wrapped: it, + return func(yield func(tuple.Relationship, error) bool) { + for rel, err := range it { + if err != nil { + yield(rel, err) + return + } + + if err := r.parent.validateRelationTuple(rel); err != nil { + yield(rel, err) + return + } + + if !yield(rel.WithoutIntegrity(), nil) { + return + } + } }, nil } @@ -314,9 +325,22 @@ func (r relationshipIntegrityReader) ReverseQueryRelationships(ctx context.Conte return nil, err } - return &relationshipIntegrityIterator{ - parent: r, - wrapped: it, + return func(yield func(tuple.Relationship, error) bool) { + for rel, err := range it { + if err != nil { + yield(rel, err) + return + } + + if err := r.parent.validateRelationTuple(rel); err != nil { + yield(rel, err) + return + } + + if !yield(rel.WithoutIntegrity(), nil) { + return + } + } }, nil } @@ -352,43 +376,6 @@ func (r relationshipIntegrityReader) ReadNamespaceByName(ctx context.Context, ns return r.wrapped.ReadNamespaceByName(ctx, nsName) } -type relationshipIntegrityIterator struct { - parent relationshipIntegrityReader - wrapped datastore.RelationshipIterator - err error -} - -func (r *relationshipIntegrityIterator) Close() { - r.wrapped.Close() -} - -func (r *relationshipIntegrityIterator) Cursor() (options.Cursor, error) { - return r.wrapped.Cursor() -} - -func (r *relationshipIntegrityIterator) Err() error { - if r.err != nil { - return r.err - } - - return r.wrapped.Err() -} - -func (r *relationshipIntegrityIterator) Next() *corev1.RelationTuple { - tpl := r.wrapped.Next() - if tpl == nil { - return nil - } - - err := r.parent.parent.validateRelationTuple(tpl) - if err != nil { - r.err = err - return nil - } - - return tpl -} - type relationshipIntegrityTx struct { datastore.ReadWriteTransaction @@ -397,31 +384,30 @@ type relationshipIntegrityTx struct { func (r *relationshipIntegrityTx) WriteRelationships( ctx context.Context, - mutations []*corev1.RelationTupleUpdate, + mutations []tuple.RelationshipUpdate, ) error { // Add integrity data to the relationships. key := r.parent.primaryKey hashedAt := timestamppb.Now() - updated := make([]*corev1.RelationTupleUpdate, 0, len(mutations)) + updated := make([]tuple.RelationshipUpdate, 0, len(mutations)) for _, mutation := range mutations { - if mutation.Tuple.Integrity != nil { - return spiceerrors.MustBugf("relationship %s already has integrity data", mutation.Tuple) + if mutation.Relationship.OptionalIntegrity != nil { + return spiceerrors.MustBugf("relationship %v already has integrity data", mutation.Relationship) } - hash, err := computeRelationshipHash(mutation.Tuple, key) + hash, err := computeRelationshipHash(mutation.Relationship, key) if err != nil { return err } // NOTE: Callers expect to be able to reuse the tuple, so we need to clone it. - cloned := mutation.CloneVT() - cloned.Tuple.Integrity = &corev1.RelationshipIntegrity{ + mutation.Relationship.OptionalIntegrity = &corev1.RelationshipIntegrity{ HashedAt: hashedAt, Hash: append([]byte{versionByte}, hash...), KeyId: key.keyID, } - updated = append(updated, cloned) + updated = append(updated, mutation) } return r.ReadWriteTransaction.WriteRelationships(ctx, updated) @@ -440,33 +426,33 @@ type integrityAddingBulkLoadInterator struct { parent *relationshipIntegrityProxy } -func (w integrityAddingBulkLoadInterator) Next(ctx context.Context) (*corev1.RelationTuple, error) { - tpl, err := w.wrapped.Next(ctx) +func (w integrityAddingBulkLoadInterator) Next(ctx context.Context) (*tuple.Relationship, error) { + rel, err := w.wrapped.Next(ctx) if err != nil { return nil, err } - if tpl == nil { + if rel == nil { return nil, nil } key := w.parent.primaryKey hashedAt := timestamppb.Now() - hash, err := computeRelationshipHash(tpl, key) + hash, err := computeRelationshipHash(*rel, key) if err != nil { return nil, err } - if tpl.Integrity != nil { - return nil, spiceerrors.MustBugf("relationship %s already has integrity data", tpl) + if rel.OptionalIntegrity != nil { + return nil, spiceerrors.MustBugf("relationship %v already has integrity data", rel) } - tpl.Integrity = &corev1.RelationshipIntegrity{ + rel.OptionalIntegrity = &corev1.RelationshipIntegrity{ HashedAt: hashedAt, Hash: append([]byte{versionByte}, hash...), KeyId: key.keyID, } - return tpl, nil + return rel, nil } diff --git a/internal/datastore/proxy/relationshipintegrity_test.go b/internal/datastore/proxy/relationshipintegrity_test.go index 603b9ad98c..f8f03ecc54 100644 --- a/internal/datastore/proxy/relationshipintegrity_test.go +++ b/internal/datastore/proxy/relationshipintegrity_test.go @@ -66,10 +66,10 @@ func TestWriteWithPredefinedIntegrity(t *testing.T) { require.Panics(t, func() { _, _ = pds.ReadWriteTx(context.Background(), func(ctx context.Context, tx datastore.ReadWriteTransaction) error { - tpl := tuple.MustParse("resource:foo#viewer@user:tom") - tpl.Integrity = &core.RelationshipIntegrity{} - return tx.WriteRelationships(context.Background(), []*core.RelationTupleUpdate{ - tuple.Create(tpl), + rel := tuple.MustParse("resource:foo#viewer@user:tom") + rel.OptionalIntegrity = &core.RelationshipIntegrity{} + return tx.WriteRelationships(context.Background(), []tuple.RelationshipUpdate{ + tuple.Create(rel), }) }) }) @@ -81,9 +81,9 @@ func TestReadWithMissingIntegrity(t *testing.T) { // Write a relationship to the underlying datastore without integrity information. _, err = ds.ReadWriteTx(context.Background(), func(ctx context.Context, tx datastore.ReadWriteTransaction) error { - tpl := tuple.MustParse("resource:foo#viewer@user:tom") - return tx.WriteRelationships(context.Background(), []*core.RelationTupleUpdate{ - tuple.Create(tpl), + rel := tuple.MustParse("resource:foo#viewer@user:tom") + return tx.WriteRelationships(context.Background(), []tuple.RelationshipUpdate{ + tuple.Create(rel), }) }) require.NoError(t, err) @@ -102,12 +102,9 @@ func TestReadWithMissingIntegrity(t *testing.T) { ) require.NoError(t, err) - found := iter.Next() - require.Nil(t, found) - require.Error(t, iter.Err()) - require.ErrorContains(t, iter.Err(), "is missing required integrity data") - - iter.Close() + _, err = datastore.IteratorToSlice(iter) + require.Error(t, err) + require.ErrorContains(t, err, "is missing required integrity data") } func TestBasicIntegrityFailureDueToInvalidHashVersion(t *testing.T) { @@ -119,7 +116,7 @@ func TestBasicIntegrityFailureDueToInvalidHashVersion(t *testing.T) { // Write some relationships. _, err = pds.ReadWriteTx(context.Background(), func(ctx context.Context, tx datastore.ReadWriteTransaction) error { - return tx.WriteRelationships(context.Background(), []*core.RelationTupleUpdate{ + return tx.WriteRelationships(context.Background(), []tuple.RelationshipUpdate{ tuple.Create(tuple.MustParse("resource:foo#viewer@user:tom")), tuple.Create(tuple.MustParse("resource:foo#viewer@user:fred")), tuple.Touch(tuple.MustParse("resource:bar#viewer@user:sarah")), @@ -131,13 +128,13 @@ func TestBasicIntegrityFailureDueToInvalidHashVersion(t *testing.T) { // the proxy. _, err = ds.ReadWriteTx(context.Background(), func(ctx context.Context, tx datastore.ReadWriteTransaction) error { invalidTpl := tuple.MustParse("resource:foo#viewer@user:jimmy") - invalidTpl.Integrity = &core.RelationshipIntegrity{ + invalidTpl.OptionalIntegrity = &core.RelationshipIntegrity{ KeyId: "defaultfortest", Hash: []byte("invalidhash"), HashedAt: timestamppb.Now(), } - return tx.WriteRelationships(context.Background(), []*core.RelationTupleUpdate{ + return tx.WriteRelationships(context.Background(), []tuple.RelationshipUpdate{ tuple.Create(invalidTpl), }) }) @@ -152,29 +149,11 @@ func TestBasicIntegrityFailureDueToInvalidHashVersion(t *testing.T) { context.Background(), datastore.RelationshipsFilter{OptionalResourceType: "resource"}, ) - t.Cleanup(iter.Close) require.NoError(t, err) - var foundError error - for { - rel := iter.Next() - if rel == nil { - break - } - - err := iter.Err() - if err != nil { - foundError = err - break - } - } - - if foundError == nil { - foundError = iter.Err() - } - - require.Error(t, foundError) - require.ErrorContains(t, foundError, "has invalid integrity data") + _, err = datastore.IteratorToSlice(iter) + require.Error(t, err) + require.ErrorContains(t, err, "has invalid integrity data") } func TestBasicIntegrityFailureDueToInvalidHashSignature(t *testing.T) { @@ -186,7 +165,7 @@ func TestBasicIntegrityFailureDueToInvalidHashSignature(t *testing.T) { // Write some relationships. _, err = pds.ReadWriteTx(context.Background(), func(ctx context.Context, tx datastore.ReadWriteTransaction) error { - return tx.WriteRelationships(context.Background(), []*core.RelationTupleUpdate{ + return tx.WriteRelationships(context.Background(), []tuple.RelationshipUpdate{ tuple.Create(tuple.MustParse("resource:foo#viewer@user:tom")), tuple.Create(tuple.MustParse("resource:foo#viewer@user:fred")), tuple.Touch(tuple.MustParse("resource:bar#viewer@user:sarah")), @@ -198,13 +177,13 @@ func TestBasicIntegrityFailureDueToInvalidHashSignature(t *testing.T) { // the _, err = ds.ReadWriteTx(context.Background(), func(ctx context.Context, tx datastore.ReadWriteTransaction) error { invalidTpl := tuple.MustParse("resource:foo#viewer@user:jimmy") - invalidTpl.Integrity = &core.RelationshipIntegrity{ + invalidTpl.OptionalIntegrity = &core.RelationshipIntegrity{ KeyId: "defaultfortest", Hash: append([]byte{0x01}, []byte("someinvalidhashaasd")[0:hashLength]...), HashedAt: timestamppb.Now(), } - return tx.WriteRelationships(context.Background(), []*core.RelationTupleUpdate{ + return tx.WriteRelationships(context.Background(), []tuple.RelationshipUpdate{ tuple.Create(invalidTpl), }) }) @@ -219,29 +198,11 @@ func TestBasicIntegrityFailureDueToInvalidHashSignature(t *testing.T) { context.Background(), datastore.RelationshipsFilter{OptionalResourceType: "resource"}, ) - t.Cleanup(iter.Close) require.NoError(t, err) - var foundError error - for { - rel := iter.Next() - if rel == nil { - break - } - - err := iter.Err() - if err != nil { - foundError = err - break - } - } - - if foundError == nil { - foundError = iter.Err() - } - - require.Error(t, foundError) - require.ErrorContains(t, foundError, "has invalid integrity hash") + _, err = datastore.IteratorToSlice(iter) + require.Error(t, err) + require.ErrorContains(t, err, "has invalid integrity hash") } func TestBasicIntegrityFailureDueToWriteWithExpiredKey(t *testing.T) { @@ -254,7 +215,7 @@ func TestBasicIntegrityFailureDueToWriteWithExpiredKey(t *testing.T) { // Write some relationships. _, err = epds.ReadWriteTx(context.Background(), func(ctx context.Context, tx datastore.ReadWriteTransaction) error { - return tx.WriteRelationships(context.Background(), []*core.RelationTupleUpdate{ + return tx.WriteRelationships(context.Background(), []tuple.RelationshipUpdate{ tuple.Create(tuple.MustParse("resource:foo#viewer@user:tom")), tuple.Create(tuple.MustParse("resource:foo#viewer@user:fred")), tuple.Touch(tuple.MustParse("resource:bar#viewer@user:sarah")), @@ -276,29 +237,11 @@ func TestBasicIntegrityFailureDueToWriteWithExpiredKey(t *testing.T) { context.Background(), datastore.RelationshipsFilter{OptionalResourceType: "resource"}, ) - t.Cleanup(iter.Close) require.NoError(t, err) - var foundError error - for { - rel := iter.Next() - if rel == nil { - break - } - - err := iter.Err() - if err != nil { - foundError = err - break - } - } - - if foundError == nil { - foundError = iter.Err() - } - - require.Error(t, foundError) - require.ErrorContains(t, foundError, "is signed by an expired key") + _, err = datastore.IteratorToSlice(iter) + require.Error(t, err) + require.ErrorContains(t, err, "is signed by an expired key") } func TestWatchIntegrityFailureDueToInvalidHashSignature(t *testing.T) { @@ -317,13 +260,13 @@ func TestWatchIntegrityFailureDueToInvalidHashSignature(t *testing.T) { // the proxy. _, err = ds.ReadWriteTx(context.Background(), func(ctx context.Context, tx datastore.ReadWriteTransaction) error { invalidTpl := tuple.MustParse("resource:foo#viewer@user:jimmy") - invalidTpl.Integrity = &core.RelationshipIntegrity{ + invalidTpl.OptionalIntegrity = &core.RelationshipIntegrity{ KeyId: "defaultfortest", Hash: append([]byte{0x01}, []byte("someinvalidhashaasd")[0:hashLength]...), HashedAt: timestamppb.Now(), } - return tx.WriteRelationships(context.Background(), []*core.RelationTupleUpdate{ + return tx.WriteRelationships(context.Background(), []tuple.RelationshipUpdate{ tuple.Create(invalidTpl), }) }) @@ -354,9 +297,9 @@ func BenchmarkQueryRelsWithIntegrity(b *testing.B) { _, err = pds.ReadWriteTx(context.Background(), func(ctx context.Context, tx datastore.ReadWriteTransaction) error { for i := 0; i < 1000; i++ { - tpl := tuple.MustParse(fmt.Sprintf("resource:foo#viewer@user:user-%d", i)) - if err := tx.WriteRelationships(context.Background(), []*core.RelationTupleUpdate{ - tuple.Create(tpl), + rel := tuple.MustParse(fmt.Sprintf("resource:foo#viewer@user:user-%d", i)) + if err := tx.WriteRelationships(context.Background(), []tuple.RelationshipUpdate{ + tuple.Create(rel), }); err != nil { return err } @@ -383,19 +326,8 @@ func BenchmarkQueryRelsWithIntegrity(b *testing.B) { ) require.NoError(b, err) - for { - err := iter.Err() - if err != nil { - require.NoError(b, err) - } - - rel := iter.Next() - if rel == nil { - break - } - } - - iter.Close() + _, err = datastore.IteratorToSlice(iter) + require.NoError(b, err) } b.StopTimer() }) @@ -408,9 +340,9 @@ func BenchmarkComputeRelationshipHash(b *testing.B) { pool: poolForKey(DefaultKeyForTesting.Bytes), } - tpl := tuple.MustParse("resource:foo#viewer@user:tom") + rel := tuple.MustParse("resource:foo#viewer@user:tom") for i := 0; i < b.N; i++ { - _, err := computeRelationshipHash(tpl, config) + _, err := computeRelationshipHash(rel, config) require.NoError(b, err) } } diff --git a/internal/datastore/spanner/reader.go b/internal/datastore/spanner/reader.go index 5d0f63e00d..821de67035 100644 --- a/internal/datastore/spanner/reader.go +++ b/internal/datastore/spanner/reader.go @@ -2,6 +2,7 @@ package spanner import ( "context" + "errors" "fmt" "time" @@ -15,6 +16,7 @@ import ( "github.com/authzed/spicedb/pkg/datastore" "github.com/authzed/spicedb/pkg/datastore/options" core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/tuple" ) // The underlying Spanner shared read transaction interface is not exposed, so we re-create @@ -52,7 +54,7 @@ func (sr spannerReader) CountRelationships(ctx context.Context, name string) (in return 0, err } - builder, err := common.NewSchemaQueryFilterer(schema, countTuples, sr.filterMaximumIDCount).FilterWithRelationshipsFilter(relFilter) + builder, err := common.NewSchemaQueryFilterer(schema, countRels, sr.filterMaximumIDCount).FilterWithRelationshipsFilter(relFilter) if err != nil { return 0, err } @@ -167,52 +169,79 @@ func (sr spannerReader) ReverseQueryRelationships( ) } -func queryExecutor(txSource txFactory) common.ExecuteQueryFunc { - return func(ctx context.Context, sql string, args []any) ([]*core.RelationTuple, error) { - span := trace.SpanFromContext(ctx) - span.AddEvent("Query issued to database") - iter := txSource().Query(ctx, statementFromSQL(sql, args)) - defer iter.Stop() - - var tuples []*core.RelationTuple - - span.AddEvent("start reading iterator") - if err := iter.Do(func(row *spanner.Row) error { - nextTuple := &core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{}, - Subject: &core.ObjectAndRelation{}, - } - var caveatName spanner.NullString - var caveatCtx spanner.NullJSON - err := row.Columns( - &nextTuple.ResourceAndRelation.Namespace, - &nextTuple.ResourceAndRelation.ObjectId, - &nextTuple.ResourceAndRelation.Relation, - &nextTuple.Subject.Namespace, - &nextTuple.Subject.ObjectId, - &nextTuple.Subject.Relation, - &caveatName, - &caveatCtx, - ) - if err != nil { - return err - } +var errStopIterator = fmt.Errorf("stop iteration") - nextTuple.Caveat, err = ContextualizedCaveatFrom(caveatName, caveatCtx) - if err != nil { - return err +func queryExecutor(txSource txFactory) common.ExecuteQueryFunc { + return func(ctx context.Context, sql string, args []any) (datastore.RelationshipIterator, error) { + return func(yield func(tuple.Relationship, error) bool) { + span := trace.SpanFromContext(ctx) + span.AddEvent("Query issued to database") + iter := txSource().Query(ctx, statementFromSQL(sql, args)) + defer iter.Stop() + + span.AddEvent("start reading iterator") + defer span.AddEvent("finished reading iterator") + + relCount := 0 + defer span.SetAttributes(attribute.Int("count", relCount)) + + if err := iter.Do(func(row *spanner.Row) error { + var resourceObjectType string + var resourceObjectID string + var relation string + var subjectObjectType string + var subjectObjectID string + var subjectRelation string + var caveatName spanner.NullString + var caveatCtx spanner.NullJSON + err := row.Columns( + &resourceObjectType, + &resourceObjectID, + &relation, + &subjectObjectType, + &subjectObjectID, + &subjectRelation, + &caveatName, + &caveatCtx, + ) + if err != nil { + return err + } + + caveat, err := ContextualizedCaveatFrom(caveatName, caveatCtx) + if err != nil { + return err + } + + relCount++ + if !yield(tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.ObjectAndRelation{ + ObjectType: resourceObjectType, + ObjectID: resourceObjectID, + Relation: relation, + }, + Subject: tuple.ObjectAndRelation{ + ObjectType: subjectObjectType, + ObjectID: subjectObjectID, + Relation: subjectRelation, + }, + }, + OptionalCaveat: caveat, + }, nil) { + return errStopIterator + } + + return nil + }); err != nil { + if errors.Is(err, errStopIterator) { + return + } + + yield(tuple.Relationship{}, err) + return } - - tuples = append(tuples, nextTuple) - - return nil - }); err != nil { - return nil, err - } - - span.AddEvent("finished reading iterator", trace.WithAttributes(attribute.Int("tupleCount", len(tuples)))) - span.SetAttributes(attribute.Int("count", len(tuples))) - return tuples, nil + }, nil } } @@ -328,7 +357,7 @@ var queryTuples = sql.Select( colCaveatContext, ).From(tableRelationship) -var countTuples = sql.Select("COUNT(*)").From(tableRelationship) +var countRels = sql.Select("COUNT(*)").From(tableRelationship) var queryTuplesForDelete = sql.Select( colNamespace, diff --git a/internal/datastore/spanner/readwrite.go b/internal/datastore/spanner/readwrite.go index a83fe194f2..afc0b2c510 100644 --- a/internal/datastore/spanner/readwrite.go +++ b/internal/datastore/spanner/readwrite.go @@ -17,6 +17,7 @@ import ( "github.com/authzed/spicedb/pkg/datastore/options" core "github.com/authzed/spicedb/pkg/proto/core/v1" "github.com/authzed/spicedb/pkg/spiceerrors" + "github.com/authzed/spicedb/pkg/tuple" ) type spannerReadWriteTXN struct { @@ -104,10 +105,10 @@ func (rwt spannerReadWriteTXN) StoreCounterValue(ctx context.Context, name strin return nil } -func (rwt spannerReadWriteTXN) WriteRelationships(ctx context.Context, mutations []*core.RelationTupleUpdate) error { +func (rwt spannerReadWriteTXN) WriteRelationships(ctx context.Context, mutations []tuple.RelationshipUpdate) error { var rowCountChange int64 for _, mutation := range mutations { - txnMut, countChange, err := spannerMutation(ctx, mutation.Operation, mutation.Tuple) + txnMut, countChange, err := spannerMutation(ctx, mutation.Operation, mutation.Relationship) if err != nil { return fmt.Errorf(errUnableToWriteRelationships, err) } @@ -123,22 +124,22 @@ func (rwt spannerReadWriteTXN) WriteRelationships(ctx context.Context, mutations func spannerMutation( ctx context.Context, - operation core.RelationTupleUpdate_Operation, - tpl *core.RelationTuple, + operation tuple.UpdateOperation, + rel tuple.Relationship, ) (txnMut *spanner.Mutation, countChange int64, err error) { switch operation { - case core.RelationTupleUpdate_TOUCH: + case tuple.UpdateOperationTouch: countChange = 1 - txnMut = spanner.InsertOrUpdate(tableRelationship, allRelationshipCols, upsertVals(tpl)) - case core.RelationTupleUpdate_CREATE: + txnMut = spanner.InsertOrUpdate(tableRelationship, allRelationshipCols, upsertVals(rel)) + case tuple.UpdateOperationCreate: countChange = 1 - txnMut = spanner.Insert(tableRelationship, allRelationshipCols, upsertVals(tpl)) - case core.RelationTupleUpdate_DELETE: + txnMut = spanner.Insert(tableRelationship, allRelationshipCols, upsertVals(rel)) + case tuple.UpdateOperationDelete: countChange = -1 - txnMut = spanner.Delete(tableRelationship, keyFromRelationship(tpl)) + txnMut = spanner.Delete(tableRelationship, keyFromRelationship(rel)) default: - log.Ctx(ctx).Error().Stringer("operation", operation).Msg("unknown operation type") - err = fmt.Errorf("unknown mutation operation: %s", operation) + log.Ctx(ctx).Error().Msg("unknown operation type") + err = fmt.Errorf("unknown mutation operation: %v", operation) return } @@ -213,23 +214,41 @@ func deleteWithFilterAndLimit(ctx context.Context, rwt *spanner.ReadWriteTransac defer iter.Stop() if err := iter.Do(func(row *spanner.Row) error { - nextTuple := &core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{}, - Subject: &core.ObjectAndRelation{}, - } + var resourceObjectType string + var resourceObjectID string + var relation string + var subjectObjectType string + var subjectObjectID string + var subjectRelation string + err := row.Columns( - &nextTuple.ResourceAndRelation.Namespace, - &nextTuple.ResourceAndRelation.ObjectId, - &nextTuple.ResourceAndRelation.Relation, - &nextTuple.Subject.Namespace, - &nextTuple.Subject.ObjectId, - &nextTuple.Subject.Relation, + &resourceObjectType, + &resourceObjectID, + &relation, + &subjectObjectType, + &subjectObjectID, + &subjectRelation, ) if err != nil { return err } - mutations = append(mutations, spanner.Delete(tableRelationship, keyFromRelationship(nextTuple))) + nextRel := tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.ObjectAndRelation{ + ObjectType: resourceObjectType, + ObjectID: resourceObjectID, + Relation: relation, + }, + Subject: tuple.ObjectAndRelation{ + ObjectType: subjectObjectType, + ObjectID: subjectObjectID, + Relation: subjectRelation, + }, + }, + } + + mutations = append(mutations, spanner.Delete(tableRelationship, keyFromRelationship(nextRel))) return nil }); err != nil { return -1, err @@ -297,31 +316,31 @@ func applyFilterToQuery[T builder[T]](query T, filter *v1.RelationshipFilter) (T return query, nil } -func upsertVals(r *core.RelationTuple) []any { +func upsertVals(r tuple.Relationship) []any { key := keyFromRelationship(r) key = append(key, spanner.CommitTimestamp) key = append(key, caveatVals(r)...) return key } -func keyFromRelationship(r *core.RelationTuple) spanner.Key { +func keyFromRelationship(r tuple.Relationship) spanner.Key { return spanner.Key{ - r.ResourceAndRelation.Namespace, - r.ResourceAndRelation.ObjectId, - r.ResourceAndRelation.Relation, - r.Subject.Namespace, - r.Subject.ObjectId, + r.Resource.ObjectType, + r.Resource.ObjectID, + r.Resource.Relation, + r.Subject.ObjectType, + r.Subject.ObjectID, r.Subject.Relation, } } -func caveatVals(r *core.RelationTuple) []any { - if r.Caveat == nil { +func caveatVals(r tuple.Relationship) []any { + if r.OptionalCaveat == nil { return []any{"", nil} } - vals := []any{r.Caveat.CaveatName} - if r.Caveat.Context != nil { - vals = append(vals, spanner.NullJSON{Value: r.Caveat.Context, Valid: true}) + vals := []any{r.OptionalCaveat.CaveatName} + if r.OptionalCaveat.Context != nil { + vals = append(vals, spanner.NullJSON{Value: r.OptionalCaveat.Context, Valid: true}) } else { vals = append(vals, nil) } @@ -366,10 +385,10 @@ func (rwt spannerReadWriteTXN) DeleteNamespaces(ctx context.Context, nsNames ... func (rwt spannerReadWriteTXN) BulkLoad(ctx context.Context, iter datastore.BulkWriteRelationshipSource) (uint64, error) { var numLoaded uint64 - var tpl *core.RelationTuple + var rel *tuple.Relationship var err error - for tpl, err = iter.Next(ctx); err == nil && tpl != nil; tpl, err = iter.Next(ctx) { - txnMut, _, err := spannerMutation(ctx, core.RelationTupleUpdate_CREATE, tpl) + for rel, err = iter.Next(ctx); err == nil && rel != nil; rel, err = iter.Next(ctx) { + txnMut, _, err := spannerMutation(ctx, tuple.UpdateOperationCreate, *rel) if err != nil { return 0, fmt.Errorf(errUnableToBulkLoadRelationships, err) } diff --git a/internal/datastore/spanner/spanner.go b/internal/datastore/spanner/spanner.go index 76ec854b2a..8c80fceb35 100644 --- a/internal/datastore/spanner/spanner.go +++ b/internal/datastore/spanner/spanner.go @@ -30,7 +30,7 @@ import ( log "github.com/authzed/spicedb/internal/logging" "github.com/authzed/spicedb/pkg/datastore" "github.com/authzed/spicedb/pkg/datastore/options" - core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/tuple" ) func init() { @@ -381,16 +381,18 @@ func convertToWriteConstraintError(err error) error { description := spanner.ErrDesc(err) found := alreadyExistsRegex.FindStringSubmatch(description) if found != nil { - return common.NewCreateRelationshipExistsError(&core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: found[1], - ObjectId: found[2], - Relation: found[3], - }, - Subject: &core.ObjectAndRelation{ - Namespace: found[4], - ObjectId: found[5], - Relation: found[6], + return common.NewCreateRelationshipExistsError(&tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.ObjectAndRelation{ + ObjectType: found[1], + ObjectID: found[2], + Relation: found[3], + }, + Subject: tuple.ObjectAndRelation{ + ObjectType: found[4], + ObjectID: found[5], + Relation: found[6], + }, }, }) } diff --git a/internal/datastore/spanner/spanner_test.go b/internal/datastore/spanner/spanner_test.go index 35cb4a3926..2ae0662716 100644 --- a/internal/datastore/spanner/spanner_test.go +++ b/internal/datastore/spanner/spanner_test.go @@ -18,7 +18,6 @@ import ( testdatastore "github.com/authzed/spicedb/internal/testserver/datastore" "github.com/authzed/spicedb/pkg/datastore" "github.com/authzed/spicedb/pkg/datastore/test" - corev1 "github.com/authzed/spicedb/pkg/proto/core/v1" "github.com/authzed/spicedb/pkg/tuple" ) @@ -106,7 +105,7 @@ func FakeStatsTest(t *testing.T, ds datastore.Datastore) { // Add some relationships. _, err = ds.ReadWriteTx(ctx, func(ctx context.Context, tx datastore.ReadWriteTransaction) error { - return tx.WriteRelationships(ctx, []*corev1.RelationTupleUpdate{ + return tx.WriteRelationships(ctx, []tuple.RelationshipUpdate{ tuple.Create(tuple.MustParse("document:foo#viewer@user:tom")), tuple.Create(tuple.MustParse("document:foo#viewer@user:sarah")), tuple.Create(tuple.MustParse("document:foo#viewer@user:fred")), @@ -134,7 +133,7 @@ func FakeStatsTest(t *testing.T, ds datastore.Datastore) { // Add some more relationships. _, err = ds.ReadWriteTx(ctx, func(ctx context.Context, tx datastore.ReadWriteTransaction) error { - return tx.WriteRelationships(ctx, []*corev1.RelationTupleUpdate{ + return tx.WriteRelationships(ctx, []tuple.RelationshipUpdate{ tuple.Create(tuple.MustParse("document:foo#viewer@user:tommy1236512365123651236512365123612365123655")), tuple.Create(tuple.MustParse("document:foo#viewer@user:sara1236512365123651236512365123651236512365")), tuple.Create(tuple.MustParse("document:foo#viewer@user:freddy1236512365123651236512365123651236512365")), diff --git a/internal/datastore/spanner/watch.go b/internal/datastore/spanner/watch.go index 5a533e4b8a..af8d68f78f 100644 --- a/internal/datastore/spanner/watch.go +++ b/internal/datastore/spanner/watch.go @@ -20,6 +20,7 @@ import ( "github.com/authzed/spicedb/pkg/datastore" core "github.com/authzed/spicedb/pkg/proto/core/v1" "github.com/authzed/spicedb/pkg/spiceerrors" + "github.com/authzed/spicedb/pkg/tuple" ) const ( @@ -198,19 +199,19 @@ func (sd *spannerDatastore) watch( case "DELETE": switch dcr.TableName { case tableRelationship: - relationTuple := relationTupleFromPrimaryKey(primaryKeyColumnValues) + relationship := relationshipFromPrimaryKey(primaryKeyColumnValues) oldValues, ok := mod.OldValues.Value.(map[string]any) if !ok { return spiceerrors.MustBugf("error converting old values map") } - relationTuple.Caveat, err = contextualizedCaveatFromValues(oldValues) + relationship.OptionalCaveat, err = contextualizedCaveatFromValues(oldValues) if err != nil { return err } - err := tracked.AddRelationshipChange(ctx, changeRevision, relationTuple, core.RelationTupleUpdate_DELETE) + err := tracked.AddRelationshipChange(ctx, changeRevision, relationship, tuple.UpdateOperationDelete) if err != nil { return err } @@ -262,7 +263,7 @@ func (sd *spannerDatastore) watch( switch dcr.TableName { case tableRelationship: - relationTuple := relationTupleFromPrimaryKey(primaryKeyColumnValues) + relationship := relationshipFromPrimaryKey(primaryKeyColumnValues) oldValues, ok := mod.OldValues.Value.(map[string]any) if !ok { @@ -282,12 +283,12 @@ func (sd *spannerDatastore) watch( continue } - relationTuple.Caveat, err = contextualizedCaveatFromValues(newValues) + relationship.OptionalCaveat, err = contextualizedCaveatFromValues(newValues) if err != nil { return err } - err := tracked.AddRelationshipChange(ctx, changeRevision, relationTuple, core.RelationTupleUpdate_TOUCH) + err := tracked.AddRelationshipChange(ctx, changeRevision, relationship, tuple.UpdateOperationTouch) if err != nil { return err } @@ -389,17 +390,19 @@ func unmarshalSchemaDefinition(def unmarshallable, configValue any) error { return nil } -func relationTupleFromPrimaryKey(primaryKeyColumnValues map[string]any) *core.RelationTuple { - return &core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: primaryKeyColumnValues[colNamespace].(string), - ObjectId: primaryKeyColumnValues[colObjectID].(string), - Relation: primaryKeyColumnValues[colRelation].(string), - }, - Subject: &core.ObjectAndRelation{ - Namespace: primaryKeyColumnValues[colUsersetNamespace].(string), - ObjectId: primaryKeyColumnValues[colUsersetObjectID].(string), - Relation: primaryKeyColumnValues[colUsersetRelation].(string), +func relationshipFromPrimaryKey(primaryKeyColumnValues map[string]any) tuple.Relationship { + return tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.ObjectAndRelation{ + ObjectType: primaryKeyColumnValues[colNamespace].(string), + ObjectID: primaryKeyColumnValues[colObjectID].(string), + Relation: primaryKeyColumnValues[colRelation].(string), + }, + Subject: tuple.ObjectAndRelation{ + ObjectType: primaryKeyColumnValues[colUsersetNamespace].(string), + ObjectID: primaryKeyColumnValues[colUsersetObjectID].(string), + Relation: primaryKeyColumnValues[colUsersetRelation].(string), + }, }, } } diff --git a/internal/developmentmembership/foundsubject.go b/internal/developmentmembership/foundsubject.go index 520ee72ac6..bf93d56293 100644 --- a/internal/developmentmembership/foundsubject.go +++ b/internal/developmentmembership/foundsubject.go @@ -10,15 +10,15 @@ import ( ) // NewFoundSubject creates a new FoundSubject for a subject and a set of its resources. -func NewFoundSubject(subject *core.DirectSubject, resources ...*core.ObjectAndRelation) FoundSubject { - return FoundSubject{subject.Subject, nil, subject.CaveatExpression, NewONRSet(resources...)} +func NewFoundSubject(subject *core.DirectSubject, resources ...tuple.ObjectAndRelation) FoundSubject { + return FoundSubject{tuple.FromCoreObjectAndRelation(subject.Subject), nil, subject.CaveatExpression, NewONRSet(resources...)} } // FoundSubject contains a single found subject and all the relationships in which that subject // is a member which were found via the ONRs expansion. type FoundSubject struct { // subject is the subject found. - subject *core.ObjectAndRelation + subject tuple.ObjectAndRelation // excludedSubjects are any subjects excluded. Only should be set if subject is a wildcard. excludedSubjects []FoundSubject @@ -26,16 +26,16 @@ type FoundSubject struct { // caveatExpression is the conditional expression on the found subject. caveatExpression *core.CaveatExpression - // relations are the relations under which the subject lives that informed the locating + // resources are the resources under which the subject lives that informed the locating // of this subject for the root ONR. - relationships ONRSet + resources ONRSet } // GetSubjectId is named to match the Subject interface for the BaseSubjectSet. // //nolint:all func (fs FoundSubject) GetSubjectId() string { - return fs.subject.ObjectId + return fs.subject.ObjectID } func (fs FoundSubject) GetCaveatExpression() *core.CaveatExpression { @@ -47,14 +47,14 @@ func (fs FoundSubject) GetExcludedSubjects() []FoundSubject { } // Subject returns the Subject of the FoundSubject. -func (fs FoundSubject) Subject() *core.ObjectAndRelation { +func (fs FoundSubject) Subject() tuple.ObjectAndRelation { return fs.subject } // WildcardType returns the object type for the wildcard subject, if this is a wildcard subject. func (fs FoundSubject) WildcardType() (string, bool) { - if fs.subject.ObjectId == tuple.PublicWildcard { - return fs.subject.Namespace, true + if fs.subject.ObjectID == tuple.PublicWildcard { + return fs.subject.ObjectType, true } return "", false @@ -63,18 +63,13 @@ func (fs FoundSubject) WildcardType() (string, bool) { // ExcludedSubjectsFromWildcard returns those subjects excluded from the wildcard subject. // If not a wildcard subject, returns false. func (fs FoundSubject) ExcludedSubjectsFromWildcard() ([]FoundSubject, bool) { - if fs.subject.ObjectId == tuple.PublicWildcard { + if fs.subject.ObjectID == tuple.PublicWildcard { return fs.excludedSubjects, true } return nil, false } -// Relationships returns all the relationships in which the subject was found as per the expand. -func (fs FoundSubject) Relationships() []*core.ObjectAndRelation { - return fs.relationships.AsSlice() -} - func (fs FoundSubject) excludedSubjectStrings() []string { excludedStrings := make([]string, 0, len(fs.excludedSubjects)) for _, excludedSubject := range fs.excludedSubjects { @@ -110,6 +105,11 @@ func (fs FoundSubject) String() string { return fs.ToValidationString() } +// ParentResources returns all the resources in which the subject was found as per the expand. +func (fs FoundSubject) ParentResources() []tuple.ObjectAndRelation { + return fs.resources.AsSlice() +} + // FoundSubjects contains the subjects found for a specific ONR. type FoundSubjects struct { // subjects is a map from the Subject ONR (as a string) to the FoundSubject information. @@ -122,6 +122,6 @@ func (fs FoundSubjects) ListFound() []FoundSubject { } // LookupSubject returns the FoundSubject for a matching subject, if any. -func (fs FoundSubjects) LookupSubject(subject *core.ObjectAndRelation) (FoundSubject, bool) { +func (fs FoundSubjects) LookupSubject(subject tuple.ObjectAndRelation) (FoundSubject, bool) { return fs.subjects.Get(subject) } diff --git a/internal/developmentmembership/foundsubject_test.go b/internal/developmentmembership/foundsubject_test.go index 2d907fb46c..7c2991bf8a 100644 --- a/internal/developmentmembership/foundsubject_test.go +++ b/internal/developmentmembership/foundsubject_test.go @@ -7,19 +7,20 @@ import ( "github.com/stretchr/testify/require" "github.com/authzed/spicedb/internal/caveats" + "github.com/authzed/spicedb/pkg/tuple" "github.com/authzed/spicedb/pkg/validationfile/blocks" ) func cfs(subjectType string, subjectID string, subjectRel string, excludedSubjectIDs []string, caveatName string) FoundSubject { excludedSubjects := make([]FoundSubject, 0, len(excludedSubjectIDs)) for _, excludedSubjectID := range excludedSubjectIDs { - excludedSubjects = append(excludedSubjects, FoundSubject{subject: ONR(subjectType, excludedSubjectID, subjectRel)}) + excludedSubjects = append(excludedSubjects, FoundSubject{subject: tuple.ONR(subjectType, excludedSubjectID, subjectRel)}) } return FoundSubject{ - subject: ONR(subjectType, subjectID, subjectRel), + subject: tuple.ONR(subjectType, subjectID, subjectRel), excludedSubjects: excludedSubjects, - relationships: NewONRSet(), + resources: NewONRSet(), caveatExpression: caveats.CaveatExprForTesting(caveatName), } } diff --git a/internal/developmentmembership/membership.go b/internal/developmentmembership/membership.go index 5c4d28fa20..caa5e8f846 100644 --- a/internal/developmentmembership/membership.go +++ b/internal/developmentmembership/membership.go @@ -34,7 +34,7 @@ func NewMembershipSet() *Set { // AddExpansion adds the expansion of an ONR to the membership set. Returns false if the ONR was already added. // // NOTE: The expansion tree *should* be the fully recursive expansion. -func (ms *Set) AddExpansion(onr *core.ObjectAndRelation, expansion *core.RelationTupleTreeNode) (FoundSubjects, bool, error) { +func (ms *Set) AddExpansion(onr tuple.ObjectAndRelation, expansion *core.RelationTupleTreeNode) (FoundSubjects, bool, error) { onrString := tuple.StringONR(onr) existing, ok := ms.objectsAndRelations[onrString] if ok { @@ -53,13 +53,13 @@ func (ms *Set) AddExpansion(onr *core.ObjectAndRelation, expansion *core.Relatio // AccessibleExpansionSubjects returns a TrackingSubjectSet representing the set of accessible subjects in the expansion. func AccessibleExpansionSubjects(treeNode *core.RelationTupleTreeNode) (*TrackingSubjectSet, error) { - return populateFoundSubjects(treeNode.Expanded, treeNode) + return populateFoundSubjects(tuple.FromCoreObjectAndRelation(treeNode.Expanded), treeNode) } -func populateFoundSubjects(rootONR *core.ObjectAndRelation, treeNode *core.RelationTupleTreeNode) (*TrackingSubjectSet, error) { +func populateFoundSubjects(rootONR tuple.ObjectAndRelation, treeNode *core.RelationTupleTreeNode) (*TrackingSubjectSet, error) { resource := rootONR if treeNode.Expanded != nil { - resource = treeNode.Expanded + resource = tuple.FromCoreObjectAndRelation(treeNode.Expanded) } switch typed := treeNode.NodeType.(type) { @@ -155,7 +155,7 @@ func populateFoundSubjects(rootONR *core.ObjectAndRelation, treeNode *core.Relat return nil, err } - fs.relationships.Add(resource) + fs.resources.Add(resource) } toReturn.ApplyParentCaveatExpression(treeNode.CaveatExpression) diff --git a/internal/developmentmembership/membership_test.go b/internal/developmentmembership/membership_test.go index d674ce700b..21e476650e 100644 --- a/internal/developmentmembership/membership_test.go +++ b/internal/developmentmembership/membership_test.go @@ -16,7 +16,7 @@ import ( ) var ( - ONR = tuple.ObjectAndRelation + ONR = tuple.CoreONR Ellipsis = "..." ) @@ -34,33 +34,35 @@ func CaveatedDS(objectType string, objectID string, objectRelation string, cavea } var ( - _this *core.ObjectAndRelation + _this *tuple.ObjectAndRelation + ownerONR = tuple.ONR("folder", "company", "owner") - companyOwner = graph.Leaf(ONR("folder", "company", "owner"), + companyOwner = graph.Leaf(&ownerONR, (DS("user", "owner", Ellipsis)), ) - companyEditor = graph.Union(ONR("folder", "company", "editor"), + companyEditor = graph.Union(tuple.ONRRef("folder", "company", "editor"), graph.Leaf(_this, (DS("user", "writer", Ellipsis))), companyOwner, ) - auditorsOwner = graph.Leaf(ONR("folder", "auditors", "owner")) + auditorONR = tuple.ONR("folder", "auditors", "owner") + auditorsOwner = graph.Leaf(&auditorONR) - auditorsEditor = graph.Union(ONR("folder", "auditors", "editor"), + auditorsEditor = graph.Union(tuple.ONRRef("folder", "auditors", "editor"), graph.Leaf(_this), auditorsOwner, ) - auditorsViewerRecursive = graph.Union(ONR("folder", "auditors", "viewer"), + auditorsViewerRecursive = graph.Union(tuple.ONRRef("folder", "auditors", "viewer"), graph.Leaf(_this, (DS("user", "auditor", "...")), ), auditorsEditor, - graph.Union(ONR("folder", "auditors", "viewer")), + graph.Union(tuple.ONRRef("folder", "auditors", "viewer")), ) - companyViewerRecursive = graph.Union(ONR("folder", "company", "viewer"), - graph.Union(ONR("folder", "company", "viewer"), + companyViewerRecursive = graph.Union(tuple.ONRRef("folder", "company", "viewer"), + graph.Union(tuple.ONRRef("folder", "company", "viewer"), auditorsViewerRecursive, graph.Leaf(_this, (DS("user", "legal", "...")), @@ -68,7 +70,7 @@ var ( ), ), companyEditor, - graph.Union(ONR("folder", "company", "viewer")), + graph.Union(tuple.ONRRef("folder", "company", "viewer")), ) ) @@ -77,17 +79,17 @@ func TestMembershipSetBasic(t *testing.T) { ms := NewMembershipSet() // Add some expansion trees. - fso, ok, err := ms.AddExpansion(ONR("folder", "company", "owner"), companyOwner) + fso, ok, err := ms.AddExpansion(tuple.ONR("folder", "company", "owner"), companyOwner) require.True(ok) require.NoError(err) verifySubjects(t, require, fso, "user:owner") - fse, ok, err := ms.AddExpansion(ONR("folder", "company", "editor"), companyEditor) + fse, ok, err := ms.AddExpansion(tuple.ONR("folder", "company", "editor"), companyEditor) require.True(ok) require.NoError(err) verifySubjects(t, require, fse, "user:owner", "user:writer") - fsv, ok, err := ms.AddExpansion(ONR("folder", "company", "viewer"), companyViewerRecursive) + fsv, ok, err := ms.AddExpansion(tuple.ONR("folder", "company", "viewer"), companyViewerRecursive) require.True(ok) require.NoError(err) verifySubjects(t, require, fsv, "folder:auditors#viewer", "user:auditor", "user:legal", "user:owner", "user:writer") @@ -97,7 +99,7 @@ func TestMembershipSetIntersectionBasic(t *testing.T) { require := require.New(t) ms := NewMembershipSet() - intersection := graph.Intersection(ONR("folder", "company", "viewer"), + intersection := graph.Intersection(tuple.ONRRef("folder", "company", "viewer"), graph.Leaf(_this, (DS("user", "legal", "...")), ), @@ -107,7 +109,7 @@ func TestMembershipSetIntersectionBasic(t *testing.T) { ), ) - fso, ok, err := ms.AddExpansion(ONR("folder", "company", "viewer"), intersection) + fso, ok, err := ms.AddExpansion(tuple.ONR("folder", "company", "viewer"), intersection) require.True(ok) require.NoError(err) verifySubjects(t, require, fso, "user:legal") @@ -117,7 +119,7 @@ func TestMembershipSetIntersectionWithDifferentTypesOneMissingLeft(t *testing.T) require := require.New(t) ms := NewMembershipSet() - intersection := graph.Intersection(ONR("folder", "company", "viewer"), + intersection := graph.Intersection(tuple.ONRRef("folder", "company", "viewer"), graph.Leaf(_this, (DS("user", "legal", "...")), (DS("folder", "foobar", "...")), @@ -128,7 +130,7 @@ func TestMembershipSetIntersectionWithDifferentTypesOneMissingLeft(t *testing.T) ), ) - fso, ok, err := ms.AddExpansion(ONR("folder", "company", "viewer"), intersection) + fso, ok, err := ms.AddExpansion(tuple.ONR("folder", "company", "viewer"), intersection) require.True(ok) require.NoError(err) verifySubjects(t, require, fso, "user:legal") @@ -138,7 +140,7 @@ func TestMembershipSetIntersectionWithDifferentTypesOneMissingRight(t *testing.T require := require.New(t) ms := NewMembershipSet() - intersection := graph.Intersection(ONR("folder", "company", "viewer"), + intersection := graph.Intersection(tuple.ONRRef("folder", "company", "viewer"), graph.Leaf(_this, (DS("user", "legal", "...")), ), @@ -149,7 +151,7 @@ func TestMembershipSetIntersectionWithDifferentTypesOneMissingRight(t *testing.T ), ) - fso, ok, err := ms.AddExpansion(ONR("folder", "company", "viewer"), intersection) + fso, ok, err := ms.AddExpansion(tuple.ONR("folder", "company", "viewer"), intersection) require.True(ok) require.NoError(err) verifySubjects(t, require, fso, "user:legal") @@ -159,7 +161,7 @@ func TestMembershipSetIntersectionWithDifferentTypes(t *testing.T) { require := require.New(t) ms := NewMembershipSet() - intersection := graph.Intersection(ONR("folder", "company", "viewer"), + intersection := graph.Intersection(tuple.ONRRef("folder", "company", "viewer"), graph.Leaf(_this, (DS("user", "legal", "...")), (DS("folder", "foobar", "...")), @@ -172,7 +174,7 @@ func TestMembershipSetIntersectionWithDifferentTypes(t *testing.T) { ), ) - fso, ok, err := ms.AddExpansion(ONR("folder", "company", "viewer"), intersection) + fso, ok, err := ms.AddExpansion(tuple.ONR("folder", "company", "viewer"), intersection) require.True(ok) require.NoError(err) verifySubjects(t, require, fso, "folder:barbaz", "user:legal") @@ -182,7 +184,7 @@ func TestMembershipSetExclusion(t *testing.T) { require := require.New(t) ms := NewMembershipSet() - exclusion := graph.Exclusion(ONR("folder", "company", "viewer"), + exclusion := graph.Exclusion(tuple.ONRRef("folder", "company", "viewer"), graph.Leaf(_this, (DS("user", "owner", "...")), (DS("user", "legal", "...")), @@ -192,7 +194,7 @@ func TestMembershipSetExclusion(t *testing.T) { ), ) - fso, ok, err := ms.AddExpansion(ONR("folder", "company", "viewer"), exclusion) + fso, ok, err := ms.AddExpansion(tuple.ONR("folder", "company", "viewer"), exclusion) require.True(ok) require.NoError(err) verifySubjects(t, require, fso, "user:owner") @@ -202,7 +204,7 @@ func TestMembershipSetExclusionMultiple(t *testing.T) { require := require.New(t) ms := NewMembershipSet() - exclusion := graph.Exclusion(ONR("folder", "company", "viewer"), + exclusion := graph.Exclusion(tuple.ONRRef("folder", "company", "viewer"), graph.Leaf(_this, (DS("user", "owner", "...")), (DS("user", "legal", "...")), @@ -216,7 +218,7 @@ func TestMembershipSetExclusionMultiple(t *testing.T) { ), ) - fso, ok, err := ms.AddExpansion(ONR("folder", "company", "viewer"), exclusion) + fso, ok, err := ms.AddExpansion(tuple.ONR("folder", "company", "viewer"), exclusion) require.True(ok) require.NoError(err) verifySubjects(t, require, fso, "user:third") @@ -226,7 +228,7 @@ func TestMembershipSetExclusionMultipleWithWildcard(t *testing.T) { require := require.New(t) ms := NewMembershipSet() - exclusion := graph.Exclusion(ONR("folder", "company", "viewer"), + exclusion := graph.Exclusion(tuple.ONRRef("folder", "company", "viewer"), graph.Leaf(_this, (DS("user", "owner", "...")), (DS("user", "legal", "...")), @@ -239,7 +241,7 @@ func TestMembershipSetExclusionMultipleWithWildcard(t *testing.T) { ), ) - fso, ok, err := ms.AddExpansion(ONR("folder", "company", "viewer"), exclusion) + fso, ok, err := ms.AddExpansion(tuple.ONR("folder", "company", "viewer"), exclusion) require.True(ok) require.NoError(err) verifySubjects(t, require, fso) @@ -249,7 +251,7 @@ func TestMembershipSetExclusionMultipleMiddle(t *testing.T) { require := require.New(t) ms := NewMembershipSet() - exclusion := graph.Exclusion(ONR("folder", "company", "viewer"), + exclusion := graph.Exclusion(tuple.ONRRef("folder", "company", "viewer"), graph.Leaf(_this, (DS("user", "owner", "...")), (DS("user", "legal", "...")), @@ -263,7 +265,7 @@ func TestMembershipSetExclusionMultipleMiddle(t *testing.T) { ), ) - fso, ok, err := ms.AddExpansion(ONR("folder", "company", "viewer"), exclusion) + fso, ok, err := ms.AddExpansion(tuple.ONR("folder", "company", "viewer"), exclusion) require.True(ok) require.NoError(err) verifySubjects(t, require, fso, "user:third", "user:legal") @@ -273,7 +275,7 @@ func TestMembershipSetIntersectionWithOneWildcard(t *testing.T) { require := require.New(t) ms := NewMembershipSet() - intersection := graph.Intersection(ONR("folder", "company", "viewer"), + intersection := graph.Intersection(tuple.ONRRef("folder", "company", "viewer"), graph.Leaf(_this, (DS("user", "owner", "...")), (DS("user", "*", "...")), @@ -283,7 +285,7 @@ func TestMembershipSetIntersectionWithOneWildcard(t *testing.T) { ), ) - fso, ok, err := ms.AddExpansion(ONR("folder", "company", "viewer"), intersection) + fso, ok, err := ms.AddExpansion(tuple.ONR("folder", "company", "viewer"), intersection) require.True(ok) require.NoError(err) verifySubjects(t, require, fso, "user:legal") @@ -293,7 +295,7 @@ func TestMembershipSetIntersectionWithAllWildcardLeft(t *testing.T) { require := require.New(t) ms := NewMembershipSet() - intersection := graph.Intersection(ONR("folder", "company", "viewer"), + intersection := graph.Intersection(tuple.ONRRef("folder", "company", "viewer"), graph.Leaf(_this, (DS("user", "owner", "...")), (DS("user", "*", "...")), @@ -303,7 +305,7 @@ func TestMembershipSetIntersectionWithAllWildcardLeft(t *testing.T) { ), ) - fso, ok, err := ms.AddExpansion(ONR("folder", "company", "viewer"), intersection) + fso, ok, err := ms.AddExpansion(tuple.ONR("folder", "company", "viewer"), intersection) require.True(ok) require.NoError(err) verifySubjects(t, require, fso, "user:*", "user:owner") @@ -313,7 +315,7 @@ func TestMembershipSetIntersectionWithAllWildcardRight(t *testing.T) { require := require.New(t) ms := NewMembershipSet() - intersection := graph.Intersection(ONR("folder", "company", "viewer"), + intersection := graph.Intersection(tuple.ONRRef("folder", "company", "viewer"), graph.Leaf(_this, (DS("user", "*", "...")), ), @@ -323,7 +325,7 @@ func TestMembershipSetIntersectionWithAllWildcardRight(t *testing.T) { ), ) - fso, ok, err := ms.AddExpansion(ONR("folder", "company", "viewer"), intersection) + fso, ok, err := ms.AddExpansion(tuple.ONR("folder", "company", "viewer"), intersection) require.True(ok) require.NoError(err) verifySubjects(t, require, fso, "user:*", "user:owner") @@ -333,7 +335,7 @@ func TestMembershipSetExclusionWithLeftWildcard(t *testing.T) { require := require.New(t) ms := NewMembershipSet() - exclusion := graph.Exclusion(ONR("folder", "company", "viewer"), + exclusion := graph.Exclusion(tuple.ONRRef("folder", "company", "viewer"), graph.Leaf(_this, (DS("user", "owner", "...")), (DS("user", "*", "...")), @@ -343,7 +345,7 @@ func TestMembershipSetExclusionWithLeftWildcard(t *testing.T) { ), ) - fso, ok, err := ms.AddExpansion(ONR("folder", "company", "viewer"), exclusion) + fso, ok, err := ms.AddExpansion(tuple.ONR("folder", "company", "viewer"), exclusion) require.True(ok) require.NoError(err) verifySubjects(t, require, fso, "user:*", "user:owner") @@ -353,7 +355,7 @@ func TestMembershipSetExclusionWithRightWildcard(t *testing.T) { require := require.New(t) ms := NewMembershipSet() - exclusion := graph.Exclusion(ONR("folder", "company", "viewer"), + exclusion := graph.Exclusion(tuple.ONRRef("folder", "company", "viewer"), graph.Leaf(_this, (DS("user", "owner", "...")), (DS("user", "legal", "...")), @@ -363,7 +365,7 @@ func TestMembershipSetExclusionWithRightWildcard(t *testing.T) { ), ) - fso, ok, err := ms.AddExpansion(ONR("folder", "company", "viewer"), exclusion) + fso, ok, err := ms.AddExpansion(tuple.ONR("folder", "company", "viewer"), exclusion) require.True(ok) require.NoError(err) verifySubjects(t, require, fso) @@ -373,7 +375,7 @@ func TestMembershipSetIntersectionWithThreeWildcards(t *testing.T) { require := require.New(t) ms := NewMembershipSet() - intersection := graph.Intersection(ONR("folder", "company", "viewer"), + intersection := graph.Intersection(tuple.ONRRef("folder", "company", "viewer"), graph.Leaf(_this, (DS("user", "owner", "...")), (DS("user", "legal", "...")), @@ -386,7 +388,7 @@ func TestMembershipSetIntersectionWithThreeWildcards(t *testing.T) { ), ) - fso, ok, err := ms.AddExpansion(ONR("folder", "company", "viewer"), intersection) + fso, ok, err := ms.AddExpansion(tuple.ONR("folder", "company", "viewer"), intersection) require.True(ok) require.NoError(err) verifySubjects(t, require, fso, "user:owner", "user:legal") @@ -396,7 +398,7 @@ func TestMembershipSetIntersectionWithOneBranchMissingWildcards(t *testing.T) { require := require.New(t) ms := NewMembershipSet() - intersection := graph.Intersection(ONR("folder", "company", "viewer"), + intersection := graph.Intersection(tuple.ONRRef("folder", "company", "viewer"), graph.Leaf(_this, (DS("user", "owner", "...")), (DS("user", "legal", "...")), @@ -410,7 +412,7 @@ func TestMembershipSetIntersectionWithOneBranchMissingWildcards(t *testing.T) { ), ) - fso, ok, err := ms.AddExpansion(ONR("folder", "company", "viewer"), intersection) + fso, ok, err := ms.AddExpansion(tuple.ONR("folder", "company", "viewer"), intersection) require.True(ok) require.NoError(err) verifySubjects(t, require, fso, "user:owner") @@ -420,7 +422,7 @@ func TestMembershipSetIntersectionWithTwoBranchesMissingWildcards(t *testing.T) require := require.New(t) ms := NewMembershipSet() - intersection := graph.Intersection(ONR("folder", "company", "viewer"), + intersection := graph.Intersection(tuple.ONRRef("folder", "company", "viewer"), graph.Leaf(_this, (DS("user", "owner", "...")), (DS("user", "legal", "...")), @@ -433,7 +435,7 @@ func TestMembershipSetIntersectionWithTwoBranchesMissingWildcards(t *testing.T) ), ) - fso, ok, err := ms.AddExpansion(ONR("folder", "company", "viewer"), intersection) + fso, ok, err := ms.AddExpansion(tuple.ONR("folder", "company", "viewer"), intersection) require.True(ok) require.NoError(err) verifySubjects(t, require, fso) @@ -443,7 +445,7 @@ func TestMembershipSetWithCaveats(t *testing.T) { require := require.New(t) ms := NewMembershipSet() - intersection := graph.Intersection(ONR("folder", "company", "viewer"), + intersection := graph.Intersection(tuple.ONRRef("folder", "company", "viewer"), graph.Leaf(_this, (DS("user", "owner", "...")), (DS("user", "legal", "...")), @@ -458,13 +460,13 @@ func TestMembershipSetWithCaveats(t *testing.T) { ) intersection.CaveatExpression = caveats.CaveatExprForTesting("anothercaveat") - fso, ok, err := ms.AddExpansion(ONR("folder", "company", "viewer"), intersection) + fso, ok, err := ms.AddExpansion(tuple.ONR("folder", "company", "viewer"), intersection) require.True(ok) require.NoError(err) verifySubjects(t, require, fso, "user:owner") // Verify the caveat on the user:owner. - subject, ok := fso.LookupSubject(ONR("user", "owner", "...")) + subject, ok := fso.LookupSubject(tuple.ONR("user", "owner", "...")) require.True(ok) testutil.RequireProtoEqual(t, subject.GetCaveatExpression(), caveats.And( @@ -474,7 +476,7 @@ func TestMembershipSetWithCaveats(t *testing.T) { } func verifySubjects(t *testing.T, require *require.Assertions, fs FoundSubjects, expected ...string) { - foundSubjects := []*core.ObjectAndRelation{} + foundSubjects := []tuple.ObjectAndRelation{} for _, found := range fs.ListFound() { foundSubjects = append(foundSubjects, found.Subject()) diff --git a/internal/developmentmembership/onrset.go b/internal/developmentmembership/onrset.go index 1da1cb5717..ad7fcfd3c7 100644 --- a/internal/developmentmembership/onrset.go +++ b/internal/developmentmembership/onrset.go @@ -4,25 +4,20 @@ import ( "github.com/ccoveille/go-safecast" "github.com/authzed/spicedb/pkg/genutil/mapz" - core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/tuple" ) -// onrStruct is a struct holding a namespace and relation. -type onrStruct struct { - Namespace string - ObjectID string - Relation string -} +// TODO(jschorr): Replace with the generic set over tuple.ObjectAndRelation // ONRSet is a set of ObjectAndRelation's. type ONRSet struct { - onrs *mapz.Set[onrStruct] + onrs *mapz.Set[tuple.ObjectAndRelation] } // NewONRSet creates a new set. -func NewONRSet(onrs ...*core.ObjectAndRelation) ONRSet { +func NewONRSet(onrs ...tuple.ObjectAndRelation) ONRSet { created := ONRSet{ - onrs: mapz.NewSet[onrStruct](), + onrs: mapz.NewSet[tuple.ObjectAndRelation](), } created.Update(onrs) return created @@ -41,20 +36,18 @@ func (ons ONRSet) IsEmpty() bool { } // Has returns true if the set contains the given ONR. -func (ons ONRSet) Has(onr *core.ObjectAndRelation) bool { - key := onrStruct{onr.Namespace, onr.ObjectId, onr.Relation} - return ons.onrs.Has(key) +func (ons ONRSet) Has(onr tuple.ObjectAndRelation) bool { + return ons.onrs.Has(onr) } // Add adds the given ONR to the set. Returns true if the object was not in the set before this // call and false otherwise. -func (ons ONRSet) Add(onr *core.ObjectAndRelation) bool { - key := onrStruct{onr.Namespace, onr.ObjectId, onr.Relation} - return ons.onrs.Add(key) +func (ons ONRSet) Add(onr tuple.ObjectAndRelation) bool { + return ons.onrs.Add(onr) } // Update updates the set by adding the given ONRs to it. -func (ons ONRSet) Update(onrs []*core.ObjectAndRelation) { +func (ons ONRSet) Update(onrs []tuple.ObjectAndRelation) { for _, onr := range onrs { ons.Add(onr) } @@ -84,14 +77,10 @@ func (ons ONRSet) Union(otherSet ONRSet) ONRSet { } // AsSlice returns the ONRs found in the set as a slice. -func (ons ONRSet) AsSlice() []*core.ObjectAndRelation { - slice := make([]*core.ObjectAndRelation, 0, ons.Length()) - _ = ons.onrs.ForEach(func(rr onrStruct) error { - slice = append(slice, &core.ObjectAndRelation{ - Namespace: rr.Namespace, - ObjectId: rr.ObjectID, - Relation: rr.Relation, - }) +func (ons ONRSet) AsSlice() []tuple.ObjectAndRelation { + slice := make([]tuple.ObjectAndRelation, 0, ons.Length()) + _ = ons.onrs.ForEach(func(onr tuple.ObjectAndRelation) error { + slice = append(slice, onr) return nil }) return slice diff --git a/internal/developmentmembership/onrset_test.go b/internal/developmentmembership/onrset_test.go index d6384d9b70..755d43b700 100644 --- a/internal/developmentmembership/onrset_test.go +++ b/internal/developmentmembership/onrset_test.go @@ -5,7 +5,6 @@ import ( "github.com/stretchr/testify/require" - core "github.com/authzed/spicedb/pkg/proto/core/v1" "github.com/authzed/spicedb/pkg/tuple" ) @@ -14,61 +13,61 @@ func TestONRSet(t *testing.T) { require.True(t, set.IsEmpty()) require.Equal(t, uint64(0), set.Length()) - require.True(t, set.Add(tuple.ParseONR("resource:1#viewer"))) + require.True(t, set.Add(tuple.MustParseONR("resource:1#viewer"))) require.False(t, set.IsEmpty()) require.Equal(t, uint64(1), set.Length()) - require.True(t, set.Add(tuple.ParseONR("resource:2#viewer"))) - require.True(t, set.Add(tuple.ParseONR("resource:3#viewer"))) + require.True(t, set.Add(tuple.MustParseONR("resource:2#viewer"))) + require.True(t, set.Add(tuple.MustParseONR("resource:3#viewer"))) require.Equal(t, uint64(3), set.Length()) - require.False(t, set.Add(tuple.ParseONR("resource:1#viewer"))) - require.True(t, set.Add(tuple.ParseONR("resource:1#editor"))) + require.False(t, set.Add(tuple.MustParseONR("resource:1#viewer"))) + require.True(t, set.Add(tuple.MustParseONR("resource:1#editor"))) - require.True(t, set.Has(tuple.ParseONR("resource:1#viewer"))) - require.True(t, set.Has(tuple.ParseONR("resource:1#editor"))) - require.False(t, set.Has(tuple.ParseONR("resource:1#owner"))) - require.False(t, set.Has(tuple.ParseONR("resource:1#admin"))) - require.False(t, set.Has(tuple.ParseONR("resource:1#reader"))) + require.True(t, set.Has(tuple.MustParseONR("resource:1#viewer"))) + require.True(t, set.Has(tuple.MustParseONR("resource:1#editor"))) + require.False(t, set.Has(tuple.MustParseONR("resource:1#owner"))) + require.False(t, set.Has(tuple.MustParseONR("resource:1#admin"))) + require.False(t, set.Has(tuple.MustParseONR("resource:1#reader"))) - require.True(t, set.Has(tuple.ParseONR("resource:2#viewer"))) + require.True(t, set.Has(tuple.MustParseONR("resource:2#viewer"))) } func TestONRSetUpdate(t *testing.T) { set := NewONRSet() - set.Update([]*core.ObjectAndRelation{ - tuple.ParseONR("resource:1#viewer"), - tuple.ParseONR("resource:2#viewer"), - tuple.ParseONR("resource:3#viewer"), + set.Update([]tuple.ObjectAndRelation{ + tuple.MustParseONR("resource:1#viewer"), + tuple.MustParseONR("resource:2#viewer"), + tuple.MustParseONR("resource:3#viewer"), }) require.Equal(t, uint64(3), set.Length()) - set.Update([]*core.ObjectAndRelation{ - tuple.ParseONR("resource:1#viewer"), - tuple.ParseONR("resource:1#editor"), - tuple.ParseONR("resource:1#owner"), - tuple.ParseONR("resource:1#admin"), - tuple.ParseONR("resource:1#reader"), + set.Update([]tuple.ObjectAndRelation{ + tuple.MustParseONR("resource:1#viewer"), + tuple.MustParseONR("resource:1#editor"), + tuple.MustParseONR("resource:1#owner"), + tuple.MustParseONR("resource:1#admin"), + tuple.MustParseONR("resource:1#reader"), }) require.Equal(t, uint64(7), set.Length()) } func TestONRSetIntersect(t *testing.T) { set1 := NewONRSet() - set1.Update([]*core.ObjectAndRelation{ - tuple.ParseONR("resource:1#viewer"), - tuple.ParseONR("resource:2#viewer"), - tuple.ParseONR("resource:3#viewer"), + set1.Update([]tuple.ObjectAndRelation{ + tuple.MustParseONR("resource:1#viewer"), + tuple.MustParseONR("resource:2#viewer"), + tuple.MustParseONR("resource:3#viewer"), }) set2 := NewONRSet() - set2.Update([]*core.ObjectAndRelation{ - tuple.ParseONR("resource:1#viewer"), - tuple.ParseONR("resource:1#editor"), - tuple.ParseONR("resource:1#owner"), - tuple.ParseONR("resource:1#admin"), - tuple.ParseONR("resource:2#viewer"), - tuple.ParseONR("resource:1#reader"), + set2.Update([]tuple.ObjectAndRelation{ + tuple.MustParseONR("resource:1#viewer"), + tuple.MustParseONR("resource:1#editor"), + tuple.MustParseONR("resource:1#owner"), + tuple.MustParseONR("resource:1#admin"), + tuple.MustParseONR("resource:2#viewer"), + tuple.MustParseONR("resource:1#reader"), }) require.Equal(t, uint64(2), set1.Intersect(set2).Length()) @@ -77,20 +76,20 @@ func TestONRSetIntersect(t *testing.T) { func TestONRSetSubtract(t *testing.T) { set1 := NewONRSet() - set1.Update([]*core.ObjectAndRelation{ - tuple.ParseONR("resource:1#viewer"), - tuple.ParseONR("resource:2#viewer"), - tuple.ParseONR("resource:3#viewer"), + set1.Update([]tuple.ObjectAndRelation{ + tuple.MustParseONR("resource:1#viewer"), + tuple.MustParseONR("resource:2#viewer"), + tuple.MustParseONR("resource:3#viewer"), }) set2 := NewONRSet() - set2.Update([]*core.ObjectAndRelation{ - tuple.ParseONR("resource:1#viewer"), - tuple.ParseONR("resource:1#editor"), - tuple.ParseONR("resource:1#owner"), - tuple.ParseONR("resource:1#admin"), - tuple.ParseONR("resource:2#viewer"), - tuple.ParseONR("resource:1#reader"), + set2.Update([]tuple.ObjectAndRelation{ + tuple.MustParseONR("resource:1#viewer"), + tuple.MustParseONR("resource:1#editor"), + tuple.MustParseONR("resource:1#owner"), + tuple.MustParseONR("resource:1#admin"), + tuple.MustParseONR("resource:2#viewer"), + tuple.MustParseONR("resource:1#reader"), }) require.Equal(t, uint64(1), set1.Subtract(set2).Length()) @@ -99,20 +98,20 @@ func TestONRSetSubtract(t *testing.T) { func TestONRSetUnion(t *testing.T) { set1 := NewONRSet() - set1.Update([]*core.ObjectAndRelation{ - tuple.ParseONR("resource:1#viewer"), - tuple.ParseONR("resource:2#viewer"), - tuple.ParseONR("resource:3#viewer"), + set1.Update([]tuple.ObjectAndRelation{ + tuple.MustParseONR("resource:1#viewer"), + tuple.MustParseONR("resource:2#viewer"), + tuple.MustParseONR("resource:3#viewer"), }) set2 := NewONRSet() - set2.Update([]*core.ObjectAndRelation{ - tuple.ParseONR("resource:1#viewer"), - tuple.ParseONR("resource:1#editor"), - tuple.ParseONR("resource:1#owner"), - tuple.ParseONR("resource:1#admin"), - tuple.ParseONR("resource:2#viewer"), - tuple.ParseONR("resource:1#reader"), + set2.Update([]tuple.ObjectAndRelation{ + tuple.MustParseONR("resource:1#viewer"), + tuple.MustParseONR("resource:1#editor"), + tuple.MustParseONR("resource:1#owner"), + tuple.MustParseONR("resource:1#admin"), + tuple.MustParseONR("resource:2#viewer"), + tuple.MustParseONR("resource:1#reader"), }) require.Equal(t, uint64(7), set1.Union(set2).Length()) @@ -121,23 +120,23 @@ func TestONRSetUnion(t *testing.T) { func TestONRSetWith(t *testing.T) { set1 := NewONRSet() - set1.Update([]*core.ObjectAndRelation{ - tuple.ParseONR("resource:1#viewer"), - tuple.ParseONR("resource:2#viewer"), - tuple.ParseONR("resource:3#viewer"), + set1.Update([]tuple.ObjectAndRelation{ + tuple.MustParseONR("resource:1#viewer"), + tuple.MustParseONR("resource:2#viewer"), + tuple.MustParseONR("resource:3#viewer"), }) - added := set1.Union(NewONRSet(tuple.ParseONR("resource:1#editor"))) + added := set1.Union(NewONRSet(tuple.MustParseONR("resource:1#editor"))) require.Equal(t, uint64(3), set1.Length()) require.Equal(t, uint64(4), added.Length()) } func TestONRSetAsSlice(t *testing.T) { set := NewONRSet() - set.Update([]*core.ObjectAndRelation{ - tuple.ParseONR("resource:1#viewer"), - tuple.ParseONR("resource:2#viewer"), - tuple.ParseONR("resource:3#viewer"), + set.Update([]tuple.ObjectAndRelation{ + tuple.MustParseONR("resource:1#viewer"), + tuple.MustParseONR("resource:2#viewer"), + tuple.MustParseONR("resource:3#viewer"), }) require.Equal(t, 3, len(set.AsSlice())) diff --git a/internal/developmentmembership/trackingsubjectset.go b/internal/developmentmembership/trackingsubjectset.go index b66a08940d..00f8836a88 100644 --- a/internal/developmentmembership/trackingsubjectset.go +++ b/internal/developmentmembership/trackingsubjectset.go @@ -12,13 +12,13 @@ import ( // NOTE: This is designed solely for the developer API and testing and should *not* be used in any // performance sensitive code. type TrackingSubjectSet struct { - setByType map[string]datasets.BaseSubjectSet[FoundSubject] + setByType map[tuple.RelationReference]datasets.BaseSubjectSet[FoundSubject] } // NewTrackingSubjectSet creates a new TrackingSubjectSet func NewTrackingSubjectSet() *TrackingSubjectSet { tss := &TrackingSubjectSet{ - setByType: map[string]datasets.BaseSubjectSet[FoundSubject]{}, + setByType: map[tuple.RelationReference]datasets.BaseSubjectSet[FoundSubject]{}, } return tss } @@ -81,26 +81,25 @@ func (tss *TrackingSubjectSet) Add(subjectsAndResources ...FoundSubject) error { return nil } -func (tss *TrackingSubjectSet) getSetForKey(key string) datasets.BaseSubjectSet[FoundSubject] { +func (tss *TrackingSubjectSet) getSetForKey(key tuple.RelationReference) datasets.BaseSubjectSet[FoundSubject] { if existing, ok := tss.setByType[key]; ok { return existing } - ns, rel := tuple.MustSplitRelRef(key) created := datasets.NewBaseSubjectSet( func(subjectID string, caveatExpression *core.CaveatExpression, excludedSubjects []FoundSubject, sources ...FoundSubject) FoundSubject { fs := NewFoundSubject(&core.DirectSubject{ Subject: &core.ObjectAndRelation{ - Namespace: ns, + Namespace: key.ObjectType, ObjectId: subjectID, - Relation: rel, + Relation: key.Relation, }, CaveatExpression: caveatExpression, }) fs.excludedSubjects = excludedSubjects fs.caveatExpression = caveatExpression for _, source := range sources { - fs.relationships.UpdateFrom(source.relationships) + fs.resources.UpdateFrom(source.resources) } return fs }, @@ -110,21 +109,21 @@ func (tss *TrackingSubjectSet) getSetForKey(key string) datasets.BaseSubjectSet[ } func (tss *TrackingSubjectSet) getSet(fs FoundSubject) datasets.BaseSubjectSet[FoundSubject] { - return tss.getSetForKey(tuple.JoinRelRef(fs.subject.Namespace, fs.subject.Relation)) + return tss.getSetForKey(fs.subject.RelationReference()) } // Get returns the found subject in the set, if any. -func (tss *TrackingSubjectSet) Get(subject *core.ObjectAndRelation) (FoundSubject, bool) { - set, ok := tss.setByType[tuple.JoinRelRef(subject.Namespace, subject.Relation)] +func (tss *TrackingSubjectSet) Get(subject tuple.ObjectAndRelation) (FoundSubject, bool) { + set, ok := tss.setByType[subject.RelationReference()] if !ok { return FoundSubject{}, false } - return set.Get(subject.ObjectId) + return set.Get(subject.ObjectID) } // Contains returns true if the set contains the given subject. -func (tss *TrackingSubjectSet) Contains(subject *core.ObjectAndRelation) bool { +func (tss *TrackingSubjectSet) Contains(subject tuple.ObjectAndRelation) bool { _, ok := tss.Get(subject) return ok } @@ -190,9 +189,9 @@ func (tss *TrackingSubjectSet) ApplyParentCaveatExpression(parentCaveatExpr *cor // removeExact removes the given subject(s) from the set. If the subject is a wildcard, only // the exact matching wildcard will be removed. -func (tss *TrackingSubjectSet) removeExact(subjects ...*core.ObjectAndRelation) { +func (tss *TrackingSubjectSet) removeExact(subjects ...tuple.ObjectAndRelation) { for _, subject := range subjects { - if set, ok := tss.setByType[tuple.JoinRelRef(subject.Namespace, subject.Relation)]; ok { + if set, ok := tss.setByType[subject.RelationReference()]; ok { set.UnsafeRemoveExact(FoundSubject{ subject: subject, }) diff --git a/internal/developmentmembership/trackingsubjectset_test.go b/internal/developmentmembership/trackingsubjectset_test.go index 700662288e..e02b06a314 100644 --- a/internal/developmentmembership/trackingsubjectset_test.go +++ b/internal/developmentmembership/trackingsubjectset_test.go @@ -7,6 +7,7 @@ import ( "github.com/authzed/spicedb/pkg/genutil/mapz" core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/tuple" ) func set(subjects ...*core.DirectSubject) *TrackingSubjectSet { @@ -45,13 +46,13 @@ func subtract(firstSet *TrackingSubjectSet, sets ...*TrackingSubjectSet) *Tracki func fs(subjectType string, subjectID string, subjectRel string, excludedSubjectIDs ...string) FoundSubject { excludedSubjects := make([]FoundSubject, 0, len(excludedSubjectIDs)) for _, excludedSubjectID := range excludedSubjectIDs { - excludedSubjects = append(excludedSubjects, FoundSubject{subject: ONR(subjectType, excludedSubjectID, subjectRel)}) + excludedSubjects = append(excludedSubjects, FoundSubject{subject: tuple.ONR(subjectType, excludedSubjectID, subjectRel)}) } return FoundSubject{ - subject: ONR(subjectType, subjectID, subjectRel), + subject: tuple.ONR(subjectType, subjectID, subjectRel), excludedSubjects: excludedSubjects, - relationships: NewONRSet(), + resources: NewONRSet(), } } @@ -351,35 +352,35 @@ func TestTrackingSubjectSet(t *testing.T) { func TestTrackingSubjectSetResourceTracking(t *testing.T) { tss := NewTrackingSubjectSet() - tss.MustAdd(NewFoundSubject(DS("user", "tom", "..."), ONR("resource", "foo", "viewer"))) - tss.MustAdd(NewFoundSubject(DS("user", "tom", "..."), ONR("resource", "bar", "viewer"))) + tss.MustAdd(NewFoundSubject(DS("user", "tom", "..."), tuple.ONR("resource", "foo", "viewer"))) + tss.MustAdd(NewFoundSubject(DS("user", "tom", "..."), tuple.ONR("resource", "bar", "viewer"))) - found, ok := tss.Get(ONR("user", "tom", "...")) + found, ok := tss.Get(tuple.ONR("user", "tom", "...")) require.True(t, ok) - require.Equal(t, 2, len(found.Relationships())) + require.Equal(t, 2, len(found.ParentResources())) sss := NewTrackingSubjectSet() - sss.MustAdd(NewFoundSubject(DS("user", "tom", "..."), ONR("resource", "baz", "viewer"))) + sss.MustAdd(NewFoundSubject(DS("user", "tom", "..."), tuple.ONR("resource", "baz", "viewer"))) intersection, err := tss.Intersect(sss) require.NoError(t, err) - found, ok = intersection.Get(ONR("user", "tom", "...")) + found, ok = intersection.Get(tuple.ONR("user", "tom", "...")) require.True(t, ok) - require.Equal(t, 3, len(found.Relationships())) + require.Equal(t, 3, len(found.ParentResources())) } func TestTrackingSubjectSetResourceTrackingWithWildcard(t *testing.T) { tss := NewTrackingSubjectSet() - tss.MustAdd(NewFoundSubject(DS("user", "tom", "..."), ONR("resource", "foo", "viewer"))) + tss.MustAdd(NewFoundSubject(DS("user", "tom", "..."), tuple.ONR("resource", "foo", "viewer"))) sss := NewTrackingSubjectSet() - sss.MustAdd(NewFoundSubject(DS("user", "*", "..."), ONR("resource", "baz", "viewer"))) + sss.MustAdd(NewFoundSubject(DS("user", "*", "..."), tuple.ONR("resource", "baz", "viewer"))) intersection, err := tss.Intersect(sss) require.NoError(t, err) - found, ok := intersection.Get(ONR("user", "tom", "...")) + found, ok := intersection.Get(tuple.ONR("user", "tom", "...")) require.True(t, ok) - require.Equal(t, 1, len(found.Relationships())) + require.Equal(t, 1, len(found.ParentResources())) } diff --git a/internal/dispatch/caching/cachingdispatch_test.go b/internal/dispatch/caching/cachingdispatch_test.go index 089d63cbc7..535b8411c8 100644 --- a/internal/dispatch/caching/cachingdispatch_test.go +++ b/internal/dispatch/caching/cachingdispatch_test.go @@ -94,18 +94,20 @@ func TestMaxDepthCaching(t *testing.T) { for _, step := range tc.script { if step.expectPassthrough { - parsed := tuple.ParseONR(step.start) + parsed, err := tuple.ParseONR(step.start) + require.NoError(err) + delegate.On("DispatchCheck", &v1.DispatchCheckRequest{ - ResourceRelation: RR(parsed.Namespace, parsed.Relation), - ResourceIds: []string{parsed.ObjectId}, - Subject: tuple.ParseSubjectONR(step.goal), + ResourceRelation: RR(parsed.ObjectType, parsed.Relation), + ResourceIds: []string{parsed.ObjectID}, + Subject: tuple.MustParseSubjectONR(step.goal).ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: step.atRevision.String(), DepthRemaining: step.depthRemaining, }, }).Return(&v1.DispatchCheckResponse{ ResultsByResourceId: map[string]*v1.ResourceCheckResult{ - parsed.ObjectId: { + parsed.ObjectID: { Membership: v1.ResourceCheckResult_MEMBER, }, }, @@ -123,18 +125,20 @@ func TestMaxDepthCaching(t *testing.T) { defer dispatch.Close() for _, step := range tc.script { - parsed := tuple.ParseONR(step.start) + parsed, err := tuple.ParseONR(step.start) + require.NoError(err) + resp, err := dispatch.DispatchCheck(context.Background(), &v1.DispatchCheckRequest{ - ResourceRelation: RR(parsed.Namespace, parsed.Relation), - ResourceIds: []string{parsed.ObjectId}, - Subject: tuple.ParseSubjectONR(step.goal), + ResourceRelation: RR(parsed.ObjectType, parsed.Relation), + ResourceIds: []string{parsed.ObjectID}, + Subject: tuple.MustParseSubjectONR(step.goal).ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: step.atRevision.String(), DepthRemaining: step.depthRemaining, }, }) require.NoError(err) - require.Equal(v1.ResourceCheckResult_MEMBER, resp.ResultsByResourceId[parsed.ObjectId].Membership) + require.Equal(v1.ResourceCheckResult_MEMBER, resp.ResultsByResourceId[parsed.ObjectID].Membership) // We have to sleep a while to let the cache converge time.Sleep(10 * time.Millisecond) diff --git a/internal/dispatch/combined/combined_test.go b/internal/dispatch/combined/combined_test.go index 8562838eef..a15e3196ff 100644 --- a/internal/dispatch/combined/combined_test.go +++ b/internal/dispatch/combined/combined_test.go @@ -32,7 +32,7 @@ func TestCombinedRecursiveCall(t *testing.T) { relation viewer: resource#viewer | user permission view = viewer } - `, []*core.RelationTuple{ + `, []tuple.Relationship{ tuple.MustParse("resource:someresource#viewer@resource:someresource#viewer"), }, require.New(t)) diff --git a/internal/dispatch/graph/check_test.go b/internal/dispatch/graph/check_test.go index 80a7615879..83453c7eb5 100644 --- a/internal/dispatch/graph/check_test.go +++ b/internal/dispatch/graph/check_test.go @@ -26,7 +26,7 @@ import ( "github.com/authzed/spicedb/pkg/tuple" ) -var ONR = tuple.ObjectAndRelation +var ONR = tuple.ONR func TestSimpleCheck(t *testing.T) { t.Parallel() @@ -37,7 +37,7 @@ func TestSimpleCheck(t *testing.T) { } type userset struct { - userset *core.ObjectAndRelation + userset tuple.ObjectAndRelation expected []expected } @@ -108,8 +108,8 @@ func TestSimpleCheck(t *testing.T) { tc.namespace, tc.objectID, expected.relation, - userset.userset.Namespace, - userset.userset.ObjectId, + userset.userset.ObjectType, + userset.userset.ObjectID, userset.userset.Relation, expected.isMember, ) @@ -124,10 +124,10 @@ func TestSimpleCheck(t *testing.T) { ctx, dispatch, revision := newLocalDispatcher(t) checkResult, err := dispatch.DispatchCheck(ctx, &v1.DispatchCheckRequest{ - ResourceRelation: RR(tc.namespace, expected.relation), + ResourceRelation: RR(tc.namespace, expected.relation).ToCoreRR(), ResourceIds: []string{tc.objectID}, ResultsSetting: v1.DispatchCheckRequest_ALLOW_SINGLE_RESULT, - Subject: userset.userset, + Subject: userset.userset.ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -158,21 +158,21 @@ func TestMaxDepth(t *testing.T) { ds, _ := testfixtures.StandardDatastoreWithSchema(rawDS, require) - mutation := tuple.Create(tuple.Parse("folder:oops#parent@folder:oops")) + mutation := tuple.Create(tuple.MustParse("folder:oops#parent@folder:oops")) ctx := log.Logger.WithContext(datastoremw.ContextWithHandle(context.Background())) require.NoError(datastoremw.SetInContext(ctx, ds)) - revision, err := common.UpdateTuplesInDatastore(ctx, ds, mutation) + revision, err := common.UpdateRelationshipsInDatastore(ctx, ds, mutation) require.NoError(err) dispatch := NewLocalOnlyDispatcher(10, 100) _, err = dispatch.DispatchCheck(ctx, &v1.DispatchCheckRequest{ - ResourceRelation: RR("folder", "view"), + ResourceRelation: RR("folder", "view").ToCoreRR(), ResourceIds: []string{"oops"}, ResultsSetting: v1.DispatchCheckRequest_ALLOW_SINGLE_RESULT, - Subject: ONR("user", "fake", graph.Ellipsis), + Subject: tuple.CoreONR("user", "fake", graph.Ellipsis), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -192,7 +192,7 @@ func TestCheckMetadata(t *testing.T) { } type userset struct { - userset *core.ObjectAndRelation + userset tuple.ObjectAndRelation expected []expected } @@ -247,8 +247,8 @@ func TestCheckMetadata(t *testing.T) { tc.namespace, tc.objectID, expected.relation, - userset.userset.Namespace, - userset.userset.ObjectId, + userset.userset.ObjectType, + userset.userset.ObjectID, userset.userset.Relation, expected.isMember, ) @@ -262,10 +262,10 @@ func TestCheckMetadata(t *testing.T) { ctx, dispatch, revision := newLocalDispatcher(t) checkResult, err := dispatch.DispatchCheck(ctx, &v1.DispatchCheckRequest{ - ResourceRelation: RR(tc.namespace, expected.relation), + ResourceRelation: RR(tc.namespace, expected.relation).ToCoreRR(), ResourceIds: []string{tc.objectID}, ResultsSetting: v1.DispatchCheckRequest_ALLOW_SINGLE_RESULT, - Subject: userset.userset, + Subject: userset.userset.ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -293,9 +293,9 @@ func TestCheckPermissionOverSchema(t *testing.T) { testCases := []struct { name string schema string - relationships []*core.RelationTuple - resource *core.ObjectAndRelation - subject *core.ObjectAndRelation + relationships []tuple.Relationship + resource tuple.ObjectAndRelation + subject tuple.ObjectAndRelation expectedPermissionship v1.ResourceCheckResult_Membership expectedCaveat *core.CaveatExpression alternativeExpectedCaveat *core.CaveatExpression @@ -309,7 +309,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation viewer: user permission view = viewer + editor }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:first#viewer@user:tom"), }, ONR("document", "first", "view"), @@ -327,7 +327,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation viewer: user permission view = viewer & editor }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:first#viewer@user:tom"), tuple.MustParse("document:first#editor@user:tom"), }, @@ -346,7 +346,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation viewer: user permission view = viewer - editor }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:first#viewer@user:tom"), }, ONR("document", "first", "view"), @@ -364,7 +364,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation viewer: user permission view = viewer + editor }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:first#viewer@user:tom"), tuple.MustParse("document:first#editor@user:tom"), }, @@ -383,7 +383,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation viewer: user permission view = viewer + editor }`, - []*core.RelationTuple{}, + []tuple.Relationship{}, ONR("document", "first", "view"), ONR("user", "tom", "..."), v1.ResourceCheckResult_NOT_MEMBER, @@ -399,7 +399,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation viewer: user permission view = viewer & editor }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:first#viewer@user:tom"), }, ONR("document", "first", "view"), @@ -417,7 +417,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation viewer: user permission view = viewer - banned }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:first#viewer@user:tom"), tuple.MustParse("document:first#banned@user:tom"), }, @@ -441,7 +441,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation group: group permission view = group->view }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:first#group@group:first"), tuple.MustParse("document:first#group@group:second"), tuple.MustParse("group:first#member@user:tom"), @@ -468,7 +468,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation group: group permission view = group->view }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:first#group@group:first"), tuple.MustParse("document:first#group@group:second"), tuple.MustParse("group:first#member@user:tom"), @@ -495,7 +495,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation group: group permission view = group->view }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:first#group@group:first"), tuple.MustParse("document:first#group@group:second"), tuple.MustParse("group:first#member@user:tom"), @@ -523,7 +523,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation group: group permission view = group->view }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:first#group@group:first"), tuple.MustParse("document:first#group@group:second"), tuple.MustParse("group:first#member@user:tom"), @@ -547,7 +547,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation orgs: organization permission view = orgs->member }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:first#orgs@organization:first"), tuple.MustParse("document:first#orgs@organization:second"), tuple.MustParse("organization:second#member@user:tom"), @@ -570,7 +570,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation orgs: organization permission view = orgs.any(member) }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:first#orgs@organization:first"), tuple.MustParse("document:first#orgs@organization:second"), tuple.MustParse("organization:second#member@user:tom"), @@ -593,7 +593,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation orgs: organization permission view = orgs.all(member) }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:first#orgs@organization:first"), tuple.MustParse("document:first#orgs@organization:second"), tuple.MustParse("organization:second#member@user:tom"), @@ -616,7 +616,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation orgs: organization permission view = orgs.all(member) }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:first#orgs@organization:first"), tuple.MustParse("document:first#orgs@organization:second"), tuple.MustParse("organization:first#member@user:tom"), @@ -644,7 +644,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation orgs: organization | someotherresource permission view = orgs.all(member) }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:first#orgs@organization:first"), tuple.MustParse("document:first#orgs@organization:second"), tuple.MustParse("organization:first#member@user:tom"), @@ -672,7 +672,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation orgs: organization | someotherresource permission view = orgs.all(member) }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:first#orgs@organization:first"), tuple.MustParse("document:first#orgs@organization:second"), tuple.MustParse("document:first#orgs@someotherresource:other"), @@ -701,7 +701,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation orgs: organization | someotherresource permission view = orgs.all(member) }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:first#orgs@organization:first"), tuple.MustParse("document:first#orgs@organization:second"), tuple.MustParse("document:first#orgs@someotherresource:other"), @@ -727,7 +727,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation orgs: organization permission view = orgs.all(member) }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:first#orgs@organization:first"), tuple.MustParse("organization:first#member@user:tom"), }, @@ -749,7 +749,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation orgs: organization permission view = orgs.all(member) }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("organization:first#member@user:tom"), }, ONR("document", "first", "view"), @@ -772,7 +772,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { permission view_by_all = team.all(member) permission view_by_any = team.any(member) }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("team:first#direct_member@user:tom"), tuple.MustParse("team:first#direct_member@user:fred"), tuple.MustParse("team:first#direct_member@user:sarah"), @@ -807,7 +807,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { permission view_by_all = team.all(member) + viewer permission view_by_any = team.any(member) + viewer }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("team:first#direct_member@user:tom"), tuple.MustParse("team:first#direct_member@user:fred"), tuple.MustParse("team:first#direct_member@user:sarah"), @@ -843,7 +843,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { permission view_by_all = team.all(member) + viewer permission view_by_any = team.any(member) + viewer }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("team:first#direct_member@user:tom"), tuple.MustParse("team:first#direct_member@user:fred"), tuple.MustParse("team:first#direct_member@user:sarah"), @@ -881,7 +881,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation team: team with somecaveat permission view_by_all = team.all(member) }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("team:first#direct_member@user:tom"), tuple.MustParse("resource:oneteam#team@team:first[somecaveat]"), }, @@ -908,7 +908,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation team: team permission view_by_all = team.all(member) }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("team:first#direct_member@user:tom[somecaveat]"), tuple.MustParse("resource:oneteam#team@team:first"), }, @@ -935,7 +935,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation team: team with somecaveat permission view_by_all = team.all(member) }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("team:first#direct_member@user:tom[somecaveat]"), tuple.MustParse("resource:oneteam#team@team:first[somecaveat]"), }, @@ -966,7 +966,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation team: team with somecaveat permission view_by_all = team.all(member) }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse(`team:first#direct_member@user:tom[anothercaveat:{"someparam": 43}]`), tuple.MustParse(`resource:oneteam#team@team:first[somecaveat:{"someparam": 42}]`), }, @@ -996,7 +996,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation team: team with somecaveat permission view_by_all = team.all(member) }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse(`resource:someresource#team@team:first[somecaveat:{"someparam": 41}]`), tuple.MustParse(`resource:someresource#team@team:second[somecaveat:{"someparam": 42}]`), tuple.MustParse(`team:first#direct_member@user:tom`), @@ -1028,7 +1028,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation team: team permission view_by_all = team.all(member) }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse(`resource:someresource#team@team:first`), tuple.MustParse(`resource:someresource#team@team:second`), tuple.MustParse(`team:first#direct_member@user:tom[somecaveat:{"someparam": 41}]`), @@ -1060,7 +1060,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation team: team with somecaveat | team permission view_by_all = team.all(member) }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse(`resource:someresource#team@team:first`), tuple.MustParse(`resource:someresource#team@team:second[somecaveat:{"someparam": 42}]`), tuple.MustParse(`team:first#direct_member@user:tom`), @@ -1089,7 +1089,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation team: team permission view_by_all = team.all(member) }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse(`resource:someresource#team@team:first`), tuple.MustParse(`resource:someresource#team@team:second`), tuple.MustParse(`team:first#direct_member@user:tom`), @@ -1118,7 +1118,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation team: team with somecaveat | team#direct_member with somecaveat permission view_by_all = team.all(member) // Note: this points to the same team twice }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse(`resource:someresource#team@team:first`), tuple.MustParse(`resource:someresource#team@team:first#direct_member[somecaveat]`), tuple.MustParse(`team:first#direct_member@user:tom`), @@ -1144,7 +1144,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation folder: folder permission view = folder.all(view) }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("folder:root1#owner@user:tom"), tuple.MustParse("folder:root1#owner@user:fred"), tuple.MustParse("folder:root1#owner@user:sarah"), @@ -1181,7 +1181,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation folder: folder permission view = folder.all(view) }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("folder:root1#owner@user:tom"), tuple.MustParse("folder:root1#owner@user:fred"), tuple.MustParse("folder:root1#owner@user:sarah"), @@ -1218,7 +1218,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { relation folder: folder permission view = folder.all(view) }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("folder:root1#owner@user:tom"), tuple.MustParse("folder:root1#owner@user:fred"), tuple.MustParse("folder:root1#owner@user:sarah"), @@ -1259,7 +1259,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { permission view = viewer } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse(`role:firstrole#member@user:tom[somecaveat:{"somevalue":40}]`), tuple.MustParse(`role:secondrole#member@user:tom[somecaveat:{"somevalue":42}]`), tuple.MustParse(`resource:doc1#viewer@role:firstrole#member`), @@ -1295,7 +1295,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { permission view = viewer } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse(`role:secondrole#member@user:tom[somecaveat:{"somevalue":42}]`), tuple.MustParse(`role:firstrole#member@user:tom[somecaveat:{"somevalue":40}]`), tuple.MustParse(`resource:doc1#viewer@role:secondrole#member`), @@ -1331,9 +1331,9 @@ func TestCheckPermissionOverSchema(t *testing.T) { require.NoError(datastoremw.SetInContext(ctx, ds)) resp, err := dispatcher.DispatchCheck(ctx, &v1.DispatchCheckRequest{ - ResourceRelation: RR(tc.resource.Namespace, tc.resource.Relation), - ResourceIds: []string{tc.resource.ObjectId}, - Subject: tc.subject, + ResourceRelation: RR(tc.resource.ObjectType, tc.resource.Relation).ToCoreRR(), + ResourceIds: []string{tc.resource.ObjectID}, + Subject: tc.subject.ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -1343,22 +1343,22 @@ func TestCheckPermissionOverSchema(t *testing.T) { require.NoError(err) membership := v1.ResourceCheckResult_NOT_MEMBER - if r, ok := resp.ResultsByResourceId[tc.resource.ObjectId]; ok { + if r, ok := resp.ResultsByResourceId[tc.resource.ObjectID]; ok { membership = r.Membership } require.Equal(tc.expectedPermissionship, membership) if tc.expectedCaveat != nil && tc.alternativeExpectedCaveat == nil { - require.NotEmpty(resp.ResultsByResourceId[tc.resource.ObjectId].Expression) - testutil.RequireProtoEqual(t, tc.expectedCaveat, resp.ResultsByResourceId[tc.resource.ObjectId].Expression, "mismatch in caveat") + require.NotEmpty(resp.ResultsByResourceId[tc.resource.ObjectID].Expression) + testutil.RequireProtoEqual(t, tc.expectedCaveat, resp.ResultsByResourceId[tc.resource.ObjectID].Expression, "mismatch in caveat") } if tc.expectedCaveat != nil && tc.alternativeExpectedCaveat != nil { - require.NotEmpty(resp.ResultsByResourceId[tc.resource.ObjectId].Expression) + require.NotEmpty(resp.ResultsByResourceId[tc.resource.ObjectID].Expression) - if testutil.AreProtoEqual(tc.expectedCaveat, resp.ResultsByResourceId[tc.resource.ObjectId].Expression, "mismatch in caveat") != nil { - testutil.RequireProtoEqual(t, tc.alternativeExpectedCaveat, resp.ResultsByResourceId[tc.resource.ObjectId].Expression, "mismatch in caveat") + if testutil.AreProtoEqual(tc.expectedCaveat, resp.ResultsByResourceId[tc.resource.ObjectID].Expression, "mismatch in caveat") != nil { + testutil.RequireProtoEqual(t, tc.alternativeExpectedCaveat, resp.ResultsByResourceId[tc.resource.ObjectID].Expression, "mismatch in caveat") } } }) @@ -1375,7 +1375,7 @@ func addFrame(trace *v1.CheckDebugTrace, foundFrames *mapz.Set[string]) { func TestCheckDebugging(t *testing.T) { t.Parallel() type expectedFrame struct { - resourceType *core.RelationReference + resourceType tuple.RelationReference resourceIDs []string } @@ -1383,7 +1383,7 @@ func TestCheckDebugging(t *testing.T) { namespace string objectID string permission string - subject *core.ObjectAndRelation + subject tuple.ObjectAndRelation expectedFrames []expectedFrame }{ { @@ -1444,8 +1444,8 @@ func TestCheckDebugging(t *testing.T) { tc.namespace, tc.objectID, tc.permission, - tc.subject.Namespace, - tc.subject.ObjectId, + tc.subject.ObjectType, + tc.subject.ObjectID, tc.subject.Relation, ) @@ -1456,10 +1456,10 @@ func TestCheckDebugging(t *testing.T) { ctx, dispatch, revision := newLocalDispatcher(t) checkResult, err := dispatch.DispatchCheck(ctx, &v1.DispatchCheckRequest{ - ResourceRelation: RR(tc.namespace, tc.permission), + ResourceRelation: RR(tc.namespace, tc.permission).ToCoreRR(), ResourceIds: []string{tc.objectID}, ResultsSetting: v1.DispatchCheckRequest_ALLOW_SINGLE_RESULT, - Subject: tc.subject, + Subject: tc.subject.ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -1474,7 +1474,7 @@ func TestCheckDebugging(t *testing.T) { expectedFrames := mapz.NewSet[string]() for _, expectedFrame := range tc.expectedFrames { - expectedFrames.Add(fmt.Sprintf("%s:%s#%s", expectedFrame.resourceType.Namespace, strings.Join(expectedFrame.resourceIDs, ","), expectedFrame.resourceType.Relation)) + expectedFrames.Add(fmt.Sprintf("%s:%s#%s", expectedFrame.resourceType.ObjectType, strings.Join(expectedFrame.resourceIDs, ","), expectedFrame.resourceType.Relation)) } foundFrames := mapz.NewSet[string]() @@ -1490,9 +1490,9 @@ func TestCheckWithHints(t *testing.T) { testCases := []struct { name string schema string - relationships []*core.RelationTuple - resource *core.ObjectAndRelation - subject *core.ObjectAndRelation + relationships []tuple.Relationship + resource tuple.ObjectAndRelation + subject tuple.ObjectAndRelation hints []*v1.CheckHint expectedPermissionship bool }{ @@ -1505,7 +1505,7 @@ func TestCheckWithHints(t *testing.T) { relation viewer: user permission view = viewer + editor }`, - []*core.RelationTuple{}, + []tuple.Relationship{}, ONR("document", "somedoc", "view"), ONR("user", "tom", graph.Ellipsis), nil, @@ -1520,7 +1520,7 @@ func TestCheckWithHints(t *testing.T) { relation viewer: user permission view = viewer + editor }`, - []*core.RelationTuple{}, + []tuple.Relationship{}, ONR("document", "somedoc", "view"), ONR("user", "tom", graph.Ellipsis), []*v1.CheckHint{hints.CheckHintForComputedUserset("document", "somedoc", "viewer", ONR("user", "tom", graph.Ellipsis), &v1.ResourceCheckResult{ @@ -1537,7 +1537,7 @@ func TestCheckWithHints(t *testing.T) { relation viewer: user permission view = viewer + editor }`, - []*core.RelationTuple{}, + []tuple.Relationship{}, ONR("document", "somedoc", "view"), ONR("user", "tom", graph.Ellipsis), []*v1.CheckHint{hints.CheckHintForComputedUserset("document", "anotherdoc", "viewer", ONR("user", "tom", graph.Ellipsis), &v1.ResourceCheckResult{ @@ -1554,7 +1554,7 @@ func TestCheckWithHints(t *testing.T) { relation viewer: user permission view = viewer + editor }`, - []*core.RelationTuple{}, + []tuple.Relationship{}, ONR("document", "somedoc", "view"), ONR("user", "tom", graph.Ellipsis), []*v1.CheckHint{hints.CheckHintForComputedUserset("document", "somedoc", "viewer", ONR("user", "anotheruser", graph.Ellipsis), &v1.ResourceCheckResult{ @@ -1574,7 +1574,7 @@ func TestCheckWithHints(t *testing.T) { relation org: organization permission view = org->member }`, - []*core.RelationTuple{}, + []tuple.Relationship{}, ONR("document", "somedoc", "view"), ONR("user", "tom", graph.Ellipsis), []*v1.CheckHint{hints.CheckHintForArrow("document", "somedoc", "org", "member", ONR("user", "tom", graph.Ellipsis), &v1.ResourceCheckResult{ @@ -1594,7 +1594,7 @@ func TestCheckWithHints(t *testing.T) { relation org: organization permission view = org->member }`, - []*core.RelationTuple{}, + []tuple.Relationship{}, ONR("document", "somedoc", "view"), ONR("user", "tom", graph.Ellipsis), []*v1.CheckHint{hints.CheckHintForArrow("document", "somedoc", "anotherrel", "member", ONR("user", "tom", graph.Ellipsis), &v1.ResourceCheckResult{ @@ -1614,7 +1614,7 @@ func TestCheckWithHints(t *testing.T) { relation org: organization permission view = org->member }`, - []*core.RelationTuple{}, + []tuple.Relationship{}, ONR("document", "somedoc", "view"), ONR("user", "tom", graph.Ellipsis), []*v1.CheckHint{hints.CheckHintForArrow("document", "somedoc", "org", "membersssss", ONR("user", "tom", graph.Ellipsis), &v1.ResourceCheckResult{ @@ -1631,7 +1631,7 @@ func TestCheckWithHints(t *testing.T) { relation viewer: user permission view = viewer & editor }`, - []*core.RelationTuple{}, + []tuple.Relationship{}, ONR("document", "somedoc", "view"), ONR("user", "tom", graph.Ellipsis), []*v1.CheckHint{hints.CheckHintForComputedUserset("document", "somedoc", "viewer", ONR("user", "tom", graph.Ellipsis), &v1.ResourceCheckResult{ @@ -1648,7 +1648,7 @@ func TestCheckWithHints(t *testing.T) { relation viewer: user permission view = viewer & editor }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:somedoc#editor@user:tom"), }, ONR("document", "somedoc", "view"), @@ -1667,7 +1667,7 @@ func TestCheckWithHints(t *testing.T) { relation viewer: user permission view = viewer & editor }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:somedoc#editor@user:tom"), }, ONR("document", "somedoc", "view"), @@ -1686,7 +1686,7 @@ func TestCheckWithHints(t *testing.T) { relation viewer: user permission view = viewer - banned }`, - []*core.RelationTuple{}, + []tuple.Relationship{}, ONR("document", "somedoc", "view"), ONR("user", "tom", graph.Ellipsis), []*v1.CheckHint{hints.CheckHintForComputedUserset("document", "somedoc", "viewer", ONR("user", "tom", graph.Ellipsis), &v1.ResourceCheckResult{ @@ -1702,7 +1702,7 @@ func TestCheckWithHints(t *testing.T) { relation viewer: user:* permission view = viewer }`, - []*core.RelationTuple{}, + []tuple.Relationship{}, ONR("document", "somedoc", "view"), ONR("user", "tom", graph.Ellipsis), []*v1.CheckHint{hints.CheckHintForComputedUserset("document", "somedoc", "viewer", ONR("user", "tom", graph.Ellipsis), &v1.ResourceCheckResult{ @@ -1718,7 +1718,7 @@ func TestCheckWithHints(t *testing.T) { relation viewer: user:* permission view = viewer }`, - []*core.RelationTuple{}, + []tuple.Relationship{}, ONR("document", "somedoc", "view"), ONR("user", "tom", graph.Ellipsis), []*v1.CheckHint{hints.CheckHintForComputedUserset("document", "somedoc", "viewer", ONR("user", "tom", graph.Ellipsis), &v1.ResourceCheckResult{ @@ -1735,7 +1735,7 @@ func TestCheckWithHints(t *testing.T) { relation editor: user permission view = viewer & editor }`, - []*core.RelationTuple{}, + []tuple.Relationship{}, ONR("document", "somedoc", "view"), ONR("user", "tom", graph.Ellipsis), []*v1.CheckHint{ @@ -1757,7 +1757,7 @@ func TestCheckWithHints(t *testing.T) { relation banned: user permission view = viewer - banned }`, - []*core.RelationTuple{}, + []tuple.Relationship{}, ONR("document", "somedoc", "view"), ONR("user", "tom", graph.Ellipsis), []*v1.CheckHint{ @@ -1779,7 +1779,7 @@ func TestCheckWithHints(t *testing.T) { relation banned: user permission view = viewer - banned }`, - []*core.RelationTuple{}, + []tuple.Relationship{}, ONR("document", "somedoc", "view"), ONR("user", "tom", graph.Ellipsis), []*v1.CheckHint{ @@ -1801,7 +1801,7 @@ func TestCheckWithHints(t *testing.T) { relation banned: user permission view = viewer - banned }`, - []*core.RelationTuple{}, + []tuple.Relationship{}, ONR("document", "somedoc", "view"), ONR("user", "tom", graph.Ellipsis), []*v1.CheckHint{ @@ -1832,9 +1832,9 @@ func TestCheckWithHints(t *testing.T) { require.NoError(datastoremw.SetInContext(ctx, ds)) resp, err := dispatcher.DispatchCheck(ctx, &v1.DispatchCheckRequest{ - ResourceRelation: RR(tc.resource.Namespace, tc.resource.Relation), - ResourceIds: []string{tc.resource.ObjectId}, - Subject: tc.subject, + ResourceRelation: RR(tc.resource.ObjectType, tc.resource.Relation).ToCoreRR(), + ResourceIds: []string{tc.resource.ObjectID}, + Subject: tc.subject.ToCoreONR(), ResultsSetting: v1.DispatchCheckRequest_ALLOW_SINGLE_RESULT, Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), @@ -1844,13 +1844,13 @@ func TestCheckWithHints(t *testing.T) { }) require.NoError(err) - _, ok := resp.ResultsByResourceId[tc.resource.ObjectId] + _, ok := resp.ResultsByResourceId[tc.resource.ObjectID] if tc.expectedPermissionship { require.True(ok) - require.Equal(v1.ResourceCheckResult_MEMBER, resp.ResultsByResourceId[tc.resource.ObjectId].Membership) + require.Equal(v1.ResourceCheckResult_MEMBER, resp.ResultsByResourceId[tc.resource.ObjectID].Membership) } else { if ok { - require.Equal(v1.ResourceCheckResult_NOT_MEMBER, resp.ResultsByResourceId[tc.resource.ObjectId].Membership) + require.Equal(v1.ResourceCheckResult_NOT_MEMBER, resp.ResultsByResourceId[tc.resource.ObjectID].Membership) } } }) @@ -1874,7 +1874,7 @@ func TestCheckHintsPartialApplication(t *testing.T) { permission view = viewer } - `, []*core.RelationTuple{ + `, []tuple.Relationship{ tuple.MustParse("document:somedoc#viewer@user:tom"), }, require) @@ -1882,9 +1882,9 @@ func TestCheckHintsPartialApplication(t *testing.T) { require.NoError(datastoremw.SetInContext(ctx, ds)) resp, err := dispatcher.DispatchCheck(ctx, &v1.DispatchCheckRequest{ - ResourceRelation: RR("document", "view"), + ResourceRelation: RR("document", "view").ToCoreRR(), ResourceIds: []string{"somedoc", "anotherdoc", "thirddoc"}, - Subject: ONR("user", "tom", graph.Ellipsis), + Subject: tuple.CoreONR("user", "tom", graph.Ellipsis), ResultsSetting: v1.DispatchCheckRequest_REQUIRE_ALL_RESULTS, Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), @@ -1924,7 +1924,7 @@ func TestCheckHintsPartialApplicationOverArrow(t *testing.T) { permission view = org->member } - `, []*core.RelationTuple{ + `, []tuple.Relationship{ tuple.MustParse("document:somedoc#org@organization:someorg"), tuple.MustParse("organization:someorg#member@user:tom"), }, require) @@ -1933,9 +1933,9 @@ func TestCheckHintsPartialApplicationOverArrow(t *testing.T) { require.NoError(datastoremw.SetInContext(ctx, ds)) resp, err := dispatcher.DispatchCheck(ctx, &v1.DispatchCheckRequest{ - ResourceRelation: RR("document", "view"), + ResourceRelation: RR("document", "view").ToCoreRR(), ResourceIds: []string{"somedoc", "anotherdoc", "thirddoc"}, - Subject: ONR("user", "tom", graph.Ellipsis), + Subject: tuple.CoreONR("user", "tom", graph.Ellipsis), ResultsSetting: v1.DispatchCheckRequest_REQUIRE_ALL_RESULTS, Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), @@ -1976,7 +1976,7 @@ func newLocalDispatcher(t testing.TB) (context.Context, dispatch.Dispatcher, dat return newLocalDispatcherWithConcurrencyLimit(t, 10) } -func newLocalDispatcherWithSchemaAndRels(t testing.TB, schema string, rels []*core.RelationTuple) (context.Context, dispatch.Dispatcher, datastore.Revision) { +func newLocalDispatcherWithSchemaAndRels(t testing.TB, schema string, rels []tuple.Relationship) (context.Context, dispatch.Dispatcher, datastore.Revision) { rawDS, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC) require.NoError(t, err) diff --git a/internal/dispatch/graph/dispatch_test.go b/internal/dispatch/graph/dispatch_test.go index 48ab615e63..091f30040f 100644 --- a/internal/dispatch/graph/dispatch_test.go +++ b/internal/dispatch/graph/dispatch_test.go @@ -7,7 +7,6 @@ import ( "github.com/authzed/spicedb/internal/dispatch" "github.com/authzed/spicedb/internal/graph" - core "github.com/authzed/spicedb/pkg/proto/core/v1" v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" "github.com/authzed/spicedb/pkg/tuple" @@ -26,11 +25,11 @@ func TestDispatchChunking(t *testing.T) { permission view = owner->self }` - resources := make([]*core.RelationTuple, 0, math.MaxUint16+1) - enabled := make([]*core.RelationTuple, 0, math.MaxUint16+1) + resources := make([]tuple.Relationship, 0, math.MaxUint16+1) + enabled := make([]tuple.Relationship, 0, math.MaxUint16+1) for i := 0; i < math.MaxUint16+1; i++ { - resources = append(resources, tuple.Parse(fmt.Sprintf("res:res1#owner@user:user%d", i))) - enabled = append(enabled, tuple.Parse(fmt.Sprintf("user:user%d#self@user:user%d", i, i))) + resources = append(resources, tuple.MustParse(fmt.Sprintf("res:res1#owner@user:user%d", i))) + enabled = append(enabled, tuple.MustParse(fmt.Sprintf("user:user%d#self@user:user%d", i, i))) } ctx, dispatcher, revision := newLocalDispatcherWithSchemaAndRels(t, schema, append(enabled, resources...)) @@ -39,10 +38,10 @@ func TestDispatchChunking(t *testing.T) { t.Parallel() for _, tpl := range resources[:1] { checkResult, err := dispatcher.DispatchCheck(ctx, &v1.DispatchCheckRequest{ - ResourceRelation: RR(tpl.ResourceAndRelation.Namespace, "view"), - ResourceIds: []string{tpl.ResourceAndRelation.ObjectId}, + ResourceRelation: RR(tpl.Resource.ObjectType, "view").ToCoreRR(), + ResourceIds: []string{tpl.Resource.ObjectID}, ResultsSetting: v1.DispatchCheckRequest_ALLOW_SINGLE_RESULT, - Subject: ONR(tpl.Subject.Namespace, tpl.Subject.ObjectId, graph.Ellipsis), + Subject: tuple.CoreONR(tpl.Subject.ObjectType, tpl.Subject.ObjectID, graph.Ellipsis), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -51,8 +50,8 @@ func TestDispatchChunking(t *testing.T) { require.NoError(t, err) require.NotNil(t, checkResult) - require.NotEmpty(t, checkResult.ResultsByResourceId, "expected membership for resource %s", tpl.ResourceAndRelation.ObjectId) - require.Equal(t, v1.ResourceCheckResult_MEMBER, checkResult.ResultsByResourceId[tpl.ResourceAndRelation.ObjectId].Membership) + require.NotEmpty(t, checkResult.ResultsByResourceId, "expected membership for resource %s", tpl.Resource.ObjectID) + require.Equal(t, v1.ResourceCheckResult_MEMBER, checkResult.ResultsByResourceId[tpl.Resource.ObjectID].Membership) } }) @@ -62,8 +61,8 @@ func TestDispatchChunking(t *testing.T) { for _, tpl := range resources[:1] { stream := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupResourcesResponse](ctx) err := dispatcher.DispatchLookupResources(&v1.DispatchLookupResourcesRequest{ - ObjectRelation: RR(tpl.ResourceAndRelation.Namespace, "view"), - Subject: ONR(tpl.Subject.Namespace, tpl.Subject.ObjectId, graph.Ellipsis), + ObjectRelation: RR(tpl.Resource.ObjectType, "view").ToCoreRR(), + Subject: tuple.CoreONR(tpl.Subject.ObjectType, tpl.Subject.ObjectID, graph.Ellipsis), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -85,9 +84,9 @@ func TestDispatchChunking(t *testing.T) { stream := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupSubjectsResponse](ctx) err := dispatcher.DispatchLookupSubjects(&v1.DispatchLookupSubjectsRequest{ - ResourceRelation: RR(tpl.ResourceAndRelation.Namespace, "view"), - ResourceIds: []string{tpl.ResourceAndRelation.ObjectId}, - SubjectRelation: RR(tpl.Subject.Namespace, graph.Ellipsis), + ResourceRelation: RR(tpl.Resource.ObjectType, "view").ToCoreRR(), + ResourceIds: []string{tpl.Resource.ObjectID}, + SubjectRelation: RR(tpl.Subject.ObjectType, graph.Ellipsis).ToCoreRR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, diff --git a/internal/dispatch/graph/expand_test.go b/internal/dispatch/graph/expand_test.go index 40b6aa552b..051199bacb 100644 --- a/internal/dispatch/graph/expand_test.go +++ b/internal/dispatch/graph/expand_test.go @@ -28,106 +28,108 @@ import ( func DS(objectType string, objectID string, objectRelation string) *core.DirectSubject { return &core.DirectSubject{ - Subject: ONR(objectType, objectID, objectRelation), + Subject: tuple.CoreONR(objectType, objectID, objectRelation), } } +var ONRRef = tuple.ONRRef + var ( - companyOwner = graph.Leaf(ONR("folder", "company", "owner"), + companyOwner = graph.Leaf(ONRRef("folder", "company", "owner"), (DS("user", "owner", expand.Ellipsis)), ) - companyEditor = graph.Leaf(ONR("folder", "company", "editor")) + companyEditor = graph.Leaf(ONRRef("folder", "company", "editor")) - companyEdit = graph.Union(ONR("folder", "company", "edit"), + companyEdit = graph.Union(ONRRef("folder", "company", "edit"), companyEditor, companyOwner, ) - companyViewer = graph.Leaf(ONR("folder", "company", "viewer"), + companyViewer = graph.Leaf(ONRRef("folder", "company", "viewer"), (DS("user", "legal", "...")), (DS("folder", "auditors", "viewer")), ) - companyView = graph.Union(ONR("folder", "company", "view"), + companyView = graph.Union(ONRRef("folder", "company", "view"), companyViewer, companyEdit, - graph.Union(ONR("folder", "company", "view")), + graph.Union(ONRRef("folder", "company", "view")), ) - auditorsOwner = graph.Leaf(ONR("folder", "auditors", "owner")) + auditorsOwner = graph.Leaf(ONRRef("folder", "auditors", "owner")) - auditorsEditor = graph.Leaf(ONR("folder", "auditors", "editor")) + auditorsEditor = graph.Leaf(ONRRef("folder", "auditors", "editor")) - auditorsEdit = graph.Union(ONR("folder", "auditors", "edit"), + auditorsEdit = graph.Union(ONRRef("folder", "auditors", "edit"), auditorsEditor, auditorsOwner, ) - auditorsViewer = graph.Leaf(ONR("folder", "auditors", "viewer"), + auditorsViewer = graph.Leaf(ONRRef("folder", "auditors", "viewer"), (DS("user", "auditor", "...")), ) - auditorsViewRecursive = graph.Union(ONR("folder", "auditors", "view"), + auditorsViewRecursive = graph.Union(ONRRef("folder", "auditors", "view"), auditorsViewer, auditorsEdit, - graph.Union(ONR("folder", "auditors", "view")), + graph.Union(ONRRef("folder", "auditors", "view")), ) - companyViewRecursive = graph.Union(ONR("folder", "company", "view"), - graph.Union(ONR("folder", "company", "viewer"), - graph.Leaf(ONR("folder", "auditors", "viewer"), + companyViewRecursive = graph.Union(ONRRef("folder", "company", "view"), + graph.Union(ONRRef("folder", "company", "viewer"), + graph.Leaf(ONRRef("folder", "auditors", "viewer"), (DS("user", "auditor", "..."))), - graph.Leaf(ONR("folder", "company", "viewer"), + graph.Leaf(ONRRef("folder", "company", "viewer"), (DS("user", "legal", "...")), (DS("folder", "auditors", "viewer")))), - graph.Union(ONR("folder", "company", "edit"), - graph.Leaf(ONR("folder", "company", "editor")), - graph.Leaf(ONR("folder", "company", "owner"), + graph.Union(ONRRef("folder", "company", "edit"), + graph.Leaf(ONRRef("folder", "company", "editor")), + graph.Leaf(ONRRef("folder", "company", "owner"), (DS("user", "owner", "...")))), - graph.Union(ONR("folder", "company", "view"))) + graph.Union(ONRRef("folder", "company", "view"))) - docOwner = graph.Leaf(ONR("document", "masterplan", "owner"), + docOwner = graph.Leaf(ONRRef("document", "masterplan", "owner"), (DS("user", "product_manager", "...")), ) - docEditor = graph.Leaf(ONR("document", "masterplan", "editor")) + docEditor = graph.Leaf(ONRRef("document", "masterplan", "editor")) - docEdit = graph.Union(ONR("document", "masterplan", "edit"), + docEdit = graph.Union(ONRRef("document", "masterplan", "edit"), docOwner, docEditor, ) - docViewer = graph.Leaf(ONR("document", "masterplan", "viewer"), + docViewer = graph.Leaf(ONRRef("document", "masterplan", "viewer"), (DS("user", "eng_lead", "...")), ) - docView = graph.Union(ONR("document", "masterplan", "view"), + docView = graph.Union(ONRRef("document", "masterplan", "view"), docViewer, docEdit, - graph.Union(ONR("document", "masterplan", "view"), - graph.Union(ONR("folder", "plans", "view"), - graph.Leaf(ONR("folder", "plans", "viewer"), + graph.Union(ONRRef("document", "masterplan", "view"), + graph.Union(ONRRef("folder", "plans", "view"), + graph.Leaf(ONRRef("folder", "plans", "viewer"), (DS("user", "chief_financial_officer", "...")), ), - graph.Union(ONR("folder", "plans", "edit"), - graph.Leaf(ONR("folder", "plans", "editor")), - graph.Leaf(ONR("folder", "plans", "owner"))), - graph.Union(ONR("folder", "plans", "view"))), - graph.Union(ONR("folder", "strategy", "view"), - graph.Leaf(ONR("folder", "strategy", "viewer")), - graph.Union(ONR("folder", "strategy", "edit"), - graph.Leaf(ONR("folder", "strategy", "editor")), - graph.Leaf(ONR("folder", "strategy", "owner"), + graph.Union(ONRRef("folder", "plans", "edit"), + graph.Leaf(ONRRef("folder", "plans", "editor")), + graph.Leaf(ONRRef("folder", "plans", "owner"))), + graph.Union(ONRRef("folder", "plans", "view"))), + graph.Union(ONRRef("folder", "strategy", "view"), + graph.Leaf(ONRRef("folder", "strategy", "viewer")), + graph.Union(ONRRef("folder", "strategy", "edit"), + graph.Leaf(ONRRef("folder", "strategy", "editor")), + graph.Leaf(ONRRef("folder", "strategy", "owner"), (DS("user", "vp_product", "...")))), - graph.Union(ONR("folder", "strategy", "view"), - graph.Union(ONR("folder", "company", "view"), - graph.Leaf(ONR("folder", "company", "viewer"), + graph.Union(ONRRef("folder", "strategy", "view"), + graph.Union(ONRRef("folder", "company", "view"), + graph.Leaf(ONRRef("folder", "company", "viewer"), (DS("user", "legal", "...")), (DS("folder", "auditors", "viewer"))), - graph.Union(ONR("folder", "company", "edit"), - graph.Leaf(ONR("folder", "company", "editor")), - graph.Leaf(ONR("folder", "company", "owner"), + graph.Union(ONRRef("folder", "company", "edit"), + graph.Leaf(ONRRef("folder", "company", "editor")), + graph.Leaf(ONRRef("folder", "company", "owner"), (DS("user", "owner", "...")))), - graph.Union(ONR("folder", "company", "view")), + graph.Union(ONRRef("folder", "company", "view")), ), ), ), @@ -139,7 +141,7 @@ func TestExpand(t *testing.T) { t.Parallel() testCases := []struct { - start *core.ObjectAndRelation + start tuple.ObjectAndRelation expansionMode v1.DispatchExpandRequest_ExpansionMode expected *core.RelationTupleTreeNode expectedDispatchCount int @@ -169,7 +171,7 @@ func TestExpand(t *testing.T) { ctx, dispatch, revision := newLocalDispatcher(t) expandResult, err := dispatch.DispatchExpand(ctx, &v1.DispatchExpandRequest{ - ResourceAndRelation: tc.start, + ResourceAndRelation: tc.start.ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -283,17 +285,17 @@ func TestMaxDepthExpand(t *testing.T) { ds, _ := testfixtures.StandardDatastoreWithSchema(rawDS, require) - tpl := tuple.Parse("folder:oops#parent@folder:oops") + tpl := tuple.MustParse("folder:oops#parent@folder:oops") ctx := datastoremw.ContextWithHandle(context.Background()) - revision, err := common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, tpl) + revision, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, tpl) require.NoError(err) require.NoError(datastoremw.SetInContext(ctx, ds)) dispatch := NewLocalOnlyDispatcher(10, 100) _, err = dispatch.DispatchExpand(ctx, &v1.DispatchExpandRequest{ - ResourceAndRelation: ONR("folder", "oops", "view"), + ResourceAndRelation: tuple.CoreONR("folder", "oops", "view"), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -310,9 +312,9 @@ func TestExpandOverSchema(t *testing.T) { testCases := []struct { name string schema string - relationships []*core.RelationTuple + relationships []tuple.Relationship - start *core.ObjectAndRelation + start tuple.ObjectAndRelation expansionMode v1.DispatchExpandRequest_ExpansionMode expectedTreeText string @@ -330,7 +332,7 @@ func TestExpandOverSchema(t *testing.T) { relation folder: folder permission view = folder->viewer }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:testdoc#folder@folder:testfolder1"), tuple.MustParse("document:testdoc#folder@folder:testfolder2"), tuple.MustParse("folder:testfolder1#viewer@user:tom"), @@ -338,7 +340,7 @@ func TestExpandOverSchema(t *testing.T) { tuple.MustParse("folder:testfolder2#viewer@user:sarah"), }, - tuple.ParseONR("document:testdoc#view"), + tuple.MustParseONR("document:testdoc#view"), v1.DispatchExpandRequest_SHALLOW, `intermediate_node: { @@ -412,7 +414,7 @@ func TestExpandOverSchema(t *testing.T) { relation folder: folder permission view = folder.all(viewer) }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:testdoc#folder@folder:testfolder1"), tuple.MustParse("document:testdoc#folder@folder:testfolder2"), tuple.MustParse("folder:testfolder1#viewer@user:tom"), @@ -421,7 +423,7 @@ func TestExpandOverSchema(t *testing.T) { tuple.MustParse("folder:testfolder2#viewer@user:sarah"), }, - tuple.ParseONR("document:testdoc#view"), + tuple.MustParseONR("document:testdoc#view"), v1.DispatchExpandRequest_SHALLOW, ` @@ -504,11 +506,11 @@ func TestExpandOverSchema(t *testing.T) { relation viewer: user with somecaveat | user } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:testdoc#viewer@user:sarah[somecaveat]"), tuple.MustParse("document:testdoc#viewer@user:mary"), }, - tuple.ParseONR("document:testdoc#viewer"), + tuple.MustParseONR("document:testdoc#viewer"), v1.DispatchExpandRequest_SHALLOW, ` leaf_node: { @@ -557,11 +559,11 @@ func TestExpandOverSchema(t *testing.T) { relation viewer: group#member with somecaveat } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:testdoc#viewer@group:test#member[somecaveat]"), tuple.MustParse("group:test#member@user:mary"), }, - tuple.ParseONR("document:testdoc#viewer"), + tuple.MustParseONR("document:testdoc#viewer"), v1.DispatchExpandRequest_SHALLOW, ` leaf_node: { @@ -607,12 +609,12 @@ func TestExpandOverSchema(t *testing.T) { relation viewer: group#member with somecaveat } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:testdoc#viewer@group:test#member[somecaveat]"), tuple.MustParse("group:test#member@user:mary"), tuple.MustParse("group:test#member@user:sarah[anothercaveat]"), }, - tuple.ParseONR("document:testdoc#viewer"), + tuple.MustParseONR("document:testdoc#viewer"), v1.DispatchExpandRequest_RECURSIVE, ` intermediate_node: { @@ -709,14 +711,14 @@ func TestExpandOverSchema(t *testing.T) { permission view = viewer + org->admin } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:testdoc#viewer@user:tom"), tuple.MustParse("document:testdoc#viewer@user:fred[anothercaveat]"), tuple.MustParse("document:testdoc#org@organization:someorg[somecaveat]"), tuple.MustParse("organization:someorg#admin@user:sarah"), tuple.MustParse("organization:someorg#admin@user:mary[orgcaveat]"), }, - tuple.ParseONR("document:testdoc#view"), + tuple.MustParseONR("document:testdoc#view"), v1.DispatchExpandRequest_SHALLOW, ` intermediate_node: { @@ -821,13 +823,13 @@ func TestExpandOverSchema(t *testing.T) { relation folder: folder permission view = folder->view }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("resource:someresource#folder@folder:first"), tuple.MustParse("folder:first#container@folder:second[somecaveat]"), tuple.MustParse("folder:first#member@user:notreachable"), tuple.MustParse("folder:second#member@user:tom"), }, - tuple.ParseONR("resource:someresource#view"), + tuple.MustParseONR("resource:someresource#view"), v1.DispatchExpandRequest_RECURSIVE, ` intermediate_node: { @@ -902,7 +904,7 @@ func TestExpandOverSchema(t *testing.T) { ctx, dispatch, revision := newLocalDispatcherWithSchemaAndRels(t, tc.schema, tc.relationships) expandResult, err := dispatch.DispatchExpand(ctx, &v1.DispatchExpandRequest{ - ResourceAndRelation: tc.start, + ResourceAndRelation: tc.start.ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, diff --git a/internal/dispatch/graph/graph.go b/internal/dispatch/graph/graph.go index 135146369b..9412e1837d 100644 --- a/internal/dispatch/graph/graph.go +++ b/internal/dispatch/graph/graph.go @@ -175,12 +175,12 @@ func (ld *localDispatcher) lookupRelation(_ context.Context, ns *core.NamespaceD // DispatchCheck implements dispatch.Check interface func (ld *localDispatcher) DispatchCheck(ctx context.Context, req *v1.DispatchCheckRequest) (*v1.DispatchCheckResponse, error) { - resourceType := tuple.StringRR(req.ResourceRelation) + resourceType := tuple.StringCoreRR(req.ResourceRelation) spanName := "DispatchCheck → " + resourceType + "@" + req.Subject.Namespace + "#" + req.Subject.Relation ctx, span := tracer.Start(ctx, spanName, trace.WithAttributes( attribute.String("resource-type", resourceType), attribute.StringSlice("resource-ids", req.ResourceIds), - attribute.String("subject", tuple.StringONR(req.Subject)), + attribute.String("subject", tuple.StringCoreONR(req.Subject)), )) defer span.End() @@ -262,7 +262,7 @@ func (ld *localDispatcher) DispatchCheck(ctx context.Context, req *v1.DispatchCh // DispatchExpand implements dispatch.Expand interface func (ld *localDispatcher) DispatchExpand(ctx context.Context, req *v1.DispatchExpandRequest) (*v1.DispatchExpandResponse, error) { ctx, span := tracer.Start(ctx, "DispatchExpand", trace.WithAttributes( - attribute.String("start", tuple.StringONR(req.ResourceAndRelation)), + attribute.String("start", tuple.StringCoreONR(req.ResourceAndRelation)), )) defer span.End() @@ -296,8 +296,8 @@ func (ld *localDispatcher) DispatchReachableResources( req *v1.DispatchReachableResourcesRequest, stream dispatch.ReachableResourcesStream, ) error { - resourceType := tuple.StringRR(req.ResourceRelation) - subjectRelation := tuple.StringRR(req.SubjectRelation) + resourceType := tuple.StringCoreRR(req.ResourceRelation) + subjectRelation := tuple.StringCoreRR(req.SubjectRelation) spanName := "DispatchReachableResources → " + resourceType + "@" + subjectRelation ctx, span := tracer.Start(stream.Context(), spanName, trace.WithAttributes( attribute.String("resource-type", resourceType), @@ -330,8 +330,8 @@ func (ld *localDispatcher) DispatchLookupResources( stream dispatch.LookupResourcesStream, ) error { ctx, span := tracer.Start(stream.Context(), "DispatchLookupResources", trace.WithAttributes( - attribute.String("resource-type", tuple.StringRR(req.ObjectRelation)), - attribute.String("subject", tuple.StringONR(req.Subject)), + attribute.String("resource-type", tuple.StringCoreRR(req.ObjectRelation)), + attribute.String("subject", tuple.StringCoreONR(req.Subject)), )) defer span.End() @@ -358,8 +358,8 @@ func (ld *localDispatcher) DispatchLookupResources2( stream dispatch.LookupResources2Stream, ) error { ctx, span := tracer.Start(stream.Context(), "DispatchLookupResources2", trace.WithAttributes( - attribute.String("resource-type", tuple.StringRR(req.ResourceRelation)), - attribute.String("subject", tuple.StringONR(req.TerminalSubject)), + attribute.String("resource-type", tuple.StringCoreRR(req.ResourceRelation)), + attribute.String("subject", tuple.StringCoreONR(req.TerminalSubject)), )) defer span.End() @@ -386,8 +386,8 @@ func (ld *localDispatcher) DispatchLookupSubjects( req *v1.DispatchLookupSubjectsRequest, stream dispatch.LookupSubjectsStream, ) error { - resourceType := tuple.StringRR(req.ResourceRelation) - subjectRelation := tuple.StringRR(req.SubjectRelation) + resourceType := tuple.StringCoreRR(req.ResourceRelation) + subjectRelation := tuple.StringCoreRR(req.SubjectRelation) spanName := "DispatchLookupSubjects → " + resourceType + "@" + subjectRelation ctx, span := tracer.Start(stream.Context(), spanName, trace.WithAttributes( diff --git a/internal/dispatch/graph/lookupresources2_test.go b/internal/dispatch/graph/lookupresources2_test.go index d55118fc5e..7d7a1851d9 100644 --- a/internal/dispatch/graph/lookupresources2_test.go +++ b/internal/dispatch/graph/lookupresources2_test.go @@ -20,7 +20,6 @@ import ( "github.com/authzed/spicedb/pkg/datastore" "github.com/authzed/spicedb/pkg/datastore/options" "github.com/authzed/spicedb/pkg/genutil/mapz" - core "github.com/authzed/spicedb/pkg/proto/core/v1" v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" "github.com/authzed/spicedb/pkg/tuple" ) @@ -28,8 +27,8 @@ import ( func TestSimpleLookupResources2(t *testing.T) { // FIXME marking this parallel makes goleak detect a leaked goroutine testCases := []struct { - start *core.RelationReference - target *core.ObjectAndRelation + start tuple.RelationReference + target tuple.ObjectAndRelation expectedResources []*v1.PossibleResource expectedDispatchCount uint32 expectedDepthRequired uint32 @@ -93,7 +92,7 @@ func TestSimpleLookupResources2(t *testing.T) { for _, tc := range testCases { name := fmt.Sprintf( "%s#%s->%s", - tc.start.Namespace, + tc.start.ObjectType, tc.start.Relation, tuple.StringONR(tc.target), ) @@ -108,10 +107,10 @@ func TestSimpleLookupResources2(t *testing.T) { stream := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupResources2Response](ctx) err := dispatcher.DispatchLookupResources2(&v1.DispatchLookupResources2Request{ - ResourceRelation: tc.start, - SubjectRelation: RR(tc.target.Namespace, tc.target.Relation), - SubjectIds: []string{tc.target.ObjectId}, - TerminalSubject: tc.target, + ResourceRelation: tc.start.ToCoreRR(), + SubjectRelation: RR(tc.target.ObjectType, tc.target.Relation).ToCoreRR(), + SubjectIds: []string{tc.target.ObjectID}, + TerminalSubject: tc.target.ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -134,10 +133,10 @@ func TestSimpleLookupResources2(t *testing.T) { // Run again with the cache available. stream = dispatch.NewCollectingDispatchStream[*v1.DispatchLookupResources2Response](ctx) err = dispatcher.DispatchLookupResources2(&v1.DispatchLookupResources2Request{ - ResourceRelation: tc.start, - SubjectRelation: RR(tc.target.Namespace, tc.target.Relation), - SubjectIds: []string{tc.target.ObjectId}, - TerminalSubject: tc.target, + ResourceRelation: tc.start.ToCoreRR(), + SubjectRelation: RR(tc.target.ObjectType, tc.target.Relation).ToCoreRR(), + SubjectIds: []string{tc.target.ObjectID}, + TerminalSubject: tc.target.ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -191,10 +190,10 @@ func TestSimpleLookupResourcesWithCursor2(t *testing.T) { stream := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupResources2Response](ctx) err := dispatcher.DispatchLookupResources2(&v1.DispatchLookupResources2Request{ - ResourceRelation: RR("document", "view"), - SubjectRelation: RR("user", "..."), + ResourceRelation: RR("document", "view").ToCoreRR(), + SubjectRelation: RR("user", "...").ToCoreRR(), SubjectIds: []string{tc.subject}, - TerminalSubject: ONR("user", tc.subject, "..."), + TerminalSubject: ONR("user", tc.subject, "...").ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -214,10 +213,10 @@ func TestSimpleLookupResourcesWithCursor2(t *testing.T) { stream = dispatch.NewCollectingDispatchStream[*v1.DispatchLookupResources2Response](ctx) err = dispatcher.DispatchLookupResources2(&v1.DispatchLookupResources2Request{ - ResourceRelation: RR("document", "view"), - SubjectRelation: RR("user", "..."), + ResourceRelation: RR("document", "view").ToCoreRR(), + SubjectRelation: RR("user", "...").ToCoreRR(), SubjectIds: []string{tc.subject}, - TerminalSubject: ONR("user", tc.subject, "..."), + TerminalSubject: ONR("user", tc.subject, "...").ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -251,10 +250,10 @@ func TestLookupResourcesCursorStability2(t *testing.T) { // Make the first first request. err := dispatcher.DispatchLookupResources2(&v1.DispatchLookupResources2Request{ - ResourceRelation: RR("document", "view"), - SubjectRelation: RR("user", "..."), + ResourceRelation: RR("document", "view").ToCoreRR(), + SubjectRelation: RR("user", "...").ToCoreRR(), SubjectIds: []string{"owner"}, - TerminalSubject: ONR("user", "owner", "..."), + TerminalSubject: ONR("user", "owner", "...").ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -271,10 +270,10 @@ func TestLookupResourcesCursorStability2(t *testing.T) { // Make the same request and ensure the cursor has not changed. stream = dispatch.NewCollectingDispatchStream[*v1.DispatchLookupResources2Response](ctx) err = dispatcher.DispatchLookupResources2(&v1.DispatchLookupResources2Request{ - ResourceRelation: RR("document", "view"), - SubjectRelation: RR("user", "..."), + ResourceRelation: RR("document", "view").ToCoreRR(), + SubjectRelation: RR("user", "...").ToCoreRR(), SubjectIds: []string{"owner"}, - TerminalSubject: ONR("user", "owner", "..."), + TerminalSubject: ONR("user", "owner", "...").ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -324,10 +323,10 @@ func TestMaxDepthLookup2(t *testing.T) { stream := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupResources2Response](ctx) err = dispatcher.DispatchLookupResources2(&v1.DispatchLookupResources2Request{ - ResourceRelation: RR("document", "view"), - SubjectRelation: RR("user", "..."), + ResourceRelation: RR("document", "view").ToCoreRR(), + SubjectRelation: RR("user", "...").ToCoreRR(), SubjectIds: []string{"legal"}, - TerminalSubject: ONR("user", "legal", "..."), + TerminalSubject: ONR("user", "legal", "...").ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 0, @@ -342,9 +341,9 @@ func TestLookupResources2OverSchemaWithCursors(t *testing.T) { testCases := []struct { name string schema string - relationships []*core.RelationTuple - permission *core.RelationReference - subject *core.ObjectAndRelation + relationships []tuple.Relationship + permission tuple.RelationReference + subject tuple.ObjectAndRelation optionalCaveatContext map[string]any expectedResourceIDs []string expectedMissingFields []string @@ -359,8 +358,8 @@ func TestLookupResources2OverSchemaWithCursors(t *testing.T) { permission view = viewer + editor }`, joinTuples( - genTuples("document", "viewer", "user", "tom", 1510), - genTuples("document", "editor", "user", "tom", 1510), + genRels("document", "viewer", "user", "tom", 1510), + genRels("document", "editor", "user", "tom", 1510), ), RR("document", "view"), ONR("user", "tom", "..."), @@ -377,7 +376,7 @@ func TestLookupResources2OverSchemaWithCursors(t *testing.T) { relation viewer: user permission view = viewer - banned }`, - genTuples("document", "viewer", "user", "tom", 1010), + genRels("document", "viewer", "user", "tom", 1010), RR("document", "view"), ONR("user", "tom", "..."), nil, @@ -394,8 +393,8 @@ func TestLookupResources2OverSchemaWithCursors(t *testing.T) { permission view = viewer & editor }`, joinTuples( - genTuples("document", "viewer", "user", "tom", 510), - genTuples("document", "editor", "user", "tom", 510), + genRels("document", "viewer", "user", "tom", 510), + genRels("document", "editor", "user", "tom", 510), ), RR("document", "view"), ONR("user", "tom", "..."), @@ -415,8 +414,8 @@ func TestLookupResources2OverSchemaWithCursors(t *testing.T) { permission view = can_view + editor }`, joinTuples( - genTuples("document", "viewer", "user", "tom", 1310), - genTuplesWithOffset("document", "editor", "user", "tom", 1250, 1200), + genRels("document", "viewer", "user", "tom", 1310), + genRelsWithOffset("document", "editor", "user", "tom", 1250, 1200), ), RR("document", "view"), ONR("user", "tom", "..."), @@ -436,7 +435,7 @@ func TestLookupResources2OverSchemaWithCursors(t *testing.T) { relation viewer: user with somecaveat permission view = viewer }`, - genTuplesWithCaveat("document", "viewer", "user", "tom", "somecaveat", map[string]any{"somecondition": 42}, 0, 2450), + genRelsWithCaveat("document", "viewer", "user", "tom", "somecaveat", map[string]any{"somecondition": 42}, 0, 2450), RR("document", "view"), ONR("user", "tom", "..."), nil, @@ -453,8 +452,8 @@ func TestLookupResources2OverSchemaWithCursors(t *testing.T) { permission view = viewer - banned }`, joinTuples( - genTuples("document", "viewer", "user", "tom", 1310), - genTuplesWithOffset("document", "banned", "user", "tom", 1210, 100), + genRels("document", "viewer", "user", "tom", 1310), + genRelsWithOffset("document", "banned", "user", "tom", 1210, 100), ), RR("document", "view"), ONR("user", "tom", "..."), @@ -474,7 +473,7 @@ func TestLookupResources2OverSchemaWithCursors(t *testing.T) { relation viewer: user with somecaveat permission view = viewer }`, - genTuplesWithCaveat("document", "viewer", "user", "tom", "somecaveat", map[string]any{}, 0, 2450), + genRelsWithCaveat("document", "viewer", "user", "tom", "somecaveat", map[string]any{}, 0, 2450), RR("document", "view"), ONR("user", "tom", "..."), nil, @@ -494,8 +493,8 @@ func TestLookupResources2OverSchemaWithCursors(t *testing.T) { permission view = folder->viewer }`, joinTuples( - genTuples("folder", "viewer", "user", "tom", 150), - genSubjectTuples("document", "folder", "folder", "...", 150), + genRels("folder", "viewer", "user", "tom", 150), + genSubjectRels("document", "folder", "folder", "...", 150), ), RR("document", "view"), ONR("user", "tom", "..."), @@ -513,8 +512,8 @@ func TestLookupResources2OverSchemaWithCursors(t *testing.T) { permission view = viewer + editor }`, joinTuples( - genTuples("document", "viewer", "user", "tom", 15100), - genTuples("document", "editor", "user", "tom", 15100), + genRels("document", "viewer", "user", "tom", 15100), + genRels("document", "editor", "user", "tom", 15100), ), RR("document", "view"), ONR("user", "tom", "..."), @@ -536,9 +535,9 @@ func TestLookupResources2OverSchemaWithCursors(t *testing.T) { permission view = org->member & viewer }`, joinTuples( - genTuples("document", "viewer", "user", "tom", 510), - genTuples("document", "org", "organization", "someorg", 510), - []*core.RelationTuple{ + genRels("document", "viewer", "user", "tom", 510), + genRels("document", "org", "organization", "someorg", 510), + []tuple.Relationship{ tuple.MustParse("organization:someorg#member@user:tom"), }, ), @@ -561,7 +560,7 @@ func TestLookupResources2OverSchemaWithCursors(t *testing.T) { relation viewer: user permission view = parent.all(viewer) + viewer }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:doc0#parent@folder:folder0"), tuple.MustParse("folder:folder0#viewer@user:tom"), @@ -602,9 +601,9 @@ func TestLookupResources2OverSchemaWithCursors(t *testing.T) { permission view = indirect - banned }`, joinTuples( - genTuples("document", "viewer", "user", "tom", 1510), - genTuples("document", "editor", "user", "tom", 1510), - genTuplesWithOffset("document", "banned", "user", "tom", 1410, 100), + genRels("document", "viewer", "user", "tom", 1510), + genRels("document", "editor", "user", "tom", 1510), + genRelsWithOffset("document", "banned", "user", "tom", 1410, 100), ), RR("document", "view"), ONR("user", "tom", "..."), @@ -629,12 +628,12 @@ func TestLookupResources2OverSchemaWithCursors(t *testing.T) { permission view = indirect & admin }`, joinTuples( - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("folder:folder0#viewer@user:tom"), }, - genTuples("document", "folder", "folder", "folder0", 1510), - genTuples("document", "editor", "user", "tom", 1510), - genTuples("document", "admin", "user", "tom", 1410), + genRels("document", "folder", "folder", "folder0", 1510), + genRels("document", "editor", "user", "tom", 1510), + genRels("document", "admin", "user", "tom", 1410), ), RR("document", "view"), ONR("user", "tom", "..."), @@ -659,12 +658,12 @@ func TestLookupResources2OverSchemaWithCursors(t *testing.T) { permission view = indirect & admin }`, joinTuples( - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("folder:folder0#viewer@user:tom"), }, - genTuples("document", "folder", "folder", "folder0", 1510), - genTuples("document", "editor", "user", "tom", 1510), - genTuples("document", "admin", "user", "tom", 1410), + genRels("document", "folder", "folder", "folder0", 1510), + genRels("document", "editor", "user", "tom", 1510), + genRels("document", "admin", "user", "tom", 1410), ), RR("document", "view"), ONR("user", "tom", "..."), @@ -696,13 +695,13 @@ func TestLookupResources2OverSchemaWithCursors(t *testing.T) { } `, joinTuples( - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("folder:folder0#viewer@user:tom"), tuple.MustParse("folder:folder1#viewer@user:tom"), }, - genTuples("middle", "folder", "folder", "folder0", 1510), - genTuples("middle", "editor", "user", "tom", 1), - genTuplesWithCaveatAndSubjectRelation("document", "viewer", "middle", "middle-0", "view", "", nil, 0, 2000), + genRels("middle", "folder", "folder", "folder0", 1510), + genRels("middle", "editor", "user", "tom", 1), + genRelsWithCaveatAndSubjectRelation("document", "viewer", "middle", "middle-0", "view", "", nil, 0, 2000), ), RR("document", "view"), ONR("user", "tom", "..."), @@ -729,11 +728,11 @@ func TestLookupResources2OverSchemaWithCursors(t *testing.T) { permission view = viewer & container->accesses }`, joinTuples( - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("container:somecontainer#access@user:tom[somecaveat]"), }, - genTuplesWithCaveat("document", "viewer", "user", "tom", "somecaveat", map[string]any{}, 0, 2450), - genTuples("document", "container", "container", "somecontainer", 2450), + genRelsWithCaveat("document", "viewer", "user", "tom", "somecaveat", map[string]any{}, 0, 2450), + genRels("document", "container", "container", "somecontainer", 2450), ), RR("document", "view"), ONR("user", "tom", "..."), @@ -779,10 +778,10 @@ func TestLookupResources2OverSchemaWithCursors(t *testing.T) { } err = dispatcher.DispatchLookupResources2(&v1.DispatchLookupResources2Request{ - ResourceRelation: tc.permission, - SubjectRelation: RR(tc.subject.Namespace, "..."), - SubjectIds: []string{tc.subject.ObjectId}, - TerminalSubject: tc.subject, + ResourceRelation: tc.permission.ToCoreRR(), + SubjectRelation: RR(tc.subject.ObjectType, "...").ToCoreRR(), + SubjectIds: []string{tc.subject.ObjectID}, + TerminalSubject: tc.subject.ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -847,10 +846,10 @@ func TestLookupResources2ImmediateTimeout(t *testing.T) { stream := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupResources2Response](cctx) err = dispatcher.DispatchLookupResources2(&v1.DispatchLookupResources2Request{ - ResourceRelation: RR("document", "view"), - SubjectRelation: RR("user", "..."), + ResourceRelation: RR("document", "view").ToCoreRR(), + SubjectRelation: RR("user", "...").ToCoreRR(), SubjectIds: []string{"legal"}, - TerminalSubject: ONR("user", "legal", "..."), + TerminalSubject: ONR("user", "legal", "...").ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 10, @@ -882,10 +881,10 @@ func TestLookupResources2WithError(t *testing.T) { stream := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupResources2Response](cctx) err = dispatcher.DispatchLookupResources2(&v1.DispatchLookupResources2Request{ - ResourceRelation: RR("document", "view"), - SubjectRelation: RR("user", "..."), + ResourceRelation: RR("document", "view").ToCoreRR(), + SubjectRelation: RR("user", "...").ToCoreRR(), SubjectIds: []string{"legal"}, - TerminalSubject: ONR("user", "legal", "..."), + TerminalSubject: ONR("user", "legal", "...").ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 10, @@ -902,12 +901,12 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { tcs := []struct { name string schema string - relationships []*core.RelationTuple + relationships []tuple.Relationship - resourceRelation *core.RelationReference - subject *core.ObjectAndRelation + resourceRelation tuple.RelationReference + subject tuple.ObjectAndRelation - disallowedQueries []*core.RelationReference + disallowedQueries []tuple.RelationReference expectedResources []string expectedError string }{ @@ -920,7 +919,7 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { relation viewer: user permission view = viewer & editor }`, - relationships: []*core.RelationTuple{ + relationships: []tuple.Relationship{ tuple.MustParse("document:masterplan#viewer@user:tom"), tuple.MustParse("document:masterplan#editor@user:tom"), tuple.MustParse("document:anotherplan#viewer@user:tom"), @@ -928,7 +927,7 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { }, resourceRelation: RR("document", "view"), subject: ONR("user", "tom", "..."), - disallowedQueries: []*core.RelationReference{ + disallowedQueries: []tuple.RelationReference{ RR("document", "viewer"), }, expectedResources: []string{"masterplan", "anotherplan"}, @@ -946,14 +945,14 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { relation editor: user permission view = org->member & editor }`, - relationships: []*core.RelationTuple{ + relationships: []tuple.Relationship{ tuple.MustParse("document:masterplan#org@organization:someorg"), tuple.MustParse("organization:someorg#member@user:tom"), tuple.MustParse("document:masterplan#editor@user:tom"), }, resourceRelation: RR("document", "view"), subject: ONR("user", "tom", "..."), - disallowedQueries: []*core.RelationReference{ + disallowedQueries: []tuple.RelationReference{ RR("organization", "member"), }, expectedResources: []string{"masterplan"}, @@ -967,13 +966,13 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { relation viewer: user permission view = viewer & editor }`, - relationships: []*core.RelationTuple{ + relationships: []tuple.Relationship{ tuple.MustParse("document:masterplan#viewer@user:tom"), tuple.MustParse("document:masterplan#editor@user:tom"), }, resourceRelation: RR("document", "view"), subject: ONR("user", "tom", "..."), - disallowedQueries: []*core.RelationReference{ + disallowedQueries: []tuple.RelationReference{ RR("document", "editor"), }, expectedError: "disallowed query: document#editor", @@ -989,13 +988,13 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { permission indirect_editor = editor permission view = indirect_viewer & indirect_editor }`, - relationships: []*core.RelationTuple{ + relationships: []tuple.Relationship{ tuple.MustParse("document:masterplan#viewer@user:tom"), tuple.MustParse("document:masterplan#editor@user:tom"), }, resourceRelation: RR("document", "view"), subject: ONR("user", "tom", "..."), - disallowedQueries: []*core.RelationReference{ + disallowedQueries: []tuple.RelationReference{ RR("document", "viewer"), }, expectedResources: []string{"masterplan"}, @@ -1010,13 +1009,13 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { permission indirect_view = viewer & editor permission view = indirect_view }`, - relationships: []*core.RelationTuple{ + relationships: []tuple.Relationship{ tuple.MustParse("document:masterplan#viewer@user:tom"), tuple.MustParse("document:masterplan#editor@user:tom"), }, resourceRelation: RR("document", "view"), subject: ONR("user", "tom", "..."), - disallowedQueries: []*core.RelationReference{ + disallowedQueries: []tuple.RelationReference{ RR("document", "viewer"), }, expectedResources: []string{"masterplan"}, @@ -1032,13 +1031,13 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { permission indirect_editor = editor permission view = indirect_viewer & indirect_editor }`, - relationships: []*core.RelationTuple{ + relationships: []tuple.Relationship{ tuple.MustParse("document:masterplan#viewer@user:tom"), tuple.MustParse("document:masterplan#editor@user:tom"), }, resourceRelation: RR("document", "view"), subject: ONR("user", "tom", "..."), - disallowedQueries: []*core.RelationReference{ + disallowedQueries: []tuple.RelationReference{ RR("document", "viewer"), }, expectedResources: []string{"masterplan"}, @@ -1052,13 +1051,13 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { relation viewer: user | user:* permission view = viewer & editor }`, - relationships: []*core.RelationTuple{ + relationships: []tuple.Relationship{ tuple.MustParse("document:masterplan#viewer@user:*"), tuple.MustParse("document:masterplan#editor@user:tom"), }, resourceRelation: RR("document", "view"), subject: ONR("user", "tom", "..."), - disallowedQueries: []*core.RelationReference{ + disallowedQueries: []tuple.RelationReference{ RR("document", "viewer"), }, expectedResources: []string{"masterplan"}, @@ -1076,7 +1075,7 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { permission view = viewer_of_some_kind & editor permission view_and_admin = view & admin }`, - relationships: []*core.RelationTuple{ + relationships: []tuple.Relationship{ tuple.MustParse("document:masterplan#viewer@user:tom"), tuple.MustParse("document:masterplan#viewer2@user:tom"), tuple.MustParse("document:masterplan#editor@user:tom"), @@ -1084,7 +1083,7 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { }, resourceRelation: RR("document", "view_and_admin"), subject: ONR("user", "tom", "..."), - disallowedQueries: []*core.RelationReference{ + disallowedQueries: []tuple.RelationReference{ RR("document", "viewer"), RR("document", "viewer2"), }, @@ -1102,7 +1101,7 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { permission viewer_of_some_kind = viewer + viewer2 permission view_and_admin = viewer_of_some_kind & editor & admin }`, - relationships: []*core.RelationTuple{ + relationships: []tuple.Relationship{ tuple.MustParse("document:masterplan#viewer@user:tom"), tuple.MustParse("document:masterplan#viewer2@user:tom"), tuple.MustParse("document:masterplan#editor@user:tom"), @@ -1110,7 +1109,7 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { }, resourceRelation: RR("document", "view_and_admin"), subject: ONR("user", "tom", "..."), - disallowedQueries: []*core.RelationReference{ + disallowedQueries: []tuple.RelationReference{ RR("document", "viewer"), RR("document", "viewer2"), }, @@ -1129,7 +1128,7 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { relation viewer: group#member permission view = viewer & editor }`, - relationships: []*core.RelationTuple{ + relationships: []tuple.Relationship{ tuple.MustParse("document:masterplan#viewer@group:first#member"), tuple.MustParse("document:masterplan#viewer@group:second#member"), tuple.MustParse("document:masterplan#editor@user:tom"), @@ -1138,7 +1137,7 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { }, resourceRelation: RR("document", "view"), subject: ONR("user", "tom", "..."), - disallowedQueries: []*core.RelationReference{ + disallowedQueries: []tuple.RelationReference{ RR("document", "viewer"), }, expectedResources: []string{"masterplan"}, @@ -1156,7 +1155,7 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { relation viewer: group#member permission view = editor & viewer }`, - relationships: []*core.RelationTuple{ + relationships: []tuple.Relationship{ tuple.MustParse("document:masterplan#viewer@group:first#member"), tuple.MustParse("document:masterplan#viewer@group:second#member"), tuple.MustParse("document:masterplan#editor@user:tom"), @@ -1165,7 +1164,7 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { }, resourceRelation: RR("document", "view"), subject: ONR("user", "tom", "..."), - disallowedQueries: []*core.RelationReference{ + disallowedQueries: []tuple.RelationReference{ RR("document", "editor"), }, expectedResources: []string{"masterplan"}, @@ -1183,7 +1182,7 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { relation viewer: group permission view = viewer->member & editor }`, - relationships: []*core.RelationTuple{ + relationships: []tuple.Relationship{ tuple.MustParse("document:masterplan#viewer@group:first"), tuple.MustParse("document:masterplan#viewer@group:second"), tuple.MustParse("document:masterplan#editor@user:tom"), @@ -1192,7 +1191,7 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { }, resourceRelation: RR("document", "view"), subject: ONR("user", "tom", "..."), - disallowedQueries: []*core.RelationReference{ + disallowedQueries: []tuple.RelationReference{ RR("document", "viewer"), }, expectedResources: []string{"masterplan"}, @@ -1210,7 +1209,7 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { relation viewer: group permission view = editor & viewer->member }`, - relationships: []*core.RelationTuple{ + relationships: []tuple.Relationship{ tuple.MustParse("document:masterplan#viewer@group:first"), tuple.MustParse("document:masterplan#viewer@group:second"), tuple.MustParse("document:masterplan#editor@user:tom"), @@ -1219,7 +1218,7 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { }, resourceRelation: RR("document", "view"), subject: ONR("user", "tom", "..."), - disallowedQueries: []*core.RelationReference{ + disallowedQueries: []tuple.RelationReference{ RR("document", "editor"), }, expectedResources: []string{"masterplan"}, @@ -1243,7 +1242,7 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { permission view = folder->view } `, - relationships: []*core.RelationTuple{ + relationships: []tuple.Relationship{ tuple.MustParse("group:first#member@user:tom"), tuple.MustParse("group:second#member@user:tom"), tuple.MustParse("folder:folder1#group@group:first"), @@ -1253,7 +1252,7 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { }, resourceRelation: RR("document", "view"), subject: ONR("user", "tom", "..."), - disallowedQueries: []*core.RelationReference{ + disallowedQueries: []tuple.RelationReference{ RR("group", "member"), RR("folder", "group"), }, @@ -1282,7 +1281,7 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { permission view = folder->view } `, - relationships: []*core.RelationTuple{ + relationships: []tuple.Relationship{ tuple.MustParse("group:first#member@user:tom"), tuple.MustParse("group:second#member@user:tom"), tuple.MustParse("folder:folder1#group@group:first"), @@ -1292,7 +1291,7 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { }, resourceRelation: RR("document", "view"), subject: ONR("user", "tom", "..."), - disallowedQueries: []*core.RelationReference{ + disallowedQueries: []tuple.RelationReference{ RR("group", "member"), RR("folder", "group"), }, @@ -1321,7 +1320,7 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { permission view = folder->view } `, - relationships: []*core.RelationTuple{ + relationships: []tuple.Relationship{ tuple.MustParse("group:first#member@user:tom[somecaveat]"), tuple.MustParse("folder:folder1#group@group:first"), tuple.MustParse("folder:folder1#editor@user:tom"), @@ -1329,7 +1328,7 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { }, resourceRelation: RR("document", "view"), subject: ONR("user", "tom", "..."), - disallowedQueries: []*core.RelationReference{ + disallowedQueries: []tuple.RelationReference{ RR("group", "member"), RR("folder", "group"), }, @@ -1360,10 +1359,10 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { stream := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupResources2Response](cctx) err = dispatcher.DispatchLookupResources2(&v1.DispatchLookupResources2Request{ - ResourceRelation: tc.resourceRelation, - SubjectRelation: RR(tc.subject.Namespace, tc.subject.Relation), - SubjectIds: []string{tc.subject.ObjectId}, - TerminalSubject: tc.subject, + ResourceRelation: tc.resourceRelation.ToCoreRR(), + SubjectRelation: RR(tc.subject.ObjectType, tc.subject.Relation).ToCoreRR(), + SubjectIds: []string{tc.subject.ObjectID}, + TerminalSubject: tc.subject.ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -1397,7 +1396,7 @@ func TestLookupResources2EnsureCheckHints(t *testing.T) { type disallowedWrapper struct { datastore.Datastore - disallowedQueries []*core.RelationReference + disallowedQueries []tuple.RelationReference } func (dw disallowedWrapper) SnapshotReader(rev datastore.Revision) datastore.Reader { @@ -1406,7 +1405,7 @@ func (dw disallowedWrapper) SnapshotReader(rev datastore.Revision) datastore.Rea type disallowedReader struct { datastore.Reader - disallowedQueries []*core.RelationReference + disallowedQueries []tuple.RelationReference } func (dr disallowedReader) QueryRelationships( @@ -1415,7 +1414,7 @@ func (dr disallowedReader) QueryRelationships( options ...options.QueryOptionsOption, ) (datastore.RelationshipIterator, error) { for _, disallowedQuery := range dr.disallowedQueries { - if disallowedQuery.Namespace == filter.OptionalResourceType && disallowedQuery.Relation == filter.OptionalResourceRelation { + if disallowedQuery.ObjectType == filter.OptionalResourceType && disallowedQuery.Relation == filter.OptionalResourceRelation { return nil, fmt.Errorf("disallowed query: %s", tuple.StringRR(disallowedQuery)) } } diff --git a/internal/dispatch/graph/lookupresources_test.go b/internal/dispatch/graph/lookupresources_test.go index d8ecdca6b0..ec0f731c23 100644 --- a/internal/dispatch/graph/lookupresources_test.go +++ b/internal/dispatch/graph/lookupresources_test.go @@ -15,19 +15,13 @@ import ( datastoremw "github.com/authzed/spicedb/internal/middleware/datastore" "github.com/authzed/spicedb/internal/testfixtures" "github.com/authzed/spicedb/pkg/genutil/mapz" - core "github.com/authzed/spicedb/pkg/proto/core/v1" v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" "github.com/authzed/spicedb/pkg/tuple" ) const veryLargeLimit = 1000000000 -func RR(namespaceName string, relationName string) *core.RelationReference { - return &core.RelationReference{ - Namespace: namespaceName, - Relation: relationName, - } -} +var RR = tuple.RR func resolvedRes(resourceID string) *v1.ResolvedResource { return &v1.ResolvedResource{ @@ -40,8 +34,8 @@ func TestSimpleLookupResources(t *testing.T) { t.Parallel() testCases := []struct { - start *core.RelationReference - target *core.ObjectAndRelation + start tuple.RelationReference + target tuple.ObjectAndRelation expectedResources []*v1.ResolvedResource expectedDispatchCount uint32 expectedDepthRequired uint32 @@ -105,7 +99,7 @@ func TestSimpleLookupResources(t *testing.T) { for _, tc := range testCases { name := fmt.Sprintf( "%s#%s->%s", - tc.start.Namespace, + tc.start.ObjectType, tc.start.Relation, tuple.StringONR(tc.target), ) @@ -118,8 +112,8 @@ func TestSimpleLookupResources(t *testing.T) { stream := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupResourcesResponse](ctx) err := dispatcher.DispatchLookupResources(&v1.DispatchLookupResourcesRequest{ - ObjectRelation: tc.start, - Subject: tc.target, + ObjectRelation: tc.start.ToCoreRR(), + Subject: tc.target.ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -142,8 +136,8 @@ func TestSimpleLookupResources(t *testing.T) { // Run again with the cache available. stream = dispatch.NewCollectingDispatchStream[*v1.DispatchLookupResourcesResponse](ctx) err = dispatcher.DispatchLookupResources(&v1.DispatchLookupResourcesRequest{ - ObjectRelation: tc.start, - Subject: tc.target, + ObjectRelation: tc.start.ToCoreRR(), + Subject: tc.target.ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -197,8 +191,8 @@ func TestSimpleLookupResourcesWithCursor(t *testing.T) { stream := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupResourcesResponse](ctx) err := dispatcher.DispatchLookupResources(&v1.DispatchLookupResourcesRequest{ - ObjectRelation: RR("document", "view"), - Subject: ONR("user", tc.subject, "..."), + ObjectRelation: RR("document", "view").ToCoreRR(), + Subject: ONR("user", tc.subject, "...").ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -218,8 +212,8 @@ func TestSimpleLookupResourcesWithCursor(t *testing.T) { stream = dispatch.NewCollectingDispatchStream[*v1.DispatchLookupResourcesResponse](ctx) err = dispatcher.DispatchLookupResources(&v1.DispatchLookupResourcesRequest{ - ObjectRelation: RR("document", "view"), - Subject: ONR("user", tc.subject, "..."), + ObjectRelation: RR("document", "view").ToCoreRR(), + Subject: ONR("user", tc.subject, "...").ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -253,8 +247,8 @@ func TestLookupResourcesCursorStability(t *testing.T) { // Make the first first request. err := dispatcher.DispatchLookupResources(&v1.DispatchLookupResourcesRequest{ - ObjectRelation: RR("document", "view"), - Subject: ONR("user", "owner", "..."), + ObjectRelation: RR("document", "view").ToCoreRR(), + Subject: ONR("user", "owner", "...").ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -271,8 +265,8 @@ func TestLookupResourcesCursorStability(t *testing.T) { // Make the same request and ensure the cursor has not changed. stream = dispatch.NewCollectingDispatchStream[*v1.DispatchLookupResourcesResponse](ctx) err = dispatcher.DispatchLookupResources(&v1.DispatchLookupResourcesRequest{ - ObjectRelation: RR("document", "view"), - Subject: ONR("user", "owner", "..."), + ObjectRelation: RR("document", "view").ToCoreRR(), + Subject: ONR("user", "owner", "...").ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -321,8 +315,8 @@ func TestMaxDepthLookup(t *testing.T) { stream := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupResourcesResponse](ctx) err = dispatcher.DispatchLookupResources(&v1.DispatchLookupResourcesRequest{ - ObjectRelation: RR("document", "view"), - Subject: ONR("user", "legal", "..."), + ObjectRelation: RR("document", "view").ToCoreRR(), + Subject: ONR("user", "legal", "...").ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 0, @@ -332,7 +326,7 @@ func TestMaxDepthLookup(t *testing.T) { require.Error(err) } -func joinTuples(first []*core.RelationTuple, others ...[]*core.RelationTuple) []*core.RelationTuple { +func joinTuples(first []tuple.Relationship, others ...[]tuple.Relationship) []tuple.Relationship { current := first for _, second := range others { current = append(current, second...) @@ -340,43 +334,49 @@ func joinTuples(first []*core.RelationTuple, others ...[]*core.RelationTuple) [] return current } -func genTuplesWithOffset(resourceName string, relation string, subjectName string, subjectID string, offset int, number int) []*core.RelationTuple { - return genTuplesWithCaveat(resourceName, relation, subjectName, subjectID, "", nil, offset, number) +func genRelsWithOffset(resourceName string, relation string, subjectName string, subjectID string, offset int, number int) []tuple.Relationship { + return genRelsWithCaveat(resourceName, relation, subjectName, subjectID, "", nil, offset, number) } -func genTuples(resourceName string, relation string, subjectName string, subjectID string, number int) []*core.RelationTuple { - return genTuplesWithOffset(resourceName, relation, subjectName, subjectID, 0, number) +func genRels(resourceName string, relation string, subjectName string, subjectID string, number int) []tuple.Relationship { + return genRelsWithOffset(resourceName, relation, subjectName, subjectID, 0, number) } -func genSubjectTuples(resourceName string, relation string, subjectName string, subjectRelation string, number int) []*core.RelationTuple { - tuples := make([]*core.RelationTuple, 0, number) +func genSubjectRels(resourceName string, relation string, subjectName string, subjectRelation string, number int) []tuple.Relationship { + rels := make([]tuple.Relationship, 0, number) for i := 0; i < number; i++ { - tpl := &core.RelationTuple{ - ResourceAndRelation: ONR(resourceName, fmt.Sprintf("%s-%d", resourceName, i), relation), - Subject: ONR(subjectName, fmt.Sprintf("%s-%d", subjectName, i), subjectRelation), + rel := tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: ONR(resourceName, fmt.Sprintf("%s-%d", resourceName, i), relation), + Subject: ONR(subjectName, fmt.Sprintf("%s-%d", subjectName, i), subjectRelation), + }, } - tuples = append(tuples, tpl) + rels = append(rels, rel) } - return tuples + + return rels } -func genTuplesWithCaveat(resourceName string, relation string, subjectName string, subjectID string, caveatName string, context map[string]any, offset int, number int) []*core.RelationTuple { - return genTuplesWithCaveatAndSubjectRelation(resourceName, relation, subjectName, subjectID, "...", caveatName, context, offset, number) +func genRelsWithCaveat(resourceName string, relation string, subjectName string, subjectID string, caveatName string, context map[string]any, offset int, number int) []tuple.Relationship { + return genRelsWithCaveatAndSubjectRelation(resourceName, relation, subjectName, subjectID, "...", caveatName, context, offset, number) } -func genTuplesWithCaveatAndSubjectRelation(resourceName string, relation string, subjectName string, subjectID string, subjectRelation string, caveatName string, context map[string]any, offset int, number int) []*core.RelationTuple { - tuples := make([]*core.RelationTuple, 0, number) +func genRelsWithCaveatAndSubjectRelation(resourceName string, relation string, subjectName string, subjectID string, subjectRelation string, caveatName string, context map[string]any, offset int, number int) []tuple.Relationship { + rels := make([]tuple.Relationship, 0, number) for i := 0; i < number; i++ { - tpl := &core.RelationTuple{ - ResourceAndRelation: ONR(resourceName, fmt.Sprintf("%s-%d", resourceName, i+offset), relation), - Subject: ONR(subjectName, subjectID, subjectRelation), + rel := tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: ONR(resourceName, fmt.Sprintf("%s-%d", resourceName, i+offset), relation), + Subject: ONR(subjectName, subjectID, subjectRelation), + }, } + if caveatName != "" { - tpl = tuple.MustWithCaveat(tpl, caveatName, context) + rel = tuple.MustWithCaveat(rel, caveatName, context) } - tuples = append(tuples, tpl) + rels = append(rels, rel) } - return tuples + return rels } func genResourceIds(resourceName string, number int) []string { @@ -392,9 +392,9 @@ func TestLookupResourcesOverSchemaWithCursors(t *testing.T) { testCases := []struct { name string schema string - relationships []*core.RelationTuple - permission *core.RelationReference - subject *core.ObjectAndRelation + relationships []tuple.Relationship + permission tuple.RelationReference + subject tuple.ObjectAndRelation expectedResourceIDs []string }{ { @@ -407,8 +407,8 @@ func TestLookupResourcesOverSchemaWithCursors(t *testing.T) { permission view = viewer + editor }`, joinTuples( - genTuples("document", "viewer", "user", "tom", 1510), - genTuples("document", "editor", "user", "tom", 1510), + genRels("document", "viewer", "user", "tom", 1510), + genRels("document", "editor", "user", "tom", 1510), ), RR("document", "view"), ONR("user", "tom", "..."), @@ -423,7 +423,7 @@ func TestLookupResourcesOverSchemaWithCursors(t *testing.T) { relation viewer: user permission view = viewer - banned }`, - genTuples("document", "viewer", "user", "tom", 1010), + genRels("document", "viewer", "user", "tom", 1010), RR("document", "view"), ONR("user", "tom", "..."), genResourceIds("document", 1010), @@ -438,8 +438,8 @@ func TestLookupResourcesOverSchemaWithCursors(t *testing.T) { permission view = viewer & editor }`, joinTuples( - genTuples("document", "viewer", "user", "tom", 510), - genTuples("document", "editor", "user", "tom", 510), + genRels("document", "viewer", "user", "tom", 510), + genRels("document", "editor", "user", "tom", 510), ), RR("document", "view"), ONR("user", "tom", "..."), @@ -457,8 +457,8 @@ func TestLookupResourcesOverSchemaWithCursors(t *testing.T) { permission view = can_view + editor }`, joinTuples( - genTuples("document", "viewer", "user", "tom", 1310), - genTuplesWithOffset("document", "editor", "user", "tom", 1250, 1200), + genRels("document", "viewer", "user", "tom", 1310), + genRelsWithOffset("document", "editor", "user", "tom", 1250, 1200), ), RR("document", "view"), ONR("user", "tom", "..."), @@ -476,7 +476,7 @@ func TestLookupResourcesOverSchemaWithCursors(t *testing.T) { relation viewer: user with somecaveat permission view = viewer }`, - genTuplesWithCaveat("document", "viewer", "user", "tom", "somecaveat", map[string]any{"somecondition": 42}, 0, 2450), + genRelsWithCaveat("document", "viewer", "user", "tom", "somecaveat", map[string]any{"somecondition": 42}, 0, 2450), RR("document", "view"), ONR("user", "tom", "..."), genResourceIds("document", 2450), @@ -491,8 +491,8 @@ func TestLookupResourcesOverSchemaWithCursors(t *testing.T) { permission view = viewer - banned }`, joinTuples( - genTuples("document", "viewer", "user", "tom", 1310), - genTuplesWithOffset("document", "banned", "user", "tom", 1210, 100), + genRels("document", "viewer", "user", "tom", 1310), + genRelsWithOffset("document", "banned", "user", "tom", 1210, 100), ), RR("document", "view"), ONR("user", "tom", "..."), @@ -510,7 +510,7 @@ func TestLookupResourcesOverSchemaWithCursors(t *testing.T) { relation viewer: user with somecaveat permission view = viewer }`, - genTuplesWithCaveat("document", "viewer", "user", "tom", "somecaveat", map[string]any{}, 0, 2450), + genRelsWithCaveat("document", "viewer", "user", "tom", "somecaveat", map[string]any{}, 0, 2450), RR("document", "view"), ONR("user", "tom", "..."), genResourceIds("document", 2450), @@ -528,8 +528,8 @@ func TestLookupResourcesOverSchemaWithCursors(t *testing.T) { permission view = folder->viewer }`, joinTuples( - genTuples("folder", "viewer", "user", "tom", 150), - genSubjectTuples("document", "folder", "folder", "...", 150), + genRels("folder", "viewer", "user", "tom", 150), + genSubjectRels("document", "folder", "folder", "...", 150), ), RR("document", "view"), ONR("user", "tom", "..."), @@ -545,8 +545,8 @@ func TestLookupResourcesOverSchemaWithCursors(t *testing.T) { permission view = viewer + editor }`, joinTuples( - genTuples("document", "viewer", "user", "tom", 15100), - genTuples("document", "editor", "user", "tom", 15100), + genRels("document", "viewer", "user", "tom", 15100), + genRels("document", "editor", "user", "tom", 15100), ), RR("document", "view"), ONR("user", "tom", "..."), @@ -565,7 +565,7 @@ func TestLookupResourcesOverSchemaWithCursors(t *testing.T) { relation viewer: user permission view = parent.all(viewer) + viewer }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:doc0#parent@folder:folder0"), tuple.MustParse("folder:folder0#viewer@user:tom"), @@ -622,8 +622,8 @@ func TestLookupResourcesOverSchemaWithCursors(t *testing.T) { require.NoError(err) err = dispatcher.DispatchLookupResources(&v1.DispatchLookupResourcesRequest{ - ObjectRelation: tc.permission, - Subject: tc.subject, + ObjectRelation: tc.permission.ToCoreRR(), + Subject: tc.subject.ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -680,8 +680,8 @@ func TestLookupResourcesImmediateTimeout(t *testing.T) { stream := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupResourcesResponse](cctx) err = dispatcher.DispatchLookupResources(&v1.DispatchLookupResourcesRequest{ - ObjectRelation: RR("document", "view"), - Subject: ONR("user", "legal", "..."), + ObjectRelation: RR("document", "view").ToCoreRR(), + Subject: ONR("user", "legal", "...").ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 10, @@ -713,8 +713,8 @@ func TestLookupResourcesWithError(t *testing.T) { stream := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupResourcesResponse](cctx) err = dispatcher.DispatchLookupResources(&v1.DispatchLookupResourcesRequest{ - ObjectRelation: RR("document", "view"), - Subject: ONR("user", "legal", "..."), + ObjectRelation: RR("document", "view").ToCoreRR(), + Subject: ONR("user", "legal", "...").ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 1, // Set depth 1 to cause an error within reachable resources diff --git a/internal/dispatch/graph/lookupsubjects_test.go b/internal/dispatch/graph/lookupsubjects_test.go index 98abf655db..d441749a12 100644 --- a/internal/dispatch/graph/lookupsubjects_test.go +++ b/internal/dispatch/graph/lookupsubjects_test.go @@ -16,7 +16,6 @@ import ( datastoremw "github.com/authzed/spicedb/internal/middleware/datastore" "github.com/authzed/spicedb/internal/testfixtures" itestutil "github.com/authzed/spicedb/internal/testutil" - corev1 "github.com/authzed/spicedb/pkg/proto/core/v1" v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" "github.com/authzed/spicedb/pkg/tuple" ) @@ -141,9 +140,9 @@ func TestSimpleLookupSubjects(t *testing.T) { stream := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupSubjectsResponse](ctx) err := dis.DispatchLookupSubjects(&v1.DispatchLookupSubjectsRequest{ - ResourceRelation: RR(tc.resourceType, tc.permission), + ResourceRelation: RR(tc.resourceType, tc.permission).ToCoreRR(), ResourceIds: []string{tc.resourceID}, - SubjectRelation: RR(tc.subjectType, tc.subjectRelation), + SubjectRelation: RR(tc.subjectType, tc.subjectRelation).ToCoreRR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -173,10 +172,10 @@ func TestSimpleLookupSubjects(t *testing.T) { // Ensure every subject found has access. for _, subjectID := range foundSubjectIds { checkResult, err := dis.DispatchCheck(ctx, &v1.DispatchCheckRequest{ - ResourceRelation: RR(tc.resourceType, tc.permission), + ResourceRelation: RR(tc.resourceType, tc.permission).ToCoreRR(), ResourceIds: []string{tc.resourceID}, ResultsSetting: v1.DispatchCheckRequest_ALLOW_SINGLE_RESULT, - Subject: ONR(tc.subjectType, subjectID, tc.subjectRelation), + Subject: ONR(tc.subjectType, subjectID, tc.subjectRelation).ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -203,17 +202,17 @@ func TestLookupSubjectsMaxDepth(t *testing.T) { ctx := log.Logger.WithContext(datastoremw.ContextWithHandle(context.Background())) require.NoError(datastoremw.SetInContext(ctx, ds)) - tpl := tuple.Parse("folder:oops#owner@folder:oops#owner") - revision, err := common.WriteTuples(ctx, ds, corev1.RelationTupleUpdate_CREATE, tpl) + tpl := tuple.MustParse("folder:oops#owner@folder:oops#owner") + revision, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, tpl) require.NoError(err) dis := NewLocalOnlyDispatcher(10, 100) stream := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupSubjectsResponse](ctx) err = dis.DispatchLookupSubjects(&v1.DispatchLookupSubjectsRequest{ - ResourceRelation: RR("folder", "owner"), + ResourceRelation: RR("folder", "owner").ToCoreRR(), ResourceIds: []string{"oops"}, - SubjectRelation: RR("user", "..."), + SubjectRelation: RR("user", "...").ToCoreRR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -259,9 +258,9 @@ func TestLookupSubjectsDispatchCount(t *testing.T) { stream := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupSubjectsResponse](ctx) err := dis.DispatchLookupSubjects(&v1.DispatchLookupSubjectsRequest{ - ResourceRelation: RR(tc.resourceType, tc.permission), + ResourceRelation: RR(tc.resourceType, tc.permission).ToCoreRR(), ResourceIds: []string{tc.resourceID}, - SubjectRelation: RR(tc.subjectType, tc.subjectRelation), + SubjectRelation: RR(tc.subjectType, tc.subjectRelation).ToCoreRR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -281,9 +280,9 @@ func TestLookupSubjectsOverSchema(t *testing.T) { testCases := []struct { name string schema string - relationships []*corev1.RelationTuple - start *corev1.ObjectAndRelation - target *corev1.RelationReference + relationships []tuple.Relationship + start tuple.ObjectAndRelation + target tuple.RelationReference expected []*v1.FoundSubject }{ { @@ -298,7 +297,7 @@ func TestLookupSubjectsOverSchema(t *testing.T) { relation viewer: user | user with somecaveat permission view = viewer }`, - []*corev1.RelationTuple{ + []tuple.Relationship{ tuple.MustWithCaveat(tuple.MustParse("document:first#viewer@user:tom"), "somecaveat"), tuple.MustParse("document:first#viewer@user:sarah"), }, @@ -327,7 +326,7 @@ func TestLookupSubjectsOverSchema(t *testing.T) { relation editor: user | user with somecaveat permission view = viewer + editor }`, - []*corev1.RelationTuple{ + []tuple.Relationship{ tuple.MustWithCaveat(tuple.MustParse("document:first#viewer@user:tom"), "somecaveat"), tuple.MustWithCaveat(tuple.MustParse("document:first#editor@user:tom"), "somecaveat"), }, @@ -353,7 +352,7 @@ func TestLookupSubjectsOverSchema(t *testing.T) { relation editor: user | user with somecaveat permission view = viewer + editor }`, - []*corev1.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:first#viewer@user:tom"), tuple.MustWithCaveat(tuple.MustParse("document:first#editor@user:tom"), "somecaveat"), }, @@ -382,7 +381,7 @@ func TestLookupSubjectsOverSchema(t *testing.T) { relation editor: user | user with anothercaveat permission view = viewer & editor }`, - []*corev1.RelationTuple{ + []tuple.Relationship{ tuple.MustWithCaveat(tuple.MustParse("document:first#viewer@user:tom"), "somecaveat"), tuple.MustWithCaveat(tuple.MustParse("document:first#editor@user:tom"), "anothercaveat"), tuple.MustWithCaveat(tuple.MustParse("document:first#viewer@user:sarah"), "somecaveat"), @@ -416,7 +415,7 @@ func TestLookupSubjectsOverSchema(t *testing.T) { relation banned: user | user with anothercaveat permission view = viewer - banned }`, - []*corev1.RelationTuple{ + []tuple.Relationship{ tuple.MustWithCaveat(tuple.MustParse("document:first#viewer@user:tom"), "somecaveat"), tuple.MustWithCaveat(tuple.MustParse("document:first#banned@user:tom"), "anothercaveat"), tuple.MustWithCaveat(tuple.MustParse("document:first#viewer@user:sarah"), "somecaveat"), @@ -453,7 +452,7 @@ func TestLookupSubjectsOverSchema(t *testing.T) { relation org: org with somecaveat permission view = org->viewer }`, - []*corev1.RelationTuple{ + []tuple.Relationship{ tuple.MustWithCaveat(tuple.MustParse("document:first#org@org:someorg"), "somecaveat"), tuple.MustParse("org:someorg#viewer@user:tom"), }, @@ -486,7 +485,7 @@ func TestLookupSubjectsOverSchema(t *testing.T) { relation org: org with somecaveat permission view = org->viewer }`, - []*corev1.RelationTuple{ + []tuple.Relationship{ tuple.MustWithCaveat(tuple.MustParse("document:first#org@org:someorg"), "somecaveat"), tuple.MustWithCaveat(tuple.MustParse("org:someorg#viewer@user:tom"), "anothercaveat"), }, @@ -519,7 +518,7 @@ func TestLookupSubjectsOverSchema(t *testing.T) { relation banned: user with anothercaveat permission view = viewer - banned }`, - []*corev1.RelationTuple{ + []tuple.Relationship{ tuple.MustWithCaveat(tuple.MustParse("document:first#viewer@user:*"), "somecaveat"), tuple.MustWithCaveat(tuple.MustParse("document:first#banned@user:tom"), "anothercaveat"), }, @@ -560,7 +559,7 @@ func TestLookupSubjectsOverSchema(t *testing.T) { relation explicitly_allowed: user with thirdcaveat permission view = (viewer - banned) + explicitly_allowed }`, - []*corev1.RelationTuple{ + []tuple.Relationship{ tuple.MustWithCaveat(tuple.MustParse("document:first#viewer@user:*"), "somecaveat"), tuple.MustWithCaveat(tuple.MustParse("document:first#banned@user:tom"), "anothercaveat"), tuple.MustWithCaveat(tuple.MustParse("document:first#explicitly_allowed@user:tom"), "thirdcaveat"), @@ -611,7 +610,7 @@ func TestLookupSubjectsOverSchema(t *testing.T) { relation org: org with somecaveat | org with thirdcaveat permission view = org->viewer }`, - []*corev1.RelationTuple{ + []tuple.Relationship{ tuple.MustWithCaveat(tuple.MustParse("document:first#org@org:someorg"), "somecaveat"), tuple.MustWithCaveat(tuple.MustParse("org:someorg#viewer@user:tom"), "anothercaveat"), tuple.MustParse("org:someorg#viewer@user:sarah"), @@ -656,7 +655,7 @@ func TestLookupSubjectsOverSchema(t *testing.T) { relation folder: folder | folder#parent permission view = folder->view }`, - []*corev1.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("folder:folder1#viewer@user:tom"), tuple.MustParse("folder:folder2#viewer@user:fred"), tuple.MustParse("document:somedoc#folder@folder:folder1"), @@ -691,7 +690,7 @@ func TestLookupSubjectsOverSchema(t *testing.T) { relation folder: folder | folder#parent with somecaveat permission view = folder->view }`, - []*corev1.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("folder:folder1#viewer@user:tom"), tuple.MustParse("folder:folder2#viewer@user:fred"), tuple.MustParse("document:somedoc#folder@folder:folder1"), @@ -722,7 +721,7 @@ func TestLookupSubjectsOverSchema(t *testing.T) { relation folder: folder permission view = folder->view }`, - []*corev1.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("folder:folder1#viewer@user:tom"), tuple.MustParse("folder:folder1#viewer@user:fred"), tuple.MustParse("document:somedoc#folder@folder:folder1"), @@ -751,7 +750,7 @@ func TestLookupSubjectsOverSchema(t *testing.T) { relation folder: folder permission view = folder.any(view) }`, - []*corev1.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("folder:folder1#viewer@user:tom"), tuple.MustParse("folder:folder1#viewer@user:fred"), tuple.MustParse("document:somedoc#folder@folder:folder1"), @@ -780,7 +779,7 @@ func TestLookupSubjectsOverSchema(t *testing.T) { relation folder: folder permission view = folder.all(view) }`, - []*corev1.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("folder:folder1#viewer@user:tom"), tuple.MustParse("folder:folder1#viewer@user:fred"), tuple.MustParse("document:somedoc#folder@folder:folder1"), @@ -809,7 +808,7 @@ func TestLookupSubjectsOverSchema(t *testing.T) { relation folder: folder permission view = folder.all(view) }`, - []*corev1.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("folder:folder1#viewer@user:tom"), tuple.MustParse("folder:folder1#viewer@user:fred"), tuple.MustParse("folder:folder2#viewer@user:fred"), @@ -841,7 +840,7 @@ func TestLookupSubjectsOverSchema(t *testing.T) { relation folder: folder permission view = folder->view }`, - []*corev1.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:somedoc#folder@folder:folder1"), tuple.MustParse("document:somedoc#folder@folder:folder2"), tuple.MustParse("folder:folder1#parent@organization:org1"), @@ -887,7 +886,7 @@ func TestLookupSubjectsOverSchema(t *testing.T) { relation team: team with caveat1 | team with caveat2 permission view = team.all(member) }`, - []*corev1.RelationTuple{ + []tuple.Relationship{ tuple.MustParse(`document:somedoc#team@team:team1[caveat1:{":someparam1":42}]`), tuple.MustParse(`document:somedoc#team@team:team2[caveat2:{":someparam2":43}]`), tuple.MustParse(`team:team1#member@user:tom`), @@ -932,7 +931,7 @@ func TestLookupSubjectsOverSchema(t *testing.T) { relation folder: folder permission view = folder.all(view) - banned }`, - []*corev1.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("folder:folder1#viewer@user:tom"), tuple.MustParse("folder:folder1#viewer@user:fred"), tuple.MustParse("document:somedoc#folder@folder:folder1"), @@ -962,7 +961,7 @@ func TestLookupSubjectsOverSchema(t *testing.T) { relation folder: folder permission view = folder.all(view) }`, - []*corev1.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("folder:root1#owner@user:tom"), tuple.MustParse("folder:root1#owner@user:fred"), tuple.MustParse("folder:root1#owner@user:sarah"), @@ -1008,12 +1007,9 @@ func TestLookupSubjectsOverSchema(t *testing.T) { stream := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupSubjectsResponse](ctx) err = dispatcher.DispatchLookupSubjects(&v1.DispatchLookupSubjectsRequest{ - ResourceRelation: &corev1.RelationReference{ - Namespace: tc.start.Namespace, - Relation: tc.start.Relation, - }, - ResourceIds: []string{tc.start.ObjectId}, - SubjectRelation: tc.target, + ResourceRelation: tc.start.RelationReference().ToCoreRR(), + ResourceIds: []string{tc.start.ObjectID}, + SubjectRelation: tc.target.ToCoreRR(), Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, diff --git a/internal/dispatch/graph/reachableresources_test.go b/internal/dispatch/graph/reachableresources_test.go index 4a73ec2888..e4442b2a29 100644 --- a/internal/dispatch/graph/reachableresources_test.go +++ b/internal/dispatch/graph/reachableresources_test.go @@ -34,7 +34,7 @@ type reachableResource struct { hasPermission bool } -func reachable(onr *core.ObjectAndRelation, hasPermission bool) reachableResource { +func reachable(onr tuple.ObjectAndRelation, hasPermission bool) reachableResource { return reachableResource{ tuple.StringONR(onr), hasPermission, } @@ -44,8 +44,8 @@ func TestSimpleReachableResources(t *testing.T) { t.Parallel() testCases := []struct { - start *core.RelationReference - target *core.ObjectAndRelation + start tuple.RelationReference + target tuple.ObjectAndRelation reachable []reachableResource }{ { @@ -146,7 +146,7 @@ func TestSimpleReachableResources(t *testing.T) { for _, tc := range testCases { name := fmt.Sprintf( "%s#%s->%s", - tc.start.Namespace, + tc.start.ObjectType, tc.start.Relation, tuple.StringONR(tc.target), ) @@ -160,12 +160,12 @@ func TestSimpleReachableResources(t *testing.T) { stream := dispatch.NewCollectingDispatchStream[*v1.DispatchReachableResourcesResponse](ctx) err := dispatcher.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ - ResourceRelation: tc.start, + ResourceRelation: tc.start.ToCoreRR(), SubjectRelation: &core.RelationReference{ - Namespace: tc.target.Namespace, + Namespace: tc.target.ObjectType, Relation: tc.target.Relation, }, - SubjectIds: []string{tc.target.ObjectId}, + SubjectIds: []string{tc.target.ObjectID}, Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -176,11 +176,9 @@ func TestSimpleReachableResources(t *testing.T) { results := []reachableResource{} for _, streamResult := range stream.Results() { results = append(results, reachableResource{ - tuple.StringONR(&core.ObjectAndRelation{ - Namespace: tc.start.Namespace, - ObjectId: streamResult.Resource.ResourceId, - Relation: tc.start.Relation, - }), + tuple.StringONR( + tuple.ONR(tc.start.ObjectType, streamResult.Resource.ResourceId, tc.start.Relation), + ), streamResult.Resource.ResultStatus == v1.ReachableResource_HAS_PERMISSION, }) } @@ -203,8 +201,8 @@ func TestMaxDepthreachableResources(t *testing.T) { stream := dispatch.NewCollectingDispatchStream[*v1.DispatchReachableResourcesResponse](ctx) err := dispatcher.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ - ResourceRelation: RR("document", "view"), - SubjectRelation: RR("user", "..."), + ResourceRelation: RR("document", "view").ToCoreRR(), + SubjectRelation: RR("user", "...").ToCoreRR(), SubjectIds: []string{"legal"}, Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), @@ -225,8 +223,8 @@ func byONRAndPermission(a, b reachableResource) int { func BenchmarkReachableResources(b *testing.B) { testCases := []struct { - start *core.RelationReference - target *core.ObjectAndRelation + start tuple.RelationReference + target tuple.ObjectAndRelation }{ { RR("document", "view"), @@ -253,7 +251,7 @@ func BenchmarkReachableResources(b *testing.B) { for _, tc := range testCases { name := fmt.Sprintf( "%s#%s->%s", - tc.start.Namespace, + tc.start.ObjectType, tc.start.Relation, tuple.StringONR(tc.target), ) @@ -274,12 +272,12 @@ func BenchmarkReachableResources(b *testing.B) { for n := 0; n < b.N; n++ { stream := dispatch.NewCollectingDispatchStream[*v1.DispatchReachableResourcesResponse](ctx) err := dispatcher.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ - ResourceRelation: tc.start, + ResourceRelation: tc.start.ToCoreRR(), SubjectRelation: &core.RelationReference{ - Namespace: tc.target.Namespace, + Namespace: tc.target.ObjectType, Relation: tc.target.Relation, }, - SubjectIds: []string{tc.target.ObjectId}, + SubjectIds: []string{tc.target.ObjectID}, Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -287,13 +285,9 @@ func BenchmarkReachableResources(b *testing.B) { }, stream) require.NoError(err) - results := []*core.ObjectAndRelation{} + results := []tuple.ObjectAndRelation{} for _, streamResult := range stream.Results() { - results = append(results, &core.ObjectAndRelation{ - Namespace: tc.start.Namespace, - ObjectId: streamResult.Resource.ResourceId, - Relation: tc.start.Relation, - }) + results = append(results, tuple.ONR(tc.start.ObjectType, streamResult.Resource.ResourceId, tc.start.Relation)) } require.GreaterOrEqual(len(results), 0) } @@ -306,9 +300,9 @@ func TestCaveatedReachableResources(t *testing.T) { testCases := []struct { name string schema string - relationships []*core.RelationTuple - start *core.RelationReference - target *core.ObjectAndRelation + relationships []tuple.Relationship + start tuple.RelationReference + target tuple.ObjectAndRelation reachable []reachableResource }{ { @@ -334,7 +328,7 @@ func TestCaveatedReachableResources(t *testing.T) { permission view = viewer } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:foo#viewer@user:tom"), }, RR("document", "view"), @@ -354,7 +348,7 @@ func TestCaveatedReachableResources(t *testing.T) { somecondition == 42 } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustWithCaveat(tuple.MustParse("document:foo#viewer@user:tom"), "testcaveat"), }, RR("document", "view"), @@ -378,7 +372,7 @@ func TestCaveatedReachableResources(t *testing.T) { somecondition == 42 } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:somedoc#parent@organization:foo"), tuple.MustWithCaveat(tuple.MustParse("organization:foo#viewer@user:tom"), "testcaveat"), }, @@ -403,7 +397,7 @@ func TestCaveatedReachableResources(t *testing.T) { somecondition == 42 } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("organization:foo#viewer@user:tom"), tuple.MustWithCaveat(tuple.MustParse("document:somedoc#parent@organization:foo"), "testcaveat"), }, @@ -424,7 +418,7 @@ func TestCaveatedReachableResources(t *testing.T) { somecondition == 42 } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustWithCaveat(tuple.MustParse("document:foo#viewer@user:tom"), "testcaveat"), tuple.MustParse("document:bar#viewer@user:tom"), }, @@ -449,7 +443,7 @@ func TestCaveatedReachableResources(t *testing.T) { somecondition == 42 } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:bar#editor@user:tom"), tuple.MustParse("document:bar#viewer@user:tom"), tuple.MustWithCaveat(tuple.MustParse("document:foo#viewer@user:tom"), "testcaveat"), @@ -476,7 +470,7 @@ func TestCaveatedReachableResources(t *testing.T) { somecondition == 42 } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustWithCaveat(tuple.MustParse("document:foo#viewer@user:tom"), "testcaveat"), tuple.MustParse("document:foo#editor@user:tom"), }, @@ -500,7 +494,7 @@ func TestCaveatedReachableResources(t *testing.T) { somecondition == 42 } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustWithCaveat(tuple.MustParse("document:foo#viewer@user:tom"), "testcaveat"), tuple.MustParse("document:foo#banned@user:tom"), }, @@ -527,7 +521,7 @@ func TestCaveatedReachableResources(t *testing.T) { somecondition == 42 } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustWithCaveat(tuple.MustParse("document:foo#folder@folder:maybe"), "testcaveat"), tuple.MustParse("document:foo#folder@folder:always"), tuple.MustParse("folder:always#viewer@user:tom"), @@ -553,7 +547,7 @@ func TestCaveatedReachableResources(t *testing.T) { somecondition == 42 } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustWithCaveat(tuple.MustParse("document:foo#viewer@user:tom"), "testcaveat"), tuple.MustParse("document:foo#editor@user:tom"), }, @@ -583,12 +577,12 @@ func TestCaveatedReachableResources(t *testing.T) { stream := dispatch.NewCollectingDispatchStream[*v1.DispatchReachableResourcesResponse](ctx) err = dispatcher.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ - ResourceRelation: tc.start, + ResourceRelation: tc.start.ToCoreRR(), SubjectRelation: &core.RelationReference{ - Namespace: tc.target.Namespace, + Namespace: tc.target.ObjectType, Relation: tc.target.Relation, }, - SubjectIds: []string{tc.target.ObjectId}, + SubjectIds: []string{tc.target.ObjectID}, Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -598,11 +592,7 @@ func TestCaveatedReachableResources(t *testing.T) { results := []reachableResource{} for _, streamResult := range stream.Results() { results = append(results, reachableResource{ - tuple.StringONR(&core.ObjectAndRelation{ - Namespace: tc.start.Namespace, - ObjectId: streamResult.Resource.ResourceId, - Relation: tc.start.Relation, - }), + tuple.StringONR(tuple.ONR(tc.start.ObjectType, streamResult.Resource.ResourceId, tc.start.Relation)), streamResult.Resource.ResultStatus == v1.ReachableResource_HAS_PERMISSION, }) } @@ -623,12 +613,12 @@ func TestReachableResourcesWithConsistencyLimitOf1(t *testing.T) { target := ONR("user", "owner", "...") stream := dispatch.NewCollectingDispatchStream[*v1.DispatchReachableResourcesResponse](ctx) err := dispatcher.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ - ResourceRelation: RR("folder", "view"), + ResourceRelation: RR("folder", "view").ToCoreRR(), SubjectRelation: &core.RelationReference{ - Namespace: target.Namespace, + Namespace: target.ObjectType, Relation: target.Relation, }, - SubjectIds: []string{target.ObjectId}, + SubjectIds: []string{target.ObjectID}, Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -649,7 +639,7 @@ func TestReachableResourcesMultipleEntrypointEarlyCancel(t *testing.T) { rawDS, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC) require.NoError(t, err) - testRels := make([]*core.RelationTuple, 0) + testRels := make([]tuple.Relationship, 0) for i := 0; i < 25; i++ { testRels = append(testRels, tuple.MustParse(fmt.Sprintf("resource:res%d#viewer@user:tom", i))) testRels = append(testRels, tuple.MustParse(fmt.Sprintf("resource:res%d#namespace@namespace:ns%d", i, i))) @@ -698,7 +688,7 @@ func TestReachableResourcesMultipleEntrypointEarlyCancel(t *testing.T) { ctxWithCancel, cancel := context.WithCancel(ctx) stream := dispatch.NewCollectingDispatchStream[*v1.DispatchReachableResourcesResponse](ctxWithCancel) err = dispatcher.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ - ResourceRelation: RR("resource", "view"), + ResourceRelation: RR("resource", "view").ToCoreRR(), SubjectRelation: &core.RelationReference{ Namespace: "user", Relation: "...", @@ -725,7 +715,7 @@ func TestReachableResourcesCursors(t *testing.T) { rawDS, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC) require.NoError(t, err) - testRels := make([]*core.RelationTuple, 0) + testRels := make([]tuple.Relationship, 0) // tom and sarah have access via a single role on each. for i := 0; i < 410; i++ { @@ -771,7 +761,7 @@ func TestReachableResourcesCursors(t *testing.T) { ctxWithCancel, cancel := context.WithCancel(ctx) stream := dispatch.NewCollectingDispatchStream[*v1.DispatchReachableResourcesResponse](ctxWithCancel) err = dispatcher.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ - ResourceRelation: RR("resource", "view"), + ResourceRelation: RR("resource", "view").ToCoreRR(), SubjectRelation: &core.RelationReference{ Namespace: "user", Relation: "...", @@ -807,7 +797,7 @@ func TestReachableResourcesCursors(t *testing.T) { // and then move forward from there. stream2 := dispatch.NewCollectingDispatchStream[*v1.DispatchReachableResourcesResponse](ctx) err = dispatcher.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ - ResourceRelation: RR("resource", "view"), + ResourceRelation: RR("resource", "view").ToCoreRR(), SubjectRelation: &core.RelationReference{ Namespace: "user", Relation: "...", @@ -841,7 +831,7 @@ func TestReachableResourcesPaginationWithLimit(t *testing.T) { rawDS, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC) require.NoError(t, err) - testRels := make([]*core.RelationTuple, 0) + testRels := make([]tuple.Relationship, 0) for i := 0; i < 410; i++ { testRels = append(testRels, tuple.MustParse(fmt.Sprintf("resource:res%03d#viewer@user:tom", i))) @@ -878,7 +868,7 @@ func TestReachableResourcesPaginationWithLimit(t *testing.T) { ctxWithCancel, cancel := context.WithCancel(ctx) stream := dispatch.NewCollectingDispatchStream[*v1.DispatchReachableResourcesResponse](ctxWithCancel) err = dispatcher.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ - ResourceRelation: RR("resource", "view"), + ResourceRelation: RR("resource", "view").ToCoreRR(), SubjectRelation: &core.RelationReference{ Namespace: "user", Relation: "...", @@ -922,7 +912,7 @@ func TestReachableResourcesWithQueryError(t *testing.T) { rawDS, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC) require.NoError(t, err) - testRels := make([]*core.RelationTuple, 0) + testRels := make([]tuple.Relationship, 0) for i := 0; i < 410; i++ { testRels = append(testRels, tuple.MustParse(fmt.Sprintf("resource:res%03d#viewer@user:tom", i))) @@ -954,7 +944,7 @@ func TestReachableResourcesWithQueryError(t *testing.T) { ctxWithCancel, cancel := context.WithCancel(ctx) stream := dispatch.NewCollectingDispatchStream[*v1.DispatchReachableResourcesResponse](ctxWithCancel) err = dispatcher.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ - ResourceRelation: RR("resource", "view"), + ResourceRelation: RR("resource", "view").ToCoreRR(), SubjectRelation: &core.RelationReference{ Namespace: "user", Relation: "...", @@ -1005,9 +995,9 @@ func TestReachableResourcesOverSchema(t *testing.T) { testCases := []struct { name string schema string - relationships []*core.RelationTuple - permission *core.RelationReference - subject *core.ObjectAndRelation + relationships []tuple.Relationship + permission tuple.RelationReference + subject tuple.ObjectAndRelation expectedResourceIDs []string }{ { @@ -1020,8 +1010,8 @@ func TestReachableResourcesOverSchema(t *testing.T) { permission view = viewer + editor }`, joinTuples( - genTuples("document", "viewer", "user", "tom", 1510), - genTuples("document", "editor", "user", "tom", 1510), + genRels("document", "viewer", "user", "tom", 1510), + genRels("document", "editor", "user", "tom", 1510), ), RR("document", "view"), ONR("user", "tom", "..."), @@ -1036,7 +1026,7 @@ func TestReachableResourcesOverSchema(t *testing.T) { relation viewer: user permission view = viewer - banned }`, - genTuples("document", "viewer", "user", "tom", 1010), + genRels("document", "viewer", "user", "tom", 1010), RR("document", "view"), ONR("user", "tom", "..."), genResourceIds("document", 1010), @@ -1051,8 +1041,8 @@ func TestReachableResourcesOverSchema(t *testing.T) { permission view = viewer & editor }`, joinTuples( - genTuples("document", "viewer", "user", "tom", 510), - genTuples("document", "editor", "user", "tom", 510), + genRels("document", "viewer", "user", "tom", 510), + genRels("document", "editor", "user", "tom", 510), ), RR("document", "view"), ONR("user", "tom", "..."), @@ -1070,8 +1060,8 @@ func TestReachableResourcesOverSchema(t *testing.T) { permission view = can_view + editor }`, joinTuples( - genTuples("document", "viewer", "user", "tom", 1310), - genTuplesWithOffset("document", "editor", "user", "tom", 1250, 1200), + genRels("document", "viewer", "user", "tom", 1310), + genRelsWithOffset("document", "editor", "user", "tom", 1250, 1200), ), RR("document", "view"), ONR("user", "tom", "..."), @@ -1089,7 +1079,7 @@ func TestReachableResourcesOverSchema(t *testing.T) { relation viewer: user with somecaveat permission view = viewer }`, - genTuplesWithCaveat("document", "viewer", "user", "tom", "somecaveat", map[string]any{"somecondition": 42}, 0, 2450), + genRelsWithCaveat("document", "viewer", "user", "tom", "somecaveat", map[string]any{"somecondition": 42}, 0, 2450), RR("document", "view"), ONR("user", "tom", "..."), genResourceIds("document", 2450), @@ -1104,8 +1094,8 @@ func TestReachableResourcesOverSchema(t *testing.T) { permission view = viewer - banned }`, joinTuples( - genTuples("document", "viewer", "user", "tom", 1310), - genTuplesWithOffset("document", "banned", "user", "tom", 1210, 100), + genRels("document", "viewer", "user", "tom", 1310), + genRelsWithOffset("document", "banned", "user", "tom", 1210, 100), ), RR("document", "view"), ONR("user", "tom", "..."), @@ -1123,7 +1113,7 @@ func TestReachableResourcesOverSchema(t *testing.T) { relation viewer: user with somecaveat permission view = viewer }`, - genTuplesWithCaveat("document", "viewer", "user", "tom", "somecaveat", map[string]any{}, 0, 2450), + genRelsWithCaveat("document", "viewer", "user", "tom", "somecaveat", map[string]any{}, 0, 2450), RR("document", "view"), ONR("user", "tom", "..."), genResourceIds("document", 2450), @@ -1141,8 +1131,8 @@ func TestReachableResourcesOverSchema(t *testing.T) { permission view = folder->viewer }`, joinTuples( - genTuples("folder", "viewer", "user", "tom", 150), - genSubjectTuples("document", "folder", "folder", "...", 150), + genRels("folder", "viewer", "user", "tom", 150), + genSubjectRels("document", "folder", "folder", "...", 150), ), RR("document", "view"), ONR("user", "tom", "..."), @@ -1158,8 +1148,8 @@ func TestReachableResourcesOverSchema(t *testing.T) { permission view = viewer + editor }`, joinTuples( - genTuples("document", "viewer", "user", "tom", 15100), - genTuples("document", "editor", "user", "tom", 15100), + genRels("document", "viewer", "user", "tom", 15100), + genRels("document", "editor", "user", "tom", 15100), ), RR("document", "view"), ONR("user", "tom", "..."), @@ -1178,28 +1168,19 @@ func TestReachableResourcesOverSchema(t *testing.T) { relation parent: folder permission view = parent->view }`, - (func() []*core.RelationTuple { + (func() []tuple.Relationship { // Generate 200 folders with tom as a viewer - tuples := make([]*core.RelationTuple, 0, 200*200) + rels := make([]tuple.Relationship, 0, 200*200) for folderID := 0; folderID < 200; folderID++ { - tpl := &core.RelationTuple{ - ResourceAndRelation: ONR("folder", fmt.Sprintf("folder-%d", folderID), "viewer"), - Subject: ONR("user", "tom", "..."), - } - tuples = append(tuples, tpl) + rels = append(rels, tuple.MustParse(fmt.Sprintf("folder:folder-%d#viewer@user:tom", folderID))) // Generate 200 documents for each folder. for documentID := 0; documentID < 200; documentID++ { - docID := fmt.Sprintf("doc-%d-%d", folderID, documentID) - tpl := &core.RelationTuple{ - ResourceAndRelation: ONR("document", docID, "parent"), - Subject: ONR("folder", fmt.Sprintf("folder-%d", folderID), "..."), - } - tuples = append(tuples, tpl) + rels = append(rels, tuple.MustParse(fmt.Sprintf("document:doc-%d-%d#parent@folder:folder-%d", folderID, documentID, folderID))) } } - return tuples + return rels })(), RR("document", "view"), ONR("user", "tom", "..."), @@ -1245,12 +1226,12 @@ func TestReachableResourcesOverSchema(t *testing.T) { require.NoError(err) err = dispatcher.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ - ResourceRelation: tc.permission, + ResourceRelation: tc.permission.ToCoreRR(), SubjectRelation: &core.RelationReference{ - Namespace: tc.subject.Namespace, + Namespace: tc.subject.ObjectType, Relation: tc.subject.Relation, }, - SubjectIds: []string{tc.subject.ObjectId}, + SubjectIds: []string{tc.subject.ObjectID}, Metadata: &v1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 50, @@ -1291,7 +1272,7 @@ func TestReachableResourcesWithPreCancelation(t *testing.T) { rawDS, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC) require.NoError(t, err) - testRels := make([]*core.RelationTuple, 0) + testRels := make([]tuple.Relationship, 0) for i := 0; i < 410; i++ { testRels = append(testRels, tuple.MustParse(fmt.Sprintf("resource:res%03d#viewer@user:tom", i))) @@ -1325,7 +1306,7 @@ func TestReachableResourcesWithPreCancelation(t *testing.T) { stream := dispatch.NewCollectingDispatchStream[*v1.DispatchReachableResourcesResponse](ctxWithCancel) err = dispatcher.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ - ResourceRelation: RR("resource", "view"), + ResourceRelation: RR("resource", "view").ToCoreRR(), SubjectRelation: &core.RelationReference{ Namespace: "user", Relation: "...", @@ -1345,7 +1326,7 @@ func TestReachableResourcesWithUnexpectedContextCancelation(t *testing.T) { rawDS, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC) require.NoError(t, err) - testRels := make([]*core.RelationTuple, 0) + testRels := make([]tuple.Relationship, 0) for i := 0; i < 410; i++ { testRels = append(testRels, tuple.MustParse(fmt.Sprintf("resource:res%03d#viewer@user:tom", i))) @@ -1377,7 +1358,7 @@ func TestReachableResourcesWithUnexpectedContextCancelation(t *testing.T) { ctxWithCancel, cancel := context.WithCancel(ctx) stream := dispatch.NewCollectingDispatchStream[*v1.DispatchReachableResourcesResponse](ctxWithCancel) err = dispatcher.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ - ResourceRelation: RR("resource", "view"), + ResourceRelation: RR("resource", "view").ToCoreRR(), SubjectRelation: &core.RelationReference{ Namespace: "user", Relation: "...", @@ -1429,7 +1410,7 @@ func TestReachableResourcesWithCachingInParallelTest(t *testing.T) { rawDS, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC) require.NoError(t, err) - testRels := make([]*core.RelationTuple, 0) + testRels := make([]tuple.Relationship, 0) expectedResources := mapz.NewSet[string]() for i := 0; i < 410; i++ { @@ -1474,7 +1455,7 @@ func TestReachableResourcesWithCachingInParallelTest(t *testing.T) { stream := dispatch.NewCollectingDispatchStream[*v1.DispatchReachableResourcesResponse](ctx) err = cachingDispatcher.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ - ResourceRelation: RR("resource", "view"), + ResourceRelation: RR("resource", "view").ToCoreRR(), SubjectRelation: &core.RelationReference{ Namespace: "user", Relation: "...", diff --git a/internal/dispatch/keys/computed.go b/internal/dispatch/keys/computed.go index a9794d2f22..5d4ea33412 100644 --- a/internal/dispatch/keys/computed.go +++ b/internal/dispatch/keys/computed.go @@ -60,7 +60,7 @@ func checkRequestToKeyWithCanonical(req *v1.DispatchCheckRequest, canonicalKey s ) if canonicalKey == "" { - return cacheKey, spiceerrors.MustBugf("given empty canonical key for request: %s => %s", req.ResourceRelation, tuple.StringONR(req.Subject)) + return cacheKey, spiceerrors.MustBugf("given empty canonical key for request: %s => %s", req.ResourceRelation, tuple.StringCoreONR(req.Subject)) } return cacheKey, nil diff --git a/internal/dispatch/keys/computed_test.go b/internal/dispatch/keys/computed_test.go index 9069fecfe2..a26a09272f 100644 --- a/internal/dispatch/keys/computed_test.go +++ b/internal/dispatch/keys/computed_test.go @@ -25,8 +25,8 @@ func TestKeyPrefixOverlap(t *testing.T) { } var ( - ONR = tuple.ObjectAndRelation - RR = tuple.RelationReference + ONR = tuple.CoreONR + RR = tuple.CoreRR ) func TestStableCacheKeys(t *testing.T) { @@ -857,10 +857,10 @@ func TestCacheKeyNoOverlap(t *testing.T) { t.Run(strings.Join(subjectIds, ","), func(t *testing.T) { for _, resourceRelation := range resourceRelations { resourceRelation := resourceRelation - t.Run(tuple.StringRR(resourceRelation), func(t *testing.T) { + t.Run(tuple.StringCoreRR(resourceRelation), func(t *testing.T) { for _, subjectRelation := range subjectRelations { subjectRelation := subjectRelation - t.Run(tuple.StringRR(subjectRelation), func(t *testing.T) { + t.Run(tuple.StringCoreRR(subjectRelation), func(t *testing.T) { for _, revision := range revisions { revision := revision t.Run(revision, func(t *testing.T) { diff --git a/internal/dispatch/keys/hasher_common.go b/internal/dispatch/keys/hasher_common.go index e6e95e60df..b1400b6681 100644 --- a/internal/dispatch/keys/hasher_common.go +++ b/internal/dispatch/keys/hasher_common.go @@ -23,7 +23,7 @@ type hashableRelationReference struct { } func (hrr hashableRelationReference) AppendToHash(hasher hasherInterface) { - hasher.WriteString(tuple.StringRR(hrr.RelationReference)) + hasher.WriteString(tuple.StringCoreRR(hrr.RelationReference)) } type hashableResultSetting v1.DispatchCheckRequest_ResultsSetting diff --git a/internal/dispatch/singleflight/singleflight_test.go b/internal/dispatch/singleflight/singleflight_test.go index 6612b42806..51db7f1eb9 100644 --- a/internal/dispatch/singleflight/singleflight_test.go +++ b/internal/dispatch/singleflight/singleflight_test.go @@ -30,9 +30,9 @@ func TestSingleFlightDispatcher(t *testing.T) { disp := New(mockDispatcher{f: f}, &keys.DirectKeyHandler{}) req := &v1.DispatchCheckRequest{ - ResourceRelation: tuple.RelationReference("document", "view"), + ResourceRelation: tuple.RR("document", "view").ToCoreRR(), ResourceIds: []string{"foo", "bar"}, - Subject: tuple.ObjectAndRelation("user", "tom", "..."), + Subject: tuple.ONRStringToCore("user", "tom", "..."), Metadata: &v1.ResolverMeta{ AtRevision: "1234", TraversalBloom: v1.MustNewTraversalBloomFilter(defaultBloomFilterSize), @@ -78,9 +78,9 @@ func TestSingleFlightDispatcherDetectsLoop(t *testing.T) { disp := New(mockDispatcher{f: f}, keyHandler) req := &v1.DispatchCheckRequest{ - ResourceRelation: tuple.RelationReference("document", "view"), + ResourceRelation: tuple.RR("document", "view").ToCoreRR(), ResourceIds: []string{"foo", "bar"}, - Subject: tuple.ObjectAndRelation("user", "tom", "..."), + Subject: tuple.ONRStringToCore("user", "tom", "..."), Metadata: &v1.ResolverMeta{ AtRevision: "1234", TraversalBloom: v1.MustNewTraversalBloomFilter(defaultBloomFilterSize), @@ -134,9 +134,9 @@ func TestSingleFlightDispatcherDetectsLoopThroughDelegate(t *testing.T) { disp := New(New(mockDispatcher{f: f}, keyHandler), keyHandler) req := &v1.DispatchCheckRequest{ - ResourceRelation: tuple.RelationReference("document", "view"), + ResourceRelation: tuple.RR("document", "view").ToCoreRR(), ResourceIds: []string{"foo", "bar"}, - Subject: tuple.ObjectAndRelation("user", "tom", "..."), + Subject: tuple.ONRStringToCore("user", "tom", "..."), Metadata: &v1.ResolverMeta{ AtRevision: "1234", TraversalBloom: v1.MustNewTraversalBloomFilter(defaultBloomFilterSize), @@ -175,9 +175,9 @@ func TestSingleFlightDispatcherCancelation(t *testing.T) { } req := &v1.DispatchCheckRequest{ - ResourceRelation: tuple.RelationReference("document", "view"), + ResourceRelation: tuple.RR("document", "view").ToCoreRR(), ResourceIds: []string{"foo", "bar"}, - Subject: tuple.ObjectAndRelation("user", "tom", "..."), + Subject: tuple.ONRStringToCore("user", "tom", "..."), Metadata: &v1.ResolverMeta{ AtRevision: "1234", TraversalBloom: v1.MustNewTraversalBloomFilter(defaultBloomFilterSize), @@ -223,7 +223,7 @@ func TestSingleFlightDispatcherExpand(t *testing.T) { disp := New(mockDispatcher{f: f}, &keys.DirectKeyHandler{}) req := &v1.DispatchExpandRequest{ - ResourceAndRelation: tuple.ObjectAndRelation("document", "foo", "view"), + ResourceAndRelation: tuple.ONRStringToCore("document", "foo", "view"), Metadata: &v1.ResolverMeta{ AtRevision: "1234", TraversalBloom: v1.MustNewTraversalBloomFilter(defaultBloomFilterSize), @@ -267,9 +267,9 @@ func TestSingleFlightDispatcherCheckBypassesIfMissingBloomFiler(t *testing.T) { disp := New(mockDispatcher{f: f}, &keys.DirectKeyHandler{}) req := &v1.DispatchCheckRequest{ - ResourceRelation: tuple.RelationReference("document", "view"), + ResourceRelation: tuple.RR("document", "view").ToCoreRR(), ResourceIds: []string{"foo", "bar"}, - Subject: tuple.ObjectAndRelation("user", "tom", "..."), + Subject: tuple.ONRStringToCore("user", "tom", "..."), Metadata: &v1.ResolverMeta{ AtRevision: "1234", }, @@ -292,7 +292,7 @@ func TestSingleFlightDispatcherExpandBypassesIfMissingBloomFiler(t *testing.T) { disp := New(mockDispatcher{f: f}, &keys.DirectKeyHandler{}) req := &v1.DispatchExpandRequest{ - ResourceAndRelation: tuple.ObjectAndRelation("document", "foo", "view"), + ResourceAndRelation: tuple.ONRStringToCore("document", "foo", "view"), Metadata: &v1.ResolverMeta{ AtRevision: "1234", }, diff --git a/internal/graph/check.go b/internal/graph/check.go index 77f498ed29..4d9a2d1562 100644 --- a/internal/graph/check.go +++ b/internal/graph/check.go @@ -192,16 +192,17 @@ func (cc *ConcurrentChecker) checkInternal(ctx context.Context, req ValidatedChe // Filter for check hints, if any. if len(req.CheckHints) > 0 { + subject := tuple.FromCoreObjectAndRelation(req.Subject) filteredResourcesIdsSet := mapz.NewSet(filteredResourcesIds...) for _, checkHint := range req.CheckHints { - resourceID, ok := hints.AsCheckHintForComputedUserset(checkHint, req.ResourceRelation.Namespace, req.ResourceRelation.Relation, req.Subject) + resourceID, ok := hints.AsCheckHintForComputedUserset(checkHint, req.ResourceRelation.Namespace, req.ResourceRelation.Relation, subject) if ok { filteredResourcesIdsSet.Delete(resourceID) continue } if req.OriginalRelationName != "" { - resourceID, ok = hints.AsCheckHintForComputedUserset(checkHint, req.ResourceRelation.Namespace, req.OriginalRelationName, req.Subject) + resourceID, ok = hints.AsCheckHintForComputedUserset(checkHint, req.ResourceRelation.Namespace, req.OriginalRelationName, subject) if ok { filteredResourcesIdsSet.Delete(resourceID) } @@ -266,11 +267,12 @@ func combineWithCheckHints(result CheckResult, req ValidatedCheckRequest) CheckR return result } + subject := tuple.FromCoreObjectAndRelation(req.Subject) for _, checkHint := range req.CheckHints { - resourceID, ok := hints.AsCheckHintForComputedUserset(checkHint, req.ResourceRelation.Namespace, req.ResourceRelation.Relation, req.Subject) + resourceID, ok := hints.AsCheckHintForComputedUserset(checkHint, req.ResourceRelation.Namespace, req.ResourceRelation.Relation, subject) if !ok { if req.OriginalRelationName != "" { - resourceID, ok = hints.AsCheckHintForComputedUserset(checkHint, req.ResourceRelation.Namespace, req.OriginalRelationName, req.Subject) + resourceID, ok = hints.AsCheckHintForComputedUserset(checkHint, req.ResourceRelation.Namespace, req.OriginalRelationName, subject) } if !ok { @@ -383,27 +385,21 @@ func (cc *ConcurrentChecker) checkDirect(ctx context.Context, crc currentRequest if err != nil { return checkResultError(NewCheckFailureErr(err), emptyMetadata) } - defer it.Close() queryCount += 1.0 // Find the matching subject(s). - for tpl := it.Next(); tpl != nil; tpl = it.Next() { - if it.Err() != nil { - return checkResultError(NewCheckFailureErr(it.Err()), emptyMetadata) + for rel, err := range it { + if err != nil { + return checkResultError(NewCheckFailureErr(err), emptyMetadata) } - spiceerrors.DebugAssert(func() bool { - return tuple.OnrEqualOrWildcard(tpl.Subject, crc.parentReq.Subject) - }, "somehow got invalid ONR for direct check matching") - // If the subject of the relationship matches the target subject, then we've found // a result. - foundResources.AddDirectMember(tpl.ResourceAndRelation.ObjectId, tpl.Caveat) + foundResources.AddDirectMember(rel.Resource.ObjectID, rel.OptionalCaveat) if crc.resultsSetting == v1.DispatchCheckRequest_ALLOW_SINGLE_RESULT && foundResources.HasDeterminedMember() { return checkResultsForMembership(foundResources, emptyMetadata) } } - it.Close() } // Filter down the resource IDs for further dispatch based on whether they exist as found @@ -438,19 +434,17 @@ func (cc *ConcurrentChecker) checkDirect(ctx context.Context, crc currentRequest if err != nil { return checkResultError(NewCheckFailureErr(err), emptyMetadata) } - defer it.Close() queryCount += 1.0 // Build the set of subjects over which to dispatch, along with metadata for // mapping over caveats (if any). checksToDispatch := newCheckDispatchSet() - for tpl := it.Next(); tpl != nil; tpl = it.Next() { - if it.Err() != nil { - return checkResultError(NewCheckFailureErr(it.Err()), emptyMetadata) + for rel, err := range it { + if err != nil { + return checkResultError(NewCheckFailureErr(err), emptyMetadata) } - checksToDispatch.addForRelationship(tpl) + checksToDispatch.addForRelationship(rel) } - it.Close() // Dispatch and map to the associated resource ID(s). toDispatch := checksToDispatch.dispatchChunks(crc.dispatchChunkSize) @@ -464,7 +458,7 @@ func (cc *ConcurrentChecker) checkDirect(ctx context.Context, crc currentRequest childResult := cc.dispatch(ctx, crc, ValidatedCheckRequest{ &v1.DispatchCheckRequest{ - ResourceRelation: tuple.RelationReference(dd.resourceType.namespace, dd.resourceType.relation), + ResourceRelation: dd.resourceType.ToCoreRR(), ResourceIds: dd.resourceIds, Subject: crc.parentReq.Subject, ResultsSetting: resultsSetting, @@ -487,11 +481,11 @@ func (cc *ConcurrentChecker) checkDirect(ctx context.Context, crc currentRequest return combineResultWithFoundResources(result, foundResources) } -func mapFoundResources(result CheckResult, resourceType relationRef, checksToDispatch *checkDispatchSet) CheckResult { +func mapFoundResources(result CheckResult, resourceType tuple.RelationReference, checksToDispatch *checkDispatchSet) CheckResult { // Map any resources found to the parent resource IDs. membershipSet := NewMembershipSet() for foundResourceID, result := range result.Resp.ResultsByResourceId { - resourceIDAndCaveats := checksToDispatch.mappingsForSubject(resourceType.namespace, foundResourceID, resourceType.relation) + resourceIDAndCaveats := checksToDispatch.mappingsForSubject(resourceType.ObjectType, foundResourceID, resourceType.Relation) spiceerrors.DebugAssert(func() bool { return len(resourceIDAndCaveats) > 0 @@ -566,7 +560,7 @@ func (cc *ConcurrentChecker) runSetOperation(ctx context.Context, crc currentReq } } -func (cc *ConcurrentChecker) checkComputedUserset(ctx context.Context, crc currentRequestContext, cu *core.ComputedUserset, rr *relationRef, resourceIds []string) CheckResult { +func (cc *ConcurrentChecker) checkComputedUserset(ctx context.Context, crc currentRequestContext, cu *core.ComputedUserset, rr *tuple.RelationReference, resourceIds []string) CheckResult { ctx, span := tracer.Start(ctx, cu.Relation) defer span.End() @@ -577,7 +571,7 @@ func (cc *ConcurrentChecker) checkComputedUserset(ctx context.Context, crc curre return checkResultError(spiceerrors.MustBugf("computed userset for tupleset without tuples"), emptyMetadata) } - startNamespace = rr.namespace + startNamespace = rr.ObjectType targetResourceIds = resourceIds } else if cu.Object == core.ComputedUserset_TUPLE_OBJECT { if rr != nil { @@ -664,7 +658,7 @@ type ttu[T relation] interface { type checkResultWithType struct { CheckResult - relationType relationRef + relationType tuple.RelationReference } func checkIntersectionTupleToUserset( @@ -688,19 +682,17 @@ func checkIntersectionTupleToUserset( if err != nil { return checkResultError(NewCheckFailureErr(err), emptyMetadata) } - defer it.Close() checksToDispatch := newCheckDispatchSet() - subjectsByResourceID := mapz.NewMultiMap[string, *core.ObjectAndRelation]() - for tpl := it.Next(); tpl != nil; tpl = it.Next() { - if it.Err() != nil { - return checkResultError(NewCheckFailureErr(it.Err()), emptyMetadata) + subjectsByResourceID := mapz.NewMultiMap[string, tuple.ObjectAndRelation]() + for rel, err := range it { + if err != nil { + return checkResultError(NewCheckFailureErr(err), emptyMetadata) } - checksToDispatch.addForRelationship(tpl) - subjectsByResourceID.Add(tpl.ResourceAndRelation.ObjectId, tpl.Subject) + checksToDispatch.addForRelationship(rel) + subjectsByResourceID.Add(rel.Resource.ObjectID, rel.Subject) } - it.Close() // Convert the subjects into batched requests. toDispatch := checksToDispatch.dispatchChunks(crc.dispatchChunkSize) @@ -735,7 +727,7 @@ func checkIntersectionTupleToUserset( } // Create a membership set per-subject-type, representing the membership for each of the dispatched subjects. - resultsByDispatchedSubject := map[relationRef]*MembershipSet{} + resultsByDispatchedSubject := map[tuple.RelationReference]*MembershipSet{} combinedMetadata := emptyMetadata for _, result := range chunkResults { if result.Err != nil { @@ -767,14 +759,14 @@ func checkIntersectionTupleToUserset( // was found for each. If any are not found, then the resource ID is not a member. // We also collect up the caveats for each subject, as they will be added to the final result. for _, subject := range subjects { - subjectTypeKey := relationRef{subject.Namespace, subject.Relation} + subjectTypeKey := subject.RelationReference() results, ok := resultsByDispatchedSubject[subjectTypeKey] if !ok { hasAllSubjects = false break } - hasMembership, caveat := results.GetResourceID(subject.ObjectId) + hasMembership, caveat := results.GetResourceID(subject.ObjectID) if !hasMembership { hasAllSubjects = false break @@ -785,7 +777,7 @@ func checkIntersectionTupleToUserset( } // Add any caveats on the subject from the starting relationship(s) as well. - resourceIDAndCaveats := checksToDispatch.mappingsForSubject(subject.Namespace, subject.ObjectId, subject.Relation) + resourceIDAndCaveats := checksToDispatch.mappingsForSubject(subject.ObjectType, subject.ObjectID, subject.Relation) for _, riac := range resourceIDAndCaveats { if riac.caveat != nil { caveats = append(caveats, wrapCaveat(riac.caveat)) @@ -821,7 +813,7 @@ func checkTupleToUserset[T relation]( crc.parentReq.ResourceRelation.Namespace, ttu.GetTupleset().GetRelation(), ttu.GetComputedUserset().Relation, - crc.parentReq.Subject, + tuple.FromCoreObjectAndRelation(crc.parentReq.Subject), ) if !ok { continue @@ -851,16 +843,14 @@ func checkTupleToUserset[T relation]( if err != nil { return checkResultError(NewCheckFailureErr(err), emptyMetadata) } - defer it.Close() checksToDispatch := newCheckDispatchSet() - for tpl := it.Next(); tpl != nil; tpl = it.Next() { - if it.Err() != nil { - return checkResultError(NewCheckFailureErr(it.Err()), emptyMetadata) + for rel, err := range it { + if err != nil { + return checkResultError(NewCheckFailureErr(err), emptyMetadata) } - checksToDispatch.addForRelationship(tpl) + checksToDispatch.addForRelationship(rel) } - it.Close() toDispatch := checksToDispatch.dispatchChunks(crc.dispatchChunkSize) return combineWithComputedHints(union( diff --git a/internal/graph/checkdispatchset.go b/internal/graph/checkdispatchset.go index 331157e580..ed3f3cb898 100644 --- a/internal/graph/checkdispatchset.go +++ b/internal/graph/checkdispatchset.go @@ -7,6 +7,7 @@ import ( "github.com/authzed/spicedb/pkg/genutil/slicez" core "github.com/authzed/spicedb/pkg/proto/core/v1" "github.com/authzed/spicedb/pkg/spiceerrors" + "github.com/authzed/spicedb/pkg/tuple" ) // checkDispatchSet is the set of subjects over which check will need to dispatch @@ -15,17 +16,17 @@ type checkDispatchSet struct { // bySubjectType is a map from the type of subject to the set of subjects of that type // over which to dispatch, along with information indicating whether caveats are present // for that chunk. - bySubjectType map[relationRef]map[string]bool + bySubjectType map[tuple.RelationReference]map[string]bool // bySubject is a map from the subject to the set of resources for which the subject // has a relationship, along with the caveats that apply to that relationship. - bySubject *mapz.MultiMap[subjectRef, resourceIDAndCaveat] + bySubject *mapz.MultiMap[tuple.ObjectAndRelation, resourceIDAndCaveat] } // checkDispatchChunk is a chunk of subjects over which to dispatch a check operation. type checkDispatchChunk struct { // resourceType is the type of the subjects in this chunk. - resourceType relationRef + resourceType tuple.RelationReference // resourceIds is the set of subjects in this chunk. resourceIds []string @@ -55,56 +56,37 @@ type resourceIDAndCaveat struct { caveat *core.ContextualizedCaveat } -// relationRef is a tuple of a namespace and a relation. -type relationRef struct { - namespace string - relation string -} - -// subjectRef is a tuple of a namespace, an object ID, and a relation. -type subjectRef struct { - namespace string - objectID string - relation string -} - // newCheckDispatchSet creates and returns a new checkDispatchSet. func newCheckDispatchSet() *checkDispatchSet { return &checkDispatchSet{ - bySubjectType: map[relationRef]map[string]bool{}, - bySubject: mapz.NewMultiMap[subjectRef, resourceIDAndCaveat](), + bySubjectType: map[tuple.RelationReference]map[string]bool{}, + bySubject: mapz.NewMultiMap[tuple.ObjectAndRelation, resourceIDAndCaveat](), } } // Add adds the specified ObjectAndRelation to the set. -func (s *checkDispatchSet) addForRelationship(tpl *core.RelationTuple) { +func (s *checkDispatchSet) addForRelationship(rel tuple.Relationship) { // Add an entry for the subject pointing to the resource ID and caveat for the subject. riac := resourceIDAndCaveat{ - resourceID: tpl.ResourceAndRelation.ObjectId, - caveat: tpl.Caveat, - } - subjectRef := subjectRef{ - namespace: tpl.Subject.Namespace, - objectID: tpl.Subject.ObjectId, - relation: tpl.Subject.Relation, + resourceID: rel.Resource.ObjectID, + caveat: rel.OptionalCaveat, } - s.bySubject.Add(subjectRef, riac) + s.bySubject.Add(rel.Subject, riac) // Add the subject ID to the map of subjects for the type of subject. siac := subjectIDAndHasCaveat{ - objectID: tpl.Subject.ObjectId, - hasIncomingCaveats: tpl.Caveat != nil && tpl.Caveat.CaveatName != "", + objectID: rel.Subject.ObjectID, + hasIncomingCaveats: rel.OptionalCaveat != nil && rel.OptionalCaveat.CaveatName != "", } - subjectTypeRef := relationRef{namespace: tpl.Subject.Namespace, relation: tpl.Subject.Relation} - subjectIDsForType, ok := s.bySubjectType[subjectTypeRef] + subjectIDsForType, ok := s.bySubjectType[rel.Subject.RelationReference()] if !ok { subjectIDsForType = make(map[string]bool) - s.bySubjectType[subjectTypeRef] = subjectIDsForType + s.bySubjectType[rel.Subject.RelationReference()] = subjectIDsForType } // If a caveat exists for the subject ID in any branch, the whole branch is considered caveated. - subjectIDsForType[tpl.Subject.ObjectId] = siac.hasIncomingCaveats || subjectIDsForType[tpl.Subject.ObjectId] + subjectIDsForType[rel.Subject.ObjectID] = siac.hasIncomingCaveats || subjectIDsForType[rel.Subject.ObjectID] } func (s *checkDispatchSet) dispatchChunks(dispatchChunkSize uint16) []checkDispatchChunk { @@ -156,11 +138,7 @@ func (s *checkDispatchSet) dispatchChunks(dispatchChunkSize uint16) []checkDispa // subject and any of its resources. The returned caveats include the resource ID of the resource // that the subject has a relationship with. func (s *checkDispatchSet) mappingsForSubject(subjectType string, subjectObjectID string, subjectRelation string) []resourceIDAndCaveat { - results, ok := s.bySubject.Get(subjectRef{ - namespace: subjectType, - objectID: subjectObjectID, - relation: subjectRelation, - }) + results, ok := s.bySubject.Get(tuple.ONR(subjectType, subjectObjectID, subjectRelation)) spiceerrors.DebugAssert(func() bool { return ok }, "no caveats found for subject %s:%s:%s", subjectType, subjectObjectID, subjectRelation) return results } diff --git a/internal/graph/checkdispatchset_test.go b/internal/graph/checkdispatchset_test.go index cefbd17aed..04aafdcd34 100644 --- a/internal/graph/checkdispatchset_test.go +++ b/internal/graph/checkdispatchset_test.go @@ -7,7 +7,6 @@ import ( "github.com/stretchr/testify/require" "github.com/authzed/spicedb/internal/caveats" - core "github.com/authzed/spicedb/pkg/proto/core/v1" "github.com/authzed/spicedb/pkg/tuple" ) @@ -16,14 +15,14 @@ var caveatForTesting = caveats.CaveatForTesting func TestCheckDispatchSet(t *testing.T) { tcs := []struct { name string - relationships []*core.RelationTuple + relationships []tuple.Relationship dispatchChunkSize uint16 expectedChunks []checkDispatchChunk expectedMappings map[string][]resourceIDAndCaveat }{ { "basic", - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:somedoc#viewer@group:1#member"), tuple.MustParse("document:somedoc#viewer@group:2#member"), tuple.MustParse("document:somedoc#viewer@group:3#member"), @@ -31,7 +30,7 @@ func TestCheckDispatchSet(t *testing.T) { 100, []checkDispatchChunk{ { - resourceType: relationRef{namespace: "group", relation: "member"}, + resourceType: tuple.RelationReference{ObjectType: "group", Relation: "member"}, resourceIds: []string{"1", "2", "3"}, hasIncomingCaveats: false, }, @@ -50,7 +49,7 @@ func TestCheckDispatchSet(t *testing.T) { }, { "basic chunking", - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:somedoc#viewer@group:1#member"), tuple.MustParse("document:somedoc#viewer@group:2#member"), tuple.MustParse("document:somedoc#viewer@group:3#member"), @@ -58,12 +57,12 @@ func TestCheckDispatchSet(t *testing.T) { 2, []checkDispatchChunk{ { - resourceType: relationRef{namespace: "group", relation: "member"}, + resourceType: tuple.RelationReference{ObjectType: "group", Relation: "member"}, resourceIds: []string{"1", "2"}, hasIncomingCaveats: false, }, { - resourceType: relationRef{namespace: "group", relation: "member"}, + resourceType: tuple.RelationReference{ObjectType: "group", Relation: "member"}, resourceIds: []string{"3"}, hasIncomingCaveats: false, }, @@ -82,7 +81,7 @@ func TestCheckDispatchSet(t *testing.T) { }, { "different subject types", - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:somedoc#viewer@group:1#member"), tuple.MustParse("document:somedoc#viewer@group:2#member"), tuple.MustParse("document:somedoc#viewer@group:3#member"), @@ -93,12 +92,12 @@ func TestCheckDispatchSet(t *testing.T) { 100, []checkDispatchChunk{ { - resourceType: relationRef{namespace: "group", relation: "member"}, + resourceType: tuple.RelationReference{ObjectType: "group", Relation: "member"}, resourceIds: []string{"1", "2", "3"}, hasIncomingCaveats: false, }, { - resourceType: relationRef{namespace: "anothertype", relation: "member"}, + resourceType: tuple.RelationReference{ObjectType: "anothertype", Relation: "member"}, resourceIds: []string{"1", "2", "3"}, hasIncomingCaveats: false, }, @@ -126,7 +125,7 @@ func TestCheckDispatchSet(t *testing.T) { }, { "different subject types mixed", - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:somedoc#viewer@group:1#member"), tuple.MustParse("document:somedoc#viewer@anothertype:1#member"), tuple.MustParse("document:somedoc#viewer@anothertype:2#member"), @@ -137,12 +136,12 @@ func TestCheckDispatchSet(t *testing.T) { 100, []checkDispatchChunk{ { - resourceType: relationRef{namespace: "group", relation: "member"}, + resourceType: tuple.RelationReference{ObjectType: "group", Relation: "member"}, resourceIds: []string{"1", "2", "3"}, hasIncomingCaveats: false, }, { - resourceType: relationRef{namespace: "anothertype", relation: "member"}, + resourceType: tuple.RelationReference{ObjectType: "anothertype", Relation: "member"}, resourceIds: []string{"1", "2", "3"}, hasIncomingCaveats: false, }, @@ -170,7 +169,7 @@ func TestCheckDispatchSet(t *testing.T) { }, { "different subject types with chunking", - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:somedoc#viewer@group:1#member"), tuple.MustParse("document:somedoc#viewer@group:2#member"), tuple.MustParse("document:somedoc#viewer@group:3#member"), @@ -181,22 +180,22 @@ func TestCheckDispatchSet(t *testing.T) { 2, []checkDispatchChunk{ { - resourceType: relationRef{namespace: "group", relation: "member"}, + resourceType: tuple.RelationReference{ObjectType: "group", Relation: "member"}, resourceIds: []string{"1", "2"}, hasIncomingCaveats: false, }, { - resourceType: relationRef{namespace: "group", relation: "member"}, + resourceType: tuple.RelationReference{ObjectType: "group", Relation: "member"}, resourceIds: []string{"3"}, hasIncomingCaveats: false, }, { - resourceType: relationRef{namespace: "anothertype", relation: "member"}, + resourceType: tuple.RelationReference{ObjectType: "anothertype", Relation: "member"}, resourceIds: []string{"1", "2"}, hasIncomingCaveats: false, }, { - resourceType: relationRef{namespace: "anothertype", relation: "member"}, + resourceType: tuple.RelationReference{ObjectType: "anothertype", Relation: "member"}, resourceIds: []string{"3"}, hasIncomingCaveats: false, }, @@ -224,7 +223,7 @@ func TestCheckDispatchSet(t *testing.T) { }, { "some caveated members", - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:somedoc#viewer@group:1#member[somecaveat]"), tuple.MustParse("document:somedoc#viewer@group:2#member"), tuple.MustParse("document:somedoc#viewer@group:3#member"), @@ -232,7 +231,7 @@ func TestCheckDispatchSet(t *testing.T) { 100, []checkDispatchChunk{ { - resourceType: relationRef{namespace: "group", relation: "member"}, + resourceType: tuple.RelationReference{ObjectType: "group", Relation: "member"}, resourceIds: []string{"1", "2", "3"}, hasIncomingCaveats: true, }, @@ -251,7 +250,7 @@ func TestCheckDispatchSet(t *testing.T) { }, { "caveated members combined when chunking", - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:somedoc#viewer@group:1#member[somecaveat]"), tuple.MustParse("document:somedoc#viewer@group:2#member"), tuple.MustParse("document:somedoc#viewer@group:3#member"), @@ -260,12 +259,12 @@ func TestCheckDispatchSet(t *testing.T) { 2, []checkDispatchChunk{ { - resourceType: relationRef{namespace: "group", relation: "member"}, + resourceType: tuple.RelationReference{ObjectType: "group", Relation: "member"}, resourceIds: []string{"2", "3"}, hasIncomingCaveats: false, }, { - resourceType: relationRef{namespace: "group", relation: "member"}, + resourceType: tuple.RelationReference{ObjectType: "group", Relation: "member"}, resourceIds: []string{"1", "4"}, hasIncomingCaveats: true, }, @@ -287,7 +286,7 @@ func TestCheckDispatchSet(t *testing.T) { }, { "different resources leading to the same subject", - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:somedoc#viewer@group:1#member"), tuple.MustParse("document:anotherdoc#viewer@group:1#member"), tuple.MustParse("document:somedoc#viewer@group:2#member"), @@ -296,7 +295,7 @@ func TestCheckDispatchSet(t *testing.T) { 2, []checkDispatchChunk{ { - resourceType: relationRef{namespace: "group", relation: "member"}, + resourceType: tuple.RelationReference{ObjectType: "group", Relation: "member"}, resourceIds: []string{"1", "2"}, hasIncomingCaveats: false, }, @@ -314,7 +313,7 @@ func TestCheckDispatchSet(t *testing.T) { }, { "different resources leading to the same subject with caveats", - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:somedoc#viewer@group:1#member[somecaveat]"), tuple.MustParse("document:anotherdoc#viewer@group:1#member"), tuple.MustParse("document:somedoc#viewer@group:2#member"), @@ -323,7 +322,7 @@ func TestCheckDispatchSet(t *testing.T) { 2, []checkDispatchChunk{ { - resourceType: relationRef{namespace: "group", relation: "member"}, + resourceType: tuple.RelationReference{ObjectType: "group", Relation: "member"}, resourceIds: []string{"1", "2"}, hasIncomingCaveats: true, }, @@ -341,7 +340,7 @@ func TestCheckDispatchSet(t *testing.T) { }, { "different resource leading to the same subject with caveats", - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:anotherdoc#viewer@group:1#member"), tuple.MustParse("document:thirddoc#viewer@group:1#member"), tuple.MustParse("document:somedoc#viewer@group:1#member[somecaveat]"), @@ -349,7 +348,7 @@ func TestCheckDispatchSet(t *testing.T) { 2, []checkDispatchChunk{ { - resourceType: relationRef{namespace: "group", relation: "member"}, + resourceType: tuple.RelationReference{ObjectType: "group", Relation: "member"}, resourceIds: []string{"1"}, hasIncomingCaveats: true, }, @@ -379,10 +378,11 @@ func TestCheckDispatchSet(t *testing.T) { require.ElementsMatch(t, tc.expectedChunks, chunks, "difference in expected chunks. found: %v", chunks) for subjectString, expectedMappings := range tc.expectedMappings { - parsed := tuple.ParseSubjectONR(subjectString) + parsed, err := tuple.ParseSubjectONR(subjectString) + require.NoError(t, err) require.NotNil(t, parsed) - mappings := set.mappingsForSubject(parsed.Namespace, parsed.ObjectId, parsed.Relation) + mappings := set.mappingsForSubject(parsed.ObjectType, parsed.ObjectID, parsed.Relation) require.ElementsMatch(t, expectedMappings, mappings) } }) diff --git a/internal/graph/checkingresourcestream.go b/internal/graph/checkingresourcestream.go index 62769220a5..86905532ef 100644 --- a/internal/graph/checkingresourcestream.go +++ b/internal/graph/checkingresourcestream.go @@ -14,6 +14,7 @@ import ( "github.com/authzed/spicedb/pkg/genutil/mapz" v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" "github.com/authzed/spicedb/pkg/spiceerrors" + "github.com/authzed/spicedb/pkg/tuple" ) // possibleResource is a resource that was returned by reachable resources and, after processing, @@ -461,8 +462,8 @@ func (crs *checkingResourceStream) runProcess(alwaysProcess bool) (bool, error) crs.ctx, crs.checker, computed.CheckParameters{ - ResourceType: crs.req.ObjectRelation, - Subject: crs.req.Subject, + ResourceType: tuple.FromCoreRelationReference(crs.req.ObjectRelation), + Subject: tuple.FromCoreObjectAndRelation(crs.req.Subject), CaveatContext: crs.req.Context.AsMap(), AtRevision: crs.req.Revision, MaximumDepth: crs.req.Metadata.DepthRemaining, diff --git a/internal/graph/computed/computecheck.go b/internal/graph/computed/computecheck.go index cf0860d236..a3d86cb916 100644 --- a/internal/graph/computed/computecheck.go +++ b/internal/graph/computed/computecheck.go @@ -8,9 +8,9 @@ import ( datastoremw "github.com/authzed/spicedb/internal/middleware/datastore" "github.com/authzed/spicedb/pkg/datastore" "github.com/authzed/spicedb/pkg/genutil/slicez" - core "github.com/authzed/spicedb/pkg/proto/core/v1" v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" "github.com/authzed/spicedb/pkg/spiceerrors" + "github.com/authzed/spicedb/pkg/tuple" ) // DebugOption defines the various debug level options for Checks. @@ -40,8 +40,8 @@ const ( // CheckParameters are the parameters for the ComputeCheck call. *All* are required. type CheckParameters struct { - ResourceType *core.RelationReference - Subject *core.ObjectAndRelation + ResourceType tuple.RelationReference + Subject tuple.ObjectAndRelation CaveatContext map[string]any AtRevision datastore.Revision MaximumDepth uint32 @@ -113,10 +113,10 @@ func computeCheck(ctx context.Context, // TODO(jschorr): Should we make this run in parallel via the preloadedTaskRunner? _, err = slicez.ForEachChunkUntil(resourceIDs, dispatchChunkSize, func(resourceIDsToCheck []string) (bool, error) { checkResult, err := d.DispatchCheck(ctx, &v1.DispatchCheckRequest{ - ResourceRelation: params.ResourceType, + ResourceRelation: params.ResourceType.ToCoreRR(), ResourceIds: resourceIDsToCheck, ResultsSetting: setting, - Subject: params.Subject, + Subject: params.Subject.ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: params.AtRevision.String(), DepthRemaining: params.MaximumDepth, diff --git a/internal/graph/computed/computecheck_test.go b/internal/graph/computed/computecheck_test.go index 6a3c39dc54..1a2f4c430b 100644 --- a/internal/graph/computed/computecheck_test.go +++ b/internal/graph/computed/computecheck_test.go @@ -24,7 +24,7 @@ import ( ) type caveatedUpdate struct { - Operation core.RelationTupleUpdate_Operation + Operation tuple.UpdateOperation tuple string caveatName string context map[string]any @@ -71,18 +71,19 @@ func TestComputeCheckWithCaveats(t *testing.T) { } `, []caveatedUpdate{ - {core.RelationTupleUpdate_CREATE, "organization:someorg#admin@user:sarah", "testcaveat", nil}, - {core.RelationTupleUpdate_CREATE, "organization:someorg#admin@user:john", "testcaveat", map[string]any{"somecondition": "42", "somebool": true}}, - {core.RelationTupleUpdate_CREATE, "organization:someorg#admin@user:jane", "", nil}, - {core.RelationTupleUpdate_CREATE, "document:foo#org@organization:someorg", "anothercaveat", nil}, - {core.RelationTupleUpdate_CREATE, "document:bar#org@organization:someorg", "", nil}, - {core.RelationTupleUpdate_CREATE, "document:foo#editor@user:vic", "testcaveat", map[string]any{"somecondition": "42", "somebool": true}}, - {core.RelationTupleUpdate_CREATE, "document:foo#viewer@user:vic", "testcaveat", map[string]any{"somecondition": "42", "somebool": true}}, - {core.RelationTupleUpdate_CREATE, "document:foo#viewer@user:blippy", "testcaveat", map[string]any{"somecondition": "42", "somebool": false}}, - {core.RelationTupleUpdate_CREATE, "document:foo#viewer@user:noa", "testcaveat", map[string]any{"somecondition": "42", "somebool": false}}, - {core.RelationTupleUpdate_CREATE, "document:foo#editor@user:noa", "testcaveat", map[string]any{"somecondition": "42", "somebool": false}}, - {core.RelationTupleUpdate_CREATE, "document:foo#editor@user:wayne", "invalid", nil}, + {tuple.UpdateOperationCreate, "organization:someorg#admin@user:sarah", "testcaveat", nil}, + {tuple.UpdateOperationCreate, "organization:someorg#admin@user:john", "testcaveat", map[string]any{"somecondition": "42", "somebool": true}}, + {tuple.UpdateOperationCreate, "organization:someorg#admin@user:jane", "", nil}, + {tuple.UpdateOperationCreate, "document:foo#org@organization:someorg", "anothercaveat", nil}, + {tuple.UpdateOperationCreate, "document:bar#org@organization:someorg", "", nil}, + {tuple.UpdateOperationCreate, "document:foo#editor@user:vic", "testcaveat", map[string]any{"somecondition": "42", "somebool": true}}, + {tuple.UpdateOperationCreate, "document:foo#viewer@user:vic", "testcaveat", map[string]any{"somecondition": "42", "somebool": true}}, + {tuple.UpdateOperationCreate, "document:foo#viewer@user:blippy", "testcaveat", map[string]any{"somecondition": "42", "somebool": false}}, + {tuple.UpdateOperationCreate, "document:foo#viewer@user:noa", "testcaveat", map[string]any{"somecondition": "42", "somebool": false}}, + {tuple.UpdateOperationCreate, "document:foo#editor@user:noa", "testcaveat", map[string]any{"somecondition": "42", "somebool": false}}, + {tuple.UpdateOperationCreate, "document:foo#editor@user:wayne", "invalid", nil}, }, + []check{ { "document:foo#view@user:sarah", @@ -194,7 +195,7 @@ func TestComputeCheckWithCaveats(t *testing.T) { } `, []caveatedUpdate{ - {core.RelationTupleUpdate_CREATE, "document:foo#viewer@user:tom", "testcaveat", map[string]any{ + {tuple.UpdateOperationCreate, "document:foo#viewer@user:tom", "testcaveat", map[string]any{ "somecondition": 41, // not allowed }}, }, @@ -237,13 +238,13 @@ func TestComputeCheckWithCaveats(t *testing.T) { `, []caveatedUpdate{ { - core.RelationTupleUpdate_CREATE, + tuple.UpdateOperationCreate, "document:foo#viewer@user:tom", "viewcaveat", nil, }, { - core.RelationTupleUpdate_CREATE, + tuple.UpdateOperationCreate, "document:foo#editor@user:tom", "editcaveat", nil, @@ -302,13 +303,13 @@ func TestComputeCheckWithCaveats(t *testing.T) { `, []caveatedUpdate{ { - core.RelationTupleUpdate_CREATE, + tuple.UpdateOperationCreate, "document:foo#viewer@user:tom", "viewcaveat", nil, }, { - core.RelationTupleUpdate_CREATE, + tuple.UpdateOperationCreate, "document:foo#banned@user:tom", "bannedcaveat", nil, @@ -371,25 +372,25 @@ func TestComputeCheckWithCaveats(t *testing.T) { `, []caveatedUpdate{ { - core.RelationTupleUpdate_CREATE, + tuple.UpdateOperationCreate, "repository:foobar#owner@organization:myorg", "", nil, }, { - core.RelationTupleUpdate_CREATE, + tuple.UpdateOperationCreate, "organization:myorg#members@user:johndoe", "", nil, }, { - core.RelationTupleUpdate_CREATE, + tuple.UpdateOperationCreate, "repository:foobar#reader@user:johndoe", "", nil, }, { - core.RelationTupleUpdate_CREATE, + tuple.UpdateOperationCreate, "organization:myorg#ip_allowlist_policy@organization:myorg#members", "ip_allowlist", map[string]any{ @@ -448,7 +449,7 @@ func TestComputeCheckWithCaveats(t *testing.T) { `, []caveatedUpdate{ { - core.RelationTupleUpdate_CREATE, + tuple.UpdateOperationCreate, "group:ui_apps#member@application:frontend_app", "attributes_match", map[string]any{ @@ -456,7 +457,7 @@ func TestComputeCheckWithCaveats(t *testing.T) { }, }, { - core.RelationTupleUpdate_CREATE, + tuple.UpdateOperationCreate, "group:backend_apps#member@application:backend_app", "attributes_match", map[string]any{ @@ -540,7 +541,7 @@ func TestComputeCheckWithCaveats(t *testing.T) { `, []caveatedUpdate{ { - core.RelationTupleUpdate_CREATE, + tuple.UpdateOperationCreate, "resource:foo#creation_policy@root:root#actors", "created_before", map[string]any{ @@ -548,7 +549,7 @@ func TestComputeCheckWithCaveats(t *testing.T) { }, }, { - core.RelationTupleUpdate_CREATE, + tuple.UpdateOperationCreate, "root:root#actors@actor:johndoe", "", nil, @@ -589,7 +590,7 @@ func TestComputeCheckWithCaveats(t *testing.T) { definition user {}`, []caveatedUpdate{ { - core.RelationTupleUpdate_CREATE, + tuple.UpdateOperationCreate, "resource:foo#reader@user:sarah", "not_expired", map[string]any{ @@ -598,7 +599,7 @@ func TestComputeCheckWithCaveats(t *testing.T) { }, }, { - core.RelationTupleUpdate_CREATE, + tuple.UpdateOperationCreate, "resource:foo#reader@user:john", "not_expired", map[string]any{ @@ -642,13 +643,13 @@ func TestComputeCheckWithCaveats(t *testing.T) { }`, []caveatedUpdate{ { - core.RelationTupleUpdate_CREATE, + tuple.UpdateOperationCreate, "user:son#dependent_of@user:father", "", nil, }, { - core.RelationTupleUpdate_CREATE, + tuple.UpdateOperationCreate, "claim:broken_leg#dependent_of@user:son#dependent_of", "legal_guardian", map[string]any{ @@ -657,13 +658,13 @@ func TestComputeCheckWithCaveats(t *testing.T) { }, }, { - core.RelationTupleUpdate_CREATE, + tuple.UpdateOperationCreate, "user:daughter#dependent_of@user:father", "", nil, }, { - core.RelationTupleUpdate_CREATE, + tuple.UpdateOperationCreate, "claim:broken_arm#dependent_of@user:daughter#dependent_of", "legal_guardian", map[string]any{ @@ -672,7 +673,7 @@ func TestComputeCheckWithCaveats(t *testing.T) { }, }, { - core.RelationTupleUpdate_CREATE, + tuple.UpdateOperationCreate, "claim:sensitive_matter#dependent_of@user:daughter#dependent_of", "legal_guardian", map[string]any{ @@ -720,7 +721,7 @@ func TestComputeCheckWithCaveats(t *testing.T) { } `, []caveatedUpdate{ - {core.RelationTupleUpdate_CREATE, "document:foo#viewer@user:sarah", "testcaveat", nil}, + {tuple.UpdateOperationCreate, "document:foo#viewer@user:sarah", "testcaveat", nil}, }, []check{ { @@ -758,7 +759,7 @@ func TestComputeCheckWithCaveats(t *testing.T) { permission view = viewer }`, []caveatedUpdate{ - {core.RelationTupleUpdate_CREATE, "document:foo#viewer@user:sarah", "testcaveat", nil}, + {tuple.UpdateOperationCreate, "document:foo#viewer@user:sarah", "testcaveat", nil}, }, []check{ { @@ -821,17 +822,14 @@ func TestComputeCheckWithCaveats(t *testing.T) { result, _, err := computed.ComputeCheck(ctx, dispatch, computed.CheckParameters{ - ResourceType: &core.RelationReference{ - Namespace: rel.ResourceAndRelation.Namespace, - Relation: rel.ResourceAndRelation.Relation, - }, + ResourceType: rel.Resource.RelationReference(), Subject: rel.Subject, CaveatContext: r.context, AtRevision: revision, MaximumDepth: 50, DebugOption: computed.BasicDebuggingEnabled, }, - rel.ResourceAndRelation.ObjectId, + rel.Resource.ObjectID, 100, ) @@ -866,11 +864,8 @@ func TestComputeCheckError(t *testing.T) { _, _, err = computed.ComputeCheck(ctx, dispatch, computed.CheckParameters{ - ResourceType: &core.RelationReference{ - Namespace: "a", - Relation: "b", - }, - Subject: &core.ObjectAndRelation{}, + ResourceType: tuple.RR("a", "b"), + Subject: tuple.ONR("c", "d", "..."), CaveatContext: nil, AtRevision: datastore.NoRevision, MaximumDepth: 50, @@ -902,12 +897,12 @@ func TestComputeBulkCheck(t *testing.T) { permission view = viewer } `, []caveatedUpdate{ - {core.RelationTupleUpdate_CREATE, "document:direct#viewer@user:tom", "", nil}, - {core.RelationTupleUpdate_CREATE, "document:first#viewer@user:tom", "somecaveat", map[string]any{ + {tuple.UpdateOperationCreate, "document:direct#viewer@user:tom", "", nil}, + {tuple.UpdateOperationCreate, "document:first#viewer@user:tom", "somecaveat", map[string]any{ "somecondition": 42, }}, - {core.RelationTupleUpdate_CREATE, "document:second#viewer@user:tom", "somecaveat", map[string]any{}}, - {core.RelationTupleUpdate_CREATE, "document:third#viewer@user:tom", "somecaveat", map[string]any{ + {tuple.UpdateOperationCreate, "document:second#viewer@user:tom", "somecaveat", map[string]any{}}, + {tuple.UpdateOperationCreate, "document:third#viewer@user:tom", "somecaveat", map[string]any{ "somecondition": 32, }}, }) @@ -915,15 +910,8 @@ func TestComputeBulkCheck(t *testing.T) { resp, _, err := computed.ComputeBulkCheck(ctx, dispatch, computed.CheckParameters{ - ResourceType: &core.RelationReference{ - Namespace: "document", - Relation: "view", - }, - Subject: &core.ObjectAndRelation{ - Namespace: "user", - ObjectId: "tom", - Relation: "...", - }, + ResourceType: tuple.RR("document", "view"), + Subject: tuple.ONR("user", "tom", "..."), CaveatContext: nil, AtRevision: revision, MaximumDepth: 50, @@ -958,11 +946,11 @@ func writeCaveatedTuples(ctx context.Context, _ *testing.T, ds datastore.Datasto return err } - var rtu []*core.RelationTupleUpdate + var rtu []tuple.RelationshipUpdate for _, updt := range updates { - rtu = append(rtu, &core.RelationTupleUpdate{ - Operation: updt.Operation, - Tuple: caveatedRelationTuple(updt.tuple, updt.caveatName, updt.context), + rtu = append(rtu, tuple.RelationshipUpdate{ + Operation: updt.Operation, + Relationship: caveatedRelationTuple(updt.tuple, updt.caveatName, updt.context), }) } @@ -970,14 +958,14 @@ func writeCaveatedTuples(ctx context.Context, _ *testing.T, ds datastore.Datasto }) } -func caveatedRelationTuple(relationTuple string, caveatName string, context map[string]any) *core.RelationTuple { +func caveatedRelationTuple(relationTuple string, caveatName string, context map[string]any) tuple.Relationship { c := tuple.MustParse(relationTuple) strct, err := structpb.NewStruct(context) if err != nil { panic(err) } if caveatName != "" { - c.Caveat = &core.ContextualizedCaveat{ + c.OptionalCaveat = &core.ContextualizedCaveat{ CaveatName: caveatName, Context: strct, } diff --git a/internal/graph/cursors.go b/internal/graph/cursors.go index d73fd5bb8d..29fdaae32c 100644 --- a/internal/graph/cursors.go +++ b/internal/graph/cursors.go @@ -161,7 +161,7 @@ func withDatastoreCursorInCursor[T any, Q any]( var datastoreCursor options.Cursor datastoreCursorString, _ := ci.headSectionValue() if datastoreCursorString != "" { - datastoreCursor = tuple.MustParse(datastoreCursorString) + datastoreCursor = options.ToCursor(tuple.MustParse(datastoreCursorString)) } if ci.limits.hasExhaustedLimit() { @@ -185,7 +185,13 @@ func withDatastoreCursorInCursor[T any, Q any]( getItemCursor := func(taskIndex int) (cursorInformation, error) { // Create an updated cursor referencing the current item's cursor, so that any items returned know to resume from this point. - currentCursor, err := ci.withOutgoingSection(tuple.StringWithoutCaveat(itemsToBeProcessed[taskIndex].cursor)) + cursorRel := options.ToRelationship(itemsToBeProcessed[taskIndex].cursor) + cursorSection := "" + if cursorRel != nil { + cursorSection = tuple.StringWithoutCaveat(*cursorRel) + } + + currentCursor, err := ci.withOutgoingSection(cursorSection) if err != nil { return currentCursor, err } diff --git a/internal/graph/cursors_test.go b/internal/graph/cursors_test.go index 84d5e0259f..4b3eef1b73 100644 --- a/internal/graph/cursors_test.go +++ b/internal/graph/cursors_test.go @@ -323,9 +323,9 @@ func TestWithDatastoreCursorInCursor(t *testing.T) { 5, func(queryCursor options.Cursor) ([]itemAndPostCursor[int], error) { return []itemAndPostCursor[int]{ - {1, tuple.MustParse("document:foo#viewer@user:tom")}, - {2, tuple.MustParse("document:foo#viewer@user:sarah")}, - {3, tuple.MustParse("document:foo#viewer@user:fred")}, + {1, options.ToCursor(tuple.MustParse("document:foo#viewer@user:tom"))}, + {2, options.ToCursor(tuple.MustParse("document:foo#viewer@user:sarah"))}, + {3, options.ToCursor(tuple.MustParse("document:foo#viewer@user:fred"))}, }, nil }, func(ctx context.Context, cc cursorInformation, item int, stream dispatch.Stream[int]) error { @@ -358,17 +358,17 @@ func TestWithDatastoreCursorInCursorWithStartingCursor(t *testing.T) { lock := sync.Mutex{} parentStream := dispatch.NewCollectingDispatchStream[int](context.Background()) - err = withDatastoreCursorInCursor[int, int]( + err = withDatastoreCursorInCursor( context.Background(), ci, parentStream, 5, func(queryCursor options.Cursor) ([]itemAndPostCursor[int], error) { - require.Equal(t, "", tuple.MustString(queryCursor)) + require.Nil(t, queryCursor) return []itemAndPostCursor[int]{ - {2, tuple.MustParse("document:foo#viewer@user:sarah")}, - {3, tuple.MustParse("document:foo#viewer@user:fred")}, + {2, options.ToCursor(tuple.MustParse("document:foo#viewer@user:sarah"))}, + {3, options.ToCursor(tuple.MustParse("document:foo#viewer@user:fred"))}, }, nil }, func(ctx context.Context, cc cursorInformation, item int, stream dispatch.Stream[int]) error { diff --git a/internal/graph/expand.go b/internal/graph/expand.go index 937486d921..09bef7340c 100644 --- a/internal/graph/expand.go +++ b/internal/graph/expand.go @@ -14,6 +14,7 @@ import ( core "github.com/authzed/spicedb/pkg/proto/core/v1" v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" "github.com/authzed/spicedb/pkg/spiceerrors" + "github.com/authzed/spicedb/pkg/tuple" ) // NewConcurrentExpander creates an instance of ConcurrentExpander @@ -66,27 +67,26 @@ func (ce *ConcurrentExpander) expandDirect( resultChan <- expandResultError(NewExpansionFailureErr(err), emptyMetadata) return } - defer it.Close() var foundNonTerminalUsersets []*core.DirectSubject var foundTerminalUsersets []*core.DirectSubject - for tpl := it.Next(); tpl != nil; tpl = it.Next() { - if it.Err() != nil { - resultChan <- expandResultError(NewExpansionFailureErr(it.Err()), emptyMetadata) + for rel, err := range it { + if err != nil { + resultChan <- expandResultError(NewExpansionFailureErr(err), emptyMetadata) return } ds := &core.DirectSubject{ - Subject: tpl.Subject, - CaveatExpression: caveats.CaveatAsExpr(tpl.Caveat), + Subject: rel.Subject.ToCoreONR(), + CaveatExpression: caveats.CaveatAsExpr(rel.OptionalCaveat), } - if tpl.Subject.Relation == Ellipsis { + + if rel.Subject.Relation == Ellipsis { foundTerminalUsersets = append(foundTerminalUsersets, ds) } else { foundNonTerminalUsersets = append(foundNonTerminalUsersets, ds) } } - it.Close() // If only shallow expansion was required, or there are no non-terminal subjects found, // nothing more to do. @@ -221,26 +221,26 @@ func (ce *ConcurrentExpander) dispatch(req ValidatedExpandRequest) ReduceableExp } } -func (ce *ConcurrentExpander) expandComputedUserset(ctx context.Context, req ValidatedExpandRequest, cu *core.ComputedUserset, tpl *core.RelationTuple) ReduceableExpandFunc { +func (ce *ConcurrentExpander) expandComputedUserset(ctx context.Context, req ValidatedExpandRequest, cu *core.ComputedUserset, rel *tuple.Relationship) ReduceableExpandFunc { log.Ctx(ctx).Trace().Str("relation", cu.Relation).Msg("computed userset") - var start *core.ObjectAndRelation + var start tuple.ObjectAndRelation if cu.Object == core.ComputedUserset_TUPLE_USERSET_OBJECT { - if tpl == nil { + if rel == nil { return expandError(spiceerrors.MustBugf("computed userset for tupleset without tuple")) } - start = tpl.Subject + start = rel.Subject } else if cu.Object == core.ComputedUserset_TUPLE_OBJECT { - if tpl != nil { - start = tpl.ResourceAndRelation + if rel != nil { + start = rel.Resource } else { - start = req.ResourceAndRelation + start = tuple.FromCoreObjectAndRelation(req.ResourceAndRelation) } } // Check if the target relation exists. If not, return nothing. ds := datastoremw.MustFromContext(ctx).SnapshotReader(req.Revision) - err := namespace.CheckNamespaceAndRelation(ctx, start.Namespace, cu.Relation, true, ds) + err := namespace.CheckNamespaceAndRelation(ctx, start.ObjectType, cu.Relation, true, ds) if err != nil { if errors.As(err, &namespace.ErrRelationNotFound{}) { return emptyExpansion(req.ResourceAndRelation) @@ -252,8 +252,8 @@ func (ce *ConcurrentExpander) expandComputedUserset(ctx context.Context, req Val return ce.dispatch(ValidatedExpandRequest{ &v1.DispatchExpandRequest{ ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: start.Namespace, - ObjectId: start.ObjectId, + Namespace: start.ObjectType, + ObjectId: start.ObjectID, Relation: cu.Relation, }, Metadata: decrementDepth(req.Metadata), @@ -283,19 +283,17 @@ func expandTupleToUserset[T relation]( resultChan <- expandResultError(NewExpansionFailureErr(err), emptyMetadata) return } - defer it.Close() var requestsToDispatch []ReduceableExpandFunc - for tpl := it.Next(); tpl != nil; tpl = it.Next() { - if it.Err() != nil { - resultChan <- expandResultError(NewExpansionFailureErr(it.Err()), emptyMetadata) + for rel, err := range it { + if err != nil { + resultChan <- expandResultError(NewExpansionFailureErr(err), emptyMetadata) return } - toDispatch := ce.expandComputedUserset(ctx, req, ttu.GetComputedUserset(), tpl) - requestsToDispatch = append(requestsToDispatch, decorateWithCaveatIfNecessary(toDispatch, caveats.CaveatAsExpr(tpl.Caveat))) + toDispatch := ce.expandComputedUserset(ctx, req, ttu.GetComputedUserset(), &rel) + requestsToDispatch = append(requestsToDispatch, decorateWithCaveatIfNecessary(toDispatch, caveats.CaveatAsExpr(rel.OptionalCaveat))) } - it.Close() resultChan <- expandFunc(ctx, req.ResourceAndRelation, requestsToDispatch) } diff --git a/internal/graph/hints/checkhints.go b/internal/graph/hints/checkhints.go index a1e05a7e19..1f60286569 100644 --- a/internal/graph/hints/checkhints.go +++ b/internal/graph/hints/checkhints.go @@ -4,24 +4,25 @@ import ( core "github.com/authzed/spicedb/pkg/proto/core/v1" v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" "github.com/authzed/spicedb/pkg/spiceerrors" + "github.com/authzed/spicedb/pkg/tuple" "github.com/authzed/spicedb/pkg/typesystem" ) // CheckHintForComputedUserset creates a CheckHint for a relation and a subject. -func CheckHintForComputedUserset(resourceType string, resourceID string, relation string, subject *core.ObjectAndRelation, result *v1.ResourceCheckResult) *v1.CheckHint { +func CheckHintForComputedUserset(resourceType string, resourceID string, relation string, subject tuple.ObjectAndRelation, result *v1.ResourceCheckResult) *v1.CheckHint { return &v1.CheckHint{ Resource: &core.ObjectAndRelation{ Namespace: resourceType, ObjectId: resourceID, Relation: relation, }, - Subject: subject, + Subject: subject.ToCoreONR(), Result: result, } } // CheckHintForArrow creates a CheckHint for an arrow and a subject. -func CheckHintForArrow(resourceType string, resourceID string, tuplesetRelation string, computedUsersetRelation string, subject *core.ObjectAndRelation, result *v1.ResourceCheckResult) *v1.CheckHint { +func CheckHintForArrow(resourceType string, resourceID string, tuplesetRelation string, computedUsersetRelation string, subject tuple.ObjectAndRelation, result *v1.ResourceCheckResult) *v1.CheckHint { return &v1.CheckHint{ Resource: &core.ObjectAndRelation{ Namespace: resourceType, @@ -29,18 +30,18 @@ func CheckHintForArrow(resourceType string, resourceID string, tuplesetRelation Relation: tuplesetRelation, }, TtuComputedUsersetRelation: computedUsersetRelation, - Subject: subject, + Subject: subject.ToCoreONR(), Result: result, } } // AsCheckHintForComputedUserset returns the resourceID if the checkHint is for the given relation and subject. -func AsCheckHintForComputedUserset(checkHint *v1.CheckHint, resourceType string, relationName string, subject *core.ObjectAndRelation) (string, bool) { +func AsCheckHintForComputedUserset(checkHint *v1.CheckHint, resourceType string, relationName string, subject tuple.ObjectAndRelation) (string, bool) { if checkHint.TtuComputedUsersetRelation != "" { return "", false } - if checkHint.Resource.Namespace == resourceType && checkHint.Resource.Relation == relationName && checkHint.Subject.EqualVT(subject) { + if checkHint.Resource.Namespace == resourceType && checkHint.Resource.Relation == relationName && checkHint.Subject.EqualVT(subject.ToCoreONR()) { return checkHint.Resource.ObjectId, true } @@ -48,12 +49,12 @@ func AsCheckHintForComputedUserset(checkHint *v1.CheckHint, resourceType string, } // AsCheckHintForArrow returns the resourceID if the checkHint is for the given arrow and subject. -func AsCheckHintForArrow(checkHint *v1.CheckHint, resourceType string, tuplesetRelation string, computedUsersetRelation string, subject *core.ObjectAndRelation) (string, bool) { +func AsCheckHintForArrow(checkHint *v1.CheckHint, resourceType string, tuplesetRelation string, computedUsersetRelation string, subject tuple.ObjectAndRelation) (string, bool) { if checkHint.TtuComputedUsersetRelation != computedUsersetRelation { return "", false } - if checkHint.Resource.Namespace == resourceType && checkHint.Resource.Relation == tuplesetRelation && checkHint.Subject.EqualVT(subject) { + if checkHint.Resource.Namespace == resourceType && checkHint.Resource.Relation == tuplesetRelation && checkHint.Subject.EqualVT(subject.ToCoreONR()) { return checkHint.Resource.ObjectId, true } @@ -61,7 +62,7 @@ func AsCheckHintForArrow(checkHint *v1.CheckHint, resourceType string, tuplesetR } // HintForEntrypoint returns a CheckHint for the given reachability graph entrypoint and associated subject and result. -func HintForEntrypoint(re typesystem.ReachabilityEntrypoint, resourceID string, subject *core.ObjectAndRelation, result *v1.ResourceCheckResult) (*v1.CheckHint, error) { +func HintForEntrypoint(re typesystem.ReachabilityEntrypoint, resourceID string, subject tuple.ObjectAndRelation, result *v1.ResourceCheckResult) (*v1.CheckHint, error) { switch re.EntrypointKind() { case core.ReachabilityEntrypoint_RELATION_ENTRYPOINT: return nil, spiceerrors.MustBugf("cannot call CheckHintForResource for kind %v", re.EntrypointKind()) diff --git a/internal/graph/hints/checkhints_test.go b/internal/graph/hints/checkhints_test.go index 5b9ba12676..1513fc0f01 100644 --- a/internal/graph/hints/checkhints_test.go +++ b/internal/graph/hints/checkhints_test.go @@ -40,7 +40,7 @@ func TestHintForEntrypoint(t *testing.T) { "org", "member", []*v1.CheckHint{ - CheckHintForComputedUserset("org", "someid", "member", tuple.ParseSubjectONR("user:tom"), &v1.ResourceCheckResult{}), + CheckHintForComputedUserset("org", "someid", "member", tuple.MustParseSubjectONR("user:tom"), &v1.ResourceCheckResult{}), }, }, { @@ -58,7 +58,7 @@ func TestHintForEntrypoint(t *testing.T) { "org", "is_member", []*v1.CheckHint{ - CheckHintForArrow("resource", "someid", "org", "is_member", tuple.ParseSubjectONR("user:tom"), &v1.ResourceCheckResult{}), + CheckHintForArrow("resource", "someid", "org", "is_member", tuple.MustParseSubjectONR("user:tom"), &v1.ResourceCheckResult{}), }, }, } @@ -66,7 +66,7 @@ func TestHintForEntrypoint(t *testing.T) { for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { rg := buildReachabilityGraph(t, tc.schema) - subject := tuple.ParseSubjectONR("user:tom") + subject := tuple.MustParseSubjectONR("user:tom") entrypoints, err := rg.OptimizedEntrypointsForSubjectToResource(context.Background(), &core.RelationReference{ Namespace: tc.subjectType, @@ -136,11 +136,7 @@ func TestCheckHintForComputedUserset(t *testing.T) { resourceType := "resourceType" resourceID := "resourceID" relation := "relation" - subject := &core.ObjectAndRelation{ - Namespace: "subjectNamespace", - ObjectId: "subjectObjectId", - Relation: "subjectRelation", - } + subject := tuple.ONR("subjectNamespace", "subjectObjectId", "subjectRelation") result := &v1.ResourceCheckResult{ Membership: v1.ResourceCheckResult_MEMBER, } @@ -150,7 +146,7 @@ func TestCheckHintForComputedUserset(t *testing.T) { require.Equal(t, resourceType, checkHint.Resource.Namespace) require.Equal(t, resourceID, checkHint.Resource.ObjectId) require.Equal(t, relation, checkHint.Resource.Relation) - require.Equal(t, subject, checkHint.Subject) + require.Equal(t, subject.ToCoreONR(), checkHint.Subject) require.Equal(t, result, checkHint.Result) require.Empty(t, checkHint.TtuComputedUsersetRelation) @@ -164,11 +160,7 @@ func TestCheckHintForArrow(t *testing.T) { resourceID := "resourceID" tuplesetRelation := "tuplesetRelation" computedUsersetRelation := "computedUsersetRelation" - subject := &core.ObjectAndRelation{ - Namespace: "subjectNamespace", - ObjectId: "subjectObjectId", - Relation: "subjectRelation", - } + subject := tuple.ONR("subjectNamespace", "subjectObjectId", "subjectRelation") result := &v1.ResourceCheckResult{ Membership: v1.ResourceCheckResult_MEMBER, } @@ -178,7 +170,7 @@ func TestCheckHintForArrow(t *testing.T) { require.Equal(t, resourceType, checkHint.Resource.Namespace) require.Equal(t, resourceID, checkHint.Resource.ObjectId) require.Equal(t, tuplesetRelation, checkHint.Resource.Relation) - require.Equal(t, subject, checkHint.Subject) + require.Equal(t, subject.ToCoreONR(), checkHint.Subject) require.Equal(t, result, checkHint.Result) require.Equal(t, computedUsersetRelation, checkHint.TtuComputedUsersetRelation) @@ -196,57 +188,57 @@ func TestAsCheckHintForComputedUserset(t *testing.T) { }{ { "matching resource and subject", - CheckHintForComputedUserset("resourceType", "resourceID", "relation", tuple.ParseSubjectONR("user:tom"), &v1.ResourceCheckResult{}), + CheckHintForComputedUserset("resourceType", "resourceID", "relation", tuple.MustParseSubjectONR("user:tom"), &v1.ResourceCheckResult{}), func(ch *v1.CheckHint) (string, bool) { - return AsCheckHintForComputedUserset(ch, "resourceType", "relation", tuple.ParseSubjectONR("user:tom")) + return AsCheckHintForComputedUserset(ch, "resourceType", "relation", tuple.MustParseSubjectONR("user:tom")) }, "resourceID", }, { "mismatch subject ID", - CheckHintForComputedUserset("resourceType", "resourceID", "relation", tuple.ParseSubjectONR("user:anothersubject"), &v1.ResourceCheckResult{}), + CheckHintForComputedUserset("resourceType", "resourceID", "relation", tuple.MustParseSubjectONR("user:anothersubject"), &v1.ResourceCheckResult{}), func(ch *v1.CheckHint) (string, bool) { - return AsCheckHintForComputedUserset(ch, "resourceType", "relation", tuple.ParseSubjectONR("user:tom")) + return AsCheckHintForComputedUserset(ch, "resourceType", "relation", tuple.MustParseSubjectONR("user:tom")) }, "", }, { "mismatch subject type", - CheckHintForComputedUserset("resourceType", "resourceID", "relation", tuple.ParseSubjectONR("githubuser:tom"), &v1.ResourceCheckResult{}), + CheckHintForComputedUserset("resourceType", "resourceID", "relation", tuple.MustParseSubjectONR("githubuser:tom"), &v1.ResourceCheckResult{}), func(ch *v1.CheckHint) (string, bool) { - return AsCheckHintForComputedUserset(ch, "resourceType", "relation", tuple.ParseSubjectONR("user:tom")) + return AsCheckHintForComputedUserset(ch, "resourceType", "relation", tuple.MustParseSubjectONR("user:tom")) }, "", }, { "mismatch subject relation", - CheckHintForComputedUserset("resourceType", "resourceID", "relation", tuple.ParseSubjectONR("user:tom#foo"), &v1.ResourceCheckResult{}), + CheckHintForComputedUserset("resourceType", "resourceID", "relation", tuple.MustParseSubjectONR("user:tom#foo"), &v1.ResourceCheckResult{}), func(ch *v1.CheckHint) (string, bool) { - return AsCheckHintForComputedUserset(ch, "resourceType", "relation", tuple.ParseSubjectONR("user:tom")) + return AsCheckHintForComputedUserset(ch, "resourceType", "relation", tuple.MustParseSubjectONR("user:tom")) }, "", }, { "mismatch resource type", - CheckHintForComputedUserset("anotherType", "resourceID", "relation", tuple.ParseSubjectONR("user:tom"), &v1.ResourceCheckResult{}), + CheckHintForComputedUserset("anotherType", "resourceID", "relation", tuple.MustParseSubjectONR("user:tom"), &v1.ResourceCheckResult{}), func(ch *v1.CheckHint) (string, bool) { - return AsCheckHintForComputedUserset(ch, "resourceType", "relation", tuple.ParseSubjectONR("user:tom")) + return AsCheckHintForComputedUserset(ch, "resourceType", "relation", tuple.MustParseSubjectONR("user:tom")) }, "", }, { "mismatch resource relation", - CheckHintForComputedUserset("resourceType", "resourceID", "anotherRelation", tuple.ParseSubjectONR("user:tom"), &v1.ResourceCheckResult{}), + CheckHintForComputedUserset("resourceType", "resourceID", "anotherRelation", tuple.MustParseSubjectONR("user:tom"), &v1.ResourceCheckResult{}), func(ch *v1.CheckHint) (string, bool) { - return AsCheckHintForComputedUserset(ch, "resourceType", "relation", tuple.ParseSubjectONR("user:tom#...")) + return AsCheckHintForComputedUserset(ch, "resourceType", "relation", tuple.MustParseSubjectONR("user:tom#...")) }, "", }, { "mismatch kind", - CheckHintForArrow("resourceType", "resourceID", "ttu", "clu", tuple.ParseSubjectONR("user:tom"), &v1.ResourceCheckResult{}), + CheckHintForArrow("resourceType", "resourceID", "ttu", "clu", tuple.MustParseSubjectONR("user:tom"), &v1.ResourceCheckResult{}), func(ch *v1.CheckHint) (string, bool) { - return AsCheckHintForComputedUserset(ch, "resourceType", "relation", tuple.ParseSubjectONR("user:tom#...")) + return AsCheckHintForComputedUserset(ch, "resourceType", "relation", tuple.MustParseSubjectONR("user:tom#...")) }, "", }, @@ -275,73 +267,73 @@ func TestAsCheckHintForArrow(t *testing.T) { }{ { "matching resource and subject", - CheckHintForArrow("resourceType", "resourceID", "ttu", "cur", tuple.ParseSubjectONR("user:tom"), &v1.ResourceCheckResult{}), + CheckHintForArrow("resourceType", "resourceID", "ttu", "cur", tuple.MustParseSubjectONR("user:tom"), &v1.ResourceCheckResult{}), func(ch *v1.CheckHint) (string, bool) { - return AsCheckHintForArrow(ch, "resourceType", "ttu", "cur", tuple.ParseSubjectONR("user:tom")) + return AsCheckHintForArrow(ch, "resourceType", "ttu", "cur", tuple.MustParseSubjectONR("user:tom")) }, "resourceID", }, { "mismatch TTU", - CheckHintForArrow("resourceType", "resourceID", "anotherttu", "cur", tuple.ParseSubjectONR("user:tom"), &v1.ResourceCheckResult{}), + CheckHintForArrow("resourceType", "resourceID", "anotherttu", "cur", tuple.MustParseSubjectONR("user:tom"), &v1.ResourceCheckResult{}), func(ch *v1.CheckHint) (string, bool) { - return AsCheckHintForArrow(ch, "resourceType", "ttu", "cur", tuple.ParseSubjectONR("user:tom")) + return AsCheckHintForArrow(ch, "resourceType", "ttu", "cur", tuple.MustParseSubjectONR("user:tom")) }, "", }, { "mismatch computeduserset", - CheckHintForArrow("resourceType", "resourceID", "ttu", "anothercur", tuple.ParseSubjectONR("user:tom"), &v1.ResourceCheckResult{}), + CheckHintForArrow("resourceType", "resourceID", "ttu", "anothercur", tuple.MustParseSubjectONR("user:tom"), &v1.ResourceCheckResult{}), func(ch *v1.CheckHint) (string, bool) { - return AsCheckHintForArrow(ch, "resourceType", "ttu", "cur", tuple.ParseSubjectONR("user:tom")) + return AsCheckHintForArrow(ch, "resourceType", "ttu", "cur", tuple.MustParseSubjectONR("user:tom")) }, "", }, { "mismatch subject ID", - CheckHintForArrow("resourceType", "resourceID", "ttu", "cur", tuple.ParseSubjectONR("user:anothersubject"), &v1.ResourceCheckResult{}), + CheckHintForArrow("resourceType", "resourceID", "ttu", "cur", tuple.MustParseSubjectONR("user:anothersubject"), &v1.ResourceCheckResult{}), func(ch *v1.CheckHint) (string, bool) { - return AsCheckHintForArrow(ch, "resourceType", "ttu", "cur", tuple.ParseSubjectONR("user:tom")) + return AsCheckHintForArrow(ch, "resourceType", "ttu", "cur", tuple.MustParseSubjectONR("user:tom")) }, "", }, { "mismatch subject type", - CheckHintForArrow("resourceType", "resourceID", "ttu", "cur", tuple.ParseSubjectONR("githubuser:tom"), &v1.ResourceCheckResult{}), + CheckHintForArrow("resourceType", "resourceID", "ttu", "cur", tuple.MustParseSubjectONR("githubuser:tom"), &v1.ResourceCheckResult{}), func(ch *v1.CheckHint) (string, bool) { - return AsCheckHintForArrow(ch, "resourceType", "ttu", "cur", tuple.ParseSubjectONR("user:tom")) + return AsCheckHintForArrow(ch, "resourceType", "ttu", "cur", tuple.MustParseSubjectONR("user:tom")) }, "", }, { "mismatch subject relation", - CheckHintForArrow("resourceType", "resourceID", "ttu", "cur", tuple.ParseSubjectONR("user:tom#something"), &v1.ResourceCheckResult{}), + CheckHintForArrow("resourceType", "resourceID", "ttu", "cur", tuple.MustParseSubjectONR("user:tom#something"), &v1.ResourceCheckResult{}), func(ch *v1.CheckHint) (string, bool) { - return AsCheckHintForArrow(ch, "resourceType", "ttu", "cur", tuple.ParseSubjectONR("user:tom")) + return AsCheckHintForArrow(ch, "resourceType", "ttu", "cur", tuple.MustParseSubjectONR("user:tom")) }, "", }, { "mismatch resource type", - CheckHintForArrow("anotherType", "resourceID", "ttu", "cur", tuple.ParseSubjectONR("user:tom"), &v1.ResourceCheckResult{}), + CheckHintForArrow("anotherType", "resourceID", "ttu", "cur", tuple.MustParseSubjectONR("user:tom"), &v1.ResourceCheckResult{}), func(ch *v1.CheckHint) (string, bool) { - return AsCheckHintForArrow(ch, "resourceType", "ttu", "cur", tuple.ParseSubjectONR("user:tom")) + return AsCheckHintForArrow(ch, "resourceType", "ttu", "cur", tuple.MustParseSubjectONR("user:tom")) }, "", }, { "mismatch resource relation", - CheckHintForArrow("resourceType", "resourceID", "anotherttu", "cur", tuple.ParseSubjectONR("user:tom"), &v1.ResourceCheckResult{}), + CheckHintForArrow("resourceType", "resourceID", "anotherttu", "cur", tuple.MustParseSubjectONR("user:tom"), &v1.ResourceCheckResult{}), func(ch *v1.CheckHint) (string, bool) { - return AsCheckHintForArrow(ch, "resourceType", "ttu", "cur", tuple.ParseSubjectONR("user:tom")) + return AsCheckHintForArrow(ch, "resourceType", "ttu", "cur", tuple.MustParseSubjectONR("user:tom")) }, "", }, { "mismatch kind", - CheckHintForComputedUserset("resourceType", "resourceID", "relation", tuple.ParseSubjectONR("user:tom"), &v1.ResourceCheckResult{}), + CheckHintForComputedUserset("resourceType", "resourceID", "relation", tuple.MustParseSubjectONR("user:tom"), &v1.ResourceCheckResult{}), func(ch *v1.CheckHint) (string, bool) { - return AsCheckHintForArrow(ch, "resourceType", "ttu", "cur", tuple.ParseSubjectONR("user:tom")) + return AsCheckHintForArrow(ch, "resourceType", "ttu", "cur", tuple.MustParseSubjectONR("user:tom")) }, "", }, diff --git a/internal/graph/lookupresources2.go b/internal/graph/lookupresources2.go index f8e908ed13..7411d7c073 100644 --- a/internal/graph/lookupresources2.go +++ b/internal/graph/lookupresources2.go @@ -281,7 +281,6 @@ func (crr *CursoredLookupResources2) redispatchOrReportOverDatabaseQuery( if err != nil { return nil, err } - defer it.Close() // Chunk based on the FilterMaximumIDCount, to ensure we never send more than that amount of // results to a downstream dispatch. @@ -289,16 +288,16 @@ func (crr *CursoredLookupResources2) redispatchOrReportOverDatabaseQuery( toBeHandled := make([]itemAndPostCursor[dispatchableResourcesSubjectMap2], 0) currentCursor := queryCursor - for tpl := it.Next(); tpl != nil; tpl = it.Next() { - if it.Err() != nil { - return nil, it.Err() + for rel, err := range it { + if err != nil { + return nil, err } var missingContextParameters []string // If a caveat exists on the relationship, run it and filter the results, marking those that have missing context. - if tpl.Caveat != nil && tpl.Caveat.CaveatName != "" { - caveatExpr := caveats.CaveatAsExpr(tpl.Caveat) + if rel.OptionalCaveat != nil && rel.OptionalCaveat.CaveatName != "" { + caveatExpr := caveats.CaveatAsExpr(rel.OptionalCaveat) runResult, err := caveats.RunCaveatExpression(ctx, caveatExpr, config.parentRequest.Context.AsMap(), config.reader, caveats.RunCaveatExpressionNoDebugging) if err != nil { return nil, err @@ -318,7 +317,7 @@ func (crr *CursoredLookupResources2) redispatchOrReportOverDatabaseQuery( } } - if err := rsm.addRelationship(tpl, missingContextParameters); err != nil { + if err := rsm.addRelationship(rel, missingContextParameters); err != nil { return nil, err } @@ -328,10 +327,9 @@ func (crr *CursoredLookupResources2) redispatchOrReportOverDatabaseQuery( cursor: currentCursor, }) rsm = newResourcesSubjectMap2WithCapacity(config.sourceResourceType, uint32(crr.dispatchChunkSize)) - currentCursor = tpl + currentCursor = options.ToCursor(rel) } } - it.Close() if rsm.len() > 0 { toBeHandled = append(toBeHandled, itemAndPostCursor[dispatchableResourcesSubjectMap2]{ @@ -502,7 +500,7 @@ func (crr *CursoredLookupResources2) redispatchOrReport( checkHint, err := hints.HintForEntrypoint( entrypoint, resource.ResourceId, - parentRequest.TerminalSubject, + tuple.FromCoreObjectAndRelation(parentRequest.TerminalSubject), &v1.ResourceCheckResult{ Membership: v1.ResourceCheckResult_MEMBER, }) @@ -513,8 +511,8 @@ func (crr *CursoredLookupResources2) redispatchOrReport( } resultsByResourceID, checkMetadata, err := computed.ComputeBulkCheck(ctx, crr.dc, computed.CheckParameters{ - ResourceType: parentRequest.ResourceRelation, - Subject: parentRequest.TerminalSubject, + ResourceType: tuple.FromCoreRelationReference(parentRequest.ResourceRelation), + Subject: tuple.FromCoreObjectAndRelation(parentRequest.TerminalSubject), CaveatContext: parentRequest.Context.AsMap(), AtRevision: parentRequest.Revision, MaximumDepth: parentRequest.Metadata.DepthRemaining - 1, diff --git a/internal/graph/lookupsubjects.go b/internal/graph/lookupsubjects.go index 542678fdbd..c2f24ff483 100644 --- a/internal/graph/lookupsubjects.go +++ b/internal/graph/lookupsubjects.go @@ -106,6 +106,11 @@ func (cl *ConcurrentLookupSubjects) lookupDirectSubjects( reader datastore.Reader, ) error { // TODO(jschorr): use type information to skip subject relations that cannot reach the subject type. + + toDispatchByType := datasets.NewSubjectByTypeSet() + foundSubjectsByResourceID := datasets.NewSubjectSetByResourceID() + relationshipsBySubjectONR := mapz.NewMultiMap[tuple.ObjectAndRelation, tuple.Relationship]() + it, err := reader.QueryRelationships(ctx, datastore.RelationshipsFilter{ OptionalResourceType: req.ResourceRelation.Namespace, OptionalResourceRelation: req.ResourceRelation.Relation, @@ -114,33 +119,28 @@ func (cl *ConcurrentLookupSubjects) lookupDirectSubjects( if err != nil { return err } - defer it.Close() - toDispatchByType := datasets.NewSubjectByTypeSet() - foundSubjectsByResourceID := datasets.NewSubjectSetByResourceID() - relationshipsBySubjectONR := mapz.NewMultiMap[string, *core.RelationTuple]() - for tpl := it.Next(); tpl != nil; tpl = it.Next() { - if it.Err() != nil { - return it.Err() + for rel, err := range it { + if err != nil { + return err } - if tpl.Subject.Namespace == req.SubjectRelation.Namespace && - tpl.Subject.Relation == req.SubjectRelation.Relation { - if err := foundSubjectsByResourceID.AddFromRelationship(tpl); err != nil { + if rel.Subject.ObjectType == req.SubjectRelation.Namespace && + rel.Subject.Relation == req.SubjectRelation.Relation { + if err := foundSubjectsByResourceID.AddFromRelationship(rel); err != nil { return fmt.Errorf("failed to call AddFromRelationship in lookupDirectSubjects: %w", err) } } - if tpl.Subject.Relation != tuple.Ellipsis { - err := toDispatchByType.AddSubjectOf(tpl) + if rel.Subject.Relation != tuple.Ellipsis { + err := toDispatchByType.AddSubjectOf(rel) if err != nil { return err } - relationshipsBySubjectONR.Add(tuple.StringONR(tpl.Subject), tpl) + relationshipsBySubjectONR.Add(rel.Subject, rel) } } - it.Close() if !foundSubjectsByResourceID.IsEmpty() { if err := stream.Publish(&v1.DispatchLookupSubjectsResponse{ @@ -224,7 +224,6 @@ func lookupViaIntersectionTupleToUserset( if err != nil { return err } - defer it.Close() // TODO(jschorr): Find a means of doing this without dispatching per subject, per resource. Perhaps // there is a way we can still dispatch to all the subjects at once, and then intersect the results @@ -238,18 +237,18 @@ func lookupViaIntersectionTupleToUserset( // We need to intersect between *all* the found subjects for each resource ID. var ttuCaveat *core.CaveatExpression taskrunner := taskrunner.NewPreloadedTaskRunner(cancelCtx, cl.concurrencyLimit, 1) - for tpl := it.Next(); tpl != nil; tpl = it.Next() { - if it.Err() != nil { - return it.Err() + for rel, err := range it { + if err != nil { + return err } // If the relationship has a caveat, add it to the overall TTU caveat. Since this is an intersection // of *all* branches, the caveat will be applied to all found subjects, so this is a safe approach. - if tpl.Caveat != nil { - ttuCaveat = caveatAnd(ttuCaveat, wrapCaveat(tpl.Caveat)) + if rel.OptionalCaveat != nil { + ttuCaveat = caveatAnd(ttuCaveat, wrapCaveat(rel.OptionalCaveat)) } - if err := namespace.CheckNamespaceAndRelation(ctx, tpl.Subject.Namespace, ttu.GetComputedUserset().Relation, false, ds); err != nil { + if err := namespace.CheckNamespaceAndRelation(ctx, rel.Subject.ObjectType, ttu.GetComputedUserset().Relation, false, ds); err != nil { if !errors.As(err, &namespace.ErrRelationNotFound{}) { return err } @@ -259,7 +258,7 @@ func lookupViaIntersectionTupleToUserset( // Create a data structure to track the intersection of subjects for the particular resource. If the resource's subject set // ends up empty anywhere along the way, the dispatches for *that resource* will be canceled early. - resourceID := tpl.ResourceAndRelation.ObjectId + resourceID := rel.Resource.ObjectID dispatchInfoForResource, ok := resourceDispatchTrackerByResourceID[resourceID] if !ok { dispatchCtx, cancelDispatch := context.WithCancel(cancelCtx) @@ -275,7 +274,7 @@ func lookupViaIntersectionTupleToUserset( resourceDispatchTrackerByResourceID[resourceID] = dispatchInfoForResource } - tpl := tpl + rel := rel taskrunner.Add(func(ctx context.Context) error { // Collect all results for this branch of the resource ID. // TODO(jschorr): once LS has cursoring (and thus, ordering), we can move to not collecting everything up before intersecting @@ -283,10 +282,10 @@ func lookupViaIntersectionTupleToUserset( collectingStream := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupSubjectsResponse](dispatchInfoForResource.ctx) err := cl.d.DispatchLookupSubjects(&v1.DispatchLookupSubjectsRequest{ ResourceRelation: &core.RelationReference{ - Namespace: tpl.Subject.Namespace, + Namespace: rel.Subject.ObjectType, Relation: ttu.GetComputedUserset().Relation, }, - ResourceIds: []string{tpl.Subject.ObjectId}, + ResourceIds: []string{rel.Subject.ObjectID}, SubjectRelation: parentRequest.SubjectRelation, Metadata: &v1.ResolverMeta{ AtRevision: parentRequest.Revision.String(), @@ -351,7 +350,6 @@ func lookupViaIntersectionTupleToUserset( return nil }) } - it.Close() // Wait for all dispatched operations to complete. if err := taskrunner.StartAndWait(); err != nil { @@ -383,6 +381,9 @@ func lookupViaTupleToUserset[T relation]( parentStream dispatch.LookupSubjectsStream, ttu ttu[T], ) error { + toDispatchByTuplesetType := datasets.NewSubjectByTypeSet() + relationshipsBySubjectONR := mapz.NewMultiMap[tuple.ObjectAndRelation, tuple.Relationship]() + ds := datastoremw.MustFromContext(ctx).SnapshotReader(parentRequest.Revision) it, err := ds.QueryRelationships(ctx, datastore.RelationshipsFilter{ OptionalResourceType: parentRequest.ResourceRelation.Namespace, @@ -392,30 +393,22 @@ func lookupViaTupleToUserset[T relation]( if err != nil { return err } - defer it.Close() - toDispatchByTuplesetType := datasets.NewSubjectByTypeSet() - relationshipsBySubjectONR := mapz.NewMultiMap[string, *core.RelationTuple]() - for tpl := it.Next(); tpl != nil; tpl = it.Next() { - if it.Err() != nil { - return it.Err() + for rel, err := range it { + if err != nil { + return err } // Add the subject to be dispatched. - err := toDispatchByTuplesetType.AddSubjectOf(tpl) + err := toDispatchByTuplesetType.AddSubjectOf(rel) if err != nil { return err } // Add the *rewritten* subject to the relationships multimap for mapping back to the associated // relationship, as we will be mapping from the computed relation, not the tupleset relation. - relationshipsBySubjectONR.Add(tuple.StringONR(&core.ObjectAndRelation{ - Namespace: tpl.Subject.Namespace, - ObjectId: tpl.Subject.ObjectId, - Relation: ttu.GetComputedUserset().Relation, - }), tpl) + relationshipsBySubjectONR.Add(tuple.ONR(rel.Subject.ObjectType, rel.Subject.ObjectID, ttu.GetComputedUserset().Relation), rel) } - it.Close() // Map the found subject types by the computed userset relation, so that we dispatch to it. toDispatchByComputedRelationType, err := toDispatchByTuplesetType.Map(func(resourceType *core.RelationReference) (*core.RelationReference, error) { @@ -531,7 +524,7 @@ func (cl *ConcurrentLookupSubjects) dispatchTo( ctx context.Context, parentRequest ValidatedLookupSubjectsRequest, toDispatchByType *datasets.SubjectByTypeSet, - relationshipsBySubjectONR *mapz.MultiMap[string, *core.RelationTuple], + relationshipsBySubjectONR *mapz.MultiMap[tuple.ObjectAndRelation, tuple.Relationship], parentStream dispatch.LookupSubjectsStream, ) error { if toDispatchByType.IsEmpty() { @@ -571,27 +564,22 @@ func (cl *ConcurrentLookupSubjects) dispatchTo( // mappedFoundSubjects := make(map[string]*v1.FoundSubjects) for childResourceID, foundSubjects := range result.FoundSubjectsByResourceId { - subjectKey := tuple.StringONR(&core.ObjectAndRelation{ - Namespace: resourceType.Namespace, - ObjectId: childResourceID, - Relation: resourceType.Relation, - }) - + subjectKey := tuple.ONR(resourceType.Namespace, childResourceID, resourceType.Relation) relationships, _ := relationshipsBySubjectONR.Get(subjectKey) if len(relationships) == 0 { return nil, false, fmt.Errorf("missing relationships for subject key %v; please report this error", subjectKey) } for _, relationship := range relationships { - existing := mappedFoundSubjects[relationship.ResourceAndRelation.ObjectId] + existing := mappedFoundSubjects[relationship.Resource.ObjectID] // If the relationship has no caveat, simply map the resource ID. - if relationship.GetCaveat() == nil { + if relationship.OptionalCaveat == nil { combined, err := combineFoundSubjects(existing, foundSubjects) if err != nil { return nil, false, fmt.Errorf("could not combine caveat-less subjects: %w", err) } - mappedFoundSubjects[relationship.ResourceAndRelation.ObjectId] = combined + mappedFoundSubjects[relationship.Resource.ObjectID] = combined continue } @@ -604,13 +592,13 @@ func (cl *ConcurrentLookupSubjects) dispatchTo( combined, err := combineFoundSubjects( existing, - foundSubjectSet.WithParentCaveatExpression(wrapCaveat(relationship.Caveat)).AsFoundSubjects(), + foundSubjectSet.WithParentCaveatExpression(wrapCaveat(relationship.OptionalCaveat)).AsFoundSubjects(), ) if err != nil { return nil, false, fmt.Errorf("could not combine caveated subjects: %w", err) } - mappedFoundSubjects[relationship.ResourceAndRelation.ObjectId] = combined + mappedFoundSubjects[relationship.Resource.ObjectID] = combined } } diff --git a/internal/graph/lr2streams.go b/internal/graph/lr2streams.go index 79ca7eb955..e1296a96a8 100644 --- a/internal/graph/lr2streams.go +++ b/internal/graph/lr2streams.go @@ -13,6 +13,7 @@ import ( core "github.com/authzed/spicedb/pkg/proto/core/v1" v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" "github.com/authzed/spicedb/pkg/spiceerrors" + "github.com/authzed/spicedb/pkg/tuple" "github.com/authzed/spicedb/pkg/typesystem" ) @@ -107,7 +108,7 @@ func (rdc *checkAndDispatchRunner) runChecker(ctx context.Context, startingIndex checkHint, err := hints.HintForEntrypoint( rdc.entrypoint, resourceID, - rdc.parentRequest.TerminalSubject, + tuple.FromCoreObjectAndRelation(rdc.parentRequest.TerminalSubject), &v1.ResourceCheckResult{ Membership: v1.ResourceCheckResult_MEMBER, }) @@ -120,8 +121,8 @@ func (rdc *checkAndDispatchRunner) runChecker(ctx context.Context, startingIndex // NOTE: we are checking the containing permission here, *not* the target relation, as // the goal is to shear for the containing permission. resultsByResourceID, checkMetadata, err := computed.ComputeBulkCheck(ctx, rdc.checkDispatcher, computed.CheckParameters{ - ResourceType: rdc.newSubjectType, - Subject: rdc.parentRequest.TerminalSubject, + ResourceType: tuple.FromCoreRelationReference(rdc.newSubjectType), + Subject: tuple.FromCoreObjectAndRelation(rdc.parentRequest.TerminalSubject), CaveatContext: rdc.parentRequest.Context.AsMap(), AtRevision: rdc.parentRequest.Revision, MaximumDepth: rdc.parentRequest.Metadata.DepthRemaining - 1, diff --git a/internal/graph/membershipset.go b/internal/graph/membershipset.go index 6c9157a329..8ab20f4d7d 100644 --- a/internal/graph/membershipset.go +++ b/internal/graph/membershipset.go @@ -4,6 +4,7 @@ import ( "github.com/authzed/spicedb/internal/caveats" core "github.com/authzed/spicedb/pkg/proto/core/v1" v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" + "github.com/authzed/spicedb/pkg/tuple" ) var ( @@ -56,9 +57,9 @@ func (ms *MembershipSet) AddDirectMember(resourceID string, caveat *core.Context func (ms *MembershipSet) AddMemberViaRelationship( resourceID string, resourceCaveatExpression *core.CaveatExpression, - parentRelationship *core.RelationTuple, + parentRelationship tuple.Relationship, ) { - ms.AddMemberWithParentCaveat(resourceID, resourceCaveatExpression, parentRelationship.Caveat) + ms.AddMemberWithParentCaveat(resourceID, resourceCaveatExpression, parentRelationship.OptionalCaveat) } // AddMemberWithParentCaveat adds the given resource ID as a member with the parent caveat diff --git a/internal/graph/membershipset_test.go b/internal/graph/membershipset_test.go index e86c885400..202438fa2d 100644 --- a/internal/graph/membershipset_test.go +++ b/internal/graph/membershipset_test.go @@ -145,7 +145,7 @@ func TestMembershipSetAddMemberViaRelationship(t *testing.T) { existingMembers map[string]*core.CaveatExpression resourceID string resourceCaveatExpression *core.CaveatExpression - parentRelationship *core.RelationTuple + parentRelationship tuple.Relationship expectedMembers map[string]*core.CaveatExpression hasDeterminedMember bool }{ @@ -816,7 +816,7 @@ func unwrapCaveat(ce *core.CaveatExpression) *core.ContextualizedCaveat { return ce.GetCaveat() } -func withCaveat(tple *core.RelationTuple, ce *core.CaveatExpression) *core.RelationTuple { - tple.Caveat = unwrapCaveat(ce) +func withCaveat(tple tuple.Relationship, ce *core.CaveatExpression) tuple.Relationship { + tple.OptionalCaveat = unwrapCaveat(ce) return tple } diff --git a/internal/graph/reachableresources.go b/internal/graph/reachableresources.go index f8c0157392..e83e9d103f 100644 --- a/internal/graph/reachableresources.go +++ b/internal/graph/reachableresources.go @@ -278,7 +278,6 @@ func (crr *CursoredReachableResources) redispatchOrReportOverDatabaseQuery( if err != nil { return nil, err } - defer it.Close() // Chunk based on the FilterMaximumIDCount, to ensure we never send more than that amount of // results to a downstream dispatch. @@ -286,12 +285,12 @@ func (crr *CursoredReachableResources) redispatchOrReportOverDatabaseQuery( toBeHandled := make([]itemAndPostCursor[dispatchableResourcesSubjectMap], 0) currentCursor := queryCursor - for tpl := it.Next(); tpl != nil; tpl = it.Next() { - if it.Err() != nil { - return nil, it.Err() + for rel, err := range it { + if err != nil { + return nil, err } - if err := rsm.addRelationship(tpl); err != nil { + if err := rsm.addRelationship(rel); err != nil { return nil, err } @@ -301,10 +300,9 @@ func (crr *CursoredReachableResources) redispatchOrReportOverDatabaseQuery( cursor: currentCursor, }) rsm = newResourcesSubjectMapWithCapacity(config.sourceResourceType, uint32(crr.dispatchChunkSize)) - currentCursor = tpl + currentCursor = options.ToCursor(rel) } } - it.Close() if rsm.len() > 0 { toBeHandled = append(toBeHandled, itemAndPostCursor[dispatchableResourcesSubjectMap]{ diff --git a/internal/graph/resourcesubjectsmap.go b/internal/graph/resourcesubjectsmap.go index a71b8e2e2c..950617dc6b 100644 --- a/internal/graph/resourcesubjectsmap.go +++ b/internal/graph/resourcesubjectsmap.go @@ -17,7 +17,7 @@ type syncONRSet struct { } func (s *syncONRSet) Add(onr *core.ObjectAndRelation) bool { - key := tuple.StringONR(onr) + key := tuple.StringONR(tuple.FromCoreObjectAndRelation(onr)) s.Lock() _, existed := s.items[key] if !existed { @@ -70,13 +70,13 @@ func subjectIDsToResourcesMap(resourceType *core.RelationReference, subjectIDs [ // addRelationship adds the relationship to the resource subject map, recording a mapping from // the resource of the relationship to the subject, as well as whether the relationship was caveated. -func (rsm resourcesSubjectMap) addRelationship(rel *core.RelationTuple) error { - if rel.ResourceAndRelation.Namespace != rsm.resourceType.Namespace || - rel.ResourceAndRelation.Relation != rsm.resourceType.Relation { - return spiceerrors.MustBugf("invalid relationship for addRelationship. expected: %v, found: %v", rsm.resourceType, rel.ResourceAndRelation) +func (rsm resourcesSubjectMap) addRelationship(rel tuple.Relationship) error { + if rel.Resource.ObjectType != rsm.resourceType.Namespace || + rel.Resource.Relation != rsm.resourceType.Relation { + return spiceerrors.MustBugf("invalid relationship for addRelationship. expected: %v, found: %v", rsm.resourceType, rel.Resource) } - rsm.resourcesAndSubjects.Add(rel.ResourceAndRelation.ObjectId, subjectInfo{rel.Subject.ObjectId, rel.Caveat != nil && rel.Caveat.CaveatName != ""}) + rsm.resourcesAndSubjects.Add(rel.Resource.ObjectID, subjectInfo{rel.Subject.ObjectID, rel.OptionalCaveat != nil && rel.OptionalCaveat.CaveatName != ""}) return nil } diff --git a/internal/graph/resourcesubjectsmap2.go b/internal/graph/resourcesubjectsmap2.go index f62202dfe3..582a8791e4 100644 --- a/internal/graph/resourcesubjectsmap2.go +++ b/internal/graph/resourcesubjectsmap2.go @@ -7,6 +7,7 @@ import ( core "github.com/authzed/spicedb/pkg/proto/core/v1" v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" "github.com/authzed/spicedb/pkg/spiceerrors" + "github.com/authzed/spicedb/pkg/tuple" ) // resourcesSubjectMap2 is a multimap which tracks mappings from found resource IDs @@ -47,17 +48,16 @@ func subjectIDsToResourcesMap2(resourceType *core.RelationReference, subjectIDs // addRelationship adds the relationship to the resource subject map, recording a mapping from // the resource of the relationship to the subject, as well as whether the relationship was caveated. -func (rsm resourcesSubjectMap2) addRelationship(rel *core.RelationTuple, missingContextParameters []string) error { - if rel.ResourceAndRelation.Namespace != rsm.resourceType.Namespace || - rel.ResourceAndRelation.Relation != rsm.resourceType.Relation { - return spiceerrors.MustBugf("invalid relationship for addRelationship. expected: %v, found: %v", rsm.resourceType, rel.ResourceAndRelation) - } +func (rsm resourcesSubjectMap2) addRelationship(rel tuple.Relationship, missingContextParameters []string) error { + spiceerrors.DebugAssert(func() bool { + return rel.Resource.ObjectType == rsm.resourceType.Namespace && rel.Resource.Relation == rsm.resourceType.Relation + }, "invalid relationship for addRelationship. expected: %v, found: %v", rsm.resourceType, rel.Resource) - if len(missingContextParameters) > 0 && rel.Caveat == nil { - return spiceerrors.MustBugf("missing caveat for caveated relationship") - } + spiceerrors.DebugAssert(func() bool { + return len(missingContextParameters) == 0 || rel.OptionalCaveat != nil + }, "missing context parameters must be empty if there is no caveat") - rsm.resourcesAndSubjects.Add(rel.ResourceAndRelation.ObjectId, subjectInfo2{rel.Subject.ObjectId, missingContextParameters}) + rsm.resourcesAndSubjects.Add(rel.Resource.ObjectID, subjectInfo2{rel.Subject.ObjectID, missingContextParameters}) return nil } diff --git a/internal/graph/resourcesubjectsmap2_test.go b/internal/graph/resourcesubjectsmap2_test.go index 2a5ccf56bf..dd8f73fb6c 100644 --- a/internal/graph/resourcesubjectsmap2_test.go +++ b/internal/graph/resourcesubjectsmap2_test.go @@ -63,7 +63,7 @@ func TestResourcesSubjectsMap2Basic(t *testing.T) { } type relAndMissingContext struct { - rel *core.RelationTuple + rel tuple.Relationship missingContext []string } diff --git a/internal/graph/resourcesubjectsmap_test.go b/internal/graph/resourcesubjectsmap_test.go index d88332d1e1..2bbe5520ef 100644 --- a/internal/graph/resourcesubjectsmap_test.go +++ b/internal/graph/resourcesubjectsmap_test.go @@ -93,17 +93,17 @@ func TestResourcesSubjectsMapBasic(t *testing.T) { func TestResourcesSubjectsMapAsReachableResources(t *testing.T) { tcs := []struct { name string - rels []*core.RelationTuple + rels []tuple.Relationship expected []*v1.ReachableResource }{ { "empty", - []*core.RelationTuple{}, + []tuple.Relationship{}, []*v1.ReachableResource{}, }, { "basic", - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:first#view@user:tom"), tuple.MustParse("document:second#view@user:sarah"), }, @@ -122,7 +122,7 @@ func TestResourcesSubjectsMapAsReachableResources(t *testing.T) { }, { "caveated and non-caveated", - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:first#view@user:tom"), tuple.MustParse("document:first#view@user:sarah[somecaveat]"), }, @@ -136,7 +136,7 @@ func TestResourcesSubjectsMapAsReachableResources(t *testing.T) { }, { "all caveated", - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:first#view@user:tom[anothercaveat]"), tuple.MustParse("document:first#view@user:sarah[somecaveat]"), }, @@ -150,7 +150,7 @@ func TestResourcesSubjectsMapAsReachableResources(t *testing.T) { }, { "full", - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:first#view@user:tom[anothercaveat]"), tuple.MustParse("document:first#view@user:sarah[somecaveat]"), tuple.MustParse("document:second#view@user:tom"), @@ -215,19 +215,19 @@ func TestResourcesSubjectsMapAsReachableResources(t *testing.T) { func TestResourcesSubjectsMapMapFoundResources(t *testing.T) { tcs := []struct { name string - rels []*core.RelationTuple + rels []tuple.Relationship foundResources []*v1.ReachableResource expected []*v1.ReachableResource }{ { "empty", - []*core.RelationTuple{}, + []tuple.Relationship{}, []*v1.ReachableResource{}, []*v1.ReachableResource{}, }, { "basic no caveats", - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("group:firstgroup#member@organization:foo"), tuple.MustParse("group:firstgroup#member@organization:bar"), }, @@ -248,7 +248,7 @@ func TestResourcesSubjectsMapMapFoundResources(t *testing.T) { }, { "caveated all found", - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("group:firstgroup#member@organization:foo[somecaveat]"), tuple.MustParse("group:firstgroup#member@organization:bar[somecvaeat]"), }, @@ -269,7 +269,7 @@ func TestResourcesSubjectsMapMapFoundResources(t *testing.T) { }, { "simple short circuit", - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("group:firstgroup#member@organization:foo[somecaveat]"), tuple.MustParse("group:firstgroup#member@organization:bar"), }, @@ -290,7 +290,7 @@ func TestResourcesSubjectsMapMapFoundResources(t *testing.T) { }, { "check requires on incoming subject", - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("group:firstgroup#member@organization:foo"), tuple.MustParse("group:firstgroup#member@organization:bar"), }, @@ -311,7 +311,7 @@ func TestResourcesSubjectsMapMapFoundResources(t *testing.T) { }, { "multi-input short circuit", - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("group:firstgroup#member@organization:foo"), tuple.MustParse("group:firstgroup#member@organization:bar"), tuple.MustParse("group:secondgroup#member@organization:foo[somecaveat]"), @@ -333,7 +333,7 @@ func TestResourcesSubjectsMapMapFoundResources(t *testing.T) { }, { "multi-input short circuit from single input", - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("group:firstgroup#member@organization:bar"), tuple.MustParse("group:secondgroup#member@organization:foo[somecaveat]"), }, @@ -354,7 +354,7 @@ func TestResourcesSubjectsMapMapFoundResources(t *testing.T) { }, { "multi-input short circuit from single input with check required on parent", - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("group:firstgroup#member@organization:bar"), tuple.MustParse("group:secondgroup#member@organization:foo[somecaveat]"), }, @@ -375,7 +375,7 @@ func TestResourcesSubjectsMapMapFoundResources(t *testing.T) { }, { "multi-input all caveated", - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("group:firstgroup#member@organization:bar[anothercaveat]"), tuple.MustParse("group:secondgroup#member@organization:foo[somecaveat]"), }, diff --git a/internal/relationships/errors.go b/internal/relationships/errors.go index 9e0d97e3e6..07a320f27d 100644 --- a/internal/relationships/errors.go +++ b/internal/relationships/errors.go @@ -23,32 +23,32 @@ import ( // allowed on relation. type ErrInvalidSubjectType struct { error - tuple *core.RelationTuple + relationship tuple.Relationship relationType *core.AllowedRelation additionalDetails map[string]string } // NewInvalidSubjectTypeError constructs a new error for attempting to write an invalid subject type. func NewInvalidSubjectTypeError( - update *core.RelationTuple, + relationship tuple.Relationship, relationType *core.AllowedRelation, typeSystem *typesystem.TypeSystem, ) error { - allowedTypes, err := typeSystem.AllowedDirectRelationsAndWildcards(update.ResourceAndRelation.Relation) + allowedTypes, err := typeSystem.AllowedDirectRelationsAndWildcards(relationship.Resource.Relation) if err != nil { return err } // Special case: if the subject is uncaveated but only a caveated version is allowed, return // a more descriptive error. - if update.Caveat == nil { + if relationship.OptionalCaveat == nil { allowedCaveatsForSubject := mapz.NewSet[string]() for _, allowedType := range allowedTypes { if allowedType.RequiredCaveat != nil && allowedType.RequiredCaveat.CaveatName != "" && - allowedType.Namespace == update.Subject.Namespace && - allowedType.GetRelation() == update.Subject.Relation { + allowedType.Namespace == relationship.Subject.ObjectType && + allowedType.GetRelation() == relationship.Subject.Relation { allowedCaveatsForSubject.Add(allowedType.RequiredCaveat.CaveatName) } } @@ -58,11 +58,11 @@ func NewInvalidSubjectTypeError( error: fmt.Errorf( "subjects of type `%s` are not allowed on relation `%s#%s` without one of the following caveats: %s", typesystem.SourceForAllowedRelation(relationType), - update.ResourceAndRelation.Namespace, - update.ResourceAndRelation.Relation, + relationship.Resource.ObjectType, + relationship.Resource.Relation, strings.Join(allowedCaveatsForSubject.AsSlice(), ","), ), - tuple: update, + relationship: relationship, relationType: relationType, additionalDetails: map[string]string{ "allowed_caveats": strings.Join(allowedCaveatsForSubject.AsSlice(), ","), @@ -83,11 +83,11 @@ func NewInvalidSubjectTypeError( error: fmt.Errorf( "subjects of type `%s` are not allowed on relation `%s#%s`; did you mean `%s`?", typesystem.SourceForAllowedRelation(relationType), - update.ResourceAndRelation.Namespace, - update.ResourceAndRelation.Relation, + relationship.Resource.ObjectType, + relationship.Resource.Relation, matches[0].Target, ), - tuple: update, + relationship: relationship, relationType: relationType, additionalDetails: nil, } @@ -97,10 +97,10 @@ func NewInvalidSubjectTypeError( error: fmt.Errorf( "subjects of type `%s` are not allowed on relation `%s#%s`", typesystem.SourceForAllowedRelation(relationType), - update.ResourceAndRelation.Namespace, - update.ResourceAndRelation.Relation, + relationship.Resource.ObjectType, + relationship.Resource.Relation, ), - tuple: update, + relationship: relationship, relationType: relationType, additionalDetails: nil, } @@ -109,8 +109,8 @@ func NewInvalidSubjectTypeError( // GRPCStatus implements retrieving the gRPC status for the error. func (err ErrInvalidSubjectType) GRPCStatus() *status.Status { details := map[string]string{ - "definition_name": err.tuple.ResourceAndRelation.Namespace, - "relation_name": err.tuple.ResourceAndRelation.Relation, + "definition_name": err.relationship.Resource.ObjectType, + "relation_name": err.relationship.Resource.Relation, "subject_type": typesystem.SourceForAllowedRelation(err.relationType), } @@ -131,18 +131,18 @@ func (err ErrInvalidSubjectType) GRPCStatus() *status.Status { // ErrCannotWriteToPermission indicates that a write was attempted on a permission. type ErrCannotWriteToPermission struct { error - tuple *core.RelationTuple + rel tuple.Relationship } // NewCannotWriteToPermissionError constructs a new error for attempting to write to a permission. -func NewCannotWriteToPermissionError(update *core.RelationTuple) ErrCannotWriteToPermission { +func NewCannotWriteToPermissionError(rel tuple.Relationship) ErrCannotWriteToPermission { return ErrCannotWriteToPermission{ error: fmt.Errorf( "cannot write a relationship to permission `%s` under definition `%s`", - update.ResourceAndRelation.Relation, - update.ResourceAndRelation.Namespace, + rel.Resource.Relation, + rel.Resource.ObjectType, ), - tuple: update, + rel: rel, } } @@ -154,8 +154,8 @@ func (err ErrCannotWriteToPermission) GRPCStatus() *status.Status { spiceerrors.ForReason( v1.ErrorReason_ERROR_REASON_CANNOT_UPDATE_PERMISSION, map[string]string{ - "definition_name": err.tuple.ResourceAndRelation.Namespace, - "permission_name": err.tuple.ResourceAndRelation.Relation, + "definition_name": err.rel.Resource.ObjectType, + "permission_name": err.rel.Resource.Relation, }, ), ) @@ -164,18 +164,18 @@ func (err ErrCannotWriteToPermission) GRPCStatus() *status.Status { // ErrCaveatNotFound indicates that a caveat referenced in a relationship update was not found. type ErrCaveatNotFound struct { error - tuple *core.RelationTuple + relationship tuple.Relationship } // NewCaveatNotFoundError constructs a new caveat not found error. -func NewCaveatNotFoundError(update *core.RelationTuple) ErrCaveatNotFound { +func NewCaveatNotFoundError(relationship tuple.Relationship) ErrCaveatNotFound { return ErrCaveatNotFound{ error: fmt.Errorf( "the caveat `%s` was not found for relationship `%s`", - update.Caveat.CaveatName, - tuple.MustString(update), + relationship.OptionalCaveat.CaveatName, + tuple.MustString(relationship), ), - tuple: update, + relationship: relationship, } } @@ -187,7 +187,7 @@ func (err ErrCaveatNotFound) GRPCStatus() *status.Status { spiceerrors.ForReason( v1.ErrorReason_ERROR_REASON_UNKNOWN_CAVEAT, map[string]string{ - "caveat_name": err.tuple.Caveat.CaveatName, + "caveat_name": err.relationship.OptionalCaveat.CaveatName, }, ), ) diff --git a/internal/relationships/validation.go b/internal/relationships/validation.go index 27ebc0e827..069106f88b 100644 --- a/internal/relationships/validation.go +++ b/internal/relationships/validation.go @@ -21,10 +21,10 @@ import ( func ValidateRelationshipUpdates( ctx context.Context, reader datastore.Reader, - updates []*core.RelationTupleUpdate, + updates []tuple.RelationshipUpdate, ) error { - rels := lo.Map(updates, func(item *core.RelationTupleUpdate, _ int) *core.RelationTuple { - return item.Tuple + rels := lo.Map(updates, func(item tuple.RelationshipUpdate, _ int) tuple.Relationship { + return item.Relationship }) // Load namespaces and caveats. @@ -36,14 +36,14 @@ func ValidateRelationshipUpdates( // Validate each updates's types. for _, update := range updates { option := ValidateRelationshipForCreateOrTouch - if update.Operation == core.RelationTupleUpdate_DELETE { + if update.Operation == tuple.UpdateOperationDelete { option = ValidateRelationshipForDeletion } if err := ValidateOneRelationship( referencedNamespaceMap, referencedCaveatMap, - update.Tuple, + update.Relationship, option, ); err != nil { return err @@ -60,7 +60,7 @@ func ValidateRelationshipUpdates( func ValidateRelationshipsForCreateOrTouch( ctx context.Context, reader datastore.Reader, - rels []*core.RelationTuple, + rels ...tuple.Relationship, ) error { // Load namespaces and caveats. referencedNamespaceMap, referencedCaveatMap, err := loadNamespacesAndCaveats(ctx, rels, reader) @@ -83,14 +83,14 @@ func ValidateRelationshipsForCreateOrTouch( return nil } -func loadNamespacesAndCaveats(ctx context.Context, rels []*core.RelationTuple, reader datastore.Reader) (map[string]*typesystem.TypeSystem, map[string]*core.CaveatDefinition, error) { +func loadNamespacesAndCaveats(ctx context.Context, rels []tuple.Relationship, reader datastore.Reader) (map[string]*typesystem.TypeSystem, map[string]*core.CaveatDefinition, error) { referencedNamespaceNames := mapz.NewSet[string]() referencedCaveatNamesWithContext := mapz.NewSet[string]() for _, rel := range rels { - referencedNamespaceNames.Insert(rel.ResourceAndRelation.Namespace) - referencedNamespaceNames.Insert(rel.Subject.Namespace) + referencedNamespaceNames.Insert(rel.Resource.ObjectType) + referencedNamespaceNames.Insert(rel.Subject.ObjectType) if hasNonEmptyCaveatContext(rel) { - referencedCaveatNamesWithContext.Insert(rel.Caveat.CaveatName) + referencedCaveatNamesWithContext.Insert(rel.OptionalCaveat.CaveatName) } } @@ -143,57 +143,57 @@ const ( func ValidateOneRelationship( namespaceMap map[string]*typesystem.TypeSystem, caveatMap map[string]*core.CaveatDefinition, - rel *core.RelationTuple, + rel tuple.Relationship, rule ValidationRelationshipRule, ) error { // Validate the IDs of the resource and subject. - if err := tuple.ValidateResourceID(rel.ResourceAndRelation.ObjectId); err != nil { + if err := tuple.ValidateResourceID(rel.Resource.ObjectID); err != nil { return err } - if err := tuple.ValidateSubjectID(rel.Subject.ObjectId); err != nil { + if err := tuple.ValidateSubjectID(rel.Subject.ObjectID); err != nil { return err } // Validate the namespace and relation for the resource. - resourceTS, ok := namespaceMap[rel.ResourceAndRelation.Namespace] + resourceTS, ok := namespaceMap[rel.Resource.ObjectType] if !ok { - return namespace.NewNamespaceNotFoundErr(rel.ResourceAndRelation.Namespace) + return namespace.NewNamespaceNotFoundErr(rel.Resource.ObjectType) } - if !resourceTS.HasRelation(rel.ResourceAndRelation.Relation) { - return namespace.NewRelationNotFoundErr(rel.ResourceAndRelation.Namespace, rel.ResourceAndRelation.Relation) + if !resourceTS.HasRelation(rel.Resource.Relation) { + return namespace.NewRelationNotFoundErr(rel.Resource.ObjectType, rel.Resource.Relation) } // Validate the namespace and relation for the subject. - subjectTS, ok := namespaceMap[rel.Subject.Namespace] + subjectTS, ok := namespaceMap[rel.Subject.ObjectType] if !ok { - return namespace.NewNamespaceNotFoundErr(rel.Subject.Namespace) + return namespace.NewNamespaceNotFoundErr(rel.Subject.ObjectType) } if rel.Subject.Relation != tuple.Ellipsis { if !subjectTS.HasRelation(rel.Subject.Relation) { - return namespace.NewRelationNotFoundErr(rel.Subject.Namespace, rel.Subject.Relation) + return namespace.NewRelationNotFoundErr(rel.Subject.ObjectType, rel.Subject.Relation) } } // Validate that the relationship is not writing to a permission. - if resourceTS.IsPermission(rel.ResourceAndRelation.Relation) { + if resourceTS.IsPermission(rel.Resource.Relation) { return NewCannotWriteToPermissionError(rel) } // Validate the subject against the allowed relation(s). var caveat *core.AllowedCaveat - if rel.Caveat != nil { - caveat = ns.AllowedCaveat(rel.Caveat.CaveatName) + if rel.OptionalCaveat != nil { + caveat = ns.AllowedCaveat(rel.OptionalCaveat.CaveatName) } var relationToCheck *core.AllowedRelation - if rel.Subject.ObjectId == tuple.PublicWildcard { - relationToCheck = ns.AllowedPublicNamespaceWithCaveat(rel.Subject.Namespace, caveat) + if rel.Subject.ObjectID == tuple.PublicWildcard { + relationToCheck = ns.AllowedPublicNamespaceWithCaveat(rel.Subject.ObjectType, caveat) } else { relationToCheck = ns.AllowedRelationWithCaveat( - rel.Subject.Namespace, + rel.Subject.ObjectType, rel.Subject.Relation, caveat) } @@ -202,7 +202,7 @@ func ValidateOneRelationship( case rule == ValidateRelationshipForCreateOrTouch || caveat != nil: // For writing or when the caveat was specified, the caveat must be a direct match. isAllowed, err := resourceTS.HasAllowedRelation( - rel.ResourceAndRelation.Relation, + rel.Resource.Relation, relationToCheck) if err != nil { return err @@ -214,8 +214,8 @@ func ValidateOneRelationship( case rule == ValidateRelationshipForDeletion && caveat == nil: // For deletion, the caveat *can* be ignored if not specified. - if rel.Subject.ObjectId == tuple.PublicWildcard { - isAllowed, err := resourceTS.IsAllowedPublicNamespace(rel.ResourceAndRelation.Relation, rel.Subject.Namespace) + if rel.Subject.ObjectID == tuple.PublicWildcard { + isAllowed, err := resourceTS.IsAllowedPublicNamespace(rel.Resource.Relation, rel.Subject.ObjectType) if err != nil { return err } @@ -224,7 +224,7 @@ func ValidateOneRelationship( return NewInvalidSubjectTypeError(rel, relationToCheck, resourceTS) } } else { - isAllowed, err := resourceTS.IsAllowedDirectRelation(rel.ResourceAndRelation.Relation, rel.Subject.Namespace, rel.Subject.Relation) + isAllowed, err := resourceTS.IsAllowedDirectRelation(rel.Resource.Relation, rel.Subject.ObjectType, rel.Subject.Relation) if err != nil { return err } @@ -240,7 +240,7 @@ func ValidateOneRelationship( // Validate caveat and its context, if applicable. if hasNonEmptyCaveatContext(rel) { - caveat, ok := caveatMap[rel.Caveat.CaveatName] + caveat, ok := caveatMap[rel.OptionalCaveat.CaveatName] if !ok { // Should ideally never happen since the caveat is type checked above, but just in case. return NewCaveatNotFoundError(rel) @@ -248,7 +248,7 @@ func ValidateOneRelationship( // Verify that the provided context information matches the types of the parameters defined. _, err := caveats.ConvertContextToParameters( - rel.Caveat.Context.AsMap(), + rel.OptionalCaveat.Context.AsMap(), caveat.ParameterTypes, caveats.ErrorForUnknownParameters, ) @@ -260,9 +260,9 @@ func ValidateOneRelationship( return nil } -func hasNonEmptyCaveatContext(update *core.RelationTuple) bool { - return update.Caveat != nil && - update.Caveat.CaveatName != "" && - update.Caveat.Context != nil && - len(update.Caveat.Context.GetFields()) > 0 +func hasNonEmptyCaveatContext(relationship tuple.Relationship) bool { + return relationship.OptionalCaveat != nil && + relationship.OptionalCaveat.CaveatName != "" && + relationship.OptionalCaveat.Context != nil && + len(relationship.OptionalCaveat.Context.GetFields()) > 0 } diff --git a/internal/relationships/validation_test.go b/internal/relationships/validation_test.go index a7d42430a9..f487fd006e 100644 --- a/internal/relationships/validation_test.go +++ b/internal/relationships/validation_test.go @@ -259,7 +259,7 @@ func TestValidateRelationshipOperations(t *testing.T) { } // Validate update. - err = ValidateRelationshipUpdates(context.Background(), reader, []*core.RelationTupleUpdate{ + err = ValidateRelationshipUpdates(context.Background(), reader, []tuple.RelationshipUpdate{ op(tuple.MustParse(tc.relationship)), }) if tc.expectedError != "" { @@ -270,9 +270,7 @@ func TestValidateRelationshipOperations(t *testing.T) { // Validate create/touch. if tc.operation != core.RelationTupleUpdate_DELETE { - err = ValidateRelationshipsForCreateOrTouch(context.Background(), reader, []*core.RelationTuple{ - tuple.MustParse(tc.relationship), - }) + err = ValidateRelationshipsForCreateOrTouch(context.Background(), reader, tuple.MustParse(tc.relationship)) if tc.expectedError != "" { req.ErrorContains(err, tc.expectedError) } else { diff --git a/internal/services/integrationtesting/benchmark_test.go b/internal/services/integrationtesting/benchmark_test.go index ed78628b5c..a639890d69 100644 --- a/internal/services/integrationtesting/benchmark_test.go +++ b/internal/services/integrationtesting/benchmark_test.go @@ -20,7 +20,6 @@ import ( "github.com/authzed/spicedb/internal/testserver/datastore/config" dsconfig "github.com/authzed/spicedb/pkg/cmd/datastore" "github.com/authzed/spicedb/pkg/datastore" - core "github.com/authzed/spicedb/pkg/proto/core/v1" "github.com/authzed/spicedb/pkg/tuple" "github.com/authzed/spicedb/pkg/validationfile" ) @@ -38,13 +37,13 @@ func BenchmarkServices(b *testing.B) { "basic lookup of view for a user", "testconfigs/basicrbac.yaml", func(ctx context.Context, b *testing.B, tester consistencytestutil.ServiceTester, revision datastore.Revision) error { - results, _, err := tester.LookupResources(ctx, &core.RelationReference{ - Namespace: "example/document", - Relation: "view", - }, &core.ObjectAndRelation{ - Namespace: "example/user", - ObjectId: "tom", - Relation: tuple.Ellipsis, + results, _, err := tester.LookupResources(ctx, tuple.RelationReference{ + ObjectType: "example/document", + Relation: "view", + }, tuple.ObjectAndRelation{ + ObjectType: "example/user", + ObjectID: "tom", + Relation: tuple.Ellipsis, }, revision, nil, 0, nil) require.GreaterOrEqual(b, len(results), 0) return err @@ -54,13 +53,13 @@ func BenchmarkServices(b *testing.B) { "recursively through groups", "testconfigs/simplerecursive.yaml", func(ctx context.Context, b *testing.B, tester consistencytestutil.ServiceTester, revision datastore.Revision) error { - results, _, err := tester.LookupResources(ctx, &core.RelationReference{ - Namespace: "srrr/resource", - Relation: "viewer", - }, &core.ObjectAndRelation{ - Namespace: "srrr/user", - ObjectId: "someguy", - Relation: tuple.Ellipsis, + results, _, err := tester.LookupResources(ctx, tuple.RelationReference{ + ObjectType: "srrr/resource", + Relation: "viewer", + }, tuple.ObjectAndRelation{ + ObjectType: "srrr/user", + ObjectID: "someguy", + Relation: tuple.Ellipsis, }, revision, nil, 0, nil) require.GreaterOrEqual(b, len(results), 0) return err @@ -70,13 +69,13 @@ func BenchmarkServices(b *testing.B) { "recursively through wide groups", "benchconfigs/widegroups.yaml", func(ctx context.Context, b *testing.B, tester consistencytestutil.ServiceTester, revision datastore.Revision) error { - results, _, err := tester.LookupResources(ctx, &core.RelationReference{ - Namespace: "resource", - Relation: "view", - }, &core.ObjectAndRelation{ - Namespace: "user", - ObjectId: "tom", - Relation: tuple.Ellipsis, + results, _, err := tester.LookupResources(ctx, tuple.RelationReference{ + ObjectType: "resource", + Relation: "view", + }, tuple.ObjectAndRelation{ + ObjectType: "user", + ObjectID: "tom", + Relation: tuple.Ellipsis, }, revision, nil, 0, nil) require.GreaterOrEqual(b, len(results), 0) return err @@ -86,13 +85,13 @@ func BenchmarkServices(b *testing.B) { "lookup with intersection", "benchconfigs/lookupintersection.yaml", func(ctx context.Context, b *testing.B, tester consistencytestutil.ServiceTester, revision datastore.Revision) error { - results, _, err := tester.LookupResources(ctx, &core.RelationReference{ - Namespace: "resource", - Relation: "view", - }, &core.ObjectAndRelation{ - Namespace: "user", - ObjectId: "tom", - Relation: tuple.Ellipsis, + results, _, err := tester.LookupResources(ctx, tuple.RelationReference{ + ObjectType: "resource", + Relation: "view", + }, tuple.ObjectAndRelation{ + ObjectType: "user", + ObjectID: "tom", + Relation: tuple.Ellipsis, }, revision, nil, 0, nil) require.Equal(b, len(results), 499) return err @@ -102,14 +101,14 @@ func BenchmarkServices(b *testing.B) { "basic check for a user", "testconfigs/basicrbac.yaml", func(ctx context.Context, b *testing.B, tester consistencytestutil.ServiceTester, revision datastore.Revision) error { - result, err := tester.Check(ctx, &core.ObjectAndRelation{ - Namespace: "example/document", - ObjectId: "firstdoc", - Relation: "view", - }, &core.ObjectAndRelation{ - Namespace: "example/user", - ObjectId: "tom", - Relation: tuple.Ellipsis, + result, err := tester.Check(ctx, tuple.ObjectAndRelation{ + ObjectType: "example/document", + ObjectID: "firstdoc", + Relation: "view", + }, tuple.ObjectAndRelation{ + ObjectType: "example/user", + ObjectID: "tom", + Relation: tuple.Ellipsis, }, revision, nil) require.Equal(b, v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, result) return err @@ -119,14 +118,14 @@ func BenchmarkServices(b *testing.B) { "recursive check for a user", "testconfigs/quay.yaml", func(ctx context.Context, b *testing.B, tester consistencytestutil.ServiceTester, revision datastore.Revision) error { - result, err := tester.Check(ctx, &core.ObjectAndRelation{ - Namespace: "quay/repo", - ObjectId: "buynlarge/orgrepo", - Relation: "view", - }, &core.ObjectAndRelation{ - Namespace: "quay/user", - ObjectId: "cto", - Relation: tuple.Ellipsis, + result, err := tester.Check(ctx, tuple.ObjectAndRelation{ + ObjectType: "quay/repo", + ObjectID: "buynlarge/orgrepo", + Relation: "view", + }, tuple.ObjectAndRelation{ + ObjectType: "quay/user", + ObjectID: "cto", + Relation: tuple.Ellipsis, }, revision, nil) require.Equal(b, v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, result) return err @@ -136,14 +135,14 @@ func BenchmarkServices(b *testing.B) { "wide groups check for a user", "benchconfigs/checkwidegroups.yaml", func(ctx context.Context, b *testing.B, tester consistencytestutil.ServiceTester, revision datastore.Revision) error { - result, err := tester.Check(ctx, &core.ObjectAndRelation{ - Namespace: "resource", - ObjectId: "someresource", - Relation: "view", - }, &core.ObjectAndRelation{ - Namespace: "user", - ObjectId: "tom", - Relation: tuple.Ellipsis, + result, err := tester.Check(ctx, tuple.ObjectAndRelation{ + ObjectType: "resource", + ObjectID: "someresource", + Relation: "view", + }, tuple.ObjectAndRelation{ + ObjectType: "user", + ObjectID: "tom", + Relation: tuple.Ellipsis, }, revision, nil) require.Equal(b, v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, result) return err @@ -153,14 +152,14 @@ func BenchmarkServices(b *testing.B) { "wide direct relation check", "benchconfigs/checkwidedirect.yaml", func(ctx context.Context, b *testing.B, tester consistencytestutil.ServiceTester, revision datastore.Revision) error { - result, err := tester.Check(ctx, &core.ObjectAndRelation{ - Namespace: "resource", - ObjectId: "someresource", - Relation: "view", - }, &core.ObjectAndRelation{ - Namespace: "user", - ObjectId: "tom", - Relation: tuple.Ellipsis, + result, err := tester.Check(ctx, tuple.ObjectAndRelation{ + ObjectType: "resource", + ObjectID: "someresource", + Relation: "view", + }, tuple.ObjectAndRelation{ + ObjectType: "user", + ObjectID: "tom", + Relation: tuple.Ellipsis, }, revision, nil) require.Equal(b, v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, result) return err diff --git a/internal/services/integrationtesting/cert_test.go b/internal/services/integrationtesting/cert_test.go index 7b30b511b2..9f95dc6cd4 100644 --- a/internal/services/integrationtesting/cert_test.go +++ b/internal/services/integrationtesting/cert_test.go @@ -207,7 +207,7 @@ func TestCertRotation(t *testing.T) { }() // requests work with the old key client := v1.NewPermissionsServiceClient(conn) - rel := tuple.MustToRelationship(tuple.Parse(tf.StandardTuples[0])) + rel := tuple.ToV1Relationship(tuple.MustParse(tf.StandardRelationships[0])) _, err = client.CheckPermission(ctx, &v1.CheckPermissionRequest{ Consistency: &v1.Consistency{ Requirement: &v1.Consistency_AtLeastAsFresh{ diff --git a/internal/services/integrationtesting/consistency_test.go b/internal/services/integrationtesting/consistency_test.go index 21545a4dfb..b68a0d3dcb 100644 --- a/internal/services/integrationtesting/consistency_test.go +++ b/internal/services/integrationtesting/consistency_test.go @@ -171,11 +171,11 @@ func testForEachRelationship( t *testing.T, vctx validationContext, prefix string, - handler func(t *testing.T, relationship *core.RelationTuple), + handler func(t *testing.T, relationship tuple.Relationship), ) { t.Helper() - for _, relationship := range vctx.clusterAndData.Populated.Tuples { + for _, relationship := range vctx.clusterAndData.Populated.Relationships { relationship := relationship t.Run(fmt.Sprintf("%s_%s", prefix, tuple.MustString(relationship)), func(t *testing.T) { @@ -189,7 +189,7 @@ func testForEachResource( t *testing.T, vctx validationContext, prefix string, - handler func(t *testing.T, resource *core.ObjectAndRelation), + handler func(t *testing.T, resource tuple.ObjectAndRelation), ) { t.Helper() @@ -204,12 +204,12 @@ func testForEachResource( relation := relation for _, resource := range resources { resource := resource - t.Run(fmt.Sprintf("%s_%s_%s_%s", prefix, resourceType.Name, resource.ObjectId, relation.Name), + t.Run(fmt.Sprintf("%s_%s_%s_%s", prefix, resourceType.Name, resource.ObjectID, relation.Name), func(t *testing.T) { - handler(t, &core.ObjectAndRelation{ - Namespace: resourceType.Name, - ObjectId: resource.ObjectId, - Relation: relation.Name, + handler(t, tuple.ObjectAndRelation{ + ObjectType: resourceType.Name, + ObjectID: resource.ObjectID, + Relation: relation.Name, }) }) } @@ -222,7 +222,7 @@ func testForEachResourceType( t *testing.T, vctx validationContext, prefix string, - handler func(t *testing.T, resourceType *core.RelationReference), + handler func(t *testing.T, resourceType tuple.RelationReference), ) { for _, resourceType := range vctx.clusterAndData.Populated.NamespaceDefinitions { resourceType := resourceType @@ -230,9 +230,9 @@ func testForEachResourceType( relation := relation t.Run(fmt.Sprintf("%s_%s_%s_", prefix, resourceType.Name, relation.Name), func(t *testing.T) { - handler(t, &core.RelationReference{ - Namespace: resourceType.Name, - Relation: relation.Name, + handler(t, tuple.RelationReference{ + ObjectType: resourceType.Name, + Relation: relation.Name, }) }) } @@ -256,9 +256,9 @@ func ensureRelationshipWrites(t *testing.T, vctx validationContext) { // validateRelationshipReads ensures that all defined relationships are returned by the Read API. func validateRelationshipReads(t *testing.T, vctx validationContext) { - testForEachRelationship(t, vctx, "read", func(t *testing.T, relationship *core.RelationTuple) { + testForEachRelationship(t, vctx, "read", func(t *testing.T, relationship tuple.Relationship) { foundRelationships, err := vctx.serviceTester.Read(context.Background(), - relationship.ResourceAndRelation.Namespace, + relationship.Resource.ObjectType, vctx.revision, ) require.NoError(t, err) @@ -275,7 +275,7 @@ func validateRelationshipReads(t *testing.T, vctx validationContext) { // ensureNoExpansionErrors runs basic expansion on each relation and ensures no errors are raised. func ensureNoExpansionErrors(t *testing.T, vctx validationContext) { testForEachResource(t, vctx, "run_expand", - func(t *testing.T, resource *core.ObjectAndRelation) { + func(t *testing.T, resource tuple.ObjectAndRelation) { _, err := vctx.serviceTester.Expand(context.Background(), resource, vctx.revision, @@ -287,12 +287,12 @@ func ensureNoExpansionErrors(t *testing.T, vctx validationContext) { // validateExpansionSubjects runs a fully recursive expand on each relation and ensures that all expected terminal subjects are reached. func validateExpansionSubjects(t *testing.T, vctx validationContext) { testForEachResource(t, vctx, "validate_expand", - func(t *testing.T, resource *core.ObjectAndRelation) { + func(t *testing.T, resource tuple.ObjectAndRelation) { // Run a *recursive* expansion to collect all the reachable subjects. resp, err := vctx.dispatcher.DispatchExpand( vctx.clusterAndData.Ctx, &dispatchv1.DispatchExpandRequest{ - ResourceAndRelation: resource, + ResourceAndRelation: resource.ToCoreONR(), Metadata: &dispatchv1.ResolverMeta{ AtRevision: vctx.revision.String(), DepthRemaining: 100, @@ -364,7 +364,7 @@ func validateLookupResources(t *testing.T, vctx validationContext) { // Run a lookup resources for each resource type and ensure that the returned objects are those // that are accessible to the subject. testForEachResourceType(t, vctx, "validate_lookup_resources", - func(t *testing.T, resourceRelation *core.RelationReference) { + func(t *testing.T, resourceRelation tuple.RelationReference) { for _, subject := range vctx.accessibilitySet.AllSubjectsNoWildcards() { subject := subject t.Run(tuple.StringONR(subject), func(t *testing.T) { @@ -404,10 +404,10 @@ func validateLookupResources(t *testing.T, vctx validationContext) { for _, resolvedResource := range resolvedResources { permissionship, err := vctx.serviceTester.Check(context.Background(), - &core.ObjectAndRelation{ - Namespace: resourceRelation.Namespace, - Relation: resourceRelation.Relation, - ObjectId: resolvedResource.ResourceObjectId, + tuple.ObjectAndRelation{ + ObjectType: resourceRelation.ObjectType, + ObjectID: resolvedResource.ResourceObjectId, + Relation: resourceRelation.Relation, }, subject, vctx.revision, @@ -426,7 +426,7 @@ func validateLookupResources(t *testing.T, vctx validationContext) { expectedPermissionship, permissionship, "Found Check failure for relation %s:%s#%s and subject %s in lookup resources; expected %v, found %v", - resourceRelation.Namespace, + resourceRelation.ObjectType, resolvedResource.ResourceObjectId, resourceRelation.Relation, tuple.StringONR(subject), @@ -436,14 +436,14 @@ func validateLookupResources(t *testing.T, vctx validationContext) { checkBulkItems = append(checkBulkItems, &v1.CheckBulkPermissionsRequestItem{ Resource: &v1.ObjectReference{ - ObjectType: resourceRelation.Namespace, + ObjectType: resourceRelation.ObjectType, ObjectId: resolvedResource.ResourceObjectId, }, Permission: resourceRelation.Relation, Subject: &v1.SubjectReference{ Object: &v1.ObjectReference{ - ObjectType: subject.Namespace, - ObjectId: subject.ObjectId, + ObjectType: subject.ObjectType, + ObjectId: subject.ObjectID, }, OptionalRelation: stringz.Default(subject.Relation, "", tuple.Ellipsis), }, @@ -469,10 +469,10 @@ func validateLookupResources(t *testing.T, vctx validationContext) { // validateLookupSubjects validates that the subjects that can access it are those expected. func validateLookupSubjects(t *testing.T, vctx validationContext) { testForEachResource(t, vctx, "validate_lookup_subjects", - func(t *testing.T, resource *core.ObjectAndRelation) { + func(t *testing.T, resource tuple.ObjectAndRelation) { for _, subjectType := range vctx.accessibilitySet.SubjectTypes() { subjectType := subjectType - t.Run(fmt.Sprintf("%s#%s", subjectType.Namespace, subjectType.Relation), + t.Run(fmt.Sprintf("%s#%s", subjectType.ObjectType, subjectType.Relation), func(t *testing.T) { resolvedSubjects, err := vctx.serviceTester.LookupSubjects(context.Background(), resource, subjectType, vctx.revision, nil) require.NoError(t, err) @@ -501,12 +501,12 @@ func validateLookupSubjects(t *testing.T, vctx validationContext) { }, } { for _, assertion := range entry.assertions { - assertionRel := tuple.MustFromRelationship[*v1.ObjectReference, *v1.SubjectReference, *v1.ContextualizedCaveat](assertion.Relationship) - if !assertionRel.ResourceAndRelation.EqualVT(resource) { + assertionRel := assertion.Relationship + if !tuple.ONREqual(assertionRel.Resource, resource) { continue } - if assertionRel.Subject.Namespace != subjectType.Namespace || + if assertionRel.Subject.ObjectType != subjectType.ObjectType || assertionRel.Subject.Relation != subjectType.Relation { continue } @@ -537,16 +537,16 @@ func validateLookupSubjects(t *testing.T, vctx validationContext) { // can be caveated. for _, excludedSubject := range resolvedSubject.ExcludedSubjects { if entry.requiresPermission { - require.NotEqual(t, excludedSubject.SubjectObjectId, assertionRel.Subject.ObjectId, "wildcard excludes the asserted subject ID: %s", assertionRel.Subject.ObjectId) - } else if excludedSubject.SubjectObjectId == assertionRel.Subject.ObjectId { - require.NotEqual(t, v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_HAS_PERMISSION, excludedSubject.Permissionship, "wildcard concretely excludes the asserted subject ID: %s", assertionRel.Subject.ObjectId) + require.NotEqual(t, excludedSubject.SubjectObjectId, assertionRel.Subject.ObjectID, "wildcard excludes the asserted subject ID: %s", assertionRel.Subject.ObjectID) + } else if excludedSubject.SubjectObjectId == assertionRel.Subject.ObjectID { + require.NotEqual(t, v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_HAS_PERMISSION, excludedSubject.Permissionship, "wildcard concretely excludes the asserted subject ID: %s", assertionRel.Subject.ObjectID) } } continue } - _, ok = resolvedSubjects[assertionRel.Subject.ObjectId] - require.True(t, ok, "missing expected subject %s from assertion %s", assertionRel.Subject.ObjectId, assertion.RelationshipWithContextString) + _, ok = resolvedSubjects[assertionRel.Subject.ObjectID] + require.True(t, ok, "missing expected subject %s from assertion %s", assertionRel.Subject.ObjectID, assertion.RelationshipWithContextString) } } } @@ -560,10 +560,10 @@ func validateLookupSubjects(t *testing.T, vctx validationContext) { for _, excludedSubject := range resolvedSubject.ExcludedSubjects { permissionship, err := vctx.serviceTester.Check(context.Background(), resource, - &core.ObjectAndRelation{ - Namespace: subjectType.Namespace, - ObjectId: excludedSubject.SubjectObjectId, - Relation: subjectType.Relation, + tuple.ObjectAndRelation{ + ObjectType: subjectType.ObjectType, + ObjectID: excludedSubject.SubjectObjectId, + Relation: subjectType.Relation, }, vctx.revision, nil, @@ -594,10 +594,10 @@ func validateLookupSubjects(t *testing.T, vctx validationContext) { continue } - subject := &core.ObjectAndRelation{ - Namespace: subjectType.Namespace, - ObjectId: resolvedSubject.Subject.SubjectObjectId, - Relation: subjectType.Relation, + subject := tuple.ObjectAndRelation{ + ObjectType: subjectType.ObjectType, + ObjectID: resolvedSubject.Subject.SubjectObjectId, + Relation: subjectType.Relation, } permissionship, err := vctx.serviceTester.Check(context.Background(), @@ -664,26 +664,25 @@ func runAssertions(t *testing.T, vctx validationContext) { caveatContext = built } + rel := tuple.ToV1Relationship(assertion.Relationship) + bulkCheckItems = append(bulkCheckItems, &v1.BulkCheckPermissionRequestItem{ - Resource: assertion.Relationship.Resource, - Permission: assertion.Relationship.Relation, - Subject: assertion.Relationship.Subject, + Resource: rel.Resource, + Permission: rel.Relation, + Subject: rel.Subject, Context: caveatContext, }) // Run each individual assertion. assertion := assertion t.Run(assertion.RelationshipWithContextString, func(t *testing.T) { - rel := tuple.MustFromRelationship[*v1.ObjectReference, *v1.SubjectReference, *v1.ContextualizedCaveat](assertion.Relationship) - permissionship, err := vctx.serviceTester.Check(context.Background(), rel.ResourceAndRelation, rel.Subject, vctx.revision, assertion.CaveatContext) + rel := assertion.Relationship + permissionship, err := vctx.serviceTester.Check(context.Background(), rel.Resource, rel.Subject, vctx.revision, assertion.CaveatContext) require.NoError(t, err) require.Equal(t, entry.expectedPermissionship, permissionship, "Assertion `%s` returned %s; expected %s", tuple.MustString(rel), permissionship, entry.expectedPermissionship) // Ensure the assertion passes LookupResources with context, directly. - resolvedDirectResources, _, err := vctx.serviceTester.LookupResources(context.Background(), &core.RelationReference{ - Namespace: rel.ResourceAndRelation.Namespace, - Relation: rel.ResourceAndRelation.Relation, - }, rel.Subject, vctx.revision, nil, 0, assertion.CaveatContext) + resolvedDirectResources, _, err := vctx.serviceTester.LookupResources(context.Background(), rel.Resource.RelationReference(), rel.Subject, vctx.revision, nil, 0, assertion.CaveatContext) require.NoError(t, err) resolvedDirectResourcesMap := map[string]*v1.LookupResourcesResponse{} @@ -692,10 +691,7 @@ func runAssertions(t *testing.T, vctx validationContext) { } // Ensure the assertion passes LookupResources without context, indirectly. - resolvedIndirectResources, _, err := vctx.serviceTester.LookupResources(context.Background(), &core.RelationReference{ - Namespace: rel.ResourceAndRelation.Namespace, - Relation: rel.ResourceAndRelation.Relation, - }, rel.Subject, vctx.revision, nil, 0, nil) + resolvedIndirectResources, _, err := vctx.serviceTester.LookupResources(context.Background(), rel.Resource.RelationReference(), rel.Subject, vctx.revision, nil, 0, nil) require.NoError(t, err) resolvedIndirectResourcesMap := map[string]*v1.LookupResourcesResponse{} @@ -707,45 +703,45 @@ func runAssertions(t *testing.T, vctx validationContext) { resolvedDirectResourceIds := maps.Keys(resolvedDirectResourcesMap) switch permissionship { case v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION: - require.NotContains(t, resolvedDirectResourceIds, rel.ResourceAndRelation.ObjectId, "Found unexpected object %s in direct lookup for assertion %s", rel.ResourceAndRelation, rel) + require.NotContains(t, resolvedDirectResourceIds, rel.Resource.ObjectID, "Found unexpected object %s in direct lookup for assertion %s", rel.Resource, rel) case v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION: - require.Contains(t, resolvedDirectResourceIds, rel.ResourceAndRelation.ObjectId, "Missing object %s in lookup for assertion %s", rel.ResourceAndRelation, rel) - require.Equal(t, v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_HAS_PERMISSION, resolvedDirectResourcesMap[rel.ResourceAndRelation.ObjectId].Permissionship) + require.Contains(t, resolvedDirectResourceIds, rel.Resource.ObjectID, "Missing object %s in lookup for assertion %s", rel.Resource, rel) + require.Equal(t, v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_HAS_PERMISSION, resolvedDirectResourcesMap[rel.Resource.ObjectID].Permissionship) case v1.CheckPermissionResponse_PERMISSIONSHIP_CONDITIONAL_PERMISSION: - require.Contains(t, resolvedDirectResourceIds, rel.ResourceAndRelation.ObjectId, "Missing object %s in lookup for assertion %s", rel.ResourceAndRelation, rel) - require.Equal(t, v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_CONDITIONAL_PERMISSION, resolvedDirectResourcesMap[rel.ResourceAndRelation.ObjectId].Permissionship) + require.Contains(t, resolvedDirectResourceIds, rel.Resource.ObjectID, "Missing object %s in lookup for assertion %s", rel.Resource, rel) + require.Equal(t, v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_CONDITIONAL_PERMISSION, resolvedDirectResourcesMap[rel.Resource.ObjectID].Permissionship) } // Check the assertion was returned for an indirect (without context) lookup. resolvedIndirectResourceIds := maps.Keys(resolvedIndirectResourcesMap) - accessibility, _, _ := vctx.accessibilitySet.AccessibiliyAndPermissionshipFor(rel.ResourceAndRelation, rel.Subject) + accessibility, _, _ := vctx.accessibilitySet.AccessibiliyAndPermissionshipFor(rel.Resource, rel.Subject) switch permissionship { case v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION: // If the caveat context given is empty, then the lookup result must not exist at all. // Otherwise, it *could* be caveated or not exist, depending on the context given. if len(assertion.CaveatContext) == 0 { - require.NotContains(t, resolvedIndirectResourceIds, rel.ResourceAndRelation.ObjectId, "Found unexpected object %s in indirect lookup for assertion %s", rel.ResourceAndRelation, rel) + require.NotContains(t, resolvedIndirectResourceIds, rel.Resource.ObjectID, "Found unexpected object %s in indirect lookup for assertion %s", rel.Resource, rel) } else if accessibility == consistencytestutil.NotAccessible { - found, ok := resolvedIndirectResourcesMap[rel.ResourceAndRelation.ObjectId] + found, ok := resolvedIndirectResourcesMap[rel.Resource.ObjectID] 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 } else if accessibility != consistencytestutil.NotAccessibleDueToPrespecifiedCaveat { - require.Equal(t, v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_CONDITIONAL_PERMISSION, resolvedIndirectResourcesMap[rel.ResourceAndRelation.ObjectId].Permissionship) + require.Equal(t, v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_CONDITIONAL_PERMISSION, resolvedIndirectResourcesMap[rel.Resource.ObjectID].Permissionship) } case v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION: - require.Contains(t, resolvedIndirectResourceIds, rel.ResourceAndRelation.ObjectId, "Missing object %s in lookup for assertion %s", rel.ResourceAndRelation, rel) + require.Contains(t, resolvedIndirectResourceIds, rel.Resource.ObjectID, "Missing object %s in lookup for assertion %s", rel.Resource, rel) // If the caveat context given is empty, then the lookup result must be fully permissioned. // Otherwise, it *could* be caveated or fully permissioned, depending on the context given. if len(assertion.CaveatContext) == 0 { - require.Equal(t, v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_HAS_PERMISSION, resolvedIndirectResourcesMap[rel.ResourceAndRelation.ObjectId].Permissionship) + require.Equal(t, v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_HAS_PERMISSION, resolvedIndirectResourcesMap[rel.Resource.ObjectID].Permissionship) } case v1.CheckPermissionResponse_PERMISSIONSHIP_CONDITIONAL_PERMISSION: - require.Contains(t, resolvedIndirectResourceIds, rel.ResourceAndRelation.ObjectId, "Missing object %s in lookup for assertion %s", rel.ResourceAndRelation, rel) - require.Equal(t, v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_CONDITIONAL_PERMISSION, resolvedIndirectResourcesMap[rel.ResourceAndRelation.ObjectId].Permissionship) + require.Contains(t, resolvedIndirectResourceIds, rel.Resource.ObjectID, "Missing object %s in lookup for assertion %s", rel.Resource, rel) + require.Equal(t, v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_CONDITIONAL_PERMISSION, resolvedIndirectResourcesMap[rel.Resource.ObjectID].Permissionship) default: panic("unknown permissionship") @@ -769,9 +765,14 @@ func runAssertions(t *testing.T, vctx validationContext) { // validateDevelopment runs the development package against the validation context and // ensures its output matches that expected. func validateDevelopment(t *testing.T, vctx validationContext) { + rels := make([]*core.RelationTuple, 0, len(vctx.clusterAndData.Populated.Relationships)) + for _, rel := range vctx.clusterAndData.Populated.Relationships { + rels = append(rels, rel.ToCoreTuple()) + } + reqContext := &devinterface.RequestContext{ Schema: vctx.clusterAndData.Populated.Schema, - Relationships: vctx.clusterAndData.Populated.Tuples, + Relationships: rels, } devContext, _, err := development.NewDevContext(context.Background(), reqContext) @@ -791,7 +792,7 @@ func validateDevelopment(t *testing.T, vctx validationContext) { // returns the expected permissionship. func validateDevelopmentChecks(t *testing.T, devContext *development.DevContext, vctx validationContext) { testForEachResource(t, vctx, "validate_check_watch", - func(t *testing.T, resource *core.ObjectAndRelation) { + func(t *testing.T, resource tuple.ObjectAndRelation) { for _, subject := range vctx.accessibilitySet.AllSubjectsNoWildcards() { subject := subject t.Run(tuple.StringONR(subject), func(t *testing.T) { @@ -861,7 +862,7 @@ func validateDevelopmentExpectedRels(t *testing.T, devContext *development.DevCo } relationship := tuple.MustParse(relString) - expectedMap[tuple.StringONR(relationship.ResourceAndRelation)] = []string{} + expectedMap[tuple.StringONR(relationship.Resource)] = []string{} } expectedRelations, err := yamlv2.Marshal(expectedMap) @@ -888,7 +889,7 @@ func validateDevelopmentExpectedRels(t *testing.T, devContext *development.DevCo require.NotNil(t, subjectWithExceptions, "Found expected relation without subject: %s", expectedSubject.ValidationString) // For non-wildcard subjects, ensure they are accessible. - if subjectWithExceptions.Subject.Subject.ObjectId != tuple.PublicWildcard { + if subjectWithExceptions.Subject.Subject.ObjectID != tuple.PublicWildcard { accessibility, permissionship, ok := vctx.accessibilitySet.AccessibiliyAndPermissionshipFor(resourceAndRelation, subjectWithExceptions.Subject.Subject) require.True(t, ok, "missing expected subject %s in accessibility set", tuple.StringONR(subjectWithExceptions.Subject.Subject)) diff --git a/internal/services/integrationtesting/consistencytestutil/accessibilityset.go b/internal/services/integrationtesting/consistencytestutil/accessibilityset.go index 848ec22681..e41f4afc0e 100644 --- a/internal/services/integrationtesting/consistencytestutil/accessibilityset.go +++ b/internal/services/integrationtesting/consistencytestutil/accessibilityset.go @@ -12,7 +12,6 @@ import ( "github.com/authzed/spicedb/internal/graph/computed" "github.com/authzed/spicedb/pkg/datastore" "github.com/authzed/spicedb/pkg/genutil/mapz" - core "github.com/authzed/spicedb/pkg/proto/core/v1" dispatchv1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" "github.com/authzed/spicedb/pkg/tuple" ) @@ -51,13 +50,13 @@ const ( // and subjects found for consistency testing. type AccessibilitySet struct { // ResourcesByNamespace is a multimap of all defined resources, by resource namespace. - ResourcesByNamespace *mapz.MultiMap[string, *core.ObjectAndRelation] + ResourcesByNamespace *mapz.MultiMap[string, tuple.ObjectAndRelation] // SubjectsByNamespace is a multimap of all defined subjects, by subject namespace. - SubjectsByNamespace *mapz.MultiMap[string, *core.ObjectAndRelation] + SubjectsByNamespace *mapz.MultiMap[string, tuple.ObjectAndRelation] // RelationshipsByResourceNamespace is a multimap of all defined relationships, by resource namespace. - RelationshipsByResourceNamespace *mapz.MultiMap[string, *core.RelationTuple] + RelationshipsByResourceNamespace *mapz.MultiMap[string, tuple.Relationship] // UncomputedPermissionshipByRelationship is a map from a relationship string of the form // "resourceType:resourceObjectID#permission@subjectType:subjectObjectID" to its @@ -80,19 +79,19 @@ type AccessibilitySet struct { // outside of testing. func BuildAccessibilitySet(t *testing.T, ccd ConsistencyClusterAndData) *AccessibilitySet { // Compute all relationships and objects by namespace. - relsByResourceNamespace := mapz.NewMultiMap[string, *core.RelationTuple]() - resourcesByNamespace := mapz.NewMultiMap[string, *core.ObjectAndRelation]() - subjectsByNamespace := mapz.NewMultiMap[string, *core.ObjectAndRelation]() + relsByResourceNamespace := mapz.NewMultiMap[string, tuple.Relationship]() + resourcesByNamespace := mapz.NewMultiMap[string, tuple.ObjectAndRelation]() + subjectsByNamespace := mapz.NewMultiMap[string, tuple.ObjectAndRelation]() allObjectIds := mapz.NewSet[string]() - for _, tpl := range ccd.Populated.Tuples { - relsByResourceNamespace.Add(tpl.ResourceAndRelation.Namespace, tpl) - resourcesByNamespace.Add(tpl.ResourceAndRelation.Namespace, tpl.ResourceAndRelation) - subjectsByNamespace.Add(tpl.Subject.Namespace, tpl.Subject) - allObjectIds.Add(tpl.ResourceAndRelation.ObjectId) + for _, tpl := range ccd.Populated.Relationships { + relsByResourceNamespace.Add(tpl.Resource.ObjectType, tpl) + resourcesByNamespace.Add(tpl.Resource.ObjectType, tpl.Resource) + subjectsByNamespace.Add(tpl.Subject.ObjectType, tpl.Subject) + allObjectIds.Add(tpl.Resource.ObjectID) - if tpl.Subject.ObjectId != tuple.PublicWildcard { - allObjectIds.Add(tpl.Subject.ObjectId) + if tpl.Subject.ObjectID != tuple.PublicWildcard { + allObjectIds.Add(tpl.Subject.ObjectID) } } @@ -116,19 +115,19 @@ func BuildAccessibilitySet(t *testing.T, ccd ConsistencyClusterAndData) *Accessi for _, possibleResourceID := range allObjectIds.AsSlice() { for _, relationOrPermission := range resourceType.Relation { for _, subject := range subjectsByNamespace.Values() { - if subject.ObjectId == tuple.PublicWildcard { + if subject.ObjectID == tuple.PublicWildcard { continue } - resourceRelation := &core.RelationReference{ - Namespace: resourceType.Name, - Relation: relationOrPermission.Name, + resourceRelation := tuple.RelationReference{ + ObjectType: resourceType.Name, + Relation: relationOrPermission.Name, } results, err := dispatcher.DispatchCheck(ccd.Ctx, &dispatchv1.DispatchCheckRequest{ - ResourceRelation: resourceRelation, + ResourceRelation: resourceRelation.ToCoreRR(), ResourceIds: []string{possibleResourceID}, - Subject: subject, + Subject: subject.ToCoreONR(), ResultsSetting: dispatchv1.DispatchCheckRequest_ALLOW_SINGLE_RESULT, Metadata: &dispatchv1.ResolverMeta{ AtRevision: headRevision.String(), @@ -138,14 +137,16 @@ func BuildAccessibilitySet(t *testing.T, ccd ConsistencyClusterAndData) *Accessi }) require.NoError(t, err) - resourceAndRelation := &core.ObjectAndRelation{ - Namespace: resourceType.Name, - ObjectId: possibleResourceID, - Relation: relationOrPermission.Name, + resourceAndRelation := tuple.ObjectAndRelation{ + ObjectType: resourceType.Name, + ObjectID: possibleResourceID, + Relation: relationOrPermission.Name, } - permString := tuple.MustString(&core.RelationTuple{ - ResourceAndRelation: resourceAndRelation, - Subject: subject, + permString := tuple.MustString(tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: resourceAndRelation, + Subject: subject, + }, }) if result, ok := results.ResultsByResourceId[possibleResourceID]; ok { @@ -181,7 +182,7 @@ func BuildAccessibilitySet(t *testing.T, ccd ConsistencyClusterAndData) *Accessi fallthrough case dispatchv1.ResourceCheckResult_MEMBER: - if resourceAndRelation.EqualVT(subject) { + if tuple.ONREqual(resourceAndRelation, subject) { accessibilityByRelationship[permString] = AccessibleBecauseTheSame } else { if isAccessibleViaWildcardOnly(t, ccd, dispatcher, headRevision, resourceAndRelation, subject) { @@ -216,10 +217,12 @@ func BuildAccessibilitySet(t *testing.T, ccd ConsistencyClusterAndData) *Accessi // UncomputedPermissionshipFor returns the uncomputed permissionship for the given // resource+permission and subject. If not found, returns false. -func (as *AccessibilitySet) UncomputedPermissionshipFor(resourceAndRelation *core.ObjectAndRelation, subject *core.ObjectAndRelation) (dispatchv1.ResourceCheckResult_Membership, bool) { - relString := tuple.MustString(&core.RelationTuple{ - ResourceAndRelation: resourceAndRelation, - Subject: subject, +func (as *AccessibilitySet) UncomputedPermissionshipFor(resourceAndRelation tuple.ObjectAndRelation, subject tuple.ObjectAndRelation) (dispatchv1.ResourceCheckResult_Membership, bool) { + relString := tuple.MustString(tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: resourceAndRelation, + Subject: subject, + }, }) permissionship, ok := as.UncomputedPermissionshipByRelationship[relString] return permissionship, ok @@ -227,10 +230,12 @@ func (as *AccessibilitySet) UncomputedPermissionshipFor(resourceAndRelation *cor // AccessibiliyAndPermissionshipFor returns the computed accessibility and permissionship for the // given resource+permission and subject. If not found, returns false. -func (as *AccessibilitySet) AccessibiliyAndPermissionshipFor(resourceAndRelation *core.ObjectAndRelation, subject *core.ObjectAndRelation) (Accessibility, dispatchv1.ResourceCheckResult_Membership, bool) { - relString := tuple.MustString(&core.RelationTuple{ - ResourceAndRelation: resourceAndRelation, - Subject: subject, +func (as *AccessibilitySet) AccessibiliyAndPermissionshipFor(resourceAndRelation tuple.ObjectAndRelation, subject tuple.ObjectAndRelation) (Accessibility, dispatchv1.ResourceCheckResult_Membership, bool) { + relString := tuple.MustString(tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: resourceAndRelation, + Subject: subject, + }, }) accessibility, ok := as.AccessibilityByRelationship[relString] if !ok { @@ -243,15 +248,15 @@ func (as *AccessibilitySet) AccessibiliyAndPermissionshipFor(resourceAndRelation // DirectlyAccessibleDefinedSubjects returns all subjects that have direct access/permission on the // resource+permission. Direct access is defined as not being granted access via a wildcard. -func (as *AccessibilitySet) DirectlyAccessibleDefinedSubjects(resourceAndRelation *core.ObjectAndRelation) []*core.ObjectAndRelation { - found := make([]*core.ObjectAndRelation, 0) +func (as *AccessibilitySet) DirectlyAccessibleDefinedSubjects(resourceAndRelation tuple.ObjectAndRelation) []tuple.ObjectAndRelation { + found := make([]tuple.ObjectAndRelation, 0) for relString, accessibility := range as.AccessibilityByRelationship { if accessibility != AccessibleDirectly { continue } parsed := tuple.MustParse(relString) - if !parsed.ResourceAndRelation.EqualVT(resourceAndRelation) { + if !tuple.ONREqual(parsed.Resource, resourceAndRelation) { continue } @@ -263,7 +268,7 @@ func (as *AccessibilitySet) DirectlyAccessibleDefinedSubjects(resourceAndRelatio // DirectlyAccessibleDefinedSubjectsOfType returns all subjects that have direct access/permission on the // resource+permission and match the given subject type. // Direct access is defined as not being granted access via a wildcard. -func (as *AccessibilitySet) DirectlyAccessibleDefinedSubjectsOfType(resourceAndRelation *core.ObjectAndRelation, subjectType *core.RelationReference) map[string]ObjectAndPermission { +func (as *AccessibilitySet) DirectlyAccessibleDefinedSubjectsOfType(resourceAndRelation tuple.ObjectAndRelation, subjectType tuple.RelationReference) map[string]ObjectAndPermission { found := map[string]ObjectAndPermission{} for relString, accessibility := range as.AccessibilityByRelationship { // NOTE: we also ignore subjects granted access by being themselves. @@ -272,18 +277,18 @@ func (as *AccessibilitySet) DirectlyAccessibleDefinedSubjectsOfType(resourceAndR } parsed := tuple.MustParse(relString) - if !parsed.ResourceAndRelation.EqualVT(resourceAndRelation) { + if !tuple.ONREqual(parsed.Resource, resourceAndRelation) { continue } - if parsed.Subject.Namespace != subjectType.Namespace || parsed.Subject.Relation != subjectType.Relation { + if parsed.Subject.ObjectType != subjectType.ObjectType || parsed.Subject.Relation != subjectType.Relation { continue } permissionship := as.PermissionshipByRelationship[relString] - found[parsed.Subject.ObjectId] = ObjectAndPermission{ - ObjectID: parsed.Subject.ObjectId, + found[parsed.Subject.ObjectID] = ObjectAndPermission{ + ObjectID: parsed.Subject.ObjectID, IsCaveated: permissionship == dispatchv1.ResourceCheckResult_CAVEATED_MEMBER, } } @@ -291,24 +296,20 @@ func (as *AccessibilitySet) DirectlyAccessibleDefinedSubjectsOfType(resourceAndR } // SubjectTypes returns all *defined* subject types found. -func (as *AccessibilitySet) SubjectTypes() []*core.RelationReference { - subjectTypes := map[string]*core.RelationReference{} +func (as *AccessibilitySet) SubjectTypes() []tuple.RelationReference { + subjectTypes := map[string]tuple.RelationReference{} for _, subject := range as.SubjectsByNamespace.Values() { - rr := &core.RelationReference{ - Namespace: subject.Namespace, - Relation: subject.Relation, - } - subjectTypes[tuple.StringRR(rr)] = rr + subjectTypes[tuple.StringRR(subject.RelationReference())] = subject.RelationReference() } return maps.Values(subjectTypes) } // AllSubjectsNoWildcards returns all *defined*, non-wildcard subjects found. -func (as *AccessibilitySet) AllSubjectsNoWildcards() []*core.ObjectAndRelation { - subjects := make([]*core.ObjectAndRelation, 0) +func (as *AccessibilitySet) AllSubjectsNoWildcards() []tuple.ObjectAndRelation { + subjects := make([]tuple.ObjectAndRelation, 0) seenSubjects := mapz.NewSet[string]() for _, subject := range as.SubjectsByNamespace.Values() { - if subject.ObjectId == tuple.PublicWildcard { + if subject.ObjectID == tuple.PublicWildcard { continue } if seenSubjects.Add(tuple.StringONR(subject)) { @@ -320,7 +321,7 @@ func (as *AccessibilitySet) AllSubjectsNoWildcards() []*core.ObjectAndRelation { // LookupAccessibleResources returns all resources of the given type that are accessible to the // given subject. -func (as *AccessibilitySet) LookupAccessibleResources(resourceType *core.RelationReference, subject *core.ObjectAndRelation) map[string]ObjectAndPermission { +func (as *AccessibilitySet) LookupAccessibleResources(resourceType tuple.RelationReference, subject tuple.ObjectAndRelation) map[string]ObjectAndPermission { foundResources := map[string]ObjectAndPermission{} for permString, permissionship := range as.PermissionshipByRelationship { if permissionship == dispatchv1.ResourceCheckResult_NOT_MEMBER { @@ -328,19 +329,19 @@ func (as *AccessibilitySet) LookupAccessibleResources(resourceType *core.Relatio } parsed := tuple.MustParse(permString) - if parsed.ResourceAndRelation.Namespace != resourceType.Namespace || - parsed.ResourceAndRelation.Relation != resourceType.Relation { + if parsed.Resource.ObjectType != resourceType.ObjectType || + parsed.Resource.Relation != resourceType.Relation { continue } - if parsed.Subject.Namespace != subject.Namespace || - parsed.Subject.ObjectId != subject.ObjectId || + if parsed.Subject.ObjectType != subject.ObjectType || + parsed.Subject.ObjectID != subject.ObjectID || parsed.Subject.Relation != subject.Relation { continue } - foundResources[parsed.ResourceAndRelation.ObjectId] = ObjectAndPermission{ - ObjectID: parsed.ResourceAndRelation.ObjectId, + foundResources[parsed.Resource.ObjectID] = ObjectAndPermission{ + ObjectID: parsed.Resource.ObjectID, IsCaveated: permissionship == dispatchv1.ResourceCheckResult_CAVEATED_MEMBER, } } @@ -353,11 +354,11 @@ func isAccessibleViaWildcardOnly( ccd ConsistencyClusterAndData, dispatcher dispatch.Dispatcher, revision datastore.Revision, - resourceAndPermission *core.ObjectAndRelation, - subject *core.ObjectAndRelation, + resourceAndPermission tuple.ObjectAndRelation, + subject tuple.ObjectAndRelation, ) bool { resp, err := dispatcher.DispatchExpand(ccd.Ctx, &dispatchv1.DispatchExpandRequest{ - ResourceAndRelation: resourceAndPermission, + ResourceAndRelation: resourceAndPermission.ToCoreONR(), Metadata: &dispatchv1.ResolverMeta{ AtRevision: revision.String(), DepthRemaining: 100, diff --git a/internal/services/integrationtesting/consistencytestutil/servicetester.go b/internal/services/integrationtesting/consistencytestutil/servicetester.go index 468773280b..f9b31debd3 100644 --- a/internal/services/integrationtesting/consistencytestutil/servicetester.go +++ b/internal/services/integrationtesting/consistencytestutil/servicetester.go @@ -24,12 +24,12 @@ func ServiceTesters(conn *grpc.ClientConn) []ServiceTester { type ServiceTester interface { Name() string - Check(ctx context.Context, resource *core.ObjectAndRelation, subject *core.ObjectAndRelation, atRevision datastore.Revision, caveatContext map[string]any) (v1.CheckPermissionResponse_Permissionship, error) - Expand(ctx context.Context, resource *core.ObjectAndRelation, atRevision datastore.Revision) (*core.RelationTupleTreeNode, error) - Write(ctx context.Context, relationship *core.RelationTuple) error - Read(ctx context.Context, namespaceName string, atRevision datastore.Revision) ([]*core.RelationTuple, error) - LookupResources(ctx context.Context, resourceRelation *core.RelationReference, subject *core.ObjectAndRelation, atRevision datastore.Revision, cursor *v1.Cursor, limit uint32, caveatContext map[string]any) ([]*v1.LookupResourcesResponse, *v1.Cursor, error) - LookupSubjects(ctx context.Context, resource *core.ObjectAndRelation, subjectRelation *core.RelationReference, atRevision datastore.Revision, caveatContext map[string]any) (map[string]*v1.LookupSubjectsResponse, error) + Check(ctx context.Context, resource tuple.ObjectAndRelation, subject tuple.ObjectAndRelation, atRevision datastore.Revision, caveatContext map[string]any) (v1.CheckPermissionResponse_Permissionship, error) + Expand(ctx context.Context, resource tuple.ObjectAndRelation, atRevision datastore.Revision) (*core.RelationTupleTreeNode, error) + Write(ctx context.Context, relationship tuple.Relationship) error + Read(ctx context.Context, namespaceName string, atRevision datastore.Revision) ([]tuple.Relationship, error) + LookupResources(ctx context.Context, resourceRelation tuple.RelationReference, subject tuple.ObjectAndRelation, atRevision datastore.Revision, cursor *v1.Cursor, limit uint32, caveatContext map[string]any) ([]*v1.LookupResourcesResponse, *v1.Cursor, error) + LookupSubjects(ctx context.Context, resource tuple.ObjectAndRelation, subjectRelation tuple.RelationReference, atRevision datastore.Revision, caveatContext map[string]any) (map[string]*v1.LookupSubjectsResponse, error) // NOTE: ExperimentalService/BulkCheckPermission has been promoted to PermissionsService/CheckBulkPermissions BulkCheck(ctx context.Context, items []*v1.BulkCheckPermissionRequestItem, atRevision datastore.Revision) ([]*v1.BulkCheckPermissionPair, error) CheckBulk(ctx context.Context, items []*v1.CheckBulkPermissionsRequestItem, atRevision datastore.Revision) ([]*v1.CheckBulkPermissionsPair, error) @@ -53,7 +53,7 @@ func (v1st v1ServiceTester) Name() string { return "v1" } -func (v1st v1ServiceTester) Check(ctx context.Context, resource *core.ObjectAndRelation, subject *core.ObjectAndRelation, atRevision datastore.Revision, caveatContext map[string]any) (v1.CheckPermissionResponse_Permissionship, error) { +func (v1st v1ServiceTester) Check(ctx context.Context, resource tuple.ObjectAndRelation, subject tuple.ObjectAndRelation, atRevision datastore.Revision, caveatContext map[string]any) (v1.CheckPermissionResponse_Permissionship, error) { var context *structpb.Struct if caveatContext != nil { built, err := structpb.NewStruct(caveatContext) @@ -65,14 +65,14 @@ func (v1st v1ServiceTester) Check(ctx context.Context, resource *core.ObjectAndR checkResp, err := v1st.permClient.CheckPermission(ctx, &v1.CheckPermissionRequest{ Resource: &v1.ObjectReference{ - ObjectType: resource.Namespace, - ObjectId: resource.ObjectId, + ObjectType: resource.ObjectType, + ObjectId: resource.ObjectID, }, Permission: resource.Relation, Subject: &v1.SubjectReference{ Object: &v1.ObjectReference{ - ObjectType: subject.Namespace, - ObjectId: subject.ObjectId, + ObjectType: subject.ObjectType, + ObjectId: subject.ObjectID, }, OptionalRelation: optionalizeRelation(subject.Relation), }, @@ -89,11 +89,11 @@ func (v1st v1ServiceTester) Check(ctx context.Context, resource *core.ObjectAndR return checkResp.Permissionship, nil } -func (v1st v1ServiceTester) Expand(ctx context.Context, resource *core.ObjectAndRelation, atRevision datastore.Revision) (*core.RelationTupleTreeNode, error) { +func (v1st v1ServiceTester) Expand(ctx context.Context, resource tuple.ObjectAndRelation, atRevision datastore.Revision) (*core.RelationTupleTreeNode, error) { expandResp, err := v1st.permClient.ExpandPermissionTree(ctx, &v1.ExpandPermissionTreeRequest{ Resource: &v1.ObjectReference{ - ObjectType: resource.Namespace, - ObjectId: resource.ObjectId, + ObjectType: resource.ObjectType, + ObjectId: resource.ObjectID, }, Permission: resource.Relation, Consistency: &v1.Consistency{ @@ -108,20 +108,20 @@ func (v1st v1ServiceTester) Expand(ctx context.Context, resource *core.ObjectAnd return v1svc.TranslateRelationshipTree(expandResp.TreeRoot), nil } -func (v1st v1ServiceTester) Write(ctx context.Context, relationship *core.RelationTuple) error { +func (v1st v1ServiceTester) Write(ctx context.Context, relationship tuple.Relationship) error { _, err := v1st.permClient.WriteRelationships(ctx, &v1.WriteRelationshipsRequest{ OptionalPreconditions: []*v1.Precondition{ { Operation: v1.Precondition_OPERATION_MUST_MATCH, - Filter: tuple.MustToFilter(relationship), + Filter: tuple.ToV1Filter(relationship), }, }, - Updates: []*v1.RelationshipUpdate{tuple.UpdateToRelationshipUpdate(tuple.Touch(relationship))}, + Updates: []*v1.RelationshipUpdate{tuple.MustUpdateToV1RelationshipUpdate(tuple.Touch(relationship))}, }) return err } -func (v1st v1ServiceTester) Read(_ context.Context, namespaceName string, atRevision datastore.Revision) ([]*core.RelationTuple, error) { +func (v1st v1ServiceTester) Read(_ context.Context, namespaceName string, atRevision datastore.Revision) ([]tuple.Relationship, error) { readResp, err := v1st.permClient.ReadRelationships(context.Background(), &v1.ReadRelationshipsRequest{ RelationshipFilter: &v1.RelationshipFilter{ ResourceType: namespaceName, @@ -136,7 +136,7 @@ func (v1st v1ServiceTester) Read(_ context.Context, namespaceName string, atRevi return nil, err } - var tuples []*core.RelationTuple + var rels []tuple.Relationship for { resp, err := readResp.Recv() if errors.Is(err, io.EOF) { @@ -147,13 +147,13 @@ func (v1st v1ServiceTester) Read(_ context.Context, namespaceName string, atRevi return nil, err } - tuples = append(tuples, tuple.MustFromRelationship[*v1.ObjectReference, *v1.SubjectReference, *v1.ContextualizedCaveat](resp.Relationship)) + rels = append(rels, tuple.FromV1Relationship(resp.Relationship)) } - return tuples, nil + return rels, nil } -func (v1st v1ServiceTester) LookupResources(_ context.Context, resourceRelation *core.RelationReference, subject *core.ObjectAndRelation, atRevision datastore.Revision, cursor *v1.Cursor, limit uint32, caveatContext map[string]any) ([]*v1.LookupResourcesResponse, *v1.Cursor, error) { +func (v1st v1ServiceTester) LookupResources(_ context.Context, resourceRelation tuple.RelationReference, subject tuple.ObjectAndRelation, atRevision datastore.Revision, cursor *v1.Cursor, limit uint32, caveatContext map[string]any) ([]*v1.LookupResourcesResponse, *v1.Cursor, error) { var builtContext *structpb.Struct if caveatContext != nil { built, err := structpb.NewStruct(caveatContext) @@ -164,12 +164,12 @@ func (v1st v1ServiceTester) LookupResources(_ context.Context, resourceRelation } lookupResp, err := v1st.permClient.LookupResources(context.Background(), &v1.LookupResourcesRequest{ - ResourceObjectType: resourceRelation.Namespace, + ResourceObjectType: resourceRelation.ObjectType, Permission: resourceRelation.Relation, Subject: &v1.SubjectReference{ Object: &v1.ObjectReference{ - ObjectType: subject.Namespace, - ObjectId: subject.ObjectId, + ObjectType: subject.ObjectType, + ObjectId: subject.ObjectID, }, OptionalRelation: optionalizeRelation(subject.Relation), }, @@ -204,7 +204,7 @@ func (v1st v1ServiceTester) LookupResources(_ context.Context, resourceRelation return found, lastCursor, nil } -func (v1st v1ServiceTester) LookupSubjects(_ context.Context, resource *core.ObjectAndRelation, subjectRelation *core.RelationReference, atRevision datastore.Revision, caveatContext map[string]any) (map[string]*v1.LookupSubjectsResponse, error) { +func (v1st v1ServiceTester) LookupSubjects(_ context.Context, resource tuple.ObjectAndRelation, subjectRelation tuple.RelationReference, atRevision datastore.Revision, caveatContext map[string]any) (map[string]*v1.LookupSubjectsResponse, error) { var builtContext *structpb.Struct if caveatContext != nil { built, err := structpb.NewStruct(caveatContext) @@ -216,11 +216,11 @@ func (v1st v1ServiceTester) LookupSubjects(_ context.Context, resource *core.Obj lookupResp, err := v1st.permClient.LookupSubjects(context.Background(), &v1.LookupSubjectsRequest{ Resource: &v1.ObjectReference{ - ObjectType: resource.Namespace, - ObjectId: resource.ObjectId, + ObjectType: resource.ObjectType, + ObjectId: resource.ObjectID, }, Permission: resource.Relation, - SubjectObjectType: subjectRelation.Namespace, + SubjectObjectType: subjectRelation.ObjectType, OptionalSubjectRelation: optionalizeRelation(subjectRelation.Relation), Consistency: &v1.Consistency{ Requirement: &v1.Consistency_AtLeastAsFresh{ diff --git a/internal/services/integrationtesting/dispatch_test.go b/internal/services/integrationtesting/dispatch_test.go index 8aa2816b3e..4a4322dd4c 100644 --- a/internal/services/integrationtesting/dispatch_test.go +++ b/internal/services/integrationtesting/dispatch_test.go @@ -47,15 +47,15 @@ func TestDispatchIntegration(t *testing.T) { Updates: []*v1.RelationshipUpdate{ { Operation: v1.RelationshipUpdate_OPERATION_CREATE, - Relationship: tuple.MustToRelationship(tuple.MustParse("resource:foo#viewer@user:tom")), + Relationship: tuple.ToV1Relationship(tuple.MustParse("resource:foo#viewer@user:tom")), }, { Operation: v1.RelationshipUpdate_OPERATION_CREATE, - Relationship: tuple.MustToRelationship(tuple.MustParse("resource:foo#parent@resource:bar")), + Relationship: tuple.ToV1Relationship(tuple.MustParse("resource:foo#parent@resource:bar")), }, { Operation: v1.RelationshipUpdate_OPERATION_CREATE, - Relationship: tuple.MustToRelationship(tuple.MustParse("resource:bar#viewer@user:jill")), + Relationship: tuple.ToV1Relationship(tuple.MustParse("resource:bar#viewer@user:jill")), }, }, }) @@ -120,7 +120,7 @@ func TestDispatchIntegration(t *testing.T) { Updates: []*v1.RelationshipUpdate{ { Operation: v1.RelationshipUpdate_OPERATION_CREATE, - Relationship: tuple.MustToRelationship(tuple.MustParse("resource:foo#parent@someothertype:bar")), + Relationship: tuple.ToV1Relationship(tuple.MustParse("resource:foo#parent@someothertype:bar")), }, }, }) @@ -161,7 +161,7 @@ func TestDispatchIntegration(t *testing.T) { Updates: []*v1.RelationshipUpdate{ { Operation: v1.RelationshipUpdate_OPERATION_CREATE, - Relationship: tuple.MustToRelationship(tuple.MustParse("resource:foo#viewer@user:someuser")), + Relationship: tuple.ToV1Relationship(tuple.MustParse("resource:foo#viewer@user:someuser")), }, }, }) @@ -225,7 +225,7 @@ func TestDispatchIntegration(t *testing.T) { Updates: []*v1.RelationshipUpdate{ { Operation: v1.RelationshipUpdate_OPERATION_CREATE, - Relationship: tuple.MustToRelationship(tuple.MustParse("resource:someresource#viewer@user:someuser")), + Relationship: tuple.ToV1Relationship(tuple.MustParse("resource:someresource#viewer@user:someuser")), }, }, }) @@ -259,7 +259,7 @@ func TestDispatchIntegration(t *testing.T) { Updates: []*v1.RelationshipUpdate{ { Operation: v1.RelationshipUpdate_OPERATION_CREATE, - Relationship: tuple.MustToRelationship(tuple.MustParse("resource:someresource#viewer@user:sarah")), + Relationship: tuple.ToV1Relationship(tuple.MustParse("resource:someresource#viewer@user:sarah")), }, }, }) diff --git a/internal/services/integrationtesting/ops_test.go b/internal/services/integrationtesting/ops_test.go index 263f384603..afadef84c5 100644 --- a/internal/services/integrationtesting/ops_test.go +++ b/internal/services/integrationtesting/ops_test.go @@ -66,7 +66,7 @@ func (wr createCaveatedRelationship) Execute(tester opsTester) error { } rel := tuple.MustParse(wr.relString) - rel.Caveat = &core.ContextualizedCaveat{ + rel.OptionalCaveat = &core.ContextualizedCaveat{ CaveatName: wr.caveatName, Context: ctx, } @@ -86,7 +86,7 @@ func (wr touchCaveatedRelationship) Execute(tester opsTester) error { } rel := tuple.MustParse(wr.relString) - rel.Caveat = &core.ContextualizedCaveat{ + rel.OptionalCaveat = &core.ContextualizedCaveat{ CaveatName: wr.caveatName, Context: ctx, } @@ -112,7 +112,7 @@ type deleteCaveatedRelationship struct { func (dr deleteCaveatedRelationship) Execute(tester opsTester) error { rel := tuple.MustParse(dr.relString) - rel.Caveat = &core.ContextualizedCaveat{ + rel.OptionalCaveat = &core.ContextualizedCaveat{ CaveatName: dr.caveatName, } @@ -781,32 +781,32 @@ type opsTester interface { Name() string ReadSchema(ctx context.Context) (string, error) WriteSchema(ctx context.Context, schemaString string) error - CreateRelationship(ctx context.Context, relationship *core.RelationTuple) error - TouchRelationship(ctx context.Context, relationship *core.RelationTuple) error - DeleteRelationship(ctx context.Context, relationship *core.RelationTuple) error + CreateRelationship(ctx context.Context, relationship tuple.Relationship) error + TouchRelationship(ctx context.Context, relationship tuple.Relationship) error + DeleteRelationship(ctx context.Context, relationship tuple.Relationship) error } type baseOpsTester struct { permClient v1.PermissionsServiceClient } -func (st baseOpsTester) CreateRelationship(ctx context.Context, relationship *core.RelationTuple) error { +func (st baseOpsTester) CreateRelationship(ctx context.Context, relationship tuple.Relationship) error { _, err := st.permClient.WriteRelationships(ctx, &v1.WriteRelationshipsRequest{ - Updates: []*v1.RelationshipUpdate{tuple.UpdateToRelationshipUpdate(tuple.Create(relationship))}, + Updates: []*v1.RelationshipUpdate{tuple.MustUpdateToV1RelationshipUpdate(tuple.Create(relationship))}, }) return err } -func (st baseOpsTester) TouchRelationship(ctx context.Context, relationship *core.RelationTuple) error { +func (st baseOpsTester) TouchRelationship(ctx context.Context, relationship tuple.Relationship) error { _, err := st.permClient.WriteRelationships(ctx, &v1.WriteRelationshipsRequest{ - Updates: []*v1.RelationshipUpdate{tuple.UpdateToRelationshipUpdate(tuple.Touch(relationship))}, + Updates: []*v1.RelationshipUpdate{tuple.MustUpdateToV1RelationshipUpdate(tuple.Touch(relationship))}, }) return err } -func (st baseOpsTester) DeleteRelationship(ctx context.Context, relationship *core.RelationTuple) error { +func (st baseOpsTester) DeleteRelationship(ctx context.Context, relationship tuple.Relationship) error { _, err := st.permClient.WriteRelationships(ctx, &v1.WriteRelationshipsRequest{ - Updates: []*v1.RelationshipUpdate{tuple.UpdateToRelationshipUpdate(tuple.Delete(relationship))}, + Updates: []*v1.RelationshipUpdate{tuple.MustUpdateToV1RelationshipUpdate(tuple.Delete(relationship))}, }) return err } diff --git a/internal/services/integrationtesting/perf_test.go b/internal/services/integrationtesting/perf_test.go index f411bce80e..e641ab89fc 100644 --- a/internal/services/integrationtesting/perf_test.go +++ b/internal/services/integrationtesting/perf_test.go @@ -49,7 +49,7 @@ func TestBurst(t *testing.T) { client := v1.NewPermissionsServiceClient(conns[0]) var wg sync.WaitGroup for i := 0; i < 100; i++ { - rel := tuple.MustToRelationship(tuple.Parse(tf.StandardTuples[i%(len(tf.StandardTuples))])) + rel := tuple.ToV1Relationship(tuple.MustParse(tf.StandardRelationships[i%(len(tf.StandardRelationships))])) run := make(chan struct{}) wg.Add(1) go func() { diff --git a/internal/services/shared/schema.go b/internal/services/shared/schema.go index 7263ddcb57..7c5b736484 100644 --- a/internal/services/shared/schema.go +++ b/internal/services/shared/schema.go @@ -292,7 +292,6 @@ func ensureNoRelationshipsExist(ctx context.Context, rwt datastore.ReadWriteTran "cannot delete object definition `%s`, as a relationship references it", namespaceName, ) - qy.Close() if err != nil { return err } @@ -345,7 +344,6 @@ func sanityCheckNamespaceChanges( qy, qyErr, "cannot delete relation `%s` in object definition `%s`, as a relationship references it", delta.RelationName, nsdef.Name) - qy.Close() if err != nil { return diff, err } @@ -389,7 +387,6 @@ func sanityCheckNamespaceChanges( qyrErr, "cannot remove allowed type `%s` from relation `%s` in object definition `%s`, as a relationship exists with it", typesystem.SourceForAllowedRelation(delta.AllowedType), delta.RelationName, nsdef.Name) - qyr.Close() if err != nil { return diff, err } @@ -404,14 +401,13 @@ func errorIfTupleIteratorReturnsTuples(_ context.Context, qy datastore.Relations if qyErr != nil { return qyErr } - defer qy.Close() - if rt := qy.Next(); rt != nil { - if qy.Err() != nil { - return qy.Err() + for _, err := range qy { + if err != nil { + return err } - return NewSchemaWriteDataValidationError(message, args...) } + return nil } diff --git a/internal/services/v1/bulkcheck.go b/internal/services/v1/bulkcheck.go index 3b07363f72..7b3193071b 100644 --- a/internal/services/v1/bulkcheck.go +++ b/internal/services/v1/bulkcheck.go @@ -172,12 +172,12 @@ func (bc *bulkChecker) checkBulkPermissions(ctx context.Context, req *v1.CheckBu err := namespace.CheckNamespaceAndRelations(ctx, []namespace.TypeAndRelationToCheck{ { - NamespaceName: group.params.ResourceType.Namespace, + NamespaceName: group.params.ResourceType.ObjectType, RelationName: group.params.ResourceType.Relation, AllowEllipsis: false, }, { - NamespaceName: group.params.Subject.Namespace, + NamespaceName: group.params.Subject.ObjectType, RelationName: stringz.DefaultEmpty(group.params.Subject.Relation, graph.Ellipsis), AllowEllipsis: true, }, diff --git a/internal/services/v1/debug.go b/internal/services/v1/debug.go index 803b18f965..3a940c78a7 100644 --- a/internal/services/v1/debug.go +++ b/internal/services/v1/debug.go @@ -140,7 +140,7 @@ func convertCheckTrace(ctx context.Context, caveatContext map[string]any, ct *di } slices.SortFunc(subProblems, func(a, b *v1.CheckDebugTrace) int { - return cmp.Compare(tuple.StringObjectRef(a.Resource), tuple.StringObjectRef(a.Resource)) + return cmp.Compare(tuple.V1StringObjectRef(a.Resource), tuple.V1StringObjectRef(a.Resource)) }) return &v1.CheckDebugTrace{ diff --git a/internal/services/v1/debug_test.go b/internal/services/v1/debug_test.go index b5481d6502..003b9a39ea 100644 --- a/internal/services/v1/debug_test.go +++ b/internal/services/v1/debug_test.go @@ -21,7 +21,6 @@ import ( "github.com/authzed/spicedb/internal/testserver" "github.com/authzed/spicedb/pkg/datastore" "github.com/authzed/spicedb/pkg/genutil/mapz" - core "github.com/authzed/spicedb/pkg/proto/core/v1" "github.com/authzed/spicedb/pkg/tuple" "github.com/authzed/spicedb/pkg/zedtoken" ) @@ -98,7 +97,7 @@ func TestCheckPermissionWithDebug(t *testing.T) { tcs := []struct { name string schema string - relationships []*core.RelationTuple + relationships []tuple.Relationship toTest []debugCheckInfo }{ { @@ -112,7 +111,7 @@ func TestCheckPermissionWithDebug(t *testing.T) { permission view = viewer + edit } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:first#viewer@user:tom"), tuple.MustParse("document:first#editor@user:sarah"), }, @@ -169,7 +168,7 @@ func TestCheckPermissionWithDebug(t *testing.T) { permission view = viewer + another } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:first#viewer@user:tom"), tuple.MustParse("document:first#viewer@user:sarah[somecaveat]"), }, @@ -236,7 +235,7 @@ func TestCheckPermissionWithDebug(t *testing.T) { permission view = viewer + folder->fview } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:first#viewer@user:tom"), tuple.MustParse("document:first#folder@folder:f1"), tuple.MustParse("document:first#folder@folder:f2"), @@ -314,7 +313,7 @@ func TestCheckPermissionWithDebug(t *testing.T) { definition resource { relation viewer: user | user with has_valid_ip }`, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse(`resource:first#viewer@user:sarah[has_valid_ip:{"allowed_range":"192.168.0.0/16"}]`), }, []debugCheckInfo{ @@ -355,7 +354,7 @@ func TestCheckPermissionWithDebug(t *testing.T) { permission view = parent->member } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:first#parent@org:someorg[anothercaveat]"), tuple.MustParse("org:someorg#member@user:sarah[somecaveat]"), }, @@ -460,7 +459,7 @@ func TestCheckPermissionWithDebug(t *testing.T) { permission view = parent->member } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse(`document:first#parent@org:someorg[anothercaveat:{"somecondition":41}]`), tuple.MustParse(`org:someorg#member@user:sarah[somecaveat:{"somecondition":42}]`), }, diff --git a/internal/services/v1/errors.go b/internal/services/v1/errors.go index 068de6de17..5e37e2bcff 100644 --- a/internal/services/v1/errors.go +++ b/internal/services/v1/errors.go @@ -236,7 +236,7 @@ func NewDuplicateRelationshipErr(update *v1.RelationshipUpdate) ErrDuplicateRela return ErrDuplicateRelationshipError{ error: fmt.Errorf( "found more than one update with relationship `%s` in this request; a relationship can only be specified in an update once per overall WriteRelationships request", - tuple.StringRelationshipWithoutCaveat(update.Relationship), + tuple.V1StringRelationshipWithoutCaveat(update.Relationship), ), update: update, } @@ -251,7 +251,7 @@ func (err ErrDuplicateRelationshipError) GRPCStatus() *status.Status { v1.ErrorReason_ERROR_REASON_UPDATES_ON_SAME_RELATIONSHIP, map[string]string{ "definition_name": err.update.Relationship.Resource.ObjectType, - "relationship": tuple.MustRelString(err.update.Relationship), + "relationship": tuple.MustV1StringRelationship(err.update.Relationship), }, ), ) @@ -270,7 +270,7 @@ func NewMaxRelationshipContextError(update *v1.RelationshipUpdate, maxAllowedSiz return ErrMaxRelationshipContextError{ error: fmt.Errorf( "provided relationship `%s` exceeded maximum allowed caveat size of %d", - tuple.StringRelationshipWithoutCaveat(update.Relationship), + tuple.V1StringRelationshipWithoutCaveat(update.Relationship), maxAllowedSize, ), update: update, @@ -286,7 +286,7 @@ func (err ErrMaxRelationshipContextError) GRPCStatus() *status.Status { spiceerrors.ForReason( v1.ErrorReason_ERROR_REASON_MAX_RELATIONSHIP_CONTEXT_SIZE, map[string]string{ - "relationship": tuple.StringRelationshipWithoutCaveat(err.update.Relationship), + "relationship": tuple.V1StringRelationshipWithoutCaveat(err.update.Relationship), "max_allowed_size": strconv.Itoa(err.maxAllowedSize), "context_size": strconv.Itoa(proto.Size(err.update.Relationship)), }, diff --git a/internal/services/v1/experimental.go b/internal/services/v1/experimental.go index 6561626dde..21feb5effa 100644 --- a/internal/services/v1/experimental.go +++ b/internal/services/v1/experimental.go @@ -3,6 +3,7 @@ package v1 import ( "context" "errors" + "fmt" "io" "slices" "sort" @@ -13,6 +14,7 @@ import ( "google.golang.org/grpc/codes" "github.com/ccoveille/go-safecast" + "github.com/jzelinskie/stringz" "github.com/authzed/spicedb/internal/dispatch" log "github.com/authzed/spicedb/internal/logging" @@ -126,7 +128,7 @@ type bulkLoadAdapter struct { stream v1.ExperimentalService_BulkImportRelationshipsServer referencedNamespaceMap map[string]*typesystem.TypeSystem referencedCaveatMap map[string]*core.CaveatDefinition - current core.RelationTuple + current tuple.Relationship caveat core.ContextualizedCaveat awaitingNamespaces []string @@ -137,7 +139,7 @@ type bulkLoadAdapter struct { err error } -func (a *bulkLoadAdapter) Next(_ context.Context) (*core.RelationTuple, error) { +func (a *bulkLoadAdapter) Next(_ context.Context) (*tuple.Relationship, error) { for a.err == nil && a.numSent == len(a.currentBatch) { // Load a new batch batch, err := a.stream.Recv() @@ -164,14 +166,25 @@ func (a *bulkLoadAdapter) Next(_ context.Context) (*core.RelationTuple, error) { return nil, nil } - a.current.Caveat = &a.caveat - a.current.Integrity = nil - tuple.CopyRelationshipToRelationTuple(a.currentBatch[a.numSent], &a.current) + if a.caveat.CaveatName != "" { + a.current.OptionalCaveat = &a.caveat + } else { + a.current.OptionalCaveat = nil + } + + a.current.OptionalIntegrity = nil + + a.current.RelationshipReference.Resource.ObjectType = a.currentBatch[a.numSent].Resource.ObjectType + a.current.RelationshipReference.Resource.ObjectID = a.currentBatch[a.numSent].Resource.ObjectId + a.current.RelationshipReference.Resource.Relation = a.currentBatch[a.numSent].Relation + a.current.Subject.ObjectType = a.currentBatch[a.numSent].Subject.Object.ObjectType + a.current.Subject.ObjectID = a.currentBatch[a.numSent].Subject.Object.ObjectId + a.current.Subject.Relation = stringz.DefaultEmpty(a.currentBatch[a.numSent].Subject.OptionalRelation, tuple.Ellipsis) if err := relationships.ValidateOneRelationship( a.referencedNamespaceMap, a.referencedCaveatMap, - &a.current, + a.current, relationships.ValidateRelationshipForCreateOrTouch, ); err != nil { return nil, err @@ -218,11 +231,8 @@ func (es *experimentalServer) BulkImportRelationships(stream v1.ExperimentalServ stream: stream, referencedNamespaceMap: loadedNamespaces, referencedCaveatMap: loadedCaveats, - current: core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{}, - Subject: &core.ObjectAndRelation{}, - }, - caveat: core.ContextualizedCaveat{}, + current: tuple.Relationship{}, + caveat: core.ContextualizedCaveat{}, } resolver := typesystem.ResolverForDatastoreReader(rwt) @@ -385,17 +395,32 @@ func BulkExport(ctx context.Context, ds datastore.ReadOnlyDatastore, batchSize u // Lop off any rels we've already sent rels = rels[:0] - tplFn := func(tpl *core.RelationTuple) { + relFn := func(rel tuple.Relationship) { offset := len(rels) rels = append(rels, &relsArray[offset]) // nozero - tuple.CopyRelationTupleToRelationship(tpl, &relsArray[offset], &caveatArray[offset]) + + v1Rel := &relsArray[offset] + v1Rel.Resource.ObjectType = rel.RelationshipReference.Resource.ObjectType + v1Rel.Resource.ObjectId = rel.RelationshipReference.Resource.ObjectID + v1Rel.Relation = rel.RelationshipReference.Resource.Relation + v1Rel.Subject.Object.ObjectType = rel.RelationshipReference.Subject.ObjectType + v1Rel.Subject.Object.ObjectId = rel.RelationshipReference.Subject.ObjectID + v1Rel.Subject.OptionalRelation = denormalizeSubjectRelation(rel.RelationshipReference.Subject.Relation) + + if rel.OptionalCaveat != nil { + caveatArray[offset].CaveatName = rel.OptionalCaveat.CaveatName + caveatArray[offset].Context = rel.OptionalCaveat.Context + } else { + caveatArray[offset].CaveatName = "" + caveatArray[offset].Context = nil + } } cur, err = queryForEach( ctx, reader, relationshipFilter, - tplFn, + relFn, dsoptions.WithLimit(&limit), dsoptions.WithAfter(cur), dsoptions.WithSort(dsoptions.ByResource), @@ -414,7 +439,7 @@ func BulkExport(ctx context.Context, ds datastore.ReadOnlyDatastore, batchSize u Revision: atRevision.String(), Sections: []string{ ns.Definition.Name, - tuple.MustString(cur), + tuple.MustString(*dsoptions.ToRelationship(cur)), }, }, }, @@ -722,37 +747,27 @@ func queryForEach( ctx context.Context, reader datastore.Reader, filter datastore.RelationshipsFilter, - fn func(tpl *core.RelationTuple), + fn func(rel tuple.Relationship), opts ...dsoptions.QueryOptionsOption, -) (*core.RelationTuple, error) { +) (dsoptions.Cursor, error) { iter, err := reader.QueryRelationships(ctx, filter, opts...) if err != nil { return nil, err } - defer iter.Close() - var hadTuples bool - for tpl := iter.Next(); tpl != nil; tpl = iter.Next() { - fn(tpl) - hadTuples = true - } - if iter.Err() != nil { - return nil, err - } - - var cur *core.RelationTuple - if hadTuples { - cur, err = iter.Cursor() - iter.Close() + var cursor dsoptions.Cursor + for rel, err := range iter { if err != nil { return nil, err } - } - return cur, nil + fn(rel) + cursor = dsoptions.ToCursor(rel) + } + return cursor, nil } -func decodeCursor(ds datastore.ReadOnlyDatastore, encoded *v1.Cursor) (datastore.Revision, string, *core.RelationTuple, error) { +func decodeCursor(ds datastore.ReadOnlyDatastore, encoded *v1.Cursor) (datastore.Revision, string, dsoptions.Cursor, error) { decoded, err := cursor.Decode(encoded) if err != nil { return datastore.NoRevision, "", nil, err @@ -771,11 +786,11 @@ func decodeCursor(ds datastore.ReadOnlyDatastore, encoded *v1.Cursor) (datastore return datastore.NoRevision, "", nil, err } - cur := tuple.Parse(decoded.GetV1().GetSections()[1]) - if cur == nil { - return datastore.NoRevision, "", nil, errors.New("malformed cursor: invalid encoded relation tuple") + cur, err := tuple.Parse(decoded.GetV1().GetSections()[1]) + if err != nil { + return datastore.NoRevision, "", nil, fmt.Errorf("malformed cursor: invalid encoded relation tuple: %w", err) } // Returns the current namespace and the cursor. - return atRevision, decoded.GetV1().GetSections()[0], cur, nil + return atRevision, decoded.GetV1().GetSections()[0], dsoptions.ToCursor(cur), nil } diff --git a/internal/services/v1/experimental_test.go b/internal/services/v1/experimental_test.go index 28f5c9ea39..b098909f51 100644 --- a/internal/services/v1/experimental_test.go +++ b/internal/services/v1/experimental_test.go @@ -14,6 +14,7 @@ import ( v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" "github.com/authzed/grpcutil" "github.com/ccoveille/go-safecast" + "github.com/jzelinskie/stringz" "github.com/scylladb/go-set" "github.com/stretchr/testify/require" "go.uber.org/goleak" @@ -29,7 +30,6 @@ import ( "github.com/authzed/spicedb/internal/testserver" "github.com/authzed/spicedb/pkg/datastore" "github.com/authzed/spicedb/pkg/genutil/mapz" - core "github.com/authzed/spicedb/pkg/proto/core/v1" "github.com/authzed/spicedb/pkg/testutil" "github.com/authzed/spicedb/pkg/tuple" ) @@ -177,7 +177,7 @@ func TestBulkExportRelationships(t *testing.T) { "", ) batch[i] = rel - expectedRels.Add(tuple.MustStringRelationship(rel)) + expectedRels.Add(tuple.MustV1RelString(rel)) } ctx := context.Background() @@ -240,7 +240,7 @@ func TestBulkExportRelationships(t *testing.T) { totalRead += len(batch.Relationships) for _, rel := range batch.Relationships { - remainingRels.Remove(tuple.MustStringRelationship(rel)) + remainingRels.Remove(tuple.MustV1RelString(rel)) } } @@ -338,12 +338,12 @@ func TestBulkExportRelationshipsWithFilter(t *testing.T) { if tc.filter != nil { filter, err := datastore.RelationshipsFilterFromPublicFilter(tc.filter) require.NoError(err) - if !filter.Test(tuple.MustFromRelationship(rel)) { + if !filter.Test(tuple.FromV1Relationship(rel)) { continue } } - expectedRels.Add(tuple.MustStringRelationship(rel)) + expectedRels.Add(tuple.MustV1RelString(rel)) } require.Equal(tc.expectedCount, expectedRels.Size()) @@ -394,12 +394,12 @@ func TestBulkExportRelationshipsWithFilter(t *testing.T) { if tc.filter != nil { filter, err := datastore.RelationshipsFilterFromPublicFilter(tc.filter) require.NoError(err) - require.True(filter.Test(tuple.MustFromRelationship(rel)), "relationship did not match filter: %s", rel) + require.True(filter.Test(tuple.FromV1Relationship(rel)), "relationship did not match filter: %s", rel) } - require.True(remainingRels.Has(tuple.MustStringRelationship(rel)), "relationship was not expected or was repeated: %s", rel) - remainingRels.Remove(tuple.MustStringRelationship(rel)) - foundRels.Add(tuple.MustStringRelationship(rel)) + require.True(remainingRels.Has(tuple.MustV1RelString(rel)), "relationship was not expected or was repeated: %s", rel) + remainingRels.Remove(tuple.MustV1RelString(rel)) + foundRels.Add(tuple.MustV1RelString(rel)) } cancel() @@ -638,17 +638,28 @@ func TestBulkCheckPermission(t *testing.T) { expected := make([]*v1.BulkCheckPermissionPair, 0, len(tt.response)) for _, r := range tt.response { - reqRel := tuple.ParseRel(r.req) + reqRel := tuple.MustParse(r.req) resp := &v1.BulkCheckPermissionPair_Item{ Item: &v1.BulkCheckPermissionResponseItem{ Permissionship: r.resp, }, } + + rel := stringz.Default(reqRel.Subject.Relation, "", tuple.Ellipsis) pair := &v1.BulkCheckPermissionPair{ Request: &v1.BulkCheckPermissionRequestItem{ - Resource: reqRel.Resource, - Permission: reqRel.Relation, - Subject: reqRel.Subject, + Resource: &v1.ObjectReference{ + ObjectType: reqRel.Resource.ObjectType, + ObjectId: reqRel.Resource.ObjectID, + }, + Permission: reqRel.Resource.Relation, + Subject: &v1.SubjectReference{ + Object: &v1.ObjectReference{ + ObjectType: reqRel.Subject.ObjectType, + ObjectId: reqRel.Subject.ObjectID, + }, + OptionalRelation: rel, + }, }, Response: resp, } @@ -686,11 +697,22 @@ func TestBulkCheckPermission(t *testing.T) { } func relToBulkRequestItem(rel string) *v1.BulkCheckPermissionRequestItem { - r := tuple.ParseRel(rel) + r := tuple.MustParse(rel) + subjectRel := stringz.Default(r.Subject.Relation, "", tuple.Ellipsis) + item := &v1.BulkCheckPermissionRequestItem{ - Resource: r.Resource, - Permission: r.Relation, - Subject: r.Subject, + Resource: &v1.ObjectReference{ + ObjectType: r.Resource.ObjectType, + ObjectId: r.Resource.ObjectID, + }, + Permission: r.Resource.Relation, + Subject: &v1.SubjectReference{ + Object: &v1.ObjectReference{ + ObjectType: r.Subject.ObjectType, + ObjectId: r.Subject.ObjectID, + }, + OptionalRelation: subjectRel, + }, } if r.OptionalCaveat != nil { item.Context = r.OptionalCaveat.Context @@ -1663,20 +1685,23 @@ func TestExperimentalCountRelationships(t *testing.T) { require.NoError(t, err) // Write some relationships. + updates, err := tuple.UpdatesToV1RelationshipUpdates([]tuple.RelationshipUpdate{ + tuple.Create(tuple.MustParse("document:doc1#viewer@user:alice")), + tuple.Create(tuple.MustParse("document:doc1#viewer@user:bob")), + tuple.Create(tuple.MustParse("document:doc1#viewer@user:charlie")), + tuple.Create(tuple.MustParse("document:doc1#editor@user:alice")), + tuple.Create(tuple.MustParse("document:doc1#editor@user:bob")), + + tuple.Create(tuple.MustParse("document:doc2#viewer@user:alice")), + tuple.Create(tuple.MustParse("document:doc2#viewer@user:adam")), + + tuple.Create(tuple.MustParse("document:adoc#viewer@user:alice")), + tuple.Create(tuple.MustParse("document:anotherdoc#viewer@user:alice")), + }) + require.NoError(t, err) + _, err = permsClient.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ - Updates: tuple.UpdatesToRelationshipUpdates([]*core.RelationTupleUpdate{ - tuple.Create(tuple.MustParse("document:doc1#viewer@user:alice")), - tuple.Create(tuple.MustParse("document:doc1#viewer@user:bob")), - tuple.Create(tuple.MustParse("document:doc1#viewer@user:charlie")), - tuple.Create(tuple.MustParse("document:doc1#editor@user:alice")), - tuple.Create(tuple.MustParse("document:doc1#editor@user:bob")), - - tuple.Create(tuple.MustParse("document:doc2#viewer@user:alice")), - tuple.Create(tuple.MustParse("document:doc2#viewer@user:adam")), - - tuple.Create(tuple.MustParse("document:adoc#viewer@user:alice")), - tuple.Create(tuple.MustParse("document:anotherdoc#viewer@user:alice")), - }), + Updates: updates, }) require.NoError(t, err) diff --git a/internal/services/v1/grouping.go b/internal/services/v1/grouping.go index 04bcd6b85e..621eeabf4f 100644 --- a/internal/services/v1/grouping.go +++ b/internal/services/v1/grouping.go @@ -7,7 +7,7 @@ import ( "github.com/authzed/spicedb/internal/graph/computed" "github.com/authzed/spicedb/pkg/datastore" - core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/tuple" ) type groupedCheckParameters struct { @@ -56,15 +56,8 @@ func checkParametersFromCheckBulkPermissionsRequestItem( caveatContext map[string]any, ) *computed.CheckParameters { return &computed.CheckParameters{ - ResourceType: &core.RelationReference{ - Namespace: bc.Resource.ObjectType, - Relation: bc.Permission, - }, - Subject: &core.ObjectAndRelation{ - Namespace: bc.Subject.Object.ObjectType, - ObjectId: bc.Subject.Object.ObjectId, - Relation: normalizeSubjectRelation(bc.Subject), - }, + ResourceType: tuple.RR(bc.Resource.ObjectType, bc.Permission), + Subject: tuple.ONR(bc.Subject.Object.ObjectType, bc.Subject.Object.ObjectId, normalizeSubjectRelation(bc.Subject)), CaveatContext: caveatContext, AtRevision: params.atRevision, MaximumDepth: params.maximumAPIDepth, diff --git a/internal/services/v1/grouping_test.go b/internal/services/v1/grouping_test.go index 0a0a737c52..cd0492e495 100644 --- a/internal/services/v1/grouping_test.go +++ b/internal/services/v1/grouping_test.go @@ -13,7 +13,6 @@ import ( "github.com/authzed/spicedb/internal/graph/computed" "github.com/authzed/spicedb/pkg/datastore" - "github.com/authzed/spicedb/pkg/testutil" "github.com/authzed/spicedb/pkg/tuple" ) @@ -181,7 +180,9 @@ func TestGroupItems(t *testing.T) { t.Run(tt.name, func(t *testing.T) { var items []*v1.CheckBulkPermissionsRequestItem for _, r := range tt.requests { - rel := tuple.ParseRel(r) + rel, err := tuple.ParseV1Rel(r) + require.NoError(t, err) + item := &v1.CheckBulkPermissionsRequestItem{ Resource: rel.Resource, Permission: rel.Relation, @@ -221,10 +222,10 @@ func TestGroupItems(t *testing.T) { firstParams := ccp[first].params secondParams := ccp[second].params - firstKey := firstParams.ResourceType.Namespace + firstParams.ResourceType.Relation + - firstParams.Subject.Namespace + firstParams.Subject.ObjectId + firstParams.Subject.Relation + strings.Join(ccp[first].resourceIDs, ",") - secondKey := secondParams.ResourceType.Namespace + secondParams.ResourceType.Relation + - secondParams.Subject.Namespace + secondParams.Subject.ObjectId + secondParams.Subject.Relation + strings.Join(ccp[second].resourceIDs, ",") + firstKey := firstParams.ResourceType.ObjectType + firstParams.ResourceType.Relation + + firstParams.Subject.ObjectType + firstParams.Subject.ObjectID + firstParams.Subject.Relation + strings.Join(ccp[first].resourceIDs, ",") + secondKey := secondParams.ResourceType.ObjectType + secondParams.ResourceType.Relation + + secondParams.Subject.ObjectType + secondParams.Subject.ObjectID + secondParams.Subject.Relation + strings.Join(ccp[second].resourceIDs, ",") return firstKey < secondKey }) @@ -238,11 +239,8 @@ func TestGroupItems(t *testing.T) { require.Equal(t, cp.atRevision, ccp[i].params.AtRevision) require.Equal(t, computed.NoDebugging, ccp[i].params.DebugOption) - err := testutil.AreProtoEqual(tuple.RelationReference(expected.resourceType, expected.resourceRel), ccp[i].params.ResourceType, "resource type diff") - require.NoError(t, err) - - err = testutil.AreProtoEqual(tuple.ParseSubjectONR(expected.subject), ccp[i].params.Subject, "resource type diff") - require.NoError(t, err) + require.Equal(t, tuple.RR(expected.resourceType, expected.resourceRel), ccp[i].params.ResourceType, "resource type diff") + require.Equal(t, tuple.MustParseSubjectONR(expected.subject), ccp[i].params.Subject, "resource type diff") } } }) @@ -255,7 +253,10 @@ func TestCaveatContextSizeLimitIsEnforced(t *testing.T) { maxCaveatContextSize: 1, maximumAPIDepth: 1, } - rel := tuple.ParseRel(`document:1#view@user:1[somecaveat:{"hey": "bud"}]`) + + rel, err := tuple.ParseV1Rel(`document:1#view@user:1[somecaveat:{"hey": "bud"}]`) + require.NoError(t, err) + items := []*v1.CheckBulkPermissionsRequestItem{ { Resource: rel.Resource, @@ -264,6 +265,6 @@ func TestCaveatContextSizeLimitIsEnforced(t *testing.T) { Context: rel.OptionalCaveat.Context, }, } - _, err := groupItems(context.Background(), cp, items) + _, err = groupItems(context.Background(), cp, items) require.ErrorContains(t, err, "request caveat context should have less than 1 bytes but had 14") } diff --git a/internal/services/v1/hash.go b/internal/services/v1/hash.go index 5d8a3c1e19..1754669e92 100644 --- a/internal/services/v1/hash.go +++ b/internal/services/v1/hash.go @@ -60,7 +60,7 @@ func computeLRRequestHash(req *v1.LookupResourcesRequest) (string, error) { return computeCallHash("v1.lookupresources", req.Consistency, map[string]any{ "resource-type": req.ResourceObjectType, "permission": req.Permission, - "subject": tuple.StringSubjectRef(req.Subject), + "subject": tuple.V1StringSubjectRef(req.Subject), "limit": req.OptionalLimit, "context": req.Context, }) diff --git a/internal/services/v1/metadata_test.go b/internal/services/v1/metadata_test.go index 10dae57766..3acbc131a4 100644 --- a/internal/services/v1/metadata_test.go +++ b/internal/services/v1/metadata_test.go @@ -84,7 +84,7 @@ func TestAllMethodsReturnMetadata(t *testing.T) { Updates: []*v1.RelationshipUpdate{ { Operation: v1.RelationshipUpdate_OPERATION_TOUCH, - Relationship: tuple.MustToRelationship(tuple.MustParse("document:anotherdoc#viewer@user:tom")), + Relationship: tuple.ToV1Relationship(tuple.MustParse("document:anotherdoc#viewer@user:tom")), }, }, }, grpc.Trailer(&trailer)) diff --git a/internal/services/v1/permissions.go b/internal/services/v1/permissions.go index 7b39e3a96f..23ae27eae9 100644 --- a/internal/services/v1/permissions.go +++ b/internal/services/v1/permissions.go @@ -96,15 +96,8 @@ func (ps *permissionServer) CheckPermission(ctx context.Context, req *v1.CheckPe cr, metadata, err := computed.ComputeCheck(ctx, ps.dispatch, computed.CheckParameters{ - ResourceType: &core.RelationReference{ - Namespace: req.Resource.ObjectType, - Relation: req.Permission, - }, - Subject: &core.ObjectAndRelation{ - Namespace: req.Subject.Object.ObjectType, - ObjectId: req.Subject.Object.ObjectId, - Relation: normalizeSubjectRelation(req.Subject), - }, + ResourceType: tuple.RR(req.Resource.ObjectType, req.Permission), + Subject: tuple.ONR(req.Subject.Object.ObjectType, req.Subject.Object.ObjectId, normalizeSubjectRelation(req.Subject)), CaveatContext: caveatContext, AtRevision: atRevision, MaximumDepth: ps.config.MaximumAPIDepth, @@ -189,14 +182,14 @@ func pairItemFromCheckResult(checkResult *dispatch.ResourceCheckResult) *v1.Chec func requestItemFromResourceAndParameters(params *computed.CheckParameters, resourceID string) (*v1.CheckBulkPermissionsRequestItem, error) { item := &v1.CheckBulkPermissionsRequestItem{ Resource: &v1.ObjectReference{ - ObjectType: params.ResourceType.Namespace, + ObjectType: params.ResourceType.ObjectType, ObjectId: resourceID, }, Permission: params.ResourceType.Relation, Subject: &v1.SubjectReference{ Object: &v1.ObjectReference{ - ObjectType: params.Subject.Namespace, - ObjectId: params.Subject.ObjectId, + ObjectType: params.Subject.ObjectType, + ObjectId: params.Subject.ObjectID, }, OptionalRelation: denormalizeSubjectRelation(params.Subject.Relation), }, @@ -877,7 +870,7 @@ type loadBulkAdapter struct { stream grpc.ClientStreamingServer[v1.ImportBulkRelationshipsRequest, v1.ImportBulkRelationshipsResponse] referencedNamespaceMap map[string]*typesystem.TypeSystem referencedCaveatMap map[string]*core.CaveatDefinition - current core.RelationTuple + current tuple.Relationship caveat core.ContextualizedCaveat awaitingNamespaces []string @@ -888,7 +881,7 @@ type loadBulkAdapter struct { err error } -func (a *loadBulkAdapter) Next(_ context.Context) (*core.RelationTuple, error) { +func (a *loadBulkAdapter) Next(_ context.Context) (*tuple.Relationship, error) { for a.err == nil && a.numSent == len(a.currentBatch) { // Load a new batch batch, err := a.stream.Recv() @@ -915,14 +908,25 @@ func (a *loadBulkAdapter) Next(_ context.Context) (*core.RelationTuple, error) { return nil, nil } - a.current.Caveat = &a.caveat - a.current.Integrity = nil - tuple.CopyRelationshipToRelationTuple(a.currentBatch[a.numSent], &a.current) + if a.caveat.CaveatName != "" { + a.current.OptionalCaveat = &a.caveat + } else { + a.current.OptionalCaveat = nil + } + + a.current.OptionalIntegrity = nil + + a.current.RelationshipReference.Resource.ObjectType = a.currentBatch[a.numSent].Resource.ObjectType + a.current.RelationshipReference.Resource.ObjectID = a.currentBatch[a.numSent].Resource.ObjectId + a.current.RelationshipReference.Resource.Relation = a.currentBatch[a.numSent].Relation + a.current.Subject.ObjectType = a.currentBatch[a.numSent].Subject.Object.ObjectType + a.current.Subject.ObjectID = a.currentBatch[a.numSent].Subject.Object.ObjectId + a.current.Subject.Relation = stringz.DefaultEmpty(a.currentBatch[a.numSent].Subject.OptionalRelation, tuple.Ellipsis) if err := relationships.ValidateOneRelationship( a.referencedNamespaceMap, a.referencedCaveatMap, - &a.current, + a.current, relationships.ValidateRelationshipForCreateOrTouch, ); err != nil { return nil, err @@ -944,11 +948,7 @@ func (ps *permissionServer) ImportBulkRelationships(stream grpc.ClientStreamingS stream: stream, referencedNamespaceMap: loadedNamespaces, referencedCaveatMap: loadedCaveats, - current: core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{}, - Subject: &core.ObjectAndRelation{}, - }, - caveat: core.ContextualizedCaveat{}, + caveat: core.ContextualizedCaveat{}, } resolver := typesystem.ResolverForDatastoreReader(rwt) @@ -1112,17 +1112,32 @@ func ExportBulk(ctx context.Context, ds datastore.Datastore, batchSize uint64, r // Lop off any rels we've already sent rels = rels[:0] - tplFn := func(tpl *core.RelationTuple) { + relFn := func(rel tuple.Relationship) { offset := len(rels) rels = append(rels, &relsArray[offset]) // nozero - tuple.CopyRelationTupleToRelationship(tpl, &relsArray[offset], &caveatArray[offset]) + + v1Rel := &relsArray[offset] + v1Rel.Resource.ObjectType = rel.RelationshipReference.Resource.ObjectType + v1Rel.Resource.ObjectId = rel.RelationshipReference.Resource.ObjectID + v1Rel.Relation = rel.RelationshipReference.Resource.Relation + v1Rel.Subject.Object.ObjectType = rel.RelationshipReference.Subject.ObjectType + v1Rel.Subject.Object.ObjectId = rel.RelationshipReference.Subject.ObjectID + v1Rel.Subject.OptionalRelation = denormalizeSubjectRelation(rel.RelationshipReference.Subject.Relation) + + if rel.OptionalCaveat != nil { + caveatArray[offset].CaveatName = rel.OptionalCaveat.CaveatName + caveatArray[offset].Context = rel.OptionalCaveat.Context + } else { + caveatArray[offset].CaveatName = "" + caveatArray[offset].Context = nil + } } cur, err = queryForEach( ctx, reader, relationshipFilter, - tplFn, + relFn, dsoptions.WithLimit(&limit), dsoptions.WithAfter(cur), dsoptions.WithSort(dsoptions.ByResource), @@ -1141,7 +1156,7 @@ func ExportBulk(ctx context.Context, ds datastore.Datastore, batchSize uint64, r Revision: atRevision.String(), Sections: []string{ ns.Definition.Name, - tuple.MustString(cur), + tuple.MustString(*dsoptions.ToRelationship(cur)), }, }, }, diff --git a/internal/services/v1/permissions_test.go b/internal/services/v1/permissions_test.go index fc3b76de7c..082b7f9ba0 100644 --- a/internal/services/v1/permissions_test.go +++ b/internal/services/v1/permissions_test.go @@ -305,9 +305,9 @@ func TestCheckPermissions(t *testing.T) { debugInfo := checkResp.DebugTrace require.NotNil(debugInfo.Check) require.NotNil(debugInfo.Check.Duration) - require.Equal(tuple.StringObjectRef(tc.resource), tuple.StringObjectRef(debugInfo.Check.Resource)) + require.Equal(tuple.V1StringObjectRef(tc.resource), tuple.V1StringObjectRef(debugInfo.Check.Resource)) require.Equal(tc.permission, debugInfo.Check.Permission) - require.Equal(tuple.StringSubjectRef(tc.subject), tuple.StringSubjectRef(debugInfo.Check.Subject)) + require.Equal(tuple.V1StringSubjectRef(tc.subject), tuple.V1StringSubjectRef(debugInfo.Check.Subject)) } else { require.Nil(encodedDebugInfo) } @@ -403,7 +403,7 @@ func TestCheckPermissionWithDebugInfoInError(t *testing.T) { permission view = viewer } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:doc1#viewer@user:tom"), tuple.MustParse("document:doc1#viewer@document:doc2#view"), tuple.MustParse("document:doc2#viewer@document:doc3#view"), @@ -772,11 +772,9 @@ func countLeafs(node *v1.PermissionRelationshipTree) int { } } -var ONR = tuple.ObjectAndRelation - func DS(objectType string, objectID string, objectRelation string) *core.DirectSubject { return &core.DirectSubject{ - Subject: ONR(objectType, objectID, objectRelation), + Subject: tuple.CoreONR(objectType, objectID, objectRelation), } } @@ -1088,7 +1086,7 @@ func TestCheckWithCaveatErrors(t *testing.T) { permission view = viewer } `, - []*core.RelationTuple{tuple.MustParse("document:firstdoc#viewer@user:tom[somecaveat]")}, + []tuple.Relationship{tuple.MustParse("document:firstdoc#viewer@user:tom[somecaveat]")}, assertions, ) }) @@ -1174,7 +1172,7 @@ func TestLookupResourcesWithCaveats(t *testing.T) { relation viewer: user | user with testcaveat permission view = viewer } - `, []*core.RelationTuple{ + `, []tuple.Relationship{ tuple.MustParse("document:first#viewer@user:tom"), tuple.MustWithCaveat(tuple.MustParse("document:second#viewer@user:tom"), "testcaveat"), }, require) @@ -1293,7 +1291,7 @@ func TestLookupSubjectsWithCaveats(t *testing.T) { relation viewer: user | user with testcaveat permission view = viewer } - `, []*core.RelationTuple{ + `, []tuple.Relationship{ tuple.MustParse("document:first#viewer@user:tom"), tuple.MustWithCaveat(tuple.MustParse("document:first#viewer@user:sarah"), "testcaveat"), }, require) @@ -1457,7 +1455,7 @@ func TestLookupSubjectsWithCaveatedWildcards(t *testing.T) { relation banned: user with testcaveat permission view = viewer - banned } - `, []*core.RelationTuple{ + `, []tuple.Relationship{ tuple.MustWithCaveat(tuple.MustParse("document:first#viewer@user:*"), "testcaveat"), tuple.MustWithCaveat(tuple.MustParse("document:first#banned@user:bannedguy"), "anothercaveat"), }, require) @@ -1714,7 +1712,7 @@ func TestLookupResourcesDeduplication(t *testing.T) { relation editor: user permission view = viewer + editor } - `, []*core.RelationTuple{ + `, []tuple.Relationship{ tuple.MustParse("document:first#viewer@user:tom"), tuple.MustParse("document:first#editor@user:tom"), }, require) @@ -1982,12 +1980,14 @@ func TestCheckBulkPermissions(t *testing.T) { } for _, r := range tt.requests { - req.Items = append(req.Items, relToCheckBulkRequestItem(r)) + req.Items = append(req.Items, mustRelToCheckBulkRequestItem(r)) } expected := make([]*v1.CheckBulkPermissionsPair, 0, len(tt.response)) for _, r := range tt.response { - reqRel := tuple.ParseRel(r.req) + reqRel, err := tuple.ParseV1Rel(r.req) + require.NoError(t, err) + resp := &v1.CheckBulkPermissionsPair_Item{ Item: &v1.CheckBulkPermissionsResponseItem{ Permissionship: r.resp, @@ -2034,8 +2034,12 @@ func TestCheckBulkPermissions(t *testing.T) { } } -func relToCheckBulkRequestItem(rel string) *v1.CheckBulkPermissionsRequestItem { - r := tuple.ParseRel(rel) +func mustRelToCheckBulkRequestItem(rel string) *v1.CheckBulkPermissionsRequestItem { + r, err := tuple.ParseV1Rel(rel) + if err != nil { + panic(err) + } + item := &v1.CheckBulkPermissionsRequestItem{ Resource: r.Resource, Permission: r.Relation, @@ -2170,7 +2174,7 @@ func TestExportBulkRelationships(t *testing.T) { "", ) batch[i] = rel - expectedRels.Add(tuple.MustStringRelationship(rel)) + expectedRels.Add(tuple.MustV1RelString(rel)) } ctx := context.Background() @@ -2233,7 +2237,7 @@ func TestExportBulkRelationships(t *testing.T) { totalRead += len(batch.Relationships) for _, rel := range batch.Relationships { - remainingRels.Remove(tuple.MustStringRelationship(rel)) + remainingRels.Remove(tuple.MustV1RelString(rel)) } } @@ -2330,12 +2334,12 @@ func TestExportBulkRelationshipsWithFilter(t *testing.T) { if tc.filter != nil { filter, err := datastore.RelationshipsFilterFromPublicFilter(tc.filter) require.NoError(err) - if !filter.Test(tuple.MustFromRelationship(rel)) { + if !filter.Test(tuple.FromV1Relationship(rel)) { continue } } - expectedRels.Add(tuple.MustStringRelationship(rel)) + expectedRels.Add(tuple.MustV1RelString(rel)) } require.Equal(tc.expectedCount, expectedRels.Size()) @@ -2386,12 +2390,12 @@ func TestExportBulkRelationshipsWithFilter(t *testing.T) { if tc.filter != nil { filter, err := datastore.RelationshipsFilterFromPublicFilter(tc.filter) require.NoError(err) - require.True(filter.Test(tuple.MustFromRelationship(rel)), "relationship did not match filter: %s", rel) + require.True(filter.Test(tuple.FromV1Relationship(rel)), "relationship did not match filter: %s", rel) } - require.True(remainingRels.Has(tuple.MustStringRelationship(rel)), "relationship was not expected or was repeated: %s", rel) - remainingRels.Remove(tuple.MustStringRelationship(rel)) - foundRels.Add(tuple.MustStringRelationship(rel)) + require.True(remainingRels.Has(tuple.MustV1RelString(rel)), "relationship was not expected or was repeated: %s", rel) + remainingRels.Remove(tuple.MustV1RelString(rel)) + foundRels.Add(tuple.MustV1RelString(rel)) } cancel() diff --git a/internal/services/v1/preconditions.go b/internal/services/v1/preconditions.go index e7a0e47378..4c8612a5f9 100644 --- a/internal/services/v1/preconditions.go +++ b/internal/services/v1/preconditions.go @@ -29,21 +29,19 @@ func checkPreconditions( if err != nil { return fmt.Errorf("error reading relationships: %w", err) } - defer iter.Close() - first := iter.Next() - if first == nil && iter.Err() != nil { + _, ok, err := datastore.FirstRelationshipIn(iter) + if err != nil { return fmt.Errorf("error reading relationships from iterator: %w", err) } - iter.Close() switch precond.Operation { case v1.Precondition_OPERATION_MUST_NOT_MATCH: - if first != nil { + if ok { return NewPreconditionFailedErr(precond) } case v1.Precondition_OPERATION_MUST_MATCH: - if first == nil { + if !ok { return NewPreconditionFailedErr(precond) } default: diff --git a/internal/services/v1/relationships.go b/internal/services/v1/relationships.go index 327c05bdc0..56a8d3decd 100644 --- a/internal/services/v1/relationships.go +++ b/internal/services/v1/relationships.go @@ -196,12 +196,12 @@ func (ps *permissionServer) ReadRelationships(req *v1.ReadRelationshipsRequest, return ps.rewriteError(ctx, NewInvalidCursorErr("did not find expected resume relationship")) } - parsed := tuple.Parse(decodedCursor.Sections[0]) - if parsed == nil { + parsed, err := tuple.Parse(decodedCursor.Sections[0]) + if err != nil { return ps.rewriteError(ctx, NewInvalidCursorErr("could not parse resume relationship")) } - startCursor = options.Cursor(parsed) + startCursor = options.ToCursor(parsed) } pageSize := ps.config.MaxDatastoreReadPageSize @@ -217,7 +217,7 @@ func (ps *permissionServer) ReadRelationships(req *v1.ReadRelationshipsRequest, return ps.rewriteError(ctx, fmt.Errorf("error filtering: %w", err)) } - tupleIterator, err := pagination.NewPaginatedIterator( + it, err := pagination.NewPaginatedIterator( ctx, ds, dsFilter, @@ -228,50 +228,47 @@ func (ps *permissionServer) ReadRelationships(req *v1.ReadRelationshipsRequest, if err != nil { return ps.rewriteError(ctx, err) } - defer tupleIterator.Close() response := &v1.ReadRelationshipsResponse{ ReadAt: revisionReadAt, + Relationship: &v1.Relationship{ + Resource: &v1.ObjectReference{}, + Subject: &v1.SubjectReference{ + Object: &v1.ObjectReference{}, + }, + }, } - targetRel := tuple.NewRelationship() - targetCaveat := &v1.ContextualizedCaveat{} - var returnedCount uint64 dispatchCursor := &dispatchv1.Cursor{ DispatchVersion: 1, Sections: []string{""}, } - for tpl := tupleIterator.Next(); tpl != nil; tpl = tupleIterator.Next() { - if limit > 0 && returnedCount >= limit { - break + var returnedCount uint64 + for rel, err := range it { + if err != nil { + return ps.rewriteError(ctx, fmt.Errorf("error when reading tuples: %w", err)) } - if tupleIterator.Err() != nil { - return ps.rewriteError(ctx, fmt.Errorf("error when reading tuples: %w", tupleIterator.Err())) + if limit > 0 && returnedCount >= limit { + break } - dispatchCursor.Sections[0] = tuple.StringWithoutCaveat(tpl) + dispatchCursor.Sections[0] = tuple.StringWithoutCaveat(rel) encodedCursor, err := cursor.EncodeFromDispatchCursor(dispatchCursor, rrRequestHash, atRevision, nil) if err != nil { return ps.rewriteError(ctx, err) } - tuple.MustToRelationshipMutating(tpl, targetRel, targetCaveat) - response.Relationship = targetRel + tuple.CopyToV1Relationship(rel, response.Relationship) response.AfterResultCursor = encodedCursor + err = resp.Send(response) if err != nil { return ps.rewriteError(ctx, fmt.Errorf("error when streaming tuple: %w", err)) } returnedCount++ } - - if tupleIterator.Err() != nil { - return ps.rewriteError(ctx, fmt.Errorf("error when reading tuples: %w", tupleIterator.Err())) - } - - tupleIterator.Close() return nil } @@ -302,7 +299,8 @@ func (ps *permissionServer) WriteRelationships(ctx context.Context, req *v1.Writ // Check for duplicate updates and create the set of caveat names to load. updateRelationshipSet := mapz.NewSet[string]() for _, update := range req.Updates { - tupleStr := tuple.StringRelationshipWithoutCaveat(update.Relationship) + // TODO(jschorr): Change to struct-based keys. + tupleStr := tuple.V1StringRelationshipWithoutCaveat(update.Relationship) if !updateRelationshipSet.Add(tupleStr) { return nil, ps.rewriteError( ctx, @@ -319,7 +317,11 @@ func (ps *permissionServer) WriteRelationships(ctx context.Context, req *v1.Writ // Execute the write operation(s). span.AddEvent("read write transaction") - tupleUpdates := tuple.UpdateFromRelationshipUpdates(req.Updates) + relUpdates, err := tuple.UpdatesFromV1RelationshipUpdates(req.Updates) + if err != nil { + return nil, ps.rewriteError(ctx, err) + } + revision, err := ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { span.AddEvent("preconditions") @@ -332,7 +334,7 @@ func (ps *permissionServer) WriteRelationships(ctx context.Context, req *v1.Writ // Validate the updates. span.AddEvent("validate updates") - err := relationships.ValidateRelationshipUpdates(ctx, rwt, tupleUpdates) + err := relationships.ValidateRelationshipUpdates(ctx, rwt, relUpdates) if err != nil { return ps.rewriteError(ctx, err) } @@ -353,7 +355,7 @@ func (ps *permissionServer) WriteRelationships(ctx context.Context, req *v1.Writ } span.AddEvent("write relationships") - return rwt.WriteRelationships(ctx, tupleUpdates) + return rwt.WriteRelationships(ctx, relUpdates) }, options.WithMetadata(req.OptionalTransactionMetadata)) if err != nil { return nil, ps.rewriteError(ctx, err) @@ -445,15 +447,14 @@ func (ps *permissionServer) DeleteRelationships(ctx context.Context, req *v1.Del return ps.rewriteError(ctx, err) } - iter, err := rwt.QueryRelationships(ctx, filter, options.WithLimit(&limitPlusOne)) + it, err := rwt.QueryRelationships(ctx, filter, options.WithLimit(&limitPlusOne)) if err != nil { return ps.rewriteError(ctx, err) } - defer iter.Close() counter := uint64(0) - for tpl := iter.Next(); tpl != nil; tpl = iter.Next() { - if iter.Err() != nil { + for _, err := range it { + if err != nil { return ps.rewriteError(ctx, err) } @@ -463,7 +464,6 @@ func (ps *permissionServer) DeleteRelationships(ctx context.Context, req *v1.Del counter++ } - iter.Close() } // Delete with the specified limit. diff --git a/internal/services/v1/relationships_test.go b/internal/services/v1/relationships_test.go index dc2a50dbb5..d770c703f7 100644 --- a/internal/services/v1/relationships_test.go +++ b/internal/services/v1/relationships_test.go @@ -327,9 +327,9 @@ func TestReadRelationships(t *testing.T) { dsFilter, err := datastore.RelationshipsFilterFromPublicFilter(tc.filter) require.NoError(err) - require.True(dsFilter.Test(tuple.MustFromRelationship(rel.Relationship)), "relationship did not match filter: %v", rel.Relationship) + require.True(dsFilter.Test(tuple.FromV1Relationship(rel.Relationship)), "relationship did not match filter: %v", rel.Relationship) - relString := tuple.MustRelString(rel.Relationship) + relString := tuple.MustV1RelString(rel.Relationship) _, found := tc.expected[relString] require.True(found, "relationship was not expected: %s", relString) @@ -367,7 +367,7 @@ func TestWriteRelationships(t *testing.T) { client := v1.NewPermissionsServiceClient(conn) t.Cleanup(cleanup) - toWrite := []*core.RelationTuple{ + toWrite := []tuple.Relationship{ tuple.MustParse("document:totallynew#parent@folder:plans"), tuple.MustParse("document:--base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==#owner@user:--base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK=="), tuple.MustParse("document:veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryincrediblysuuperlong#owner@user:veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryincrediblysuuperlong"), @@ -377,11 +377,11 @@ func TestWriteRelationships(t *testing.T) { resp, err := client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ Updates: []*v1.RelationshipUpdate{{ Operation: v1.RelationshipUpdate_OPERATION_CREATE, - Relationship: tuple.MustToRelationship(toWrite[0]), + Relationship: tuple.ToV1Relationship(toWrite[0]), }}, OptionalPreconditions: []*v1.Precondition{{ Operation: v1.Precondition_OPERATION_MUST_MATCH, - Filter: tuple.MustToFilter(toWrite[0]), + Filter: tuple.ToV1Filter(toWrite[0]), }}, }) require.Nil(resp) @@ -396,7 +396,7 @@ func TestWriteRelationships(t *testing.T) { "precondition_subject_type", ) - existing := tuple.Parse(tf.StandardTuples[0]) + existing := tuple.MustParse(tf.StandardRelationships[0]) require.NotNil(existing) // Write with a succeeding precondition @@ -404,14 +404,14 @@ func TestWriteRelationships(t *testing.T) { for _, tpl := range toWrite { toWriteUpdates = append(toWriteUpdates, &v1.RelationshipUpdate{ Operation: v1.RelationshipUpdate_OPERATION_CREATE, - Relationship: tuple.MustToRelationship(tpl), + Relationship: tuple.ToV1Relationship(tpl), }) } resp, err = client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ Updates: toWriteUpdates, OptionalPreconditions: []*v1.Precondition{{ Operation: v1.Precondition_OPERATION_MUST_MATCH, - Filter: tuple.MustToFilter(existing), + Filter: tuple.ToV1Filter(existing), }}, }) require.NoError(err) @@ -421,8 +421,8 @@ func TestWriteRelationships(t *testing.T) { // Ensure the written relationships exist for _, tpl := range toWrite { findWritten := &v1.RelationshipFilter{ - ResourceType: tpl.ResourceAndRelation.Namespace, - OptionalResourceId: tpl.ResourceAndRelation.ObjectId, + ResourceType: tpl.Resource.ObjectType, + OptionalResourceId: tpl.Resource.ObjectID, } stream, err := client.ReadRelationships(context.Background(), &v1.ReadRelationshipsRequest{ @@ -431,7 +431,8 @@ func TestWriteRelationships(t *testing.T) { require.NoError(err) rel, err := stream.Recv() require.NoError(err) - relStr, err := tuple.StringRelationship(rel.Relationship) + + relStr, err := tuple.V1StringRelationship(rel.Relationship) require.NoError(err) require.Equal(tuple.MustString(tpl), relStr) @@ -442,7 +443,7 @@ func TestWriteRelationships(t *testing.T) { deleted, err := client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ Updates: []*v1.RelationshipUpdate{{ Operation: v1.RelationshipUpdate_OPERATION_DELETE, - Relationship: tuple.MustToRelationship(tpl), + Relationship: tuple.ToV1Relationship(tpl), }}, }) require.NoError(err) @@ -473,7 +474,7 @@ func TestDeleteRelationshipViaWriteNoop(t *testing.T) { _, err := client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ Updates: []*v1.RelationshipUpdate{{ Operation: v1.RelationshipUpdate_OPERATION_DELETE, - Relationship: tuple.MustToRelationship(toDelete), + Relationship: tuple.ToV1Relationship(toDelete), }}, }) require.NoError(err) @@ -492,12 +493,13 @@ func TestWriteCaveatedRelationships(t *testing.T) { caveatCtx, err := structpb.NewStruct(map[string]any{"expectedSecret": "hi"}) req.NoError(err) - toWrite.Caveat = &core.ContextualizedCaveat{ + toWrite.OptionalCaveat = &core.ContextualizedCaveat{ CaveatName: "doesnotexist", Context: caveatCtx, } - toWrite.Caveat.Context = caveatCtx - relWritten := tuple.MustToRelationship(toWrite) + toWrite.OptionalCaveat.Context = caveatCtx + + relWritten := tuple.ToV1Relationship(toWrite) writeReq := &v1.WriteRelationshipsRequest{ Updates: []*v1.RelationshipUpdate{{ Operation: v1.RelationshipUpdate_OPERATION_CREATE, @@ -522,9 +524,9 @@ func TestWriteCaveatedRelationships(t *testing.T) { req.True(proto.Equal(relWritten, relRead)) // issue the deletion - relToDelete := tuple.MustToRelationship(tuple.MustParse("document:companyplan#caveated_viewer@user:johndoe#...")) + relToDelete := tuple.ToV1Relationship(tuple.MustParse("document:companyplan#caveated_viewer@user:johndoe#...")) if deleteWithCaveat { - relToDelete = tuple.MustToRelationship(tuple.MustParse("document:companyplan#caveated_viewer@user:johndoe#...[test]")) + relToDelete = tuple.ToV1Relationship(tuple.MustParse("document:companyplan#caveated_viewer@user:johndoe#...[test]")) } deleteReq := &v1.WriteRelationshipsRequest{ @@ -1594,7 +1596,7 @@ func TestReadRelationshipsWithTimeout(t *testing.T) { counter++ updates = append(updates, &v1.RelationshipUpdate{ Operation: v1.RelationshipUpdate_OPERATION_CREATE, - Relationship: tuple.MustToRelationship(tuple.Parse(fmt.Sprintf("document:doc%d#viewer@user:someguy", counter))), + Relationship: tuple.ToV1Relationship(tuple.MustParse(fmt.Sprintf("document:doc%d#viewer@user:someguy", counter))), }) } @@ -1682,7 +1684,7 @@ func readOfType(require *require.Assertions, resourceType string, client v1.Perm } require.NoError(err) - got[tuple.MustRelString(rel.Relationship)] = struct{}{} + got[tuple.MustV1RelString(rel.Relationship)] = struct{}{} } return got } @@ -1698,8 +1700,8 @@ func readAll(require *require.Assertions, client v1.PermissionsServiceClient, to } func standardTuplesWithout(without map[string]struct{}) map[string]struct{} { - out := make(map[string]struct{}, len(tf.StandardTuples)-len(without)) - for _, t := range tf.StandardTuples { + out := make(map[string]struct{}, len(tf.StandardRelationships)-len(without)) + for _, t := range tf.StandardRelationships { t = tuple.MustString(tuple.MustParse(t)) if _, ok := without[t]; ok { continue @@ -1727,7 +1729,7 @@ func TestManyConcurrentWriteRelationshipsReturnsSerializationErrorOnMemdb(t *tes for j := 0; j < 500; j++ { updates = append(updates, &v1.RelationshipUpdate{ Operation: v1.RelationshipUpdate_OPERATION_CREATE, - Relationship: tuple.MustToRelationship(tuple.MustParse(fmt.Sprintf("document:doc-%d-%d#viewer@user:tom", i, j))), + Relationship: tuple.ToV1Relationship(tuple.MustParse(fmt.Sprintf("document:doc-%d-%d#viewer@user:tom", i, j))), }) } diff --git a/internal/services/v1/schema_test.go b/internal/services/v1/schema_test.go index 1f8d8853cd..c3df95e77c 100644 --- a/internal/services/v1/schema_test.go +++ b/internal/services/v1/schema_test.go @@ -106,7 +106,7 @@ func TestSchemaDeleteRelation(t *testing.T) { // Write a relationship for one of the relations. _, err = v1client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ - Updates: []*v1.RelationshipUpdate{tuple.UpdateToRelationshipUpdate(tuple.Create( + Updates: []*v1.RelationshipUpdate{tuple.MustUpdateToV1RelationshipUpdate(tuple.Create( tuple.MustParse("example/document:somedoc#somerelation@example/user:someuser#..."), ))}, }) @@ -136,7 +136,7 @@ func TestSchemaDeleteRelation(t *testing.T) { // Delete the relationship. _, err = v1client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ - Updates: []*v1.RelationshipUpdate{tuple.UpdateToRelationshipUpdate(tuple.Delete( + Updates: []*v1.RelationshipUpdate{tuple.MustUpdateToV1RelationshipUpdate(tuple.Delete( tuple.MustParse("example/document:somedoc#somerelation@example/user:someuser#..."), ))}, }) @@ -173,7 +173,7 @@ func TestSchemaDeletePermission(t *testing.T) { // Write a relationship for one of the relations. _, err = v1client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ - Updates: []*v1.RelationshipUpdate{tuple.UpdateToRelationshipUpdate(tuple.Create( + Updates: []*v1.RelationshipUpdate{tuple.MustUpdateToV1RelationshipUpdate(tuple.Create( tuple.MustParse("example/document:somedoc#somerelation@example/user:someuser#..."), ))}, }) @@ -211,7 +211,7 @@ func TestSchemaChangeRelationToPermission(t *testing.T) { // Write a relationship for one of the relations. _, err = v1client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ - Updates: []*v1.RelationshipUpdate{tuple.UpdateToRelationshipUpdate(tuple.Create( + Updates: []*v1.RelationshipUpdate{tuple.MustUpdateToV1RelationshipUpdate(tuple.Create( tuple.MustParse("example/document:somedoc#anotherrelation@example/user:someuser#..."), ))}, }) @@ -231,7 +231,7 @@ func TestSchemaChangeRelationToPermission(t *testing.T) { // Delete the relationship. _, err = v1client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ - Updates: []*v1.RelationshipUpdate{tuple.UpdateToRelationshipUpdate(tuple.Delete( + Updates: []*v1.RelationshipUpdate{tuple.MustUpdateToV1RelationshipUpdate(tuple.Delete( tuple.MustParse("example/document:somedoc#anotherrelation@example/user:someuser#..."), ))}, }) @@ -269,7 +269,7 @@ func TestSchemaDeleteDefinition(t *testing.T) { // Write a relationship for one of the relations. _, err = v1client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ - Updates: []*v1.RelationshipUpdate{tuple.UpdateToRelationshipUpdate(tuple.Create( + Updates: []*v1.RelationshipUpdate{tuple.MustUpdateToV1RelationshipUpdate(tuple.Create( tuple.MustParse("example/document:somedoc#somerelation@example/user:someuser#..."), ))}, }) @@ -283,7 +283,7 @@ func TestSchemaDeleteDefinition(t *testing.T) { // Delete the relationship. _, err = v1client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ - Updates: []*v1.RelationshipUpdate{tuple.UpdateToRelationshipUpdate(tuple.Delete( + Updates: []*v1.RelationshipUpdate{tuple.MustUpdateToV1RelationshipUpdate(tuple.Delete( tuple.MustParse("example/document:somedoc#somerelation@example/user:someuser#..."), ))}, }) @@ -319,7 +319,7 @@ func TestSchemaRemoveWildcard(t *testing.T) { // Write the wildcard relationship. _, err = v1client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ - Updates: []*v1.RelationshipUpdate{tuple.UpdateToRelationshipUpdate(tuple.Create( + Updates: []*v1.RelationshipUpdate{tuple.MustUpdateToV1RelationshipUpdate(tuple.Create( tuple.MustParse("example/document:somedoc#somerelation@example/user:*"), ))}, }) @@ -344,7 +344,7 @@ definition example/user {}` // Delete the relationship. _, err = v1client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ - Updates: []*v1.RelationshipUpdate{tuple.UpdateToRelationshipUpdate(tuple.Delete( + Updates: []*v1.RelationshipUpdate{tuple.MustUpdateToV1RelationshipUpdate(tuple.Delete( tuple.MustParse("example/document:somedoc#somerelation@example/user:*"), ))}, }) @@ -381,7 +381,7 @@ func TestSchemaEmpty(t *testing.T) { // Write a relationship for one of the relations. _, err = v1client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ - Updates: []*v1.RelationshipUpdate{tuple.UpdateToRelationshipUpdate(tuple.Create( + Updates: []*v1.RelationshipUpdate{tuple.MustUpdateToV1RelationshipUpdate(tuple.Create( tuple.MustParse("example/document:somedoc#somerelation@example/user:someuser#..."), ))}, }) @@ -395,7 +395,7 @@ func TestSchemaEmpty(t *testing.T) { // Delete the relationship. _, err = v1client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ - Updates: []*v1.RelationshipUpdate{tuple.UpdateToRelationshipUpdate(tuple.Delete( + Updates: []*v1.RelationshipUpdate{tuple.MustUpdateToV1RelationshipUpdate(tuple.Delete( tuple.MustParse("example/document:somedoc#somerelation@example/user:someuser#..."), ))}, }) @@ -477,13 +477,13 @@ func TestSchemaRemoveCaveat(t *testing.T) { require.NoError(t, err) toWrite := tuple.MustParse("document:somedoc#somerelation@user:tom") - toWrite.Caveat = &core.ContextualizedCaveat{ + toWrite.OptionalCaveat = &core.ContextualizedCaveat{ CaveatName: "somecaveat", Context: caveatCtx, } _, err = v1client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ - Updates: []*v1.RelationshipUpdate{tuple.UpdateToRelationshipUpdate(tuple.Create( + Updates: []*v1.RelationshipUpdate{tuple.MustUpdateToV1RelationshipUpdate(tuple.Create( toWrite, ))}, }) @@ -504,7 +504,7 @@ definition user {}` // Delete the relationship. _, err = v1client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ - Updates: []*v1.RelationshipUpdate{tuple.UpdateToRelationshipUpdate(tuple.Delete( + Updates: []*v1.RelationshipUpdate{tuple.MustUpdateToV1RelationshipUpdate(tuple.Delete( toWrite, ))}, }) diff --git a/internal/services/v1/watch.go b/internal/services/v1/watch.go index 45f53adcc4..9fc85b170b 100644 --- a/internal/services/v1/watch.go +++ b/internal/services/v1/watch.go @@ -15,7 +15,6 @@ import ( "github.com/authzed/spicedb/internal/services/shared" "github.com/authzed/spicedb/pkg/datastore" "github.com/authzed/spicedb/pkg/genutil/mapz" - core "github.com/authzed/spicedb/pkg/proto/core/v1" dispatchv1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" "github.com/authzed/spicedb/pkg/tuple" "github.com/authzed/spicedb/pkg/zedtoken" @@ -95,8 +94,13 @@ func (ws *watchServer) Watch(req *v1.WatchRequest, stream v1.WatchService_WatchS if ok { filtered := filterUpdates(objectTypes, filters, update.RelationshipChanges) if len(filtered) > 0 { + converted, err := tuple.UpdatesToV1RelationshipUpdates(filtered) + if err != nil { + return status.Errorf(codes.Internal, "failed to convert updates: %s", err) + } + if err := stream.Send(&v1.WatchResponse{ - Updates: filtered, + Updates: converted, ChangesThrough: zedtoken.MustNewFromRevision(update.Revision), OptionalTransactionMetadata: update.Metadata, }); err != nil { @@ -121,16 +125,14 @@ func (ws *watchServer) rewriteError(ctx context.Context, err error) error { return shared.RewriteError(ctx, err, &shared.ConfigForErrors{}) } -func filterUpdates(objectTypes *mapz.Set[string], filters []datastore.RelationshipsFilter, candidates []*core.RelationTupleUpdate) []*v1.RelationshipUpdate { - updates := tuple.UpdatesToRelationshipUpdates(candidates) - +func filterUpdates(objectTypes *mapz.Set[string], filters []datastore.RelationshipsFilter, updates []tuple.RelationshipUpdate) []tuple.RelationshipUpdate { if objectTypes.IsEmpty() && len(filters) == 0 { return updates } - filtered := make([]*v1.RelationshipUpdate, 0, len(updates)) + filtered := make([]tuple.RelationshipUpdate, 0, len(updates)) for _, update := range updates { - objectType := update.GetRelationship().GetResource().GetObjectType() + objectType := update.Relationship.Resource.ObjectType if !objectTypes.IsEmpty() && !objectTypes.Has(objectType) { continue } @@ -139,8 +141,7 @@ func filterUpdates(objectTypes *mapz.Set[string], filters []datastore.Relationsh // If there are filters, we need to check if the update matches any of them. matched := false for _, filter := range filters { - // TODO(jschorr): Maybe we should add TestRelationship to avoid the conversion? - if filter.Test(tuple.MustFromRelationship(update.GetRelationship())) { + if filter.Test(update.Relationship) { matched = true break } diff --git a/internal/services/v1/watch_test.go b/internal/services/v1/watch_test.go index 09de4fcecd..7b9ea41651 100644 --- a/internal/services/v1/watch_test.go +++ b/internal/services/v1/watch_test.go @@ -284,7 +284,7 @@ func sortUpdates(in []*v1.RelationshipUpdate) []*v1.RelationshipUpdate { out = append(out, in...) sort.Slice(out, func(i, j int) bool { left, right := out[i], out[j] - compareResult := strings.Compare(tuple.MustRelString(left.Relationship), tuple.MustRelString(right.Relationship)) + compareResult := strings.Compare(tuple.MustV1RelString(left.Relationship), tuple.MustV1RelString(right.Relationship)) if compareResult < 0 { return true } diff --git a/internal/testfixtures/datastore.go b/internal/testfixtures/datastore.go index 2dbdf77e0c..230bae950f 100644 --- a/internal/testfixtures/datastore.go +++ b/internal/testfixtures/datastore.go @@ -11,6 +11,8 @@ import ( "github.com/authzed/spicedb/pkg/caveats" caveattypes "github.com/authzed/spicedb/pkg/caveats/types" "github.com/authzed/spicedb/pkg/datastore" + "github.com/authzed/spicedb/pkg/datastore/options" + "github.com/authzed/spicedb/pkg/genutil/mapz" ns "github.com/authzed/spicedb/pkg/namespace" core "github.com/authzed/spicedb/pkg/proto/core/v1" "github.com/authzed/spicedb/pkg/schemadsl/compiler" @@ -105,10 +107,10 @@ var FolderNS = ns.Namespace( ), ) -// StandardTuples defines standard tuples for tests. -// NOTE: some tests index directly into this slice, so if you're adding a new tuple, add it +// StandardRelationships defines standard relationships for tests. +// NOTE: some tests index directly into this slice, so if you're adding a new relationship, add it // at the *end*. -var StandardTuples = []string{ +var StandardRelationships = []string{ "document:companyplan#parent@folder:company#...", "document:masterplan#parent@folder:strategy#...", "folder:strategy#parent@folder:company#...", @@ -151,13 +153,14 @@ func StandardDatastoreWithData(ds datastore.Datastore, require *require.Assertio ds, _ = StandardDatastoreWithSchema(ds, require) ctx := context.Background() - tuples := make([]*core.RelationTuple, 0, len(StandardTuples)) - for _, tupleStr := range StandardTuples { - tpl := tuple.Parse(tupleStr) - require.NotNil(tpl) - tuples = append(tuples, tpl) + rels := make([]tuple.Relationship, 0, len(StandardRelationships)) + for _, tupleStr := range StandardRelationships { + rel, err := tuple.Parse(tupleStr) + require.NoError(err) + require.NotNil(rel) + rels = append(rels, rel) } - revision, err := common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, tuples...) + revision, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, rels...) require.NoError(err) return ds, revision @@ -174,17 +177,20 @@ func StandardDatastoreWithCaveatedData(ds datastore.Datastore, require *require. }) require.NoError(err) - caveatedTpls := make([]*core.RelationTuple, 0, len(StandardTuples)) - for _, tupleStr := range StandardTuples { - tpl := tuple.Parse(tupleStr) - require.NotNil(tpl) - tpl.Caveat = &core.ContextualizedCaveat{ + rels := make([]tuple.Relationship, 0, len(StandardRelationships)) + for _, tupleStr := range StandardRelationships { + rel, err := tuple.Parse(tupleStr) + require.NoError(err) + require.NotNil(rel) + + rel.OptionalCaveat = &core.ContextualizedCaveat{ CaveatName: "test", Context: mustProtoStruct(map[string]any{"expectedSecret": "1234"}), } - caveatedTpls = append(caveatedTpls, tpl) + rels = append(rels, rel) } - revision, err := common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, caveatedTpls...) + + revision, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, rels...) require.NoError(err) return ds, revision @@ -212,7 +218,7 @@ func createTestCaveat(require *require.Assertions) []*core.CaveatDefinition { // DatastoreFromSchemaAndTestRelationships returns a validating datastore wrapping that specified, // loaded with the given scehma and relationships. -func DatastoreFromSchemaAndTestRelationships(ds datastore.Datastore, schema string, relationships []*core.RelationTuple, require *require.Assertions) (datastore.Datastore, datastore.Revision) { +func DatastoreFromSchemaAndTestRelationships(ds datastore.Datastore, schema string, relationships []tuple.Relationship, require *require.Assertions) (datastore.Datastore, datastore.Revision) { ctx := context.Background() validating := NewValidatingDatastore(ds) @@ -225,9 +231,9 @@ func DatastoreFromSchemaAndTestRelationships(ds datastore.Datastore, schema stri _ = writeDefinitions(validating, require, compiled.ObjectDefinitions, compiled.CaveatDefinitions) newRevision, err := validating.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { - mutations := make([]*core.RelationTupleUpdate, 0, len(relationships)) + mutations := make([]tuple.RelationshipUpdate, 0, len(relationships)) for _, rel := range relationships { - mutations = append(mutations, tuple.Create(rel.CloneVT())) + mutations = append(mutations, tuple.Create(rel)) } err = rwt.WriteRelationships(ctx, mutations) require.NoError(err) @@ -271,15 +277,15 @@ func writeDefinitions(ds datastore.Datastore, require *require.Assertions, objec return newRevision } -// TupleChecker is a helper type which provides an easy way for collecting relationships/tuples from +// RelationshipChecker is a helper type which provides an easy way for collecting relationships from // an iterator and verify those found. -type TupleChecker struct { +type RelationshipChecker struct { Require *require.Assertions DS datastore.Datastore } -func (tc TupleChecker) ExactRelationshipIterator(ctx context.Context, tpl *core.RelationTuple, rev datastore.Revision) datastore.RelationshipIterator { - filter := tuple.MustToFilter(tpl) +func (tc RelationshipChecker) ExactRelationshipIterator(ctx context.Context, rel tuple.Relationship, rev datastore.Revision) datastore.RelationshipIterator { + filter := tuple.ToV1Filter(rel) dsFilter, err := datastore.RelationshipsFilterFromPublicFilter(filter) tc.Require.NoError(err) @@ -288,59 +294,62 @@ func (tc TupleChecker) ExactRelationshipIterator(ctx context.Context, tpl *core. return iter } -func (tc TupleChecker) VerifyIteratorCount(iter datastore.RelationshipIterator, count int) { +func (tc RelationshipChecker) VerifyIteratorCount(iter datastore.RelationshipIterator, count int) { foundCount := 0 - for found := iter.Next(); found != nil; found = iter.Next() { + for _, err := range iter { + tc.Require.NoError(err) foundCount++ } - tc.Require.NoError(iter.Err()) tc.Require.Equal(count, foundCount) } -func (tc TupleChecker) VerifyIteratorResults(iter datastore.RelationshipIterator, tpls ...*core.RelationTuple) { - defer iter.Close() - - toFind := make(map[string]struct{}, 1024) - - for _, tpl := range tpls { - toFind[tuple.MustString(tpl)] = struct{}{} +func (tc RelationshipChecker) VerifyIteratorResults(iter datastore.RelationshipIterator, rels ...tuple.Relationship) { + toFind := mapz.NewSet[string]() + for _, rel := range rels { + toFind.Add(tuple.MustString(rel)) } - for found := iter.Next(); found != nil; found = iter.Next() { - tc.Require.NoError(iter.Err()) + for found, err := range iter { + tc.Require.NoError(err) + foundStr := tuple.MustString(found) - _, ok := toFind[foundStr] - tc.Require.True(ok, "found unexpected tuple %s in iterator", foundStr) - delete(toFind, foundStr) + tc.Require.True(toFind.Has(foundStr), "found unexpected relationship %s in iterator", foundStr) + toFind.Delete(foundStr) } - tc.Require.NoError(iter.Err()) - tc.Require.Zero(len(toFind), "did not find some expected tuples: %#v", toFind) + tc.Require.True(toFind.IsEmpty(), "did not find some expected relationships: %#v", toFind.AsSlice()) } -func (tc TupleChecker) VerifyOrderedIteratorResults(iter datastore.RelationshipIterator, tpls ...*core.RelationTuple) { - for _, tpl := range tpls { - expectedStr := tuple.MustString(tpl) +func (tc RelationshipChecker) VerifyOrderedIteratorResults(iter datastore.RelationshipIterator, rels ...tuple.Relationship) options.Cursor { + expected := make([]tuple.Relationship, 0, len(rels)) + for rel, err := range iter { + tc.Require.NoError(err) + expected = append(expected, rel) + } - found := iter.Next() - tc.Require.NotNil(found, "expected %s, but found no additional results", expectedStr) + var cursor options.Cursor + for index, rel := range rels { + expectedStr := tuple.MustString(rel) - foundStr := tuple.MustString(found) + if index > len(expected)-1 { + tc.Require.Fail("expected %s, but found no additional results", expectedStr) + } + + foundStr := tuple.MustString(expected[index]) tc.Require.Equal(expectedStr, foundStr) - } - pastLast := iter.Next() - tc.Require.Nil(pastLast) - tc.Require.Nil(iter.Err()) + cursor = options.ToCursor(rel) + } + return cursor } -func (tc TupleChecker) TupleExists(ctx context.Context, tpl *core.RelationTuple, rev datastore.Revision) { - iter := tc.ExactRelationshipIterator(ctx, tpl, rev) - tc.VerifyIteratorResults(iter, tpl) +func (tc RelationshipChecker) RelationshipExists(ctx context.Context, rel tuple.Relationship, rev datastore.Revision) { + iter := tc.ExactRelationshipIterator(ctx, rel, rev) + tc.VerifyIteratorResults(iter, rel) } -func (tc TupleChecker) NoTupleExists(ctx context.Context, tpl *core.RelationTuple, rev datastore.Revision) { - iter := tc.ExactRelationshipIterator(ctx, tpl, rev) +func (tc RelationshipChecker) NoRelationshipExists(ctx context.Context, rel tuple.Relationship, rev datastore.Revision) { + iter := tc.ExactRelationshipIterator(ctx, rel, rev) tc.VerifyIteratorResults(iter) } diff --git a/internal/testfixtures/generator.go b/internal/testfixtures/generator.go index 751f28271e..4b06938c3d 100644 --- a/internal/testfixtures/generator.go +++ b/internal/testfixtures/generator.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/authzed/spicedb/pkg/datastore" - core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/tuple" ) const ( @@ -30,41 +30,44 @@ func RandomObjectID(length uint8) string { return string(b) } -func NewBulkTupleGenerator(objectType, relation, subjectType string, count int, t *testing.T) *BulkTupleGenerator { - return &BulkTupleGenerator{ +func NewBulkRelationshipGenerator(objectType, relation, subjectType string, count int, t *testing.T) *BulkRelationshipGenerator { + return &BulkRelationshipGenerator{ count, t, - core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: objectType, - Relation: relation, - }, - Subject: &core.ObjectAndRelation{ - Namespace: subjectType, - Relation: datastore.Ellipsis, - }, - }, + objectType, + relation, + subjectType, } } -type BulkTupleGenerator struct { - remaining int - t *testing.T - - current core.RelationTuple +type BulkRelationshipGenerator struct { + remaining int + t *testing.T + objectType string + relation string + subjectType string } -func (btg *BulkTupleGenerator) Next(_ context.Context) (*core.RelationTuple, error) { +func (btg *BulkRelationshipGenerator) Next(_ context.Context) (*tuple.Relationship, error) { if btg.remaining <= 0 { return nil, nil } btg.remaining-- - btg.current.ResourceAndRelation.ObjectId = strconv.Itoa(btg.remaining) - btg.current.Subject.ObjectId = strconv.Itoa(btg.remaining) - btg.current.Caveat = nil - btg.current.Integrity = nil - return &btg.current, nil + return &tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.ObjectAndRelation{ + ObjectType: btg.objectType, + ObjectID: strconv.Itoa(btg.remaining), + Relation: btg.relation, + }, + Subject: tuple.ObjectAndRelation{ + ObjectType: btg.subjectType, + ObjectID: strconv.Itoa(btg.remaining), + Relation: datastore.Ellipsis, + }, + }, + }, nil } -var _ datastore.BulkWriteRelationshipSource = &BulkTupleGenerator{} +var _ datastore.BulkWriteRelationshipSource = &BulkRelationshipGenerator{} diff --git a/internal/testfixtures/validating.go b/internal/testfixtures/validating.go index bd88453b3b..75b83aeebe 100644 --- a/internal/testfixtures/validating.go +++ b/internal/testfixtures/validating.go @@ -209,7 +209,7 @@ func (vrwt validatingReadWriteTransaction) DeleteNamespaces(ctx context.Context, return vrwt.delegate.DeleteNamespaces(ctx, nsNames...) } -func (vrwt validatingReadWriteTransaction) WriteRelationships(ctx context.Context, mutations []*core.RelationTupleUpdate) error { +func (vrwt validatingReadWriteTransaction) WriteRelationships(ctx context.Context, mutations []tuple.RelationshipUpdate) error { if err := validateUpdatesToWrite(mutations...); err != nil { return err } @@ -217,12 +217,8 @@ func (vrwt validatingReadWriteTransaction) WriteRelationships(ctx context.Contex // Ensure there are no duplicate mutations. tupleSet := mapz.NewSet[string]() for _, mutation := range mutations { - if err := mutation.Validate(); err != nil { - return err - } - - if !tupleSet.Add(tuple.StringWithoutCaveat(mutation.Tuple)) { - return fmt.Errorf("found duplicate update for relationship %s", tuple.StringWithoutCaveat(mutation.Tuple)) + if !tupleSet.Add(tuple.StringWithoutCaveat(mutation.Relationship)) { + return fmt.Errorf("found duplicate update for relationship %s", tuple.StringWithoutCaveat(mutation.Relationship)) } } @@ -250,20 +246,24 @@ func (vrwt validatingReadWriteTransaction) BulkLoad(ctx context.Context, source } // validateUpdatesToWrite performs basic validation on relationship updates going into datastores. -func validateUpdatesToWrite(updates ...*core.RelationTupleUpdate) error { +func validateUpdatesToWrite(updates ...tuple.RelationshipUpdate) error { for _, update := range updates { - err := tuple.UpdateToRelationshipUpdate(update).HandwrittenValidate() + up, err := tuple.UpdateToV1RelationshipUpdate(update) if err != nil { return err } - if update.Tuple.Subject.Relation == "" { - return fmt.Errorf("expected ... instead of an empty relation string relation in %v", update.Tuple) + + if err := up.HandwrittenValidate(); err != nil { + return err + } + if update.Relationship.Subject.Relation == "" { + return fmt.Errorf("expected ... instead of an empty relation string relation in %v", update.Relationship) } - if update.Tuple.Subject.ObjectId == tuple.PublicWildcard && update.Tuple.Subject.Relation != tuple.Ellipsis { + if update.Relationship.Subject.ObjectID == tuple.PublicWildcard && update.Relationship.Subject.Relation != tuple.Ellipsis { return fmt.Errorf( "attempt to write a wildcard relationship (`%s`) with a non-empty relation `%v`. Please report this bug", - tuple.MustString(update.Tuple), - update.Tuple.Subject.Relation, + tuple.MustString(update.Relationship), + update.Relationship.Subject.Relation, ) } } diff --git a/magefiles/go.mod b/magefiles/go.mod index 8ea280d2ad..830bdd7fa4 100644 --- a/magefiles/go.mod +++ b/magefiles/go.mod @@ -1,6 +1,6 @@ module magefiles -go 1.22.7 +go 1.23.1 require ( github.com/agnivade/wasmbrowsertest v0.8.0 diff --git a/magefiles/lint.go b/magefiles/lint.go index e07b6215e7..4615e987d4 100644 --- a/magefiles/lint.go +++ b/magefiles/lint.go @@ -86,9 +86,6 @@ func (Lint) Analyzers() error { "-nilvaluecheck.disallowed-nil-return-type-paths=*github.com/authzed/spicedb/pkg/proto/dispatch/v1.DispatchCheckResponse,*github.com/authzed/spicedb/pkg/proto/dispatch/v1.DispatchExpandResponse,*github.com/authzed/spicedb/pkg/proto/dispatch/v1.DispatchLookupResponse", "-exprstatementcheck", "-exprstatementcheck.disallowed-expr-statement-types=*github.com/rs/zerolog.Event:MarshalZerologObject:missing Send or Msg on zerolog log Event", - "-closeafterusagecheck", - "-closeafterusagecheck.must-be-closed-after-usage-types=github.com/authzed/spicedb/pkg/datastore.RelationshipIterator", - "-closeafterusagecheck.skip-pkg=github.com/authzed/spicedb/pkg/datastore,github.com/authzed/spicedb/internal/datastore,github.com/authzed/spicedb/internal/testfixtures", "-paniccheck", "-paniccheck.skip-files=_test,zz_", "-zerologmarshalcheck", diff --git a/pkg/datastore/datastore.go b/pkg/datastore/datastore.go index dbca36ee68..76fff5cd48 100644 --- a/pkg/datastore/datastore.go +++ b/pkg/datastore/datastore.go @@ -3,6 +3,7 @@ package datastore import ( "context" "fmt" + "iter" "slices" "sort" "strings" @@ -47,7 +48,7 @@ type RevisionChanges struct { Revision Revision // RelationshipChanges are any relationships that were changed at this revision. - RelationshipChanges []*core.RelationTupleUpdate + RelationshipChanges []tuple.RelationshipUpdate // ChangedDefinitions are any definitions that were added or changed at this revision. ChangedDefinitions []SchemaDefinition @@ -108,20 +109,20 @@ type RelationshipsFilter struct { } // Test returns true iff the given relationship is matched by this filter. -func (rf RelationshipsFilter) Test(relationship *core.RelationTuple) bool { - if rf.OptionalResourceType != "" && rf.OptionalResourceType != relationship.ResourceAndRelation.Namespace { +func (rf RelationshipsFilter) Test(relationship tuple.Relationship) bool { + if rf.OptionalResourceType != "" && rf.OptionalResourceType != relationship.Resource.ObjectType { return false } - if len(rf.OptionalResourceIds) > 0 && !slices.Contains(rf.OptionalResourceIds, relationship.ResourceAndRelation.ObjectId) { + if len(rf.OptionalResourceIds) > 0 && !slices.Contains(rf.OptionalResourceIds, relationship.Resource.ObjectID) { return false } - if rf.OptionalResourceIDPrefix != "" && !strings.HasPrefix(relationship.ResourceAndRelation.ObjectId, rf.OptionalResourceIDPrefix) { + if rf.OptionalResourceIDPrefix != "" && !strings.HasPrefix(relationship.Resource.ObjectID, rf.OptionalResourceIDPrefix) { return false } - if rf.OptionalResourceRelation != "" && rf.OptionalResourceRelation != relationship.ResourceAndRelation.Relation { + if rf.OptionalResourceRelation != "" && rf.OptionalResourceRelation != relationship.Resource.Relation { return false } @@ -135,7 +136,7 @@ func (rf RelationshipsFilter) Test(relationship *core.RelationTuple) bool { } if rf.OptionalCaveatName != "" { - if relationship.Caveat == nil || relationship.Caveat.CaveatName != rf.OptionalCaveatName { + if relationship.OptionalCaveat == nil || relationship.OptionalCaveat.CaveatName != rf.OptionalCaveatName { return false } } @@ -288,12 +289,12 @@ type SubjectsSelector struct { } // Test returns true iff the given subject is matched by this filter. -func (ss SubjectsSelector) Test(subject *core.ObjectAndRelation) bool { - if ss.OptionalSubjectType != "" && ss.OptionalSubjectType != subject.Namespace { +func (ss SubjectsSelector) Test(subject tuple.ObjectAndRelation) bool { + if ss.OptionalSubjectType != "" && ss.OptionalSubjectType != subject.ObjectType { return false } - if len(ss.OptionalSubjectIds) > 0 && !slices.Contains(ss.OptionalSubjectIds, subject.ObjectId) { + if len(ss.OptionalSubjectIds) > 0 && !slices.Contains(ss.OptionalSubjectIds, subject.ObjectID) { return false } @@ -446,7 +447,7 @@ type ReadWriteTransaction interface { CounterRegisterer // WriteRelationships takes a list of tuple mutations and applies them to the datastore. - WriteRelationships(ctx context.Context, mutations []*core.RelationTupleUpdate) error + WriteRelationships(ctx context.Context, mutations []tuple.RelationshipUpdate) error // DeleteRelationships deletes relationships that match the provided filter, with // the optional limit. If a limit is provided and reached, the method will return @@ -489,7 +490,7 @@ type BulkWriteRelationshipSource interface { // // Note: sources may re-use the same memory address for every tuple, data // may change on every call to next even if the pointer has not changed. - Next(ctx context.Context) (*core.RelationTuple, error) + Next(ctx context.Context) (*tuple.Relationship, error) } type WatchContent int @@ -751,21 +752,32 @@ type Stats struct { ObjectTypeStatistics []ObjectTypeStat } -// RelationshipIterator is an iterator over matched tuples. -type RelationshipIterator interface { - // Next returns the next tuple in the result set. - Next() *core.RelationTuple +// RelationshipIterator is an iterator over matched tuples. It is a single use +// iterator. +type RelationshipIterator iter.Seq2[tuple.Relationship, error] - // Cursor returns a cursor that can be used to resume reading of relationships - // from the last relationship returned. Only applies if a sort ordering was - // requested. - Cursor() (options.Cursor, error) +func IteratorToSlice(iter RelationshipIterator) ([]tuple.Relationship, error) { + results := make([]tuple.Relationship, 0) + for rel, err := range iter { + if err != nil { + return nil, err + } + results = append(results, rel) + } + return results, nil +} - // Err after receiving a nil response, the caller must check for an error. - Err() error +// FirstRelationshipIn returns the first relationship found via the iterator, if any. +func FirstRelationshipIn(iter RelationshipIterator) (tuple.Relationship, bool, error) { + for rel, err := range iter { + if err != nil { + return tuple.Relationship{}, false, err + } + + return rel, true, nil + } - // Close cancels the query and closes any open connections. - Close() + return tuple.Relationship{}, false, nil } // Revision is an interface for a comparable revision type that can be different for diff --git a/pkg/datastore/options/options.go b/pkg/datastore/options/options.go index 1d13f2ca11..f8fe66f4f4 100644 --- a/pkg/datastore/options/options.go +++ b/pkg/datastore/options/options.go @@ -3,7 +3,8 @@ package options import ( "google.golang.org/protobuf/types/known/structpb" - core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/spiceerrors" + "github.com/authzed/spicedb/pkg/tuple" ) //go:generate go run github.com/ecordell/optgen -output zz_generated.query_options.go . QueryOptions ReverseQueryOptions RWTOptions @@ -26,7 +27,19 @@ const ( BySubject ) -type Cursor *core.RelationTuple +type Cursor *tuple.Relationship + +func ToCursor(r tuple.Relationship) Cursor { + spiceerrors.DebugAssert(r.ValidateNotEmpty, "cannot create cursor from empty relationship") + return Cursor(&r) +} + +func ToRelationship(c Cursor) *tuple.Relationship { + if c == nil { + return nil + } + return (*tuple.Relationship)(c) +} // QueryOptions are the options that can affect the results of a normal forward query. type QueryOptions struct { diff --git a/pkg/datastore/pagination/iterator.go b/pkg/datastore/pagination/iterator.go index 0d1e810d7f..02626cc45b 100644 --- a/pkg/datastore/pagination/iterator.go +++ b/pkg/datastore/pagination/iterator.go @@ -2,12 +2,10 @@ package pagination import ( "context" - "errors" - "github.com/authzed/spicedb/internal/datastore/common" "github.com/authzed/spicedb/pkg/datastore" "github.com/authzed/spicedb/pkg/datastore/options" - core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/tuple" ) // NewPaginatedIterator creates an implementation of the datastore.Iterator @@ -20,103 +18,49 @@ func NewPaginatedIterator( order options.SortOrder, startCursor options.Cursor, ) (datastore.RelationshipIterator, error) { - pi := &paginatedIterator{ - ctx: ctx, - reader: reader, - filter: filter, - pageSize: pageSize, - order: order, - delegate: common.NewSliceRelationshipIterator(nil, options.ByResource), - } - - pi.startNewBatch(startCursor) - - return pi, pi.err -} - -type paginatedIterator struct { - ctx context.Context - reader datastore.Reader - filter datastore.RelationshipsFilter - pageSize uint64 - order options.SortOrder - - delegate datastore.RelationshipIterator - returnedFromBatch uint64 - err error - closed bool -} - -func (pi *paginatedIterator) Next() *core.RelationTuple { - if pi.Err() != nil { - return nil + iter, err := reader.QueryRelationships( + ctx, + filter, + options.WithSort(order), + options.WithLimit(&pageSize), + options.WithAfter(startCursor), + ) + if err != nil { + return nil, err } - var next *core.RelationTuple - for next = pi.delegate.Next(); next == nil; next = pi.delegate.Next() { - if pi.delegate.Err() != nil { - pi.err = pi.delegate.Err() - return nil - } - - if pi.returnedFromBatch < pi.pageSize { - // No more tuples to get - return nil - } - - cursor, err := pi.delegate.Cursor() - if err != nil { - if errors.Is(err, datastore.ErrCursorEmpty) { - // The last batch had no data - return nil + return func(yield func(tuple.Relationship, error) bool) { + cursor := startCursor + for { + var counter uint64 + for rel, err := range iter { + if !yield(rel, err) { + return + } + + cursor = options.ToCursor(rel) + counter++ + + if counter >= pageSize { + break + } } - pi.err = err - return nil - } + if counter < pageSize { + return + } - pi.startNewBatch(cursor) - if pi.err != nil { - return nil + iter, err = reader.QueryRelationships( + ctx, + filter, + options.WithSort(order), + options.WithLimit(&pageSize), + options.WithAfter(cursor), + ) + if err != nil { + yield(tuple.Relationship{}, err) + return + } } - } - - pi.returnedFromBatch++ - return next -} - -func (pi *paginatedIterator) startNewBatch(cursor options.Cursor) { - pi.delegate.Close() - pi.returnedFromBatch = 0 - pi.delegate, pi.err = pi.reader.QueryRelationships( - pi.ctx, - pi.filter, - options.WithSort(pi.order), - options.WithLimit(&pi.pageSize), - options.WithAfter(cursor), - ) -} - -func (pi *paginatedIterator) Cursor() (options.Cursor, error) { - return pi.delegate.Cursor() -} - -func (pi *paginatedIterator) Err() error { - switch { - case pi.closed: - return datastore.ErrClosedIterator - case pi.err != nil: - return pi.err - case pi.ctx.Err() != nil: - return pi.ctx.Err() - default: - return nil - } -} - -func (pi *paginatedIterator) Close() { - pi.closed = true - if pi.delegate != nil { - pi.delegate.Close() - } + }, nil } diff --git a/pkg/datastore/pagination/iterator_test.go b/pkg/datastore/pagination/iterator_test.go index 22a924e2e5..448b9a2b9d 100644 --- a/pkg/datastore/pagination/iterator_test.go +++ b/pkg/datastore/pagination/iterator_test.go @@ -2,11 +2,11 @@ package pagination import ( "context" - "errors" "fmt" "strconv" "testing" + "github.com/ccoveille/go-safecast" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -14,123 +14,14 @@ import ( "github.com/authzed/spicedb/pkg/datastore" "github.com/authzed/spicedb/pkg/datastore/options" core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/tuple" ) -func TestDownstreamErrors(t *testing.T) { - defaultPageSize := uint64(10) - defaultSortOrder := options.ByResource - defaultError := errors.New("something went wrong") - ctx := context.Background() - var nilIter *mockedIterator - var nilRel *core.RelationTuple - - t.Run("OnReader", func(t *testing.T) { - require := require.New(t) - ds := &mockedReader{} - ds. - On("QueryRelationships", options.Cursor(nil), defaultSortOrder, defaultPageSize). - Return(nilIter, defaultError) - - _, err := NewPaginatedIterator(ctx, ds, datastore.RelationshipsFilter{}, defaultPageSize, defaultSortOrder, nil) - require.ErrorIs(err, defaultError) - require.True(ds.AssertExpectations(t)) - }) - - t.Run("OnIterator", func(t *testing.T) { - require := require.New(t) - - iterMock := &mockedIterator{} - iterMock.On("Next").Return(nilRel) - iterMock.On("Err").Return(defaultError) - iterMock.On("Close") - - ds := &mockedReader{} - ds. - On("QueryRelationships", options.Cursor(nil), defaultSortOrder, defaultPageSize). - Return(iterMock, nil) - - iter, err := NewPaginatedIterator(ctx, ds, datastore.RelationshipsFilter{}, defaultPageSize, defaultSortOrder, nil) - require.NoError(err) - require.NotNil(iter) - - require.Nil(iter.Next()) - require.ErrorIs(iter.Err(), defaultError) - - iter.Close() - require.Nil(iter.Next()) - require.ErrorIs(iter.Err(), datastore.ErrClosedIterator) - - require.True(iterMock.AssertExpectations(t)) - require.True(ds.AssertExpectations(t)) - }) - - t.Run("OnIterator", func(t *testing.T) { - require := require.New(t) - - iterMock := &mockedIterator{} - iterMock.On("Next").Return(&core.RelationTuple{}) - iterMock.On("Cursor").Return(options.Cursor(nil), defaultError) - iterMock.On("Close") - - ds := &mockedReader{} - ds. - On("QueryRelationships", options.Cursor(nil), defaultSortOrder, defaultPageSize). - Return(iterMock, nil) - - iter, err := NewPaginatedIterator(ctx, ds, datastore.RelationshipsFilter{}, defaultPageSize, defaultSortOrder, nil) - require.NoError(err) - require.NotNil(iter) - - require.NotNil(iter.Next()) - require.NoError(iter.Err()) - - cursor, err := iter.Cursor() - require.Nil(cursor) - require.ErrorIs(err, defaultError) - - iter.Close() - require.Nil(iter.Next()) - require.ErrorIs(iter.Err(), datastore.ErrClosedIterator) - - require.True(iterMock.AssertExpectations(t)) - require.True(ds.AssertExpectations(t)) - }) - - t.Run("OnReaderAfterSuccess", func(t *testing.T) { - require := require.New(t) - - iterMock := &mockedIterator{} - iterMock.On("Next").Return(&core.RelationTuple{}).Once() - iterMock.On("Next").Return(nil).Once() - iterMock.On("Err").Return(nil).Once() - iterMock.On("Cursor").Return(options.Cursor(nil), nil).Once() - iterMock.On("Close") - - ds := &mockedReader{} - ds. - On("QueryRelationships", options.Cursor(nil), defaultSortOrder, uint64(1)). - Return(iterMock, nil).Once(). - On("QueryRelationships", options.Cursor(nil), defaultSortOrder, uint64(1)). - Return(nil, defaultError).Once() - - iter, err := NewPaginatedIterator(ctx, ds, datastore.RelationshipsFilter{}, 1, defaultSortOrder, nil) - require.NoError(err) - require.NotNil(iter) - - require.NotNil(iter.Next()) - require.NoError(iter.Err()) - - require.Nil(iter.Next()) - require.Error(iter.Err()) - iter.Close() - }) -} - func TestPaginatedIterator(t *testing.T) { testCases := []struct { order options.SortOrder - pageSize uint64 - totalRelationships uint64 + pageSize int + totalRelationships int }{ {options.ByResource, 1, 0}, {options.ByResource, 1, 1}, @@ -147,52 +38,38 @@ func TestPaginatedIterator(t *testing.T) { t.Parallel() require := require.New(t) - tpls := make([]*core.RelationTuple, 0, tc.totalRelationships) - for i := uint64(0); i < tc.totalRelationships; i++ { - tpls = append(tpls, &core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: "document", - ObjectId: strconv.FormatUint(i, 10), - Relation: "owner", - }, - Subject: &core.ObjectAndRelation{ - Namespace: "user", - ObjectId: strconv.FormatUint(i, 10), - Relation: datastore.Ellipsis, + rels := make([]tuple.Relationship, 0, tc.totalRelationships) + for i := 0; i < tc.totalRelationships; i++ { + rels = append(rels, tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.ObjectAndRelation{ + ObjectType: "document", + ObjectID: strconv.Itoa(i), + Relation: "owner", + }, + Subject: tuple.ObjectAndRelation{ + ObjectType: "user", + ObjectID: strconv.Itoa(i), + Relation: datastore.Ellipsis, + }, }, }) } - ds := generateMock(tpls, tc.pageSize, options.ByResource) + ds := generateMock(t, rels, tc.pageSize, options.ByResource) + + pageSize, err := safecast.ToUint64(tc.pageSize) + require.NoError(err) ctx := context.Background() iter, err := NewPaginatedIterator(ctx, ds, datastore.RelationshipsFilter{ OptionalResourceType: "unused", - }, tc.pageSize, options.ByResource, nil) + }, pageSize, options.ByResource, nil) require.NoError(err) - defer iter.Close() - - cursor, err := iter.Cursor() - require.ErrorIs(err, datastore.ErrCursorEmpty) - require.Nil(cursor) - - var count uint64 - for tpl := iter.Next(); tpl != nil; tpl = iter.Next() { - count++ - require.NoError(iter.Err()) - - cursor, err := iter.Cursor() - require.NoError(err) - require.NotNil(cursor) - } - - require.Equal(tc.totalRelationships, count) - require.NoError(iter.Err()) - - iter.Close() - require.Nil(iter.Next()) - require.Error(iter.Err(), datastore.ErrClosedIterator) + slice, err := datastore.IteratorToSlice(iter) + require.NoError(err) + require.Len(slice, tc.totalRelationships) // Make sure everything got called require.True(ds.AssertExpectations(t)) @@ -200,21 +77,25 @@ func TestPaginatedIterator(t *testing.T) { } } -func generateMock(tpls []*core.RelationTuple, pageSize uint64, order options.SortOrder) *mockedReader { +func generateMock(t *testing.T, rels []tuple.Relationship, pageSize int, order options.SortOrder) *mockedReader { mock := &mockedReader{} - tplsLen := uint64(len(tpls)) + relsLen := len(rels) var last options.Cursor - for i := uint64(0); i <= tplsLen; i += pageSize { + for i := 0; i <= relsLen; i += pageSize { pastLastIndex := i + pageSize - if pastLastIndex > tplsLen { - pastLastIndex = tplsLen + if pastLastIndex > relsLen { + pastLastIndex = relsLen } - iter := common.NewSliceRelationshipIterator(tpls[i:pastLastIndex], order) - mock.On("QueryRelationships", last, order, pageSize).Return(iter, nil) - if tplsLen > 0 { - last = tpls[pastLastIndex-1] + pageSize64, err := safecast.ToUint64(pageSize) + require.NoError(t, err) + + iter := common.NewSliceRelationshipIterator(rels[i:pastLastIndex]) + mock.On("QueryRelationships", last, order, pageSize64).Return(iter, nil) + if relsLen > 0 { + l := rels[pastLastIndex-1] + last = options.Cursor(&l) } } @@ -280,36 +161,3 @@ func (m *mockedReader) ListAllNamespaces(_ context.Context) ([]datastore.Revisio func (m *mockedReader) LookupNamespacesWithNames(_ context.Context, _ []string) ([]datastore.RevisionedNamespace, error) { panic("not implemented") } - -type mockedIterator struct { - mock.Mock -} - -var _ datastore.RelationshipIterator = &mockedIterator{} - -func (m *mockedIterator) Next() *core.RelationTuple { - args := m.Called() - potentialTuple := args.Get(0) - if potentialTuple == nil { - return nil - } - return potentialTuple.(*core.RelationTuple) -} - -func (m *mockedIterator) Cursor() (options.Cursor, error) { - args := m.Called() - potentialCursor := args.Get(0) - if potentialCursor == nil { - return nil, args.Error(1) - } - return potentialCursor.(options.Cursor), args.Error(1) -} - -func (m *mockedIterator) Err() error { - args := m.Called() - return args.Error(0) -} - -func (m *mockedIterator) Close() { - m.Called() -} diff --git a/pkg/datastore/test/basic.go b/pkg/datastore/test/basic.go index 4e2120c386..2dc5a50e04 100644 --- a/pkg/datastore/test/basic.go +++ b/pkg/datastore/test/basic.go @@ -43,13 +43,11 @@ func DeleteAllDataTest(t *testing.T, tester DatastoreTester) { for _, nsDef := range nsDefs { iter, err := reader.QueryRelationships(ctx, datastore.RelationshipsFilter{OptionalResourceType: nsDef.Definition.Name}) require.NoError(t, err) - t.Cleanup(iter.Close) - if iter.Next() != nil { + for range iter { foundRels = true + break } - - iter.Close() } require.True(t, foundRels, "no relationships provided") @@ -69,9 +67,9 @@ func DeleteAllDataTest(t *testing.T, tester DatastoreTester) { for _, nsDef := range nsDefs { iter, err := reader.QueryRelationships(ctx, datastore.RelationshipsFilter{OptionalResourceType: nsDef.Definition.Name}) require.NoError(t, err) - t.Cleanup(iter.Close) - require.Nil(t, iter.Next(), "relationships still exist") - iter.Close() + for range iter { + require.Fail(t, "relationships still exist") + } } } diff --git a/pkg/datastore/test/bulk.go b/pkg/datastore/test/bulk.go index 89da428ad6..151be63337 100644 --- a/pkg/datastore/test/bulk.go +++ b/pkg/datastore/test/bulk.go @@ -16,6 +16,7 @@ import ( "github.com/authzed/spicedb/pkg/datastore" "github.com/authzed/spicedb/pkg/datastore/options" core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/tuple" ) func BulkUploadTest(t *testing.T, tester DatastoreTester) { @@ -30,7 +31,7 @@ func BulkUploadTest(t *testing.T, tester DatastoreTester) { require.NoError(err) ds, _ := testfixtures.StandardDatastoreWithSchema(rawDS, require) - bulkSource := testfixtures.NewBulkTupleGenerator( + bulkSource := testfixtures.NewBulkRelationshipGenerator( testfixtures.DocumentNS.Name, "viewer", testfixtures.UserNS.Name, @@ -48,7 +49,7 @@ func BulkUploadTest(t *testing.T, tester DatastoreTester) { }) require.NoError(err) - tRequire := testfixtures.TupleChecker{Require: require, DS: ds} + tRequire := testfixtures.RelationshipChecker{Require: require, DS: ds} head, err := ds.HeadRevision(ctx) require.NoError(err) @@ -57,7 +58,6 @@ func BulkUploadTest(t *testing.T, tester DatastoreTester) { OptionalResourceType: testfixtures.DocumentNS.Name, }) require.NoError(err) - defer iter.Close() tRequire.VerifyIteratorCount(iter, tc) }) @@ -94,7 +94,7 @@ func BulkUploadAlreadyExistsSameCallErrorTest(t *testing.T, tester DatastoreTest ds, _ := testfixtures.StandardDatastoreWithSchema(rawDS, require) _, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { - inserted, err := rwt.BulkLoad(ctx, testfixtures.NewBulkTupleGenerator( + inserted, err := rwt.BulkLoad(ctx, testfixtures.NewBulkRelationshipGenerator( testfixtures.DocumentNS.Name, "viewer", testfixtures.UserNS.Name, @@ -104,7 +104,7 @@ func BulkUploadAlreadyExistsSameCallErrorTest(t *testing.T, tester DatastoreTest require.NoError(err) require.Equal(uint64(1), inserted) - _, serr := rwt.BulkLoad(ctx, testfixtures.NewBulkTupleGenerator( + _, serr := rwt.BulkLoad(ctx, testfixtures.NewBulkRelationshipGenerator( testfixtures.DocumentNS.Name, "viewer", testfixtures.UserNS.Name, @@ -131,7 +131,7 @@ func BulkUploadEditCaveat(t *testing.T, tester DatastoreTester) { require.NoError(err) ds, _ := testfixtures.StandardDatastoreWithSchema(rawDS, require) - bulkSource := testfixtures.NewBulkTupleGenerator( + bulkSource := testfixtures.NewBulkRelationshipGenerator( testfixtures.DocumentNS.Name, "caveated_viewer", testfixtures.UserNS.Name, @@ -153,22 +153,16 @@ func BulkUploadEditCaveat(t *testing.T, tester DatastoreTester) { OptionalResourceType: testfixtures.DocumentNS.Name, }) require.NoError(err) - defer iter.Close() - - updates := make([]*core.RelationTupleUpdate, 0, tc) - - for found := iter.Next(); found != nil; found = iter.Next() { - updates = append(updates, &core.RelationTupleUpdate{ - Operation: core.RelationTupleUpdate_TOUCH, - Tuple: &core.RelationTuple{ - ResourceAndRelation: found.ResourceAndRelation, - Subject: found.Subject, - Caveat: &core.ContextualizedCaveat{ - CaveatName: testfixtures.CaveatDef.Name, - Context: nil, - }, - }, - }) + + updates := make([]tuple.RelationshipUpdate, 0, tc) + + for found, err := range iter { + require.NoError(err) + + updates = append(updates, tuple.Touch(found.WithCaveat(&core.ContextualizedCaveat{ + CaveatName: testfixtures.CaveatDef.Name, + Context: nil, + }))) } require.Equal(tc, len(updates)) @@ -184,13 +178,12 @@ func BulkUploadEditCaveat(t *testing.T, tester DatastoreTester) { OptionalResourceType: testfixtures.DocumentNS.Name, }) require.NoError(err) - defer iter.Close() foundChanged := 0 - - for found := iter.Next(); found != nil; found = iter.Next() { - require.NotNil(found.Caveat) - require.NotEmpty(found.Caveat.CaveatName) + for found, err := range iter { + require.NoError(err) + require.NotNil(found.OptionalCaveat) + require.NotEmpty(found.OptionalCaveat.CaveatName) foundChanged++ } @@ -208,7 +201,7 @@ func BulkUploadAlreadyExistsErrorTest(t *testing.T, tester DatastoreTester) { // Bulk write a single relationship. _, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { - inserted, err := rwt.BulkLoad(ctx, testfixtures.NewBulkTupleGenerator( + inserted, err := rwt.BulkLoad(ctx, testfixtures.NewBulkRelationshipGenerator( testfixtures.DocumentNS.Name, "viewer", testfixtures.UserNS.Name, @@ -223,7 +216,7 @@ func BulkUploadAlreadyExistsErrorTest(t *testing.T, tester DatastoreTester) { // Bulk write it again and ensure we get the expected error. _, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { - _, serr := rwt.BulkLoad(ctx, testfixtures.NewBulkTupleGenerator( + _, serr := rwt.BulkLoad(ctx, testfixtures.NewBulkRelationshipGenerator( testfixtures.DocumentNS.Name, "viewer", testfixtures.UserNS.Name, @@ -245,7 +238,7 @@ type onlyErrorSource struct{} var errOnlyError = errors.New("source iterator error") -func (oes onlyErrorSource) Next(_ context.Context) (*core.RelationTuple, error) { +func (oes onlyErrorSource) Next(_ context.Context) (*tuple.Relationship, error) { return nil, errOnlyError } diff --git a/pkg/datastore/test/caveat.go b/pkg/datastore/test/caveat.go index efdb8ea90e..0e4db51c9b 100644 --- a/pkg/datastore/test/caveat.go +++ b/pkg/datastore/test/caveat.go @@ -143,59 +143,62 @@ func WriteCaveatedRelationshipTest(t *testing.T, tester DatastoreTester) { _, err = writeCaveats(ctx, ds, coreCaveat, anotherCoreCaveat) req.NoError(err) - tpl := createTestCaveatedTuple(t, "document:companyplan#somerelation@folder:company#...", coreCaveat.Name) - rev, err := common.WriteTuples(ctx, sds, core.RelationTupleUpdate_CREATE, tpl) + rel := createTestCaveatedRel(t, "document:companyplan#somerelation@folder:company#...", coreCaveat.Name) + rev, err := common.WriteRelationships(ctx, sds, tuple.UpdateOperationCreate, rel) req.NoError(err) - assertTupleCorrectlyStored(req, ds, rev, tpl) + assertRelCorrectlyStored(req, ds, rev, rel) // RelationTupleUpdate_CREATE of the same tuple and different caveat context will fail - _, err = common.WriteTuples(ctx, sds, core.RelationTupleUpdate_CREATE, tpl) + _, err = common.WriteRelationships(ctx, sds, tuple.UpdateOperationCreate, rel) req.ErrorAs(err, &common.CreateRelationshipExistsError{}) // RelationTupleUpdate_TOUCH does update the caveat context for a caveated relationship that already exists - currentMap := tpl.Caveat.Context.AsMap() + currentMap := rel.OptionalCaveat.Context.AsMap() delete(currentMap, "b") st, err := structpb.NewStruct(currentMap) require.NoError(t, err) - tpl.Caveat.Context = st - rev, err = common.WriteTuples(ctx, sds, core.RelationTupleUpdate_TOUCH, tpl) + rel.OptionalCaveat.Context = st + rev, err = common.WriteRelationships(ctx, sds, tuple.UpdateOperationTouch, rel) req.NoError(err) - assertTupleCorrectlyStored(req, ds, rev, tpl) + assertRelCorrectlyStored(req, ds, rev, rel) // RelationTupleUpdate_TOUCH does update the caveat name for a caveated relationship that already exists - tpl.Caveat.CaveatName = anotherCoreCaveat.Name - rev, err = common.WriteTuples(ctx, sds, core.RelationTupleUpdate_TOUCH, tpl) + rel.OptionalCaveat.CaveatName = anotherCoreCaveat.Name + rev, err = common.WriteRelationships(ctx, sds, tuple.UpdateOperationTouch, rel) req.NoError(err) - assertTupleCorrectlyStored(req, ds, rev, tpl) + assertRelCorrectlyStored(req, ds, rev, rel) // TOUCH can remove caveat from relationship - caveatContext := tpl.Caveat - tpl.Caveat = nil - rev, err = common.WriteTuples(ctx, sds, core.RelationTupleUpdate_TOUCH, tpl) + caveatContext := rel.OptionalCaveat + rel.OptionalCaveat = nil + rev, err = common.WriteRelationships(ctx, sds, tuple.UpdateOperationTouch, rel) req.NoError(err) - assertTupleCorrectlyStored(req, ds, rev, tpl) + assertRelCorrectlyStored(req, ds, rev, rel) // TOUCH can store caveat in relationship with no caveat - tpl.Caveat = caveatContext - rev, err = common.WriteTuples(ctx, sds, core.RelationTupleUpdate_TOUCH, tpl) + rel.OptionalCaveat = caveatContext + rev, err = common.WriteRelationships(ctx, sds, tuple.UpdateOperationTouch, rel) req.NoError(err) - assertTupleCorrectlyStored(req, ds, rev, tpl) + assertRelCorrectlyStored(req, ds, rev, rel) // RelationTupleUpdate_DELETE ignores caveat part of the request - tpl.Caveat.CaveatName = "rando" - rev, err = common.WriteTuples(ctx, sds, core.RelationTupleUpdate_DELETE, tpl) + rel.OptionalCaveat.CaveatName = "rando" + rev, err = common.WriteRelationships(ctx, sds, tuple.UpdateOperationDelete, rel) req.NoError(err) iter, err := ds.SnapshotReader(rev).QueryRelationships(context.Background(), datastore.RelationshipsFilter{ - OptionalResourceType: tpl.ResourceAndRelation.Namespace, + OptionalResourceType: rel.Resource.ObjectType, }) req.NoError(err) - defer iter.Close() - req.Nil(iter.Next()) + + for _, err := range iter { + req.NoError(err) + req.Fail("expected no relationships to be found") + } // Caveated tuple can reference non-existing caveat - controller layer is responsible for validation - tpl = createTestCaveatedTuple(t, "document:rando#somerelation@folder:company#...", "rando") - _, err = common.WriteTuples(ctx, sds, core.RelationTupleUpdate_CREATE, tpl) + rel = createTestCaveatedRel(t, "document:rando#somerelation@folder:company#...", "rando") + _, err = common.WriteRelationships(ctx, sds, tuple.UpdateOperationCreate, rel) req.NoError(err) } @@ -216,29 +219,29 @@ func CaveatedRelationshipFilterTest(t *testing.T, tester DatastoreTester) { _, err = writeCaveats(ctx, ds, coreCaveat, anotherCoreCaveat) req.NoError(err) - tpl := createTestCaveatedTuple(t, "document:companyplan#parent@folder:company#...", coreCaveat.Name) - anotherTpl := createTestCaveatedTuple(t, "document:anothercompanyplan#parent@folder:company#...", anotherCoreCaveat.Name) + rel := createTestCaveatedRel(t, "document:companyplan#parent@folder:company#...", coreCaveat.Name) + anotherTpl := createTestCaveatedRel(t, "document:anothercompanyplan#parent@folder:company#...", anotherCoreCaveat.Name) nonCaveatedTpl := tuple.MustParse("document:yetanothercompanyplan#parent@folder:company#...") - rev, err := common.WriteTuples(ctx, sds, core.RelationTupleUpdate_CREATE, tpl, anotherTpl, nonCaveatedTpl) + rev, err := common.WriteRelationships(ctx, sds, tuple.UpdateOperationCreate, rel, anotherTpl, nonCaveatedTpl) req.NoError(err) // filter by first caveat iter, err := ds.SnapshotReader(rev).QueryRelationships(ctx, datastore.RelationshipsFilter{ - OptionalResourceType: tpl.ResourceAndRelation.Namespace, + OptionalResourceType: rel.Resource.ObjectType, OptionalCaveatName: coreCaveat.Name, }) req.NoError(err) - expectTuple(req, iter, tpl) + expectRel(req, iter, rel) // filter by second caveat iter, err = ds.SnapshotReader(rev).QueryRelationships(ctx, datastore.RelationshipsFilter{ - OptionalResourceType: anotherTpl.ResourceAndRelation.Namespace, + OptionalResourceType: anotherTpl.Resource.ObjectType, OptionalCaveatName: anotherCoreCaveat.Name, }) req.NoError(err) - expectTuple(req, iter, anotherTpl) + expectRel(req, iter, anotherTpl) } func CaveatSnapshotReadsTest(t *testing.T, tester DatastoreTester) { @@ -290,49 +293,49 @@ func CaveatedRelationshipWatchTest(t *testing.T, tester DatastoreTester) { req.NoError(err) // test relationship with caveat and context - tupleWithContext := createTestCaveatedTuple(t, "document:a#parent@folder:company#...", coreCaveat.Name) + relWithContext := createTestCaveatedRel(t, "document:a#parent@folder:company#...", coreCaveat.Name) revBeforeWrite, err := ds.HeadRevision(ctx) require.NoError(t, err) - writeRev, err := common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, tupleWithContext) + writeRev, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, relWithContext) require.NoError(t, err) require.NotEqual(t, revBeforeWrite, writeRev, "found same transaction IDs: %v and %v", revBeforeWrite, writeRev) - expectTupleChange(t, ds, revBeforeWrite, tupleWithContext) + expectRelChange(t, ds, revBeforeWrite, relWithContext) // test relationship with caveat and empty context - tupleWithEmptyContext := createTestCaveatedTuple(t, "document:b#parent@folder:company#...", coreCaveat.Name) + tupleWithEmptyContext := createTestCaveatedRel(t, "document:b#parent@folder:company#...", coreCaveat.Name) strct, err := structpb.NewStruct(nil) req.NoError(err) - tupleWithEmptyContext.Caveat.Context = strct + tupleWithEmptyContext.OptionalCaveat.Context = strct secondRevBeforeWrite, err := ds.HeadRevision(ctx) require.NoError(t, err) - secondWriteRev, err := common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, tupleWithEmptyContext) + secondWriteRev, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, tupleWithEmptyContext) require.NoError(t, err) require.NotEqual(t, secondRevBeforeWrite, secondWriteRev) - expectTupleChange(t, ds, secondRevBeforeWrite, tupleWithEmptyContext) + expectRelChange(t, ds, secondRevBeforeWrite, tupleWithEmptyContext) // test relationship with caveat and empty context - tupleWithNilContext := createTestCaveatedTuple(t, "document:c#parent@folder:company#...", coreCaveat.Name) - tupleWithNilContext.Caveat.Context = nil + tupleWithNilContext := createTestCaveatedRel(t, "document:c#parent@folder:company#...", coreCaveat.Name) + tupleWithNilContext.OptionalCaveat.Context = nil thirdRevBeforeWrite, err := ds.HeadRevision(ctx) require.NoError(t, err) - thirdWriteRev, err := common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, tupleWithNilContext) + thirdWriteRev, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, tupleWithNilContext) req.NoError(err) require.NotEqual(t, thirdRevBeforeWrite, thirdWriteRev) - tupleWithNilContext.Caveat.Context = &structpb.Struct{} // nil struct comes back as zero-value struct - expectTupleChange(t, ds, thirdRevBeforeWrite, tupleWithNilContext) + tupleWithNilContext.OptionalCaveat.Context = &structpb.Struct{} // nil struct comes back as zero-value struct + expectRelChange(t, ds, thirdRevBeforeWrite, tupleWithNilContext) } -func expectTupleChange(t *testing.T, ds datastore.Datastore, revBeforeWrite datastore.Revision, expectedTuple *core.RelationTuple) { +func expectRelChange(t *testing.T, ds datastore.Datastore, revBeforeWrite datastore.Revision, expectedRel tuple.Relationship) { t.Helper() ctx, cancel := context.WithCancel(context.Background()) @@ -345,33 +348,29 @@ func expectTupleChange(t *testing.T, ds datastore.Datastore, revBeforeWrite data select { case change, ok := <-chanRevisionChanges: require.True(t, ok) - - // do not check length of change, may contain duplicates - foundDiff := cmp.Diff(expectedTuple, change.RelationshipChanges[0].Tuple, protocmp.Transform()) - require.Empty(t, foundDiff) + require.True(t, tuple.Equal(change.RelationshipChanges[0].Relationship, expectedRel)) case <-changeWait.C: require.Fail(t, "timed out waiting for relationship update via Watch API") } } -func expectTuple(req *require.Assertions, iter datastore.RelationshipIterator, tpl *core.RelationTuple) { - defer iter.Close() - readTpl := iter.Next() - foundDiff := cmp.Diff(tpl, readTpl, protocmp.Transform()) - req.Empty(foundDiff) - req.Nil(iter.Next()) +func expectRel(req *require.Assertions, iter datastore.RelationshipIterator, rel tuple.Relationship) { + for found, err := range iter { + req.NoError(err) + req.True(tuple.Equal(found, rel)) + } } -func assertTupleCorrectlyStored(req *require.Assertions, ds datastore.Datastore, rev datastore.Revision, expected *core.RelationTuple) { +func assertRelCorrectlyStored(req *require.Assertions, ds datastore.Datastore, rev datastore.Revision, expected tuple.Relationship) { iter, err := ds.SnapshotReader(rev).QueryRelationships(context.Background(), datastore.RelationshipsFilter{ - OptionalResourceType: expected.ResourceAndRelation.Namespace, + OptionalResourceType: expected.Resource.ObjectType, }) req.NoError(err) - defer iter.Close() - readTpl := iter.Next() - foundDiff := cmp.Diff(expected, readTpl, protocmp.Transform()) - req.Empty(foundDiff) + for found, err := range iter { + req.NoError(err) + req.True(tuple.Equal(found, expected)) + } } func skipIfNotCaveatStorer(t *testing.T, ds datastore.Datastore) { @@ -385,16 +384,14 @@ func skipIfNotCaveatStorer(t *testing.T, ds datastore.Datastore) { }) } -func createTestCaveatedTuple(t *testing.T, tplString string, caveatName string) *core.RelationTuple { - tpl := tuple.MustParse(tplString) +func createTestCaveatedRel(t *testing.T, relString string, caveatName string) tuple.Relationship { + rel := tuple.MustParse(relString) st, err := structpb.NewStruct(map[string]interface{}{"a": 1, "b": "test"}) require.NoError(t, err) - - tpl.Caveat = &core.ContextualizedCaveat{ + return rel.WithCaveat(&core.ContextualizedCaveat{ CaveatName: caveatName, Context: st, - } - return tpl + }) } func writeCaveats(ctx context.Context, ds datastore.Datastore, coreCaveat ...*core.CaveatDefinition) (datastore.Revision, error) { diff --git a/pkg/datastore/test/counters.go b/pkg/datastore/test/counters.go index 3b91eff24a..0f82379900 100644 --- a/pkg/datastore/test/counters.go +++ b/pkg/datastore/test/counters.go @@ -62,11 +62,10 @@ func RelationshipCountersTest(t *testing.T, tester DatastoreTester) { }) require.NoError(t, err) - for iter.Next() != nil { + for _, err := range iter { expectedCount++ - require.NoError(t, iter.Err()) + require.NoError(t, err) } - iter.Close() count, err := reader.CountRelationships(context.Background(), "document") require.NoError(t, err) @@ -79,11 +78,10 @@ func RelationshipCountersTest(t *testing.T, tester DatastoreTester) { }) require.NoError(t, err) - for iter.Next() != nil { + for _, err := range iter { expectedCount++ - require.NoError(t, iter.Err()) + require.NoError(t, err) } - iter.Close() count, err = reader.CountRelationships(context.Background(), "another") require.NoError(t, err) diff --git a/pkg/datastore/test/datastore.go b/pkg/datastore/test/datastore.go index 605e334ded..6c3f0c2ea0 100644 --- a/pkg/datastore/test/datastore.go +++ b/pkg/datastore/test/datastore.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/require" - core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/tuple" "github.com/authzed/spicedb/pkg/datastore" "github.com/authzed/spicedb/pkg/namespace" @@ -142,7 +142,6 @@ func AllWithExceptions(t *testing.T, tester DatastoreTester, except Categories, t.Run("TestLimit", runner(tester, LimitTest)) t.Run("TestOrderedLimit", runner(tester, OrderedLimitTest)) t.Run("TestResume", runner(tester, ResumeTest)) - t.Run("TestCursorErrors", runner(tester, CursorErrorsTest)) t.Run("TestReverseQueryCursor", runner(tester, ReverseQueryCursorTest)) t.Run("TestRevisionQuantization", runner(tester, RevisionQuantizationTest)) @@ -214,17 +213,19 @@ var testGroupNS = namespace.Namespace( var testUserNS = namespace.Namespace(testUserNamespace) -func makeTestTuple(resourceID, userID string) *core.RelationTuple { - return &core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: testResourceNamespace, - ObjectId: resourceID, - Relation: testReaderRelation, - }, - Subject: &core.ObjectAndRelation{ - Namespace: testUserNamespace, - ObjectId: userID, - Relation: ellipsis, +func makeTestRel(resourceID, userID string) tuple.Relationship { + return tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.ObjectAndRelation{ + ObjectType: testResourceNamespace, + ObjectID: resourceID, + Relation: testReaderRelation, + }, + Subject: tuple.ObjectAndRelation{ + ObjectType: testUserNamespace, + ObjectID: userID, + Relation: ellipsis, + }, }, } } diff --git a/pkg/datastore/test/namespace.go b/pkg/datastore/test/namespace.go index 0343c48a7f..7207afe866 100644 --- a/pkg/datastore/test/namespace.go +++ b/pkg/datastore/test/namespace.go @@ -149,14 +149,16 @@ func NamespaceDeleteTest(t *testing.T, tester DatastoreTester) { ds, revision := testfixtures.StandardDatastoreWithData(rawDS, require) ctx := context.Background() - tRequire := testfixtures.TupleChecker{Require: require, DS: ds} - docTpl := tuple.Parse(testfixtures.StandardTuples[0]) + tRequire := testfixtures.RelationshipChecker{Require: require, DS: ds} + docTpl, err := tuple.Parse(testfixtures.StandardRelationships[0]) + require.NoError(err) require.NotNil(docTpl) - tRequire.TupleExists(ctx, docTpl, revision) + tRequire.RelationshipExists(ctx, docTpl, revision) - folderTpl := tuple.Parse(testfixtures.StandardTuples[2]) + folderTpl, err := tuple.Parse(testfixtures.StandardRelationships[2]) + require.NoError(err) require.NotNil(folderTpl) - tRequire.TupleExists(ctx, folderTpl, revision) + tRequire.RelationshipExists(ctx, folderTpl, revision) deletedRev, err := ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { return rwt.DeleteNamespaces(ctx, testfixtures.DocumentNS.Name) @@ -187,7 +189,7 @@ func NamespaceDeleteTest(t *testing.T, tester DatastoreTester) { require.NoError(err) tRequire.VerifyIteratorResults(iter) - tRequire.TupleExists(ctx, folderTpl, deletedRevision) + tRequire.RelationshipExists(ctx, folderTpl, deletedRevision) } func NamespaceMultiDeleteTest(t *testing.T, tester DatastoreTester) { diff --git a/pkg/datastore/test/pagination.go b/pkg/datastore/test/pagination.go index fe25022c50..fd7b09edbd 100644 --- a/pkg/datastore/test/pagination.go +++ b/pkg/datastore/test/pagination.go @@ -14,7 +14,6 @@ import ( "github.com/authzed/spicedb/pkg/datastore" "github.com/authzed/spicedb/pkg/datastore/options" "github.com/authzed/spicedb/pkg/genutil/mapz" - core "github.com/authzed/spicedb/pkg/proto/core/v1" "github.com/authzed/spicedb/pkg/tuple" ) @@ -36,7 +35,7 @@ func OrderingTest(t *testing.T, tester DatastoreTester) { require.NoError(t, err) ds, rev := testfixtures.StandardDatastoreWithData(rawDS, require.New(t)) - tRequire := testfixtures.TupleChecker{Require: require.New(t), DS: ds} + tRequire := testfixtures.RelationshipChecker{Require: require.New(t), DS: ds} for _, tc := range testCases { tc := tc @@ -53,46 +52,15 @@ func OrderingTest(t *testing.T, tester DatastoreTester) { }, options.WithSort(tc.ordering)) require.NoError(err) - defer iter.Close() - - cursor, err := iter.Cursor() - require.Empty(cursor) - require.ErrorIs(err, datastore.ErrCursorEmpty) - tRequire.VerifyOrderedIteratorResults(iter, expected...) - cursor, err = iter.Cursor() - if len(expected) > 0 { - require.NotEmpty(cursor) - require.NoError(err) - } else { - require.Empty(cursor) - require.ErrorIs(err, datastore.ErrCursorEmpty) - } - // Check a reader from with a transaction _, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { iter, err := rwt.QueryRelationships(ctx, datastore.RelationshipsFilter{ OptionalResourceType: tc.resourceType, }, options.WithSort(tc.ordering)) require.NoError(err) - defer iter.Close() - - cursor, err := iter.Cursor() - require.Empty(cursor) - require.ErrorIs(err, datastore.ErrCursorEmpty) - tRequire.VerifyOrderedIteratorResults(iter, expected...) - - cursor, err = iter.Cursor() - if len(expected) > 0 { - require.NotEmpty(cursor) - require.NoError(err) - } else { - require.Empty(cursor) - require.ErrorIs(err, datastore.ErrCursorEmpty) - } - return nil }) require.NoError(err) @@ -111,7 +79,7 @@ func LimitTest(t *testing.T, tester DatastoreTester) { require.NoError(t, err) ds, rev := testfixtures.StandardDatastoreWithData(rawDS, require.New(t)) - tRequire := testfixtures.TupleChecker{Require: require.New(t), DS: ds} + tRequire := testfixtures.RelationshipChecker{Require: require.New(t), DS: ds} for _, objectType := range testCases { expected := sortedStandardData(objectType, options.ByResource) @@ -127,22 +95,13 @@ func LimitTest(t *testing.T, tester DatastoreTester) { }, options.WithLimit(&testLimit)) require.NoError(err) - defer iter.Close() expectedCount := limit if expectedCount > len(expected) { expectedCount = len(expected) } - cursor, err := iter.Cursor() - require.Empty(cursor) - require.ErrorIs(err, datastore.ErrCursorsWithoutSorting) - tRequire.VerifyIteratorCount(iter, expectedCount) - - cursor, err = iter.Cursor() - require.Empty(cursor) - require.ErrorIs(err, datastore.ErrCursorsWithoutSorting) }) }) } @@ -206,7 +165,7 @@ func OrderedLimitTest(t *testing.T, tester DatastoreTester) { require.NoError(t, err) ds, rev := testfixtures.StandardDatastoreWithData(rawDS, require.New(t)) - tRequire := testfixtures.TupleChecker{Require: require.New(t), DS: ds} + tRequire := testfixtures.RelationshipChecker{Require: require.New(t), DS: ds} for _, tc := range orderedTestCases { expected := sortedStandardData(tc.objectType, tc.sortOrder) @@ -232,22 +191,7 @@ func OrderedLimitTest(t *testing.T, tester DatastoreTester) { } require.NoError(err) - defer iter.Close() - - cursor, err := iter.Cursor() - require.Empty(cursor) - require.ErrorIs(err, datastore.ErrCursorEmpty) - tRequire.VerifyOrderedIteratorResults(iter, expected[0:limit]...) - - cursor, err = iter.Cursor() - if len(expected) > 0 { - require.NotEmpty(cursor) - require.NoError(err) - } else { - require.Empty(cursor) - require.ErrorIs(err, datastore.ErrCursorEmpty) - } }) }) } @@ -259,7 +203,7 @@ func ResumeTest(t *testing.T, tester DatastoreTester) { require.NoError(t, err) ds, rev := testfixtures.StandardDatastoreWithData(rawDS, require.New(t)) - tRequire := testfixtures.TupleChecker{Require: require.New(t), DS: ds} + tRequire := testfixtures.RelationshipChecker{Require: require.New(t), DS: ds} for _, tc := range orderedTestCases { expected := sortedStandardData(tc.objectType, tc.sortOrder) @@ -288,27 +232,13 @@ func ResumeTest(t *testing.T, tester DatastoreTester) { } require.NoError(err) - defer iter.Close() - - emptyCursor, err := iter.Cursor() - require.Empty(emptyCursor) - require.ErrorIs(err, datastore.ErrCursorEmpty) upperBound := offset + batchSize if upperBound > len(expected) { upperBound = len(expected) } - tRequire.VerifyOrderedIteratorResults(iter, expected[offset:upperBound]...) - - cursor, err = iter.Cursor() - if upperBound-offset > 0 { - require.NotEmpty(cursor) - require.NoError(err) - } else { - require.Empty(cursor) - require.ErrorIs(err, datastore.ErrCursorEmpty) - } + cursor = tRequire.VerifyOrderedIteratorResults(iter, expected[offset:upperBound]...) } }) }) @@ -316,67 +246,6 @@ func ResumeTest(t *testing.T, tester DatastoreTester) { } } -func CursorErrorsTest(t *testing.T, tester DatastoreTester) { - testCases := []struct { - order options.SortOrder - defaultCursorError error - }{ - {options.Unsorted, datastore.ErrCursorsWithoutSorting}, - {options.ByResource, nil}, - } - - rawDS, err := tester.New(0, veryLargeGCInterval, veryLargeGCWindow, 1) - require.NoError(t, err) - - ds, rev := testfixtures.StandardDatastoreWithData(rawDS, require.New(t)) - ctx := context.Background() - - for _, tc := range testCases { - t.Run(fmt.Sprintf("Order-%d", tc.order), func(t *testing.T) { - require := require.New(t) - - foreachTxType(ctx, ds, rev, func(reader datastore.Reader) { - iter, err := reader.QueryRelationships(ctx, datastore.RelationshipsFilter{ - OptionalResourceType: testfixtures.DocumentNS.Name, - }, options.WithSort(tc.order)) - require.NoError(err) - require.NotNil(iter) - defer iter.Close() - - cursor, err := iter.Cursor() - require.Nil(cursor) - if tc.defaultCursorError != nil { - require.ErrorIs(err, tc.defaultCursorError) - } else { - require.ErrorIs(err, datastore.ErrCursorEmpty) - } - - val := iter.Next() - require.NotNil(val) - - cursor, err = iter.Cursor() - if tc.defaultCursorError != nil { - require.Nil(cursor) - require.ErrorIs(err, tc.defaultCursorError) - } else { - require.NotNil(cursor) - require.Nil(err) - } - - iter.Close() - valAfterClose := iter.Next() - require.Nil(valAfterClose) - - err = iter.Err() - require.ErrorIs(err, datastore.ErrClosedIterator) - cursorAfterClose, err := iter.Cursor() - require.Nil(cursorAfterClose) - require.ErrorIs(err, datastore.ErrClosedIterator) - }) - }) - } -} - func ReverseQueryCursorTest(t *testing.T, tester DatastoreTester) { rawDS, err := tester.New(0, veryLargeGCInterval, veryLargeGCWindow, 1) require.NoError(t, err) @@ -386,7 +255,7 @@ func ReverseQueryCursorTest(t *testing.T, tester DatastoreTester) { // Add test relationships. rev, err := ds.ReadWriteTx(context.Background(), func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { - return rwt.WriteRelationships(ctx, []*core.RelationTupleUpdate{ + return rwt.WriteRelationships(ctx, []tuple.RelationshipUpdate{ tuple.Create(tuple.MustParse("document:firstdoc#viewer@user:alice")), tuple.Create(tuple.MustParse("document:firstdoc#viewer@user:tom")), tuple.Create(tuple.MustParse("document:firstdoc#viewer@user:fred")), @@ -412,17 +281,12 @@ func ReverseQueryCursorTest(t *testing.T, tester DatastoreTester) { SubjectType: testfixtures.UserNS.Name, }, options.WithSortForReverse(sortBy), options.WithLimitForReverse(&limit), options.WithAfterForReverse(cursor)) require.NoError(t, err) - defer iter.Close() encounteredTuples := mapz.NewSet[string]() - for { - rel := iter.Next() - if rel == nil { - break - } - + for rel, err := range iter { + require.NoError(t, err) require.True(t, encounteredTuples.Add(tuple.MustString(rel))) - cursor = rel + cursor = options.ToCursor(rel) } require.LessOrEqual(t, encounteredTuples.Len(), 2) @@ -452,19 +316,19 @@ func foreachTxType( }) } -func sortedStandardData(resourceType string, order options.SortOrder) []*core.RelationTuple { - asTuples := lo.Map(testfixtures.StandardTuples, func(item string, _ int) *core.RelationTuple { - return tuple.Parse(item) +func sortedStandardData(resourceType string, order options.SortOrder) []tuple.Relationship { + asTuples := lo.Map(testfixtures.StandardRelationships, func(item string, _ int) tuple.Relationship { + return tuple.MustParse(item) }) - filteredToType := lo.Filter(asTuples, func(item *core.RelationTuple, _ int) bool { - return item.ResourceAndRelation.Namespace == resourceType + filteredToType := lo.Filter(asTuples, func(item tuple.Relationship, _ int) bool { + return item.Resource.ObjectType == resourceType }) sort.Slice(filteredToType, func(i, j int) bool { - lhsResource := tuple.StringONR(filteredToType[i].ResourceAndRelation) + lhsResource := tuple.StringONR(filteredToType[i].Resource) lhsSubject := tuple.StringONR(filteredToType[i].Subject) - rhsResource := tuple.StringONR(filteredToType[j].ResourceAndRelation) + rhsResource := tuple.StringONR(filteredToType[j].Resource) rhsSubject := tuple.StringONR(filteredToType[j].Subject) switch order { case options.ByResource: @@ -479,22 +343,22 @@ func sortedStandardData(resourceType string, order options.SortOrder) []*core.Re return filteredToType } -func sortedStandardDataBySubject(subjectType string, order options.SortOrder) []*core.RelationTuple { - asTuples := lo.Map(testfixtures.StandardTuples, func(item string, _ int) *core.RelationTuple { - return tuple.Parse(item) +func sortedStandardDataBySubject(subjectType string, order options.SortOrder) []tuple.Relationship { + asTuples := lo.Map(testfixtures.StandardRelationships, func(item string, _ int) tuple.Relationship { + return tuple.MustParse(item) }) - filteredToType := lo.Filter(asTuples, func(item *core.RelationTuple, _ int) bool { + filteredToType := lo.Filter(asTuples, func(item tuple.Relationship, _ int) bool { if subjectType == "" { return true } - return item.Subject.Namespace == subjectType + return item.Subject.ObjectType == subjectType }) sort.Slice(filteredToType, func(i, j int) bool { - lhsResource := tuple.StringONR(filteredToType[i].ResourceAndRelation) + lhsResource := tuple.StringONR(filteredToType[i].Resource) lhsSubject := tuple.StringONR(filteredToType[i].Subject) - rhsResource := tuple.StringONR(filteredToType[j].ResourceAndRelation) + rhsResource := tuple.StringONR(filteredToType[j].Resource) rhsSubject := tuple.StringONR(filteredToType[j].Subject) switch order { case options.ByResource: diff --git a/pkg/datastore/test/tuples.go b/pkg/datastore/test/relationships.go similarity index 73% rename from pkg/datastore/test/tuples.go rename to pkg/datastore/test/relationships.go index a022a626e5..0c64f12dd8 100644 --- a/pkg/datastore/test/tuples.go +++ b/pkg/datastore/test/relationships.go @@ -22,7 +22,6 @@ import ( "github.com/authzed/spicedb/pkg/datastore" "github.com/authzed/spicedb/pkg/datastore/options" "github.com/authzed/spicedb/pkg/genutil/mapz" - core "github.com/authzed/spicedb/pkg/proto/core/v1" "github.com/authzed/spicedb/pkg/tuple" ) @@ -41,9 +40,9 @@ const ( func SimpleTest(t *testing.T, tester DatastoreTester) { testCases := []int{1, 2, 4, 32, 256} - for _, numTuples := range testCases { - numTuples := numTuples - t.Run(strconv.Itoa(numTuples), func(t *testing.T) { + for _, numRels := range testCases { + numRels := numRels + t.Run(strconv.Itoa(numRels), func(t *testing.T) { require := require.New(t) ds, err := tester.New(0, veryLargeGCInterval, veryLargeGCWindow, 1) @@ -58,113 +57,109 @@ func SimpleTest(t *testing.T, tester DatastoreTester) { setupDatastore(ds, require) - tRequire := testfixtures.TupleChecker{Require: require, DS: ds} + tRequire := testfixtures.RelationshipChecker{Require: require, DS: ds} - var testTuples []*core.RelationTuple - for i := 0; i < numTuples; i++ { + var testRels []tuple.Relationship + for i := 0; i < numRels; i++ { resourceName := fmt.Sprintf("resource%d", i) userName := fmt.Sprintf("user%d", i) - newTuple := makeTestTuple(resourceName, userName) - testTuples = append(testTuples, newTuple) + newRel := makeTestRel(resourceName, userName) + testRels = append(testRels, newRel) } - lastRevision, err := common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, testTuples...) + lastRevision, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, testRels...) require.NoError(err) - for _, toCheck := range testTuples { - tRequire.TupleExists(ctx, toCheck, lastRevision) + for _, toCheck := range testRels { + tRequire.RelationshipExists(ctx, toCheck, lastRevision) } - // Write a duplicate tuple to make sure the datastore rejects it - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, testTuples...) + // Write a duplicate relationship to make sure the datastore rejects it + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, testRels...) require.Error(err) dsReader := ds.SnapshotReader(lastRevision) - for _, tupleToFind := range testTuples { - tupleSubject := tupleToFind.Subject + for _, relToFind := range testRels { + relSubject := relToFind.Subject - // Check that we can find the tuple a number of ways + // Check that we can find the relationship a number of ways iter, err := dsReader.QueryRelationships(ctx, datastore.RelationshipsFilter{ - OptionalResourceType: tupleToFind.ResourceAndRelation.Namespace, - OptionalResourceIds: []string{tupleToFind.ResourceAndRelation.ObjectId}, + OptionalResourceType: relToFind.Resource.ObjectType, + OptionalResourceIds: []string{relToFind.Resource.ObjectID}, }) require.NoError(err) - tRequire.VerifyIteratorResults(iter, tupleToFind) + tRequire.VerifyIteratorResults(iter, relToFind) // Check without a resource type. iter, err = dsReader.QueryRelationships(ctx, datastore.RelationshipsFilter{ - OptionalResourceIds: []string{tupleToFind.ResourceAndRelation.ObjectId}, + OptionalResourceIds: []string{relToFind.Resource.ObjectID}, }) require.NoError(err) - tRequire.VerifyIteratorResults(iter, tupleToFind) + tRequire.VerifyIteratorResults(iter, relToFind) iter, err = dsReader.QueryRelationships(ctx, datastore.RelationshipsFilter{ - OptionalResourceType: tupleToFind.ResourceAndRelation.Namespace, - OptionalResourceIds: []string{tupleToFind.ResourceAndRelation.ObjectId}, - OptionalResourceRelation: tupleToFind.ResourceAndRelation.Relation, + OptionalResourceType: relToFind.Resource.ObjectType, + OptionalResourceIds: []string{relToFind.Resource.ObjectID}, + OptionalResourceRelation: relToFind.Resource.Relation, }) require.NoError(err) - tRequire.VerifyIteratorResults(iter, tupleToFind) + tRequire.VerifyIteratorResults(iter, relToFind) iter, err = dsReader.ReverseQueryRelationships( ctx, - onrToSubjectsFilter(tupleSubject), + onrToSubjectsFilter(relSubject), options.WithResRelation(&options.ResourceRelation{ - Namespace: tupleToFind.ResourceAndRelation.Namespace, - Relation: tupleToFind.ResourceAndRelation.Relation, + Namespace: relToFind.Resource.ObjectType, + Relation: relToFind.Resource.Relation, }), ) require.NoError(err) - tRequire.VerifyIteratorResults(iter, tupleToFind) + tRequire.VerifyIteratorResults(iter, relToFind) iter, err = dsReader.ReverseQueryRelationships( ctx, - onrToSubjectsFilter(tupleSubject), + onrToSubjectsFilter(relSubject), options.WithResRelation(&options.ResourceRelation{ - Namespace: tupleToFind.ResourceAndRelation.Namespace, - Relation: tupleToFind.ResourceAndRelation.Relation, + Namespace: relToFind.Resource.ObjectType, + Relation: relToFind.Resource.Relation, }), options.WithLimitForReverse(options.LimitOne), ) require.NoError(err) - tRequire.VerifyIteratorResults(iter, tupleToFind) + tRequire.VerifyIteratorResults(iter, relToFind) - // Check that we fail to find the tuple with the wrong filters + // Check that we fail to find the relationship with the wrong filters iter, err = dsReader.QueryRelationships(ctx, datastore.RelationshipsFilter{ - OptionalResourceType: tupleToFind.ResourceAndRelation.Namespace, - OptionalResourceIds: []string{tupleToFind.ResourceAndRelation.ObjectId}, + OptionalResourceType: relToFind.Resource.ObjectType, + OptionalResourceIds: []string{relToFind.Resource.ObjectID}, OptionalResourceRelation: "fake", }) require.NoError(err) tRequire.VerifyIteratorResults(iter) - incorrectUserset := &core.ObjectAndRelation{ - Namespace: tupleSubject.Namespace, - ObjectId: tupleSubject.ObjectId, - Relation: "fake", - } + incorrectUserset := relSubject.WithRelation("fake") iter, err = dsReader.ReverseQueryRelationships( ctx, onrToSubjectsFilter(incorrectUserset), options.WithResRelation(&options.ResourceRelation{ - Namespace: tupleToFind.ResourceAndRelation.Namespace, - Relation: tupleToFind.ResourceAndRelation.Relation, + Namespace: relToFind.Resource.ObjectType, + Relation: relToFind.Resource.Relation, }), ) require.NoError(err) tRequire.VerifyIteratorResults(iter) } - // Check a query that returns a number of tuples + // Check a query that returns a number of relationships iter, err := dsReader.QueryRelationships(ctx, datastore.RelationshipsFilter{ OptionalResourceType: testResourceNamespace, }) require.NoError(err) - tRequire.VerifyIteratorResults(iter, testTuples...) + tRequire.VerifyIteratorResults(iter, testRels...) - // Filter it down to a single tuple with a userset + // Filter it down to a single relationship with a userset iter, err = dsReader.QueryRelationships(ctx, datastore.RelationshipsFilter{ OptionalResourceType: testResourceNamespace, OptionalSubjectsSelectors: []datastore.SubjectsSelector{ @@ -175,75 +170,74 @@ func SimpleTest(t *testing.T, tester DatastoreTester) { }, }) require.NoError(err) - tRequire.VerifyIteratorResults(iter, testTuples[0]) + tRequire.VerifyIteratorResults(iter, testRels[0]) // Check for larger reverse queries. iter, err = dsReader.ReverseQueryRelationships(ctx, datastore.SubjectsFilter{ SubjectType: testUserNamespace, }) require.NoError(err) - tRequire.VerifyIteratorResults(iter, testTuples...) + tRequire.VerifyIteratorResults(iter, testRels...) // Check limit. - if len(testTuples) > 1 { - // This should be non-negative. - limit, _ := safecast.ToUint64(len(testTuples) - 1) + if len(testRels) > 1 { + limit, _ := safecast.ToUint64(len(testRels) - 1) iter, err := dsReader.ReverseQueryRelationships(ctx, datastore.SubjectsFilter{ SubjectType: testUserNamespace, }, options.WithLimitForReverse(&limit)) require.NoError(err) - defer iter.Close() - tRequire.VerifyIteratorCount(iter, len(testTuples)-1) + + tRequire.VerifyIteratorCount(iter, len(testRels)-1) } - // Check that we can find the group of tuples too + // Check that we can find the group of relationships too iter, err = dsReader.QueryRelationships(ctx, datastore.RelationshipsFilter{ - OptionalResourceType: testTuples[0].ResourceAndRelation.Namespace, + OptionalResourceType: testRels[0].Resource.ObjectType, }) require.NoError(err) - tRequire.VerifyIteratorResults(iter, testTuples...) + tRequire.VerifyIteratorResults(iter, testRels...) iter, err = dsReader.QueryRelationships(ctx, datastore.RelationshipsFilter{ - OptionalResourceType: testTuples[0].ResourceAndRelation.Namespace, - OptionalResourceRelation: testTuples[0].ResourceAndRelation.Relation, + OptionalResourceType: testRels[0].Resource.ObjectType, + OptionalResourceRelation: testRels[0].Resource.Relation, }) require.NoError(err) - tRequire.VerifyIteratorResults(iter, testTuples...) + tRequire.VerifyIteratorResults(iter, testRels...) // Try some bad queries iter, err = dsReader.QueryRelationships(ctx, datastore.RelationshipsFilter{ - OptionalResourceType: testTuples[0].ResourceAndRelation.Namespace, + OptionalResourceType: testRels[0].Resource.ObjectType, OptionalResourceIds: []string{"fakeobectid"}, }) require.NoError(err) tRequire.VerifyIteratorResults(iter) - // Delete the first tuple - deletedAt, err := common.WriteTuples(ctx, ds, core.RelationTupleUpdate_DELETE, testTuples[0]) + // Delete the first relationship. + deletedAt, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationDelete, testRels[0]) require.NoError(err) // Delete it AGAIN (idempotent delete) and make sure there's no error - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_DELETE, testTuples[0]) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationDelete, testRels[0]) require.NoError(err) // Verify it can still be read at the old revision - tRequire.TupleExists(ctx, testTuples[0], lastRevision) + tRequire.RelationshipExists(ctx, testRels[0], lastRevision) // Verify that it does not show up at the new revision - tRequire.NoTupleExists(ctx, testTuples[0], deletedAt) + tRequire.NoRelationshipExists(ctx, testRels[0], deletedAt) alreadyDeletedIter, err := ds.SnapshotReader(deletedAt).QueryRelationships( ctx, datastore.RelationshipsFilter{ - OptionalResourceType: testTuples[0].ResourceAndRelation.Namespace, + OptionalResourceType: testRels[0].Resource.ObjectType, }, ) require.NoError(err) - tRequire.VerifyIteratorResults(alreadyDeletedIter, testTuples[1:]...) + tRequire.VerifyIteratorResults(alreadyDeletedIter, testRels[1:]...) // Write it back - returnedAt, err := common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, testTuples[0]) + returnedAt, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, testRels[0]) require.NoError(err) - tRequire.TupleExists(ctx, testTuples[0], returnedAt) + tRequire.RelationshipExists(ctx, testRels[0], returnedAt) // Delete with DeleteRelationship deletedAt, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { @@ -254,7 +248,7 @@ func SimpleTest(t *testing.T, tester DatastoreTester) { return err }) require.NoError(err) - tRequire.NoTupleExists(ctx, testTuples[0], deletedAt) + tRequire.NoRelationshipExists(ctx, testRels[0], deletedAt) }) } } @@ -276,15 +270,14 @@ func ObjectIDsTest(t *testing.T, tester DatastoreTester) { require.NoError(err) defer ds.Close() - tpl := makeTestTuple(tc, tc) - require.NoError(tpl.Validate()) + rel := makeTestRel(tc, tc) - // Write the test tuple + // Write the test relationship _, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { - return rwt.WriteRelationships(ctx, []*core.RelationTupleUpdate{ + return rwt.WriteRelationships(ctx, []tuple.RelationshipUpdate{ { - Operation: core.RelationTupleUpdate_CREATE, - Tuple: tpl, + Operation: tuple.UpdateOperationCreate, + Relationship: rel, }, }) }) @@ -298,16 +291,15 @@ func ObjectIDsTest(t *testing.T, tester DatastoreTester) { OptionalResourceIds: []string{tc}, }) require.NoError(err) - defer iter.Close() - first := iter.Next() - require.NotNil(first) - require.Equal(tc, first.ResourceAndRelation.ObjectId) - require.Equal(tc, first.Subject.ObjectId) + found, err := datastore.IteratorToSlice(iter) + require.NoError(err) + require.Len(found, 1) - shouldBeNil := iter.Next() - require.Nil(shouldBeNil) - require.NoError(iter.Err()) + first := found[0] + require.NotNil(first) + require.Equal(tc, first.Resource.ObjectID) + require.Equal(tc, first.Subject.ObjectID) }) } } @@ -315,86 +307,86 @@ func ObjectIDsTest(t *testing.T, tester DatastoreTester) { // DeleteRelationshipsTest tests whether or not the requirements for deleting // relationships hold for a particular datastore. func DeleteRelationshipsTest(t *testing.T, tester DatastoreTester) { - var testTuples []*core.RelationTuple + var testRels []tuple.Relationship for i := 0; i < 10; i++ { - newTuple := makeTestTuple(fmt.Sprintf("resource%d", i), fmt.Sprintf("user%d", i%2)) - testTuples = append(testTuples, newTuple) + newRel := makeTestRel(fmt.Sprintf("resource%d", i), fmt.Sprintf("user%d", i%2)) + testRels = append(testRels, newRel) } - testTuples[len(testTuples)-1].ResourceAndRelation.Relation = "writer" + testRels[len(testRels)-1].Resource.Relation = "writer" table := []struct { - name string - inputTuples []*core.RelationTuple - filter *v1.RelationshipFilter - expectedExistingTuples []*core.RelationTuple - expectedNonExistingTuples []*core.RelationTuple + name string + inputRels []tuple.Relationship + filter *v1.RelationshipFilter + expectedExistingRels []tuple.Relationship + expectedNonExistingRels []tuple.Relationship }{ { "resourceID", - testTuples, + testRels, &v1.RelationshipFilter{ ResourceType: testResourceNamespace, OptionalResourceId: "resource0", }, - testTuples[1:], - testTuples[:1], + testRels[1:], + testRels[:1], }, { "only resourceID", - testTuples, + testRels, &v1.RelationshipFilter{ OptionalResourceId: "resource0", }, - testTuples[1:], - testTuples[:1], + testRels[1:], + testRels[:1], }, { "only relation", - testTuples, + testRels, &v1.RelationshipFilter{ OptionalRelation: "writer", }, - testTuples[:len(testTuples)-1], - []*core.RelationTuple{testTuples[len(testTuples)-1]}, + testRels[:len(testRels)-1], + []tuple.Relationship{testRels[len(testRels)-1]}, }, { "relation", - testTuples, + testRels, &v1.RelationshipFilter{ ResourceType: testResourceNamespace, OptionalRelation: "writer", }, - testTuples[:len(testTuples)-1], - []*core.RelationTuple{testTuples[len(testTuples)-1]}, + testRels[:len(testRels)-1], + []tuple.Relationship{testRels[len(testRels)-1]}, }, { "subjectID", - testTuples, + testRels, &v1.RelationshipFilter{ ResourceType: testResourceNamespace, OptionalSubjectFilter: &v1.SubjectFilter{SubjectType: testUserNamespace, OptionalSubjectId: "user0"}, }, - []*core.RelationTuple{testTuples[1], testTuples[3], testTuples[5], testTuples[7], testTuples[9]}, - []*core.RelationTuple{testTuples[0], testTuples[2], testTuples[4], testTuples[6], testTuples[8]}, + []tuple.Relationship{testRels[1], testRels[3], testRels[5], testRels[7], testRels[9]}, + []tuple.Relationship{testRels[0], testRels[2], testRels[4], testRels[6], testRels[8]}, }, { "subjectID without resource type", - testTuples, + testRels, &v1.RelationshipFilter{ OptionalSubjectFilter: &v1.SubjectFilter{SubjectType: testUserNamespace, OptionalSubjectId: "user0"}, }, - []*core.RelationTuple{testTuples[1], testTuples[3], testTuples[5], testTuples[7], testTuples[9]}, - []*core.RelationTuple{testTuples[0], testTuples[2], testTuples[4], testTuples[6], testTuples[8]}, + []tuple.Relationship{testRels[1], testRels[3], testRels[5], testRels[7], testRels[9]}, + []tuple.Relationship{testRels[0], testRels[2], testRels[4], testRels[6], testRels[8]}, }, { "subjectRelation", - testTuples, + testRels, &v1.RelationshipFilter{ ResourceType: testResourceNamespace, OptionalSubjectFilter: &v1.SubjectFilter{SubjectType: testUserNamespace, OptionalRelation: &v1.SubjectFilter_RelationFilter{Relation: ""}}, }, nil, - testTuples, + testRels, }, } @@ -410,10 +402,10 @@ func DeleteRelationshipsTest(t *testing.T, tester DatastoreTester) { setupDatastore(ds, require) - tRequire := testfixtures.TupleChecker{Require: require, DS: ds} + tRequire := testfixtures.RelationshipChecker{Require: require, DS: ds} - toTouch := make([]*core.RelationTupleUpdate, 0, len(tt.inputTuples)) - for _, tpl := range tt.inputTuples { + toTouch := make([]tuple.RelationshipUpdate, 0, len(tt.inputRels)) + for _, tpl := range tt.inputRels { toTouch = append(toTouch, tuple.Touch(tpl)) } @@ -429,12 +421,12 @@ func DeleteRelationshipsTest(t *testing.T, tester DatastoreTester) { }) require.NoError(err) - for _, tpl := range tt.expectedExistingTuples { - tRequire.TupleExists(ctx, tpl, deletedAt) + for _, tpl := range tt.expectedExistingRels { + tRequire.RelationshipExists(ctx, tpl, deletedAt) } - for _, tpl := range tt.expectedNonExistingTuples { - tRequire.NoTupleExists(ctx, tpl, deletedAt) + for _, tpl := range tt.expectedNonExistingRels { + tRequire.NoRelationshipExists(ctx, tpl, deletedAt) } }) } @@ -462,8 +454,8 @@ func InvalidReadsTest(t *testing.T, tester DatastoreTester) { revisionErr := datastore.ErrInvalidRevision{} require.True(errors.As(err, &revisionErr)) - newTuple := makeTestTuple("one", "one") - firstWrite, err := common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, newTuple) + newRel := makeTestRel("one", "one") + firstWrite, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, newRel) require.NoError(err) // Check that we can read at the just written revision @@ -473,8 +465,8 @@ func InvalidReadsTest(t *testing.T, tester DatastoreTester) { // Wait the duration required to allow the revision to expire time.Sleep(testGCDuration * 2) - // Write another tuple which will allow the first revision to expire - nextWrite, err := common.WriteTuples(ctx, ds, core.RelationTupleUpdate_TOUCH, newTuple) + // Write another relationship which will allow the first revision to expire + nextWrite, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationTouch, newRel) require.NoError(err) // Check that we can read at the just written revision @@ -499,7 +491,7 @@ func DeleteNotExistantTest(t *testing.T, tester DatastoreTester) { ctx := context.Background() _, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { - err := rwt.WriteRelationships(ctx, []*core.RelationTupleUpdate{ + err := rwt.WriteRelationships(ctx, []tuple.RelationshipUpdate{ tuple.Delete(tuple.MustParse("document:foo#viewer@user:tom#...")), }) require.NoError(err) @@ -521,7 +513,7 @@ func DeleteAlreadyDeletedTest(t *testing.T, tester DatastoreTester) { _, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { // Write the relationship. - return rwt.WriteRelationships(ctx, []*core.RelationTupleUpdate{ + return rwt.WriteRelationships(ctx, []tuple.RelationshipUpdate{ tuple.Create(tuple.MustParse("document:foo#viewer@user:tom#...")), }) }) @@ -529,7 +521,7 @@ func DeleteAlreadyDeletedTest(t *testing.T, tester DatastoreTester) { _, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { // Delete the relationship. - return rwt.WriteRelationships(ctx, []*core.RelationTupleUpdate{ + return rwt.WriteRelationships(ctx, []tuple.RelationshipUpdate{ tuple.Delete(tuple.MustParse("document:foo#viewer@user:tom#...")), }) }) @@ -537,7 +529,7 @@ func DeleteAlreadyDeletedTest(t *testing.T, tester DatastoreTester) { _, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { // Delete the relationship again. - return rwt.WriteRelationships(ctx, []*core.RelationTupleUpdate{ + return rwt.WriteRelationships(ctx, []tuple.RelationshipUpdate{ tuple.Delete(tuple.MustParse("document:foo#viewer@user:tom#...")), }) }) @@ -554,21 +546,21 @@ func WriteDeleteWriteTest(t *testing.T, tester DatastoreTester) { ds, _ := testfixtures.StandardDatastoreWithData(rawDS, require) ctx := context.Background() - tpl := makeTestTuple("foo", "tom") - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, tpl) + tpl := makeTestRel("foo", "tom") + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, tpl) require.NoError(err) - ensureTuples(ctx, require, ds, tpl) + ensureRelationships(ctx, require, ds, tpl) - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_DELETE, tpl) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationDelete, tpl) require.NoError(err) - ensureNotTuples(ctx, require, ds, tpl) + ensureNotRelationships(ctx, require, ds, tpl) - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, tpl) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, tpl) require.NoError(err) - ensureTuples(ctx, require, ds, tpl) + ensureRelationships(ctx, require, ds, tpl) } // CreateAlreadyExistingTest tests creating a relationship twice. @@ -581,18 +573,18 @@ func CreateAlreadyExistingTest(t *testing.T, tester DatastoreTester) { ds, _ := testfixtures.StandardDatastoreWithData(rawDS, require) ctx := context.Background() - tpl1 := makeTestTuple("foo", "tom") - tpl2 := makeTestTuple("foo", "sarah") - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, tpl1, tpl2) + tpl1 := makeTestRel("foo", "tom") + tpl2 := makeTestRel("foo", "sarah") + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, tpl1, tpl2) require.NoError(err) - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, tpl1) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, tpl1) require.ErrorAs(err, &common.CreateRelationshipExistsError{}) require.Contains(err.Error(), "could not CREATE relationship ") grpcutil.RequireStatus(t, codes.AlreadyExists, err) f := func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { - _, err := rwt.BulkLoad(ctx, testfixtures.NewBulkTupleGenerator(testResourceNamespace, testReaderRelation, testUserNamespace, 1, t)) + _, err := rwt.BulkLoad(ctx, testfixtures.NewBulkRelationshipGenerator(testResourceNamespace, testReaderRelation, testUserNamespace, 1, t)) return err } _, _ = ds.ReadWriteTx(ctx, f) @@ -611,24 +603,24 @@ func TouchAlreadyExistingTest(t *testing.T, tester DatastoreTester) { ds, _ := testfixtures.StandardDatastoreWithData(rawDS, require) ctx := context.Background() - tpl1 := makeTestTuple("foo", "tom") - tpl2 := makeTestTuple("foo", "sarah") + tpl1 := makeTestRel("foo", "tom") + tpl2 := makeTestRel("foo", "sarah") - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, tpl1, tpl2) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, tpl1, tpl2) require.NoError(err) - ensureTuples(ctx, require, ds, tpl1, tpl2) + ensureRelationships(ctx, require, ds, tpl1, tpl2) - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_TOUCH, tpl1, tpl2) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationTouch, tpl1, tpl2) require.NoError(err) - ensureTuples(ctx, require, ds, tpl1, tpl2) + ensureRelationships(ctx, require, ds, tpl1, tpl2) - tpl3 := makeTestTuple("foo", "fred") - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_TOUCH, tpl1, tpl3) + tpl3 := makeTestRel("foo", "fred") + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationTouch, tpl1, tpl3) require.NoError(err) - ensureTuples(ctx, require, ds, tpl1, tpl2, tpl3) + ensureRelationships(ctx, require, ds, tpl1, tpl2, tpl3) } // CreateDeleteTouchTest tests writing a relationship, deleting it, and then touching it. @@ -641,23 +633,23 @@ func CreateDeleteTouchTest(t *testing.T, tester DatastoreTester) { ds, _ := testfixtures.StandardDatastoreWithData(rawDS, require) ctx := context.Background() - tpl1 := makeTestTuple("foo", "tom") - tpl2 := makeTestTuple("foo", "sarah") + tpl1 := makeTestRel("foo", "tom") + tpl2 := makeTestRel("foo", "sarah") - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, tpl1, tpl2) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, tpl1, tpl2) require.NoError(err) - ensureTuples(ctx, require, ds, tpl1, tpl2) + ensureRelationships(ctx, require, ds, tpl1, tpl2) - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_DELETE, tpl1, tpl2) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationDelete, tpl1, tpl2) require.NoError(err) - ensureNotTuples(ctx, require, ds, tpl1, tpl2) + ensureNotRelationships(ctx, require, ds, tpl1, tpl2) - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_TOUCH, tpl1, tpl2) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationTouch, tpl1, tpl2) require.NoError(err) - ensureTuples(ctx, require, ds, tpl1, tpl2) + ensureRelationships(ctx, require, ds, tpl1, tpl2) } // DeleteOneThousandIndividualInOneCallTest tests deleting 1000 relationships, individually. @@ -671,28 +663,28 @@ func DeleteOneThousandIndividualInOneCallTest(t *testing.T, tester DatastoreTest ctx := context.Background() // Write the 1000 relationships. - tuples := make([]*core.RelationTuple, 0, 1000) + relationships := make([]tuple.Relationship, 0, 1000) for i := 0; i < 1000; i++ { - tpl := makeTestTuple("foo", fmt.Sprintf("user%d", i)) - tuples = append(tuples, tpl) + tpl := makeTestRel("foo", fmt.Sprintf("user%d", i)) + relationships = append(relationships, tpl) } - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, tuples...) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, relationships...) require.NoError(err) - ensureTuples(ctx, require, ds, tuples...) + ensureRelationships(ctx, require, ds, relationships...) - // Add an extra tuple. - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, makeTestTuple("foo", "extra")) + // Add an extra relationship. + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, makeTestRel("foo", "extra")) require.NoError(err) - ensureTuples(ctx, require, ds, makeTestTuple("foo", "extra")) + ensureRelationships(ctx, require, ds, makeTestRel("foo", "extra")) - // Delete the first 1000 tuples. - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_DELETE, tuples...) + // Delete the first 1000 relationships. + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationDelete, relationships...) require.NoError(err) - ensureNotTuples(ctx, require, ds, tuples...) + ensureNotRelationships(ctx, require, ds, relationships...) - // Ensure the extra tuple is still present. - ensureTuples(ctx, require, ds, makeTestTuple("foo", "extra")) + // Ensure the extra relationship is still present. + ensureRelationships(ctx, require, ds, makeTestRel("foo", "extra")) } // DeleteWithLimitTest tests deleting relationships with a limit. @@ -706,17 +698,16 @@ func DeleteWithLimitTest(t *testing.T, tester DatastoreTester) { ctx := context.Background() // Write the 1000 relationships. - tuples := make([]*core.RelationTuple, 0, 1000) + rels := make([]tuple.Relationship, 0, 1000) for i := 0; i < 1000; i++ { - tpl := makeTestTuple("foo", fmt.Sprintf("user%d", i)) - tuples = append(tuples, tpl) + rels = append(rels, makeTestRel("foo", fmt.Sprintf("user%d", i))) } - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, tuples...) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, rels...) require.NoError(err) - ensureTuples(ctx, require, ds, tuples...) + ensureRelationships(ctx, require, ds, rels...) - // Delete 100 tuples. + // Delete 100 rels. var deleteLimit uint64 = 100 _, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { limitReached, err := rwt.DeleteRelationships(ctx, &v1.RelationshipFilter{ @@ -728,8 +719,8 @@ func DeleteWithLimitTest(t *testing.T, tester DatastoreTester) { }) require.NoError(err) - // Ensure 900 tuples remain. - found := countTuples(ctx, require, ds, testResourceNamespace) + // Ensure 900 rels remain. + found := countRels(ctx, require, ds, testResourceNamespace) require.Equal(900, found) // Delete the remainder. @@ -744,7 +735,7 @@ func DeleteWithLimitTest(t *testing.T, tester DatastoreTester) { }) require.NoError(err) - found = countTuples(ctx, require, ds, testResourceNamespace) + found = countRels(ctx, require, ds, testResourceNamespace) require.Equal(0, found) } @@ -758,18 +749,20 @@ func DeleteCaveatedTupleTest(t *testing.T, tester DatastoreTester) { ds, _ := testfixtures.StandardDatastoreWithData(rawDS, require) ctx := context.Background() - tpl := tuple.Parse("test/resource:someresource#viewer@test/user:someuser[somecaveat]") + tpl, err := tuple.Parse("test/resource:someresource#viewer@test/user:someuser[somecaveat]") + require.NoError(err) - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, tpl) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, tpl) require.NoError(err) - ensureTuples(ctx, require, ds, tpl) + ensureRelationships(ctx, require, ds, tpl) // Delete the tuple. - withoutCaveat := tuple.Parse("test/resource:someresource#viewer@test/user:someuser") + withoutCaveat, err := tuple.Parse("test/resource:someresource#viewer@test/user:someuser") + require.NoError(err) - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_DELETE, withoutCaveat) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationDelete, withoutCaveat) require.NoError(err) - ensureNotTuples(ctx, require, ds, tpl, withoutCaveat) + ensureNotRelationships(ctx, require, ds, tpl, withoutCaveat) } // DeleteRelationshipsWithVariousFiltersTest tests deleting relationships with various filters. @@ -883,7 +876,7 @@ func DeleteRelationshipsWithVariousFiltersTest(t *testing.T, tester DatastoreTes allRelationships.Add(rel) tpl := tuple.MustParse(rel) - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, tpl) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, tpl) require.NoError(err) } @@ -913,19 +906,16 @@ func DeleteRelationshipsWithVariousFiltersTest(t *testing.T, tester DatastoreTes reader := ds.SnapshotReader(headRev) iter, err := reader.QueryRelationships(ctx, filter) require.NoError(err) - t.Cleanup(iter.Close) - found := iter.Next() - if found != nil { - require.Nil(found, "got relationship: %s", tuple.MustString(found)) - } - iter.Close() + found, err := datastore.IteratorToSlice(iter) + require.NoError(err) + require.Empty(found, "got relationships: %v", found) // Ensure the expected relationships were deleted. resourceTypes := mapz.NewSet[string]() for _, rel := range tc.relationships { tpl := tuple.MustParse(rel) - resourceTypes.Add(tpl.ResourceAndRelation.Namespace) + resourceTypes.Add(tpl.Resource.ObjectType) } allRemainingRelationships := mapz.NewSet[string]() @@ -934,16 +924,11 @@ func DeleteRelationshipsWithVariousFiltersTest(t *testing.T, tester DatastoreTes OptionalResourceType: resourceType, }) require.NoError(err) - t.Cleanup(iter.Close) - for { - rel := iter.Next() - if rel == nil { - break - } + for rel, err := range iter { + require.NoError(err) allRemainingRelationships.Add(tuple.MustString(rel)) } - iter.Close() } deletedRelationships := allRelationships.Subtract(allRemainingRelationships).AsSlice() @@ -957,16 +942,11 @@ func DeleteRelationshipsWithVariousFiltersTest(t *testing.T, tester DatastoreTes OptionalResourceType: resourceType, }) require.NoError(err) - t.Cleanup(iter.Close) - for { - rel := iter.Next() - if rel == nil { - break - } + for rel, err := range iter { + require.NoError(err) allInitialRelationships.Add(tuple.MustString(rel)) } - iter.Close() } require.ElementsMatch(tc.relationships, allInitialRelationships.AsSlice()) @@ -985,13 +965,13 @@ func RecreateRelationshipsAfterDeleteWithFilter(t *testing.T, tester DatastoreTe ds, _ := testfixtures.StandardDatastoreWithSchema(rawDS, require) ctx := context.Background() - relationships := make([]*core.RelationTuple, 100) + relationships := make([]tuple.Relationship, 100) for i := 0; i < 100; i++ { relationships[i] = tuple.MustParse(fmt.Sprintf("document:%d#owner@user:first", i)) } writeRelationships := func() error { - _, err := common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, relationships...) + _, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, relationships...) return err } @@ -1387,7 +1367,7 @@ func QueryRelationshipsWithVariousFiltersTest(t *testing.T, tester DatastoreTest for _, rel := range tc.relationships { tpl := tuple.MustParse(rel) - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, tpl) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, tpl) require.NoError(err) } @@ -1399,17 +1379,10 @@ func QueryRelationshipsWithVariousFiltersTest(t *testing.T, tester DatastoreTest require.NoError(err) var results []string - for { - tpl := iter.Next() - if tpl == nil { - err := iter.Err() - require.NoError(err) - break - } - - results = append(results, tuple.MustString(tpl)) + for rel, err := range iter { + require.NoError(err) + results = append(results, tuple.MustString(rel)) } - iter.Close() require.ElementsMatch(tc.expected, results) }) @@ -1426,15 +1399,16 @@ func TypedTouchAlreadyExistingTest(t *testing.T, tester DatastoreTester) { ds, _ := testfixtures.StandardDatastoreWithData(rawDS, require) ctx := context.Background() - tpl1 := tuple.Parse("document:foo#viewer@user:tom") + tpl1, err := tuple.Parse("document:foo#viewer@user:tom") + require.NoError(err) - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_TOUCH, tpl1) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationTouch, tpl1) require.NoError(err) - ensureTuples(ctx, require, ds, tpl1) + ensureRelationships(ctx, require, ds, tpl1) - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_TOUCH, tpl1) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationTouch, tpl1) require.NoError(err) - ensureTuples(ctx, require, ds, tpl1) + ensureRelationships(ctx, require, ds, tpl1) } // TypedTouchAlreadyExistingWithCaveatTest tests touching a relationship twice, when valid type information is provided. @@ -1447,17 +1421,19 @@ func TypedTouchAlreadyExistingWithCaveatTest(t *testing.T, tester DatastoreTeste ds, _ := testfixtures.StandardDatastoreWithData(rawDS, require) ctx := context.Background() - ctpl1 := tuple.Parse("document:foo#caveated_viewer@user:tom[test:{\"foo\":\"bar\"}]") + ctpl1, err := tuple.Parse("document:foo#caveated_viewer@user:tom[test:{\"foo\":\"bar\"}]") + require.NoError(err) - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_TOUCH, ctpl1) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationTouch, ctpl1) require.NoError(err) - ensureTuples(ctx, require, ds, ctpl1) + ensureRelationships(ctx, require, ds, ctpl1) - ctpl1Updated := tuple.Parse("document:foo#caveated_viewer@user:tom[test:{\"foo\":\"baz\"}]") + ctpl1Updated, err := tuple.Parse("document:foo#caveated_viewer@user:tom[test:{\"foo\":\"baz\"}]") + require.NoError(err) - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_TOUCH, ctpl1Updated) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationTouch, ctpl1Updated) require.NoError(err) - ensureTuples(ctx, require, ds, ctpl1Updated) + ensureRelationships(ctx, require, ds, ctpl1Updated) } // CreateTouchDeleteTouchTest tests writing a relationship, touching it, deleting it, and then touching it. @@ -1470,28 +1446,28 @@ func CreateTouchDeleteTouchTest(t *testing.T, tester DatastoreTester) { ds, _ := testfixtures.StandardDatastoreWithData(rawDS, require) ctx := context.Background() - tpl1 := makeTestTuple("foo", "tom") - tpl2 := makeTestTuple("foo", "sarah") + tpl1 := makeTestRel("foo", "tom") + tpl2 := makeTestRel("foo", "sarah") - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, tpl1, tpl2) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, tpl1, tpl2) require.NoError(err) - ensureTuples(ctx, require, ds, tpl1, tpl2) + ensureRelationships(ctx, require, ds, tpl1, tpl2) - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_TOUCH, tpl1, tpl2) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationTouch, tpl1, tpl2) require.NoError(err) - ensureTuples(ctx, require, ds, tpl1, tpl2) + ensureRelationships(ctx, require, ds, tpl1, tpl2) - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_DELETE, tpl1, tpl2) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationDelete, tpl1, tpl2) require.NoError(err) - ensureNotTuples(ctx, require, ds, tpl1, tpl2) + ensureNotRelationships(ctx, require, ds, tpl1, tpl2) - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_TOUCH, tpl1, tpl2) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationTouch, tpl1, tpl2) require.NoError(err) - ensureTuples(ctx, require, ds, tpl1, tpl2) + ensureRelationships(ctx, require, ds, tpl1, tpl2) } // TouchAlreadyExistingCaveatedTest tests touching a relationship twice. @@ -1504,20 +1480,20 @@ func TouchAlreadyExistingCaveatedTest(t *testing.T, tester DatastoreTester) { ds, _ := testfixtures.StandardDatastoreWithData(rawDS, require) ctx := context.Background() - tpl1 := tuple.MustWithCaveat(makeTestTuple("foo", "tom"), "formercaveat") - tpl2 := makeTestTuple("foo", "sarah") - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_TOUCH, tpl1, tpl2) + tpl1 := tuple.MustWithCaveat(makeTestRel("foo", "tom"), "formercaveat") + tpl2 := makeTestRel("foo", "sarah") + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationTouch, tpl1, tpl2) require.NoError(err) - ensureTuples(ctx, require, ds, tpl1, tpl2) + ensureRelationships(ctx, require, ds, tpl1, tpl2) - ctpl1 := tuple.MustWithCaveat(makeTestTuple("foo", "tom"), "somecaveat") - tpl3 := makeTestTuple("foo", "fred") + ctpl1 := tuple.MustWithCaveat(makeTestRel("foo", "tom"), "somecaveat") + tpl3 := makeTestRel("foo", "fred") - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_TOUCH, ctpl1, tpl3) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationTouch, ctpl1, tpl3) require.NoError(err) - ensureTuples(ctx, require, ds, tpl2, tpl3, ctpl1) + ensureRelationships(ctx, require, ds, tpl2, tpl3, ctpl1) } func MultipleReadsInRWTTest(t *testing.T, tester DatastoreTester) { @@ -1534,13 +1510,19 @@ func MultipleReadsInRWTTest(t *testing.T, tester DatastoreTester) { OptionalResourceType: "document", }) require.NoError(err) - it.Close() + + for range it { + break + } it, err = rwt.QueryRelationships(ctx, datastore.RelationshipsFilter{ OptionalResourceType: "folder", }) require.NoError(err) - it.Close() + + for range it { + break + } return nil }) @@ -1572,15 +1554,18 @@ func ConcurrentWriteSerializationTest(t *testing.T, tester DatastoreTester) { iter, err := rwt.QueryRelationships(ctx, datastore.RelationshipsFilter{ OptionalResourceType: testResourceNamespace, }) - iter.Close() if err != nil { return err } + for range iter { + break + } + // We do NOT assert the error here because serialization problems can manifest as errors // on the individual writes. - rtu := tuple.Touch(makeTestTuple("new_resource", "new_user")) - err = rwt.WriteRelationships(ctx, []*core.RelationTupleUpdate{rtu}) + rtu := tuple.Touch(makeTestRel("new_resource", "new_user")) + err = rwt.WriteRelationships(ctx, []tuple.RelationshipUpdate{rtu}) waitToStartCloser.Do(func() { close(waitToStart) @@ -1601,8 +1586,8 @@ func ConcurrentWriteSerializationTest(t *testing.T, tester DatastoreTester) { close(waitToFinish) }) - rtu := tuple.Touch(makeTestTuple("another_resource", "another_user")) - return rwt.WriteRelationships(ctx, []*core.RelationTupleUpdate{rtu}) + rtu := tuple.Touch(makeTestRel("another_resource", "another_user")) + return rwt.WriteRelationships(ctx, []tuple.RelationshipUpdate{rtu}) }) require.NoError(err) require.NoError(g.Wait()) @@ -1621,12 +1606,12 @@ func BulkDeleteRelationshipsTest(t *testing.T, tester DatastoreTester) { // Write a bunch of relationships. t.Log(time.Now(), "starting write") _, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { - _, err := rwt.BulkLoad(ctx, testfixtures.NewBulkTupleGenerator(testResourceNamespace, testReaderRelation, testUserNamespace, 1000, t)) + _, err := rwt.BulkLoad(ctx, testfixtures.NewBulkRelationshipGenerator(testResourceNamespace, testReaderRelation, testUserNamespace, 1000, t)) if err != nil { return err } - _, err = rwt.BulkLoad(ctx, testfixtures.NewBulkTupleGenerator(testResourceNamespace, testEditorRelation, testUserNamespace, 1000, t)) + _, err = rwt.BulkLoad(ctx, testfixtures.NewBulkRelationshipGenerator(testResourceNamespace, testEditorRelation, testUserNamespace, 1000, t)) if err != nil { return err } @@ -1659,66 +1644,65 @@ func BulkDeleteRelationshipsTest(t *testing.T, tester DatastoreTester) { OptionalResourceRelation: testReaderRelation, }) require.NoError(err) - defer iter.Close() - require.Nil(iter.Next(), "expected no results") + found, err := datastore.IteratorToSlice(iter) + require.NoError(err) + require.Empty(found) } -func onrToSubjectsFilter(onr *core.ObjectAndRelation) datastore.SubjectsFilter { +func onrToSubjectsFilter(onr tuple.ObjectAndRelation) datastore.SubjectsFilter { return datastore.SubjectsFilter{ - SubjectType: onr.Namespace, - OptionalSubjectIds: []string{onr.ObjectId}, + SubjectType: onr.ObjectType, + OptionalSubjectIds: []string{onr.ObjectID}, RelationFilter: datastore.SubjectRelationFilter{}.WithNonEllipsisRelation(onr.Relation), } } -func ensureTuples(ctx context.Context, require *require.Assertions, ds datastore.Datastore, tpls ...*core.RelationTuple) { - ensureTuplesStatus(ctx, require, ds, tpls, true) +func ensureRelationships(ctx context.Context, require *require.Assertions, ds datastore.Datastore, rels ...tuple.Relationship) { + ensureRelationshipsStatus(ctx, require, ds, rels, true) } -func ensureNotTuples(ctx context.Context, require *require.Assertions, ds datastore.Datastore, tpls ...*core.RelationTuple) { - ensureTuplesStatus(ctx, require, ds, tpls, false) +func ensureNotRelationships(ctx context.Context, require *require.Assertions, ds datastore.Datastore, rels ...tuple.Relationship) { + ensureRelationshipsStatus(ctx, require, ds, rels, false) } -func ensureTuplesStatus(ctx context.Context, require *require.Assertions, ds datastore.Datastore, tpls []*core.RelationTuple, mustExist bool) { +func ensureRelationshipsStatus(ctx context.Context, require *require.Assertions, ds datastore.Datastore, rels []tuple.Relationship, mustExist bool) { headRev, err := ds.HeadRevision(ctx) require.NoError(err) reader := ds.SnapshotReader(headRev) - for _, tpl := range tpls { + for _, rel := range rels { iter, err := reader.QueryRelationships(ctx, datastore.RelationshipsFilter{ - OptionalResourceType: tpl.ResourceAndRelation.Namespace, - OptionalResourceIds: []string{tpl.ResourceAndRelation.ObjectId}, - OptionalResourceRelation: tpl.ResourceAndRelation.Relation, + OptionalResourceType: rel.Resource.ObjectType, + OptionalResourceIds: []string{rel.Resource.ObjectID}, + OptionalResourceRelation: rel.Resource.Relation, OptionalSubjectsSelectors: []datastore.SubjectsSelector{ { - OptionalSubjectType: tpl.Subject.Namespace, - OptionalSubjectIds: []string{tpl.Subject.ObjectId}, + OptionalSubjectType: rel.Subject.ObjectType, + OptionalSubjectIds: []string{rel.Subject.ObjectID}, }, }, }) require.NoError(err) - defer iter.Close() - found := iter.Next() - require.NoError(iter.Err()) + found, err := datastore.IteratorToSlice(iter) + require.NoError(err) if mustExist { - require.NotNil(found, "expected tuple %s", tuple.MustString(tpl)) + require.NotEmpty(found, "expected relationship %s", tuple.MustString(rel)) } else { - require.Nil(found, "expected tuple %s to not exist", tuple.MustString(tpl)) + require.Empty(found, "expected relationship %s to not exist", tuple.MustString(rel)) } - iter.Close() - if mustExist { - require.Equal(tuple.MustString(tpl), tuple.MustString(found)) + require.Equal(1, len(found)) + require.Equal(tuple.MustString(rel), tuple.MustString(found[0])) } } } -func countTuples(ctx context.Context, require *require.Assertions, ds datastore.Datastore, resourceType string) int { +func countRels(ctx context.Context, require *require.Assertions, ds datastore.Datastore, resourceType string) int { headRev, err := ds.HeadRevision(ctx) require.NoError(err) @@ -1728,15 +1712,10 @@ func countTuples(ctx context.Context, require *require.Assertions, ds datastore. OptionalResourceType: resourceType, }) require.NoError(err) - defer iter.Close() counter := 0 - for { - rel := iter.Next() - if rel == nil { - break - } - + for _, err := range iter { + require.NoError(err) counter++ } diff --git a/pkg/datastore/test/revisions.go b/pkg/datastore/test/revisions.go index b6eef01533..0354329997 100644 --- a/pkg/datastore/test/revisions.go +++ b/pkg/datastore/test/revisions.go @@ -14,6 +14,7 @@ import ( ns "github.com/authzed/spicedb/pkg/namespace" core "github.com/authzed/spicedb/pkg/proto/core/v1" dispatch "github.com/authzed/spicedb/pkg/proto/dispatch/v1" + "github.com/authzed/spicedb/pkg/tuple" ) // RevisionQuantizationTest tests whether or not the requirements for revisions hold @@ -44,9 +45,9 @@ func RevisionQuantizationTest(t *testing.T, tester DatastoreTester) { // Create some revisions var writtenAt datastore.Revision - tpl := makeTestTuple("first", "owner") + tpl := makeTestRel("first", "owner") for i := 0; i < 10; i++ { - writtenAt, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_TOUCH, tpl) + writtenAt, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationTouch, tpl) require.NoError(err) } require.True(writtenAt.GreaterThan(postSetupRevision)) diff --git a/pkg/datastore/test/watch.go b/pkg/datastore/test/watch.go index 41f28f59b5..961260d72c 100644 --- a/pkg/datastore/test/watch.go +++ b/pkg/datastore/test/watch.go @@ -79,16 +79,16 @@ func WatchTest(t *testing.T, tester DatastoreTester) { changes, errchan := ds.Watch(ctx, lowestRevision, opts) require.Zero(len(errchan)) - var testUpdates [][]*core.RelationTupleUpdate - var bulkDeletes []*core.RelationTupleUpdate + var testUpdates [][]tuple.RelationshipUpdate + var bulkDeletes []tuple.RelationshipUpdate for i := 0; i < tc.numTuples; i++ { - newRelationship := makeTestTuple(fmt.Sprintf("relation%d", i), "test_user") + newRelationship := makeTestRel(fmt.Sprintf("relation%d", i), "test_user") newUpdate := tuple.Touch(newRelationship) - batch := []*core.RelationTupleUpdate{newUpdate} + batch := []tuple.RelationshipUpdate{newUpdate} testUpdates = append(testUpdates, batch) - _, err := common.UpdateTuplesInDatastore(ctx, ds, newUpdate) + _, err := common.UpdateRelationshipsInDatastore(ctx, ds, newUpdate) require.NoError(err) if i != 0 { @@ -96,18 +96,18 @@ func WatchTest(t *testing.T, tester DatastoreTester) { } } - updateUpdate := tuple.Touch(tuple.MustWithCaveat(makeTestTuple("relation0", "test_user"), "somecaveat")) - createUpdate := tuple.Touch(makeTestTuple("another_relation", "somestuff")) + updateUpdate := tuple.Touch(tuple.MustWithCaveat(makeTestRel("relation0", "test_user"), "somecaveat")) + createUpdate := tuple.Touch(makeTestRel("another_relation", "somestuff")) - batch := []*core.RelationTupleUpdate{updateUpdate, createUpdate} - _, err = common.UpdateTuplesInDatastore(ctx, ds, batch...) + batch := []tuple.RelationshipUpdate{updateUpdate, createUpdate} + _, err = common.UpdateRelationshipsInDatastore(ctx, ds, batch...) require.NoError(err) - deleteUpdate := tuple.Delete(makeTestTuple("relation0", "test_user")) - _, err = common.UpdateTuplesInDatastore(ctx, ds, deleteUpdate) + deleteUpdate := tuple.Delete(makeTestRel("relation0", "test_user")) + _, err = common.UpdateRelationshipsInDatastore(ctx, ds, deleteUpdate) require.NoError(err) - testUpdates = append(testUpdates, batch, []*core.RelationTupleUpdate{deleteUpdate}) + testUpdates = append(testUpdates, batch, []tuple.RelationshipUpdate{deleteUpdate}) _, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { _, err := rwt.DeleteRelationships(ctx, &v1.RelationshipFilter{ @@ -138,7 +138,7 @@ func WatchTest(t *testing.T, tester DatastoreTester) { func VerifyUpdates( require *require.Assertions, - testUpdates [][]*core.RelationTupleUpdate, + testUpdates [][]tuple.RelationshipUpdate, changes <-chan *datastore.RevisionChanges, errchan <-chan error, expectDisconnect bool, @@ -222,10 +222,10 @@ func VerifyUpdatesWithMetadata( require.False(expectDisconnect, "all changes verified without expected disconnect") } -func setOfChanges(changes []*core.RelationTupleUpdate) *strset.Set { +func setOfChanges(changes []tuple.RelationshipUpdate) *strset.Set { changeSet := strset.NewWithSize(len(changes)) for _, change := range changes { - changeSet.Add(fmt.Sprintf("OPERATION_%s(%s)", change.Operation, tuple.StringWithoutCaveat(change.Tuple))) + changeSet.Add(change.DebugString()) } return changeSet } @@ -244,7 +244,7 @@ func WatchCancelTest(t *testing.T, tester DatastoreTester) { changes, errchan := ds.Watch(ctx, startWatchRevision, datastore.WatchJustRelationships()) require.Zero(len(errchan)) - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_CREATE, makeTestTuple("test", "test")) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationCreate, makeTestRel("test", "test")) require.NoError(err) cancel() @@ -255,7 +255,7 @@ func WatchCancelTest(t *testing.T, tester DatastoreTester) { case created, ok := <-changes: if ok { foundDiff := cmp.Diff( - []*core.RelationTupleUpdate{tuple.Touch(makeTestTuple("test", "test"))}, + []tuple.RelationshipUpdate{tuple.Touch(makeTestRel("test", "test"))}, created.RelationshipChanges, protocmp.Transform(), ) @@ -296,24 +296,24 @@ func WatchWithTouchTest(t *testing.T, tester DatastoreTester) { changes, errchan := ds.Watch(ctx, lowestRevision, datastore.WatchJustRelationships()) require.Zero(len(errchan)) - afterTouchRevision, err := common.WriteTuples(ctx, ds, core.RelationTupleUpdate_TOUCH, - tuple.Parse("document:firstdoc#viewer@user:tom"), - tuple.Parse("document:firstdoc#viewer@user:sarah"), - tuple.Parse("document:firstdoc#viewer@user:fred[thirdcaveat]"), + afterTouchRevision, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationTouch, + tuple.MustParse("document:firstdoc#viewer@user:tom"), + tuple.MustParse("document:firstdoc#viewer@user:sarah"), + tuple.MustParse("document:firstdoc#viewer@user:fred[thirdcaveat]"), ) require.NoError(err) - ensureTuples(ctx, require, ds, - tuple.Parse("document:firstdoc#viewer@user:tom"), - tuple.Parse("document:firstdoc#viewer@user:sarah"), - tuple.Parse("document:firstdoc#viewer@user:fred[thirdcaveat]"), + ensureRelationships(ctx, require, ds, + tuple.MustParse("document:firstdoc#viewer@user:tom"), + tuple.MustParse("document:firstdoc#viewer@user:sarah"), + tuple.MustParse("document:firstdoc#viewer@user:fred[thirdcaveat]"), ) - VerifyUpdates(require, [][]*core.RelationTupleUpdate{ + VerifyUpdates(require, [][]tuple.RelationshipUpdate{ { - tuple.Touch(tuple.Parse("document:firstdoc#viewer@user:tom")), - tuple.Touch(tuple.Parse("document:firstdoc#viewer@user:sarah")), - tuple.Touch(tuple.Parse("document:firstdoc#viewer@user:fred[thirdcaveat]")), + tuple.Touch(tuple.MustParse("document:firstdoc#viewer@user:tom")), + tuple.Touch(tuple.MustParse("document:firstdoc#viewer@user:sarah")), + tuple.Touch(tuple.MustParse("document:firstdoc#viewer@user:fred[thirdcaveat]")), }, }, changes, @@ -325,13 +325,13 @@ func WatchWithTouchTest(t *testing.T, tester DatastoreTester) { changes, errchan = ds.Watch(ctx, afterTouchRevision, datastore.WatchJustRelationships()) require.Zero(len(errchan)) - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_TOUCH, tuple.Parse("document:firstdoc#viewer@user:tom")) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationTouch, tuple.MustParse("document:firstdoc#viewer@user:tom")) require.NoError(err) - ensureTuples(ctx, require, ds, - tuple.Parse("document:firstdoc#viewer@user:tom"), - tuple.Parse("document:firstdoc#viewer@user:sarah"), - tuple.Parse("document:firstdoc#viewer@user:fred[thirdcaveat]"), + ensureRelationships(ctx, require, ds, + tuple.MustParse("document:firstdoc#viewer@user:tom"), + tuple.MustParse("document:firstdoc#viewer@user:sarah"), + tuple.MustParse("document:firstdoc#viewer@user:fred[thirdcaveat]"), ) verifyNoUpdates(require, @@ -344,17 +344,17 @@ func WatchWithTouchTest(t *testing.T, tester DatastoreTester) { changes, errchan = ds.Watch(ctx, afterTouchRevision, datastore.WatchJustRelationships()) require.Zero(len(errchan)) - afterNameChange, err := common.WriteTuples(ctx, ds, core.RelationTupleUpdate_TOUCH, tuple.Parse("document:firstdoc#viewer@user:tom[somecaveat]")) + afterNameChange, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationTouch, tuple.MustParse("document:firstdoc#viewer@user:tom[somecaveat]")) require.NoError(err) - ensureTuples(ctx, require, ds, - tuple.Parse("document:firstdoc#viewer@user:tom[somecaveat]"), - tuple.Parse("document:firstdoc#viewer@user:sarah"), - tuple.Parse("document:firstdoc#viewer@user:fred[thirdcaveat]"), + ensureRelationships(ctx, require, ds, + tuple.MustParse("document:firstdoc#viewer@user:tom[somecaveat]"), + tuple.MustParse("document:firstdoc#viewer@user:sarah"), + tuple.MustParse("document:firstdoc#viewer@user:fred[thirdcaveat]"), ) - VerifyUpdates(require, [][]*core.RelationTupleUpdate{ - {tuple.Touch(tuple.Parse("document:firstdoc#viewer@user:tom[somecaveat]"))}, + VerifyUpdates(require, [][]tuple.RelationshipUpdate{ + {tuple.Touch(tuple.MustParse("document:firstdoc#viewer@user:tom[somecaveat]"))}, }, changes, errchan, @@ -365,17 +365,17 @@ func WatchWithTouchTest(t *testing.T, tester DatastoreTester) { changes, errchan = ds.Watch(ctx, afterNameChange, datastore.WatchJustRelationships()) require.Zero(len(errchan)) - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_TOUCH, tuple.Parse("document:firstdoc#viewer@user:tom[somecaveat:{\"somecondition\": 42}]")) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationTouch, tuple.MustParse("document:firstdoc#viewer@user:tom[somecaveat:{\"somecondition\": 42}]")) require.NoError(err) - ensureTuples(ctx, require, ds, - tuple.Parse("document:firstdoc#viewer@user:tom[somecaveat:{\"somecondition\": 42}]"), - tuple.Parse("document:firstdoc#viewer@user:sarah"), - tuple.Parse("document:firstdoc#viewer@user:fred[thirdcaveat]"), + ensureRelationships(ctx, require, ds, + tuple.MustParse("document:firstdoc#viewer@user:tom[somecaveat:{\"somecondition\": 42}]"), + tuple.MustParse("document:firstdoc#viewer@user:sarah"), + tuple.MustParse("document:firstdoc#viewer@user:fred[thirdcaveat]"), ) - VerifyUpdates(require, [][]*core.RelationTupleUpdate{ - {tuple.Touch(tuple.Parse("document:firstdoc#viewer@user:tom[somecaveat:{\"somecondition\": 42}]"))}, + VerifyUpdates(require, [][]tuple.RelationshipUpdate{ + {tuple.Touch(tuple.MustParse("document:firstdoc#viewer@user:tom[somecaveat:{\"somecondition\": 42}]"))}, }, changes, errchan, @@ -384,7 +384,7 @@ func WatchWithTouchTest(t *testing.T, tester DatastoreTester) { } type updateWithMetadata struct { - updates []*core.RelationTupleUpdate + updates []tuple.RelationshipUpdate metadata map[string]any } @@ -409,7 +409,7 @@ func WatchWithMetadataTest(t *testing.T, tester DatastoreTester) { require.NoError(err) _, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { - return rwt.WriteRelationships(ctx, []*core.RelationTupleUpdate{ + return rwt.WriteRelationships(ctx, []tuple.RelationshipUpdate{ tuple.Create(tuple.MustParse("document:firstdoc#viewer@user:tom")), }) }, options.WithMetadata(metadata)) @@ -417,7 +417,7 @@ func WatchWithMetadataTest(t *testing.T, tester DatastoreTester) { VerifyUpdatesWithMetadata(require, []updateWithMetadata{ { - updates: []*core.RelationTupleUpdate{tuple.Touch(tuple.Parse("document:firstdoc#viewer@user:tom"))}, + updates: []tuple.RelationshipUpdate{tuple.Touch(tuple.MustParse("document:firstdoc#viewer@user:tom"))}, metadata: map[string]any{"somekey": "somevalue"}, }, }, @@ -445,24 +445,24 @@ func WatchWithDeleteTest(t *testing.T, tester DatastoreTester) { changes, errchan := ds.Watch(ctx, lowestRevision, datastore.WatchJustRelationships()) require.Zero(len(errchan)) - afterTouchRevision, err := common.WriteTuples(ctx, ds, core.RelationTupleUpdate_TOUCH, - tuple.Parse("document:firstdoc#viewer@user:tom"), - tuple.Parse("document:firstdoc#viewer@user:sarah"), - tuple.Parse("document:firstdoc#viewer@user:fred[thirdcaveat]"), + afterTouchRevision, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationTouch, + tuple.MustParse("document:firstdoc#viewer@user:tom"), + tuple.MustParse("document:firstdoc#viewer@user:sarah"), + tuple.MustParse("document:firstdoc#viewer@user:fred[thirdcaveat]"), ) require.NoError(err) - ensureTuples(ctx, require, ds, - tuple.Parse("document:firstdoc#viewer@user:tom"), - tuple.Parse("document:firstdoc#viewer@user:sarah"), - tuple.Parse("document:firstdoc#viewer@user:fred[thirdcaveat]"), + ensureRelationships(ctx, require, ds, + tuple.MustParse("document:firstdoc#viewer@user:tom"), + tuple.MustParse("document:firstdoc#viewer@user:sarah"), + tuple.MustParse("document:firstdoc#viewer@user:fred[thirdcaveat]"), ) - VerifyUpdates(require, [][]*core.RelationTupleUpdate{ + VerifyUpdates(require, [][]tuple.RelationshipUpdate{ { - tuple.Touch(tuple.Parse("document:firstdoc#viewer@user:tom")), - tuple.Touch(tuple.Parse("document:firstdoc#viewer@user:sarah")), - tuple.Touch(tuple.Parse("document:firstdoc#viewer@user:fred[thirdcaveat]")), + tuple.Touch(tuple.MustParse("document:firstdoc#viewer@user:tom")), + tuple.Touch(tuple.MustParse("document:firstdoc#viewer@user:sarah")), + tuple.Touch(tuple.MustParse("document:firstdoc#viewer@user:fred[thirdcaveat]")), }, }, changes, @@ -474,16 +474,16 @@ func WatchWithDeleteTest(t *testing.T, tester DatastoreTester) { changes, errchan = ds.Watch(ctx, afterTouchRevision, datastore.WatchJustRelationships()) require.Zero(len(errchan)) - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_DELETE, tuple.Parse("document:firstdoc#viewer@user:tom")) + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationDelete, tuple.MustParse("document:firstdoc#viewer@user:tom")) require.NoError(err) - ensureTuples(ctx, require, ds, - tuple.Parse("document:firstdoc#viewer@user:sarah"), - tuple.Parse("document:firstdoc#viewer@user:fred[thirdcaveat]"), + ensureRelationships(ctx, require, ds, + tuple.MustParse("document:firstdoc#viewer@user:sarah"), + tuple.MustParse("document:firstdoc#viewer@user:fred[thirdcaveat]"), ) - VerifyUpdates(require, [][]*core.RelationTupleUpdate{ - {tuple.Delete(tuple.Parse("document:firstdoc#viewer@user:tom"))}, + VerifyUpdates(require, [][]tuple.RelationshipUpdate{ + {tuple.Delete(tuple.MustParse("document:firstdoc#viewer@user:tom"))}, }, changes, errchan, @@ -623,18 +623,18 @@ func WatchAllTest(t *testing.T, tester DatastoreTester) { }, changes, errchan, false) // Write some relationships. - _, err = common.WriteTuples(ctx, ds, core.RelationTupleUpdate_TOUCH, - tuple.Parse("document:firstdoc#viewer@user:tom"), - tuple.Parse("document:firstdoc#viewer@user:sarah"), - tuple.Parse("document:firstdoc#viewer@user:fred[thirdcaveat]"), + _, err = common.WriteRelationships(ctx, ds, tuple.UpdateOperationTouch, + tuple.MustParse("document:firstdoc#viewer@user:tom"), + tuple.MustParse("document:firstdoc#viewer@user:sarah"), + tuple.MustParse("document:firstdoc#viewer@user:fred[thirdcaveat]"), ) require.NoError(err) verifyMixedUpdates(require, [][]string{ { - "rel:OPERATION_TOUCH(document:firstdoc#viewer@user:fred)", - "rel:OPERATION_TOUCH(document:firstdoc#viewer@user:sarah)", - "rel:OPERATION_TOUCH(document:firstdoc#viewer@user:tom)", + "rel:TOUCH(document:firstdoc#viewer@user:fred)", + "rel:TOUCH(document:firstdoc#viewer@user:sarah)", + "rel:TOUCH(document:firstdoc#viewer@user:tom)", }, }, changes, errchan, false) @@ -693,7 +693,7 @@ func verifyMixedUpdates( } for _, update := range change.RelationshipChanges { - foundChanges.Insert("rel:" + fmt.Sprintf("OPERATION_%s(%s)", update.Operation, tuple.StringWithoutCaveat(update.Tuple))) + foundChanges.Insert("rel:" + update.DebugString()) } found := foundChanges.AsSlice() @@ -731,8 +731,8 @@ func WatchCheckpointsTest(t *testing.T, tester DatastoreTester) { }) require.Zero(len(errchan)) - afterTouchRevision, err := common.WriteTuples(ctx, ds, core.RelationTupleUpdate_TOUCH, - tuple.Parse("document:firstdoc#viewer@user:tom"), + afterTouchRevision, err := common.WriteRelationships(ctx, ds, tuple.UpdateOperationTouch, + tuple.MustParse("document:firstdoc#viewer@user:tom"), ) require.NoError(err) diff --git a/pkg/development/assertions.go b/pkg/development/assertions.go index 83e000c4ad..7b08e6b8c2 100644 --- a/pkg/development/assertions.go +++ b/pkg/development/assertions.go @@ -3,13 +3,11 @@ package development import ( "fmt" - v1t "github.com/authzed/authzed-go/proto/authzed/api/v1" "github.com/ccoveille/go-safecast" log "github.com/authzed/spicedb/internal/logging" devinterface "github.com/authzed/spicedb/pkg/proto/developer/v1" v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" - "github.com/authzed/spicedb/pkg/tuple" "github.com/authzed/spicedb/pkg/validationfile/blocks" ) @@ -42,8 +40,6 @@ func runAssertions(devContext *DevContext, assertions []blocks.Assertion, expect var failures []*devinterface.DeveloperError for _, assertion := range assertions { - tpl := tuple.MustFromRelationship[*v1t.ObjectReference, *v1t.SubjectReference, *v1t.ContextualizedCaveat](assertion.Relationship) - // NOTE: zeroes are fine here to mean "unknown" lineNumber, err := safecast.ToUint32(assertion.SourcePosition.LineNumber) if err != nil { @@ -54,7 +50,8 @@ func runAssertions(devContext *DevContext, assertions []blocks.Assertion, expect log.Err(err).Msg("could not cast columnPosition to uint32") } - if tpl.Caveat != nil { + rel := assertion.Relationship + if rel.OptionalCaveat != nil { failures = append(failures, &devinterface.DeveloperError{ Message: fmt.Sprintf("cannot specify a caveat on an assertion: `%s`", assertion.RelationshipWithContextString), Source: devinterface.DeveloperError_ASSERTION, @@ -66,7 +63,7 @@ func runAssertions(devContext *DevContext, assertions []blocks.Assertion, expect continue } - cr, err := RunCheck(devContext, tpl.ResourceAndRelation, tpl.Subject, assertion.CaveatContext) + cr, err := RunCheck(devContext, rel.Resource, rel.Subject, assertion.CaveatContext) if err != nil { devErr, wireErr := DistinguishGraphError( devContext, diff --git a/pkg/development/check.go b/pkg/development/check.go index 73de32370f..6553f77242 100644 --- a/pkg/development/check.go +++ b/pkg/development/check.go @@ -5,8 +5,8 @@ import ( "github.com/authzed/spicedb/internal/graph/computed" v1 "github.com/authzed/spicedb/internal/services/v1" - core "github.com/authzed/spicedb/pkg/proto/core/v1" v1dispatch "github.com/authzed/spicedb/pkg/proto/dispatch/v1" + "github.com/authzed/spicedb/pkg/tuple" ) const defaultWasmDispatchChunkSize = 100 @@ -23,21 +23,18 @@ type CheckResult struct { // // Note that it is up to the caller to call DistinguishGraphError on the error // if they want to distinguish between user errors and internal errors. -func RunCheck(devContext *DevContext, resource *core.ObjectAndRelation, subject *core.ObjectAndRelation, caveatContext map[string]any) (CheckResult, error) { +func RunCheck(devContext *DevContext, resource tuple.ObjectAndRelation, subject tuple.ObjectAndRelation, caveatContext map[string]any) (CheckResult, error) { ctx := devContext.Ctx cr, meta, err := computed.ComputeCheck(ctx, devContext.Dispatcher, computed.CheckParameters{ - ResourceType: &core.RelationReference{ - Namespace: resource.Namespace, - Relation: resource.Relation, - }, + ResourceType: resource.RelationReference(), Subject: subject, CaveatContext: caveatContext, AtRevision: devContext.Revision, MaximumDepth: maxDispatchDepth, DebugOption: computed.TraceDebuggingEnabled, }, - resource.ObjectId, + resource.ObjectID, defaultWasmDispatchChunkSize, ) if err != nil { diff --git a/pkg/development/devcontext.go b/pkg/development/devcontext.go index a4fc92dec1..9b43b9f418 100644 --- a/pkg/development/devcontext.go +++ b/pkg/development/devcontext.go @@ -88,14 +88,45 @@ func newDevContextWithDatastore(ctx context.Context, requestContext *devinterfac if err != nil || len(inputErrors) > 0 { return err } + // Load the test relationships into the datastore. - inputErrors, err = loadTuples(ctx, requestContext.Relationships, rwt) - if err != nil || len(inputErrors) > 0 { - return err + relationships := make([]tuple.Relationship, 0, len(requestContext.Relationships)) + for _, rel := range requestContext.Relationships { + if err := rel.Validate(); err != nil { + inputErrors = append(inputErrors, &devinterface.DeveloperError{ + Message: err.Error(), + Source: devinterface.DeveloperError_RELATIONSHIP, + Kind: devinterface.DeveloperError_PARSE_ERROR, + Context: tuple.CoreRelationToString(rel), + }) + } + + convertedRel := tuple.FromCoreRelationTuple(rel) + if err := convertedRel.Validate(); err != nil { + tplString, serr := tuple.String(convertedRel) + if serr != nil { + return serr + } + + inputErrors = append(inputErrors, &devinterface.DeveloperError{ + Message: err.Error(), + Source: devinterface.DeveloperError_RELATIONSHIP, + Kind: devinterface.DeveloperError_PARSE_ERROR, + Context: tplString, + }) + } + + relationships = append(relationships, convertedRel) + } + + ie, lerr := loadsRels(ctx, relationships, rwt) + if len(ie) > 0 { + inputErrors = append(inputErrors, ie...) } - return nil + return lerr }) + if err != nil || len(inputErrors) > 0 { return nil, &devinterface.DeveloperErrors{InputErrors: inputErrors}, err } @@ -184,42 +215,30 @@ func (dc *DevContext) Dispose() { } } -func loadTuples(ctx context.Context, tuples []*core.RelationTuple, rwt datastore.ReadWriteTransaction) ([]*devinterface.DeveloperError, error) { - devErrors := make([]*devinterface.DeveloperError, 0, len(tuples)) - updates := make([]*core.RelationTupleUpdate, 0, len(tuples)) - for _, tpl := range tuples { - tplString, err := tuple.String(tpl) - if err != nil { - return nil, err - } +func loadsRels(ctx context.Context, rels []tuple.Relationship, rwt datastore.ReadWriteTransaction) ([]*devinterface.DeveloperError, error) { + devErrors := make([]*devinterface.DeveloperError, 0, len(rels)) + updates := make([]tuple.RelationshipUpdate, 0, len(rels)) + for _, rel := range rels { + if err := relationships.ValidateRelationshipsForCreateOrTouch(ctx, rwt, rel); err != nil { + relString, serr := tuple.String(rel) + if serr != nil { + return nil, serr + } - verr := tpl.Validate() - if verr != nil { - devErrors = append(devErrors, &devinterface.DeveloperError{ - Message: verr.Error(), - Source: devinterface.DeveloperError_RELATIONSHIP, - Kind: devinterface.DeveloperError_PARSE_ERROR, - Context: tplString, - }) - continue - } + devErr, wireErr := distinguishGraphError(ctx, err, devinterface.DeveloperError_RELATIONSHIP, 0, 0, relString) + if wireErr != nil { + return devErrors, wireErr + } - err = relationships.ValidateRelationshipsForCreateOrTouch(ctx, rwt, []*core.RelationTuple{tpl}) - if err != nil { - devErr, wireErr := distinguishGraphError(ctx, err, devinterface.DeveloperError_RELATIONSHIP, 0, 0, tplString) if devErr != nil { devErrors = append(devErrors, devErr) - continue } - - return devErrors, wireErr } - updates = append(updates, tuple.Touch(tpl)) + updates = append(updates, tuple.Touch(rel)) } err := rwt.WriteRelationships(ctx, updates) - return devErrors, err } diff --git a/pkg/development/development_test.go b/pkg/development/development_test.go index 7d28e07b9f..bb5b73ffa5 100644 --- a/pkg/development/development_test.go +++ b/pkg/development/development_test.go @@ -25,7 +25,7 @@ definition document { } `, Relationships: []*core.RelationTuple{ - tuple.MustParse("document:somedoc#viewer@user:someuser"), + tuple.MustParse("document:somedoc#viewer@user:someuser").ToCoreTuple(), }, }) @@ -36,7 +36,7 @@ definition document { AssertTrue: []blocks.Assertion{ { RelationshipWithContextString: "document:somedoc#viewer@user:someuser", - Relationship: tuple.MustToRelationship(tuple.MustParse("document:somedoc#viewer@user:someuser")), + Relationship: tuple.MustParse("document:somedoc#viewer@user:someuser"), }, }, } @@ -73,7 +73,7 @@ definition document { }) require.Error(t, err) - require.Contains(t, err.Error(), "invalid resource id") + require.ErrorContains(t, err, "invalid resource id; must match") } func TestDevelopmentCaveatedExpectedRels(t *testing.T) { @@ -91,7 +91,7 @@ definition document { } `, Relationships: []*core.RelationTuple{ - tuple.MustParse("document:somedoc#viewer@user:someuser[somecaveat]"), + tuple.MustParse("document:somedoc#viewer@user:someuser[somecaveat]").ToCoreTuple(), }, }) @@ -122,7 +122,7 @@ definition document { } `, Relationships: []*core.RelationTuple{ - tuple.MustParse("document:somedoc#viewer@user:someuser"), + tuple.MustParse("document:somedoc#viewer@user:someuser").ToCoreTuple(), }, }) diff --git a/pkg/development/validation.go b/pkg/development/validation.go index cb1b1375ef..99a4ff3fa2 100644 --- a/pkg/development/validation.go +++ b/pkg/development/validation.go @@ -24,13 +24,9 @@ func RunValidation(devContext *DevContext, validation *blocks.ParsedExpectedRela ctx := devContext.Ctx for onrKey, expectedSubjects := range validation.ValidationMap { - if onrKey.ObjectAndRelation == nil { - return nil, nil, fmt.Errorf("got nil ObjectAndRelation for key %s", onrKey.ObjectRelationString) - } - // Run a full recursive expansion over the ONR. er, derr := devContext.Dispatcher.DispatchExpand(ctx, &v1.DispatchExpandRequest{ - ResourceAndRelation: onrKey.ObjectAndRelation, + ResourceAndRelation: onrKey.ObjectAndRelation.ToCoreONR(), Metadata: &v1.ResolverMeta{ AtRevision: devContext.Revision.String(), DepthRemaining: maxDispatchDepth, @@ -72,7 +68,7 @@ func RunValidation(devContext *DevContext, validation *blocks.ParsedExpectedRela return membershipSet, nil, nil } -func wrapRelationships(onrStrings []string) []string { +func wrapResources(onrStrings []string) []string { wrapped := make([]string, 0, len(onrStrings)) for _, str := range onrStrings { wrapped = append(wrapped, "<"+str+">") @@ -128,18 +124,17 @@ func validateSubjects(onrKey blocks.ObjectRelation, fs developmentmembership.Fou continue } - foundRelationships := subject.Relationships() - // Verify that the relationships are the same. + foundParentResources := subject.ParentResources() expectedONRStrings := tuple.StringsONRs(expectedSubject.Resources) - foundONRStrings := tuple.StringsONRs(foundRelationships) + foundONRStrings := tuple.StringsONRs(foundParentResources) if !cmp.Equal(expectedONRStrings, foundONRStrings) { failures = append(failures, &devinterface.DeveloperError{ Message: fmt.Sprintf("For object and permission/relation `%s`, found different relationships for subject `%s`: Specified: `%s`, Computed: `%s`", tuple.StringONR(onr), tuple.StringONR(subjectWithExceptions.Subject.Subject), - strings.Join(wrapRelationships(expectedONRStrings), "/"), - strings.Join(wrapRelationships(foundONRStrings), "/"), + strings.Join(wrapResources(expectedONRStrings), "/"), + strings.Join(wrapResources(foundONRStrings), "/"), ), Source: devinterface.DeveloperError_VALIDATION_YAML, Kind: devinterface.DeveloperError_MISSING_EXPECTED_RELATIONSHIP, @@ -164,8 +159,8 @@ func validateSubjects(onrKey blocks.ObjectRelation, fs developmentmembership.Fou Message: fmt.Sprintf("For object and permission/relation `%s`, found different excluded subjects for subject `%s`: Specified: `%s`, Computed: `%s`", tuple.StringONR(onr), tuple.StringONR(subjectWithExceptions.Subject.Subject), - strings.Join(wrapRelationships(expectedExcludedStrings), ", "), - strings.Join(wrapRelationships(foundExcludedONRStrings), ", "), + strings.Join(wrapResources(expectedExcludedStrings), ", "), + strings.Join(wrapResources(foundExcludedONRStrings), ", "), ), Source: devinterface.DeveloperError_VALIDATION_YAML, Kind: devinterface.DeveloperError_MISSING_EXPECTED_RELATIONSHIP, @@ -253,7 +248,7 @@ func GenerateValidation(membershipSet *developmentmembership.Set) (string, error strs = append(strs, fmt.Sprintf("[%s] is %s", fs.ToValidationString(), - strings.Join(wrapRelationships(tuple.StringsONRs(fs.Relationships())), "/"), + strings.Join(wrapResources(tuple.StringsONRs(fs.ParentResources())), "/"), )) } diff --git a/pkg/development/wasm/operations.go b/pkg/development/wasm/operations.go index 630f16d345..88ce05ea5d 100644 --- a/pkg/development/wasm/operations.go +++ b/pkg/development/wasm/operations.go @@ -9,7 +9,6 @@ import ( "strings" "github.com/authzed/spicedb/pkg/development" - core "github.com/authzed/spicedb/pkg/proto/core/v1" devinterface "github.com/authzed/spicedb/pkg/proto/developer/v1" v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" "github.com/authzed/spicedb/pkg/schemadsl/generator" @@ -39,8 +38,8 @@ func runOperation(devContext *development.DevContext, operation *devinterface.Op cr, err := development.RunCheck( devContext, - operation.CheckParameters.Resource, - operation.CheckParameters.Subject, + tuple.FromCoreObjectAndRelation(operation.CheckParameters.Resource), + tuple.FromCoreObjectAndRelation(operation.CheckParameters.Subject), caveatContext, ) if err != nil { @@ -49,9 +48,11 @@ func runOperation(devContext *development.DevContext, operation *devinterface.Op err, devinterface.DeveloperError_CHECK_WATCH, 0, 0, - tuple.MustString(&core.RelationTuple{ - ResourceAndRelation: operation.CheckParameters.Resource, - Subject: operation.CheckParameters.Subject, + tuple.MustString(tuple.Relationship{ + RelationshipReference: tuple.RelationshipReference{ + Resource: tuple.FromCoreObjectAndRelation(operation.CheckParameters.Resource), + Subject: tuple.FromCoreObjectAndRelation(operation.CheckParameters.Subject), + }, }), ) if wireErr != nil { diff --git a/pkg/development/wasm/operations_test.go b/pkg/development/wasm/operations_test.go index 49856566e5..49e0bc4b44 100644 --- a/pkg/development/wasm/operations_test.go +++ b/pkg/development/wasm/operations_test.go @@ -9,14 +9,14 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/structpb" - core "github.com/authzed/spicedb/pkg/proto/core/v1" + corev1 "github.com/authzed/spicedb/pkg/proto/core/v1" devinterface "github.com/authzed/spicedb/pkg/proto/developer/v1" "github.com/authzed/spicedb/pkg/testutil" "github.com/authzed/spicedb/pkg/tuple" ) type editCheckResult struct { - Relationship *core.RelationTuple + Relationship tuple.Relationship IsMember bool Error *devinterface.DeveloperError IsConditional bool @@ -79,8 +79,8 @@ func TestCheckOperation(t *testing.T) { type testCase struct { name string schema string - relationships []*core.RelationTuple - checkRelationship *core.RelationTuple + relationships []tuple.Relationship + checkRelationship tuple.Relationship caveatContext map[string]any expectedError *devinterface.DeveloperError expectedResult *editCheckResult @@ -92,7 +92,7 @@ func TestCheckOperation(t *testing.T) { `def foo { relation bar: }`, - []*core.RelationTuple{}, + []tuple.Relationship{}, tuple.MustParse("somenamespace:someobj#anotherrel@user:foo"), nil, &devinterface.DeveloperError{ @@ -110,7 +110,7 @@ func TestCheckOperation(t *testing.T) { `definition foo { relation bar: }`, - []*core.RelationTuple{}, + []tuple.Relationship{}, tuple.MustParse("somenamespace:someobj#anotherrel@user:foo"), nil, &devinterface.DeveloperError{ @@ -126,7 +126,7 @@ func TestCheckOperation(t *testing.T) { { "invalid namespace name", `definition fo {}`, - []*core.RelationTuple{}, + []tuple.Relationship{}, tuple.MustParse("somenamespace:someobj#anotherrel@user:foo"), nil, &devinterface.DeveloperError{ @@ -146,7 +146,7 @@ func TestCheckOperation(t *testing.T) { relation writer: user permission writer = writer }`, - []*core.RelationTuple{}, + []tuple.Relationship{}, tuple.MustParse("somenamespace:someobj#anotherrel@user:foo"), nil, &devinterface.DeveloperError{ @@ -167,7 +167,7 @@ func TestCheckOperation(t *testing.T) { relation somerel: user } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("somenamespace:someobj#somerel@user:foo"), }, tuple.MustParse("somenamespace:someobj#anotherrel@user:foo"), @@ -191,7 +191,7 @@ func TestCheckOperation(t *testing.T) { relation somerel: user } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("somenamespace:someobj#somerel@user:foo"), }, tuple.MustParse("somenamespace:someobj#somerel@user:foo"), @@ -210,7 +210,7 @@ func TestCheckOperation(t *testing.T) { relation somerel: user } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("somenamespace:someobj#somerel@user:foo"), }, tuple.MustParse("somenamespace:someobj#somerel@user:bar"), @@ -229,7 +229,7 @@ func TestCheckOperation(t *testing.T) { relation somerel: user | user:* } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("somenamespace:someobj#somerel@user:*"), }, tuple.MustParse("somenamespace:someobj#somerel@user:foo"), @@ -248,7 +248,7 @@ func TestCheckOperation(t *testing.T) { permission empty = nil } `, - []*core.RelationTuple{}, + []tuple.Relationship{}, tuple.MustParse("somenamespace:someobj#empty@user:foo"), nil, nil, @@ -265,7 +265,7 @@ func TestCheckOperation(t *testing.T) { relation viewer: user | document#viewer } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:someobj#viewer@document:someobj#viewer"), }, tuple.MustParse("document:someobj#viewer@user:foo"), @@ -293,7 +293,7 @@ func TestCheckOperation(t *testing.T) { relation somerel: user with somecaveat } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("somenamespace:someobj#somerel@user:foo[somecaveat]"), }, tuple.MustParse("somenamespace:someobj#somerel@user:foo"), @@ -316,7 +316,7 @@ func TestCheckOperation(t *testing.T) { relation somerel: user with somecaveat } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("somenamespace:someobj#somerel@user:foo[somecaveat]"), }, tuple.MustParse("somenamespace:someobj#somerel@user:foo"), @@ -339,7 +339,7 @@ func TestCheckOperation(t *testing.T) { relation somerel: user with somecaveat } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("somenamespace:someobj#somerel@user:foo[somecaveat]"), }, tuple.MustParse("somenamespace:someobj#somerel@user:foo"), @@ -359,7 +359,7 @@ func TestCheckOperation(t *testing.T) { relation viewer: user permission view = viewer }`, - []*core.RelationTuple{tuple.MustParse("resource:someobj#viewer@resource:foo")}, + []tuple.Relationship{tuple.MustParse("resource:someobj#viewer@resource:foo")}, tuple.MustParse("resource:someobj#view@user:foo"), nil, &devinterface.DeveloperError{ @@ -382,7 +382,7 @@ func TestCheckOperation(t *testing.T) { relation viewer: user with somecaveat permission view = viewer }`, - []*core.RelationTuple{tuple.MustParse("resource:someobj#viewer@user:foo")}, + []tuple.Relationship{tuple.MustParse("resource:someobj#viewer@user:foo")}, tuple.MustParse("resource:someobj#view@user:foo"), nil, &devinterface.DeveloperError{ @@ -401,7 +401,7 @@ func TestCheckOperation(t *testing.T) { permission empty = nil } `, - []*core.RelationTuple{}, + []tuple.Relationship{}, tuple.MustParse("somenamespace:--=base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==#empty@user:veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryverylong"), nil, nil, @@ -421,16 +421,21 @@ func TestCheckOperation(t *testing.T) { caveatContext = cc } + relationships := make([]*corev1.RelationTuple, 0, len(tc.relationships)) + for _, rel := range tc.relationships { + relationships = append(relationships, rel.ToCoreTuple()) + } + response := run(t, &devinterface.DeveloperRequest{ Context: &devinterface.RequestContext{ Schema: tc.schema, - Relationships: tc.relationships, + Relationships: relationships, }, Operations: []*devinterface.Operation{ { CheckParameters: &devinterface.CheckOperationParameters{ - Resource: tc.checkRelationship.ResourceAndRelation, - Subject: tc.checkRelationship.Subject, + Resource: tc.checkRelationship.Resource.ToCoreONR(), + Subject: tc.checkRelationship.Subject.ToCoreONR(), CaveatContext: caveatContext, }, }, @@ -481,7 +486,7 @@ func TestRunAssertionsAndValidationOperations(t *testing.T) { type testCase struct { name string schema string - relationships []*core.RelationTuple + relationships []tuple.Relationship validationYaml string assertionsYaml string expectedError *devinterface.DeveloperError @@ -493,7 +498,7 @@ func TestRunAssertionsAndValidationOperations(t *testing.T) { { "valid namespace", `definition somenamespace {}`, - []*core.RelationTuple{}, + []tuple.Relationship{}, "", "", nil, @@ -503,7 +508,7 @@ func TestRunAssertionsAndValidationOperations(t *testing.T) { { "invalid validation yaml", `definition somenamespace {}`, - []*core.RelationTuple{}, + []tuple.Relationship{}, `asdkjhgasd`, "", &devinterface.DeveloperError{ @@ -519,7 +524,7 @@ func TestRunAssertionsAndValidationOperations(t *testing.T) { { "invalid assertions yaml", `definition somenamespace {}`, - []*core.RelationTuple{}, + []tuple.Relationship{}, "", `asdhasjdkhjasd`, &devinterface.DeveloperError{ @@ -535,7 +540,7 @@ func TestRunAssertionsAndValidationOperations(t *testing.T) { { "assertions yaml with garbage", `definition somenamespace {}`, - []*core.RelationTuple{}, + []tuple.Relationship{}, "", `assertTrue: - document:firstdoc#view@user:tom @@ -555,7 +560,7 @@ assertFalse: garbage { "assertions yaml with indented garbage", `definition somenamespace {}`, - []*core.RelationTuple{}, + []tuple.Relationship{}, "", `assertTrue: - document:firstdoc#view@user:tom @@ -577,12 +582,12 @@ assertFalse: garbage { "invalid assertions true yaml", `definition somenamespace {}`, - []*core.RelationTuple{}, + []tuple.Relationship{}, "", `assertTrue: - something`, &devinterface.DeveloperError{ - Message: "error parsing relationship in assertion `something`", + Message: "error parsing relationship in assertion `something`: invalid relationship string", Kind: devinterface.DeveloperError_PARSE_ERROR, Source: devinterface.DeveloperError_ASSERTION, Line: 2, @@ -600,7 +605,7 @@ assertFalse: garbage relation viewer: user } `, - []*core.RelationTuple{tuple.MustParse("document:somedoc#viewer@user:jimmy")}, + []tuple.Relationship{tuple.MustParse("document:somedoc#viewer@user:jimmy")}, "", `assertTrue: - document:somedoc#viewer@user:jake`, @@ -623,7 +628,7 @@ assertFalse: garbage relation viewer: user } `, - []*core.RelationTuple{tuple.MustParse("document:somedoc#viewer@user:jimmy")}, + []tuple.Relationship{tuple.MustParse("document:somedoc#viewer@user:jimmy")}, "", `assertFalse: - document:somedoc#viewer@user:jimmy`, @@ -644,7 +649,7 @@ assertFalse: garbage definition user {} definition document {} `, - []*core.RelationTuple{}, + []tuple.Relationship{}, "", `assertFalse: - document:somedoc#viewer@user:jimmy[somecaveat]`, @@ -665,7 +670,7 @@ assertFalse: garbage definition user {} definition document {} `, - []*core.RelationTuple{}, + []tuple.Relationship{}, "", `assertFalse: - document:somedoc#viewer@user:jimmy`, @@ -690,7 +695,7 @@ assertFalse: garbage permission view = viewer + writer } `, - []*core.RelationTuple{tuple.MustParse("document:somedoc#writer@user:jimmy")}, + []tuple.Relationship{tuple.MustParse("document:somedoc#writer@user:jimmy")}, `"document:somedoc#view":`, `assertTrue: - document:somedoc#view@user:jimmy`, @@ -717,7 +722,7 @@ assertFalse: garbage permission view = viewer + writer } `, - []*core.RelationTuple{tuple.MustParse("document:somedoc#writer@user:jimmy")}, + []tuple.Relationship{tuple.MustParse("document:somedoc#writer@user:jimmy")}, `"document:somedoc#view": - "[user:jimmy] is " - "[user:jake] is "`, @@ -746,13 +751,13 @@ assertFalse: garbage permission view = viewer + writer } `, - []*core.RelationTuple{tuple.MustParse("document:somedoc#writer@user:jimmy")}, + []tuple.Relationship{tuple.MustParse("document:somedoc#writer@user:jimmy")}, `"document:somedoc#view": - "[user] is "`, `assertTrue: - document:somedoc#view@user:jimmy`, &devinterface.DeveloperError{ - Message: "invalid subject: `user`", + Message: "invalid subject: `user`: invalid subject ONR: user", Kind: devinterface.DeveloperError_PARSE_ERROR, Source: devinterface.DeveloperError_VALIDATION_YAML, Context: "user", @@ -772,13 +777,13 @@ assertFalse: garbage permission view = viewer + writer } `, - []*core.RelationTuple{tuple.MustParse("document:somedoc#writer@user:jimmy")}, + []tuple.Relationship{tuple.MustParse("document:somedoc#writer@user:jimmy")}, `"document:somedoc#view": - "[user:jimmy] is "`, `assertTrue: - document:somedoc#view@user:jimmy`, &devinterface.DeveloperError{ - Message: "invalid resource and relation: `document:som`", + Message: "invalid resource and relation: `document:som`: invalid ONR: document:som", Kind: devinterface.DeveloperError_PARSE_ERROR, Source: devinterface.DeveloperError_VALIDATION_YAML, Context: "document:som", @@ -798,7 +803,7 @@ assertFalse: garbage permission view = viewer + writer } `, - []*core.RelationTuple{tuple.MustParse("document:somedoc#writer@user:jimmy")}, + []tuple.Relationship{tuple.MustParse("document:somedoc#writer@user:jimmy")}, `"document:somedoc#view": - "[user:jimmy] is "`, `assertTrue: @@ -831,7 +836,7 @@ assertFalse: garbage permission view = viewer + writer } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:somedoc#writer@user:jimmy"), tuple.MustParse("document:somedoc#viewer@user:jake"), tuple.MustParse("document:somedoc#viewer@user:sarah[testcaveat]"), @@ -879,7 +884,7 @@ assertFalse: permission view = viewer + writer } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:somedoc#writer@user:jimmy"), tuple.MustParse("document:somedoc#viewer@user:jimmy"), }, @@ -904,7 +909,7 @@ assertFalse: permission view = viewer + writer } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:somedoc#writer@user:jimmy"), tuple.MustParse("document:somedoc#viewer@user:jimmy"), }, @@ -931,7 +936,7 @@ assertFalse: ` definition user {} `, - []*core.RelationTuple{tuple.MustParse("document:somedoc#writer@user:jimmy")}, + []tuple.Relationship{tuple.MustParse("document:somedoc#writer@user:jimmy")}, ``, ``, &devinterface.DeveloperError{ @@ -949,7 +954,7 @@ assertFalse: definition user {} definition document {} `, - []*core.RelationTuple{tuple.MustParse("document:somedoc#writer@user:jimmy")}, + []tuple.Relationship{tuple.MustParse("document:somedoc#writer@user:jimmy")}, ``, ``, &devinterface.DeveloperError{ @@ -971,7 +976,7 @@ assertFalse: permission view = viewer + writer } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:somedoc#writer@user:jimmy"), tuple.MustParse("document:somedoc#viewer@user:*"), }, @@ -1001,7 +1006,7 @@ assertFalse: permission view = viewer - banned } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:somedoc#banned@user:jimmy"), tuple.MustParse("document:somedoc#viewer@user:*"), }, @@ -1027,7 +1032,7 @@ assertFalse: permission view = viewer - banned } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:somedoc#banned@user:jimmy"), tuple.MustParse("document:somedoc#banned@user:fred"), tuple.MustParse("document:somedoc#viewer@user:*"), @@ -1056,7 +1061,7 @@ assertFalse: permission view = (viewer - banned) & (viewer - other) } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:somedoc#other@user:sarah"), tuple.MustParse("document:somedoc#banned@user:jimmy"), tuple.MustParse("document:somedoc#viewer@user:*"), @@ -1084,7 +1089,7 @@ assertFalse: permission empty = nil } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:somedoc#viewer@user:jill"), tuple.MustParse("document:somedoc#viewer@user:tom"), }, @@ -1111,7 +1116,7 @@ assertFalse: permission view = viewer } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:somedoc#viewer@user:jill"), tuple.MustParse("document:somedoc#viewer@user:tom"), }, @@ -1147,7 +1152,7 @@ assertFalse: permission view = viewer } `, - []*core.RelationTuple{ + []tuple.Relationship{ tuple.MustParse("document:somedoc#viewer@user:sarah[testcaveat]"), }, `"document:somedoc#view": @@ -1179,10 +1184,15 @@ assertFalse: t.Run(tc.name, func(t *testing.T) { require := require.New(t) + relationships := make([]*corev1.RelationTuple, 0, len(tc.relationships)) + for _, rel := range tc.relationships { + relationships = append(relationships, rel.ToCoreTuple()) + } + response := run(t, &devinterface.DeveloperRequest{ Context: &devinterface.RequestContext{ Schema: tc.schema, - Relationships: tc.relationships, + Relationships: relationships, }, Operations: []*devinterface.Operation{ { diff --git a/pkg/graph/tree.go b/pkg/graph/tree.go index fc807857c6..f8fae34e54 100644 --- a/pkg/graph/tree.go +++ b/pkg/graph/tree.go @@ -2,26 +2,37 @@ package graph import ( core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/tuple" ) // Leaf constructs a RelationTupleTreeNode leaf. -func Leaf(start *core.ObjectAndRelation, subjects ...*core.DirectSubject) *core.RelationTupleTreeNode { +func Leaf(start *tuple.ObjectAndRelation, subjects ...*core.DirectSubject) *core.RelationTupleTreeNode { + var startONR *core.ObjectAndRelation + if start != nil { + startONR = start.ToCoreONR() + } + return &core.RelationTupleTreeNode{ NodeType: &core.RelationTupleTreeNode_LeafNode{ LeafNode: &core.DirectSubjects{ Subjects: subjects, }, }, - Expanded: start, + Expanded: startONR, CaveatExpression: nil, // Set by caller if necessary } } func setResult( op core.SetOperationUserset_Operation, - start *core.ObjectAndRelation, + start *tuple.ObjectAndRelation, children []*core.RelationTupleTreeNode, ) *core.RelationTupleTreeNode { + var startONR *core.ObjectAndRelation + if start != nil { + startONR = start.ToCoreONR() + } + return &core.RelationTupleTreeNode{ NodeType: &core.RelationTupleTreeNode_IntermediateNode{ IntermediateNode: &core.SetOperationUserset{ @@ -29,22 +40,22 @@ func setResult( ChildNodes: children, }, }, - Expanded: start, + Expanded: startONR, CaveatExpression: nil, // Set by caller if necessary } } // Union constructs a RelationTupleTreeNode union operation. -func Union(start *core.ObjectAndRelation, children ...*core.RelationTupleTreeNode) *core.RelationTupleTreeNode { +func Union(start *tuple.ObjectAndRelation, children ...*core.RelationTupleTreeNode) *core.RelationTupleTreeNode { return setResult(core.SetOperationUserset_UNION, start, children) } // Intersection constructs a RelationTupleTreeNode intersection operation. -func Intersection(start *core.ObjectAndRelation, children ...*core.RelationTupleTreeNode) *core.RelationTupleTreeNode { +func Intersection(start *tuple.ObjectAndRelation, children ...*core.RelationTupleTreeNode) *core.RelationTupleTreeNode { return setResult(core.SetOperationUserset_INTERSECTION, start, children) } // Exclusion constructs a RelationTupleTreeNode exclusion operation. -func Exclusion(start *core.ObjectAndRelation, children ...*core.RelationTupleTreeNode) *core.RelationTupleTreeNode { +func Exclusion(start *tuple.ObjectAndRelation, children ...*core.RelationTupleTreeNode) *core.RelationTupleTreeNode { return setResult(core.SetOperationUserset_EXCLUSION, start, children) } diff --git a/pkg/proto/dispatch/v1/00_zerolog.go b/pkg/proto/dispatch/v1/00_zerolog.go index cbd71671a2..7e77a8e73d 100644 --- a/pkg/proto/dispatch/v1/00_zerolog.go +++ b/pkg/proto/dispatch/v1/00_zerolog.go @@ -9,8 +9,8 @@ import ( // MarshalZerologObject implements zerolog object marshalling. func (cr *DispatchCheckRequest) MarshalZerologObject(e *zerolog.Event) { e.Object("metadata", cr.Metadata) - e.Str("resource-type", tuple.StringRR(cr.ResourceRelation)) - e.Str("subject", tuple.StringONR(cr.Subject)) + e.Str("resource-type", tuple.StringCoreRR(cr.ResourceRelation)) + e.Str("subject", tuple.StringCoreONR(cr.Subject)) e.Array("resource-ids", strArray(cr.ResourceIds)) } @@ -28,7 +28,7 @@ func (cr *DispatchCheckResponse) MarshalZerologObject(e *zerolog.Event) { // MarshalZerologObject implements zerolog object marshalling. func (er *DispatchExpandRequest) MarshalZerologObject(e *zerolog.Event) { e.Object("metadata", er.Metadata) - e.Str("expand", tuple.StringONR(er.ResourceAndRelation)) + e.Str("expand", tuple.StringCoreONR(er.ResourceAndRelation)) e.Stringer("mode", er.ExpansionMode) } @@ -40,16 +40,16 @@ func (cr *DispatchExpandResponse) MarshalZerologObject(e *zerolog.Event) { // MarshalZerologObject implements zerolog object marshalling. func (lr *DispatchLookupResourcesRequest) MarshalZerologObject(e *zerolog.Event) { e.Object("metadata", lr.Metadata) - e.Str("object", tuple.StringRR(lr.ObjectRelation)) - e.Str("subject", tuple.StringONR(lr.Subject)) + e.Str("object", tuple.StringCoreRR(lr.ObjectRelation)) + e.Str("subject", tuple.StringCoreONR(lr.Subject)) e.Interface("context", lr.Context) } // MarshalZerologObject implements zerolog object marshalling. func (lr *DispatchLookupResources2Request) MarshalZerologObject(e *zerolog.Event) { e.Object("metadata", lr.Metadata) - e.Str("resource", tuple.StringRR(lr.ResourceRelation)) - e.Str("subject", tuple.StringRR(lr.SubjectRelation)) + e.Str("resource", tuple.StringCoreRR(lr.ResourceRelation)) + e.Str("subject", tuple.StringCoreRR(lr.SubjectRelation)) e.Array("subject-ids", strArray(lr.SubjectIds)) e.Interface("context", lr.Context) } @@ -57,16 +57,16 @@ func (lr *DispatchLookupResources2Request) MarshalZerologObject(e *zerolog.Event // MarshalZerologObject implements zerolog object marshalling. func (lr *DispatchReachableResourcesRequest) MarshalZerologObject(e *zerolog.Event) { e.Object("metadata", lr.Metadata) - e.Str("resource-type", tuple.StringRR(lr.ResourceRelation)) - e.Str("subject-type", tuple.StringRR(lr.SubjectRelation)) + e.Str("resource-type", tuple.StringCoreRR(lr.ResourceRelation)) + e.Str("subject-type", tuple.StringCoreRR(lr.SubjectRelation)) e.Array("subject-ids", strArray(lr.SubjectIds)) } // MarshalZerologObject implements zerolog object marshalling. func (ls *DispatchLookupSubjectsRequest) MarshalZerologObject(e *zerolog.Event) { e.Object("metadata", ls.Metadata) - e.Str("resource-type", tuple.StringRR(ls.ResourceRelation)) - e.Str("subject-type", tuple.StringRR(ls.SubjectRelation)) + e.Str("resource-type", tuple.StringCoreRR(ls.ResourceRelation)) + e.Str("subject-type", tuple.StringCoreRR(ls.SubjectRelation)) e.Array("resource-ids", strArray(ls.ResourceIds)) } diff --git a/pkg/spiceerrors/assert_off.go b/pkg/spiceerrors/assert_off.go index d537c77625..fa0ac4731e 100644 --- a/pkg/spiceerrors/assert_off.go +++ b/pkg/spiceerrors/assert_off.go @@ -8,6 +8,11 @@ func DebugAssert(condition func() bool, format string, args ...any) { // Do nothing on purpose } +// DebugAssertNotNil is a no-op in non-CI builds +func DebugAssertNotNil(obj any, format string, args ...any) { + // Do nothing on purpose +} + // SetFinalizerForDebugging is a no-op in non-CI builds func SetFinalizerForDebugging[T any](obj interface{}, finalizer func(obj T)) { // Do nothing on purpose diff --git a/pkg/spiceerrors/assert_on.go b/pkg/spiceerrors/assert_on.go index d64ec7d333..b71f8de614 100644 --- a/pkg/spiceerrors/assert_on.go +++ b/pkg/spiceerrors/assert_on.go @@ -15,6 +15,13 @@ func DebugAssert(condition func() bool, format string, args ...any) { } } +// DebugAssertNotNil panics if the object is nil in CI builds. +func DebugAssertNotNil(obj any, format string, args ...any) { + if obj == nil { + panic(fmt.Sprintf(format, args...)) + } +} + // SetFinalizerForDebugging sets a finalizer on the object for debugging purposes // in CI builds. func SetFinalizerForDebugging[T any](obj interface{}, finalizer func(obj T)) { diff --git a/pkg/tuple/comparison.go b/pkg/tuple/comparison.go new file mode 100644 index 0000000000..4847855d03 --- /dev/null +++ b/pkg/tuple/comparison.go @@ -0,0 +1,34 @@ +package tuple + +import ( + "google.golang.org/protobuf/proto" + + core "github.com/authzed/spicedb/pkg/proto/core/v1" +) + +// ONREqual checks if two ObjectAndRelation instances are equal. +func ONREqual(lhs, rhs ObjectAndRelation) bool { + return lhs == rhs +} + +// ONREqualOrWildcard checks if an ObjectAndRelation matches another ObjectAndRelation or is a wildcard. +func ONREqualOrWildcard(onr, target ObjectAndRelation) bool { + return ONREqual(onr, target) || (onr.ObjectID == PublicWildcard && onr.ObjectType == target.ObjectType) +} + +// Equal returns true if the two relationships are exactly the same. +func Equal(lhs, rhs Relationship) bool { + return ONREqual(lhs.Resource, rhs.Resource) && ONREqual(lhs.Subject, rhs.Subject) && caveatEqual(lhs.OptionalCaveat, rhs.OptionalCaveat) +} + +func caveatEqual(lhs, rhs *core.ContextualizedCaveat) bool { + if lhs == nil && rhs == nil { + return true + } + + if lhs == nil || rhs == nil { + return false + } + + return lhs.CaveatName == rhs.CaveatName && proto.Equal(lhs.Context, rhs.Context) +} diff --git a/pkg/tuple/comparison_test.go b/pkg/tuple/comparison_test.go new file mode 100644 index 0000000000..53d3144219 --- /dev/null +++ b/pkg/tuple/comparison_test.go @@ -0,0 +1,252 @@ +package tuple + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestONREqual(t *testing.T) { + tests := []struct { + name string + lhs ObjectAndRelation + rhs ObjectAndRelation + want bool + }{ + { + name: "equal", + lhs: ObjectAndRelation{"testns", "testobj", "testrel"}, + rhs: ObjectAndRelation{"testns", "testobj", "testrel"}, + want: true, + }, + { + name: "different object type", + lhs: ObjectAndRelation{"testns1", "testobj", "testrel"}, + rhs: ObjectAndRelation{"testns2", "testobj", "testrel"}, + want: false, + }, + { + name: "different object id", + lhs: ObjectAndRelation{"testns", "testobj1", "testrel"}, + rhs: ObjectAndRelation{"testns", "testobj2", "testrel"}, + want: false, + }, + { + name: "different relation", + lhs: ObjectAndRelation{"testns", "testobj", "testrel1"}, + rhs: ObjectAndRelation{"testns", "testobj", "testrel2"}, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ONREqual(tt.lhs, tt.rhs) + require.Equal(t, tt.want, got) + }) + } +} + +func TestEqual(t *testing.T) { + equalTestCases := []Relationship{ + makeRel( + StringToONR("testns", "testobj", "testrel"), + StringToONR("user", "testusr", "..."), + ), + MustWithCaveat( + makeRel( + StringToONR("testns", "testobj", "testrel"), + StringToONR("user", "testusr", "..."), + ), + "somecaveat", + map[string]any{ + "context": map[string]any{ + "deeply": map[string]any{ + "nested": true, + }, + }, + }, + ), + MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":\"there\"}]"), + MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":{\"yo\":123}}]"), + MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":{\"yo\":{\"hey\":true}}, \"hi2\":{\"yo2\":{\"hey2\":false}}}]"), + MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":{\"yo\":{\"hey\":true}}, \"hi2\":{\"yo2\":{\"hey2\":[1,2,3]}}}]"), + } + + for _, tc := range equalTestCases { + t.Run(MustString(tc), func(t *testing.T) { + require := require.New(t) + require.True(Equal(tc, MustParse(MustString(tc)))) + }) + } + + notEqualTestCases := []struct { + lhs Relationship + rhs Relationship + name string + }{ + { + name: "Mismatch Resource Type", + lhs: makeRel( + StringToONR("testns1", "testobj", "testrel"), + StringToONR("user", "testusr", "..."), + ), + rhs: makeRel( + StringToONR("testns2", "testobj", "testrel"), + StringToONR("user", "testusr", "..."), + ), + }, + { + name: "Mismatch Resource ID", + lhs: makeRel( + StringToONR("testns", "testobj1", "testrel"), + StringToONR("user", "testusr", "..."), + ), + rhs: makeRel( + StringToONR("testns", "testobj2", "testrel"), + StringToONR("user", "testusr", "..."), + ), + }, + { + name: "Mismatch Resource Relationship", + lhs: makeRel( + StringToONR("testns", "testobj", "testrel1"), + StringToONR("user", "testusr", "..."), + ), + rhs: makeRel( + StringToONR("testns", "testobj", "testrel2"), + StringToONR("user", "testusr", "..."), + ), + }, + { + name: "Mismatch Subject Type", + lhs: makeRel( + StringToONR("testns", "testobj", "testrel"), + StringToONR("user1", "testusr", "..."), + ), + rhs: makeRel( + StringToONR("testns", "testobj", "testrel"), + StringToONR("user2", "testusr", "..."), + ), + }, + { + name: "Mismatch Subject ID", + lhs: makeRel( + StringToONR("testns", "testobj", "testrel"), + StringToONR("user", "testusr1", "..."), + ), + rhs: makeRel( + StringToONR("testns", "testobj", "testrel"), + StringToONR("user", "testusr2", "..."), + ), + }, + { + name: "Mismatch Subject Relationship", + lhs: makeRel( + StringToONR("testns", "testobj", "testrel"), + StringToONR("user", "testusr", "testrel1"), + ), + rhs: makeRel( + StringToONR("testns", "testobj", "testrel"), + StringToONR("user", "testusr", "testrel2"), + ), + }, + { + name: "Mismatch Caveat Name", + lhs: MustWithCaveat( + makeRel( + StringToONR("testns", "testobj", "testrel"), + StringToONR("user", "testusr", "..."), + ), + "somecaveat1", + map[string]any{ + "context": map[string]any{ + "deeply": map[string]any{ + "nested": true, + }, + }, + }, + ), + rhs: MustWithCaveat( + makeRel( + StringToONR("testns", "testobj", "testrel"), + StringToONR("user", "testusr", "..."), + ), + "somecaveat2", + map[string]any{ + "context": map[string]any{ + "deeply": map[string]any{ + "nested": true, + }, + }, + }, + ), + }, + { + name: "Mismatch Caveat Content", + lhs: MustWithCaveat( + makeRel( + StringToONR("testns", "testobj", "testrel"), + StringToONR("user", "testusr", "..."), + ), + "somecaveat", + map[string]any{ + "context": map[string]any{ + "deeply": map[string]any{ + "nested": "1", + }, + }, + }, + ), + rhs: MustWithCaveat( + makeRel( + StringToONR("testns", "testobj", "testrel"), + StringToONR("user", "testusr", "..."), + ), + "somecaveat", + map[string]any{ + "context": map[string]any{ + "deeply": map[string]any{ + "nested": "2", + }, + }, + }, + ), + }, + { + name: "missing caveat context via string", + lhs: MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":\"there\"}]"), + rhs: MustParse("document:foo#viewer@user:tom[somecaveat]"), + }, + { + name: "mismatch caveat context via string", + lhs: MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":\"there\"}]"), + rhs: MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":\"there2\"}]"), + }, + { + name: "mismatch caveat name", + lhs: MustParse("document:foo#viewer@user:tom[somecaveat]"), + rhs: MustParse("document:foo#viewer@user:tom[somecaveat2]"), + }, + { + name: "mismatch caveat context, deeply nested", + lhs: MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":{\"yo\":123}}]"), + rhs: MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":{\"yo\":124}}]"), + }, + { + name: "mismatch caveat context, deeply nested with array", + lhs: MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":{\"yo\":[1,2,3]}}]"), + rhs: MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":{\"yo\":[1,2,4]}}]"), + }, + } + + for _, tc := range notEqualTestCases { + t.Run(tc.name, func(t *testing.T) { + require := require.New(t) + require.False(Equal(tc.lhs, tc.rhs)) + require.False(Equal(tc.rhs, tc.lhs)) + require.False(Equal(tc.lhs, MustParse(MustString(tc.rhs)))) + require.False(Equal(tc.rhs, MustParse(MustString(tc.lhs)))) + }) + } +} diff --git a/pkg/tuple/core.go b/pkg/tuple/core.go new file mode 100644 index 0000000000..c81997aee1 --- /dev/null +++ b/pkg/tuple/core.go @@ -0,0 +1,113 @@ +package tuple + +import ( + core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/spiceerrors" +) + +// ONRStringToCore creates an ONR from string pieces. +func ONRStringToCore(ns, oid, rel string) *core.ObjectAndRelation { + spiceerrors.DebugAssert(func() bool { + return ns != "" && oid != "" && rel != "" + }, "namespace, object ID, and relation must not be empty") + + return &core.ObjectAndRelation{ + Namespace: ns, + ObjectId: oid, + Relation: rel, + } +} + +// CoreRelationToString creates a string from a core.RelationTuple. +func CoreRelationToString(rel *core.RelationTuple) string { + if rel.Subject.Relation == Ellipsis { + return rel.ResourceAndRelation.Namespace + ":" + rel.ResourceAndRelation.ObjectId + "@" + rel.Subject.Namespace + ":" + rel.Subject.ObjectId + } + + return rel.ResourceAndRelation.Namespace + ":" + rel.ResourceAndRelation.ObjectId + "@" + rel.Subject.Namespace + ":" + rel.Subject.ObjectId + "#" + rel.ResourceAndRelation.Relation +} + +// RRStringToCore creates a RelationReference from the string pieces. +func RRStringToCore(namespaceName string, relationName string) *core.RelationReference { + spiceerrors.DebugAssert(func() bool { + return namespaceName != "" && relationName != "" + }, "namespace and relation must not be empty") + + return &core.RelationReference{ + Namespace: namespaceName, + Relation: relationName, + } +} + +// FromCoreRelationTuple creates a Relationship from a core.RelationTuple. +func FromCoreRelationTuple(rt *core.RelationTuple) Relationship { + spiceerrors.DebugAssert(func() bool { + return rt.Validate() == nil + }, "relation tuple must be valid") + + return Relationship{ + RelationshipReference: RelationshipReference{ + Resource: ObjectAndRelation{ + ObjectType: rt.ResourceAndRelation.Namespace, + ObjectID: rt.ResourceAndRelation.ObjectId, + Relation: rt.ResourceAndRelation.Relation, + }, + Subject: ObjectAndRelation{ + ObjectType: rt.Subject.Namespace, + ObjectID: rt.Subject.ObjectId, + Relation: rt.Subject.Relation, + }, + }, + OptionalCaveat: rt.Caveat, + } +} + +// FromCoreObjectAndRelation creates an ObjectAndRelation from a core.ObjectAndRelation. +func FromCoreObjectAndRelation(oar *core.ObjectAndRelation) ObjectAndRelation { + spiceerrors.DebugAssert(func() bool { + return oar.Validate() == nil + }, "object and relation must be valid") + + return ObjectAndRelation{ + ObjectType: oar.Namespace, + ObjectID: oar.ObjectId, + Relation: oar.Relation, + } +} + +// CoreONR creates a core ObjectAndRelation from the string pieces. +func CoreONR(namespace, objectID, relation string) *core.ObjectAndRelation { + spiceerrors.DebugAssert(func() bool { + return namespace != "" && objectID != "" && relation != "" + }, "namespace, object ID, and relation must not be empty") + + return &core.ObjectAndRelation{ + Namespace: namespace, + ObjectId: objectID, + Relation: relation, + } +} + +// CoreRR creates a core RelationReference from the string pieces. +func CoreRR(namespace, relation string) *core.RelationReference { + spiceerrors.DebugAssert(func() bool { + return namespace != "" && relation != "" + }, "namespace and relation must not be empty") + + return &core.RelationReference{ + Namespace: namespace, + Relation: relation, + } +} + +// FromCoreRelationshipReference creates a RelationshipReference from a core.RelationshipReference. +func FromCoreRelationReference(rr *core.RelationReference) RelationReference { + spiceerrors.DebugAssert(func() bool { + return rr.Validate() == nil + }, "relation reference must be valid") + + return RelationReference{ + ObjectType: rr.Namespace, + Relation: rr.Relation, + } +} diff --git a/pkg/tuple/core_test.go b/pkg/tuple/core_test.go new file mode 100644 index 0000000000..3b2bf9986d --- /dev/null +++ b/pkg/tuple/core_test.go @@ -0,0 +1,64 @@ +package tuple + +import ( + "testing" + + "github.com/stretchr/testify/require" + + core "github.com/authzed/spicedb/pkg/proto/core/v1" +) + +func TestONRStringToCore(t *testing.T) { + tests := []struct { + expected *core.ObjectAndRelation + name string + ns string + oid string + rel string + }{ + { + name: "basic", + ns: "testns", + oid: "testobj", + rel: "testrel", + expected: &core.ObjectAndRelation{ + Namespace: "testns", + ObjectId: "testobj", + Relation: "testrel", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ONRStringToCore(tt.ns, tt.oid, tt.rel) + require.Equal(t, tt.expected, got) + }) + } +} + +func TestRelationReference(t *testing.T) { + tests := []struct { + expected *core.RelationReference + name string + ns string + rel string + }{ + { + name: "basic", + ns: "testns", + rel: "testrel", + expected: &core.RelationReference{ + Namespace: "testns", + Relation: "testrel", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := RRStringToCore(tt.ns, tt.rel) + require.Equal(t, tt.expected, got) + }) + } +} diff --git a/pkg/tuple/hashing.go b/pkg/tuple/hashing.go new file mode 100644 index 0000000000..c6c8ebf727 --- /dev/null +++ b/pkg/tuple/hashing.go @@ -0,0 +1,101 @@ +package tuple + +import ( + "bytes" + "fmt" + "sort" + + "google.golang.org/protobuf/types/known/structpb" + + "github.com/authzed/spicedb/pkg/spiceerrors" +) + +// CanonicalBytes converts a tuple to a canonical set of bytes. +// Can be used for hashing purposes. +func CanonicalBytes(rel Relationship) ([]byte, error) { + spiceerrors.DebugAssert(rel.ValidateNotEmpty, "relationship must not be empty") + + var sb bytes.Buffer + sb.WriteString(rel.Resource.ObjectType) + sb.WriteString(":") + sb.WriteString(rel.Resource.ObjectID) + sb.WriteString("#") + sb.WriteString(rel.Resource.Relation) + sb.WriteString("@") + sb.WriteString(rel.Subject.ObjectType) + sb.WriteString(":") + sb.WriteString(rel.Subject.ObjectID) + sb.WriteString("#") + sb.WriteString(rel.Subject.Relation) + + if rel.OptionalCaveat != nil && rel.OptionalCaveat.CaveatName != "" { + sb.WriteString(" with ") + sb.WriteString(rel.OptionalCaveat.CaveatName) + + if rel.OptionalCaveat.Context != nil && len(rel.OptionalCaveat.Context.Fields) > 0 { + sb.WriteString(":") + if err := writeCanonicalContext(&sb, rel.OptionalCaveat.Context); err != nil { + return nil, err + } + } + } + + return sb.Bytes(), nil +} + +func writeCanonicalContext(sb *bytes.Buffer, context *structpb.Struct) error { + sb.WriteString("{") + for i, key := range sortedContextKeys(context.Fields) { + if i > 0 { + sb.WriteString(",") + } + sb.WriteString(key) + sb.WriteString(":") + if err := writeCanonicalContextValue(sb, context.Fields[key]); err != nil { + return err + } + } + sb.WriteString("}") + return nil +} + +func writeCanonicalContextValue(sb *bytes.Buffer, value *structpb.Value) error { + switch value.Kind.(type) { + case *structpb.Value_NullValue: + sb.WriteString("null") + case *structpb.Value_NumberValue: + sb.WriteString(fmt.Sprintf("%f", value.GetNumberValue())) + case *structpb.Value_StringValue: + sb.WriteString(value.GetStringValue()) + case *structpb.Value_BoolValue: + sb.WriteString(fmt.Sprintf("%t", value.GetBoolValue())) + case *structpb.Value_StructValue: + if err := writeCanonicalContext(sb, value.GetStructValue()); err != nil { + return err + } + case *structpb.Value_ListValue: + sb.WriteString("[") + for i, elem := range value.GetListValue().Values { + if i > 0 { + sb.WriteString(",") + } + if err := writeCanonicalContextValue(sb, elem); err != nil { + return err + } + } + sb.WriteString("]") + default: + return spiceerrors.MustBugf("unknown structpb.Value type: %T", value.Kind) + } + + return nil +} + +func sortedContextKeys(fields map[string]*structpb.Value) []string { + keys := make([]string, 0, len(fields)) + for key := range fields { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} diff --git a/pkg/tuple/hashing_test.go b/pkg/tuple/hashing_test.go new file mode 100644 index 0000000000..180f6749c9 --- /dev/null +++ b/pkg/tuple/hashing_test.go @@ -0,0 +1,53 @@ +package tuple + +import ( + b64 "encoding/base64" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCanonicalBytes(t *testing.T) { + foundBytes := make(map[string]string) + + for _, tc := range testCases { + tc := tc + if tc.relFormat.Resource.ObjectType == "" { + continue + } + + t.Run(tc.input, func(t *testing.T) { + // Ensure the serialization is stable. + serialized, err := CanonicalBytes(tc.relFormat) + require.NoError(t, err) + + encoded := b64.StdEncoding.EncodeToString(serialized) + require.Equal(t, tc.stableCanonicalization, encoded) + + // Ensure the serialization is unique. + existing, ok := foundBytes[string(serialized)] + if ok { + parsedInput := MustParse(tc.input) + parsedExisting := MustParse(existing) + require.True(t, Equal(parsedExisting, parsedInput), "duplicate canonical bytes found. input: %s; found for input: %s", tc.input, existing) + } + foundBytes[string(serialized)] = tc.input + }) + } +} + +func BenchmarkCanonicalBytes(b *testing.B) { + for _, tc := range testCases { + tc := tc + if tc.relFormat.Resource.ObjectType == "" { + continue + } + + b.Run(tc.input, func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := CanonicalBytes(tc.relFormat) + require.NoError(b, err) + } + }) + } +} diff --git a/pkg/tuple/onr.go b/pkg/tuple/onr.go index 2854e8bdcc..83ad51287b 100644 --- a/pkg/tuple/onr.go +++ b/pkg/tuple/onr.go @@ -1,124 +1,75 @@ package tuple import ( + "fmt" + "regexp" "slices" - "sort" - "strings" - - core "github.com/authzed/spicedb/pkg/proto/core/v1" ) -// ObjectAndRelation creates an ONR from string pieces. -func ObjectAndRelation(ns, oid, rel string) *core.ObjectAndRelation { - return &core.ObjectAndRelation{ - Namespace: ns, - ObjectId: oid, - Relation: rel, - } -} +var ( + onrRegex = regexp.MustCompile(fmt.Sprintf("^%s$", onrExpr)) + subjectRegex = regexp.MustCompile(fmt.Sprintf("^%s$", subjectExpr)) +) -// RelationReference creates a RelationReference from the string pieces. -func RelationReference(namespaceName string, relationName string) *core.RelationReference { - return &core.RelationReference{ - Namespace: namespaceName, - Relation: relationName, - } -} +var ( + onrSubjectRelIndex = slices.Index(subjectRegex.SubexpNames(), "subjectRel") + onrSubjectTypeIndex = slices.Index(subjectRegex.SubexpNames(), "subjectType") + onrSubjectIDIndex = slices.Index(subjectRegex.SubexpNames(), "subjectID") + onrResourceTypeIndex = slices.Index(onrRegex.SubexpNames(), "resourceType") + onrResourceIDIndex = slices.Index(onrRegex.SubexpNames(), "resourceID") + onrResourceRelIndex = slices.Index(onrRegex.SubexpNames(), "resourceRel") +) -// ParseSubjectONR converts a string representation of a Subject ONR to a proto object. Unlike +// ParseSubjectONR converts a string representation of a Subject ONR to an ObjectAndRelation. Unlike // ParseONR, this method allows for objects without relations. If an object without a relation // is given, the relation will be set to ellipsis. -func ParseSubjectONR(subjectOnr string) *core.ObjectAndRelation { +func ParseSubjectONR(subjectOnr string) (ObjectAndRelation, error) { groups := subjectRegex.FindStringSubmatch(subjectOnr) - if len(groups) == 0 { - return nil + return ObjectAndRelation{}, fmt.Errorf("invalid subject ONR: %s", subjectOnr) } relation := Ellipsis - subjectRelIndex := slices.Index(subjectRegex.SubexpNames(), "subjectRel") - if len(groups[subjectRelIndex]) > 0 { - relation = groups[subjectRelIndex] - } - - return &core.ObjectAndRelation{ - Namespace: groups[slices.Index(subjectRegex.SubexpNames(), "subjectType")], - ObjectId: groups[slices.Index(subjectRegex.SubexpNames(), "subjectID")], - Relation: relation, - } -} - -// ParseONR converts a string representation of an ONR to a proto object. -func ParseONR(onr string) *core.ObjectAndRelation { - groups := onrRegex.FindStringSubmatch(onr) - - if len(groups) == 0 { - return nil + if len(groups[onrSubjectRelIndex]) > 0 { + relation = groups[onrSubjectRelIndex] } - return &core.ObjectAndRelation{ - Namespace: groups[slices.Index(onrRegex.SubexpNames(), "resourceType")], - ObjectId: groups[slices.Index(onrRegex.SubexpNames(), "resourceID")], - Relation: groups[slices.Index(onrRegex.SubexpNames(), "resourceRel")], - } -} - -// JoinRelRef joins the namespace and relation together into the same -// format as `StringRR()`. -func JoinRelRef(namespace, relation string) string { return namespace + "#" + relation } - -// MustSplitRelRef splits a string produced by `JoinRelRef()` and panics if -// it fails. -func MustSplitRelRef(relRef string) (namespace, relation string) { - var ok bool - namespace, relation, ok = strings.Cut(relRef, "#") - if !ok { - panic("improperly formatted relation reference") - } - return + return ObjectAndRelation{ + ObjectType: groups[onrSubjectTypeIndex], + ObjectID: groups[onrSubjectIDIndex], + Relation: relation, + }, nil } -// StringRR converts a RR object to a string. -func StringRR(rr *core.RelationReference) string { - if rr == nil { - return "" +// MustParseSubjectONR converts a string representation of a Subject ONR to an ObjectAndRelation. +// Panics on error. +func MustParseSubjectONR(subjectOnr string) ObjectAndRelation { + parsed, err := ParseSubjectONR(subjectOnr) + if err != nil { + panic(err) } - - return JoinRelRef(rr.Namespace, rr.Relation) + return parsed } -// StringONR converts an ONR object to a string. -func StringONR(onr *core.ObjectAndRelation) string { - if onr == nil { - return "" +// ParseONR converts a string representation of an ONR to an ObjectAndRelation object. +func ParseONR(onr string) (ObjectAndRelation, error) { + groups := onrRegex.FindStringSubmatch(onr) + if len(groups) == 0 { + return ObjectAndRelation{}, fmt.Errorf("invalid ONR: %s", onr) } - return StringONRStrings(onr.Namespace, onr.ObjectId, onr.Relation) + return ObjectAndRelation{ + ObjectType: groups[onrResourceTypeIndex], + ObjectID: groups[onrResourceIDIndex], + Relation: groups[onrResourceRelIndex], + }, nil } -func StringONRStrings(namespace, objectID, relation string) string { - if relation == Ellipsis { - return JoinObjectRef(namespace, objectID) +// MustParseONR converts a string representation of an ONR to an ObjectAndRelation object. Panics on error. +func MustParseONR(onr string) ObjectAndRelation { + parsed, err := ParseONR(onr) + if err != nil { + panic(err) } - return JoinRelRef(JoinObjectRef(namespace, objectID), relation) -} - -// StringsONRs converts ONR objects to a string slice, sorted. -func StringsONRs(onrs []*core.ObjectAndRelation) []string { - onrstrings := make([]string, 0, len(onrs)) - for _, onr := range onrs { - onrstrings = append(onrstrings, StringONR(onr)) - } - - sort.Strings(onrstrings) - return onrstrings -} - -func OnrEqual(lhs, rhs *core.ObjectAndRelation) bool { - // Properties are sorted by highest to lowest cardinality to optimize for short-circuiting. - return lhs.ObjectId == rhs.ObjectId && lhs.Relation == rhs.Relation && lhs.Namespace == rhs.Namespace -} - -func OnrEqualOrWildcard(tpl, target *core.ObjectAndRelation) bool { - return OnrEqual(tpl, target) || (tpl.ObjectId == PublicWildcard && tpl.Namespace == target.Namespace) + return parsed } diff --git a/pkg/tuple/onr_test.go b/pkg/tuple/onr_test.go index 74cb2e12f8..a92e9310af 100644 --- a/pkg/tuple/onr_test.go +++ b/pkg/tuple/onr_test.go @@ -3,49 +3,43 @@ package tuple import ( "testing" - core "github.com/authzed/spicedb/pkg/proto/core/v1" - "github.com/stretchr/testify/require" ) var onrTestCases = []struct { serialized string - objectFormat *core.ObjectAndRelation + objectFormat ObjectAndRelation }{ { serialized: "tenant/testns:testobj#testrel", - objectFormat: ObjectAndRelation("tenant/testns", "testobj", "testrel"), + objectFormat: StringToONR("tenant/testns", "testobj", "testrel"), }, { serialized: "tenant/testns:veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryverylong#testrel", - objectFormat: ObjectAndRelation("tenant/testns", "veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryverylong", "testrel"), + objectFormat: StringToONR("tenant/testns", "veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryverylong", "testrel"), }, { serialized: "tenant/testns:-base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==#testrel", - objectFormat: ObjectAndRelation("tenant/testns", "-base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==", "testrel"), + objectFormat: StringToONR("tenant/testns", "-base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==", "testrel"), }, { - serialized: "tenant/testns:*#testrel", - objectFormat: nil, + serialized: "tenant/testns:*#testrel", }, { - serialized: "tenant/testns:testobj#...", - objectFormat: nil, + serialized: "tenant/testns:testobj#...", }, { - serialized: "tenant/testns:testobj", - objectFormat: nil, + serialized: "tenant/testns:testobj", }, { - serialized: "", - objectFormat: nil, + serialized: "", }, } func TestSerializeONR(t *testing.T) { for _, tc := range onrTestCases { tc := tc - if tc.objectFormat == nil { + if tc.objectFormat.ObjectType == "" && tc.objectFormat.ObjectID == "" && tc.objectFormat.Relation == "" { continue } @@ -63,7 +57,13 @@ func TestParseONR(t *testing.T) { t.Run(tc.serialized, func(t *testing.T) { require := require.New(t) - parsed := ParseONR(tc.serialized) + parsed, err := ParseONR(tc.serialized) + if tc.objectFormat.ObjectType == "" && tc.objectFormat.ObjectID == "" && tc.objectFormat.Relation == "" { + require.Error(err) + return + } + + require.NoError(err) require.Equal(tc.objectFormat, parsed) }) } @@ -71,43 +71,40 @@ func TestParseONR(t *testing.T) { var subjectOnrTestCases = []struct { serialized string - objectFormat *core.ObjectAndRelation + objectFormat ObjectAndRelation }{ { serialized: "tenant/testns:testobj#testrel", - objectFormat: ObjectAndRelation("tenant/testns", "testobj", "testrel"), + objectFormat: StringToONR("tenant/testns", "testobj", "testrel"), }, { serialized: "tenant/testns:testobj#...", - objectFormat: ObjectAndRelation("tenant/testns", "testobj", "..."), + objectFormat: StringToONR("tenant/testns", "testobj", "..."), }, { serialized: "tenant/testns:*#...", - objectFormat: ObjectAndRelation("tenant/testns", "*", "..."), + objectFormat: StringToONR("tenant/testns", "*", "..."), }, { serialized: "tenant/testns:testobj", - objectFormat: ObjectAndRelation("tenant/testns", "testobj", "..."), + objectFormat: StringToONR("tenant/testns", "testobj", "..."), }, { serialized: "tenant/testns:veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryverylong", - objectFormat: ObjectAndRelation("tenant/testns", "veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryverylong", "..."), + objectFormat: StringToONR("tenant/testns", "veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryverylong", "..."), }, { serialized: "tenant/testns:-base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==", - objectFormat: ObjectAndRelation("tenant/testns", "-base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==", "..."), + objectFormat: StringToONR("tenant/testns", "-base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==", "..."), }, { - serialized: "tenant/testns:testobj#", - objectFormat: nil, + serialized: "tenant/testns:testobj#", }, { - serialized: "tenant/testns:testobj:", - objectFormat: nil, + serialized: "tenant/testns:testobj:", }, { - serialized: "", - objectFormat: nil, + serialized: "", }, } @@ -117,7 +114,12 @@ func TestParseSubjectONR(t *testing.T) { t.Run(tc.serialized, func(t *testing.T) { require := require.New(t) - parsed := ParseSubjectONR(tc.serialized) + parsed, err := ParseSubjectONR(tc.serialized) + if tc.objectFormat.ObjectType == "" && tc.objectFormat.ObjectID == "" && tc.objectFormat.Relation == "" { + require.Error(err) + return + } + require.Equal(tc.objectFormat, parsed) }) } diff --git a/pkg/tuple/parsing.go b/pkg/tuple/parsing.go new file mode 100644 index 0000000000..71fab65282 --- /dev/null +++ b/pkg/tuple/parsing.go @@ -0,0 +1,204 @@ +package tuple + +import ( + "encoding/json" + "fmt" + "maps" + "regexp" + "slices" + + "google.golang.org/protobuf/types/known/structpb" + + "github.com/jzelinskie/stringz" + + core "github.com/authzed/spicedb/pkg/proto/core/v1" +) + +const ( + namespaceNameExpr = "([a-z][a-z0-9_]{1,61}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]" + resourceIDExpr = "([a-zA-Z0-9/_|\\-=+]{1,})" + subjectIDExpr = "([a-zA-Z0-9/_|\\-=+]{1,})|\\*" + relationExpr = "[a-z][a-z0-9_]{1,62}[a-z0-9]" + caveatNameExpr = "([a-z][a-z0-9_]{1,61}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]" +) + +var onrExpr = fmt.Sprintf( + `(?P(%s)):(?P%s)#(?P%s)`, + namespaceNameExpr, + resourceIDExpr, + relationExpr, +) + +var subjectExpr = fmt.Sprintf( + `(?P(%s)):(?P%s)(#(?P%s|\.\.\.))?`, + namespaceNameExpr, + subjectIDExpr, + relationExpr, +) + +var caveatExpr = fmt.Sprintf(`\[(?P(%s))(:(?P(\{(.+)\})))?\]`, caveatNameExpr) + +var ( + resourceIDRegex = regexp.MustCompile(fmt.Sprintf("^%s$", resourceIDExpr)) + subjectIDRegex = regexp.MustCompile(fmt.Sprintf("^%s$", subjectIDExpr)) +) + +var parserRegex = regexp.MustCompile( + fmt.Sprintf( + `^%s@%s(%s)?$`, + onrExpr, + subjectExpr, + caveatExpr, + ), +) + +// ValidateResourceID ensures that the given resource ID is valid. Returns an error if not. +func ValidateResourceID(objectID string) error { + if !resourceIDRegex.MatchString(objectID) { + return fmt.Errorf("invalid resource id; must match %s", resourceIDExpr) + } + if len(objectID) > 1024 { + return fmt.Errorf("invalid resource id; must be <= 1024 characters") + } + + return nil +} + +// ValidateSubjectID ensures that the given object ID (under a subject reference) is valid. Returns an error if not. +func ValidateSubjectID(subjectID string) error { + if !subjectIDRegex.MatchString(subjectID) { + return fmt.Errorf("invalid subject id; must be alphanumeric and between 1 and 127 characters or a star for public") + } + if len(subjectID) > 1024 { + return fmt.Errorf("invalid resource id; must be <= 1024 characters") + } + + return nil +} + +// MustParse wraps Parse such that any failures panic rather than returning an error. +func MustParse(relString string) Relationship { + parsed, err := Parse(relString) + if err != nil { + panic(err) + } + return parsed +} + +var ( + subjectRelIndex = slices.Index(parserRegex.SubexpNames(), "subjectRel") + caveatNameIndex = slices.Index(parserRegex.SubexpNames(), "caveatName") + caveatContextIndex = slices.Index(parserRegex.SubexpNames(), "caveatContext") + resourceIDIndex = slices.Index(parserRegex.SubexpNames(), "resourceID") + subjectIDIndex = slices.Index(parserRegex.SubexpNames(), "subjectID") + resourceTypeIndex = slices.Index(parserRegex.SubexpNames(), "resourceType") + resourceRelIndex = slices.Index(parserRegex.SubexpNames(), "resourceRel") + subjectTypeIndex = slices.Index(parserRegex.SubexpNames(), "subjectType") +) + +// Parse unmarshals the string form of a Tuple and returns an error on failure, +// +// This function treats both missing and Ellipsis relations equally. +func Parse(relString string) (Relationship, error) { + groups := parserRegex.FindStringSubmatch(relString) + if len(groups) == 0 { + return Relationship{}, fmt.Errorf("invalid relationship string") + } + + subjectRelation := Ellipsis + if len(groups[subjectRelIndex]) > 0 { + subjectRelation = stringz.DefaultEmpty(groups[subjectRelIndex], Ellipsis) + } + + caveatName := groups[caveatNameIndex] + var optionalCaveat *core.ContextualizedCaveat + if caveatName != "" { + optionalCaveat = &core.ContextualizedCaveat{ + CaveatName: caveatName, + } + + caveatContextString := groups[caveatContextIndex] + if len(caveatContextString) > 0 { + contextMap := make(map[string]any, 1) + err := json.Unmarshal([]byte(caveatContextString), &contextMap) + if err != nil { + return Relationship{}, fmt.Errorf("invalid caveat context JSON: %w", err) + } + + caveatContext, err := structpb.NewStruct(contextMap) + if err != nil { + return Relationship{}, fmt.Errorf("invalid caveat context: %w", err) + } + + optionalCaveat.Context = caveatContext + } + } + + resourceID := groups[resourceIDIndex] + if err := ValidateResourceID(resourceID); err != nil { + return Relationship{}, fmt.Errorf("invalid resource id: %w", err) + } + + subjectID := groups[subjectIDIndex] + if err := ValidateSubjectID(subjectID); err != nil { + return Relationship{}, fmt.Errorf("invalid subject id: %w", err) + } + + return Relationship{ + RelationshipReference: RelationshipReference{ + Resource: ObjectAndRelation{ + ObjectType: groups[resourceTypeIndex], + ObjectID: resourceID, + Relation: groups[resourceRelIndex], + }, + Subject: ObjectAndRelation{ + ObjectType: groups[subjectTypeIndex], + ObjectID: subjectID, + Relation: subjectRelation, + }, + }, + OptionalCaveat: optionalCaveat, + }, nil +} + +// MustWithCaveat adds the given caveat name to the relationship. This is for testing only. +func MustWithCaveat(rel Relationship, caveatName string, contexts ...map[string]any) Relationship { + wc, err := WithCaveat(rel, caveatName, contexts...) + if err != nil { + panic(err) + } + return wc +} + +// WithCaveat adds the given caveat name to the relationship. This is for testing only. +func WithCaveat(rel Relationship, caveatName string, contexts ...map[string]any) (Relationship, error) { + var context *structpb.Struct + + if len(contexts) > 0 { + combined := map[string]any{} + for _, current := range contexts { + maps.Copy(combined, current) + } + + contextStruct, err := structpb.NewStruct(combined) + if err != nil { + return Relationship{}, err + } + context = contextStruct + } + + rel.OptionalCaveat = &core.ContextualizedCaveat{ + CaveatName: caveatName, + Context: context, + } + return rel, nil +} + +// StringToONR creates an ONR from string pieces. +func StringToONR(ns, oid, rel string) ObjectAndRelation { + return ObjectAndRelation{ + ObjectType: ns, + ObjectID: oid, + Relation: rel, + } +} diff --git a/pkg/tuple/parsing_test.go b/pkg/tuple/parsing_test.go new file mode 100644 index 0000000000..2e02d65eed --- /dev/null +++ b/pkg/tuple/parsing_test.go @@ -0,0 +1,517 @@ +package tuple + +import ( + "strings" + "testing" + + v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/authzed/spicedb/pkg/testutil" +) + +func makeRel(onr ObjectAndRelation, subject ObjectAndRelation) Relationship { + return Relationship{ + RelationshipReference: RelationshipReference{ + Resource: onr, + Subject: subject, + }, + } +} + +func v1rel(resType, resID, relation, subType, subID, subRel string) *v1.Relationship { + return &v1.Relationship{ + Resource: &v1.ObjectReference{ + ObjectType: resType, + ObjectId: resID, + }, + Relation: relation, + Subject: &v1.SubjectReference{ + Object: &v1.ObjectReference{ + ObjectType: subType, + ObjectId: subID, + }, + OptionalRelation: subRel, + }, + } +} + +func cv1rel(resType, resID, relation, subType, subID, subRel, caveatName string, caveatContext map[string]any) *v1.Relationship { + context, err := structpb.NewStruct(caveatContext) + if err != nil { + panic(err) + } + + if len(context.Fields) == 0 { + context = nil + } + + return &v1.Relationship{ + Resource: &v1.ObjectReference{ + ObjectType: resType, + ObjectId: resID, + }, + Relation: relation, + Subject: &v1.SubjectReference{ + Object: &v1.ObjectReference{ + ObjectType: subType, + ObjectId: subID, + }, + OptionalRelation: subRel, + }, + OptionalCaveat: &v1.ContextualizedCaveat{ + CaveatName: caveatName, + Context: context, + }, + } +} + +var superLongID = strings.Repeat("f", 1024) + +var testCases = []struct { + input string + expectedOutput string + relFormat Relationship + v1Format *v1.Relationship + stableCanonicalization string +}{ + { + input: "testns:testobj#testrel@user:testusr", + expectedOutput: "testns:testobj#testrel@user:testusr", + relFormat: makeRel( + StringToONR("testns", "testobj", "testrel"), + StringToONR("user", "testusr", "..."), + ), + v1Format: v1rel("testns", "testobj", "testrel", "user", "testusr", ""), + stableCanonicalization: "dGVzdG5zOnRlc3RvYmojdGVzdHJlbEB1c2VyOnRlc3R1c3IjLi4u", + }, + { + input: "testns:testobj#testrel@user:testusr#...", + expectedOutput: "testns:testobj#testrel@user:testusr", + relFormat: makeRel( + StringToONR("testns", "testobj", "testrel"), + StringToONR("user", "testusr", "..."), + ), + v1Format: v1rel("testns", "testobj", "testrel", "user", "testusr", ""), + stableCanonicalization: "dGVzdG5zOnRlc3RvYmojdGVzdHJlbEB1c2VyOnRlc3R1c3IjLi4u", + }, + { + input: "tenant/testns:testobj#testrel@tenant/user:testusr", + expectedOutput: "tenant/testns:testobj#testrel@tenant/user:testusr", + relFormat: makeRel( + StringToONR("tenant/testns", "testobj", "testrel"), + StringToONR("tenant/user", "testusr", "..."), + ), + v1Format: v1rel("tenant/testns", "testobj", "testrel", "tenant/user", "testusr", ""), + stableCanonicalization: "dGVuYW50L3Rlc3Ruczp0ZXN0b2JqI3Rlc3RyZWxAdGVuYW50L3VzZXI6dGVzdHVzciMuLi4=", + }, + { + input: "tenant/testns:testobj#testrel@tenant/user:testusr#...", + expectedOutput: "tenant/testns:testobj#testrel@tenant/user:testusr", + relFormat: makeRel( + StringToONR("tenant/testns", "testobj", "testrel"), + StringToONR("tenant/user", "testusr", "..."), + ), + v1Format: v1rel("tenant/testns", "testobj", "testrel", "tenant/user", "testusr", ""), + stableCanonicalization: "dGVuYW50L3Rlc3Ruczp0ZXN0b2JqI3Rlc3RyZWxAdGVuYW50L3VzZXI6dGVzdHVzciMuLi4=", + }, + { + input: "tenant/testns:testobj#testrel@tenant/user:testusr#somerel", + expectedOutput: "tenant/testns:testobj#testrel@tenant/user:testusr#somerel", + relFormat: makeRel( + StringToONR("tenant/testns", "testobj", "testrel"), + StringToONR("tenant/user", "testusr", "somerel"), + ), + v1Format: v1rel("tenant/testns", "testobj", "testrel", "tenant/user", "testusr", "somerel"), + stableCanonicalization: "dGVuYW50L3Rlc3Ruczp0ZXN0b2JqI3Rlc3RyZWxAdGVuYW50L3VzZXI6dGVzdHVzciNzb21lcmVs", + }, + { + input: "org/division/team/testns:testobj#testrel@org/division/identity_team/user:testusr#somerel", + expectedOutput: "org/division/team/testns:testobj#testrel@org/division/identity_team/user:testusr#somerel", + relFormat: makeRel( + StringToONR("org/division/team/testns", "testobj", "testrel"), + StringToONR("org/division/identity_team/user", "testusr", "somerel"), + ), + v1Format: v1rel("org/division/team/testns", "testobj", "testrel", "org/division/identity_team/user", "testusr", "somerel"), + stableCanonicalization: "b3JnL2RpdmlzaW9uL3RlYW0vdGVzdG5zOnRlc3RvYmojdGVzdHJlbEBvcmcvZGl2aXNpb24vaWRlbnRpdHlfdGVhbS91c2VyOnRlc3R1c3Ijc29tZXJlbA==", + }, + { + input: "tenant/testns:testobj#testrel@tenant/user:testusr something", + expectedOutput: "tenant/testns:testobj#testrel@tenant/user:testusr", + }, + { + input: "tenant/testns:testobj#testrel@tenant/user:testusr:", + expectedOutput: "tenant/testns:testobj#testrel@tenant/user:testusr", + }, + { + input: "tenant/testns:testobj#testrel@tenant/user:testusr#", + expectedOutput: "tenant/testns:testobj#testrel@tenant/user:testusr", + }, + { + input: "", + expectedOutput: "", + }, + { + input: "foos:bar#bazzy@groo:grar#...", + expectedOutput: "foos:bar#bazzy@groo:grar", + relFormat: makeRel( + StringToONR("foos", "bar", "bazzy"), + StringToONR("groo", "grar", "..."), + ), + v1Format: v1rel("foos", "bar", "bazzy", "groo", "grar", ""), + stableCanonicalization: "Zm9vczpiYXIjYmF6enlAZ3JvbzpncmFyIy4uLg==", + }, + { + input: "tenant/testns:testobj#testrel@tenant/user:*#...", + expectedOutput: "tenant/testns:testobj#testrel@tenant/user:*", + relFormat: makeRel( + StringToONR("tenant/testns", "testobj", "testrel"), + StringToONR("tenant/user", "*", "..."), + ), + v1Format: v1rel("tenant/testns", "testobj", "testrel", "tenant/user", "*", ""), + stableCanonicalization: "dGVuYW50L3Rlc3Ruczp0ZXN0b2JqI3Rlc3RyZWxAdGVuYW50L3VzZXI6KiMuLi4=", + }, + { + input: "tenant/testns:testobj#testrel@tenant/user:authn|foo", + expectedOutput: "tenant/testns:testobj#testrel@tenant/user:authn|foo", + relFormat: makeRel( + StringToONR("tenant/testns", "testobj", "testrel"), + StringToONR("tenant/user", "authn|foo", "..."), + ), + v1Format: v1rel("tenant/testns", "testobj", "testrel", "tenant/user", "authn|foo", ""), + stableCanonicalization: "dGVuYW50L3Rlc3Ruczp0ZXN0b2JqI3Rlc3RyZWxAdGVuYW50L3VzZXI6YXV0aG58Zm9vIy4uLg==", + }, + { + input: "document:foo#viewer@user:tom[somecaveat]", + expectedOutput: "document:foo#viewer@user:tom[somecaveat]", + relFormat: MustWithCaveat( + makeRel( + StringToONR("document", "foo", "viewer"), + StringToONR("user", "tom", "..."), + ), + "somecaveat", + ), + v1Format: cv1rel("document", "foo", "viewer", "user", "tom", "", "somecaveat", nil), + stableCanonicalization: "ZG9jdW1lbnQ6Zm9vI3ZpZXdlckB1c2VyOnRvbSMuLi4gd2l0aCBzb21lY2F2ZWF0", + }, + { + input: "document:foo#viewer@user:tom[tenant/somecaveat]", + expectedOutput: "document:foo#viewer@user:tom[tenant/somecaveat]", + relFormat: MustWithCaveat( + makeRel( + StringToONR("document", "foo", "viewer"), + StringToONR("user", "tom", "..."), + ), + "tenant/somecaveat", + ), + v1Format: cv1rel("document", "foo", "viewer", "user", "tom", "", "tenant/somecaveat", nil), + stableCanonicalization: "ZG9jdW1lbnQ6Zm9vI3ZpZXdlckB1c2VyOnRvbSMuLi4gd2l0aCB0ZW5hbnQvc29tZWNhdmVhdA==", + }, + { + input: "document:foo#viewer@user:tom[tenant/division/somecaveat]", + expectedOutput: "document:foo#viewer@user:tom[tenant/division/somecaveat]", + relFormat: MustWithCaveat( + makeRel( + StringToONR("document", "foo", "viewer"), + StringToONR("user", "tom", "..."), + ), + "tenant/division/somecaveat", + ), + v1Format: cv1rel("document", "foo", "viewer", "user", "tom", "", "tenant/division/somecaveat", nil), + stableCanonicalization: "ZG9jdW1lbnQ6Zm9vI3ZpZXdlckB1c2VyOnRvbSMuLi4gd2l0aCB0ZW5hbnQvZGl2aXNpb24vc29tZWNhdmVhdA==", + }, + { + input: "document:foo#viewer@user:tom[somecaveat", + expectedOutput: "", + }, + { + input: "document:foo#viewer@user:tom[]", + expectedOutput: "", + }, + { + input: `document:foo#viewer@user:tom[somecaveat:{"hi": "there"}]`, + expectedOutput: `document:foo#viewer@user:tom[somecaveat:{"hi":"there"}]`, + relFormat: MustWithCaveat( + makeRel( + StringToONR("document", "foo", "viewer"), + StringToONR("user", "tom", "..."), + ), + "somecaveat", + map[string]any{ + "hi": "there", + }, + ), + v1Format: cv1rel("document", "foo", "viewer", "user", "tom", "", "somecaveat", map[string]any{"hi": "there"}), + stableCanonicalization: "ZG9jdW1lbnQ6Zm9vI3ZpZXdlckB1c2VyOnRvbSMuLi4gd2l0aCBzb21lY2F2ZWF0OntoaTp0aGVyZX0=", + }, + { + input: `document:foo#viewer@user:tom[somecaveat:{"hi":{"yo": 123}}]`, + expectedOutput: `document:foo#viewer@user:tom[somecaveat:{"hi":{"yo":123}}]`, + relFormat: MustWithCaveat( + makeRel( + StringToONR("document", "foo", "viewer"), + StringToONR("user", "tom", "..."), + ), + "somecaveat", + map[string]any{ + "hi": map[string]any{ + "yo": 123, + }, + }, + ), + v1Format: cv1rel("document", "foo", "viewer", "user", "tom", "", "somecaveat", map[string]any{ + "hi": map[string]any{ + "yo": 123, + }, + }), + stableCanonicalization: "ZG9jdW1lbnQ6Zm9vI3ZpZXdlckB1c2VyOnRvbSMuLi4gd2l0aCBzb21lY2F2ZWF0OntoaTp7eW86MTIzLjAwMDAwMH19", + }, + { + input: `document:foo#viewer@user:tom[somecaveat:{"hi":{"yo":{"hey":true}}}]`, + expectedOutput: `document:foo#viewer@user:tom[somecaveat:{"hi":{"yo":{"hey":true}}}]`, + relFormat: MustWithCaveat( + makeRel( + StringToONR("document", "foo", "viewer"), + StringToONR("user", "tom", "..."), + ), + "somecaveat", + map[string]any{ + "hi": map[string]any{ + "yo": map[string]any{ + "hey": true, + }, + }, + }, + ), + v1Format: cv1rel("document", "foo", "viewer", "user", "tom", "", "somecaveat", map[string]any{ + "hi": map[string]any{ + "yo": map[string]any{ + "hey": true, + }, + }, + }), + stableCanonicalization: "ZG9jdW1lbnQ6Zm9vI3ZpZXdlckB1c2VyOnRvbSMuLi4gd2l0aCBzb21lY2F2ZWF0OntoaTp7eW86e2hleTp0cnVlfX19", + }, + { + input: `document:foo#viewer@user:tom[somecaveat:{"hi":{"yo":{"hey":[1,2,3]}}}]`, + expectedOutput: `document:foo#viewer@user:tom[somecaveat:{"hi":{"yo":{"hey":[1,2,3]}}}]`, + relFormat: MustWithCaveat( + makeRel( + StringToONR("document", "foo", "viewer"), + StringToONR("user", "tom", "..."), + ), + "somecaveat", + map[string]any{ + "hi": map[string]any{ + "yo": map[string]any{ + "hey": []any{1, 2, 3}, + }, + }, + }, + ), + v1Format: cv1rel("document", "foo", "viewer", "user", "tom", "", "somecaveat", map[string]any{ + "hi": map[string]any{ + "yo": map[string]any{ + "hey": []any{1, 2, 3}, + }, + }, + }), + stableCanonicalization: "ZG9jdW1lbnQ6Zm9vI3ZpZXdlckB1c2VyOnRvbSMuLi4gd2l0aCBzb21lY2F2ZWF0OntoaTp7eW86e2hleTpbMS4wMDAwMDAsMi4wMDAwMDAsMy4wMDAwMDBdfX19", + }, + { + input: `document:foo#viewer@user:tom[somecaveat:{"hi":{"yo":"hey":true}}}]`, + expectedOutput: "", + }, + { + input: "testns:" + superLongID + "#testrel@user:testusr", + expectedOutput: "testns:" + superLongID + "#testrel@user:testusr", + relFormat: makeRel( + StringToONR("testns", superLongID, "testrel"), + StringToONR("user", "testusr", "..."), + ), + v1Format: v1rel("testns", superLongID, "testrel", "user", "testusr", ""), + stableCanonicalization: "dGVzdG5zOmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYjdGVzdHJlbEB1c2VyOnRlc3R1c3IjLi4u", + }, + { + input: "testns:foo#testrel@user:" + superLongID, + expectedOutput: "testns:foo#testrel@user:" + superLongID, + relFormat: makeRel( + StringToONR("testns", "foo", "testrel"), + StringToONR("user", superLongID, "..."), + ), + v1Format: v1rel("testns", "foo", "testrel", "user", superLongID, ""), + stableCanonicalization: "dGVzdG5zOmZvbyN0ZXN0cmVsQHVzZXI6ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZiMuLi4=", + }, + { + input: "testns:foo#testrel@user:" + superLongID + "more", + expectedOutput: "", + }, + { + input: "testns:-base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==#testrel@user:-base65YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==", + expectedOutput: "testns:-base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==#testrel@user:-base65YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==", + relFormat: makeRel( + StringToONR("testns", "-base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==", "testrel"), + StringToONR("user", "-base65YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==", "..."), + ), + v1Format: v1rel("testns", "-base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==", "testrel", "user", "-base65YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==", ""), + stableCanonicalization: "dGVzdG5zOi1iYXNlNjRZV1p6WkdaaC1aSE5tWkhQd241aUs4SitZaXZDL2ZtSXJ3bjVpSz09I3Rlc3RyZWxAdXNlcjotYmFzZTY1WVdaelpHWmgtWkhObVpIUHduNWlLOEorWWl2Qy9mbUlyd241aUs9PSMuLi4=", + }, + { + input: `document:foo#viewer@user:tom[somecaveat:{"hi":"a@example.com"}]`, + expectedOutput: `document:foo#viewer@user:tom[somecaveat:{"hi":"a@example.com"}]`, + relFormat: MustWithCaveat( + makeRel( + StringToONR("document", "foo", "viewer"), + StringToONR("user", "tom", "..."), + ), + "somecaveat", + map[string]any{ + "hi": "a@example.com", + }, + ), + v1Format: cv1rel("document", "foo", "viewer", "user", "tom", "", "somecaveat", map[string]any{ + "hi": "a@example.com", + }), + stableCanonicalization: "ZG9jdW1lbnQ6Zm9vI3ZpZXdlckB1c2VyOnRvbSMuLi4gd2l0aCBzb21lY2F2ZWF0OntoaTphQGV4YW1wbGUuY29tfQ==", + }, + { + input: `document:foo#viewer@user:tom[somecaveat:{"first":"a@example.com", "second": "b@example.com"}]`, + expectedOutput: `document:foo#viewer@user:tom[somecaveat:{"first":"a@example.com","second":"b@example.com"}]`, + relFormat: MustWithCaveat( + makeRel( + StringToONR("document", "foo", "viewer"), + StringToONR("user", "tom", "..."), + ), + "somecaveat", + map[string]any{ + "first": "a@example.com", + "second": "b@example.com", + }, + ), + v1Format: cv1rel("document", "foo", "viewer", "user", "tom", "", "somecaveat", map[string]any{ + "first": "a@example.com", + "second": "b@example.com", + }), + stableCanonicalization: "ZG9jdW1lbnQ6Zm9vI3ZpZXdlckB1c2VyOnRvbSMuLi4gd2l0aCBzb21lY2F2ZWF0OntmaXJzdDphQGV4YW1wbGUuY29tLHNlY29uZDpiQGV4YW1wbGUuY29tfQ==", + }, + { + input: `document:foo#viewer@user:tom[somecaveat:{"second": "b@example.com", "first":"a@example.com"}]`, + expectedOutput: `document:foo#viewer@user:tom[somecaveat:{"first":"a@example.com","second":"b@example.com"}]`, + relFormat: MustWithCaveat( + makeRel( + StringToONR("document", "foo", "viewer"), + StringToONR("user", "tom", "..."), + ), + "somecaveat", + map[string]any{ + "first": "a@example.com", + "second": "b@example.com", + }, + ), + v1Format: cv1rel("document", "foo", "viewer", "user", "tom", "", "somecaveat", map[string]any{ + "first": "a@example.com", + "second": "b@example.com", + }), + stableCanonicalization: "ZG9jdW1lbnQ6Zm9vI3ZpZXdlckB1c2VyOnRvbSMuLi4gd2l0aCBzb21lY2F2ZWF0OntmaXJzdDphQGV4YW1wbGUuY29tLHNlY29uZDpiQGV4YW1wbGUuY29tfQ==", + }, +} + +func TestSerialize(t *testing.T) { + for _, tc := range testCases { + tc := tc + t.Run("tuple/"+tc.input, func(t *testing.T) { + if tc.relFormat.Resource.ObjectType == "" { + return + } + + serialized := strings.Replace(MustString(tc.relFormat), " ", "", -1) + require.Equal(t, tc.expectedOutput, serialized) + + withoutCaveat := StringWithoutCaveat(tc.relFormat) + require.Contains(t, tc.expectedOutput, withoutCaveat) + require.NotContains(t, withoutCaveat, "[") + }) + } + + for _, tc := range testCases { + tc := tc + t.Run("relationship/"+tc.input, func(t *testing.T) { + if tc.v1Format == nil { + return + } + + serialized := strings.Replace(MustV1RelString(tc.v1Format), " ", "", -1) + require.Equal(t, tc.expectedOutput, serialized) + + withoutCaveat := V1StringRelationshipWithoutCaveat(tc.v1Format) + require.Contains(t, tc.expectedOutput, withoutCaveat) + require.NotContains(t, withoutCaveat, "[") + }) + } +} + +func TestParse(t *testing.T) { + for _, tc := range testCases { + tc := tc + t.Run("relationship/"+tc.input, func(t *testing.T) { + parsed, err := Parse(tc.input) + if tc.relFormat.Resource.ObjectType == "" { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.True(t, Equal(tc.relFormat, parsed), "found difference in parsed relationship: %v vs %v", tc.relFormat, parsed) + }) + } + + for _, tc := range testCases { + tc := tc + t.Run("v1/"+tc.input, func(t *testing.T) { + parsed, err := ParseV1Rel(tc.input) + if tc.relFormat.Resource.ObjectType == "" { + require.Error(t, err) + return + } + + require.NoError(t, err) + testutil.RequireProtoEqual(t, tc.v1Format, parsed, "found difference in parsed V1 relationship") + }) + } +} + +func TestConvert(t *testing.T) { + for _, tc := range testCases { + tc := tc + t.Run(tc.input, func(t *testing.T) { + require := require.New(t) + + parsed, err := Parse(tc.input) + if tc.relFormat.Resource.ObjectType == "" { + require.Error(err) + return + } + + require.NoError(err) + require.True(Equal(tc.relFormat, parsed)) + + relationship := ToV1Relationship(parsed) + relString := strings.Replace(MustV1RelString(relationship), " ", "", -1) + require.Equal(tc.expectedOutput, relString) + }) + } +} + +func TestValidate(t *testing.T) { + for _, tc := range testCases { + tc := tc + t.Run("validate/"+tc.input, func(t *testing.T) { + parsed, err := ParseV1Rel(tc.input) + if err == nil { + require.NoError(t, ValidateResourceID(parsed.Resource.ObjectId)) + require.NoError(t, ValidateSubjectID(parsed.Subject.Object.ObjectId)) + } + }) + } +} diff --git a/pkg/tuple/proto_interface.go b/pkg/tuple/proto_interface.go deleted file mode 100644 index 4b0d56856c..0000000000 --- a/pkg/tuple/proto_interface.go +++ /dev/null @@ -1,26 +0,0 @@ -package tuple - -import "google.golang.org/protobuf/types/known/structpb" - -type objectReference interface { - GetObjectType() string - GetObjectId() string -} - -type subjectReference[T objectReference] interface { - GetOptionalRelation() string - GetObject() T -} - -type caveat interface { - GetCaveatName() string - GetContext() *structpb.Struct -} - -type relationship[R objectReference, S subjectReference[R], C caveat] interface { - Validate() error - GetResource() R - GetRelation() string - GetSubject() S - GetOptionalCaveat() C -} diff --git a/pkg/tuple/relationship.go b/pkg/tuple/relationship.go deleted file mode 100644 index c2cce7bc8e..0000000000 --- a/pkg/tuple/relationship.go +++ /dev/null @@ -1,78 +0,0 @@ -package tuple - -import ( - v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" -) - -// JoinObject joins the namespace and the objectId together into the standard -// format. -// -// This function assumes that the provided values have already been validated. -func JoinObjectRef(namespace, objectID string) string { return namespace + ":" + objectID } - -// StringObjectRef marshals a *v1.ObjectReference into a string. -// -// This function assumes that the provided values have already been validated. -func StringObjectRef(ref *v1.ObjectReference) string { - return JoinObjectRef(ref.ObjectType, ref.ObjectId) -} - -// StringSubjectRef marshals a *v1.SubjectReference into a string. -// -// This function assumes that the provided values have already been validated. -func StringSubjectRef(ref *v1.SubjectReference) string { - if ref.OptionalRelation == "" { - return StringObjectRef(ref.Object) - } - return JoinRelRef(StringObjectRef(ref.Object), ref.OptionalRelation) -} - -// MustStringRelationship converts a v1.Relationship to a string. -func MustStringRelationship(rel *v1.Relationship) string { - relString, err := StringRelationship(rel) - if err != nil { - panic(err) - } - return relString -} - -// StringRelationship converts a v1.Relationship to a string. -func StringRelationship(rel *v1.Relationship) (string, error) { - if rel == nil || rel.Resource == nil || rel.Subject == nil { - return "", nil - } - - caveatString, err := StringCaveatRef(rel.OptionalCaveat) - if err != nil { - return "", err - } - - return StringRelationshipWithoutCaveat(rel) + caveatString, nil -} - -// StringRelationshipWithoutCaveat converts a v1.Relationship to a string, excluding any caveat. -func StringRelationshipWithoutCaveat(rel *v1.Relationship) string { - if rel == nil || rel.Resource == nil || rel.Subject == nil { - return "" - } - - return StringObjectRef(rel.Resource) + "#" + rel.Relation + "@" + StringSubjectRef(rel.Subject) -} - -// StringCaveatRef converts a v1.ContextualizedCaveat to a string. -func StringCaveatRef(caveat *v1.ContextualizedCaveat) (string, error) { - if caveat == nil || caveat.CaveatName == "" { - return "", nil - } - - contextString, err := StringCaveatContext(caveat.Context) - if err != nil { - return "", err - } - - if len(contextString) > 0 { - contextString = ":" + contextString - } - - return "[" + caveat.CaveatName + contextString + "]", nil -} diff --git a/pkg/tuple/strings.go b/pkg/tuple/strings.go new file mode 100644 index 0000000000..2a3ddccc78 --- /dev/null +++ b/pkg/tuple/strings.go @@ -0,0 +1,145 @@ +package tuple + +import ( + "sort" + "strings" + + "google.golang.org/protobuf/types/known/structpb" + + core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/spiceerrors" +) + +// JoinRelRef joins the namespace and relation together into the same +// format as `StringRR()`. +func JoinRelRef(namespace, relation string) string { return namespace + "#" + relation } + +// MustSplitRelRef splits a string produced by `JoinRelRef()` and panics if +// it fails. +func MustSplitRelRef(relRef string) (namespace, relation string) { + var ok bool + namespace, relation, ok = strings.Cut(relRef, "#") + if !ok { + panic("improperly formatted relation reference") + } + return +} + +// StringRR converts a RR object to a string. +func StringRR(rr RelationReference) string { + return JoinRelRef(rr.ObjectType, rr.Relation) +} + +// StringONR converts an ONR object to a string. +func StringONR(onr ObjectAndRelation) string { + return StringONRStrings(onr.ObjectType, onr.ObjectID, onr.Relation) +} + +func StringCoreRR(rr *core.RelationReference) string { + if rr == nil { + return "" + } + + return JoinRelRef(rr.Namespace, rr.Relation) +} + +// StringCoreONR converts a core ONR object to a string. +func StringCoreONR(onr *core.ObjectAndRelation) string { + if onr == nil { + return "" + } + + return StringONRStrings(onr.Namespace, onr.ObjectId, onr.Relation) +} + +// StringONRStrings converts ONR strings to a string. +func StringONRStrings(namespace, objectID, relation string) string { + if relation == Ellipsis { + return JoinObjectRef(namespace, objectID) + } + return JoinRelRef(JoinObjectRef(namespace, objectID), relation) +} + +// StringsONRs converts ONR objects to a string slice, sorted. +func StringsONRs(onrs []ObjectAndRelation) []string { + onrstrings := make([]string, 0, len(onrs)) + for _, onr := range onrs { + onrstrings = append(onrstrings, StringONR(onr)) + } + + sort.Strings(onrstrings) + return onrstrings +} + +// MustString converts a relationship to a string. +func MustString(rel Relationship) string { + tplString, err := String(rel) + if err != nil { + panic(err) + } + return tplString +} + +// String converts a relationship to a string. +func String(rel Relationship) (string, error) { + spiceerrors.DebugAssert(rel.ValidateNotEmpty, "relationship must not be empty") + + caveatString, err := StringCaveat(rel.OptionalCaveat) + if err != nil { + return "", err + } + + return StringONR(rel.Resource) + "@" + StringONR(rel.Subject) + caveatString, nil +} + +// StringWithoutCaveat converts a relationship to a string, without its caveat included. +func StringWithoutCaveat(rel Relationship) string { + spiceerrors.DebugAssert(rel.ValidateNotEmpty, "relationship must not be empty") + + return StringONR(rel.Resource) + "@" + StringONR(rel.Subject) +} + +func MustStringCaveat(caveat *core.ContextualizedCaveat) string { + caveatString, err := StringCaveat(caveat) + if err != nil { + panic(err) + } + return caveatString +} + +// StringCaveat converts a contextualized caveat to a string. If the caveat is nil or empty, returns empty string. +func StringCaveat(caveat *core.ContextualizedCaveat) (string, error) { + if caveat == nil || caveat.CaveatName == "" { + return "", nil + } + + contextString, err := StringCaveatContext(caveat.Context) + if err != nil { + return "", err + } + + if len(contextString) > 0 { + contextString = ":" + contextString + } + + return "[" + caveat.CaveatName + contextString + "]", nil +} + +// StringCaveatContext converts the context of a caveat to a string. If the context is nil or empty, returns an empty string. +func StringCaveatContext(context *structpb.Struct) (string, error) { + if context == nil || len(context.Fields) == 0 { + return "", nil + } + + contextBytes, err := context.MarshalJSON() + if err != nil { + return "", err + } + return string(contextBytes), nil +} + +// JoinObject joins the namespace and the objectId together into the standard +// format. +// +// This function assumes that the provided values have already been validated. +func JoinObjectRef(namespace, objectID string) string { return namespace + ":" + objectID } diff --git a/pkg/tuple/structs.go b/pkg/tuple/structs.go new file mode 100644 index 0000000000..3a520d6637 --- /dev/null +++ b/pkg/tuple/structs.go @@ -0,0 +1,200 @@ +package tuple + +import ( + "errors" + "fmt" + + core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/spiceerrors" +) + +const ( + // Ellipsis is the Ellipsis relation in v0 style subjects. + Ellipsis = "..." + + // PublicWildcard is the wildcard value for subject object IDs that indicates public access + // for the subject type. + PublicWildcard = "*" +) + +// ObjectAndRelation represents an object and its relation. +type ObjectAndRelation struct { + ObjectID string + ObjectType string + Relation string +} + +const onrStructSize = 48 /* size of the struct itself */ + +func (onr ObjectAndRelation) SizeVT() int { + return len(onr.ObjectID) + len(onr.ObjectType) + len(onr.Relation) + onrStructSize +} + +// WithRelation returns a copy of the object and relation with the given relation. +func (onr ObjectAndRelation) WithRelation(relation string) ObjectAndRelation { + onr.Relation = relation + return onr +} + +// RelationReference returns a RelationReference for the object and relation. +func (onr ObjectAndRelation) RelationReference() RelationReference { + return RelationReference{ + ObjectType: onr.ObjectType, + Relation: onr.Relation, + } +} + +// ToCoreONR converts the ObjectAndRelation to a core.ObjectAndRelation. +func (onr ObjectAndRelation) ToCoreONR() *core.ObjectAndRelation { + return &core.ObjectAndRelation{ + Namespace: onr.ObjectType, + ObjectId: onr.ObjectID, + Relation: onr.Relation, + } +} + +func (onr ObjectAndRelation) String() string { + return fmt.Sprintf("%s:%s#%s", onr.ObjectType, onr.ObjectID, onr.Relation) +} + +// RelationshipReference represents a reference to a relationship, i.e. those portions +// of a relationship that are not the integrity or caveat and thus form the unique +// identifier of the relationship. +type RelationshipReference struct { + Resource ObjectAndRelation + Subject ObjectAndRelation +} + +// Relationship represents a relationship between two objects. +type Relationship struct { + OptionalCaveat *core.ContextualizedCaveat + OptionalIntegrity *core.RelationshipIntegrity + RelationshipReference +} + +// ToCoreTuple converts the Relationship to a core.RelationTuple. +func (r Relationship) ToCoreTuple() *core.RelationTuple { + return &core.RelationTuple{ + ResourceAndRelation: r.Resource.ToCoreONR(), + Subject: r.Subject.ToCoreONR(), + Caveat: r.OptionalCaveat, + Integrity: r.OptionalIntegrity, + } +} + +const relStructSize = 112 /* size of the struct itself */ + +func (r Relationship) SizeVT() int { + size := r.Resource.SizeVT() + r.Subject.SizeVT() + relStructSize + if r.OptionalCaveat != nil { + size += r.OptionalCaveat.SizeVT() + } + return size +} + +// ValidateNotEmpty returns true if the relationship is not empty. +func (r Relationship) ValidateNotEmpty() bool { + return r.Resource.ObjectType != "" && r.Resource.ObjectID != "" && r.Subject.ObjectType != "" && r.Subject.ObjectID != "" && r.Resource.Relation != "" && r.Subject.Relation != "" +} + +// Validate returns an error if the relationship is invalid. +func (r Relationship) Validate() error { + if !r.ValidateNotEmpty() { + return errors.New("object and relation must not be empty") + } + + if r.RelationshipReference.Resource.ObjectID == PublicWildcard { + return errors.New("invalid resource id") + } + + return nil +} + +// WithoutIntegrity returns a copy of the relationship without its integrity. +func (r Relationship) WithoutIntegrity() Relationship { + r.OptionalIntegrity = nil + return r +} + +// WithCaveat returns a copy of the relationship with the given caveat. +func (r Relationship) WithCaveat(caveat *core.ContextualizedCaveat) Relationship { + r.OptionalCaveat = caveat + return r +} + +// UpdateOperation represents the type of update to a relationship. +type UpdateOperation int + +const ( + UpdateOperationTouch UpdateOperation = iota + UpdateOperationCreate + UpdateOperationDelete +) + +// RelationshipUpdate represents an update to a relationship. +type RelationshipUpdate struct { + Relationship Relationship + Operation UpdateOperation +} + +func (ru RelationshipUpdate) OperationString() string { + switch ru.Operation { + case UpdateOperationTouch: + return "TOUCH" + case UpdateOperationCreate: + return "CREATE" + case UpdateOperationDelete: + return "DELETE" + default: + return "unknown" + } +} + +func (ru RelationshipUpdate) DebugString() string { + return fmt.Sprintf("%s(%s)", ru.OperationString(), StringWithoutCaveat(ru.Relationship)) +} + +// RelationReference represents a reference to a relation. +type RelationReference struct { + ObjectType string + Relation string +} + +// ToCoreRR converts the RelationReference to a core.RelationReference. +func (rr RelationReference) ToCoreRR() *core.RelationReference { + return &core.RelationReference{ + Namespace: rr.ObjectType, + Relation: rr.Relation, + } +} + +// ONR creates an ObjectAndRelation. +func ONR(namespace, objectID, relation string) ObjectAndRelation { + spiceerrors.DebugAssert(func() bool { + return namespace != "" && objectID != "" && relation != "" + }, "invalid ONR: %s %s %s", namespace, objectID, relation) + + return ObjectAndRelation{ + ObjectType: namespace, + ObjectID: objectID, + Relation: relation, + } +} + +// ONRRef creates an ObjectAndRelation reference. +func ONRRef(namespace, objectID, relation string) *ObjectAndRelation { + onr := ONR(namespace, objectID, relation) + return &onr +} + +// RR creates a RelationReference. +func RR(namespace, relation string) RelationReference { + spiceerrors.DebugAssert(func() bool { + return namespace != "" && relation != "" + }, "invalid RR: %s %s", namespace, relation) + + return RelationReference{ + ObjectType: namespace, + Relation: relation, + } +} diff --git a/pkg/tuple/structs_test.go b/pkg/tuple/structs_test.go new file mode 100644 index 0000000000..cf3be66a44 --- /dev/null +++ b/pkg/tuple/structs_test.go @@ -0,0 +1,18 @@ +package tuple + +import ( + "testing" + "unsafe" + + "github.com/stretchr/testify/require" +) + +func TestONRStructSize(t *testing.T) { + size := int(unsafe.Sizeof(ObjectAndRelation{})) + require.Equal(t, onrStructSize, size) +} + +func TestRelationshipStructSize(t *testing.T) { + size := int(unsafe.Sizeof(Relationship{})) + require.Equal(t, relStructSize, size) +} diff --git a/pkg/tuple/tuple.go b/pkg/tuple/tuple.go deleted file mode 100644 index ca0a1b1494..0000000000 --- a/pkg/tuple/tuple.go +++ /dev/null @@ -1,669 +0,0 @@ -package tuple - -import ( - "bytes" - "encoding/json" - "fmt" - "maps" - "reflect" - "regexp" - "slices" - "sort" - - v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" - "github.com/jzelinskie/stringz" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/structpb" - - core "github.com/authzed/spicedb/pkg/proto/core/v1" - "github.com/authzed/spicedb/pkg/spiceerrors" -) - -const ( - // Ellipsis is the Ellipsis relation in v0 style subjects. - Ellipsis = "..." - - // PublicWildcard is the wildcard value for subject object IDs that indicates public access - // for the subject type. - PublicWildcard = "*" -) - -const ( - namespaceNameExpr = "([a-z][a-z0-9_]{1,61}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]" - resourceIDExpr = "([a-zA-Z0-9/_|\\-=+]{1,})" - subjectIDExpr = "([a-zA-Z0-9/_|\\-=+]{1,})|\\*" - relationExpr = "[a-z][a-z0-9_]{1,62}[a-z0-9]" - caveatNameExpr = "([a-z][a-z0-9_]{1,61}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]" -) - -var onrExpr = fmt.Sprintf( - `(?P(%s)):(?P%s)#(?P%s)`, - namespaceNameExpr, - resourceIDExpr, - relationExpr, -) - -var subjectExpr = fmt.Sprintf( - `(?P(%s)):(?P%s)(#(?P%s|\.\.\.))?`, - namespaceNameExpr, - subjectIDExpr, - relationExpr, -) - -var caveatExpr = fmt.Sprintf(`\[(?P(%s))(:(?P(\{(.+)\})))?\]`, caveatNameExpr) - -var ( - onrRegex = regexp.MustCompile(fmt.Sprintf("^%s$", onrExpr)) - subjectRegex = regexp.MustCompile(fmt.Sprintf("^%s$", subjectExpr)) - resourceIDRegex = regexp.MustCompile(fmt.Sprintf("^%s$", resourceIDExpr)) - subjectIDRegex = regexp.MustCompile(fmt.Sprintf("^%s$", subjectIDExpr)) -) - -var parserRegex = regexp.MustCompile( - fmt.Sprintf( - `^%s@%s(%s)?$`, - onrExpr, - subjectExpr, - caveatExpr, - ), -) - -// ValidateResourceID ensures that the given resource ID is valid. Returns an error if not. -func ValidateResourceID(objectID string) error { - if !resourceIDRegex.MatchString(objectID) { - return fmt.Errorf("invalid resource id; must match %s", resourceIDExpr) - } - if len(objectID) > 1024 { - return fmt.Errorf("invalid resource id; must be <= 1024 characters") - } - - return nil -} - -// ValidateSubjectID ensures that the given object ID (under a subject reference) is valid. Returns an error if not. -func ValidateSubjectID(subjectID string) error { - if !subjectIDRegex.MatchString(subjectID) { - return fmt.Errorf("invalid subject id; must be alphanumeric and between 1 and 127 characters or a star for public") - } - if len(subjectID) > 1024 { - return fmt.Errorf("invalid resource id; must be <= 1024 characters") - } - - return nil -} - -// MustString converts a tuple to a string. If the tuple is nil or empty, returns empty string. -func MustString(tpl *core.RelationTuple) string { - tplString, err := String(tpl) - if err != nil { - panic(err) - } - return tplString -} - -// String converts a tuple to a string. If the tuple is nil or empty, returns empty string. -func String(tpl *core.RelationTuple) (string, error) { - if tpl == nil || tpl.ResourceAndRelation == nil || tpl.Subject == nil { - return "", nil - } - - caveatString, err := StringCaveat(tpl.Caveat) - if err != nil { - return "", err - } - - return fmt.Sprintf("%s@%s%s", StringONR(tpl.ResourceAndRelation), StringONR(tpl.Subject), caveatString), nil -} - -// StringWithoutCaveat converts a tuple to a string, without its caveat included. -func StringWithoutCaveat(tpl *core.RelationTuple) string { - if tpl == nil || tpl.ResourceAndRelation == nil || tpl.Subject == nil { - return "" - } - - return fmt.Sprintf("%s@%s", StringONR(tpl.ResourceAndRelation), StringONR(tpl.Subject)) -} - -func MustStringCaveat(caveat *core.ContextualizedCaveat) string { - caveatString, err := StringCaveat(caveat) - if err != nil { - panic(err) - } - return caveatString -} - -// StringCaveat converts a contextualized caveat to a string. If the caveat is nil or empty, returns empty string. -func StringCaveat(caveat *core.ContextualizedCaveat) (string, error) { - if caveat == nil || caveat.CaveatName == "" { - return "", nil - } - - contextString, err := StringCaveatContext(caveat.Context) - if err != nil { - return "", err - } - - if len(contextString) > 0 { - contextString = ":" + contextString - } - - return fmt.Sprintf("[%s%s]", caveat.CaveatName, contextString), nil -} - -// StringCaveatContext converts the context of a caveat to a string. If the context is nil or empty, returns an empty string. -func StringCaveatContext(context *structpb.Struct) (string, error) { - if context == nil || len(context.Fields) == 0 { - return "", nil - } - - contextBytes, err := context.MarshalJSON() - if err != nil { - return "", err - } - return string(contextBytes), nil -} - -// MustRelString converts a relationship into a string. Will panic if -// the Relationship does not validate. -func MustRelString(rel *v1.Relationship) string { - if err := rel.Validate(); err != nil { - panic(fmt.Sprintf("invalid relationship: %#v %s", rel, err)) - } - return MustStringRelationship(rel) -} - -// MustParse wraps Parse such that any failures panic rather than returning -// nil. -func MustParse(tpl string) *core.RelationTuple { - if parsed := Parse(tpl); parsed != nil { - return parsed - } - panic("failed to parse tuple: " + tpl) -} - -// Parse unmarshals the string form of a Tuple and returns nil if there is a -// failure. -// -// This function treats both missing and Ellipsis relations equally. -func Parse(tpl string) *core.RelationTuple { - groups := parserRegex.FindStringSubmatch(tpl) - if len(groups) == 0 { - return nil - } - - subjectRelation := Ellipsis - subjectRelIndex := slices.Index(parserRegex.SubexpNames(), "subjectRel") - if len(groups[subjectRelIndex]) > 0 { - subjectRelation = groups[subjectRelIndex] - } - - caveatName := groups[slices.Index(parserRegex.SubexpNames(), "caveatName")] - var optionalCaveat *core.ContextualizedCaveat - if caveatName != "" { - optionalCaveat = &core.ContextualizedCaveat{ - CaveatName: caveatName, - } - - caveatContextString := groups[slices.Index(parserRegex.SubexpNames(), "caveatContext")] - if len(caveatContextString) > 0 { - contextMap := make(map[string]any, 1) - err := json.Unmarshal([]byte(caveatContextString), &contextMap) - if err != nil { - return nil - } - - caveatContext, err := structpb.NewStruct(contextMap) - if err != nil { - return nil - } - - optionalCaveat.Context = caveatContext - } - } - - resourceID := groups[slices.Index(parserRegex.SubexpNames(), "resourceID")] - if err := ValidateResourceID(resourceID); err != nil { - return nil - } - - subjectID := groups[slices.Index(parserRegex.SubexpNames(), "subjectID")] - if err := ValidateSubjectID(subjectID); err != nil { - return nil - } - - return &core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: groups[slices.Index(parserRegex.SubexpNames(), "resourceType")], - ObjectId: resourceID, - Relation: groups[slices.Index(parserRegex.SubexpNames(), "resourceRel")], - }, - Subject: &core.ObjectAndRelation{ - Namespace: groups[slices.Index(parserRegex.SubexpNames(), "subjectType")], - ObjectId: subjectID, - Relation: subjectRelation, - }, - Caveat: optionalCaveat, - } -} - -func ParseRel(rel string) *v1.Relationship { - tpl := Parse(rel) - if tpl == nil { - return nil - } - return ToRelationship(tpl) -} - -func Create(tpl *core.RelationTuple) *core.RelationTupleUpdate { - return &core.RelationTupleUpdate{ - Operation: core.RelationTupleUpdate_CREATE, - Tuple: tpl, - } -} - -func Touch(tpl *core.RelationTuple) *core.RelationTupleUpdate { - return &core.RelationTupleUpdate{ - Operation: core.RelationTupleUpdate_TOUCH, - Tuple: tpl, - } -} - -func Delete(tpl *core.RelationTuple) *core.RelationTupleUpdate { - return &core.RelationTupleUpdate{ - Operation: core.RelationTupleUpdate_DELETE, - Tuple: tpl, - } -} - -// Equal returns true if the two relationships are exactly the same. -func Equal(lhs, rhs *core.RelationTuple) bool { - return OnrEqual(lhs.ResourceAndRelation, rhs.ResourceAndRelation) && OnrEqual(lhs.Subject, rhs.Subject) && caveatEqual(lhs.Caveat, rhs.Caveat) -} - -func caveatEqual(lhs, rhs *core.ContextualizedCaveat) bool { - if lhs == nil && rhs == nil { - return true - } - - if lhs == nil || rhs == nil { - return false - } - - return lhs.CaveatName == rhs.CaveatName && proto.Equal(lhs.Context, rhs.Context) -} - -// MustToRelationship converts a RelationTuple into a Relationship. Will panic if -// the RelationTuple does not validate. -func MustToRelationship(tpl *core.RelationTuple) *v1.Relationship { - if err := tpl.Validate(); err != nil { - panic(fmt.Sprintf("invalid tuple: %#v %s", tpl, err)) - } - - return ToRelationship(tpl) -} - -// ToRelationship converts a RelationTuple into a Relationship. -func ToRelationship(tpl *core.RelationTuple) *v1.Relationship { - var caveat *v1.ContextualizedCaveat - if tpl.Caveat != nil { - caveat = &v1.ContextualizedCaveat{ - CaveatName: tpl.Caveat.CaveatName, - Context: tpl.Caveat.Context, - } - } - return &v1.Relationship{ - Resource: &v1.ObjectReference{ - ObjectType: tpl.ResourceAndRelation.Namespace, - ObjectId: tpl.ResourceAndRelation.ObjectId, - }, - Relation: tpl.ResourceAndRelation.Relation, - Subject: &v1.SubjectReference{ - Object: &v1.ObjectReference{ - ObjectType: tpl.Subject.Namespace, - ObjectId: tpl.Subject.ObjectId, - }, - OptionalRelation: stringz.Default(tpl.Subject.Relation, "", Ellipsis), - }, - OptionalCaveat: caveat, - } -} - -// NewRelationship creates a new Relationship value with all its required child structures allocated -func NewRelationship() *v1.Relationship { - return &v1.Relationship{ - Resource: &v1.ObjectReference{}, - Subject: &v1.SubjectReference{ - Object: &v1.ObjectReference{}, - }, - } -} - -// MustToRelationshipMutating sets target relationship to all the values provided in the source tuple. -func MustToRelationshipMutating(source *core.RelationTuple, targetRel *v1.Relationship, targetCaveat *v1.ContextualizedCaveat) { - targetRel.Resource.ObjectType = source.ResourceAndRelation.Namespace - targetRel.Resource.ObjectId = source.ResourceAndRelation.ObjectId - targetRel.Relation = source.ResourceAndRelation.Relation - targetRel.Subject.Object.ObjectType = source.Subject.Namespace - targetRel.Subject.Object.ObjectId = source.Subject.ObjectId - targetRel.Subject.OptionalRelation = stringz.Default(source.Subject.Relation, "", Ellipsis) - targetRel.OptionalCaveat = nil - - if source.Caveat != nil { - if targetCaveat == nil { - panic("expected a provided target caveat") - } - targetCaveat.CaveatName = source.Caveat.CaveatName - targetCaveat.Context = source.Caveat.Context - targetRel.OptionalCaveat = targetCaveat - } -} - -// MustToFilter converts a RelationTuple into a RelationshipFilter. Will panic if -// the RelationTuple does not validate. -func MustToFilter(tpl *core.RelationTuple) *v1.RelationshipFilter { - if err := tpl.Validate(); err != nil { - panic(fmt.Sprintf("invalid tuple: %#v %s", tpl, err)) - } - - return ToFilter(tpl) -} - -// ToFilter converts a RelationTuple into a RelationshipFilter. -func ToFilter(tpl *core.RelationTuple) *v1.RelationshipFilter { - return &v1.RelationshipFilter{ - ResourceType: tpl.ResourceAndRelation.Namespace, - OptionalResourceId: tpl.ResourceAndRelation.ObjectId, - OptionalRelation: tpl.ResourceAndRelation.Relation, - OptionalSubjectFilter: UsersetToSubjectFilter(tpl.Subject), - } -} - -// UsersetToSubjectFilter converts a userset to the equivalent exact SubjectFilter. -func UsersetToSubjectFilter(userset *core.ObjectAndRelation) *v1.SubjectFilter { - return &v1.SubjectFilter{ - SubjectType: userset.Namespace, - OptionalSubjectId: userset.ObjectId, - OptionalRelation: &v1.SubjectFilter_RelationFilter{ - Relation: stringz.Default(userset.Relation, "", Ellipsis), - }, - } -} - -// RelToFilter converts a Relationship into a RelationshipFilter. -func RelToFilter(rel *v1.Relationship) *v1.RelationshipFilter { - return &v1.RelationshipFilter{ - ResourceType: rel.Resource.ObjectType, - OptionalResourceId: rel.Resource.ObjectId, - OptionalRelation: rel.Relation, - OptionalSubjectFilter: &v1.SubjectFilter{ - SubjectType: rel.Subject.Object.ObjectType, - OptionalSubjectId: rel.Subject.Object.ObjectId, - OptionalRelation: &v1.SubjectFilter_RelationFilter{ - Relation: rel.Subject.OptionalRelation, - }, - }, - } -} - -// UpdatesToRelationshipUpdates converts a slice of RelationTupleUpdate into a -// slice of RelationshipUpdate. -func UpdatesToRelationshipUpdates(updates []*core.RelationTupleUpdate) []*v1.RelationshipUpdate { - relationshipUpdates := make([]*v1.RelationshipUpdate, 0, len(updates)) - - for _, update := range updates { - relationshipUpdates = append(relationshipUpdates, UpdateToRelationshipUpdate(update)) - } - - return relationshipUpdates -} - -func UpdateFromRelationshipUpdates(updates []*v1.RelationshipUpdate) []*core.RelationTupleUpdate { - relationshipUpdates := make([]*core.RelationTupleUpdate, 0, len(updates)) - - for _, update := range updates { - relationshipUpdates = append(relationshipUpdates, UpdateFromRelationshipUpdate(update)) - } - - return relationshipUpdates -} - -// UpdateToRelationshipUpdate converts a RelationTupleUpdate into a -// RelationshipUpdate. -func UpdateToRelationshipUpdate(update *core.RelationTupleUpdate) *v1.RelationshipUpdate { - var op v1.RelationshipUpdate_Operation - switch update.Operation { - case core.RelationTupleUpdate_CREATE: - op = v1.RelationshipUpdate_OPERATION_CREATE - case core.RelationTupleUpdate_DELETE: - op = v1.RelationshipUpdate_OPERATION_DELETE - case core.RelationTupleUpdate_TOUCH: - op = v1.RelationshipUpdate_OPERATION_TOUCH - default: - panic("unknown tuple mutation") - } - - return &v1.RelationshipUpdate{ - Operation: op, - Relationship: ToRelationship(update.Tuple), - } -} - -// MustFromRelationship converts a Relationship into a RelationTuple. -func MustFromRelationship[R objectReference, S subjectReference[R], C caveat](r relationship[R, S, C]) *core.RelationTuple { - if err := r.Validate(); err != nil { - panic(fmt.Sprintf("invalid relationship: %#v %s", r, err)) - } - return FromRelationship(r) -} - -// MustFromRelationships converts a slice of Relationship's into a slice of RelationTuple's. -func MustFromRelationships[R objectReference, S subjectReference[R], C caveat](rels []relationship[R, S, C]) []*core.RelationTuple { - tuples := make([]*core.RelationTuple, 0, len(rels)) - for _, rel := range rels { - tpl := MustFromRelationship(rel) - tuples = append(tuples, tpl) - } - return tuples -} - -// FromRelationship converts a Relationship into a RelationTuple. -func FromRelationship[T objectReference, S subjectReference[T], C caveat](r relationship[T, S, C]) *core.RelationTuple { - rel := &core.RelationTuple{ - ResourceAndRelation: &core.ObjectAndRelation{}, - Subject: &core.ObjectAndRelation{}, - Caveat: &core.ContextualizedCaveat{}, - } - - CopyRelationshipToRelationTuple(r, rel) - - return rel -} - -func CopyRelationshipToRelationTuple[T objectReference, S subjectReference[T], C caveat](r relationship[T, S, C], dst *core.RelationTuple) { - if !reflect.ValueOf(r.GetOptionalCaveat()).IsZero() { - dst.Caveat.CaveatName = r.GetOptionalCaveat().GetCaveatName() - dst.Caveat.Context = r.GetOptionalCaveat().GetContext() - } else { - dst.Caveat = nil - } - - dst.ResourceAndRelation.Namespace = r.GetResource().GetObjectType() - dst.ResourceAndRelation.ObjectId = r.GetResource().GetObjectId() - dst.ResourceAndRelation.Relation = r.GetRelation() - dst.Subject.Namespace = r.GetSubject().GetObject().GetObjectType() - dst.Subject.ObjectId = r.GetSubject().GetObject().GetObjectId() - dst.Subject.Relation = stringz.DefaultEmpty(r.GetSubject().GetOptionalRelation(), Ellipsis) -} - -// CopyRelationTupleToRelationship copies a source core.RelationTuple to a -// destination v1.Relationship without allocating new memory. It requires that -// the structure for the destination be pre-allocated for the fixed parts, and -// an optional caveat context be provided for use when the source contains a -// caveat. -func CopyRelationTupleToRelationship( - src *core.RelationTuple, - dst *v1.Relationship, - dstCaveat *v1.ContextualizedCaveat, -) { - dst.Resource.ObjectType = src.ResourceAndRelation.Namespace - dst.Resource.ObjectId = src.ResourceAndRelation.ObjectId - dst.Relation = src.ResourceAndRelation.Relation - dst.Subject.Object.ObjectType = src.Subject.Namespace - dst.Subject.Object.ObjectId = src.Subject.ObjectId - dst.Subject.OptionalRelation = stringz.Default(src.Subject.Relation, "", Ellipsis) - - if src.Caveat != nil { - dst.OptionalCaveat = dstCaveat - dst.OptionalCaveat.CaveatName = src.Caveat.CaveatName - dst.OptionalCaveat.Context = src.Caveat.Context - } else { - dst.OptionalCaveat = nil - } -} - -// UpdateFromRelationshipUpdate converts a RelationshipUpdate into a -// RelationTupleUpdate. -func UpdateFromRelationshipUpdate(update *v1.RelationshipUpdate) *core.RelationTupleUpdate { - var op core.RelationTupleUpdate_Operation - switch update.Operation { - case v1.RelationshipUpdate_OPERATION_CREATE: - op = core.RelationTupleUpdate_CREATE - case v1.RelationshipUpdate_OPERATION_DELETE: - op = core.RelationTupleUpdate_DELETE - case v1.RelationshipUpdate_OPERATION_TOUCH: - op = core.RelationTupleUpdate_TOUCH - default: - panic("unknown tuple mutation") - } - - return &core.RelationTupleUpdate{ - Operation: op, - Tuple: FromRelationship[*v1.ObjectReference, *v1.SubjectReference, *v1.ContextualizedCaveat](update.Relationship), - } -} - -// MustWithCaveat adds the given caveat name to the tuple. This is for testing only. -func MustWithCaveat(tpl *core.RelationTuple, caveatName string, contexts ...map[string]any) *core.RelationTuple { - wc, err := WithCaveat(tpl, caveatName, contexts...) - if err != nil { - panic(err) - } - return wc -} - -// WithCaveat adds the given caveat name to the tuple. This is for testing only. -func WithCaveat(tpl *core.RelationTuple, caveatName string, contexts ...map[string]any) (*core.RelationTuple, error) { - var context *structpb.Struct - - if len(contexts) > 0 { - combined := map[string]any{} - for _, current := range contexts { - maps.Copy(combined, current) - } - - contextStruct, err := structpb.NewStruct(combined) - if err != nil { - return nil, err - } - context = contextStruct - } - - tpl = tpl.CloneVT() - tpl.Caveat = &core.ContextualizedCaveat{ - CaveatName: caveatName, - Context: context, - } - return tpl, nil -} - -// CanonicalBytes converts a tuple to a canonical set of bytes. If the tuple is nil or empty, returns nil. -// Can be used for hashing purposes. -func CanonicalBytes(tpl *core.RelationTuple) ([]byte, error) { - if tpl == nil { - return nil, nil - } - - var sb bytes.Buffer - sb.WriteString(tpl.ResourceAndRelation.Namespace) - sb.WriteString(":") - sb.WriteString(tpl.ResourceAndRelation.ObjectId) - sb.WriteString("#") - sb.WriteString(tpl.ResourceAndRelation.Relation) - sb.WriteString("@") - sb.WriteString(tpl.Subject.Namespace) - sb.WriteString(":") - sb.WriteString(tpl.Subject.ObjectId) - sb.WriteString("#") - sb.WriteString(tpl.Subject.Relation) - - if tpl.Caveat != nil && tpl.Caveat.CaveatName != "" { - sb.WriteString(" with ") - sb.WriteString(tpl.Caveat.CaveatName) - - if tpl.Caveat.Context != nil && len(tpl.Caveat.Context.Fields) > 0 { - sb.WriteString(":") - if err := writeCanonicalContext(&sb, tpl.Caveat.Context); err != nil { - return nil, err - } - } - } - - return sb.Bytes(), nil -} - -func writeCanonicalContext(sb *bytes.Buffer, context *structpb.Struct) error { - sb.WriteString("{") - for i, key := range sortedContextKeys(context.Fields) { - if i > 0 { - sb.WriteString(",") - } - sb.WriteString(key) - sb.WriteString(":") - if err := writeCanonicalContextValue(sb, context.Fields[key]); err != nil { - return err - } - } - sb.WriteString("}") - return nil -} - -func writeCanonicalContextValue(sb *bytes.Buffer, value *structpb.Value) error { - switch value.Kind.(type) { - case *structpb.Value_NullValue: - sb.WriteString("null") - case *structpb.Value_NumberValue: - sb.WriteString(fmt.Sprintf("%f", value.GetNumberValue())) - case *structpb.Value_StringValue: - sb.WriteString(value.GetStringValue()) - case *structpb.Value_BoolValue: - sb.WriteString(fmt.Sprintf("%t", value.GetBoolValue())) - case *structpb.Value_StructValue: - if err := writeCanonicalContext(sb, value.GetStructValue()); err != nil { - return err - } - case *structpb.Value_ListValue: - sb.WriteString("[") - for i, elem := range value.GetListValue().Values { - if i > 0 { - sb.WriteString(",") - } - if err := writeCanonicalContextValue(sb, elem); err != nil { - return err - } - } - sb.WriteString("]") - default: - return spiceerrors.MustBugf("unknown structpb.Value type: %T", value.Kind) - } - - return nil -} - -func sortedContextKeys(fields map[string]*structpb.Value) []string { - keys := make([]string, 0, len(fields)) - for key := range fields { - keys = append(keys, key) - } - sort.Strings(keys) - return keys -} diff --git a/pkg/tuple/tuple_test.go b/pkg/tuple/tuple_test.go deleted file mode 100644 index 5e7ea7695b..0000000000 --- a/pkg/tuple/tuple_test.go +++ /dev/null @@ -1,879 +0,0 @@ -package tuple - -import ( - "strings" - "testing" - - b64 "encoding/base64" - - v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/structpb" - - core "github.com/authzed/spicedb/pkg/proto/core/v1" - "github.com/authzed/spicedb/pkg/testutil" -) - -func makeTuple(onr *core.ObjectAndRelation, subject *core.ObjectAndRelation) *core.RelationTuple { - return &core.RelationTuple{ - ResourceAndRelation: onr, - Subject: subject, - } -} - -func rel(resType, resID, relation, subType, subID, subRel string) *v1.Relationship { - return &v1.Relationship{ - Resource: &v1.ObjectReference{ - ObjectType: resType, - ObjectId: resID, - }, - Relation: relation, - Subject: &v1.SubjectReference{ - Object: &v1.ObjectReference{ - ObjectType: subType, - ObjectId: subID, - }, - OptionalRelation: subRel, - }, - } -} - -func crel(resType, resID, relation, subType, subID, subRel, caveatName string, caveatContext map[string]any) *v1.Relationship { - context, err := structpb.NewStruct(caveatContext) - if err != nil { - panic(err) - } - - if len(context.Fields) == 0 { - context = nil - } - - return &v1.Relationship{ - Resource: &v1.ObjectReference{ - ObjectType: resType, - ObjectId: resID, - }, - Relation: relation, - Subject: &v1.SubjectReference{ - Object: &v1.ObjectReference{ - ObjectType: subType, - ObjectId: subID, - }, - OptionalRelation: subRel, - }, - OptionalCaveat: &v1.ContextualizedCaveat{ - CaveatName: caveatName, - Context: context, - }, - } -} - -var superLongID = strings.Repeat("f", 1024) - -var testCases = []struct { - input string - expectedOutput string - tupleFormat *core.RelationTuple - relFormat *v1.Relationship - stableCanonicalization string -}{ - { - input: "testns:testobj#testrel@user:testusr", - expectedOutput: "testns:testobj#testrel@user:testusr", - tupleFormat: makeTuple( - ObjectAndRelation("testns", "testobj", "testrel"), - ObjectAndRelation("user", "testusr", "..."), - ), - relFormat: rel("testns", "testobj", "testrel", "user", "testusr", ""), - stableCanonicalization: "dGVzdG5zOnRlc3RvYmojdGVzdHJlbEB1c2VyOnRlc3R1c3IjLi4u", - }, - { - input: "testns:testobj#testrel@user:testusr#...", - expectedOutput: "testns:testobj#testrel@user:testusr", - tupleFormat: makeTuple( - ObjectAndRelation("testns", "testobj", "testrel"), - ObjectAndRelation("user", "testusr", "..."), - ), - relFormat: rel("testns", "testobj", "testrel", "user", "testusr", ""), - stableCanonicalization: "dGVzdG5zOnRlc3RvYmojdGVzdHJlbEB1c2VyOnRlc3R1c3IjLi4u", - }, - { - input: "tenant/testns:testobj#testrel@tenant/user:testusr", - expectedOutput: "tenant/testns:testobj#testrel@tenant/user:testusr", - tupleFormat: makeTuple( - ObjectAndRelation("tenant/testns", "testobj", "testrel"), - ObjectAndRelation("tenant/user", "testusr", "..."), - ), - relFormat: rel("tenant/testns", "testobj", "testrel", "tenant/user", "testusr", ""), - stableCanonicalization: "dGVuYW50L3Rlc3Ruczp0ZXN0b2JqI3Rlc3RyZWxAdGVuYW50L3VzZXI6dGVzdHVzciMuLi4=", - }, - { - input: "tenant/testns:testobj#testrel@tenant/user:testusr#...", - expectedOutput: "tenant/testns:testobj#testrel@tenant/user:testusr", - tupleFormat: makeTuple( - ObjectAndRelation("tenant/testns", "testobj", "testrel"), - ObjectAndRelation("tenant/user", "testusr", "..."), - ), - relFormat: rel("tenant/testns", "testobj", "testrel", "tenant/user", "testusr", ""), - stableCanonicalization: "dGVuYW50L3Rlc3Ruczp0ZXN0b2JqI3Rlc3RyZWxAdGVuYW50L3VzZXI6dGVzdHVzciMuLi4=", - }, - { - input: "tenant/testns:testobj#testrel@tenant/user:testusr#somerel", - expectedOutput: "tenant/testns:testobj#testrel@tenant/user:testusr#somerel", - tupleFormat: makeTuple( - ObjectAndRelation("tenant/testns", "testobj", "testrel"), - ObjectAndRelation("tenant/user", "testusr", "somerel"), - ), - relFormat: rel("tenant/testns", "testobj", "testrel", "tenant/user", "testusr", "somerel"), - stableCanonicalization: "dGVuYW50L3Rlc3Ruczp0ZXN0b2JqI3Rlc3RyZWxAdGVuYW50L3VzZXI6dGVzdHVzciNzb21lcmVs", - }, - { - input: "org/division/team/testns:testobj#testrel@org/division/identity_team/user:testusr#somerel", - expectedOutput: "org/division/team/testns:testobj#testrel@org/division/identity_team/user:testusr#somerel", - tupleFormat: makeTuple( - ObjectAndRelation("org/division/team/testns", "testobj", "testrel"), - ObjectAndRelation("org/division/identity_team/user", "testusr", "somerel"), - ), - relFormat: rel("org/division/team/testns", "testobj", "testrel", "org/division/identity_team/user", "testusr", "somerel"), - stableCanonicalization: "b3JnL2RpdmlzaW9uL3RlYW0vdGVzdG5zOnRlc3RvYmojdGVzdHJlbEBvcmcvZGl2aXNpb24vaWRlbnRpdHlfdGVhbS91c2VyOnRlc3R1c3Ijc29tZXJlbA==", - }, - { - input: "tenant/testns:testobj#testrel@tenant/user:testusr something", - expectedOutput: "tenant/testns:testobj#testrel@tenant/user:testusr", - tupleFormat: nil, - relFormat: nil, - }, - { - input: "tenant/testns:testobj#testrel@tenant/user:testusr:", - expectedOutput: "tenant/testns:testobj#testrel@tenant/user:testusr", - tupleFormat: nil, - relFormat: nil, - }, - { - input: "tenant/testns:testobj#testrel@tenant/user:testusr#", - expectedOutput: "tenant/testns:testobj#testrel@tenant/user:testusr", - tupleFormat: nil, - relFormat: nil, - }, - { - input: "", - expectedOutput: "", - tupleFormat: nil, - relFormat: nil, - }, - { - input: "foos:bar#bazzy@groo:grar#...", - expectedOutput: "foos:bar#bazzy@groo:grar", - tupleFormat: makeTuple( - ObjectAndRelation("foos", "bar", "bazzy"), - ObjectAndRelation("groo", "grar", "..."), - ), - relFormat: rel("foos", "bar", "bazzy", "groo", "grar", ""), - stableCanonicalization: "Zm9vczpiYXIjYmF6enlAZ3JvbzpncmFyIy4uLg==", - }, - { - input: "tenant/testns:testobj#testrel@tenant/user:*#...", - expectedOutput: "tenant/testns:testobj#testrel@tenant/user:*", - tupleFormat: makeTuple( - ObjectAndRelation("tenant/testns", "testobj", "testrel"), - ObjectAndRelation("tenant/user", "*", "..."), - ), - relFormat: rel("tenant/testns", "testobj", "testrel", "tenant/user", "*", ""), - stableCanonicalization: "dGVuYW50L3Rlc3Ruczp0ZXN0b2JqI3Rlc3RyZWxAdGVuYW50L3VzZXI6KiMuLi4=", - }, - { - input: "tenant/testns:testobj#testrel@tenant/user:authn|foo", - expectedOutput: "tenant/testns:testobj#testrel@tenant/user:authn|foo", - tupleFormat: makeTuple( - ObjectAndRelation("tenant/testns", "testobj", "testrel"), - ObjectAndRelation("tenant/user", "authn|foo", "..."), - ), - relFormat: rel("tenant/testns", "testobj", "testrel", "tenant/user", "authn|foo", ""), - stableCanonicalization: "dGVuYW50L3Rlc3Ruczp0ZXN0b2JqI3Rlc3RyZWxAdGVuYW50L3VzZXI6YXV0aG58Zm9vIy4uLg==", - }, - { - input: "document:foo#viewer@user:tom[somecaveat]", - expectedOutput: "document:foo#viewer@user:tom[somecaveat]", - tupleFormat: MustWithCaveat( - makeTuple( - ObjectAndRelation("document", "foo", "viewer"), - ObjectAndRelation("user", "tom", "..."), - ), - "somecaveat", - ), - relFormat: crel("document", "foo", "viewer", "user", "tom", "", "somecaveat", nil), - stableCanonicalization: "ZG9jdW1lbnQ6Zm9vI3ZpZXdlckB1c2VyOnRvbSMuLi4gd2l0aCBzb21lY2F2ZWF0", - }, - { - input: "document:foo#viewer@user:tom[tenant/somecaveat]", - expectedOutput: "document:foo#viewer@user:tom[tenant/somecaveat]", - tupleFormat: MustWithCaveat( - makeTuple( - ObjectAndRelation("document", "foo", "viewer"), - ObjectAndRelation("user", "tom", "..."), - ), - "tenant/somecaveat", - ), - relFormat: crel("document", "foo", "viewer", "user", "tom", "", "tenant/somecaveat", nil), - stableCanonicalization: "ZG9jdW1lbnQ6Zm9vI3ZpZXdlckB1c2VyOnRvbSMuLi4gd2l0aCB0ZW5hbnQvc29tZWNhdmVhdA==", - }, - { - input: "document:foo#viewer@user:tom[tenant/division/somecaveat]", - expectedOutput: "document:foo#viewer@user:tom[tenant/division/somecaveat]", - tupleFormat: MustWithCaveat( - makeTuple( - ObjectAndRelation("document", "foo", "viewer"), - ObjectAndRelation("user", "tom", "..."), - ), - "tenant/division/somecaveat", - ), - relFormat: crel("document", "foo", "viewer", "user", "tom", "", "tenant/division/somecaveat", nil), - stableCanonicalization: "ZG9jdW1lbnQ6Zm9vI3ZpZXdlckB1c2VyOnRvbSMuLi4gd2l0aCB0ZW5hbnQvZGl2aXNpb24vc29tZWNhdmVhdA==", - }, - { - input: "document:foo#viewer@user:tom[somecaveat", - expectedOutput: "", - tupleFormat: nil, - relFormat: nil, - }, - { - input: "document:foo#viewer@user:tom[]", - expectedOutput: "", - tupleFormat: nil, - relFormat: nil, - }, - { - input: `document:foo#viewer@user:tom[somecaveat:{"hi": "there"}]`, - expectedOutput: `document:foo#viewer@user:tom[somecaveat:{"hi":"there"}]`, - tupleFormat: MustWithCaveat( - makeTuple( - ObjectAndRelation("document", "foo", "viewer"), - ObjectAndRelation("user", "tom", "..."), - ), - "somecaveat", - map[string]any{ - "hi": "there", - }, - ), - relFormat: crel("document", "foo", "viewer", "user", "tom", "", "somecaveat", map[string]any{"hi": "there"}), - stableCanonicalization: "ZG9jdW1lbnQ6Zm9vI3ZpZXdlckB1c2VyOnRvbSMuLi4gd2l0aCBzb21lY2F2ZWF0OntoaTp0aGVyZX0=", - }, - { - input: `document:foo#viewer@user:tom[somecaveat:{"hi":{"yo": 123}}]`, - expectedOutput: `document:foo#viewer@user:tom[somecaveat:{"hi":{"yo":123}}]`, - tupleFormat: MustWithCaveat( - makeTuple( - ObjectAndRelation("document", "foo", "viewer"), - ObjectAndRelation("user", "tom", "..."), - ), - "somecaveat", - map[string]any{ - "hi": map[string]any{ - "yo": 123, - }, - }, - ), - relFormat: crel("document", "foo", "viewer", "user", "tom", "", "somecaveat", map[string]any{ - "hi": map[string]any{ - "yo": 123, - }, - }), - stableCanonicalization: "ZG9jdW1lbnQ6Zm9vI3ZpZXdlckB1c2VyOnRvbSMuLi4gd2l0aCBzb21lY2F2ZWF0OntoaTp7eW86MTIzLjAwMDAwMH19", - }, - { - input: `document:foo#viewer@user:tom[somecaveat:{"hi":{"yo":{"hey":true}}}]`, - expectedOutput: `document:foo#viewer@user:tom[somecaveat:{"hi":{"yo":{"hey":true}}}]`, - tupleFormat: MustWithCaveat( - makeTuple( - ObjectAndRelation("document", "foo", "viewer"), - ObjectAndRelation("user", "tom", "..."), - ), - "somecaveat", - map[string]any{ - "hi": map[string]any{ - "yo": map[string]any{ - "hey": true, - }, - }, - }, - ), - relFormat: crel("document", "foo", "viewer", "user", "tom", "", "somecaveat", map[string]any{ - "hi": map[string]any{ - "yo": map[string]any{ - "hey": true, - }, - }, - }), - stableCanonicalization: "ZG9jdW1lbnQ6Zm9vI3ZpZXdlckB1c2VyOnRvbSMuLi4gd2l0aCBzb21lY2F2ZWF0OntoaTp7eW86e2hleTp0cnVlfX19", - }, - { - input: `document:foo#viewer@user:tom[somecaveat:{"hi":{"yo":{"hey":[1,2,3]}}}]`, - expectedOutput: `document:foo#viewer@user:tom[somecaveat:{"hi":{"yo":{"hey":[1,2,3]}}}]`, - tupleFormat: MustWithCaveat( - makeTuple( - ObjectAndRelation("document", "foo", "viewer"), - ObjectAndRelation("user", "tom", "..."), - ), - "somecaveat", - map[string]any{ - "hi": map[string]any{ - "yo": map[string]any{ - "hey": []any{1, 2, 3}, - }, - }, - }, - ), - relFormat: crel("document", "foo", "viewer", "user", "tom", "", "somecaveat", map[string]any{ - "hi": map[string]any{ - "yo": map[string]any{ - "hey": []any{1, 2, 3}, - }, - }, - }), - stableCanonicalization: "ZG9jdW1lbnQ6Zm9vI3ZpZXdlckB1c2VyOnRvbSMuLi4gd2l0aCBzb21lY2F2ZWF0OntoaTp7eW86e2hleTpbMS4wMDAwMDAsMi4wMDAwMDAsMy4wMDAwMDBdfX19", - }, - { - input: `document:foo#viewer@user:tom[somecaveat:{"hi":{"yo":"hey":true}}}]`, - expectedOutput: "", - tupleFormat: nil, - relFormat: nil, - }, - { - input: "testns:" + superLongID + "#testrel@user:testusr", - expectedOutput: "testns:" + superLongID + "#testrel@user:testusr", - tupleFormat: makeTuple( - ObjectAndRelation("testns", superLongID, "testrel"), - ObjectAndRelation("user", "testusr", "..."), - ), - relFormat: rel("testns", superLongID, "testrel", "user", "testusr", ""), - stableCanonicalization: "dGVzdG5zOmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYjdGVzdHJlbEB1c2VyOnRlc3R1c3IjLi4u", - }, - { - input: "testns:foo#testrel@user:" + superLongID, - expectedOutput: "testns:foo#testrel@user:" + superLongID, - tupleFormat: makeTuple( - ObjectAndRelation("testns", "foo", "testrel"), - ObjectAndRelation("user", superLongID, "..."), - ), - relFormat: rel("testns", "foo", "testrel", "user", superLongID, ""), - stableCanonicalization: "dGVzdG5zOmZvbyN0ZXN0cmVsQHVzZXI6ZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZiMuLi4=", - }, - { - input: "testns:foo#testrel@user:" + superLongID + "more", - expectedOutput: "", - tupleFormat: nil, - relFormat: nil, - }, - { - input: "testns:-base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==#testrel@user:-base65YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==", - expectedOutput: "testns:-base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==#testrel@user:-base65YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==", - tupleFormat: makeTuple( - ObjectAndRelation("testns", "-base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==", "testrel"), - ObjectAndRelation("user", "-base65YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==", "..."), - ), - relFormat: rel("testns", "-base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==", "testrel", "user", "-base65YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==", ""), - stableCanonicalization: "dGVzdG5zOi1iYXNlNjRZV1p6WkdaaC1aSE5tWkhQd241aUs4SitZaXZDL2ZtSXJ3bjVpSz09I3Rlc3RyZWxAdXNlcjotYmFzZTY1WVdaelpHWmgtWkhObVpIUHduNWlLOEorWWl2Qy9mbUlyd241aUs9PSMuLi4=", - }, - { - input: `document:foo#viewer@user:tom[somecaveat:{"hi":"a@example.com"}]`, - expectedOutput: `document:foo#viewer@user:tom[somecaveat:{"hi":"a@example.com"}]`, - tupleFormat: MustWithCaveat( - makeTuple( - ObjectAndRelation("document", "foo", "viewer"), - ObjectAndRelation("user", "tom", "..."), - ), - "somecaveat", - map[string]any{ - "hi": "a@example.com", - }, - ), - relFormat: crel("document", "foo", "viewer", "user", "tom", "", "somecaveat", map[string]any{ - "hi": "a@example.com", - }), - stableCanonicalization: "ZG9jdW1lbnQ6Zm9vI3ZpZXdlckB1c2VyOnRvbSMuLi4gd2l0aCBzb21lY2F2ZWF0OntoaTphQGV4YW1wbGUuY29tfQ==", - }, - { - input: `document:foo#viewer@user:tom[somecaveat:{"first":"a@example.com", "second": "b@example.com"}]`, - expectedOutput: `document:foo#viewer@user:tom[somecaveat:{"first":"a@example.com","second":"b@example.com"}]`, - tupleFormat: MustWithCaveat( - makeTuple( - ObjectAndRelation("document", "foo", "viewer"), - ObjectAndRelation("user", "tom", "..."), - ), - "somecaveat", - map[string]any{ - "first": "a@example.com", - "second": "b@example.com", - }, - ), - relFormat: crel("document", "foo", "viewer", "user", "tom", "", "somecaveat", map[string]any{ - "first": "a@example.com", - "second": "b@example.com", - }), - stableCanonicalization: "ZG9jdW1lbnQ6Zm9vI3ZpZXdlckB1c2VyOnRvbSMuLi4gd2l0aCBzb21lY2F2ZWF0OntmaXJzdDphQGV4YW1wbGUuY29tLHNlY29uZDpiQGV4YW1wbGUuY29tfQ==", - }, - { - input: `document:foo#viewer@user:tom[somecaveat:{"second": "b@example.com", "first":"a@example.com"}]`, - expectedOutput: `document:foo#viewer@user:tom[somecaveat:{"first":"a@example.com","second":"b@example.com"}]`, - tupleFormat: MustWithCaveat( - makeTuple( - ObjectAndRelation("document", "foo", "viewer"), - ObjectAndRelation("user", "tom", "..."), - ), - "somecaveat", - map[string]any{ - "first": "a@example.com", - "second": "b@example.com", - }, - ), - relFormat: crel("document", "foo", "viewer", "user", "tom", "", "somecaveat", map[string]any{ - "first": "a@example.com", - "second": "b@example.com", - }), - stableCanonicalization: "ZG9jdW1lbnQ6Zm9vI3ZpZXdlckB1c2VyOnRvbSMuLi4gd2l0aCBzb21lY2F2ZWF0OntmaXJzdDphQGV4YW1wbGUuY29tLHNlY29uZDpiQGV4YW1wbGUuY29tfQ==", - }, -} - -func TestCanonicalBytes(t *testing.T) { - foundBytes := make(map[string]string) - - for _, tc := range testCases { - if tc.tupleFormat == nil { - continue - } - - tc := tc - - t.Run(tc.input, func(t *testing.T) { - // Ensure the serialization is stable. - serialized, err := CanonicalBytes(tc.tupleFormat) - require.NoError(t, err) - - encoded := b64.StdEncoding.EncodeToString(serialized) - require.Equal(t, tc.stableCanonicalization, encoded) - - // Ensure the serialization is unique. - existing, ok := foundBytes[string(serialized)] - if ok { - parsedInput := MustParse(tc.input) - parsedExisting := MustParse(existing) - - require.True(t, parsedInput.EqualVT(parsedExisting), "duplicate canonical bytes found. input: %s; found for input: %s", tc.input, existing) - } - foundBytes[string(serialized)] = tc.input - }) - } -} - -func BenchmarkMustCanonicalBytes(b *testing.B) { - for _, tc := range testCases { - tc := tc - if tc.tupleFormat == nil { - continue - } - - b.Run(tc.input, func(b *testing.B) { - for i := 0; i < b.N; i++ { - _, err := CanonicalBytes(tc.tupleFormat) - require.NoError(b, err) - } - }) - } -} - -func TestSerialize(t *testing.T) { - for _, tc := range testCases { - tc := tc - t.Run("tuple/"+tc.input, func(t *testing.T) { - if tc.tupleFormat == nil { - return - } - - serialized := strings.Replace(MustString(tc.tupleFormat), " ", "", -1) - require.Equal(t, tc.expectedOutput, serialized) - - withoutCaveat := StringWithoutCaveat(tc.tupleFormat) - require.Contains(t, tc.expectedOutput, withoutCaveat) - require.NotContains(t, withoutCaveat, "[") - }) - } - - for _, tc := range testCases { - tc := tc - t.Run("relationship/"+tc.input, func(t *testing.T) { - if tc.relFormat == nil { - return - } - - serialized := strings.Replace(MustRelString(tc.relFormat), " ", "", -1) - require.Equal(t, tc.expectedOutput, serialized) - - withoutCaveat := StringRelationshipWithoutCaveat(tc.relFormat) - require.Contains(t, tc.expectedOutput, withoutCaveat) - require.NotContains(t, withoutCaveat, "[") - }) - } -} - -func TestParse(t *testing.T) { - for _, tc := range testCases { - tc := tc - t.Run("tuple/"+tc.input, func(t *testing.T) { - testutil.RequireProtoEqual(t, tc.tupleFormat, Parse(tc.input), "found difference in parsed tuple") - }) - } - - for _, tc := range testCases { - tc := tc - t.Run("relationship/"+tc.input, func(t *testing.T) { - testutil.RequireProtoEqual(t, tc.relFormat, ParseRel(tc.input), "found difference in parsed relationship") - }) - } -} - -func TestConvert(t *testing.T) { - for _, tc := range testCases { - tc := tc - t.Run(tc.input, func(t *testing.T) { - require := require.New(t) - - parsed := Parse(tc.input) - testutil.RequireProtoEqual(t, tc.tupleFormat, parsed, "found difference in parsed tuple") - if parsed == nil { - return - } - - relationship := ToRelationship(parsed) - relString := strings.Replace(MustRelString(relationship), " ", "", -1) - require.Equal(tc.expectedOutput, relString) - - backToTpl := FromRelationship[*v1.ObjectReference, *v1.SubjectReference, *v1.ContextualizedCaveat](relationship) - testutil.RequireProtoEqual(t, tc.tupleFormat, backToTpl, "found difference in converted tuple") - - serialized := strings.Replace(MustString(backToTpl), " ", "", -1) - require.Equal(tc.expectedOutput, serialized) - }) - } -} - -func TestValidate(t *testing.T) { - for _, tc := range testCases { - tc := tc - t.Run("validate/"+tc.input, func(t *testing.T) { - parsed := ParseRel(tc.input) - if parsed != nil { - require.NoError(t, ValidateResourceID(parsed.Resource.ObjectId)) - require.NoError(t, ValidateSubjectID(parsed.Subject.Object.ObjectId)) - } - }) - } -} - -func TestCopyRelationTupleToRelationship(t *testing.T) { - testCases := []*core.RelationTuple{ - { - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: "abc", - ObjectId: "def", - Relation: "ghi", - }, - Subject: &core.ObjectAndRelation{ - Namespace: "jkl", - ObjectId: "mno", - Relation: "pqr", - }, - }, - { - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: "abc", - ObjectId: "def", - Relation: "ghi", - }, - Subject: &core.ObjectAndRelation{ - Namespace: "jkl", - ObjectId: "mno", - Relation: "...", - }, - }, - { - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: "abc", - ObjectId: "def", - Relation: "ghi", - }, - Subject: &core.ObjectAndRelation{ - Namespace: "jkl", - ObjectId: "mno", - Relation: "pqr", - }, - Caveat: &core.ContextualizedCaveat{ - CaveatName: "stu", - Context: &structpb.Struct{}, - }, - }, - { - ResourceAndRelation: &core.ObjectAndRelation{ - Namespace: "abc", - ObjectId: "def", - Relation: "ghi", - }, - Subject: &core.ObjectAndRelation{ - Namespace: "jkl", - ObjectId: "mno", - Relation: "pqr", - }, - Caveat: &core.ContextualizedCaveat{ - CaveatName: "stu", - Context: &structpb.Struct{ - Fields: map[string]*structpb.Value{ - "vwx": { - Kind: &structpb.Value_StringValue{ - StringValue: "yz", - }, - }, - }, - }, - }, - }, - } - - for _, tc := range testCases { - t.Run(MustString(tc), func(t *testing.T) { - require := require.New(t) - - dst := &v1.Relationship{ - Resource: &v1.ObjectReference{}, - Subject: &v1.SubjectReference{ - Object: &v1.ObjectReference{}, - }, - } - optionalCaveat := &v1.ContextualizedCaveat{} - - CopyRelationTupleToRelationship(tc, dst, optionalCaveat) - - expectedSubjectRelation := tc.Subject.Relation - if tc.Subject.Relation == "..." { - expectedSubjectRelation = "" - } - - require.Equal(tc.ResourceAndRelation.Namespace, dst.Resource.ObjectType) - require.Equal(tc.ResourceAndRelation.ObjectId, dst.Resource.ObjectId) - require.Equal(tc.ResourceAndRelation.Relation, dst.Relation) - require.Equal(tc.Subject.Namespace, dst.Subject.Object.ObjectType) - require.Equal(tc.Subject.ObjectId, dst.Subject.Object.ObjectId) - require.Equal(expectedSubjectRelation, dst.Subject.OptionalRelation) - - if tc.Caveat != nil { - require.Equal(tc.Caveat.CaveatName, dst.OptionalCaveat.CaveatName) - require.Equal(tc.Caveat.Context, dst.OptionalCaveat.Context) - } else { - require.Nil(dst.OptionalCaveat) - } - }) - } -} - -func TestEqual(t *testing.T) { - equalTestCases := []*core.RelationTuple{ - makeTuple( - ObjectAndRelation("testns", "testobj", "testrel"), - ObjectAndRelation("user", "testusr", "..."), - ), - MustWithCaveat( - makeTuple( - ObjectAndRelation("testns", "testobj", "testrel"), - ObjectAndRelation("user", "testusr", "..."), - ), - "somecaveat", - map[string]any{ - "context": map[string]any{ - "deeply": map[string]any{ - "nested": true, - }, - }, - }, - ), - MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":\"there\"}]"), - MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":{\"yo\":123}}]"), - MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":{\"yo\":{\"hey\":true}}, \"hi2\":{\"yo2\":{\"hey2\":false}}}]"), - MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":{\"yo\":{\"hey\":true}}, \"hi2\":{\"yo2\":{\"hey2\":[1,2,3]}}}]"), - } - - for _, tc := range equalTestCases { - t.Run(MustString(tc), func(t *testing.T) { - require := require.New(t) - require.True(Equal(tc, tc.CloneVT())) - require.True(Equal(tc, MustParse(MustString(tc)))) - }) - } - - notEqualTestCases := []struct { - name string - lhs *core.RelationTuple - rhs *core.RelationTuple - }{ - { - name: "Mismatch Resource Type", - lhs: makeTuple( - ObjectAndRelation("testns1", "testobj", "testrel"), - ObjectAndRelation("user", "testusr", "..."), - ), - rhs: makeTuple( - ObjectAndRelation("testns2", "testobj", "testrel"), - ObjectAndRelation("user", "testusr", "..."), - ), - }, - { - name: "Mismatch Resource ID", - lhs: makeTuple( - ObjectAndRelation("testns", "testobj1", "testrel"), - ObjectAndRelation("user", "testusr", "..."), - ), - rhs: makeTuple( - ObjectAndRelation("testns", "testobj2", "testrel"), - ObjectAndRelation("user", "testusr", "..."), - ), - }, - { - name: "Mismatch Resource Relationship", - lhs: makeTuple( - ObjectAndRelation("testns", "testobj", "testrel1"), - ObjectAndRelation("user", "testusr", "..."), - ), - rhs: makeTuple( - ObjectAndRelation("testns", "testobj", "testrel2"), - ObjectAndRelation("user", "testusr", "..."), - ), - }, - { - name: "Mismatch Subject Type", - lhs: makeTuple( - ObjectAndRelation("testns", "testobj", "testrel"), - ObjectAndRelation("user1", "testusr", "..."), - ), - rhs: makeTuple( - ObjectAndRelation("testns", "testobj", "testrel"), - ObjectAndRelation("user2", "testusr", "..."), - ), - }, - { - name: "Mismatch Subject ID", - lhs: makeTuple( - ObjectAndRelation("testns", "testobj", "testrel"), - ObjectAndRelation("user", "testusr1", "..."), - ), - rhs: makeTuple( - ObjectAndRelation("testns", "testobj", "testrel"), - ObjectAndRelation("user", "testusr2", "..."), - ), - }, - { - name: "Mismatch Subject Relationship", - lhs: makeTuple( - ObjectAndRelation("testns", "testobj", "testrel"), - ObjectAndRelation("user", "testusr", "testrel1"), - ), - rhs: makeTuple( - ObjectAndRelation("testns", "testobj", "testrel"), - ObjectAndRelation("user", "testusr", "testrel2"), - ), - }, - { - name: "Mismatch Caveat Name", - lhs: MustWithCaveat( - makeTuple( - ObjectAndRelation("testns", "testobj", "testrel"), - ObjectAndRelation("user", "testusr", "..."), - ), - "somecaveat1", - map[string]any{ - "context": map[string]any{ - "deeply": map[string]any{ - "nested": true, - }, - }, - }, - ), - rhs: MustWithCaveat( - makeTuple( - ObjectAndRelation("testns", "testobj", "testrel"), - ObjectAndRelation("user", "testusr", "..."), - ), - "somecaveat2", - map[string]any{ - "context": map[string]any{ - "deeply": map[string]any{ - "nested": true, - }, - }, - }, - ), - }, - { - name: "Mismatch Caveat Content", - lhs: MustWithCaveat( - makeTuple( - ObjectAndRelation("testns", "testobj", "testrel"), - ObjectAndRelation("user", "testusr", "..."), - ), - "somecaveat", - map[string]any{ - "context": map[string]any{ - "deeply": map[string]any{ - "nested": "1", - }, - }, - }, - ), - rhs: MustWithCaveat( - makeTuple( - ObjectAndRelation("testns", "testobj", "testrel"), - ObjectAndRelation("user", "testusr", "..."), - ), - "somecaveat", - map[string]any{ - "context": map[string]any{ - "deeply": map[string]any{ - "nested": "2", - }, - }, - }, - ), - }, - { - name: "missing caveat context via string", - lhs: MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":\"there\"}]"), - rhs: MustParse("document:foo#viewer@user:tom[somecaveat]"), - }, - { - name: "mismatch caveat context via string", - lhs: MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":\"there\"}]"), - rhs: MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":\"there2\"}]"), - }, - { - name: "mismatch caveat name", - lhs: MustParse("document:foo#viewer@user:tom[somecaveat]"), - rhs: MustParse("document:foo#viewer@user:tom[somecaveat2]"), - }, - { - name: "mismatch caveat context, deeply nested", - lhs: MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":{\"yo\":123}}]"), - rhs: MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":{\"yo\":124}}]"), - }, - { - name: "mismatch caveat context, deeply nested with array", - lhs: MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":{\"yo\":[1,2,3]}}]"), - rhs: MustParse("document:foo#viewer@user:tom[somecaveat:{\"hi\":{\"yo\":[1,2,4]}}]"), - }, - } - - for _, tc := range notEqualTestCases { - t.Run(tc.name, func(t *testing.T) { - require := require.New(t) - require.False(Equal(tc.lhs, tc.rhs)) - require.False(Equal(tc.rhs, tc.lhs)) - require.False(Equal(tc.lhs, MustParse(MustString(tc.rhs)))) - require.False(Equal(tc.rhs, MustParse(MustString(tc.lhs)))) - }) - } -} diff --git a/pkg/tuple/updates.go b/pkg/tuple/updates.go new file mode 100644 index 0000000000..73eafe9123 --- /dev/null +++ b/pkg/tuple/updates.go @@ -0,0 +1,22 @@ +package tuple + +func Create(rel Relationship) RelationshipUpdate { + return RelationshipUpdate{ + Operation: UpdateOperationCreate, + Relationship: rel, + } +} + +func Touch(rel Relationship) RelationshipUpdate { + return RelationshipUpdate{ + Operation: UpdateOperationTouch, + Relationship: rel, + } +} + +func Delete(rel Relationship) RelationshipUpdate { + return RelationshipUpdate{ + Operation: UpdateOperationDelete, + Relationship: rel, + } +} diff --git a/pkg/tuple/updates_test.go b/pkg/tuple/updates_test.go new file mode 100644 index 0000000000..504d51536d --- /dev/null +++ b/pkg/tuple/updates_test.go @@ -0,0 +1,46 @@ +package tuple + +import ( + "fmt" + "testing" +) + +var testRel = MustParse("tenant/testns:testobj#testrel@user:testusr") + +func TestUpdateMethods(t *testing.T) { + tcs := []struct { + input RelationshipUpdate + expected RelationshipUpdate + }{ + { + input: Create(testRel), + expected: RelationshipUpdate{ + Operation: UpdateOperationCreate, + Relationship: testRel, + }, + }, + { + input: Touch(testRel), + expected: RelationshipUpdate{ + Operation: UpdateOperationTouch, + Relationship: testRel, + }, + }, + { + input: Delete(testRel), + expected: RelationshipUpdate{ + Operation: UpdateOperationDelete, + Relationship: testRel, + }, + }, + } + + for _, tc := range tcs { + tc := tc + t.Run(fmt.Sprintf("%d", tc.expected.Operation), func(t *testing.T) { + if tc.input != tc.expected { + t.Errorf("expected %v, got %v", tc.expected, tc.input) + } + }) + } +} diff --git a/pkg/tuple/v1.go b/pkg/tuple/v1.go new file mode 100644 index 0000000000..8fbca99c33 --- /dev/null +++ b/pkg/tuple/v1.go @@ -0,0 +1,294 @@ +package tuple + +import ( + "fmt" + + v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" + "github.com/jzelinskie/stringz" + + core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/spiceerrors" +) + +// ParseV1Rel parses a string representation of a relationship into a v1.Relationship object. +func ParseV1Rel(relString string) (*v1.Relationship, error) { + parsed, err := Parse(relString) + if err != nil { + return nil, err + } + + return ToV1Relationship(parsed), nil +} + +// MustV1RelString converts a relationship into a string. Will panic if +// the Relationship does not validate. +func MustV1RelString(rel *v1.Relationship) string { + if err := rel.Validate(); err != nil { + panic(fmt.Sprintf("invalid relationship: %#v %s", rel, err)) + } + return MustV1StringRelationship(rel) +} + +// StringObjectRef marshals a *v1.ObjectReference into a string. +// +// This function assumes that the provided values have already been validated. +func V1StringObjectRef(ref *v1.ObjectReference) string { + return JoinObjectRef(ref.ObjectType, ref.ObjectId) +} + +// StringSubjectRef marshals a *v1.SubjectReference into a string. +// +// This function assumes that the provided values have already been validated. +func V1StringSubjectRef(ref *v1.SubjectReference) string { + if ref.OptionalRelation == "" { + return V1StringObjectRef(ref.Object) + } + return JoinRelRef(V1StringObjectRef(ref.Object), ref.OptionalRelation) +} + +// MustV1StringRelationship converts a v1.Relationship to a string. +func MustV1StringRelationship(rel *v1.Relationship) string { + relString, err := V1StringRelationship(rel) + if err != nil { + panic(err) + } + return relString +} + +// V1StringRelationship converts a v1.Relationship to a string. +func V1StringRelationship(rel *v1.Relationship) (string, error) { + if rel == nil || rel.Resource == nil || rel.Subject == nil { + return "", nil + } + + caveatString, err := V1StringCaveatRef(rel.OptionalCaveat) + if err != nil { + return "", err + } + + return V1StringRelationshipWithoutCaveat(rel) + caveatString, nil +} + +// V1StringRelationshipWithoutCaveat converts a v1.Relationship to a string, excluding any caveat. +func V1StringRelationshipWithoutCaveat(rel *v1.Relationship) string { + if rel == nil || rel.Resource == nil || rel.Subject == nil { + return "" + } + + return V1StringObjectRef(rel.Resource) + "#" + rel.Relation + "@" + V1StringSubjectRef(rel.Subject) +} + +// V1StringCaveatRef converts a v1.ContextualizedCaveat to a string. +func V1StringCaveatRef(caveat *v1.ContextualizedCaveat) (string, error) { + if caveat == nil || caveat.CaveatName == "" { + return "", nil + } + + contextString, err := StringCaveatContext(caveat.Context) + if err != nil { + return "", err + } + + if len(contextString) > 0 { + contextString = ":" + contextString + } + + return "[" + caveat.CaveatName + contextString + "]", nil +} + +// UpdateToV1RelationshipUpdate converts a RelationshipUpdate into a +// v1.RelationshipUpdate. +func UpdateToV1RelationshipUpdate(update RelationshipUpdate) (*v1.RelationshipUpdate, error) { + var op v1.RelationshipUpdate_Operation + switch update.Operation { + case UpdateOperationCreate: + op = v1.RelationshipUpdate_OPERATION_CREATE + case UpdateOperationDelete: + op = v1.RelationshipUpdate_OPERATION_DELETE + case UpdateOperationTouch: + op = v1.RelationshipUpdate_OPERATION_TOUCH + default: + return nil, spiceerrors.MustBugf("unknown update operation: %v", update.Operation) + } + + return &v1.RelationshipUpdate{ + Operation: op, + Relationship: ToV1Relationship(update.Relationship), + }, nil +} + +// MustUpdateToV1RelationshipUpdate converts a RelationshipUpdate into a +// v1.RelationshipUpdate. Panics on error. +func MustUpdateToV1RelationshipUpdate(update RelationshipUpdate) *v1.RelationshipUpdate { + v1rel, err := UpdateToV1RelationshipUpdate(update) + if err != nil { + panic(err) + } + + return v1rel +} + +// UpdateFromV1RelationshipUpdate converts a RelationshipUpdate into a +// RelationTupleUpdate. +func UpdateFromV1RelationshipUpdate(update *v1.RelationshipUpdate) (RelationshipUpdate, error) { + var op UpdateOperation + switch update.Operation { + case v1.RelationshipUpdate_OPERATION_CREATE: + op = UpdateOperationCreate + case v1.RelationshipUpdate_OPERATION_DELETE: + op = UpdateOperationDelete + case v1.RelationshipUpdate_OPERATION_TOUCH: + op = UpdateOperationTouch + default: + return RelationshipUpdate{}, spiceerrors.MustBugf("unknown update operation: %v", update.Operation) + } + + return RelationshipUpdate{ + Operation: op, + Relationship: FromV1Relationship(update.Relationship), + }, nil +} + +// FromV1Relationship converts a v1.Relationship into a Relationship. +func FromV1Relationship(rel *v1.Relationship) Relationship { + var caveat *core.ContextualizedCaveat + if rel.OptionalCaveat != nil { + caveat = &core.ContextualizedCaveat{ + CaveatName: rel.OptionalCaveat.CaveatName, + Context: rel.OptionalCaveat.Context, + } + } + return Relationship{ + RelationshipReference: RelationshipReference{ + Resource: ObjectAndRelation{ + ObjectID: rel.Resource.ObjectId, + ObjectType: rel.Resource.ObjectType, + Relation: rel.Relation, + }, + Subject: ObjectAndRelation{ + ObjectID: rel.Subject.Object.ObjectId, + ObjectType: rel.Subject.Object.ObjectType, + Relation: stringz.Default(rel.Subject.OptionalRelation, Ellipsis, ""), + }, + }, + OptionalCaveat: caveat, + } +} + +// ToV1Relationship converts a Relationship into a v1.Relationship. +func ToV1Relationship(rel Relationship) *v1.Relationship { + var caveat *v1.ContextualizedCaveat + if rel.OptionalCaveat != nil { + caveat = &v1.ContextualizedCaveat{ + CaveatName: rel.OptionalCaveat.CaveatName, + Context: rel.OptionalCaveat.Context, + } + } + return &v1.Relationship{ + Resource: &v1.ObjectReference{ + ObjectType: rel.Resource.ObjectType, + ObjectId: rel.Resource.ObjectID, + }, + Relation: rel.Resource.Relation, + Subject: &v1.SubjectReference{ + Object: &v1.ObjectReference{ + ObjectType: rel.Subject.ObjectType, + ObjectId: rel.Subject.ObjectID, + }, + OptionalRelation: stringz.Default(rel.Subject.Relation, "", Ellipsis), + }, + OptionalCaveat: caveat, + } +} + +// CopyToV1Relationship copies a Relationship into a v1.Relationship. +func CopyToV1Relationship(rel Relationship, v1rel *v1.Relationship) { + v1rel.Resource.ObjectType = rel.Resource.ObjectType + v1rel.Resource.ObjectId = rel.Resource.ObjectID + v1rel.Relation = rel.Resource.Relation + v1rel.Subject.Object.ObjectType = rel.Subject.ObjectType + v1rel.Subject.Object.ObjectId = rel.Subject.ObjectID + v1rel.Subject.OptionalRelation = stringz.Default(rel.Subject.Relation, "", Ellipsis) + + if rel.OptionalCaveat != nil { + if v1rel.OptionalCaveat == nil { + v1rel.OptionalCaveat = &v1.ContextualizedCaveat{} + } + + v1rel.OptionalCaveat.CaveatName = rel.OptionalCaveat.CaveatName + v1rel.OptionalCaveat.Context = rel.OptionalCaveat.Context + } else { + v1rel.OptionalCaveat = nil + } +} + +// UpdatesToV1RelationshipUpdates converts a slice of RelationshipUpdate into a +// slice of v1.RelationshipUpdate. +func UpdatesToV1RelationshipUpdates(updates []RelationshipUpdate) ([]*v1.RelationshipUpdate, error) { + relationshipUpdates := make([]*v1.RelationshipUpdate, 0, len(updates)) + + for _, update := range updates { + converted, err := UpdateToV1RelationshipUpdate(update) + if err != nil { + return nil, err + } + + relationshipUpdates = append(relationshipUpdates, converted) + } + + return relationshipUpdates, nil +} + +// UpdatesFromV1RelationshipUpdates converts a slice of v1.RelationshipUpdate into a +// slice of RelationshipUpdate. +func UpdatesFromV1RelationshipUpdates(updates []*v1.RelationshipUpdate) ([]RelationshipUpdate, error) { + relationshipUpdates := make([]RelationshipUpdate, 0, len(updates)) + + for _, update := range updates { + converted, err := UpdateFromV1RelationshipUpdate(update) + if err != nil { + return nil, err + } + + relationshipUpdates = append(relationshipUpdates, converted) + } + + return relationshipUpdates, nil +} + +// ToV1Filter converts a RelationTuple into a RelationshipFilter. +func ToV1Filter(rel Relationship) *v1.RelationshipFilter { + return &v1.RelationshipFilter{ + ResourceType: rel.Resource.ObjectType, + OptionalResourceId: rel.Resource.ObjectID, + OptionalRelation: rel.Resource.Relation, + OptionalSubjectFilter: SubjectONRToSubjectFilter(rel.Subject), + } +} + +// SubjectONRToSubjectFilter converts a userset to the equivalent exact SubjectFilter. +func SubjectONRToSubjectFilter(subject ObjectAndRelation) *v1.SubjectFilter { + return &v1.SubjectFilter{ + SubjectType: subject.ObjectType, + OptionalSubjectId: subject.ObjectID, + OptionalRelation: &v1.SubjectFilter_RelationFilter{ + Relation: stringz.Default(subject.Relation, "", Ellipsis), + }, + } +} + +// RelToFilter converts a Relationship into a RelationshipFilter. +func RelToFilter(rel *v1.Relationship) *v1.RelationshipFilter { + return &v1.RelationshipFilter{ + ResourceType: rel.Resource.ObjectType, + OptionalResourceId: rel.Resource.ObjectId, + OptionalRelation: rel.Relation, + OptionalSubjectFilter: &v1.SubjectFilter{ + SubjectType: rel.Subject.Object.ObjectType, + OptionalSubjectId: rel.Subject.Object.ObjectId, + OptionalRelation: &v1.SubjectFilter_RelationFilter{ + Relation: rel.Subject.OptionalRelation, + }, + }, + } +} diff --git a/pkg/tuple/relationship_test.go b/pkg/tuple/v1_test.go similarity index 82% rename from pkg/tuple/relationship_test.go rename to pkg/tuple/v1_test.go index 092499eba0..948160e408 100644 --- a/pkg/tuple/relationship_test.go +++ b/pkg/tuple/v1_test.go @@ -26,8 +26,8 @@ func TestStringObjectRef(t *testing.T) { {objRef(":", ":"), ":::"}, } for _, tt := range table { - require.Equal(t, tt.expected, StringObjectRef(tt.ref)) - require.Equal(t, tt.expected, StringSubjectRef(subRef(tt.ref.ObjectType, tt.ref.ObjectId, ""))) + require.Equal(t, tt.expected, V1StringObjectRef(tt.ref)) + require.Equal(t, tt.expected, V1StringSubjectRef(subRef(tt.ref.ObjectType, tt.ref.ObjectId, ""))) } } @@ -41,6 +41,6 @@ func TestJoinSubjectRef(t *testing.T) { {subRef("document", "1", "reader"), "document:1#reader"}, } for _, tt := range table { - require.Equal(t, tt.expected, StringSubjectRef(tt.ref)) + require.Equal(t, tt.expected, V1StringSubjectRef(tt.ref)) } } diff --git a/pkg/typesystem/reachabilitygraph.go b/pkg/typesystem/reachabilitygraph.go index 23c5d8da5b..dd370cd258 100644 --- a/pkg/typesystem/reachabilitygraph.go +++ b/pkg/typesystem/reachabilitygraph.go @@ -265,7 +265,8 @@ func (rg *ReachabilityGraph) HasOptimizedEntrypointsForSubjectToResource( subjectType *core.RelationReference, resourceType *core.RelationReference, ) (bool, error) { - cacheKey := tuple.StringRR(subjectType) + "=>" + tuple.StringRR(resourceType) + // TODO(jschorr): Change this to be indexed by a struct + cacheKey := tuple.StringCoreRR(subjectType) + "=>" + tuple.StringCoreRR(resourceType) if result, ok := rg.hasOptimizedEntrypointCache.Load(cacheKey); ok { return result.(bool), nil } @@ -334,8 +335,8 @@ func (rg *ReachabilityGraph) computeEntrypoints( func (rg *ReachabilityGraph) getOrBuildGraph(ctx context.Context, resourceType *core.RelationReference, reachabilityOption reachabilityOption) (*core.ReachabilityGraph, error) { // Check the cache. - // TODO(jschorr): Move this to a global cache. - cacheKey := tuple.StringRR(resourceType) + "-" + strconv.Itoa(int(reachabilityOption)) + // TODO(jschorr): Change to be indexed by a struct. + cacheKey := tuple.StringCoreRR(resourceType) + "-" + strconv.Itoa(int(reachabilityOption)) if cached, ok := rg.cachedGraphs.Load(cacheKey); ok { return cached.(*core.ReachabilityGraph), nil } diff --git a/pkg/typesystem/reachabilitygraph_test.go b/pkg/typesystem/reachabilitygraph_test.go index bd0d9d1bee..8ca9386d09 100644 --- a/pkg/typesystem/reachabilitygraph_test.go +++ b/pkg/typesystem/reachabilitygraph_test.go @@ -247,7 +247,7 @@ func TestRelationsEncounteredForSubject(t *testing.T) { relationStrs := make([]string, 0, len(relations)) for _, relation := range relations { - relationStrs = append(relationStrs, tuple.StringRR(relation)) + relationStrs = append(relationStrs, tuple.StringCoreRR(relation)) } sort.Strings(relationStrs) @@ -616,7 +616,7 @@ func TestRelationsEncounteredForResource(t *testing.T) { relationStrs := make([]string, 0, len(relations)) for _, relation := range relations { - relationStrs = append(relationStrs, tuple.StringRR(relation)) + relationStrs = append(relationStrs, tuple.StringCoreRR(relation)) } sort.Strings(relationStrs) @@ -1234,14 +1234,14 @@ func verifyEntrypoints(require *require.Assertions, foundEntrypoints []Reachabil expectedEntrypointRelations := make([]string, 0, len(expectedEntrypoints)) isDirectMap := map[string]bool{} for _, expected := range expectedEntrypoints { - expectedEntrypointRelations = append(expectedEntrypointRelations, tuple.StringRR(expected.relationRef)) - isDirectMap[tuple.StringRR(expected.relationRef)] = expected.isDirect + expectedEntrypointRelations = append(expectedEntrypointRelations, tuple.StringCoreRR(expected.relationRef)) + isDirectMap[tuple.StringCoreRR(expected.relationRef)] = expected.isDirect } foundRelations := make([]string, 0, len(foundEntrypoints)) for _, entrypoint := range foundEntrypoints { - foundRelations = append(foundRelations, tuple.StringRR(entrypoint.ContainingRelationOrPermission())) - if isDirect, ok := isDirectMap[tuple.StringRR(entrypoint.ContainingRelationOrPermission())]; ok { + foundRelations = append(foundRelations, tuple.StringCoreRR(entrypoint.ContainingRelationOrPermission())) + if isDirect, ok := isDirectMap[tuple.StringCoreRR(entrypoint.ContainingRelationOrPermission())]; ok { require.Equal(isDirect, entrypoint.IsDirectResult(), "found mismatch for whether a direct result for entrypoint for %s", entrypoint.parentRelation.Relation) } } diff --git a/pkg/typesystem/typesystem.go b/pkg/typesystem/typesystem.go index 0a909d7c2d..08429d5dfe 100644 --- a/pkg/typesystem/typesystem.go +++ b/pkg/typesystem/typesystem.go @@ -460,7 +460,7 @@ func (nts *TypeSystem) Validate(ctx context.Context) (*ValidatedNamespaceTypeSys relation.Name, relationName, referencedWildcard.WildcardType.GetNamespace(), - tuple.StringRR(referencedWildcard.ReferencingRelation), + tuple.StringCoreRR(referencedWildcard.ReferencingRelation), ), childOneof, relationName, ), nil @@ -506,7 +506,7 @@ func (nts *TypeSystem) Validate(ctx context.Context) (*ValidatedNamespaceTypeSys relation.Name, relationName, referencedWildcard.WildcardType.GetNamespace(), - tuple.StringRR(referencedWildcard.ReferencingRelation), + tuple.StringCoreRR(referencedWildcard.ReferencingRelation), ), childOneof, relationName, ), nil @@ -617,7 +617,7 @@ func (nts *TypeSystem) Validate(ctx context.Context) (*ValidatedNamespaceTypeSys allowedRelation.Namespace, allowedRelation.GetRelation(), referencedWildcard.WildcardType.GetNamespace(), - tuple.StringRR(referencedWildcard.ReferencingRelation), + tuple.StringCoreRR(referencedWildcard.ReferencingRelation), ), allowedRelation, tuple.JoinRelRef(allowedRelation.GetNamespace(), allowedRelation.GetRelation()), diff --git a/pkg/typesystem/typesystem_test.go b/pkg/typesystem/typesystem_test.go index 8378db15b9..3d4bdfccbf 100644 --- a/pkg/typesystem/typesystem_test.go +++ b/pkg/typesystem/typesystem_test.go @@ -419,12 +419,12 @@ func requireSameAllowedRelations(t *testing.T, found []*core.AllowedRelation, ex func requireSameSubjectRelations(t *testing.T, found []*core.RelationReference, expected ...*core.RelationReference) { foundSet := mapz.NewSet[string]() for _, f := range found { - foundSet.Add(tuple.StringRR(f)) + foundSet.Add(tuple.StringCoreRR(f)) } expectSet := mapz.NewSet[string]() for _, e := range expected { - expectSet.Add(tuple.StringRR(e)) + expectSet.Add(tuple.StringCoreRR(e)) } foundSlice := foundSet.AsSlice() diff --git a/pkg/validationfile/blocks/assertions.go b/pkg/validationfile/blocks/assertions.go index 82b832cec8..56ef6cc693 100644 --- a/pkg/validationfile/blocks/assertions.go +++ b/pkg/validationfile/blocks/assertions.go @@ -5,7 +5,6 @@ import ( "fmt" "strings" - v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" "github.com/ccoveille/go-safecast" yamlv3 "gopkg.in/yaml.v3" @@ -38,7 +37,7 @@ type Assertion struct { // Relationship is the parsed relationship on which the assertion is being // run. - Relationship *v1.Relationship + Relationship tuple.Relationship // CaveatContext is the caveat context for the assertion, if any. CaveatContext map[string]any @@ -102,17 +101,17 @@ func (a *Assertion) UnmarshalYAML(node *yamlv3.Node) error { ) } - tpl := tuple.Parse(strings.TrimSpace(parts[0])) - if tpl == nil { + relationship, err := tuple.Parse(strings.TrimSpace(parts[0])) + if err != nil { return spiceerrors.NewErrorWithSource( - fmt.Errorf("error parsing relationship in assertion `%s`", trimmed), + fmt.Errorf("error parsing relationship in assertion `%s`: %w", trimmed, err), trimmed, line, column, ) } - a.Relationship = tuple.MustToRelationship(tpl) + a.Relationship = relationship if len(parts) == 2 { caveatContextMap := make(map[string]any, 0) diff --git a/pkg/validationfile/blocks/assertions_test.go b/pkg/validationfile/blocks/assertions_test.go index 7a086c2601..70cda20a79 100644 --- a/pkg/validationfile/blocks/assertions_test.go +++ b/pkg/validationfile/blocks/assertions_test.go @@ -34,7 +34,7 @@ func TestParseAssertions(t *testing.T) { AssertTrue: []Assertion{ { "document:foo#view@user:someone", - tuple.MustToRelationship(tuple.MustParse("document:foo#view@user:someone")), + tuple.MustParse("document:foo#view@user:someone"), nil, spiceerrors.SourcePosition{LineNumber: 2, ColumnPosition: 3}, }, @@ -54,13 +54,13 @@ assertFalse: AssertTrue: []Assertion{ { "document:foo#view@user:someone", - tuple.MustToRelationship(tuple.MustParse("document:foo#view@user:someone")), + tuple.MustParse("document:foo#view@user:someone"), nil, spiceerrors.SourcePosition{LineNumber: 2, ColumnPosition: 3}, }, { "document:bar#view@user:sometwo", - tuple.MustToRelationship(tuple.MustParse("document:bar#view@user:sometwo")), + tuple.MustParse("document:bar#view@user:sometwo"), nil, spiceerrors.SourcePosition{LineNumber: 3, ColumnPosition: 3}, }, @@ -68,7 +68,7 @@ assertFalse: AssertFalse: []Assertion{ { "document:foo#write@user:someone", - tuple.MustToRelationship(tuple.MustParse("document:foo#write@user:someone")), + tuple.MustParse("document:foo#write@user:someone"), nil, spiceerrors.SourcePosition{LineNumber: 5, ColumnPosition: 3}, }, @@ -103,7 +103,7 @@ assertFalse: garbage AssertTrue: []Assertion{ { `document:foo#view@user:someone with {"foo": "bar"}`, - tuple.MustToRelationship(tuple.MustParse("document:foo#view@user:someone")), + tuple.MustParse("document:foo#view@user:someone"), map[string]any{ "foo": "bar", }, @@ -129,7 +129,7 @@ assertFalse: garbage AssertTrue: []Assertion{ { ` document:foo#view@user:someone with {"foo": "bar"} `, - tuple.MustToRelationship(tuple.MustParse("document:foo#view@user:someone")), + tuple.MustParse("document:foo#view@user:someone"), map[string]any{ "foo": "bar", }, diff --git a/pkg/validationfile/blocks/expectedrelations.go b/pkg/validationfile/blocks/expectedrelations.go index 2c4c112799..9c0bb1db4d 100644 --- a/pkg/validationfile/blocks/expectedrelations.go +++ b/pkg/validationfile/blocks/expectedrelations.go @@ -10,8 +10,6 @@ import ( "github.com/ccoveille/go-safecast" - core "github.com/authzed/spicedb/pkg/proto/core/v1" - "github.com/authzed/spicedb/pkg/spiceerrors" "github.com/authzed/spicedb/pkg/tuple" ) @@ -48,7 +46,7 @@ type ObjectRelation struct { ObjectRelationString string // ObjectAndRelation is the parsed object and relation. - ObjectAndRelation *core.ObjectAndRelation + ObjectAndRelation tuple.ObjectAndRelation // SourcePosition is the position of the expected relations in the file. SourcePosition spiceerrors.SourcePosition @@ -70,10 +68,10 @@ func (ors *ObjectRelation) UnmarshalYAML(node *yamlv3.Node) error { return err } - parsed := tuple.ParseONR(ors.ObjectRelationString) - if parsed == nil { + parsed, err := tuple.ParseONR(ors.ObjectRelationString) + if err != nil { return spiceerrors.NewErrorWithSource( - fmt.Errorf("could not parse %s", ors.ObjectRelationString), + fmt.Errorf("could not parse %s: %w", ors.ObjectRelationString, err), ors.ObjectRelationString, line, column, @@ -102,7 +100,7 @@ type ExpectedSubject struct { SubjectWithExceptions *SubjectWithExceptions // Resources are the resources under which the subject is found. - Resources []*core.ObjectAndRelation + Resources []tuple.ObjectAndRelation // SourcePosition is the position of the expected subject in the file. SourcePosition spiceerrors.SourcePosition @@ -111,7 +109,7 @@ type ExpectedSubject struct { // SubjectAndCaveat returns a subject and whether it is caveated. type SubjectAndCaveat struct { // Subject is the subject found. - Subject *core.ObjectAndRelation + Subject tuple.ObjectAndRelation // IsCaveated indicates whether the subject is caveated. IsCaveated bool @@ -199,9 +197,9 @@ func (vs ValidationString) Subject() (*SubjectWithExceptions, *spiceerrors.Error } subjectONRString := groups[slices.Index(vsSubjectWithExceptionsOrCaveatRegex.SubexpNames(), "subject_onr")] - subjectONR := tuple.ParseSubjectONR(subjectONRString) - if subjectONR == nil { - return nil, spiceerrors.NewErrorWithSource(fmt.Errorf("invalid subject: `%s`", subjectONRString), subjectONRString, 0, 0) + subjectONR, err := tuple.ParseSubjectONR(subjectONRString) + if err != nil { + return nil, spiceerrors.NewErrorWithSource(fmt.Errorf("invalid subject: `%s`: %w", subjectONRString, err), subjectONRString, 0, 0) } exceptionsString := strings.TrimSpace(groups[slices.Index(vsSubjectWithExceptionsOrCaveatRegex.SubexpNames(), "exceptions")]) @@ -217,9 +215,9 @@ func (vs ValidationString) Subject() (*SubjectWithExceptions, *spiceerrors.Error isCaveated = true } - exceptionONR := tuple.ParseSubjectONR(strings.TrimSpace(exceptionString)) - if exceptionONR == nil { - return nil, spiceerrors.NewErrorWithSource(fmt.Errorf("invalid subject: `%s`", exceptionString), exceptionString, 0, 0) + exceptionONR, err := tuple.ParseSubjectONR(strings.TrimSpace(exceptionString)) + if err != nil { + return nil, spiceerrors.NewErrorWithSource(fmt.Errorf("invalid subject: `%s`: %w", exceptionString, err), exceptionString, 0, 0) } exceptions = append(exceptions, SubjectAndCaveat{exceptionONR, isCaveated}) @@ -241,13 +239,13 @@ func (vs ValidationString) ONRStrings() []string { } // ONRS returns the subject ONRs in the ValidationString, if any. -func (vs ValidationString) ONRS() ([]*core.ObjectAndRelation, *spiceerrors.ErrorWithSource) { +func (vs ValidationString) ONRS() ([]tuple.ObjectAndRelation, *spiceerrors.ErrorWithSource) { onrStrings := vs.ONRStrings() - onrs := []*core.ObjectAndRelation{} + onrs := []tuple.ObjectAndRelation{} for _, onrString := range onrStrings { - found := tuple.ParseONR(onrString) - if found == nil { - return nil, spiceerrors.NewErrorWithSource(fmt.Errorf("invalid resource and relation: `%s`", onrString), onrString, 0, 0) + found, err := tuple.ParseONR(onrString) + if err != nil { + return nil, spiceerrors.NewErrorWithSource(fmt.Errorf("invalid resource and relation: `%s`: %w", onrString, err), onrString, 0, 0) } onrs = append(onrs, found) diff --git a/pkg/validationfile/blocks/relationships.go b/pkg/validationfile/blocks/relationships.go index 562294e598..94ac4c68dc 100644 --- a/pkg/validationfile/blocks/relationships.go +++ b/pkg/validationfile/blocks/relationships.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" - v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" "github.com/ccoveille/go-safecast" yamlv3 "gopkg.in/yaml.v3" @@ -21,7 +20,7 @@ type ParsedRelationships struct { SourcePosition spiceerrors.SourcePosition // Relationships are the fully parsed relationships. - Relationships []*v1.Relationship + Relationships []tuple.Relationship } // UnmarshalYAML is a custom unmarshaller. @@ -38,7 +37,7 @@ func (pr *ParsedRelationships) UnmarshalYAML(node *yamlv3.Node) error { seenTuples := map[string]bool{} lines := strings.Split(relationshipsString, "\n") - relationships := make([]*v1.Relationship, 0, len(lines)) + relationships := make([]tuple.Relationship, 0, len(lines)) for index, line := range lines { trimmed := strings.TrimSpace(line) if len(trimmed) == 0 || strings.HasPrefix(trimmed, "//") { @@ -55,17 +54,17 @@ func (pr *ParsedRelationships) UnmarshalYAML(node *yamlv3.Node) error { return err } - tpl := tuple.Parse(trimmed) - if tpl == nil { + rel, err := tuple.Parse(trimmed) + if err != nil { return spiceerrors.NewErrorWithSource( - fmt.Errorf("error parsing relationship `%s`", trimmed), + fmt.Errorf("error parsing relationship `%s`: %w", trimmed, err), trimmed, errorLine, column, ) } - _, ok := seenTuples[tuple.StringWithoutCaveat(tpl)] + _, ok := seenTuples[tuple.StringWithoutCaveat(rel)] if ok { return spiceerrors.NewErrorWithSource( fmt.Errorf("found repeated relationship `%s`", trimmed), @@ -74,8 +73,8 @@ func (pr *ParsedRelationships) UnmarshalYAML(node *yamlv3.Node) error { column, ) } - seenTuples[tuple.StringWithoutCaveat(tpl)] = true - relationships = append(relationships, tuple.MustToRelationship(tpl)) + seenTuples[tuple.StringWithoutCaveat(rel)] = true + relationships = append(relationships, rel) } pr.Relationships = relationships diff --git a/pkg/validationfile/fileformat_test.go b/pkg/validationfile/fileformat_test.go index 0a62f30606..3d9d056b06 100644 --- a/pkg/validationfile/fileformat_test.go +++ b/pkg/validationfile/fileformat_test.go @@ -114,7 +114,7 @@ relationships: >- errWithSource, ok := spiceerrors.AsErrorWithSource(err) require.True(t, ok) - require.Equal(t, err.Error(), "error parsing relationship `document:firstdocwriter@user:tom`") + require.Equal(t, err.Error(), "error parsing relationship `document:firstdocwriter@user:tom`: invalid relationship string") require.Equal(t, uint64(5), errWithSource.LineNumber) } @@ -131,7 +131,7 @@ relationships: >- errWithSource, ok := spiceerrors.AsErrorWithSource(err) require.True(t, ok) - require.Equal(t, err.Error(), "error parsing relationship `document:firstdoc#readeruser:fred`") + require.Equal(t, err.Error(), "error parsing relationship `document:firstdoc#readeruser:fred`: invalid relationship string") require.Equal(t, uint64(7), errWithSource.LineNumber) } @@ -154,7 +154,7 @@ relationships: >- errWithSource, ok := spiceerrors.AsErrorWithSource(err) require.True(t, ok) - require.Equal(t, err.Error(), "error parsing relationship `document:firstdoc#readeruser:fred`") + require.Equal(t, err.Error(), "error parsing relationship `document:firstdoc#readeruser:fred`: invalid relationship string") require.Equal(t, uint64(13), errWithSource.LineNumber) } diff --git a/pkg/validationfile/loader.go b/pkg/validationfile/loader.go index c82d464d39..9ab311be7d 100644 --- a/pkg/validationfile/loader.go +++ b/pkg/validationfile/loader.go @@ -29,9 +29,9 @@ type PopulatedValidationFile struct { // direct or compiled from schema form. CaveatDefinitions []*core.CaveatDefinition - // Tuples are the relation tuples defined in the validation file, either directly + // Relationships are the relationships defined in the validation file, either directly // or in the relationships block. - Tuples []*core.RelationTuple + Relationships []tuple.Relationship // ParsedFiles are the underlying parsed validation files. ParsedFiles []ValidationFile @@ -60,8 +60,8 @@ func PopulateFromFilesContents(ctx context.Context, ds datastore.Datastore, file var schema string var objectDefs []*core.NamespaceDefinition var caveatDefs []*core.CaveatDefinition - var tuples []*core.RelationTuple - var updates []*core.RelationTupleUpdate + var rels []tuple.Relationship + var updates []tuple.RelationshipUpdate var revision datastore.Revision @@ -105,9 +105,8 @@ func PopulateFromFilesContents(ctx context.Context, ds datastore.Datastore, file // Parse relationships for updates. for _, rel := range parsed.Relationships.Relationships { - tpl := tuple.MustFromRelationship(rel) - updates = append(updates, tuple.Touch(tpl)) - tuples = append(tuples, tpl) + updates = append(updates, tuple.Touch(rel)) + rels = append(rels, rel) } } @@ -149,17 +148,17 @@ func PopulateFromFilesContents(ctx context.Context, ds datastore.Datastore, file return err }) - slicez.ForEachChunk(updates, 500, func(chunked []*core.RelationTupleUpdate) { + slicez.ForEachChunk(updates, 500, func(chunked []tuple.RelationshipUpdate) { if err != nil { return } - chunkedTuples := make([]*core.RelationTuple, 0, len(chunked)) + chunkedRels := make([]tuple.Relationship, 0, len(chunked)) for _, update := range chunked { - chunkedTuples = append(chunkedTuples, update.Tuple) + chunkedRels = append(chunkedRels, update.Relationship) } revision, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { - err = relationships.ValidateRelationshipsForCreateOrTouch(ctx, rwt, chunkedTuples) + err = relationships.ValidateRelationshipsForCreateOrTouch(ctx, rwt, chunkedRels...) if err != nil { return err } @@ -172,5 +171,5 @@ func PopulateFromFilesContents(ctx context.Context, ds datastore.Datastore, file return nil, nil, err } - return &PopulatedValidationFile{schema, objectDefs, caveatDefs, tuples, files}, revision, err + return &PopulatedValidationFile{schema, objectDefs, caveatDefs, rels, files}, revision, err } diff --git a/pkg/validationfile/loader_test.go b/pkg/validationfile/loader_test.go index 5a36210c86..09741be873 100644 --- a/pkg/validationfile/loader_test.go +++ b/pkg/validationfile/loader_test.go @@ -134,9 +134,9 @@ func TestPopulateFromFiles(t *testing.T) { if tt.expectedError == "" { require.NoError(err) - foundRelationships := make([]string, 0, len(parsed.Tuples)) - for _, tpl := range parsed.Tuples { - foundRelationships = append(foundRelationships, tuple.MustString(tpl)) + foundRelationships := make([]string, 0, len(parsed.Relationships)) + for _, rel := range parsed.Relationships { + foundRelationships = append(foundRelationships, tuple.MustString(rel)) } sort.Strings(tt.want) diff --git a/tools/analyzers/go.mod b/tools/analyzers/go.mod index d6653138b0..b3128bcc2d 100644 --- a/tools/analyzers/go.mod +++ b/tools/analyzers/go.mod @@ -1,6 +1,6 @@ module github.com/authzed/spicedb/tools/analyzers -go 1.22.7 +go 1.23.1 require ( github.com/samber/lo v1.47.0 diff --git a/tools/analyzers/go.work b/tools/analyzers/go.work index 90f38e0660..51209e67b4 100644 --- a/tools/analyzers/go.work +++ b/tools/analyzers/go.work @@ -1,6 +1,6 @@ -go 1.22.7 +go 1.23.1 -toolchain go1.22.7 +toolchain go1.23.1 use ( .