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`); + }); +});