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
This commit is contained in:
claire bontempo 2025-06-02 09:05:04 -07:00 committed by GitHub
parent 8442bcac7b
commit d82d82ef80
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 85 additions and 9 deletions

View File

@ -11,10 +11,10 @@
{{#each @model.tuneAttrs as |attr|}}
{{#if (not (includes attr.name @model.userLockoutConfig.modelAttrs))}}
<FormField data-test-field @attr={{attr}} @model={{@model}} />
{{#if (eq attr.name "config.listingVisibility")}}
{{#if (and (eq attr.name "config.listingVisibility") @model.directLoginLink)}}
<div class="has-top-margin-negative-s has-bottom-margin-l is-flex-center">
<Hds::Text::Body @tag="p" @color="faint">UI login link:</Hds::Text::Body>
<Hds::Copy::Snippet @textToCopy={{this.getLoginLink}} />
<Hds::Copy::Snippet @textToCopy={{@model.directLoginLink}} />
</div>
{{/if}}
{{/if}}

View File

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

View File

@ -154,7 +154,7 @@ export default class AuthPage extends Component<Args> {
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<Args> {
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) {

View File

@ -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',
})

View File

@ -4,6 +4,11 @@
}}
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
{{#if @model.directLoginLink}}
<InfoTableRow @alwaysRender={{true}} @label="UI login link">
<Hds::Copy::Snippet @textToCopy={{@model.directLoginLink}} />
</InfoTableRow>
{{/if}}
{{#each @model.attrs as |attr|}}
{{#if (eq attr.type "object")}}
<InfoTableRow

View File

@ -65,7 +65,7 @@
{{else}}
<EmptyState
@title="No UI login settings yet"
@message="Login rules can be used to select default and back up login methods and customize which methods display in the web UI login form. Available to be created via the CLI or HTTP API."
@message="Login settings can be used to customize which methods display in the web UI login form by setting a default and back up login methods. Available to be created via the CLI or HTTP API."
>
{{! TODO: update href with tutorial link }}
{{! <Hds::Link::Standalone @icon="arrow-right" @iconPosition="trailing" @text="Learn more" @href="/" /> }}

View File

@ -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));
});
});

View File

@ -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`<AuthMethod::Configuration @model={{this.model}} />`);
});
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`);
});
});