github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/services/v1/relationships_test.go (about)

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