[ui] Policies UI (#13976)
Co-authored-by: Mike Nomitch <mail@mikenomitch.com>
This commit is contained in:
parent
d3aac1fe08
commit
ce0ffdd077
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
ui: Added a Policy Editor interface for management tokens
|
||||
```
|
|
@ -0,0 +1,12 @@
|
|||
import AbstractAbility from './abstract';
|
||||
import { alias } from '@ember/object/computed';
|
||||
import classic from 'ember-classic-decorator';
|
||||
|
||||
@classic
|
||||
export default class Policy extends AbstractAbility {
|
||||
@alias('selfTokenIsManagement') canRead;
|
||||
@alias('selfTokenIsManagement') canList;
|
||||
@alias('selfTokenIsManagement') canWrite;
|
||||
@alias('selfTokenIsManagement') canUpdate;
|
||||
@alias('selfTokenIsManagement') canDestroy;
|
||||
}
|
|
@ -4,4 +4,12 @@ import classic from 'ember-classic-decorator';
|
|||
@classic
|
||||
export default class PolicyAdapter extends ApplicationAdapter {
|
||||
namespace = namespace + '/acl';
|
||||
|
||||
urlForCreateRecord(_modelName, model) {
|
||||
return this.urlForUpdateRecord(model.attr('name'), 'policy');
|
||||
}
|
||||
|
||||
urlForDeleteRecord(id) {
|
||||
return this.urlForUpdateRecord(id, 'policy');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
<form class="edit-policy" autocomplete="off" {{on "submit" this.save}}>
|
||||
{{#if @policy.isNew }}
|
||||
<label>
|
||||
<span>
|
||||
Policy Name
|
||||
</span>
|
||||
<Input
|
||||
data-test-policy-name-input
|
||||
@type="text"
|
||||
@value={{@policy.name}}
|
||||
class="input"
|
||||
{{autofocus}}
|
||||
/>
|
||||
</label>
|
||||
{{/if}}
|
||||
|
||||
<div class="boxed-section">
|
||||
<div class="boxed-section-head">
|
||||
Policy Definition
|
||||
</div>
|
||||
<div class="boxed-section-body is-full-bleed">
|
||||
|
||||
<div
|
||||
class="policy-editor"
|
||||
data-test-policy-editor
|
||||
{{code-mirror
|
||||
screenReaderLabel="Policy definition"
|
||||
theme="hashi"
|
||||
mode="ruby"
|
||||
content=@policy.rules
|
||||
onUpdate=this.updatePolicyRules
|
||||
autofocus=(not @policy.isNew)
|
||||
extraKeys=(hash Cmd-Enter=this.save)
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
<span>
|
||||
Description (optional)
|
||||
</span>
|
||||
<Input
|
||||
data-test-policy-description
|
||||
@value={{@policy.description}}
|
||||
class="input"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
{{#if (can "update policy")}}
|
||||
<button
|
||||
class="button is-primary"
|
||||
type="submit"
|
||||
>
|
||||
Save Policy
|
||||
</button>
|
||||
{{/if}}
|
||||
</footer>
|
||||
</form>
|
|
@ -0,0 +1,62 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { alias } from '@ember/object/computed';
|
||||
import messageForError from 'nomad-ui/utils/message-from-adapter-error';
|
||||
|
||||
export default class PolicyEditorComponent extends Component {
|
||||
@service flashMessages;
|
||||
@service router;
|
||||
@service store;
|
||||
|
||||
@alias('args.policy') policy;
|
||||
|
||||
@action updatePolicyRules(value) {
|
||||
this.policy.set('rules', value);
|
||||
}
|
||||
|
||||
@action async save(e) {
|
||||
if (e instanceof Event) {
|
||||
e.preventDefault(); // code-mirror "command+enter" submits the form, but doesnt have a preventDefault()
|
||||
}
|
||||
try {
|
||||
const nameRegex = '^[a-zA-Z0-9-]{1,128}$';
|
||||
if (!this.policy.name?.match(nameRegex)) {
|
||||
throw new Error(
|
||||
'Policy name must be 1-128 characters long and can only contain letters, numbers, and dashes.'
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
this.policy.isNew &&
|
||||
this.store.peekRecord('policy', this.policy.name)
|
||||
) {
|
||||
throw new Error(
|
||||
`A policy with name ${this.policy.name} already exists.`
|
||||
);
|
||||
}
|
||||
|
||||
this.policy.id = this.policy.name;
|
||||
|
||||
await this.policy.save();
|
||||
|
||||
this.flashMessages.add({
|
||||
title: 'Policy Saved',
|
||||
type: 'success',
|
||||
destroyOnClick: false,
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
this.router.transitionTo('policies');
|
||||
} catch (error) {
|
||||
console.log('error and its', error);
|
||||
this.flashMessages.add({
|
||||
title: `Error creating Policy ${this.policy.name}`,
|
||||
message: messageForError(error),
|
||||
type: 'error',
|
||||
destroyOnClick: false,
|
||||
sticky: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class PoliciesIndexController extends Controller {
|
||||
@service router;
|
||||
get policies() {
|
||||
return this.model.policies.map((policy) => {
|
||||
policy.tokens = this.model.tokens.filter((token) => {
|
||||
return token.policies.includes(policy);
|
||||
});
|
||||
return policy;
|
||||
});
|
||||
}
|
||||
|
||||
@action openPolicy(policy) {
|
||||
this.router.transitionTo('policies.policy', policy.name);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
// @ts-check
|
||||
import Controller from '@ember/controller';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
export default class PoliciesPolicyController extends Controller {
|
||||
@service flashMessages;
|
||||
@service router;
|
||||
|
||||
@tracked isDeleting = false;
|
||||
|
||||
@action
|
||||
onDeletePrompt() {
|
||||
this.isDeleting = true;
|
||||
}
|
||||
|
||||
@action
|
||||
onDeleteCancel() {
|
||||
this.isDeleting = false;
|
||||
}
|
||||
|
||||
@task(function* () {
|
||||
try {
|
||||
yield this.model.deleteRecord();
|
||||
yield this.model.save();
|
||||
this.flashMessages.add({
|
||||
title: 'Policy Deleted',
|
||||
type: 'success',
|
||||
destroyOnClick: false,
|
||||
timeout: 5000,
|
||||
});
|
||||
this.router.transitionTo('policies');
|
||||
} catch (err) {
|
||||
this.flashMessages.add({
|
||||
title: `Error deleting Policy ${this.model.name}`,
|
||||
message: err,
|
||||
type: 'error',
|
||||
destroyOnClick: false,
|
||||
sticky: true,
|
||||
});
|
||||
}
|
||||
})
|
||||
deletePolicy;
|
||||
}
|
|
@ -8,8 +8,18 @@ import 'codemirror/addon/selection/active-line';
|
|||
import 'codemirror/addon/lint/lint.js';
|
||||
import 'codemirror/addon/lint/json-lint.js';
|
||||
import 'codemirror/mode/javascript/javascript';
|
||||
import 'codemirror/mode/ruby/ruby';
|
||||
|
||||
export default class CodeMirrorModifier extends Modifier {
|
||||
get autofocus() {
|
||||
if (Object.hasOwn({ ...this.args.named }, 'autofocus')) {
|
||||
// spread (...) because proxy, and because Ember over-eagerly prevents named prop lookups for modifier args.
|
||||
return this.args.named.autofocus;
|
||||
} else {
|
||||
return !this.args.named.readOnly;
|
||||
}
|
||||
}
|
||||
|
||||
didInstall() {
|
||||
this._setup();
|
||||
}
|
||||
|
@ -49,6 +59,10 @@ export default class CodeMirrorModifier extends Modifier {
|
|||
screenReaderLabel: this.args.named.screenReaderLabel || '',
|
||||
});
|
||||
|
||||
if (this.autofocus) {
|
||||
editor.focus();
|
||||
}
|
||||
|
||||
editor.on('change', bind(this, this._onChange));
|
||||
|
||||
this._editor = editor;
|
||||
|
|
|
@ -98,6 +98,14 @@ Router.map(function () {
|
|||
path: '/path/*absolutePath',
|
||||
});
|
||||
});
|
||||
|
||||
this.route('policies', function () {
|
||||
this.route('new');
|
||||
|
||||
this.route('policy', {
|
||||
path: '/:name',
|
||||
});
|
||||
});
|
||||
// Mirage-only route for testing OIDC flow
|
||||
if (config['ember-cli-mirage']) {
|
||||
this.route('oidc-mock');
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
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 { hash } from 'rsvp';
|
||||
|
||||
export default class PoliciesRoute extends Route.extend(
|
||||
withForbiddenState,
|
||||
WithModelErrorHandling
|
||||
) {
|
||||
@service can;
|
||||
@service store;
|
||||
@service router;
|
||||
|
||||
beforeModel() {
|
||||
if (this.can.cannot('list policies')) {
|
||||
this.router.transitionTo('/jobs');
|
||||
}
|
||||
}
|
||||
|
||||
async model() {
|
||||
return await hash({
|
||||
policies: this.store.query('policy', { reload: true }),
|
||||
tokens: this.store.query('token', { reload: true }),
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
const INITIAL_POLICY_RULES = `# See https://developer.hashicorp.com/nomad/tutorials/access-control/access-control-policies for ACL Policy details
|
||||
|
||||
# Example policy structure:
|
||||
|
||||
namespace "default" {
|
||||
policy = "deny"
|
||||
capabilities = []
|
||||
}
|
||||
|
||||
namespace "example-ns" {
|
||||
policy = "deny"
|
||||
capabilities = ["list-jobs", "read-job"]
|
||||
variables {
|
||||
# list access to variables in all paths, full access in nested/variables/*
|
||||
path "*" {
|
||||
capabilities = ["list"]
|
||||
}
|
||||
path "nested/variables/*" {
|
||||
capabilities = ["write", "read", "destroy", "list"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
host_volume "example-volume" {
|
||||
policy = "deny"
|
||||
}
|
||||
|
||||
agent {
|
||||
policy = "deny"
|
||||
}
|
||||
|
||||
node {
|
||||
policy = "deny"
|
||||
}
|
||||
|
||||
quota {
|
||||
policy = "deny"
|
||||
}
|
||||
|
||||
operator {
|
||||
policy = "deny"
|
||||
}
|
||||
|
||||
# Possible Namespace Policies:
|
||||
# * deny
|
||||
# * read
|
||||
# * write
|
||||
# * scale
|
||||
|
||||
# Possible Namespace Capabilities:
|
||||
# * list-jobs
|
||||
# * parse-job
|
||||
# * read-job
|
||||
# * submit-job
|
||||
# * dispatch-job
|
||||
# * read-logs
|
||||
# * read-fs
|
||||
# * alloc-exec
|
||||
# * alloc-lifecycle
|
||||
# * csi-write-volume
|
||||
# * csi-mount-volume
|
||||
# * list-scaling-policies
|
||||
# * read-scaling-policy
|
||||
# * read-job-scaling
|
||||
# * scale-job
|
||||
|
||||
# Possible Variables capabilities
|
||||
# * write
|
||||
# * read
|
||||
# * destroy
|
||||
# * list
|
||||
|
||||
# Possible Policies for "agent", "node", "quota", "operator", and "host_volume":
|
||||
# * deny
|
||||
# * read
|
||||
# * write
|
||||
`;
|
||||
|
||||
export default class PoliciesNewRoute extends Route {
|
||||
@service can;
|
||||
@service router;
|
||||
|
||||
beforeModel() {
|
||||
if (this.can.cannot('write policy')) {
|
||||
this.router.transitionTo('/policies');
|
||||
}
|
||||
}
|
||||
|
||||
model() {
|
||||
return this.store.createRecord('policy', {
|
||||
name: '',
|
||||
rules: INITIAL_POLICY_RULES,
|
||||
});
|
||||
}
|
||||
|
||||
resetController(controller, isExiting) {
|
||||
// If the user navigates away from /new, clear the path
|
||||
controller.set('path', null);
|
||||
if (isExiting) {
|
||||
// If user didn't save, delete the freshly created model
|
||||
if (controller.model.isNew) {
|
||||
controller.model.destroyRecord();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
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';
|
||||
|
||||
export default class PoliciesPolicyRoute extends Route.extend(
|
||||
withForbiddenState,
|
||||
WithModelErrorHandling
|
||||
) {
|
||||
@service store;
|
||||
model(params) {
|
||||
return this.store.findRecord('policy', decodeURIComponent(params.name), {
|
||||
reload: true,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -2,9 +2,17 @@ import ApplicationSerializer from './application';
|
|||
import classic from 'ember-classic-decorator';
|
||||
|
||||
@classic
|
||||
export default class Policy extends ApplicationSerializer {
|
||||
export default class PolicySerializer extends ApplicationSerializer {
|
||||
primaryKey = 'Name';
|
||||
|
||||
normalize(typeHash, hash) {
|
||||
hash.ID = hash.Name;
|
||||
return super.normalize(typeHash, hash);
|
||||
}
|
||||
|
||||
serialize(snapshot, options) {
|
||||
const hash = super.serialize(snapshot, options);
|
||||
hash.ID = hash.Name;
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,3 +51,4 @@
|
|||
@import './components/services';
|
||||
@import './components/task-sub-row';
|
||||
@import './components/authorization';
|
||||
@import './components/policies';
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
table.policies {
|
||||
tr {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover td {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
a {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
}
|
||||
.number-expired {
|
||||
color: $red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-policy {
|
||||
.policy-editor {
|
||||
max-height: 600px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.input {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ section.notifications {
|
|||
position: fixed;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
z-index: 100;
|
||||
|
||||
.flash-message {
|
||||
width: 300px;
|
||||
|
|
|
@ -7,9 +7,11 @@
|
|||
border-collapse: separate;
|
||||
width: 100%;
|
||||
|
||||
@media #{$mq-table-overflow} {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
&:not(.no-mobile-condense) {
|
||||
@media #{$mq-table-overflow} {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-fixed {
|
||||
|
|
|
@ -114,7 +114,7 @@
|
|||
</li>
|
||||
</ul>
|
||||
<p class="menu-label">
|
||||
Debugging
|
||||
Operations
|
||||
</p>
|
||||
<ul class="menu-list">
|
||||
<li {{keyboard-shortcut menuLevel=true pattern=(array "g" "e") }}>
|
||||
|
@ -126,6 +126,23 @@
|
|||
Evaluations
|
||||
</LinkTo>
|
||||
</li>
|
||||
{{#if (can "list policies")}}
|
||||
<li
|
||||
{{keyboard-shortcut
|
||||
menuLevel=true
|
||||
pattern=(array "g" "l")
|
||||
action=(action this.transitionTo 'policies')
|
||||
}}
|
||||
>
|
||||
<LinkTo
|
||||
@route="policies"
|
||||
@activeClass="is-active"
|
||||
data-test-gutter-link="policies"
|
||||
>
|
||||
Policies
|
||||
</LinkTo>
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</aside>
|
||||
{{#if this.system.agent.version}}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
<Breadcrumb @crumb={{hash label="Policies" args=(array "policies.index")}} />
|
||||
<PageLayout>
|
||||
{{outlet}}
|
||||
</PageLayout>
|
|
@ -0,0 +1,67 @@
|
|||
{{page-title "Policies"}}
|
||||
<section class="section">
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-item is-right-aligned is-mobile-full-width">
|
||||
<div class="button-bar">
|
||||
{{#if (can "write policy")}}
|
||||
<LinkTo
|
||||
@route="policies.new"
|
||||
class="button is-primary"
|
||||
data-test-create-policy
|
||||
>
|
||||
Create Policy
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
<button
|
||||
class="button is-primary is-disabled tooltip is-right-aligned"
|
||||
aria-label="You don’t have sufficient permissions"
|
||||
disabled
|
||||
type="button"
|
||||
data-test-disabled-create-policy
|
||||
>
|
||||
Create Policy
|
||||
</button>
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{#if this.policies.length}}
|
||||
<ListTable
|
||||
@source={{this.policies}}
|
||||
@class="policies no-mobile-condense" as |t|>
|
||||
<t.head>
|
||||
<th>Policy Name</th>
|
||||
<th>Tokens</th>
|
||||
</t.head>
|
||||
<t.body as |row|>
|
||||
<tr data-test-policy-row {{on "click" (action "openPolicy" row.model)}}
|
||||
{{keyboard-shortcut
|
||||
enumerated=true
|
||||
action=(action "openPolicy" row.model)
|
||||
}}>
|
||||
<td data-test-policy-name>
|
||||
<LinkTo @route="policies.policy" @model={{row.model.name}}>{{row.model.name}}</LinkTo>
|
||||
</td>
|
||||
<td data-test-policy-token-count>
|
||||
<span>
|
||||
{{row.model.tokens.length}}
|
||||
{{#if (filter-by "isExpired" row.model.tokens)}}
|
||||
<span class="number-expired">({{get (filter-by "isExpired" row.model.tokens) "length"}} expired)</span>
|
||||
{{/if}}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</t.body>
|
||||
</ListTable>
|
||||
{{else}}
|
||||
<div class="empty-message">
|
||||
<h3 data-test-empty-policies-list-headline class="empty-message-headline">
|
||||
No Policies
|
||||
</h3>
|
||||
<p class="empty-message-body">
|
||||
Get started by <LinkTo @route="policies.new">creating a new policy</LinkTo>
|
||||
</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
</section>
|
|
@ -0,0 +1,10 @@
|
|||
<Breadcrumb @crumb={{hash label="New" args=(array "policies.new")}} />
|
||||
{{page-title "Create Policy"}}
|
||||
<section class="section">
|
||||
<h1 class="title with-flex" data-test-title>
|
||||
Create Policy
|
||||
</h1>
|
||||
<PolicyEditor
|
||||
@policy={{this.model}}
|
||||
/>
|
||||
</section>
|
|
@ -0,0 +1,29 @@
|
|||
<Breadcrumb @crumb={{hash label=this.model.name args=(array "policies.policy" this.model.name)}} />
|
||||
|
||||
{{page-title "Policy"}}
|
||||
<section class="section">
|
||||
<h1 class="title with-flex" data-test-title>
|
||||
<div>
|
||||
{{this.model.name}}
|
||||
</div>
|
||||
{{#if (can "destroy policy")}}
|
||||
<div>
|
||||
<TwoStepButton
|
||||
data-test-delete-button
|
||||
@idleText="Delete"
|
||||
@cancelText="Cancel"
|
||||
@confirmText="Yes, delete"
|
||||
@confirmationMessage="Are you sure?"
|
||||
@awaitingConfirmation={{this.deletePolicy.isRunning}}
|
||||
@onConfirm={{perform this.deletePolicy}}
|
||||
@onPrompt={{this.onDeletePrompt}}
|
||||
@onCancel={{this.onDeleteCancel}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
</h1>
|
||||
<PolicyEditor
|
||||
@policy={{this.model}}
|
||||
/>
|
||||
</section>
|
||||
{{outlet}}
|
|
@ -17,7 +17,7 @@ module.exports = function (defaults) {
|
|||
},
|
||||
},
|
||||
codemirror: {
|
||||
modes: ['javascript'],
|
||||
modes: ['javascript', 'ruby'],
|
||||
},
|
||||
babel: {
|
||||
include: ['proposal-optional-chaining'],
|
||||
|
|
|
@ -443,8 +443,7 @@ export default function () {
|
|||
return JSON.stringify(findLeader(schema));
|
||||
});
|
||||
|
||||
// Note: Mirage-only route, for UI testing and not part of the Nomad API
|
||||
this.get('/acl/tokens', function ({ tokens }, req) {
|
||||
this.get('/acl/tokens', function ({tokens}, req) {
|
||||
return this.serialize(tokens.all());
|
||||
});
|
||||
|
||||
|
@ -499,7 +498,7 @@ export default function () {
|
|||
);
|
||||
|
||||
this.get('/acl/policy/:id', function ({ policies, tokens }, req) {
|
||||
const policy = policies.find(req.params.id);
|
||||
const policy = policies.findBy({ name: req.params.id });
|
||||
const secret = req.requestHeaders['X-Nomad-Token'];
|
||||
const tokenForSecret = tokens.findBy({ secretId: secret });
|
||||
|
||||
|
@ -526,6 +525,33 @@ export default function () {
|
|||
return new Response(403, {}, null);
|
||||
});
|
||||
|
||||
this.get('/acl/policies', function ({ policies }, req) {
|
||||
return this.serialize(policies.all());
|
||||
});
|
||||
|
||||
this.delete('/acl/policy/:id', function (schema, request) {
|
||||
const { id } = request.params;
|
||||
schema.tokens.all().models.filter(token => token.policyIds.includes(id)).forEach(token => {
|
||||
token.update({ policyIds: token.policyIds.filter(pid => pid !== id) });
|
||||
});
|
||||
server.db.policies.remove(id);
|
||||
return '';
|
||||
});
|
||||
|
||||
this.put('/acl/policy/:id', function (schema, request) {
|
||||
return new Response(200, {}, {});
|
||||
});
|
||||
|
||||
this.post('/acl/policy/:id', function (schema, request) {
|
||||
const { Name, Description, Rules } = JSON.parse(request.requestBody);
|
||||
return server.create('policy', {
|
||||
name: Name,
|
||||
description: Description,
|
||||
rules: Rules,
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
this.get('/regions', function ({ regions }) {
|
||||
return this.serialize(regions.all());
|
||||
});
|
||||
|
|
|
@ -7,8 +7,7 @@ export default Factory.extend({
|
|||
return this.id;
|
||||
},
|
||||
description: () => (faker.random.number(10) >= 2 ? faker.lorem.sentence() : null),
|
||||
rules: `
|
||||
# Allow read only access to the default namespace
|
||||
rules: `# Allow read only access to the default namespace
|
||||
namespace "default" {
|
||||
policy = "read"
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ export const allScenarios = {
|
|||
emptyCluster,
|
||||
variableTestCluster,
|
||||
servicesTestCluster,
|
||||
policiesTestCluster,
|
||||
...topoScenarios,
|
||||
...sysbatchScenarios,
|
||||
};
|
||||
|
@ -259,6 +260,13 @@ function variableTestCluster(server) {
|
|||
});
|
||||
}
|
||||
|
||||
function policiesTestCluster(server) {
|
||||
faker.seed(1);
|
||||
createTokens(server);
|
||||
server.createList('agent', 3, 'withConsulLink', 'withVaultLink');
|
||||
}
|
||||
|
||||
|
||||
function servicesTestCluster(server) {
|
||||
faker.seed(1);
|
||||
server.create('feature', { name: 'Dynamic Application Sizing' });
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { visit, currentURL, click, typeIn, findAll } from '@ember/test-helpers';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { allScenarios } from '../../mirage/scenarios/default';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import percySnapshot from '@percy/ember';
|
||||
import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
|
||||
|
||||
module('Acceptance | policies', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
test('Policies index route looks good', async function (assert) {
|
||||
assert.expect(4);
|
||||
allScenarios.policiesTestCluster(server);
|
||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
||||
await visit('/policies');
|
||||
assert.dom('[data-test-gutter-link="policies"]').exists();
|
||||
assert.equal(currentURL(), '/policies');
|
||||
assert
|
||||
.dom('[data-test-policy-row]')
|
||||
.exists({ count: server.db.policies.length });
|
||||
await a11yAudit(assert);
|
||||
await percySnapshot(assert);
|
||||
// Reset Token
|
||||
window.localStorage.nomadTokenSecret = null;
|
||||
});
|
||||
|
||||
test('Prevents policies access if you lack a management token', async function (assert) {
|
||||
allScenarios.policiesTestCluster(server);
|
||||
window.localStorage.nomadTokenSecret = server.db.tokens[1].secretId;
|
||||
await visit('/policies');
|
||||
assert.equal(currentURL(), '/jobs');
|
||||
assert.dom('[data-test-gutter-link="policies"]').doesNotExist();
|
||||
// Reset Token
|
||||
window.localStorage.nomadTokenSecret = null;
|
||||
});
|
||||
|
||||
test('Modifying an existing policy', async function (assert) {
|
||||
allScenarios.policiesTestCluster(server);
|
||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
||||
await visit('/policies');
|
||||
await click('[data-test-policy-row]:first-child');
|
||||
assert.equal(currentURL(), `/policies/${server.db.policies[0].name}`);
|
||||
assert.dom('[data-test-policy-editor]').exists();
|
||||
assert.dom('[data-test-title]').includesText(server.db.policies[0].name);
|
||||
await click('button[type="submit"]');
|
||||
assert.dom('.flash-message.alert-success').exists();
|
||||
assert.equal(currentURL(), '/policies');
|
||||
// Reset Token
|
||||
window.localStorage.nomadTokenSecret = null;
|
||||
});
|
||||
|
||||
test('Creating a new policy', async function (assert) {
|
||||
assert.expect(7);
|
||||
allScenarios.policiesTestCluster(server);
|
||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
||||
await visit('/policies');
|
||||
await click('[data-test-create-policy]');
|
||||
assert.equal(currentURL(), '/policies/new');
|
||||
await typeIn('[data-test-policy-name-input]', 'My Fun Policy');
|
||||
await click('button[type="submit"]');
|
||||
assert
|
||||
.dom('.flash-message.alert-error')
|
||||
.exists('Doesnt let you save a bad name');
|
||||
assert.equal(currentURL(), '/policies/new');
|
||||
document.querySelector('[data-test-policy-name-input]').value = ''; // clear
|
||||
await typeIn('[data-test-policy-name-input]', 'My-Fun-Policy');
|
||||
await click('button[type="submit"]');
|
||||
assert.dom('.flash-message.alert-success').exists();
|
||||
assert.equal(currentURL(), '/policies');
|
||||
const newPolicy = [...findAll('[data-test-policy-name]')].filter((a) =>
|
||||
a.textContent.includes('My-Fun-Policy')
|
||||
)[0];
|
||||
assert.ok(newPolicy, 'Policy is in the list');
|
||||
await click(newPolicy);
|
||||
assert.equal(currentURL(), '/policies/My-Fun-Policy');
|
||||
await percySnapshot(assert);
|
||||
// Reset Token
|
||||
window.localStorage.nomadTokenSecret = null;
|
||||
});
|
||||
|
||||
test('Deleting a policy', async function (assert) {
|
||||
allScenarios.policiesTestCluster(server);
|
||||
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
|
||||
await visit('/policies');
|
||||
const firstPolicyName = server.db.policies[0].name;
|
||||
const firstPolicyRow = [...findAll('[data-test-policy-name]')].filter(
|
||||
(row) => row.textContent.includes(firstPolicyName)
|
||||
)[0];
|
||||
await click(firstPolicyRow);
|
||||
assert.equal(currentURL(), `/policies/${firstPolicyName}`);
|
||||
await click('[data-test-delete-button] button');
|
||||
assert.dom('[data-test-confirm-button]').exists();
|
||||
await click('[data-test-confirm-button]');
|
||||
assert.dom('.flash-message.alert-success').exists();
|
||||
assert.equal(currentURL(), '/policies');
|
||||
assert.dom(`[data-test-policy-name="${firstPolicyName}"]`).doesNotExist();
|
||||
// Reset Token
|
||||
window.localStorage.nomadTokenSecret = null;
|
||||
});
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
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';
|
||||
|
||||
module('Integration | Component | policy-editor', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
assert.expect(1);
|
||||
await render(hbs`<PolicyEditor />`);
|
||||
await componentA11yAudit(this.element, assert);
|
||||
});
|
||||
|
||||
test('Only has editable name if new', async function (assert) {
|
||||
const newMockPolicy = {
|
||||
isNew: true,
|
||||
name: 'New Policy',
|
||||
};
|
||||
|
||||
const oldMockPolicy = {
|
||||
isNew: false,
|
||||
name: 'Old Policy',
|
||||
};
|
||||
|
||||
this.set('newMockPolicy', newMockPolicy);
|
||||
this.set('oldMockPolicy', oldMockPolicy);
|
||||
|
||||
await render(hbs`<PolicyEditor @policy={{this.newMockPolicy}} />`);
|
||||
assert.dom('[data-test-policy-name-input]').exists();
|
||||
await render(hbs`<PolicyEditor @policy={{this.oldMockPolicy}} />`);
|
||||
assert.dom('[data-test-policy-name-input]').doesNotExist();
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue