diff --git a/changelog/30592.txt b/changelog/30592.txt
new file mode 100644
index 0000000000..daec2b89e4
--- /dev/null
+++ b/changelog/30592.txt
@@ -0,0 +1,7 @@
+```release-note:feature
+**Custom login settings**: Adding view to list and delete custom login rules
+```
+
+```release-note:change
+ui: 'Custom messages' renamed to 'System Messages'
+```
\ No newline at end of file
diff --git a/ui/app/components/auth-config-form/options.hbs b/ui/app/components/auth-config-form/options.hbs
index f2dd3328c0..17bc6efaf8 100644
--- a/ui/app/components/auth-config-form/options.hbs
+++ b/ui/app/components/auth-config-form/options.hbs
@@ -11,6 +11,12 @@
{{#each @model.tuneAttrs as |attr|}}
{{#if (not (includes attr.name @model.userLockoutConfig.modelAttrs))}}
+ {{#if (eq attr.name "config.listingVisibility")}}
+
+ UI login link:
+
+
+ {{/if}}
{{/if}}
{{/each}}
diff --git a/ui/app/components/auth-config-form/options.js b/ui/app/components/auth-config-form/options.js
index 5f345291d0..7cbcbb90a7 100644
--- a/ui/app/components/auth-config-form/options.js
+++ b/ui/app/components/auth-config-form/options.js
@@ -65,4 +65,8 @@ 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/sidebar/nav/cluster.hbs b/ui/app/components/sidebar/nav/cluster.hbs
index bdc32ec252..b7ab90efb4 100644
--- a/ui/app/components/sidebar/nav/cluster.hbs
+++ b/ui/app/components/sidebar/nav/cluster.hbs
@@ -122,8 +122,16 @@
Settings
+ {{#if (or this.isRootNamespace this.namespace.isHvdAdminNamespace)}}
+
+ {{/if}}
{{/if}}
\ No newline at end of file
diff --git a/ui/app/models/mount-config.js b/ui/app/models/mount-config.js
index 5b53be5b83..bee7e758d9 100644
--- a/ui/app/models/mount-config.js
+++ b/ui/app/models/mount-config.js
@@ -33,8 +33,12 @@ export default class MountConfigModel extends Model {
auditNonHmacResponseKeys;
@attr('mountVisibility', {
- editType: 'boolean',
- label: 'List method when unauthenticated',
+ label: 'Use as preferred UI login method',
+ editType: 'toggleButton',
+ helperTextEnabled:
+ 'This mount will be included in the unauthenticated UI login endpoint and display as a preferred login method.',
+ helperTextDisabled:
+ 'Turn on the toggle to use this auth mount as a preferred login method during UI login.',
defaultValue: false,
})
listingVisibility;
diff --git a/ui/app/utils/constants/capabilities.ts b/ui/app/utils/constants/capabilities.ts
index 5513ab322e..070b634683 100644
--- a/ui/app/utils/constants/capabilities.ts
+++ b/ui/app/utils/constants/capabilities.ts
@@ -16,6 +16,7 @@ export const SUDO_PATHS = [
export const SUDO_PATH_PREFIXES = ['sys/leases/revoke-prefix', 'sys/leases/revoke-force'];
export const PATH_MAP = {
+ customLogin: apiPath`sys/config/ui/login/default-auth/${'id'}`,
customMessages: apiPath`sys/config/ui/custom-messages/${'id'}`,
syncActivate: apiPath`sys/activation-flags/secrets-sync/activate`,
syncDestination: apiPath`sys/sync/destinations/${'type'}/${'name'}`,
diff --git a/ui/lib/config-ui/addon/components/login-settings/page/details.hbs b/ui/lib/config-ui/addon/components/login-settings/page/details.hbs
new file mode 100644
index 0000000000..7467bbbaef
--- /dev/null
+++ b/ui/lib/config-ui/addon/components/login-settings/page/details.hbs
@@ -0,0 +1,59 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+}}
+
+
+
+
+
+
+
+ {{@rule.name}}
+
+
+
+
+
+
+
+
+ {{#if (has-capability this.capabilities "delete" pathKey="customLogin" params=@rule)}}
+
+ {{/if}}
+
+
+{{#each-in @rule as |key value|}}
+ {{#if (eq key "defaultAuthType")}}
+
+ {{else if (eq key "backupAuthTypes")}}
+
+ {{else if (eq key "disableInheritance")}}
+
+ {{else}}
+
+ {{/if}}
+{{/each-in}}
+
+{{#if this.showConfirmModal}}
+
+{{/if}}
\ No newline at end of file
diff --git a/ui/lib/config-ui/addon/components/login-settings/page/details.js b/ui/lib/config-ui/addon/components/login-settings/page/details.js
new file mode 100644
index 0000000000..d91d5d55e6
--- /dev/null
+++ b/ui/lib/config-ui/addon/components/login-settings/page/details.js
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { service } from '@ember/service';
+import { action } from '@ember/object';
+import errorMessage from 'vault/utils/error-message';
+
+/**
+ * @module Page::LoginSettingsRuleDetails
+ * Page::LoginSettingsRuleDetails component is used to display rule information.
+ * Shows detailed data, (eg which namespace it applies to, auth type used etc.) on a selected login rule from the custom login settings list.
+ *
+ * @example
+ * ```js
+ *
+ * ```
+ * @param {object} rule - holds login rule data, { backup_auth_types: string[] eg. ['token'], default_auth_type: string "oidc", disable_inheritance: boolean,
+ * name: string "Login rule 1", namespace: string eg "admin/"}
+ * */
+
+export default class LoginSettingsRuleDetails extends Component {
+ @service capabilities;
+ @service flashMessages;
+ @service('app-router') router;
+ @service api;
+
+ @tracked showConfirmModal = false;
+
+ @action
+ async onDelete() {
+ const { rule } = this.args;
+
+ try {
+ await this.api.sys.uiLoginDefaultAuthDeleteConfiguration(rule.name);
+
+ this.flashMessages.success(`Successfully deleted rule ${rule.name}.`);
+
+ this.router.transitionTo('vault.cluster.config-ui.login-settings.index');
+ } catch (error) {
+ const message = errorMessage(error, 'Error deleting rule. Please try again.');
+ this.flashMessages.danger(message);
+ }
+ }
+}
diff --git a/ui/lib/config-ui/addon/components/login-settings/page/list.hbs b/ui/lib/config-ui/addon/components/login-settings/page/list.hbs
new file mode 100644
index 0000000000..949e7dcd83
--- /dev/null
+++ b/ui/lib/config-ui/addon/components/login-settings/page/list.hbs
@@ -0,0 +1,66 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+}}
+
+
+
+
+ UI Login Rules
+
+
+
+
+
+
+{{#if @loginRules}}
+ {{#each @loginRules as |rule|}}
+
+ {{/each}}
+{{else}}
+
+ {{! TODO: update href with tutorial link }}
+ {{! }}
+
+{{/if}}
+
+{{#if this.ruleToDelete}}
+
+{{/if}}
\ No newline at end of file
diff --git a/ui/lib/config-ui/addon/components/login-settings/page/list.js b/ui/lib/config-ui/addon/components/login-settings/page/list.js
new file mode 100644
index 0000000000..757e25fb01
--- /dev/null
+++ b/ui/lib/config-ui/addon/components/login-settings/page/list.js
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { service } from '@ember/service';
+import { action } from '@ember/object';
+import errorMessage from 'vault/utils/error-message';
+
+/**
+ * @module Page::LoginSettingsList
+ * Page::LoginSettingsList components are used to display list of rules.
+ * @example
+ * ```js
+ *
+ * ```
+ * @param {array} loginRules - array of rule objects
+ */
+
+export default class LoginSettingsList extends Component {
+ @service capabilities;
+ @service flashMessages;
+ @service('app-router') router;
+ @tracked ruleToDelete = null; // set to the rule intended to delete
+ @service api;
+
+ @action
+ async onDelete() {
+ try {
+ await this.api.sys.uiLoginDefaultAuthDeleteConfiguration(this.ruleToDelete.id);
+ this.flashMessages.success(`Successfully deleted rule ${this.ruleToDelete.id}.`);
+
+ this.router.transitionTo('vault.cluster.config-ui.login-settings');
+ } catch (error) {
+ const message = errorMessage(error, 'Error deleting rule. Please try again.');
+ this.flashMessages.danger(message);
+ }
+ }
+}
diff --git a/ui/lib/config-ui/addon/routes.js b/ui/lib/config-ui/addon/routes.js
index 14b397a940..10629d7011 100644
--- a/ui/lib/config-ui/addon/routes.js
+++ b/ui/lib/config-ui/addon/routes.js
@@ -13,4 +13,10 @@ export default buildRoutes(function () {
this.route('edit');
});
});
+
+ this.route('login-settings', function () {
+ this.route('rule', { path: '/:name' }, function () {
+ this.route('details');
+ });
+ });
});
diff --git a/ui/lib/config-ui/addon/routes/login-settings/index.js b/ui/lib/config-ui/addon/routes/login-settings/index.js
new file mode 100644
index 0000000000..73556e90c1
--- /dev/null
+++ b/ui/lib/config-ui/addon/routes/login-settings/index.js
@@ -0,0 +1,18 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Route from '@ember/routing/route';
+import { service } from '@ember/service';
+
+export default class LoginSettingsRoute extends Route {
+ @service api;
+
+ async model() {
+ const res = await this.api.sys.uiLoginDefaultAuthList(true);
+ const loginRules = this.api.keyInfoToArray({ keyInfo: res.keyInfo, keys: res.keys });
+
+ return { loginRules };
+ }
+}
diff --git a/ui/lib/config-ui/addon/routes/login-settings/rule/details.js b/ui/lib/config-ui/addon/routes/login-settings/rule/details.js
new file mode 100644
index 0000000000..338f0f8afc
--- /dev/null
+++ b/ui/lib/config-ui/addon/routes/login-settings/rule/details.js
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Route from '@ember/routing/route';
+import { service } from '@ember/service';
+
+export default class LoginSettingsRuleDetailsRoute extends Route {
+ @service('app-router') router;
+ @service api;
+
+ beforeModel() {
+ const { name } = this.paramsFor('login-settings.rule');
+ if (!name) {
+ this.router.transitionTo('vault.cluster.config-ui.login-settings.index');
+ }
+ }
+
+ async model() {
+ const { name } = this.paramsFor('login-settings.rule');
+
+ const rule = await this.api.sys.uiLoginDefaultAuthReadConfiguration(name);
+
+ return { rule: { name, ...rule.data } };
+ }
+
+ setupController(controller, resolvedModel) {
+ super.setupController(controller, resolvedModel);
+
+ controller.breadcrumbs = [
+ { label: 'UI login rules', route: 'login-settings' },
+ { label: resolvedModel.rule.name },
+ ];
+ }
+}
diff --git a/ui/lib/config-ui/addon/routes/login-settings/rule/index.js b/ui/lib/config-ui/addon/routes/login-settings/rule/index.js
new file mode 100644
index 0000000000..edcf738ee0
--- /dev/null
+++ b/ui/lib/config-ui/addon/routes/login-settings/rule/index.js
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Route from '@ember/routing/route';
+import { service } from '@ember/service';
+
+export default class LoginSettingsRuleIndexRoute extends Route {
+ @service('app-router') router;
+
+ redirect() {
+ this.router.transitionTo('vault.cluster.config-ui.login-settings.rule.details');
+ }
+}
diff --git a/ui/lib/config-ui/addon/templates/error.hbs b/ui/lib/config-ui/addon/templates/error.hbs
new file mode 100644
index 0000000000..d6c06a3caa
--- /dev/null
+++ b/ui/lib/config-ui/addon/templates/error.hbs
@@ -0,0 +1,6 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+}}
+
+
\ No newline at end of file
diff --git a/ui/lib/config-ui/addon/templates/login-settings/index.hbs b/ui/lib/config-ui/addon/templates/login-settings/index.hbs
new file mode 100644
index 0000000000..a35d4f00df
--- /dev/null
+++ b/ui/lib/config-ui/addon/templates/login-settings/index.hbs
@@ -0,0 +1,6 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+}}
+
+
\ No newline at end of file
diff --git a/ui/lib/config-ui/addon/templates/login-settings/rule/details.hbs b/ui/lib/config-ui/addon/templates/login-settings/rule/details.hbs
new file mode 100644
index 0000000000..d050739f2d
--- /dev/null
+++ b/ui/lib/config-ui/addon/templates/login-settings/rule/details.hbs
@@ -0,0 +1,6 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+}}
+
+
\ No newline at end of file
diff --git a/ui/tests/acceptance/auth/test-helper.js b/ui/tests/acceptance/auth/test-helper.js
index 531530d0af..15e883a127 100644
--- a/ui/tests/acceptance/auth/test-helper.js
+++ b/ui/tests/acceptance/auth/test-helper.js
@@ -12,6 +12,8 @@ const assertFields = (assert, fields, customSelectors = {}) => {
fields.forEach((param) => {
if (Object.keys(customSelectors).includes(param)) {
assert.dom(customSelectors[param]).exists();
+ } else if (param === 'config.listingVisibility') {
+ assert.dom(GENERAL.toggleInput('toggle-config.listingVisibility')).exists();
} else {
assert.dom(GENERAL.inputByAttr(param)).exists();
}
diff --git a/ui/tests/acceptance/config-ui/login-settings-test.js b/ui/tests/acceptance/config-ui/login-settings-test.js
new file mode 100644
index 0000000000..802a5d4e33
--- /dev/null
+++ b/ui/tests/acceptance/config-ui/login-settings-test.js
@@ -0,0 +1,82 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { module, test } from 'qunit';
+import { setupApplicationTest } from 'ember-qunit';
+import { click, visit, currentRouteName } from '@ember/test-helpers';
+import { login } from 'vault/tests/helpers/auth/auth-helpers';
+import { GENERAL } from 'vault/tests/helpers/general-selectors';
+import { runCmd } from 'vault/tests/helpers/commands';
+
+module('Acceptance | Enterprise | config-ui/login-settings', function (hooks) {
+ setupApplicationTest(hooks);
+
+ hooks.beforeEach(async function () {
+ await login();
+
+ // create login rules
+ await runCmd([
+ `write sys/config/ui/login/default-auth/testRule backup_auth_types=userpass default_auth_type=okta disable_inheritance=false namespace=ns1`,
+ 'write sys/config/ui/login/default-auth/testRule2 backup_auth_types=oidc default_auth_type=ldap disable_inheritance=true namespace=ns2',
+ ]);
+ });
+
+ hooks.afterEach(async function () {
+ await login();
+
+ // cleanup login rules
+ await runCmd([
+ 'delete sys/config/ui/login/default-auth/testRule',
+ 'delete sys/config/ui/login/default-auth/testRule2',
+ ]);
+ });
+
+ test('fetched login rule list renders', async function (assert) {
+ // Visit the login settings list index page
+ await visit('vault/config-ui/login-settings');
+
+ // verify fetched rules are rendered in list
+ assert.dom('.linked-block-item').exists({ count: 2 });
+ // verify rule data namespaces render
+ assert.dom('[data-test-rule-path="ns1/"]').exists();
+ assert.dom('[data-test-rule-path="ns2/"]').exists();
+ });
+
+ test('delete rule from list view', async function (assert) {
+ // Visit the login settings list index page
+ await visit('vault/config-ui/login-settings');
+
+ await click(GENERAL.menuTrigger);
+ await click(GENERAL.menuItem('delete-rule'));
+
+ assert.dom(GENERAL.confirmationModal).exists();
+ await click(GENERAL.confirmButton);
+
+ // verify success message from deletion
+ assert.dom(GENERAL.latestFlashContent).includesText('Successfully deleted rule testRule.');
+ assert.dom('[data-test-rule-name="testRule"]').doesNotExist();
+ });
+
+ test('navigate to rule details page and renders rule data', async function (assert) {
+ // visit individual rule page
+ await visit('vault/config-ui/login-settings');
+
+ await click(GENERAL.menuTrigger);
+ await click(GENERAL.menuItem('view-rule'));
+
+ // verify that user is redirected to the rule details page
+ assert.strictEqual(
+ currentRouteName(),
+ 'vault.cluster.config-ui.login-settings.rule.details',
+ 'goes to rule details page'
+ );
+
+ // verify fetched rule data is rendered
+ assert.dom(GENERAL.infoRowValue('Name')).hasText('testRule');
+ assert.dom(GENERAL.infoRowValue('Namespace')).hasText('ns1/');
+ assert.dom(GENERAL.infoRowValue('Backup methods')).hasText('userpass');
+ assert.dom(GENERAL.infoRowValue('Inheritance')).hasText('true');
+ });
+});
diff --git a/ui/tests/helpers/config-ui/message-selectors.ts b/ui/tests/helpers/config-ui/message-selectors.ts
index 7f3d63ced7..cf60e0b54f 100644
--- a/ui/tests/helpers/config-ui/message-selectors.ts
+++ b/ui/tests/helpers/config-ui/message-selectors.ts
@@ -7,7 +7,7 @@ export const CUSTOM_MESSAGES = {
// General selectors that are common between custom messages
inlineErrorMessage: `[data-test-inline-error-message]`,
unauthCreateFormInfo: '[data-test-unauth-info]',
- navLink: '[data-test-sidebar-nav-link="Custom Messages"]',
+ navLink: '[data-test-sidebar-nav-link="System Messages"]',
radio: (radioName: string) => `[data-test-radio="${radioName}"]`,
field: (fieldName: string) => `[data-test-field="${fieldName}"]`,
input: (input: string) => `[data-test-input="${input}"]`,
diff --git a/ui/tests/integration/components/auth-config-form/options-test.js b/ui/tests/integration/components/auth-config-form/options-test.js
index 8f51ae81eb..354ac9637a 100644
--- a/ui/tests/integration/components/auth-config-form/options-test.js
+++ b/ui/tests/integration/components/auth-config-form/options-test.js
@@ -66,7 +66,7 @@ module('Integration | Component | auth-config-form options', function (hooks) {
assert.dom('[data-test-user-lockout-section]').hasText('User lockout configuration');
- await click(GENERAL.inputByAttr('config.listingVisibility'));
+ await click(GENERAL.toggleInput('toggle-config.listingVisibility'));
await fillIn(GENERAL.inputByAttr('config.tokenType'), 'default-batch');
await click(GENERAL.ttl.toggle('Default Lease TTL'));
@@ -124,7 +124,7 @@ module('Integration | Component | auth-config-form options', function (hooks) {
.dom('[data-test-user-lockout-section]')
.doesNotExist(`${type} method does not render user lockout section`);
- await click(GENERAL.inputByAttr('config.listingVisibility'));
+ await click(GENERAL.toggleInput('toggle-config.listingVisibility'));
await fillIn(GENERAL.inputByAttr('config.tokenType'), 'default-batch');
await click(GENERAL.ttl.toggle('Default Lease TTL'));
@@ -178,7 +178,7 @@ module('Integration | Component | auth-config-form options', function (hooks) {
.dom(GENERAL.inputByAttr('config.tokenType'))
.doesNotExist('does not render tokenType for token auth method');
- await click(GENERAL.inputByAttr('config.listingVisibility'));
+ await click(GENERAL.toggleInput('toggle-config.listingVisibility'));
await click(GENERAL.ttl.toggle('Default Lease TTL'));
await fillIn(GENERAL.ttl.input('Default Lease TTL'), '30');
diff --git a/ui/tests/integration/components/sidebar/nav/cluster-test.js b/ui/tests/integration/components/sidebar/nav/cluster-test.js
index e2ea7593c6..86d3bf6e7d 100644
--- a/ui/tests/integration/components/sidebar/nav/cluster-test.js
+++ b/ui/tests/integration/components/sidebar/nav/cluster-test.js
@@ -74,7 +74,8 @@ module('Integration | Component | sidebar-nav-cluster', function (hooks) {
'Vault Usage',
'License',
'Seal Vault',
- 'Custom Messages',
+ 'System Messages',
+ 'UI Login Rules',
];
stubFeaturesAndPermissions(this.owner, true, true);
await renderComponent();