Icu 12933 updates to target model to support aliasing (#2214)
* feat: 🎸 target aliases association API, form creation
This commit is contained in:
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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"]');
|
||||
|
||||
|
||||
Reference in new issue
Block a user