ui: Remove legacy ACLs (#11096)

This commit is contained in:
John Cowen 2021-09-22 18:32:51 +01:00 committed by GitHub
parent 5da06645b0
commit 51149cdae2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 282 additions and 1647 deletions

View File

@ -1,74 +0,0 @@
import Adapter, { DATACENTER_QUERY_PARAM as API_DATACENTER_KEY } from './application';
import { SLUG_KEY } from 'consul-ui/models/acl';
import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc';
// The old ACL system doesn't support the `ns=` query param
// TODO: Update to use this.formatDatacenter()
export default class AclAdapter extends Adapter {
requestForQuery(request, { dc, index }) {
// https://www.consul.io/api/acl.html#list-acls
return request`
GET /v1/acl/list?${{ dc }}
${{ index }}
`;
}
requestForQueryRecord(request, { dc, index, id }) {
if (typeof id === 'undefined') {
throw new Error('You must specify an id');
}
// https://www.consul.io/api/acl.html#read-acl-token
return request`
GET /v1/acl/info/${id}?${{ dc }}
${{ index }}
`;
}
requestForCreateRecord(request, serialized, data) {
// https://www.consul.io/api/acl.html#create-acl-token
return request`
PUT /v1/acl/create?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }}
${serialized}
`;
}
requestForUpdateRecord(request, serialized, data) {
// the id is in the data, don't add it into the URL
// https://www.consul.io/api/acl.html#update-acl-token
return request`
PUT /v1/acl/update?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }}
${serialized}
`;
}
requestForDeleteRecord(request, serialized, data) {
// https://www.consul.io/api/acl.html#delete-acl-token
return request`
PUT /v1/acl/destroy/${data[SLUG_KEY]}?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }}
`;
}
requestForCloneRecord(request, serialized, data) {
// https://www.consul.io/api/acl.html#clone-acl-token
return request`
PUT /v1/acl/clone/${data[SLUG_KEY]}?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }}
`;
}
clone(store, type, id, snapshot) {
return this.rpc(
function(adapter, request, serialized, unserialized) {
return adapter.requestForCloneRecord(request, serialized, unserialized);
},
function(serializer, respond, serialized, unserialized) {
return serializer.respondForCreateRecord(respond, serialized, unserialized);
},
snapshot,
type.modelName
);
}
}

View File

@ -1,130 +0,0 @@
<div
class="consul-acl-list"
...attributes
>
<TabularCollection
@items={{@items}}
as |item index|
>
<BlockSlot @name="header">
<th>Name</th>
<th>Type</th>
</BlockSlot>
<BlockSlot @name="row">
<td data-test-acl={{item.Name}}>
<a href={{href-to 'dc.acls.edit' item.ID}}>{{item.Name}}</a>
</td>
<td>
{{#if (eq item.Type 'management')}}
<strong>{{item.Type}}</strong>
{{else}}
<span>{{item.Type}}</span>
{{/if}}
</td>
</BlockSlot>
<BlockSlot @name="actions" as |index change checked|>
<PopoverMenu @expanded={{if (eq checked index) true false}} @onchange={{action change index}} @keyboardAccess={{false}} @submenus={{array "logout" "use" "delete"}}>
<BlockSlot @name="trigger">
More
</BlockSlot>
<BlockSlot @name="menu" as |confirm send keypressClick|>
<li role="none">
<a data-test-edit role="menuitem" tabindex="-1" href={{href-to 'dc.acls.edit' item.ID}}>
{{#if (can "write acl" item=item)}}
Edit
{{else}}
View
{{/if}}
</a>
</li>
{{#if (eq item.ID token.SecretID) }}
<li role="none">
<label for={{concat confirm 'logout'}} role="menuitem" tabindex="-1" onkeypress={{keypressClick}} data-test-logout>Stop using</label>
<div role="menu">
<div class="confirmation-alert warning">
<div>
<header>
Confirm logout
</header>
<p>
Are you sure you want to stop using this ACL token? This will log you out.
</p>
</div>
<ul>
<li class="dangerous">
<button tabindex="-1" type="button" onclick={{action send 'logout' item}}>Logout</button>
</li>
<li>
<label for={{concat confirm 'logout'}}>Cancel</label>
</li>
</ul>
</div>
</div>
</li>
{{else}}
<li role="none">
<label for={{concat confirm 'use'}} role="menuitem" tabindex="-1" onkeypress={{keypressClick}} data-test-use>Use</label>
<div role="menu">
<div class="confirmation-alert warning">
<div>
<header>
Confirm use
</header>
<p>
Are you sure you want to use this ACL token?
</p>
</div>
<ul>
<li class="dangerous">
<button
{{on 'click' (fn @onuse item)}}
data-test-confirm-use
tabindex="-1"
type="button"
>
Use
</button>
</li>
<li>
<label for={{concat confirm 'use'}}>Cancel</label>
</li>
</ul>
</div>
</div>
</li>
{{/if}}
{{#if (can "duplicate acl" item=item)}}
<li role="none">
<button role="menuitem" tabindex="-1" type="button" data-test-clone {{action @onclone item}}>Duplicate</button>
</li>
{{/if}}
{{#if (can "delete acl" item=item)}}
<li role="none" class="dangerous">
<label for={{concat confirm 'delete'}} role="menuitem" tabindex="-1" onkeypress={{keypressClick}} data-test-delete>Delete</label>
<div role="menu">
<div class="confirmation-alert warning">
<div>
<header>
Confirm Delete
</header>
<p>
Are you sure you want to delete this token?
</p>
</div>
<ul>
<li class="dangerous">
<button tabindex="-1" type="button" class="type-delete" onclick={{action @ondelete item}}>Delete</button>
</li>
<li>
<label for={{concat confirm 'delete'}}>Cancel</label>
</li>
</ul>
</div>
</div>
</li>
{{/if}}
</BlockSlot>
</PopoverMenu>
</BlockSlot>
</TabularCollection>
</div>

View File

@ -1,31 +0,0 @@
{{#if (eq @type 'create')}}
{{#if (eq @status 'success') }}
Your ACL token has been added.
{{else}}
There was an error adding your ACL token.
{{/if}}
{{else if (eq @type 'update') }}
{{#if (eq @status 'success') }}
Your ACL token has been saved.
{{else}}
There was an error saving your ACL token.
{{/if}}
{{ else if (eq @type 'delete')}}
{{#if (eq @status 'success') }}
Your ACL token was deleted.
{{else}}
There was an error deleting your ACL token.
{{/if}}
{{ else if (eq @type 'use')}}
{{#if (eq @status 'success') }}
Now using new ACL token.
{{else}}
There was an error using that ACL token.
{{/if}}
{{ else if (eq @type 'clone')}}
{{#if (eq @status 'success') }}
Your ACL token was cloned.
{{else}}
There was an error cloning your ACL token.
{{/if}}
{{/if}}

View File

@ -1,127 +0,0 @@
<SearchBar
class="consul-acl-search-bar"
...attributes
@filter={{@filter}}
>
<:status as |search|>
{{#let
(t (concat "components.consul.acl.search-bar." search.status.key)
default=(array
(concat "common.search." search.status.key)
(concat "common.consul." search.status.key)
)
)
(t (concat "components.consul.acl.search-bar." search.status.value)
default=(array
(concat "common.search." search.status.value)
(concat "common.consul." search.status.value)
(concat "common.brand." search.status.value)
)
)
as |key value|}}
<search.RemoveFilter
aria-label={{t "common.ui.remove" item=(concat key " " value)}}
>
<dl>
<dt>{{key}}</dt>
<dd>{{value}}</dd>
</dl>
</search.RemoveFilter>
{{/let}}
</:status>
<:search as |search|>
<search.Search
@onsearch={{action @onsearch}}
@value={{@search}}
@placeholder={{t "common.search.search"}}
>
{{#if @filter.searchproperty}}
<search.Select
class="type-search-properties"
@position="right"
@onchange={{action @filter.searchproperty.change}}
@multiple={{true}}
@required={{true}}
as |components|>
<BlockSlot @name="selected">
<span>
{{t "common.search.searchproperty"}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
{{#each @filter.searchproperty.default as |prop|}}
<Option @value={{prop}} @selected={{contains prop @filter.searchproperty.value}}>
{{t (concat "common.consul." (lowercase prop))}}
</Option>
{{/each}}
{{/let}}
</BlockSlot>
</search.Select>
{{/if}}
</search.Search>
</:search>
<:filter as |search|>
<search.Select
class="type-status"
@position="left"
@onchange={{action @filter.kind.change}}
@multiple={{true}}
as |components|>
<BlockSlot @name="selected">
<span>
{{t "components.consul.acl.search-bar.kind.name"}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
{{#each (array "management" "client") as |state|}}
<Option class="value-{{state}}" @value={{state}} @selected={{contains state @filter.status.value}}>
{{t (concat "components.acl.search-bar.kind.options." state)
default=(array
(concat "common.search." state)
)
}}
</Option>
{{/each}}
{{/let}}
</BlockSlot>
</search.Select>
</:filter>
<:sort as |search|>
<search.Select
class="type-sort"
data-test-sort-control
@position="right"
@onchange={{action @sort.change}}
@multiple={{false}}
@required={{true}}
as |components|>
<BlockSlot @name="selected">
<span>
{{#let (from-entries (array
(array "Name:asc" (t "common.sort.alpha.asc"))
(array "Name:desc" (t "common.sort.alpha.desc"))
))
as |selectable|
}}
{{get selectable @sort.value}}
{{/let}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Optgroup @label={{t "common.consul.name"}}>
<Option @value="Name:asc" @selected={{eq "Name:asc" @sort.value}}>{{t "common.sort.alpha.asc"}}</Option>
<Option @value="Name:desc" @selected={{eq "Name:desc" @sort.value}}>{{t "common.sort.alpha.desc"}}</Option>
</Optgroup>
{{/let}}
</BlockSlot>
</search.Select>
</:sort>
</SearchBar>

View File

@ -1,2 +0,0 @@
import Controller from './edit';
export default class CreateController extends Controller {}

View File

@ -1,30 +0,0 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
export default Controller.extend({
builder: service('form'),
dom: service('dom'),
init: function() {
this._super(...arguments);
this.form = this.builder.form('acl');
},
setProperties: function(model) {
// essentially this replaces the data with changesets
this._super(
Object.keys(model).reduce((prev, key, i) => {
switch (key) {
case 'item':
prev[key] = this.form.setData(prev[key]).getData();
break;
}
return prev;
}, model)
);
},
actions: {
change: function(e, value, item) {
const event = this.dom.normalizeEvent(e, value);
this.form.handleEvent(event);
},
},
});

View File

@ -1,6 +0,0 @@
export default {
kind: {
management: (item, value) => item.Type === value,
client: (item, value) => item.Type === value,
},
};

View File

@ -1,6 +0,0 @@
import validations from 'consul-ui/validations/acl';
import builderFactory from 'consul-ui/utils/form/builder';
const builder = builderFactory();
export default function(container, name = '', v = validations, form = builder) {
return form(name, {}).setValidators(v);
}

View File

@ -1,33 +0,0 @@
import Mixin from '@ember/object/mixin';
import { get } from '@ember/object';
import { inject as service } from '@ember/service';
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
export default Mixin.create(WithBlockingActions, {
settings: service('settings'),
actions: {
use: function(item) {
return this.settings.persist({
token: {
Namespace: 'default',
AccessorID: null,
SecretID: get(item, 'ID'),
},
});
},
logout: function(item) {
return this.settings.delete('token');
},
clone: function(item) {
return this.feedback.execute(() => {
return this.repo.clone(item).then(item => {
// cloning is similar to delete in that
// if you clone from the listing page, stay on the listing page
// whereas if you clone form another token, take me back to the listing page
// so I can see it
return this.afterDelete(...arguments);
});
}, 'clone');
},
},
});

View File

@ -1,19 +0,0 @@
import Model, { attr } from '@ember-data/model';
export const PRIMARY_KEY = 'uid';
export const SLUG_KEY = 'ID';
export default class Acl extends Model {
@attr('string') uid;
@attr('string') ID;
@attr('string') Datacenter;
// TODO: Why didn't I have to do this for KV's? This is to ensure that Name
// is '' and not null when creating maybe its due to the fact that `Key` is
// the primaryKey in Kv's
@attr('string', { defaultValue: () => '' }) Name;
@attr('string') Type;
@attr('string') Rules;
@attr('number') CreateIndex;
@attr('number') ModifyIndex;
}

View File

@ -1,227 +1,236 @@
/* globals requirejs */ /* globals requirejs */
import EmberRouter from '@ember/routing/router'; import EmberRouter from '@ember/routing/router';
import config from './config/environment';
import { runInDebug } from '@ember/debug'; import { runInDebug } from '@ember/debug';
import merge from 'deepmerge';
import { env } from 'consul-ui/env'; import { env } from 'consul-ui/env';
import walk, { dump } from 'consul-ui/utils/routing/walk'; import walk, { dump } from 'consul-ui/utils/routing/walk';
export const routes = { const doc = document;
// Our parent datacenter resource sets the namespace const appName = config.modulePrefix;
// for the entire application const appNameJS = appName
dc: { .split('-')
_options: { path: '/:dc' }, .map((item, i) => (i ? `${item.substr(0, 1).toUpperCase()}${item.substr(1)}` : item))
// Services represent a consul service .join('');
services: {
_options: { path: '/services' }, export const routes = merge.all(
// Show an individual service [
show: { {
_options: { path: '/:name' }, // Our parent datacenter resource sets the namespace
instances: { // for the entire application
_options: { path: '/instances' }, dc: {
_options: { path: '/:dc' },
// Services represent a consul service
services: {
_options: { path: '/services' },
// Show an individual service
show: {
_options: { path: '/:name' },
instances: {
_options: { path: '/instances' },
},
intentions: {
_options: { path: '/intentions' },
edit: {
_options: { path: '/:intention_id' },
},
create: {
_options: { path: '/create' },
},
},
topology: {
_options: { path: '/topology' },
},
services: {
_options: { path: '/services' },
},
upstreams: {
_options: { path: '/upstreams' },
},
routing: {
_options: { path: '/routing' },
},
tags: {
_options: { path: '/tags' },
},
},
instance: {
_options: { path: '/:name/instances/:node/:id' },
healthchecks: {
_options: { path: '/health-checks' },
},
upstreams: {
_options: { path: '/upstreams' },
},
exposedpaths: {
_options: { path: '/exposed-paths' },
},
addresses: {
_options: { path: '/addresses' },
},
metadata: {
_options: { path: '/metadata' },
},
},
notfound: {
_options: { path: '/:name/:node/:id' },
},
}, },
// Nodes represent a consul node
nodes: {
_options: { path: '/nodes' },
// Show an individual node
show: {
_options: { path: '/:name' },
healthchecks: {
_options: { path: '/health-checks' },
},
services: {
_options: { path: '/service-instances' },
},
rtt: {
_options: { path: '/round-trip-time' },
},
sessions: {
_options: { path: '/lock-sessions' },
},
metadata: {
_options: { path: '/metadata' },
},
},
},
// Intentions represent a consul intention
intentions: { intentions: {
_options: { path: '/intentions' }, _options: { path: '/intentions' },
edit: { edit: {
_options: { path: '/:intention_id' }, _options: {
path: '/:intention_id',
abilities: ['read intentions'],
},
}, },
create: { create: {
_options: { path: '/create' }, _options: {
path: '/create',
abilities: ['create intentions'],
},
}, },
}, },
topology: { // Key/Value
_options: { path: '/topology' }, kv: {
_options: { path: '/kv' },
folder: {
_options: { path: '/*key' },
},
edit: {
_options: { path: '/*key/edit' },
},
create: {
_options: {
path: '/*key/create',
abilities: ['create kvs'],
},
},
'root-create': {
_options: {
path: '/create',
abilities: ['create kvs'],
},
},
}, },
services: { // ACLs
_options: { path: '/services' }, acls: {
_options: {
path: '/acls',
abilities: ['access acls'],
},
policies: {
_options: {
path: '/policies',
abilities: ['read policies'],
},
edit: {
_options: { path: '/:id' },
},
create: {
_options: {
path: '/create',
abilities: ['create policies'],
},
},
},
roles: {
_options: {
path: '/roles',
abilities: ['read roles'],
},
edit: {
_options: { path: '/:id' },
},
create: {
_options: {
path: '/create',
abilities: ['create roles'],
},
},
},
tokens: {
_options: {
path: '/tokens',
abilities: ['access acls'],
},
edit: {
_options: { path: '/:id' },
},
create: {
_options: {
path: '/create',
abilities: ['create tokens'],
},
},
},
'auth-methods': {
_options: {
path: '/auth-methods',
abilities: ['read auth-methods'],
},
show: {
_options: { path: '/:id' },
'auth-method': {
_options: { path: '/auth-method' },
},
'binding-rules': {
_options: { path: '/binding-rules' },
},
'nspace-rules': {
_options: { path: '/nspace-rules' },
},
},
},
}, },
upstreams: { 'routing-config': {
_options: { path: '/upstreams' }, _options: { path: '/routing-config/:name' },
},
routing: {
_options: { path: '/routing' },
},
tags: {
_options: { path: '/tags' },
}, },
}, },
instance: { // Shows a datacenter picker. If you only have one
_options: { path: '/:name/instances/:node/:id' }, // it just redirects you through.
healthchecks: { index: {
_options: { path: '/health-checks' }, _options: { path: '/' },
}, },
upstreams: { // The settings page is global.
_options: { path: '/upstreams' }, settings: {
}, _options: { path: '/setting' },
exposedpaths: {
_options: { path: '/exposed-paths' },
},
addresses: {
_options: { path: '/addresses' },
},
metadata: {
_options: { path: '/metadata' },
},
}, },
notfound: { notfound: {
_options: { path: '/:name/:node/:id' }, _options: { path: '/*notfound' },
}, },
}, },
// Nodes represent a consul node ].concat(
nodes: { ...[...doc.querySelectorAll(`script[data-${appName}-routes]`)].map($item =>
_options: { path: '/nodes' }, JSON.parse($item.dataset[`${appNameJS}Routes`])
// Show an individual node )
show: { )
_options: { path: '/:name' }, );
healthchecks: {
_options: { path: '/health-checks' },
},
services: {
_options: { path: '/service-instances' },
},
rtt: {
_options: { path: '/round-trip-time' },
},
sessions: {
_options: { path: '/lock-sessions' },
},
metadata: {
_options: { path: '/metadata' },
},
},
},
// Intentions represent a consul intention
intentions: {
_options: { path: '/intentions' },
edit: {
_options: {
path: '/:intention_id',
abilities: ['read intentions'],
},
},
create: {
_options: {
path: '/create',
abilities: ['create intentions'],
},
},
},
// Key/Value
kv: {
_options: { path: '/kv' },
folder: {
_options: { path: '/*key' },
},
edit: {
_options: { path: '/*key/edit' },
},
create: {
_options: {
path: '/*key/create',
abilities: ['create kvs'],
},
},
'root-create': {
_options: {
path: '/create',
abilities: ['create kvs'],
},
},
},
// ACLs
acls: {
_options: {
path: '/acls',
abilities: ['access acls'],
},
edit: {
_options: { path: '/:acl' },
},
create: {
_options: {
path: '/create',
abilities: ['create acls'],
},
},
policies: {
_options: {
path: '/policies',
abilities: ['read policies'],
},
edit: {
_options: { path: '/:id' },
},
create: {
_options: {
path: '/create',
abilities: ['create policies'],
},
},
},
roles: {
_options: {
path: '/roles',
abilities: ['read roles'],
},
edit: {
_options: { path: '/:id' },
},
create: {
_options: {
path: '/create',
abilities: ['create roles'],
},
},
},
tokens: {
_options: {
path: '/tokens',
abilities: env('CONSUL_ACLS_ENABLED') ? ['read tokens'] : ['access acls'],
},
edit: {
_options: { path: '/:id' },
},
create: {
_options: {
path: '/create',
abilities: ['create tokens'],
},
},
},
'auth-methods': {
_options: {
path: '/auth-methods',
abilities: ['read auth-methods'],
},
show: {
_options: { path: '/:id' },
'auth-method': {
_options: { path: '/auth-method' },
},
'binding-rules': {
_options: { path: '/binding-rules' },
},
'nspace-rules': {
_options: { path: '/nspace-rules' },
},
},
},
},
'routing-config': {
_options: { path: '/routing-config/:name' },
},
},
// Shows a datacenter picker. If you only have one
// it just redirects you through.
index: {
_options: { path: '/' },
},
// The settings page is global.
settings: {
_options: { path: '/setting' },
},
notfound: {
_options: { path: '/*notfound' },
},
};
if (env('CONSUL_NSPACES_ENABLED')) { if (env('CONSUL_NSPACES_ENABLED')) {
routes.dc.nspaces = { routes.dc.nspaces = {
_options: { _options: {
@ -242,7 +251,7 @@ if (env('CONSUL_NSPACES_ENABLED')) {
runInDebug(() => { runInDebug(() => {
// check to see if we are running docfy and if so add its routes to our // check to see if we are running docfy and if so add its routes to our
// route config // route config
const docfyOutput = requirejs.entries['consul-ui/docfy-output']; const docfyOutput = requirejs.entries[`${appName}/docfy-output`];
if (typeof docfyOutput !== 'undefined') { if (typeof docfyOutput !== 'undefined') {
const output = {}; const output = {};
docfyOutput.callback(output); docfyOutput.callback(output);
@ -269,13 +278,6 @@ runInDebug(() => {
})(routes, output.default.nested); })(routes, output.default.nested);
} }
}); });
export default class Router extends EmberRouter {
location = env('locationType');
rootURL = env('rootURL');
}
Router.map(walk(routes));
// To print the Ember route DSL use `Routes()` in Web Inspectors console // To print the Ember route DSL use `Routes()` in Web Inspectors console
// or `javascript:Routes()` in the location bar of your browser // or `javascript:Routes()` in the location bar of your browser
runInDebug(() => { runInDebug(() => {
@ -295,3 +297,9 @@ runInDebug(() => {
return; return;
}; };
}); });
export default class Router extends EmberRouter {
location = env('locationType');
rootURL = env('rootURL');
}
Router.map(walk(routes));

View File

@ -1,3 +0,0 @@
import Route from 'consul-ui/routing/route';
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
export default class AclsRoute extends Route.extend(WithBlockingActions) {}

View File

@ -1,39 +0,0 @@
import { inject as service } from '@ember/service';
import Route from 'consul-ui/routing/route';
import { hash } from 'rsvp';
import { get } from '@ember/object';
import WithAclActions from 'consul-ui/mixins/acl/with-actions';
export default class CreateRoute extends Route.extend(WithAclActions) {
templateName = 'dc/acls/edit';
@service('repository/acl')
repo;
beforeModel() {
this.repo.invalidate();
}
model(params) {
this.item = this.repo.create({
Datacenter: this.modelFor('dc').dc.Name,
});
return hash({
create: true,
item: this.item,
types: ['management', 'client'],
});
}
setupController(controller, model) {
super.setupController(...arguments);
controller.setProperties(model);
}
deactivate() {
if (get(this.item, 'isNew')) {
this.item.destroyRecord();
}
}
}

View File

@ -1,28 +0,0 @@
import { inject as service } from '@ember/service';
import Route from 'consul-ui/routing/route';
import { hash } from 'rsvp';
import WithAclActions from 'consul-ui/mixins/acl/with-actions';
export default class EditRoute extends Route.extend(WithAclActions) {
@service('repository/acl')
repo;
@service('settings')
settings;
model(params) {
return hash({
item: this.repo.findBySlug({
dc: this.modelFor('dc').dc.Name,
id: params.id,
}),
types: ['management', 'client'],
});
}
setupController(controller, model) {
super.setupController(...arguments);
controller.setProperties(model);
}
}

View File

@ -1,45 +0,0 @@
import { inject as service } from '@ember/service';
import Route from 'consul-ui/routing/route';
import { get } from '@ember/object';
import WithAclActions from 'consul-ui/mixins/acl/with-actions';
export default class IndexRoute extends Route.extend(WithAclActions) {
@service('repository/acl') repo;
@service('settings') settings;
queryParams = {
sortBy: 'sort',
kind: 'kind',
search: {
as: 'filter',
replace: true,
},
};
async beforeModel(transition) {
const token = await this.settings.findBySlug('token');
// If you don't have a token set or you have a
// token set with AccessorID set to not null (new ACL mode)
// then rewrite to the new acls
if (!token || get(token, 'AccessorID') !== null) {
// If you return here, you get a TransitionAborted error in the tests only
// everything works fine either way checking things manually
this.replaceWith('dc.acls.tokens');
}
}
async model(params) {
const _items = this.repo.findAllByDatacenter({ dc: this.modelFor('dc').dc.Name });
const _token = this.settings.findBySlug('token');
return {
items: await _items,
token: await _token,
};
}
setupController(controller, model) {
super.setupController(...arguments);
controller.setProperties(model);
}
}

View File

@ -15,6 +15,22 @@ export default class BaseRoute extends Route {
@service('repository/permission') permissions; @service('repository/permission') permissions;
@service('router') router; @service('router') router;
redirect(model, transition) {
// remove any references to index as it is the same as the root routeName
const routeName = this.routeName
.split('.')
.filter(item => item !== 'index')
.join('.');
const to = get(routes, `${routeName}._options.redirect`);
if (typeof to !== 'undefined') {
// TODO: Does this need to return?
// Almost remember things getting strange if you returned from here
// which is why I didn't do it originally so be sure to look properly if
// you feel like adding a return
this.replaceWith(`${routeName}${to}`, model);
}
}
/** /**
* Inspects a custom `abilities` array on the router for this route. Every * Inspects a custom `abilities` array on the router for this route. Every
* abililty needs to 'pass' for the route not to throw a 403 error. Anything * abililty needs to 'pass' for the route not to throw a 403 error. Anything

View File

@ -1,14 +0,0 @@
import Serializer from './application';
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/acl';
export default class AclSerializer extends Serializer {
primaryKey = PRIMARY_KEY;
slugKey = SLUG_KEY;
respondForQueryRecord(respond, query) {
return super.respondForQueryRecord(
cb => respond((headers, body) => cb(headers, body[0])),
query
);
}
}

View File

@ -1,7 +1,6 @@
import Service from '@ember/service'; import Service from '@ember/service';
import { andOr } from 'consul-ui/utils/filter'; import { andOr } from 'consul-ui/utils/filter';
import acl from 'consul-ui/filter/predicates/acl';
import service from 'consul-ui/filter/predicates/service'; import service from 'consul-ui/filter/predicates/service';
import serviceInstance from 'consul-ui/filter/predicates/service-instance'; import serviceInstance from 'consul-ui/filter/predicates/service-instance';
import healthCheck from 'consul-ui/filter/predicates/health-check'; import healthCheck from 'consul-ui/filter/predicates/health-check';
@ -13,7 +12,6 @@ import policy from 'consul-ui/filter/predicates/policy';
import authMethod from 'consul-ui/filter/predicates/auth-method'; import authMethod from 'consul-ui/filter/predicates/auth-method';
const predicates = { const predicates = {
acl: andOr(acl),
service: andOr(service), service: andOr(service),
['service-instance']: andOr(serviceInstance), ['service-instance']: andOr(serviceInstance),
['health-check']: andOr(healthCheck), ['health-check']: andOr(healthCheck),

View File

@ -2,7 +2,6 @@ import Service, { inject as service } from '@ember/service';
import builderFactory from 'consul-ui/utils/form/builder'; import builderFactory from 'consul-ui/utils/form/builder';
import kv from 'consul-ui/forms/kv'; import kv from 'consul-ui/forms/kv';
import acl from 'consul-ui/forms/acl';
import token from 'consul-ui/forms/token'; import token from 'consul-ui/forms/token';
import policy from 'consul-ui/forms/policy'; import policy from 'consul-ui/forms/policy';
import role from 'consul-ui/forms/role'; import role from 'consul-ui/forms/role';
@ -13,7 +12,6 @@ const builder = builderFactory();
const forms = { const forms = {
kv: kv, kv: kv,
acl: acl,
token: token, token: token,
policy: policy, policy: policy,
role: role, role: role,

View File

@ -1,17 +0,0 @@
import RepositoryService from 'consul-ui/services/repository';
import { get } from '@ember/object';
import { PRIMARY_KEY } from 'consul-ui/models/acl';
const modelName = 'acl';
export default class AclService extends RepositoryService {
getModelName() {
return modelName;
}
getPrimaryKey() {
return PRIMARY_KEY;
}
clone(item) {
return this.store.clone(this.getModelName(), get(item, this.getPrimaryKey()));
}
}

View File

@ -1,11 +1,7 @@
import RepositoryService from 'consul-ui/services/repository'; import RepositoryService from 'consul-ui/services/repository';
import statusFactory from 'consul-ui/utils/acls-status';
import isValidServerErrorFactory from 'consul-ui/utils/http/acl/is-valid-server-error';
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/auth-method'; import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/auth-method';
import dataSource from 'consul-ui/decorators/data-source'; import dataSource from 'consul-ui/decorators/data-source';
const isValidServerError = isValidServerErrorFactory();
const status = statusFactory(isValidServerError, Promise);
const MODEL_NAME = 'auth-method'; const MODEL_NAME = 'auth-method';
export default class AuthMethodService extends RepositoryService { export default class AuthMethodService extends RepositoryService {
@ -30,8 +26,4 @@ export default class AuthMethodService extends RepositoryService {
async findBySlug() { async findBySlug() {
return super.findBySlug(...arguments); return super.findBySlug(...arguments);
} }
status(obj) {
return status(obj);
}
} }

View File

@ -1,11 +1,7 @@
import RepositoryService from 'consul-ui/services/repository'; import RepositoryService from 'consul-ui/services/repository';
import statusFactory from 'consul-ui/utils/acls-status';
import isValidServerErrorFactory from 'consul-ui/utils/http/acl/is-valid-server-error';
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/binding-rule'; import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/binding-rule';
import dataSource from 'consul-ui/decorators/data-source'; import dataSource from 'consul-ui/decorators/data-source';
const isValidServerError = isValidServerErrorFactory();
const status = statusFactory(isValidServerError, Promise);
const MODEL_NAME = 'binding-rule'; const MODEL_NAME = 'binding-rule';
export default class BindingRuleService extends RepositoryService { export default class BindingRuleService extends RepositoryService {
@ -25,8 +21,4 @@ export default class BindingRuleService extends RepositoryService {
async findAllByAuthMethod() { async findAllByAuthMethod() {
return super.findAll(...arguments); return super.findAll(...arguments);
} }
status(obj) {
return status(obj);
}
} }

View File

@ -1,13 +1,9 @@
import RepositoryService from 'consul-ui/services/repository'; import RepositoryService from 'consul-ui/services/repository';
import { get } from '@ember/object'; import { get } from '@ember/object';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import statusFactory from 'consul-ui/utils/acls-status';
import isValidServerErrorFactory from 'consul-ui/utils/http/acl/is-valid-server-error';
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/policy'; import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/policy';
import dataSource from 'consul-ui/decorators/data-source'; import dataSource from 'consul-ui/decorators/data-source';
const isValidServerError = isValidServerErrorFactory();
const status = statusFactory(isValidServerError, Promise);
const MODEL_NAME = 'policy'; const MODEL_NAME = 'policy';
export default class PolicyService extends RepositoryService { export default class PolicyService extends RepositoryService {
@ -47,10 +43,6 @@ export default class PolicyService extends RepositoryService {
.getData(); .getData();
} }
status(obj) {
return status(obj);
}
persist(item) { persist(item) {
// only if a policy doesn't have a template, save it // only if a policy doesn't have a template, save it
// right now only ServiceIdentities have templates and // right now only ServiceIdentities have templates and

View File

@ -1,12 +1,8 @@
import RepositoryService from 'consul-ui/services/repository'; import RepositoryService from 'consul-ui/services/repository';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import statusFactory from 'consul-ui/utils/acls-status';
import isValidServerErrorFactory from 'consul-ui/utils/http/acl/is-valid-server-error';
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/role'; import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/role';
import dataSource from 'consul-ui/decorators/data-source'; import dataSource from 'consul-ui/decorators/data-source';
const isValidServerError = isValidServerErrorFactory();
const status = statusFactory(isValidServerError, Promise);
const MODEL_NAME = 'role'; const MODEL_NAME = 'role';
export default class RoleService extends RepositoryService { export default class RoleService extends RepositoryService {
@ -45,8 +41,4 @@ export default class RoleService extends RepositoryService {
.setData(item) .setData(item)
.getData(); .getData();
} }
status(obj) {
return status(obj);
}
} }

View File

@ -2,12 +2,8 @@ import RepositoryService from 'consul-ui/services/repository';
import { get } from '@ember/object'; import { get } from '@ember/object';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/token'; import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/token';
import statusFactory from 'consul-ui/utils/acls-status';
import isValidServerErrorFactory from 'consul-ui/utils/http/acl/is-valid-server-error';
import dataSource from 'consul-ui/decorators/data-source'; import dataSource from 'consul-ui/decorators/data-source';
const isValidServerError = isValidServerErrorFactory();
const status = statusFactory(isValidServerError, Promise);
const MODEL_NAME = 'token'; const MODEL_NAME = 'token';
export default class TokenService extends RepositoryService { export default class TokenService extends RepositoryService {
@ -24,10 +20,6 @@ export default class TokenService extends RepositoryService {
return SLUG_KEY; return SLUG_KEY;
} }
status(obj) {
return status(obj);
}
@dataSource('/:partition/:ns/:dc/tokens') @dataSource('/:partition/:ns/:dc/tokens')
async findAll() { async findAll() {
return super.findAll(...arguments); return super.findAll(...arguments);
@ -53,21 +45,14 @@ export default class TokenService extends RepositoryService {
@dataSource('/:partition/:ns/:dc/token/self/:secret') @dataSource('/:partition/:ns/:dc/token/self/:secret')
self(params) { self(params) {
// TODO: Does this need ns passing through? // This request does not need ns or partition passing through as its
// inferred from the token itself.
return this.store return this.store
.self(this.getModelName(), { .self(this.getModelName(), {
secret: params.secret, secret: params.secret,
dc: params.dc, dc: params.dc,
}) })
.catch(e => { .catch(e => {
// If we get this 500 RPC error, it means we are a legacy ACL cluster
// set AccessorID to null - which for the frontend means legacy mode
if (isValidServerError(e)) {
return {
AccessorID: null,
SecretID: params.secret,
};
}
return Promise.reject(e); return Promise.reject(e);
}); });
} }

View File

@ -2,7 +2,6 @@ import Service from '@ember/service';
import service from 'consul-ui/sort/comparators/service'; import service from 'consul-ui/sort/comparators/service';
import serviceInstance from 'consul-ui/sort/comparators/service-instance'; import serviceInstance from 'consul-ui/sort/comparators/service-instance';
import upstreamInstance from 'consul-ui/sort/comparators/upstream-instance'; import upstreamInstance from 'consul-ui/sort/comparators/upstream-instance';
import acl from 'consul-ui/sort/comparators/acl';
import kv from 'consul-ui/sort/comparators/kv'; import kv from 'consul-ui/sort/comparators/kv';
import healthCheck from 'consul-ui/sort/comparators/health-check'; import healthCheck from 'consul-ui/sort/comparators/health-check';
import intention from 'consul-ui/sort/comparators/intention'; import intention from 'consul-ui/sort/comparators/intention';
@ -34,7 +33,6 @@ const comparators = {
['upstream-instance']: upstreamInstance(options), ['upstream-instance']: upstreamInstance(options),
['health-check']: healthCheck(options), ['health-check']: healthCheck(options),
['auth-method']: authMethod(options), ['auth-method']: authMethod(options),
acl: acl(options),
kv: kv(options), kv: kv(options),
intention: intention(options), intention: intention(options),
token: token(options), token: token(options),

View File

@ -1,3 +0,0 @@
export default ({ properties }) => (key = 'Name:asc') => {
return properties(['Name'])(key);
};

View File

@ -1,51 +0,0 @@
<form>
<fieldset
disabled={{if (not (can "write acl" item=item)) "disabled"}}
>
<label class="type-text{{if item.error.Name ' has-error'}}">
<span>Name</span>
<Input @value={{item.Name}} @name="name" @autofocus="autofocus" />
</label>
<div role="radiogroup" class={{if item.error.Type ' has-error'}}>
{{#each types as |type|}}
<label>
<span>{{capitalize type}}</span>
<input type="radio" name="Type" value="{{type}}" checked={{if (eq item.Type type) 'checked'}} onchange={{ action 'change' }}/>
</label>
{{/each}}
</div>
<label class="type-text">
<span>Policy <a href="{{env 'CONSUL_DOCS_URL'}}/guides/acl.html#rule-specification" rel="help noopener noreferrer" target="_blank">(HCL Format)</a></span>
<CodeEditor @class={{if item.error.Rules "error"}} @name="Rules" @value={{item.Rules}} @syntax="hcl" @onkeyup={{action "change" "Rules"}} />
</label>
{{#if create }}
<label class="type-text">
<span>ID</span>
<Input @value={{item.ID}} />
<em>We'll generate a UUID if this field is left empty.</em>
</label>
{{/if}}
</fieldset>
<div>
{{#if (and create (can "create acls")) }}
{{! we only need to check for an empty name here as ember munges autofocus, once we have autofocus back revisit this}}
<button type="submit" {{ action "create" item}} disabled={{if (or item.isPristine item.isInvalid (eq item.Name '')) 'disabled'}}>Save</button>
{{else}}
{{#if (can "write acl" item=item)}}
<button type="submit" {{ action "update" item}} disabled={{if item.isInvalid 'disabled'}}>Save</button>
{{/if}}
{{/if}}
<button type="reset" {{ action "cancel" item}}>Cancel</button>
{{# if (and (not create) (can "delete acl" item=item) ) }}
<ConfirmationDialog @message="Are you sure you want to delete this ACL token?">
<BlockSlot @name="action" as |confirm|>
<button type="button" data-test-delete class="type-delete" {{action confirm 'delete' item parent}}>Delete</button>
</BlockSlot>
<BlockSlot @name="dialog" as |execute cancel message|>
<DeleteConfirmation @message={{message}} @execute={{execute}} @cancel={{cancel}} />
</BlockSlot>
</ConfirmationDialog>
{{/if}}
</div>
</form>

View File

@ -1,53 +0,0 @@
<Route
@name={{routeName}}
@title={{if create 'New ACL' 'Edit ACL'}}
as |route|>
<AppView>
<BlockSlot @name="notification" as |status type item error|>
<Consul::Acl::Notifications
@status={{status}}
@type={{type}}
@error={{error}}
/>
</BlockSlot>
<BlockSlot @name="breadcrumbs">
<ol>
<li><a data-test-back href={{href-to 'dc.acls'}}>All Tokens</a></li>
</ol>
</BlockSlot>
<BlockSlot @name="header">
<h1>
{{#if item.Name }}
{{item.Name}}
{{else}}
New token
{{/if}}
</h1>
</BlockSlot>
<BlockSlot @name="actions">
{{#if (not create) }}
<CopyButton @value={{item.ID}} @name="token ID">
Copy token ID
</CopyButton>
{{#if (can "duplicate acl" item=item)}}
<button type="button" {{ action "clone" item }}>Clone token</button>
<ConfirmationDialog @message="Are you sure you want to use this ACL token?">
<BlockSlot @name="action" as |confirm|>
<button data-test-use type="button" {{ action confirm 'use' item }}>Use token</button>
</BlockSlot>
<BlockSlot @name="dialog" as |execute cancel message|>
<p>
{{message}}
</p>
<button type="button" class="type-delete" {{action execute}}>Confirm Use</button>
<button type="button" class="type-cancel" {{action cancel}}>Cancel</button>
</BlockSlot>
</ConfirmationDialog>
{{/if}}
{{/if}}
</BlockSlot>
<BlockSlot @name="content">
{{ partial 'dc/acls/form'}}
</BlockSlot>
</AppView>
</Route>

View File

@ -1,101 +1,5 @@
<Route <Route
@name={{routeName}} @name={{routeName}}
@title="ACLs"
as |route|> as |route|>
{{#let {{did-insert (route-action 'replaceWith' 'dc.acls.tokens')}}
(hash
value=(or sortBy "Name:asc")
change=(action (mut sortBy) value="target.selected")
)
(hash
kind=(hash
value=(if kind (split kind ',') undefined)
change=(action (mut kind) value="target.selectedItems")
)
)
items
as |sort filters items|}}
<AppView>
<BlockSlot @name="notification" as |status type item error|>
<Consul::Acl::Notifications
@status={{status}}
@type={{type}}
@error={{error}}
/>
</BlockSlot>
<BlockSlot @name="header">
<h1>
ACL Tokens <em>{{format-number items.length}} total</em>
</h1>
<label for="toolbar-toggle"></label>
</BlockSlot>
<BlockSlot @name="actions">
{{#if (can "create acls")}}
<a data-test-create href="{{href-to 'dc.acls.create'}}" class="type-create">Create</a>
{{/if}}
</BlockSlot>
<BlockSlot @name="toolbar">
{{#if (gt items.length 0) }}
<Consul::Acl::SearchBar
@search={{search}}
@onsearch={{action (mut search) value="target.value"}}
@sort={{sort}}
@filter={{filters}}
/>
{{/if}}
</BlockSlot>
<BlockSlot @name="content">
<DataCollection
@type="acl"
@sort={{sort.value}}
@filters={{filters}}
@search={{search}}
@items={{items}}
as |collection|>
<collection.Collection>
<Consul::Acl::List
@items={{collection.items}}
@ondelete={{route-action 'delete'}}
@onuse={{route-action 'use'}}
@onclone={{route-action 'clone'}}
>
</Consul::Acl::List>
</collection.Collection>
<collection.Empty>
<EmptyState
@login={{route.model.app.login.open}}
>
<BlockSlot @name="header">
<h2>
{{#if (gt items.length 0)}}
No ACLs found
{{else}}
Welcome to ACLs
{{/if}}
</h2>
</BlockSlot>
<BlockSlot @name="body">
<p>
{{#if (gt items.length 0)}}
No ACLs where found matching that search, or you may not have access to view the ACLs you are searching for.
{{else}}
There don't seem to be any ACLs yet, or you may not have access to view ACLs yet.
{{/if}}
</p>
</BlockSlot>
</EmptyState>
</collection.Empty>
</DataCollection>
</BlockSlot>
</AppView>
{{/let}}
</Route> </Route>

View File

@ -1,64 +0,0 @@
// This is used by all acl routes to check whether
// acls are enabled on the server, and whether the user
// has a valid token
// Right now this is very acl specific, but is likely to be
// made a bit more less specific
export default function(isValidServerError, P = Promise) {
return function(obj) {
const propName = Object.keys(obj)[0];
const p = obj[propName];
let authorize;
let enable;
return {
isAuthorized: new P(function(resolve) {
authorize = function(bool) {
resolve(bool);
};
}),
isEnabled: new P(function(resolve) {
enable = function(bool) {
resolve(bool);
};
}),
[propName]: p
.catch(function(e) {
if (e.errors && e.errors[0]) {
switch (e.errors[0].status) {
case '500':
if (isValidServerError(e)) {
enable(true);
authorize(false);
} else {
enable(false);
authorize(false);
return P.reject(e);
}
break;
case '403':
enable(true);
authorize(false);
break;
case '401':
enable(false);
authorize(false);
break;
default:
enable(false);
authorize(false);
throw e;
}
return [];
}
enable(false);
authorize(false);
throw e;
})
.then(function(res) {
enable(true);
authorize(true);
return res;
}),
};
};
}

View File

@ -1,12 +0,0 @@
// very specific error check just for one specific ACL case
// likely to be reused at a later date, so lets use the specific
// case we need right now as default
const UNKNOWN_METHOD_ERROR = "rpc error making call: rpc: can't find method ACL";
export default function(response = UNKNOWN_METHOD_ERROR) {
return function(e) {
if (e && e.errors && e.errors[0] && e.errors[0].detail) {
return e.errors[0].detail.indexOf(response) !== -1;
}
return false;
};
}

View File

@ -1,5 +0,0 @@
import { validatePresence, validateLength } from 'ember-changeset-validations/validators';
export default {
Name: [validatePresence(true), validateLength({ min: 1 })],
Type: validatePresence(true),
};

View File

@ -163,6 +163,9 @@ module.exports = function(defaults, $ = process.env) {
app.import('vendor/metrics-providers/prometheus.js', { app.import('vendor/metrics-providers/prometheus.js', {
outputFile: 'assets/metrics-providers/prometheus.js', outputFile: 'assets/metrics-providers/prometheus.js',
}); });
app.import('vendor/acls/routes.js', {
outputFile: 'assets/acls/routes.js',
});
app.import('vendor/init.js', { app.import('vendor/init.js', {
outputFile: 'assets/init.js', outputFile: 'assets/init.js',
}); });

View File

@ -41,6 +41,26 @@ ${environment === 'production' ? `{{jsonEncode .}}` : JSON.stringify(config.oper
"codemirror/mode/yaml/yaml.js": "${rootURL}assets/codemirror/mode/yaml/yaml.js" "codemirror/mode/yaml/yaml.js": "${rootURL}assets/codemirror/mode/yaml/yaml.js"
} }
</script> </script>
${
environment === 'production'
? `
{{if .ACLsEnabled}}
<script data-${appName}-routing src="${rootURL}assets/acls/routes.js"></script>
{{end}}
`
: `
<script>
if(document.cookie['CONSUL_ACLS_ENABLED']) {
const appName = '${appName}';
const appNameJS = appName.split('-').map((item, i) => i ? \`\${item.substr(0, 1).toUpperCase()}\${item.substr(1)}\` : item).join('');
const $script = document.createElement('script');
$script.setAttribute('src', '${rootURL}assets/acls/routes.js');
$script.dataset[\`\${appNameJS}Routes\`] = null;
document.body.appendChild($script);
}
</script>
`
}
<script src="${rootURL}assets/init.js"></script> <script src="${rootURL}assets/init.js"></script>
<script src="${rootURL}assets/vendor.js"></script> <script src="${rootURL}assets/vendor.js"></script>
${environment === 'test' ? `<script src="${rootURL}assets/test-support.js"></script>` : ``} ${environment === 'test' ? `<script src="${rootURL}assets/test-support.js"></script>` : ``}

View File

@ -85,6 +85,7 @@
"d3-selection": "^2.0.0", "d3-selection": "^2.0.0",
"d3-shape": "^2.0.0", "d3-shape": "^2.0.0",
"dayjs": "^1.9.3", "dayjs": "^1.9.3",
"deepmerge": "^4.2.2",
"ember-assign-helper": "^0.3.0", "ember-assign-helper": "^0.3.0",
"ember-auto-import": "^1.5.3", "ember-auto-import": "^1.5.3",
"ember-can": "^3.0.0", "ember-can": "^3.0.0",

View File

@ -29,7 +29,7 @@ module.exports = function(app, options) {
// sets the base CSP policy for the UI // sets the base CSP policy for the UI
app.use(function(request, response, next) { app.use(function(request, response, next) {
response.set({ response.set({
'Content-Security-Policy': `default-src 'self' ws: localhost:${options.liveReloadPort} http: localhost:${options.liveReloadPort}; img-src 'self' data: ; style-src 'self' 'unsafe-inline'`, 'Content-Security-Policy': `default-src 'self' 'unsafe-inline' ws: localhost:${options.liveReloadPort} http: localhost:${options.liveReloadPort}; img-src 'self' data: ; style-src 'self' 'unsafe-inline'`,
}); });
next(); next();
}); });

View File

@ -1,106 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Integration | Adapter | acl', function(hooks) {
setupTest(hooks);
const dc = 'dc-1';
const id = 'token-name';
test('requestForQuery returns the correct url', function(assert) {
const adapter = this.owner.lookup('adapter:acl');
const client = this.owner.lookup('service:client/http');
const request = client.url.bind(client);
const expected = `GET /v1/acl/list?dc=${dc}`;
const actual = adapter.requestForQuery(request, {
dc: dc,
});
assert.equal(actual, expected);
});
test('requestForQueryRecord returns the correct url', function(assert) {
const adapter = this.owner.lookup('adapter:acl');
const client = this.owner.lookup('service:client/http');
const request = client.url.bind(client);
const expected = `GET /v1/acl/info/${id}?dc=${dc}`;
const actual = adapter.requestForQueryRecord(request, {
dc: dc,
id: id,
});
assert.equal(actual, expected);
});
test("requestForQueryRecord throws if you don't specify an id", function(assert) {
const adapter = this.owner.lookup('adapter:acl');
const client = this.owner.lookup('service:client/http');
const request = client.url.bind(client);
assert.throws(function() {
adapter.requestForQueryRecord(request, {
dc: dc,
});
});
});
test('requestForCreateRecord returns the correct url', function(assert) {
const adapter = this.owner.lookup('adapter:acl');
const client = this.owner.lookup('service:client/http');
const request = client.url.bind(client);
const expected = `PUT /v1/acl/create?dc=${dc}`;
const actual = adapter
.requestForCreateRecord(
request,
{},
{
Datacenter: dc,
ID: id,
}
)
.split('\n')[0];
assert.equal(actual, expected);
});
test('requestForUpdateRecord returns the correct url', function(assert) {
const adapter = this.owner.lookup('adapter:acl');
const client = this.owner.lookup('service:client/http');
const request = client.url.bind(client);
const expected = `PUT /v1/acl/update?dc=${dc}`;
const actual = adapter
.requestForUpdateRecord(
request,
{},
{
Datacenter: dc,
ID: id,
}
)
.split('\n')[0];
assert.equal(actual, expected);
});
test('requestForDeleteRecord returns the correct url', function(assert) {
const adapter = this.owner.lookup('adapter:acl');
const client = this.owner.lookup('service:client/http');
const request = client.url.bind(client);
const expected = `PUT /v1/acl/destroy/${id}?dc=${dc}`;
const actual = adapter
.requestForDeleteRecord(
request,
{},
{
Datacenter: dc,
ID: id,
}
)
.split('/n')[0];
assert.equal(actual, expected);
});
test('requestForCloneRecord returns the correct url', function(assert) {
const adapter = this.owner.lookup('adapter:acl');
const client = this.owner.lookup('service:client/http');
const request = client.url.bind(client);
const expected = `PUT /v1/acl/clone/${id}?dc=${dc}`;
const actual = adapter
.requestForCloneRecord(
request,
{},
{
Datacenter: dc,
ID: id,
}
)
.split('\n')[0];
assert.equal(actual, expected);
});
});

View File

@ -1,12 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Adapter | acl', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let adapter = this.owner.lookup('adapter:acl');
assert.ok(adapter);
});
});

View File

@ -1,12 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Controller | dc/acls/create', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let controller = this.owner.lookup('controller:dc/acls/create');
assert.ok(controller);
});
});

View File

@ -1,12 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Controller | dc/acls/edit', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let controller = this.owner.lookup('controller:dc/acls/edit');
assert.ok(controller);
});
});

View File

@ -1,75 +0,0 @@
import { module } from 'qunit';
import { setupTest } from 'ember-qunit';
import test from 'ember-sinon-qunit/test-support/test';
import Service from '@ember/service';
import Route from 'consul-ui/routes/dc/acls/index';
import Mixin from 'consul-ui/mixins/acl/with-actions';
module('Unit | Mixin | acl/with actions', function(hooks) {
setupTest(hooks);
hooks.beforeEach(function() {
this.subject = function() {
const MixedIn = Route.extend(Mixin);
this.owner.register('test-container:acl/with-actions-object', MixedIn);
return this.owner.lookup('test-container:acl/with-actions-object');
};
});
// Replace this with your real tests.
test('it works', function(assert) {
const subject = this.subject();
assert.ok(subject);
});
test('use persists the token', function(assert) {
assert.expect(2);
const item = { ID: 'id' };
const expected = { Namespace: 'default', AccessorID: null, SecretID: item.ID };
this.owner.register(
'service:settings',
Service.extend({
persist: function(actual) {
assert.deepEqual(actual.token, expected);
return Promise.resolve(actual);
},
})
);
const subject = this.subject();
return subject.actions.use
.bind(subject)(item)
.then(function(actual) {
assert.deepEqual(actual.token, expected);
});
});
test('clone clones the token and calls afterDelete correctly', function(assert) {
assert.expect(4);
this.owner.register(
'service:feedback',
Service.extend({
execute: function(cb, name) {
assert.equal(name, 'clone');
return cb();
},
})
);
const expected = { ID: 'id' };
this.owner.register(
'service:repository/acl',
Service.extend({
clone: function(actual) {
assert.deepEqual(actual, expected);
return Promise.resolve(actual);
},
})
);
const subject = this.subject();
const afterDelete = this.stub(subject, 'afterDelete').returnsArg(0);
return subject.actions.clone
.bind(subject)(expected)
.then(function(actual) {
assert.ok(afterDelete.calledOnce);
assert.equal(actual, expected);
});
});
});

View File

@ -1,14 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { run } from '@ember/runloop';
module('Unit | Model | acl', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let store = this.owner.lookup('service:store');
let model = run(() => store.createRecord('acl', {}));
assert.ok(model);
});
});

View File

@ -1,11 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Route | dc/acls', function(hooks) {
setupTest(hooks);
test('it exists', function(assert) {
let route = this.owner.lookup('route:dc/acls');
assert.ok(route);
});
});

View File

@ -1,11 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Route | dc/acls/create', function(hooks) {
setupTest(hooks);
test('it exists', function(assert) {
let route = this.owner.lookup('route:dc/acls/create');
assert.ok(route);
});
});

View File

@ -1,11 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Route | dc/acls/edit', function(hooks) {
setupTest(hooks);
test('it exists', function(assert) {
let route = this.owner.lookup('route:dc/acls/edit');
assert.ok(route);
});
});

View File

@ -1,11 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Route | dc/acls/index', function(hooks) {
setupTest(hooks);
test('it exists', function(assert) {
let route = this.owner.lookup('route:dc/acls/index');
assert.ok(route);
});
});

View File

@ -1,43 +0,0 @@
import { module, test } from 'qunit';
import ExactSearch from 'consul-ui/utils/search/exact';
import predicates from 'consul-ui/search/predicates/acl';
module('Unit | Search | Predicate | acl', function() {
test('items are found by properties', function(assert) {
const actual = new ExactSearch(
[
{
ID: 'HIT-id',
Name: 'name',
},
{
ID: 'id',
Name: 'name',
},
{
ID: 'id',
Name: 'name-HIT',
},
],
{
finders: predicates,
}
).search('hit');
assert.equal(actual.length, 2);
});
test('items are not found', function(assert) {
const actual = new ExactSearch(
[
{
ID: 'id',
Name: 'name',
},
],
{
finders: predicates,
}
).search('hit');
assert.equal(actual.length, 0);
});
});

View File

@ -1,24 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { run } from '@ember/runloop';
module('Unit | Serializer | acl', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let store = this.owner.lookup('service:store');
let serializer = store.serializerFor('acl');
assert.ok(serializer);
});
test('it serializes records', function(assert) {
let store = this.owner.lookup('service:store');
let record = run(() => store.createRecord('acl', {}));
let serializedRecord = record.serialize();
assert.ok(serializedRecord);
});
});

View File

@ -1,12 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Service | acl', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let service = this.owner.lookup('service:repository/acl');
assert.ok(service);
});
});

View File

@ -1,89 +0,0 @@
import { module } from 'qunit';
import test from 'ember-sinon-qunit/test-support/test';
import aclsStatus from 'consul-ui/utils/acls-status';
module('Unit | Utility | acls status', function() {
test('it rejects and nothing is enabled or authorized', function(assert) {
const isValidServerError = this.stub().returns(false);
const status = aclsStatus(isValidServerError);
[
this.stub().rejects(),
this.stub().rejects({ errors: [] }),
this.stub().rejects({ errors: [{ status: '404' }] }),
].forEach(function(reject) {
const actual = status({
response: reject(),
});
assert.rejects(actual.response);
['isAuthorized', 'isEnabled'].forEach(function(prop) {
actual[prop].then(function(actual) {
assert.notOk(actual);
});
});
});
});
test('with a 401 it resolves with an empty array and nothing is enabled or authorized', function(assert) {
assert.expect(3);
const isValidServerError = this.stub().returns(false);
const status = aclsStatus(isValidServerError);
const actual = status({
response: this.stub().rejects({ errors: [{ status: '401' }] })(),
});
actual.response.then(function(actual) {
assert.deepEqual(actual, []);
});
['isAuthorized', 'isEnabled'].forEach(function(prop) {
actual[prop].then(function(actual) {
assert.notOk(actual);
});
});
});
test("with a 403 it resolves with an empty array and it's enabled but not authorized", function(assert) {
assert.expect(3);
const isValidServerError = this.stub().returns(false);
const status = aclsStatus(isValidServerError);
const actual = status({
response: this.stub().rejects({ errors: [{ status: '403' }] })(),
});
actual.response.then(function(actual) {
assert.deepEqual(actual, []);
});
actual.isEnabled.then(function(actual) {
assert.ok(actual);
});
actual.isAuthorized.then(function(actual) {
assert.notOk(actual);
});
});
test("with a 500 (but not a 'valid' error) it rejects and nothing is enabled or authorized", function(assert) {
assert.expect(3);
const isValidServerError = this.stub().returns(false);
const status = aclsStatus(isValidServerError);
const actual = status({
response: this.stub().rejects({ errors: [{ status: '500' }] })(),
});
assert.rejects(actual.response);
['isAuthorized', 'isEnabled'].forEach(function(prop) {
actual[prop].then(function(actual) {
assert.notOk(actual);
});
});
});
test("with a 500 and a 'valid' error, it resolves with an empty array and it's enabled but not authorized", function(assert) {
assert.expect(3);
const isValidServerError = this.stub().returns(true);
const status = aclsStatus(isValidServerError);
const actual = status({
response: this.stub().rejects({ errors: [{ status: '500' }] })(),
});
actual.response.then(function(actual) {
assert.deepEqual(actual, []);
});
actual.isEnabled.then(function(actual) {
assert.ok(actual);
});
actual.isAuthorized.then(function(actual) {
assert.notOk(actual);
});
});
});

View File

@ -1,49 +0,0 @@
import createIsValidServerError from 'consul-ui/utils/http/acl/is-valid-server-error';
import { module, test } from 'qunit';
module('Unit | Utility | http/acl/is valid server error', function() {
const createEmberDataError = function(response) {
return {
errors: [
{
detail: response,
},
],
};
};
test('it returns a function', function(assert) {
const isValidServerError = createIsValidServerError();
assert.ok(typeof isValidServerError === 'function');
});
test("it returns false if there is no 'correctly' formatted error", function(assert) {
const isValidServerError = createIsValidServerError();
assert.notOk(isValidServerError());
assert.notOk(isValidServerError({}));
assert.notOk(isValidServerError({ errors: {} }));
assert.notOk(isValidServerError({ errors: [{}] }));
assert.notOk(isValidServerError({ errors: [{ notDetail: '' }] }));
});
// don't go too crazy with these, just enough for sanity check, we are essentially testing indexOf
test("it returns false if the response doesn't contain the exact error response", function(assert) {
const isValidServerError = createIsValidServerError();
[
"pc error making call: rpc: can't find method ACL",
"rpc error making call: rpc: can't find method",
"rpc rror making call: rpc: can't find method ACL",
].forEach(function(response) {
const e = createEmberDataError(response);
assert.notOk(isValidServerError(e));
});
});
test('it returns true if the response contains the exact error response', function(assert) {
const isValidServerError = createIsValidServerError();
[
"rpc error making call: rpc: can't find method ACL",
" rpc error making call: rpc: can't find method ACL",
"rpc error making call: rpc: rpc error making call: rpc: rpc error making call: rpc: can't find method ACL",
].forEach(function(response) {
const e = createEmberDataError(response);
assert.ok(isValidServerError(e));
});
});
});

View File

@ -0,0 +1,15 @@
(function(appNameJS = 'consulUi', doc = document) {
const scripts = doc.getElementsByTagName('script');
const script = scripts[scripts.length - 1];
script.dataset[`${appNameJS}Routes`] = JSON.stringify({
dc: {
acls: {
tokens: {
_options: {
abilities: ['read tokens'],
},
},
},
},
});
})();

View File

@ -5102,6 +5102,11 @@ deep-is@^0.1.3, deep-is@~0.1.3:
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
deepmerge@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
defaults@^1.0.3: defaults@^1.0.3:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d"