diff --git a/addons/api/addon/generated/models/host-catalog.js b/addons/api/addon/generated/models/host-catalog.js index a300bf59d..30ada833f 100644 --- a/addons/api/addon/generated/models/host-catalog.js +++ b/addons/api/addon/generated/models/host-catalog.js @@ -66,7 +66,6 @@ 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.', }) @@ -78,6 +77,7 @@ export default class GeneratedHostCatalogModel extends BaseModel { }) region; + // AWS static credentials @attr('string', { isNestedSecret: true, description: '', @@ -90,6 +90,31 @@ export default class GeneratedHostCatalogModel extends BaseModel { }) secret_access_key; + // AWS dynamic credentials + @attr('string', { + isNestedAttribute: true, + description: 'The role ARN to use.', + }) + role_arn; + + @attr('string', { + isNestedAttribute: true, + description: 'The role external ID to use.', + }) + role_external_id; + + @attr('string', { + isNestedAttribute: true, + description: 'The role session to use.', + }) + role_session_name; + + @attr('object-as-array', { + isNestedAttribute: true, + description: 'The role tags to use.', + }) + role_tags; + // Azure specific @attr('string', { diff --git a/addons/api/addon/models/host-catalog.js b/addons/api/addon/models/host-catalog.js index 4ede285f2..4a556035a 100644 --- a/addons/api/addon/models/host-catalog.js +++ b/addons/api/addon/models/host-catalog.js @@ -19,9 +19,18 @@ export const TYPES_HOST_CATALOG_PLUGIN = [ TYPE_HOST_CATALOG_PLUGIN_AZURE, ]; +export const TYPE_CREDENTIAL_STATIC = 'static-credential'; +export const TYPE_CREDENTIAL_DYNAMIC = 'dynamic-credential'; + +export const TYPES_CREDENTIALS = Object.freeze([ + TYPE_CREDENTIAL_STATIC, + TYPE_CREDENTIAL_DYNAMIC, +]); + export default class HostCatalogModel extends GeneratedHostCatalogModel { // =attributes + #credentialType; /** * True if the host catalog is static. * @type {boolean} @@ -38,6 +47,29 @@ export default class HostCatalogModel extends GeneratedHostCatalogModel { return this.type === 'plugin'; } + /** + * Gets a value from credentialType + * @type {string} + */ + get credentialType() { + if (!this.#credentialType) { + if (this.role_arn) { + this.#credentialType = TYPE_CREDENTIAL_DYNAMIC; + } else { + this.#credentialType = TYPE_CREDENTIAL_STATIC; + } + } + return this.#credentialType; + } + + /** + * Sets a value to credentialType + * @type {string} + */ + set credentialType(type) { + this.#credentialType = type; + } + /** * True if the host catalog is an unknown type. * @type {boolean} diff --git a/addons/api/addon/serializers/host-catalog.js b/addons/api/addon/serializers/host-catalog.js index 0f978ca67..2e3163df6 100644 --- a/addons/api/addon/serializers/host-catalog.js +++ b/addons/api/addon/serializers/host-catalog.js @@ -4,30 +4,53 @@ */ import ApplicationSerializer from './application'; +import { + TYPE_HOST_CATALOG_PLUGIN_AWS, + TYPE_HOST_CATALOG_PLUGIN_AZURE, +} from '../models/host-catalog'; -const fieldsByType = { - aws: [ - 'disable_credential_rotation', - 'region', +// These fields are Azure specific +const azureFields = [ + 'disable_credential_rotation', + 'tenant_id', + 'client_id', + 'subscription_id', + 'secret_id', + 'secret_value', +]; + +// These fields are AWS specific, AWS provider now has two credentialTypes: static and dynamic [not to be confused with static and dynamic host-catalogs] +const AWSfieldsWithCredentialType = { + 'static-credential': [ 'access_key_id', 'secret_access_key', + 'region', + 'disable_credential_rotation', 'worker_filter', ], - azure: [ + 'dynamic-credential': [ + 'role_arn', + 'role_external_id', + 'role_session_name', + 'role_tags', + 'region', 'disable_credential_rotation', - 'tenant_id', - 'client_id', - 'subscription_id', - 'secret_id', - 'secret_value', + 'worker_filter', ], }; export default class HostCatalogSerializer extends ApplicationSerializer { - // =methods serialize(snapshot) { + const { compositeType } = snapshot.record; const serialized = super.serialize(...arguments); - switch (snapshot.record.compositeType) { + + // This is a hack to remove the worker_filter field from the serialized data if the compositeType is not AWS + // Ideally we should refactor the model to include the compositeType using `for` attribute + if (compositeType !== TYPE_HOST_CATALOG_PLUGIN_AWS) { + delete serialized.worker_filter; + } + + switch (compositeType) { case 'static': return this.serializeStatic(...arguments); default: @@ -37,31 +60,28 @@ export default class HostCatalogSerializer extends ApplicationSerializer { serializeAttribute(snapshot, json, key, attribute) { const value = super.serializeAttribute(...arguments); - const { isPlugin, compositeType } = snapshot.record; + const { compositeType, credentialType, isPlugin } = snapshot.record; const { options } = attribute; - // Delete any fields that don't belong to the compositeType - if ( - isPlugin && - json && - options?.for && - !options.for.includes(compositeType) && - !fieldsByType[compositeType].includes(key) - ) { - delete json[key]; - } + const fields = + compositeType === TYPE_HOST_CATALOG_PLUGIN_AZURE + ? azureFields + : AWSfieldsWithCredentialType[credentialType]; // Delete any nested attribute fields that don't belong to the record type if (isPlugin && options.isNestedAttribute && json.attributes) { - if (!fieldsByType[compositeType].includes(key)) { + if (!fields.includes(key)) { delete json.attributes[key]; } } // Delete any secret fields that don't belong to the record type if (isPlugin && options.isNestedSecret && json.secrets) { - if (!fieldsByType[compositeType].includes(key)) { + if (!fields.includes(key)) { delete json.secrets[key]; + if (json['secrets'] && Object.keys(json.secrets).length === 0) { + delete json.secrets; + } } } return value; @@ -72,6 +92,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; } } diff --git a/addons/api/tests/unit/serializers/host-catalog-test.js b/addons/api/tests/unit/serializers/host-catalog-test.js index c38352d32..b8b1b7530 100644 --- a/addons/api/tests/unit/serializers/host-catalog-test.js +++ b/addons/api/tests/unit/serializers/host-catalog-test.js @@ -5,6 +5,11 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; +import { + TYPE_HOST_CATALOG_PLUGIN_AWS, + TYPE_CREDENTIAL_STATIC, + TYPE_CREDENTIAL_DYNAMIC, +} from 'api/models/host-catalog'; module('Unit | Serializer | host catalog', function (hooks) { setupTest(hooks); @@ -31,15 +36,104 @@ module('Unit | Serializer | host catalog', function (hooks) { assert.deepEqual(record.serialize(), expectedResult); }); - test('it serializes a new aws plugin as expected, ignoring azure fields', async function (assert) { + test('it serializes a new aws static plugin as expected', async function (assert) { const store = this.owner.lookup('service:store'); const record = store.createRecord('host-catalog', { - compositeType: 'aws', + compositeType: TYPE_HOST_CATALOG_PLUGIN_AWS, + credentialType: TYPE_CREDENTIAL_STATIC, name: 'AWS', 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', + role_external_id: 'test', + role_session_name: 'test', + role_tags: [ + { key: 'Project', value: 'Automation' }, + { key: 'foo', value: 'bar' }, + ], + }); + const expectedResult = { + name: 'AWS', + description: 'this is a Aws plugin host-catalog', + type: 'plugin', + worker_filter: 'workerfilter', + attributes: { + disable_credential_rotation: true, + region: 'west', + }, + secrets: { + access_key_id: 'foobars', + secret_access_key: 'testing', + }, + }; + assert.deepEqual(record.serialize(), expectedResult); + }); + + test('it serializes a new aws dynamic plugin as expected', async function (assert) { + const store = this.owner.lookup('service:store'); + const record = store.createRecord('host-catalog', { + compositeType: TYPE_HOST_CATALOG_PLUGIN_AWS, + name: 'AWS', + credentialType: TYPE_CREDENTIAL_DYNAMIC, + 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', + role_external_id: 'test', + role_session_name: 'test', + role_tags: [ + { key: 'Project', value: 'Automation' }, + { key: 'foo', value: 'bar' }, + ], + }); + const expectedResult = { + name: 'AWS', + description: 'this is a Aws plugin host-catalog', + type: 'plugin', + worker_filter: 'workerfilter', + attributes: { + disable_credential_rotation: true, + region: 'west', + role_external_id: 'test', + role_session_name: 'test', + role_arn: 'test', + role_tags: { Project: 'Automation', foo: 'bar' }, + }, + }; + assert.deepEqual(record.serialize(), expectedResult); + }); + + test('it serializes a new aws static plugin as expected, ignoring azure fields', async function (assert) { + const store = this.owner.lookup('service:store'); + const record = store.createRecord('host-catalog', { + compositeType: TYPE_HOST_CATALOG_PLUGIN_AWS, + name: 'AWS', + credentialType: TYPE_CREDENTIAL_STATIC, + description: 'this is a Aws plugin host-catalog', + disable_credential_rotation: true, + worker_filter: 'workerfilter', + // these are AWS fields and should be included region: 'spain', access_key_id: 'foobars', secret_access_key: 'testing', @@ -109,6 +203,7 @@ module('Unit | Serializer | host catalog', function (hooks) { data: { id: '1', type: 'host-catalog', + credentialType: 'static', attributes: { type: 'plugin', name: 'aws', diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml index 75bf4d0af..6b2e80bd5 100644 --- a/addons/core/translations/resources/en-us.yaml +++ b/addons/core/translations/resources/en-us.yaml @@ -218,6 +218,17 @@ host-catalog: aws: AWS azure: Azure plugin: Dynamic + credential: + label: Credential type + help: Select the credential type that you want to use. + static-credential: + edit: Static Credentials + label: Use an access key (Static Credentials) + help: Use an access key that you have generated in AWS to use with this AWS Host Catalog. + dynamic-credential: + edit: Dynamic Credentials + label: Use Assume Role (Dynamic Credentials) + help: Specify an Assume Role used to generate credentials to use with this AWS Host Catalog. form: aws_region: label: AWS Region diff --git a/ui/admin/app/components/form/host-catalog/aws/index.hbs b/ui/admin/app/components/form/host-catalog/aws/index.hbs index b2a0891cb..4b47803cc 100644 --- a/ui/admin/app/components/form/host-catalog/aws/index.hbs +++ b/ui/admin/app/components/form/host-catalog/aws/index.hbs @@ -19,12 +19,18 @@ {{on 'input' (set-from-event @model 'name')}} as |F| > - {{t 'form.name.label'}} - {{t 'form.name.help'}} + + {{t 'form.name.label'}} + + + {{t 'form.name.help'}} + {{#if @model.errors.name}} {{#each @model.errors.name as |error|}} - {{error.message}} + + {{error.message}} + {{/each}} {{/if}} @@ -38,21 +44,28 @@ disabled={{form.disabled}} as |F| > - {{t 'form.description.label'}} - {{t 'form.description.help'}} + + {{t 'form.description.label'}} + + + {{t 'form.description.help'}} + {{#if @model.errors.description}} {{#each @model.errors.description as |error|}} - {{error.message}} + + {{error.message}} + {{/each}} {{/if}} {{#if @model.isNew}} - - {{t 'form.type.label'}} + + {{t 'form.type.label'}} + {{#each this.hostCatalogTypes as |hostCatalogType|}} - {{t - (concat 'resources.host-catalog.types.' hostCatalogType) - }} - {{t - (concat 'resources.host-catalog.help.' hostCatalogType) - }} + + {{t (concat 'resources.host-catalog.types.' hostCatalogType)}} + + + {{t (concat 'resources.host-catalog.help.' hostCatalogType)}} + {{/each}} - {{t 'titles.provider'}} - {{t 'titles.choose-a-provider'}} - {{t 'descriptions.choose-a-provider'}} + + {{t 'titles.provider'}} + + + {{t 'titles.choose-a-provider'}} + + + {{t 'descriptions.choose-a-provider'}} + {{#each-in this.mapResourceTypeWithIcon as |pluginType icon|}} - {{t - (concat 'resources.host-catalog.types.' pluginType) - }} + + {{t (concat 'resources.host-catalog.types.' pluginType)}} + - {{/each-in}} @@ -98,8 +116,11 @@ readonly={{true}} as |F| > - {{t 'titles.provider'}} - {{t 'descriptions.provider'}} + + {{t 'titles.provider'}} + + + {{t 'descriptions.provider'}} {{/if}} @@ -112,7 +133,9 @@ {{on 'input' (set-from-event @model 'region')}} as |F| > - {{t 'resources.host-catalog.form.aws_region.label'}} + + {{t 'resources.host-catalog.form.aws_region.label'}} + {{t 'resources.host-catalog.form.aws_region.help'}} @@ -121,39 +144,225 @@ - - {{t 'form.access_key_id.label'}} - - {{t 'form.access_key_id.help'}} - - {{t 'actions.learn-more'}} - - - + {{#if @model.isNew}} + + + {{t 'resources.host-catalog.types.credential.label'}} + + + {{t 'resources.host-catalog.types.credential.help'}} + - - {{t 'form.secret_access_key.label'}} - - {{t 'form.secret_access_key.help'}} - - {{t 'actions.learn-more'}} - - - + {{#each this.credentials as |credentialType|}} + + + {{t + (concat + 'resources.host-catalog.types.credential.' + credentialType + '.label' + ) + }} + + + {{t + (concat + 'resources.host-catalog.types.credential.' + credentialType + '.help' + ) + }} + + + {{/each}} + + {{else}} + + + {{t 'resources.host-catalog.types.credential.label'}} + + + + + + {{/if}} + + {{#if this.showDynamicCredentials}} + {{! role_arn }} + + + {{t 'resources.storage-bucket.form.role_arn.label'}} + + + {{t 'resources.storage-bucket.form.role_arn.help'}} + + {{#if @model.errors.role_arn}} + + {{#each @model.errors.role_arn as |error|}} + + {{error.message}} + + {{/each}} + + {{/if}} + + + {{! role_external_id }} + + + {{t 'resources.storage-bucket.form.role_external_id.label'}} + + + {{t 'resources.storage-bucket.form.role_external_id.help'}} + + {{#if @model.errors.role_external_id}} + + {{#each @model.errors.role_external_id as |error|}} + + {{error.message}} + + {{/each}} + + {{/if}} + + + {{! role_session_name }} + + + {{t 'resources.storage-bucket.form.role_session_name.label'}} + + + {{t 'resources.storage-bucket.form.role_session_name.help'}} + + {{#if @model.errors.role_session_name}} + + {{#each @model.errors.role_session_name as |error|}} + + {{error.message}} + + {{/each}} + + {{/if}} + + + {{! role_tags }} + + <:fieldset as |F|> + + {{t 'resources.storage-bucket.form.role_tags.label'}} + + + {{t 'resources.storage-bucket.form.role_tags.help'}} + + + {{#if @model.errors.role_tags}} + + {{#each @model.errors.role_tags as |error|}} + + {{error.message}} + + {{/each}} + + {{/if}} + + <:field as |F|> + + <:key as |K|> + + + <:value as |V|> + + + + + + + {{else}} + + + {{t 'form.access_key_id.label'}} + + + {{t 'form.access_key_id.help'}} + + {{t 'actions.learn-more'}} + + + + + + + {{t 'form.secret_access_key.label'}} + + + {{t 'form.secret_access_key.help'}} + + {{t 'actions.learn-more'}} + + + + {{/if}} {{#each @model.errors.worker_filter as |error|}} - {{error.message}} + + {{error.message}} + {{/each}} {{/if}} @@ -188,7 +399,9 @@ {{on 'change' (fn this.toggleDisableCredentialRotation @model)}} as |F| > - {{t 'form.disable_credential_rotation.label'}} + + {{t 'form.disable_credential_rotation.label'}} +   diff --git a/ui/admin/app/components/form/host-catalog/aws/index.js b/ui/admin/app/components/form/host-catalog/aws/index.js index 166e0c2f0..af42090d0 100644 --- a/ui/admin/app/components/form/host-catalog/aws/index.js +++ b/ui/admin/app/components/form/host-catalog/aws/index.js @@ -8,7 +8,10 @@ import { action } from '@ember/object'; import { TYPES_HOST_CATALOG, TYPES_HOST_CATALOG_PLUGIN, + TYPES_CREDENTIALS, + TYPE_CREDENTIAL_DYNAMIC, } from 'api/models/host-catalog'; +import { tracked } from '@glimmer/tracking'; //Note: this is a temporary solution till we have resource type helper in place const icons = ['aws-color', 'azure-color']; @@ -16,6 +19,21 @@ const icons = ['aws-color', 'azure-color']; export default class FormHostCatalogAwsComponent extends Component { // =properties hostCatalogTypes = TYPES_HOST_CATALOG; + + // =attributes + @tracked selectedCredentialType = this.args.model.credentialType; + + @tracked showDynamicCredentials = + this.args.model.credentialType === TYPE_CREDENTIAL_DYNAMIC; + + /** + * returns an array of available credential types + * @type {array} + */ + get credentials() { + return TYPES_CREDENTIALS; + } + /** * maps resource type with icon * @type {object} @@ -32,4 +50,19 @@ export default class FormHostCatalogAwsComponent extends Component { toggleDisableCredentialRotation(model) { model.disable_credential_rotation = !model.disable_credential_rotation; } + + /** + * Allows to update the credential type + * @param type {string} + */ + @action + updateCredentialTypeSelection(type) { + this.selectedCredentialType = type; + if (type === TYPE_CREDENTIAL_DYNAMIC) { + this.showDynamicCredentials = true; + } else { + this.showDynamicCredentials = false; + } + this.args.changeCredentialType(type); + } } diff --git a/ui/admin/app/components/form/host-catalog/index.hbs b/ui/admin/app/components/form/host-catalog/index.hbs index 5b1666529..49a01af82 100644 --- a/ui/admin/app/components/form/host-catalog/index.hbs +++ b/ui/admin/app/components/form/host-catalog/index.hbs @@ -9,4 +9,5 @@ submit=@submit cancel=@cancel changeType=@changeType + changeCredentialType=@changeCredentialType }} \ No newline at end of file diff --git a/ui/admin/app/components/form/storage-bucket/aws/index.hbs b/ui/admin/app/components/form/storage-bucket/aws/index.hbs index c38a88a27..4006830f6 100644 --- a/ui/admin/app/components/form/storage-bucket/aws/index.hbs +++ b/ui/admin/app/components/form/storage-bucket/aws/index.hbs @@ -424,7 +424,7 @@ {{t 'resources.storage-bucket.form.role_tags.label'}} - {{t 'resources.storage-bucket.form.role_tags.label'}} + {{t 'resources.storage-bucket.form.role_tags.help'}} {{#if @model.errors.role_tags}} @@ -436,11 +436,18 @@ {{/if}} <:field as |F|> - + > + <:key as |K|> + + + <:value as |V|> + + + {{/if}} diff --git a/ui/admin/app/controllers/scopes/scope/host-catalogs/index.js b/ui/admin/app/controllers/scopes/scope/host-catalogs/index.js index 6fed907f7..ad67ebade 100644 --- a/ui/admin/app/controllers/scopes/scope/host-catalogs/index.js +++ b/ui/admin/app/controllers/scopes/scope/host-catalogs/index.js @@ -99,4 +99,14 @@ export default class ScopesScopeHostCatalogsIndexController extends Controller { if (type === 'plugin') type = 'aws'; this.router.replaceWith({ queryParams: { type } }); } + + /** + * Updates credential type + * @param {model} hostcatalog + * @param {string} credentialType + */ + @action + changeCredentialType(hostCatalog, credentialType) { + hostCatalog.credentialType = credentialType; + } } diff --git a/ui/admin/app/templates/scopes/scope/host-catalogs/new.hbs b/ui/admin/app/templates/scopes/scope/host-catalogs/new.hbs index b73a75b92..674fb32cc 100644 --- a/ui/admin/app/templates/scopes/scope/host-catalogs/new.hbs +++ b/ui/admin/app/templates/scopes/scope/host-catalogs/new.hbs @@ -29,6 +29,7 @@ @submit={{fn this.hostCatalogs.save @model}} @cancel={{fn this.hostCatalogs.cancel @model}} @changeType={{this.hostCatalogs.changeType}} + @changeCredentialType={{fn this.hostCatalogs.changeCredentialType @model}} />