ui: Logout button (#7604)
* ui: Logout button This commit adds an easier way to logout of the UI using a logout button Notes: - Added a Logout button to the main navigation when you are logged in, meaning you have easy access to a way to log out of the UI. - Changed all wording to use 'Log in/out' vocabulary instad of 'stop using'. - The logout button opens a panel to show you your current ACL token and a logout button in order to logout. - When using legacy ACLs we don't show the current ACL token as legacy ACLs tokens only have secret values, whereas the new ACLs use a non-secret ID plus a secret ID (that we don't show). - We also added a new `<EmptyState />` component to use for all our empty states. We currently only use this for the ACLs disabled screen to provide more outgoing links to more readind material/documentation to help you to understand and enable ACLs. - The `<DataSink />` component is the sibling to our `<DataSource />` component and whilst is much simpler (as it doesn't require polling support), its tries to use the same code patterns for consistencies sake. - We had a fun problem with ember-data's `store.unloadAll` here, and in the end went with `store.init` to empty the ember-data store instead due to timing issues. - We've tried to use already existing patterns in the Consul UI here such as our preexisting `feedback` service, although these are likely to change in the future. The thinking here is to add this feature with as little change as possible. Overall this is a precursor to a much larger piece of work centered on auth in the UI. We figured this was a feature complete piece of work as it is and thought it was worthwhile to PR as a feature on its own, which also means the larger piece of work will be a smaller scoped PR also.
This commit is contained in:
parent
17f10ffd0d
commit
e34c16a90c
|
@ -3,14 +3,45 @@
|
|||
<header>
|
||||
{{#each flashMessages.queue as |flash|}}
|
||||
<FlashMessage @flash={{flash}} as |component flash|>
|
||||
{{! flashes automatically ucfirst the type }}
|
||||
{{#let (lowercase component.flashType) (lowercase flash.action) as |status type|}}
|
||||
{{! flashes automatically ucfirst the type }}
|
||||
|
||||
<p data-notification class="{{lowercase component.flashType}} notification-{{lowercase flash.action}}">
|
||||
<strong>
|
||||
{{component.flashType}}!
|
||||
</strong>
|
||||
<YieldSlot @name="notification" @params={{block-params (lowercase component.flashType) (lowercase flash.action) flash.item}}>{{yield}}</YieldSlot>
|
||||
</p>
|
||||
<p data-notification class={{concat status ' notification-' type}}>
|
||||
<strong>
|
||||
{{capitalize status}}!
|
||||
</strong>
|
||||
{{#yield-slot name="notification" params=(block-params status type flash.item)}}
|
||||
{{yield}}
|
||||
{{#if (eq type 'logout')}}
|
||||
{{#if (eq status 'success') }}
|
||||
You are now logged out.
|
||||
{{else}}
|
||||
There was an error logging out.
|
||||
{{/if}}
|
||||
{{else if (eq type 'authorize')}}
|
||||
{{#if (eq status 'success') }}
|
||||
You are now logged in.
|
||||
{{else}}
|
||||
There was an error, please check your SecretID/Token
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if (eq type 'logout')}}
|
||||
{{#if (eq status 'success') }}
|
||||
You are now logged out.
|
||||
{{else}}
|
||||
There was an error logging out.
|
||||
{{/if}}
|
||||
{{else if (eq type 'authorize')}}
|
||||
{{#if (eq status 'success') }}
|
||||
You are now logged in.
|
||||
{{else}}
|
||||
There was an error, please check your SecretID/Token
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/yield-slot}}
|
||||
</p>
|
||||
{{/let}}
|
||||
</FlashMessage>
|
||||
{{/each}}
|
||||
<div>
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
## DataSink
|
||||
|
||||
```handlebars
|
||||
<DataSink
|
||||
@sink="/dc/nspace/intentions/{{intentions.uid}}"
|
||||
@onchange={{action (mut items) value="data"}}
|
||||
@onerror={{action (mut error) value="error"}}
|
||||
as |api|
|
||||
></DataSink>
|
||||
```
|
||||
|
||||
### Arguments
|
||||
|
||||
| Argument | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `sink` | `String` | | The location of the sink, this should map to a string based URI |
|
||||
| `data` | `Object` | | The data to be saved to the current instance, null or an empty string means remove |
|
||||
| `onchange` | `Function` | | The action to fire when the data has arrived to the sink. Emits an Event-like object with a `data` property containing the data, if the data was deleted this is `undefined`. |
|
||||
| `onerror` | `Function` | | The action to fire when an error occurs. Emits ErrorEvent object with an `error` property containing the Error. |
|
||||
|
||||
### Methods/Actions/api
|
||||
|
||||
| Method/Action | Description |
|
||||
| --- | --- |
|
||||
| `open` | Manually add or remove fom the data sink |
|
||||
|
||||
The component takes a `sink` or an identifier (a uri) for the location of a sink and then emits `onchange` events whenever that data has been arrived to the sink (whether persisted or removed). If an error occurs whilst listening for data changes, an `onerror` event is emitted.
|
||||
|
||||
Behind the scenes in the Consul UI we map URIs back to our `ember-data` backed `Repositories` meaning we can essentially redesign the URIs used for our data to more closely fit our needs. For example we currently require that **all** HTTP API URIs begin with `/dc/nspace/` values whether they require them or not.
|
||||
|
||||
`DataSink` is not just restricted to HTTP API data, and can be configured to listen for data changes using a variety of methods and sources. For example we have also configured `DataSink` to send data to `LocalStorage` using the `settings://` pseudo-protocol in the URI (See examples below).
|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
```handlebars
|
||||
<DataSink @src="/dc/nspace/intentions/{{intention.uid}}"
|
||||
@onchange={{action (mut item) value="data"}}
|
||||
@onerror={{action (mut error) value="error"}}
|
||||
as |api|
|
||||
>
|
||||
<button type="button" onclick={{action api.open (hash Name="New Name")}}>Create/Update</button>
|
||||
<button type="button" onclick={{action api.open null}}>Delete</button>
|
||||
</DataSink>
|
||||
{{item.Name}}
|
||||
```
|
||||
|
||||
```handlebars
|
||||
<DataSink @src="/dc/nspace/intentions/{{intention.uid}}"
|
||||
@data=(hash Name="New Name")
|
||||
@onchange={{action (mut item) value="data"}}
|
||||
@onerror={{action (mut error) value="error"}}
|
||||
></DataSink>
|
||||
{{item.Name}}
|
||||
```
|
||||
|
||||
### See
|
||||
|
||||
- [Component Source Code](./index.js)
|
||||
- [Template Source Code](./index.hbs)
|
||||
|
||||
---
|
|
@ -0,0 +1,4 @@
|
|||
{{yield (hash
|
||||
open=(action 'open')
|
||||
state=state
|
||||
)}}
|
|
@ -0,0 +1,105 @@
|
|||
import Component from '@ember/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { set, get, computed } from '@ember/object';
|
||||
|
||||
import { once } from 'consul-ui/utils/dom/event-source';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
|
||||
service: service('data-sink/service'),
|
||||
dom: service('dom'),
|
||||
logger: service('logger'),
|
||||
|
||||
onchange: function(e) {},
|
||||
onerror: function(e) {},
|
||||
|
||||
state: computed('instance', 'instance.{dirtyType,isSaving}', function() {
|
||||
let id;
|
||||
const isSaving = get(this, 'instance.isSaving');
|
||||
const dirtyType = get(this, 'instance.dirtyType');
|
||||
if (typeof isSaving === 'undefined' && typeof dirtyType === 'undefined') {
|
||||
id = 'idle';
|
||||
} else {
|
||||
switch (dirtyType) {
|
||||
case 'created':
|
||||
id = isSaving ? 'creating' : 'create';
|
||||
break;
|
||||
case 'updated':
|
||||
id = isSaving ? 'updating' : 'update';
|
||||
break;
|
||||
case 'deleted':
|
||||
case undefined:
|
||||
id = isSaving ? 'removing' : 'remove';
|
||||
break;
|
||||
}
|
||||
id = `active.${id}`;
|
||||
}
|
||||
return {
|
||||
matches: name => id.indexOf(name) !== -1,
|
||||
};
|
||||
}),
|
||||
|
||||
init: function() {
|
||||
this._super(...arguments);
|
||||
this._listeners = this.dom.listeners();
|
||||
},
|
||||
willDestroy: function() {
|
||||
this._super(...arguments);
|
||||
this._listeners.remove();
|
||||
},
|
||||
source: function(cb) {
|
||||
const source = once(cb);
|
||||
const error = err => {
|
||||
set(this, 'instance', undefined);
|
||||
try {
|
||||
this.onerror(err);
|
||||
this.logger.execute(err);
|
||||
} catch (err) {
|
||||
this.logger.execute(err);
|
||||
}
|
||||
};
|
||||
this._listeners.add(source, {
|
||||
message: e => {
|
||||
try {
|
||||
set(this, 'instance', undefined);
|
||||
this.onchange(e);
|
||||
} catch (err) {
|
||||
error(err);
|
||||
}
|
||||
},
|
||||
error: e => error(e),
|
||||
});
|
||||
return source;
|
||||
},
|
||||
didInsertElement: function() {
|
||||
this._super(...arguments);
|
||||
if (typeof this.data !== 'undefined') {
|
||||
this.actions.open.apply(this, [this.data]);
|
||||
}
|
||||
},
|
||||
persist: function(data, instance) {
|
||||
set(this, 'instance', this.service.prepare(this.sink, data, instance));
|
||||
this.source(() => this.service.persist(this.sink, this.instance));
|
||||
},
|
||||
remove: function(instance) {
|
||||
set(this, 'instance', this.service.prepare(this.sink, null, instance));
|
||||
this.source(() => this.service.remove(this.sink, this.instance));
|
||||
},
|
||||
actions: {
|
||||
open: function(data, instance) {
|
||||
if (instance instanceof Event) {
|
||||
instance = undefined;
|
||||
}
|
||||
if (typeof data === 'undefined') {
|
||||
throw new Error('You must specify data to save, or null to remove');
|
||||
}
|
||||
// potentially allow {} and "" as 'remove' flags
|
||||
if (data === null || data === '') {
|
||||
this.remove(instance);
|
||||
} else {
|
||||
this.persist(data, instance);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,21 @@
|
|||
{{yield}}
|
||||
<div class="empty-state" ...attributes>
|
||||
<header>
|
||||
{{#yield-slot name="header"}}
|
||||
{{yield}}
|
||||
{{/yield-slot}}
|
||||
{{#yield-slot name="subheader"}}
|
||||
{{yield}}
|
||||
{{/yield-slot}}
|
||||
</header>
|
||||
<p>
|
||||
{{#yield-slot name="body"}}
|
||||
{{yield}}
|
||||
{{/yield-slot}}
|
||||
</p>
|
||||
{{#yield-slot name="actions"}}
|
||||
<ul>
|
||||
{{yield}}
|
||||
</ul>
|
||||
{{/yield-slot}}
|
||||
</div>
|
|
@ -0,0 +1,6 @@
|
|||
import Component from '@ember/component';
|
||||
import Slotted from 'block-slots';
|
||||
|
||||
export default Component.extend(Slotted, {
|
||||
tagName: '',
|
||||
});
|
|
@ -111,6 +111,44 @@
|
|||
<li data-test-main-nav-settings class={{if (is-href 'settings') 'is-active'}}>
|
||||
<a href={{href-to 'settings'}}>Settings</a>
|
||||
</li>
|
||||
{{#if (env 'CONSUL_ACLS_ENABLED')}}
|
||||
<DataSource
|
||||
@src="settings://consul:token"
|
||||
@onchange={{action "changeToken" value="data"}}
|
||||
/>
|
||||
<DataSink
|
||||
@sink="settings://consul:token"
|
||||
as |tokenSink|>
|
||||
{{#if (not-eq token.AccessorID undefined)}}
|
||||
<li data-test-main-nav-auth>
|
||||
<PopoverMenu @position="right">
|
||||
<BlockSlot @name="trigger">
|
||||
Logout
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="menu">
|
||||
{{#if token.AccessorID}}
|
||||
<li role="none">
|
||||
<dl>
|
||||
<dt>
|
||||
<span>My ACL Token</span><br />
|
||||
AccessorID
|
||||
</dt>
|
||||
<dd>
|
||||
{{substr token.AccessorID -8}}
|
||||
</dd>
|
||||
</dl>
|
||||
</li>
|
||||
<li role="separator"></li>
|
||||
{{/if}}
|
||||
<li class="dangerous" role="none">
|
||||
<button type="button" tabindex="-1" role="menuitem" onclick={{action tokenSink.open null}}>Logout</button>
|
||||
</li>
|
||||
</BlockSlot>
|
||||
</PopoverMenu>
|
||||
</li>
|
||||
{{/if}}
|
||||
</DataSink>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
import Component from '@ember/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { computed } from '@ember/object';
|
||||
import { get, set, computed } from '@ember/object';
|
||||
import { getOwner } from '@ember/application';
|
||||
|
||||
export default Component.extend({
|
||||
dom: service('dom'),
|
||||
env: service('env'),
|
||||
feedback: service('feedback'),
|
||||
router: service('router'),
|
||||
http: service('repository/type/event-source'),
|
||||
client: service('client/http'),
|
||||
store: service('store'),
|
||||
settings: service('settings'),
|
||||
|
||||
didInsertElement: function() {
|
||||
this.dom.root().classList.remove('template-with-vertical-menu');
|
||||
},
|
||||
|
@ -16,7 +25,103 @@ export default Component.extend({
|
|||
}) !== 'undefined'
|
||||
);
|
||||
}),
|
||||
forwardForACL: function(token) {
|
||||
let routeName = this.router.currentRouteName;
|
||||
const route = getOwner(this).lookup(`route:${routeName}`);
|
||||
// a null AccessorID means we are in legacy mode
|
||||
// take the user to the legacy acls
|
||||
// otherwise just refresh the page
|
||||
if (get(token, 'AccessorID') === null) {
|
||||
// returning false for a feedback action means even though
|
||||
// its successful, please skip this notification and don't display it
|
||||
return route.transitionTo('dc.acls');
|
||||
} else {
|
||||
// TODO: Ideally we wouldn't need to use env() at a component level
|
||||
// transitionTo should probably remove it instead if NSPACES aren't enabled
|
||||
if (this.env.var('CONSUL_NSPACES_ENABLED') && get(token, 'Namespace') !== this.nspace) {
|
||||
if (!routeName.startsWith('nspace')) {
|
||||
routeName = `nspace.${routeName}`;
|
||||
}
|
||||
return route.transitionTo(`${routeName}`, `~${get(token, 'Namespace')}`, this.dc.Name);
|
||||
} else {
|
||||
if (route.routeName === 'dc.acls.index') {
|
||||
return route.transitionTo('dc.acls.tokens.index');
|
||||
}
|
||||
return route.refresh();
|
||||
}
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
send: function(el, method, ...rest) {
|
||||
const component = this.dom.component(el);
|
||||
component.actions[method].apply(component, rest || []);
|
||||
},
|
||||
changeToken: function(token = {}) {
|
||||
const prev = this.token;
|
||||
if (token === '') {
|
||||
token = {};
|
||||
}
|
||||
set(this, 'token', token);
|
||||
// if this is just the initial 'find out what the current token is'
|
||||
// then don't do anything
|
||||
if (typeof prev === 'undefined') {
|
||||
return;
|
||||
}
|
||||
let notification;
|
||||
let action = () => this.forwardForACL(token);
|
||||
switch (true) {
|
||||
case get(this, 'token.AccessorID') === null && get(this, 'token.SecretID') === null:
|
||||
// 'everything is null, 403 this needs deleting' token
|
||||
this.settings.delete('token');
|
||||
return;
|
||||
case get(prev, 'AccessorID') === null && get(prev, 'SecretID') === null:
|
||||
// we just had an 'everything is null, this needs deleting' token
|
||||
// reject and break so this acts differently to just logging out
|
||||
action = () => Promise.reject({});
|
||||
notification = 'authorize';
|
||||
break;
|
||||
case typeof get(prev, 'AccessorID') !== 'undefined' &&
|
||||
typeof get(this, 'token.AccessorID') !== 'undefined':
|
||||
// change of both Accessor and Secret, means use
|
||||
notification = 'use';
|
||||
break;
|
||||
case get(this, 'token.AccessorID') === null &&
|
||||
typeof get(this, 'token.SecretID') !== 'undefined':
|
||||
// legacy login, don't do anything as we don't use self for auth here but the endpoint itself
|
||||
// self is successful, but skip this notification and don't display it
|
||||
return this.forwardForACL(token);
|
||||
case typeof get(prev, 'AccessorID') === 'undefined' &&
|
||||
typeof get(this, 'token.AccessorID') !== 'undefined':
|
||||
// normal login
|
||||
notification = 'authorize';
|
||||
break;
|
||||
case (typeof get(prev, 'AccessorID') !== 'undefined' || get(prev, 'AccessorID') === null) &&
|
||||
typeof get(this, 'token.AccessorID') === 'undefined':
|
||||
//normal logout
|
||||
notification = 'logout';
|
||||
break;
|
||||
}
|
||||
this.actions.reauthorize.apply(this, [
|
||||
{
|
||||
type: notification,
|
||||
action: action,
|
||||
},
|
||||
]);
|
||||
},
|
||||
reauthorize: function(e) {
|
||||
this.client.abort();
|
||||
this.http.resetCache();
|
||||
this.store.init();
|
||||
const type = get(e, 'type');
|
||||
this.feedback.execute(
|
||||
e.action,
|
||||
type,
|
||||
function(type, e) {
|
||||
return type;
|
||||
},
|
||||
{}
|
||||
);
|
||||
},
|
||||
change: function(e) {
|
||||
const win = this.dom.viewport();
|
||||
const $root = this.dom.root();
|
||||
|
|
|
@ -7,34 +7,16 @@ export default Mixin.create(WithBlockingActions, {
|
|||
settings: service('settings'),
|
||||
actions: {
|
||||
use: function(item) {
|
||||
return this.feedback.execute(() => {
|
||||
// old style legacy ACLs don't have AccessorIDs or Namespaces
|
||||
// therefore set AccessorID to null, this way the frontend knows
|
||||
// to use legacy ACLs
|
||||
// set the Namespace to just use default
|
||||
return this.settings
|
||||
.persist({
|
||||
token: {
|
||||
Namespace: 'default',
|
||||
AccessorID: null,
|
||||
SecretID: get(item, 'ID'),
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
return this.transitionTo('dc.services');
|
||||
});
|
||||
}, 'use');
|
||||
return this.settings.persist({
|
||||
token: {
|
||||
Namespace: 'default',
|
||||
AccessorID: null,
|
||||
SecretID: get(item, 'ID'),
|
||||
},
|
||||
});
|
||||
},
|
||||
// TODO: This is also used in tokens, probably an opportunity to dry this out
|
||||
logout: function(item) {
|
||||
return this.feedback.execute(() => {
|
||||
return this.settings.delete('token').then(() => {
|
||||
// in this case we don't do the same as delete as we want to go to the new
|
||||
// dc.acls.tokens page. If we get there via the dc.acls redirect/rewrite
|
||||
// then we lose the flash message
|
||||
return this.transitionTo('dc.acls.tokens');
|
||||
});
|
||||
}, 'logout');
|
||||
return this.settings.delete('token');
|
||||
},
|
||||
clone: function(item) {
|
||||
return this.feedback.execute(() => {
|
||||
|
|
|
@ -7,40 +7,24 @@ export default Mixin.create(WithBlockingActions, {
|
|||
settings: service('settings'),
|
||||
actions: {
|
||||
use: function(item) {
|
||||
return this.feedback.execute(() => {
|
||||
return this.repo
|
||||
.findBySlug(
|
||||
get(item, 'AccessorID'),
|
||||
this.modelFor('dc').dc.Name,
|
||||
this.modelFor('nspace').nspace.substr(1)
|
||||
)
|
||||
.then(item => {
|
||||
return this.settings
|
||||
.persist({
|
||||
token: {
|
||||
AccessorID: get(item, 'AccessorID'),
|
||||
SecretID: get(item, 'SecretID'),
|
||||
Namespace: get(item, 'Namespace'),
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
// using is similar to delete in that
|
||||
// if you use from the listing page, stay on the listing page
|
||||
// whereas if you use from the detail page, take me back to the listing page
|
||||
return this.afterDelete(...arguments);
|
||||
});
|
||||
return this.repo
|
||||
.findBySlug(
|
||||
get(item, 'AccessorID'),
|
||||
this.modelFor('dc').dc.Name,
|
||||
this.modelFor('nspace').nspace.substr(1)
|
||||
)
|
||||
.then(item => {
|
||||
return this.settings.persist({
|
||||
token: {
|
||||
AccessorID: get(item, 'AccessorID'),
|
||||
SecretID: get(item, 'SecretID'),
|
||||
Namespace: get(item, 'Namespace'),
|
||||
},
|
||||
});
|
||||
}, 'use');
|
||||
});
|
||||
},
|
||||
logout: function(item) {
|
||||
return this.feedback.execute(() => {
|
||||
return this.settings.delete('token').then(() => {
|
||||
// logging out is similar to delete in that
|
||||
// if you log out from the listing page, stay on the listing page
|
||||
// whereas if you logout from the detail page, take me back to the listing page
|
||||
return this.afterDelete(...arguments);
|
||||
});
|
||||
}, 'logout');
|
||||
return this.settings.delete('token');
|
||||
},
|
||||
clone: function(item) {
|
||||
let cloned;
|
||||
|
|
|
@ -63,12 +63,18 @@ export default Route.extend(WithBlockingActions, {
|
|||
// 403 page
|
||||
// To note: Consul only gives you back a 403 if a non-existent token has been sent in the header
|
||||
// if a token has not been sent at all, it just gives you a 200 with an empty dataset
|
||||
// We set a completely null token here, which is different to just deleting a token
|
||||
// in that deleting a token means 'logout' whereas setting it to completely null means
|
||||
// there was a 403. This is only required to get around the legacy tokens
|
||||
// a lot of this can go once we don't support legacy tokens
|
||||
if (error.status === '403') {
|
||||
return this.feedback.execute(() => {
|
||||
return this.settings.delete('token').then(() => {
|
||||
return Promise.reject(this.transitionTo('dc.acls.tokens', model.dc.Name));
|
||||
});
|
||||
}, 'authorize');
|
||||
return this.settings.persist({
|
||||
token: {
|
||||
AccessorID: null,
|
||||
SecretID: null,
|
||||
Namespace: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (error.status === '') {
|
||||
error.message = 'Error';
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { get } from '@ember/object';
|
||||
import { env } from 'consul-ui/env';
|
||||
import { inject as service } from '@ember/service';
|
||||
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
|
||||
export default Route.extend(WithBlockingActions, {
|
||||
|
@ -11,42 +10,29 @@ export default Route.extend(WithBlockingActions, {
|
|||
actions: {
|
||||
authorize: function(secret, nspace) {
|
||||
const dc = this.modelFor('dc').dc.Name;
|
||||
return this.feedback.execute(() => {
|
||||
return this.repo.self(secret, dc).then(item => {
|
||||
return this.settings
|
||||
.persist({
|
||||
token: {
|
||||
Namespace: get(item, 'Namespace'),
|
||||
AccessorID: get(item, 'AccessorID'),
|
||||
SecretID: secret,
|
||||
},
|
||||
})
|
||||
.then(item => {
|
||||
// a null AccessorID means we are in legacy mode
|
||||
// take the user to the legacy acls
|
||||
// otherwise just refresh the page
|
||||
if (get(item, 'token.AccessorID') === null) {
|
||||
// returning false for a feedback action means even though
|
||||
// its successful, please skip this notification and don't display it
|
||||
return this.transitionTo('dc.acls').then(function() {
|
||||
return false;
|
||||
});
|
||||
} else {
|
||||
// TODO: Ideally we wouldn't need to use env() at a route level
|
||||
// transitionTo should probably remove it instead if NSPACES aren't enabled
|
||||
if (env('CONSUL_NSPACES_ENABLED') && get(item, 'token.Namespace') !== nspace) {
|
||||
let routeName = this.router.currentRouteName;
|
||||
if (!routeName.startsWith('nspace')) {
|
||||
routeName = `nspace.${routeName}`;
|
||||
}
|
||||
return this.transitionTo(`${routeName}`, `~${get(item, 'token.Namespace')}`, dc);
|
||||
} else {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
});
|
||||
return this.repo
|
||||
.self(secret, dc)
|
||||
.then(item => {
|
||||
return this.settings.persist({
|
||||
token: {
|
||||
Namespace: get(item, 'Namespace'),
|
||||
AccessorID: get(item, 'AccessorID'),
|
||||
SecretID: secret,
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
this.feedback.execute(
|
||||
() => {
|
||||
return Promise.resolve();
|
||||
},
|
||||
'authorize',
|
||||
function(type, e) {
|
||||
return 'error';
|
||||
},
|
||||
{}
|
||||
);
|
||||
});
|
||||
}, 'authorize');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import Service, { inject as service } from '@ember/service';
|
||||
import { setProperties } from '@ember/object';
|
||||
|
||||
export default Service.extend({
|
||||
settings: service('settings'),
|
||||
intention: service('repository/intention'),
|
||||
prepare: function(sink, data, instance) {
|
||||
const [, dc, nspace, model, slug] = sink.split('/');
|
||||
const repo = this[model];
|
||||
if (slug === '') {
|
||||
instance = repo.create({
|
||||
Datacenter: dc,
|
||||
Namespace: nspace,
|
||||
});
|
||||
} else {
|
||||
if (typeof instance === 'undefined') {
|
||||
instance = repo.peek(slug);
|
||||
}
|
||||
}
|
||||
setProperties(instance, data);
|
||||
return instance;
|
||||
},
|
||||
persist: function(sink, instance) {
|
||||
const [, , , /*dc*/ /*nspace*/ model] = sink.split('/');
|
||||
const repo = this[model];
|
||||
return repo.persist(instance);
|
||||
},
|
||||
remove: function(sink, instance) {
|
||||
const [, , , /*dc*/ /*nspace*/ model] = sink.split('/');
|
||||
const repo = this[model];
|
||||
return repo.remove(instance);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,25 @@
|
|||
import Service, { inject as service } from '@ember/service';
|
||||
import { setProperties } from '@ember/object';
|
||||
|
||||
export default Service.extend({
|
||||
settings: service('settings'),
|
||||
prepare: function(sink, data, instance) {
|
||||
if (data === null || data || '') {
|
||||
return instance;
|
||||
}
|
||||
setProperties(instance, data);
|
||||
return instance;
|
||||
},
|
||||
persist: function(sink, instance) {
|
||||
const slug = sink.split(':').pop();
|
||||
const repo = this.settings;
|
||||
return repo.persist({
|
||||
[slug]: instance,
|
||||
});
|
||||
},
|
||||
remove: function(sink, instance) {
|
||||
const slug = sink.split(':').pop();
|
||||
const repo = this.settings;
|
||||
return repo.delete(slug);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
import Service, { inject as service } from '@ember/service';
|
||||
|
||||
const parts = function(uri) {
|
||||
if (uri.indexOf('://') === -1) {
|
||||
uri = `data://${uri}`;
|
||||
}
|
||||
const url = new URL(uri);
|
||||
let pathname = url.pathname;
|
||||
if (pathname.startsWith('//')) {
|
||||
pathname = pathname.substr(2);
|
||||
}
|
||||
const providerName = url.protocol.substr(0, url.protocol.length - 1);
|
||||
return [providerName, pathname];
|
||||
};
|
||||
export default Service.extend({
|
||||
data: service('data-sink/protocols/http'),
|
||||
settings: service('data-sink/protocols/local-storage'),
|
||||
|
||||
prepare: function(uri, data, assign) {
|
||||
const [providerName, pathname] = parts(uri);
|
||||
const provider = this[providerName];
|
||||
return provider.prepare(pathname, data, assign);
|
||||
},
|
||||
persist: function(uri, data) {
|
||||
const [providerName, pathname] = parts(uri);
|
||||
const provider = this[providerName];
|
||||
return provider.persist(pathname, data);
|
||||
},
|
||||
remove: function(uri, data) {
|
||||
const [providerName, pathname] = parts(uri);
|
||||
const provider = this[providerName];
|
||||
return provider.remove(pathname, data);
|
||||
},
|
||||
});
|
|
@ -2,7 +2,12 @@ import Service from '@ember/service';
|
|||
import { env } from 'consul-ui/env';
|
||||
|
||||
export default Service.extend({
|
||||
// deprecated
|
||||
// TODO: Remove this elsewhere in the app and use var instead
|
||||
env: function(key) {
|
||||
return env(key);
|
||||
},
|
||||
var: function(key) {
|
||||
return env(key);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -83,6 +83,7 @@ const createProxy = function(repo, find, settings, cache, serialize = JSON.strin
|
|||
};
|
||||
};
|
||||
let cache = null;
|
||||
let cacheMap = null;
|
||||
export default LazyProxyService.extend({
|
||||
store: service('store'),
|
||||
settings: service('settings'),
|
||||
|
@ -91,10 +92,18 @@ export default LazyProxyService.extend({
|
|||
init: function() {
|
||||
this._super(...arguments);
|
||||
if (cache === null) {
|
||||
cache = createCache({});
|
||||
this.resetCache();
|
||||
}
|
||||
},
|
||||
resetCache: function() {
|
||||
Object.entries(cacheMap || {}).forEach(function([key, item]) {
|
||||
item.close();
|
||||
});
|
||||
cacheMap = {};
|
||||
cache = createCache(cacheMap);
|
||||
},
|
||||
willDestroy: function() {
|
||||
cacheMap = null;
|
||||
cache = null;
|
||||
},
|
||||
shouldProxy: function(content, method) {
|
||||
|
|
|
@ -23,6 +23,9 @@
|
|||
top: 0;
|
||||
left: calc(100% + 10px);
|
||||
}
|
||||
%menu-panel dl {
|
||||
padding: 0.9em 1em;
|
||||
}
|
||||
%menu-panel > ul > li > div[role='menu'] {
|
||||
@extend %menu-panel-sub-panel;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
border-color: $gray-300;
|
||||
background-color: $white;
|
||||
}
|
||||
%menu-panel dd {
|
||||
color: $gray-500;
|
||||
}
|
||||
%menu-panel > ul > li {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
|
|
@ -4,3 +4,18 @@
|
|||
@import '../confirmation-alert/index';
|
||||
@import './skin';
|
||||
@import './layout';
|
||||
%with-popover-menu > input {
|
||||
@extend %popover-menu;
|
||||
}
|
||||
%popover-menu {
|
||||
@extend %display-toggle-siblings;
|
||||
}
|
||||
%popover-menu + label + div {
|
||||
@extend %popover-menu-panel;
|
||||
}
|
||||
%popover-menu + label > * {
|
||||
@extend %toggle-button;
|
||||
}
|
||||
%popover-menu-panel {
|
||||
@extend %menu-panel;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
%with-popover-menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
%more-popover-menu {
|
||||
@extend %display-toggle-siblings;
|
||||
}
|
||||
|
|
|
@ -72,16 +72,12 @@ main {
|
|||
[role='tabpanel'] > p:only-child,
|
||||
.template-error > div,
|
||||
%app-view-content > p:only-child,
|
||||
%app-view > div.disabled > div,
|
||||
%app-view.empty > div {
|
||||
@extend %app-view-content-empty;
|
||||
}
|
||||
[role='tabpanel'] > *:first-child {
|
||||
margin-top: 1.25em;
|
||||
}
|
||||
%app-view > div.disabled > div {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
/* toggleable toolbar for short screens */
|
||||
[for='toolbar-toggle'] {
|
||||
|
|
|
@ -47,7 +47,6 @@
|
|||
}
|
||||
[role='tabpanel'] > p:only-child,
|
||||
.template-error > div,
|
||||
%app-view-content > p:only-child,
|
||||
%app-view > div.disabled > div {
|
||||
%app-view-content > p:only-child {
|
||||
@extend %frame-gray-500;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
@import './empty-state/index';
|
||||
.empty-state {
|
||||
@extend %empty-state;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
@import './skin';
|
||||
@import './layout';
|
||||
%empty-state header :first-child {
|
||||
@extend %empty-state-header;
|
||||
}
|
||||
%empty-state header :nth-child(2) {
|
||||
@extend %empty-state-subheader;
|
||||
}
|
||||
%empty-state > ul > li > *,
|
||||
%empty-state > ul > li > label > button {
|
||||
@extend %empty-state-anchor;
|
||||
}
|
||||
%empty-state > ul > li {
|
||||
@extend %with-popover-menu;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
%empty-state-header {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
%empty-state {
|
||||
width: 320px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
%empty-state-header {
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
%empty-state header {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
%empty-state > ul {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
margin-top: 1.5em;
|
||||
padding-top: 0.5em;
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
%empty-state-header {
|
||||
border-bottom: none;
|
||||
}
|
||||
%empty-state header {
|
||||
color: $gray-500;
|
||||
}
|
||||
%empty-state header::before {
|
||||
background-color: $gray-500;
|
||||
font-size: 2.6em;
|
||||
position: relative;
|
||||
top: -3px;
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
}
|
||||
%empty-state-anchor {
|
||||
@extend %anchor;
|
||||
}
|
||||
%empty-state-anchor::before {
|
||||
margin-right: 0.5em;
|
||||
background-color: $blue-500;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
%empty-state.unauthorized header::before {
|
||||
@extend %with-alert-circle-outline-mask, %as-pseudo;
|
||||
}
|
||||
%empty-state .docs-link > *::before {
|
||||
@extend %with-docs-mask, %as-pseudo;
|
||||
}
|
||||
%empty-state .learn-link > *::before {
|
||||
@extend %with-learn-mask, %as-pseudo;
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
@import './feedback-dialog/index';
|
||||
main .with-feedback {
|
||||
.with-feedback {
|
||||
@extend %feedback-dialog-inline;
|
||||
}
|
||||
%feedback-dialog-inline .feedback-dialog-out {
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
@import './sort-control';
|
||||
@import './discovery-chain';
|
||||
@import './consul-intention-list';
|
||||
@import './empty-state';
|
||||
|
||||
@import './tabular-details';
|
||||
@import './tabular-collection';
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
%main-nav-horizontal button {
|
||||
%main-nav-horizontal label > button {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
|
|
|
@ -44,6 +44,8 @@ pre code,
|
|||
%phrase-editor input {
|
||||
@extend %p1;
|
||||
}
|
||||
%menu-panel dl,
|
||||
%empty-state-anchor,
|
||||
.type-dialog,
|
||||
%table td p,
|
||||
%table td,
|
||||
|
@ -56,9 +58,9 @@ pre code,
|
|||
@extend %p2;
|
||||
}
|
||||
.template-error > div,
|
||||
%empty-state-subheader,
|
||||
%button,
|
||||
%main-content p,
|
||||
%app-view > div.disabled > div,
|
||||
%form-element-note,
|
||||
%menu-panel-separator,
|
||||
%form-element-error > strong {
|
||||
|
@ -77,6 +79,7 @@ pre code,
|
|||
%button {
|
||||
font-weight: $typo-weight-semibold;
|
||||
}
|
||||
%menu-panel dt,
|
||||
%route-card section dt,
|
||||
%route-card header:not(.short) dd,
|
||||
%splitter-card > header {
|
||||
|
@ -88,6 +91,8 @@ pre code,
|
|||
/**/
|
||||
|
||||
/* resets */
|
||||
%menu-panel dt span,
|
||||
%empty-state-subheader,
|
||||
%main-content label a[rel*='help'],
|
||||
%pill,
|
||||
%form-element > strong,
|
||||
|
|
|
@ -18,9 +18,9 @@
|
|||
.template-token.template-list main .notice {
|
||||
margin-top: -20px;
|
||||
}
|
||||
.template-token.template-edit dd {
|
||||
.template-token.template-edit main dd {
|
||||
display: flex;
|
||||
}
|
||||
.template-token.template-edit dl {
|
||||
.template-token.template-edit main dl {
|
||||
@extend %form-row;
|
||||
}
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
<fieldset>
|
||||
<label class="type-text">
|
||||
<span>SecretID or Token</span>
|
||||
<Input @type="password" @value={{secret}} @name="secret" placeholder="SecretID or Token" />
|
||||
<input type="password" name="secret" placeholder="SecretID or Token" oninput={{action (mut secret) value="target.value"}} />
|
||||
</label>
|
||||
</fieldset>
|
||||
{{! authorize is in the routes/dc/acls.js route }}
|
||||
<button type="submit" {{action 'authorize' secret nspace}}>Save</button>
|
||||
<button type="submit" {{action 'authorize' secret nspace}}>Login</button>
|
||||
</form>
|
||||
|
|
|
@ -1,6 +1,18 @@
|
|||
<p>
|
||||
ACLs are disabled in this Consul cluster. This is the default behavior, as you have to explicitly enable them.
|
||||
</p>
|
||||
<p>
|
||||
Learn more in the <a href="{{ env 'CONSUL_DOCS_URL'}}/guides/acl.html" rel="noopener noreferrer" target="_blank">ACL documentation</a>
|
||||
</p>
|
||||
<EmptyState>
|
||||
<BlockSlot @name="header">
|
||||
<h2>Welcome to ACLs</h2>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="body">
|
||||
<p>
|
||||
ACLs are not enabled. We strongly encourage the use of ACLs in production environments for the best security practices.
|
||||
</p>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="actions">
|
||||
<li class="docs-link">
|
||||
<a href="{{env 'CONSUL_DOCS_URL'}}/acl/index.html" rel="noopener noreferrer" target="_blank">Read the documentation</a>
|
||||
</li>
|
||||
<li class="learn-link">
|
||||
<a href="{{env 'CONSUL_DOCS_LEARN_URL'}}/consul/security-networking/production-acls" rel="noopener noreferrer" target="_blank">Follow the guide</a>
|
||||
</li>
|
||||
</BlockSlot>
|
||||
</EmptyState>
|
||||
|
|
|
@ -16,12 +16,6 @@
|
|||
{{else}}
|
||||
There was an error deleting your ACL token.
|
||||
{{/if}}
|
||||
{{ else if (eq type 'logout')}}
|
||||
{{#if (eq status 'success') }}
|
||||
You are now logged out.
|
||||
{{else}}
|
||||
There was an error logging out.
|
||||
{{/if}}
|
||||
{{ else if (eq type 'use')}}
|
||||
{{#if (eq status 'success') }}
|
||||
Now using new ACL token.
|
||||
|
|
|
@ -7,7 +7,12 @@
|
|||
{{else}}
|
||||
{{title 'Access Controls'}}
|
||||
{{/if}}
|
||||
<AppView @class={{concat "policy " (if (or isAuthorized isEnabled) "edit" "list")}} @loading={{isLoading}} @authorized={{isAuthorized}} @enabled={{isEnabled}}>
|
||||
<AppView
|
||||
@class={{concat "policy " (if isAuthorized "edit" "list")}}
|
||||
@loading={{isLoading}}
|
||||
@authorized={{isAuthorized}}
|
||||
@enabled={{isEnabled}}
|
||||
>
|
||||
<BlockSlot @name="notification" as |status type|>
|
||||
{{partial 'dc/acls/policies/notifications'}}
|
||||
</BlockSlot>
|
||||
|
|
|
@ -3,7 +3,12 @@
|
|||
{{else}}
|
||||
{{title 'Access Controls'}}
|
||||
{{/if}}
|
||||
<AppView @class={{concat "policy " (if (not isAuthorized) "edit" "list")}} @loading={{isLoading}} @authorized={{isAuthorized}} @enabled={{isEnabled}}>
|
||||
<AppView
|
||||
@class="policy list"
|
||||
@loading={{isLoading}}
|
||||
@authorized={{isAuthorized}}
|
||||
@enabled={{isEnabled}}
|
||||
>
|
||||
<BlockSlot @name="notification" as |status type|>
|
||||
{{partial 'dc/acls/policies/notifications'}}
|
||||
</BlockSlot>
|
||||
|
|
|
@ -7,7 +7,12 @@
|
|||
{{else}}
|
||||
{{title 'Access Controls'}}
|
||||
{{/if}}
|
||||
<AppView @class={{concat "role " (if (or isAuthorized isEnabled) "edit" "list")}} @loading={{isLoading}} @authorized={{isAuthorized}} @enabled={{isEnabled}}>
|
||||
<AppView
|
||||
@class={{concat "role " (if isAuthorized "edit" "list")}}
|
||||
@loading={{isLoading}}
|
||||
@authorized={{isAuthorized}}
|
||||
@enabled={{isEnabled}}
|
||||
>
|
||||
<BlockSlot @name="notification" as |status type|>
|
||||
{{partial 'dc/acls/roles/notifications'}}
|
||||
</BlockSlot>
|
||||
|
|
|
@ -3,7 +3,12 @@
|
|||
{{else}}
|
||||
{{title 'Access Controls'}}
|
||||
{{/if}}
|
||||
<AppView @class={{concat "role " (if (not isAuthorized) "edit" "list")}} @loading={{isLoading}} @authorized={{isAuthorized}} @enabled={{isEnabled}}>
|
||||
<AppView
|
||||
@class="role list"
|
||||
@loading={{isLoading}}
|
||||
@authorized={{isAuthorized}}
|
||||
@enabled={{isEnabled}}
|
||||
>
|
||||
<BlockSlot @name="notification" as |status type|>
|
||||
{{partial 'dc/acls/roles/notifications'}}
|
||||
</BlockSlot>
|
||||
|
|
|
@ -16,24 +16,12 @@
|
|||
{{else}}
|
||||
There was an error deleting the token.
|
||||
{{/if}}
|
||||
{{ else if (eq type 'logout')}}
|
||||
{{#if (eq status 'success') }}
|
||||
You are now logged out.
|
||||
{{else}}
|
||||
There was an error logging out.
|
||||
{{/if}}
|
||||
{{ else if (eq type 'clone')}}
|
||||
{{#if (eq status 'success') }}
|
||||
The token has been cloned as {{truncate subject.AccessorID 8 false}}
|
||||
{{else}}
|
||||
There was an error cloning the token.
|
||||
{{/if}}
|
||||
{{ else if (eq type 'authorize')}}
|
||||
{{#if (eq status 'success') }}
|
||||
You are now logged in.
|
||||
{{else}}
|
||||
There was an error, please check your SecretID/Token
|
||||
{{/if}}
|
||||
{{ else if (eq type 'use')}}
|
||||
{{#if (eq status 'success') }}
|
||||
You are now using the new ACL token
|
||||
|
|
|
@ -7,7 +7,12 @@
|
|||
{{else}}
|
||||
{{title 'Access Controls'}}
|
||||
{{/if}}
|
||||
<AppView @class={{concat "token " (if (or isAuthorized isEnabled) "edit" "list")}} @loading={{isLoading}} @authorized={{isAuthorized}} @enabled={{isEnabled}}>
|
||||
<AppView
|
||||
@class={{concat "token " (if isAuthorized "edit" "list")}}
|
||||
@loading={{isLoading}}
|
||||
@authorized={{isAuthorized}}
|
||||
@enabled={{isEnabled}}
|
||||
>
|
||||
<BlockSlot @name="notification" as |status type|>
|
||||
{{partial 'dc/acls/tokens/notifications'}}
|
||||
</BlockSlot>
|
||||
|
|
|
@ -3,7 +3,12 @@
|
|||
{{else}}
|
||||
{{title 'Access Controls'}}
|
||||
{{/if}}
|
||||
<AppView @class={{concat "token " (if (and isEnabled (not isAuthorized)) "edit" "list")}} @loading={{isLoading}} @authorized={{isAuthorized}} @enabled={{isEnabled}}>
|
||||
<AppView
|
||||
@class="token list"
|
||||
@loading={{isLoading}}
|
||||
@authorized={{isAuthorized}}
|
||||
@enabled={{isEnabled}}
|
||||
>
|
||||
<BlockSlot @name="notification" as |status type subject|>
|
||||
{{partial 'dc/acls/tokens/notifications'}}
|
||||
</BlockSlot>
|
||||
|
@ -81,8 +86,8 @@
|
|||
</li>
|
||||
{{/if}}
|
||||
{{#if (eq item.AccessorID token.AccessorID) }}
|
||||
<li role="none">
|
||||
<label for={{concat confirm 'logout'}} role="menuitem" tabindex="-1" onkeypress={{keypressClick}} data-test-logout>Stop using</label>
|
||||
<li role="none" class="dangerous">
|
||||
<label for={{concat confirm 'logout'}} role="menuitem" tabindex="-1" onkeypress={{keypressClick}} data-test-logout>Log out</label>
|
||||
<div role="menu">
|
||||
<div class="confirmation-alert warning">
|
||||
<div>
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
{{title 'Services'}}
|
||||
<AppView @class="service list">
|
||||
<BlockSlot @name="notification" as |status type|>
|
||||
{{partial 'dc/services/notifications'}}
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="header">
|
||||
<h1>
|
||||
Services <em>{{format-number items.length}} total</em>
|
||||
|
|
|
@ -8,9 +8,12 @@ Feature: dc / acls / tokens / own-no-delete: The your current token has no delet
|
|||
SecretID: ee52203d-989f-4f7a-ab5a-2bef004164ca
|
||||
---
|
||||
Scenario: On the listing page
|
||||
Then I have settings like yaml
|
||||
Given settings from yaml
|
||||
---
|
||||
consul:token: ~
|
||||
consul:token:
|
||||
SecretID: secret
|
||||
AccessorID: accessor
|
||||
Namespace: default
|
||||
---
|
||||
When I visit the tokens page for yaml
|
||||
---
|
||||
|
|
|
@ -7,15 +7,18 @@ Feature: dc / acls / tokens / use: Using an ACL token
|
|||
AccessorID: token
|
||||
SecretID: ee52203d-989f-4f7a-ab5a-2bef004164ca
|
||||
---
|
||||
And settings from yaml
|
||||
---
|
||||
consul:token:
|
||||
SecretID: secret
|
||||
AccessorID: accessor
|
||||
Namespace: default
|
||||
---
|
||||
Scenario: Using an ACL token from the listing page
|
||||
When I visit the tokens page for yaml
|
||||
---
|
||||
dc: datacenter
|
||||
---
|
||||
Then I have settings like yaml
|
||||
---
|
||||
consul:token: ~
|
||||
---
|
||||
And I click actions on the tokens
|
||||
And I click use on the tokens
|
||||
And I click confirmUse on the tokens
|
||||
|
@ -31,10 +34,6 @@ Feature: dc / acls / tokens / use: Using an ACL token
|
|||
dc: datacenter
|
||||
token: token
|
||||
---
|
||||
Then I have settings like yaml
|
||||
---
|
||||
consul:token: ~
|
||||
---
|
||||
And I click use
|
||||
And I click confirmUse
|
||||
Then "[data-notification]" has the "notification-use" class
|
||||
|
|
|
@ -51,12 +51,15 @@ const mb = function(path) {
|
|||
};
|
||||
export default function(assert, library) {
|
||||
const pauseUntil = function(run, message = 'assertion timed out') {
|
||||
return new Promise(function(r, reject) {
|
||||
return new Promise(function(r) {
|
||||
let count = 0;
|
||||
let resolved = false;
|
||||
const retry = function() {
|
||||
return Promise.resolve();
|
||||
};
|
||||
const reject = function() {
|
||||
return Promise.reject();
|
||||
};
|
||||
const resolve = function(str = message) {
|
||||
resolved = true;
|
||||
assert.ok(resolved, str);
|
||||
|
|
|
@ -22,36 +22,24 @@ module('Unit | Mixin | acl/with actions', function(hooks) {
|
|||
const subject = this.subject();
|
||||
assert.ok(subject);
|
||||
});
|
||||
test('use persists the token and calls transitionTo correctly', function(assert) {
|
||||
assert.expect(4);
|
||||
this.owner.register(
|
||||
'service:feedback',
|
||||
Service.extend({
|
||||
execute: function(cb, name) {
|
||||
assert.equal(name, 'use');
|
||||
return cb();
|
||||
},
|
||||
})
|
||||
);
|
||||
test('use persists the token', function(assert) {
|
||||
assert.expect(2);
|
||||
const item = { ID: 'id' };
|
||||
const expectedToken = { Namespace: 'default', AccessorID: null, SecretID: item.ID };
|
||||
const expected = { Namespace: 'default', AccessorID: null, SecretID: item.ID };
|
||||
this.owner.register(
|
||||
'service:settings',
|
||||
Service.extend({
|
||||
persist: function(actual) {
|
||||
assert.deepEqual(actual.token, expectedToken);
|
||||
assert.deepEqual(actual.token, expected);
|
||||
return Promise.resolve(actual);
|
||||
},
|
||||
})
|
||||
);
|
||||
const subject = this.subject();
|
||||
const expected = 'dc.services';
|
||||
const transitionTo = this.stub(subject, 'transitionTo').returnsArg(0);
|
||||
return subject.actions.use
|
||||
.bind(subject)(item)
|
||||
.then(function(actual) {
|
||||
assert.ok(transitionTo.calledOnce);
|
||||
assert.equal(actual, expected);
|
||||
assert.deepEqual(actual.token, expected);
|
||||
});
|
||||
});
|
||||
test('clone clones the token and calls afterDelete correctly', function(assert) {
|
||||
|
|
Loading…
Reference in New Issue