Unverified Commit 9b367a5f authored by Phil Renaud's avatar Phil Renaud Committed by GitHub
Browse files

[ui] "Can Read" checks on individual Secure Variables (#14020)

* Changelog and lintfix

* Changelog removed

* Forbidden state on individual variables

* CanRead checked on variable path links

* Mirage fixture with lesser secure variables access, temporary fix for * namespaces

* Read flow acceptance tests

* Unit tests for variable.canRead

* lintfix

* TODO squashed, thanks Jai

* explicitly link mirage fixture vars to jobs via namespace

* Typofix; delete to read

* Linking the original alloc

* Percy snapshots uniquely named

* Guarantee that the alloc we depend on has tasks within it

* Logging variables

* Trying to skip delete

* Now without create flow either

* Dedicated cluster fixture for testing variables

* Disambiguate percy calls
parent 4ac50de4
Showing with 421 additions and 73 deletions
+421 -73
......@@ -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;
}
......
......@@ -25,9 +25,14 @@
{{/each}}
{{#each this.files as |file|}}
<tr data-test-file-row {{on "click" (fn this.handleFileClick file.absoluteFilePath)}}>
<tr
data-test-file-row
{{on "click" (fn this.handleFileClick file)}}
class={{if (can "read variable" path=file.absoluteFilePath namespace=file.variable.namespace) "" "inaccessible"}}
>
<td>
<FlightIcon @name="file-text" />
{{#if (can "read variable" path=file.absoluteFilePath namespace=file.variable.namespace)}}
<LinkTo
@route="variables.variable"
@model={{file.absoluteFilePath}}
......@@ -35,6 +40,9 @@
>
{{file.name}}
</LinkTo>
{{else}}
<span title="Your access policy does not allow you to view the contents of {{file.name}}">{{file.name}}</span>
{{/if}}
</td>
<td>
{{file.variable.namespace}}
......
......@@ -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<Object.<string, Object>>}
......@@ -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);
}
}
}
......@@ -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],
});
......
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'));
}
}
......@@ -103,9 +103,8 @@
table.path-tree {
tr {
cursor: pointer;
a {
color: #0a0a0a;
text-decoration: none;
&.inaccessible {
cursor: not-allowed;
}
svg {
margin-bottom: -2px;
......
......@@ -3,6 +3,10 @@
<Breadcrumb @crumb={{crumb}} />
{{/each}}
<section class="section single-variable">
{{outlet}}
{{#if this.isForbidden}}
<ForbiddenMessage />
{{else}}
{{outlet}}
{{/if}}
</section>
\ No newline at end of file
......@@ -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 }) {
......
......@@ -34,23 +34,79 @@ 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"]
# Base access is to all abilities for all secure variables
path "*" {
capabilities = ["list", "read", "destroy", "create"]
}
}
}
node {
policy = "read"
}
`,
# full access to secrets in all project paths
rulesJSON: {
Namespaces: [
{
Name: '*',
Capabilities: ['list-jobs', 'alloc-exec', 'read-logs'],
SecureVariables: {
Paths: [
{
Capabilities: ['write', 'read', 'destroy', 'list'],
PathSpec: '*',
},
],
},
},
],
},
};
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 = ["write", "read", "destroy", "list"]
capabilities = ["list"]
}
}
}
# read/list access within a "system" path belonging to administrators
path "system/*" {
capabilities = ["read", "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"]
}
}
}
......@@ -63,21 +119,41 @@ node {
rulesJSON: {
Namespaces: [
{
Name: 'default',
Name: '*',
Capabilities: ['list-jobs', 'alloc-exec', 'read-logs'],
SecureVariables: {
Paths: [
{
Capabilities: ['write', 'read', 'destroy', 'list'],
PathSpec: 'blue/*',
Capabilities: ['list'],
PathSpec: '*',
},
],
},
},
{
Name: 'namespace-1',
Capabilities: ['list-jobs', 'alloc-exec', 'read-logs'],
SecureVariables: {
Paths: [
{
Capabilities: ['write', 'read', 'destroy', 'list'],
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: ['read', 'list'],
PathSpec: 'system/*',
Capabilities: ['list', 'read', 'create'],
PathSpec: 'nomad/jobs/*',
},
],
},
......@@ -85,8 +161,8 @@ node {
],
},
};
server.create('policy', variableMakerPolicy);
token.policyIds.push(variableMakerPolicy.id);
server.create('policy', variableViewerPolicy);
token.policyIds.push(variableViewerPolicy.id);
}
},
});
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,
});
......
......@@ -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);
}
......
......@@ -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/<something-other-than-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;
......@@ -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;
......
/* 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'));
......
......@@ -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({
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment