Icu 5476 refactor target model serializer and mirage (#1235)

chore: 🤖 refactor target model to support credentials
This commit is contained in:
Dharini Jeeva authored and GitHub committed 2022-08-08 09:21:07 -07:00
1 parent 2cda4f639e
commit 1f0baceb5e
12 files changed
+210 -66

No files matched your search

+22 -18
View File
@@ -48,17 +48,21 @@ export default class TargetModel extends GeneratedTargetModel {
}
/**
* An array of resolved credential library instances. Model instances
* An array of resolved credential library and credential instances. Model instances
* must already be loaded into the store (this method will not load unloaded
* instances). Unresolvable instances are excluded from the array.
* @type {[CredentialLibraryModel]}
* @type {[CredentialLibraryModel, CredentialModel]}
*/
@computed('application_credential_source_ids.[]', 'store')
get credentialLibraries() {
get credentialSources() {
return this.application_credential_source_ids
.map((source) =>
this.store.peekRecord('credential-library', source.value)
)
.map((source) => {
if (source.value.startsWith('cred')) {
return this.store.peekRecord('credential', source.value);
} else {
return this.store.peekRecord('credential-library', source.value);
}
})
.filter(Boolean);
}
@@ -150,17 +154,17 @@ export default class TargetModel extends GeneratedTargetModel {
}
/**
* Adds credential libraries via the `add-credential-sources` method.
* Adds credential sources via the `add-credential-sources` method.
* See serializer and adapter for more information.
* @param {[string]} credentialLibraryIDs
* @param {[string]} credentialSourceIDs
* @param {object} options
* @param {object} options.adapterOptions
* @return {Promise}
*/
addCredentialSources(credentialLibraryIDs, options = { adapterOptions: {} }) {
addCredentialSources(credentialSourceIDs, options = { adapterOptions: {} }) {
const defaultAdapterOptions = {
method: 'add-credential-sources',
credentialLibraryIDs,
credentialSourceIDs,
};
// There is no "deep merge" in ES.
return this.save({
@@ -173,20 +177,20 @@ export default class TargetModel extends GeneratedTargetModel {
}
/**
* Delete credential libraries via the `remove-credential-sources` method.
* Delete credential libraries and credentials via the `remove-credential-sources` method.
* See serializer and adapter for more information.
* @param {[string]} credentialLibraryIDs
* @param {[string]} credentialSourceIDs
* @param {object} options
* @param {object} options.adapterOptions
* @return {Promise}
*/
removeCredentialSources(
credentialLibraryIDs,
credentialSourceIDs,
options = { adapterOptions: {} }
) {
const defaultAdapterOptions = {
method: 'remove-credential-sources',
credentialLibraryIDs,
credentialSourceIDs,
};
// There is no "deep merge" in ES.
return this.save({
@@ -199,13 +203,13 @@ export default class TargetModel extends GeneratedTargetModel {
}
/**
* Delete a single credential library set via the `remove-credential-sources` method.
* @param {number} credentialLibraryID
* Delete a single credential library/credential set via the `remove-credential-sources` method.
* @param {number} credentialSourceID
* @param {object} options
* @return {Promise}
*/
removeCredentialSource(credentialLibraryID, options) {
return this.removeCredentialSources([credentialLibraryID], options);
removeCredentialSource(credentialSourceID, options) {
return this.removeCredentialSources([credentialSourceID], options);
}
/**
+5 -5
View File
@@ -7,10 +7,10 @@ export default class TargetSerializer extends ApplicationSerializer {
* If `adapterOptions.hostSets` is set to an array of host set models,
* the resulting target serialization should include **only host sets**
* and the version.
* If `adapterOptions.credentialLibraries` is set to an array of
* credential library models, the resulting target serialization should
* include **only credential libraries** and the version.
* Normally, neither host sets or credential libraries are not serialized.
* If `adapterOptions.credentialSources` is set to an array of
* credential library and credential models, the resulting target serialization should
* include **only credential libraries and crendentials** and the version.
* Normally, neither host sets or credential libraries or credentials are not serialized.
* @override
* @param {Snapshot} snapshot
* @return {object}
@@ -21,7 +21,7 @@ export default class TargetSerializer extends ApplicationSerializer {
if (hostSourceIDs) {
serialized = this.serializeWithHostSources(snapshot, hostSourceIDs);
}
const credentialSourceIDs = snapshot?.adapterOptions?.credentialLibraryIDs;
const credentialSourceIDs = snapshot?.adapterOptions?.credentialSourceIDs;
if (credentialSourceIDs)
serialized = this.serializeWithCredentialSources(
snapshot,
+14 -2
View File
@@ -464,19 +464,31 @@ export default function () {
// If adding credential sources, push them into the array
if (method === 'add-credential-sources') {
updatedAttrs.credentialLibraryIds = target.credentialLibraryIds;
updatedAttrs.credentialIds = target.credentialIds;
attrs.applicationCredentialSourceIds.forEach((id) => {
if (!updatedAttrs.credentialLibraryIds.includes(id)) {
updatedAttrs.credentialLibraryIds.push(id);
if (
!updatedAttrs.credentialLibraryIds.includes(id) ||
!updatedAttrs.credentialIds.includes(id)
) {
if (id.startsWith('cred')) {
updatedAttrs.credentialIds.push(id);
} else {
updatedAttrs.credentialLibraryIds.push(id);
}
}
});
}
// If deleting credential sources, filter them out of the array
if (method === 'remove-credential-sources') {
updatedAttrs.credentialLibraryIds = target.credentialLibraryIds;
updatedAttrs.credentialIds = target.credentialIds;
updatedAttrs.credentialLibraryIds =
updatedAttrs.credentialLibraryIds.filter((id) => {
return !attrs.applicationCredentialSourceIds.includes(id);
});
updatedAttrs.credentialIds = updatedAttrs.credentialIds.filter((id) => {
return !attrs.applicationCredentialSourceIds.includes(id);
});
}
return target.update(updatedAttrs);
}
+7 -2
View File
@@ -49,7 +49,9 @@ export default factory.extend({
*/
withAssociations: trait({
afterCreate(target, server) {
let randomlySelectedHostSets, randomlySelectedCredentialLibraries;
let randomlySelectedHostSets,
randomlySelectedCredentialLibraries,
randomlySelectedCredentials;
randomlySelectedHostSets = server.schema.hostSets
// BLERG: fun fact, for no reason at all, the element passed
// into a where function is not a full model instance, as you might
@@ -64,10 +66,13 @@ export default factory.extend({
(credentialLibrary) => credentialLibrary.scopeId === target.scope.id
)
.models.filter(() => randomBoolean());
randomlySelectedCredentials = server.schema.credentials
.where((credential) => credential.scopeId === target.scope.id)
.models.filter(() => randomBoolean());
target.update({
hostSets: randomlySelectedHostSets,
credentialLibraries: randomlySelectedCredentialLibraries,
credentials: randomlySelectedCredentials,
});
},
}),
+1
View File
@@ -5,4 +5,5 @@ export default Model.extend({
scope: belongsTo(),
hostSets: hasMany(),
credentialLibraries: hasMany(),
credentials: hasMany(),
});
+5 -2
View File
@@ -23,8 +23,11 @@ export default ApplicationSerializer.extend({
return { id: host_set_id, host_catalog_id };
});
}
if (model.credentialLibraryIds?.length) {
json.application_credential_source_ids = model.credentialLibraryIds;
if (model.credentialLibraryIds?.length || model.credentialIds?.length) {
json.application_credential_source_ids = [
...model.credentialLibraryIds,
...model.credentialIds,
];
}
return json;
},
+6 -6
View File
@@ -231,7 +231,7 @@ module('Unit | Model | target', function (hooks) {
);
});
test('it has a `credentialLibraries` array of resolved model instances (if those instances are already in the store)', function (assert) {
test('it has a `credentialSources` array of resolved model instances (if those instances are already in the store)', function (assert) {
assert.expect(4);
const store = this.owner.lookup('service:store');
store.push({
@@ -250,9 +250,9 @@ module('Unit | Model | target', function (hooks) {
'Target has two entires in application_credential_source_ids'
);
assert.strictEqual(
target.credentialLibraries.length,
target.credentialSources.length,
0,
'Target has no resolved credentialLibraries because they are not loaded yet'
'Target has no resolved credentialSources because they are not loaded yet'
);
store.push({
data: {
@@ -268,7 +268,7 @@ module('Unit | Model | target', function (hooks) {
attributes: {},
},
});
// Since `credentialLibraries` is computed on `application_credential_source_ids`,
// Since `credentialSources` is computed on `application_credential_source_ids`,
// not the store itself, it's necessary to do this assignment to kick-off the
// computed update.
/* eslint-disable no-self-assign */
@@ -281,9 +281,9 @@ module('Unit | Model | target', function (hooks) {
'Target has two entires in application_credential_source_ids'
);
assert.strictEqual(
target.credentialLibraries.length,
target.credentialSources.length,
2,
'Target has two resolved credentialLibraries'
'Target has two resolved credentialSources'
);
});
@@ -66,7 +66,7 @@ module('Unit | Serializer | target', function (hooks) {
});
});
test('it serializes only credential sources and version when an `adapterOptions.credentialLibraryIDs` array is passed', function (assert) {
test('it serializes only credential sources and version when an `adapterOptions.credentialSourceIDs` array is passed', function (assert) {
assert.expect(1);
const store = this.owner.lookup('service:store');
const serializer = store.serializerFor('target');
@@ -78,7 +78,7 @@ module('Unit | Serializer | target', function (hooks) {
});
const snapshot = record._createSnapshot();
snapshot.adapterOptions = {
credentialLibraryIDs: ['4', '5'],
credentialSourceIDs: ['4', '5'],
};
const serializedRecord = serializer.serialize(snapshot);
assert.deepEqual(serializedRecord, {
@@ -378,6 +378,7 @@ credential-library:
types:
vault: Vault
static: Static
username_password: Username & Password
credential:
title: Credential
title_plural: Credentials
@@ -10,15 +10,19 @@ export default class ScopesScopeTargetsTargetCredentialSourcesRoute extends Rout
/**
* Loads all credential libraries under the current target.
* @return {Promise{[CredentialLibraryModel]}}
* @return {Promise{[CredentialLibraryModel, CredentialModel]}}
*/
beforeModel() {
const { application_credential_source_ids: sourceIDFragments } =
this.modelFor('scopes.scope.targets.target');
return all(
sourceIDFragments.map(({ value }) => {
const isStoreOrLibrary = !value.includes('cred');
if (isStoreOrLibrary) {
const isStatic = value.includes('cred');
if (isStatic) {
return this.store.findRecord('credential', value, {
reload: true,
});
} else {
return this.store.findRecord('credential-library', value, {
reload: true,
});
@@ -19,7 +19,7 @@
</page.actions>
<page.body>
{{#if @model.credentialLibraries}}
{{#if @model.credentialSources}}
<Rose::Table as |table|>
<table.header as |header|>
@@ -31,35 +31,48 @@
</header.row>
</table.header>
<table.body as |body|>
{{#each @model.credentialLibraries as |credentialLibrary|}}
{{#each @model.credentialSources as |credentialSource|}}
<body.row as |row|>
<row.headerCell>
<LinkTo
@route='scopes.scope.credential-stores.credential-store.credential-libraries.credential-library'
@models={{array
credentialLibrary.credential_store_id
credentialLibrary.id
}}
>
{{credentialLibrary.displayName}}
</LinkTo>
<p>{{credentialLibrary.description}}</p>
{{#if (eq credentialSource.type 'username_password')}}
<LinkTo
@route='scopes.scope.credential-stores.credential-store.credentials.credential'
@models={{array
credentialSource.credential_store_id
credentialSource.id
}}
>
{{credentialSource.displayName}}
</LinkTo>
{{else}}
<LinkTo
@route='scopes.scope.credential-stores.credential-store.credential-libraries.credential-library'
@models={{array
credentialSource.credential_store_id
credentialSource.id
}}
>
{{credentialSource.displayName}}
</LinkTo>
{{/if}}
<p>{{credentialSource.description}}</p>
</row.headerCell>
<row.cell>
<Rose::Badge>{{t
(concat
'resources.credential-library.types.'
credentialLibrary.type
'resources.target.credential-source.types.'
credentialSource.type
)
}}</Rose::Badge>
</row.cell>
<row.cell>
<Copyable
@text={{credentialLibrary.id}}
@text={{credentialSource.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<code>{{credentialLibrary.id}}</code>
<code>{{credentialSource.id}}</code>
</Copyable>
</row.cell>
<row.cell>
@@ -77,7 +90,7 @@
{{on
'click'
(route-action
'removeCredentialSource' @model credentialLibrary
'removeCredentialSource' @model credentialSource
)
}}
>
@@ -30,6 +30,7 @@ module('Acceptance | targets | credential sources', function (hooks) {
credentialLibraries: null,
credentialLibrary: null,
credentials: null,
credential: null,
};
const urls = {
globalScope: null,
@@ -74,6 +75,7 @@ module('Acceptance | targets | credential sources', function (hooks) {
}
);
instances.credentialLibrary = instances.credentialLibraries[0];
instances.credential = instances.credentials[0];
instances.target = this.server.create('target', {
scope: instances.scopes.project,
credentialLibraries: instances.credentialLibraries,
@@ -86,6 +88,7 @@ module('Acceptance | targets | credential sources', function (hooks) {
urls.target = `${urls.targets}/${instances.target.id}`;
urls.credentialSources = `${urls.target}/credential-sources`;
urls.credentialLibrary = `${urls.projectScope}/credential-stores/${instances.credentialLibrary.credentialStoreId}/credential-libraries/${instances.credentialLibrary.id}`;
urls.credential = `${urls.projectScope}/credential-stores/${instances.credential.credentialStoreId}/credentials/${instances.credential.id}`;
urls.addCredentialSources = `${urls.target}/add-credential-sources`;
// Generate resource counter
getCredentialLibraryCount = () =>
@@ -104,7 +107,7 @@ module('Acceptance | targets | credential sources', function (hooks) {
assert.strictEqual(findAll('tbody tr').length, getCredentialLibraryCount());
});
test('can navigate to a credential library', async function (assert) {
test('can navigate to a vault type credential library', async function (assert) {
assert.expect(1);
await visit(urls.credentialSources);
await click('main tbody .rose-table-header-cell:nth-child(1) a');
@@ -112,6 +115,18 @@ module('Acceptance | targets | credential sources', function (hooks) {
assert.strictEqual(currentURL(), urls.credentialLibrary);
});
test('can navigate to a username & password type credential', async function (assert) {
assert.expect(1);
instances.target.update({
credentialLibraries: [],
credentials: instances.credentials,
});
await visit(urls.credentialSources);
await click('main tbody .rose-table-header-cell:nth-child(1) a');
await a11yAudit();
assert.strictEqual(currentURL(), urls.credential);
});
test('visiting add credential sources', async function (assert) {
assert.expect(1);
await visit(urls.addCredentialSources);
@@ -165,19 +180,46 @@ module('Acceptance | targets | credential sources', function (hooks) {
assert.strictEqual(currentURL(), urls.credentialSources);
});
test('can select and save credential sources to add', async function (assert) {
test('can select and save a vault type credential library to add', async function (assert) {
assert.expect(4);
instances.target.update({ credentialLibraries: [] });
await visit(urls.credentialSources);
assert.strictEqual(findAll('tbody tr').length, 0);
await visit(urls.addCredentialSources);
assert.strictEqual(findAll('tbody tr').length, credentialSourceCount);
await click('tbody label');
await click('tbody tr:first-child label');
await click('form [type="submit"]');
assert.strictEqual(currentURL(), urls.credentialSources);
assert.strictEqual(findAll('tbody tr').length, 1);
});
test('can select and save a username & password type credential to add', async function (assert) {
assert.expect(4);
instances.target.update({ credentialLibraries: [] });
await visit(urls.credentialSources);
assert.strictEqual(findAll('tbody tr').length, 0);
await visit(urls.addCredentialSources);
assert.strictEqual(findAll('tbody tr').length, credentialSourceCount);
await click('tbody tr:last-child label');
await click('form [type="submit"]');
assert.strictEqual(currentURL(), urls.credentialSources);
assert.strictEqual(findAll('tbody tr').length, 1);
});
test('can select and save both a credential-library and a credential to add', async function (assert) {
assert.expect(4);
instances.target.update({ credentialLibraries: [] });
await visit(urls.credentialSources);
assert.strictEqual(findAll('tbody tr').length, 0);
await visit(urls.addCredentialSources);
assert.strictEqual(findAll('tbody tr').length, credentialSourceCount);
await click('tbody tr:last-child label');
await click('tbody tr:first-child label');
await click('form [type="submit"]');
assert.strictEqual(currentURL(), urls.credentialSources);
assert.strictEqual(findAll('tbody tr').length, 2);
});
test('cannot add credential sources without proper authorization', async function (assert) {
assert.expect(1);
instances.target.authorized_actions =
@@ -201,6 +243,20 @@ module('Acceptance | targets | credential sources', function (hooks) {
assert.strictEqual(findAll('tbody tr').length, 0);
});
test('can select multiple credential sources to add and cancel', async function (assert) {
assert.expect(4);
instances.target.update({ credentialLibraries: [] });
await visit(urls.credentialSources);
assert.strictEqual(findAll('tbody tr').length, 0);
await visit(urls.addCredentialSources);
assert.strictEqual(findAll('tbody tr').length, credentialSourceCount);
await click('tbody tr:last-child label');
await click('tbody tr:first-child label');
await click('form [type="button"]');
assert.strictEqual(currentURL(), urls.credentialSources);
assert.strictEqual(findAll('tbody tr').length, 0);
});
test('adding credential sources which errors displays error message', async function (assert) {
assert.expect(1);
this.server.post('/targets/:idMethod', () => {
@@ -217,18 +273,38 @@ module('Acceptance | targets | credential sources', function (hooks) {
});
instances.target.update({ credentialLibraries: [] });
await visit(urls.addCredentialSources);
await click('tbody label');
await click('tbody tr:last-child label');
await click('tbody tr:first-child label');
await click('form [type="submit"]');
assert.ok(find('[role="alert"]'));
});
test('can remove a credential library', async function (assert) {
assert.expect(2);
const count = getCredentialLibraryCount();
test('can remove a vault type credential library', async function (assert) {
assert.expect(3);
const credentialLibraryCount = getCredentialLibraryCount();
const credentialCount = getCredentialCount();
await visit(urls.credentialSources);
assert.strictEqual(findAll('tbody tr').length, count);
assert.strictEqual(findAll('tbody tr').length, credentialLibraryCount);
await click('tbody tr .rose-dropdown-button-danger');
assert.strictEqual(findAll('tbody tr').length, count - 1);
assert.strictEqual(findAll('tbody tr').length, credentialLibraryCount - 1);
await visit(urls.addCredentialSources);
assert.strictEqual(findAll('tbody tr').length, credentialCount + 1);
});
test('can remove a username & password type credential', async function (assert) {
assert.expect(3);
instances.target.update({
credentialLibraries: [],
credentials: instances.credentials,
});
const credentialCount = getCredentialCount();
const credentialLibraryCount = getCredentialLibraryCount();
await visit(urls.credentialSources);
assert.strictEqual(findAll('tbody tr').length, credentialCount);
await click('tbody tr .rose-dropdown-button-danger');
assert.strictEqual(findAll('tbody tr').length, credentialCount - 1);
await visit(urls.addCredentialSources);
assert.strictEqual(findAll('tbody tr').length, credentialLibraryCount + 1);
});
test('cannot remove credential libraries without proper authorization', async function (assert) {
@@ -261,4 +337,29 @@ module('Acceptance | targets | credential sources', function (hooks) {
await click('tbody tr .rose-dropdown-button-danger');
assert.ok(find('[role="alert"]'));
});
test('removing a target credential which errors displays error messages', async function (assert) {
assert.expect(2);
instances.target.update({
credentialLibraries: [],
credentials: instances.credentials,
});
this.server.post('/targets/:idMethod', () => {
return new Response(
400,
{},
{
status: 400,
code: 'invalid_argument',
message: 'The request was invalid.',
details: {},
}
);
});
const count = getCredentialCount();
await visit(urls.credentialSources);
assert.strictEqual(findAll('tbody tr').length, count);
await click('tbody tr .rose-dropdown-button-danger');
assert.ok(find('[role="alert"]'));
});
});