diff --git a/ui/app/abilities/variable.js b/ui/app/abilities/variable.js
index 40f77fe5b..9692e0b38 100644
--- a/ui/app/abilities/variable.js
+++ b/ui/app/abilities/variable.js
@@ -39,6 +39,13 @@ export default class Variable extends AbstractAbility {
)
canDestroy;
+ @or(
+ 'bypassAuthorization',
+ 'selfTokenIsManagement',
+ 'policiesSupportVariableRead'
+ )
+ canRead;
+
@computed('token.selfTokenPolicies')
get policiesSupportVariableList() {
return this.policyNamespacesIncludeSecureVariablesCapabilities(
@@ -47,6 +54,14 @@ export default class Variable extends AbstractAbility {
);
}
+ @computed('path', 'allPaths')
+ get policiesSupportVariableRead() {
+ const matchingPath = this._nearestMatchingPath(this.path);
+ return this.allPaths
+ .find((path) => path.name === matchingPath)
+ ?.capabilities?.includes('read');
+ }
+
/**
*
* Map to your policy's namespaces,
@@ -159,7 +174,6 @@ export default class Variable extends AbstractAbility {
_nearestMatchingPath(path) {
const pathNames = this.allPaths.map((path) => path.name);
-
if (pathNames.includes(path)) {
return path;
}
diff --git a/ui/app/components/variable-paths.hbs b/ui/app/components/variable-paths.hbs
index 5341971ea..668af2325 100644
--- a/ui/app/components/variable-paths.hbs
+++ b/ui/app/components/variable-paths.hbs
@@ -25,9 +25,14 @@
{{/each}}
{{#each this.files as |file|}}
-
|
+ {{#if (can "read variable" path=file.absoluteFilePath namespace=file.variable.namespace)}}
{{file.name}}
+ {{else}}
+ {{file.name}}
+ {{/if}}
|
{{file.variable.namespace}}
diff --git a/ui/app/components/variable-paths.js b/ui/app/components/variable-paths.js
index 60a741441..ea361dfc0 100644
--- a/ui/app/components/variable-paths.js
+++ b/ui/app/components/variable-paths.js
@@ -5,6 +5,7 @@ import { inject as service } from '@ember/service';
import compactPath from '../utils/compact-path';
export default class VariablePathsComponent extends Component {
@service router;
+ @service can;
/**
* @returns {Array>}
@@ -25,7 +26,9 @@ export default class VariablePathsComponent extends Component {
}
@action
- async handleFileClick(path) {
- this.router.transitionTo('variables.variable', path);
+ async handleFileClick({ path, variable: { namespace } }) {
+ if (this.can.can('read variable', null, { path, namespace })) {
+ this.router.transitionTo('variables.variable', path);
+ }
}
}
diff --git a/ui/app/controllers/variables/variable.js b/ui/app/controllers/variables/variable.js
index 3fd68ad8f..bbb578fdd 100644
--- a/ui/app/controllers/variables/variable.js
+++ b/ui/app/controllers/variables/variable.js
@@ -3,11 +3,11 @@ import Controller from '@ember/controller';
export default class VariablesVariableController extends Controller {
get breadcrumbs() {
let crumbs = [];
- this.model.path.split('/').reduce((m, n) => {
+ this.params.path.split('/').reduce((m, n) => {
crumbs.push({
label: n,
args:
- m + n === this.model.path // If the last crumb, link to the var itself
+ m + n === this.params.path // If the last crumb, link to the var itself
? [`variables.variable`, m + n]
: [`variables.path`, m + n],
});
diff --git a/ui/app/routes/variables/variable.js b/ui/app/routes/variables/variable.js
index 6150b2ddd..3e01dafed 100644
--- a/ui/app/routes/variables/variable.js
+++ b/ui/app/routes/variables/variable.js
@@ -1,16 +1,21 @@
import Route from '@ember/routing/route';
import withForbiddenState from 'nomad-ui/mixins/with-forbidden-state';
-import WithModelErrorHandling from 'nomad-ui/mixins/with-model-error-handling';
import { inject as service } from '@ember/service';
+import notifyForbidden from 'nomad-ui/utils/notify-forbidden';
export default class VariablesVariableRoute extends Route.extend(
- withForbiddenState,
- WithModelErrorHandling
+ withForbiddenState
) {
@service store;
model(params) {
- return this.store.findRecord('variable', decodeURIComponent(params.path), {
- reload: true,
- });
+ return this.store
+ .findRecord('variable', decodeURIComponent(params.path), {
+ reload: true,
+ })
+ .catch(notifyForbidden(this));
+ }
+ setupController(controller) {
+ super.setupController(controller);
+ controller.set('params', this.paramsFor('variables.variable'));
}
}
diff --git a/ui/app/styles/components/secure-variables.scss b/ui/app/styles/components/secure-variables.scss
index 12ae38730..d957e1dda 100644
--- a/ui/app/styles/components/secure-variables.scss
+++ b/ui/app/styles/components/secure-variables.scss
@@ -103,9 +103,8 @@
table.path-tree {
tr {
cursor: pointer;
- a {
- color: #0a0a0a;
- text-decoration: none;
+ &.inaccessible {
+ cursor: not-allowed;
}
svg {
margin-bottom: -2px;
diff --git a/ui/app/templates/variables/variable.hbs b/ui/app/templates/variables/variable.hbs
index 70d6e4446..7eea2143a 100644
--- a/ui/app/templates/variables/variable.hbs
+++ b/ui/app/templates/variables/variable.hbs
@@ -3,6 +3,10 @@
{{/each}}
- {{outlet}}
+ {{#if this.isForbidden}}
+
+ {{else}}
+ {{outlet}}
+ {{/if}}
\ No newline at end of file
diff --git a/ui/mirage/config.js b/ui/mirage/config.js
index 57ff741cc..d10f1eaa3 100644
--- a/ui/mirage/config.js
+++ b/ui/mirage/config.js
@@ -838,8 +838,12 @@ export default function () {
//#region Secure Variables
- this.get('/vars', function (schema) {
- return schema.variables.all();
+ this.get('/vars', function (schema, { queryParams: { namespace } }) {
+ if (namespace && namespace !== '*') {
+ return schema.variables.all().filter((v) => v.namespace === namespace);
+ } else {
+ return schema.variables.all();
+ }
});
this.get('/var/:id', function ({ variables }, { params }) {
diff --git a/ui/mirage/factories/token.js b/ui/mirage/factories/token.js
index 7971bc510..6b3d47245 100644
--- a/ui/mirage/factories/token.js
+++ b/ui/mirage/factories/token.js
@@ -34,23 +34,13 @@ export default Factory.extend({
id: 'Variable Maker',
rules: `
# Allow read only access to the default namespace
-namespace "default" {
+namespace "*" {
policy = "read"
capabilities = ["list-jobs", "alloc-exec", "read-logs"]
secure_variables {
- # full access to secrets in all project paths
- path "blue/*" {
- capabilities = ["write", "read", "destroy", "list"]
- }
-
- # full access to secrets in all project paths
+ # Base access is to all abilities for all secure variables
path "*" {
- capabilities = ["write", "read", "destroy", "list"]
- }
-
- # read/list access within a "system" path belonging to administrators
- path "system/*" {
- capabilities = ["read", "list"]
+ capabilities = ["list", "read", "destroy", "create"]
}
}
}
@@ -63,22 +53,14 @@ node {
rulesJSON: {
Namespaces: [
{
- Name: 'default',
+ Name: '*',
Capabilities: ['list-jobs', 'alloc-exec', 'read-logs'],
SecureVariables: {
Paths: [
- {
- Capabilities: ['write', 'read', 'destroy', 'list'],
- PathSpec: 'blue/*',
- },
{
Capabilities: ['write', 'read', 'destroy', 'list'],
PathSpec: '*',
},
- {
- Capabilities: ['read', 'list'],
- PathSpec: 'system/*',
- },
],
},
},
@@ -88,5 +70,99 @@ node {
server.create('policy', variableMakerPolicy);
token.policyIds.push(variableMakerPolicy.id);
}
+ if (token.id === 'f3w3r-53cur3-v4r14bl35') {
+ const variableViewerPolicy = {
+ id: 'Variable Viewer',
+ rules: `
+# Allow read only access to the default namespace
+namespace "*" {
+ policy = "read"
+ capabilities = ["list-jobs", "alloc-exec", "read-logs"]
+ secure_variables {
+ # Base access is to all abilities for all secure variables
+ path "*" {
+ capabilities = ["list"]
+ }
+ }
+}
+
+namespace "namespace-1" {
+ policy = "read"
+ capabilities = ["list-jobs", "alloc-exec", "read-logs"]
+ secure_variables {
+ # Base access is to all abilities for all secure variables
+ path "*" {
+ capabilities = ["list", "read", "destroy", "create"]
+ }
+ }
+}
+
+namespace "namespace-2" {
+ policy = "read"
+ capabilities = ["list-jobs", "alloc-exec", "read-logs"]
+ secure_variables {
+ # Base access is to all abilities for all secure variables
+ path "blue/*" {
+ capabilities = ["list", "read", "destroy", "create"]
+ }
+ path "nomad/jobs/*" {
+ capabilities = ["list", "read", "create"]
+ }
+ }
+}
+
+node {
+ policy = "read"
+}
+ `,
+
+ rulesJSON: {
+ Namespaces: [
+ {
+ Name: '*',
+ Capabilities: ['list-jobs', 'alloc-exec', 'read-logs'],
+ SecureVariables: {
+ Paths: [
+ {
+ Capabilities: ['list'],
+ PathSpec: '*',
+ },
+ ],
+ },
+ },
+ {
+ Name: 'namespace-1',
+ Capabilities: ['list-jobs', 'alloc-exec', 'read-logs'],
+ SecureVariables: {
+ Paths: [
+ {
+ Capabilities: ['list', 'read', 'destroy', 'create'],
+ PathSpec: '*',
+ },
+ ],
+ },
+ },
+ {
+ Name: 'namespace-2',
+ Capabilities: ['list-jobs', 'alloc-exec', 'read-logs'],
+ SecureVariables: {
+ Paths: [
+ {
+ Capabilities: ['list', 'read', 'destroy', 'create'],
+ PathSpec: 'blue/*',
+ },
+ {
+ Capabilities: ['list', 'read', 'create'],
+ PathSpec: 'nomad/jobs/*',
+ },
+ ],
+ },
+ },
+ ],
+ },
+ };
+ server.create('policy', variableViewerPolicy);
+ token.policyIds.push(variableViewerPolicy.id);
+ }
},
});
diff --git a/ui/mirage/factories/variable.js b/ui/mirage/factories/variable.js
index c2adb12a0..77eb0fd3b 100644
--- a/ui/mirage/factories/variable.js
+++ b/ui/mirage/factories/variable.js
@@ -1,5 +1,6 @@
import { Factory } from 'ember-cli-mirage';
import faker from 'nomad-ui/mirage/faker';
+import { provide, pickOne } from '../utils';
export default Factory.extend({
id: () => faker.random.words(3).split(' ').join('/').toLowerCase(),
@@ -25,8 +26,7 @@ export default Factory.extend({
afterCreate(variable, server) {
if (!variable.namespaceId) {
- const namespace =
- (server.db.jobs && server.db.jobs[0]?.namespace) || 'default';
+ const namespace = pickOne(server.db.jobs)?.namespace || 'default';
variable.update({
namespace,
});
diff --git a/ui/mirage/scenarios/default.js b/ui/mirage/scenarios/default.js
index 956e3e0ab..125bc0e17 100644
--- a/ui/mirage/scenarios/default.js
+++ b/ui/mirage/scenarios/default.js
@@ -7,7 +7,7 @@ const withNamespaces = getConfigValue('mirageWithNamespaces', false);
const withTokens = getConfigValue('mirageWithTokens', true);
const withRegions = getConfigValue('mirageWithRegions', false);
-const allScenarios = {
+export const allScenarios = {
smallCluster,
mediumCluster,
largeCluster,
@@ -16,6 +16,7 @@ const allScenarios = {
allNodeTypes,
everyFeature,
emptyCluster,
+ variableTestCluster,
...topoScenarios,
...sysbatchScenarios,
};
@@ -75,11 +76,23 @@ function smallCluster(server) {
'just some arbitrary file',
'another arbitrary file',
'another arbitrary file again',
- `nomad/jobs/${variableLinkedJob.id}/${variableLinkedGroup.name}/${variableLinkedTask.name}`,
- `nomad/jobs/${variableLinkedJob.id}/${variableLinkedGroup.name}`,
- `nomad/jobs/${variableLinkedJob.id}`,
].forEach((path) => server.create('variable', { id: path }));
+ server.create('variable', {
+ id: `nomad/jobs/${variableLinkedJob.id}/${variableLinkedGroup.name}/${variableLinkedTask.name}`,
+ namespaceId: variableLinkedJob.namespace,
+ });
+
+ server.create('variable', {
+ id: `nomad/jobs/${variableLinkedJob.id}/${variableLinkedGroup.name}`,
+ namespaceId: variableLinkedJob.namespace,
+ });
+
+ server.create('variable', {
+ id: `nomad/jobs/${variableLinkedJob.id}`,
+ namespaceId: variableLinkedJob.namespace,
+ });
+
// #region evaluations
// Branching: a single eval that relates to N-1 mutually-unrelated evals
@@ -156,6 +169,58 @@ function mediumCluster(server) {
server.createList('job', 25);
}
+function variableTestCluster(server) {
+ createTokens(server);
+ createNamespaces(server);
+ server.createList('agent', 3, 'withConsulLink', 'withVaultLink');
+ server.createList('node', 5);
+ server.createList('job', 3);
+ server.createList('variable', 3);
+ // server.createList('allocFile', 5);
+ // server.create('allocFile', 'dir', { depth: 2 });
+ // server.createList('csi-plugin', 2);
+
+ const variableLinkedJob = server.db.jobs[0];
+ const variableLinkedGroup = server.db.taskGroups.findBy({
+ jobId: variableLinkedJob.id,
+ });
+ const variableLinkedTask = server.db.tasks.findBy({
+ taskGroupId: variableLinkedGroup.id,
+ });
+ [
+ 'a/b/c/foo0',
+ 'a/b/c/bar1',
+ 'a/b/c/d/e/foo2',
+ 'a/b/c/d/e/bar3',
+ 'a/b/c/d/e/f/foo4',
+ 'a/b/c/d/e/f/g/foo5',
+ 'a/b/c/x/y/z/foo6',
+ 'a/b/c/x/y/z/bar7',
+ 'a/b/c/x/y/z/baz8',
+ 'w/x/y/foo9',
+ 'w/x/y/z/foo10',
+ 'w/x/y/z/bar11',
+ 'just some arbitrary file',
+ 'another arbitrary file',
+ 'another arbitrary file again',
+ ].forEach((path) => server.create('variable', { id: path }));
+
+ server.create('variable', {
+ id: `nomad/jobs/${variableLinkedJob.id}/${variableLinkedGroup.name}/${variableLinkedTask.name}`,
+ namespaceId: variableLinkedJob.namespace,
+ });
+
+ server.create('variable', {
+ id: `nomad/jobs/${variableLinkedJob.id}/${variableLinkedGroup.name}`,
+ namespaceId: variableLinkedJob.namespace,
+ });
+
+ server.create('variable', {
+ id: `nomad/jobs/${variableLinkedJob.id}`,
+ namespaceId: variableLinkedJob.namespace,
+ });
+}
+
// Due to Mirage performance, large cluster scenarios will be slow
function largeCluster(server) {
server.createList('agent', 5);
@@ -238,6 +303,10 @@ function createTokens(server) {
name: 'Secure McVariables',
id: '53cur3-v4r14bl35',
});
+ server.create('token', {
+ name: "Safe O'Constants",
+ id: 'f3w3r-53cur3-v4r14bl35',
+ });
logTokens(server);
}
diff --git a/ui/tests/acceptance/secure-variables-test.js b/ui/tests/acceptance/secure-variables-test.js
index 36bd51403..7cf0a1727 100644
--- a/ui/tests/acceptance/secure-variables-test.js
+++ b/ui/tests/acceptance/secure-variables-test.js
@@ -15,7 +15,7 @@ import {
import { setupApplicationTest } from 'ember-qunit';
import { module, test } from 'qunit';
import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
-import defaultScenario from '../../mirage/scenarios/default';
+import { allScenarios } from '../../mirage/scenarios/default';
import cleanWhitespace from '../utils/clean-whitespace';
import percySnapshot from '@percy/ember';
@@ -23,6 +23,7 @@ import Variables from 'nomad-ui/tests/pages/variables';
import Layout from 'nomad-ui/tests/pages/layout';
const SECURE_TOKEN_ID = '53cur3-v4r14bl35';
+const LIMITED_SECURE_TOKEN_ID = 'f3w3r-53cur3-v4r14bl35';
module('Acceptance | secure variables', function (hooks) {
setupApplicationTest(hooks);
@@ -38,7 +39,7 @@ module('Acceptance | secure variables', function (hooks) {
});
test('it allows access for management level tokens', async function (assert) {
- defaultScenario(server);
+ allScenarios.variableTestCluster(server);
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
await Variables.visit();
assert.equal(currentURL(), '/variables');
@@ -47,7 +48,7 @@ module('Acceptance | secure variables', function (hooks) {
test('it allows access for list-variables allowed ACL rules', async function (assert) {
assert.expect(2);
- defaultScenario(server);
+ allScenarios.variableTestCluster(server);
const variablesToken = server.db.tokens.find(SECURE_TOKEN_ID);
window.localStorage.nomadTokenSecret = variablesToken.secretId;
@@ -59,7 +60,7 @@ module('Acceptance | secure variables', function (hooks) {
test('it correctly traverses to and deletes a variable', async function (assert) {
assert.expect(13);
- defaultScenario(server);
+ allScenarios.variableTestCluster(server);
const variablesToken = server.db.tokens.find(SECURE_TOKEN_ID);
window.localStorage.nomadTokenSecret = variablesToken.secretId;
server.db.variables.update({ namespace: 'default' });
@@ -110,7 +111,7 @@ module('Acceptance | secure variables', function (hooks) {
const deleteButton = find('[data-test-delete-button] button');
assert.dom(deleteButton).exists('delete button is present');
- await percySnapshot(assert);
+ await percySnapshot('deeply nested variable');
await click(deleteButton);
assert
@@ -144,7 +145,7 @@ module('Acceptance | secure variables', function (hooks) {
test('variables prefixed with nomad/jobs/ correctly link to entities', async function (assert) {
assert.expect(23);
- defaultScenario(server);
+ allScenarios.variableTestCluster(server);
const variablesToken = server.db.tokens.find(SECURE_TOKEN_ID);
const variableLinkedJob = server.db.jobs[0];
@@ -154,10 +155,10 @@ module('Acceptance | secure variables', function (hooks) {
const variableLinkedTask = server.db.tasks.findBy({
taskGroupId: variableLinkedGroup.id,
});
- const variableLinkedTaskAlloc = server.db.allocations.filterBy(
- 'taskGroup',
- variableLinkedGroup.name
- )[1];
+ const variableLinkedTaskAlloc = server.db.allocations
+ .filterBy('taskGroup', variableLinkedGroup.name)
+ ?.find((alloc) => alloc.taskStateIds.length);
+
window.localStorage.nomadTokenSecret = variablesToken.secretId;
// Non-job variable
@@ -213,7 +214,7 @@ module('Acceptance | secure variables', function (hooks) {
'Related Entities box is job-oriented'
);
- await percySnapshot(assert);
+ await percySnapshot('related entities box for job variable');
let relatedJobLink = find('.related-entities a');
await click(relatedJobLink);
@@ -249,7 +250,7 @@ module('Acceptance | secure variables', function (hooks) {
'Related Entities box is group-oriented'
);
- await percySnapshot(assert);
+ await percySnapshot('related entities box for group variable');
let relatedGroupLink = find('.related-entities a');
await click(relatedGroupLink);
@@ -287,7 +288,7 @@ module('Acceptance | secure variables', function (hooks) {
'Related Entities box is task-oriented'
);
- await percySnapshot(assert);
+ await percySnapshot('related entities box for task variable');
let relatedTaskLink = find('.related-entities a');
await click(relatedTaskLink);
@@ -316,7 +317,7 @@ module('Acceptance | secure variables', function (hooks) {
test('it does not allow you to save if you lack Items', async function (assert) {
assert.expect(5);
- defaultScenario(server);
+ allScenarios.variableTestCluster(server);
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
await Variables.visitNew();
assert.equal(currentURL(), '/variables/new');
@@ -339,7 +340,7 @@ module('Acceptance | secure variables', function (hooks) {
test('it passes an accessibility audit', async function (assert) {
assert.expect(1);
- defaultScenario(server);
+ allScenarios.variableTestCluster(server);
const variablesToken = server.db.tokens.find(SECURE_TOKEN_ID);
window.localStorage.nomadTokenSecret = variablesToken.secretId;
await Variables.visit();
@@ -349,7 +350,7 @@ module('Acceptance | secure variables', function (hooks) {
module('create flow', function () {
test('allows a user with correct permissions to create a secure variable', async function (assert) {
// Arrange Test Set-up
- defaultScenario(server);
+ allScenarios.variableTestCluster(server);
server.createList('variable', 3);
const variablesToken = server.db.tokens.find(SECURE_TOKEN_ID);
window.localStorage.nomadTokenSecret = variablesToken.secretId;
@@ -402,7 +403,7 @@ module('Acceptance | secure variables', function (hooks) {
test('prevents users from creating a secure variable without proper permissions', async function (assert) {
// Arrange Test Set-up
- defaultScenario(server);
+ allScenarios.variableTestCluster(server);
server.createList('variable', 3);
const variablesToken = server.db.tokens.find(SECURE_TOKEN_ID);
window.localStorage.nomadTokenSecret = variablesToken.secretId;
@@ -425,7 +426,7 @@ module('Acceptance | secure variables', function (hooks) {
test('allows creating a variable that starts with nomad/jobs/', async function (assert) {
// Arrange Test Set-up
- defaultScenario(server);
+ allScenarios.variableTestCluster(server);
server.createList('variable', 3);
const variablesToken = server.db.tokens.find(SECURE_TOKEN_ID);
window.localStorage.nomadTokenSecret = variablesToken.secretId;
@@ -452,7 +453,7 @@ module('Acceptance | secure variables', function (hooks) {
test('disallows creating a variable that starts with nomad//', async function (assert) {
// Arrange Test Set-up
- defaultScenario(server);
+ allScenarios.variableTestCluster(server);
server.createList('variable', 3);
const variablesToken = server.db.tokens.find(SECURE_TOKEN_ID);
window.localStorage.nomadTokenSecret = variablesToken.secretId;
@@ -477,14 +478,14 @@ module('Acceptance | secure variables', function (hooks) {
test('allows a user with correct permissions to edit a secure variable', async function (assert) {
assert.expect(8);
// Arrange Test Set-up
- defaultScenario(server);
+ allScenarios.variableTestCluster(server);
server.createList('variable', 3);
const variablesToken = server.db.tokens.find(SECURE_TOKEN_ID);
window.localStorage.nomadTokenSecret = variablesToken.secretId;
const policy = server.db.policies.find('Variable Maker');
policy.rulesJSON.Namespaces[0].SecureVariables.Paths.find(
(path) => path.PathSpec === '*'
- ).Capabilities = ['list', 'write'];
+ ).Capabilities = ['list', 'read', 'write'];
server.db.variables.update({ namespace: 'default' });
await Variables.visit();
await click('[data-test-file-row]');
@@ -532,14 +533,14 @@ module('Acceptance | secure variables', function (hooks) {
test('prevents users from editing a secure variable without proper permissions', async function (assert) {
// Arrange Test Set-up
- defaultScenario(server);
+ allScenarios.variableTestCluster(server);
server.createList('variable', 3);
const variablesToken = server.db.tokens.find(SECURE_TOKEN_ID);
window.localStorage.nomadTokenSecret = variablesToken.secretId;
const policy = server.db.policies.find('Variable Maker');
policy.rulesJSON.Namespaces[0].SecureVariables.Paths.find(
(path) => path.PathSpec === '*'
- ).Capabilities = ['list'];
+ ).Capabilities = ['list', 'read'];
await Variables.visit();
await click('[data-test-file-row]');
// End Test Set-up
@@ -557,19 +558,18 @@ module('Acceptance | secure variables', function (hooks) {
module('delete flow', function () {
test('allows a user with correct permissions to delete a secure variable', async function (assert) {
// Arrange Test Set-up
- defaultScenario(server);
+ allScenarios.variableTestCluster(server);
server.createList('variable', 3);
const variablesToken = server.db.tokens.find(SECURE_TOKEN_ID);
window.localStorage.nomadTokenSecret = variablesToken.secretId;
const policy = server.db.policies.find('Variable Maker');
policy.rulesJSON.Namespaces[0].SecureVariables.Paths.find(
(path) => path.PathSpec === '*'
- ).Capabilities = ['list', 'destroy'];
+ ).Capabilities = ['list', 'read', 'destroy'];
server.db.variables.update({ namespace: 'default' });
await Variables.visit();
await click('[data-test-file-row]');
// End Test Set-up
-
assert.equal(currentRouteName(), 'variables.variable.index');
assert
.dom('[data-test-delete-button]')
@@ -594,14 +594,14 @@ module('Acceptance | secure variables', function (hooks) {
test('prevents users from delete a secure variable without proper permissions', async function (assert) {
// Arrange Test Set-up
- defaultScenario(server);
+ allScenarios.variableTestCluster(server);
server.createList('variable', 3);
const variablesToken = server.db.tokens.find(SECURE_TOKEN_ID);
window.localStorage.nomadTokenSecret = variablesToken.secretId;
const policy = server.db.policies.find('Variable Maker');
policy.rulesJSON.Namespaces[0].SecureVariables.Paths.find(
(path) => path.PathSpec === '*'
- ).Capabilities = ['list'];
+ ).Capabilities = ['list', 'read'];
await Variables.visit();
await click('[data-test-file-row]');
// End Test Set-up
@@ -616,12 +616,51 @@ module('Acceptance | secure variables', function (hooks) {
});
});
+ module('read flow', function () {
+ test('allows a user with correct permissions to read a secure variable', async function (assert) {
+ allScenarios.variableTestCluster(server);
+ const variablesToken = server.db.tokens.find(SECURE_TOKEN_ID);
+ window.localStorage.nomadTokenSecret = variablesToken.secretId;
+ await Variables.visit();
+
+ assert
+ .dom('[data-test-file-row]:not(.inaccessible)')
+ .exists(
+ { count: 3 },
+ 'Shows 3 variable files, none of which are inaccessible'
+ );
+
+ await click('[data-test-file-row]');
+ assert.equal(currentRouteName(), 'variables.variable.index');
+
+ // Reset Token
+ window.localStorage.nomadTokenSecret = null;
+ });
+
+ test('prevents users from reading a secure variable without proper permissions', async function (assert) {
+ allScenarios.variableTestCluster(server);
+ const variablesToken = server.db.tokens.find(LIMITED_SECURE_TOKEN_ID);
+ window.localStorage.nomadTokenSecret = variablesToken.secretId;
+ await Variables.visit();
+
+ assert
+ .dom('[data-test-file-row].inaccessible')
+ .exists(
+ { count: 3 },
+ 'Shows 3 variable files, all of which are inaccessible'
+ );
+
+ // Reset Token
+ window.localStorage.nomadTokenSecret = null;
+ });
+ });
+
module('namespace filtering', function () {
test('allows a user to filter variables by namespace', async function (assert) {
assert.expect(3);
// Arrange
- defaultScenario(server);
+ allScenarios.variableTestCluster(server);
server.createList('variable', 3);
const variablesToken = server.db.tokens.find(SECURE_TOKEN_ID);
window.localStorage.nomadTokenSecret = variablesToken.secretId;
@@ -653,7 +692,7 @@ module('Acceptance | secure variables', function (hooks) {
});
test('does not show namespace filtering if the user only has access to one namespace', async function (assert) {
- defaultScenario(server);
+ allScenarios.variableTestCluster(server);
server.createList('variable', 3);
const variablesToken = server.db.tokens.find(SECURE_TOKEN_ID);
window.localStorage.nomadTokenSecret = variablesToken.secretId;
@@ -676,7 +715,7 @@ module('Acceptance | secure variables', function (hooks) {
assert.expect(4);
// Arrange
- defaultScenario(server);
+ allScenarios.variableTestCluster(server);
server.createList('variable', 3);
const variablesToken = server.db.tokens.find(SECURE_TOKEN_ID);
window.localStorage.nomadTokenSecret = variablesToken.secretId;
@@ -715,7 +754,7 @@ module('Acceptance | secure variables', function (hooks) {
});
test('does not show namespace filtering if the user only has access to one namespace', async function (assert) {
- defaultScenario(server);
+ allScenarios.variableTestCluster(server);
server.createList('variable', 3);
const variablesToken = server.db.tokens.find(SECURE_TOKEN_ID);
window.localStorage.nomadTokenSecret = variablesToken.secretId;
diff --git a/ui/tests/integration/components/variable-paths-test.js b/ui/tests/integration/components/variable-paths-test.js
index aaf2a5d65..fedfbf0df 100644
--- a/ui/tests/integration/components/variable-paths-test.js
+++ b/ui/tests/integration/components/variable-paths-test.js
@@ -1,9 +1,11 @@
+/* eslint-disable ember/avoid-leaking-state-in-ember-objects */
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit';
import pathTree from 'nomad-ui/utils/path-tree';
+import Service from '@ember/service';
const PATHSTRINGS = [
{ path: '/foo/bar/baz' },
@@ -69,6 +71,36 @@ module('Integration | Component | variable-paths', function (hooks) {
});
test('it allows for traversal: Files', async function (assert) {
+ // Arrange Test Set-up
+ const mockToken = Service.extend({
+ selfTokenPolicies: [
+ [
+ {
+ rulesJSON: {
+ Namespaces: [
+ {
+ Name: '*',
+ Capabilities: ['list-jobs', 'alloc-exec', 'read-logs'],
+ SecureVariables: {
+ Paths: [
+ {
+ Capabilities: ['list', 'read'],
+ PathSpec: '*',
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ ],
+ ],
+ });
+
+ this.owner.register('service:token', mockToken);
+
+ // End Test Set-up
+
assert.expect(5);
this.set('tree', tree.findPath('foo/bar'));
diff --git a/ui/tests/unit/abilities/variable-test.js b/ui/tests/unit/abilities/variable-test.js
index 89335b810..3434c00b6 100644
--- a/ui/tests/unit/abilities/variable-test.js
+++ b/ui/tests/unit/abilities/variable-test.js
@@ -406,6 +406,101 @@ module('Unit | Ability | variable', function (hooks) {
});
});
+ 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 SecureVariables with read capabilities in its rules', function (assert) {
+ const mockToken = Service.extend({
+ aclEnabled: true,
+ selfToken: { type: 'client' },
+ selfTokenPolicies: [
+ {
+ rulesJSON: {
+ Namespaces: [
+ {
+ Name: 'default',
+ Capabilities: [],
+ SecureVariables: {
+ 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: [],
+ SecureVariables: {
+ Paths: [{ Capabilities: ['list'], PathSpec: 'foo/bar' }],
+ },
+ },
+ {
+ Name: 'pablo',
+ Capabilities: [],
+ SecureVariables: {
+ 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({
|