diff --git a/changelog/16821.txt b/changelog/16821.txt new file mode 100644 index 000000000..c414d8015 --- /dev/null +++ b/changelog/16821.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: redirect_to param forwards from auth route when authenticated +``` \ No newline at end of file diff --git a/ui/app/lib/route-paths.js b/ui/app/lib/route-paths.js new file mode 100644 index 000000000..221a6401c --- /dev/null +++ b/ui/app/lib/route-paths.js @@ -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']; diff --git a/ui/app/mixins/cluster-route.js b/ui/app/mixins/cluster-route.js index 87ad33b2b..d3f83318d 100644 --- a/ui/app/mixins/cluster-route.js +++ b/ui/app/mixins/cluster-route.js @@ -1,19 +1,20 @@ import { inject as service } from '@ember/service'; import Mixin from '@ember/object/mixin'; import RSVP from 'rsvp'; -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 OIDC_PROVIDER = 'vault.cluster.oidc-provider'; -const NS_OIDC_PROVIDER = 'vault.cluster.oidc-provider-ns'; -const DR_REPLICATION_SECONDARY = 'vault.cluster.replication-dr-promote'; -const DR_REPLICATION_SECONDARY_DETAILS = 'vault.cluster.replication-dr-promote.details'; -const EXCLUDED_REDIRECT_URLS = ['/vault/logout']; - -export { INIT, UNSEAL, AUTH, CLUSTER, CLUSTER_INDEX, DR_REPLICATION_SECONDARY }; +import { + INIT, + UNSEAL, + AUTH, + CLUSTER, + CLUSTER_INDEX, + OIDC_CALLBACK, + OIDC_PROVIDER, + NS_OIDC_PROVIDER, + DR_REPLICATION_SECONDARY, + DR_REPLICATION_SECONDARY_DETAILS, + EXCLUDED_REDIRECT_URLS, + REDIRECT, +} from 'vault/lib/route-paths'; export default Mixin.create({ auth: service(), @@ -96,11 +97,14 @@ export default Mixin.create({ if ( (!cluster.needsInit && this.routeName === INIT) || (!cluster.sealed && this.routeName === UNSEAL) || - (!cluster?.dr?.isSecondary && this.routeName === DR_REPLICATION_SECONDARY) || - (isAuthed && this.routeName === AUTH) + (!cluster?.dr?.isSecondary && this.routeName === DR_REPLICATION_SECONDARY) ) { 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; }, }); diff --git a/ui/app/router.js b/ui/app/router.js index 3cd477670..0640b2b52 100644 --- a/ui/app/router.js +++ b/ui/app/router.js @@ -13,6 +13,7 @@ Router.map(function () { this.route('oidc-provider', { path: '/identity/oidc/provider/:provider_name/authorize' }); this.route('oidc-callback', { path: '/auth/*auth_path/oidc/callback' }); this.route('auth'); + this.route('redirect'); this.route('init'); this.route('logout'); this.mount('open-api-explorer', { path: '/api-explorer' }); diff --git a/ui/app/routes/vault/cluster/redirect.js b/ui/app/routes/vault/cluster/redirect.js new file mode 100644 index 000000000..2f58ddfd2 --- /dev/null +++ b/ui/app/routes/vault/cluster/redirect.js @@ -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', ''); + }); + } +} diff --git a/ui/app/templates/vault/cluster/redirect.hbs b/ui/app/templates/vault/cluster/redirect.hbs new file mode 100644 index 000000000..143f5191c --- /dev/null +++ b/ui/app/templates/vault/cluster/redirect.hbs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/tests/unit/mixins/cluster-route-test.js b/ui/tests/unit/mixins/cluster-route-test.js index 623d2d8eb..46fb5289f 100644 --- a/ui/tests/unit/mixins/cluster-route-test.js +++ b/ui/tests/unit/mixins/cluster-route-test.js @@ -8,7 +8,8 @@ import { CLUSTER, CLUSTER_INDEX, DR_REPLICATION_SECONDARY, -} from 'vault/mixins/cluster-route'; + REDIRECT, +} from 'vault/lib/route-paths'; import { module, test } from 'qunit'; 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'); 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; 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) { let redirectRouteURL = '/vault/secrets/secret/create'; let subject = createClusterRoute({ needsInit: false, sealed: false }); diff --git a/ui/tests/unit/routes/vault/cluster/redirect-test.js b/ui/tests/unit/routes/vault/cluster/redirect-test.js new file mode 100644 index 000000000..256330a6a --- /dev/null +++ b/ui/tests/unit/routes/vault/cluster/redirect-test.js @@ -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; + }); +});