UI - support redirecting to an intended URL after authentication (#7088)
* add redirect_to query param * alias auth controller state to vault controller where the query param is defined * capture the current url before redirecting a user to auth if they're being redirected * consume and reset the redirectTo query param when authenticating * make sure that the current url when logging out does not get set as the redirect_to query param * add unit tests for the mixin and make it so that redirects from the root don't end up in redirect_to * acceptance tests for redirect
This commit is contained in:
parent
1a7a71385a
commit
3da6487cf4
|
@ -192,12 +192,20 @@ export default Component.extend(DEFAULTS, {
|
|||
|
||||
authenticate: task(function*(backendType, data) {
|
||||
let clusterId = this.cluster.id;
|
||||
let targetRoute = this.redirectTo || 'vault.cluster';
|
||||
try {
|
||||
let authResponse = yield this.auth.authenticate({ clusterId, backend: backendType, data });
|
||||
|
||||
let { isRoot, namespace } = authResponse;
|
||||
let transition = this.router.transitionTo(targetRoute, { queryParams: { namespace } });
|
||||
let transition;
|
||||
let { redirectTo } = this;
|
||||
if (redirectTo) {
|
||||
// reset the value on the controller because it's bound here
|
||||
this.set('redirectTo', '');
|
||||
// here we don't need the namespace because it will be encoded in redirectTo
|
||||
transition = this.router.transitionTo(redirectTo);
|
||||
} else {
|
||||
transition = this.router.transitionTo('vault.cluster', { queryParams: { namespace } });
|
||||
}
|
||||
// returning this w/then because if we keep it
|
||||
// in the task, it will get cancelled when the component in un-rendered
|
||||
yield transition.followRedirects().then(() => {
|
||||
|
|
|
@ -7,9 +7,11 @@ export default Controller.extend({
|
|||
queryParams: [
|
||||
{
|
||||
wrappedToken: 'wrapped_token',
|
||||
redirectTo: 'redirect_to',
|
||||
},
|
||||
],
|
||||
wrappedToken: '',
|
||||
redirectTo: '',
|
||||
env: config.environment,
|
||||
auth: service(),
|
||||
store: service(),
|
||||
|
|
|
@ -11,7 +11,7 @@ export default Controller.extend({
|
|||
queryParams: [{ authMethod: 'with' }],
|
||||
wrappedToken: alias('vaultController.wrappedToken'),
|
||||
authMethod: '',
|
||||
redirectTo: null,
|
||||
redirectTo: alias('vaultController.redirectTo'),
|
||||
|
||||
updateNamespace: task(function*(value) {
|
||||
// debounce
|
||||
|
|
|
@ -6,26 +6,41 @@ const INIT = 'vault.cluster.init';
|
|||
const UNSEAL = 'vault.cluster.unseal';
|
||||
const AUTH = 'vault.cluster.auth';
|
||||
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';
|
||||
|
||||
export { INIT, UNSEAL, AUTH, CLUSTER, DR_REPLICATION_SECONDARY };
|
||||
export { INIT, UNSEAL, AUTH, CLUSTER, CLUSTER_INDEX, DR_REPLICATION_SECONDARY };
|
||||
|
||||
export default Mixin.create({
|
||||
auth: service(),
|
||||
store: service(),
|
||||
router: service(),
|
||||
|
||||
transitionToTargetRoute(transition) {
|
||||
transitionToTargetRoute(transition = {}) {
|
||||
const targetRoute = this.targetRouteName(transition);
|
||||
if (targetRoute && targetRoute !== this.routeName) {
|
||||
|
||||
if (
|
||||
targetRoute &&
|
||||
targetRoute !== this.routeName &&
|
||||
targetRoute !== transition.targetName &&
|
||||
targetRoute !== this.router.currentRouteName
|
||||
) {
|
||||
if (
|
||||
// only want to redirect if we're going to authenticate
|
||||
targetRoute === AUTH &&
|
||||
transition.targetName !== CLUSTER_INDEX
|
||||
) {
|
||||
return this.transitionTo(targetRoute, { queryParams: { redirect_to: this.router.currentURL } });
|
||||
}
|
||||
return this.transitionTo(targetRoute);
|
||||
}
|
||||
|
||||
return RSVP.resolve();
|
||||
},
|
||||
|
||||
beforeModel() {
|
||||
return this.transitionToTargetRoute();
|
||||
beforeModel(transition) {
|
||||
return this.transitionToTargetRoute(transition);
|
||||
},
|
||||
|
||||
clusterModel() {
|
||||
|
|
|
@ -22,7 +22,7 @@ export default Route.extend(ModelBoundaryRoute, {
|
|||
this.console.set('isOpen', false);
|
||||
this.console.clearLog(true);
|
||||
this.clearModelCache();
|
||||
this.replaceWith('vault.cluster');
|
||||
this.replaceWith('vault.cluster.auth', { queryParams: { redirect_to: '' } });
|
||||
this.flashMessages.clearMessages();
|
||||
this.permissions.reset();
|
||||
},
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import { currentURL, visit } from '@ember/test-helpers';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import authPage from 'vault/tests/pages/auth';
|
||||
|
||||
module('Acceptance | redirect_to functionality', function(hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
|
||||
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`,
|
||||
'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();
|
||||
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');
|
||||
});
|
||||
|
||||
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`,
|
||||
'encodes url for the query param'
|
||||
);
|
||||
await authPage.tokenInput('root').submit();
|
||||
assert.equal(currentURL(), url, 'navigates to the redirect_to with the query param after auth');
|
||||
});
|
||||
});
|
|
@ -1,13 +1,21 @@
|
|||
import { assign } from '@ember/polyfills';
|
||||
import EmberObject from '@ember/object';
|
||||
import ClusterRouteMixin from 'vault/mixins/cluster-route';
|
||||
import { INIT, UNSEAL, AUTH, CLUSTER, DR_REPLICATION_SECONDARY } from 'vault/mixins/cluster-route';
|
||||
import {
|
||||
INIT,
|
||||
UNSEAL,
|
||||
AUTH,
|
||||
CLUSTER,
|
||||
CLUSTER_INDEX,
|
||||
DR_REPLICATION_SECONDARY,
|
||||
} from 'vault/mixins/cluster-route';
|
||||
import { module, test } from 'qunit';
|
||||
import sinon from 'sinon';
|
||||
|
||||
module('Unit | Mixin | cluster route', function() {
|
||||
function createClusterRoute(
|
||||
clusterModel = {},
|
||||
methods = { hasKeyData: () => false, authToken: () => null }
|
||||
methods = { router: {}, hasKeyData: () => false, authToken: () => null, transitionTo: () => {} }
|
||||
) {
|
||||
let ClusterRouteObject = EmberObject.extend(
|
||||
ClusterRouteMixin,
|
||||
|
@ -80,4 +88,35 @@ module('Unit | Mixin | cluster route', function() {
|
|||
'forwards when not a DR secondary and navigating to DR_REPLICATION_SECONDARY'
|
||||
);
|
||||
});
|
||||
|
||||
test('#transitionToTargetRoute', function(assert) {
|
||||
let redirectRouteURL = '/vault/secrets/secret/create';
|
||||
let subject = createClusterRoute({ needsInit: false, sealed: false });
|
||||
subject.router.currentURL = redirectRouteURL;
|
||||
let spy = sinon.spy(subject, 'transitionTo');
|
||||
subject.transitionToTargetRoute();
|
||||
assert.ok(
|
||||
spy.calledWithExactly(AUTH, { queryParams: { redirect_to: redirectRouteURL } }),
|
||||
'calls transitionTo with the expected args'
|
||||
);
|
||||
|
||||
spy.restore();
|
||||
});
|
||||
|
||||
test('#transitionToTargetRoute with auth as a target', function(assert) {
|
||||
let subject = createClusterRoute({ needsInit: false, sealed: false });
|
||||
let spy = sinon.spy(subject, 'transitionTo');
|
||||
// in this case it's already transitioning to the AUTH route so we don't need to call transitionTo again
|
||||
subject.transitionToTargetRoute({ targetName: AUTH });
|
||||
assert.ok(spy.notCalled, 'transitionTo is not called');
|
||||
spy.restore();
|
||||
});
|
||||
|
||||
test('#transitionToTargetRoute with auth target, coming from cluster route', function(assert) {
|
||||
let subject = createClusterRoute({ needsInit: false, sealed: false });
|
||||
let spy = sinon.spy(subject, 'transitionTo');
|
||||
subject.transitionToTargetRoute({ targetName: CLUSTER_INDEX });
|
||||
assert.ok(spy.calledWithExactly(AUTH), 'calls transitionTo without redirect_to');
|
||||
spy.restore();
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue