From a89f8d52288f1d5ffae5430cf06a7d4e395e5d48 Mon Sep 17 00:00:00 2001 From: Mark Lewis Date: Fri, 20 Sep 2024 02:28:50 +0100 Subject: [PATCH 1/7] Update index.mdx (#28350) Typo Co-authored-by: Sarah Chavis <62406755+schavis@users.noreply.github.com> --- website/content/docs/agent-and-proxy/agent/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/agent-and-proxy/agent/index.mdx b/website/content/docs/agent-and-proxy/agent/index.mdx index eb77ea534059..48855f7ebe83 100644 --- a/website/content/docs/agent-and-proxy/agent/index.mdx +++ b/website/content/docs/agent-and-proxy/agent/index.mdx @@ -298,7 +298,7 @@ runtime metrics about its performance, the auto-auth and the cache status: | `vault.agent.cache.hit` | Number of cache hits | counter | | `vault.agent.cache.miss` | Number of cache misses | counter | -## Start Vault agent +## Start Vault Agent To run Vault Agent: From e848f1683dbc50130cbfa2395839bf4c6b271e64 Mon Sep 17 00:00:00 2001 From: Owen Zhang <86668876+owenzorrin@users.noreply.github.com> Date: Thu, 19 Sep 2024 18:28:58 -0700 Subject: [PATCH 2/7] Update namespaces.mdx (#28314) Add "@include 'alerts/enterprise-only.mdx'" since namespace is an enterprise feature Co-authored-by: Sarah Chavis <62406755+schavis@users.noreply.github.com> --- website/content/api-docs/system/namespaces.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/content/api-docs/system/namespaces.mdx b/website/content/api-docs/system/namespaces.mdx index 84fc6d5d1a83..d98255b0c39a 100644 --- a/website/content/api-docs/system/namespaces.mdx +++ b/website/content/api-docs/system/namespaces.mdx @@ -6,6 +6,8 @@ description: The `/sys/namespaces` endpoint is used manage namespaces in Vault. # `/sys/namespaces` +@include 'alerts/enterprise-only.mdx' + The `/sys/namespaces` endpoint is used manage namespaces in Vault. ## List namespaces From 2ce68778e4af8619b5a0ddba4473fe316debcb5e Mon Sep 17 00:00:00 2001 From: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:44:29 -0500 Subject: [PATCH 3/7] UI: Fix LDAP Mirage Handler (#28432) * update ldap mirage scenario to allow check-in/check-out action * update libraries test to mount engine * update mirage, fix tests * update lease renew CLI command * fix test * update tests --- .../page/library/details/accounts.ts | 2 +- ui/mirage/handlers/ldap.js | 71 +++++++++++++++---- ui/mirage/models/ldap-account-status.js | 13 ++++ ui/mirage/scenarios/ldap.js | 13 +++- .../secrets/backend/ldap/libraries-test.js | 39 ++++++++-- .../secrets/backend/ldap/overview-test.js | 62 +++++++++++----- .../secrets/backend/ldap/roles-test.js | 36 +++++++--- ui/tests/helpers/ldap/ldap-helpers.js | 10 +-- .../page/library/details/accounts-test.js | 5 +- 9 files changed, 195 insertions(+), 56 deletions(-) create mode 100644 ui/mirage/models/ldap-account-status.js diff --git a/ui/lib/ldap/addon/components/page/library/details/accounts.ts b/ui/lib/ldap/addon/components/page/library/details/accounts.ts index 6e039f56893b..7e119375dbc0 100644 --- a/ui/lib/ldap/addon/components/page/library/details/accounts.ts +++ b/ui/lib/ldap/addon/components/page/library/details/accounts.ts @@ -27,7 +27,7 @@ export default class LdapLibraryDetailsAccountsPageComponent extends Component new Response(204)); - server.get('/sys/internal/ui/mounts/:path', () => ({ - data: { - accessor: 'ldap_ade94329', - type: 'ldap', - path: 'ldap-test/', - uuid: '35e9119d-5708-4b6b-58d2-f913e27f242d', - config: {}, - }, - })); // config server.post('/:backend/config', (schema, req) => createOrUpdateRecord(schema, req, 'ldapConfigs')); server.get('/:backend/config', (schema, req) => getRecord(schema, req, 'ldapConfigs')); @@ -67,8 +56,60 @@ export default function (server) { server.post('/:backend/library/:name', (schema, req) => createOrUpdateRecord(schema, req, 'ldapLibraries')); server.get('/:backend/library/:name', (schema, req) => getRecord(schema, req, 'ldapLibraries')); server.get('/:backend/library', (schema) => listRecords(schema, 'ldapLibraries')); - server.get('/:backend/library/:name/status', () => ({ - 'bob.johnson': { available: false, borrower_client_token: '8b80c305eb3a7dbd161ef98f10ea60a116ce0910' }, - 'mary.smith': { available: true }, - })); + server.get('/:backend/library/:name/status', (schema) => { + const data = schema.db['ldapAccountStatuses'].reduce((prev, curr) => { + prev[curr.account] = { + available: curr.available, + borrower_client_token: curr.borrower_client_token, + }; + return prev; + }, {}); + return { data }; + }); + // check-out / check-in + server.post('/:backend/library/:set_name/check-in', (schema, req) => { + // Check-in makes an unavailable account available again + const { service_account_names } = JSON.parse(req.requestBody); + const dbCollection = schema.db['ldapAccountStatuses']; + const updated = dbCollection.find(service_account_names).map((f) => ({ + ...f, + available: true, + borrower_client_token: undefined, + })); + updated.forEach((u) => { + dbCollection.update(u.id, u); + }); + return { + data: { + check_ins: service_account_names, + }, + }; + }); + server.post('/:backend/library/:set_name/check-out', (schema, req) => { + const { set_name, backend } = req.params; + const dbCollection = schema.db['ldapAccountStatuses']; + const available = dbCollection.where({ available: true }); + if (available) { + return Response(404, {}, { errors: ['no accounts available to check out'] }); + } + const checkOut = { + ...available[0], + available: false, + borrower_client_token: crypto.randomUUID(), + }; + dbCollection.update(checkOut.id, checkOut); + return { + request_id: '364a17d4-e5ab-998b-ceee-b49929229e0c', + lease_id: `${backend}/library/${set_name}/check-out/aoBsaBEI4PK96VnukubvYDlZ`, + renewable: true, + lease_duration: 36000, + data: { + password: crypto.randomUUID(), + service_account_name: checkOut.account, + }, + wrap_info: null, + warnings: null, + auth: null, + }; + }); } diff --git a/ui/mirage/models/ldap-account-status.js b/ui/mirage/models/ldap-account-status.js new file mode 100644 index 000000000000..557261f6cd78 --- /dev/null +++ b/ui/mirage/models/ldap-account-status.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { Model } from 'miragejs'; + +export default Model.extend({ + account: '', // should match ID + library: '', + available: false, + borrower_client_token: undefined, +}); diff --git a/ui/mirage/scenarios/ldap.js b/ui/mirage/scenarios/ldap.js index 19b6b61f4c58..d946dc68529d 100644 --- a/ui/mirage/scenarios/ldap.js +++ b/ui/mirage/scenarios/ldap.js @@ -4,8 +4,19 @@ */ export default function (server) { - server.create('ldap-config', { path: 'kubernetes' }); + server.create('ldap-config', { path: 'kubernetes', backend: 'ldap-test' }); server.create('ldap-role', 'static', { name: 'static-role' }); server.create('ldap-role', 'dynamic', { name: 'dynamic-role' }); server.create('ldap-library', { name: 'test-library' }); + server.create('ldap-account-status', { + id: 'bob.johnson', + account: 'bob.johnson', + available: false, + borrower_client_token: '8b80c305eb3a7dbd161ef98f10ea60a116ce0910', + }); + server.create('ldap-account-status', { + id: 'mary.smith', + account: 'mary.smith', + available: true, + }); } diff --git a/ui/tests/acceptance/secrets/backend/ldap/libraries-test.js b/ui/tests/acceptance/secrets/backend/ldap/libraries-test.js index dd3d0e0725a6..4e9cc5a57059 100644 --- a/ui/tests/acceptance/secrets/backend/ldap/libraries-test.js +++ b/ui/tests/acceptance/secrets/backend/ldap/libraries-test.js @@ -6,11 +6,13 @@ import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; +import { v4 as uuidv4 } from 'uuid'; import ldapMirageScenario from 'vault/mirage/scenarios/ldap'; import ldapHandlers from 'vault/mirage/handlers/ldap'; import authPage from 'vault/tests/pages/auth'; import { click } from '@ember/test-helpers'; import { isURL, visitURL } from 'vault/tests/helpers/ldap/ldap-helpers'; +import { deleteEngineCmd, mountEngineCmd, runCmd } from 'vault/tests/helpers/commands'; module('Acceptance | ldap | libraries', function (hooks) { setupApplicationTest(hooks); @@ -19,21 +21,41 @@ module('Acceptance | ldap | libraries', function (hooks) { hooks.beforeEach(async function () { ldapHandlers(this.server); ldapMirageScenario(this.server); + this.backend = `ldap-test-${uuidv4()}`; await authPage.login(); - return visitURL('libraries'); + // mount & configure + await runCmd([ + mountEngineCmd('ldap', this.backend), + `write ${this.backend}/config binddn=foo bindpass=bar url=http://localhost:8208`, + ]); + return visitURL('libraries', this.backend); + }); + + hooks.afterEach(async function () { + await runCmd(deleteEngineCmd(this.backend)); + }); + + test('it should show libraries on overview page', async function (assert) { + await visitURL('overview', this.backend); + assert.dom('[data-test-libraries-count]').hasText('1'); }); test('it should transition to create library route on toolbar link click', async function (assert) { await click('[data-test-toolbar-action="library"]'); - assert.true(isURL('libraries/create'), 'Transitions to library create route on toolbar link click'); + assert.true( + isURL('libraries/create', this.backend), + 'Transitions to library create route on toolbar link click' + ); }); test('it should transition to library details route on list item click', async function (assert) { await click('[data-test-list-item-link] a'); assert.true( - isURL('libraries/test-library/details/accounts'), + isURL('libraries/test-library/details/accounts', this.backend), 'Transitions to library details accounts route on list item click' ); + assert.dom('[data-test-account-name]').exists({ count: 2 }, 'lists the accounts'); + assert.dom('[data-test-checked-out-account]').exists({ count: 1 }, 'lists the checked out accounts'); }); test('it should transition to routes from list item action menu', async function (assert) { @@ -44,7 +66,7 @@ module('Acceptance | ldap | libraries', function (hooks) { await click(`[data-test-${action}]`); const uri = action === 'details' ? 'details/accounts' : action; assert.true( - isURL(`libraries/test-library/${uri}`), + isURL(`libraries/test-library/${uri}`, this.backend), `Transitions to ${action} route on list item action menu click` ); await click('[data-test-breadcrumb="libraries"] a'); @@ -55,13 +77,13 @@ module('Acceptance | ldap | libraries', function (hooks) { await click('[data-test-list-item-link] a'); await click('[data-test-tab="config"]'); assert.true( - isURL('libraries/test-library/details/configuration'), + isURL('libraries/test-library/details/configuration', this.backend), 'Transitions to configuration route on tab click' ); await click('[data-test-tab="accounts"]'); assert.true( - isURL('libraries/test-library/details/accounts'), + isURL('libraries/test-library/details/accounts', this.backend), 'Transitions to accounts route on tab click' ); }); @@ -69,6 +91,9 @@ module('Acceptance | ldap | libraries', function (hooks) { test('it should transition to routes from library details toolbar links', async function (assert) { await click('[data-test-list-item-link] a'); await click('[data-test-edit]'); - assert.true(isURL('libraries/test-library/edit'), 'Transitions to credentials route from toolbar link'); + assert.true( + isURL('libraries/test-library/edit', this.backend), + 'Transitions to credentials route from toolbar link' + ); }); }); diff --git a/ui/tests/acceptance/secrets/backend/ldap/overview-test.js b/ui/tests/acceptance/secrets/backend/ldap/overview-test.js index 8fe90cccf2e1..1490e40e00f3 100644 --- a/ui/tests/acceptance/secrets/backend/ldap/overview-test.js +++ b/ui/tests/acceptance/secrets/backend/ldap/overview-test.js @@ -6,12 +6,14 @@ import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; +import { v4 as uuidv4 } from 'uuid'; import ldapMirageScenario from 'vault/mirage/scenarios/ldap'; import ldapHandlers from 'vault/mirage/handlers/ldap'; import authPage from 'vault/tests/pages/auth'; import { click, fillIn, visit } from '@ember/test-helpers'; import { selectChoose } from 'ember-power-select/test-support'; import { isURL, visitURL } from 'vault/tests/helpers/ldap/ldap-helpers'; +import { deleteEngineCmd, mountEngineCmd, runCmd } from 'vault/tests/helpers/commands'; module('Acceptance | ldap | overview', function (hooks) { setupApplicationTest(hooks); @@ -19,77 +21,101 @@ module('Acceptance | ldap | overview', function (hooks) { hooks.beforeEach(async function () { ldapHandlers(this.server); + this.backend = `ldap-test-${uuidv4()}`; + this.mountAndConfig = (backend) => { + return runCmd([ + mountEngineCmd('ldap', backend), + `write ${backend}/config binddn=foo bindpass=bar url=http://localhost:8208`, + ]); + }; return authPage.login(); }); test('it should transition to ldap overview on mount success', async function (assert) { + const backend = 'ldap-test-mount'; await visit('/vault/secrets'); await click('[data-test-enable-engine]'); await click('[data-test-mount-type="ldap"]'); - await fillIn('[data-test-input="path"]', 'ldap-test'); + await fillIn('[data-test-input="path"]', backend); await click('[data-test-mount-submit]'); - assert.true(isURL('overview'), 'Transitions to ldap overview route on mount success'); + assert.true(isURL('overview', backend), 'Transitions to ldap overview route on mount success'); + assert.dom('[data-test-header-title]').hasText(backend); + // cleanup mounted engine + await visit('/vault/secrets'); + await runCmd(deleteEngineCmd(backend)); }); test('it should transition to routes on tab link click', async function (assert) { assert.expect(4); + await this.mountAndConfig(this.backend); - await visitURL('overview'); + await visitURL('overview', this.backend); for (const tab of ['roles', 'libraries', 'config', 'overview']) { await click(`[data-test-tab="${tab}"]`); const route = tab === 'config' ? 'configuration' : tab; - assert.true(isURL(route), `Transitions to ${route} route on tab link click`); + assert.true(isURL(route, this.backend), `Transitions to ${route} route on tab link click`); } }); test('it should transition to configuration route when engine is not configured', async function (assert) { - await visitURL('overview'); + await runCmd(mountEngineCmd('ldap', this.backend)); + await visitURL('overview', this.backend); await click('[data-test-config-cta] a'); - assert.true(isURL('configure'), 'Transitions to configure route on cta link click'); + assert.true(isURL('configure', this.backend), 'Transitions to configure route on cta link click'); - await click('[data-test-breadcrumb="ldap-test"] a'); + await click(`[data-test-breadcrumb="${this.backend}"] a`); await click('[data-test-toolbar-action="config"]'); - assert.true(isURL('configure'), 'Transitions to configure route on toolbar link click'); + assert.true(isURL('configure', this.backend), 'Transitions to configure route on toolbar link click'); }); // including a test for the configuration route here since it is the only one needed test('it should transition to configuration edit on toolbar link click', async function (assert) { ldapMirageScenario(this.server); - await visitURL('overview'); + await this.mountAndConfig(this.backend); + await visitURL('overview', this.backend); await click('[data-test-tab="config"]'); await click('[data-test-toolbar-config-action]'); - assert.true(isURL('configure'), 'Transitions to configure route on toolbar link click'); + assert.true(isURL('configure', this.backend), 'Transitions to configure route on toolbar link click'); }); test('it should transition to create role route on card action link click', async function (assert) { ldapMirageScenario(this.server); - await visitURL('overview'); + await this.mountAndConfig(this.backend); + await visitURL('overview', this.backend); await click('[data-test-overview-card="Roles"] a'); - assert.true(isURL('roles/create'), 'Transitions to role create route on card action link click'); + assert.true( + isURL('roles/create', this.backend), + 'Transitions to role create route on card action link click' + ); }); test('it should transition to create library route on card action link click', async function (assert) { ldapMirageScenario(this.server); - await visitURL('overview'); + await this.mountAndConfig(this.backend); + await visitURL('overview', this.backend); await click('[data-test-overview-card="Libraries"] a'); - assert.true(isURL('libraries/create'), 'Transitions to library create route on card action link click'); + assert.true( + isURL('libraries/create', this.backend), + 'Transitions to library create route on card action link click' + ); }); test('it should transition to role credentials route on generate credentials action', async function (assert) { ldapMirageScenario(this.server); - await visitURL('overview'); + await this.mountAndConfig(this.backend); + await visitURL('overview', this.backend); await selectChoose('.search-select', 'static-role'); await click('[data-test-generate-credential-button]'); assert.true( - isURL('roles/static/static-role/credentials'), + isURL('roles/static/static-role/credentials', this.backend), 'Transitions to role credentials route on generate credentials action' ); - await click('[data-test-breadcrumb="ldap-test"] a'); + await click(`[data-test-breadcrumb="${this.backend}"] a`); await selectChoose('.search-select', 'dynamic-role'); await click('[data-test-generate-credential-button]'); assert.true( - isURL('roles/dynamic/dynamic-role/credentials'), + isURL('roles/dynamic/dynamic-role/credentials', this.backend), 'Transitions to role credentials route on generate credentials action' ); }); diff --git a/ui/tests/acceptance/secrets/backend/ldap/roles-test.js b/ui/tests/acceptance/secrets/backend/ldap/roles-test.js index 15d1df74df2a..739b604dfc33 100644 --- a/ui/tests/acceptance/secrets/backend/ldap/roles-test.js +++ b/ui/tests/acceptance/secrets/backend/ldap/roles-test.js @@ -6,11 +6,14 @@ import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; +import { v4 as uuidv4 } from 'uuid'; import ldapMirageScenario from 'vault/mirage/scenarios/ldap'; import ldapHandlers from 'vault/mirage/handlers/ldap'; import authPage from 'vault/tests/pages/auth'; -import { click, fillIn } from '@ember/test-helpers'; +import { click, fillIn, waitFor } from '@ember/test-helpers'; import { isURL, visitURL } from 'vault/tests/helpers/ldap/ldap-helpers'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; +import { deleteEngineCmd, mountEngineCmd, runCmd } from 'vault/tests/helpers/commands'; module('Acceptance | ldap | roles', function (hooks) { setupApplicationTest(hooks); @@ -19,26 +22,39 @@ module('Acceptance | ldap | roles', function (hooks) { hooks.beforeEach(async function () { ldapHandlers(this.server); ldapMirageScenario(this.server); + this.backend = `ldap-test-${uuidv4()}`; await authPage.login(); - return visitURL('roles'); + // mount & configure + await runCmd([ + mountEngineCmd('ldap', this.backend), + `write ${this.backend}/config binddn=foo bindpass=bar url=http://localhost:8208`, + ]); + return visitURL('roles', this.backend); + }); + + hooks.afterEach(async function () { + await runCmd(deleteEngineCmd(this.backend)); }); test('it should transition to create role route on toolbar link click', async function (assert) { await click('[data-test-toolbar-action="role"]'); - assert.true(isURL('roles/create'), 'Transitions to role create route on toolbar link click'); + assert.true( + isURL('roles/create', this.backend), + 'Transitions to role create route on toolbar link click' + ); }); test('it should transition to role details route on list item click', async function (assert) { await click('[data-test-list-item-link]:nth-of-type(1) a'); assert.true( - isURL('roles/dynamic/dynamic-role/details'), + isURL('roles/dynamic/dynamic-role/details', this.backend), 'Transitions to role details route on list item click' ); await click('[data-test-breadcrumb="roles"] a'); await click('[data-test-list-item-link]:nth-of-type(2) a'); assert.true( - isURL('roles/static/static-role/details'), + isURL('roles/static/static-role/details', this.backend), 'Transitions to role details route on list item click' ); }); @@ -51,7 +67,7 @@ module('Acceptance | ldap | roles', function (hooks) { await click(`[data-test-${action}]`); const uri = action === 'get-creds' ? 'credentials' : action; assert.true( - isURL(`roles/dynamic/dynamic-role/${uri}`), + isURL(`roles/dynamic/dynamic-role/${uri}`, this.backend), `Transitions to ${uri} route on list item action menu click` ); await click('[data-test-breadcrumb="roles"] a'); @@ -62,13 +78,16 @@ module('Acceptance | ldap | roles', function (hooks) { await click('[data-test-list-item-link]:nth-of-type(1) a'); await click('[data-test-get-credentials]'); assert.true( - isURL('roles/dynamic/dynamic-role/credentials'), + isURL('roles/dynamic/dynamic-role/credentials', this.backend), 'Transitions to credentials route from toolbar link' ); await click('[data-test-breadcrumb="dynamic-role"] a'); await click('[data-test-edit]'); - assert.true(isURL('roles/dynamic/dynamic-role/edit'), 'Transitions to edit route from toolbar link'); + assert.true( + isURL('roles/dynamic/dynamic-role/edit', this.backend), + 'Transitions to edit route from toolbar link' + ); }); test('it should clear roles page filter value on route exit', async function (assert) { @@ -76,6 +95,7 @@ module('Acceptance | ldap | roles', function (hooks) { assert .dom('[data-test-filter-input]') .hasValue('foo', 'Roles page filter value set after model refresh and rerender'); + await waitFor(GENERAL.emptyStateTitle); await click('[data-test-tab="libraries"]'); await click('[data-test-tab="roles"]'); assert.dom('[data-test-filter-input]').hasNoValue('Roles page filter value cleared on route exit'); diff --git a/ui/tests/helpers/ldap/ldap-helpers.js b/ui/tests/helpers/ldap/ldap-helpers.js index a08e02275361..b184878a0ff6 100644 --- a/ui/tests/helpers/ldap/ldap-helpers.js +++ b/ui/tests/helpers/ldap/ldap-helpers.js @@ -28,13 +28,13 @@ export const generateBreadcrumbs = (backend, childRoute) => { return breadcrumbs; }; -const baseURL = '/vault/secrets/ldap-test/ldap/'; +const baseURL = (backend) => `/vault/secrets/${backend}/ldap/`; const stripLeadingSlash = (uri) => (uri.charAt(0) === '/' ? uri.slice(1) : uri); -export const isURL = (uri) => { - return currentURL() === `${baseURL}${stripLeadingSlash(uri)}`; +export const isURL = (uri, backend = 'ldap-test') => { + return currentURL() === `${baseURL(backend)}${stripLeadingSlash(uri)}`; }; -export const visitURL = (uri) => { - return visit(`${baseURL}${stripLeadingSlash(uri)}`); +export const visitURL = (uri, backend = 'ldap-test') => { + return visit(`${baseURL(backend)}${stripLeadingSlash(uri)}`); }; diff --git a/ui/tests/integration/components/ldap/page/library/details/accounts-test.js b/ui/tests/integration/components/ldap/page/library/details/accounts-test.js index 94b0048cc512..8bb1ab696921 100644 --- a/ui/tests/integration/components/ldap/page/library/details/accounts-test.js +++ b/ui/tests/integration/components/ldap/page/library/details/accounts-test.js @@ -77,6 +77,9 @@ module('Integration | Component | ldap | Page::Library::Details::Accounts', func assert .dom('[data-test-accounts-code-block] code') - .hasText('vault lease renew ad/library/test-library/check-out/:lease_id', 'Renew cli command renders'); + .hasText( + 'vault lease renew ldap-test/library/test-library/check-out/:lease_id', + 'Renew cli command renders with backend path' + ); }); }); From 520f1416082b86ae4ee93b5048dc20ba0aa18543 Mon Sep 17 00:00:00 2001 From: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:52:59 -0500 Subject: [PATCH 4/7] UI: refactor KMIP role model (#28418) * update kmip/role model and adapter * New KMIP role form component * cleanup on kmip role adapter/model * fix role details view * update tests to check for kmip role form and details validity * cleanup * Add kmip-role-fields test * add headers, remove old component * Address PR comments --- ui/app/adapters/kmip/role.js | 33 +++--- ui/app/models/kmip/role.js | 75 +++++------- ui/app/styles/components/kmip-role-edit.scss | 5 +- ui/app/utils/kmip-role-fields.js | 27 +++++ .../addon/components/edit-form-kmip-role.js | 55 --------- .../kmip/addon/components/kmip/role-form.hbs | 82 ++++++++++++++ .../kmip/addon/components/kmip/role-form.js | 107 ++++++++++++++++++ .../kmip/addon/routes/scope/roles/create.js | 1 + .../components/edit-form-kmip-role.hbs | 107 ------------------ ui/lib/kmip/addon/templates/role/edit.hbs | 5 +- .../addon/templates/scope/roles/create.hbs | 6 +- ui/lib/kmip/index.js | 4 + ui/tests/acceptance/enterprise-kmip-test.js | 46 ++++---- ui/tests/pages/secrets/backend/kmip/roles.js | 2 +- ui/tests/unit/adapters/kmip/role-test.js | 10 +- ui/tests/unit/utils/kmip-role-fields-test.js | 52 +++++++++ 16 files changed, 360 insertions(+), 257 deletions(-) create mode 100644 ui/app/utils/kmip-role-fields.js delete mode 100644 ui/lib/kmip/addon/components/edit-form-kmip-role.js create mode 100644 ui/lib/kmip/addon/components/kmip/role-form.hbs create mode 100644 ui/lib/kmip/addon/components/kmip/role-form.js delete mode 100644 ui/lib/kmip/addon/templates/components/edit-form-kmip-role.hbs create mode 100644 ui/tests/unit/utils/kmip-role-fields-test.js diff --git a/ui/app/adapters/kmip/role.js b/ui/app/adapters/kmip/role.js index 9779ad67a564..693ae6327e11 100644 --- a/ui/app/adapters/kmip/role.js +++ b/ui/app/adapters/kmip/role.js @@ -6,10 +6,11 @@ import BaseAdapter from './base'; import { decamelize } from '@ember/string'; import { getProperties } from '@ember/object'; +import { nonOperationFields } from 'vault/utils/kmip-role-fields'; export default BaseAdapter.extend({ createRecord(store, type, snapshot) { - const name = snapshot.id || snapshot.attr('name'); + const name = snapshot.id || snapshot.record.role; const url = this._url( type.modelName, { @@ -18,10 +19,11 @@ export default BaseAdapter.extend({ }, name ); - return this.ajax(url, 'POST', { data: this.serialize(snapshot) }).then(() => { + const data = this.serialize(snapshot); + return this.ajax(url, 'POST', { data }).then(() => { return { id: name, - name, + role: name, backend: snapshot.record.backend, scope: snapshot.record.scope, }; @@ -29,7 +31,8 @@ export default BaseAdapter.extend({ }, deleteRecord(store, type, snapshot) { - const name = snapshot.id || snapshot.attr('name'); + // records must always have IDs + const name = snapshot.id; const url = this._url( type.modelName, { @@ -41,35 +44,35 @@ export default BaseAdapter.extend({ return this.ajax(url, 'DELETE'); }, + updateRecord() { + return this.createRecord(...arguments); + }, + serialize(snapshot) { // the endpoint here won't allow sending `operation_all` and `operation_none` at the same time or with // other operation_ values, so we manually check for them and send an abbreviated object const json = snapshot.serialize(); - const keys = snapshot.record.nonOperationFields.map(decamelize); - const nonOperationFields = getProperties(json, keys); - for (const field in nonOperationFields) { - if (nonOperationFields[field] == null) { - delete nonOperationFields[field]; + const keys = nonOperationFields(snapshot.record.editableFields).map(decamelize); + const nonOp = getProperties(json, keys); + for (const field in nonOp) { + if (nonOp[field] == null) { + delete nonOp[field]; } } if (json.operation_all) { return { operation_all: true, - ...nonOperationFields, + ...nonOp, }; } if (json.operation_none) { return { operation_none: true, - ...nonOperationFields, + ...nonOp, }; } delete json.operation_none; delete json.operation_all; return json; }, - - updateRecord() { - return this.createRecord(...arguments); - }, }); diff --git a/ui/app/models/kmip/role.js b/ui/app/models/kmip/role.js index 8083acd7b331..520efcf1b517 100644 --- a/ui/app/models/kmip/role.js +++ b/ui/app/models/kmip/role.js @@ -4,52 +4,35 @@ */ import Model, { attr } from '@ember-data/model'; -import { computed } from '@ember/object'; -import fieldToAttrs, { expandAttributeMeta } from 'vault/utils/field-to-attrs'; import apiPath from 'vault/utils/api-path'; import lazyCapabilities from 'vault/macros/lazy-capabilities'; +import { withExpandedAttributes } from 'vault/decorators/model-expanded-attributes'; +import { operationFields, operationFieldsWithoutSpecial, tlsFields } from 'vault/utils/kmip-role-fields'; import { removeManyFromArray } from 'vault/helpers/remove-from-array'; -const COMPUTEDS = { - operationFields: computed('newFields', function () { - return this.newFields.filter((key) => key.startsWith('operation')); - }), +@withExpandedAttributes() +export default class KmipRoleModel extends Model { + @attr({ readOnly: true }) backend; + @attr({ readOnly: true }) scope; - operationFieldsWithoutSpecial: computed('operationFields', function () { - return removeManyFromArray(this.operationFields, ['operationAll', 'operationNone']); - }), + get editableFields() { + return Object.keys(this.allByKey).filter((k) => !['backend', 'scope', 'role'].includes(k)); + } - tlsFields: computed(function () { - return ['tlsClientKeyBits', 'tlsClientKeyType', 'tlsClientTtl']; - }), - - // For rendering on the create/edit pages - defaultFields: computed('newFields', 'operationFields', 'tlsFields', function () { - const excludeFields = ['role'].concat(this.operationFields, this.tlsFields); - return removeManyFromArray(this.newFields, excludeFields); - }), - - // For adapter/serializer - nonOperationFields: computed('newFields', 'operationFields', function () { - return removeManyFromArray(this.newFields, this.operationFields); - }), -}; - -export default Model.extend(COMPUTEDS, { - backend: attr({ readOnly: true }), - scope: attr({ readOnly: true }), - name: attr({ readOnly: true }), - - fieldGroups: computed('fields', 'defaultFields.length', 'tlsFields', function () { - const groups = [{ TLS: this.tlsFields }]; - if (this.defaultFields.length) { - groups.unshift({ default: this.defaultFields }); + get fieldGroups() { + const tls = tlsFields(); + const groups = [{ TLS: tls }]; + // op fields are shown in OperationFieldDisplay + const opFields = operationFields(this.editableFields); + // not op fields, tls fields, or role/backend/scope + const defaultFields = this.editableFields.filter((f) => ![...opFields, ...tls].includes(f)); + if (defaultFields.length) { + groups.unshift({ default: defaultFields }); } - const ret = fieldToAttrs(this, groups); - return ret; - }), + return this._expandGroups(groups); + } - operationFormFields: computed('operationFieldsWithoutSpecial', function () { + get operationFormFields() { const objects = [ 'operationCreate', 'operationActivate', @@ -62,7 +45,7 @@ export default Model.extend(COMPUTEDS, { const attributes = ['operationAddAttribute', 'operationGetAttributes']; const server = ['operationDiscoverVersions']; - const others = removeManyFromArray(this.operationFieldsWithoutSpecial, [ + const others = removeManyFromArray(operationFieldsWithoutSpecial(this.editableFields), [ ...objects, ...attributes, ...server, @@ -77,14 +60,8 @@ export default Model.extend(COMPUTEDS, { Other: others, }); } - return fieldToAttrs(this, groups); - }), - tlsFormFields: computed('tlsFields', function () { - return expandAttributeMeta(this, this.tlsFields); - }), - fields: computed('defaultFields', function () { - return expandAttributeMeta(this, this.defaultFields); - }), + return this._expandGroups(groups); + } - updatePath: lazyCapabilities(apiPath`${'backend'}/scope/${'scope'}/role/${'id'}`, 'backend', 'scope', 'id'), -}); + @lazyCapabilities(apiPath`${'backend'}/scope/${'scope'}/role/${'id'}`, 'backend', 'scope', 'id') updatePath; +} diff --git a/ui/app/styles/components/kmip-role-edit.scss b/ui/app/styles/components/kmip-role-edit.scss index a517c1a54313..f4fc884be41f 100644 --- a/ui/app/styles/components/kmip-role-edit.scss +++ b/ui/app/styles/components/kmip-role-edit.scss @@ -3,11 +3,14 @@ * SPDX-License-Identifier: BUSL-1.1 */ +.kmip-role-operations { + column-count: 2; +} .kmip-role-allowed-operations { @extend .box; flex: 1 1 auto; box-shadow: none; - padding: 0; + padding: $spacing-4 0; } .kmip-role-allowed-operations .field { margin-bottom: $spacing-4; diff --git a/ui/app/utils/kmip-role-fields.js b/ui/app/utils/kmip-role-fields.js new file mode 100644 index 000000000000..3503ad58f449 --- /dev/null +++ b/ui/app/utils/kmip-role-fields.js @@ -0,0 +1,27 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { removeManyFromArray } from 'vault/helpers/remove-from-array'; + +export const operationFields = (fieldNames) => { + if (!Array.isArray(fieldNames)) { + throw new Error('fieldNames must be an array'); + } + return fieldNames.filter((key) => key.startsWith('operation')); +}; + +export const operationFieldsWithoutSpecial = (fieldNames) => { + const opFields = operationFields(fieldNames); + return removeManyFromArray(opFields, ['operationAll', 'operationNone']); +}; + +export const nonOperationFields = (fieldNames) => { + const opFields = operationFields(fieldNames); + return removeManyFromArray(fieldNames, opFields); +}; + +export const tlsFields = () => { + return ['tlsClientKeyBits', 'tlsClientKeyType', 'tlsClientTtl']; +}; diff --git a/ui/lib/kmip/addon/components/edit-form-kmip-role.js b/ui/lib/kmip/addon/components/edit-form-kmip-role.js deleted file mode 100644 index 43a230b80b19..000000000000 --- a/ui/lib/kmip/addon/components/edit-form-kmip-role.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import EditForm from 'core/components/edit-form'; -import { computed } from '@ember/object'; -import layout from '../templates/components/edit-form-kmip-role'; - -export default EditForm.extend({ - layout, - model: null, - - cancelLink: computed('cancelLinkParams.[]', function () { - if (!Array.isArray(this.cancelLinkParams) || !this.cancelLinkParams.length) return; - const [route, ...models] = this.cancelLinkParams; - return { route, models }; - }), - - init() { - this._super(...arguments); - - if (this.model.isNew) { - this.model.operationAll = true; - } - }, - - actions: { - toggleOperationSpecial(checked) { - this.model.operationNone = !checked; - this.model.operationAll = checked; - }, - - // when operationAll is true, we want all of the items - // to appear checked, but we don't want to override what items - // a user has selected - so this action creates an object that we - // pass to the FormField component as the model instead of the real model - placeholderOrModel(isOperationAll, attr) { - return isOperationAll ? { [attr.name]: true } : this.model; - }, - - preSave(model) { - // if we have operationAll or operationNone, we want to clear - // out the others so that display shows the right data - if (model.operationAll || model.operationNone) { - model.operationFieldsWithoutSpecial.forEach((field) => model.set(field, null)); - } - // set operationNone if user unchecks 'operationAll' instead of toggling the 'operationNone' input - // doing here instead of on the 'operationNone' input because a user might deselect all, then reselect some options - // and immediately setting operationNone will hide all of the checkboxes in the UI - this.model.operationNone = - model.operationFieldsWithoutSpecial.every((attr) => !model[attr]) && !this.model.operationAll; - }, - }, -}); diff --git a/ui/lib/kmip/addon/components/kmip/role-form.hbs b/ui/lib/kmip/addon/components/kmip/role-form.hbs new file mode 100644 index 000000000000..d10fb57add42 --- /dev/null +++ b/ui/lib/kmip/addon/components/kmip/role-form.hbs @@ -0,0 +1,82 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 +~}} + +
+ +
+ + {{#if @model.isNew}} + {{! Show role name only in create mode }} + + {{/if}} +
+ + +
+ {{#unless @model.operationNone}} + +

+ Allowed Operations +

+
+
+ +
+
+ {{#each this.operationFormGroups as |group|}} +
+

{{group.name}}

+ {{#each group.fields as |attr|}} + + {{/each}} +
+ {{/each}} +
+
+ {{/unless}} +
+

+ TLS +

+ {{#each this.tlsFormFields as |attr|}} + + {{/each}} +
+ {{#each @model.fields as |attr|}} + + {{/each}} +
+ +
+ + + + +
+ \ No newline at end of file diff --git a/ui/lib/kmip/addon/components/kmip/role-form.js b/ui/lib/kmip/addon/components/kmip/role-form.js new file mode 100644 index 000000000000..496dde4106a0 --- /dev/null +++ b/ui/lib/kmip/addon/components/kmip/role-form.js @@ -0,0 +1,107 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import AdapterError from '@ember-data/adapter/error'; +import { action } from '@ember/object'; +import { service } from '@ember/service'; +import Component from '@glimmer/component'; +import { task } from 'ember-concurrency'; +import { removeManyFromArray } from 'vault/helpers/remove-from-array'; +import { operationFieldsWithoutSpecial, tlsFields } from 'vault/utils/kmip-role-fields'; + +export default class KmipRoleFormComponent extends Component { + @service flashMessages; + @service store; + + // Actual attribute fields + get tlsFormFields() { + return tlsFields().map((attr) => this.args.model.allByKey[attr]); + } + get operationFormGroups() { + const objects = [ + 'operationCreate', + 'operationActivate', + 'operationGet', + 'operationLocate', + 'operationRekey', + 'operationRevoke', + 'operationDestroy', + ]; + const attributes = ['operationAddAttribute', 'operationGetAttributes']; + const server = ['operationDiscoverVersions']; + const others = removeManyFromArray(operationFieldsWithoutSpecial(this.args.model.editableFields), [ + ...objects, + ...attributes, + ...server, + ]); + const groups = [ + { name: 'Managed Cryptographic Objects', fields: objects }, + { name: 'Object Attributes', fields: attributes }, + { name: 'Server', fields: server }, + ]; + if (others.length) { + groups.push({ + name: 'Other', + fields: others, + }); + } + // expand field names to attributes + return groups.map((group) => ({ + ...group, + fields: group.fields.map((attr) => this.args.model.allByKey[attr]), + })); + } + + placeholderOrModel = (model, attrName) => { + return model.operationAll ? { [attrName]: true } : model; + }; + + preSave() { + const opFieldsWithoutSpecial = operationFieldsWithoutSpecial(this.args.model.editableFields); + // if we have operationAll or operationNone, we want to clear + // out the others so that display shows the right data + if (this.args.model.operationAll || this.args.model.operationNone) { + opFieldsWithoutSpecial.forEach((field) => (this.args.model[field] = null)); + } + // set operationNone if user unchecks 'operationAll' instead of toggling the 'operationNone' input + // doing here instead of on the 'operationNone' input because a user might deselect all, then reselect some options + // and immediately setting operationNone will hide all of the checkboxes in the UI + this.args.model.operationNone = + opFieldsWithoutSpecial.every((attr) => this.args.model[attr] !== true) && !this.args.model.operationAll; + return this.args.model; + } + + @action toggleOperationSpecial(evt) { + const { checked } = evt.target; + this.args.model.operationNone = !checked; + this.args.model.operationAll = checked; + } + + save = task(async (evt) => { + evt.preventDefault(); + const model = this.preSave(); + try { + await model.save(); + this.flashMessages.success(`Saved role ${model.role}`); + } catch (err) { + // err will display via model state + // AdapterErrors are handled by the error-message component + if (err instanceof AdapterError === false) { + throw err; + } + return; + } + this.args.onSave(); + }); + + willDestroy() { + // components are torn down after store is unloaded and will cause an error if attempt to unload record + const noTeardown = this.store && !this.store.isDestroying; + if (noTeardown && this.args?.model?.isDirty) { + this.args.model.rollbackAttributes(); + } + super.willDestroy(); + } +} diff --git a/ui/lib/kmip/addon/routes/scope/roles/create.js b/ui/lib/kmip/addon/routes/scope/roles/create.js index 82984cddcb63..7070b4c27408 100644 --- a/ui/lib/kmip/addon/routes/scope/roles/create.js +++ b/ui/lib/kmip/addon/routes/scope/roles/create.js @@ -21,6 +21,7 @@ export default Route.extend({ const model = this.store.createRecord('kmip/role', { backend: this.secretMountPath.currentPath, scope: this.scope(), + operationAll: true, }); return model; }, diff --git a/ui/lib/kmip/addon/templates/components/edit-form-kmip-role.hbs b/ui/lib/kmip/addon/templates/components/edit-form-kmip-role.hbs deleted file mode 100644 index 3aea47a1d8df..000000000000 --- a/ui/lib/kmip/addon/templates/components/edit-form-kmip-role.hbs +++ /dev/null @@ -1,107 +0,0 @@ -{{! - Copyright (c) HashiCorp, Inc. - SPDX-License-Identifier: BUSL-1.1 -~}} - -
- -
- - {{#if (eq @mode "create")}} - - {{/if}} -
- - -
- {{#unless this.model.operationNone}} - -

- Allowed Operations -

-
-
- -
-
-
- {{#each-in (get this.model.operationFormFields 0) as |groupName fieldsInGroup|}} -

{{groupName}}

- {{#each fieldsInGroup as |attr|}} - - {{/each}} - {{/each-in}} -
-
- {{#each (drop 1 (or this.model.operationFormFields (array))) as |group|}} -
- {{#each-in group as |groupName fieldsInGroup|}} -

{{groupName}}

- {{#each fieldsInGroup as |attr|}} - - {{/each}} - {{/each-in}} -
- {{/each}} -
-
-
- {{/unless}} -
-

- TLS -

- {{#each this.model.tlsFormFields as |attr|}} - - {{/each}} -
- {{#each this.model.fields as |attr|}} - - {{/each}} -
- -
- - - {{#if this.cancelLink}} - - {{/if}} - -
- \ No newline at end of file diff --git a/ui/lib/kmip/addon/templates/role/edit.hbs b/ui/lib/kmip/addon/templates/role/edit.hbs index ab2003d85783..482083290567 100644 --- a/ui/lib/kmip/addon/templates/role/edit.hbs +++ b/ui/lib/kmip/addon/templates/role/edit.hbs @@ -13,8 +13,9 @@ - \ No newline at end of file diff --git a/ui/lib/kmip/addon/templates/scope/roles/create.hbs b/ui/lib/kmip/addon/templates/scope/roles/create.hbs index 42fcc4bf4074..71af81aa1bad 100644 --- a/ui/lib/kmip/addon/templates/scope/roles/create.hbs +++ b/ui/lib/kmip/addon/templates/scope/roles/create.hbs @@ -13,9 +13,9 @@ - \ No newline at end of file diff --git a/ui/lib/kmip/index.js b/ui/lib/kmip/index.js index 2d2ddc66e948..635b19df758a 100644 --- a/ui/lib/kmip/index.js +++ b/ui/lib/kmip/index.js @@ -17,6 +17,10 @@ module.exports = EngineAddon.extend({ enabled: true, }, + babel: { + plugins: [require.resolve('ember-concurrency/async-arrow-task-transform')], + }, + isDevelopingAddon() { return true; }, diff --git a/ui/tests/acceptance/enterprise-kmip-test.js b/ui/tests/acceptance/enterprise-kmip-test.js index a2f743227640..916cc31da051 100644 --- a/ui/tests/acceptance/enterprise-kmip-test.js +++ b/ui/tests/acceptance/enterprise-kmip-test.js @@ -16,7 +16,7 @@ import { import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; -import authPage from 'vault/tests/pages/auth'; +import { login } from 'vault/tests/helpers/auth/auth-helpers'; import scopesPage from 'vault/tests/pages/secrets/backend/kmip/scopes'; import rolesPage from 'vault/tests/pages/secrets/backend/kmip/roles'; import credentialsPage from 'vault/tests/pages/secrets/backend/kmip/credentials'; @@ -91,7 +91,7 @@ module('Acceptance | Enterprise | KMIP secrets', function (hooks) { hooks.beforeEach(async function () { this.backend = `kmip-${uuidv4()}`; - await authPage.login(); + await login(); return; }); @@ -221,16 +221,11 @@ module('Acceptance | Enterprise | KMIP secrets', function (hooks) { test('it can create a role', async function (assert) { // moving create scope here to help with flaky test - const backend = await mountWithConfig(this.backend); - await settled(); const scope = `scope-for-can-create-role`; - await settled(); - const res = await runCmd([`write ${backend}/scope/${scope} -force`]); - await settled(); - if (res.includes('Error')) { - throw new Error(`Error creating scope: ${res}`); - } const role = `role-new-role`; + const backend = await mountWithConfig(this.backend); + await settled(); + await runCmd([`write ${backend}/scope/${scope} -force`], true); await rolesPage.visit({ backend, scope }); await settled(); assert.ok(rolesPage.isEmpty, 'renders the empty role page'); @@ -241,11 +236,15 @@ module('Acceptance | Enterprise | KMIP secrets', function (hooks) { `/vault/secrets/${backend}/kmip/scopes/${scope}/roles/create`, 'links to the role create form' ); + // check that the role form looks right + assert.dom(GENERAL.inputByAttr('operationNone')).isChecked('allows role to perform roles by default'); + assert.dom(GENERAL.inputByAttr('operationAll')).isChecked('operationAll is checked by default'); + assert.dom('[data-test-kmip-section]').exists({ count: 2 }); + assert.dom('[data-test-kmip-operations]').exists({ count: 4 }); await rolesPage.roleName(role); await settled(); - await rolesPage.submit(); - await settled(); + await click(GENERAL.saveButton); assert.strictEqual( currentURL(), `/vault/secrets/${backend}/kmip/scopes/${scope}/roles`, @@ -253,6 +252,13 @@ module('Acceptance | Enterprise | KMIP secrets', function (hooks) { ); assert.strictEqual(rolesPage.listItemLinks.length, 1, 'renders a single role'); + await rolesPage.visitDetail({ backend, scope, role }); + // check that the role details looks right + assert.dom('h2').exists({ count: 2 }, 'renders correct section headings'); + assert.dom('[data-test-inline-error-message]').hasText('This role allows all KMIP operations'); + ['Managed Cryptographic Objects', 'Object Attributes', 'Server', 'Other'].forEach((title) => { + assert.dom(`[data-test-row-label="${title}"]`).exists(`Renders allowed operations row for: ${title}`); + }); }); test('it navigates to kmip roles view using breadcrumbs', async function (assert) { @@ -304,8 +310,7 @@ module('Acceptance | Enterprise | KMIP secrets', function (hooks) { `/vault/secrets/${backend}/kmip/scopes/${scope}/roles/${role}/edit`, 'navigates to role edit' ); - await rolesPage.cancelLink(); - await settled(); + await click(GENERAL.cancelButton); assert.strictEqual( currentURL(), `/vault/secrets/${backend}/kmip/scopes/${scope}/roles/${role}`, @@ -364,12 +369,12 @@ module('Acceptance | Enterprise | KMIP secrets', function (hooks) { this.store = this.owner.lookup('service:store'); this.scope = 'my-scope'; this.name = 'my-role'; - await authPage.login(); + await login(); await runCmd(mountEngineCmd('kmip', this.backend), false); await runCmd([`write ${this.backend}/scope/${this.scope} -force`]); await rolesPage.visit({ backend: this.backend, scope: this.scope }); this.setModel = async () => { - await click('[data-test-edit-form-submit]'); + await click(GENERAL.saveButton); await visit(`/vault/secrets/${this.backend}/kmip/scopes/${this.scope}/roles/${this.name}`); this.model = this.store.peekRecord('kmip/role', this.name); }; @@ -382,7 +387,7 @@ module('Acceptance | Enterprise | KMIP secrets', function (hooks) { assert.expect(3); await click('[data-test-role-create]'); - await fillIn(GENERAL.inputByAttr('name'), this.name); + await fillIn(GENERAL.inputByAttr('role'), this.name); assert.dom(GENERAL.inputByAttr('operationAll')).isChecked('operationAll is checked by default'); await this.setModel(); assert.true(this.model.operationAll, 'operationAll is true'); @@ -393,7 +398,7 @@ module('Acceptance | Enterprise | KMIP secrets', function (hooks) { assert.expect(4); await click('[data-test-role-create]'); - await fillIn(GENERAL.inputByAttr('name'), this.name); + await fillIn(GENERAL.inputByAttr('role'), this.name); await click(GENERAL.inputByAttr('operationNone')); assert .dom(GENERAL.inputByAttr('operationNone')) @@ -410,9 +415,10 @@ module('Acceptance | Enterprise | KMIP secrets', function (hooks) { assert.expect(2); await click('[data-test-role-create]'); - await fillIn(GENERAL.inputByAttr('name'), this.name); + await fillIn(GENERAL.inputByAttr('role'), this.name); await click(GENERAL.inputByAttr('operationAll')); await this.setModel(); + assert.strictEqual(this.model.operationAll, undefined, 'operationAll is unset'); assert.true(this.model.operationNone, 'operationNone is true'); }); @@ -421,7 +427,7 @@ module('Acceptance | Enterprise | KMIP secrets', function (hooks) { assert.expect(6); await click('[data-test-role-create]'); - await fillIn(GENERAL.inputByAttr('name'), this.name); + await fillIn(GENERAL.inputByAttr('role'), this.name); await click(GENERAL.inputByAttr('operationAll')); await click(GENERAL.inputByAttr('operationGet')); await click(GENERAL.inputByAttr('operationGetAttributes')); diff --git a/ui/tests/pages/secrets/backend/kmip/roles.js b/ui/tests/pages/secrets/backend/kmip/roles.js index aafaeba988a0..bc06acb4a584 100644 --- a/ui/tests/pages/secrets/backend/kmip/roles.js +++ b/ui/tests/pages/secrets/backend/kmip/roles.js @@ -11,7 +11,7 @@ export default create({ visit: visitable('/vault/secrets/:backend/kmip/scopes/:scope/roles'), visitDetail: visitable('/vault/secrets/:backend/kmip/scopes/:scope/roles/:role'), create: clickable('[data-test-role-create]'), - roleName: fillable('[data-test-input="name"]'), + roleName: fillable('[data-test-input="role"]'), submit: clickable('[data-test-edit-form-submit]'), detailEditLink: clickable('[data-test-kmip-link-edit-role]'), cancelLink: clickable('[data-test-edit-form-cancel]'), diff --git a/ui/tests/unit/adapters/kmip/role-test.js b/ui/tests/unit/adapters/kmip/role-test.js index a9d20d900459..36dcf9a883de 100644 --- a/ui/tests/unit/adapters/kmip/role-test.js +++ b/ui/tests/unit/adapters/kmip/role-test.js @@ -9,6 +9,8 @@ import { setupTest } from 'ember-qunit'; module('Unit | Adapter | kmip/role', function (hooks) { setupTest(hooks); + // these are only some of the actual editable fields + const editableFields = ['tlsTtl', 'operationAll', 'operationNone', 'operationGet', 'operationCreate']; const serializeTests = [ [ 'operation_all is the only operation item present after serialization', @@ -17,7 +19,7 @@ module('Unit | Adapter | kmip/role', function (hooks) { return { operation_all: true, operation_get: true, operation_create: true, tls_ttl: '10s' }; }, record: { - nonOperationFields: ['tlsTtl'], + editableFields, }, }, { @@ -32,7 +34,7 @@ module('Unit | Adapter | kmip/role', function (hooks) { return { operation_all: true, operation_get: true, operation_create: true }; }, record: { - nonOperationFields: ['tlsTtl'], + editableFields, }, }, { @@ -46,7 +48,7 @@ module('Unit | Adapter | kmip/role', function (hooks) { return { operation_none: true, operation_get: true, operation_add_attribute: true, tls_ttl: '10s' }; }, record: { - nonOperationFields: ['tlsTtl'], + editableFields, }, }, { @@ -67,7 +69,7 @@ module('Unit | Adapter | kmip/role', function (hooks) { }; }, record: { - nonOperationFields: ['tlsTtl'], + editableFields, }, }, { diff --git a/ui/tests/unit/utils/kmip-role-fields-test.js b/ui/tests/unit/utils/kmip-role-fields-test.js new file mode 100644 index 000000000000..effbda02fb5c --- /dev/null +++ b/ui/tests/unit/utils/kmip-role-fields-test.js @@ -0,0 +1,52 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import { + nonOperationFields, + operationFields, + operationFieldsWithoutSpecial, +} from 'vault/utils/kmip-role-fields'; + +module('Unit | Util | kmip role fields', function (hooks) { + setupTest(hooks); + + [ + { + name: 'when fields is empty', + fields: [], + opFields: [], + nonOpFields: [], + opWithoutSpecial: [], + }, + { + name: 'when no op fields', + fields: ['foo', 'bar'], + opFields: [], + nonOpFields: ['foo', 'bar'], + opWithoutSpecial: [], + }, + { + name: 'when op fields', + fields: ['foo', 'bar', 'operationFoo', 'operationBar', 'operationAll'], + opFields: ['operationFoo', 'operationBar', 'operationAll'], + nonOpFields: ['foo', 'bar'], + opWithoutSpecial: ['operationFoo', 'operationBar'], + }, + ].forEach(({ name, fields, opFields, nonOpFields, opWithoutSpecial }) => { + test(`${name}`, function (assert) { + const originalFields = JSON.parse(JSON.stringify(fields)); + assert.deepEqual(operationFields(fields), opFields, 'operation fields correct'); + assert.deepEqual(nonOperationFields(fields), nonOpFields, 'non operation fields'); + assert.deepEqual( + operationFieldsWithoutSpecial(fields), + opWithoutSpecial, + 'operation fields without special' + ); + assert.deepEqual(fields, originalFields, 'does not mutate the original'); + }); + }); +}); From 0c986fc660b816bb87db13338ee318495701e450 Mon Sep 17 00:00:00 2001 From: rajesht-source Date: Sat, 21 Sep 2024 02:23:38 +0530 Subject: [PATCH 5/7] updated vault helm chart doc with usecase of nlb (#27690) * updated vault helm chart doc with usecase of nlb Signed-off-by: Kumar, Rajesh (XINBM1A) * Update index.mdx - changes as per the pr comment Signed-off-by: Kumar, Rajesh (XINBM1A) --------- Signed-off-by: Kumar, Rajesh (XINBM1A) --- website/content/docs/platform/k8s/helm/index.mdx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/website/content/docs/platform/k8s/helm/index.mdx b/website/content/docs/platform/k8s/helm/index.mdx index 53f410103b93..6cc6c6bb1ed9 100644 --- a/website/content/docs/platform/k8s/helm/index.mdx +++ b/website/content/docs/platform/k8s/helm/index.mdx @@ -59,6 +59,15 @@ cluster](https://kubernetes.io/docs/tasks/administer-cluster/securing-a-cluster/ options](/vault/docs/platform/k8s/helm/configuration), and read the [production deployment checklist](/vault/docs/platform/k8s/helm/run#architecture). + + + + If you use AWS features (e.g, AWS PrivateLink) that require a network load + balancer (NLB), you must provision your NLB **before** your application load + balancer (ALB). + + + ## Tutorial Refer to the [Kubernetes](/vault/tutorials/kubernetes) From 2fc8e35ec355f59f7b8669f1a089c997245e2859 Mon Sep 17 00:00:00 2001 From: Andrew Talbot Date: Fri, 20 Sep 2024 17:39:16 -0400 Subject: [PATCH 6/7] docs(lambda-extension): update distributed tracing headers warning; (#28319) * docs(lambda-extension): update cache header warning; * docs(lambda-extension): tweak language; * docs(lambda-extension): accept pr comment Co-authored-by: Sarah Chavis <62406755+schavis@users.noreply.github.com> --------- Co-authored-by: Sarah Chavis <62406755+schavis@users.noreply.github.com> --- .../docs/platform/aws/lambda-extension.mdx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/website/content/docs/platform/aws/lambda-extension.mdx b/website/content/docs/platform/aws/lambda-extension.mdx index 94b6b8b8d99e..1df42ecf0cee 100644 --- a/website/content/docs/platform/aws/lambda-extension.mdx +++ b/website/content/docs/platform/aws/lambda-extension.mdx @@ -243,12 +243,16 @@ request will be forwarded to Vault and the response returned and cached. Currently, the cache key is a hash of the request URL path, headers, body, and token. - - - The Vault Lambda Extension cache key includes headers from proxy requests. As - a result, distributed tracing headers like `traceparent` can lead to - individualized hashes that make repeated requests appear unique and cause cache - misses. + + + The Vault Lambda Extension cache key includes headers from proxy requests, but + excludes the standard distributed tracing headers `traceparent` and + `tracestate` because trace IDs are unique per request and would lead to unique + hashes for repeated requests. + + Some distributed tracing tools may add nonstandard tracing headers, which can + also lead to individualized hashes that make repeated requests unique and + cause cache misses. From fc5ed22fd140300bfca7e1f1087227b6cfc03046 Mon Sep 17 00:00:00 2001 From: Brian Howe <30811239+bhowe34@users.noreply.github.com> Date: Mon, 23 Sep 2024 07:46:15 -0500 Subject: [PATCH 7/7] pass context to S3 and dynamoDB storage calls (#27927) * pass context to S3 and dynamoDB storage calls * add changelog * fix changelog --- changelog/27927.txt | 6 ++++++ physical/dynamodb/dynamodb.go | 18 +++++++++--------- physical/s3/s3.go | 8 ++++---- 3 files changed, 19 insertions(+), 13 deletions(-) create mode 100644 changelog/27927.txt diff --git a/changelog/27927.txt b/changelog/27927.txt new file mode 100644 index 000000000000..afc37a7acbd3 --- /dev/null +++ b/changelog/27927.txt @@ -0,0 +1,6 @@ +```release-note:improvement +storage/s3: Pass context to AWS SDK calls +``` +```release-note:improvement +storage/dynamodb: Pass context to AWS SDK calls +``` diff --git a/physical/dynamodb/dynamodb.go b/physical/dynamodb/dynamodb.go index c4484d20d446..bc27def0c987 100644 --- a/physical/dynamodb/dynamodb.go +++ b/physical/dynamodb/dynamodb.go @@ -294,7 +294,7 @@ func (d *DynamoDBBackend) Put(ctx context.Context, entry *physical.Entry) error }) } - return d.batchWriteRequests(requests) + return d.batchWriteRequests(ctx, requests) } // Get is used to fetch an entry @@ -304,7 +304,7 @@ func (d *DynamoDBBackend) Get(ctx context.Context, key string) (*physical.Entry, d.permitPool.Acquire() defer d.permitPool.Release() - resp, err := d.client.GetItem(&dynamodb.GetItemInput{ + resp, err := d.client.GetItemWithContext(ctx, &dynamodb.GetItemInput{ TableName: aws.String(d.table), ConsistentRead: aws.Bool(true), Key: map[string]*dynamodb.AttributeValue{ @@ -363,7 +363,7 @@ func (d *DynamoDBBackend) Delete(ctx context.Context, key string) error { excluded = append(excluded, recordKeyForVaultKey(prefixes[index-1])) } - hasChildren, err := d.hasChildren(prefix, excluded) + hasChildren, err := d.hasChildren(ctx, prefix, excluded) if err != nil { return err } @@ -387,7 +387,7 @@ func (d *DynamoDBBackend) Delete(ctx context.Context, key string) error { } } - return d.batchWriteRequests(requests) + return d.batchWriteRequests(ctx, requests) } // List is used to list all the keys under a given @@ -420,7 +420,7 @@ func (d *DynamoDBBackend) List(ctx context.Context, prefix string) ([]string, er d.permitPool.Acquire() defer d.permitPool.Release() - err := d.client.QueryPages(queryInput, func(out *dynamodb.QueryOutput, lastPage bool) bool { + err := d.client.QueryPagesWithContext(ctx, queryInput, func(out *dynamodb.QueryOutput, lastPage bool) bool { var record DynamoDBRecord for _, item := range out.Items { dynamodbattribute.UnmarshalMap(item, &record) @@ -443,7 +443,7 @@ func (d *DynamoDBBackend) List(ctx context.Context, prefix string) ([]string, er // before any deletes take place. To account for that hasChildren accepts a slice of // strings representing values we expect to find that should NOT be counted as children // because they are going to be deleted. -func (d *DynamoDBBackend) hasChildren(prefix string, exclude []string) (bool, error) { +func (d *DynamoDBBackend) hasChildren(ctx context.Context, prefix string, exclude []string) (bool, error) { prefix = strings.TrimSuffix(prefix, "/") prefix = escapeEmptyPath(prefix) @@ -473,7 +473,7 @@ func (d *DynamoDBBackend) hasChildren(prefix string, exclude []string) (bool, er d.permitPool.Acquire() defer d.permitPool.Release() - out, err := d.client.Query(queryInput) + out, err := d.client.QueryWithContext(ctx, queryInput) if err != nil { return false, err } @@ -519,7 +519,7 @@ func (d *DynamoDBBackend) HAEnabled() bool { // batchWriteRequests takes a list of write requests and executes them in badges // with a maximum size of 25 (which is the limit of BatchWriteItem requests). -func (d *DynamoDBBackend) batchWriteRequests(requests []*dynamodb.WriteRequest) error { +func (d *DynamoDBBackend) batchWriteRequests(ctx context.Context, requests []*dynamodb.WriteRequest) error { for len(requests) > 0 { batchSize := int(math.Min(float64(len(requests)), 25)) batch := map[string][]*dynamodb.WriteRequest{d.table: requests[:batchSize]} @@ -534,7 +534,7 @@ func (d *DynamoDBBackend) batchWriteRequests(requests []*dynamodb.WriteRequest) for len(batch) > 0 { var output *dynamodb.BatchWriteItemOutput - output, err = d.client.BatchWriteItem(&dynamodb.BatchWriteItemInput{ + output, err = d.client.BatchWriteItemWithContext(ctx, &dynamodb.BatchWriteItemInput{ RequestItems: batch, }) if err != nil { diff --git a/physical/s3/s3.go b/physical/s3/s3.go index da82acccd3ca..b1687a91622e 100644 --- a/physical/s3/s3.go +++ b/physical/s3/s3.go @@ -183,7 +183,7 @@ func (s *S3Backend) Put(ctx context.Context, entry *physical.Entry) error { putObjectInput.SSEKMSKeyId = aws.String(s.kmsKeyId) } - _, err := s.client.PutObject(putObjectInput) + _, err := s.client.PutObjectWithContext(ctx, putObjectInput) if err != nil { return err } @@ -201,7 +201,7 @@ func (s *S3Backend) Get(ctx context.Context, key string) (*physical.Entry, error // Setup key key = path.Join(s.path, key) - resp, err := s.client.GetObject(&s3.GetObjectInput{ + resp, err := s.client.GetObjectWithContext(ctx, &s3.GetObjectInput{ Bucket: aws.String(s.bucket), Key: aws.String(key), }) @@ -254,7 +254,7 @@ func (s *S3Backend) Delete(ctx context.Context, key string) error { // Setup key key = path.Join(s.path, key) - _, err := s.client.DeleteObject(&s3.DeleteObjectInput{ + _, err := s.client.DeleteObjectWithContext(ctx, &s3.DeleteObjectInput{ Bucket: aws.String(s.bucket), Key: aws.String(key), }) @@ -289,7 +289,7 @@ func (s *S3Backend) List(ctx context.Context, prefix string) ([]string, error) { keys := []string{} - err := s.client.ListObjectsV2Pages(params, + err := s.client.ListObjectsV2PagesWithContext(ctx, params, func(page *s3.ListObjectsV2Output, lastPage bool) bool { if page != nil { // Add truncated 'folder' paths