Update credentials (#2523)

* feat: 🎸 allow AWS host catalog credential type update
This commit is contained in:
Dharini Jeeva authored and GitHub committed 2024-10-18 14:33:14 -07:00
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: '',
})
+8 -1
View File
@@ -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;
+18 -4
View File
@@ -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);