diff --git a/addons/api/addon/generated/models/credential-store.js b/addons/api/addon/generated/models/credential-store.js new file mode 100644 index 000000000..78834dc51 --- /dev/null +++ b/addons/api/addon/generated/models/credential-store.js @@ -0,0 +1,42 @@ +import BaseModel from '../../models/base'; +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 { + // =attributes + + @attr('string', { + description: 'The type of the resource, to help differentiate schemas', + }) + type; + + @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/models/credential-store.js b/addons/api/addon/models/credential-store.js new file mode 100644 index 000000000..d8d52ba3a --- /dev/null +++ b/addons/api/addon/models/credential-store.js @@ -0,0 +1,34 @@ +import GeneratedCredentialStoreModel from '../generated/models/credential-store'; +import { attr } from '@ember-data/model'; +import { fragment } from 'ember-data-model-fragments/attributes'; +import { equal } from '@ember/object/computed'; + +export default class CredentialStoreModel extends GeneratedCredentialStoreModel { + // =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_address; + @attr('string', { readOnly: true }) attributes_namespace; + @attr('string', { readOnly: true }) attributes_ca_cert; + @attr('string', { readOnly: true }) attributes_tls_server_name; + @attr('string', { readOnly: true }) attributes_tls_skip_verify; + @attr('string', { readOnly: true }) attributes_token; + @attr('string', { readOnly: true }) attributes_client_certificate; + @attr('string', { readOnly: true }) attributes_client_certificate_key; + + // =attributes + + /** + * Attributes of this resource, if any, represented as a JSON fragment. + * @type {FragmentAuthMethodAttributesModel} + */ + @fragment('fragment-credential-store-attributes', { defaultValue: {} }) + attributes; + + /** + * @type {boolean} + */ + @equal('type', 'vault') isVault; +} diff --git a/addons/api/addon/models/fragment-credential-store-attributes.js b/addons/api/addon/models/fragment-credential-store-attributes.js new file mode 100644 index 000000000..bbc9e2b9c --- /dev/null +++ b/addons/api/addon/models/fragment-credential-store-attributes.js @@ -0,0 +1,72 @@ +import Fragment from 'ember-data-model-fragments/fragment'; +import { attr } from '@ember-data/model'; + +export default class FragmentCredentialStoreAttributesModel extends Fragment { + // =attributes (vault) + @attr('string', { + for: 'vault', + description: + "The address to Vault server. This should be a complete URL such as 'https://127.0.0.1:8200'", + }) + address; + + @attr('string', { + for: 'vault', + description: 'The namespace within Vault to use.', + }) + namespace; + + @attr('string', { + for: 'vault', + description: + "A PEM-encoded CA certificate to verify the Vault server's TLS certificate.", + }) + ca_cert; + + @attr('string', { + for: 'vault', + description: + 'Name to use as the SNI host when connecting to Vault via TLS.', + }) + tls_server_name; + + @attr('boolean', { + for: 'vault', + description: 'Whether or not to skip TLS verification.', + }) + tls_skip_verify; + + @attr('string', { + for: 'vault', + description: 'A token used for accessing Vault.', + }) + token; + + @attr('string', { + for: 'vault', + description: 'The Vault token hmac.', + readOnly: true, + }) + token_hmac; + + @attr('string', { + for: 'vault', + description: + 'A PEM-encoded client certificate to use for TLS authentication to the Vault server.', + }) + client_certificate; + + @attr('string', { + for: 'vault', + description: + "A PEM-encoded private key matching the client certificate from 'client_certificate'.", + }) + client_certificate_key; + + @attr('string', { + for: 'vault', + description: 'The Vault client certificate key hmac.', + readOnly: true, + }) + client_certificate_key_hmac; +} diff --git a/addons/api/addon/serializers/credential-store.js b/addons/api/addon/serializers/credential-store.js new file mode 100644 index 000000000..602e4601f --- /dev/null +++ b/addons/api/addon/serializers/credential-store.js @@ -0,0 +1,3 @@ +import ApplicationSerializer from './application'; + +export default class CredentialStoreSerializer extends ApplicationSerializer {} diff --git a/addons/api/addon/serializers/fragment-credential-store-attributes.js b/addons/api/addon/serializers/fragment-credential-store-attributes.js new file mode 100644 index 000000000..547c6a4f7 --- /dev/null +++ b/addons/api/addon/serializers/fragment-credential-store-attributes.js @@ -0,0 +1,24 @@ +import JSONSerializer from '@ember-data/serializer/json'; + +export default class FragmentCredentialStoreAttributesSerializer extends JSONSerializer { + /** + * If an attribute is annotated as readOnly in the model, don't serialize it. + * Otherwise delegate to default attribute serializer. + * + * @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]; + return value; + } +} diff --git a/addons/api/app/models/credential-store.js b/addons/api/app/models/credential-store.js new file mode 100644 index 000000000..a714f8acd --- /dev/null +++ b/addons/api/app/models/credential-store.js @@ -0,0 +1 @@ +export { default } from 'api/models/credential-store'; diff --git a/addons/api/app/models/fragment-credential-store-attributes.js b/addons/api/app/models/fragment-credential-store-attributes.js new file mode 100644 index 000000000..642d34069 --- /dev/null +++ b/addons/api/app/models/fragment-credential-store-attributes.js @@ -0,0 +1 @@ +export { default } from 'api/models/fragment-credential-store-attributes'; diff --git a/addons/api/app/serializers/credential-store.js b/addons/api/app/serializers/credential-store.js new file mode 100644 index 000000000..f6aef76bb --- /dev/null +++ b/addons/api/app/serializers/credential-store.js @@ -0,0 +1 @@ +export { default } from 'api/serializers/credential-store'; diff --git a/addons/api/app/serializers/fragment-credential-store-attributes.js b/addons/api/app/serializers/fragment-credential-store-attributes.js new file mode 100644 index 000000000..b29c3913a --- /dev/null +++ b/addons/api/app/serializers/fragment-credential-store-attributes.js @@ -0,0 +1 @@ +export { default } from 'api/serializers/fragment-credential-store-attributes'; diff --git a/addons/api/mirage/factories/credential-store.js b/addons/api/mirage/factories/credential-store.js new file mode 100644 index 000000000..4809a7d1d --- /dev/null +++ b/addons/api/mirage/factories/credential-store.js @@ -0,0 +1,25 @@ +import factory from '../generated/factories/credential-store'; +import { random, internet, datatype } from 'faker'; + +const types = ['vault']; + +export default factory.extend({ + id: (i) => `credential-store-id-${i}`, + type: (i) => types[i % types.length], + + attributes() { + switch (this.type) { + case 'vault': + return { + address: internet.ip(), + namespace: random.word(), + ca_cert: 'ca-cert-123', + tls_server_name: internet.address(), + tls_skip_verify: datatype.boolean(), + token_hmac: 'token-hmac-abcde0987654321', + client_certificate: 'client-cert-123', + client_certificate_key_hmac: 'client-cert-key-hmac-abcde0987654321', + }; + } + }, +}); diff --git a/addons/api/mirage/generated/factories/credential-store.js b/addons/api/mirage/generated/factories/credential-store.js new file mode 100644 index 000000000..1fb24e39e --- /dev/null +++ b/addons/api/mirage/generated/factories/credential-store.js @@ -0,0 +1,12 @@ +import { Factory } from 'ember-cli-mirage'; +import { random, date } from 'faker'; + +/** + * GeneratedAuthMethodModelFactory + */ +export default Factory.extend({ + name: () => random.words(), + description: () => random.words(), + created_time: () => date.recent(), + updated_time: () => date.recent(), +}); diff --git a/addons/api/mirage/models/credential-store.js b/addons/api/mirage/models/credential-store.js new file mode 100644 index 000000000..7faa2b5a2 --- /dev/null +++ b/addons/api/mirage/models/credential-store.js @@ -0,0 +1,5 @@ +import { Model, belongsTo } from 'ember-cli-mirage'; + +export default Model.extend({ + scope: belongsTo(), +}); diff --git a/addons/api/mirage/scenarios/default.js b/addons/api/mirage/scenarios/default.js index 62018788c..12adb6c5d 100644 --- a/addons/api/mirage/scenarios/default.js +++ b/addons/api/mirage/scenarios/default.js @@ -49,5 +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 }); }); } diff --git a/addons/api/mirage/serializers/credential-store.js b/addons/api/mirage/serializers/credential-store.js new file mode 100644 index 000000000..3b0208512 --- /dev/null +++ b/addons/api/mirage/serializers/credential-store.js @@ -0,0 +1,17 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + modelName: 'credential-store', + + _hashForModel(/* model */) { + const json = ApplicationSerializer.prototype._hashForModel.apply( + this, + arguments + ); + // in a real API, the following fields are set-only and returned in + // associated HMAC fields + delete json.attributes?.token; + delete json.attributes?.client_certificate_key; + return json; + }, +}); diff --git a/addons/api/tests/unit/models/credential-store-test.js b/addons/api/tests/unit/models/credential-store-test.js new file mode 100644 index 000000000..6275d3454 --- /dev/null +++ b/addons/api/tests/unit/models/credential-store-test.js @@ -0,0 +1,13 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Model | credential store', 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-store', {}); + assert.ok(model); + }); +}); diff --git a/addons/api/tests/unit/models/fragment-credential-store-attributes-test.js b/addons/api/tests/unit/models/fragment-credential-store-attributes-test.js new file mode 100644 index 000000000..33f21b67b --- /dev/null +++ b/addons/api/tests/unit/models/fragment-credential-store-attributes-test.js @@ -0,0 +1,13 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Model | fragment credential store 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-store-attributes', {}); + assert.ok(model); + }); +}); diff --git a/addons/api/tests/unit/serializers/credential-store-test.js b/addons/api/tests/unit/serializers/credential-store-test.js new file mode 100644 index 000000000..6beae653c --- /dev/null +++ b/addons/api/tests/unit/serializers/credential-store-test.js @@ -0,0 +1,93 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Serializer | credential store', function (hooks) { + setupTest(hooks); + + test('it serializes vault-type correctly on create', function (assert) { + assert.expect(1); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('credential-store'); + const record = store.createRecord('credential-store', { + type: 'vault', + name: 'Name', + description: 'Description', + attributes: { + address: 'http://localhost:5000', + ca_cert: 'ca-cert-123', + client_certificate: 'client-cert-123', + client_certificate_key: 'client-cert-key-456', + namespace: 'foobar', + tls_server_name: 'yes', + tls_skip_verify: false, + token: 'foo', + }, + version: 1, + }); + const snapshot = record._createSnapshot(); + const serializedRecord = serializer.serialize(snapshot); + assert.deepEqual(serializedRecord, { + type: 'vault', + name: 'Name', + description: 'Description', + version: 1, + attributes: { + address: 'http://localhost:5000', + ca_cert: 'ca-cert-123', + client_certificate: 'client-cert-123', + client_certificate_key: 'client-cert-key-456', + namespace: 'foobar', + tls_server_name: 'yes', + tls_skip_verify: false, + token: 'foo', + }, + }); + }); + + test('it serializes vault-type correctly on update', function (assert) { + assert.expect(1); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('credential-store'); + store.push({ + data: { + id: '1', + type: 'credential-store', + attributes: { + type: 'vault', + name: 'Name', + description: 'Description', + attributes: { + address: 'http://localhost:5000', + ca_cert: 'ca-cert-123', + client_certificate: 'client-cert-123', + client_certificate_key: 'client-cert-key-456', + namespace: 'foobar', + tls_server_name: 'yes', + tls_skip_verify: false, + token: 'foo', + }, + version: 1, + }, + }, + }); + const record = store.peekRecord('credential-store', '1'); + const snapshot = record._createSnapshot(); + const serializedRecord = serializer.serialize(snapshot); + assert.deepEqual(serializedRecord, { + type: 'vault', + name: 'Name', + description: 'Description', + attributes: { + address: 'http://localhost:5000', + ca_cert: 'ca-cert-123', + client_certificate: 'client-cert-123', + client_certificate_key: 'client-cert-key-456', + namespace: 'foobar', + tls_server_name: 'yes', + tls_skip_verify: false, + token: 'foo', + }, + version: 1, + }); + }); +}); diff --git a/addons/api/tests/unit/serializers/fragment-credential-store-attributes-test.js b/addons/api/tests/unit/serializers/fragment-credential-store-attributes-test.js new file mode 100644 index 000000000..7fbbd0744 --- /dev/null +++ b/addons/api/tests/unit/serializers/fragment-credential-store-attributes-test.js @@ -0,0 +1,31 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module( + 'Unit | Serializer | fragment credential store attributes', + function (hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function (assert) { + let store = this.owner.lookup('service:store'); + let serializer = store.serializerFor( + 'fragment-credential-store-attributes' + ); + + assert.ok(serializer); + }); + + test('it serializes records', function (assert) { + let store = this.owner.lookup('service:store'); + let record = store.createRecord( + 'fragment-credential-store-attributes', + {} + ); + + let serializedRecord = record.serialize(); + + assert.ok(serializedRecord); + }); + } +);