diff --git a/ui/app/decorators/model-expanded-attributes.js b/ui/app/decorators/model-expanded-attributes.js new file mode 100644 index 000000000..60db028a3 --- /dev/null +++ b/ui/app/decorators/model-expanded-attributes.js @@ -0,0 +1,85 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { expandAttributeMeta } from 'vault/utils/field-to-attrs'; +import Model from '@ember-data/model'; +import { assert } from '@ember/debug'; + +/** + * sets allByKey properties on model class. These are all the attributes on the model + * and any belongsTo models, expanded with attribute metadata. The value returned is an + * object where the key is the attribute name, and the value is the expanded attribute + * metadata. + * This decorator also exposes a helper function `_expandGroups` which, when given groups + * as expected in field-to-attrs util, will return a similar object with the expanded + * attributes in place of the strings in the array. + */ + +export function withExpandedAttributes() { + return function decorator(SuperClass) { + if (!Object.prototype.isPrototypeOf.call(Model, SuperClass)) { + // eslint-disable-next-line + console.error( + 'withExpandedAttributes decorator must be used on instance of ember-data Model class. Decorator not applied to returned class' + ); + return SuperClass; + } + return class ModelExpandedAttrs extends SuperClass { + // Helper method for expanding dynamic groups on model + _expandGroups(groups) { + if (!Array.isArray(groups)) { + throw new Error('_expandGroups expects an array of objects'); + } + /* Expects group shape to be something like: + [ + { default: ['ttl', 'maxTtl'] }, + { "Method Options": ['other', 'fieldNames'] }, + ]*/ + return groups.map((obj) => { + const [key, stringArray] = Object.entries(obj)[0]; + const expanded = stringArray.map((fieldName) => this.allByKey[fieldName]).filter((f) => !!f); + assert(`all fields found in allByKey for group ${key}`, expanded.length === stringArray.length); + return { [key]: expanded }; + }); + } + + _allByKey = null; + get allByKey() { + // Caching like this ensures allByKey only gets calculated once + if (!this._allByKey) { + const byKey = {}; + // First, get attr names which are on the model directly + // By this time, OpenAPI should have populated non-explicit attrs + const mainFields = []; + this.eachAttribute(function (key) { + mainFields.push(key); + }); + const expanded = expandAttributeMeta(this, mainFields); + expanded.forEach((attr) => { + // Add expanded attributes from the model + byKey[attr.name] = attr; + }); + + // Next, fetch and expand attrs for related models + this.eachRelationship(function (name, descriptor) { + // We don't worry about getting hasMany relationships + if (descriptor.kind !== 'belongsTo') return; + const rModel = this[name]; + const rAttrNames = []; + rModel.eachAttribute(function (key) { + rAttrNames.push(key); + }); + const expanded = expandAttributeMeta(rModel, rAttrNames); + expanded.forEach((attr) => { + byKey[`${name}.${attr.name}`] = attr; + }); + }, this); + this._allByKey = byKey; + } + return this._allByKey; + } + }; + }; +} diff --git a/ui/app/models/secret-engine.js b/ui/app/models/secret-engine.js index 8cf3fafb2..f1db0420a 100644 --- a/ui/app/models/secret-engine.js +++ b/ui/app/models/secret-engine.js @@ -6,8 +6,8 @@ import Model, { attr, belongsTo } from '@ember-data/model'; import { computed } from '@ember/object'; // eslint-disable-line import { equal } from '@ember/object/computed'; // eslint-disable-line -import fieldToAttrs, { expandAttributeMeta } from 'vault/utils/field-to-attrs'; import { withModelValidations } from 'vault/decorators/model-validations'; +import { withExpandedAttributes } from 'vault/decorators/model-expanded-attributes'; // identity will be managed separately and the inclusion // of the system backend is an implementation detail @@ -22,89 +22,156 @@ const validations = { }; @withModelValidations(validations) -class SecretEngineModel extends Model {} -export default SecretEngineModel.extend({ - path: attr('string'), - accessor: attr('string'), - name: attr('string'), - type: attr('string', { - label: 'Secret engine type', - }), - description: attr('string', { +@withExpandedAttributes() +export default class SecretEngineModel extends Model { + @attr('string') path; + @attr('string') type; + @attr('string', { editType: 'textarea', - }), - // will only have value for kv type - version: attr('number', { + }) + description; + @belongsTo('mount-config', { async: false, inverse: null }) config; + + // Enterprise options (still available on OSS) + @attr('boolean', { + helpText: + 'When Replication is enabled, a local mount will not be replicated across clusters. This can only be specified at mount time.', + }) + local; + @attr('boolean', { + helpText: + 'When enabled - if a seal supporting seal wrapping is specified in the configuration, all critical security parameters (CSPs) in this backend will be seal wrapped. (For K/V mounts, all values will be seal wrapped.) This can only be specified at mount time.', + }) + sealWrap; + @attr('boolean') externalEntropyAccess; + + // options.version + @attr('number', { label: 'Version', helpText: 'The KV Secrets Engine can operate in different modes. Version 1 is the original generic Secrets Engine the allows for storing of static key/value pairs. Version 2 added more features including data versioning, TTLs, and check and set.', possibleValues: [2, 1], // This shouldn't be defaultValue because if no version comes back from API we should assume it's v1 defaultFormValue: 2, // Set the form to 2 by default - }), - config: belongsTo('mount-config', { async: false, inverse: null }), - local: attr('boolean', { - helpText: - 'When Replication is enabled, a local mount will not be replicated across clusters. This can only be specified at mount time.', - }), - sealWrap: attr('boolean', { - helpText: - 'When enabled - if a seal supporting seal wrapping is specified in the configuration, all critical security parameters (CSPs) in this backend will be seal wrapped. (For K/V mounts, all values will be seal wrapped.) This can only be specified at mount time.', - }), + }) + version; + + // SSH specific attributes + @attr('string') privateKey; + @attr('string') publicKey; + @attr('boolean', { + defaultValue: true, + }) + generateSigningKey; + + // AWS specific attributes + @attr('string') lease; + @attr('string') leaseMax; + + // Returned from API response + @attr('string') accessor; + // KV 2 additional config default options - maxVersions: attr('number', { + @attr('number', { defaultValue: 0, label: 'Maximum number of versions', subText: 'The number of versions to keep per key. Once the number of keys exceeds the maximum number set here, the oldest version will be permanently deleted. This value applies to all keys, but a key’s metadata settings can overwrite this value. When 0 is used or the value is unset, Vault will keep 10 versions.', - }), - casRequired: attr('boolean', { + }) + maxVersions; + @attr('boolean', { defaultValue: false, label: 'Require Check and Set', subText: 'If checked, all keys will require the cas parameter to be set on all write requests. A key’s metadata settings can overwrite this value.', - }), - deleteVersionAfter: attr({ + }) + casRequired; + @attr({ defaultValue: 0, editType: 'ttl', label: 'Automate secret deletion', helperTextDisabled: 'A secret’s version must be manually deleted.', helperTextEnabled: 'Delete all new versions of this secret after', - }), + }) + deleteVersionAfter; - modelTypeForKV: computed('engineType', 'version', function () { - const type = this.engineType; - let modelType = 'secret'; - if ((type === 'kv' || type === 'generic') && this.version === 2) { - modelType = 'secret-v2'; + /* GETTERS */ + get modelTypeForKV() { + const engineType = this.engineType; + if ((engineType === 'kv' || engineType === 'generic') && this.version === 2) { + return 'secret-v2'; } - return modelType; - }), + return 'secret'; + } + get isV2KV() { + return this.modelTypeForKV === 'secret-v2'; + } - isV2KV: equal('modelTypeForKV', 'secret-v2'), + get attrs() { + return this.formFields.map((fieldName) => { + return this.allByKey[fieldName]; + }); + } - formFields: computed('engineType', 'version', function () { + get fieldGroups() { + return this._expandGroups(this.formFieldGroups); + } + + get icon() { + if (!this.engineType || this.engineType === 'kmip') { + return 'secrets'; + } + if (this.engineType === 'keymgmt') { + return 'key'; + } + return this.engineType; + } + + get engineType() { + return (this.type || '').replace(/^ns_/, ''); + } + + get shouldIncludeInList() { + return !LIST_EXCLUDED_BACKENDS.includes(this.engineType); + } + + get localDisplay() { + return this.local ? 'local' : 'replicated'; + } + + get formFields() { const type = this.engineType; const fields = ['type', 'path', 'description', 'accessor', 'local', 'sealWrap']; // no ttl options for keymgmt - const ttl = type !== 'keymgmt' ? 'defaultLeaseTtl,maxLeaseTtl,' : ''; + if (type !== 'keymgmt') { + fields.push('config.defaultLeaseTtl', 'config.maxLeaseTtl'); + } fields.push( - `config.{${ttl}auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders,allowedResponseHeaders}` + 'config.auditNonHmacRequestKeys', + 'config.auditNonHmacResponseKeys', + 'config.passthroughRequestHeaders', + 'config.allowedResponseHeaders' ); if (type === 'kv' || type === 'generic') { fields.push('version'); } // version comes in as number not string - if (type === 'kv' && this.version === 2) { + if (type === 'kv' && parseInt(this.version, 10) === 2) { fields.push('casRequired', 'deleteVersionAfter', 'maxVersions'); } return fields; - }), + } - formFieldGroups: computed('engineType', function () { + get formFieldGroups() { let defaultFields = ['path']; let optionFields; const CORE_OPTIONS = ['description', 'config.listingVisibility', 'local', 'sealWrap']; + const STANDARD_CONFIG = [ + 'config.auditNonHmacRequestKeys', + 'config.auditNonHmacResponseKeys', + 'config.passthroughRequestHeaders', + 'config.allowedResponseHeaders', + ]; switch (this.engineType) { case 'kv': @@ -112,37 +179,32 @@ export default SecretEngineModel.extend({ optionFields = [ 'version', ...CORE_OPTIONS, - `config.{defaultLeaseTtl,maxLeaseTtl,auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders,allowedResponseHeaders}`, + 'config.defaultLeaseTtl', + 'config.maxLeaseTtl', + ...STANDARD_CONFIG, ]; break; case 'generic': optionFields = [ 'version', ...CORE_OPTIONS, - `config.{defaultLeaseTtl,maxLeaseTtl,auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders,allowedResponseHeaders}`, + 'config.defaultLeaseTtl', + 'config.maxLeaseTtl', + ...STANDARD_CONFIG, ]; break; case 'database': // Highlight TTLs in default - defaultFields = ['path', 'config.{defaultLeaseTtl}', 'config.{maxLeaseTtl}']; - optionFields = [ - ...CORE_OPTIONS, - 'config.{auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders,allowedResponseHeaders}', - ]; + defaultFields = ['path', 'config.defaultLeaseTtl', 'config.maxLeaseTtl']; + optionFields = [...CORE_OPTIONS, ...STANDARD_CONFIG]; break; case 'keymgmt': // no ttl options for keymgmt - optionFields = [ - ...CORE_OPTIONS, - 'config.{auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders,allowedResponseHeaders}', - ]; + optionFields = [...CORE_OPTIONS, ...STANDARD_CONFIG]; break; default: defaultFields = ['path']; - optionFields = [ - ...CORE_OPTIONS, - `config.{defaultLeaseTtl,maxLeaseTtl,auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders,allowedResponseHeaders}`, - ]; + optionFields = [...CORE_OPTIONS, 'config.defaultLeaseTtl', 'config.maxLeaseTtl', ...STANDARD_CONFIG]; break; } @@ -152,57 +214,17 @@ export default SecretEngineModel.extend({ 'Method Options': optionFields, }, ]; - }), - - attrs: computed('formFields', function () { - return expandAttributeMeta(this, this.formFields); - }), - - fieldGroups: computed('formFieldGroups', function () { - return fieldToAttrs(this, this.formFieldGroups); - }), - - icon: computed('engineType', function () { - if (!this.engineType || this.engineType === 'kmip') { - return 'secrets'; - } - if (this.engineType === 'keymgmt') { - return 'key'; - } - return this.engineType; - }), - - // namespaces introduced types with a `ns_` prefix for built-in engines - // so we need to strip that to normalize the type - engineType: computed('type', function () { - return (this.type || '').replace(/^ns_/, ''); - }), - - shouldIncludeInList: computed('engineType', function () { - return !LIST_EXCLUDED_BACKENDS.includes(this.engineType); - }), - - localDisplay: computed('local', function () { - return this.local ? 'local' : 'replicated'; - }), - - // ssh specific ones - privateKey: attr('string'), - publicKey: attr('string'), - generateSigningKey: attr('boolean', { - defaultValue: true, - }), + } + /* ACTIONS */ saveCA(options) { if (this.type !== 'ssh') { return; } if (options.isDelete) { - this.setProperties({ - privateKey: null, - publicKey: null, - generateSigningKey: false, - }); + this.privateKey = null; + this.publicKey = null; + this.generateSigningKey = false; } return this.save({ adapterOptions: { @@ -211,7 +233,7 @@ export default SecretEngineModel.extend({ attrsToSend: ['privateKey', 'publicKey', 'generateSigningKey'], }, }); - }, + } saveZeroAddressConfig() { return this.save({ @@ -219,9 +241,5 @@ export default SecretEngineModel.extend({ adapterMethod: 'saveZeroAddressConfig', }, }); - }, - - // aws backend attrs - lease: attr('string'), - leaseMax: attr('string'), -}); + } +} diff --git a/ui/app/routes/vault/cluster/secrets/backend/create-root.js b/ui/app/routes/vault/cluster/secrets/backend/create-root.js index db5b8a89f..d8e8c0306 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/create-root.js +++ b/ui/app/routes/vault/cluster/secrets/backend/create-root.js @@ -9,7 +9,7 @@ import EditBase from './secret-edit'; const secretModel = (store, backend, key) => { const backendModel = store.peekRecord('secret-engine', backend); - const modelType = backendModel.get('modelTypeForKV'); + const modelType = backendModel.modelTypeForKV; if (modelType !== 'secret-v2') { const model = store.createRecord(modelType, { path: key, diff --git a/ui/app/routes/vault/cluster/secrets/backend/list.js b/ui/app/routes/vault/cluster/secrets/backend/list.js index e2a6c9360..575b5b718 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/list.js +++ b/ui/app/routes/vault/cluster/secrets/backend/list.js @@ -10,6 +10,7 @@ import { supportedSecretBackends } from 'vault/helpers/supported-secret-backends import { allEngines } from 'vault/helpers/mountable-secret-engines'; import { inject as service } from '@ember/service'; import { normalizePath } from 'vault/utils/path-encoding-helpers'; +import { assert } from '@ember/debug'; const SUPPORTED_BACKENDS = supportedSecretBackends(); @@ -68,7 +69,8 @@ export default Route.extend({ const backend = this.enginePathParam(); const { tab } = this.paramsFor('vault.cluster.secrets.backend.list-root'); const secretEngine = this.store.peekRecord('secret-engine', backend); - const type = secretEngine && secretEngine.get('engineType'); + const type = secretEngine?.engineType; + assert('secretEngine.engineType is not defined', !!type); const engineRoute = allEngines().findBy('type', type)?.engineRoute; if (!type || !SUPPORTED_BACKENDS.includes(type)) { @@ -88,7 +90,7 @@ export default Route.extend({ getModelType(backend, tab) { const secretEngine = this.store.peekRecord('secret-engine', backend); - const type = secretEngine.get('engineType'); + const type = secretEngine.engineType; const types = { database: tab === 'role' ? 'database/role' : 'database/connection', transit: 'transit-key', @@ -98,9 +100,9 @@ export default Route.extend({ pki: `pki/${tab || 'pki-role'}`, // secret or secret-v2 cubbyhole: 'secret', - kv: secretEngine.get('modelTypeForKV'), + kv: secretEngine.modelTypeForKV, keymgmt: `keymgmt/${tab || 'key'}`, - generic: secretEngine.get('modelTypeForKV'), + generic: secretEngine.modelTypeForKV, }; return types[type]; }, diff --git a/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js b/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js index afe82c82c..90cbf8c8e 100644 --- a/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js +++ b/ui/app/routes/vault/cluster/secrets/backend/secret-edit.js @@ -111,9 +111,9 @@ export default Route.extend(UnloadModelRoute, { aws: 'role-aws', pki: secret && secret.startsWith('cert/') ? 'pki/cert' : 'pki/pki-role', cubbyhole: 'secret', - kv: backendModel.get('modelTypeForKV'), + kv: backendModel.modelTypeForKV, keymgmt: `keymgmt/${options.queryParams?.itemType || 'key'}`, - generic: backendModel.get('modelTypeForKV'), + generic: backendModel.modelTypeForKV, }; return types[type]; }, diff --git a/ui/tests/acceptance/settings/mount-secret-backend-test.js b/ui/tests/acceptance/settings/mount-secret-backend-test.js index 083a1288f..a95509536 100644 --- a/ui/tests/acceptance/settings/mount-secret-backend-test.js +++ b/ui/tests/acceptance/settings/mount-secret-backend-test.js @@ -51,8 +51,8 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) { .maxTTLVal(maxTTLHours) .submit(); await configPage.visit({ backend: path }); - assert.strictEqual(configPage.defaultTTL, defaultTTLSeconds, 'shows the proper TTL'); - assert.strictEqual(configPage.maxTTL, maxTTLSeconds, 'shows the proper max TTL'); + assert.strictEqual(configPage.defaultTTL, `${defaultTTLSeconds}s`, 'shows the proper TTL'); + assert.strictEqual(configPage.maxTTL, `${maxTTLSeconds}s`, 'shows the proper max TTL'); }); test('it sets the ttl when enabled then disabled', async function (assert) { @@ -77,7 +77,7 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) { .submit(); await configPage.visit({ backend: path }); assert.strictEqual(configPage.defaultTTL, '0', 'shows the proper TTL'); - assert.strictEqual(configPage.maxTTL, maxTTLSeconds, 'shows the proper max TTL'); + assert.strictEqual(configPage.maxTTL, `${maxTTLSeconds}s`, 'shows the proper max TTL'); }); test('it throws error if setting duplicate path name', async function (assert) { diff --git a/ui/tests/unit/decorators/model-expanded-attributes-test.js b/ui/tests/unit/decorators/model-expanded-attributes-test.js new file mode 100644 index 000000000..f7c12cffb --- /dev/null +++ b/ui/tests/unit/decorators/model-expanded-attributes-test.js @@ -0,0 +1,100 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import sinon from 'sinon'; +import Model, { attr } from '@ember-data/model'; +import { withExpandedAttributes } from 'vault/decorators/model-expanded-attributes'; + +// create class using decorator +const createClass = () => { + @withExpandedAttributes() + class Foo extends Model { + @attr('string', { + label: 'Foo', + subText: 'A form field', + }) + foo; + @attr('boolean', { + label: 'Bar', + subText: 'Maybe a checkbox', + }) + bar; + @attr('number', { + label: 'Baz', + subText: 'A number field', + }) + baz; + + get fieldGroups() { + return [{ default: ['baz'] }, { 'Other options': ['foo', 'bar'] }]; + } + } + return new Foo(); +}; + +module('Unit | Decorators | model-expanded-attributes', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + this.spy = sinon.spy(console, 'error'); + this.fooField = { + name: 'foo', + options: { label: 'Foo', subText: 'A form field' }, + type: 'string', + }; + this.barField = { + name: 'bar', + options: { label: 'Bar', subText: 'Maybe a checkbox' }, + type: 'boolean', + }; + this.bazField = { + name: 'baz', + options: { label: 'Baz', subText: 'A number field' }, + type: 'number', + }; + }); + hooks.afterEach(function () { + this.spy.restore(); + }); + + test('it should warn when applying decorator to class that does not extend Model', function (assert) { + @withExpandedAttributes() + class Foo {} // eslint-disable-line + const message = + 'withExpandedAttributes decorator must be used on instance of ember-data Model class. Decorator not applied to returned class'; + assert.ok(this.spy.calledWith(message), 'Error is printed to console'); + }); + + test('it adds allByKey value to model', function (assert) { + assert.expect(1); + const model = createClass(); + assert.deepEqual( + { foo: this.fooField, bar: this.barField, baz: this.bazField }, + model.allByKey, + 'allByKey set on Model class' + ); + }); + + test('_expandGroups helper works correctly', function (assert) { + const model = createClass(); + const result = model._expandGroups(model.fieldGroups); + assert.deepEqual(result, [ + { default: [this.bazField] }, + { 'Other options': [this.fooField, this.barField] }, + ]); + }); + + test('_expandGroups throws assertion when incorrect inputs', function (assert) { + assert.expect(1); + const model = createClass(); + try { + model._expandGroups({ foo: ['bar'] }); + } catch (e) { + assert.strictEqual(e.message, '_expandGroups expects an array of objects'); + } + }); +}); diff --git a/ui/tests/unit/models/secret-engine-test.js b/ui/tests/unit/models/secret-engine-test.js index fb2c1cd2b..1b977dbb6 100644 --- a/ui/tests/unit/models/secret-engine-test.js +++ b/ui/tests/unit/models/secret-engine-test.js @@ -3,59 +3,154 @@ * SPDX-License-Identifier: MPL-2.0 */ -import { run } from '@ember/runloop'; +import sinon from 'sinon'; import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; module('Unit | Model | secret-engine', function (hooks) { setupTest(hooks); + hooks.beforeEach(function () { + this.store = this.owner.lookup('service:store'); + }); + + module('modelTypeForKV', function () { + test('is secret by default', function (assert) { + assert.expect(1); + const model = this.store.createRecord('secret-engine'); + assert.strictEqual(model.get('modelTypeForKV'), 'secret'); + }); + + test('is secret-v2 for kv v2', function (assert) { + assert.expect(1); + const model = this.store.createRecord('secret-engine', { + version: 2, + type: 'kv', + }); + assert.strictEqual(model.get('modelTypeForKV'), 'secret-v2'); + }); + + test('is secret-v2 for generic v2', function (assert) { + assert.expect(1); + const model = this.store.createRecord('secret-engine', { + version: 2, + type: 'kv', + }); + + assert.strictEqual(model.get('modelTypeForKV'), 'secret-v2'); + }); + + test('is secret when v2 if not kv or generic', function (assert) { + assert.expect(1); + const model = this.store.createRecord('secret-engine', { + version: 2, + type: 'ssh', + }); - test('modelTypeForKV is secret by default', function (assert) { - assert.expect(1); - let model; - run(() => { - model = run(() => this.owner.lookup('service:store').createRecord('secret-engine')); assert.strictEqual(model.get('modelTypeForKV'), 'secret'); }); }); - test('modelTypeForKV is secret-v2 for kv v2', function (assert) { - assert.expect(1); - let model; - run(() => { - model = run(() => - this.owner.lookup('service:store').createRecord('secret-engine', { - version: 2, - type: 'kv', - }) - ); - assert.strictEqual(model.get('modelTypeForKV'), 'secret-v2'); + module('formFields', function () { + test('it returns correct fields by default', function (assert) { + assert.expect(1); + const model = this.store.createRecord('secret-engine', { + type: '', + }); + + assert.deepEqual(model.get('formFields'), [ + 'type', + 'path', + 'description', + 'accessor', + 'local', + 'sealWrap', + 'config.defaultLeaseTtl', + 'config.maxLeaseTtl', + 'config.auditNonHmacRequestKeys', + 'config.auditNonHmacResponseKeys', + 'config.passthroughRequestHeaders', + 'config.allowedResponseHeaders', + ]); + }); + + test('it returns correct fields for KV v1', function (assert) { + assert.expect(1); + const model = this.store.createRecord('secret-engine', { + type: 'kv', + }); + + assert.deepEqual(model.get('formFields'), [ + 'type', + 'path', + 'description', + 'accessor', + 'local', + 'sealWrap', + 'config.defaultLeaseTtl', + 'config.maxLeaseTtl', + 'config.auditNonHmacRequestKeys', + 'config.auditNonHmacResponseKeys', + 'config.passthroughRequestHeaders', + 'config.allowedResponseHeaders', + 'version', + ]); + }); + + test('it returns correct fields for KV v2', function (assert) { + assert.expect(1); + const model = this.store.createRecord('secret-engine', { + type: 'kv', + version: '2', + }); + + assert.deepEqual(model.get('formFields'), [ + 'type', + 'path', + 'description', + 'accessor', + 'local', + 'sealWrap', + 'config.defaultLeaseTtl', + 'config.maxLeaseTtl', + 'config.auditNonHmacRequestKeys', + 'config.auditNonHmacResponseKeys', + 'config.passthroughRequestHeaders', + 'config.allowedResponseHeaders', + 'version', + 'casRequired', + 'deleteVersionAfter', + 'maxVersions', + ]); + }); + + test('it returns correct fields for keymgmt', function (assert) { + assert.expect(1); + const model = this.store.createRecord('secret-engine', { + type: 'keymgmt', + }); + + assert.deepEqual(model.get('formFields'), [ + 'type', + 'path', + 'description', + 'accessor', + 'local', + 'sealWrap', + 'config.auditNonHmacRequestKeys', + 'config.auditNonHmacResponseKeys', + 'config.passthroughRequestHeaders', + 'config.allowedResponseHeaders', + ]); }); }); - test('modelTypeForKV is secret-v2 for generic v2', function (assert) { - assert.expect(1); - let model; - run(() => { - model = run(() => - this.owner.lookup('service:store').createRecord('secret-engine', { - version: 2, - type: 'kv', - }) - ); - assert.strictEqual(model.get('modelTypeForKV'), 'secret-v2'); - }); - }); + module('formFieldGroups', function () { + test('returns correct values by default', function (assert) { + assert.expect(1); + const model = this.store.createRecord('secret-engine', { + type: 'aws', + }); - test('formFieldGroups returns correct values by default', function (assert) { - assert.expect(1); - let model; - run(() => { - model = run(() => - this.owner.lookup('service:store').createRecord('secret-engine', { - type: 'aws', - }) - ); assert.deepEqual(model.get('formFieldGroups'), [ { default: ['path'] }, { @@ -64,22 +159,22 @@ module('Unit | Model | secret-engine', function (hooks) { 'config.listingVisibility', 'local', 'sealWrap', - 'config.{defaultLeaseTtl,maxLeaseTtl,auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders,allowedResponseHeaders}', + 'config.defaultLeaseTtl', + 'config.maxLeaseTtl', + 'config.auditNonHmacRequestKeys', + 'config.auditNonHmacResponseKeys', + 'config.passthroughRequestHeaders', + 'config.allowedResponseHeaders', ], }, ]); }); - }); + test('returns correct values for KV', function (assert) { + assert.expect(1); + const model = this.store.createRecord('secret-engine', { + type: 'kv', + }); - test('formFieldGroups returns correct values for KV', function (assert) { - assert.expect(1); - let model; - run(() => { - model = run(() => - this.owner.lookup('service:store').createRecord('secret-engine', { - type: 'kv', - }) - ); assert.deepEqual(model.get('formFieldGroups'), [ { default: ['path', 'maxVersions', 'casRequired', 'deleteVersionAfter'] }, { @@ -89,22 +184,23 @@ module('Unit | Model | secret-engine', function (hooks) { 'config.listingVisibility', 'local', 'sealWrap', - 'config.{defaultLeaseTtl,maxLeaseTtl,auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders,allowedResponseHeaders}', + 'config.defaultLeaseTtl', + 'config.maxLeaseTtl', + 'config.auditNonHmacRequestKeys', + 'config.auditNonHmacResponseKeys', + 'config.passthroughRequestHeaders', + 'config.allowedResponseHeaders', ], }, ]); }); - }); - test('formFieldGroups returns correct values for generic', function (assert) { - assert.expect(1); - let model; - run(() => { - model = run(() => - this.owner.lookup('service:store').createRecord('secret-engine', { - type: 'generic', - }) - ); + test('returns correct values for generic', function (assert) { + assert.expect(1); + const model = this.store.createRecord('secret-engine', { + type: 'generic', + }); + assert.deepEqual(model.get('formFieldGroups'), [ { default: ['path'] }, { @@ -114,46 +210,46 @@ module('Unit | Model | secret-engine', function (hooks) { 'config.listingVisibility', 'local', 'sealWrap', - 'config.{defaultLeaseTtl,maxLeaseTtl,auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders,allowedResponseHeaders}', + 'config.defaultLeaseTtl', + 'config.maxLeaseTtl', + 'config.auditNonHmacRequestKeys', + 'config.auditNonHmacResponseKeys', + 'config.passthroughRequestHeaders', + 'config.allowedResponseHeaders', ], }, ]); }); - }); - test('formFieldGroups returns correct values for database', function (assert) { - assert.expect(1); - let model; - run(() => { - model = run(() => - this.owner.lookup('service:store').createRecord('secret-engine', { - type: 'database', - }) - ); + test('returns correct values for database', function (assert) { + assert.expect(1); + const model = this.store.createRecord('secret-engine', { + type: 'database', + }); + assert.deepEqual(model.get('formFieldGroups'), [ - { default: ['path', 'config.{defaultLeaseTtl}', 'config.{maxLeaseTtl}'] }, + { default: ['path', 'config.defaultLeaseTtl', 'config.maxLeaseTtl'] }, { 'Method Options': [ 'description', 'config.listingVisibility', 'local', 'sealWrap', - 'config.{auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders,allowedResponseHeaders}', + 'config.auditNonHmacRequestKeys', + 'config.auditNonHmacResponseKeys', + 'config.passthroughRequestHeaders', + 'config.allowedResponseHeaders', ], }, ]); }); - }); - test('formFieldGroups returns correct values for keymgmt', function (assert) { - assert.expect(1); - let model; - run(() => { - model = run(() => - this.owner.lookup('service:store').createRecord('secret-engine', { - type: 'keymgmt', - }) - ); + test('returns correct values for keymgmt', function (assert) { + assert.expect(1); + const model = this.store.createRecord('secret-engine', { + type: 'keymgmt', + }); + assert.deepEqual(model.get('formFieldGroups'), [ { default: ['path'] }, { @@ -162,10 +258,191 @@ module('Unit | Model | secret-engine', function (hooks) { 'config.listingVisibility', 'local', 'sealWrap', - 'config.{auditNonHmacRequestKeys,auditNonHmacResponseKeys,passthroughRequestHeaders,allowedResponseHeaders}', + 'config.auditNonHmacRequestKeys', + 'config.auditNonHmacResponseKeys', + 'config.passthroughRequestHeaders', + 'config.allowedResponseHeaders', ], }, ]); }); }); + + module('engineType', function () { + test('strips leading ns_ from type', function (assert) { + assert.expect(1); + const model = this.store.createRecord('secret-engine', { + // eg. ns_cubbyhole, ns_identity, ns_system + type: 'ns_identity', + }); + assert.strictEqual(model.engineType, 'identity'); + }); + test('returns type by default', function (assert) { + assert.expect(1); + const model = this.store.createRecord('secret-engine', { + type: 'zebras', + }); + assert.strictEqual(model.engineType, 'zebras'); + }); + }); + + module('icon', function () { + test('returns secrets if no engineType', function (assert) { + assert.expect(1); + const model = this.store.createRecord('secret-engine', { + type: '', + }); + assert.strictEqual(model.icon, 'secrets'); + }); + test('returns secrets if kmip', function (assert) { + assert.expect(1); + const model = this.store.createRecord('secret-engine', { + type: 'kmip', + }); + assert.strictEqual(model.icon, 'secrets'); + }); + test('returns key if keymgmt', function (assert) { + assert.expect(1); + const model = this.store.createRecord('secret-engine', { + type: 'keymgmt', + }); + assert.strictEqual(model.icon, 'key'); + }); + test('returns engineType by default', function (assert) { + assert.expect(1); + const model = this.store.createRecord('secret-engine', { + type: 'ducks', + }); + assert.strictEqual(model.icon, 'ducks'); + }); + }); + + module('shouldIncludeInList', function () { + test('returns false if excludeList includes type', function (assert) { + assert.expect(1); + const model = this.store.createRecord('secret-engine', { + type: 'system', + }); + assert.false(model.shouldIncludeInList); + }); + test('returns true if excludeList does not include type', function (assert) { + assert.expect(1); + const model = this.store.createRecord('secret-engine', { + type: 'hippos', + }); + assert.true(model.shouldIncludeInList); + }); + }); + + module('localDisplay', function () { + test('returns local if local', function (assert) { + assert.expect(1); + const model = this.store.createRecord('secret-engine', { + local: true, + }); + assert.strictEqual(model.localDisplay, 'local'); + }); + test('returns replicated if !local', function (assert) { + assert.expect(1); + const model = this.store.createRecord('secret-engine', { + local: false, + }); + assert.strictEqual(model.localDisplay, 'replicated'); + }); + }); + + module('saveCA', function () { + test('does not call endpoint if type != ssh', async function (assert) { + assert.expect(1); + const model = this.store.createRecord('secret-engine', { + type: 'not-ssh', + }); + const saveSpy = sinon.spy(model, 'save'); + await model.saveCA({}); + assert.ok(saveSpy.notCalled, 'save not called'); + }); + test('calls save with correct params', async function (assert) { + assert.expect(4); + const model = this.store.createRecord('secret-engine', { + type: 'ssh', + privateKey: 'private-key', + publicKey: 'public-key', + generateSigningKey: true, + }); + const saveStub = sinon.stub(model, 'save').callsFake((params) => { + assert.deepEqual( + params, + { + adapterOptions: { + options: {}, + apiPath: 'config/ca', + attrsToSend: ['privateKey', 'publicKey', 'generateSigningKey'], + }, + }, + 'send correct params to save' + ); + return; + }); + + await model.saveCA({}); + assert.strictEqual(model.privateKey, 'private-key', 'value exists before save'); + assert.strictEqual(model.publicKey, 'public-key', 'value exists before save'); + assert.true(model.generateSigningKey, 'value true before save'); + + saveStub.restore(); + }); + test('sets properties when isDelete', async function (assert) { + assert.expect(7); + const model = this.store.createRecord('secret-engine', { + type: 'ssh', + privateKey: 'private-key', + publicKey: 'public-key', + generateSigningKey: true, + }); + const saveStub = sinon.stub(model, 'save').callsFake((params) => { + assert.deepEqual( + params, + { + adapterOptions: { + options: { isDelete: true }, + apiPath: 'config/ca', + attrsToSend: ['privateKey', 'publicKey', 'generateSigningKey'], + }, + }, + 'send correct params to save' + ); + return; + }); + assert.strictEqual(model.privateKey, 'private-key', 'value exists before save'); + assert.strictEqual(model.publicKey, 'public-key', 'value exists before save'); + assert.true(model.generateSigningKey, 'value true before save'); + + await model.saveCA({ isDelete: true }); + assert.strictEqual(model.privateKey, null, 'value null after save'); + assert.strictEqual(model.publicKey, null, 'value null after save'); + assert.false(model.generateSigningKey, 'value false after save'); + saveStub.restore(); + }); + }); + + module('saveZeroAddressConfig', function () { + test('calls save with correct params', async function (assert) { + assert.expect(1); + const model = this.store.createRecord('secret-engine', {}); + const saveStub = sinon.stub(model, 'save').callsFake((params) => { + assert.deepEqual( + params, + { + adapterOptions: { + adapterMethod: 'saveZeroAddressConfig', + }, + }, + 'send correct params to save' + ); + return; + }); + await model.saveZeroAddressConfig(); + saveStub.restore(); + }); + }); });