From d82d82ef804d0a3a67665a74d7c7e423c0a36aa7 Mon Sep 17 00:00:00 2001
From: claire bontempo <68122737+hellobontempo@users.noreply.github.com>
Date: Mon, 2 Jun 2025 09:05:04 -0700
Subject: [PATCH] UI: Fix direct login link for auth mounts (#30800)
* fix typos, check for supported auth method and render direct link in display view too
* add namespace
* linebreak
* add tests
---
.../components/auth-config-form/options.hbs | 4 +-
ui/app/components/auth-config-form/options.js | 4 --
ui/app/components/auth/page.ts | 4 +-
ui/app/models/auth-method.js | 14 ++++++
.../components/auth-method/configuration.hbs | 5 +++
.../components/login-settings/page/list.hbs | 2 +-
.../acceptance/settings/auth/enable-test.js | 17 +++++++
.../auth-method/configuration-test.js | 44 +++++++++++++++++++
8 files changed, 85 insertions(+), 9 deletions(-)
create mode 100644 ui/tests/integration/components/auth-method/configuration-test.js
diff --git a/ui/app/components/auth-config-form/options.hbs b/ui/app/components/auth-config-form/options.hbs
index 17bc6efaf8..8b3e45514f 100644
--- a/ui/app/components/auth-config-form/options.hbs
+++ b/ui/app/components/auth-config-form/options.hbs
@@ -11,10 +11,10 @@
{{#each @model.tuneAttrs as |attr|}}
{{#if (not (includes attr.name @model.userLockoutConfig.modelAttrs))}}
- {{#if (eq attr.name "config.listingVisibility")}}
+ {{#if (and (eq attr.name "config.listingVisibility") @model.directLoginLink)}}
UI login link:
-
+
{{/if}}
{{/if}}
diff --git a/ui/app/components/auth-config-form/options.js b/ui/app/components/auth-config-form/options.js
index 7cbcbb90a7..5f345291d0 100644
--- a/ui/app/components/auth-config-form/options.js
+++ b/ui/app/components/auth-config-form/options.js
@@ -65,8 +65,4 @@ export default class AuthConfigOptions extends AuthConfigComponent {
this.router.transitionTo('vault.cluster.access.methods').followRedirects();
this.flashMessages.success('The configuration was saved successfully.');
}
-
- get getLoginLink() {
- return `${window.origin}/ui/vault/auth?with=${encodeURIComponent(this.args.model.path)}`;
- }
}
diff --git a/ui/app/components/auth/page.ts b/ui/app/components/auth/page.ts
index 677795cf60..bba5f225cf 100644
--- a/ui/app/components/auth/page.ts
+++ b/ui/app/components/auth/page.ts
@@ -154,7 +154,7 @@ export default class AuthPage extends Component {
get initialAuthType(): string {
// First, prioritize canceledMfaAuth since it's set by user interaction.
// Next, "type" from direct link since the URL query param overrides any login settings.
- // Then, first tab which is either the first backup method or visible mount tab.
+ // Then, first tab which is either the default method, first backup method or first visible mount tab.
// Finally, fallback to the most recently used auth method in localStorage.
// Token is the default otherwise.
const directLinkType = this.args.directLinkData?.type;
@@ -192,7 +192,7 @@ export default class AuthPage extends Component {
const defaultType = loginSettings?.defaultType;
const backupTypes = loginSettings?.backupTypes;
- // If a default is not set, render backup methods as the initial view
+ // If a default type is not set, render backup methods as the initial view
const preferredTypes = defaultType ? [defaultType] : backupTypes;
let defaultView;
if (preferredTypes) {
diff --git a/ui/app/models/auth-method.js b/ui/app/models/auth-method.js
index 0e48ae6515..2ea3ef645c 100644
--- a/ui/app/models/auth-method.js
+++ b/ui/app/models/auth-method.js
@@ -13,6 +13,7 @@ import lazyCapabilities from 'vault/macros/lazy-capabilities';
import { action } from '@ember/object';
import { camelize } from '@ember/string';
import { WHITESPACE_WARNING } from 'vault/utils/forms/validators';
+import { supportedTypes } from 'vault/utils/supported-login-methods';
const validations = {
path: [
@@ -27,7 +28,9 @@ const validations = {
@withModelValidations(validations)
export default class AuthMethodModel extends Model {
+ @service namespace;
@service store;
+ @service version;
@belongsTo('mount-config', { async: false, inverse: null }) config; // one-to-none that replaces former fragment
@hasMany('auth-config', { polymorphic: true, inverse: 'backend', async: false }) authConfigs;
@@ -40,11 +43,22 @@ export default class AuthMethodModel extends Model {
get methodType() {
return this.type.replace(/^ns_/, '');
}
+
get icon() {
const authMethods = allMethods().find((backend) => backend.type === this.methodType);
return authMethods?.glyph || 'users';
}
+
+ get directLoginLink() {
+ const ns = this.namespace.path;
+ const nsQueryParam = ns ? `namespace=${encodeURIComponent(ns)}&` : '';
+ const isSupported = supportedTypes(this.version.isEnterprise).includes(this.methodType);
+ return isSupported
+ ? `${window.origin}/ui/vault/auth?${nsQueryParam}with=${encodeURIComponent(this.path)}`
+ : '';
+ }
+
@attr('string', {
editType: 'textarea',
})
diff --git a/ui/app/templates/components/auth-method/configuration.hbs b/ui/app/templates/components/auth-method/configuration.hbs
index 54d1805436..7ed94c5b24 100644
--- a/ui/app/templates/components/auth-method/configuration.hbs
+++ b/ui/app/templates/components/auth-method/configuration.hbs
@@ -4,6 +4,11 @@
}}
+ {{#if @model.directLoginLink}}
+
+
+
+ {{/if}}
{{#each @model.attrs as |attr|}}
{{#if (eq attr.type "object")}}
{{! TODO: update href with tutorial link }}
{{! }}
diff --git a/ui/tests/acceptance/settings/auth/enable-test.js b/ui/tests/acceptance/settings/auth/enable-test.js
index 48c4b18719..f988b5c928 100644
--- a/ui/tests/acceptance/settings/auth/enable-test.js
+++ b/ui/tests/acceptance/settings/auth/enable-test.js
@@ -59,6 +59,9 @@ module('Acceptance | settings/auth/enable', function (hooks) {
.dom(GENERAL.infoRowValue('Default Lease TTL'))
.hasText('1 month 1 day', 'shows system default TTL');
assert.dom(GENERAL.infoRowValue('Max Lease TTL')).hasText('1 month 1 day', 'shows the proper max TTL');
+ assert
+ .dom(GENERAL.infoRowValue('UI login link'))
+ .doesNotExist('Login link does not render for unsupported methods');
// check edit form TTL values
await click('[data-test-configure-link]');
@@ -68,4 +71,18 @@ module('Acceptance | settings/auth/enable', function (hooks) {
// cleanup
await runCmd(deleteAuthCmd(path));
});
+
+ test('it renders direct login link for supported method', async function (assert) {
+ const path = `oidc-config-${this.uid}`;
+ const type = 'oidc';
+ await visit('/vault/settings/auth/enable');
+ await mountBackend(type, path);
+ await click(GENERAL.breadcrumbAtIdx(1));
+ assert
+ .dom(GENERAL.infoRowValue('UI login link'))
+ .hasText(`${window.origin}/ui/vault/auth?with=${path}%2F`);
+
+ // cleanup
+ await runCmd(deleteAuthCmd(path));
+ });
});
diff --git a/ui/tests/integration/components/auth-method/configuration-test.js b/ui/tests/integration/components/auth-method/configuration-test.js
new file mode 100644
index 0000000000..76b4f80bed
--- /dev/null
+++ b/ui/tests/integration/components/auth-method/configuration-test.js
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render } from '@ember/test-helpers';
+import hbs from 'htmlbars-inline-precompile';
+import { GENERAL } from 'vault/tests/helpers/general-selectors';
+
+module('Integration | Component | auth-method/configuration', function (hooks) {
+ setupRenderingTest(hooks);
+
+ hooks.beforeEach(function () {
+ this.store = this.owner.lookup('service:store');
+ this.createModel = (path, type) => {
+ this.model = this.store.createRecord('auth-method', { path, type });
+ this.model.set('config', this.store.createRecord('mount-config'));
+ };
+ this.renderComponent = async () => await render(hbs``);
+ });
+
+ test('it renders direct link for supported method', async function (assert) {
+ this.createModel('token/', 'token');
+ await this.renderComponent();
+ assert.dom(GENERAL.infoRowValue('UI login link')).hasText(`${window.origin}/ui/vault/auth?with=token%2F`);
+ });
+
+ test('it does not render direct link for unsupported method', async function (assert) {
+ this.createModel('my-approle/', 'approle');
+ await this.renderComponent();
+ assert.dom(GENERAL.infoRowValue('UI login link')).doesNotExist();
+ });
+
+ test('it renders direct link if within a namespace', async function (assert) {
+ this.owner.lookup('service:namespace').set('path', 'foo/bar');
+ this.createModel('token/', 'token');
+ await this.renderComponent();
+ assert
+ .dom(GENERAL.infoRowValue('UI login link'))
+ .hasText(`${window.origin}/ui/vault/auth?namespace=foo%2Fbar&with=token%2F`);
+ });
+});