UI: Only add user lockout config for supported methods (#25867)

* update model so only supported methods add user_lockout_config params

* update auth config form to only show user lockout config for supported methods

* add changelog
This commit is contained in:
claire bontempo
2024-03-11 11:01:36 -05:00
committed by GitHub
parent f09b76fab2
commit d989eae10e
5 changed files with 198 additions and 60 deletions

3
changelog/25867.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:bug
ui: remove user_lockout_config settings for unsupported methods
```

View File

@@ -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) {

View File

@@ -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() {

View File

@@ -14,16 +14,18 @@
{{/if}}
{{/each}}
<hr class="has-top-margin-xl has-bottom-margin-l has-background-gray-200" />
<Hds::Text::Display @tag="h2" @size="400" @weight="bold">User lockout configuration</Hds::Text::Display>
<Hds::Text::Body @tag="p" @size="100" @color="faint" class="has-bottom-margin-m">
Specifies the user lockout settings for this auth mount.
</Hds::Text::Body>
{{#each this.model.tuneAttrs as |attr|}}
{{#if (includes attr.name this.model.userLockoutConfig.modelAttrs)}}
<FormField @attr={{attr}} @model={{this.model}} />
{{/if}}
{{/each}}
{{#if this.model.supportsUserLockoutConfig}}
<hr class="has-top-margin-xl has-bottom-margin-l has-background-gray-200" />
<Hds::Text::Display @tag="h2" @size="400" @weight="bold" data-test-user-lockout-section>User lockout configuration</Hds::Text::Display>
<Hds::Text::Body @tag="p" @size="100" @color="faint" class="has-bottom-margin-m">
Specifies the user lockout settings for this auth mount.
</Hds::Text::Body>
{{#each this.model.tuneAttrs as |attr|}}
{{#if (includes attr.name this.model.userLockoutConfig.modelAttrs)}}
<FormField @attr={{attr}} @model={{this.model}} />
{{/if}}
{{/each}}
{{/if}}
</div>
<div class="field is-grouped box is-fullwidth is-bottomless">
<Hds::Button

View File

@@ -9,6 +9,12 @@ import { setupMirage } from 'ember-cli-mirage/test-support';
import { click, fillIn, render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
import { SELECTORS } from 'vault/tests/helpers/general-selectors';
import { methods } from 'vault/helpers/mountable-auth-methods';
const userLockoutSupported = ['approle', 'ldap', 'userpass'];
const userLockoutUnsupported = methods()
.map((m) => 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`<AuthConfigForm::Options @model={{this.model}} />`);
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`<AuthConfigForm::Options @model={{this.model}} />`);
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`<AuthConfigForm::Options @model={{this.model}} />`);
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]');
});