UI: Changing polarity for toggle logic with skip import rotation for roles (#30055)

* changing polarity for skip import toggles

* changelog & test fix

* tests

* adding to workflow test

* rename

* add opposite check and remove default

* connection change

* using beforeEach for static tests
This commit is contained in:
Dan Rivera 2025-03-31 11:12:15 -04:00 committed by GitHub
parent 0ea9fa19c4
commit 0df36cd8a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 141 additions and 23 deletions

3
changelog/30055.txt Normal file
View file

@ -0,0 +1,3 @@
```release-note:improvement
ui/database: Updating toggle buttons for skip_rotation_import to reverse polarity of values that get displayed versus whats sent to api
```

View file

@ -202,10 +202,11 @@ export default Model.extend({
// ENTERPRISE ONLY
skip_static_role_rotation_import: attr({
editType: 'toggleButton',
label: 'Skip initial rotation on static roles',
helperTextDisabled: 'Vault automatically rotates static roles upon their initial creation.',
helperTextEnabled: 'Vault will not automatically rotate static role passwords upon creation.',
label: 'Rotate static roles immediately',
helperTextEnabled: 'Vault automatically rotates static roles upon their initial creation.',
helperTextDisabled: 'Vault will not automatically rotate static role passwords upon creation.',
defaultValue: false,
isOppositeValue: true,
}),
self_managed: attr('boolean', {

View file

@ -3,6 +3,7 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import Model, { attr } from '@ember-data/model';
import { service } from '@ember/service';
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
import { getRoleFields } from 'vault/utils/model-helpers/database-helpers';
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
@ -24,9 +25,14 @@ const validations = {
};
@withModelValidations(validations)
export default class RoleModel extends Model {
@service version;
idPrefix = 'role/';
@attr('string', { readOnly: true }) backend;
@attr('string', { label: 'Role name' }) name;
@attr('array', {
label: 'Connection name',
editType: 'searchSelect',
@ -37,12 +43,14 @@ export default class RoleModel extends Model {
subText: 'The database connection for which credentials will be generated.',
})
database;
@attr('string', {
label: 'Type of role',
noDefault: true,
possibleValues: ['static', 'dynamic'],
})
type;
@attr({
editType: 'ttl',
defaultValue: '1h',
@ -51,6 +59,7 @@ export default class RoleModel extends Model {
defaultShown: 'Engine default',
})
default_ttl;
@attr({
editType: 'ttl',
defaultValue: '24h',
@ -59,7 +68,9 @@ export default class RoleModel extends Model {
defaultShown: 'Engine default',
})
max_ttl;
@attr('string', { subText: 'The database username that this Vault role corresponds to.' }) username;
@attr({
editType: 'ttl',
defaultValue: '24h',
@ -68,38 +79,36 @@ export default class RoleModel extends Model {
helperTextEnabled: 'Vault will rotate password after.',
})
rotation_period;
@attr({
label: 'Skip initial rotation',
editType: 'toggleButton',
defaultValue: false, // this defaultValue will be set in database-role-setting-form.js based on parent database value
helperTextDisabled: 'Vault will rotate password for this static role on creation.',
helperTextEnabled: "Vault will not rotate this role's password on creation.",
})
skip_import_rotation;
@attr('array', {
editType: 'stringArray',
})
creation_statements;
@attr('array', {
editType: 'stringArray',
defaultShown: 'Default',
})
revocation_statements;
@attr('array', {
editType: 'stringArray',
defaultShown: 'Default',
})
rotation_statements;
@attr('array', {
editType: 'stringArray',
defaultShown: 'Default',
})
rollback_statements;
@attr('array', {
editType: 'stringArray',
defaultShown: 'Default',
})
renew_statements;
@attr('string', {
editType: 'json',
allowReset: true,
@ -107,6 +116,7 @@ export default class RoleModel extends Model {
defaultShown: 'Default',
})
creation_statement;
@attr('string', {
editType: 'json',
allowReset: true,
@ -114,6 +124,17 @@ export default class RoleModel extends Model {
defaultShown: 'Default',
})
revocation_statement;
// ENTERPRISE ONLY
@attr({
label: 'Rotate immediately',
editType: 'toggleButton',
helperTextEnabled: 'Vault will rotate the password for this static role on creation.',
helperTextDisabled: "Vault will not rotate this role's password on creation.",
isOppositeValue: true,
})
skip_import_rotation;
/* FIELD ATTRIBUTES */
get fieldAttrs() {
// Main fields on edit/create form
@ -131,7 +152,7 @@ export default class RoleModel extends Model {
}
get roleSettingAttrs() {
// logic for which get displayed is on DatabaseRoleSettingForm
const allRoleSettingFields = [
let allRoleSettingFields = [
'default_ttl',
'max_ttl',
'username',
@ -145,6 +166,12 @@ export default class RoleModel extends Model {
'rollback_statements',
'renew_statements',
];
// remove enterprise-only attrs if on community
if (!this.version.isEnterprise) {
allRoleSettingFields = allRoleSettingFields.filter((role) => role !== 'skip_import_rotation');
}
return expandAttributeMeta(this, allRoleSettingFields);
}
/* CAPABILITIES */

View file

@ -348,6 +348,13 @@
@queryParam="role"
@type={{attr.type}}
/>
{{else if (eq attr.name "skip_static_role_rotation_import")}}
<InfoTableRow
@alwaysRender={{not (is-empty-value (get @model attr.name) hasDefault=defaultDisplay)}}
@defaultShown={{defaultDisplay}}
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{not (get @model attr.name)}}
/>
{{else}}
<InfoTableRow
@alwaysRender={{not (is-empty-value (get @model attr.name) hasDefault=defaultDisplay)}}

View file

@ -86,6 +86,14 @@
@value={{format-duration (get @model attr.name)}}
@isLink={{eq attr.name "database"}}
/>
{{else if (eq attr.name "skip_import_rotation")}}
<InfoTableRow
@alwaysRender={{true}}
@defaultShown={{defaultDisplay}}
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{not (get @model attr.name)}}
@isLink={{eq attr.name "database"}}
/>
{{else}}
<InfoTableRow
@alwaysRender={{true}}

View file

@ -9,7 +9,11 @@
<div class="form-section">
{{#each this.settingFields as |attr|}}
{{#if (and (eq @mode "edit") (includes attr.name (array "skip_import_rotation" "username")))}}
<ReadonlyFormField @attr={{attr}} @value={{get @model attr.name}} />
{{#if (eq attr.name "skip_import_rotation")}}
<ReadonlyFormField @attr={{attr}} @value={{not (get @model attr.name)}} />
{{else}}
<ReadonlyFormField @attr={{attr}} @value={{get @model attr.name}} />
{{/if}}
{{else}}
<FormField data-test-field={{true}} @attr={{attr}} @model={{@model}} @modelValidations={{@modelValidations}} />
{{#if (and (eq attr.name "skip_import_rotation") this.isOverridden)}}

View file

@ -280,7 +280,7 @@
<Toggle
@name="toggle-{{@attr.name}}"
@onChange={{this.toggleButton}}
@checked={{this.toggleInputEnabled}}
@checked={{if @attr.options.isOppositeValue (not this.toggleInputEnabled) this.toggleInputEnabled}}
data-test-toggle={{@attr.name}}
disabled={{this.disabled}}
>
@ -288,9 +288,9 @@
<div class="description has-text-grey" data-test-toggle-subtext>
<span>
{{#if this.toggleInputEnabled}}
{{@attr.options.helperTextEnabled}}
{{if @attr.options.isOppositeValue @attr.options.helperTextDisabled @attr.options.helperTextEnabled}}
{{else}}
{{@attr.options.helperTextDisabled}}
{{if @attr.options.isOppositeValue @attr.options.helperTextEnabled @attr.options.helperTextDisabled}}
{{/if}}
</span>
</div>

View file

@ -88,7 +88,7 @@ module('Acceptance | database workflow', function (hooks) {
label: 'Root rotation statements',
value: `Default`,
},
{ label: 'Skip initial rotation on static roles', value: 'No' },
{ label: 'Rotate static roles immediately', value: 'Yes' },
];
});
test('create with rotate', async function (assert) {
@ -121,7 +121,7 @@ module('Acceptance | database workflow', function (hooks) {
assert.dom(PAGE.infoRow).exists({ count: this.expectedRows.length }, 'correct number of rows');
this.expectedRows.forEach(({ label, value }) => {
const valueSelector =
label === 'Skip initial rotation on static roles'
label === 'Rotate static roles immediately'
? PAGE.infoRowValueDiv(label)
: PAGE.infoRowValue(label);
assert.dom(PAGE.infoRowLabel(label)).hasText(label, `Label for ${label} is correct`);
@ -158,7 +158,7 @@ module('Acceptance | database workflow', function (hooks) {
assert.dom(PAGE.infoRow).exists({ count: this.expectedRows.length }, 'correct number of rows');
this.expectedRows.forEach(({ label, value }) => {
const valueSelector =
label === 'Skip initial rotation on static roles'
label === 'Rotate static roles immediately'
? PAGE.infoRowValueDiv(label)
: PAGE.infoRowValue(label);
assert.dom(PAGE.infoRowLabel(label)).hasText(label, `Label for ${label} is correct`);
@ -202,7 +202,7 @@ module('Acceptance | database workflow', function (hooks) {
assert.dom(PAGE.infoRow).exists({ count: this.expectedRows.length }, 'correct number of rows');
this.expectedRows.forEach(({ label, value }) => {
const valueSelector =
label === 'Skip initial rotation on static roles'
label === 'Rotate static roles immediately'
? PAGE.infoRowValueDiv(label)
: PAGE.infoRowValue(label);
assert.dom(PAGE.infoRowLabel(label)).hasText(label, `Label for ${label} is correct`);
@ -238,7 +238,7 @@ module('Acceptance | database workflow', function (hooks) {
);
});
});
module('roles', function (hooks) {
module('dynamic roles', function (hooks) {
hooks.beforeEach(async function () {
this.connection = `connect-${this.backend}`;
await visit(`/vault/secrets/${this.backend}/create`);
@ -356,4 +356,59 @@ module('Acceptance | database workflow', function (hooks) {
.hasText(`database/creds/${roleName}/abcd`, 'shows lease ID from response');
});
});
module('static roles', function (hooks) {
hooks.beforeEach(async function () {
this.setup = async ({ toggleRotateOff = false }) => {
this.connection = `connect-${this.backend}`;
await visit(`/vault/secrets/${this.backend}/create`);
await fillOutConnection(this.connection);
if (toggleRotateOff) {
await click('[data-test-toggle-input="toggle-skip_static_role_rotation_import"]');
}
await click(FORM.saveBtn);
await visit(`/vault/secrets/${this.backend}/show/${this.connection}`);
};
});
test('set parent db to rotate static roles immediately, verify static role reflects that default', async function (assert) {
await this.setup({ toggleRotateOff: false });
const roleName = 'static-role';
await click(PAGE.addRole);
assert.strictEqual(
currentURL(),
`/vault/secrets/${this.backend}/create?initialKey=${this.connection}&itemType=role`,
'Takes you to create role page'
);
await fillIn(FORM.inputByAttr('name'), roleName);
await fillIn(FORM.inputByAttr('type'), 'static');
assert
.dom('[data-test-toggle-subtext]')
.containsText(`Vault will rotate the password for this static role on creation.`);
});
test('set parent db to not rotate static roles immediately, verify static role reflects that default', async function (assert) {
await this.setup({ toggleRotateOff: true });
const roleName = 'static-role';
await click(PAGE.addRole);
assert.strictEqual(
currentURL(),
`/vault/secrets/${this.backend}/create?initialKey=${this.connection}&itemType=role`,
'Takes you to create role page'
);
await fillIn(FORM.inputByAttr('name'), roleName);
await fillIn(FORM.inputByAttr('type'), 'static');
assert
.dom('[data-test-toggle-subtext]')
.containsText(`Vault will not rotate this role's password on creation.`);
});
});
});

View file

@ -16,6 +16,7 @@ module('Integration | Component | database-role-edit', function (hooks) {
setupMirage(hooks);
hooks.beforeEach(function () {
this.version = this.owner.lookup('service:version');
this.store = this.owner.lookup('service:store');
this.store.pushPayload('database-role', {
modelName: 'database/role',
@ -76,7 +77,8 @@ module('Integration | Component | database-role-edit', function (hooks) {
await click('[data-test-secret-save]');
});
test('it should successfully create user with skip import rotation', async function (assert) {
test('enterprise: it should successfully create user that does not rotate immediately', async function (assert) {
this.version.type = 'enterprise';
this.server.post('/sys/capabilities-self', capabilitiesStub('database/static-creds/my-role', ['create']));
this.server.post(`/database/static-roles/my-static-role`, (schema, req) => {
assert.true(true, 'request made to create static role');
@ -99,7 +101,18 @@ module('Integration | Component | database-role-edit', function (hooks) {
await click('[data-test-secret-save]');
await render(hbs`<DatabaseRoleEdit @model={{this.modelStatic}} @mode="show"/>`);
assert.dom('[data-test-value-div="Skip initial rotation"]').containsText('Yes');
assert.dom('[data-test-value-div="Rotate immediately"]').containsText('No');
});
test('enterprise: it should successfully create user that does rotate immediately', async function (assert) {
this.version.type = 'enterprise';
this.server.post('/sys/capabilities-self', capabilitiesStub('database/static-creds/my-role', ['create']));
await render(hbs`<DatabaseRoleEdit @model={{this.modelStatic}} @mode="create"/>`);
await click('[data-test-secret-save]');
await render(hbs`<DatabaseRoleEdit @model={{this.modelStatic}} @mode="show"/>`);
assert.dom('[data-test-value-div="Rotate immediately"]').containsText('Yes');
});
test('it should show Get credentials button when a user has the correct policy', async function (assert) {