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 }