Update credentials (#2523)
* feat: 🎸 allow AWS host catalog credential type update
This commit is contained in:
1 parent
98d19ef591
commit
6956b30fd6
8 files changed
+132
-72
No files matched your search
@@ -58,6 +58,7 @@ export default class GeneratedHostCatalogModel extends BaseModel {
|
||||
// AWS & Azure
|
||||
|
||||
@attr('boolean', {
|
||||
for: 'plugin',
|
||||
isNestedAttribute: true,
|
||||
description: '',
|
||||
})
|
||||
@@ -66,12 +67,14 @@ export default class GeneratedHostCatalogModel extends BaseModel {
|
||||
// AWS specific
|
||||
|
||||
@attr('string', {
|
||||
for: 'plugin',
|
||||
description:
|
||||
'An expression used to filter the workers that have network access to a service that is hosting the external object store.',
|
||||
})
|
||||
worker_filter;
|
||||
|
||||
@attr('string', {
|
||||
for: 'plugin',
|
||||
isNestedAttribute: true,
|
||||
description: '',
|
||||
})
|
||||
@@ -79,12 +82,14 @@ export default class GeneratedHostCatalogModel extends BaseModel {
|
||||
|
||||
// AWS static credentials
|
||||
@attr('string', {
|
||||
for: 'plugin',
|
||||
isNestedSecret: true,
|
||||
description: '',
|
||||
})
|
||||
access_key_id;
|
||||
|
||||
@attr('string', {
|
||||
for: 'plugin',
|
||||
isNestedSecret: true,
|
||||
description: '',
|
||||
})
|
||||
@@ -92,24 +97,28 @@ export default class GeneratedHostCatalogModel extends BaseModel {
|
||||
|
||||
// AWS dynamic credentials
|
||||
@attr('string', {
|
||||
for: 'plugin',
|
||||
isNestedAttribute: true,
|
||||
description: 'The role ARN to use.',
|
||||
})
|
||||
role_arn;
|
||||
|
||||
@attr('string', {
|
||||
for: 'plugin',
|
||||
isNestedAttribute: true,
|
||||
description: 'The role external ID to use.',
|
||||
})
|
||||
role_external_id;
|
||||
|
||||
@attr('string', {
|
||||
for: 'plugin',
|
||||
isNestedAttribute: true,
|
||||
description: 'The role session to use.',
|
||||
})
|
||||
role_session_name;
|
||||
|
||||
@attr('object-as-array', {
|
||||
for: 'plugin',
|
||||
isNestedAttribute: true,
|
||||
description: 'The role tags to use.',
|
||||
})
|
||||
@@ -118,30 +127,35 @@ export default class GeneratedHostCatalogModel extends BaseModel {
|
||||
// Azure specific
|
||||
|
||||
@attr('string', {
|
||||
for: 'plugin',
|
||||
isNestedAttribute: true,
|
||||
description: '',
|
||||
})
|
||||
tenant_id;
|
||||
|
||||
@attr('string', {
|
||||
for: 'plugin',
|
||||
isNestedAttribute: true,
|
||||
description: '',
|
||||
})
|
||||
client_id;
|
||||
|
||||
@attr('string', {
|
||||
for: 'plugin',
|
||||
isNestedAttribute: true,
|
||||
description: '',
|
||||
})
|
||||
subscription_id;
|
||||
|
||||
@attr('string', {
|
||||
for: 'plugin',
|
||||
isNestedSecret: true,
|
||||
description: '',
|
||||
})
|
||||
secret_id;
|
||||
|
||||
@attr('string', {
|
||||
for: 'plugin',
|
||||
isNestedSecret: true,
|
||||
description: '',
|
||||
})
|
||||
|
||||
@@ -27,6 +27,13 @@ export const TYPES_CREDENTIALS = Object.freeze([
|
||||
TYPE_CREDENTIAL_DYNAMIC,
|
||||
]);
|
||||
|
||||
const DYNAMIC_CREDENTIAL_FIELDS = [
|
||||
'role_arn',
|
||||
'role_external_id',
|
||||
'role_session_name',
|
||||
'role_tags',
|
||||
];
|
||||
|
||||
export default class HostCatalogModel extends GeneratedHostCatalogModel {
|
||||
// =attributes
|
||||
|
||||
@@ -53,7 +60,7 @@ export default class HostCatalogModel extends GeneratedHostCatalogModel {
|
||||
*/
|
||||
get credentialType() {
|
||||
if (!this.#credentialType) {
|
||||
if (this.role_arn) {
|
||||
if (DYNAMIC_CREDENTIAL_FIELDS.some((field) => this[field])) {
|
||||
this.#credentialType = TYPE_CREDENTIAL_DYNAMIC;
|
||||
} else {
|
||||
this.#credentialType = TYPE_CREDENTIAL_STATIC;
|
||||
|
||||
@@ -68,15 +68,29 @@ export default class HostCatalogSerializer extends ApplicationSerializer {
|
||||
const value = super.serializeAttribute(...arguments);
|
||||
const { compositeType, credentialType, isPlugin } = snapshot.record;
|
||||
const { options } = attribute;
|
||||
|
||||
const fields =
|
||||
compositeType === TYPE_HOST_CATALOG_PLUGIN_AZURE
|
||||
? azureFields
|
||||
: AWSfieldsWithCredentialType[credentialType];
|
||||
|
||||
// Delete any nested attribute fields that don't belong to the record type
|
||||
// Delete any nested attribute fields that don't belong to the record's compositeType
|
||||
if (isPlugin && options.isNestedAttribute && json.attributes) {
|
||||
if (!fields.includes(key)) {
|
||||
// If compositeType is AWS, delete azure fields from the json, and set the rest that don't belong to respective compositeType to null
|
||||
if (compositeType === TYPE_HOST_CATALOG_PLUGIN_AWS) {
|
||||
if (!AWSfieldsWithCredentialType[credentialType].includes(key)) {
|
||||
if (azureFields.includes(key)) {
|
||||
delete json.attributes[key];
|
||||
} else {
|
||||
json.attributes[key] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If compositeType is Azure, delete AWS fields from the json
|
||||
if (
|
||||
compositeType === TYPE_HOST_CATALOG_PLUGIN_AZURE &&
|
||||
!azureFields.includes(key)
|
||||
) {
|
||||
delete json.attributes[key];
|
||||
}
|
||||
}
|
||||
@@ -98,7 +112,7 @@ export default class HostCatalogSerializer extends ApplicationSerializer {
|
||||
// Delete unnecessary fields for static host-catalog
|
||||
delete serialized.attributes;
|
||||
delete serialized.secrets;
|
||||
delete serialized.worker_filter;
|
||||
|
||||
return serialized;
|
||||
}
|
||||
}
|
||||
@@ -45,14 +45,9 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
description: 'this is a Aws plugin host-catalog',
|
||||
disable_credential_rotation: true,
|
||||
worker_filter: 'workerfilter',
|
||||
// these are AWS fields and should be included
|
||||
region: 'west',
|
||||
access_key_id: 'foobars',
|
||||
secret_access_key: 'testing',
|
||||
// these are Azure fields and should be excluded
|
||||
tenant_id: 'a1b2c3',
|
||||
client_id: 'a1b2c3',
|
||||
subscription_id: 'a1b2c3',
|
||||
secret_id: 'a1b2c3',
|
||||
secret_value: 'a1b2c3',
|
||||
role_arn: 'test',
|
||||
@@ -71,6 +66,10 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
attributes: {
|
||||
disable_credential_rotation: true,
|
||||
region: 'west',
|
||||
role_arn: null,
|
||||
role_external_id: null,
|
||||
role_session_name: null,
|
||||
role_tags: null,
|
||||
},
|
||||
secrets: {
|
||||
access_key_id: 'foobars',
|
||||
@@ -152,6 +151,10 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
attributes: {
|
||||
disable_credential_rotation: true,
|
||||
region: 'spain',
|
||||
role_arn: null,
|
||||
role_external_id: null,
|
||||
role_session_name: null,
|
||||
role_tags: null,
|
||||
},
|
||||
secrets: {
|
||||
access_key_id: 'foobars',
|
||||
@@ -225,6 +228,10 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
attributes: {
|
||||
disable_credential_rotation: false,
|
||||
region: 'andorra',
|
||||
role_arn: null,
|
||||
role_external_id: null,
|
||||
role_session_name: null,
|
||||
role_tags: null,
|
||||
},
|
||||
};
|
||||
assert.deepEqual(record.serialize(), expectedResult);
|
||||
|
||||
@@ -144,74 +144,51 @@
|
||||
</F.HelperText>
|
||||
</Hds::Form::TextInput::Field>
|
||||
|
||||
{{#if @model.isNew}}
|
||||
<Hds::Form::Radio::Group
|
||||
class='dynamic-credential-selection'
|
||||
@layout='vertical'
|
||||
@name='credential_type'
|
||||
as |G|
|
||||
>
|
||||
<G.Legend>
|
||||
{{t 'resources.host-catalog.types.credential.label'}}
|
||||
</G.Legend>
|
||||
<G.HelperText>
|
||||
{{t 'resources.host-catalog.types.credential.help'}}
|
||||
</G.HelperText>
|
||||
<Hds::Form::Radio::Group
|
||||
class='dynamic-credential-selection'
|
||||
@layout='vertical'
|
||||
@name='credential_type'
|
||||
as |G|
|
||||
>
|
||||
<G.Legend>
|
||||
{{t 'resources.host-catalog.types.credential.label'}}
|
||||
</G.Legend>
|
||||
<G.HelperText>
|
||||
{{t 'resources.host-catalog.types.credential.help'}}
|
||||
</G.HelperText>
|
||||
|
||||
{{#each this.credentials as |credentialType|}}
|
||||
<G.RadioField
|
||||
name={{credentialType}}
|
||||
@value={{credentialType}}
|
||||
checked={{eq credentialType this.selectedCredentialType}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (fn this.updateCredentialTypeSelection credentialType)}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>
|
||||
{{t
|
||||
(concat
|
||||
'resources.host-catalog.types.credential.'
|
||||
credentialType
|
||||
'.label'
|
||||
)
|
||||
}}
|
||||
</F.Label>
|
||||
<F.HelperText>
|
||||
{{t
|
||||
(concat
|
||||
'resources.host-catalog.types.credential.'
|
||||
credentialType
|
||||
'.help'
|
||||
)
|
||||
}}
|
||||
</F.HelperText>
|
||||
</G.RadioField>
|
||||
{{/each}}
|
||||
</Hds::Form::Radio::Group>
|
||||
{{else}}
|
||||
<Hds::Form::Fieldset @layout='horizontal' as |F|>
|
||||
<F.Legend>
|
||||
{{t 'resources.host-catalog.types.credential.label'}}
|
||||
</F.Legend>
|
||||
<F.Control>
|
||||
<Hds::Badge
|
||||
@text={{t
|
||||
{{#each this.credentials as |credentialType|}}
|
||||
<G.RadioField
|
||||
name={{credentialType}}
|
||||
@value={{credentialType}}
|
||||
checked={{eq credentialType this.selectedCredentialType}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (fn this.updateCredentialTypeSelection credentialType)}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>
|
||||
{{t
|
||||
(concat
|
||||
'resources.host-catalog.types.credential.'
|
||||
this.selectedCredentialType
|
||||
'.edit'
|
||||
'resources.host-catalog.types.credential.' credentialType '.label'
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</F.Control>
|
||||
</Hds::Form::Fieldset>
|
||||
{{/if}}
|
||||
</F.Label>
|
||||
<F.HelperText>
|
||||
{{t
|
||||
(concat
|
||||
'resources.host-catalog.types.credential.' credentialType '.help'
|
||||
)
|
||||
}}
|
||||
</F.HelperText>
|
||||
</G.RadioField>
|
||||
{{/each}}
|
||||
</Hds::Form::Radio::Group>
|
||||
|
||||
{{#if this.showDynamicCredentials}}
|
||||
{{! role_arn }}
|
||||
<Hds::Form::TextInput::Field
|
||||
name='role_arn'
|
||||
@isRequired={{true}}
|
||||
@isOptional={{true}}
|
||||
@value={{@model.role_arn}}
|
||||
@isInvalid={{@model.errors.role_arn}}
|
||||
disabled={{form.disabled}}
|
||||
@@ -333,7 +310,7 @@
|
||||
<Hds::Form::TextInput::Field
|
||||
name='access_key_id'
|
||||
@value={{@model.access_key_id}}
|
||||
@isRequired={{true}}
|
||||
@isOptional={{true}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'access_key_id')}}
|
||||
as |F|
|
||||
@@ -352,7 +329,7 @@
|
||||
<Hds::Form::TextInput::Field
|
||||
name='secret_access_key'
|
||||
@value={{@model.secret_access_key}}
|
||||
@isRequired={{true}}
|
||||
@isOptional={{true}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'secret_access_key')}}
|
||||
as |F|
|
||||
|
||||
@@ -11,6 +11,7 @@ import { debounce } from 'core/decorators/debounce';
|
||||
import { loading } from 'ember-loading';
|
||||
import { confirm } from 'core/decorators/confirm';
|
||||
import { notifySuccess, notifyError } from 'core/decorators/notify';
|
||||
import { TYPE_STATIC_CREDENTIAL } from 'api/models/host-catalog';
|
||||
|
||||
export default class ScopesScopeHostCatalogsIndexController extends Controller {
|
||||
// =services
|
||||
@@ -63,6 +64,10 @@ export default class ScopesScopeHostCatalogsIndexController extends Controller {
|
||||
isNew ? 'notifications.create-success' : 'notifications.save-success',
|
||||
)
|
||||
async save(hostCatalog) {
|
||||
// If the role_arn is empty, then the credential type should be static
|
||||
if (!hostCatalog.role_arn) {
|
||||
hostCatalog.credentialType = TYPE_STATIC_CREDENTIAL;
|
||||
}
|
||||
await hostCatalog.save();
|
||||
if (this.can.can('read host-catalog', hostCatalog)) {
|
||||
await this.router.transitionTo(
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
@model={{@model}}
|
||||
@submit={{fn this.hostCatalogs.save @model}}
|
||||
@cancel={{fn this.hostCatalogs.cancel @model}}
|
||||
@changeCredentialType={{fn this.hostCatalogs.changeCredentialType @model}}
|
||||
/>
|
||||
</page.body>
|
||||
</Rose::Layout::Page>
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { visit, currentURL, click, fillIn } from '@ember/test-helpers';
|
||||
import { visit, currentURL, click, fillIn, find } from '@ember/test-helpers';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
import { Response } from 'miragejs';
|
||||
@@ -15,6 +15,10 @@ import {
|
||||
//invalidateSession,
|
||||
} from 'ember-simple-auth/test-support';
|
||||
import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
|
||||
import {
|
||||
TYPE_HOST_CATALOG_DYNAMIC,
|
||||
TYPE_HOST_CATALOG_PLUGIN_AWS,
|
||||
} from 'api/models/host-catalog';
|
||||
|
||||
module('Acceptance | host-catalogs | update', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
@@ -35,6 +39,7 @@ module('Acceptance | host-catalogs | update', function (hooks) {
|
||||
projectScope: null,
|
||||
hostCatalogs: null,
|
||||
hostCatalog: null,
|
||||
AWSHostCatalogWithStaticCredential: null,
|
||||
};
|
||||
|
||||
const NAME_INPUT_SELECTOR = '[name="name"]';
|
||||
@@ -44,6 +49,8 @@ module('Acceptance | host-catalogs | update', function (hooks) {
|
||||
const MODAL_DISCARD_BUTTON_SELECTOR =
|
||||
'.rose-dialog-footer button:first-child';
|
||||
const MODAL_CANCEL_BUTTON_SELECTOR = '.rose-dialog-footer button:last-child';
|
||||
const CREDENTIAL_TYPE_SELECTOR =
|
||||
'.dynamic-credential-selection input:checked';
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
// Generate resources
|
||||
@@ -59,15 +66,43 @@ module('Acceptance | host-catalogs | update', function (hooks) {
|
||||
instances.hostCatalog = this.server.create('host-catalog', {
|
||||
scope: instances.scopes.project,
|
||||
});
|
||||
instances.AWSHostCatalogWithStaticCredential = this.server.create(
|
||||
'host-catalog',
|
||||
{
|
||||
scope: instances.scopes.project,
|
||||
type: TYPE_HOST_CATALOG_DYNAMIC,
|
||||
plugin: { name: TYPE_HOST_CATALOG_PLUGIN_AWS },
|
||||
credentialType: 'static-credential',
|
||||
},
|
||||
);
|
||||
// Generate route URLs for resources
|
||||
urls.globalScope = `/scopes/global/scopes`;
|
||||
urls.orgScope = `/scopes/${instances.scopes.org.id}/scopes`;
|
||||
urls.projectScope = `/scopes/${instances.scopes.project.id}`;
|
||||
urls.hostCatalogs = `${urls.projectScope}/host-catalogs`;
|
||||
urls.hostCatalog = `${urls.hostCatalogs}/${instances.hostCatalog.id}`;
|
||||
urls.AWSHostCatalogWithStaticCredential = `${urls.hostCatalogs}/${instances.AWSHostCatalogWithStaticCredential.id}`;
|
||||
authenticateSession({});
|
||||
});
|
||||
|
||||
test('can update static AWS credentials to Dynamic AWS credentials', async function (assert) {
|
||||
await visit(urls.AWSHostCatalogWithStaticCredential);
|
||||
await click(EDIT_BUTTON_SELECTOR, 'Activate edit mode');
|
||||
assert.strictEqual(
|
||||
find(CREDENTIAL_TYPE_SELECTOR).value,
|
||||
'static-credential',
|
||||
);
|
||||
await click('[value=dynamic-credential]', 'Dynamic Credential');
|
||||
await fillIn('[name=role_arn]', 'arn:aws:iam');
|
||||
|
||||
await click(SAVE_BUTTON_SELECTOR);
|
||||
// Check if the host catalog is updated
|
||||
assert.strictEqual(
|
||||
find(CREDENTIAL_TYPE_SELECTOR).value,
|
||||
'dynamic-credential',
|
||||
);
|
||||
});
|
||||
|
||||
test('can save changes to existing host catalog', async function (assert) {
|
||||
assert.notEqual(instances.hostCatalog.name, 'random string');
|
||||
await visit(urls.hostCatalog);
|
||||
|
||||
Reference in new issue
Block a user