mirror of
https://github.com/kemko/nomad.git
synced 2026-01-06 10:25:42 +03:00
[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
This commit is contained in:
@@ -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],
|
||||
});
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user