diff --git a/.gitignore b/.gitignore
index 80cb94003..95852e8ad 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,6 +22,7 @@ testem.log
yarn-error.log
storybook-static
**/.storybook/*.html
+.watchman-cookie*
# ember-try
.node_modules.ember-try
diff --git a/addons/api/addon/generated/models/credential-library.js b/addons/api/addon/generated/models/credential-library.js
new file mode 100644
index 000000000..895c467f9
--- /dev/null
+++ b/addons/api/addon/generated/models/credential-library.js
@@ -0,0 +1,42 @@
+import BaseModel from '../../models/base';
+import { attr } from '@ember-data/model';
+
+/**
+ * A credential library is a resource that can generate and retrieve
+ * credentials.
+ */
+export default class GeneratedCredentialLibraryModel extends BaseModel {
+ // =attributes
+
+ @attr('string', {
+ description: 'The owning credential store ID.',
+ })
+ credential_store_id;
+
+ @attr('string', {
+ description: 'Optional name for identification purposes',
+ })
+ name;
+
+ @attr('string', {
+ description: 'Optional user-set description for identification purposes',
+ })
+ description;
+
+ @attr('date', {
+ description: 'The time this resource was created\nOutput only.',
+ readOnly: true,
+ })
+ created_time;
+
+ @attr('date', {
+ description: 'The time this resource was last updated\nOutput only.',
+ readOnly: true,
+ })
+ updated_time;
+
+ @attr('number', {
+ description: 'Current version number of this resource.',
+ })
+ version;
+}
diff --git a/addons/api/addon/generated/models/credential-store.js b/addons/api/addon/generated/models/credential-store.js
index 78834dc51..51dd4af87 100644
--- a/addons/api/addon/generated/models/credential-store.js
+++ b/addons/api/addon/generated/models/credential-store.js
@@ -5,7 +5,7 @@ import { attr } from '@ember-data/model';
* A credential store is a resource that can retrieve, store, and potentially
* generate credentials.
*/
-export default class GeneratedAccountModel extends BaseModel {
+export default class GeneratedCredentialStoreModel extends BaseModel {
// =attributes
@attr('string', {
diff --git a/addons/api/addon/models/credential-library.js b/addons/api/addon/models/credential-library.js
new file mode 100644
index 000000000..c95b9f73e
--- /dev/null
+++ b/addons/api/addon/models/credential-library.js
@@ -0,0 +1,23 @@
+import GeneratedCredentialLibraryModel from '../generated/models/credential-library';
+import { attr } from '@ember-data/model';
+import { fragment } from 'ember-data-model-fragments/attributes';
+
+export default class CredentialLibraryModel extends GeneratedCredentialLibraryModel {
+ // =error attributes
+ // These attributes exist solely to capture errors on nested fields.
+ // See the application adapter's error normalization method for
+ // more information.
+
+ @attr('string', { readOnly: true }) attributes_http_method;
+ @attr('string', { readOnly: true }) attributes_http_request_body;
+ @attr('string', { readOnly: true }) attributes_path;
+
+ // =attributes
+
+ /**
+ * Attributes of this resource, if any, represented as a JSON fragment.
+ * @type {FragmentCredentialLibraryAttributesModel}
+ */
+ @fragment('fragment-credential-library-attributes', { defaultValue: {} })
+ attributes;
+}
diff --git a/addons/api/addon/models/fragment-credential-library-attributes.js b/addons/api/addon/models/fragment-credential-library-attributes.js
new file mode 100644
index 000000000..dd6138199
--- /dev/null
+++ b/addons/api/addon/models/fragment-credential-library-attributes.js
@@ -0,0 +1,23 @@
+import Fragment from 'ember-data-model-fragments/fragment';
+import { attr } from '@ember-data/model';
+
+export default class FragmentCredentialLibraryAttributesModel extends Fragment {
+ //= attributes (vault)
+
+ @attr('string', {
+ description:
+ 'The HTTP method the library uses when requesting credentials from Vault.',
+ })
+ http_method;
+
+ @attr('string', {
+ description:
+ 'The body of the HTTP request the library sends to Vault when requesting credentials. Only valid if `http_method` is set to `POST`.',
+ })
+ http_request_body;
+
+ @attr('string', {
+ description: 'The path in Vault to request credentials from.',
+ })
+ path;
+}
diff --git a/addons/api/addon/serializers/credential-library.js b/addons/api/addon/serializers/credential-library.js
new file mode 100644
index 000000000..d1ced9a2b
--- /dev/null
+++ b/addons/api/addon/serializers/credential-library.js
@@ -0,0 +1,3 @@
+import ApplicationSerializer from './application';
+
+export default class CredentialLibrarySerializer extends ApplicationSerializer {}
diff --git a/addons/api/addon/serializers/fragment-credential-library-attributes.js b/addons/api/addon/serializers/fragment-credential-library-attributes.js
new file mode 100644
index 000000000..8d52b101f
--- /dev/null
+++ b/addons/api/addon/serializers/fragment-credential-library-attributes.js
@@ -0,0 +1,27 @@
+import JSONSerializer from '@ember-data/serializer/json';
+
+export default class FragmentCredentialLibraryAttributesSerializer extends JSONSerializer {
+ /**
+ * If an attribute is annotated as readOnly in the model, don't serialize it.
+ * Otherwise delegate to default attribute serializer.
+ * Only serialize http_request_body when `http_method` is set to `POST`.
+ * @override
+ * @method serializeAttribute
+ * @param {Snapshot} snapshot
+ * @param {Object} json
+ * @param {String} key
+ * @param {Object} attribute
+ */
+ serializeAttribute(snapshot, json, key, attribute) {
+ const { type, options } = attribute;
+ let value = super.serializeAttribute(...arguments);
+ // Convert empty strings to null.
+ if (type === 'string' && json[key] === '') json[key] = null;
+ // Do not serialize read-only attributes.
+ if (options.readOnly) delete json[key];
+ // Do not serialize http_request_body unless http_method is POST
+ if (key === 'http_request_body' && !json['http_method']?.match(/post/i))
+ delete json[key];
+ return value;
+ }
+}
diff --git a/addons/api/app/models/credential-library.js b/addons/api/app/models/credential-library.js
new file mode 100644
index 000000000..b9f39e29d
--- /dev/null
+++ b/addons/api/app/models/credential-library.js
@@ -0,0 +1 @@
+export { default } from 'api/models/credential-library';
diff --git a/addons/api/app/models/fragment-credential-library-attributes.js b/addons/api/app/models/fragment-credential-library-attributes.js
new file mode 100644
index 000000000..04d8f2597
--- /dev/null
+++ b/addons/api/app/models/fragment-credential-library-attributes.js
@@ -0,0 +1 @@
+export { default } from 'api/models/fragment-credential-library-attributes';
diff --git a/addons/api/app/serializers/credential-library.js b/addons/api/app/serializers/credential-library.js
new file mode 100644
index 000000000..2a76b7b03
--- /dev/null
+++ b/addons/api/app/serializers/credential-library.js
@@ -0,0 +1 @@
+export { default } from 'api/serializers/credential-library';
diff --git a/addons/api/app/serializers/fragment-credential-library-attributes.js b/addons/api/app/serializers/fragment-credential-library-attributes.js
new file mode 100644
index 000000000..d16c81e21
--- /dev/null
+++ b/addons/api/app/serializers/fragment-credential-library-attributes.js
@@ -0,0 +1 @@
+export { default } from 'api/serializers/fragment-credential-library-attributes';
diff --git a/addons/api/mirage/config.js b/addons/api/mirage/config.js
index 1b495c0b2..1fd364182 100644
--- a/addons/api/mirage/config.js
+++ b/addons/api/mirage/config.js
@@ -464,6 +464,24 @@ export default function () {
credentialStores.where({ scopeId })
);
+ this.get('/credential-stores/:id');
+ this.post('/credential-stores');
+ this.patch('/credential-stores/:id');
+
+ // credential-libraries
+
+ this.get(
+ '/credential-libraries',
+ (
+ { credentialLibraries },
+ { queryParams: { credential_store_id: credentialStoreId } }
+ ) => credentialLibraries.where({ credentialStoreId })
+ );
+ this.get('/credential-libraries/:id');
+ this.post('/credential-libraries/:id');
+ this.del('/credential-libraries/:id');
+ this.patch('/credential-libraries/:id');
+
/* Uncomment the following line and the Response import above
* Then change the response code to simulate error responses.
* this.get('/scopes', () => new Response(505));
diff --git a/addons/api/mirage/factories/credential-library.js b/addons/api/mirage/factories/credential-library.js
new file mode 100644
index 000000000..f1127d5ef
--- /dev/null
+++ b/addons/api/mirage/factories/credential-library.js
@@ -0,0 +1,14 @@
+import factory from '../generated/factories/credential-library';
+import { random, system } from 'faker';
+
+export default factory.extend({
+ id: (i) => `credential-library-id-${i}`,
+
+ attributes() {
+ return {
+ http_method: 'GET',
+ http_request_body: random.word(),
+ path: system.directoryPath(),
+ };
+ },
+});
diff --git a/addons/api/mirage/factories/credential-store.js b/addons/api/mirage/factories/credential-store.js
index e143b5906..d1b037c7d 100644
--- a/addons/api/mirage/factories/credential-store.js
+++ b/addons/api/mirage/factories/credential-store.js
@@ -1,5 +1,6 @@
import factory from '../generated/factories/credential-store';
import { random, internet, datatype } from 'faker';
+import { trait } from 'ember-cli-mirage';
const types = ['vault'];
@@ -22,4 +23,14 @@ export default factory.extend({
};
}
},
+
+ withAssociations: trait({
+ afterCreate(credentialStore, server) {
+ const { scope } = credentialStore;
+ server.createList('credential-library', 2, {
+ scope,
+ credentialStore,
+ });
+ },
+ }),
});
diff --git a/addons/api/mirage/generated/factories/credential-library.js b/addons/api/mirage/generated/factories/credential-library.js
new file mode 100644
index 000000000..b2e001493
--- /dev/null
+++ b/addons/api/mirage/generated/factories/credential-library.js
@@ -0,0 +1,12 @@
+import { Factory } from 'ember-cli-mirage';
+import { random, date } from 'faker';
+
+/**
+ * GeneratedCredentialLibraryModelFactory
+ */
+export default Factory.extend({
+ name: () => random.words(),
+ description: () => random.words(),
+ created_time: () => date.recent(),
+ updated_time: () => date.recent(),
+});
diff --git a/addons/api/mirage/generated/factories/credential-store.js b/addons/api/mirage/generated/factories/credential-store.js
index 1fb24e39e..2b8a86dd2 100644
--- a/addons/api/mirage/generated/factories/credential-store.js
+++ b/addons/api/mirage/generated/factories/credential-store.js
@@ -2,7 +2,7 @@ import { Factory } from 'ember-cli-mirage';
import { random, date } from 'faker';
/**
- * GeneratedAuthMethodModelFactory
+ * GeneratedCredentialStoreModelFactory
*/
export default Factory.extend({
name: () => random.words(),
diff --git a/addons/api/mirage/models/credential-library.js b/addons/api/mirage/models/credential-library.js
new file mode 100644
index 000000000..921a63182
--- /dev/null
+++ b/addons/api/mirage/models/credential-library.js
@@ -0,0 +1,6 @@
+import { Model, belongsTo } from 'ember-cli-mirage';
+
+export default Model.extend({
+ scope: belongsTo(),
+ credentialStore: belongsTo(),
+});
diff --git a/addons/api/mirage/scenarios/default.js b/addons/api/mirage/scenarios/default.js
index 12adb6c5d..fc5931908 100644
--- a/addons/api/mirage/scenarios/default.js
+++ b/addons/api/mirage/scenarios/default.js
@@ -49,6 +49,6 @@ export default function (server) {
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 });
+ server.createList('credential-store', 3, { scope }, 'withAssociations');
});
}
diff --git a/addons/api/mirage/serializers/credential-library.js b/addons/api/mirage/serializers/credential-library.js
new file mode 100644
index 000000000..fd65fc743
--- /dev/null
+++ b/addons/api/mirage/serializers/credential-library.js
@@ -0,0 +1,14 @@
+import ApplicationSerializer from './application';
+
+export default ApplicationSerializer.extend({
+ modelName: 'credential-library',
+
+ _hashForModel(model) {
+ const json = ApplicationSerializer.prototype._hashForModel.apply(
+ this,
+ arguments
+ );
+ json.credential_store_id = model.credentialStoreId;
+ return json;
+ },
+});
diff --git a/addons/api/tests/unit/models/credential-library-test.js b/addons/api/tests/unit/models/credential-library-test.js
new file mode 100644
index 000000000..4cd00ab05
--- /dev/null
+++ b/addons/api/tests/unit/models/credential-library-test.js
@@ -0,0 +1,13 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module('Unit | Model | credential library', function (hooks) {
+ setupTest(hooks);
+
+ // Replace this with your real tests.
+ test('it exists', function (assert) {
+ let store = this.owner.lookup('service:store');
+ let model = store.createRecord('credential-library', {});
+ assert.ok(model);
+ });
+});
diff --git a/addons/api/tests/unit/models/fragment-credential-library-attributes-test.js b/addons/api/tests/unit/models/fragment-credential-library-attributes-test.js
new file mode 100644
index 000000000..01f649c1e
--- /dev/null
+++ b/addons/api/tests/unit/models/fragment-credential-library-attributes-test.js
@@ -0,0 +1,19 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module(
+ 'Unit | Model | fragment credential library attributes',
+ function (hooks) {
+ setupTest(hooks);
+
+ // Replace this with your real tests.
+ test('it exists', function (assert) {
+ let store = this.owner.lookup('service:store');
+ let model = store.createRecord(
+ 'fragment-credential-library-attributes',
+ {}
+ );
+ assert.ok(model);
+ });
+ }
+);
diff --git a/addons/api/tests/unit/serializers/credential-library-test.js b/addons/api/tests/unit/serializers/credential-library-test.js
new file mode 100644
index 000000000..4be3c76f2
--- /dev/null
+++ b/addons/api/tests/unit/serializers/credential-library-test.js
@@ -0,0 +1,67 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module('Unit | Serializer | credential library', function (hooks) {
+ setupTest(hooks);
+
+ test('it serializes correctly on create', function (assert) {
+ assert.expect(1);
+ const store = this.owner.lookup('service:store');
+ const serializer = store.serializerFor('credential-library');
+ const record = store.createRecord('credential-library', {
+ name: 'Name',
+ description: 'Description',
+ attributes: {
+ path: '/vault/path',
+ http_method: 'GET',
+ },
+ version: 1,
+ });
+ const snapshot = record._createSnapshot();
+ const serializedRecord = serializer.serialize(snapshot);
+ assert.deepEqual(serializedRecord, {
+ credential_store_id: null,
+ name: 'Name',
+ description: 'Description',
+ version: 1,
+ attributes: {
+ path: '/vault/path',
+ http_method: 'GET',
+ },
+ });
+ });
+
+ test('it serializes correctly on update', function (assert) {
+ assert.expect(1);
+ const store = this.owner.lookup('service:store');
+ const serializer = store.serializerFor('credential-library');
+ store.push({
+ data: {
+ id: '1',
+ type: 'credential-library',
+ attributes: {
+ name: 'Name',
+ description: 'Description',
+ attributes: {
+ path: '/vault/path',
+ http_method: 'GET',
+ },
+ version: 1,
+ },
+ },
+ });
+ const record = store.peekRecord('credential-library', '1');
+ const snapshot = record._createSnapshot();
+ const serializedRecord = serializer.serialize(snapshot);
+ assert.deepEqual(serializedRecord, {
+ credential_store_id: null,
+ name: 'Name',
+ description: 'Description',
+ attributes: {
+ path: '/vault/path',
+ http_method: 'GET',
+ },
+ version: 1,
+ });
+ });
+});
diff --git a/addons/api/tests/unit/serializers/fragment-credential-library-attributes-test.js b/addons/api/tests/unit/serializers/fragment-credential-library-attributes-test.js
new file mode 100644
index 000000000..72818936c
--- /dev/null
+++ b/addons/api/tests/unit/serializers/fragment-credential-library-attributes-test.js
@@ -0,0 +1,59 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module(
+ 'Unit | Serializer | fragment credential library attributes',
+ function (hooks) {
+ setupTest(hooks);
+
+ test('it serializes empty strings to null', function (assert) {
+ assert.expect(1);
+ const store = this.owner.lookup('service:store');
+ const record = store.createRecord(
+ 'fragment-credential-library-attributes',
+ {
+ http_method: '',
+ path: null,
+ }
+ );
+ let serializedRecord = record.serialize();
+ assert.deepEqual(serializedRecord, {
+ http_method: null,
+ path: null,
+ });
+ });
+
+ test('it does not serialize http_request_body unless http_method is set to POST', function (assert) {
+ assert.expect(2);
+ const store = this.owner.lookup('service:store');
+ const record = store.createRecord(
+ 'fragment-credential-library-attributes',
+ {
+ http_method: 'GET',
+ http_request_body: 'body',
+ }
+ );
+ let serializedRecord = record.serialize();
+ assert.deepEqual(
+ serializedRecord,
+ {
+ http_method: 'GET',
+ path: null,
+ },
+ 'http_request_body attribute is not expected'
+ );
+
+ record.http_method = 'POST';
+ serializedRecord = record.serialize();
+ assert.deepEqual(
+ serializedRecord,
+ {
+ http_method: 'POST',
+ http_request_body: 'body',
+ path: null,
+ },
+ 'http_request_body attribute is expected'
+ );
+ });
+ }
+);
diff --git a/ui/admin/app/components/form/credential-store/index.hbs b/ui/admin/app/components/form/credential-store/index.hbs
new file mode 100644
index 000000000..f972854c1
--- /dev/null
+++ b/ui/admin/app/components/form/credential-store/index.hbs
@@ -0,0 +1,38 @@
+
{{t 'resources.credential-store.description'}}
+{{@model.id}}
+{{t 'resources.credential-store.description'}}