github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/dispatch/graph/check_test.go (about)

     1  package graph
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  	"testing"
     8  
     9  	"github.com/stretchr/testify/require"
    10  	"go.uber.org/goleak"
    11  
    12  	"github.com/authzed/spicedb/internal/datastore/common"
    13  	"github.com/authzed/spicedb/internal/datastore/memdb"
    14  	"github.com/authzed/spicedb/internal/dispatch"
    15  	"github.com/authzed/spicedb/internal/dispatch/caching"
    16  	"github.com/authzed/spicedb/internal/dispatch/keys"
    17  	"github.com/authzed/spicedb/internal/graph"
    18  	log "github.com/authzed/spicedb/internal/logging"
    19  	datastoremw "github.com/authzed/spicedb/internal/middleware/datastore"
    20  	"github.com/authzed/spicedb/internal/testfixtures"
    21  	"github.com/authzed/spicedb/pkg/datastore"
    22  	"github.com/authzed/spicedb/pkg/genutil/mapz"
    23  	core "github.com/authzed/spicedb/pkg/proto/core/v1"
    24  	v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1"
    25  	"github.com/authzed/spicedb/pkg/tuple"
    26  )
    27  
    28  var ONR = tuple.ObjectAndRelation
    29  
    30  var goleakIgnores = []goleak.Option{
    31  	goleak.IgnoreTopFunction("github.com/golang/glog.(*loggingT).flushDaemon"),
    32  	goleak.IgnoreTopFunction("github.com/outcaste-io/ristretto.(*lfuPolicy).processItems"),
    33  	goleak.IgnoreTopFunction("github.com/outcaste-io/ristretto.(*Cache).processItems"),
    34  	goleak.IgnoreCurrent(),
    35  }
    36  
    37  func TestSimpleCheck(t *testing.T) {
    38  	defer goleak.VerifyNone(t, goleakIgnores...)
    39  
    40  	type expected struct {
    41  		relation string
    42  		isMember bool
    43  	}
    44  
    45  	type userset struct {
    46  		userset  *core.ObjectAndRelation
    47  		expected []expected
    48  	}
    49  
    50  	testCases := []struct {
    51  		namespace string
    52  		objectID  string
    53  		usersets  []userset
    54  	}{
    55  		{"document", "masterplan", []userset{
    56  			{ONR("user", "product_manager", graph.Ellipsis), []expected{{"owner", true}, {"edit", true}, {"view", true}}},
    57  			{ONR("user", "chief_financial_officer", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", true}}},
    58  			{ONR("user", "owner", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", true}}},
    59  			{ONR("user", "legal", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", true}}},
    60  			{ONR("user", "vp_product", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", true}}},
    61  			{ONR("user", "eng_lead", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", true}}},
    62  			{ONR("user", "auditor", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", true}}},
    63  			{ONR("user", "villain", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", false}}},
    64  		}},
    65  		{"document", "healthplan", []userset{
    66  			{ONR("user", "product_manager", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", false}}},
    67  			{ONR("user", "chief_financial_officer", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", true}}},
    68  			{ONR("user", "owner", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", false}}},
    69  			{ONR("user", "legal", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", false}}},
    70  			{ONR("user", "vp_product", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", false}}},
    71  			{ONR("user", "eng_lead", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", false}}},
    72  			{ONR("user", "auditor", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", false}}},
    73  			{ONR("user", "villain", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", false}}},
    74  		}},
    75  		{"folder", "company", []userset{
    76  			{ONR("user", "product_manager", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", false}}},
    77  			{ONR("user", "chief_financial_officer", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", false}}},
    78  			{ONR("user", "owner", graph.Ellipsis), []expected{{"owner", true}, {"edit", true}, {"view", true}}},
    79  			{ONR("user", "legal", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", true}}},
    80  			{ONR("user", "vp_product", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", false}}},
    81  			{ONR("user", "eng_lead", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", false}}},
    82  			{ONR("user", "auditor", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", true}}},
    83  			{ONR("user", "villain", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", false}}},
    84  			{ONR("folder", "auditors", "viewer"), []expected{{"view", true}}},
    85  		}},
    86  		{"folder", "strategy", []userset{
    87  			{ONR("user", "product_manager", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", false}}},
    88  			{ONR("user", "chief_financial_officer", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", false}}},
    89  			{ONR("user", "owner", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", true}}},
    90  			{ONR("user", "legal", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", true}}},
    91  			{ONR("user", "vp_product", graph.Ellipsis), []expected{{"owner", true}, {"edit", true}, {"view", true}}},
    92  			{ONR("user", "eng_lead", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", false}}},
    93  			{ONR("user", "auditor", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", true}}},
    94  			{ONR("user", "villain", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", false}}},
    95  			{ONR("folder", "company", graph.Ellipsis), []expected{{"parent", true}}},
    96  		}},
    97  		{"folder", "isolated", []userset{
    98  			{ONR("user", "product_manager", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", false}}},
    99  			{ONR("user", "chief_financial_officer", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", false}}},
   100  			{ONR("user", "owner", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", false}}},
   101  			{ONR("user", "legal", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", false}}},
   102  			{ONR("user", "vp_product", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", false}}},
   103  			{ONR("user", "eng_lead", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", false}}},
   104  			{ONR("user", "auditor", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", false}}},
   105  			{ONR("user", "villain", graph.Ellipsis), []expected{{"owner", false}, {"edit", false}, {"view", true}}},
   106  		}},
   107  	}
   108  
   109  	for _, tc := range testCases {
   110  		for _, userset := range tc.usersets {
   111  			for _, expected := range userset.expected {
   112  				name := fmt.Sprintf(
   113  					"simple::%s:%s#%s@%s:%s#%s=>%t",
   114  					tc.namespace,
   115  					tc.objectID,
   116  					expected.relation,
   117  					userset.userset.Namespace,
   118  					userset.userset.ObjectId,
   119  					userset.userset.Relation,
   120  					expected.isMember,
   121  				)
   122  
   123  				tc := tc
   124  				userset := userset
   125  				expected := expected
   126  				t.Run(name, func(t *testing.T) {
   127  					require := require.New(t)
   128  
   129  					ctx, dispatch, revision := newLocalDispatcher(t)
   130  
   131  					checkResult, err := dispatch.DispatchCheck(ctx, &v1.DispatchCheckRequest{
   132  						ResourceRelation: RR(tc.namespace, expected.relation),
   133  						ResourceIds:      []string{tc.objectID},
   134  						ResultsSetting:   v1.DispatchCheckRequest_ALLOW_SINGLE_RESULT,
   135  						Subject:          userset.userset,
   136  						Metadata: &v1.ResolverMeta{
   137  							AtRevision:     revision.String(),
   138  							DepthRemaining: 50,
   139  						},
   140  					})
   141  
   142  					require.NoError(err)
   143  
   144  					isMember := false
   145  					if found, ok := checkResult.ResultsByResourceId[tc.objectID]; ok {
   146  						isMember = found.Membership == v1.ResourceCheckResult_MEMBER
   147  					}
   148  
   149  					require.Equal(expected.isMember, isMember, "For object %s in %v: ", tc.objectID, checkResult.ResultsByResourceId)
   150  					require.GreaterOrEqual(checkResult.Metadata.DepthRequired, uint32(1))
   151  				})
   152  			}
   153  		}
   154  	}
   155  }
   156  
   157  func TestMaxDepth(t *testing.T) {
   158  	require := require.New(t)
   159  
   160  	rawDS, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC)
   161  	require.NoError(err)
   162  
   163  	ds, _ := testfixtures.StandardDatastoreWithSchema(rawDS, require)
   164  
   165  	mutation := tuple.Create(tuple.Parse("folder:oops#parent@folder:oops"))
   166  
   167  	ctx := log.Logger.WithContext(datastoremw.ContextWithHandle(context.Background()))
   168  	require.NoError(datastoremw.SetInContext(ctx, ds))
   169  
   170  	revision, err := common.UpdateTuplesInDatastore(ctx, ds, mutation)
   171  	require.NoError(err)
   172  
   173  	dispatch := NewLocalOnlyDispatcher(10)
   174  
   175  	_, err = dispatch.DispatchCheck(ctx, &v1.DispatchCheckRequest{
   176  		ResourceRelation: RR("folder", "view"),
   177  		ResourceIds:      []string{"oops"},
   178  		ResultsSetting:   v1.DispatchCheckRequest_ALLOW_SINGLE_RESULT,
   179  		Subject:          ONR("user", "fake", graph.Ellipsis),
   180  		Metadata: &v1.ResolverMeta{
   181  			AtRevision:     revision.String(),
   182  			DepthRemaining: 50,
   183  		},
   184  	})
   185  
   186  	require.Error(err)
   187  }
   188  
   189  func TestCheckMetadata(t *testing.T) {
   190  	type expected struct {
   191  		relation              string
   192  		isMember              bool
   193  		expectedDispatchCount int
   194  		expectedDepthRequired int
   195  	}
   196  
   197  	type userset struct {
   198  		userset  *core.ObjectAndRelation
   199  		expected []expected
   200  	}
   201  
   202  	testCases := []struct {
   203  		namespace string
   204  		objectID  string
   205  		usersets  []userset
   206  	}{
   207  		{"document", "masterplan", []userset{
   208  			{
   209  				ONR("user", "product_manager", graph.Ellipsis),
   210  				[]expected{
   211  					{"owner", true, 1, 1},
   212  					{"edit", true, 3, 2},
   213  					{"view", true, 21, 5},
   214  				},
   215  			},
   216  			{
   217  				ONR("user", "owner", graph.Ellipsis),
   218  				[]expected{
   219  					{"owner", false, 1, 1},
   220  					{"edit", false, 3, 2},
   221  					{"view", true, 21, 5},
   222  				},
   223  			},
   224  		}},
   225  		{"folder", "strategy", []userset{
   226  			{
   227  				ONR("user", "vp_product", graph.Ellipsis),
   228  				[]expected{
   229  					{"owner", true, 1, 1},
   230  					{"edit", true, 3, 2},
   231  					{"view", true, 11, 4},
   232  				},
   233  			},
   234  		}},
   235  		{"folder", "company", []userset{
   236  			{
   237  				ONR("user", "unknown", graph.Ellipsis),
   238  				[]expected{
   239  					{"view", false, 6, 3},
   240  				},
   241  			},
   242  		}},
   243  	}
   244  
   245  	for _, tc := range testCases {
   246  		for _, userset := range tc.usersets {
   247  			for _, expected := range userset.expected {
   248  				name := fmt.Sprintf(
   249  					"metadata:%s:%s#%s@%s:%s#%s=>%t",
   250  					tc.namespace,
   251  					tc.objectID,
   252  					expected.relation,
   253  					userset.userset.Namespace,
   254  					userset.userset.ObjectId,
   255  					userset.userset.Relation,
   256  					expected.isMember,
   257  				)
   258  
   259  				tc := tc
   260  				userset := userset
   261  				expected := expected
   262  				t.Run(name, func(t *testing.T) {
   263  					require := require.New(t)
   264  
   265  					ctx, dispatch, revision := newLocalDispatcher(t)
   266  
   267  					checkResult, err := dispatch.DispatchCheck(ctx, &v1.DispatchCheckRequest{
   268  						ResourceRelation: RR(tc.namespace, expected.relation),
   269  						ResourceIds:      []string{tc.objectID},
   270  						ResultsSetting:   v1.DispatchCheckRequest_ALLOW_SINGLE_RESULT,
   271  						Subject:          userset.userset,
   272  						Metadata: &v1.ResolverMeta{
   273  							AtRevision:     revision.String(),
   274  							DepthRemaining: 50,
   275  						},
   276  					})
   277  
   278  					require.NoError(err)
   279  
   280  					isMember := false
   281  					if found, ok := checkResult.ResultsByResourceId[tc.objectID]; ok {
   282  						isMember = found.Membership == v1.ResourceCheckResult_MEMBER
   283  					}
   284  
   285  					require.Equal(expected.isMember, isMember)
   286  					require.GreaterOrEqual(expected.expectedDispatchCount, int(checkResult.Metadata.DispatchCount), "dispatch count mismatch")
   287  					require.GreaterOrEqual(expected.expectedDepthRequired, int(checkResult.Metadata.DepthRequired), "depth required mismatch")
   288  				})
   289  			}
   290  		}
   291  	}
   292  }
   293  
   294  func addFrame(trace *v1.CheckDebugTrace, foundFrames *mapz.Set[string]) {
   295  	foundFrames.Insert(fmt.Sprintf("%s:%s#%s", trace.Request.ResourceRelation.Namespace, strings.Join(trace.Request.ResourceIds, ","), trace.Request.ResourceRelation.Relation))
   296  	for _, subTrace := range trace.SubProblems {
   297  		addFrame(subTrace, foundFrames)
   298  	}
   299  }
   300  
   301  func TestCheckDebugging(t *testing.T) {
   302  	type expectedFrame struct {
   303  		resourceType *core.RelationReference
   304  		resourceIDs  []string
   305  	}
   306  
   307  	testCases := []struct {
   308  		namespace      string
   309  		objectID       string
   310  		permission     string
   311  		subject        *core.ObjectAndRelation
   312  		expectedFrames []expectedFrame
   313  	}{
   314  		{
   315  			"document", "masterplan", "view",
   316  			ONR("user", "product_manager", graph.Ellipsis),
   317  			[]expectedFrame{
   318  				{
   319  					RR("document", "view"),
   320  					[]string{"masterplan"},
   321  				},
   322  				{
   323  					RR("document", "edit"),
   324  					[]string{"masterplan"},
   325  				},
   326  				{
   327  					RR("document", "owner"),
   328  					[]string{"masterplan"},
   329  				},
   330  			},
   331  		},
   332  		{
   333  			"document", "masterplan", "view_and_edit",
   334  			ONR("user", "product_manager", graph.Ellipsis),
   335  			[]expectedFrame{
   336  				{
   337  					RR("document", "view_and_edit"),
   338  					[]string{"masterplan"},
   339  				},
   340  			},
   341  		},
   342  		{
   343  			"document", "specialplan", "view_and_edit",
   344  			ONR("user", "multiroleguy", graph.Ellipsis),
   345  			[]expectedFrame{
   346  				{
   347  					RR("document", "view_and_edit"),
   348  					[]string{"specialplan"},
   349  				},
   350  				{
   351  					RR("document", "viewer_and_editor"),
   352  					[]string{"specialplan"},
   353  				},
   354  				{
   355  					RR("document", "edit"),
   356  					[]string{"specialplan"},
   357  				},
   358  				{
   359  					RR("document", "editor"),
   360  					[]string{"specialplan"},
   361  				},
   362  			},
   363  		},
   364  	}
   365  
   366  	for _, tc := range testCases {
   367  		name := fmt.Sprintf(
   368  			"debugging::%s:%s#%s@%s:%s#%s",
   369  			tc.namespace,
   370  			tc.objectID,
   371  			tc.permission,
   372  			tc.subject.Namespace,
   373  			tc.subject.ObjectId,
   374  			tc.subject.Relation,
   375  		)
   376  
   377  		tc := tc
   378  		t.Run(name, func(t *testing.T) {
   379  			require := require.New(t)
   380  
   381  			ctx, dispatch, revision := newLocalDispatcher(t)
   382  
   383  			checkResult, err := dispatch.DispatchCheck(ctx, &v1.DispatchCheckRequest{
   384  				ResourceRelation: RR(tc.namespace, tc.permission),
   385  				ResourceIds:      []string{tc.objectID},
   386  				ResultsSetting:   v1.DispatchCheckRequest_ALLOW_SINGLE_RESULT,
   387  				Subject:          tc.subject,
   388  				Metadata: &v1.ResolverMeta{
   389  					AtRevision:     revision.String(),
   390  					DepthRemaining: 50,
   391  				},
   392  				Debug: v1.DispatchCheckRequest_ENABLE_BASIC_DEBUGGING,
   393  			})
   394  
   395  			require.NoError(err)
   396  			require.NotNil(checkResult.Metadata.DebugInfo)
   397  			require.NotNil(checkResult.Metadata.DebugInfo.Check)
   398  			require.NotNil(checkResult.Metadata.DebugInfo.Check.Duration)
   399  
   400  			expectedFrames := mapz.NewSet[string]()
   401  			for _, expectedFrame := range tc.expectedFrames {
   402  				expectedFrames.Add(fmt.Sprintf("%s:%s#%s", expectedFrame.resourceType.Namespace, strings.Join(expectedFrame.resourceIDs, ","), expectedFrame.resourceType.Relation))
   403  			}
   404  
   405  			foundFrames := mapz.NewSet[string]()
   406  			addFrame(checkResult.Metadata.DebugInfo.Check, foundFrames)
   407  
   408  			require.Empty(expectedFrames.Subtract(foundFrames).AsSlice(), "missing expected frames: %v", expectedFrames.Subtract(foundFrames).AsSlice())
   409  		})
   410  	}
   411  }
   412  
   413  func newLocalDispatcherWithConcurrencyLimit(t testing.TB, concurrencyLimit uint16) (context.Context, dispatch.Dispatcher, datastore.Revision) {
   414  	rawDS, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC)
   415  	require.NoError(t, err)
   416  
   417  	ds, revision := testfixtures.StandardDatastoreWithData(rawDS, require.New(t))
   418  
   419  	dispatch := NewLocalOnlyDispatcher(concurrencyLimit)
   420  
   421  	cachingDispatcher, err := caching.NewCachingDispatcher(caching.DispatchTestCache(t), false, "", &keys.CanonicalKeyHandler{})
   422  	cachingDispatcher.SetDelegate(dispatch)
   423  	require.NoError(t, err)
   424  
   425  	ctx := log.Logger.WithContext(datastoremw.ContextWithHandle(context.Background()))
   426  	require.NoError(t, datastoremw.SetInContext(ctx, ds))
   427  
   428  	return ctx, cachingDispatcher, revision
   429  }
   430  
   431  func newLocalDispatcher(t testing.TB) (context.Context, dispatch.Dispatcher, datastore.Revision) {
   432  	return newLocalDispatcherWithConcurrencyLimit(t, 10)
   433  }
   434  
   435  func newLocalDispatcherWithSchemaAndRels(t testing.TB, schema string, rels []*core.RelationTuple) (context.Context, dispatch.Dispatcher, datastore.Revision) {
   436  	rawDS, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC)
   437  	require.NoError(t, err)
   438  
   439  	ds, revision := testfixtures.DatastoreFromSchemaAndTestRelationships(rawDS, schema, rels, require.New(t))
   440  
   441  	dispatch := NewLocalOnlyDispatcher(10)
   442  
   443  	cachingDispatcher, err := caching.NewCachingDispatcher(caching.DispatchTestCache(t), false, "", &keys.CanonicalKeyHandler{})
   444  	cachingDispatcher.SetDelegate(dispatch)
   445  	require.NoError(t, err)
   446  
   447  	ctx := log.Logger.WithContext(datastoremw.ContextWithHandle(context.Background()))
   448  	require.NoError(t, datastoremw.SetInContext(ctx, ds))
   449  
   450  	return ctx, cachingDispatcher, revision
   451  }