From d6933e9ef414febb1627ee2fccecb4e726e9e972 Mon Sep 17 00:00:00 2001 From: Jordan Reimer Date: Tue, 26 Apr 2022 08:23:31 -0600 Subject: [PATCH] KMSE Capabilities & Phase 1 Cleanup (#15143) * fixes issues in key-edit component * adds capabilities checks for keys and providers * adds distribute component to key and provider edit --- ui/app/adapters/keymgmt/key.js | 7 + ui/app/components/keymgmt/distribute.js | 2 +- ui/app/components/keymgmt/key-edit.js | 54 +-- ui/app/models/keymgmt/key.js | 36 +- ui/app/models/keymgmt/provider.js | 59 ++- .../components/keymgmt/distribute.hbs | 7 +- .../templates/components/keymgmt/key-edit.hbs | 371 ++++++++++-------- .../components/keymgmt/provider-edit.hbs | 355 +++++++++-------- .../components/keymgmt/distribute-test.js | 21 +- .../components/keymgmt/key-edit-test.js | 1 + .../components/keymgmt/provider-edit-test.js | 6 + 11 files changed, 531 insertions(+), 388 deletions(-) diff --git a/ui/app/adapters/keymgmt/key.js b/ui/app/adapters/keymgmt/key.js index 2046d096a..8b3fcaad2 100644 --- a/ui/app/adapters/keymgmt/key.js +++ b/ui/app/adapters/keymgmt/key.js @@ -149,4 +149,11 @@ export default class KeymgmtKeyAdapter extends ApplicationAdapter { // TODO: re-fetch record data after return this.ajax(this.url(backend, id, 'ROTATE'), 'PUT'); } + + removeFromProvider(model) { + const url = `${this.buildURL()}/${model.backend}/kms/${model.provider.name}/key/${model.name}`; + return this.ajax(url, 'DELETE').then(() => { + model.provider = null; + }); + } } diff --git a/ui/app/components/keymgmt/distribute.js b/ui/app/components/keymgmt/distribute.js index a138ad31d..8400670b1 100644 --- a/ui/app/components/keymgmt/distribute.js +++ b/ui/app/components/keymgmt/distribute.js @@ -181,7 +181,7 @@ export default class KeymgmtDistribute extends Component { .distribute(backend, kms, key, data) .then(() => { this.flashMessages.success(`Successfully distributed key ${key} to ${kms}`); - this.router.transitionTo('vault.cluster.secrets.backend.show', key); + this.args.onClose(); }) .catch((e) => { this.flashMessages.danger(`Error distributing key: ${e.errors}`); diff --git a/ui/app/components/keymgmt/key-edit.js b/ui/app/components/keymgmt/key-edit.js index 74531ba0e..2c46b20ae 100644 --- a/ui/app/components/keymgmt/key-edit.js +++ b/ui/app/components/keymgmt/key-edit.js @@ -2,6 +2,8 @@ import Component from '@glimmer/component'; import { inject as service } from '@ember/service'; import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; +import { task } from 'ember-concurrency'; +import { waitFor } from '@ember/test-waiters'; /** * @module KeymgmtKeyEdit @@ -32,50 +34,50 @@ export default class KeymgmtKeyEdit extends Component { return this.store.adapterFor('keymgmt/key'); } + get isMutable() { + return ['create', 'edit'].includes(this.args.mode); + } + + get isCreating() { + return this.args.mode === 'create'; + } + @action toggleModal(bool) { this.isDeleteModalOpen = bool; } - @action - createKey(evt) { + @task + @waitFor + *saveKey(evt) { evt.preventDefault(); - this.args.model.save(); + const { model } = this.args; + try { + yield model.save(); + this.router.transitionTo(SHOW_ROUTE, model.name); + } catch (error) { + this.flashMessages.danger(error.errors.join('. ')); + } } @action - updateKey(evt) { - evt.preventDefault(); - const name = this.args.model.name; - this.args.model - .save() - .then(() => { - this.router.transitionTo(SHOW_ROUTE, name); - }) - .catch((e) => { - this.flashMessages.danger(e.errors.join('. ')); - }); - } - - @action - removeKey(id) { - // TODO: remove action - console.log('remove', id); + async removeKey() { + try { + await this.keyAdapter.removeFromProvider(this.args.model); + this.flashMessages.success('Key has been successfully removed from provider'); + } catch (error) { + this.flashMessages.danger(error.errors?.join('. ')); + } } @action deleteKey() { const secret = this.args.model; const backend = secret.backend; - console.log({ secret }); secret .destroyRecord() .then(() => { - try { - this.router.transitionTo(LIST_ROOT_ROUTE, backend, { queryParams: { tab: 'key' } }); - } catch (e) { - console.debug(e); - } + this.router.transitionTo(LIST_ROOT_ROUTE, backend); }) .catch((e) => { this.flashMessages.danger(e.errors?.join('. ')); diff --git a/ui/app/models/keymgmt/key.js b/ui/app/models/keymgmt/key.js index 2c7ce6354..e88c5ff70 100644 --- a/ui/app/models/keymgmt/key.js +++ b/ui/app/models/keymgmt/key.js @@ -1,5 +1,6 @@ import Model, { attr } from '@ember-data/model'; import { expandAttributeMeta } from 'vault/utils/field-to-attrs'; +import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; export const KEY_TYPES = [ 'aes256-gcm96', @@ -11,15 +12,23 @@ export const KEY_TYPES = [ 'ecdsa-p521', ]; export default class KeymgmtKeyModel extends Model { - @attr('string') name; - @attr('string') backend; + @attr('string', { + label: 'Key name', + subText: 'This is the name of the key that shows in Vault.', + }) + name; + + @attr('string') + backend; @attr('string', { + subText: 'The type of cryptographic key that will be created.', possibleValues: KEY_TYPES, }) type; @attr('boolean', { + label: 'Allow deletion', defaultValue: false, }) deletionAllowed; @@ -93,4 +102,27 @@ export default class KeymgmtKeyModel extends Model { { name: 'protection', type: 'string', subText: 'Where cryptographic operations are performed.' }, ]; } + + @lazyCapabilities(apiPath`${'backend'}/key/${'id'}`, 'backend', 'id') keyPath; + @lazyCapabilities(apiPath`${'backend'}/key`, 'backend') keysPath; + @lazyCapabilities(apiPath`${'backend'}/key/${'id'}/kms`, 'backend', 'id') keyProvidersPath; + + get canCreate() { + return this.keyPath.get('canCreate'); + } + get canDelete() { + return this.keyPath.get('canDelete'); + } + get canEdit() { + return this.keyPath.get('canUpdate'); + } + get canRead() { + return this.keyPath.get('canRead'); + } + get canList() { + return this.keysPath.get('canList'); + } + get canListProviders() { + return this.keyProvidersPath.get('canList'); + } } diff --git a/ui/app/models/keymgmt/provider.js b/ui/app/models/keymgmt/provider.js index 1dbdda642..58f26087e 100644 --- a/ui/app/models/keymgmt/provider.js +++ b/ui/app/models/keymgmt/provider.js @@ -2,6 +2,7 @@ import Model, { attr } from '@ember-data/model'; import { tracked } from '@glimmer/tracking'; import { expandAttributeMeta } from 'vault/utils/field-to-attrs'; import { withModelValidations } from 'vault/decorators/model-validations'; +import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; const CRED_PROPS = { azurekeyvault: ['client_id', 'client_secret', 'tenant_id'], @@ -80,7 +81,11 @@ export default class KeymgmtProviderModel extends Model { const attrs = expandAttributeMeta(this, ['name', 'created', 'keyCollection']); attrs.splice(1, 0, { hasBlock: true, label: 'Type', value: this.typeName, icon: this.icon }); const l = this.keys.length; - const value = l ? `${l} ${l > 1 ? 'keys' : 'key'}` : 'None'; + const value = l + ? `${l} ${l > 1 ? 'keys' : 'key'}` + : this.canListKeys + ? 'None' + : 'You do not have permission to list keys'; attrs.push({ hasBlock: true, isLink: l, label: 'Keys', value }); return attrs; } @@ -104,18 +109,48 @@ export default class KeymgmtProviderModel extends Model { } async fetchKeys(page) { - try { - this.keys = await this.store.lazyPaginatedQuery('keymgmt/key', { - backend: 'keymgmt', - provider: this.name, - responsePath: 'data.keys', - page, - }); - } catch (error) { - this.keys = []; - if (error.httpStatus !== 404) { - throw error; + if (this.canListKeys) { + try { + this.keys = await this.store.lazyPaginatedQuery('keymgmt/key', { + backend: 'keymgmt', + provider: this.name, + responsePath: 'data.keys', + page, + }); + } catch (error) { + this.keys = []; + if (error.httpStatus !== 404) { + throw error; + } } + } else { + this.keys = []; } } + + @lazyCapabilities(apiPath`${'backend'}/kms/${'id'}`, 'backend', 'id') providerPath; + @lazyCapabilities(apiPath`${'backend'}/kms`, 'backend') providersPath; + @lazyCapabilities(apiPath`${'backend'}/kms/${'id'}/key`, 'backend', 'id') providerKeysPath; + + get canCreate() { + return this.providerPath.get('canCreate'); + } + get canDelete() { + return this.providerPath.get('canDelete'); + } + get canEdit() { + return this.providerPath.get('canUpdate'); + } + get canRead() { + return this.providerPath.get('canRead'); + } + get canList() { + return this.providersPath.get('canList'); + } + get canListKeys() { + return this.providerKeysPath.get('canList'); + } + get canCreateKeys() { + return this.providerKeysPath.get('canCreate'); + } } diff --git a/ui/app/templates/components/keymgmt/distribute.hbs b/ui/app/templates/components/keymgmt/distribute.hbs index 16716045a..696995ed1 100644 --- a/ui/app/templates/components/keymgmt/distribute.hbs +++ b/ui/app/templates/components/keymgmt/distribute.hbs @@ -147,7 +147,12 @@ class="button is-primary" data-test-secret-save={{true}} > - Save + {{if (or (not @key) this.isNewKey) "Add key" "Distribute key"}} + + +
+
diff --git a/ui/app/templates/components/keymgmt/key-edit.hbs b/ui/app/templates/components/keymgmt/key-edit.hbs index f09dc3aa0..f25579855 100644 --- a/ui/app/templates/components/keymgmt/key-edit.hbs +++ b/ui/app/templates/components/keymgmt/key-edit.hbs @@ -4,7 +4,9 @@

- {{#if (eq @mode "create")}} + {{#if this.isDistributing}} + Distribute key + {{else if (eq @mode "create")}} Create key {{else if (eq @mode "edit")}} Edit key @@ -15,185 +17,218 @@ -{{#if (eq this.mode "show")}} -
- -
- - - - - Remove key - -
- - Rotate key - - - Edit key - -
-
-{{/if}} + Remove key + + {{/if}} + {{#if (or @model.canDelete @model.provider)}} +
+ {{/if}} + + Rotate key + + {{#if @model.canEdit}} + + Edit key + + {{/if}} + + + {{/if}} -{{#if (eq this.mode "create")}} -
- {{#each @model.createFields as |attr|}} - - {{/each}} - - -{{else if (eq this.mode "edit")}} -
- {{#each @model.updateFields as |attr|}} - - {{/each}} - - -{{else if (eq @tab "versions")}} - {{#each @model.versions as |version|}} -
-
-
- - Version {{version.id}} -
-
- {{date-from-now version.creation_time addSuffix=true}} -
-
- {{#if (eq @model.minEnabledVersion version.id)}} - - Current mininum enabled version - {{/if}} + {{#if this.isMutable}} +
+
+ {{#let (if (eq @mode "create") "createFields" "updateFields") as |fieldsKey|}} + {{#each (get @model fieldsKey) as |attr|}} + + {{/each}} +
+
+ +
+
+ + Cancel + +
+
+ {{/let}} +
+
+ {{else if (eq @tab "versions")}} + {{#each @model.versions as |version|}} +
+
+
+ + Version {{version.id}} +
+
+ {{date-from-now version.creation_time addSuffix=true}} +
+
+ {{#if (eq @model.minEnabledVersion version.id)}} + + Current mininum enabled version + {{/if}} +
-
- {{/each}} -{{else}} -
-

Key Details

- {{#each @model.showFields as |attr|}} - {{/each}} -
-
-

- Distribution Details -

- {{! TODO: Use capabilities to tell if it's not distributed vs no permissions }} - {{#if @model.provider.permissionsError}} - - {{else if @model.provider}} - - - {{@model.provider}} - - - {{#if @model.distribution}} - {{#each @model.distFields as |attr|}} - - {{/each}} - {{else}} + {{else}} +
+

Key Details

+ {{#each @model.showFields as |attr|}} + + {{/each}} +
+
+

+ Distribution Details +

+ {{#if (not @model.provider)}} + + {{#if @model.canListProviders}} + + {{/if}} + + {{else if (not @model.provider.canListKeys)}} + {{else}} + + + {{@model.provider}} + + + {{#if @model.distribution}} + {{#each @model.distFields as |attr|}} + + {{/each}} + {{else}} + + {{/if}} {{/if}} - {{else}} - - {{! TODO: Distribute link - - Distribute - }} - - {{/if}} -
+
+ {{/if}} {{/if}}

- {{#if this.isShowing}} + {{#if this.isDistributing}} + Destribute key to provider + {{else if this.isShowing}} Provider {{@model.id}} {{else}} @@ -14,177 +16,192 @@ -{{#if this.isShowing}} -
- -
- {{#unless this.viewingKeys}} - - - - - - Delete provider - - - {{#if @model.keys.length}} - -
- This provider cannot be deleted until all 20 keys distributed to it are revoked. This can be done from the - Keys tab. -
-
- {{/if}} -
-
- {{! Update once distribute route has been created }} - {{! - Distribute key - - }} - - Update credentials - -
-
- {{/unless}} +{{#if this.isDistributing}} + {{else}} -
-
- {{#if this.isCreating}} - {{#each @model.createFields as |attr index|}} - {{#if (eq index 2)}} -
-

- Provider configuration -

-
+ {{#if this.isShowing}} +
+ +
+ {{#unless this.viewingKeys}} + + + {{#if @model.canDelete}} + + + + Delete provider + + + {{#if @model.keys.length}} + +
+ This provider cannot be deleted until all + {{@model.keys.length}} + key(s) distributed to it are revoked. This can be done from the Keys tab. +
+
+ {{/if}} +
+ {{/if}} + {{#if (and @model.canDelete (or @model.canListKeys @model.canEdit))}} +
+ {{/if}} + {{#if (or @model.canListKeys @model.canCreateKeys)}} + + {{/if}} + {{#if @model.canEdit}} + + Update credentials + + {{/if}} +
+
+ {{/unless}} + {{else}} + +
+ {{#if this.isCreating}} + {{#each @model.createFields as |attr index|}} + {{#if (eq index 2)}} +
+

+ Provider configuration +

+
+ {{/if}} + + {{/each}} + {{/if}} + {{#unless this.isCreating}} +

+ New credentials +

+

+ Old credentials cannot be read and will be lost as soon as new ones are added. Do this carefully. +

+ {{/unless}} + {{#each @model.credentialFields as |cred|}} + + {{/each}} +
+
+
+ +
+
+ + Cancel + +
+
+ + {{/if}} + + {{#if this.isShowing}} +
+ {{#if this.viewingKeys}} + {{#let (options-for-backend "keymgmt" "key") as |options|}} + {{#if @model.keys.meta.total}} + {{#each @model.keys as |key|}} + + {{/each}} + {{#if (gt @model.keys.meta.lastPage 1)}} + + {{/if}} + {{else}} + + + Create key + + + {{/if}} + {{/let}} + {{else}} + {{#each @model.showFields as |attr|}} + {{#if attr.hasBlock}} + + {{#if attr.icon}} + + {{/if}} + {{#if attr.isLink}} + + {{attr.value}} + + {{else}} + {{attr.value}} + {{/if}} + + {{else}} + {{/if}} - {{/each}} {{/if}} - {{#unless this.isCreating}} -

- New credentials -

-

- Old credentials cannot be read and will be lost as soon as new ones are added. Do this carefully. -

- {{/unless}} - {{#each @model.credentialFields as |cred|}} - - {{/each}}
-
-
- -
-
- - Cancel - -
-
- -{{/if}} - -{{#if this.isShowing}} -
- {{#if this.viewingKeys}} - {{#let (options-for-backend "keymgmt" "key") as |options|}} - {{#if @model.keys.meta.total}} - {{#each @model.keys as |key|}} - - {{/each}} - {{#if (gt @model.keys.meta.lastPage 1)}} - - {{/if}} - {{else}} - - - Create key - - - {{/if}} - {{/let}} - {{else}} - {{#each @model.showFields as |attr|}} - {{#if attr.hasBlock}} - - {{#if attr.icon}} - - {{/if}} - {{#if attr.isLink}} - - {{attr.value}} - - {{else}} - {{attr.value}} - {{/if}} - - {{else}} - - {{/if}} - {{/each}} - {{/if}} -
+ {{/if}} {{/if}} \ No newline at end of file diff --git a/ui/tests/integration/components/keymgmt/distribute-test.js b/ui/tests/integration/components/keymgmt/distribute-test.js index 0220558f8..dc6b37a12 100644 --- a/ui/tests/integration/components/keymgmt/distribute-test.js +++ b/ui/tests/integration/components/keymgmt/distribute-test.js @@ -85,13 +85,10 @@ module('Integration | Component | keymgmt/distribute', function (hooks) { this.server.shutdown(); }); - test('it does not render without @backend attr', async function (assert) { - await render(hbs``); - assert.dom(SELECTORS.form).doesNotExist('Form does not exist'); - }); - test('it does not allow operation selection until valid key and provider selected', async function (assert) { - await render(hbs``); + await render( + hbs`` + ); assert.dom(SELECTORS.operationsSection).hasAttribute('disabled'); await clickTrigger(); await settled(); @@ -108,7 +105,9 @@ module('Integration | Component | keymgmt/distribute', function (hooks) { assert.dom(SELECTORS.errorProvider).exists('Shows key/provider match error on provider'); }); test('it shows key type select field if new key created', async function (assert) { - await render(hbs``); + await render( + hbs`` + ); assert.dom(SELECTORS.keyTypeSection).doesNotExist('Key Type section is not rendered by default'); // Add new item on search-select await clickTrigger(); @@ -118,7 +117,9 @@ module('Integration | Component | keymgmt/distribute', function (hooks) { assert.dom(SELECTORS.keyTypeSection).exists('Key Type selector is shown'); }); test('it hides the provider field if passed from the parent', async function (assert) { - await render(hbs``); + await render( + hbs`` + ); assert.dom(SELECTORS.providerInput).doesNotExist('Provider input is hidden'); // Select existing key await clickTrigger(); @@ -140,7 +141,9 @@ module('Integration | Component | keymgmt/distribute', function (hooks) { assert.dom(SELECTORS.errorNewKey).exists('Shows error on key type'); }); test('it hides the key field if passed from the parent', async function (assert) { - await render(hbs``); + await render( + hbs`` + ); assert.dom(SELECTORS.providerInput).exists('Provider input shown'); assert.dom(SELECTORS.keySection).doesNotExist('Key input not shown'); await select(SELECTORS.providerInput, 'provider-azure'); diff --git a/ui/tests/integration/components/keymgmt/key-edit-test.js b/ui/tests/integration/components/keymgmt/key-edit-test.js index bb809bef4..b68b4ec1e 100644 --- a/ui/tests/integration/components/keymgmt/key-edit-test.js +++ b/ui/tests/integration/components/keymgmt/key-edit-test.js @@ -23,6 +23,7 @@ module('Integration | Component | keymgmt/key-edit', function (hooks) { creation_time: now, }, ], + canDelete: true, }); this.model = model; this.tab = ''; diff --git a/ui/tests/integration/components/keymgmt/provider-edit-test.js b/ui/tests/integration/components/keymgmt/provider-edit-test.js index 2a5f94041..f8638ea1d 100644 --- a/ui/tests/integration/components/keymgmt/provider-edit-test.js +++ b/ui/tests/integration/components/keymgmt/provider-edit-test.js @@ -47,6 +47,12 @@ module('Integration | Component | keymgmt/provider-edit', function (hooks) { test('it should render show view', async function (assert) { assert.expect(13); + // override capability getters + Object.defineProperties(this.model, { + canDelete: { value: true }, + canListKeys: { value: true }, + }); + this.server.get('/keymgmt/kms/foo-bar/key', () => { return { data: {