github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/pkg/datastore/test/caveat.go (about)

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