Icu 12933 updates to target model to support aliasing (#2214)

* feat: 🎸 target aliases association API, form creation
This commit is contained in:
Dharini Jeeva authored and GitHub committed 2024-04-05 16:35:23 -07:00
1 parent ac5110d94a
commit c8839d6db1
19 files changed
+197 -60

No files matched your search

@@ -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;
}
+4 -4
View File
@@ -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);
}
/**
+8
View File
@@ -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;
+19
View File
@@ -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;
}
}
@@ -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,
}));
}
}
@@ -3,4 +3,4 @@
* SPDX-License-Identifier: MPL-2.0
*/
export { default } from 'api/transforms/principal-array';
export { default } from 'api/transforms/array';
@@ -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,
@@ -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), []);
});
});
@@ -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);
});
});
@@ -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.
@@ -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
@@ -325,6 +325,42 @@
</Hds::Form::TextInput::Field>
{{/if}}
{{#if
(and @model.isNew (can 'create model' @globalScope collection='aliases'))
}}
<Form::Field::ListWrapper
@layout='horizontal'
@isOptional={{true}}
@disabled={{form.disabled}}
>
<:fieldset as |F|>
<F.Legend>
{{t 'resources.alias.form.alias.label'}}
</F.Legend>
<F.HelperText>
{{t 'resources.alias.form.alias.help'}}
</F.HelperText>
{{#if @model.errors.with_aliases}}
<F.Error as |E|>
{{#each @model.errors.with_aliases as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</:fieldset>
<:field as |F|>
<F.TextInput
@name='with_aliases'
@options={{@model.with_aliases}}
@model={{@model}}
/>
</:field>
</Form::Field::ListWrapper>
{{/if}}
{{#if (can 'save model' @model)}}
<form.actions
@enableEditText={{t 'actions.edit-form'}}
@@ -9,6 +9,7 @@
model=@model
submit=@submit
cancel=@cancel
globalScope=@globalScope
changeType=@changeType
}}
{{/if}}
@@ -9,4 +9,5 @@
@submit={{@submit}}
@cancel={{@cancel}}
@changeType={{@changeType}}
@globalScope={{@globalScope}}
/>
@@ -9,4 +9,5 @@
@submit={{@submit}}
@cancel={{@cancel}}
@changeType={{@changeType}}
@globalScope={{@globalScope}}
/>
@@ -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
@@ -22,10 +22,10 @@
</h2>
<p>{{t 'resources.target.description'}}</p>
</page.header>
<page.body>
<Form::Target
@model={{@model}}
@globalScope={{this.globalScope}}
@submit={{route-action 'save' @model}}
@cancel={{route-action 'cancel' @model}}
@changeType={{route-action 'changeType'}}
@@ -32,6 +32,8 @@ module('Acceptance | targets | create', function (hooks) {
let getSSHTargetCount;
let featuresService;
const SAVE_BTN_SELECTOR = '[type="submit"]';
const instances = {
scopes: {
global: null,
@@ -102,7 +104,7 @@ module('Acceptance | targets | create', function (hooks) {
await click('[value="ssh"]');
await fillIn('[name="name"]', 'random string');
await fillIn('[name="worker_filter"]', 'random filter');
await click('[type="submit"]');
await click(SAVE_BTN_SELECTOR);
assert.strictEqual(getSSHTargetCount(), sshTargetCount + 1);
assert.strictEqual(getTargetCount(), targetCount + 1);
@@ -133,7 +135,7 @@ module('Acceptance | targets | create', function (hooks) {
'Egress toggle',
);
await fillIn('[name="egress_worker_filter"]', 'random filter');
await click('[type="submit"]');
await click(SAVE_BTN_SELECTOR);
assert.strictEqual(getSSHTargetCount(), sshTargetCount + 1);
assert.strictEqual(getTargetCount(), targetCount + 1);
@@ -161,7 +163,7 @@ module('Acceptance | targets | create', function (hooks) {
await click('[value="tcp"]');
await fillIn('[name="name"]', 'random string');
await fillIn('[name="worker_filter"]', 'random filter');
await click('[type="submit"]');
await click(SAVE_BTN_SELECTOR);
assert.strictEqual(getTargetCount(), targetCount + 1);
assert.strictEqual(getTCPTargetCount(), tcpTargetCount + 1);
@@ -193,7 +195,7 @@ module('Acceptance | targets | create', function (hooks) {
);
await fillIn('[name="egress_worker_filter"]', 'random filter');
await click('[type="submit"]');
await click(SAVE_BTN_SELECTOR);
assert.strictEqual(getTargetCount(), targetCount + 1);
assert.strictEqual(getTCPTargetCount(), tcpTargetCount + 1);
@@ -229,7 +231,7 @@ module('Acceptance | targets | create', function (hooks) {
);
await fillIn('[name="ingress_worker_filter"]', 'random filter');
await click('[type="submit"]');
await click(SAVE_BTN_SELECTOR);
assert.strictEqual(getTargetCount(), targetCount + 1);
assert.strictEqual(getTCPTargetCount(), tcpTargetCount + 1);
@@ -323,6 +325,43 @@ module('Acceptance | targets | create', function (hooks) {
assert.strictEqual(getTCPTargetCount(), tcpTargetCount);
});
test('can add aliases during target creation', async function (assert) {
const targetCount = getTargetCount();
const tcpTargetCount = getTCPTargetCount();
await visit(urls.targets);
const name = 'target';
await click(`[href="${urls.newTarget}"]`);
await fillIn('[name="name"]', name);
assert.ok(
instances.scopes.global.authorized_collection_actions.aliases.includes(
'create',
),
);
await fillIn(
'[name="with_aliases"] tbody tr:nth-of-type(1) input',
'alias 1',
);
await click('[name="with_aliases"] tbody tr:nth-of-type(1) button');
await fillIn(
'[name="with_aliases"] tbody tr:nth-of-type(2) input',
'alias 2',
);
await click('[name="with_aliases"] tbody tr:nth-of-type(2) button');
await click(SAVE_BTN_SELECTOR);
assert.strictEqual(getTargetCount(), targetCount + 1);
const target = this.server.schema.targets.findBy({ name });
assert.deepEqual(target.withAliases, [
{ value: 'alias 1', scope_id: 'global' },
{ value: 'alias 2', scope_id: 'global' },
]);
assert.strictEqual(getTCPTargetCount(), tcpTargetCount + 1);
});
test('can cancel create new SSH target', async function (assert) {
featuresService.enable('ssh-target');
featuresService.enable('target-worker-filters-v2');
@@ -368,7 +407,7 @@ module('Acceptance | targets | create', function (hooks) {
});
await visit(urls.newTCPTarget);
await click('[type="submit"]');
await click(SAVE_BTN_SELECTOR);
assert.dom('[role="alert"] div').hasText('The request was invalid.');
assert.dom('.hds-form-error__message').hasText('Name is required.');
@@ -395,7 +434,7 @@ module('Acceptance | targets | create', function (hooks) {
);
});
await visit(urls.newSSHTarget);
await click('[type="submit"]');
await click(SAVE_BTN_SELECTOR);
assert.dom('[role="alert"] div').hasText('The request was invalid.');
assert.dom('.hds-form-error__message').hasText('Name is required.');
@@ -411,7 +450,7 @@ module('Acceptance | targets | create', function (hooks) {
await fillIn('[name="name"]', 'random string');
await fillIn('[name="address"]', '0.0.0.0');
await click('[type="submit"]');
await click(SAVE_BTN_SELECTOR);
assert.strictEqual(getTargetCount(), targetCount + 1);
assert.strictEqual(
@@ -372,7 +372,7 @@ module('Acceptance | targets | update', function (hooks) {
await visit(urls.targets);
await click(`[href="${url}"]`);
await click('form [type="button"]', 'Activate edit mode');
await click('.rose-form-actions [type="button"]', 'Activate edit mode');
await fillIn('[name="address"]', '0.0.0.0');
await click('[type="submit"]');
@@ -417,7 +417,7 @@ module('Acceptance | targets | update', function (hooks) {
await visit(urls.targets);
await click(`[href="${url}"]`);
await click('form [type="button"]', 'Activate edit mode');
await click('.rose-form-actions [type="button"]', 'Activate edit mode');
await fillIn('[name="address"]', '0.0.0.0');
await click('[type="submit"]');