From f38a50b6b272bfe9a25023217e935501146d44a7 Mon Sep 17 00:00:00 2001 From: Matthew Irish Date: Thu, 5 Jul 2018 13:28:12 -0500 Subject: [PATCH] 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 --- ui/app/adapters/application.js | 2 +- ui/app/adapters/auth-method.js | 17 ++- ui/app/adapters/tools.js | 4 +- ui/app/components/auth-form.js | 116 ++++++++++++--- ui/app/components/console/ui-panel.js | 2 + ui/app/components/form-field.js | 5 + ui/app/controllers/vault.js | 10 ++ ui/app/controllers/vault/cluster/auth.js | 10 +- ui/app/helpers/nav-to-route.js | 4 +- ui/app/models/auth-method.js | 17 ++- ui/app/models/mount-config.js | 21 +++ ui/app/routes/vault/cluster/auth.js | 30 +++- ui/app/serializers/auth-method.js | 7 +- .../styles/components/console-ui-panel.scss | 28 ++-- ui/app/templates/components/auth-form.hbs | 52 ++++++- .../templates/components/console/log-json.hbs | 23 +-- .../templates/components/console/log-list.hbs | 17 +-- .../components/console/log-object.hbs | 20 +-- .../templates/components/console/log-text.hbs | 5 +- ui/app/templates/components/form-field.hbs | 19 +++ .../components/hover-copy-button.hbs | 2 +- ui/app/templates/components/info-tooltip.hbs | 2 +- .../auth-form/{git-hub.hbs => github.hbs} | 0 .../vault/cluster/access/methods.hbs | 11 +- ui/app/templates/vault/cluster/auth.hbs | 9 +- ui/tests/acceptance/auth-test.js | 4 +- .../integration/components/auth-form-test.js | 133 +++++++++++++++++- ui/tests/unit/adapters/tools-test.js | 9 +- 28 files changed, 449 insertions(+), 130 deletions(-) create mode 100644 ui/app/controllers/vault.js rename ui/app/templates/partials/auth-form/{git-hub.hbs => github.hbs} (100%) diff --git a/ui/app/adapters/application.js b/ui/app/adapters/application.js index df076ae8f..db936c9ed 100644 --- a/ui/app/adapters/application.js +++ b/ui/app/adapters/application.js @@ -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, diff --git a/ui/app/adapters/auth-method.js b/ui/app/adapters/auth-method.js index 03d564290..71ce75011 100644 --- a/ui/app/adapters/auth-method.js +++ b/ui/app/adapters/auth-method.js @@ -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'); diff --git a/ui/app/adapters/tools.js b/ui/app/adapters/tools.js index 0432c756d..e0ad38ac1 100644 --- a/ui/app/adapters/tools.js +++ b/ui/app/adapters/tools.js @@ -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); }, }); diff --git a/ui/app/components/auth-form.js b/ui/app/components/auth-form.js index 9a98c4d0a..aeb797fc4 100644 --- a/ui/app/components/auth-form.js +++ b/ui/app/components/auth-form.js @@ -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( diff --git a/ui/app/components/console/ui-panel.js b/ui/app/components/console/ui-panel.js index e862fb0a2..d15f20eea 100644 --- a/ui/app/components/console/ui-panel.js +++ b/ui/app/components/console/ui-panel.js @@ -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) { diff --git a/ui/app/components/form-field.js b/ui/app/components/form-field.js index db91f3a0a..b660d12a8 100644 --- a/ui/app/components/form-field.js +++ b/ui/app/components/form-field.js @@ -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; diff --git a/ui/app/controllers/vault.js b/ui/app/controllers/vault.js new file mode 100644 index 000000000..7de935e36 --- /dev/null +++ b/ui/app/controllers/vault.js @@ -0,0 +1,10 @@ +import Ember from 'ember'; + +export default Ember.Controller.extend({ + queryParams: [ + { + wrappedToken: 'wrapped_token', + }, + ], + wrappedToken: '', +}); diff --git a/ui/app/controllers/vault/cluster/auth.js b/ui/app/controllers/vault/cluster/auth.js index 6cc7ac0af..0e1ca62b9 100644 --- a/ui/app/controllers/vault/cluster/auth.js +++ b/ui/app/controllers/vault/cluster/auth.js @@ -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, }); diff --git a/ui/app/helpers/nav-to-route.js b/ui/app/helpers/nav-to-route.js index 93d348511..df0e740b1 100644 --- a/ui/app/helpers/nav-to-route.js +++ b/ui/app/helpers/nav-to-route.js @@ -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); }; diff --git a/ui/app/models/auth-method.js b/ui/app/models/auth-method.js index adeb1f931..980af5ceb 100644 --- a/ui/app/models/auth-method.js +++ b/ui/app/models/auth-method.js @@ -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() { diff --git a/ui/app/models/mount-config.js b/ui/app/models/mount-config.js index 35bbf19f1..95c939d29 100644 --- a/ui/app/models/mount-config.js +++ b/ui/app/models/mount-config.js @@ -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', + }), }); diff --git a/ui/app/routes/vault/cluster/auth.js b/ui/app/routes/vault/cluster/auth.js index 875583f71..9cc7e3738 100644 --- a/ui/app/routes/vault/cluster/auth.js +++ b/ui/app/routes/vault/cluster/auth.js @@ -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', ''); + }, +}); diff --git a/ui/app/serializers/auth-method.js b/ui/app/serializers/auth-method.js index 77b2a7fb6..3fd8aaf06 100644 --- a/ui/app/serializers/auth-method.js +++ b/ui/app/serializers/auth-method.js @@ -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); }, diff --git a/ui/app/styles/components/console-ui-panel.scss b/ui/app/styles/components/console-ui-panel.scss index ecc558015..f09fe5c0b 100644 --- a/ui/app/styles/components/console-ui-panel.scss +++ b/ui/app/styles/components/console-ui-panel.scss @@ -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); } } diff --git a/ui/app/templates/components/auth-form.hbs b/ui/app/templates/components/auth-form.hbs index d594a77d0..53510cb57 100644 --- a/ui/app/templates/components/auth-form.hbs +++ b/ui/app/templates/components/auth-form.hbs @@ -1,12 +1,29 @@
+ +
+
+ +
+
+ + {{/if}} {{partial providerPartialName}} - {{#unless (eq selectedAuthBackend.type "token")}} + {{#unless (or selectedAuthIsPath (eq selectedAuthBackend.type "token"))}}
{{toggle-button toggleTarget=this toggleAttr="useCustomPath"}}
diff --git a/ui/app/templates/components/console/log-json.hbs b/ui/app/templates/components/console/log-json.hbs index 293ee6f23..b31606323 100644 --- a/ui/app/templates/components/console/log-json.hbs +++ b/ui/app/templates/components/console/log-json.hbs @@ -1,10 +1,13 @@ -{{json-editor - value=(stringify content) - options=(hash - readOnly=true - lineNumbers=false - autoHeight=true - gutters=false - theme='hashi auto-height' - ) - }} +
+ {{json-editor + value=(stringify content) + options=(hash + readOnly=true + lineNumbers=false + autoHeight=true + gutters=false + theme='hashi auto-height' + ) + }} + +
diff --git a/ui/app/templates/components/console/log-list.hbs b/ui/app/templates/components/console/log-list.hbs index 83fc544bd..d2b79b377 100644 --- a/ui/app/templates/components/console/log-list.hbs +++ b/ui/app/templates/components/console/log-list.hbs @@ -1,21 +1,8 @@ -
+
Keys
 {{#each list as |item|}}
 {{item}}
 {{/each}}
 
-
- {{#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"}} -
- Copy -
- {{/d.content}} - {{/tool-tip}} -
+
diff --git a/ui/app/templates/components/console/log-object.hbs b/ui/app/templates/components/console/log-object.hbs index 3c8d77cf4..c77103174 100644 --- a/ui/app/templates/components/console/log-object.hbs +++ b/ui/app/templates/components/console/log-object.hbs @@ -1,18 +1,4 @@ -
-
{{columns}}
- -
- {{#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"}} -
- Copy -
- {{/d.content}} - {{/tool-tip}} -
+
+
{{columns}}
+
diff --git a/ui/app/templates/components/console/log-text.hbs b/ui/app/templates/components/console/log-text.hbs index 3da410475..9cb29971e 100644 --- a/ui/app/templates/components/console/log-text.hbs +++ b/ui/app/templates/components/console/log-text.hbs @@ -1 +1,4 @@ -
{{content}}
\ No newline at end of file +
+
{{content}}
+ +
diff --git a/ui/app/templates/components/form-field.hbs b/ui/app/templates/components/form-field.hbs index cbe64c422..25b44bba4 100644 --- a/ui/app/templates/components/form-field.hbs +++ b/ui/app/templates/components/form-field.hbs @@ -27,6 +27,25 @@
+{{else if (and (eq attr.type 'string') (eq attr.options.editType 'boolean'))}} +
+ + +
+ {{else if (eq attr.options.editType 'mountAccessor')}} {{mount-accessor-select name=attr.name diff --git a/ui/app/templates/components/hover-copy-button.hbs b/ui/app/templates/components/hover-copy-button.hbs index 7a35dc92f..0de95fbb8 100644 --- a/ui/app/templates/components/hover-copy-button.hbs +++ b/ui/app/templates/components/hover-copy-button.hbs @@ -1,4 +1,4 @@ - + {{#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 - }} +
@@ -76,5 +75,5 @@
- {{/linked-block}} +
{{/each}} diff --git a/ui/app/templates/vault/cluster/auth.hbs b/ui/app/templates/vault/cluster/auth.hbs index 292850f34..5bce0f880 100644 --- a/ui/app/templates/vault/cluster/auth.hbs +++ b/ui/app/templates/vault/cluster/auth.hbs @@ -1,4 +1,3 @@ -{{!-- {{i-con glyph="unlocked" size=20}} {{capitalize model.name}} is {{if model.unsealed 'unsealed' 'sealed'}} --}}

@@ -7,10 +6,12 @@ + @selectedAuth={{authMethod}} + />
diff --git a/ui/tests/acceptance/auth-test.js b/ui/tests/acceptance/auth-test.js index 49fb75cf9..5606832e1 100644 --- a/ui/tests/acceptance/auth-test.js +++ b/ui/tests/acceptance/auth-test.js @@ -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(); diff --git a/ui/tests/integration/components/auth-form-test.js b/ui/tests/integration/components/auth-form-test.js index 8bf7acc02..111787da3 100644 --- a/ui/tests/integration/components/auth-form-test.js +++ b/ui/tests/integration/components/auth-form-test.js @@ -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(); + }); +}); diff --git a/ui/tests/unit/adapters/tools-test.js b/ui/tests/unit/adapters/tools-test.js index 115a1b487..2d5f1277f 100644 --- a/ui/tests/unit/adapters/tools-test.js +++ b/ui/tests/unit/adapters/tools-test.js @@ -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) {