UI wrapped token fix (#7398)

* default to token auth method

* pass in selectedValue to the AuthForm

* adjust when and if tasks are called so there's no race condition with wrapped_token query param

* add some tests for wrapped_token

* adjust redirect_to behavior so that it also works with the logout route and the wrapped_token query param

* fix linting
This commit is contained in:
Matthew Irish 2019-10-01 15:30:56 -05:00 committed by GitHub
parent 0e54f829ee
commit e4e69164e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 167 additions and 60 deletions

View File

@ -47,43 +47,54 @@ export default Component.extend(DEFAULTS, {
wrappedToken: null,
// internal
oldNamespace: null,
didReceiveAttrs() {
this._super(...arguments);
let token = this.get('wrappedToken');
let newMethod = this.get('selectedAuth');
let oldMethod = this.get('oldSelectedAuth');
let {
wrappedToken: token,
oldWrappedToken: oldToken,
oldNamespace: oldNS,
namespace: ns,
selectedAuth: newMethod,
oldSelectedAuth: oldMethod,
} = this;
let ns = this.get('namespace');
let oldNS = this.get('oldNamespace');
if (oldNS === null || oldNS !== ns) {
this.get('fetchMethods').perform();
}
this.set('oldNamespace', ns);
if (oldMethod && oldMethod !== newMethod) {
this.resetDefaults();
}
this.set('oldSelectedAuth', newMethod);
if (token) {
this.get('unwrapToken').perform(token);
}
next(() => {
if (!token && (oldNS === null || oldNS !== ns)) {
this.fetchMethods.perform();
}
this.set('oldNamespace', ns);
// we only want to trigger this once
if (token && !oldToken) {
this.unwrapToken.perform(token);
this.set('oldWrappedToken', token);
}
if (oldMethod && oldMethod !== newMethod) {
this.resetDefaults();
}
this.set('oldSelectedAuth', newMethod);
});
},
didRender() {
this._super(...arguments);
let firstMethod = this.firstMethod();
// on very narrow viewports the active tab may be overflowed, so we scroll it into view here
let activeEle = this.element.querySelector('li.is-active');
if (activeEle) {
activeEle.scrollIntoView();
}
// set `with` to the first method
if (
(this.get('fetchMethods.isIdle') && firstMethod && !this.get('selectedAuth')) ||
(this.get('selectedAuth') && !this.get('selectedAuthBackend'))
) {
this.set('selectedAuth', firstMethod);
}
next(() => {
let firstMethod = this.firstMethod();
// set `with` to the first method
if (
!this.wrappedToken &&
((this.get('fetchMethods.isIdle') && firstMethod && !this.get('selectedAuth')) ||
(this.get('selectedAuth') && !this.get('selectedAuthBackend')))
) {
this.set('selectedAuth', firstMethod);
}
});
},
firstMethod() {
@ -98,18 +109,23 @@ export default Component.extend(DEFAULTS, {
},
selectedAuthIsPath: match('selectedAuth', /\/$/),
selectedAuthBackend: computed('methods', 'methods.[]', 'selectedAuth', 'selectedAuthIsPath', function() {
let methods = this.get('methods');
let selectedAuth = this.get('selectedAuth');
let keyIsPath = this.get('selectedAuthIsPath');
if (!methods) {
return {};
selectedAuthBackend: computed(
'wrappedToken',
'methods',
'methods.[]',
'selectedAuth',
'selectedAuthIsPath',
function() {
let { wrappedToken, methods, selectedAuth, selectedAuthIsPath: keyIsPath } = this;
if (!methods && !wrappedToken) {
return {};
}
if (keyIsPath) {
return methods.findBy('path', selectedAuth);
}
return BACKENDS.findBy('type', selectedAuth);
}
if (keyIsPath) {
return methods.findBy('path', selectedAuth);
}
return BACKENDS.findBy('type', selectedAuth);
}),
),
providerPartialName: computed('selectedAuthBackend', function() {
let type = this.get('selectedAuthBackend.type') || 'token';
@ -146,9 +162,7 @@ export default Component.extend(DEFAULTS, {
try {
let response = yield adapter.toolAction('unwrap', null, { clientToken: token });
this.set('token', response.auth.client_token);
next(() => {
this.send('doSubmit');
});
this.send('doSubmit');
} catch (e) {
this.set('error', `Token unwrap failed: ${e.errors[0]}`);
}
@ -239,7 +253,7 @@ export default Component.extend(DEFAULTS, {
let backendMeta = BACKENDS.find(
b => (get(b, 'type') || '').toLowerCase() === (get(backend, 'type') || '').toLowerCase()
);
let attributes = get(backendMeta || {}, 'formAttributes') || {};
let attributes = get(backendMeta || {}, 'formAttributes') || [];
data = assign(data, this.getProperties(...attributes));
if (passedData) {

View File

@ -10,7 +10,7 @@ export default Controller.extend({
namespaceQueryParam: alias('clusterController.namespaceQueryParam'),
queryParams: [{ authMethod: 'with' }],
wrappedToken: alias('vaultController.wrappedToken'),
authMethod: '',
authMethod: 'token',
redirectTo: alias('vaultController.redirectTo'),
updateNamespace: task(function*(value) {

View File

@ -9,6 +9,7 @@ const CLUSTER = 'vault.cluster';
const CLUSTER_INDEX = 'vault.cluster.index';
const OIDC_CALLBACK = 'vault.cluster.oidc-callback';
const DR_REPLICATION_SECONDARY = 'vault.cluster.replication-dr-promote';
const EXCLUDED_REDIRECT_URLS = ['/vault/logout'];
export { INIT, UNSEAL, AUTH, CLUSTER, CLUSTER_INDEX, DR_REPLICATION_SECONDARY };
@ -29,13 +30,17 @@ export default Mixin.create({
if (
// only want to redirect if we're going to authenticate
targetRoute === AUTH &&
transition.targetName !== CLUSTER_INDEX
transition.targetName !== CLUSTER_INDEX &&
!EXCLUDED_REDIRECT_URLS.includes(this.router.currentURL)
) {
return this.transitionTo(targetRoute, { queryParams: { redirect_to: this.router.currentURL } });
}
return this.transitionTo(targetRoute);
}
if (transition.abort && targetRoute === this.router.currentRouteName) {
transition.abort();
}
return RSVP.resolve();
},

View File

@ -19,9 +19,10 @@ export default ClusterRouteBase.extend({
model() {
return this._super(...arguments);
},
resetController(controller) {
controller.set('wrappedToken', '');
controller.set('authMethod', '');
controller.set('authMethod', 'token');
},
afterModel() {

View File

@ -22,8 +22,8 @@ export default Route.extend(ModelBoundaryRoute, {
this.console.set('isOpen', false);
this.console.clearLog(true);
this.clearModelCache();
this.replaceWith('vault.cluster.auth', { queryParams: { redirect_to: '' } });
this.flashMessages.clearMessages();
this.permissions.reset();
this.replaceWith('vault.cluster.auth');
},
});

View File

@ -39,7 +39,8 @@
@valueAttribute={{'type'}}
@labelAttribute={{'typeDisplay'}}
@isFullwidth={{true}}
@onChange={{action (mut selectedAuth)}}
@selectedValue={{this.selectedAuth}}
@onChange={{action (mut this.selectedAuth)}}
/>
{{/if}}
{{#if (or (eq this.selectedAuthBackend.type "jwt") (eq this.selectedAuthBackend.type "oidc"))}}

View File

@ -37,20 +37,24 @@ module('Acceptance | auth', function(hooks) {
let backends = supportedAuthBackends();
assert.expect(backends.length + 1);
await visit('/vault/auth');
assert.equal(currentURL(), '/vault/auth?with=token');
assert.equal(currentURL(), '/vault/auth');
for (let backend of backends.reverse()) {
await component.selectMethod(backend.type);
assert.equal(
currentURL(),
`/vault/auth?with=${backend.type}`,
`has the correct URL for ${backend.type}`
);
if (backend.type === 'token') {
assert.equal(currentURL(), `/vault/auth`, `has the correct URL for ${backend.type}`);
} else {
assert.equal(
currentURL(),
`/vault/auth?with=${backend.type}`,
`has the correct URL for ${backend.type}`
);
}
}
});
test('it clears token when changing selected auth method', async function(assert) {
await visit('/vault/auth');
assert.equal(currentURL(), '/vault/auth?with=token');
assert.equal(currentURL(), '/vault/auth');
await component.token('token').selectMethod('github');
await component.selectMethod('token');
assert.equal(component.tokenValue, '', 'it clears the token value when toggling methods');

View File

@ -1,28 +1,62 @@
import { currentURL, visit } from '@ember/test-helpers';
import { currentURL, visit as _visit, settled } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import authPage from 'vault/tests/pages/auth';
import { create } from 'ember-cli-page-object';
import auth from 'vault/tests/pages/auth';
import consoleClass from 'vault/tests/pages/components/console/ui-panel';
module('Acceptance | redirect_to functionality', function(hooks) {
const visit = async url => {
try {
await _visit(url);
} catch (e) {
if (e.message !== 'TransitionAborted') {
throw e;
}
}
await settled();
};
const consoleComponent = create(consoleClass);
const wrappedAuth = async () => {
await consoleComponent.runCommands(`write -field=token auth/token/create policies=default -wrap-ttl=3m`);
return consoleComponent.lastLogOutput;
};
const setupWrapping = async () => {
await auth.logout();
await auth.visit();
await auth.tokenInput('root').submit();
let wrappedToken = await wrappedAuth();
return wrappedToken;
};
module('Acceptance | redirect_to query param functionality', function(hooks) {
setupApplicationTest(hooks);
hooks.beforeEach(function() {
// normally we'd use the auth.logout helper to visit the route and reset the app, but in this case that
// also routes us to the auth page, and then all of the transitions from the auth page get redirected back
// to the auth page resulting in no redirect_to query param being set
localStorage.clear();
});
test('redirect to a route after authentication', async function(assert) {
let url = '/vault/secrets/secret/create';
await visit(url);
assert.equal(
currentURL(),
`/vault/auth?redirect_to=${encodeURIComponent(url)}&with=token`,
`/vault/auth?redirect_to=${encodeURIComponent(url)}`,
'encodes url for the query param'
);
// the login method on this page does another visit call that we don't want here
await authPage.tokenInput('root').submit();
await auth.tokenInput('root').submit();
assert.equal(currentURL(), url, 'navigates to the redirect_to url after auth');
});
test('redirect from root does not include redirect_to', async function(assert) {
let url = '/';
await visit(url);
assert.equal(currentURL(), `/vault/auth?with=token`, 'there is no redirect_to query param');
assert.equal(currentURL(), `/vault/auth`, 'there is no redirect_to query param');
});
test('redirect to a route after authentication with a query param', async function(assert) {
@ -30,10 +64,21 @@ module('Acceptance | redirect_to functionality', function(hooks) {
await visit(url);
assert.equal(
currentURL(),
`/vault/auth?redirect_to=${encodeURIComponent(url)}&with=token`,
`/vault/auth?redirect_to=${encodeURIComponent(url)}`,
'encodes url for the query param'
);
await authPage.tokenInput('root').submit();
await auth.tokenInput('root').submit();
assert.equal(currentURL(), url, 'navigates to the redirect_to with the query param after auth');
});
test('redirect to logout with wrapped token authenticates you', async function(assert) {
let wrappedToken = await setupWrapping();
let url = '/vault/secrets/cubbyhole/create';
await auth.logout({
redirect_to: url,
wrapped_token: wrappedToken,
});
assert.equal(currentURL(), url, 'authenticates then navigates to the redirect_to url after auth');
});
});

View File

@ -0,0 +1,37 @@
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { currentURL } from '@ember/test-helpers';
import { create } from 'ember-cli-page-object';
import auth from 'vault/tests/pages/auth';
import consoleClass from 'vault/tests/pages/components/console/ui-panel';
const consoleComponent = create(consoleClass);
const wrappedAuth = async () => {
await consoleComponent.runCommands(`write -field=token auth/token/create policies=default -wrap-ttl=3m`);
return consoleComponent.lastLogOutput;
};
const setupWrapping = async () => {
await auth.logout();
await auth.visit();
await auth.tokenInput('root').submit();
let token = await wrappedAuth();
await auth.logout();
return token;
};
module('Acceptance | wrapped_token query param functionality', function(hooks) {
setupApplicationTest(hooks);
test('it authenticates you if the query param is present', async function(assert) {
let token = await setupWrapping();
await auth.visit({ wrapped_token: token });
assert.equal(currentURL(), '/vault/secrets', 'authenticates and redirects to home');
});
test('it authenticates when used with the with=token query param', async function(assert) {
let token = await setupWrapping();
await auth.visit({ wrapped_token: token, with: 'token' });
assert.equal(currentURL(), '/vault/secrets', 'authenticates and redirects to home');
});
});