github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/namespace/canonicalization_test.go (about) 1 package namespace 2 3 import ( 4 "context" 5 "fmt" 6 "testing" 7 8 "github.com/stretchr/testify/require" 9 10 core "github.com/authzed/spicedb/pkg/proto/core/v1" 11 "github.com/authzed/spicedb/pkg/typesystem" 12 13 "github.com/authzed/spicedb/internal/datastore/memdb" 14 ns "github.com/authzed/spicedb/pkg/namespace" 15 "github.com/authzed/spicedb/pkg/schemadsl/compiler" 16 "github.com/authzed/spicedb/pkg/schemadsl/input" 17 ) 18 19 func TestCanonicalization(t *testing.T) { 20 testCases := []struct { 21 name string 22 toCheck *core.NamespaceDefinition 23 expectedError string 24 expectedCacheMap map[string]string 25 }{ 26 { 27 "empty canonicalization", 28 ns.Namespace( 29 "document", 30 ), 31 "", 32 map[string]string{}, 33 }, 34 { 35 "basic canonicalization", 36 ns.Namespace( 37 "document", 38 ns.MustRelation("owner", nil), 39 ns.MustRelation("viewer", nil), 40 ns.MustRelation("edit", ns.Union( 41 ns.ComputedUserset("owner"), 42 )), 43 ns.MustRelation("view", ns.Union( 44 ns.ComputedUserset("viewer"), 45 ns.ComputedUserset("edit"), 46 )), 47 ), 48 "", 49 map[string]string{ 50 "owner": "owner", 51 "viewer": "viewer", 52 "edit": computedKeyPrefix + "596a8660f9a0c085", 53 "view": computedKeyPrefix + "0cb51da20fc9f20f", 54 }, 55 }, 56 { 57 "canonicalization with aliases", 58 ns.Namespace( 59 "document", 60 ns.MustRelation("owner", nil), 61 ns.MustRelation("viewer", nil), 62 ns.MustRelation("edit", ns.Union( 63 ns.ComputedUserset("owner"), 64 )), 65 ns.MustRelation("other_edit", ns.Union( 66 ns.ComputedUserset("owner"), 67 )), 68 ), 69 "", 70 map[string]string{ 71 "owner": "owner", 72 "viewer": "viewer", 73 "edit": computedKeyPrefix + "596a8660f9a0c085", 74 "other_edit": computedKeyPrefix + "596a8660f9a0c085", 75 }, 76 }, 77 { 78 "canonicalization with nested aliases", 79 ns.Namespace( 80 "document", 81 ns.MustRelation("owner", nil), 82 ns.MustRelation("viewer", nil), 83 ns.MustRelation("edit", ns.Union( 84 ns.ComputedUserset("owner"), 85 )), 86 ns.MustRelation("other_edit", ns.Union( 87 ns.ComputedUserset("edit"), 88 )), 89 ), 90 "", 91 map[string]string{ 92 "owner": "owner", 93 "viewer": "viewer", 94 "edit": computedKeyPrefix + "596a8660f9a0c085", 95 "other_edit": computedKeyPrefix + "596a8660f9a0c085", 96 }, 97 }, 98 { 99 "canonicalization with same union expressions", 100 ns.Namespace( 101 "document", 102 ns.MustRelation("owner", nil), 103 ns.MustRelation("viewer", nil), 104 ns.MustRelation("first", ns.Union( 105 ns.ComputedUserset("owner"), 106 ns.ComputedUserset("viewer"), 107 )), 108 ns.MustRelation("second", ns.Union( 109 ns.ComputedUserset("viewer"), 110 ns.ComputedUserset("owner"), 111 )), 112 ), 113 "", 114 map[string]string{ 115 "owner": "owner", 116 "viewer": "viewer", 117 "first": computedKeyPrefix + "62152badef526205", 118 "second": computedKeyPrefix + "62152badef526205", 119 }, 120 }, 121 { 122 "canonicalization with same union expressions due to aliasing", 123 ns.Namespace( 124 "document", 125 ns.MustRelation("owner", nil), 126 ns.MustRelation("viewer", nil), 127 ns.MustRelation("edit", ns.Union( 128 ns.ComputedUserset("owner"), 129 )), 130 ns.MustRelation("first", ns.Union( 131 ns.ComputedUserset("edit"), 132 ns.ComputedUserset("viewer"), 133 )), 134 ns.MustRelation("second", ns.Union( 135 ns.ComputedUserset("viewer"), 136 ns.ComputedUserset("edit"), 137 )), 138 ), 139 "", 140 map[string]string{ 141 "owner": "owner", 142 "viewer": "viewer", 143 "edit": computedKeyPrefix + "596a8660f9a0c085", 144 "first": computedKeyPrefix + "62152badef526205", 145 "second": computedKeyPrefix + "62152badef526205", 146 }, 147 }, 148 { 149 "canonicalization with same intersection expressions", 150 ns.Namespace( 151 "document", 152 ns.MustRelation("owner", nil), 153 ns.MustRelation("viewer", nil), 154 ns.MustRelation("first", ns.Intersection( 155 ns.ComputedUserset("owner"), 156 ns.ComputedUserset("viewer"), 157 )), 158 ns.MustRelation("second", ns.Intersection( 159 ns.ComputedUserset("viewer"), 160 ns.ComputedUserset("owner"), 161 )), 162 ), 163 "", 164 map[string]string{ 165 "owner": "owner", 166 "viewer": "viewer", 167 "first": computedKeyPrefix + "18cf8af8ff02bad0", 168 "second": computedKeyPrefix + "18cf8af8ff02bad0", 169 }, 170 }, 171 { 172 "canonicalization with different expressions", 173 ns.Namespace( 174 "document", 175 ns.MustRelation("owner", nil), 176 ns.MustRelation("viewer", nil), 177 ns.MustRelation("first", ns.Exclusion( 178 ns.ComputedUserset("owner"), 179 ns.ComputedUserset("viewer"), 180 )), 181 ns.MustRelation("second", ns.Exclusion( 182 ns.ComputedUserset("viewer"), 183 ns.ComputedUserset("owner"), 184 )), 185 ), 186 "", 187 map[string]string{ 188 "owner": "owner", 189 "viewer": "viewer", 190 "first": computedKeyPrefix + "2cd554a00f7f2d94", 191 "second": computedKeyPrefix + "69d4722141f74043", 192 }, 193 }, 194 { 195 "canonicalization with arrow expressions", 196 ns.Namespace( 197 "document", 198 ns.MustRelation("owner", nil), 199 ns.MustRelation("viewer", nil), 200 ns.MustRelation("first", ns.Union( 201 ns.TupleToUserset("owner", "something"), 202 )), 203 ns.MustRelation("second", ns.Union( 204 ns.TupleToUserset("owner", "something"), 205 )), 206 ns.MustRelation("difftuple", ns.Union( 207 ns.TupleToUserset("viewer", "something"), 208 )), 209 ns.MustRelation("diffrel", ns.Union( 210 ns.TupleToUserset("owner", "somethingelse"), 211 )), 212 ), 213 "", 214 map[string]string{ 215 "owner": "owner", 216 "viewer": "viewer", 217 "first": computedKeyPrefix + "9fd2b03cabeb2e42", 218 "second": computedKeyPrefix + "9fd2b03cabeb2e42", 219 "diffrel": computedKeyPrefix + "ab86f3a255f31908", 220 "difftuple": computedKeyPrefix + "dddc650e89a7bf1a", 221 }, 222 }, 223 { 224 "canonicalization with same nested union expressions", 225 ns.Namespace( 226 "document", 227 ns.MustRelation("owner", nil), 228 ns.MustRelation("editor", nil), 229 ns.MustRelation("viewer", nil), 230 ns.MustRelation("first", ns.Union( 231 ns.ComputedUserset("owner"), 232 ns.Rewrite( 233 ns.Union( 234 ns.ComputedUserset("editor"), 235 ns.ComputedUserset("viewer"), 236 ), 237 ), 238 )), 239 ns.MustRelation("second", ns.Union( 240 ns.ComputedUserset("viewer"), 241 ns.Rewrite( 242 ns.Union( 243 ns.ComputedUserset("editor"), 244 ns.ComputedUserset("owner"), 245 ), 246 ), 247 )), 248 ), 249 "", 250 map[string]string{ 251 "owner": "owner", 252 "editor": "editor", 253 "viewer": "viewer", 254 "first": computedKeyPrefix + "4c49627fbdbaf248", 255 "second": computedKeyPrefix + "4c49627fbdbaf248", 256 }, 257 }, 258 { 259 "canonicalization with same nested intersection expressions", 260 ns.Namespace( 261 "document", 262 ns.MustRelation("owner", nil), 263 ns.MustRelation("editor", nil), 264 ns.MustRelation("viewer", nil), 265 ns.MustRelation("first", ns.Intersection( 266 ns.ComputedUserset("owner"), 267 ns.Rewrite( 268 ns.Intersection( 269 ns.ComputedUserset("editor"), 270 ns.ComputedUserset("viewer"), 271 ), 272 ), 273 )), 274 ns.MustRelation("second", ns.Intersection( 275 ns.ComputedUserset("viewer"), 276 ns.Rewrite( 277 ns.Intersection( 278 ns.ComputedUserset("editor"), 279 ns.ComputedUserset("owner"), 280 ), 281 ), 282 )), 283 ), 284 "", 285 map[string]string{ 286 "owner": "owner", 287 "editor": "editor", 288 "viewer": "viewer", 289 "first": computedKeyPrefix + "7c52666bb7593f0a", 290 "second": computedKeyPrefix + "7c52666bb7593f0a", 291 }, 292 }, 293 { 294 "canonicalization with different nested exclusion expressions", 295 ns.Namespace( 296 "document", 297 ns.MustRelation("owner", nil), 298 ns.MustRelation("editor", nil), 299 ns.MustRelation("viewer", nil), 300 ns.MustRelation("first", ns.Exclusion( 301 ns.ComputedUserset("owner"), 302 ns.Rewrite( 303 ns.Exclusion( 304 ns.ComputedUserset("editor"), 305 ns.ComputedUserset("viewer"), 306 ), 307 ), 308 )), 309 ns.MustRelation("second", ns.Exclusion( 310 ns.ComputedUserset("viewer"), 311 ns.Rewrite( 312 ns.Exclusion( 313 ns.ComputedUserset("editor"), 314 ns.ComputedUserset("owner"), 315 ), 316 ), 317 )), 318 ), 319 "", 320 map[string]string{ 321 "owner": "owner", 322 "editor": "editor", 323 "viewer": "viewer", 324 "first": computedKeyPrefix + "bb955307170373ae", 325 "second": computedKeyPrefix + "6ccf7bece2e540a1", 326 }, 327 }, 328 { 329 "canonicalization with nil expressions", 330 ns.Namespace( 331 "document", 332 ns.MustRelation("owner", nil), 333 ns.MustRelation("editor", nil), 334 ns.MustRelation("viewer", nil), 335 ns.MustRelation("first", ns.Union( 336 ns.ComputedUserset("owner"), 337 ns.Nil(), 338 )), 339 ns.MustRelation("second", ns.Union( 340 ns.ComputedUserset("viewer"), 341 ns.Nil(), 342 )), 343 ), 344 "", 345 map[string]string{ 346 "owner": "owner", 347 "editor": "editor", 348 "viewer": "viewer", 349 "first": computedKeyPrefix + "95f5633117d42867", 350 "second": computedKeyPrefix + "f786018d066f37b4", 351 }, 352 }, 353 { 354 "canonicalization with same expressions with nil expressions", 355 ns.Namespace( 356 "document", 357 ns.MustRelation("owner", nil), 358 ns.MustRelation("editor", nil), 359 ns.MustRelation("viewer", nil), 360 ns.MustRelation("first", ns.Union( 361 ns.ComputedUserset("viewer"), 362 ns.Nil(), 363 )), 364 ns.MustRelation("second", ns.Union( 365 ns.ComputedUserset("viewer"), 366 ns.Nil(), 367 )), 368 ), 369 "", 370 map[string]string{ 371 "owner": "owner", 372 "editor": "editor", 373 "viewer": "viewer", 374 "first": computedKeyPrefix + "bfc8d945d7030961", 375 "second": computedKeyPrefix + "bfc8d945d7030961", 376 }, 377 }, 378 } 379 380 for _, tc := range testCases { 381 tc := tc 382 t.Run(tc.name, func(t *testing.T) { 383 require := require.New(t) 384 385 ds, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC) 386 require.NoError(err) 387 388 ctx := context.Background() 389 390 lastRevision, err := ds.HeadRevision(context.Background()) 391 require.NoError(err) 392 393 ts, err := typesystem.NewNamespaceTypeSystem(tc.toCheck, typesystem.ResolverForDatastoreReader(ds.SnapshotReader(lastRevision))) 394 require.NoError(err) 395 396 vts, terr := ts.Validate(ctx) 397 require.NoError(terr) 398 399 aliases, aerr := computePermissionAliases(vts) 400 require.NoError(aerr) 401 402 cacheKeys, cerr := computeCanonicalCacheKeys(vts, aliases) 403 require.NoError(cerr) 404 require.Equal(tc.expectedCacheMap, cacheKeys) 405 }) 406 } 407 } 408 409 const comparisonSchemaTemplate = ` 410 definition document { 411 relation viewer: document 412 relation editor: document 413 relation owner: document 414 415 permission first = %s 416 permission second = %s 417 } 418 ` 419 420 func TestCanonicalizationComparison(t *testing.T) { 421 testCases := []struct { 422 name string 423 first string 424 second string 425 expectedSame bool 426 }{ 427 { 428 "same relation", 429 "viewer", 430 "viewer", 431 true, 432 }, 433 { 434 "different relation", 435 "viewer", 436 "owner", 437 false, 438 }, 439 { 440 "union associativity", 441 "viewer + owner", 442 "owner + viewer", 443 true, 444 }, 445 { 446 "intersection associativity", 447 "viewer & owner", 448 "owner & viewer", 449 true, 450 }, 451 { 452 "exclusion non-associativity", 453 "viewer - owner", 454 "owner - viewer", 455 false, 456 }, 457 { 458 "nested union associativity", 459 "viewer + (owner + editor)", 460 "owner + (viewer + editor)", 461 true, 462 }, 463 { 464 "nested intersection associativity", 465 "viewer & (owner & editor)", 466 "owner & (viewer & editor)", 467 true, 468 }, 469 { 470 "nested union associativity 2", 471 "(viewer + owner) + editor", 472 "(owner + viewer) + editor", 473 true, 474 }, 475 { 476 "nested intersection associativity 2", 477 "(viewer & owner) & editor", 478 "(owner & viewer) & editor", 479 true, 480 }, 481 { 482 "nested exclusion non-associativity", 483 "viewer - (owner - editor)", 484 "viewer - owner - editor", 485 false, 486 }, 487 { 488 "nested exclusion non-associativity with nil", 489 "viewer - (owner - nil)", 490 "viewer - owner - nil", 491 false, 492 }, 493 { 494 "nested intersection associativity with nil", 495 "(viewer & owner) & nil", 496 "(owner & viewer) & nil", 497 true, 498 }, 499 { 500 "nested intersection associativity with nil 2", 501 "(nil & owner) & editor", 502 "(owner & nil) & editor", 503 true, 504 }, 505 } 506 507 for _, tc := range testCases { 508 tc := tc 509 t.Run(tc.name, func(t *testing.T) { 510 require := require.New(t) 511 512 ds, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC) 513 require.NoError(err) 514 515 ctx := context.Background() 516 517 schemaText := fmt.Sprintf(comparisonSchemaTemplate, tc.first, tc.second) 518 compiled, err := compiler.Compile(compiler.InputSchema{ 519 Source: input.Source("schema"), 520 SchemaString: schemaText, 521 }, compiler.AllowUnprefixedObjectType()) 522 require.NoError(err) 523 524 lastRevision, err := ds.HeadRevision(context.Background()) 525 require.NoError(err) 526 527 ts, err := typesystem.NewNamespaceTypeSystem(compiled.ObjectDefinitions[0], typesystem.ResolverForDatastoreReader(ds.SnapshotReader(lastRevision))) 528 require.NoError(err) 529 530 vts, terr := ts.Validate(ctx) 531 require.NoError(terr) 532 533 aliases, aerr := computePermissionAliases(vts) 534 require.NoError(aerr) 535 536 cacheKeys, cerr := computeCanonicalCacheKeys(vts, aliases) 537 require.NoError(cerr) 538 require.True((cacheKeys["first"] == cacheKeys["second"]) == tc.expectedSame) 539 }) 540 } 541 }