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]'); });