github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/graph/resourcesubjectsmap_test.go (about) 1 package graph 2 3 import ( 4 "fmt" 5 "sort" 6 "strings" 7 "testing" 8 9 "github.com/stretchr/testify/require" 10 11 core "github.com/authzed/spicedb/pkg/proto/core/v1" 12 v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" 13 "github.com/authzed/spicedb/pkg/testutil" 14 "github.com/authzed/spicedb/pkg/tuple" 15 ) 16 17 func TestResourcesSubjectsMapBasic(t *testing.T) { 18 rsm := newResourcesSubjectMap(&core.RelationReference{ 19 Namespace: "document", 20 Relation: "view", 21 }) 22 23 require.Equal(t, rsm.resourceType.Namespace, "document") 24 require.Equal(t, rsm.resourceType.Relation, "view") 25 require.Equal(t, 0, rsm.len()) 26 27 rsm.addSubjectIDAsFoundResourceID("first") 28 require.Equal(t, 1, rsm.len()) 29 30 rsm.addSubjectIDAsFoundResourceID("second") 31 require.Equal(t, 2, rsm.len()) 32 33 err := rsm.addRelationship(tuple.MustParse("document:third#view@user:tom")) 34 require.NoError(t, err) 35 require.Equal(t, 3, rsm.len()) 36 37 err = rsm.addRelationship(tuple.MustParse("document:fourth#view@user:sarah[somecaveat]")) 38 require.NoError(t, err) 39 require.Equal(t, 4, rsm.len()) 40 41 locked := rsm.asReadOnly() 42 require.False(t, locked.isEmpty()) 43 44 directAsResources := locked.asReachableResources(true) 45 testutil.RequireProtoSlicesEqual(t, []*v1.ReachableResource{ 46 { 47 ResourceId: "first", 48 ResultStatus: v1.ReachableResource_HAS_PERMISSION, 49 ForSubjectIds: []string{"first"}, 50 }, 51 { 52 ResourceId: "fourth", 53 ResultStatus: v1.ReachableResource_REQUIRES_CHECK, 54 ForSubjectIds: []string{"sarah"}, 55 }, 56 { 57 ResourceId: "second", 58 ResultStatus: v1.ReachableResource_HAS_PERMISSION, 59 ForSubjectIds: []string{"second"}, 60 }, 61 { 62 ResourceId: "third", 63 ResultStatus: v1.ReachableResource_HAS_PERMISSION, 64 ForSubjectIds: []string{"tom"}, 65 }, 66 }, directAsResources, nil, "different resources") 67 68 notDirectAsResources := locked.asReachableResources(false) 69 testutil.RequireProtoSlicesEqual(t, []*v1.ReachableResource{ 70 { 71 ResourceId: "first", 72 ResultStatus: v1.ReachableResource_REQUIRES_CHECK, 73 ForSubjectIds: []string{"first"}, 74 }, 75 { 76 ResourceId: "fourth", 77 ResultStatus: v1.ReachableResource_REQUIRES_CHECK, 78 ForSubjectIds: []string{"sarah"}, 79 }, 80 { 81 ResourceId: "second", 82 ResultStatus: v1.ReachableResource_REQUIRES_CHECK, 83 ForSubjectIds: []string{"second"}, 84 }, 85 { 86 ResourceId: "third", 87 ResultStatus: v1.ReachableResource_REQUIRES_CHECK, 88 ForSubjectIds: []string{"tom"}, 89 }, 90 }, notDirectAsResources, nil, "different resources") 91 } 92 93 func TestResourcesSubjectsMapAsReachableResources(t *testing.T) { 94 tcs := []struct { 95 name string 96 rels []*core.RelationTuple 97 expected []*v1.ReachableResource 98 }{ 99 { 100 "empty", 101 []*core.RelationTuple{}, 102 []*v1.ReachableResource{}, 103 }, 104 { 105 "basic", 106 []*core.RelationTuple{ 107 tuple.MustParse("document:first#view@user:tom"), 108 tuple.MustParse("document:second#view@user:sarah"), 109 }, 110 []*v1.ReachableResource{ 111 { 112 ResourceId: "first", 113 ResultStatus: v1.ReachableResource_HAS_PERMISSION, 114 ForSubjectIds: []string{"tom"}, 115 }, 116 { 117 ResourceId: "second", 118 ResultStatus: v1.ReachableResource_HAS_PERMISSION, 119 ForSubjectIds: []string{"sarah"}, 120 }, 121 }, 122 }, 123 { 124 "caveated and non-caveated", 125 []*core.RelationTuple{ 126 tuple.MustParse("document:first#view@user:tom"), 127 tuple.MustParse("document:first#view@user:sarah[somecaveat]"), 128 }, 129 []*v1.ReachableResource{ 130 { 131 ResourceId: "first", 132 ResultStatus: v1.ReachableResource_HAS_PERMISSION, 133 ForSubjectIds: []string{"tom"}, 134 }, 135 }, 136 }, 137 { 138 "all caveated", 139 []*core.RelationTuple{ 140 tuple.MustParse("document:first#view@user:tom[anothercaveat]"), 141 tuple.MustParse("document:first#view@user:sarah[somecaveat]"), 142 }, 143 []*v1.ReachableResource{ 144 { 145 ResourceId: "first", 146 ResultStatus: v1.ReachableResource_REQUIRES_CHECK, 147 ForSubjectIds: []string{"sarah", "tom"}, 148 }, 149 }, 150 }, 151 { 152 "full", 153 []*core.RelationTuple{ 154 tuple.MustParse("document:first#view@user:tom[anothercaveat]"), 155 tuple.MustParse("document:first#view@user:sarah[somecaveat]"), 156 tuple.MustParse("document:second#view@user:tom"), 157 tuple.MustParse("document:second#view@user:sarah[somecaveat]"), 158 tuple.MustParse("document:third#view@user:tom"), 159 tuple.MustParse("document:third#view@user:sarah"), 160 }, 161 []*v1.ReachableResource{ 162 { 163 ResourceId: "first", 164 ResultStatus: v1.ReachableResource_REQUIRES_CHECK, 165 ForSubjectIds: []string{"sarah", "tom"}, 166 }, 167 { 168 ResourceId: "second", 169 ResultStatus: v1.ReachableResource_HAS_PERMISSION, 170 ForSubjectIds: []string{"tom"}, 171 }, 172 { 173 ResourceId: "third", 174 ResultStatus: v1.ReachableResource_HAS_PERMISSION, 175 ForSubjectIds: []string{"tom", "sarah"}, 176 }, 177 }, 178 }, 179 } 180 181 for _, tc := range tcs { 182 tc := tc 183 t.Run(tc.name, func(t *testing.T) { 184 for _, isDirectEntrypoint := range []bool{true, false} { 185 isDirectEntrypoint := isDirectEntrypoint 186 t.Run(fmt.Sprintf("%v", isDirectEntrypoint), func(t *testing.T) { 187 rsm := newResourcesSubjectMap(&core.RelationReference{ 188 Namespace: "document", 189 Relation: "view", 190 }) 191 192 for _, rel := range tc.rels { 193 err := rsm.addRelationship(rel) 194 require.NoError(t, err) 195 } 196 197 expected := make([]*v1.ReachableResource, 0, len(tc.expected)) 198 for _, expectedResource := range tc.expected { 199 cloned := expectedResource.CloneVT() 200 if !isDirectEntrypoint { 201 cloned.ResultStatus = v1.ReachableResource_REQUIRES_CHECK 202 } 203 expected = append(expected, cloned) 204 } 205 206 locked := rsm.asReadOnly() 207 resources := locked.asReachableResources(isDirectEntrypoint) 208 testutil.RequireProtoSlicesEqual(t, expected, resources, sortByResource, "different resources") 209 }) 210 } 211 }) 212 } 213 } 214 215 func TestResourcesSubjectsMapMapFoundResources(t *testing.T) { 216 tcs := []struct { 217 name string 218 rels []*core.RelationTuple 219 foundResources []*v1.ReachableResource 220 expected []*v1.ReachableResource 221 }{ 222 { 223 "empty", 224 []*core.RelationTuple{}, 225 []*v1.ReachableResource{}, 226 []*v1.ReachableResource{}, 227 }, 228 { 229 "basic no caveats", 230 []*core.RelationTuple{ 231 tuple.MustParse("group:firstgroup#member@organization:foo"), 232 tuple.MustParse("group:firstgroup#member@organization:bar"), 233 }, 234 []*v1.ReachableResource{ 235 { 236 ResourceId: "first", 237 ForSubjectIds: []string{"firstgroup"}, 238 ResultStatus: v1.ReachableResource_HAS_PERMISSION, 239 }, 240 }, 241 []*v1.ReachableResource{ 242 { 243 ResourceId: "first", 244 ForSubjectIds: []string{"foo", "bar"}, 245 ResultStatus: v1.ReachableResource_HAS_PERMISSION, 246 }, 247 }, 248 }, 249 { 250 "caveated all found", 251 []*core.RelationTuple{ 252 tuple.MustParse("group:firstgroup#member@organization:foo[somecaveat]"), 253 tuple.MustParse("group:firstgroup#member@organization:bar[somecvaeat]"), 254 }, 255 []*v1.ReachableResource{ 256 { 257 ResourceId: "first", 258 ForSubjectIds: []string{"firstgroup"}, 259 ResultStatus: v1.ReachableResource_HAS_PERMISSION, 260 }, 261 }, 262 []*v1.ReachableResource{ 263 { 264 ResourceId: "first", 265 ForSubjectIds: []string{"bar", "foo"}, 266 ResultStatus: v1.ReachableResource_REQUIRES_CHECK, 267 }, 268 }, 269 }, 270 { 271 "simple short circuit", 272 []*core.RelationTuple{ 273 tuple.MustParse("group:firstgroup#member@organization:foo[somecaveat]"), 274 tuple.MustParse("group:firstgroup#member@organization:bar"), 275 }, 276 []*v1.ReachableResource{ 277 { 278 ResourceId: "first", 279 ForSubjectIds: []string{"firstgroup"}, 280 ResultStatus: v1.ReachableResource_HAS_PERMISSION, 281 }, 282 }, 283 []*v1.ReachableResource{ 284 { 285 ResourceId: "first", 286 ForSubjectIds: []string{"bar"}, 287 ResultStatus: v1.ReachableResource_HAS_PERMISSION, 288 }, 289 }, 290 }, 291 { 292 "check requires on incoming subject", 293 []*core.RelationTuple{ 294 tuple.MustParse("group:firstgroup#member@organization:foo"), 295 tuple.MustParse("group:firstgroup#member@organization:bar"), 296 }, 297 []*v1.ReachableResource{ 298 { 299 ResourceId: "first", 300 ForSubjectIds: []string{"firstgroup"}, 301 ResultStatus: v1.ReachableResource_REQUIRES_CHECK, 302 }, 303 }, 304 []*v1.ReachableResource{ 305 { 306 ResourceId: "first", 307 ForSubjectIds: []string{"foo", "bar"}, 308 ResultStatus: v1.ReachableResource_REQUIRES_CHECK, 309 }, 310 }, 311 }, 312 { 313 "multi-input short circuit", 314 []*core.RelationTuple{ 315 tuple.MustParse("group:firstgroup#member@organization:foo"), 316 tuple.MustParse("group:firstgroup#member@organization:bar"), 317 tuple.MustParse("group:secondgroup#member@organization:foo[somecaveat]"), 318 }, 319 []*v1.ReachableResource{ 320 { 321 ResourceId: "somedoc", 322 ForSubjectIds: []string{"firstgroup", "secondgroup"}, 323 ResultStatus: v1.ReachableResource_HAS_PERMISSION, 324 }, 325 }, 326 []*v1.ReachableResource{ 327 { 328 ResourceId: "somedoc", 329 ForSubjectIds: []string{"foo", "bar"}, 330 ResultStatus: v1.ReachableResource_HAS_PERMISSION, 331 }, 332 }, 333 }, 334 { 335 "multi-input short circuit from single input", 336 []*core.RelationTuple{ 337 tuple.MustParse("group:firstgroup#member@organization:bar"), 338 tuple.MustParse("group:secondgroup#member@organization:foo[somecaveat]"), 339 }, 340 []*v1.ReachableResource{ 341 { 342 ResourceId: "somedoc", 343 ForSubjectIds: []string{"firstgroup", "secondgroup"}, 344 ResultStatus: v1.ReachableResource_HAS_PERMISSION, 345 }, 346 }, 347 []*v1.ReachableResource{ 348 { 349 ResourceId: "somedoc", 350 ForSubjectIds: []string{"bar"}, 351 ResultStatus: v1.ReachableResource_HAS_PERMISSION, 352 }, 353 }, 354 }, 355 { 356 "multi-input short circuit from single input with check required on parent", 357 []*core.RelationTuple{ 358 tuple.MustParse("group:firstgroup#member@organization:bar"), 359 tuple.MustParse("group:secondgroup#member@organization:foo[somecaveat]"), 360 }, 361 []*v1.ReachableResource{ 362 { 363 ResourceId: "somedoc", 364 ForSubjectIds: []string{"firstgroup", "secondgroup"}, 365 ResultStatus: v1.ReachableResource_REQUIRES_CHECK, 366 }, 367 }, 368 []*v1.ReachableResource{ 369 { 370 ResourceId: "somedoc", 371 ForSubjectIds: []string{"bar"}, 372 ResultStatus: v1.ReachableResource_REQUIRES_CHECK, 373 }, 374 }, 375 }, 376 { 377 "multi-input all caveated", 378 []*core.RelationTuple{ 379 tuple.MustParse("group:firstgroup#member@organization:bar[anothercaveat]"), 380 tuple.MustParse("group:secondgroup#member@organization:foo[somecaveat]"), 381 }, 382 []*v1.ReachableResource{ 383 { 384 ResourceId: "somedoc", 385 ForSubjectIds: []string{"firstgroup", "secondgroup"}, 386 ResultStatus: v1.ReachableResource_HAS_PERMISSION, 387 }, 388 }, 389 []*v1.ReachableResource{ 390 { 391 ResourceId: "somedoc", 392 ForSubjectIds: []string{"bar", "foo"}, 393 ResultStatus: v1.ReachableResource_REQUIRES_CHECK, 394 }, 395 }, 396 }, 397 } 398 399 for _, tc := range tcs { 400 tc := tc 401 t.Run(tc.name, func(t *testing.T) { 402 for _, isDirectEntrypoint := range []bool{true, false} { 403 isDirectEntrypoint := isDirectEntrypoint 404 t.Run(fmt.Sprintf("%v", isDirectEntrypoint), func(t *testing.T) { 405 rsm := newResourcesSubjectMap(&core.RelationReference{ 406 Namespace: "group", 407 Relation: "member", 408 }) 409 410 for _, rel := range tc.rels { 411 err := rsm.addRelationship(rel) 412 require.NoError(t, err) 413 } 414 415 expected := make([]*v1.ReachableResource, 0, len(tc.expected)) 416 for _, expectedResource := range tc.expected { 417 cloned := expectedResource.CloneVT() 418 if !isDirectEntrypoint { 419 cloned.ResultStatus = v1.ReachableResource_REQUIRES_CHECK 420 } 421 sort.Strings(cloned.ForSubjectIds) 422 expected = append(expected, cloned) 423 } 424 425 locked := rsm.asReadOnly() 426 427 resources := make([]*v1.ReachableResource, 0, len(tc.foundResources)) 428 for _, resource := range tc.foundResources { 429 r, err := locked.mapFoundResource(resource, isDirectEntrypoint) 430 require.NoError(t, err) 431 resources = append(resources, r) 432 } 433 434 for _, r := range resources { 435 sort.Strings(r.ForSubjectIds) 436 } 437 438 testutil.RequireProtoSlicesEqual(t, expected, resources, sortByResource, "different resources") 439 }) 440 } 441 }) 442 } 443 } 444 445 func sortByResource(first *v1.ReachableResource, second *v1.ReachableResource) int { 446 return strings.Compare(first.ResourceId, second.ResourceId) 447 } 448 449 func TestSubjectIDsToResourcesMap(t *testing.T) { 450 rsm := subjectIDsToResourcesMap(&core.RelationReference{ 451 Namespace: "document", 452 Relation: "view", 453 }, []string{"first", "second", "third"}) 454 455 require.Equal(t, 3, rsm.len()) 456 }