github.com/hernad/nomad@v1.6.112/ui/tests/unit/abilities/variable-test.js (about) 1 /** 2 * Copyright (c) HashiCorp, Inc. 3 * SPDX-License-Identifier: MPL-2.0 4 */ 5 6 /* eslint-disable ember/avoid-leaking-state-in-ember-objects */ 7 import { module, test } from 'qunit'; 8 import { setupTest } from 'ember-qunit'; 9 import Service from '@ember/service'; 10 import setupAbility from 'nomad-ui/tests/helpers/setup-ability'; 11 12 module('Unit | Ability | variable', function (hooks) { 13 setupTest(hooks); 14 setupAbility('variable')(hooks); 15 hooks.beforeEach(function () { 16 const mockSystem = Service.extend({ 17 features: [], 18 }); 19 20 this.owner.register('service:system', mockSystem); 21 }); 22 23 module('#list', function () { 24 test('it does not permit listing variables by default', function (assert) { 25 const mockToken = Service.extend({ 26 aclEnabled: true, 27 }); 28 29 this.owner.register('service:token', mockToken); 30 31 assert.notOk(this.ability.canList); 32 }); 33 34 test('it does not permit listing variables when token type is client', function (assert) { 35 const mockToken = Service.extend({ 36 aclEnabled: true, 37 selfToken: { type: 'client' }, 38 }); 39 40 this.owner.register('service:token', mockToken); 41 42 assert.notOk(this.ability.canList); 43 }); 44 45 test('it permits listing variables when token type is management', function (assert) { 46 const mockToken = Service.extend({ 47 aclEnabled: true, 48 selfToken: { type: 'management' }, 49 }); 50 51 this.owner.register('service:token', mockToken); 52 53 assert.ok(this.ability.canList); 54 }); 55 56 test('it permits listing variables when token has Variables with list capabilities in its rules', function (assert) { 57 const mockToken = Service.extend({ 58 aclEnabled: true, 59 selfToken: { type: 'client' }, 60 selfTokenPolicies: [ 61 { 62 rulesJSON: { 63 Namespaces: [ 64 { 65 Name: 'default', 66 Capabilities: [], 67 Variables: { 68 Paths: [{ Capabilities: ['list'], PathSpec: '*' }], 69 }, 70 }, 71 ], 72 }, 73 }, 74 ], 75 }); 76 77 this.owner.register('service:token', mockToken); 78 79 assert.ok(this.ability.canList); 80 }); 81 82 test('it does not permit listing variables when token has Variables alone in its rules', function (assert) { 83 const mockToken = Service.extend({ 84 aclEnabled: true, 85 selfToken: { type: 'client' }, 86 selfTokenPolicies: [ 87 { 88 rulesJSON: { 89 Namespaces: [ 90 { 91 Name: 'default', 92 Capabilities: [], 93 Variables: {}, 94 }, 95 ], 96 }, 97 }, 98 ], 99 }); 100 101 this.owner.register('service:token', mockToken); 102 103 assert.notOk(this.ability.canList); 104 }); 105 106 test('it does not permit listing variables when token has a null Variables block', function (assert) { 107 const mockToken = Service.extend({ 108 aclEnabled: true, 109 selfToken: { type: 'client' }, 110 selfTokenPolicies: [ 111 { 112 rulesJSON: { 113 Namespaces: [ 114 { 115 Name: 'default', 116 Capabilities: [], 117 Variables: null, 118 }, 119 ], 120 }, 121 }, 122 ], 123 }); 124 125 this.owner.register('service:token', mockToken); 126 127 assert.notOk(this.ability.canList); 128 }); 129 130 test('it does not permit listing variables when token has a Variables block where paths are without capabilities', function (assert) { 131 const mockToken = Service.extend({ 132 aclEnabled: true, 133 selfToken: { type: 'client' }, 134 selfTokenPolicies: [ 135 { 136 rulesJSON: { 137 Namespaces: [ 138 { 139 Name: 'default', 140 Capabilities: [], 141 Variables: { 142 Paths: [ 143 { Capabilities: [], PathSpec: '*' }, 144 { Capabilities: [], PathSpec: 'foo' }, 145 { Capabilities: [], PathSpec: 'foo/bar' }, 146 ], 147 }, 148 }, 149 ], 150 }, 151 }, 152 ], 153 }); 154 155 this.owner.register('service:token', mockToken); 156 157 assert.notOk(this.ability.canList); 158 }); 159 160 test('it does not permit listing variables when token has no Variables block', function (assert) { 161 const mockToken = Service.extend({ 162 aclEnabled: true, 163 selfToken: { type: 'client' }, 164 selfTokenPolicies: [ 165 { 166 rulesJSON: { 167 Namespaces: [ 168 { 169 Name: 'default', 170 Capabilities: [], 171 }, 172 ], 173 }, 174 }, 175 ], 176 }); 177 178 this.owner.register('service:token', mockToken); 179 180 assert.notOk(this.ability.canList); 181 }); 182 183 test('it permits listing variables when token multiple namespaces, only one of which having a Variables block', function (assert) { 184 const mockToken = Service.extend({ 185 aclEnabled: true, 186 selfToken: { type: 'client' }, 187 selfTokenPolicies: [ 188 { 189 rulesJSON: { 190 Namespaces: [ 191 { 192 Name: 'default', 193 Capabilities: [], 194 Variables: null, 195 }, 196 { 197 Name: 'nonsense', 198 Capabilities: [], 199 Variables: { 200 Paths: [{ Capabilities: [], PathSpec: '*' }], 201 }, 202 }, 203 { 204 Name: 'shenanigans', 205 Capabilities: [], 206 Variables: { 207 Paths: [ 208 { Capabilities: ['list'], PathSpec: 'foo/bar/baz' }, 209 ], 210 }, 211 }, 212 ], 213 }, 214 }, 215 ], 216 }); 217 218 this.owner.register('service:token', mockToken); 219 220 assert.ok(this.ability.canList); 221 }); 222 }); 223 224 module('#create', function () { 225 test('it does not permit creating variables by default', function (assert) { 226 const mockToken = Service.extend({ 227 aclEnabled: true, 228 }); 229 230 this.owner.register('service:token', mockToken); 231 232 assert.notOk(this.ability.canWrite); 233 }); 234 235 test('it permits creating variables when token type is management', function (assert) { 236 const mockToken = Service.extend({ 237 aclEnabled: true, 238 selfToken: { type: 'management' }, 239 }); 240 241 this.owner.register('service:token', mockToken); 242 243 assert.ok(this.ability.canWrite); 244 }); 245 246 test('it permits creating variables when acl is disabled', function (assert) { 247 const mockToken = Service.extend({ 248 aclEnabled: false, 249 selfToken: { type: 'client' }, 250 }); 251 252 this.owner.register('service:token', mockToken); 253 254 assert.ok(this.ability.canWrite); 255 }); 256 257 test('it permits creating variables when token has Variables with write capabilities in its rules', function (assert) { 258 const mockToken = Service.extend({ 259 aclEnabled: true, 260 selfToken: { type: 'client' }, 261 selfTokenPolicies: [ 262 { 263 rulesJSON: { 264 Namespaces: [ 265 { 266 Name: 'default', 267 Capabilities: [], 268 Variables: { 269 Paths: [{ Capabilities: ['write'], PathSpec: '*' }], 270 }, 271 }, 272 ], 273 }, 274 }, 275 ], 276 }); 277 278 this.owner.register('service:token', mockToken); 279 280 assert.ok(this.ability.canWrite); 281 }); 282 283 test('it handles namespace matching', function (assert) { 284 const mockToken = Service.extend({ 285 aclEnabled: true, 286 selfToken: { type: 'client' }, 287 selfTokenPolicies: [ 288 { 289 rulesJSON: { 290 Namespaces: [ 291 { 292 Name: 'default', 293 Capabilities: [], 294 Variables: { 295 Paths: [{ Capabilities: ['list'], PathSpec: 'foo/bar' }], 296 }, 297 }, 298 { 299 Name: 'pablo', 300 Capabilities: [], 301 Variables: { 302 Paths: [{ Capabilities: ['write'], PathSpec: 'foo/bar' }], 303 }, 304 }, 305 ], 306 }, 307 }, 308 ], 309 }); 310 311 this.owner.register('service:token', mockToken); 312 this.ability.path = 'foo/bar'; 313 this.ability.namespace = 'pablo'; 314 315 assert.ok(this.ability.canWrite); 316 }); 317 }); 318 319 module('#destroy', function () { 320 test('it does not permit destroying variables by default', function (assert) { 321 const mockToken = Service.extend({ 322 aclEnabled: true, 323 }); 324 325 this.owner.register('service:token', mockToken); 326 327 assert.notOk(this.ability.canDestroy); 328 }); 329 330 test('it permits destroying variables when token type is management', function (assert) { 331 const mockToken = Service.extend({ 332 aclEnabled: true, 333 selfToken: { type: 'management' }, 334 }); 335 336 this.owner.register('service:token', mockToken); 337 338 assert.ok(this.ability.canDestroy); 339 }); 340 341 test('it permits destroying variables when acl is disabled', function (assert) { 342 const mockToken = Service.extend({ 343 aclEnabled: false, 344 selfToken: { type: 'client' }, 345 }); 346 347 this.owner.register('service:token', mockToken); 348 349 assert.ok(this.ability.canDestroy); 350 }); 351 352 test('it permits destroying variables when token has Variables with write capabilities in its rules', function (assert) { 353 const mockToken = Service.extend({ 354 aclEnabled: true, 355 selfToken: { type: 'client' }, 356 selfTokenPolicies: [ 357 { 358 rulesJSON: { 359 Namespaces: [ 360 { 361 Name: 'default', 362 Capabilities: [], 363 Variables: { 364 Paths: [{ Capabilities: ['destroy'], PathSpec: '*' }], 365 }, 366 }, 367 ], 368 }, 369 }, 370 ], 371 }); 372 373 this.owner.register('service:token', mockToken); 374 375 assert.ok(this.ability.canDestroy); 376 }); 377 378 test('it handles namespace matching', function (assert) { 379 const mockToken = Service.extend({ 380 aclEnabled: true, 381 selfToken: { type: 'client' }, 382 selfTokenPolicies: [ 383 { 384 rulesJSON: { 385 Namespaces: [ 386 { 387 Name: 'default', 388 Capabilities: [], 389 Variables: { 390 Paths: [{ Capabilities: ['list'], PathSpec: 'foo/bar' }], 391 }, 392 }, 393 { 394 Name: 'pablo', 395 Capabilities: [], 396 Variables: { 397 Paths: [{ Capabilities: ['destroy'], PathSpec: 'foo/bar' }], 398 }, 399 }, 400 ], 401 }, 402 }, 403 ], 404 }); 405 406 this.owner.register('service:token', mockToken); 407 this.ability.path = 'foo/bar'; 408 this.ability.namespace = 'pablo'; 409 410 assert.ok(this.ability.canDestroy); 411 }); 412 }); 413 414 module('#read', function () { 415 test('it does not permit reading variables by default', function (assert) { 416 const mockToken = Service.extend({ 417 aclEnabled: true, 418 }); 419 420 this.owner.register('service:token', mockToken); 421 422 assert.notOk(this.ability.canRead); 423 }); 424 425 test('it permits reading variables when token type is management', function (assert) { 426 const mockToken = Service.extend({ 427 aclEnabled: true, 428 selfToken: { type: 'management' }, 429 }); 430 431 this.owner.register('service:token', mockToken); 432 433 assert.ok(this.ability.canRead); 434 }); 435 436 test('it permits reading variables when acl is disabled', function (assert) { 437 const mockToken = Service.extend({ 438 aclEnabled: false, 439 selfToken: { type: 'client' }, 440 }); 441 442 this.owner.register('service:token', mockToken); 443 444 assert.ok(this.ability.canRead); 445 }); 446 447 test('it permits reading variables when token has Variables with read capabilities in its rules', function (assert) { 448 const mockToken = Service.extend({ 449 aclEnabled: true, 450 selfToken: { type: 'client' }, 451 selfTokenPolicies: [ 452 { 453 rulesJSON: { 454 Namespaces: [ 455 { 456 Name: 'default', 457 Capabilities: [], 458 Variables: { 459 Paths: [{ Capabilities: ['read'], PathSpec: '*' }], 460 }, 461 }, 462 ], 463 }, 464 }, 465 ], 466 }); 467 468 this.owner.register('service:token', mockToken); 469 470 assert.ok(this.ability.canRead); 471 }); 472 473 test('it handles namespace matching', function (assert) { 474 const mockToken = Service.extend({ 475 aclEnabled: true, 476 selfToken: { type: 'client' }, 477 selfTokenPolicies: [ 478 { 479 rulesJSON: { 480 Namespaces: [ 481 { 482 Name: 'default', 483 Capabilities: [], 484 Variables: { 485 Paths: [{ Capabilities: ['list'], PathSpec: 'foo/bar' }], 486 }, 487 }, 488 { 489 Name: 'pablo', 490 Capabilities: [], 491 Variables: { 492 Paths: [{ Capabilities: ['read'], PathSpec: 'foo/bar' }], 493 }, 494 }, 495 ], 496 }, 497 }, 498 ], 499 }); 500 501 this.owner.register('service:token', mockToken); 502 this.ability.path = 'foo/bar'; 503 this.ability.namespace = 'pablo'; 504 505 assert.ok(this.ability.canRead); 506 }); 507 }); 508 509 module('#_nearestMatchingPath', function () { 510 test('returns capabilities for an exact path match', function (assert) { 511 const mockToken = Service.extend({ 512 aclEnabled: true, 513 selfToken: { type: 'client' }, 514 selfTokenPolicies: [ 515 { 516 rulesJSON: { 517 Namespaces: [ 518 { 519 Name: 'default', 520 Capabilities: [], 521 Variables: { 522 Paths: [{ Capabilities: ['write'], PathSpec: 'foo' }], 523 }, 524 }, 525 ], 526 }, 527 }, 528 ], 529 }); 530 531 this.owner.register('service:token', mockToken); 532 const path = 'foo'; 533 534 const nearestMatchingPath = this.ability._nearestMatchingPath(path); 535 536 assert.equal( 537 nearestMatchingPath, 538 'foo', 539 'It should return the exact path match.' 540 ); 541 }); 542 543 test('returns capabilities for the nearest fuzzy match if no exact match', function (assert) { 544 const mockToken = Service.extend({ 545 aclEnabled: true, 546 selfToken: { type: 'client' }, 547 selfTokenPolicies: [ 548 { 549 rulesJSON: { 550 Namespaces: [ 551 { 552 Name: 'default', 553 Capabilities: [], 554 Variables: { 555 Paths: [ 556 { Capabilities: ['write'], PathSpec: 'foo/*' }, 557 { Capabilities: ['write'], PathSpec: 'foo/bar/*' }, 558 ], 559 }, 560 }, 561 ], 562 }, 563 }, 564 ], 565 }); 566 567 this.owner.register('service:token', mockToken); 568 const path = 'foo/bar/baz'; 569 570 const nearestMatchingPath = this.ability._nearestMatchingPath(path); 571 572 assert.equal( 573 nearestMatchingPath, 574 'foo/bar/*', 575 'It should return the nearest fuzzy matching path.' 576 ); 577 }); 578 579 test('handles wildcard prefix matches', function (assert) { 580 const mockToken = Service.extend({ 581 aclEnabled: true, 582 selfToken: { type: 'client' }, 583 selfTokenPolicies: [ 584 { 585 rulesJSON: { 586 Namespaces: [ 587 { 588 Name: 'default', 589 Capabilities: [], 590 Variables: { 591 Paths: [{ Capabilities: ['write'], PathSpec: 'foo/*' }], 592 }, 593 }, 594 ], 595 }, 596 }, 597 ], 598 }); 599 600 this.owner.register('service:token', mockToken); 601 const path = 'foo/bar/baz'; 602 603 const nearestMatchingPath = this.ability._nearestMatchingPath(path); 604 605 assert.equal( 606 nearestMatchingPath, 607 'foo/*', 608 'It should handle wildcard glob.' 609 ); 610 }); 611 612 test('handles wildcard suffix matches', function (assert) { 613 const mockToken = Service.extend({ 614 aclEnabled: true, 615 selfToken: { type: 'client' }, 616 selfTokenPolicies: [ 617 { 618 rulesJSON: { 619 Namespaces: [ 620 { 621 Name: 'default', 622 Capabilities: [], 623 Variables: { 624 Paths: [ 625 { Capabilities: ['write'], PathSpec: '*/bar' }, 626 { Capabilities: ['write'], PathSpec: '*/bar/baz' }, 627 ], 628 }, 629 }, 630 ], 631 }, 632 }, 633 ], 634 }); 635 636 this.owner.register('service:token', mockToken); 637 const path = 'foo/bar/baz'; 638 639 const nearestMatchingPath = this.ability._nearestMatchingPath(path); 640 641 assert.equal( 642 nearestMatchingPath, 643 '*/bar/baz', 644 'It should return the nearest ancestor matching path.' 645 ); 646 }); 647 648 test('prioritizes wildcard suffix matches over wildcard prefix matches', function (assert) { 649 const mockToken = Service.extend({ 650 aclEnabled: true, 651 selfToken: { type: 'client' }, 652 selfTokenPolicies: [ 653 { 654 rulesJSON: { 655 Namespaces: [ 656 { 657 Name: 'default', 658 Capabilities: [], 659 Variables: { 660 Paths: [ 661 { Capabilities: ['write'], PathSpec: '*/bar' }, 662 { Capabilities: ['write'], PathSpec: 'foo/*' }, 663 ], 664 }, 665 }, 666 ], 667 }, 668 }, 669 ], 670 }); 671 672 this.owner.register('service:token', mockToken); 673 const path = 'foo/bar/baz'; 674 675 const nearestMatchingPath = this.ability._nearestMatchingPath(path); 676 677 assert.equal( 678 nearestMatchingPath, 679 'foo/*', 680 'It should prioritize suffix glob wildcard of prefix glob wildcard.' 681 ); 682 }); 683 684 test('defaults to the glob path if there is no exact match or wildcard matches', function (assert) { 685 const mockToken = Service.extend({ 686 aclEnabled: true, 687 selfToken: { type: 'client' }, 688 selfTokenPolicies: [ 689 { 690 rulesJSON: { 691 Namespaces: [ 692 { 693 Name: 'default', 694 Capabilities: [], 695 Variables: { 696 'Path "*"': { 697 Capabilities: ['write'], 698 }, 699 'Path "foo"': { 700 Capabilities: ['write'], 701 }, 702 }, 703 }, 704 ], 705 }, 706 }, 707 ], 708 }); 709 710 this.owner.register('service:token', mockToken); 711 const path = 'foo/bar/baz'; 712 713 const nearestMatchingPath = this.ability._nearestMatchingPath(path); 714 715 assert.equal( 716 nearestMatchingPath, 717 '*', 718 'It should default to glob wildcard if no matches.' 719 ); 720 }); 721 }); 722 723 module('#_computeLengthDiff', function () { 724 test('should return the difference in length between a path and a pattern', function (assert) { 725 // arrange 726 const path = 'foo'; 727 const pattern = 'bar'; 728 729 // act 730 const result = this.ability._computeLengthDiff(pattern, path); 731 732 // assert 733 assert.equal( 734 result, 735 0, 736 'it returns the difference in length between path and pattern' 737 ); 738 }); 739 740 test('should factor the number of globs into consideration', function (assert) { 741 // arrange 742 const pattern = 'foo*'; 743 const path = 'bark'; 744 745 // act 746 const result = this.ability._computeLengthDiff(pattern, path); 747 748 // assert 749 assert.equal( 750 result, 751 1, 752 'it adds the number of globs in the pattern to the difference' 753 ); 754 }); 755 }); 756 757 module('#_smallestDifference', function () { 758 test('returns the smallest difference in the list', function (assert) { 759 // arrange 760 const path = 'foo/bar'; 761 const matchingPath = 'foo/*'; 762 const matches = ['*/baz', '*', matchingPath]; 763 764 // act 765 const result = this.ability._smallestDifference(matches, path); 766 767 // assert 768 assert.equal( 769 result, 770 matchingPath, 771 'It should return the smallest difference path.' 772 ); 773 }); 774 }); 775 776 module('#allPaths', function () { 777 test('it filters by namespace and shows all matching paths on the namespace', function (assert) { 778 const mockToken = Service.extend({ 779 aclEnabled: true, 780 selfToken: { type: 'client' }, 781 selfTokenPolicies: [ 782 { 783 rulesJSON: { 784 Namespaces: [ 785 { 786 Name: 'default', 787 Capabilities: [], 788 Variables: { 789 Paths: [{ Capabilities: ['write'], PathSpec: 'foo' }], 790 }, 791 }, 792 { 793 Name: 'bar', 794 Capabilities: [], 795 Variables: { 796 Paths: [ 797 { Capabilities: ['read', 'write'], PathSpec: 'foo' }, 798 ], 799 }, 800 }, 801 ], 802 }, 803 }, 804 ], 805 }); 806 807 this.owner.register('service:token', mockToken); 808 this.ability.namespace = 'bar'; 809 810 const allPaths = this.ability.allPaths; 811 812 assert.deepEqual( 813 allPaths, 814 [ 815 { 816 capabilities: ['read', 'write'], 817 name: 'foo', 818 }, 819 ], 820 'It should return the exact path match.' 821 ); 822 }); 823 824 test('it matches on default if no namespace is selected', function (assert) { 825 const mockToken = Service.extend({ 826 aclEnabled: true, 827 selfToken: { type: 'client' }, 828 selfTokenPolicies: [ 829 { 830 rulesJSON: { 831 Namespaces: [ 832 { 833 Name: 'default', 834 Capabilities: [], 835 Variables: { 836 Paths: [{ Capabilities: ['write'], PathSpec: 'foo' }], 837 }, 838 }, 839 { 840 Name: 'bar', 841 Capabilities: [], 842 Variables: { 843 Paths: [ 844 { Capabilities: ['read', 'write'], PathSpec: 'foo' }, 845 ], 846 }, 847 }, 848 ], 849 }, 850 }, 851 ], 852 }); 853 854 this.owner.register('service:token', mockToken); 855 this.ability.namespace = undefined; 856 857 const allPaths = this.ability.allPaths; 858 859 assert.deepEqual( 860 allPaths, 861 [ 862 { 863 capabilities: ['write'], 864 name: 'foo', 865 }, 866 ], 867 'It should return the exact path match.' 868 ); 869 }); 870 871 test('it handles globs in namespaces', function (assert) { 872 const mockToken = Service.extend({ 873 aclEnabled: true, 874 selfToken: { type: 'client' }, 875 selfTokenPolicies: [ 876 { 877 rulesJSON: { 878 Namespaces: [ 879 { 880 Name: '*', 881 Capabilities: ['list-jobs', 'alloc-exec', 'read-logs'], 882 Variables: { 883 Paths: [ 884 { 885 Capabilities: ['list'], 886 PathSpec: '*', 887 }, 888 ], 889 }, 890 }, 891 { 892 Name: 'namespace-1', 893 Capabilities: ['list-jobs', 'alloc-exec', 'read-logs'], 894 Variables: { 895 Paths: [ 896 { 897 Capabilities: ['list', 'read', 'destroy', 'create'], 898 PathSpec: '*', 899 }, 900 ], 901 }, 902 }, 903 { 904 Name: 'namespace-2', 905 Capabilities: ['list-jobs', 'alloc-exec', 'read-logs'], 906 Variables: { 907 Paths: [ 908 { 909 Capabilities: ['list', 'read', 'destroy', 'create'], 910 PathSpec: 'blue/*', 911 }, 912 { 913 Capabilities: ['list', 'read', 'create'], 914 PathSpec: 'nomad/jobs/*', 915 }, 916 ], 917 }, 918 }, 919 ], 920 }, 921 }, 922 ], 923 }); 924 925 this.owner.register('service:token', mockToken); 926 this.ability.namespace = 'pablo'; 927 928 const allPaths = this.ability.allPaths; 929 930 assert.deepEqual( 931 allPaths, 932 [ 933 { 934 capabilities: ['list'], 935 name: '*', 936 }, 937 ], 938 'It should return the glob matching namespace match.' 939 ); 940 }); 941 }); 942 });