Secure Variables UI: Router setup and /variables/index route + guards (#12967)
* Route init * Bones of a mirage-mocked secure variables policy * Functinoing policy for list vars * Delog and transition on route * Basic guard test * Page guard tests for secure variables * Cleanup and unit tests for variables ability * Linter cleanup * Set expectations for test assertions * PR feedback addressed * Read label changed to View per suggestion
This commit is contained in:
parent
2019eab2c8
commit
7ddc4c8359
19
ui/app/abilities/variable.js
Normal file
19
ui/app/abilities/variable.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import AbstractAbility from './abstract';
|
||||
import { computed, get } from '@ember/object';
|
||||
import { or } from '@ember/object/computed';
|
||||
|
||||
export default class extends AbstractAbility {
|
||||
@or(
|
||||
'bypassAuthorization',
|
||||
'selfTokenIsManagement',
|
||||
'policiesSupportVariableView'
|
||||
)
|
||||
canList;
|
||||
|
||||
@computed('rulesForNamespace.@each.capabilities')
|
||||
get policiesSupportVariableView() {
|
||||
return this.rulesForNamespace.some((rules) => {
|
||||
return get(rules, 'SecureVariables');
|
||||
});
|
||||
}
|
||||
}
|
5
ui/app/controllers/variables/index.js
Normal file
5
ui/app/controllers/variables/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import Controller from '@ember/controller';
|
||||
|
||||
export default class VariablesIndexController extends Controller {
|
||||
isForbidden = false;
|
||||
}
|
|
@ -78,4 +78,5 @@ Router.map(function () {
|
|||
this.route('evaluations', function () {});
|
||||
|
||||
this.route('not-found', { path: '/*' });
|
||||
this.route('variables', function () {});
|
||||
});
|
||||
|
|
18
ui/app/routes/variables.js
Normal file
18
ui/app/routes/variables.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import withForbiddenState from 'nomad-ui/mixins/with-forbidden-state';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class VariablesRoute extends Route.extend(withForbiddenState) {
|
||||
@service can;
|
||||
@service router;
|
||||
|
||||
beforeModel() {
|
||||
if (this.can.cannot('list variables')) {
|
||||
this.router.transitionTo('/jobs');
|
||||
}
|
||||
}
|
||||
model() {
|
||||
// TODO: Populate model from /variables
|
||||
return {};
|
||||
}
|
||||
}
|
17
ui/app/routes/variables/index.js
Normal file
17
ui/app/routes/variables/index.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import withForbiddenState from '../../mixins/with-forbidden-state';
|
||||
|
||||
export default class VariablesIndexRoute extends Route.extend(
|
||||
withForbiddenState
|
||||
) {
|
||||
model() {
|
||||
// TODO: Fill in model with format from API
|
||||
return {};
|
||||
// return RSVP.hash({
|
||||
// variables: this.store
|
||||
// .query('variable', { namespace: params.qpNamespace })
|
||||
// .catch(notifyForbidden(this)),
|
||||
// namespaces: this.store.findAll('namespace'),
|
||||
// });
|
||||
}
|
||||
}
|
|
@ -32,9 +32,6 @@
|
|||
</ul>
|
||||
</div>
|
||||
{{/if}}
|
||||
<p class="menu-label">
|
||||
Workload
|
||||
</p>
|
||||
<ul class="menu-list">
|
||||
<li>
|
||||
<LinkTo
|
||||
|
@ -56,11 +53,6 @@
|
|||
</LinkTo>
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
<p class="menu-label is-minor">
|
||||
Integrations
|
||||
</p>
|
||||
<ul class="menu-list">
|
||||
<li>
|
||||
<LinkTo
|
||||
@route="csi"
|
||||
|
@ -70,6 +62,17 @@
|
|||
Storage
|
||||
</LinkTo>
|
||||
</li>
|
||||
{{#if (can "list variables")}}
|
||||
<li>
|
||||
<LinkTo
|
||||
@route="variables"
|
||||
@activeClass="is-active"
|
||||
data-test-gutter-link="variables"
|
||||
>
|
||||
Variables
|
||||
</LinkTo>
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
<p class="menu-label">
|
||||
Cluster
|
||||
|
|
4
ui/app/templates/variables.hbs
Normal file
4
ui/app/templates/variables.hbs
Normal file
|
@ -0,0 +1,4 @@
|
|||
<Breadcrumb @crumb={{hash label="Secure Variables" args=(array "variables.index")}} />
|
||||
<PageLayout>
|
||||
{{outlet}}
|
||||
</PageLayout>
|
15
ui/app/templates/variables/index.hbs
Normal file
15
ui/app/templates/variables/index.hbs
Normal file
|
@ -0,0 +1,15 @@
|
|||
{{page-title "Secure Variables"}}
|
||||
<section class="section">
|
||||
{{#if this.isForbidden}}
|
||||
<ForbiddenMessage />
|
||||
{{else}}
|
||||
<div class="empty-message">
|
||||
<h3 data-test-empty-volumes-list-headline class="empty-message-headline">
|
||||
No Secure Variables
|
||||
</h3>
|
||||
<p class="empty-message-body">
|
||||
Get started by <LinkTo @route="variables">creating a new secure variable</LinkTo>
|
||||
</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
</section>
|
|
@ -7,9 +7,9 @@ export default Factory.extend({
|
|||
return this.id;
|
||||
},
|
||||
secretId: () => faker.random.uuid(),
|
||||
name: () => faker.name.findName(),
|
||||
name: (i) => `${i === 0 ? 'Manager ' : ''}${faker.name.findName()}`,
|
||||
global: () => faker.random.boolean(),
|
||||
type: i => (i === 0 ? 'management' : 'client'),
|
||||
type: (i) => (i === 0 ? 'management' : 'client'),
|
||||
|
||||
oneTimeSecret: () => faker.random.uuid(),
|
||||
|
||||
|
@ -19,7 +19,7 @@ export default Factory.extend({
|
|||
.map(() => faker.hacker.verb())
|
||||
.uniq();
|
||||
|
||||
policyIds.forEach(policy => {
|
||||
policyIds.forEach((policy) => {
|
||||
const dbPolicy = server.db.policies.find(policy);
|
||||
if (!dbPolicy) {
|
||||
server.create('policy', { id: policy });
|
||||
|
@ -27,5 +27,44 @@ export default Factory.extend({
|
|||
});
|
||||
|
||||
token.update({ policyIds });
|
||||
|
||||
// Create a special policy with secure variables rules in place
|
||||
if (token.id === '53cur3-v4r14bl35') {
|
||||
const variableMakerPolicy = {
|
||||
id: 'Variable Maker',
|
||||
rules: `
|
||||
# Allow read only access to the default namespace
|
||||
namespace "default" {
|
||||
policy = "read"
|
||||
capabilities = ["list-jobs", "alloc-exec", "read-logs"]
|
||||
secure_variables {
|
||||
path "*" {
|
||||
capabilities = ["list"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
node {
|
||||
policy = "read"
|
||||
}
|
||||
`,
|
||||
|
||||
rulesJSON: {
|
||||
Namespaces: [
|
||||
{
|
||||
Name: 'default',
|
||||
Capabilities: ['list-jobs', 'alloc-exec', 'read-logs'],
|
||||
SecureVariables: {
|
||||
'Path "*"': {
|
||||
Capabilities: ['list'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
server.create('policy', variableMakerPolicy);
|
||||
token.policyIds.push(variableMakerPolicy.id);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -205,6 +205,10 @@ function emptyCluster(server) {
|
|||
|
||||
function createTokens(server) {
|
||||
server.createList('token', 3);
|
||||
server.create('token', {
|
||||
name: 'Secure McVariables',
|
||||
id: '53cur3-v4r14bl35',
|
||||
});
|
||||
logTokens(server);
|
||||
}
|
||||
|
||||
|
|
53
ui/tests/acceptance/secure-variables-test.js
Normal file
53
ui/tests/acceptance/secure-variables-test.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { currentURL } from '@ember/test-helpers';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
|
||||
import defaultScenario from '../../mirage/scenarios/default';
|
||||
|
||||
import Variables from 'nomad-ui/tests/pages/variables';
|
||||
import Layout from 'nomad-ui/tests/pages/layout';
|
||||
|
||||
const SECURE_TOKEN_ID = '53cur3-v4r14bl35';
|
||||
|
||||
module('Acceptance | secure variables', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
module('Guarding page access', function () {
|
||||
test('it redirects to jobs and hides the gutter link when the token lacks permissions', async function (assert) {
|
||||
await Variables.visit();
|
||||
assert.equal(currentURL(), '/jobs');
|
||||
assert.ok(Layout.gutter.variables.isHidden);
|
||||
});
|
||||
|
||||
test('it allows access for management level tokens', async function (assert) {
|
||||
defaultScenario(server);
|
||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
||||
|
||||
await Variables.visit();
|
||||
assert.equal(currentURL(), '/variables');
|
||||
assert.ok(Layout.gutter.variables.isVisible);
|
||||
});
|
||||
|
||||
test('it allows access for list-variables allowed ACL rules', async function (assert) {
|
||||
assert.expect(2);
|
||||
defaultScenario(server);
|
||||
const variablesToken = server.db.tokens.find(SECURE_TOKEN_ID);
|
||||
window.localStorage.nomadTokenSecret = variablesToken.secretId;
|
||||
|
||||
await Variables.visit();
|
||||
assert.equal(currentURL(), '/variables');
|
||||
assert.ok(Layout.gutter.variables.isVisible);
|
||||
});
|
||||
|
||||
test('it passes an accessibility audit', async function (assert) {
|
||||
assert.expect(1);
|
||||
defaultScenario(server);
|
||||
const variablesToken = server.db.tokens.find(SECURE_TOKEN_ID);
|
||||
window.localStorage.nomadTokenSecret = variablesToken.secretId;
|
||||
await Variables.visit();
|
||||
await a11yAudit(assert);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -73,6 +73,10 @@ export default create({
|
|||
scope: '[data-test-gutter-link="optimize"]',
|
||||
},
|
||||
|
||||
variables: {
|
||||
scope: '[data-test-gutter-link="variables"]',
|
||||
},
|
||||
|
||||
visitClients: clickable('[data-test-gutter-link="clients"]'),
|
||||
visitServers: clickable('[data-test-gutter-link="servers"]'),
|
||||
visitStorage: clickable('[data-test-gutter-link="storage"]'),
|
||||
|
|
5
ui/tests/pages/variables.js
Normal file
5
ui/tests/pages/variables.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { create, visitable } from 'ember-cli-page-object';
|
||||
|
||||
export default create({
|
||||
visit: visitable('/variables'),
|
||||
});
|
104
ui/tests/unit/abilities/variable-test.js
Normal file
104
ui/tests/unit/abilities/variable-test.js
Normal file
|
@ -0,0 +1,104 @@
|
|||
/* eslint-disable ember/avoid-leaking-state-in-ember-objects */
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import Service from '@ember/service';
|
||||
import setupAbility from 'nomad-ui/tests/helpers/setup-ability';
|
||||
|
||||
module('Unit | Ability | variable', function (hooks) {
|
||||
setupTest(hooks);
|
||||
setupAbility('variable')(hooks);
|
||||
|
||||
module('when the Variables feature is not present', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
const mockSystem = Service.extend({
|
||||
features: [],
|
||||
});
|
||||
|
||||
this.owner.register('service:system', mockSystem);
|
||||
});
|
||||
|
||||
test('it does not permit listing variables by default', function (assert) {
|
||||
const mockToken = Service.extend({
|
||||
aclEnabled: true,
|
||||
});
|
||||
|
||||
this.owner.register('service:token', mockToken);
|
||||
|
||||
assert.notOk(this.ability.canList);
|
||||
});
|
||||
|
||||
test('it does not permit listing variables when token type is client', function (assert) {
|
||||
const mockToken = Service.extend({
|
||||
aclEnabled: true,
|
||||
selfToken: { type: 'client' },
|
||||
});
|
||||
|
||||
this.owner.register('service:token', mockToken);
|
||||
|
||||
assert.notOk(this.ability.canList);
|
||||
});
|
||||
|
||||
test('it permits listing variables when token type is management', function (assert) {
|
||||
const mockToken = Service.extend({
|
||||
aclEnabled: true,
|
||||
selfToken: { type: 'management' },
|
||||
});
|
||||
|
||||
this.owner.register('service:token', mockToken);
|
||||
|
||||
assert.ok(this.ability.canList);
|
||||
});
|
||||
|
||||
test('it permits listing variables when token has SecureVariables with list capabilities in its rules', function (assert) {
|
||||
const mockToken = Service.extend({
|
||||
aclEnabled: true,
|
||||
selfToken: { type: 'client' },
|
||||
selfTokenPolicies: [
|
||||
{
|
||||
rulesJSON: {
|
||||
Namespaces: [
|
||||
{
|
||||
Name: 'default',
|
||||
Capabilities: [],
|
||||
SecureVariables: {
|
||||
'Path "*"': {
|
||||
Capabilities: ['list'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
this.owner.register('service:token', mockToken);
|
||||
|
||||
assert.ok(this.ability.canList);
|
||||
});
|
||||
|
||||
test('it permits listing variables when token has SecureVariables alone in its rules', function (assert) {
|
||||
const mockToken = Service.extend({
|
||||
aclEnabled: true,
|
||||
selfToken: { type: 'client' },
|
||||
selfTokenPolicies: [
|
||||
{
|
||||
rulesJSON: {
|
||||
Namespaces: [
|
||||
{
|
||||
Name: 'default',
|
||||
Capabilities: [],
|
||||
SecureVariables: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
this.owner.register('service:token', mockToken);
|
||||
|
||||
assert.ok(this.ability.canList);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue