diff --git a/changelog/25867.txt b/changelog/25867.txt
new file mode 100644
index 0000000000..c7611aaa86
--- /dev/null
+++ b/changelog/25867.txt
@@ -0,0 +1,3 @@
+```release-note:bug
+ui: remove user_lockout_config settings for unsupported methods
+```
diff --git a/ui/app/components/auth-config-form/options.js b/ui/app/components/auth-config-form/options.js
index e665830469..9c12b16adf 100644
--- a/ui/app/components/auth-config-form/options.js
+++ b/ui/app/components/auth-config-form/options.js
@@ -30,20 +30,22 @@ export default AuthConfigComponent.extend({
waitFor(function* () {
const data = this.model.config.serialize();
data.description = this.model.description;
- data.user_lockout_config = {};
+
+ if (this.model.supportsUserLockoutConfig) {
+ data.user_lockout_config = {};
+ this.model.userLockoutConfig.apiParams.forEach((attr) => {
+ if (Object.keys(data).includes(attr)) {
+ data.user_lockout_config[attr] = data[attr];
+ delete data[attr];
+ }
+ });
+ }
// token_type should not be tuneable for the token auth method.
if (this.model.methodType === 'token') {
delete data.token_type;
}
- this.model.userLockoutConfig.apiParams.forEach((attr) => {
- if (Object.keys(data).includes(attr)) {
- data.user_lockout_config[attr] = data[attr];
- delete data[attr];
- }
- });
-
try {
yield this.model.tune(data);
} catch (err) {
diff --git a/ui/app/models/auth-method.js b/ui/app/models/auth-method.js
index 459852ca70..d1b64b10bc 100644
--- a/ui/app/models/auth-method.js
+++ b/ui/app/models/auth-method.js
@@ -11,6 +11,7 @@ import { withModelValidations } from 'vault/decorators/model-validations';
import { allMethods } from 'vault/helpers/mountable-auth-methods';
import lazyCapabilities from 'vault/macros/lazy-capabilities';
import { action } from '@ember/object';
+import { camelize } from '@ember/string';
const validations = {
path: [
@@ -68,6 +69,10 @@ export default class AuthMethodModel extends Model {
return this.local ? 'local' : 'replicated';
}
+ get supportsUserLockoutConfig() {
+ return ['approle', 'ldap', 'userpass'].includes(this.methodType);
+ }
+
userLockoutConfig = {
modelAttrs: [
'config.lockoutThreshold',
@@ -79,21 +84,21 @@ export default class AuthMethodModel extends Model {
};
get tuneAttrs() {
- const { methodType } = this;
- let tuneAttrs;
- // token_type should not be tuneable for the token auth method
- if (methodType === 'token') {
- tuneAttrs = [
- 'description',
- 'config.{listingVisibility,defaultLeaseTtl,maxLeaseTtl,auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders,allowedResponseHeaders,pluginVersion,lockoutThreshold,lockoutDuration,lockoutCounterReset,lockoutDisable}',
- ];
- } else {
- tuneAttrs = [
- 'description',
- 'config.{listingVisibility,defaultLeaseTtl,maxLeaseTtl,tokenType,auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders,allowedResponseHeaders,pluginVersion,lockoutThreshold,lockoutDuration,lockoutCounterReset,lockoutDisable}',
- ];
- }
- return expandAttributeMeta(this, tuneAttrs);
+ // order here determines order tune fields render
+ const tuneAttrs = [
+ 'listingVisibility',
+ 'defaultLeaseTtl',
+ 'maxLeaseTtl',
+ ...(this.methodType === 'token' ? [] : ['tokenType']),
+ 'auditNonHmacRequestKeys',
+ 'auditNonHmacResponseKeys',
+ 'passthroughRequestHeaders',
+ 'allowedResponseHeaders',
+ 'pluginVersion',
+ ...(this.supportsUserLockoutConfig ? this.userLockoutConfig.apiParams.map((a) => camelize(a)) : []),
+ ];
+
+ return expandAttributeMeta(this, ['description', `config.{${tuneAttrs.join(',')}}`]);
}
get formFields() {
diff --git a/ui/app/templates/components/auth-config-form/options.hbs b/ui/app/templates/components/auth-config-form/options.hbs
index 56cd7cdc9d..e77b26155a 100644
--- a/ui/app/templates/components/auth-config-form/options.hbs
+++ b/ui/app/templates/components/auth-config-form/options.hbs
@@ -14,16 +14,18 @@
{{/if}}
{{/each}}
-
- User lockout configuration
-
- Specifies the user lockout settings for this auth mount.
-
- {{#each this.model.tuneAttrs as |attr|}}
- {{#if (includes attr.name this.model.userLockoutConfig.modelAttrs)}}
-
- {{/if}}
- {{/each}}
+ {{#if this.model.supportsUserLockoutConfig}}
+
+ User lockout configuration
+
+ Specifies the user lockout settings for this auth mount.
+
+ {{#each this.model.tuneAttrs as |attr|}}
+ {{#if (includes attr.name this.model.userLockoutConfig.modelAttrs)}}
+
+ {{/if}}
+ {{/each}}
+ {{/if}}
m.type)
+ .filter((m) => !userLockoutSupported.includes(m));
module('Integration | Component | auth-config-form options', function (hooks) {
setupRenderingTest(hooks);
@@ -18,57 +24,177 @@ module('Integration | Component | auth-config-form options', function (hooks) {
this.owner.lookup('service:flash-messages').registerTypes(['success']);
this.router = this.owner.lookup('service:router');
this.store = this.owner.lookup('service:store');
- this.path = 'my-auth-method/';
- this.model = this.store.createRecord('auth-method', { path: this.path, type: 'approle' });
- this.model.set('config', this.store.createRecord('mount-config'));
+ this.createModel = (path, type) => {
+ this.model = this.store.createRecord('auth-method', { path, type });
+ this.model.set('config', this.store.createRecord('mount-config'));
+ };
});
- test('it submits data correctly', async function (assert) {
- assert.expect(2);
+ for (const type of userLockoutSupported) {
+ test(`it submits data correctly for ${type} method (supports user_lockout_config)`, async function (assert) {
+ assert.expect(3);
+ const path = `my-${type}-auth/`;
+ this.createModel(path, type);
+
+ this.router.reopen({
+ transitionTo() {
+ return {
+ followRedirects() {
+ assert.ok(true, `saving ${type} calls transitionTo on save`);
+ },
+ };
+ },
+ });
+
+ this.server.post(`sys/mounts/auth/${path}/tune`, (schema, req) => {
+ const payload = JSON.parse(req.requestBody);
+ const expected = {
+ default_lease_ttl: '30s',
+ listing_visibility: 'unauth',
+ token_type: 'default-batch',
+ user_lockout_config: {
+ lockout_threshold: '7',
+ lockout_duration: '600s',
+ lockout_counter_reset: '5s',
+ lockout_disable: true,
+ },
+ };
+ assert.propEqual(payload, expected, `${type} method payload contains tune parameters`);
+ return { payload };
+ });
+ await render(hbs``);
+
+ assert.dom('[data-test-user-lockout-section]').hasText('User lockout configuration');
+
+ await click(SELECTORS.inputByAttr('config.listingVisibility'));
+ await fillIn(SELECTORS.inputByAttr('config.tokenType'), 'default-batch');
+
+ await click(SELECTORS.ttl.toggle('Default Lease TTL'));
+ await fillIn(SELECTORS.ttl.input('Default Lease TTL'), '30');
+
+ await fillIn(SELECTORS.inputByAttr('config.lockoutThreshold'), '7');
+
+ await click(SELECTORS.ttl.toggle('Lockout duration'));
+ await fillIn(SELECTORS.ttl.input('Lockout duration'), '10');
+ await fillIn(
+ `${SELECTORS.inputByAttr('config.lockoutDuration')} ${SELECTORS.selectByAttr('ttl-unit')}`,
+ 'm'
+ );
+ await click(SELECTORS.ttl.toggle('Lockout counter reset'));
+ await fillIn(SELECTORS.ttl.input('Lockout counter reset'), '5');
+
+ await click(SELECTORS.inputByAttr('config.lockoutDisable'));
+
+ await click('[data-test-save-config]');
+ });
+ }
+
+ for (const type of userLockoutUnsupported) {
+ if (type === 'token') return; // separate test below because does not include tokenType field
+
+ test(`it submits data correctly for ${type} auth method`, async function (assert) {
+ assert.expect(7);
+
+ const path = `my-${type}-auth/`;
+ this.createModel(path, type);
+
+ this.router.reopen({
+ transitionTo() {
+ return {
+ followRedirects() {
+ assert.ok(true, `saving ${type} calls transitionTo on save`);
+ },
+ };
+ },
+ });
+
+ this.server.post(`sys/mounts/auth/${path}/tune`, (schema, req) => {
+ const payload = JSON.parse(req.requestBody);
+ const expected = {
+ default_lease_ttl: '30s',
+ listing_visibility: 'unauth',
+ token_type: 'default-batch',
+ };
+ assert.propEqual(payload, expected, `${type} method payload contains tune parameters`);
+ return { payload };
+ });
+ await render(hbs``);
+
+ assert
+ .dom('[data-test-user-lockout-section]')
+ .doesNotExist(`${type} method does not render user lockout section`);
+
+ await click(SELECTORS.inputByAttr('config.listingVisibility'));
+ await fillIn(SELECTORS.inputByAttr('config.tokenType'), 'default-batch');
+
+ await click(SELECTORS.ttl.toggle('Default Lease TTL'));
+ await fillIn(SELECTORS.ttl.input('Default Lease TTL'), '30');
+
+ assert
+ .dom(SELECTORS.inputByAttr('config.lockoutThreshold'))
+ .doesNotExist(`${type} method does not render lockout threshold`);
+ assert
+ .dom(SELECTORS.ttl.toggle('Lockout duration'))
+ .doesNotExist(`${type} method does not render lockout duration `);
+ assert
+ .dom(SELECTORS.ttl.toggle('Lockout counter reset'))
+ .doesNotExist(`${type} method does not render lockout counter reset`);
+ assert
+ .dom(SELECTORS.inputByAttr('config.lockoutDisable'))
+ .doesNotExist(`${type} method does not render lockout disable`);
+
+ await click('[data-test-save-config]');
+ });
+ }
+
+ test('it submits data correctly for token auth method', async function (assert) {
+ assert.expect(8);
+ const type = 'token';
+ const path = `my-${type}-auth/`;
+ this.createModel(path, type);
+
this.router.reopen({
transitionTo() {
return {
followRedirects() {
- assert.ok('calls transitionTo on save');
+ assert.ok(true, `saving token calls transitionTo on save`);
},
};
},
});
- this.server.post(`sys/mounts/auth/${this.path}/tune`, (schema, req) => {
+ this.server.post(`sys/mounts/auth/${path}/tune`, (schema, req) => {
const payload = JSON.parse(req.requestBody);
const expected = {
default_lease_ttl: '30s',
listing_visibility: 'unauth',
- user_lockout_config: {
- lockout_threshold: '7',
- lockout_duration: '600s',
- lockout_counter_reset: '5s',
- lockout_disable: true,
- },
};
- assert.propEqual(payload, expected, 'payload contains tune parameters');
+ assert.propEqual(payload, expected, `${type} method payload contains tune parameters`);
return { payload };
});
await render(hbs``);
- await click(SELECTORS.inputByAttr('config.listingVisibility'));
+ assert
+ .dom(SELECTORS.inputByAttr('config.tokenType'))
+ .doesNotExist('does not render tokenType for token auth method');
+ await click(SELECTORS.inputByAttr('config.listingVisibility'));
await click(SELECTORS.ttl.toggle('Default Lease TTL'));
await fillIn(SELECTORS.ttl.input('Default Lease TTL'), '30');
- await fillIn(SELECTORS.inputByAttr('config.lockoutThreshold'), '7');
-
- await click(SELECTORS.ttl.toggle('Lockout duration'));
- await fillIn(SELECTORS.ttl.input('Lockout duration'), '10');
- await fillIn(
- `${SELECTORS.inputByAttr('config.lockoutDuration')} ${SELECTORS.selectByAttr('ttl-unit')}`,
- 'm'
- );
- await click(SELECTORS.ttl.toggle('Lockout counter reset'));
- await fillIn(SELECTORS.ttl.input('Lockout counter reset'), '5');
-
- await click(SELECTORS.inputByAttr('config.lockoutDisable'));
+ assert.dom('[data-test-user-lockout-section]').doesNotExist('token does not render user lockout section');
+ assert
+ .dom(SELECTORS.inputByAttr('config.lockoutThreshold'))
+ .doesNotExist('token method does not render lockout threshold');
+ assert
+ .dom(SELECTORS.ttl.toggle('Lockout duration'))
+ .doesNotExist('token method does not render lockout duration ');
+ assert
+ .dom(SELECTORS.ttl.toggle('Lockout counter reset'))
+ .doesNotExist('token method does not render lockout counter reset');
+ assert
+ .dom(SELECTORS.inputByAttr('config.lockoutDisable'))
+ .doesNotExist('token method does not render lockout disable');
await click('[data-test-save-config]');
});