diff --git a/ui/app/adapters/application.js b/ui/app/adapters/application.js index df076ae8f8f1..db936c9ed85d 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 03d56429080e..71ce75011ec8 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 0432c756da32..e0ad38ac15c7 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 9a98c4d0abff..aeb797fc469f 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, - - selectedAuthType: 'token', - selectedAuthBackend: Ember.computed('selectedAuthType', function() { - return BACKENDS.findBy('type', this.get('selectedAuthType')); - }), + 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')); + } + ), - 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 e862fb0a2c81..d15f20eea8fe 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 db91f3a0ac73..b660d12a88c8 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 000000000000..7de935e36077 --- /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 6cc7ac0afd1c..0e1ca62b9350 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 93d348511fbc..df0e740b101b 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 adeb1f9315bc..980af5cebc99 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 35bbf19f1538..95c939d291f1 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 875583f717ca..9cc7e373893a 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 77b2a7fb696e..3fd8aaf06214 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 ecc558015a51..f09fe5c0bbe2 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 d594a77d009b..53510cb57bd7 100644 --- a/ui/app/templates/components/auth-form.hbs +++ b/ui/app/templates/components/auth-form.hbs @@ -1,12 +1,29 @@