{{#if this.showCustomAuthOptions}}
- {{! URL contains "with" query param and it references a visible mount }}
- {{! treat it as a "preferred" mount and hide all other tabs }}
- {{#if @directLinkData.hasMountData}}
- {{#let @directLinkData as |mount|}}
-
- {{/let}}
+ {{#if @directLinkData.isVisibleMount}}
+ {{! URL contains a "with" query param that references a mount with listing_visibility="unauth" }}
+ {{! Treat it as a "preferred" mount and hide all other tabs }}
+
{{else}}
@@ -72,7 +66,7 @@
{{#each this.availableMethodTypes as |type|}}
{{/each}}
diff --git a/ui/app/components/auth/form-template.ts b/ui/app/components/auth/form-template.ts
index 779277fa1f..8b7a04b0a3 100644
--- a/ui/app/components/auth/form-template.ts
+++ b/ui/app/components/auth/form-template.ts
@@ -7,7 +7,7 @@ import Component from '@glimmer/component';
import { service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
-import { ALL_LOGIN_METHODS, supportedTypes } from 'vault/utils/supported-login-methods';
+import { supportedTypes } from 'vault/utils/supported-login-methods';
import { getRelativePath } from 'core/utils/sanitize-path';
import type AuthService from 'vault/vault/services/auth';
@@ -15,7 +15,7 @@ import type FlagsService from 'vault/services/flags';
import type Store from '@ember-data/store';
import type VersionService from 'vault/services/version';
import type ClusterModel from 'vault/models/cluster';
-import type { AuthTabData, AuthTabMountData } from 'vault/vault/auth/form';
+import type { UnauthMountsByType, AuthTabMountData } from 'vault/vault/auth/form';
import type { HTMLElementEvent } from 'vault/forms';
/**
@@ -29,26 +29,26 @@ import type { HTMLElementEvent } from 'vault/forms';
* dynamically renders the corresponding form.
*
*
- * @param {object} authTabData - auth methods to render as tabs, contains mount data for any mounts with listing_visibility="unauth"
- * @param {object} cluster - The route model which is the ember data cluster model. contains information such as cluster id, name and boolean for if the cluster is in standby
- * @param {function} handleNamespaceUpdate - callback task that passes user input to the controller and updates the namespace query param in the url
- * @param {boolean} hasVisibleAuthMounts - whether or not any mounts have been tuned with listing_visibility="unauth"
- * @param {string} namespaceQueryParam - namespace query param from the url
- * @param {function} onSuccess - callback after the initial authentication request, if an mfa_requirement exists the parent renders the mfa form otherwise it fires the authSuccess action in the auth controller and handles transitioning to the app
* @param {string} canceledMfaAuth - saved auth type from a cancelled mfa verification
+ * @param {object} cluster - The route model which is the ember data cluster model. contains information such as cluster id, name and boolean for if the cluster is in standby
* @param {object} directLinkData - mount data built from the "with" query param. If param is a mount path and maps to a visible mount, the login form defaults to this mount. Otherwise the form preselects the passed auth type.
+ * @param {function} handleNamespaceUpdate - callback task that passes user input to the controller and updates the namespace query param in the url
+ * @param {string} namespaceQueryParam - namespace query param from the url
+ * @param {string} oidcProviderQueryParam - oidc provider query param, set in url as "?o=someprovider". if present, disables the namespace input
+ * @param {function} onSuccess - callback after the initial authentication request, if an mfa_requirement exists the parent renders the mfa form otherwise it fires the authSuccess action in the auth controller and handles transitioning to the app
+ * @param {object} visibleMountsByType - auth methods to render as tabs, contains mount data for any mounts with listing_visibility="unauth"
*
* */
interface Args {
- authTabData: AuthTabData;
- cluster: ClusterModel;
- handleNamespaceUpdate: CallableFunction;
- hasVisibleAuthMounts: boolean;
- namespaceQueryParam: string;
- onSuccess: CallableFunction;
canceledMfaAuth: string;
- directLinkData: (AuthTabMountData & { hasMountData: boolean }) | null;
+ cluster: ClusterModel;
+ directLinkData: (AuthTabMountData & { isVisibleMount: boolean }) | null;
+ handleNamespaceUpdate: CallableFunction;
+ namespaceQueryParam: string;
+ oidcProviderQueryParam: string;
+ onSuccess: CallableFunction;
+ visibleMountsByType: UnauthMountsByType;
}
export default class AuthFormTemplate extends Component {
@@ -64,13 +64,8 @@ export default class AuthFormTemplate extends Component {
@tracked selectedAuthMethod = '';
@tracked errorMessage = '';
- displayName = (type: string) => {
- const displayName = ALL_LOGIN_METHODS?.find((t) => t.type === type)?.displayName;
- return displayName || type;
- };
-
get authTabTypes() {
- const visibleMounts = this.args.authTabData;
+ const visibleMounts = this.args.visibleMountsByType;
return visibleMounts ? Object.keys(visibleMounts) : [];
}
@@ -111,7 +106,7 @@ export default class AuthFormTemplate extends Component {
// This getter determines whether to render an alternative view (e.g., tabs or a preferred mount).
// If `true`, the "Sign in with other methods →" link is shown.
get showCustomAuthOptions() {
- const hasLoginCustomization = this.args?.directLinkData?.hasMountData || this.args.hasVisibleAuthMounts;
+ const hasLoginCustomization = this.args?.directLinkData?.isVisibleMount || this.args.visibleMountsByType;
// Show if customization exists and the user has NOT clicked "Sign in with other methods →"
return hasLoginCustomization && !this.showOtherMethods;
}
@@ -123,12 +118,12 @@ export default class AuthFormTemplate extends Component {
this.setAuthType(this.preselectedType);
} else {
// if nothing has been preselected, select first tab or set to 'token'
- const authType = this.args.hasVisibleAuthMounts ? (this.authTabTypes[0] as string) : 'token';
+ const authType = this.args.visibleMountsByType ? (this.authTabTypes[0] as string) : 'token';
this.setAuthType(authType);
}
// DETERMINES INITIAL RENDER: custom selection (direct link or tabs) vs dropdown
- if (this.args.hasVisibleAuthMounts) {
+ if (this.args.visibleMountsByType) {
// render tabs if selectedAuthMethod is one, otherwise render dropdown (i.e. showOtherMethods = false)
this.showOtherMethods = this.authTabTypes.includes(this.selectedAuthMethod) ? false : true;
} else {
diff --git a/ui/app/components/auth/mounts-display.hbs b/ui/app/components/auth/mounts-display.hbs
new file mode 100644
index 0000000000..841986f326
--- /dev/null
+++ b/ui/app/components/auth/mounts-display.hbs
@@ -0,0 +1,33 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+}}
+
+{{#if (gt @mounts.length 1)}}
+ {{! render dropdown of mount paths }}
+
+ Mount path
+
+ {{#each @mounts as |mount|}}
+
+ {{/each}}
+
+
+{{else}}
+ {{! render a single mount path }}
+ {{#let (get @mounts "0") as |mount|}}
+ {{#unless @hideType}}
+
+ {{auth-display-name mount.type}}
+
+ {{/unless}}
+ {{#if mount.description}}
+ {{mount.description}}
+ {{/if}}
+ {{! the token auth method does't support custom paths so no need to render an input }}
+ {{#if @shouldRenderPath}}
+ {{! path is hidden so it is submitted with FormData but does not clutter the login form }}
+
+ {{/if}}
+ {{/let}}
+{{/if}}
\ No newline at end of file
diff --git a/ui/app/components/auth/page.hbs b/ui/app/components/auth/page.hbs
index 68306d7672..a0fd536c65 100644
--- a/ui/app/components/auth/page.hbs
+++ b/ui/app/components/auth/page.hbs
@@ -50,15 +50,14 @@
/>
{{else}}
{{/if}}
diff --git a/ui/app/components/auth/page.js b/ui/app/components/auth/page.js
index 6b13114096..dbb0841525 100644
--- a/ui/app/components/auth/page.js
+++ b/ui/app/components/auth/page.js
@@ -30,7 +30,7 @@ import { action } from '@ember/object';
* @param {string} oidcProviderQueryParam - oidc provider query param, set in url as "?o=someprovider"
* @param {function} onAuthSuccess - callback task in controller that receives the auth response (after MFA, if enabled) when login is successful
* @param {function} onNamespaceUpdate - callback task that passes user input to the controller to update the login namespace in the url query params
- * @param {object} visibleAuthMounts - keys are auth methods to render as tabs, values is an array of mount data for mounts with listing_visibility="unauth"
+ * @param {object} visibleAuthMounts - mount paths with listing_visibility="unauth", keys are the mount path and value is it's mount data such as "type" or "description," if it exists
* */
export const CSP_ERROR =
@@ -43,7 +43,7 @@ export default class AuthPage extends Component {
@tracked mfaAuthData;
@tracked mfaErrors = '';
- get authTabData() {
+ get visibleMountsByType() {
const visibleAuthMounts = this.args.visibleAuthMounts;
if (visibleAuthMounts) {
const authMounts = visibleAuthMounts;
diff --git a/ui/app/components/auth/single-mount.hbs b/ui/app/components/auth/single-mount.hbs
deleted file mode 100644
index 7e8deb5873..0000000000
--- a/ui/app/components/auth/single-mount.hbs
+++ /dev/null
@@ -1,16 +0,0 @@
-{{!
- Copyright (c) HashiCorp, Inc.
- SPDX-License-Identifier: BUSL-1.1
-}}
-
-{{#if @type}}
- {{@type}}
-{{/if}}
-{{#if @description}}
- {{@description}} data-test-description
-{{/if}}
-{{! the token auth method does't support custom paths so no need to render an input }}
-{{#if @shouldRenderPath}}
- {{! path is hidden so it is submitted with FormData but does not clutter the login form }}
-
-{{/if}}
\ No newline at end of file
diff --git a/ui/app/components/auth/tabs.hbs b/ui/app/components/auth/tabs.hbs
index b9aa66bdfa..1a8c22c172 100644
--- a/ui/app/components/auth/tabs.hbs
+++ b/ui/app/components/auth/tabs.hbs
@@ -5,32 +5,18 @@
{{#each-in @authTabData as |methodType mounts|}}
- {{@displayNameHelper methodType}}
+ {{auth-display-name methodType}}
{{! Elements "behind" tabs always render on the DOM and are just superficially hidden/shown.
However, for accessibility, we only want to render form inputs relevant to the selected method.
By wrapping the elements in this conditional, it only renders them when the tab is selected. }}
{{#if (eq @selectedAuthMethod methodType)}}
- {{#if (gt mounts.length 1)}}
- {{! render dropdown of mount paths }}
-
- Mount path
-
- {{#each mounts as |mount|}}
-
- {{/each}}
-
-
- {{else}}
- {{#let (get mounts "0") as |mount|}}
-
- {{/let}}
- {{/if}}
+
{{/if}}
diff --git a/ui/app/components/auth/tabs.ts b/ui/app/components/auth/tabs.ts
index 0961fdb2d8..c14a2eba43 100644
--- a/ui/app/components/auth/tabs.ts
+++ b/ui/app/components/auth/tabs.ts
@@ -6,24 +6,27 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
-import type { AuthTabData } from 'vault/vault/auth/form';
+import type { UnauthMountsByType } from 'vault/vault/auth/form';
interface Args {
- authTabData: AuthTabData;
- authTabTypes: string[];
+ authTabData: UnauthMountsByType;
handleTabClick: CallableFunction;
selectedAuthMethod: string;
}
export default class AuthTabs extends Component {
+ get tabTypes() {
+ return this.args.authTabData ? Object.keys(this.args.authTabData) : [];
+ }
+
get selectedTabIndex() {
- const index = this.args.authTabTypes.indexOf(this.args.selectedAuthMethod);
+ const index = this.tabTypes.indexOf(this.args.selectedAuthMethod);
// negative index means the selected method isn't a tab, default to first tab
return index < 0 ? 0 : index;
}
@action
onClickTab(_event: Event, idx: number) {
- this.args.handleTabClick(this.args.authTabTypes[idx]);
+ this.args.handleTabClick(this.tabTypes[idx]);
}
}
diff --git a/ui/app/helpers/auth-display-name.ts b/ui/app/helpers/auth-display-name.ts
new file mode 100644
index 0000000000..6014a0947d
--- /dev/null
+++ b/ui/app/helpers/auth-display-name.ts
@@ -0,0 +1,11 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { ALL_LOGIN_METHODS } from 'vault/utils/supported-login-methods';
+
+export default function authDisplayName(methodType: string) {
+ const displayName = ALL_LOGIN_METHODS?.find((t) => t.type === methodType)?.displayName;
+ return displayName || methodType;
+}
diff --git a/ui/app/routes/vault/cluster/auth.js b/ui/app/routes/vault/cluster/auth.js
index 58b145ec47..cb97fbbd25 100644
--- a/ui/app/routes/vault/cluster/auth.js
+++ b/ui/app/routes/vault/cluster/auth.js
@@ -106,11 +106,11 @@ export default class AuthRoute extends ClusterRouteBase {
*/
getMountOrTypeData(authMount, visibleAuthMounts) {
if (visibleAuthMounts?.[authMount]) {
- return { path: authMount, ...visibleAuthMounts[authMount], hasMountData: true };
+ return { path: authMount, ...visibleAuthMounts[authMount], isVisibleMount: true };
}
const types = supportedTypes(this.version.isEnterprise);
if (types.includes(sanitizePath(authMount))) {
- return { type: authMount, hasMountData: false };
+ return { type: authMount, isVisibleMount: false };
}
// `type` is necessary because it determines which login fields to render.
// If we can't safely glean it from the query param, ignore it and return null
diff --git a/ui/tests/acceptance/auth/auth-test.js b/ui/tests/acceptance/auth/auth-test.js
index e8c748c62f..4b24db27b1 100644
--- a/ui/tests/acceptance/auth/auth-test.js
+++ b/ui/tests/acceptance/auth/auth-test.js
@@ -103,8 +103,8 @@ module('Acceptance | auth login form', function (hooks) {
test('it renders preferred mount view if "with" query param is a mount path with listing_visibility="unauth"', async function (assert) {
await visit('/vault/auth?with=my-oidc%2F');
- await waitFor(AUTH_FORM.preferredMethod('OIDC'));
- assert.dom(AUTH_FORM.preferredMethod('OIDC')).hasText('OIDC', 'it renders mount type');
+ await waitFor(AUTH_FORM.preferredMethod('oidc'));
+ assert.dom(AUTH_FORM.preferredMethod('oidc')).hasText('OIDC', 'it renders mount type');
assert.dom(GENERAL.inputByAttr('role')).exists();
assert.dom(GENERAL.inputByAttr('path')).hasAttribute('type', 'hidden');
assert.dom(GENERAL.inputByAttr('path')).hasValue('my-oidc/');
@@ -122,7 +122,7 @@ module('Acceptance | auth login form', function (hooks) {
assert
.dom(AUTH_FORM.tabBtn('oidc'))
.hasAttribute('aria-selected', 'true', 'it selects tab matching query param');
- assert.dom(AUTH_FORM.preferredMethod('OIDC')).doesNotExist('it does not render single mount view');
+ assert.dom(AUTH_FORM.preferredMethod('oidc')).doesNotExist('it does not render single mount view');
assert.dom(GENERAL.backButton).doesNotExist();
});
diff --git a/ui/tests/helpers/auth/auth-form-selectors.ts b/ui/tests/helpers/auth/auth-form-selectors.ts
index 7cf6b35aa2..5f4f34cf90 100644
--- a/ui/tests/helpers/auth/auth-form-selectors.ts
+++ b/ui/tests/helpers/auth/auth-form-selectors.ts
@@ -7,7 +7,7 @@ export const AUTH_FORM = {
selectMethod: '[data-test-select="auth type"]',
form: '[data-test-auth-form]',
login: '[data-test-auth-submit]',
- preferredMethod: (displayName: string) => `p[data-test-auth-method="${displayName}"]`, // display name => i.e "OIDC" not "oidc"
+ preferredMethod: (method: string) => `p[data-test-auth-method="${method}"]`,
tabs: '[data-test-auth-tab]',
tabBtn: (method: string) => `[data-test-auth-tab="${method}"] button`, // method is all lowercased
description: '[data-test-description]',
diff --git a/ui/tests/integration/components/auth/form-template-test.js b/ui/tests/integration/components/auth/form-template-test.js
index 5907df9e03..99eeee1304 100644
--- a/ui/tests/integration/components/auth/form-template-test.js
+++ b/ui/tests/integration/components/auth/form-template-test.js
@@ -25,12 +25,12 @@ module('Integration | Component | auth | form template', function (hooks) {
setupMirage(hooks);
hooks.beforeEach(function () {
+ window.localStorage.clear();
this.version = this.owner.lookup('service:version');
- this.authTabData = null;
+ this.visibleMountsByType = null;
this.cluster = { id: '1' };
this.directLinkData = null;
this.handleNamespaceUpdate = sinon.spy();
- this.hasVisibleAuthMounts = false;
this.namespaceQueryParam = '';
this.oidcProviderQueryParam = '';
this.onSuccess = sinon.spy();
@@ -39,11 +39,10 @@ module('Integration | Component | auth | form template', function (hooks) {
this.renderComponent = () => {
return render(hbs`