github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/services/v1/debug_test.go (about) 1 package v1_test 2 3 import ( 4 "context" 5 "sort" 6 "strings" 7 "testing" 8 9 "github.com/authzed/authzed-go/pkg/requestmeta" 10 "github.com/authzed/authzed-go/pkg/responsemeta" 11 v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" 12 "github.com/stretchr/testify/require" 13 "google.golang.org/grpc" 14 "google.golang.org/grpc/metadata" 15 "google.golang.org/protobuf/encoding/prototext" 16 "google.golang.org/protobuf/types/known/structpb" 17 18 "github.com/authzed/spicedb/internal/datastore/memdb" 19 tf "github.com/authzed/spicedb/internal/testfixtures" 20 "github.com/authzed/spicedb/internal/testserver" 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 "github.com/authzed/spicedb/pkg/tuple" 25 "github.com/authzed/spicedb/pkg/zedtoken" 26 ) 27 28 type debugCheckRequest struct { 29 resource *v1.ObjectReference 30 permission string 31 subject *v1.SubjectReference 32 caveatContext map[string]any 33 } 34 35 type rda func(req *require.Assertions, debugInfo *v1.DebugInformation) 36 37 type debugCheckInfo struct { 38 name string 39 checkRequest debugCheckRequest 40 expectedPermission v1.CheckPermissionResponse_Permissionship 41 expectedMinimumSubProblemCount int 42 runDebugAssertions []rda 43 } 44 45 func expectDebugFrames(permissionNames ...string) rda { 46 return func(req *require.Assertions, debugInfo *v1.DebugInformation) { 47 found := mapz.NewSet[string]() 48 for _, sp := range debugInfo.Check.GetSubProblems().Traces { 49 for _, permissionName := range permissionNames { 50 if sp.Permission == permissionName { 51 found.Insert(permissionName) 52 } 53 } 54 } 55 56 foundNames := found.AsSlice() 57 sort.Strings(permissionNames) 58 sort.Strings(foundNames) 59 60 req.Equal(permissionNames, foundNames, "missing expected subproblem(s)") 61 } 62 } 63 64 func expectCaveat(caveatExpression string) rda { 65 return func(req *require.Assertions, debugInfo *v1.DebugInformation) { 66 req.Equal(caveatExpression, debugInfo.Check.CaveatEvaluationInfo.Expression) 67 } 68 } 69 70 func expectMissingContext(context ...string) rda { 71 sort.Strings(context) 72 return func(req *require.Assertions, debugInfo *v1.DebugInformation) { 73 missing := debugInfo.Check.CaveatEvaluationInfo.PartialCaveatInfo.MissingRequiredContext 74 sort.Strings(missing) 75 req.Equal(context, missing) 76 } 77 } 78 79 func findFrame(checkTrace *v1.CheckDebugTrace, resourceType string, permissionName string) *v1.CheckDebugTrace { 80 if checkTrace.Resource.ObjectType == resourceType && checkTrace.Permission == permissionName { 81 return checkTrace 82 } 83 84 subProblems := checkTrace.GetSubProblems() 85 if subProblems != nil { 86 for _, sp := range subProblems.Traces { 87 found := findFrame(sp, resourceType, permissionName) 88 if found != nil { 89 return found 90 } 91 } 92 } 93 return nil 94 } 95 96 func TestCheckPermissionWithDebug(t *testing.T) { 97 tcs := []struct { 98 name string 99 schema string 100 relationships []*core.RelationTuple 101 toTest []debugCheckInfo 102 }{ 103 { 104 "basic debug", 105 `definition user {} 106 107 definition document { 108 relation editor: user 109 relation viewer: user 110 permission edit = editor 111 permission view = viewer + edit 112 } 113 `, 114 []*core.RelationTuple{ 115 tuple.MustParse("document:first#viewer@user:tom"), 116 tuple.MustParse("document:first#editor@user:sarah"), 117 }, 118 []debugCheckInfo{ 119 { 120 "sarah as editor", 121 debugCheckRequest{ 122 obj("document", "first"), 123 "view", 124 sub("user", "sarah", ""), 125 nil, 126 }, 127 v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 128 1, 129 []rda{expectDebugFrames("editor")}, 130 }, 131 { 132 "tom as viewer", 133 debugCheckRequest{ 134 obj("document", "first"), 135 "view", 136 sub("user", "tom", ""), 137 nil, 138 }, 139 v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 140 1, 141 []rda{expectDebugFrames("viewer")}, 142 }, 143 { 144 "benny as nothing", 145 debugCheckRequest{ 146 obj("document", "first"), 147 "view", 148 sub("user", "benny", ""), 149 nil, 150 }, 151 v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, 152 2, 153 []rda{expectDebugFrames("viewer", "editor")}, 154 }, 155 }, 156 }, 157 { 158 "caveated debug", 159 `definition user {} 160 161 caveat somecaveat(somecondition int) { 162 somecondition == 42 163 } 164 165 definition document { 166 relation another: user 167 relation viewer: user with somecaveat 168 permission view = viewer + another 169 } 170 `, 171 []*core.RelationTuple{ 172 tuple.MustParse("document:first#viewer@user:tom"), 173 tuple.MustParse("document:first#viewer@user:sarah[somecaveat]"), 174 }, 175 []debugCheckInfo{ 176 { 177 "sarah as viewer", 178 debugCheckRequest{ 179 obj("document", "first"), 180 "view", 181 sub("user", "sarah", ""), 182 map[string]any{ 183 "somecondition": 42, 184 }, 185 }, 186 v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 187 1, 188 []rda{expectDebugFrames("viewer"), expectCaveat("somecondition == 42")}, 189 }, 190 { 191 "sarah as not viewer", 192 debugCheckRequest{ 193 obj("document", "first"), 194 "view", 195 sub("user", "sarah", ""), 196 map[string]any{ 197 "somecondition": 31, 198 }, 199 }, 200 v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, 201 1, 202 []rda{expectDebugFrames("viewer"), expectCaveat("somecondition == 42")}, 203 }, 204 { 205 "sarah as conditional viewer", 206 debugCheckRequest{ 207 obj("document", "first"), 208 "view", 209 sub("user", "sarah", ""), 210 map[string]any{}, 211 }, 212 v1.CheckPermissionResponse_PERMISSIONSHIP_CONDITIONAL_PERMISSION, 213 1, 214 []rda{ 215 expectDebugFrames("viewer"), 216 expectCaveat("somecondition == 42"), 217 expectMissingContext("somecondition"), 218 }, 219 }, 220 }, 221 }, 222 { 223 "batched recursive", 224 `definition user {} 225 226 definition folder { 227 relation parent: folder 228 relation fviewer: user 229 permission fview = fviewer + parent->fview 230 } 231 232 definition document { 233 relation folder: folder 234 relation viewer: user 235 permission view = viewer + folder->fview 236 } 237 `, 238 []*core.RelationTuple{ 239 tuple.MustParse("document:first#viewer@user:tom"), 240 tuple.MustParse("document:first#folder@folder:f1"), 241 tuple.MustParse("document:first#folder@folder:f2"), 242 tuple.MustParse("document:first#folder@folder:f3"), 243 tuple.MustParse("document:first#folder@folder:f4"), 244 tuple.MustParse("document:first#folder@folder:f5"), 245 tuple.MustParse("document:first#folder@folder:f6"), 246 tuple.MustParse("folder:f1#parent@folder:f1p"), 247 tuple.MustParse("folder:f2#parent@folder:f2p"), 248 tuple.MustParse("folder:f3#parent@folder:f3p"), 249 tuple.MustParse("folder:f4#parent@folder:f4p"), 250 tuple.MustParse("folder:f5#parent@folder:f5p"), 251 tuple.MustParse("folder:f6#parent@folder:f6p"), 252 tuple.MustParse("folder:f6p#fviewer@user:sarah"), 253 }, 254 []debugCheckInfo{ 255 { 256 "tom as viewer", 257 debugCheckRequest{ 258 obj("document", "first"), 259 "view", 260 sub("user", "tom", ""), 261 nil, 262 }, 263 v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 264 1, 265 []rda{expectDebugFrames("viewer")}, 266 }, 267 { 268 "sarah as recursive viewer", 269 debugCheckRequest{ 270 obj("document", "first"), 271 "view", 272 sub("user", "sarah", ""), 273 nil, 274 }, 275 v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 276 1, 277 []rda{expectDebugFrames("fview")}, 278 }, 279 { 280 "benny as not viewer", 281 debugCheckRequest{ 282 obj("document", "first"), 283 "view", 284 sub("user", "benny", ""), 285 nil, 286 }, 287 v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, 288 2, 289 []rda{ 290 expectDebugFrames("viewer"), 291 func(req *require.Assertions, debugInfo *v1.DebugInformation) { 292 // Ensure that all the resource IDs are batched into a single frame. 293 found := findFrame(debugInfo.Check, "folder", "fview") 294 req.NotNil(found) 295 req.Equal(6, len(strings.Split(found.Resource.ObjectId, ","))) 296 297 // Ensure there are no more than 2 subframes, to verify we haven't 298 // accidentally fanned out. 299 req.LessOrEqual(2, len(found.GetSubProblems().Traces)) 300 }, 301 }, 302 }, 303 }, 304 }, 305 { 306 "ip address caveat", 307 `definition user {} 308 309 caveat has_valid_ip(user_ip ipaddress, allowed_range string) { 310 user_ip.in_cidr(allowed_range) 311 } 312 313 definition resource { 314 relation viewer: user | user with has_valid_ip 315 }`, 316 []*core.RelationTuple{ 317 tuple.MustParse(`resource:first#viewer@user:sarah[has_valid_ip:{"allowed_range":"192.168.0.0/16"}]`), 318 }, 319 []debugCheckInfo{ 320 { 321 "sarah as viewer", 322 debugCheckRequest{ 323 obj("resource", "first"), 324 "viewer", 325 sub("user", "sarah", ""), 326 map[string]any{ 327 "user_ip": "192.168.1.100", 328 }, 329 }, 330 v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 331 0, 332 nil, 333 }, 334 }, 335 }, 336 { 337 "multiple caveated debug", 338 `definition user {} 339 340 caveat somecaveat(somecondition int) { 341 somecondition == 42 342 } 343 344 caveat anothercaveat(anothercondition string) { 345 anothercondition == "hello world" 346 } 347 348 definition org { 349 relation member: user with somecaveat 350 } 351 352 definition document { 353 relation parent: org with anothercaveat 354 permission view = parent->member 355 } 356 `, 357 []*core.RelationTuple{ 358 tuple.MustParse("document:first#parent@org:someorg[anothercaveat]"), 359 tuple.MustParse("org:someorg#member@user:sarah[somecaveat]"), 360 }, 361 []debugCheckInfo{ 362 { 363 "sarah as viewer", 364 debugCheckRequest{ 365 obj("document", "first"), 366 "view", 367 sub("user", "sarah", ""), 368 map[string]any{ 369 "anothercondition": "hello world", 370 "somecondition": "42", 371 }, 372 }, 373 v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 374 1, 375 []rda{expectDebugFrames("member"), expectCaveat(`anothercondition == "hello world" && somecondition == 42`)}, 376 }, 377 { 378 "sarah as not viewer due to org", 379 debugCheckRequest{ 380 obj("document", "first"), 381 "view", 382 sub("user", "sarah", ""), 383 map[string]any{ 384 "anothercondition": "hi there", 385 "somecondition": "42", 386 }, 387 }, 388 v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, 389 1, 390 []rda{expectDebugFrames("member"), expectCaveat(`anothercondition == "hello world"`)}, 391 }, 392 { 393 "sarah as not viewer due to viewer", 394 debugCheckRequest{ 395 obj("document", "first"), 396 "view", 397 sub("user", "sarah", ""), 398 map[string]any{ 399 "anothercondition": "hello world", 400 "somecondition": "41", 401 }, 402 }, 403 v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, 404 1, 405 []rda{expectDebugFrames("member"), expectCaveat(`anothercondition == "hello world" && somecondition == 42`)}, 406 }, 407 { 408 "sarah as partially conditional viewer", 409 debugCheckRequest{ 410 obj("document", "first"), 411 "view", 412 sub("user", "sarah", ""), 413 map[string]any{ 414 "anothercondition": "hello world", 415 }, 416 }, 417 v1.CheckPermissionResponse_PERMISSIONSHIP_CONDITIONAL_PERMISSION, 418 1, 419 []rda{ 420 expectDebugFrames("member"), 421 expectMissingContext("somecondition"), 422 }, 423 }, 424 { 425 "sarah as fully conditional viewer", 426 debugCheckRequest{ 427 obj("document", "first"), 428 "view", 429 sub("user", "sarah", ""), 430 map[string]any{}, 431 }, 432 v1.CheckPermissionResponse_PERMISSIONSHIP_CONDITIONAL_PERMISSION, 433 1, 434 []rda{ 435 expectDebugFrames("member"), 436 expectMissingContext("anothercondition"), 437 }, 438 }, 439 }, 440 }, 441 } 442 443 for _, tc := range tcs { 444 tc := tc 445 t.Run(tc.name, func(t *testing.T) { 446 req := require.New(t) 447 conn, cleanup, _, revision := testserver.NewTestServer(req, testTimedeltas[0], memdb.DisableGC, true, 448 func(ds datastore.Datastore, require *require.Assertions) (datastore.Datastore, datastore.Revision) { 449 return tf.DatastoreFromSchemaAndTestRelationships(ds, tc.schema, tc.relationships, req) 450 }) 451 452 client := v1.NewPermissionsServiceClient(conn) 453 t.Cleanup(cleanup) 454 455 ctx := context.Background() 456 ctx = requestmeta.AddRequestHeaders(ctx, requestmeta.RequestDebugInformation) 457 458 for _, stc := range tc.toTest { 459 stc := stc 460 t.Run(stc.name, func(t *testing.T) { 461 req := require.New(t) 462 463 var trailer metadata.MD 464 caveatContext, err := structpb.NewStruct(stc.checkRequest.caveatContext) 465 req.NoError(err) 466 467 checkResp, err := client.CheckPermission(ctx, &v1.CheckPermissionRequest{ 468 Consistency: &v1.Consistency{ 469 Requirement: &v1.Consistency_AtLeastAsFresh{ 470 AtLeastAsFresh: zedtoken.MustNewFromRevision(revision), 471 }, 472 }, 473 Resource: stc.checkRequest.resource, 474 Permission: stc.checkRequest.permission, 475 Subject: stc.checkRequest.subject, 476 Context: caveatContext, 477 }, grpc.Trailer(&trailer)) 478 479 req.NoError(err) 480 req.Equal(stc.expectedPermission, checkResp.Permissionship) 481 482 encodedDebugInfo, err := responsemeta.GetResponseTrailerMetadataOrNil(trailer, responsemeta.DebugInformation) 483 req.NoError(err) 484 485 // DebugInfo No longer comes as part of the trailer 486 req.Nil(encodedDebugInfo) 487 488 debugInfo := checkResp.DebugTrace 489 req.NotEmpty(debugInfo.SchemaUsed) 490 491 req.Equal(stc.checkRequest.resource.ObjectType, debugInfo.Check.Resource.ObjectType) 492 req.Equal(stc.checkRequest.resource.ObjectId, debugInfo.Check.Resource.ObjectId) 493 req.Equal(stc.checkRequest.permission, debugInfo.Check.Permission) 494 495 if debugInfo.Check.GetSubProblems() != nil { 496 req.GreaterOrEqual(len(debugInfo.Check.GetSubProblems().Traces), stc.expectedMinimumSubProblemCount, "found traces: %s", prototext.Format(debugInfo.Check)) 497 } else { 498 req.Equal(0, stc.expectedMinimumSubProblemCount) 499 } 500 501 for _, rda := range stc.runDebugAssertions { 502 rda(req, debugInfo) 503 } 504 }) 505 } 506 }) 507 } 508 }