diff --git a/addons/api/addon/generated/models/target.js b/addons/api/addon/generated/models/target.js index d363a9d03..86605800c 100644 --- a/addons/api/addon/generated/models/target.js +++ b/addons/api/addon/generated/models/target.js @@ -108,4 +108,10 @@ export default class GeneratedTargetModel extends BaseModel { isNestedAttribute: true, }) enable_session_recording; + + @attr('array', { + description: + 'with_aliases specify the aliases that should be created when the target is created. ', + }) + with_aliases; } diff --git a/addons/api/addon/models/role.js b/addons/api/addon/models/role.js index b0d56e6b7..e574eb7e7 100644 --- a/addons/api/addon/models/role.js +++ b/addons/api/addon/models/role.js @@ -27,7 +27,7 @@ export default class RoleModel extends GeneratedRoleModel { * see obvious. Instead, the application layer is expected to load referenced * users and groups as needed. */ - @attr('principal-array', { + @attr('array', { readOnly: true, emptyArrayIfMissing: true, }) @@ -55,7 +55,7 @@ export default class RoleModel extends GeneratedRoleModel { get userIDs() { return this.principals .filter(({ type }) => type === 'user') - .map(({ principal_id }) => principal_id); + .map(({ id }) => id); } /** @@ -65,7 +65,7 @@ export default class RoleModel extends GeneratedRoleModel { get groupIDs() { return this.principals .filter(({ type }) => type === 'group') - .map(({ principal_id }) => principal_id); + .map(({ id }) => id); } /** @@ -75,7 +75,7 @@ export default class RoleModel extends GeneratedRoleModel { get managedGroupIDs() { return this.principals .filter(({ type }) => type === 'managed group') - .map(({ principal_id }) => principal_id); + .map(({ id }) => id); } /** diff --git a/addons/api/addon/serializers/target.js b/addons/api/addon/serializers/target.js index 17a00ced5..d667e3ab9 100644 --- a/addons/api/addon/serializers/target.js +++ b/addons/api/addon/serializers/target.js @@ -44,6 +44,14 @@ export default class TargetSerializer extends ApplicationSerializer { injectedApplicationCredentialSourceIDs, ); + if (serialized?.with_aliases) { + // API expects scope id along with every alias value + serialized.with_aliases = serialized.with_aliases.map((item) => ({ + ...item, + scope_id: 'global', + })); + } + // Delete session recording related fields from non-SSH targets if (type !== TYPE_TARGET_SSH) { delete serialized?.attributes?.storage_bucket_id; diff --git a/addons/api/addon/transforms/array.js b/addons/api/addon/transforms/array.js new file mode 100644 index 000000000..7df725a1e --- /dev/null +++ b/addons/api/addon/transforms/array.js @@ -0,0 +1,19 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import Transform from '@ember-data/serializer/transform'; +import { TrackedArray } from 'tracked-built-ins'; + +export default class ArrayTransform extends Transform { + deserialize(serialized) { + const arr = new TrackedArray(serialized || []); + return arr; + } + + serialize(deserialized) { + const arr = deserialized || []; + return arr; + } +} diff --git a/addons/api/addon/transforms/principal-array.js b/addons/api/addon/transforms/principal-array.js deleted file mode 100644 index 07f7aa551..000000000 --- a/addons/api/addon/transforms/principal-array.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import Transform from '@ember-data/serializer/transform'; - -export default class PrincipalArrayTransform extends Transform { - deserialize(serialized) { - const stringValues = serialized || []; - return stringValues.map(({ id: principal_id, scope_id, type }) => ({ - principal_id, - scope_id, - type, - })); - } - - serialize(deserialized) { - const strings = deserialized || []; - return strings.map(({ principal_id: id, scope_id, type }) => ({ - id, - scope_id, - type, - })); - } -} diff --git a/addons/api/app/transforms/principal-array.js b/addons/api/app/transforms/array.js similarity index 57% rename from addons/api/app/transforms/principal-array.js rename to addons/api/app/transforms/array.js index 5a644f5ba..4a049828f 100644 --- a/addons/api/app/transforms/principal-array.js +++ b/addons/api/app/transforms/array.js @@ -3,4 +3,4 @@ * SPDX-License-Identifier: MPL-2.0 */ -export { default } from 'api/transforms/principal-array'; +export { default } from 'api/transforms/array'; diff --git a/addons/api/tests/unit/serializers/target-test.js b/addons/api/tests/unit/serializers/target-test.js index 76dd6599b..bc6fa8739 100644 --- a/addons/api/tests/unit/serializers/target-test.js +++ b/addons/api/tests/unit/serializers/target-test.js @@ -36,6 +36,7 @@ module('Unit | Serializer | target', function (hooks) { address: '0.0.0.0', enable_session_recording: false, storage_bucket_id: null, + with_aliases: [{ value: 'a' }], }); const snapshot = record._createSnapshot(); snapshot.adapterOptions = {}; @@ -51,6 +52,7 @@ module('Unit | Serializer | target', function (hooks) { worker_filter: null, egress_worker_filter: null, ingress_worker_filter: null, + with_aliases: [{ value: 'a', scope_id: 'global' }], attributes: { default_port: 1234, default_client_port: 4321, @@ -85,6 +87,7 @@ module('Unit | Serializer | target', function (hooks) { enable_session_recording: false, storage_bucket_id: null, address: '0.0.0.0', + with_aliases: [{ value: 'www.test.com' }], }); const snapshot = record._createSnapshot(); snapshot.adapterOptions = {}; @@ -100,6 +103,7 @@ module('Unit | Serializer | target', function (hooks) { worker_filter: null, egress_worker_filter: null, ingress_worker_filter: null, + with_aliases: [{ value: 'www.test.com', scope_id: 'global' }], attributes: { default_port: 1234, default_client_port: 4321, @@ -189,6 +193,7 @@ module('Unit | Serializer | target', function (hooks) { enable_session_recording: true, storage_bucket_id: 'bucketID', default_port: 1234, + with_aliases: [], scope: { scope_id: 'org_1', }, @@ -206,6 +211,7 @@ module('Unit | Serializer | target', function (hooks) { worker_filter: null, egress_worker_filter: null, ingress_worker_filter: null, + with_aliases: [], attributes: { default_port: 1234, default_client_port: null, @@ -226,6 +232,7 @@ module('Unit | Serializer | target', function (hooks) { type: TYPE_TARGET_SSH, worker_filter: 'worker', default_port: 1234, + with_aliases: [], scope: { scope_id: 'org_1', }, @@ -243,6 +250,7 @@ module('Unit | Serializer | target', function (hooks) { worker_filter: 'worker', egress_worker_filter: null, ingress_worker_filter: null, + with_aliases: [], attributes: { default_port: 1234, default_client_port: null, @@ -263,6 +271,7 @@ module('Unit | Serializer | target', function (hooks) { type: TYPE_TARGET_SSH, egress_worker_filter: 'egress worker', default_port: 1234, + with_aliases: [], scope: { scope_id: 'org_1', }, @@ -280,6 +289,7 @@ module('Unit | Serializer | target', function (hooks) { worker_filter: null, egress_worker_filter: 'egress worker', ingress_worker_filter: null, + with_aliases: [], attributes: { default_port: 1234, default_client_port: null, @@ -300,6 +310,7 @@ module('Unit | Serializer | target', function (hooks) { type: TYPE_TARGET_SSH, ingress_worker_filter: 'ingress worker', default_port: 1234, + with_aliases: [], scope: { scope_id: 'org_1', }, @@ -317,6 +328,7 @@ module('Unit | Serializer | target', function (hooks) { worker_filter: null, egress_worker_filter: null, ingress_worker_filter: 'ingress worker', + with_aliases: [], attributes: { default_port: 1234, default_client_port: null, diff --git a/addons/api/tests/unit/transforms/array-test.js b/addons/api/tests/unit/transforms/array-test.js new file mode 100644 index 000000000..a2ccc3b78 --- /dev/null +++ b/addons/api/tests/unit/transforms/array-test.js @@ -0,0 +1,45 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Transform | array', function (hooks) { + setupTest(hooks); + + test('it serializes a principals array', function (assert) { + const transform = this.owner.lookup('transform:array'); + const principal = [ + { id: 2, type: 'user' }, + { id: 3, type: 'group' }, + { id: 3, type: 'managed-group' }, + ]; + const expectedArr = [ + { id: 2, type: 'user' }, + { id: 3, type: 'group' }, + { id: 3, type: 'managed-group' }, + ]; + const emptyArr = []; + assert.deepEqual(transform.serialize(principal), expectedArr); + assert.deepEqual(transform.serialize(emptyArr), []); + }); + + test('it deserializes a principals array', function (assert) { + const transform = this.owner.lookup('transform:array'); + const principal = [ + { id: 2, type: 'user' }, + { id: 3, type: 'group' }, + { id: 3, type: 'managed-group' }, + ]; + const expectedArr = [ + { id: 2, type: 'user' }, + { id: 3, type: 'group' }, + { id: 3, type: 'managed-group' }, + ]; + const emptyArr = []; + assert.deepEqual(transform.deserialize(principal), expectedArr); + assert.deepEqual(transform.deserialize(emptyArr), []); + }); +}); diff --git a/addons/api/tests/unit/transforms/principal-array-test.js b/addons/api/tests/unit/transforms/principal-array-test.js deleted file mode 100644 index 3628e465b..000000000 --- a/addons/api/tests/unit/transforms/principal-array-test.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import { module, test } from 'qunit'; -import { setupTest } from 'ember-qunit'; - -module('Unit | Transform | principal array', function (hooks) { - setupTest(hooks); - - // Replace this with your real tests. - test('it exists', function (assert) { - let transform = this.owner.lookup('transform:principal-array'); - assert.ok(transform); - }); -}); diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml index 85d29cef2..a897f54e5 100644 --- a/addons/core/translations/resources/en-us.yaml +++ b/addons/core/translations/resources/en-us.yaml @@ -1027,3 +1027,6 @@ alias: authorize_session_arguments: label: Host ID help: Always use this host when connecting using this alias + alias: + label: Aliases + help: You can create aliases for this target that will make it easier to reach. Users must have access to this project to use aliases. diff --git a/ui/admin/app/components/form/role/add-principals/index.js b/ui/admin/app/components/form/role/add-principals/index.js index d79322acb..a91cabcca 100644 --- a/ui/admin/app/components/form/role/add-principals/index.js +++ b/ui/admin/app/components/form/role/add-principals/index.js @@ -35,7 +35,7 @@ export default class FormRoleAddPrincipalsIndexComponent extends Component { get filteredPrincipals() { // Retrieve principal IDs assigned to current role const currentPrincipalIDs = this.args.model.principals.map( - (principal) => principal.principal_id, + (principal) => principal.id, ); // Retrieve principal IDs not assigned to current role diff --git a/ui/admin/app/components/form/target/details/index.hbs b/ui/admin/app/components/form/target/details/index.hbs index 91e247e64..29f0d99b3 100644 --- a/ui/admin/app/components/form/target/details/index.hbs +++ b/ui/admin/app/components/form/target/details/index.hbs @@ -325,6 +325,42 @@ {{/if}} + {{#if + (and @model.isNew (can 'create model' @globalScope collection='aliases')) + }} + + + <:fieldset as |F|> + + {{t 'resources.alias.form.alias.label'}} + + + {{t 'resources.alias.form.alias.help'}} + + + {{#if @model.errors.with_aliases}} + + {{#each @model.errors.with_aliases as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + + <:field as |F|> + + + + + {{/if}} {{#if (can 'save model' @model)}} \ No newline at end of file diff --git a/ui/admin/app/components/form/target/tcp/index.hbs b/ui/admin/app/components/form/target/tcp/index.hbs index caf4820ee..c810da580 100644 --- a/ui/admin/app/components/form/target/tcp/index.hbs +++ b/ui/admin/app/components/form/target/tcp/index.hbs @@ -9,4 +9,5 @@ @submit={{@submit}} @cancel={{@cancel}} @changeType={{@changeType}} + @globalScope={{@globalScope}} /> \ No newline at end of file diff --git a/ui/admin/app/routes/scopes/scope/targets/new.js b/ui/admin/app/routes/scopes/scope/targets/new.js index 229f24a53..2fa27cf94 100644 --- a/ui/admin/app/routes/scopes/scope/targets/new.js +++ b/ui/admin/app/routes/scopes/scope/targets/new.js @@ -68,6 +68,15 @@ export default class ScopesScopeTargetsNewRoute extends Route { return record; } + /** + * Adds global scope to the context so we can check if the user has permissions to create aliases. + * @param {Controller} controller + */ + async setupController(controller) { + super.setupController(...arguments); + const globalScope = await this.store.peekRecord('scope', 'global'); + controller.set('globalScope', globalScope); + } /** * Update type of target * @param {string} type diff --git a/ui/admin/app/templates/scopes/scope/targets/new.hbs b/ui/admin/app/templates/scopes/scope/targets/new.hbs index 7db5f04bc..9e6be2e9e 100644 --- a/ui/admin/app/templates/scopes/scope/targets/new.hbs +++ b/ui/admin/app/templates/scopes/scope/targets/new.hbs @@ -22,10 +22,10 @@

{{t 'resources.target.description'}}

-