Files
nomad/ui/tests/unit/abilities/variable-test.js
Jai d5ce37442b ui: create variable permission logic (#13447)
* ui:  inject router service into Variable ability to compute path

* ui:  test create secure variable ability

* refact:  update templates to properly check create ability

* chore:  update token factory to enable 1 path to have create ability

* refact:  remove router service injection for path variable

* refact:  update mirage factory for edit and delete perms on  path for testing

* ui:  handle path matching (#13474)

* test:  write specifications for nearestPath computation

* ui:  write logic for getting all paths

* ui:  nearestPathMatching algorithm

* test:  nearestPathMatching algorithm test

* ui:  handle namespace filtering for capabilities check (#13475)

* ui: add namespace handling

* refact:  add logical OR operator to handle unstructured  object.

* ui:  acceptance test for create flow in secure variables (#13500)

* test:  write happy path test for creating variable

* refact:  add missing data-test attributes

* test:  sad path for disabled button

* fix:  move comment in  file

* test:  acceptance test for editing a variable (#13529)

* refact:  add data-test variable

* test:  happy path and sad path for edit flow

* refact:  update test language to say disabled

* ui:  glob matching algorithm (#13533)

* ui: compute length difference (#13542)

* ui: compute length difference

* refact:  use glob matching and sorting algos in `nearestMatchingPath` (#13544)

* refact:  use const in compute

* ui:  smallest difference logic

* refact:  use glob matching and sorting algo in _nearestPathPath helper

* ui:  add can edit to variable capabilities (#13545)

* ui:  create edit capabilities getter

* ui:  add ember-can check for edit button

* refact:  update test to mock edit capabilities in policy

* fix:  remove unused var

* Edit capabilities for variables depend on Create

Co-authored-by: Phil Renaud <phil@riotindustries.com>

Co-authored-by: Phil Renaud <phil@riotindustries.com>

Co-authored-by: Phil Renaud <phil@riotindustries.com>

* refact:  update token factory (#13596)

* refact:  update rulesJSON in token factory to reflect schema update

* refact:  update capability names (#13597)

* refact:  update rules to match rulesJSON

* refact:  update create to write

* ui:  add `canDestroy` permissions (#13598)

* refact:  update rulesJSON in token factory to reflect schema update

* refact:  update rules to match rulesJSON

* refact:  update create to write

* ui:  add canDestroy capability

* test:  unit test for canDestroy

* ui:  add permission check to template

* test:  acceptance test for delete flow

* refact:  update test to use correct capability name

* refact:  update tests to reflect rulesJSON schema change

* ui:  update path matching logic to account for schema change (#13605)

* refact:  update path matching logic

* refact:  update tests to reflect rulesJSON change

Co-authored-by: Phil Renaud <phil@riotindustries.com>

Co-authored-by: Phil Renaud <phil@riotindustries.com>
2022-07-11 13:34:06 -04:00

863 lines
23 KiB
JavaScript

/* eslint-disable ember/avoid-leaking-state-in-ember-objects */
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import Service from '@ember/service';
import setupAbility from 'nomad-ui/tests/helpers/setup-ability';
module('Unit | Ability | variable', function (hooks) {
setupTest(hooks);
setupAbility('variable')(hooks);
hooks.beforeEach(function () {
const mockSystem = Service.extend({
features: [],
});
this.owner.register('service:system', mockSystem);
});
module('#list', function () {
test('it does not permit listing variables by default', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
});
this.owner.register('service:token', mockToken);
assert.notOk(this.ability.canList);
});
test('it does not permit listing variables when token type is client', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'client' },
});
this.owner.register('service:token', mockToken);
assert.notOk(this.ability.canList);
});
test('it permits listing variables when token type is management', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'management' },
});
this.owner.register('service:token', mockToken);
assert.ok(this.ability.canList);
});
test('it permits listing variables when token has SecureVariables with list capabilities in its rules', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: 'default',
Capabilities: [],
SecureVariables: {
'Path "*"': {
Capabilities: ['list'],
},
},
},
],
},
},
],
});
this.owner.register('service:token', mockToken);
assert.ok(this.ability.canList);
});
test('it permits listing variables when token has SecureVariables alone in its rules', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: 'default',
Capabilities: [],
SecureVariables: {},
},
],
},
},
],
});
this.owner.register('service:token', mockToken);
assert.ok(this.ability.canList);
});
});
module('#create', function () {
test('it does not permit creating variables by default', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
});
this.owner.register('service:token', mockToken);
assert.notOk(this.ability.canWrite);
});
test('it permits creating variables when token type is management', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'management' },
});
this.owner.register('service:token', mockToken);
assert.ok(this.ability.canWrite);
});
test('it permits creating variables when acl is disabled', function (assert) {
const mockToken = Service.extend({
aclEnabled: false,
selfToken: { type: 'client' },
});
this.owner.register('service:token', mockToken);
assert.ok(this.ability.canWrite);
});
test('it permits creating variables when token has SecureVariables with write capabilities in its rules', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: 'default',
Capabilities: [],
SecureVariables: {
Paths: [{ Capabilities: ['write'], PathSpec: '*' }],
},
},
],
},
},
],
});
this.owner.register('service:token', mockToken);
assert.ok(this.ability.canWrite);
});
test('it handles namespace matching', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: 'default',
Capabilities: [],
SecureVariables: {
Paths: [{ Capabilities: ['list'], PathSpec: 'foo/bar' }],
},
},
{
Name: 'pablo',
Capabilities: [],
SecureVariables: {
Paths: [{ Capabilities: ['write'], PathSpec: 'foo/bar' }],
},
},
],
},
},
],
});
this.owner.register('service:token', mockToken);
this.ability.path = 'foo/bar';
this.ability.namespace = 'pablo';
assert.ok(this.ability.canWrite);
});
});
module('#destroy', function () {
test('it does not permit destroying variables by default', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
});
this.owner.register('service:token', mockToken);
assert.notOk(this.ability.canDestroy);
});
test('it permits destroying variables when token type is management', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'management' },
});
this.owner.register('service:token', mockToken);
assert.ok(this.ability.canDestroy);
});
test('it permits destroying variables when acl is disabled', function (assert) {
const mockToken = Service.extend({
aclEnabled: false,
selfToken: { type: 'client' },
});
this.owner.register('service:token', mockToken);
assert.ok(this.ability.canDestroy);
});
test('it permits destroying variables when token has SecureVariables with write capabilities in its rules', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: 'default',
Capabilities: [],
SecureVariables: {
Paths: [{ Capabilities: ['destroy'], PathSpec: '*' }],
},
},
],
},
},
],
});
this.owner.register('service:token', mockToken);
assert.ok(this.ability.canDestroy);
});
test('it handles namespace matching', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: 'default',
Capabilities: [],
SecureVariables: {
Paths: [{ Capabilities: ['list'], PathSpec: 'foo/bar' }],
},
},
{
Name: 'pablo',
Capabilities: [],
SecureVariables: {
Paths: [{ Capabilities: ['destroy'], PathSpec: 'foo/bar' }],
},
},
],
},
},
],
});
this.owner.register('service:token', mockToken);
this.ability.path = 'foo/bar';
this.ability.namespace = 'pablo';
assert.ok(this.ability.canDestroy);
});
});
module('#_nearestMatchingPath', function () {
test('returns capabilities for an exact path match', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: 'default',
Capabilities: [],
SecureVariables: {
Paths: [{ Capabilities: ['write'], PathSpec: 'foo' }],
},
},
],
},
},
],
});
this.owner.register('service:token', mockToken);
const path = 'foo';
const nearestMatchingPath = this.ability._nearestMatchingPath(path);
assert.equal(
nearestMatchingPath,
'foo',
'It should return the exact path match.'
);
});
test('returns capabilities for the nearest fuzzy match if no exact match', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: 'default',
Capabilities: [],
SecureVariables: {
Paths: [
{ Capabilities: ['write'], PathSpec: 'foo/*' },
{ Capabilities: ['write'], PathSpec: 'foo/bar/*' },
],
},
},
],
},
},
],
});
this.owner.register('service:token', mockToken);
const path = 'foo/bar/baz';
const nearestMatchingPath = this.ability._nearestMatchingPath(path);
assert.equal(
nearestMatchingPath,
'foo/bar/*',
'It should return the nearest fuzzy matching path.'
);
});
test('handles wildcard prefix matches', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: 'default',
Capabilities: [],
SecureVariables: {
Paths: [{ Capabilities: ['write'], PathSpec: 'foo/*' }],
},
},
],
},
},
],
});
this.owner.register('service:token', mockToken);
const path = 'foo/bar/baz';
const nearestMatchingPath = this.ability._nearestMatchingPath(path);
assert.equal(
nearestMatchingPath,
'foo/*',
'It should handle wildcard glob.'
);
});
test('handles wildcard suffix matches', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: 'default',
Capabilities: [],
SecureVariables: {
Paths: [
{ Capabilities: ['write'], PathSpec: '*/bar' },
{ Capabilities: ['write'], PathSpec: '*/bar/baz' },
],
},
},
],
},
},
],
});
this.owner.register('service:token', mockToken);
const path = 'foo/bar/baz';
const nearestMatchingPath = this.ability._nearestMatchingPath(path);
assert.equal(
nearestMatchingPath,
'*/bar/baz',
'It should return the nearest ancestor matching path.'
);
});
test('prioritizes wildcard suffix matches over wildcard prefix matches', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: 'default',
Capabilities: [],
SecureVariables: {
Paths: [
{ Capabilities: ['write'], PathSpec: '*/bar' },
{ Capabilities: ['write'], PathSpec: 'foo/*' },
],
},
},
],
},
},
],
});
this.owner.register('service:token', mockToken);
const path = 'foo/bar/baz';
const nearestMatchingPath = this.ability._nearestMatchingPath(path);
assert.equal(
nearestMatchingPath,
'foo/*',
'It should prioritize suffix glob wildcard of prefix glob wildcard.'
);
});
test('defaults to the glob path if there is no exact match or wildcard matches', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: 'default',
Capabilities: [],
SecureVariables: {
'Path "*"': {
Capabilities: ['write'],
},
'Path "foo"': {
Capabilities: ['write'],
},
},
},
],
},
},
],
});
this.owner.register('service:token', mockToken);
const path = 'foo/bar/baz';
const nearestMatchingPath = this.ability._nearestMatchingPath(path);
assert.equal(
nearestMatchingPath,
'*',
'It should default to glob wildcard if no matches.'
);
});
});
module('#_doesMatchPattern', function () {
const edgeCaseTest = 'this is a ϗѾ test';
module('base cases', function () {
test('it handles an empty pattern', function (assert) {
// arrange
const pattern = '';
const emptyPath = '';
const nonEmptyPath = 'a';
// act
const matchingResult = this.ability._doesMatchPattern(
pattern,
emptyPath
);
const nonMatchingResult = this.ability._doesMatchPattern(
pattern,
nonEmptyPath
);
// assert
assert.ok(matchingResult, 'Empty pattern should match empty path');
assert.notOk(
nonMatchingResult,
'Empty pattern should not match non-empty path'
);
});
test('it handles an empty path', function (assert) {
// arrange
const emptyPath = '';
const emptyPattern = '';
const nonEmptyPattern = 'a';
// act
const matchingResult = this.ability._doesMatchPattern(
emptyPattern,
emptyPath
);
const nonMatchingResult = this.ability._doesMatchPattern(
nonEmptyPattern,
emptyPath
);
// assert
assert.ok(matchingResult, 'Empty path should match empty pattern');
assert.notOk(
nonMatchingResult,
'Empty path should not match non-empty pattern'
);
});
test('it handles a pattern without a glob', function (assert) {
// arrange
const path = '/foo';
const matchingPattern = '/foo';
const nonMatchingPattern = '/bar';
// act
const matchingResult = this.ability._doesMatchPattern(
matchingPattern,
path
);
const nonMatchingResult = this.ability._doesMatchPattern(
nonMatchingPattern,
path
);
// assert
assert.ok(matchingResult, 'Matches path correctly.');
assert.notOk(nonMatchingResult, 'Does not match non-matching path.');
});
test('it handles a pattern that is a lone glob', function (assert) {
// arrange
const path = '/foo';
const glob = '*';
// act
const matchingResult = this.ability._doesMatchPattern(glob, path);
// assert
assert.ok(matchingResult, 'Matches glob.');
});
test('it matches on leading glob', function (assert) {
// arrange
const pattern = '*bar';
const matchingPath = 'footbar';
const nonMatchingPath = 'rockthecasba';
// act
const matchingResult = this.ability._doesMatchPattern(
pattern,
matchingPath
);
const nonMatchingResult = this.ability._doesMatchPattern(
pattern,
nonMatchingPath
);
// assert
assert.ok(
matchingResult,
'Correctly matches when leading glob and matching path.'
);
assert.notOk(
nonMatchingResult,
'Does not match when leading glob and non-matching path.'
);
});
test('it matches on trailing glob', function (assert) {
// arrange
const pattern = 'foo*';
const matchingPath = 'footbar';
const nonMatchingPath = 'bar';
// act
const matchingResult = this.ability._doesMatchPattern(
pattern,
matchingPath
);
const nonMatchingResult = this.ability._doesMatchPattern(
pattern,
nonMatchingPath
);
// assert
assert.ok(matchingResult, 'Correctly matches on trailing glob.');
assert.notOk(
nonMatchingResult,
'Does not match on trailing glob if pattern does not match.'
);
});
test('it matches when glob is in middle', function (assert) {
// arrange
const pattern = 'foo*bar';
const matchingPath = 'footbar';
const nonMatchingPath = 'footba';
// act
const matchingResult = this.ability._doesMatchPattern(
pattern,
matchingPath
);
const nonMatchingResult = this.ability._doesMatchPattern(
pattern,
nonMatchingPath
);
// assert
assert.ok(
matchingResult,
'Correctly matches on glob in middle of path.'
);
assert.notOk(
nonMatchingResult,
'Does not match on glob in middle of path if not full pattern match.'
);
});
});
module('matching edge cases', function () {
test('it matches when string is between globs', function (assert) {
// arrange
const pattern = '*is *';
// act
const result = this.ability._doesMatchPattern(pattern, edgeCaseTest);
// assert
assert.ok(result);
});
test('it handles many non-consective globs', function (assert) {
// arrange
const pattern = '*is*a*';
// act
const result = this.ability._doesMatchPattern(pattern, edgeCaseTest);
// assert
assert.ok(result);
});
test('it handles double globs', function (assert) {
// arrange
const pattern = '**test**';
// act
const result = this.ability._doesMatchPattern(pattern, edgeCaseTest);
// assert
assert.ok(result);
});
test('it handles many consecutive globs', function (assert) {
// arrange
const pattern = '**is**a***test*';
// act
const result = this.ability._doesMatchPattern(pattern, edgeCaseTest);
// assert
assert.ok(result);
});
test('it handles white space between globs', function (assert) {
// arrange
const pattern = '* *';
// act
const result = this.ability._doesMatchPattern(pattern, edgeCaseTest);
// assert
assert.ok(result);
});
test('it handles a pattern of only globs', function (assert) {
// arrange
const pattern = '**********';
// act
const result = this.ability._doesMatchPattern(pattern, edgeCaseTest);
// assert
assert.ok(result);
});
test('it handles unicode characters', function (assert) {
// arrange
const pattern = `*Ѿ*`;
// act
const result = this.ability._doesMatchPattern(pattern, edgeCaseTest);
// assert
assert.ok(result);
});
test('it handles mixed ASCII codes', function (assert) {
// arrange
const pattern = `*is a ϗѾ *`;
// act
const result = this.ability._doesMatchPattern(pattern, edgeCaseTest);
// assert
assert.ok(result);
});
});
module('non-matching edge cases', function () {
const failingCases = [
{
case: 'test*',
message: 'Implicit substring match',
},
{
case: '*is',
message: 'Parial match',
},
{
case: '*no*',
message: 'Globs without match between them',
},
{
case: ' ',
message: 'Plain white space',
},
{
case: '* ',
message: 'Trailing white space',
},
{
case: ' *',
message: 'Leading white space',
},
{
case: '*ʤ*',
message: 'Non-matching unicode',
},
{
case: 'this*this is a test',
message: 'Repeated prefix',
},
];
failingCases.forEach(({ case: failingPattern, message }) => {
test('should fail the specified cases', function (assert) {
const result = this.ability._doesMatchPattern(
failingPattern,
edgeCaseTest
);
assert.notOk(result, `${message} should not match.`);
});
});
});
});
module('#_computeLengthDiff', function () {
test('should return the difference in length between a path and a pattern', function (assert) {
// arrange
const path = 'foo';
const pattern = 'bar';
// act
const result = this.ability._computeLengthDiff(pattern, path);
// assert
assert.equal(
result,
0,
'it returns the difference in length between path and pattern'
);
});
test('should factor the number of globs into consideration', function (assert) {
// arrange
const pattern = 'foo*';
const path = 'bark';
// act
const result = this.ability._computeLengthDiff(pattern, path);
// assert
assert.equal(
result,
1,
'it adds the number of globs in the pattern to the difference'
);
});
});
module('#_smallestDifference', function () {
test('returns the smallest difference in the list', function (assert) {
// arrange
const path = 'foo/bar';
const matchingPath = 'foo/*';
const matches = ['*/baz', '*', matchingPath];
// act
const result = this.ability._smallestDifference(matches, path);
// assert
assert.equal(
result,
matchingPath,
'It should return the smallest difference path.'
);
});
});
});