diff --git a/addons/api/addon/models/credential-library.js b/addons/api/addon/models/credential-library.js
index c95b9f73e..a77e605ba 100644
--- a/addons/api/addon/models/credential-library.js
+++ b/addons/api/addon/models/credential-library.js
@@ -2,6 +2,13 @@ import GeneratedCredentialLibraryModel from '../generated/models/credential-libr
import { attr } from '@ember-data/model';
import { fragment } from 'ember-data-model-fragments/attributes';
+/**
+ * Enum options for credential library.
+ */
+export const options = {
+ http_method: ['GET', 'POST'],
+};
+
export default class CredentialLibraryModel extends GeneratedCredentialLibraryModel {
// =error attributes
// These attributes exist solely to capture errors on nested fields.
diff --git a/addons/api/addon/models/fragment-credential-library-attributes.js b/addons/api/addon/models/fragment-credential-library-attributes.js
index dd6138199..582922601 100644
--- a/addons/api/addon/models/fragment-credential-library-attributes.js
+++ b/addons/api/addon/models/fragment-credential-library-attributes.js
@@ -1,5 +1,6 @@
import Fragment from 'ember-data-model-fragments/fragment';
import { attr } from '@ember-data/model';
+import { options } from './credential-library';
export default class FragmentCredentialLibraryAttributesModel extends Fragment {
//= attributes (vault)
@@ -7,6 +8,7 @@ export default class FragmentCredentialLibraryAttributesModel extends Fragment {
@attr('string', {
description:
'The HTTP method the library uses when requesting credentials from Vault.',
+ defaultValue: () => options.http_method[0],
})
http_method;
diff --git a/addons/api/addon/models/target.js b/addons/api/addon/models/target.js
index a00c3b182..d60596465 100644
--- a/addons/api/addon/models/target.js
+++ b/addons/api/addon/models/target.js
@@ -20,6 +20,16 @@ export default class TargetModel extends GeneratedTargetModel {
})
host_sets;
+ /**
+ * Credential library ids are read only and can be
+ * persisted via a dedicated call to `addCredentialLibraries()`.
+ */
+ @fragmentArray('fragment-string', {
+ readOnly: true,
+ emptyArrayIfMissing: true,
+ })
+ application_credential_library_ids;
+
/**
* An array of resolved host set and host catalog instances. Model instances
* must already be loaded into the store (this method will not load unloaded
@@ -36,6 +46,24 @@ export default class TargetModel extends GeneratedTargetModel {
.filter((hostSetRef) => hostSetRef.model !== null);
}
+ /**
+ * An array of resolved credential library 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]}
+ */
+ @computed('application_credential_library_ids.[]', 'store')
+ get credentialLibraries() {
+ return this.application_credential_library_ids
+ .map((credential_library_fragment) =>
+ this.store.peekRecord(
+ 'credential-library',
+ credential_library_fragment.value
+ )
+ )
+ .filter(Boolean);
+ }
+
/**
* Sessions associated with this target (but only already loaded sessions).
* @type {SessionModel[]}
@@ -122,4 +150,66 @@ export default class TargetModel extends GeneratedTargetModel {
removeHostSet(hostSetID, options) {
return this.removeHostSets([hostSetID], options);
}
+
+ /**
+ * Adds credential libraries via the `add-credential-libraries` method.
+ * See serializer and adapter for more information.
+ * @param {[string]} credentialLibraryIDs
+ * @param {object} options
+ * @param {object} options.adapterOptions
+ * @return {Promise}
+ */
+ addCredentialLibraries(
+ credentialLibraryIDs,
+ options = { adapterOptions: {} }
+ ) {
+ const defaultAdapterOptions = {
+ method: 'add-credential-libraries',
+ credentialLibraryIDs,
+ };
+ // There is no "deep merge" in ES.
+ return this.save({
+ ...options,
+ adapterOptions: {
+ ...defaultAdapterOptions,
+ ...options.adapterOptions,
+ },
+ });
+ }
+
+ /**
+ * Delete credential libraries via the `remove-credential-libraries` method.
+ * See serializer and adapter for more information.
+ * @param {[string]} credentialLibraryIDs
+ * @param {object} options
+ * @param {object} options.adapterOptions
+ * @return {Promise}
+ */
+ removeCredentialLibraries(
+ credentialLibraryIDs,
+ options = { adapterOptions: {} }
+ ) {
+ const defaultAdapterOptions = {
+ method: 'remove-credential-libraries',
+ credentialLibraryIDs,
+ };
+ // There is no "deep merge" in ES.
+ return this.save({
+ ...options,
+ adapterOptions: {
+ ...defaultAdapterOptions,
+ ...options.adapterOptions,
+ },
+ });
+ }
+
+ /**
+ * Delete a single credential library set via the `remove-credential-libraries` method.
+ * @param {number} credentialLibraryID
+ * @param {object} options
+ * @return {Promise}
+ */
+ removeCredentialLibrary(credentialLibraryID, options) {
+ return this.removeCredentialLibraries([credentialLibraryID], options);
+ }
}
diff --git a/addons/api/addon/serializers/target.js b/addons/api/addon/serializers/target.js
index 4c6745b89..c287aca46 100644
--- a/addons/api/addon/serializers/target.js
+++ b/addons/api/addon/serializers/target.js
@@ -6,7 +6,11 @@ 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. Normally, host sets are not serialized.
+ * 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.
* @override
* @param {Snapshot} snapshot
* @return {object}
@@ -16,6 +20,12 @@ export default class TargetSerializer extends ApplicationSerializer {
const hostSetIDs = snapshot?.adapterOptions?.hostSetIDs;
if (hostSetIDs)
serialized = this.serializeWithHostSets(snapshot, hostSetIDs);
+ const credentialLibraryIDs = snapshot?.adapterOptions?.credentialLibraryIDs;
+ if (credentialLibraryIDs)
+ serialized = this.serializeWithCredentialLibraries(
+ snapshot,
+ credentialLibraryIDs
+ );
return serialized;
}
@@ -32,4 +42,18 @@ export default class TargetSerializer extends ApplicationSerializer {
host_set_ids: hostSetIDs,
};
}
+
+ /**
+ * Returns a payload containing only the credential_library_ids array using IDs
+ * passed into the function (rather than existing credential libraries on the model).
+ * @param {Snapshot} snapshot
+ * @param {[string]} credentialLibraryIDs
+ * @return {object}
+ */
+ serializeWithCredentialLibraries(snapshot, credentialLibraryIDs) {
+ return {
+ version: snapshot.attr('version'),
+ application_credential_library_ids: credentialLibraryIDs,
+ };
+ }
}
diff --git a/addons/api/mirage/config.js b/addons/api/mirage/config.js
index c796d099c..c1b535a41 100644
--- a/addons/api/mirage/config.js
+++ b/addons/api/mirage/config.js
@@ -366,10 +366,10 @@ export default function () {
const target = targets.find(id);
const updatedAttrs = {
version: attrs.version,
- hostSetIds: target.hostSetIds,
};
// If adding host sets, push them into the array
if (method === 'add-host-sets') {
+ updatedAttrs.hostSetIds = target.hostSetIds;
attrs.hostSetIds.forEach((id) => {
if (!updatedAttrs.hostSetIds.includes(id)) {
updatedAttrs.hostSetIds.push(id);
@@ -378,10 +378,28 @@ export default function () {
}
// If deleting host sets, filter them out of the array
if (method === 'remove-host-sets') {
+ updatedAttrs.hostSetIds = target.hostSetIds;
updatedAttrs.hostSetIds = updatedAttrs.hostSetIds.filter((id) => {
return !attrs.hostSetIds.includes(id);
});
}
+ // If adding credential libraries, push them into the array
+ if (method === 'add-credential-libraries') {
+ updatedAttrs.credentialLibraryIds = target.credentialLibraryIds;
+ attrs.applicationCredentialLibraryIds.forEach((id) => {
+ if (!updatedAttrs.credentialLibraryIds.includes(id)) {
+ updatedAttrs.credentialLibraryIds.push(id);
+ }
+ });
+ }
+ // If deleting credential libraries, filter them out of the array
+ if (method === 'remove-credential-libraries') {
+ updatedAttrs.credentialLibraryIds = target.credentialLibraryIds;
+ updatedAttrs.credentialLibraryIds =
+ updatedAttrs.credentialLibraryIds.filter((id) => {
+ return !attrs.applicationCredentialLibraryIds.includes(id);
+ });
+ }
return target.update(updatedAttrs);
}
);
@@ -479,7 +497,15 @@ export default function () {
) => credentialLibraries.where({ credentialStoreId })
);
this.get('/credential-libraries/:id');
- this.post('/credential-libraries/:id');
+ this.post(
+ '/credential-libraries',
+ function ({ credentialStores, credentialLibraries }) {
+ const attrs = this.normalizedRequestAttrs();
+ const credentialStore = credentialStores.find(attrs.credentialStoreId);
+ attrs.scopeId = credentialStore.scope.id;
+ return credentialLibraries.create(attrs);
+ }
+ );
this.del('/credential-libraries/:id');
this.patch('/credential-libraries/:id');
diff --git a/addons/api/mirage/factories/target.js b/addons/api/mirage/factories/target.js
index dc1c97d00..e40df6abf 100644
--- a/addons/api/mirage/factories/target.js
+++ b/addons/api/mirage/factories/target.js
@@ -7,7 +7,7 @@ const hostSetChance = 0.3;
export default factory.extend({
/**
- * -1 means "unlimited" and we want to generate these on occassion.
+ * -1 means "unlimited" and we want to generate these on occasion.
*/
session_connection_limit: () => random.arrayElement([-1, datatype.number()]),
@@ -25,11 +25,11 @@ export default factory.extend({
},
/**
- * Randomly selects existing host sets to assign to target.
+ * Randomly selects existing host sets and credential libraries to assign to target.
*/
- withRandomHostSets: trait({
+ withAssociations: trait({
afterCreate(target, server) {
- let randomlySelectedHostSets;
+ let randomlySelectedHostSets, randomlySelectedCredentialLibraries;
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
@@ -38,7 +38,17 @@ export default factory.extend({
// the result set of `where` _is a collection of model instances_.
.where((hostSet) => hostSet.scopeId === target.scope.id)
.models.filter(() => randomBoolean(hostSetChance));
- target.update({ hostSets: randomlySelectedHostSets });
+
+ randomlySelectedCredentialLibraries = server.schema.credentialLibraries
+ .where(
+ (credentialLibrary) => credentialLibrary.scopeId === target.scope.id
+ )
+ .models.filter(() => randomBoolean());
+
+ target.update({
+ hostSets: randomlySelectedHostSets,
+ credentialLibraries: randomlySelectedCredentialLibraries,
+ });
},
}),
});
diff --git a/addons/api/mirage/models/target.js b/addons/api/mirage/models/target.js
index 0f03cb6ef..389e255d2 100644
--- a/addons/api/mirage/models/target.js
+++ b/addons/api/mirage/models/target.js
@@ -3,4 +3,5 @@ import { Model, belongsTo, hasMany } from 'ember-cli-mirage';
export default Model.extend({
scope: belongsTo(),
hostSets: hasMany(),
+ credentialLibraries: hasMany(),
});
diff --git a/addons/api/mirage/scenarios/default.js b/addons/api/mirage/scenarios/default.js
index fc5931908..d448a443b 100644
--- a/addons/api/mirage/scenarios/default.js
+++ b/addons/api/mirage/scenarios/default.js
@@ -47,8 +47,8 @@ export default function (server) {
// Other resources
server.schema.scopes.where({ type: 'project' }).models.forEach((scope) => {
server.createList('host-catalog', 2, { scope }, 'withChildren');
- server.createList('target', 2, { scope }, 'withRandomHostSets');
server.createList('session', 4, { scope }, 'withAssociations');
server.createList('credential-store', 3, { scope }, 'withAssociations');
+ server.createList('target', 2, { scope }, 'withAssociations');
});
}
diff --git a/addons/api/mirage/serializers/target.js b/addons/api/mirage/serializers/target.js
index 006388773..b970f4853 100644
--- a/addons/api/mirage/serializers/target.js
+++ b/addons/api/mirage/serializers/target.js
@@ -23,6 +23,9 @@ export default ApplicationSerializer.extend({
return { id: host_set_id, host_catalog_id };
});
}
+ if (model.credentialLibraryIds?.length) {
+ json.application_credential_library_ids = model.credentialLibraryIds;
+ }
return json;
},
});
diff --git a/addons/api/tests/unit/models/target-test.js b/addons/api/tests/unit/models/target-test.js
index 18be834bd..1c6fa2b22 100644
--- a/addons/api/tests/unit/models/target-test.js
+++ b/addons/api/tests/unit/models/target-test.js
@@ -223,4 +223,162 @@ module('Unit | Model | target', function (hooks) {
'Target has two associated sessions loaded in the store'
);
});
+
+ test('it has a `credentialLibraries` 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({
+ data: {
+ id: '123abc',
+ type: 'target',
+ attributes: {
+ application_credential_library_ids: [{ value: '1' }, { value: '2' }],
+ },
+ },
+ });
+ const target = store.peekRecord('target', '123abc');
+ assert.equal(
+ target.application_credential_library_ids.length,
+ 2,
+ 'Target has two entires in application_credential_library_ids'
+ );
+ assert.equal(
+ target.credentialLibraries.length,
+ 0,
+ 'Target has no resolved credentialLibraries because they are not loaded yet'
+ );
+ store.push({
+ data: {
+ id: '1',
+ type: 'credential-library',
+ attributes: {},
+ },
+ });
+ store.push({
+ data: {
+ id: '2',
+ type: 'credential-library',
+ attributes: {},
+ },
+ });
+ // Since `credentialLibraries` is computed on `application_credential_library_ids`,
+ // not the store itself, it's necessary to do this assignment to kick-off the
+ // computed update.
+ /* eslint-disable no-self-assign */
+ target.application_credential_library_ids =
+ target.application_credential_library_ids;
+ /* eslint-enable no-self-assign */
+ assert.equal(
+ target.application_credential_library_ids.length,
+ 2,
+ 'Target has two entires in application_credential_library_ids'
+ );
+ assert.equal(
+ target.credentialLibraries.length,
+ 2,
+ 'Target has two resolved credentialLibraries'
+ );
+ });
+
+ test('it has an `addCredentialLibraries` method that targets a specific POST API endpoint and serialization', async function (assert) {
+ assert.expect(1);
+ this.server.post(
+ '/v1/targets/123abc:add-credential-libraries',
+ (schema, request) => {
+ const body = JSON.parse(request.requestBody);
+ assert.deepEqual(body, {
+ application_credential_library_ids: ['123_abc', 'foobar'],
+ version: 1,
+ });
+ return { id: '123abc' };
+ }
+ );
+ const store = this.owner.lookup('service:store');
+ store.push({
+ data: {
+ id: '123abc',
+ type: 'target',
+ attributes: {
+ name: 'Target',
+ description: 'Description',
+ application_credential_library_ids: [{ value: '1' }, { value: '2' }],
+ version: 1,
+ scope: {
+ scope_id: 'o_1',
+ type: 'scope',
+ },
+ },
+ },
+ });
+ const model = store.peekRecord('target', '123abc');
+ await model.addCredentialLibraries(['123_abc', 'foobar']);
+ });
+
+ test('it has a `removeCredentialLibraries` method that targets a specific POST API endpoint and serialization', async function (assert) {
+ assert.expect(1);
+ this.server.post(
+ '/v1/targets/123abc:remove-credential-libraries',
+ (schema, request) => {
+ const body = JSON.parse(request.requestBody);
+ assert.deepEqual(body, {
+ application_credential_library_ids: ['1', '2'],
+ version: 1,
+ });
+ return { id: '123abc' };
+ }
+ );
+ const store = this.owner.lookup('service:store');
+ store.push({
+ data: {
+ id: '123abc',
+ type: 'target',
+ attributes: {
+ name: 'Target',
+ description: 'Description',
+ application_credential_library_ids: [{ value: '1' }, { value: '2' }],
+ version: 1,
+ scope: {
+ scope_id: 'o_1',
+ type: 'scope',
+ },
+ },
+ },
+ });
+ const model = store.peekRecord('target', '123abc');
+ await model.removeCredentialLibraries(['1', '2']);
+ });
+
+ test('it has a `removeCredentialLibrary` method that deletes a single credential library using `removeCredentialLibraries` method', async function (assert) {
+ assert.expect(1);
+ this.server.post(
+ '/v1/targets/123abc:remove-credential-libraries',
+ (schema, request) => {
+ const body = JSON.parse(request.requestBody);
+ assert.deepEqual(body, {
+ application_credential_library_ids: ['2'],
+ version: 1,
+ });
+ return { id: '123abc' };
+ }
+ );
+ const store = this.owner.lookup('service:store');
+ store.push({
+ data: {
+ id: '123abc',
+ type: 'target',
+ attributes: {
+ name: 'Target',
+ description: 'Description',
+ application_credential_library_ids: [{ value: '1' }, { value: '2' }],
+ version: 1,
+ scope: {
+ scope_id: 'o_1',
+ type: 'scope',
+ },
+ },
+ },
+ });
+ const model = store.peekRecord('target', '123abc');
+ await model.removeCredentialLibrary('2');
+ });
});
diff --git a/addons/api/tests/unit/serializers/application-test.js b/addons/api/tests/unit/serializers/application-test.js
index a7040e876..c2fe164c9 100644
--- a/addons/api/tests/unit/serializers/application-test.js
+++ b/addons/api/tests/unit/serializers/application-test.js
@@ -203,6 +203,7 @@ module('Unit | Serializer | application', function (hooks) {
name: 'Target 1',
scope: { scope_id: 'o_123' },
host_sets: [],
+ application_credential_library_ids: [],
},
relationships: {},
},
diff --git a/addons/api/tests/unit/serializers/target-test.js b/addons/api/tests/unit/serializers/target-test.js
index 255faa9ac..12dcacd4b 100644
--- a/addons/api/tests/unit/serializers/target-test.js
+++ b/addons/api/tests/unit/serializers/target-test.js
@@ -4,7 +4,7 @@ import { setupTest } from 'ember-qunit';
module('Unit | Serializer | target', function (hooks) {
setupTest(hooks);
- test('it serializes targets normally, without host sets', function (assert) {
+ test('it serializes targets normally, without host sets or credential libraries', function (assert) {
assert.expect(1);
const store = this.owner.lookup('service:store');
const serializer = store.serializerFor('target');
@@ -15,6 +15,7 @@ module('Unit | Serializer | target', function (hooks) {
{ host_set_id: '1', host_catalog_id: '2' },
{ host_set_id: '3', host_catalog_id: '4' },
],
+ application_credential_library_ids: [{ value: '1' }, { value: '2' }],
scope: {
scope_id: 'org_1',
type: 'org',
@@ -63,6 +64,27 @@ module('Unit | Serializer | target', function (hooks) {
});
});
+ test('it serializes only credential libraries and version when an `adapterOptions.credentialLibraryIDs` array is passed', function (assert) {
+ assert.expect(1);
+ const store = this.owner.lookup('service:store');
+ const serializer = store.serializerFor('target');
+ const record = store.createRecord('target', {
+ name: 'User',
+ description: 'Description',
+ application_credential_library_ids: [{ value: '1' }, { value: '2' }],
+ version: 1,
+ });
+ const snapshot = record._createSnapshot();
+ snapshot.adapterOptions = {
+ credentialLibraryIDs: ['4', '5'],
+ };
+ const serializedRecord = serializer.serialize(snapshot);
+ assert.deepEqual(serializedRecord, {
+ application_credential_library_ids: ['4', '5'],
+ version: 1,
+ });
+ });
+
test('it normalizes records with array fields', function (assert) {
assert.expect(1);
const store = this.owner.lookup('service:store');
@@ -75,6 +97,7 @@ module('Unit | Serializer | target', function (hooks) {
{ host_set_id: '1', host_catalog_id: '2' },
{ host_set_id: '3', host_catalog_id: '4' },
],
+ application_credential_library_ids: ['1'],
};
const normalized = serializer.normalizeSingleResponse(
store,
@@ -92,13 +115,14 @@ module('Unit | Serializer | target', function (hooks) {
{ host_set_id: '1', host_catalog_id: '2' },
{ host_set_id: '3', host_catalog_id: '4' },
],
+ application_credential_library_ids: [{ value: '1' }],
},
relationships: {},
},
});
});
- test('it normalizes missing host_sets to empty array', function (assert) {
+ test('it normalizes missing host_sets and credential libraries to empty array', function (assert) {
assert.expect(1);
const store = this.owner.lookup('service:store');
const serializer = store.serializerFor('target');
@@ -122,6 +146,7 @@ module('Unit | Serializer | target', function (hooks) {
name: 'Target 1',
scope: { scope_id: 'o_123' },
host_sets: [],
+ application_credential_library_ids: [],
},
relationships: {},
},
diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml
index 9d8564288..8094abf9e 100644
--- a/addons/core/translations/resources/en-us.yaml
+++ b/addons/core/translations/resources/en-us.yaml
@@ -277,9 +277,19 @@ credential-library:
title: Credential Library
title_plural: Credential Libraries
description: A credential library is a resource that provides credentials.
+ actions:
+ create: New Credential Library
messages:
none:
title: No Credential Libraries Available
description: No Credential Libraries available in this credential store.
- actions:
- create: New Credential Library
+ form:
+ vault_path:
+ label: Vault Path
+ help: The path the library uses when requesting credentials from Vault.
+ http_method:
+ label: HTTP Method
+ help: The HTTP method the library uses when requesting credentials from Vault.
+ http_request_body:
+ label: HTTP Method POST Request Body
+ help: The body of the HTTP request the library sends to Vault when requesting credentials. Only allowed when HTTP method is set to POST.
diff --git a/addons/rose/addon/components/rose/form/index.hbs b/addons/rose/addon/components/rose/form/index.hbs
index 8c545ec22..e23e52c0b 100644
--- a/addons/rose/addon/components/rose/form/index.hbs
+++ b/addons/rose/addon/components/rose/form/index.hbs
@@ -20,6 +20,7 @@
input=(component 'rose/form/input' disabled=this.disabled)
select=(component 'rose/form/select' disabled=this.disabled)
textarea=(component 'rose/form/textarea' disabled=this.disabled)
+ fieldset=(component 'rose/form/fieldset')
actions=(component
(if this.showEditToggleButton
'rose/form/actions/edit-toggle'
diff --git a/ui/admin/app/components/form/credential-library/index.hbs b/ui/admin/app/components/form/credential-library/index.hbs
new file mode 100644
index 000000000..beef7e8c6
--- /dev/null
+++ b/ui/admin/app/components/form/credential-library/index.hbs
@@ -0,0 +1,116 @@
+
+
+ {{#if @model.errors.name}}
+
+ {{#each @model.errors.name as |error|}}
+ {{error.message}}
+ {{/each}}
+
+ {{/if}}
+
+
+
+ {{#if @model.errors.description}}
+
+ {{#each @model.errors.description as |error|}}
+ {{error.message}}
+ {{/each}}
+
+ {{/if}}
+
+
+
+ {{#if @model.errors.attributes_path}}
+
+ {{#each @model.errors.attributes_path as |error|}}
+ {{error.message}}
+ {{/each}}
+
+ {{/if}}
+
+
+
+ <:title>{{t 'resources.credential-library.form.http_method.label'}}
+ <:description>{{t
+ 'resources.credential-library.form.http_method.help'
+ }}
+ <:body>
+
+ {{#each this.httpMethodOptions as |httpMethod|}}
+
+ {{/each}}
+
+
+
+
+ {{#if this.isHttpRequestBodyAllowed}}
+
+ {{#if @model.errors.attributes_http_request_body}}
+
+ {{#each @model.errors.attributes_http_request_body as |error|}}
+ {{error.message}}
+ {{/each}}
+
+ {{/if}}
+
+ {{/if}}
+
+
+
\ No newline at end of file
diff --git a/ui/admin/app/components/form/credential-library/index.js b/ui/admin/app/components/form/credential-library/index.js
new file mode 100644
index 000000000..b46e8d803
--- /dev/null
+++ b/ui/admin/app/components/form/credential-library/index.js
@@ -0,0 +1,20 @@
+import Component from '@glimmer/component';
+import { options } from 'api/models/credential-library';
+import { computed } from '@ember/object';
+
+export default class FormCredentialLibraryIndexComponent extends Component {
+ // =properties
+ /**
+ * @type {object}
+ */
+ httpMethodOptions = options.http_method;
+
+ /**
+ * Only allow HTTP request body field if http_method is set to POST.
+ * @type {boolean}
+ */
+ @computed('args.model.attributes.http_method')
+ get isHttpRequestBodyAllowed() {
+ return this.args.model.attributes.http_method?.match(/post/i);
+ }
+}
diff --git a/ui/admin/app/controllers/scopes/scope/credential-stores/credential-store/credential-libraries.js b/ui/admin/app/controllers/scopes/scope/credential-stores/credential-store/credential-libraries.js
new file mode 100644
index 000000000..fe9084ea7
--- /dev/null
+++ b/ui/admin/app/controllers/scopes/scope/credential-stores/credential-store/credential-libraries.js
@@ -0,0 +1,18 @@
+import Controller from '@ember/controller';
+import { inject as service } from '@ember/service';
+
+export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrariesController extends Controller {
+ // =services
+
+ @service intl;
+
+ // =attributes
+
+ /**
+ * Translated credential libraries breadcrumb
+ * @type {string}
+ */
+ get breadCrumb() {
+ return this.intl.t('resources.credential-library.title_plural');
+ }
+}
diff --git a/ui/admin/app/controllers/scopes/scope/credential-stores/credential-store/credential-libraries/new.js b/ui/admin/app/controllers/scopes/scope/credential-stores/credential-store/credential-libraries/new.js
new file mode 100644
index 000000000..ff4053f92
--- /dev/null
+++ b/ui/admin/app/controllers/scopes/scope/credential-stores/credential-store/credential-libraries/new.js
@@ -0,0 +1,18 @@
+import Controller from '@ember/controller';
+import { inject as service } from '@ember/service';
+
+export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrariesNewController extends Controller {
+ // =services
+
+ @service intl;
+
+ // =attributes
+
+ /**
+ * Translated new credential libraries breadcrumb
+ * @type {string}
+ */
+ get breadCrumb() {
+ return this.intl.t('titles.new');
+ }
+}
diff --git a/ui/admin/app/router.js b/ui/admin/app/router.js
index 4012791b2..3c359e268 100644
--- a/ui/admin/app/router.js
+++ b/ui/admin/app/router.js
@@ -91,7 +91,9 @@ Router.map(function () {
'credential-store',
{ path: ':credential_store_id' },
function () {
- this.route('credential-libraries', function () {});
+ this.route('credential-libraries', function () {
+ this.route('new');
+ });
}
);
this.route('new');
diff --git a/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credential-libraries.js b/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credential-libraries.js
index 7c8ade755..fa54e9691 100644
--- a/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credential-libraries.js
+++ b/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credential-libraries.js
@@ -1,4 +1,7 @@
import Route from '@ember/routing/route';
+import { action } from '@ember/object';
+import loading from 'ember-loading/decorator';
+import { notifySuccess, notifyError } from 'core/decorators/notify';
export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrariesRoute extends Route {
// =methods
@@ -13,4 +16,38 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrari
);
return this.store.query('credential-library', { credential_store_id });
}
+
+ // =actions
+
+ /**
+ * Rollback changes on a credential library.
+ * @param {CredentialLibraryModel} credentialLibrary
+ */
+ @action
+ cancel(credentialLibrary) {
+ const { isNew } = credentialLibrary;
+ credentialLibrary.rollbackAttributes();
+ if (isNew)
+ this.transitionTo(
+ 'scopes.scope.credential-stores.credential-store.credential-libraries'
+ );
+ }
+
+ /**
+ * Handle save of a credential library
+ * @param {CredentialLibraryModel} credentialLibrary
+ */
+ @action
+ @loading
+ @notifyError(({ message }) => message)
+ @notifySuccess(({ isNew }) =>
+ isNew ? 'notifications.create-success' : 'notifications.save-success'
+ )
+ async save(credentialLibrary) {
+ await credentialLibrary.save();
+ await this.transitionTo(
+ 'scopes.scope.credential-stores.credential-store.credential-libraries'
+ );
+ this.refresh();
+ }
}
diff --git a/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credential-libraries/new.js b/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credential-libraries/new.js
new file mode 100644
index 000000000..d83109dda
--- /dev/null
+++ b/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credential-libraries/new.js
@@ -0,0 +1,45 @@
+import Route from '@ember/routing/route';
+
+export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrariesNewRoute extends Route {
+ // =methods
+
+ /**
+ * Creates a new unsaved credential library in current credential store.
+ * @return {CredentialLibraryModel}
+ */
+ model() {
+ const { id: credential_store_id } = this.modelFor(
+ 'scopes.scope.credential-stores.credential-store'
+ );
+ return this.store.createRecord('credential-library', {
+ credential_store_id,
+ });
+ }
+
+ /**
+ * Renders new credential library specific templates for header, navigation,
+ * and action page sections.
+ * @override
+ */
+ renderTemplate() {
+ super.renderTemplate(...arguments);
+
+ this.render(
+ 'scopes/scope/credential-stores/credential-store/credential-libraries/new/-header',
+ {
+ into: 'scopes/scope/credential-stores/credential-store',
+ outlet: 'header',
+ }
+ );
+
+ this.render('_empty', {
+ into: 'scopes/scope/credential-stores/credential-store',
+ outlet: 'navigation',
+ });
+
+ this.render('_empty', {
+ into: 'scopes/scope/credential-stores/credential-store',
+ outlet: 'actions',
+ });
+ }
+}
diff --git a/ui/admin/app/templates/scopes/scope/credential-stores/credential-store/-actions.hbs b/ui/admin/app/templates/scopes/scope/credential-stores/credential-store/-actions.hbs
index e19af1ada..c08355da6 100644
--- a/ui/admin/app/templates/scopes/scope/credential-stores/credential-store/-actions.hbs
+++ b/ui/admin/app/templates/scopes/scope/credential-stores/credential-store/-actions.hbs
@@ -3,10 +3,13 @@
@dropdownRight={{true}}
as |dropdown|
>
- {{!--
+
{{t 'resources.credential-library.actions.create'}}
- --}}
+
{{t 'resources.credential-library.messages.none.description'}}
+
+
+ {{t 'titles.new'}}
+
diff --git a/ui/admin/app/templates/scopes/scope/credential-stores/credential-store/credential-libraries/new.hbs b/ui/admin/app/templates/scopes/scope/credential-stores/credential-store/credential-libraries/new.hbs
new file mode 100644
index 000000000..5d03e5b9f
--- /dev/null
+++ b/ui/admin/app/templates/scopes/scope/credential-stores/credential-store/credential-libraries/new.hbs
@@ -0,0 +1,7 @@
+{{page-title (t 'titles.new')}}
+
+
\ No newline at end of file
diff --git a/ui/admin/app/templates/scopes/scope/credential-stores/credential-store/credential-libraries/new/-header.hbs b/ui/admin/app/templates/scopes/scope/credential-stores/credential-store/credential-libraries/new/-header.hbs
new file mode 100644
index 000000000..51755b136
--- /dev/null
+++ b/ui/admin/app/templates/scopes/scope/credential-stores/credential-store/credential-libraries/new/-header.hbs
@@ -0,0 +1,3 @@
+
+ {{t 'titles.new'}}
+
\ No newline at end of file
diff --git a/ui/admin/tests/acceptance/sessions-test.js b/ui/admin/tests/acceptance/sessions-test.js
index 15a263925..d5f68ffeb 100644
--- a/ui/admin/tests/acceptance/sessions-test.js
+++ b/ui/admin/tests/acceptance/sessions-test.js
@@ -55,7 +55,7 @@ module('Acceptance | sessions', function (hooks) {
'target',
1,
{ scope: instances.scopes.project },
- 'withRandomHostSets'
+ 'withAssociations'
);
instances.sessions = this.server.createList(
'session',
diff --git a/ui/admin/tests/unit/controllers/scopes/scope/credential-stores/credential-store/credential-libraries-test.js b/ui/admin/tests/unit/controllers/scopes/scope/credential-stores/credential-store/credential-libraries-test.js
new file mode 100644
index 000000000..944329525
--- /dev/null
+++ b/ui/admin/tests/unit/controllers/scopes/scope/credential-stores/credential-store/credential-libraries-test.js
@@ -0,0 +1,17 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module(
+ 'Unit | Controller | scopes/scope/credential-stores/credential-store/credential-libraries',
+ function (hooks) {
+ setupTest(hooks);
+
+ // TODO: Replace this with your real tests.
+ test('it exists', function (assert) {
+ let controller = this.owner.lookup(
+ 'controller:scopes/scope/credential-stores/credential-store/credential-libraries'
+ );
+ assert.ok(controller);
+ });
+ }
+);
diff --git a/ui/admin/tests/unit/controllers/scopes/scope/credential-stores/credential-store/credential-libraries/new-test.js b/ui/admin/tests/unit/controllers/scopes/scope/credential-stores/credential-store/credential-libraries/new-test.js
new file mode 100644
index 000000000..2dce4303a
--- /dev/null
+++ b/ui/admin/tests/unit/controllers/scopes/scope/credential-stores/credential-store/credential-libraries/new-test.js
@@ -0,0 +1,17 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module(
+ 'Unit | Controller | scopes/scope/credential-stores/credential-store/credential-libraries/new',
+ function (hooks) {
+ setupTest(hooks);
+
+ // TODO: Replace this with your real tests.
+ test('it exists', function (assert) {
+ let controller = this.owner.lookup(
+ 'controller:scopes/scope/credential-stores/credential-store/credential-libraries/new'
+ );
+ assert.ok(controller);
+ });
+ }
+);
diff --git a/ui/admin/tests/unit/routes/scopes/scope/credential-stores/credential-store/credential-libraries/new-test.js b/ui/admin/tests/unit/routes/scopes/scope/credential-stores/credential-store/credential-libraries/new-test.js
new file mode 100644
index 000000000..3c4efdd66
--- /dev/null
+++ b/ui/admin/tests/unit/routes/scopes/scope/credential-stores/credential-store/credential-libraries/new-test.js
@@ -0,0 +1,16 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module(
+ 'Unit | Route | scopes/scope/credential-stores/credential-store/credential-libraries/new',
+ function (hooks) {
+ setupTest(hooks);
+
+ test('it exists', function (assert) {
+ let route = this.owner.lookup(
+ 'route:scopes/scope/credential-stores/credential-store/credential-libraries/new'
+ );
+ assert.ok(route);
+ });
+ }
+);
diff --git a/ui/desktop/tests/acceptance/origin-test.js b/ui/desktop/tests/acceptance/origin-test.js
index eddad56bb..6eb0c5d22 100644
--- a/ui/desktop/tests/acceptance/origin-test.js
+++ b/ui/desktop/tests/acceptance/origin-test.js
@@ -97,7 +97,7 @@ module('Acceptance | origin', function (hooks) {
instances.target = this.server.create(
'target',
{ scope: instances.scopes.project },
- 'withRandomHostSets'
+ 'withAssociations'
);
urls.scopes.global = `/scopes/${instances.scopes.global.id}`;
diff --git a/ui/desktop/tests/acceptance/projects-test.js b/ui/desktop/tests/acceptance/projects-test.js
index 38d54b480..3f6dd5506 100644
--- a/ui/desktop/tests/acceptance/projects-test.js
+++ b/ui/desktop/tests/acceptance/projects-test.js
@@ -92,7 +92,7 @@ module('Acceptance | projects', function (hooks) {
instances.target = this.server.create(
'target',
{ scope: instances.scopes.project },
- 'withRandomHostSets'
+ 'withAssociations'
);
urls.scopes.global = `/scopes/${instances.scopes.global.id}`;
diff --git a/ui/desktop/tests/acceptance/projects/sessions-test.js b/ui/desktop/tests/acceptance/projects/sessions-test.js
index 2cdbafc16..2b31e5eaa 100644
--- a/ui/desktop/tests/acceptance/projects/sessions-test.js
+++ b/ui/desktop/tests/acceptance/projects/sessions-test.js
@@ -97,7 +97,7 @@ module('Acceptance | projects | sessions', function (hooks) {
instances.target = this.server.create(
'target',
{ scope: instances.scopes.project },
- 'withRandomHostSets'
+ 'withAssociations'
);
instances.session = this.server.create(
'session',
diff --git a/ui/desktop/tests/acceptance/projects/targets-test.js b/ui/desktop/tests/acceptance/projects/targets-test.js
index 81dfbb22b..2fb027eec 100644
--- a/ui/desktop/tests/acceptance/projects/targets-test.js
+++ b/ui/desktop/tests/acceptance/projects/targets-test.js
@@ -101,7 +101,7 @@ module('Acceptance | projects | targets', function (hooks) {
instances.target = this.server.create(
'target',
{ scope: instances.scopes.project },
- 'withRandomHostSets'
+ 'withAssociations'
);
instances.session = this.server.create(
diff --git a/ui/desktop/tests/acceptance/projects/targets/sessions-test.js b/ui/desktop/tests/acceptance/projects/targets/sessions-test.js
index 6bfe8bd89..f687be5d1 100644
--- a/ui/desktop/tests/acceptance/projects/targets/sessions-test.js
+++ b/ui/desktop/tests/acceptance/projects/targets/sessions-test.js
@@ -106,7 +106,7 @@ module('Acceptance | projects | targets | sessions', function (hooks) {
instances.target = this.server.create(
'target',
{ scope: instances.scopes.project },
- 'withRandomHostSets'
+ 'withAssociations'
);
instances.session = this.server.create(
diff --git a/ui/desktop/tests/acceptance/scopes-test.js b/ui/desktop/tests/acceptance/scopes-test.js
index 93ec928bb..71907a984 100644
--- a/ui/desktop/tests/acceptance/scopes-test.js
+++ b/ui/desktop/tests/acceptance/scopes-test.js
@@ -110,7 +110,7 @@ module('Acceptance | scopes', function (hooks) {
instances.target = this.server.create(
'target',
{ scope: instances.scopes.project },
- 'withRandomHostSets'
+ 'withAssociations'
);
instances.session = this.server.create(
diff --git a/ui/desktop/tests/acceptance/targets-test.js b/ui/desktop/tests/acceptance/targets-test.js
index 85881b7ec..f92168c3a 100644
--- a/ui/desktop/tests/acceptance/targets-test.js
+++ b/ui/desktop/tests/acceptance/targets-test.js
@@ -94,7 +94,7 @@ module('Acceptance | targets', function (hooks) {
instances.target = this.server.create(
'target',
{ scope: instances.scopes.project },
- 'withRandomHostSets'
+ 'withAssociations'
);
urls.scopes.global = `/scopes/${instances.scopes.global.id}`;