UI: Forward to `redirect_to` param to when auth'd (#16821)

* Pull route paths out of cluster-route mixin

* Add redirect route and point there if authed and desired path is auth

* Cleanup test

* Use replaceWith instead of transitionTo

* Update tests

* Fix controller accessed by redirect route

* Add changelog

* Fix tests
This commit is contained in:
Chelsea Shaw 2022-08-23 11:05:00 -05:00 committed by GitHub
parent b3e8098685
commit c6bc8db441
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 174 additions and 17 deletions

3
changelog/16821.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
ui: redirect_to param forwards from auth route when authenticated
```

12
ui/app/lib/route-paths.js Normal file
View File

@ -0,0 +1,12 @@
export const INIT = 'vault.cluster.init';
export const UNSEAL = 'vault.cluster.unseal';
export const AUTH = 'vault.cluster.auth';
export const REDIRECT = 'vault.cluster.redirect';
export const CLUSTER = 'vault.cluster';
export const CLUSTER_INDEX = 'vault.cluster.index';
export const OIDC_CALLBACK = 'vault.cluster.oidc-callback';
export const OIDC_PROVIDER = 'vault.cluster.oidc-provider';
export const NS_OIDC_PROVIDER = 'vault.cluster.oidc-provider-ns';
export const DR_REPLICATION_SECONDARY = 'vault.cluster.replication-dr-promote';
export const DR_REPLICATION_SECONDARY_DETAILS = 'vault.cluster.replication-dr-promote.details';
export const EXCLUDED_REDIRECT_URLS = ['/vault/logout'];

View File

@ -1,19 +1,20 @@
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import Mixin from '@ember/object/mixin'; import Mixin from '@ember/object/mixin';
import RSVP from 'rsvp'; import RSVP from 'rsvp';
const INIT = 'vault.cluster.init'; import {
const UNSEAL = 'vault.cluster.unseal'; INIT,
const AUTH = 'vault.cluster.auth'; UNSEAL,
const CLUSTER = 'vault.cluster'; AUTH,
const CLUSTER_INDEX = 'vault.cluster.index'; CLUSTER,
const OIDC_CALLBACK = 'vault.cluster.oidc-callback'; CLUSTER_INDEX,
const OIDC_PROVIDER = 'vault.cluster.oidc-provider'; OIDC_CALLBACK,
const NS_OIDC_PROVIDER = 'vault.cluster.oidc-provider-ns'; OIDC_PROVIDER,
const DR_REPLICATION_SECONDARY = 'vault.cluster.replication-dr-promote'; NS_OIDC_PROVIDER,
const DR_REPLICATION_SECONDARY_DETAILS = 'vault.cluster.replication-dr-promote.details'; DR_REPLICATION_SECONDARY,
const EXCLUDED_REDIRECT_URLS = ['/vault/logout']; DR_REPLICATION_SECONDARY_DETAILS,
EXCLUDED_REDIRECT_URLS,
export { INIT, UNSEAL, AUTH, CLUSTER, CLUSTER_INDEX, DR_REPLICATION_SECONDARY }; REDIRECT,
} from 'vault/lib/route-paths';
export default Mixin.create({ export default Mixin.create({
auth: service(), auth: service(),
@ -96,11 +97,14 @@ export default Mixin.create({
if ( if (
(!cluster.needsInit && this.routeName === INIT) || (!cluster.needsInit && this.routeName === INIT) ||
(!cluster.sealed && this.routeName === UNSEAL) || (!cluster.sealed && this.routeName === UNSEAL) ||
(!cluster?.dr?.isSecondary && this.routeName === DR_REPLICATION_SECONDARY) || (!cluster?.dr?.isSecondary && this.routeName === DR_REPLICATION_SECONDARY)
(isAuthed && this.routeName === AUTH)
) { ) {
return CLUSTER; return CLUSTER;
} }
if (isAuthed && this.routeName === AUTH) {
// if you're already authed and you wanna go to auth, you probably want to redirect
return REDIRECT;
}
return null; return null;
}, },
}); });

View File

@ -13,6 +13,7 @@ Router.map(function () {
this.route('oidc-provider', { path: '/identity/oidc/provider/:provider_name/authorize' }); this.route('oidc-provider', { path: '/identity/oidc/provider/:provider_name/authorize' });
this.route('oidc-callback', { path: '/auth/*auth_path/oidc/callback' }); this.route('oidc-callback', { path: '/auth/*auth_path/oidc/callback' });
this.route('auth'); this.route('auth');
this.route('redirect');
this.route('init'); this.route('init');
this.route('logout'); this.route('logout');
this.mount('open-api-explorer', { path: '/api-explorer' }); this.mount('open-api-explorer', { path: '/api-explorer' });

View File

@ -0,0 +1,30 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { AUTH, CLUSTER } from 'vault/lib/route-paths';
export default class VaultClusterRedirectRoute extends Route {
@service auth;
@service router;
beforeModel({ to: { queryParams } }) {
let transition;
const isAuthed = this.auth.currentToken;
// eslint-disable-next-line ember/no-controller-access-in-routes
const controller = this.controllerFor('vault');
const { redirect_to, ...otherParams } = queryParams;
if (isAuthed && redirect_to) {
// if authenticated and redirect exists, redirect to that place and strip other params
transition = this.router.replaceWith(redirect_to);
} else if (isAuthed) {
// if authed no redirect, go to cluster
transition = this.router.replaceWith(CLUSTER, { queryParams: otherParams });
} else {
// default go to Auth
transition = this.router.replaceWith(AUTH, { queryParams: otherParams });
}
transition.followRedirects().then(() => {
controller.set('redirectTo', '');
});
}
}

View File

@ -0,0 +1 @@
<LogoSplash />

View File

@ -8,7 +8,8 @@ import {
CLUSTER, CLUSTER,
CLUSTER_INDEX, CLUSTER_INDEX,
DR_REPLICATION_SECONDARY, DR_REPLICATION_SECONDARY,
} from 'vault/mixins/cluster-route'; REDIRECT,
} from 'vault/lib/route-paths';
import { module, test } from 'qunit'; import { module, test } from 'qunit';
import sinon from 'sinon'; import sinon from 'sinon';
@ -79,7 +80,7 @@ module('Unit | Mixin | cluster route', function () {
assert.equal(subject.targetRouteName(), CLUSTER, 'forwards when unsealed and navigating to UNSEAL'); assert.equal(subject.targetRouteName(), CLUSTER, 'forwards when unsealed and navigating to UNSEAL');
subject.routeName = AUTH; subject.routeName = AUTH;
assert.equal(subject.targetRouteName(), CLUSTER, 'forwards when authenticated and navigating to AUTH'); assert.equal(subject.targetRouteName(), REDIRECT, 'forwards when authenticated and navigating to AUTH');
subject.routeName = DR_REPLICATION_SECONDARY; subject.routeName = DR_REPLICATION_SECONDARY;
assert.equal( assert.equal(
@ -89,6 +90,28 @@ module('Unit | Mixin | cluster route', function () {
); );
}); });
test('#targetRouteName happy path when not authed forwards to AUTH', function (assert) {
let subject = createClusterRoute(
{ needsInit: false, sealed: false, dr: { isSecondary: false } },
{ hasKeyData: () => false, authToken: () => null }
);
subject.routeName = INIT;
assert.equal(subject.targetRouteName(), AUTH, 'forwards when inited and navigating to INIT');
subject.routeName = UNSEAL;
assert.equal(subject.targetRouteName(), AUTH, 'forwards when unsealed and navigating to UNSEAL');
subject.routeName = AUTH;
assert.equal(subject.targetRouteName(), AUTH, 'forwards when non-authenticated and navigating to AUTH');
subject.routeName = DR_REPLICATION_SECONDARY;
assert.equal(
subject.targetRouteName(),
AUTH,
'forwards when not a DR secondary and navigating to DR_REPLICATION_SECONDARY'
);
});
test('#transitionToTargetRoute', function (assert) { test('#transitionToTargetRoute', function (assert) {
let redirectRouteURL = '/vault/secrets/secret/create'; let redirectRouteURL = '/vault/secrets/secret/create';
let subject = createClusterRoute({ needsInit: false, sealed: false }); let subject = createClusterRoute({ needsInit: false, sealed: false });

View File

@ -0,0 +1,83 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import sinon from 'sinon';
module('Unit | Route | vault/cluster/redirect', function (hooks) {
setupTest(hooks);
hooks.beforeEach(function () {
this.router = this.owner.lookup('service:router');
this.originalTransition = this.router.replaceWith;
this.router.replaceWith = sinon.stub().returns({
followRedirects: function () {
return {
then: function (callback) {
callback();
},
};
},
});
});
hooks.afterEach(function () {
this.router.replaceWith = this.originalTransition;
});
test('it calls route', function (assert) {
let route = this.owner.lookup('route:vault/cluster/redirect');
assert.ok(route);
});
test('it redirects to auth when unauthenticated', function (assert) {
let route = this.owner.lookup('route:vault/cluster/redirect');
const auth = this.owner.lookup('service:auth');
const originalToken = auth.currentToken;
auth.currentToken = null;
route.beforeModel({ to: { queryParams: { redirect_to: 'vault/cluster/tools', namespace: 'admin' } } });
assert.true(
this.router.replaceWith.calledWithExactly('vault.cluster.auth', {
queryParams: { namespace: 'admin' },
}),
'transitions to auth when not authenticated'
);
auth.currentToken = originalToken;
});
test('it redirects to cluster when authenticated without redirect param', function (assert) {
let route = this.owner.lookup('route:vault/cluster/redirect');
const auth = this.owner.lookup('service:auth');
const originalToken = auth.currentToken;
auth.currentToken = 's.xxxxxxxxx';
route.beforeModel({ to: { queryParams: { foo: 'bar' } } });
assert.true(
this.router.replaceWith.calledWithExactly('vault.cluster', { queryParams: { foo: 'bar' } }),
'transitions to cluster when authenticated but no redirect param'
);
auth.currentToken = originalToken;
});
test('it redirects to desired path when authenticated with redirect param', function (assert) {
let route = this.owner.lookup('route:vault/cluster/redirect');
const auth = this.owner.lookup('service:auth');
const originalToken = auth.currentToken;
auth.currentToken = 's.xxxxxxxxx';
route.beforeModel({
to: {
queryParams: { redirect_to: 'vault/cluster/tools?namespace=admin', namespace: 'ns1', foo: 'bar' },
},
});
assert.true(
this.router.replaceWith.calledWithExactly('vault/cluster/tools?namespace=admin'),
'transitions to redirect_to path when authenticated and removes other params'
);
auth.currentToken = originalToken;
});
});