mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-04 12:01:23 +02:00
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:
parent
0e54f829ee
commit
e4e69164e4
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
},
|
||||
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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');
|
||||
},
|
||||
});
|
||||
|
||||
@ -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"))}}
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
37
ui/tests/acceptance/wrapped-token-test.js
Normal file
37
ui/tests/acceptance/wrapped-token-test.js
Normal 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');
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user