Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI - unauthed login methods #4854

Merged
merged 19 commits into from
Jul 5, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ui/app/adapters/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
17 changes: 16 additions & 1 deletion ui/app/adapters/auth-method.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
4 changes: 2 additions & 2 deletions ui/app/adapters/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
});
116 changes: 94 additions & 22 deletions ui/app/components/auth-form.js
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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('.')}`);
},

Expand All @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions ui/app/components/console/ui-panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),

Expand Down Expand Up @@ -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) {
Expand Down
5 changes: 5 additions & 0 deletions ui/app/components/form-field.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions ui/app/controllers/vault.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Ember from 'ember';

export default Ember.Controller.extend({
queryParams: [
{
wrappedToken: 'wrapped_token',
},
],
wrappedToken: '',
});
10 changes: 4 additions & 6 deletions ui/app/controllers/vault/cluster/auth.js
Original file line number Diff line number Diff line change
@@ -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,
});
4 changes: 2 additions & 2 deletions ui/app/helpers/nav-to-route.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
Expand Down
17 changes: 14 additions & 3 deletions ui/app/models/auth-method.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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() {
Expand Down
21 changes: 21 additions & 0 deletions ui/app/models/mount-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}),
});
30 changes: 29 additions & 1 deletion ui/app/routes/vault/cluster/auth.js
Original file line number Diff line number Diff line change
@@ -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', '');
},
});
Loading