From 72fc5855c234799f1275a8a728ce587ea3e3a314 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Fri, 5 Jul 2024 15:00:19 -0400 Subject: [PATCH] Add integrity information to memdb datastore and start on datastore tests --- internal/datastore/memdb/readwrite.go | 13 +++++++ internal/datastore/memdb/schema.go | 28 ++++++++++++++- pkg/datastore/test/datastore.go | 1 + pkg/datastore/test/tuples.go | 49 +++++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 1 deletion(-) diff --git a/internal/datastore/memdb/readwrite.go b/internal/datastore/memdb/readwrite.go index 6cabafe21a..c52abb5466 100644 --- a/internal/datastore/memdb/readwrite.go +++ b/internal/datastore/memdb/readwrite.go @@ -46,6 +46,7 @@ func (rwt *memdbReadWriteTx) write(tx *memdb.Txn, mutations ...*core.RelationTup mutation.Tuple.Subject.ObjectId, mutation.Tuple.Subject.Relation, rwt.toCaveatReference(mutation), + rwt.toIntegrity(mutation), } found, err := tx.First( @@ -119,6 +120,18 @@ func (rwt *memdbReadWriteTx) toCaveatReference(mutation *core.RelationTupleUpdat return cr } +func (rwt *memdbReadWriteTx) toIntegrity(mutation *core.RelationTupleUpdate) *relationshipIntegrity { + var ri *relationshipIntegrity + if mutation.Tuple.Integrity != nil { + ri = &relationshipIntegrity{ + keyID: mutation.Tuple.Integrity.KeyId, + hash: mutation.Tuple.Integrity.Hash, + timestamp: mutation.Tuple.Integrity.HashedAt.AsTime(), + } + } + return ri +} + func (rwt *memdbReadWriteTx) DeleteRelationships(_ context.Context, filter *v1.RelationshipFilter, opts ...options.DeleteOptionsOption) (bool, error) { rwt.mustLock() defer rwt.Unlock() diff --git a/internal/datastore/memdb/schema.go b/internal/datastore/memdb/schema.go index 27b85872dd..5dc441dbd7 100644 --- a/internal/datastore/memdb/schema.go +++ b/internal/datastore/memdb/schema.go @@ -1,11 +1,14 @@ package memdb import ( + "time" + v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" "github.com/hashicorp/go-memdb" "github.com/jzelinskie/stringz" "github.com/rs/zerolog" "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" "github.com/authzed/spicedb/pkg/datastore" core "github.com/authzed/spicedb/pkg/proto/core/v1" @@ -51,6 +54,7 @@ type relationship struct { subjectObjectID string subjectRelation string caveat *contextualizedCaveat + integrity *relationshipIntegrity } type contextualizedCaveat struct { @@ -58,6 +62,24 @@ type contextualizedCaveat struct { context map[string]any } +type relationshipIntegrity struct { + keyID string + hash []byte + timestamp time.Time +} + +func (ri relationshipIntegrity) MarshalZerologObject(e *zerolog.Event) { + e.Str("keyID", ri.keyID).Bytes("hash", ri.hash).Time("timestamp", ri.timestamp) +} + +func (ri relationshipIntegrity) RelationshipIntegrity() *core.RelationshipIntegrity { + return &core.RelationshipIntegrity{ + KeyId: ri.keyID, + Hash: ri.hash, + HashedAt: timestamppb.New(ri.timestamp), + } +} + func (cr *contextualizedCaveat) ContextualizedCaveat() (*core.ContextualizedCaveat, error) { if cr == nil { return nil, nil @@ -107,6 +129,9 @@ func (r relationship) RelationTuple() (*core.RelationTuple, error) { if err != nil { return nil, err } + + ig := r.integrity.RelationshipIntegrity() + return &core.RelationTuple{ ResourceAndRelation: &core.ObjectAndRelation{ Namespace: r.namespace, @@ -118,7 +143,8 @@ func (r relationship) RelationTuple() (*core.RelationTuple, error) { ObjectId: r.subjectObjectID, Relation: r.subjectRelation, }, - Caveat: cr, + Caveat: cr, + Integrity: ig, }, nil } diff --git a/pkg/datastore/test/datastore.go b/pkg/datastore/test/datastore.go index 05f034c6ba..62325caa10 100644 --- a/pkg/datastore/test/datastore.go +++ b/pkg/datastore/test/datastore.go @@ -116,6 +116,7 @@ func AllWithExceptions(t *testing.T, tester DatastoreTester, except Categories) t.Run("TestDeleteRelationshipsWithVariousFilters", func(t *testing.T) { DeleteRelationshipsWithVariousFiltersTest(t, tester) }) t.Run("TestTouchTypedAlreadyExistingWithoutCaveat", func(t *testing.T) { TypedTouchAlreadyExistingTest(t, tester) }) t.Run("TestTouchTypedAlreadyExistingWithCaveat", func(t *testing.T) { TypedTouchAlreadyExistingWithCaveatTest(t, tester) }) + t.Run("TestRelationshipIntegrityInfo", func(t *testing.T) { RelationshipIntegrityInfoTest(t, tester) }) t.Run("TestMultipleReadsInRWT", func(t *testing.T) { MultipleReadsInRWTTest(t, tester) }) t.Run("TestConcurrentWriteSerialization", func(t *testing.T) { ConcurrentWriteSerializationTest(t, tester) }) diff --git a/pkg/datastore/test/tuples.go b/pkg/datastore/test/tuples.go index d9429dd7a1..b1cc6997b0 100644 --- a/pkg/datastore/test/tuples.go +++ b/pkg/datastore/test/tuples.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/require" "golang.org/x/sync/errgroup" "google.golang.org/grpc/codes" + "google.golang.org/protobuf/types/known/timestamppb" "github.com/authzed/grpcutil" @@ -1672,6 +1673,54 @@ func BulkDeleteRelationshipsTest(t *testing.T, tester DatastoreTester) { require.Nil(iter.Next(), "expected no results") } +func RelationshipIntegrityInfoTest(t *testing.T, tester DatastoreTester) { + require := require.New(t) + + rawDS, err := tester.New(0, veryLargeGCInterval, veryLargeGCWindow, 1) + require.NoError(err) + + ds, _ := testfixtures.StandardDatastoreWithSchema(rawDS, require) + ctx := context.Background() + + // Write a relationship with integrity information. + 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{ + KeyId: "key1", + Hash: []byte("hash1"), + HashedAt: timestamppb.New(timestamp), + } + return rwt.WriteRelationships(ctx, []*core.RelationTupleUpdate{ + tuple.Create(tpl), + }) + }) + require.NoError(err) + + // Read the relationship back and ensure the integrity information is present. + headRev, err := ds.HeadRevision(ctx) + require.NoError(err) + + reader := ds.SnapshotReader(headRev) + iter, err := reader.QueryRelationships(ctx, datastore.RelationshipsFilter{ + OptionalResourceType: "document", + OptionalResourceIds: []string{"foo"}, + OptionalResourceRelation: "viewer", + }) + require.NoError(err) + + tpl := iter.Next() + require.NotNil(tpl) + + require.NotNil(tpl.Integrity) + require.Equal("key1", tpl.Integrity.KeyId) + require.Equal([]byte("hash1"), tpl.Integrity.Hash) + require.Equal(timestamp, tpl.Integrity.HashedAt.AsTime()) + + iter.Close() +} + func onrToSubjectsFilter(onr *core.ObjectAndRelation) datastore.SubjectsFilter { return datastore.SubjectsFilter{ SubjectType: onr.Namespace,