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|}} +
+
+
+ +
{{rule.Name}}
+
+
{{rule.Namespace}}
+
+
+ + + + View + + {{#if (has-capability this.capabilities "delete" pathKey="customLogin" params=rule)}} + Delete + {{/if}} + +
+
+ {{/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();