Merge pull request #7028 from hashicorp/f-ui/node-drain-disable
UI: Disable client write actions when ACL token only allows client read
This commit is contained in:
commit
ef33a47553
|
@ -0,0 +1,26 @@
|
|||
import { Ability } from 'ember-can';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { computed, get } from '@ember/object';
|
||||
import { equal, or, not } from '@ember/object/computed';
|
||||
|
||||
export default Ability.extend({
|
||||
token: service(),
|
||||
|
||||
// Map abilities to policy options (which are coarse for nodes)
|
||||
// instead of specific behaviors.
|
||||
canWrite: or('bypassAuthorization', 'selfTokenIsManagement', 'policiesIncludeNodeWrite'),
|
||||
|
||||
bypassAuthorization: not('token.aclEnabled'),
|
||||
selfTokenIsManagement: equal('token.selfToken.type', 'management'),
|
||||
|
||||
policiesIncludeNodeWrite: computed('token.selfTokenPolicies.[]', function() {
|
||||
// For each policy record, extract the Node policy
|
||||
const policies = (this.get('token.selfTokenPolicies') || [])
|
||||
.toArray()
|
||||
.map(policy => get(policy, 'rulesJSON.Node.Policy'))
|
||||
.compact();
|
||||
|
||||
// Node write is allowed if any policy allows it
|
||||
return policies.some(policy => policy === 'write');
|
||||
}),
|
||||
});
|
|
@ -1,14 +1,15 @@
|
|||
import { Ability } from 'ember-can';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { computed, get } from '@ember/object';
|
||||
import { equal, or } from '@ember/object/computed';
|
||||
import { equal, or, not } from '@ember/object/computed';
|
||||
|
||||
export default Ability.extend({
|
||||
system: service(),
|
||||
token: service(),
|
||||
|
||||
canRun: or('selfTokenIsManagement', 'policiesSupportRunning'),
|
||||
canRun: or('bypassAuthorization', 'selfTokenIsManagement', 'policiesSupportRunning'),
|
||||
|
||||
bypassAuthorization: not('token.aclEnabled'),
|
||||
selfTokenIsManagement: equal('token.selfToken.type', 'management'),
|
||||
|
||||
activeNamespace: computed('system.activeNamespace.name', function() {
|
||||
|
|
|
@ -9,6 +9,7 @@ export default Component.extend({
|
|||
tagName: '',
|
||||
|
||||
client: null,
|
||||
isDisabled: false,
|
||||
|
||||
onError() {},
|
||||
onDrain() {},
|
||||
|
|
|
@ -16,6 +16,7 @@ export default Component.extend({
|
|||
|
||||
triggerClass: '',
|
||||
isOpen: false,
|
||||
isDisabled: false,
|
||||
label: '',
|
||||
|
||||
dropdown: null,
|
||||
|
|
|
@ -2,6 +2,7 @@ import { inject as service } from '@ember/service';
|
|||
import { reads } from '@ember/object/computed';
|
||||
import Controller from '@ember/controller';
|
||||
import { getOwner } from '@ember/application';
|
||||
import { alias } from '@ember/object/computed';
|
||||
|
||||
export default Controller.extend({
|
||||
token: service(),
|
||||
|
@ -12,7 +13,7 @@ export default Controller.extend({
|
|||
|
||||
tokenIsValid: false,
|
||||
tokenIsInvalid: false,
|
||||
tokenRecord: null,
|
||||
tokenRecord: alias('token.selfToken'),
|
||||
|
||||
resetStore() {
|
||||
this.store.unloadAll();
|
||||
|
@ -26,9 +27,9 @@ export default Controller.extend({
|
|||
this.setProperties({
|
||||
tokenIsValid: false,
|
||||
tokenIsInvalid: false,
|
||||
tokenRecord: null,
|
||||
});
|
||||
this.resetStore();
|
||||
this.token.reset();
|
||||
},
|
||||
|
||||
verifyToken() {
|
||||
|
@ -38,22 +39,20 @@ export default Controller.extend({
|
|||
this.set('token.secret', secret);
|
||||
|
||||
TokenAdapter.findSelf().then(
|
||||
token => {
|
||||
// Capture the token ID before clearing the store
|
||||
const tokenId = token.get('id');
|
||||
|
||||
() => {
|
||||
// Clear out all data to ensure only data the new token is privileged to
|
||||
// see is shown
|
||||
this.system.reset();
|
||||
this.resetStore();
|
||||
|
||||
// Immediately refetch the token now that the store is empty
|
||||
const newToken = this.store.findRecord('token', tokenId);
|
||||
// Refetch the token and associated policies
|
||||
this.get('token.fetchSelfTokenAndPolicies')
|
||||
.perform()
|
||||
.catch();
|
||||
|
||||
this.setProperties({
|
||||
tokenIsValid: true,
|
||||
tokenIsInvalid: false,
|
||||
tokenRecord: newToken,
|
||||
});
|
||||
},
|
||||
() => {
|
||||
|
@ -61,7 +60,6 @@ export default Controller.extend({
|
|||
this.setProperties({
|
||||
tokenIsValid: false,
|
||||
tokenIsInvalid: true,
|
||||
tokenRecord: null,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -78,16 +78,13 @@ export default Service.extend({
|
|||
'defaultRegion.region',
|
||||
'shouldShowRegions',
|
||||
function() {
|
||||
return this.shouldShowRegions &&
|
||||
this.activeRegion !== this.get('defaultRegion.region');
|
||||
return this.shouldShowRegions && this.activeRegion !== this.get('defaultRegion.region');
|
||||
}
|
||||
),
|
||||
|
||||
namespaces: computed('activeRegion', function() {
|
||||
return PromiseArray.create({
|
||||
promise: this.store
|
||||
.findAll('namespace')
|
||||
.then(namespaces => namespaces.compact()),
|
||||
promise: this.store.findAll('namespace').then(namespaces => namespaces.compact()),
|
||||
});
|
||||
}),
|
||||
|
||||
|
|
|
@ -11,6 +11,8 @@ export default Service.extend({
|
|||
store: service(),
|
||||
system: service(),
|
||||
|
||||
aclEnabled: true,
|
||||
|
||||
secret: computed({
|
||||
get() {
|
||||
return window.localStorage.nomadTokenSecret;
|
||||
|
@ -31,11 +33,17 @@ export default Service.extend({
|
|||
try {
|
||||
return yield TokenAdapter.findSelf();
|
||||
} catch (e) {
|
||||
const errors = e.errors ? e.errors.mapBy('detail') : [];
|
||||
if (errors.find(error => error === 'ACL support disabled')) {
|
||||
this.set('aclEnabled', false);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
|
||||
selfToken: alias('fetchSelfToken.lastSuccessful.value'),
|
||||
selfToken: computed('secret', 'fetchSelfToken.lastSuccessful.value', function() {
|
||||
if (this.secret) return this.get('fetchSelfToken.lastSuccessful.value');
|
||||
}),
|
||||
|
||||
fetchSelfTokenPolicies: task(function*() {
|
||||
try {
|
||||
|
@ -54,7 +62,9 @@ export default Service.extend({
|
|||
|
||||
fetchSelfTokenAndPolicies: task(function*() {
|
||||
yield this.fetchSelfToken.perform();
|
||||
yield this.fetchSelfTokenPolicies.perform();
|
||||
if (this.aclEnabled) {
|
||||
yield this.fetchSelfTokenPolicies.perform();
|
||||
}
|
||||
}),
|
||||
|
||||
// All non Ember Data requests should go through authorizedRequest.
|
||||
|
@ -83,6 +93,12 @@ export default Service.extend({
|
|||
|
||||
return this.authorizedRawRequest(url, options);
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.fetchSelfToken.cancelAll({ resetState: true });
|
||||
this.fetchSelfTokenPolicies.cancelAll({ resetState: true });
|
||||
this.fetchSelfTokenAndPolicies.cancelAll({ resetState: true });
|
||||
},
|
||||
});
|
||||
|
||||
function addParams(url, params) {
|
||||
|
|
|
@ -43,6 +43,17 @@ $button-box-shadow-standard: 0 2px 0 0 rgba($grey, 0.2);
|
|||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.7;
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
border-color: transparent;
|
||||
|
||||
&:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@each $name, $pair in $colors {
|
||||
$color: nth($pair, 1);
|
||||
$color-invert: nth($pair, 2);
|
||||
|
@ -116,6 +127,24 @@ $button-box-shadow-standard: 0 2px 0 0 rgba($grey, 0.2);
|
|||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
// The is-disabled styles MUST trump other modifier specificites
|
||||
&.is-disabled {
|
||||
background-color: $color;
|
||||
border-color: darken($color, 5%);
|
||||
box-shadow: none;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus,
|
||||
&.is-hovered,
|
||||
&.is-active,
|
||||
&.is-focused {
|
||||
background-color: $color;
|
||||
border-color: darken($color, 5%);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -93,7 +93,7 @@
|
|||
{{#toggle
|
||||
data-test-eligibility-toggle
|
||||
isActive=model.isEligible
|
||||
isDisabled=(or setEligibility.isRunning model.isDraining)
|
||||
isDisabled=(or setEligibility.isRunning model.isDraining (cannot "write client"))
|
||||
onToggle=(perform setEligibility (not model.isEligible))}}
|
||||
Eligible
|
||||
{{/toggle}}
|
||||
|
@ -122,6 +122,7 @@
|
|||
<div class="toolbar-item is-right-aligned is-top-aligned">
|
||||
{{drain-popover
|
||||
client=model
|
||||
isDisabled=(cannot "write client")
|
||||
onDrain=(action "drainNotify")
|
||||
onError=(action "drainError")}}
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
{{#popover-menu
|
||||
data-test-drain-popover
|
||||
isDisabled=isDisabled
|
||||
label=(if client.isDraining "Update Drain" "Drain")
|
||||
triggerClass=(concat "is-small " (if drain.isRunning "is-loading")) as |m|}}
|
||||
tooltip=(if isDisabled "Not allowed to drain clients")
|
||||
triggerClass=(concat
|
||||
"is-small "
|
||||
(if drain.isRunning "is-loading ")
|
||||
(if isDisabled "tooltip is-right-aligned")
|
||||
) as |m|}}
|
||||
<form data-test-drain-popover-form onsubmit={{action (queue (action preventDefault) (perform drain m.actions.close))}} class="form is-small">
|
||||
<h4 class="group-heading">Drain Options</h4>
|
||||
<div class="field">
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
<BasicDropdown
|
||||
@horizontalPosition="right"
|
||||
@disabled={{isDisabled}}
|
||||
@onOpen={{action (queue (action (mut isOpen) true) (action capture))}}
|
||||
@onClose={{action (mut isOpen) false}} as |dd|
|
||||
>
|
||||
<dd.Trigger data-test-popover-trigger class={{concat "popover-trigger button is-primary " triggerClass}} {{on "keydown" (action "openOnArrowDown" dd)}}>
|
||||
<dd.Trigger
|
||||
data-test-popover-trigger
|
||||
class={{concat "popover-trigger button is-primary " triggerClass (if isDisabled " is-disabled")}}
|
||||
aria-label={{tooltip}}
|
||||
{{on "keydown" (action "openOnArrowDown" dd)}}
|
||||
>
|
||||
{{label}} {{x-icon "chevron-down" class="is-text"}}
|
||||
</dd.Trigger>
|
||||
<dd.Content data-test-popover-menu class="popover-content">
|
||||
|
|
|
@ -18,7 +18,12 @@
|
|||
{{#if (can "run job")}}
|
||||
{{#link-to "jobs.run" data-test-run-job class="button is-primary"}}Run Job{{/link-to}}
|
||||
{{else}}
|
||||
<button data-test-run-job class="button tooltip is-right-aligned" aria-label="You don’t have permission to run jobs" disabled>Run Job</button>
|
||||
<button
|
||||
data-test-run-job
|
||||
class="button is-primary is-disabled tooltip is-right-aligned"
|
||||
aria-label="You don’t have permission to run jobs"
|
||||
disabled
|
||||
>Run Job</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
@ -55,7 +60,12 @@
|
|||
{{#if (can "run job")}}
|
||||
{{#link-to "jobs.run" data-test-run-job class="button is-primary"}}Run Job{{/link-to}}
|
||||
{{else}}
|
||||
<button data-test-run-job class="button tooltip is-right-aligned" aria-label="You don’t have permission to run jobs" disabled>Run Job</button>
|
||||
<button
|
||||
data-test-run-job
|
||||
class="button is-primary is-disabled tooltip is-right-aligned"
|
||||
aria-label="You don’t have permission to run jobs"
|
||||
disabled
|
||||
>Run Job</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
|
|
@ -71,3 +71,60 @@ export let Sizes = () => {
|
|||
`,
|
||||
};
|
||||
};
|
||||
|
||||
export let Disabled = () => {
|
||||
return {
|
||||
template: hbs`
|
||||
<h5 class="title is-5">Anchor elements as buttons</h5>
|
||||
<div class="block">
|
||||
<a class="button is-disabled">Button</a>
|
||||
<a class="button is-white is-disabled">White</a>
|
||||
<a class="button is-light is-disabled">Light</a>
|
||||
<a class="button is-dark is-disabled">Dark</a>
|
||||
<a class="button is-black is-disabled">Black</a>
|
||||
<a class="button is-link is-disabled">Link</a>
|
||||
</div>
|
||||
<div class="block">
|
||||
<a class="button is-primary is-disabled">Primary</a>
|
||||
<a class="button is-info is-disabled">Info</a>
|
||||
<a class="button is-success is-disabled">Success</a>
|
||||
<a class="button is-warning is-disabled">Warning</a>
|
||||
<a class="button is-danger is-disabled">Danger</a>
|
||||
</div>
|
||||
|
||||
<h5 class="title is-5">Button elements with <code>disabled</code> attribute</h5>
|
||||
<div class="block">
|
||||
<button class="button is-disabled" disabled>Button</button>
|
||||
<button class="button is-white is-disabled" disabled>White</button>
|
||||
<button class="button is-light is-disabled" disabled>Light</button>
|
||||
<button class="button is-dark is-disabled" disabled>Dark</button>
|
||||
<button class="button is-black is-disabled" disabled>Black</button>
|
||||
<button class="button is-link is-disabled" disabled>Link</button>
|
||||
</div>
|
||||
<div class="block">
|
||||
<button class="button is-primary is-disabled" disabled>Primary</button>
|
||||
<button class="button is-info is-disabled" disabled>Info</button>
|
||||
<button class="button is-success is-disabled" disabled>Success</button>
|
||||
<button class="button is-warning is-disabled" disabled>Warning</button>
|
||||
<button class="button is-danger is-disabled" disabled>Danger</button>
|
||||
</div>
|
||||
|
||||
<h5 class="title is-5">Button elements with <code>aria-disabled="true"</code></h5>
|
||||
<div class="block">
|
||||
<button class="button is-disabled" aria-disabled="true">Button</button>
|
||||
<button class="button is-white is-disabled" aria-disabled="true">White</button>
|
||||
<button class="button is-light is-disabled" aria-disabled="true">Light</button>
|
||||
<button class="button is-dark is-disabled" aria-disabled="true">Dark</button>
|
||||
<button class="button is-black is-disabled" aria-disabled="true">Black</button>
|
||||
<button class="button is-link is-disabled" aria-disabled="true">Link</button>
|
||||
</div>
|
||||
<div class="block">
|
||||
<button class="button is-primary is-disabled" aria-disabled="true">Primary</button>
|
||||
<button class="button is-info is-disabled" aria-disabled="true">Info</button>
|
||||
<button class="button is-success is-disabled" aria-disabled="true">Success</button>
|
||||
<button class="button is-warning is-disabled" aria-disabled="true">Warning</button>
|
||||
<button class="button is-danger is-disabled" aria-disabled="true">Danger</button>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -10,6 +10,8 @@ import Clients from 'nomad-ui/tests/pages/clients/list';
|
|||
import Jobs from 'nomad-ui/tests/pages/jobs/list';
|
||||
|
||||
let node;
|
||||
let managementToken;
|
||||
let clientToken;
|
||||
|
||||
const wasPreemptedFilter = allocation => !!allocation.preemptedByAllocation;
|
||||
|
||||
|
@ -21,6 +23,11 @@ module('Acceptance | client detail', function(hooks) {
|
|||
server.create('node', 'forceIPv4', { schedulingEligibility: 'eligible' });
|
||||
node = server.db.nodes[0];
|
||||
|
||||
managementToken = server.create('token');
|
||||
clientToken = server.create('token');
|
||||
|
||||
window.localStorage.nomadTokenSecret = managementToken.secretId;
|
||||
|
||||
// Related models
|
||||
server.create('agent');
|
||||
server.create('job', { createAllocations: false });
|
||||
|
@ -885,6 +892,14 @@ module('Acceptance | client detail', function(hooks) {
|
|||
|
||||
assert.notOk(ClientDetail.eligibilityError.isPresent);
|
||||
});
|
||||
|
||||
test('toggling eligibility and node drain are disabled when the active ACL token does not permit node write', async function(assert) {
|
||||
window.localStorage.nomadTokenSecret = clientToken.secretId;
|
||||
|
||||
await ClientDetail.visit({ id: node.id });
|
||||
assert.ok(ClientDetail.eligibilityToggle.isDisabled);
|
||||
assert.ok(ClientDetail.drainPopover.isDisabled);
|
||||
});
|
||||
});
|
||||
|
||||
module('Acceptance | client detail (multi-namespace)', function(hooks) {
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
export default ability => hooks => {
|
||||
hooks.beforeEach(function() {
|
||||
this.ability = this.owner.lookup(`ability:${ability}`);
|
||||
});
|
||||
|
||||
hooks.afterEach(function() {
|
||||
delete this.ability;
|
||||
});
|
||||
};
|
|
@ -117,6 +117,7 @@ export default create({
|
|||
label: text('[data-test-drain-popover] [data-test-popover-trigger]'),
|
||||
isOpen: isPresent('[data-test-drain-popover-form]'),
|
||||
toggle: clickable('[data-test-drain-popover] [data-test-popover-trigger]'),
|
||||
isDisabled: attribute('aria-disabled', '[data-test-popover-trigger]'),
|
||||
|
||||
deadlineToggle: toggle('[data-test-drain-deadline-toggle]'),
|
||||
deadlineOptions: {
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
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 | client', function(hooks) {
|
||||
setupTest(hooks);
|
||||
setupAbility('client')(hooks);
|
||||
|
||||
test('it permits client write when ACLs are disabled', function(assert) {
|
||||
const mockToken = Service.extend({
|
||||
aclEnabled: false,
|
||||
});
|
||||
this.owner.register('service:token', mockToken);
|
||||
|
||||
assert.ok(this.ability.canWrite);
|
||||
});
|
||||
|
||||
test('it permits client write for management tokens', function(assert) {
|
||||
const mockToken = Service.extend({
|
||||
aclEnabled: true,
|
||||
selfToken: { type: 'management' },
|
||||
});
|
||||
this.owner.register('service:token', mockToken);
|
||||
|
||||
assert.ok(this.ability.canWrite);
|
||||
});
|
||||
|
||||
test('it permits client write for tokens with a policy that has node-write', function(assert) {
|
||||
const mockToken = Service.extend({
|
||||
aclEnabled: true,
|
||||
selfToken: { type: 'client' },
|
||||
selfTokenPolicies: [
|
||||
{
|
||||
rulesJSON: {
|
||||
Node: {
|
||||
Policy: 'write',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
this.owner.register('service:token', mockToken);
|
||||
|
||||
assert.ok(this.ability.canWrite);
|
||||
});
|
||||
|
||||
test('it permits client write for tokens with a policy that allows write and another policy that disallows it', function(assert) {
|
||||
const mockToken = Service.extend({
|
||||
aclEnabled: true,
|
||||
selfToken: { type: 'client' },
|
||||
selfTokenPolicies: [
|
||||
{
|
||||
rulesJSON: {
|
||||
Node: {
|
||||
Policy: 'write',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
rulesJSON: {
|
||||
Node: {
|
||||
Policy: 'read',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
this.owner.register('service:token', mockToken);
|
||||
|
||||
assert.ok(this.ability.canWrite);
|
||||
});
|
||||
|
||||
test('it blocks client write for tokens with a policy that does not allow node-write', function(assert) {
|
||||
const mockToken = Service.extend({
|
||||
aclEnabled: true,
|
||||
selfToken: { type: 'client' },
|
||||
selfTokenPolicies: [
|
||||
{
|
||||
rulesJSON: {
|
||||
Node: {
|
||||
Policy: 'read',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
this.owner.register('service:token', mockToken);
|
||||
|
||||
assert.notOk(this.ability.canWrite);
|
||||
});
|
||||
});
|
|
@ -1,29 +1,43 @@
|
|||
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 | job', function(hooks) {
|
||||
setupTest(hooks);
|
||||
setupAbility('job')(hooks);
|
||||
|
||||
test('it permits job run when ACLs are disabled', function(assert) {
|
||||
const mockToken = Service.extend({
|
||||
aclEnabled: false,
|
||||
});
|
||||
|
||||
this.owner.register('service:token', mockToken);
|
||||
|
||||
assert.ok(this.ability.canRun);
|
||||
});
|
||||
|
||||
test('it permits job run for management tokens', function(assert) {
|
||||
const mockToken = Service.extend({
|
||||
aclEnabled: true,
|
||||
selfToken: { type: 'management' },
|
||||
});
|
||||
|
||||
this.owner.register('service:token', mockToken);
|
||||
|
||||
const jobAbility = this.owner.lookup('ability:job');
|
||||
assert.ok(jobAbility.canRun);
|
||||
assert.ok(this.ability.canRun);
|
||||
});
|
||||
|
||||
test('it permits job run for client tokens with a policy that has namespace submit-job', function(assert) {
|
||||
const mockSystem = Service.extend({
|
||||
aclEnabled: true,
|
||||
activeNamespace: {
|
||||
name: 'aNamespace',
|
||||
},
|
||||
});
|
||||
|
||||
const mockToken = Service.extend({
|
||||
aclEnabled: true,
|
||||
selfToken: { type: 'client' },
|
||||
selfTokenPolicies: [
|
||||
{
|
||||
|
@ -42,18 +56,19 @@ module('Unit | Ability | job', function(hooks) {
|
|||
this.owner.register('service:system', mockSystem);
|
||||
this.owner.register('service:token', mockToken);
|
||||
|
||||
const jobAbility = this.owner.lookup('ability:job');
|
||||
assert.ok(jobAbility.canRun);
|
||||
assert.ok(this.ability.canRun);
|
||||
});
|
||||
|
||||
test('it permits job run for client tokens with a policy that has default namespace submit-job and no capabilities for active namespace', function(assert) {
|
||||
const mockSystem = Service.extend({
|
||||
aclEnabled: true,
|
||||
activeNamespace: {
|
||||
name: 'anotherNamespace',
|
||||
},
|
||||
});
|
||||
|
||||
const mockToken = Service.extend({
|
||||
aclEnabled: true,
|
||||
selfToken: { type: 'client' },
|
||||
selfTokenPolicies: [
|
||||
{
|
||||
|
@ -76,18 +91,19 @@ module('Unit | Ability | job', function(hooks) {
|
|||
this.owner.register('service:system', mockSystem);
|
||||
this.owner.register('service:token', mockToken);
|
||||
|
||||
const jobAbility = this.owner.lookup('ability:job');
|
||||
assert.ok(jobAbility.canRun);
|
||||
assert.ok(this.ability.canRun);
|
||||
});
|
||||
|
||||
test('it blocks job run for client tokens with a policy that has no submit-job capability', function(assert) {
|
||||
const mockSystem = Service.extend({
|
||||
aclEnabled: true,
|
||||
activeNamespace: {
|
||||
name: 'aNamespace',
|
||||
},
|
||||
});
|
||||
|
||||
const mockToken = Service.extend({
|
||||
aclEnabled: true,
|
||||
selfToken: { type: 'client' },
|
||||
selfTokenPolicies: [
|
||||
{
|
||||
|
@ -106,18 +122,19 @@ module('Unit | Ability | job', function(hooks) {
|
|||
this.owner.register('service:system', mockSystem);
|
||||
this.owner.register('service:token', mockToken);
|
||||
|
||||
const jobAbility = this.owner.lookup('ability:job');
|
||||
assert.notOk(jobAbility.canRun);
|
||||
assert.notOk(this.ability.canRun);
|
||||
});
|
||||
|
||||
test('it handles globs in namespace names', function(assert) {
|
||||
const mockSystem = Service.extend({
|
||||
aclEnabled: true,
|
||||
activeNamespace: {
|
||||
name: 'aNamespace',
|
||||
},
|
||||
});
|
||||
|
||||
const mockToken = Service.extend({
|
||||
aclEnabled: true,
|
||||
selfToken: { type: 'client' },
|
||||
selfTokenPolicies: [
|
||||
{
|
||||
|
@ -156,28 +173,27 @@ module('Unit | Ability | job', function(hooks) {
|
|||
this.owner.register('service:system', mockSystem);
|
||||
this.owner.register('service:token', mockToken);
|
||||
|
||||
const jobAbility = this.owner.lookup('ability:job');
|
||||
const systemService = this.owner.lookup('service:system');
|
||||
|
||||
systemService.set('activeNamespace.name', 'production-web');
|
||||
assert.notOk(jobAbility.canRun);
|
||||
assert.notOk(this.ability.canRun);
|
||||
|
||||
systemService.set('activeNamespace.name', 'production-api');
|
||||
assert.ok(jobAbility.canRun);
|
||||
assert.ok(this.ability.canRun);
|
||||
|
||||
systemService.set('activeNamespace.name', 'production-other');
|
||||
assert.ok(jobAbility.canRun);
|
||||
assert.ok(this.ability.canRun);
|
||||
|
||||
systemService.set('activeNamespace.name', 'something-suffixed');
|
||||
assert.ok(jobAbility.canRun);
|
||||
assert.ok(this.ability.canRun);
|
||||
|
||||
systemService.set('activeNamespace.name', 'something-more-suffixed');
|
||||
assert.notOk(
|
||||
jobAbility.canRun,
|
||||
this.ability.canRun,
|
||||
'expected the namespace with the greatest number of matched characters to be chosen'
|
||||
);
|
||||
|
||||
systemService.set('activeNamespace.name', '000-abc-999');
|
||||
assert.ok(jobAbility.canRun, 'expected to be able to match against more than one wildcard');
|
||||
assert.ok(this.ability.canRun, 'expected to be able to match against more than one wildcard');
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue