Icu 12567 add new field credential mapping overrides (#2156)
* chore: 🤖 add credential type select list
This commit is contained in:
1 parent
956f6fd04d
commit
93c8d481fd
14 files changed
+517
-36
No files matched your search
@@ -81,6 +81,12 @@ export default class GeneratedCredentialLibraryModel extends BaseModel {
|
||||
})
|
||||
credential_type;
|
||||
|
||||
@attr('object', {
|
||||
for: 'vault-generic',
|
||||
description: 'It indicates the credential mapping overrides.',
|
||||
})
|
||||
credential_mapping_overrides;
|
||||
|
||||
@attr('string', {
|
||||
for: 'vault-ssh-certificate',
|
||||
isNestedAttribute: true,
|
||||
|
||||
@@ -12,6 +12,17 @@ export const options = {
|
||||
http_method: ['GET', 'POST'],
|
||||
key_type: ['ed25519', 'ecdsa', 'rsa'],
|
||||
credential_types: ['ssh_private_key', 'username_password'],
|
||||
mapping_overrides: {
|
||||
username_password: {
|
||||
username_attribute: 'Username',
|
||||
password_attribute: 'Password',
|
||||
},
|
||||
ssh_private_key: {
|
||||
private_key_attribute: 'Private Key',
|
||||
private_key_passphrase_attribute: 'Private Key Passphrase',
|
||||
username_attribute: 'Username',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC = 'vault-generic';
|
||||
|
||||
@@ -7,6 +7,7 @@ import ApplicationSerializer from './application';
|
||||
import {
|
||||
TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
|
||||
TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
|
||||
options,
|
||||
} from '../models/credential-library';
|
||||
|
||||
export default class CredentialLibrarySerializer extends ApplicationSerializer {
|
||||
@@ -31,9 +32,34 @@ export default class CredentialLibrarySerializer extends ApplicationSerializer {
|
||||
return super.serialize(...arguments);
|
||||
}
|
||||
}
|
||||
serializeAttribute(snapshot, json) {
|
||||
const value = super.serializeAttribute(...arguments);
|
||||
const { credential_type, type, credential_mapping_overrides, isNew } =
|
||||
snapshot.record;
|
||||
if (
|
||||
type === TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC &&
|
||||
credential_mapping_overrides &&
|
||||
credential_type &&
|
||||
!isNew
|
||||
) {
|
||||
// API expects to send null to fields if it is undefined or deleted
|
||||
json.credential_mapping_overrides = Object.keys(
|
||||
options.mapping_overrides[credential_type],
|
||||
).reduce((obj, key) => {
|
||||
if (credential_mapping_overrides[key]) {
|
||||
obj[key] = credential_mapping_overrides[key];
|
||||
} else {
|
||||
obj[key] = null;
|
||||
}
|
||||
return obj;
|
||||
}, {});
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
serializeVaultGeneric() {
|
||||
const serialized = super.serialize(...arguments);
|
||||
|
||||
if (serialized.attributes) {
|
||||
// Set `http_request_body` only if `http_method` is POST
|
||||
// otherwise set to null to have the field removed
|
||||
|
||||
@@ -11,12 +11,18 @@ import {
|
||||
TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
|
||||
TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
|
||||
TYPES_CREDENTIAL_LIBRARY,
|
||||
options,
|
||||
} from 'api/models/credential-library';
|
||||
|
||||
export default factory.extend({
|
||||
id: () => generateId('cl_'),
|
||||
type: (i) => TYPES_CREDENTIAL_LIBRARY[i % TYPES_CREDENTIAL_LIBRARY.length],
|
||||
|
||||
credential_type(i) {
|
||||
if (this.type === TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC) {
|
||||
return options.credential_types[i % options.credential_types.length];
|
||||
}
|
||||
},
|
||||
authorized_actions: () =>
|
||||
permissions.authorizedActionsFor('credential-library') || [
|
||||
'no-op',
|
||||
|
||||
@@ -24,6 +24,7 @@ module('Unit | Serializer | credential library', function (hooks) {
|
||||
http_method: 'GET',
|
||||
version: 1,
|
||||
credential_type: 'ssh_private_key',
|
||||
credential_mapping_overrides: { username_attribute: 'user' },
|
||||
});
|
||||
const snapshot = record._createSnapshot();
|
||||
const serializedRecord = serializer.serialize(snapshot);
|
||||
@@ -34,6 +35,9 @@ module('Unit | Serializer | credential library', function (hooks) {
|
||||
description: 'Description',
|
||||
version: 1,
|
||||
credential_type: 'ssh_private_key',
|
||||
credential_mapping_overrides: {
|
||||
username_attribute: 'user',
|
||||
},
|
||||
attributes: {
|
||||
path: '/vault/path',
|
||||
http_method: 'GET',
|
||||
@@ -57,6 +61,7 @@ module('Unit | Serializer | credential library', function (hooks) {
|
||||
http_method: 'GET',
|
||||
version: 1,
|
||||
credential_type: 'ssh_private_key',
|
||||
credential_mapping_overrides: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -69,6 +74,11 @@ module('Unit | Serializer | credential library', function (hooks) {
|
||||
name: 'Name',
|
||||
description: 'Description',
|
||||
credential_type: 'ssh_private_key',
|
||||
credential_mapping_overrides: {
|
||||
private_key_attribute: null,
|
||||
private_key_passphrase_attribute: null,
|
||||
username_attribute: null,
|
||||
},
|
||||
attributes: {
|
||||
path: '/vault/path',
|
||||
http_method: 'GET',
|
||||
@@ -85,6 +95,7 @@ module('Unit | Serializer | credential library', function (hooks) {
|
||||
http_method: '',
|
||||
path: null,
|
||||
credential_type: null,
|
||||
credential_mapping_overrides: {},
|
||||
});
|
||||
const vaultSSHCertificateRecord = store.createRecord('credential-library', {
|
||||
type: TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
|
||||
@@ -105,6 +116,7 @@ module('Unit | Serializer | credential library', function (hooks) {
|
||||
name: null,
|
||||
type: TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
|
||||
credential_type: null,
|
||||
credential_mapping_overrides: {},
|
||||
});
|
||||
assert.deepEqual(serializedVaultSSHCertificateRecord, {
|
||||
attributes: {
|
||||
@@ -145,6 +157,7 @@ module('Unit | Serializer | credential library', function (hooks) {
|
||||
name: null,
|
||||
type: TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
|
||||
credential_type: null,
|
||||
credential_mapping_overrides: {},
|
||||
},
|
||||
'http_request_body attribute is not expected',
|
||||
);
|
||||
@@ -164,6 +177,7 @@ module('Unit | Serializer | credential library', function (hooks) {
|
||||
name: null,
|
||||
type: TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
|
||||
credential_type: null,
|
||||
credential_mapping_overrides: {},
|
||||
},
|
||||
'http_request_body attribute is expected',
|
||||
);
|
||||
|
||||
@@ -765,6 +765,10 @@ credential-library:
|
||||
new: New Credential Library
|
||||
ssh_private_key: SSH Private Key
|
||||
username_password: Username & Password
|
||||
username_attribute: Username
|
||||
password_attribute: Password
|
||||
private_key_passphrase_attribute: Private Key Passphrase
|
||||
private_key_attribute: Private Key
|
||||
actions:
|
||||
create: New Credential Library
|
||||
delete: Delete Credential Library
|
||||
@@ -779,6 +783,9 @@ credential-library:
|
||||
credential_type:
|
||||
label: Credential Type
|
||||
help: Specify the type of credential this library will issue. This must be set to use credential injection. After this data is saved, it cannot be changed later.
|
||||
credential_mapping_overrides:
|
||||
label: Credential mapping overrides
|
||||
help: Specify the credential fields to use instead of the defaults
|
||||
http_method:
|
||||
label: HTTP Method
|
||||
help: The HTTP method the library uses when requesting credentials from Vault.
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
@width='100%'
|
||||
name='credential_type'
|
||||
disabled={{or form.disabled (not @model.isNew)}}
|
||||
{{on 'input' (set-from-event @model 'credential_type')}}
|
||||
{{on 'input' this.selectCredentialType}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>
|
||||
@@ -140,6 +140,46 @@
|
||||
{{/if}}
|
||||
</Hds::Form::Select::Field>
|
||||
|
||||
{{#if @model.credential_type}}
|
||||
<Form::Field::ListWrapper
|
||||
@layout='horizontal'
|
||||
@isOptional={{true}}
|
||||
@disabled={{form.disabled}}
|
||||
>
|
||||
<:fieldset as |F|>
|
||||
<F.Legend>
|
||||
{{t
|
||||
'resources.credential-library.form.credential_mapping_overrides.label'
|
||||
}}
|
||||
</F.Legend>
|
||||
<F.HelperText>
|
||||
{{t
|
||||
'resources.credential-library.form.credential_mapping_overrides.help'
|
||||
}}
|
||||
</F.HelperText>
|
||||
|
||||
{{#if @model.errors.credential_mapping_overrides}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.credential_mapping_overrides as |error|}}
|
||||
<E.Message>{{error.message}}</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</:fieldset>
|
||||
|
||||
<:field as |F|>
|
||||
<F.SelectText
|
||||
@name='credential_mapping_overrides'
|
||||
@selectOptions={{this.mappingOverrides}}
|
||||
@model={{@model}}
|
||||
@disabled={{form.disabled}}
|
||||
@removeDuplicates={{true}}
|
||||
@width='400px'
|
||||
/>
|
||||
</:field>
|
||||
</Form::Field::ListWrapper>
|
||||
{{/if}}
|
||||
|
||||
<Hds::Form::Radio::Group
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'http_method')}}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
options,
|
||||
TYPES_CREDENTIAL_LIBRARY,
|
||||
} from 'api/models/credential-library';
|
||||
|
||||
import { action } from '@ember/object';
|
||||
export default class FormCredentialLibraryVaultGenericComponent extends Component {
|
||||
// =properties
|
||||
/**
|
||||
@@ -30,4 +30,17 @@ export default class FormCredentialLibraryVaultGenericComponent extends Componen
|
||||
get isHttpRequestBodyAllowed() {
|
||||
return this.args.model.http_method?.match(/post/i);
|
||||
}
|
||||
|
||||
get mappingOverrides() {
|
||||
return options.mapping_overrides[this.args.model.credential_type];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the previously selected key value pair when toggling between credential types on a new form
|
||||
*/
|
||||
@action
|
||||
selectCredentialType({ target: { value } }) {
|
||||
this.args.model.credential_mapping_overrides = {};
|
||||
this.args.model.credential_type = value;
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,9 @@
|
||||
'form/field/list-wrapper/textarea' disabled=@disabled
|
||||
)
|
||||
Select=(component 'form/field/list-wrapper/select' disabled=@disabled)
|
||||
SelectText=(component
|
||||
'form/field/list-wrapper/select-text' disabled=@disabled
|
||||
)
|
||||
)
|
||||
to='field'
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
}}
|
||||
{{#let (unique-id) (unique-id) as |keyHeaderID valueHeaderID|}}
|
||||
<Hds::Table class='list-wrapper-field' name={{@name}}>
|
||||
<:head as |H|>
|
||||
<H.Tr>
|
||||
<H.Th id={{keyHeaderID}}>{{t 'form.key.label'}}</H.Th>
|
||||
<H.Th id={{valueHeaderID}}>{{t 'form.value.label'}}</H.Th>
|
||||
<H.Th>
|
||||
{{t 'titles.actions'}}
|
||||
|
||||
</H.Th>
|
||||
</H.Tr>
|
||||
</:head>
|
||||
<:body as |B|>
|
||||
{{#each-in this.options as |select val|}}
|
||||
<B.Tr>
|
||||
<B.Td>
|
||||
<Hds::Form::Select::Base
|
||||
@value={{select}}
|
||||
disabled={{@disabled}}
|
||||
@width={{@width}}
|
||||
aria-labelledby={{keyHeaderID}}
|
||||
{{on 'change' (fn this.selectChange select val)}}
|
||||
as |F|
|
||||
>
|
||||
<F.Options>
|
||||
{{#each-in @selectOptions as |key value|}}
|
||||
<option value={{key}} selected={{eq select key}}>
|
||||
{{value}}
|
||||
</option>
|
||||
{{/each-in}}
|
||||
</F.Options>
|
||||
</Hds::Form::Select::Base>
|
||||
</B.Td>
|
||||
|
||||
<B.Td>
|
||||
<Hds::Form::TextInput::Field
|
||||
@value={{val}}
|
||||
@type='text'
|
||||
@width={{@width}}
|
||||
disabled={{@disabled}}
|
||||
aria-labelledby={{valueHeaderID}}
|
||||
{{on 'change' (fn this.updateInput select)}}
|
||||
/>
|
||||
</B.Td>
|
||||
|
||||
<B.Td>
|
||||
{{#if this.removeOptionByKey}}
|
||||
<Hds::Button
|
||||
data-test-remove-button={{select}}
|
||||
@text={{t 'actions.remove'}}
|
||||
@color='critical'
|
||||
@icon='trash'
|
||||
@isIconOnly={{true}}
|
||||
disabled={{@disabled}}
|
||||
{{on 'click' (fn this.removeOptionByKey select)}}
|
||||
/>
|
||||
{{/if}}
|
||||
</B.Td>
|
||||
</B.Tr>
|
||||
{{/each-in}}
|
||||
|
||||
{{#if this.showNewRow}}
|
||||
<B.Tr>
|
||||
<B.Td>
|
||||
<Hds::Form::Select::Base
|
||||
@value={{this.newOptionKey}}
|
||||
disabled={{@disabled}}
|
||||
aria-labelledby={{keyHeaderID}}
|
||||
@width={{@width}}
|
||||
{{on 'change' (set-from-event this 'newOptionKey')}}
|
||||
as |F|
|
||||
>
|
||||
<F.Options>
|
||||
|
||||
<option hidden selected value=''>
|
||||
{{t 'titles.choose-an-option'}}
|
||||
</option>
|
||||
{{#each-in this.selectOptions as |key value|}}
|
||||
<option value={{key}} selected={{eq key this.newOptionKey}}>
|
||||
{{value}}
|
||||
</option>
|
||||
{{/each-in}}
|
||||
</F.Options>
|
||||
</Hds::Form::Select::Base>
|
||||
</B.Td>
|
||||
|
||||
<B.Td>
|
||||
<Hds::Form::TextInput::Field
|
||||
@value={{this.newOptionValue}}
|
||||
@type='text'
|
||||
@width={{@width}}
|
||||
disabled={{@disabled}}
|
||||
aria-labelledby={{valueHeaderID}}
|
||||
{{on 'input' (set-from-event this 'newOptionValue')}}
|
||||
/>
|
||||
</B.Td>
|
||||
|
||||
<B.Td>
|
||||
<Hds::Button
|
||||
data-test-select-text-add-btn
|
||||
@text={{t 'actions.add'}}
|
||||
@color='secondary'
|
||||
type='button'
|
||||
disabled={{or @disabled (not this.newOptionKey)}}
|
||||
{{on 'click' this.addOption}}
|
||||
/>
|
||||
</B.Td>
|
||||
</B.Tr>
|
||||
{{/if}}
|
||||
</:body>
|
||||
</Hds::Table>
|
||||
{{/let}}
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
import { set } from '@ember/object';
|
||||
|
||||
export default class MappingListComponent extends Component {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
@tracked newOptionKey = '';
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
@tracked newOptionValue = '';
|
||||
|
||||
/**
|
||||
* Returns an object of key/value pair that the user enters
|
||||
* @type {object}
|
||||
*/
|
||||
get options() {
|
||||
return this.args?.options || this.args?.model?.[this.args.name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if we need to show an empty row to the users to enter more key/value pairs based on removeDuplicates arg,
|
||||
* by default it is true
|
||||
* @type {object}
|
||||
*/
|
||||
get showNewRow() {
|
||||
if (this.args.removeDuplicates) {
|
||||
return (
|
||||
Object.keys(this.options || {}).length !==
|
||||
Object.keys(this.args.selectOptions || {}).length
|
||||
);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents users from selecting duplicate keys from the select list if there's the arg is set to true
|
||||
* @type {object}
|
||||
*/
|
||||
get selectOptions() {
|
||||
const previouslySelectedKeys = Object.keys(this.options || {});
|
||||
if (this.args.removeDuplicates && previouslySelectedKeys.length) {
|
||||
const newObj = { ...this.args.selectOptions };
|
||||
for (const key of Object.keys(newObj)) {
|
||||
if (previouslySelectedKeys.includes(key)) {
|
||||
delete newObj[key];
|
||||
}
|
||||
}
|
||||
return newObj;
|
||||
} else {
|
||||
return this.args.selectOptions;
|
||||
}
|
||||
}
|
||||
// =actions
|
||||
|
||||
/**
|
||||
* Handles when then user makes changes to the select list
|
||||
* @param oldkey {string}
|
||||
* @param oldVal {string}
|
||||
*/
|
||||
@action
|
||||
selectChange(oldkey, oldVal, { target: { value: newKey } }) {
|
||||
const field = this.args.name;
|
||||
const newObj = { ...this.args.model[field], [newKey]: oldVal };
|
||||
delete newObj[oldkey];
|
||||
set(this.args.model, field, newObj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles input changes
|
||||
* @param key {string}
|
||||
*/
|
||||
@action
|
||||
updateInput(key, { target: { value: newValue } }) {
|
||||
const field = this.args.name;
|
||||
this.args.model[field] = {
|
||||
...this.args.model[field],
|
||||
[key]: newValue,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* If a new key value is entered and an addOption method was specified,
|
||||
* calls addOption with the new key and value. Resets key and value.
|
||||
* Otherwise use the model argument to create the array and update the model
|
||||
*/
|
||||
|
||||
@action
|
||||
addOption() {
|
||||
if (this.args.addOption) {
|
||||
if (this.newOptionKey) {
|
||||
this.args.addOption({
|
||||
key: this.newOptionKey,
|
||||
value: this.newOptionValue,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const field = this.args.name;
|
||||
this.args.model[field] = {
|
||||
...this.args.model[field],
|
||||
[this.newOptionKey]: this.newOptionValue,
|
||||
};
|
||||
}
|
||||
|
||||
this.newOptionKey = '';
|
||||
this.newOptionValue = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* If removeOptionByIndex method was passed, use that.
|
||||
* Otherwise, use the model to remove an option by key
|
||||
* @param index {number}
|
||||
*/
|
||||
@action
|
||||
removeOptionByKey(selectedKey) {
|
||||
if (this.args.removeOptionByKey) {
|
||||
this.args.removeOptionByKey(selectedKey);
|
||||
} else {
|
||||
const field = this.args.name;
|
||||
const newObj = { ...this.args.model[field] };
|
||||
delete newObj[selectedKey];
|
||||
set(this.args.model, field, newObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,15 +95,27 @@ module('Acceptance | credential-libraries | create', function (hooks) {
|
||||
await fillIn('[name="name"]', 'random string');
|
||||
await select('[name="credential_type"]', 'ssh_private_key');
|
||||
|
||||
await select(
|
||||
'[name="credential_mapping_overrides"] tbody td:nth-of-type(1) select',
|
||||
'private_key_attribute',
|
||||
);
|
||||
await fillIn(
|
||||
'[name="credential_mapping_overrides"] tbody td:nth-of-type(2) input',
|
||||
'key',
|
||||
);
|
||||
await click('[name="credential_mapping_overrides"] button');
|
||||
|
||||
await click('[type="submit"]');
|
||||
assert.strictEqual(getCredentialLibraryCount(), count + 1);
|
||||
|
||||
const credentialLibrary = this.server.schema.credentialLibraries.findBy({
|
||||
name: 'random string',
|
||||
});
|
||||
|
||||
assert.strictEqual(credentialLibrary.name, 'random string');
|
||||
assert.strictEqual(credentialLibrary.credentialType, 'ssh_private_key');
|
||||
assert.deepEqual(credentialLibrary.credentialMappingOverrides, {
|
||||
private_key_attribute: 'key',
|
||||
});
|
||||
});
|
||||
|
||||
test('can create a new credential library of type vault ssh cert', async function (assert) {
|
||||
|
||||
@@ -22,6 +22,17 @@ module('Acceptance | credential-libraries | update', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
const BUTTON_SELECTOR = '.rose-form-actions [type="button"]';
|
||||
const SAVE_BTN_SELECTOR = '.rose-form-actions [type="submit"]';
|
||||
const NAME_INPUT_SELECTOR = '[name="name"]';
|
||||
const FIELD_ERROR_TEXT_SELECTOR = '.hds-form-error__message';
|
||||
const DESC_INPUT_SELECTOR = '[name="description"]';
|
||||
const VAULT_PATH_SELECTOR = '[name="vault_path"]';
|
||||
const CRED_TYPE_SELECTOR = '[name="credential_type"]';
|
||||
const CRED_MAPPING_OVERRIDES_SELECT =
|
||||
'[name="credential_mapping_overrides"] tbody td:nth-of-type(1) select';
|
||||
const CRED_MAPPING_OVERRIDES_INPUT =
|
||||
'[name="credential_mapping_overrides"] tbody td:nth-of-type(2) input';
|
||||
const instances = {
|
||||
scopes: {
|
||||
global: null,
|
||||
@@ -73,43 +84,60 @@ module('Acceptance | credential-libraries | update', function (hooks) {
|
||||
authenticateSession({});
|
||||
});
|
||||
|
||||
test('can update resource and save changes', async function (assert) {
|
||||
assert.notEqual(instances.credentialLibrary.name, 'random string');
|
||||
await visit(urls.credentialLibrary);
|
||||
await click('form [type="button"]', 'Activate edit mode');
|
||||
await fillIn('[name="name"]', 'random string');
|
||||
await click('.rose-form-actions [type="submit"]');
|
||||
assert.strictEqual(currentURL(), urls.credentialLibrary);
|
||||
assert.strictEqual(
|
||||
this.server.schema.credentialLibraries.all().models[0].name,
|
||||
'random string',
|
||||
);
|
||||
});
|
||||
|
||||
test('cannot update resource without proper authorization', async function (assert) {
|
||||
instances.credentialLibrary.authorized_actions =
|
||||
instances.credentialLibrary.authorized_actions.filter(
|
||||
(item) => item !== 'update',
|
||||
);
|
||||
await visit(urls.credentialLibrary);
|
||||
assert.notOk(find('form [type="button"]'));
|
||||
assert.notOk(find(BUTTON_SELECTOR));
|
||||
});
|
||||
|
||||
test('can update a credential library and cancel changes', async function (assert) {
|
||||
await visit(urls.credentialLibrary);
|
||||
await click('form [type="button"]', 'Activate edit mode');
|
||||
await fillIn('[name="name"]', 'random string');
|
||||
await click('.rose-form-actions [type="button"]');
|
||||
await click(BUTTON_SELECTOR, 'Activate edit mode');
|
||||
await fillIn(NAME_INPUT_SELECTOR, 'random string');
|
||||
await click(BUTTON_SELECTOR);
|
||||
assert.notEqual(
|
||||
this.server.schema.credentialLibraries.all().models[0].name,
|
||||
'random string',
|
||||
);
|
||||
assert.strictEqual(
|
||||
find('[name="name"]').value,
|
||||
find(NAME_INPUT_SELECTOR).value,
|
||||
instances.credentialLibrary.name,
|
||||
);
|
||||
});
|
||||
|
||||
test('can update a vault generic credential library and save changes', async function (assert) {
|
||||
await visit(
|
||||
`${urls.credentialLibraries}/${instances.credentialLibrary.id}`,
|
||||
);
|
||||
|
||||
await click(BUTTON_SELECTOR);
|
||||
|
||||
await fillIn(NAME_INPUT_SELECTOR, 'random string');
|
||||
await fillIn(DESC_INPUT_SELECTOR, 'description');
|
||||
await fillIn(VAULT_PATH_SELECTOR, 'path');
|
||||
|
||||
await select(CRED_MAPPING_OVERRIDES_SELECT, 'private_key_attribute');
|
||||
await fillIn(CRED_MAPPING_OVERRIDES_INPUT, 'key');
|
||||
|
||||
await click('[name="credential_mapping_overrides"] button');
|
||||
|
||||
await click(SAVE_BTN_SELECTOR);
|
||||
const credentialLibrary = this.server.schema.credentialLibraries.findBy({
|
||||
name: 'random string',
|
||||
});
|
||||
assert.strictEqual(credentialLibrary.name, 'random string');
|
||||
assert.strictEqual(credentialLibrary.description, 'description');
|
||||
assert.strictEqual(credentialLibrary.attributes.path, 'path');
|
||||
assert.deepEqual(credentialLibrary.credentialMappingOverrides, {
|
||||
private_key_attribute: 'key',
|
||||
private_key_passphrase_attribute: null,
|
||||
username_attribute: null,
|
||||
});
|
||||
});
|
||||
|
||||
test('saving an existing credential library with invalid fields displays error messages', async function (assert) {
|
||||
this.server.patch('/credential-libraries/:id', () => {
|
||||
return new Response(
|
||||
@@ -131,15 +159,15 @@ module('Acceptance | credential-libraries | update', function (hooks) {
|
||||
);
|
||||
});
|
||||
await visit(urls.credentialLibrary);
|
||||
await click('form [type="button"]', 'Activate edit mode');
|
||||
await fillIn('[name="name"]', 'random string');
|
||||
await click('[type="submit"]');
|
||||
await click(BUTTON_SELECTOR, 'Activate edit mode');
|
||||
await fillIn(NAME_INPUT_SELECTOR, 'random string');
|
||||
await click(SAVE_BTN_SELECTOR);
|
||||
assert.ok(
|
||||
find('[role="alert"]').textContent.trim(),
|
||||
'The request was invalid.',
|
||||
);
|
||||
assert.ok(
|
||||
find('.hds-form-error__message').textContent.trim(),
|
||||
find(FIELD_ERROR_TEXT_SELECTOR).textContent.trim(),
|
||||
'Name is required.',
|
||||
);
|
||||
});
|
||||
@@ -151,8 +179,8 @@ module('Acceptance | credential-libraries | update', function (hooks) {
|
||||
assert.notEqual(instances.credentialLibrary.name, 'random string');
|
||||
|
||||
await visit(urls.credentialLibrary);
|
||||
await click('form [type="button"]', 'Activate edit mode');
|
||||
await fillIn('[name="name"]', 'random string');
|
||||
await click(BUTTON_SELECTOR, 'Activate edit mode');
|
||||
await fillIn(NAME_INPUT_SELECTOR, 'random string');
|
||||
assert.strictEqual(currentURL(), urls.credentialLibrary);
|
||||
|
||||
try {
|
||||
@@ -174,8 +202,8 @@ module('Acceptance | credential-libraries | update', function (hooks) {
|
||||
confirmService.enabled = true;
|
||||
assert.notEqual(instances.credentialLibrary.name, 'random string');
|
||||
await visit(urls.credentialLibrary);
|
||||
await click('form [type="button"]', 'Activate edit mode');
|
||||
await fillIn('[name="name"]', 'random string');
|
||||
await click(BUTTON_SELECTOR, 'Activate edit mode');
|
||||
await fillIn(NAME_INPUT_SELECTOR, 'random string');
|
||||
assert.strictEqual(currentURL(), urls.credentialLibrary);
|
||||
|
||||
try {
|
||||
@@ -193,8 +221,8 @@ module('Acceptance | credential-libraries | update', function (hooks) {
|
||||
|
||||
test('cannot update credential type in a vault generic credential library', async function (assert) {
|
||||
await visit(urls.credentialLibrary);
|
||||
await click('form [type="button"]', 'Activate edit mode');
|
||||
assert.dom('[name=credential_type]').isDisabled();
|
||||
await click(BUTTON_SELECTOR, 'Activate edit mode');
|
||||
assert.dom(CRED_TYPE_SELECTOR).isDisabled();
|
||||
});
|
||||
|
||||
test('can update a vault ssh cert credential library and save changes', async function (assert) {
|
||||
@@ -206,10 +234,10 @@ module('Acceptance | credential-libraries | update', function (hooks) {
|
||||
await visit(
|
||||
`${urls.credentialLibraries}/${instances.credentialLibrary.id}`,
|
||||
);
|
||||
await click('form [type="button"]:not(:disabled)', 'Activate edit mode');
|
||||
await fillIn('[name="name"]', 'name');
|
||||
await fillIn('[name="description"]', 'description');
|
||||
await fillIn('[name="vault_path"]', 'path');
|
||||
await click(BUTTON_SELECTOR, 'Activate edit mode');
|
||||
await fillIn(NAME_INPUT_SELECTOR, 'name');
|
||||
await fillIn(DESC_INPUT_SELECTOR, 'description');
|
||||
await fillIn(VAULT_PATH_SELECTOR, 'path');
|
||||
await fillIn('[name="username"]', 'username');
|
||||
await select('[name="key_type"]', 'rsa');
|
||||
await fillIn('[name="key_bits"]', 100);
|
||||
@@ -234,7 +262,8 @@ module('Acceptance | credential-libraries | update', function (hooks) {
|
||||
);
|
||||
|
||||
await click('[name="extensions"] button');
|
||||
await click('[type="submit"]');
|
||||
|
||||
await click(SAVE_BTN_SELECTOR);
|
||||
const credentialLibrary = this.server.schema.credentialLibraries.findBy({
|
||||
type: TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
|
||||
});
|
||||
|
||||
@@ -163,4 +163,65 @@ module('Integration | Component | list-wrapper', function (hooks) {
|
||||
await click('.list-wrapper-field [data-test-remove-option-button=option1]');
|
||||
assert.dom('tbody tr').exists({ count: 3 });
|
||||
});
|
||||
|
||||
test('it renders multiple options with select and text input as key value pair', async function (assert) {
|
||||
this.options = {
|
||||
username_attribute: 'user',
|
||||
password_attribute: 'pass',
|
||||
};
|
||||
|
||||
this.selectOptions = {
|
||||
username_attribute: 'User Key',
|
||||
password_attribute: 'Pass Key',
|
||||
};
|
||||
await render(hbs`
|
||||
<Form::Field::ListWrapper>
|
||||
<:field as |F|>
|
||||
<F.SelectText @name="credential_mapping_overrides" @options={{this.options}} @removeDuplicates='true' @selectOptions={{this.selectOptions}}></F.SelectText>
|
||||
</:field>
|
||||
</Form::Field::ListWrapper>
|
||||
`);
|
||||
|
||||
assert.dom('tbody tr').exists({ count: 2 });
|
||||
assert
|
||||
.dom('.list-wrapper-field tbody tr:last-child input')
|
||||
.hasValue('pass');
|
||||
assert
|
||||
.dom('.list-wrapper-field tbody tr:last-child select')
|
||||
.hasValue('password_attribute');
|
||||
});
|
||||
|
||||
test('it does not render new rows when the select option limit is reached by passing in @removeDuplicates', async function (assert) {
|
||||
this.options = { username_attribute: 'user', password_attribute: 'pass' };
|
||||
this.selectOptions = {
|
||||
username_attribute: 'User Key',
|
||||
password_attribute: 'Pass Key',
|
||||
};
|
||||
await render(hbs`
|
||||
<Form::Field::ListWrapper>
|
||||
<:field as |F|>
|
||||
<F.SelectText @name="credential_mapping_overrides" @options={{this.options}} @selectOptions={{this.selectOptions}} @removeDuplicates='true'></F.SelectText>
|
||||
</:field>
|
||||
</Form::Field::ListWrapper>
|
||||
`);
|
||||
|
||||
assert.dom('tbody tr').exists({ count: 2 });
|
||||
});
|
||||
|
||||
test('it does render unlimited new rows when @removeDuplicates is not passed', async function (assert) {
|
||||
this.options = { username_attribute: 'user', password_attribute: 'pass' };
|
||||
this.selectOptions = {
|
||||
username_attribute: 'User Key',
|
||||
password_attribute: 'Pass Key',
|
||||
};
|
||||
await render(hbs`
|
||||
<Form::Field::ListWrapper>
|
||||
<:field as |F|>
|
||||
<F.SelectText @name="credential_mapping_overrides" @options={{this.options}} @selectOptions={{this.selectOptions}}></F.SelectText>
|
||||
</:field>
|
||||
</Form::Field::ListWrapper>
|
||||
`);
|
||||
|
||||
assert.dom('tbody tr').exists({ count: 3 });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user