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 wrapped token fix #7398

Merged
merged 6 commits into from
Oct 1, 2019
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
94 changes: 54 additions & 40 deletions ui/app/components/auth-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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 {};
}
if (keyIsPath) {
return methods.findBy('path', selectedAuth);
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);
}
return BACKENDS.findBy('type', selectedAuth);
}),
),

providerPartialName: computed('selectedAuthBackend', function() {
let type = this.get('selectedAuthBackend.type') || 'token';
Expand Down Expand Up @@ -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]}`);
}
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion ui/app/controllers/vault/cluster/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
7 changes: 6 additions & 1 deletion ui/app/mixins/cluster-route.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

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

Expand Down
3 changes: 2 additions & 1 deletion ui/app/routes/vault/cluster/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion ui/app/routes/vault/cluster/logout.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
},
});
3 changes: 2 additions & 1 deletion ui/app/templates/components/auth-form.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -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"))}}
Expand Down
18 changes: 11 additions & 7 deletions ui/tests/acceptance/auth-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
61 changes: 53 additions & 8 deletions ui/tests/acceptance/redirect-to-test.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,84 @@
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) {
let url = '/vault/secrets/secret/create?initialKey=hello';
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 changes: 37 additions & 0 deletions ui/tests/acceptance/wrapped-token-test.js
Original file line number Diff line number Diff line change
@@ -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');
});
});