UI - unauthed login methods (#4854)

* fetch auth methods when going to the auth route and pass them to the auth form component

* add boolean editType for form-fields

* look in the data hash in the serializer

* remove renderInPlace for info-tooltips as it does something goofy with widths

* add new fields for auth methods

* fix console refresh command on routes that use lazyPaginatedQuery

* add wrapped_token param that logs you in via the token backend and show other backends if your list contains supported ones

* handle casing when looking up supported backends

* change listingVisibility to match the new API

* move wrapped_token up to the vault route level so it works from the app root
This commit is contained in:
Matthew Irish 2018-07-05 13:28:12 -05:00 committed by GitHub
parent 32c94b2638
commit f38a50b6b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 449 additions and 130 deletions

View File

@ -24,7 +24,7 @@ export default DS.RESTAdapter.extend({
},
_preRequest(url, options) {
const token = this.get('auth.currentToken');
const token = options.clientToken || this.get('auth.currentToken');
if (token && !options.unauthenticated) {
options.headers = Ember.assign(options.headers || {}, {
'X-Vault-Token': token,

View File

@ -13,7 +13,22 @@ export default ApplicationAdapter.extend({
return 'mounts/auth';
},
findAll() {
findAll(store, type, sinceToken, snapshotRecordArray) {
let isUnauthenticated = Ember.get(snapshotRecordArray || {}, 'adapterOptions.unauthenticated');
if (isUnauthenticated) {
let url = `/${this.urlPrefix()}/internal/ui/mounts`;
return this.ajax(url, 'GET', {
unauthenticated: true,
})
.then(result => {
return {
data: result.data.auth,
};
})
.catch(() => {
return [];
});
}
return this.ajax(this.url(), 'GET').catch(e => {
if (e instanceof DS.AdapterError) {
Ember.set(e, 'policyPath', 'sys/auth');

View File

@ -15,9 +15,9 @@ export default ApplicationAdapter.extend({
},
toolAction(action, data, options = {}) {
const { wrapTTL } = options;
const { wrapTTL, clientToken } = options;
const url = this.toolUrlFor(action);
const ajaxOptions = wrapTTL ? { data, wrapTTL } : { data };
const ajaxOptions = wrapTTL ? { data, wrapTTL, clientToken } : { data, clientToken };
return this.ajax(url, 'POST', ajaxOptions);
},
});

View File

@ -1,5 +1,6 @@
import Ember from 'ember';
import { supportedAuthBackends } from 'vault/helpers/supported-auth-backends';
import { task } from 'ember-concurrency';
const BACKENDS = supportedAuthBackends();
const { computed, inject, get } = Ember;
@ -11,57 +12,125 @@ const DEFAULTS = {
export default Ember.Component.extend(DEFAULTS, {
classNames: ['auth-form'],
routing: inject.service('-routing'),
router: inject.service(),
auth: inject.service(),
flashMessages: inject.service(),
store: inject.service(),
csp: inject.service('csp-event'),
// set during init and potentially passed in via a query param
selectedAuth: null,
methods: null,
cluster: null,
redirectTo: null,
didRender() {
this._super(...arguments);
// on very narrow viewports the active tab may be overflowed, so we scroll it into view here
this.$('li.is-active').get(0).scrollIntoView();
let activeEle = this.element.querySelector('li.is-active');
if (activeEle) {
activeEle.scrollIntoView();
}
// this is here because we're changing the `with` attr and there's no way to short-circuit rendering,
// so we'll just nav -> get new attrs -> re-render
if (!this.get('selectedAuth') || (this.get('selectedAuth') && !this.get('selectedAuthBackend'))) {
this.get('router').replaceWith('vault.cluster.auth', this.get('cluster.name'), {
queryParams: {
with: this.firstMethod(),
wrappedToken: this.get('wrappedToken'),
},
});
}
},
firstMethod() {
let firstMethod = this.get('methodsToShow.firstObject');
// prefer backends with a path over those with a type
return get(firstMethod, 'path') || get(firstMethod, 'type');
},
didReceiveAttrs() {
this._super(...arguments);
let newMethod = this.get('selectedAuthType');
let oldMethod = this.get('oldSelectedAuthType');
let token = this.get('wrappedToken');
let newMethod = this.get('selectedAuth');
let oldMethod = this.get('oldSelectedAuth');
if (oldMethod && oldMethod !== newMethod) {
this.resetDefaults();
}
this.set('oldSelectedAuthType', newMethod);
this.set('oldSelectedAuth', newMethod);
if (token) {
this.get('unwrapToken').perform(token);
}
},
resetDefaults() {
this.setProperties(DEFAULTS);
},
cluster: null,
redirectTo: null,
selectedAuthIsPath: computed.match('selectedAuth', /\/$/),
selectedAuthBackend: Ember.computed(
'allSupportedMethods',
'selectedAuth',
'selectedAuthIsPath',
function() {
let methods = this.get('allSupportedMethods');
let keyIsPath = this.get('selectedAuthIsPath');
let findKey = keyIsPath ? 'path' : 'type';
return methods.findBy(findKey, this.get('selectedAuth'));
}
),
selectedAuthType: 'token',
selectedAuthBackend: Ember.computed('selectedAuthType', function() {
return BACKENDS.findBy('type', this.get('selectedAuthType'));
}),
providerPartialName: Ember.computed('selectedAuthType', function() {
const type = Ember.String.dasherize(this.get('selectedAuthType'));
return `partials/auth-form/${type}`;
providerPartialName: computed('selectedAuthBackend', function() {
let type = this.get('selectedAuthBackend.type') || 'token';
type = type.toLowerCase();
let templateName = Ember.String.dasherize(type);
return `partials/auth-form/${templateName}`;
}),
hasCSPError: computed.alias('csp.connectionViolations.firstObject'),
cspErrorText: `This is a standby Vault node but can't communicate with the active node via request forwarding. Sign in at the active node to use the Vault UI.`,
allSupportedMethods: computed('methodsToShow', 'hasMethodsWithPath', function() {
let hasMethodsWithPath = this.get('hasMethodsWithPath');
let methodsToShow = this.get('methodsToShow');
return hasMethodsWithPath ? methodsToShow.concat(BACKENDS) : methodsToShow;
}),
hasMethodsWithPath: computed('methodsToShow', function() {
return this.get('methodsToShow').isAny('path');
}),
methodsToShow: computed('methods', 'methods.[]', function() {
let methods = this.get('methods') || [];
let shownMethods = methods.filter(m =>
BACKENDS.find(b => get(b, 'type').toLowerCase() === get(m, 'type').toLowerCase())
);
return shownMethods.length ? shownMethods : BACKENDS;
}),
unwrapToken: task(function*(token) {
// will be using the token auth method, so set it here
this.set('selectedAuth', 'token');
let adapter = this.get('store').adapterFor('tools');
try {
let response = yield adapter.toolAction('unwrap', null, { clientToken: token });
this.set('token', response.auth.client_token);
this.send('doSubmit');
} catch (e) {
this.set('error', `Token unwrap failed: ${e.errors[0]}`);
}
}),
handleError(e) {
this.set('loading', false);
let errors = e.errors.map(error => {
if (error.detail) {
return error.detail;
}
return error;
});
this.set('error', `Authentication failed: ${errors.join('.')}`);
},
@ -73,19 +142,22 @@ export default Ember.Component.extend(DEFAULTS, {
error: null,
});
let targetRoute = this.get('redirectTo') || 'vault.cluster';
let backend = this.get('selectedAuthBackend');
let path = this.get('customPath');
let attributes = get(backend, 'formAttributes');
let backend = this.get('selectedAuthBackend') || {};
let path = get(backend, 'path') || this.get('customPath');
let backendMeta = BACKENDS.find(
b => get(b, 'type').toLowerCase() === get(backend, 'type').toLowerCase()
);
let attributes = get(backendMeta, 'formAttributes');
data = Ember.assign(data, this.getProperties(...attributes));
if (this.get('useCustomPath') && path) {
if (get(backend, 'path') || (this.get('useCustomPath') && path)) {
data.path = path;
}
const clusterId = this.get('cluster.id');
this.get('auth').authenticate({ clusterId, backend: get(backend, 'type'), data }).then(
({ isRoot }) => {
this.set('loading', false);
const transition = this.get('routing.router').transitionTo(targetRoute);
const transition = this.get('router').transitionTo(targetRoute);
if (isRoot) {
transition.followRedirects().then(() => {
this.get('flashMessages').warning(

View File

@ -17,6 +17,7 @@ export default Ember.Component.extend({
isFullscreen: false,
console: inject.service(),
router: inject.service(),
store: inject.service(),
inputValue: null,
log: computed.alias('console.log'),
@ -86,6 +87,7 @@ export default Ember.Component.extend({
let route = owner.lookup(`route:${routeName}`);
try {
this.get('store').clearAllDatasets();
yield route.refresh();
this.logAndOutput(null, { type: 'success', content: 'The current screen has been refreshed!' });
} catch (error) {

View File

@ -97,6 +97,11 @@ export default Ember.Component.extend({
this.get('onChange')(path, value);
},
setAndBroadcastBool(path, trueVal, falseVal, value) {
let valueToSet = value === true ? trueVal : falseVal;
this.send('setAndBroadcast', path, valueToSet);
},
codemirrorUpdated(path, value, codemirror) {
codemirror.performLint();
const hasErrors = codemirror.state.lint.marked.length > 0;

View File

@ -0,0 +1,10 @@
import Ember from 'ember';
export default Ember.Controller.extend({
queryParams: [
{
wrappedToken: 'wrapped_token',
},
],
wrappedToken: '',
});

View File

@ -1,11 +1,9 @@
import Ember from 'ember';
import { supportedAuthBackends } from 'vault/helpers/supported-auth-backends';
export default Ember.Controller.extend({
queryParams: ['with'],
with: Ember.computed(function() {
return supportedAuthBackends()[0].type;
}),
vaultController: Ember.inject.controller('vault'),
queryParams: [{ authMethod: 'with' }],
wrappedToken: Ember.computed.alias('vaultController.wrappedToken'),
authMethod: '',
redirectTo: null,
});

View File

@ -3,11 +3,11 @@ import Ember from 'ember';
const { Helper, inject } = Ember;
export default Helper.extend({
routing: inject.service('-routing'),
router: inject.service(),
compute([routeName, ...models], { replace = false }) {
return () => {
const router = this.get('routing.router');
const router = this.get('router');
const method = replace ? router.replaceWith : router.transitionTo;
return method.call(router, routeName, ...models);
};

View File

@ -44,7 +44,10 @@ export default DS.Model.extend({
}),
tuneAttrs: computed(function() {
return expandAttributeMeta(this, ['description', 'config.{defaultLeaseTtl,maxLeaseTtl}']);
return expandAttributeMeta(this, [
'description',
'config.{listingVisibility,defaultLeaseTtl,maxLeaseTtl,auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders}',
]);
}),
//sys/mounts/auth/[auth-path]/tune.
@ -61,12 +64,20 @@ export default DS.Model.extend({
'accessor',
'local',
'sealWrap',
'config.{defaultLeaseTtl,maxLeaseTtl}',
'config.{listingVisibility,defaultLeaseTtl,maxLeaseTtl,auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders}',
],
formFieldGroups: [
{ default: ['type', 'path'] },
{ 'Method Options': ['description', 'local', 'sealWrap', 'config.{defaultLeaseTtl,maxLeaseTtl}'] },
{
'Method Options': [
'description',
'config.listingVisibility',
'local',
'sealWrap',
'config.{defaultLeaseTtl,maxLeaseTtl,auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders}',
],
},
],
attrs: computed('formFields', function() {

View File

@ -10,4 +10,25 @@ export default Fragment.extend({
label: 'Max Lease TTL',
editType: 'ttl',
}),
auditNonHmacRequestKeys: attr({
label: 'Request keys excluded from HMACing in audit',
editType: 'stringArray',
helpText: "Keys that will not be HMAC'd by audit devices in the request data object.",
}),
auditNonHmacResponseKeys: attr({
label: 'Response keys excluded from HMACing in audit',
editType: 'stringArray',
helpText: "Keys that will not be HMAC'd by audit devices in the response data object.",
}),
listingVisibility: attr('string', {
editType: 'boolean',
label: 'List method when unauthenticated',
trueValue: 'unauth',
falseValue: 'hidden',
}),
passthroughRequestHeaders: attr({
label: 'Allowed passthrough request headers',
helpText: 'Headers to whitelist and pass from the request to the backend',
editType: 'stringArray',
}),
});

View File

@ -1 +1,29 @@
export { default } from './cluster-route-base';
import ClusterRouteBase from './cluster-route-base';
import Ember from 'ember';
const { RSVP } = Ember;
export default ClusterRouteBase.extend({
beforeModel() {
return this.store.unloadAll('auth-method');
},
model() {
let cluster = this._super(...arguments);
return this.store
.findAll('auth-method', {
adapterOptions: {
unauthenticated: true,
},
})
.then(result => {
return RSVP.hash({
cluster,
methods: result,
});
});
},
resetController(controller) {
controller.set('wrappedToken', '');
controller.set('authMethod', '');
},
});

View File

@ -2,10 +2,7 @@ import ApplicationSerializer from './application';
export default ApplicationSerializer.extend({
normalizeBackend(path, backend) {
let struct = {};
for (let attribute in backend) {
struct[attribute] = backend[attribute];
}
let struct = { ...backend };
// strip the trailing slash off of the path so we
// can navigate to it without getting `//` in the url
struct.id = path.slice(0, -1);
@ -17,7 +14,7 @@ export default ApplicationSerializer.extend({
const isCreate = requestType === 'createRecord';
const backends = isCreate
? payload.data
: Object.keys(payload.data).map(id => this.normalizeBackend(id, payload[id]));
: Object.keys(payload.data).map(path => this.normalizeBackend(path, payload.data[path]));
return this._super(store, primaryModelClass, backends, id, requestType);
},

View File

@ -29,6 +29,7 @@
background: none;
color: inherit;
font-size: $body-size;
min-height: 2rem;
&:not(.console-ui-command):not(.CodeMirror-line) {
padding-left: $console-spacing;
@ -57,6 +58,13 @@
}
}
.console-ui-panel .hover-copy-button,
.console-ui-panel .hover-copy-button-static {
top: auto;
bottom: 0;
right: 0;
}
.console-ui-input {
align-items: center;
display: flex;
@ -82,25 +90,13 @@
}
.console-ui-output {
transition: background-color $speed;
transition: background-color $speed ease-in-out;
will-change: background-color;
padding-right: $size-2;
position: relative;
.console-ui-output-actions {
opacity: 0;
position: absolute;
right: 0;
top: 0;
transition: opacity $speed;
will-change: opacity;
}
background-color: rgba(#000, 0);
&:hover {
background: rgba($black, 0.25);
.console-ui-output-actions {
opacity: 1;
}
background-color: rgba(#000, 0.5);
}
}

View File

@ -1,12 +1,29 @@
<nav class="tabs sub-nav is-marginless">
<ul>
{{#each (supported-auth-backends) as |backend|}}
<li class="{{if (eq selectedAuthBackend.type backend.type) 'is-active' ''}}" data-test-auth-method>
<a href="{{href-to 'vault.cluster.auth' cluster.name (query-params with=backend.type)}}" data-test-auth-method-link={{backend.type}}>
{{capitalize backend.type}}
{{#each methodsToShow as |method|}}
{{#with (or method.path method.type) as |methodKey|}}
{{#if hasMethodsWithPath}}
<li class="{{if (and selectedAuthIsPath (eq (or selectedAuthBackend.path selectedAuthBackend.type) methodKey)) 'is-active' ''}}" data-test-auth-method>
<a href="{{href-to 'vault.cluster.auth' cluster.name (query-params with=methodKey)}}" data-test-auth-method-link={{method.type}}>
{{or method.id (capitalize method.type)}}
</a>
</li>
{{else}}
<li class="{{if (eq (or selectedAuthBackend.path selectedAuthBackend.type) methodKey) 'is-active' ''}}" data-test-auth-method>
<a href="{{href-to 'vault.cluster.auth' cluster.name (query-params with=methodKey)}}" data-test-auth-method-link={{method.type}}>
{{or method.id (capitalize method.type)}}
</a>
</li>
{{/if}}
{{/with}}
{{/each}}
{{#if hasMethodsWithPath}}
<li class="{{if (not selectedAuthIsPath) 'is-active' ''}}" data-test-auth-method>
<a href="{{href-to 'vault.cluster.auth' cluster.name (query-params with='token')}}" data-test-auth-method-link=other>
Other
</a>
</li>
{{/each}}
{{/if}}
</ul>
</nav>
<form
@ -19,8 +36,31 @@
{{else}}
{{message-error errorMessage=error data-test-auth-error=true}}
{{/if}}
{{#if (and hasMethodsWithPath (not selectedAuthIsPath))}}
<div class="field">
<label for="selectedMethod" class="is-label">
Method
</label>
<div class="control is-expanded" >
<div class="select is-fullwidth">
<select
name="selectedMethod"
id="selectedMethod"
onchange={{action (mut selectedAuth) value="target.value"}}
data-test-method-select
>
{{#each (supported-auth-backends) as |method|}}
<option selected={{eq selectedAuthBackend.type method.type}} value={{method.type}}>
{{capitalize method.type}}
</option>
{{/each}}
</select>
</div>
</div>
</div>
{{/if}}
{{partial providerPartialName}}
{{#unless (eq selectedAuthBackend.type "token")}}
{{#unless (or selectedAuthIsPath (eq selectedAuthBackend.type "token"))}}
<div class="box has-slim-padding is-shadowless">
{{toggle-button toggleTarget=this toggleAttr="useCustomPath"}}
<div class="field">

View File

@ -1,10 +1,13 @@
{{json-editor
value=(stringify content)
options=(hash
readOnly=true
lineNumbers=false
autoHeight=true
gutters=false
theme='hashi auto-height'
)
}}
<div class="console-ui-output has-copy-button">
{{json-editor
value=(stringify content)
options=(hash
readOnly=true
lineNumbers=false
autoHeight=true
gutters=false
theme='hashi auto-height'
)
}}
<HoverCopyButton @copyValue={{stringify content}} />
</div>

View File

@ -1,21 +1,8 @@
<div class="console-ui-output">
<div class="console-ui-output has-copy-button">
<pre>Keys
{{#each list as |item|}}
{{item}}
{{/each}}
</pre>
<div class="console-ui-output-actions">
{{#tool-tip renderInPlace=true as |d|}}
{{#d.trigger data-test-tool-tip-trigger=true}}
{{#copy-button clipboardText=(multi-line-join list) class="button is-compact"}}
{{i-con glyph="copy" aria-hidden="true" size=16}}
{{/copy-button}}
{{/d.trigger}}
{{#d.content class="tool-tip"}}
<div class="box">
Copy
</div>
{{/d.content}}
{{/tool-tip}}
</div>
<HoverCopyButton @copyValue={{multi-line-join list}} />
</div>

View File

@ -1,18 +1,4 @@
<div class="console-ui-output">
<pre>{{columns}}</pre>
<div class="console-ui-output-actions">
{{#tool-tip renderInPlace=true as |d|}}
{{#d.trigger data-test-tool-tip-trigger=true}}
{{#copy-button clipboardText=columns class="button is-compact"}}
{{i-con glyph="copy" aria-hidden="true" size=16}}
{{/copy-button}}
{{/d.trigger}}
{{#d.content class="tool-tip"}}
<div class="box">
Copy
</div>
{{/d.content}}
{{/tool-tip}}
</div>
<div class="console-ui-output has-copy-button">
<pre>{{columns}}</pre>
<HoverCopyButton @copyValue={{columns}} />
</div>

View File

@ -1 +1,4 @@
<pre>{{content}}</pre>
<div class="console-ui-output has-copy-button">
<pre>{{content}}</pre>
<HoverCopyButton @copyValue={{content}} @alwaysShow=true />
</div>

View File

@ -27,6 +27,25 @@
</select>
</div>
</div>
{{else if (and (eq attr.type 'string') (eq attr.options.editType 'boolean'))}}
<div class="b-checkbox">
<input type="checkbox"
id="{{attr.name}}"
class="styled"
checked={{eq (get model valuePath) attr.options.trueValue}}
onchange={{action (action "setAndBroadcastBool" valuePath attr.options.trueValue attr.options.falseValue) value="target.checked"}}
data-test-input={{attr.name}}
/>
<label for="{{attr.name}}" class="is-label">
{{labelString}}
{{#if attr.options.helpText}}
{{#info-tooltip}}
{{attr.options.helpText}}
{{/info-tooltip}}
{{/if}}
</label>
</div>
{{else if (eq attr.options.editType 'mountAccessor')}}
{{mount-accessor-select
name=attr.name

View File

@ -1,4 +1,4 @@
<ToolTip @renderInPlace={{true}} @onClose={{action (mut tooltipText) "Copy"}} as |T|>
<ToolTip @onClose={{action (mut tooltipText) "Copy"}} as |T|>
<T.trigger data-test-tooltip-trigger tabindex=false>
<CopyButton
data-test-hover-copy-button

View File

@ -1,4 +1,4 @@
{{#tool-tip renderInPlace=true as |d|}}
{{#tool-tip as |d|}}
{{#d.trigger tagName="button" type="button" class=(concat "tool-tip-trigger button") data-test-tool-tip-trigger=true}}
{{i-con
glyph="information-reversed"

View File

@ -13,11 +13,10 @@
</PageHeader>
{{#each (sort-by "path" model) as |method|}}
{{#linked-block
"vault.cluster.access.methods"
class="box is-sideless is-marginless has-pointer "
data-test-auth-backend-link=method.id
}}
<div
class="box is-sideless is-marginless"
data-test-auth-backend-link={{method.id}}
>
<div class="level is-mobile">
<div class="level-left">
<div>
@ -76,5 +75,5 @@
</div>
</div>
</div>
{{/linked-block}}
</div>
{{/each}}

View File

@ -1,4 +1,3 @@
{{!-- {{i-con glyph="unlocked" size=20}} {{capitalize model.name}} is {{if model.unsealed 'unsealed' 'sealed'}} --}}
<SplashPage as |Page|>
<Page.header>
<h1 class="title is-3">
@ -7,10 +6,12 @@
</Page.header>
<Page.content>
<AuthForm
@cluster={{model}}
@wrappedToken={{wrappedToken}}
@cluster={{model.cluster}}
@methods={{model.methods}}
@redirectTo={{redirectTo}}
@selectedAuthType={{with}}
/>
@selectedAuth={{authMethod}}
/>
</Page.content>
<Page.footer>
<div class="has-short-padding">

View File

@ -21,7 +21,7 @@ test('auth query params', function(assert) {
const backends = supportedAuthBackends();
visit('/vault/auth');
andThen(() => {
assert.equal(currentURL(), '/vault/auth');
assert.equal(currentURL(), '/vault/auth?with=token');
});
backends.reverse().forEach(backend => {
click(`[data-test-auth-method-link="${backend.type}"]`);
@ -38,7 +38,7 @@ test('auth query params', function(assert) {
test('it clears token when changing selected auth method', function(assert) {
visit('/vault/auth');
andThen(() => {
assert.equal(currentURL(), '/vault/auth');
assert.equal(currentURL(), '/vault/auth?with=token');
});
component.token('token').tabs.filterBy('name', 'GitHub')[0].link();
component.tabs.filterBy('name', 'Token')[0].link();

View File

@ -1,13 +1,15 @@
import { moduleForComponent, test } from 'ember-qunit';
import { supportedAuthBackends } from 'vault/helpers/supported-auth-backends';
import Ember from 'ember';
import wait from 'ember-test-helpers/wait';
import hbs from 'htmlbars-inline-precompile';
import sinon from 'sinon';
import Pretender from 'pretender';
import { create } from 'ember-cli-page-object';
import authForm from '../../pages/components/auth-form';
const component = create(authForm);
const BACKENDS = supportedAuthBackends();
const authService = Ember.Service.extend({
authenticate() {
@ -15,11 +17,28 @@ const authService = Ember.Service.extend({
},
});
const workingAuthService = Ember.Service.extend({
authenticate() {
return Ember.RSVP.resolve({});
},
setLastFetch() {},
});
const routerService = Ember.Service.extend({
transitionTo() {
return Ember.RSVP.resolve();
},
replaceWith() {
return Ember.RSVP.resolve();
},
});
moduleForComponent('auth-form', 'Integration | Component | auth form', {
integration: true,
beforeEach() {
Ember.getOwner(this).lookup('service:csp-event').attach();
component.setContext(this);
this.register('service:router', routerService);
this.inject.service('router');
},
afterEach() {
@ -33,7 +52,8 @@ test('it renders error on CSP violation', function(assert) {
this.register('service:auth', authService);
this.inject.service('auth');
this.set('cluster', Ember.Object.create({ standby: true }));
this.render(hbs`{{auth-form cluster=cluster}}`);
this.set('selectedAuth', 'token');
this.render(hbs`{{auth-form cluster=cluster selectedAuth=selectedAuth}}`);
assert.equal(component.errorText, '');
component.login();
// because this is an ember-concurrency backed service,
@ -58,7 +78,8 @@ test('it renders with vault style errors', function(assert) {
});
this.set('cluster', Ember.Object.create({}));
this.render(hbs`{{auth-form cluster=cluster}}`);
this.set('selectedAuth', 'token');
this.render(hbs`{{auth-form cluster=cluster selectedAuth=selectedAuth}}`);
return component.login().then(() => {
assert.equal(component.errorText, 'Error Authentication failed: Not allowed');
server.shutdown();
@ -73,9 +94,113 @@ test('it renders AdapterError style errors', function(assert) {
});
this.set('cluster', Ember.Object.create({}));
this.render(hbs`{{auth-form cluster=cluster}}`);
this.set('selectedAuth', 'token');
this.render(hbs`{{auth-form cluster=cluster selectedAuth=selectedAuth}}`);
return component.login().then(() => {
assert.equal(component.errorText, 'Error Authentication failed: Bad Request');
server.shutdown();
});
});
test('it renders all the supported tabs when no methods are passed', function(assert) {
this.render(hbs`{{auth-form cluster=cluster}}`);
assert.equal(component.tabs.length, BACKENDS.length, 'renders a tab for every backend');
});
test('it renders all the supported methods and Other tab when methods are present', function(assert) {
let methods = [
{
type: 'userpass',
id: 'foo',
path: 'foo/',
},
{
type: 'approle',
id: 'approle',
path: 'approle/',
},
];
this.set('methods', methods);
this.render(hbs`{{auth-form cluster=cluster methods=methods}}`);
assert.equal(component.tabs.length, 2, 'renders a tab for userpass and Other');
assert.equal(component.tabs.objectAt(0).name, 'foo', 'uses the path in the label');
assert.equal(component.tabs.objectAt(1).name, 'Other', 'second tab is the Other tab');
});
test('it renders all the supported methods when no supported methods are present in passed methods', function(
assert
) {
let methods = [
{
type: 'approle',
id: 'approle',
path: 'approle/',
},
];
this.set('methods', methods);
this.render(hbs`{{auth-form cluster=cluster methods=methods}}`);
assert.equal(component.tabs.length, BACKENDS.length, 'renders a tab for every backend');
});
test('it makes a request to unwrap if passed a wrappedToken and logs in', function(assert) {
this.register('service:auth', workingAuthService);
this.inject.service('auth');
let authSpy = sinon.spy(this.get('auth'), 'authenticate');
let server = new Pretender(function() {
this.post('/v1/sys/wrapping/unwrap', () => {
return [
200,
{ 'Content-Type': 'application/json' },
JSON.stringify({
auth: {
client_token: '12345',
},
}),
];
});
});
let wrappedToken = '54321';
this.set('wrappedToken', wrappedToken);
this.render(hbs`{{auth-form cluster=cluster wrappedToken=wrappedToken}}`);
Ember.run.later(() => Ember.run.cancelTimers(), 50);
return wait().then(() => {
assert.equal(server.handledRequests[0].url, '/v1/sys/wrapping/unwrap', 'makes call to unwrap the token');
assert.equal(
server.handledRequests[0].requestHeaders['X-Vault-Token'],
wrappedToken,
'uses passed wrapped token for the unwrap'
);
assert.ok(authSpy.calledOnce, 'a call to authenticate was made');
server.shutdown();
authSpy.restore();
});
});
test('it shows an error if unwrap errors', function(assert) {
let server = new Pretender(function() {
this.post('/v1/sys/wrapping/unwrap', () => {
return [
400,
{ 'Content-Type': 'application/json' },
JSON.stringify({
errors: ['There was an error unwrapping!'],
}),
];
});
});
this.set('wrappedToken', '54321');
this.render(hbs`{{auth-form cluster=cluster wrappedToken=wrappedToken}}`);
Ember.run.later(() => Ember.run.cancelTimers(), 50);
return wait().then(() => {
assert.equal(
component.errorText,
'Error Token unwrap failed: There was an error unwrapping!',
'shows the error'
);
server.shutdown();
});
});

View File

@ -14,27 +14,28 @@ test('wrapping api urls', function(assert) {
},
});
let clientToken;
let data = { foo: 'bar' };
adapter.toolAction('wrap', data, { wrapTTL: '30m' });
assert.equal('/v1/sys/wrapping/wrap', url, 'wrapping:wrap url OK');
assert.equal('POST', method, 'wrapping:wrap method OK');
assert.deepEqual({ data: data, wrapTTL: '30m' }, options, 'wrapping:wrap options OK');
assert.deepEqual({ data: data, wrapTTL: '30m', clientToken }, options, 'wrapping:wrap options OK');
data = { token: 'token' };
adapter.toolAction('lookup', data);
assert.equal('/v1/sys/wrapping/lookup', url, 'wrapping:lookup url OK');
assert.equal('POST', method, 'wrapping:lookup method OK');
assert.deepEqual({ data }, options, 'wrapping:lookup options OK');
assert.deepEqual({ data, clientToken }, options, 'wrapping:lookup options OK');
adapter.toolAction('unwrap', data);
assert.equal('/v1/sys/wrapping/unwrap', url, 'wrapping:unwrap url OK');
assert.equal('POST', method, 'wrapping:unwrap method OK');
assert.deepEqual({ data }, options, 'wrapping:unwrap options OK');
assert.deepEqual({ data, clientToken }, options, 'wrapping:unwrap options OK');
adapter.toolAction('rewrap', data);
assert.equal('/v1/sys/wrapping/rewrap', url, 'wrapping:rewrap url OK');
assert.equal('POST', method, 'wrapping:rewrap method OK');
assert.deepEqual({ data }, options, 'wrapping:rewrap options OK');
assert.deepEqual({ data, clientToken }, options, 'wrapping:rewrap options OK');
});
test('tools api urls', function(assert) {