Files
nomad/ui/tests/unit/abilities/variable-test.js
Phil Renaud 2fc7544ff3 [ui] Modify variable access permissions for UI users with write in only certain namespaces (#24073)
* Modify variable access permissions for UI users with write in only certain namespaces

* Addressing some PR comments

* Variables index namespaces on * and ability checks are now namespaced

* Mistook Delete for Destroy, and update unit tests for mult-return allPaths
2024-10-02 16:02:40 -04:00

971 lines
26 KiB
JavaScript

/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
/* 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 Variables with list capabilities in its rules', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: 'default',
Capabilities: [],
Variables: {
Paths: [{ Capabilities: ['list'], PathSpec: '*' }],
},
},
],
},
},
],
});
this.owner.register('service:token', mockToken);
assert.ok(this.ability.canList);
});
test('it does not permit listing variables when token has Variables alone in its rules', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: 'default',
Capabilities: [],
Variables: {},
},
],
},
},
],
});
this.owner.register('service:token', mockToken);
assert.notOk(this.ability.canList);
});
test('it does not permit listing variables when token has a null Variables block', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: 'default',
Capabilities: [],
Variables: null,
},
],
},
},
],
});
this.owner.register('service:token', mockToken);
assert.notOk(this.ability.canList);
});
test('it does not permit listing variables when token has a Variables block where paths are without capabilities', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: 'default',
Capabilities: [],
Variables: {
Paths: [
{ Capabilities: [], PathSpec: '*' },
{ Capabilities: [], PathSpec: 'foo' },
{ Capabilities: [], PathSpec: 'foo/bar' },
],
},
},
],
},
},
],
});
this.owner.register('service:token', mockToken);
assert.notOk(this.ability.canList);
});
test('it does not permit listing variables when token has no Variables block', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: 'default',
Capabilities: [],
},
],
},
},
],
});
this.owner.register('service:token', mockToken);
assert.notOk(this.ability.canList);
});
test('it permits listing variables when token multiple namespaces, only one of which having a Variables block', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: 'default',
Capabilities: [],
Variables: null,
},
{
Name: 'nonsense',
Capabilities: [],
Variables: {
Paths: [{ Capabilities: [], PathSpec: '*' }],
},
},
{
Name: 'shenanigans',
Capabilities: [],
Variables: {
Paths: [
{ Capabilities: ['list'], PathSpec: 'foo/bar/baz' },
],
},
},
],
},
},
],
});
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 Variables with write capabilities in its rules', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: 'default',
Capabilities: [],
Variables: {
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: [],
Variables: {
Paths: [{ Capabilities: ['list'], PathSpec: 'foo/bar' }],
},
},
{
Name: 'pablo',
Capabilities: [],
Variables: {
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 Variables with write capabilities in its rules', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: 'default',
Capabilities: [],
Variables: {
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: [],
Variables: {
Paths: [{ Capabilities: ['list'], PathSpec: 'foo/bar' }],
},
},
{
Name: 'pablo',
Capabilities: [],
Variables: {
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('#read', function () {
test('it does not permit reading variables by default', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
});
this.owner.register('service:token', mockToken);
assert.notOk(this.ability.canRead);
});
test('it permits reading 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.canRead);
});
test('it permits reading 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.canRead);
});
test('it permits reading variables when token has Variables with read capabilities in its rules', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: 'default',
Capabilities: [],
Variables: {
Paths: [{ Capabilities: ['read'], PathSpec: '*' }],
},
},
],
},
},
],
});
this.owner.register('service:token', mockToken);
assert.ok(this.ability.canRead);
});
test('it handles namespace matching', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: 'default',
Capabilities: [],
Variables: {
Paths: [{ Capabilities: ['list'], PathSpec: 'foo/bar' }],
},
},
{
Name: 'pablo',
Capabilities: [],
Variables: {
Paths: [{ Capabilities: ['read'], PathSpec: 'foo/bar' }],
},
},
],
},
},
],
});
this.owner.register('service:token', mockToken);
this.ability.path = 'foo/bar';
this.ability.namespace = 'pablo';
assert.ok(this.ability.canRead);
});
});
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: [],
Variables: {
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: [],
Variables: {
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: [],
Variables: {
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: [],
Variables: {
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: [],
Variables: {
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: [],
Variables: {
'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('#_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.'
);
});
});
module('#allPaths', function () {
test('it filters by namespace and shows all matching paths on the namespace', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: 'default',
Capabilities: [],
Variables: {
Paths: [{ Capabilities: ['write'], PathSpec: 'foo' }],
},
},
{
Name: 'bar',
Capabilities: [],
Variables: {
Paths: [
{ Capabilities: ['read', 'write'], PathSpec: 'foo' },
],
},
},
],
},
},
],
});
this.owner.register('service:token', mockToken);
this.ability.namespace = 'bar';
const allPaths = this.ability.allVariablePathRules;
assert.deepEqual(
allPaths,
[
{
capabilities: ['write'],
name: 'foo',
namespace: 'default',
},
{
capabilities: ['read', 'write'],
name: 'foo',
namespace: 'bar',
},
],
'It should return the exact path match.'
);
});
test('it matches if no namespace is selected', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: 'default',
Capabilities: [],
Variables: {
Paths: [{ Capabilities: ['write'], PathSpec: 'foo' }],
},
},
{
Name: 'bar',
Capabilities: [],
Variables: {
Paths: [
{ Capabilities: ['read', 'write'], PathSpec: 'foo' },
],
},
},
],
},
},
],
});
this.owner.register('service:token', mockToken);
this.ability.namespace = undefined;
const allPaths = this.ability.allVariablePathRules;
assert.deepEqual(
allPaths,
[
{
capabilities: ['write'],
name: 'foo',
namespace: 'default',
},
{
capabilities: ['read', 'write'],
name: 'foo',
namespace: 'bar',
},
],
'It should return both matches separated by namespace.'
);
});
test('it handles globs in namespaces', function (assert) {
const mockToken = Service.extend({
aclEnabled: true,
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: '*',
Capabilities: ['list-jobs', 'alloc-exec', 'read-logs'],
Variables: {
Paths: [
{
Capabilities: ['list'],
PathSpec: '*',
},
],
},
},
{
Name: 'namespace-1',
Capabilities: ['list-jobs', 'alloc-exec', 'read-logs'],
Variables: {
Paths: [
{
Capabilities: ['list', 'read', 'destroy', 'create'],
PathSpec: '*',
},
],
},
},
{
Name: 'namespace-2',
Capabilities: ['list-jobs', 'alloc-exec', 'read-logs'],
Variables: {
Paths: [
{
Capabilities: ['list', 'read', 'destroy', 'create'],
PathSpec: 'blue/*',
},
{
Capabilities: ['list', 'read', 'create'],
PathSpec: 'nomad/jobs/*',
},
],
},
},
],
},
},
],
});
this.owner.register('service:token', mockToken);
this.ability.namespace = 'pablo';
const allPaths = this.ability.allVariablePathRules;
assert.deepEqual(
allPaths,
[
{
capabilities: ['list'],
name: '*',
namespace: '*',
},
{
capabilities: ['list', 'read', 'destroy', 'create'],
name: '*',
namespace: 'namespace-1',
},
{
capabilities: ['list', 'read', 'destroy', 'create'],
name: 'blue/*',
namespace: 'namespace-2',
},
{
capabilities: ['list', 'read', 'create'],
name: 'nomad/jobs/*',
namespace: 'namespace-2',
},
],
'It should return the glob matching namespace match.'
);
});
});
});