From 64c2fd030f73647dc24132b691ded66a3f877530 Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Wed, 29 May 2024 10:21:17 -0600 Subject: [PATCH 01/21] intial changes, haven't tested client counts or done test coverage --- ui/app/components/sidebar/nav/cluster.hbs | 14 ++++++++------ ui/app/components/sidebar/nav/cluster.js | 12 ------------ ui/app/routes/application.js | 7 +++++-- ui/app/services/flags.ts | 20 +++++++++++++++++++- ui/app/services/permissions.js | 3 +++ ui/lib/sync/addon/components/sync-header.hbs | 4 ++-- ui/lib/sync/addon/components/sync-header.ts | 14 -------------- ui/lib/sync/addon/routes/secrets.ts | 5 ----- ui/types/vault/services/permissions.d.ts | 1 + 9 files changed, 38 insertions(+), 42 deletions(-) diff --git a/ui/app/components/sidebar/nav/cluster.hbs b/ui/app/components/sidebar/nav/cluster.hbs index ee01bbeedeae..b966cee19871 100644 --- a/ui/app/components/sidebar/nav/cluster.hbs +++ b/ui/app/components/sidebar/nav/cluster.hbs @@ -13,12 +13,14 @@ @text="Secrets Engines" data-test-sidebar-nav-link="Secrets Engines" /> - + {{#if this.flags.showSecretsSync}} + + {{/if}} {{#if (has-permission "access")}} { - if (this.version.isCommunity) return; // Response could change between user sessions. // Fire off endpoint without checking if activated features are already set. try { @@ -86,4 +87,21 @@ export default class flagsService extends Service { this.secretsSyncActivatePath.get('canUpdate') !== false ); } + + get showSecretsSync() { + const isHvdManaged = this.isHvdManaged; + const onLicense = this.version.hasSecretsSync; + const isEnterprise = this.version.isEnterprise; + const isActivated = this.secretsSyncIsActivated; + + if (isHvdManaged) return true; + if (isEnterprise && !onLicense) return false; + if (!isEnterprise) return false; + if (isActivated) { + // if activated but the user does not have permissions to do anything on the `sys/sync` endpoint, hide navigation link. + return this.permissions.hasNavPermission('sync'); + } + // The only remaining option is Enterprise with Secrets Sync on the license but the feature is not activated. In this case, we want to show the upsell page and message about either activating or having an admin activate. + return true; + } } diff --git a/ui/app/services/permissions.js b/ui/app/services/permissions.js index 3233d095996b..b209e78ec9a8 100644 --- a/ui/app/services/permissions.js +++ b/ui/app/services/permissions.js @@ -49,6 +49,9 @@ const API_PATHS = { settings: { customMessages: 'sys/config/ui/custom-messages', }, + sync: { + sync: 'sys/sync', + }, }; const API_PATHS_TO_ROUTE_PARAMS = { diff --git a/ui/lib/sync/addon/components/sync-header.hbs b/ui/lib/sync/addon/components/sync-header.hbs index 88a19e6ed372..b8bb70ff40b1 100644 --- a/ui/lib/sync/addon/components/sync-header.hbs +++ b/ui/lib/sync/addon/components/sync-header.hbs @@ -16,8 +16,8 @@ {{/if}} {{@title}} - {{#if this.badgeText}} - + {{#if this.flags.isHvdManaged}} + {{/if}} diff --git a/ui/lib/sync/addon/components/sync-header.ts b/ui/lib/sync/addon/components/sync-header.ts index 0d1549910745..35d8dfc365e4 100644 --- a/ui/lib/sync/addon/components/sync-header.ts +++ b/ui/lib/sync/addon/components/sync-header.ts @@ -6,7 +6,6 @@ import Component from '@glimmer/component'; import { service } from '@ember/service'; -import type VersionService from 'vault/services/version'; import type FlagsService from 'vault/services/flags'; import type { Breadcrumb } from 'vault/vault/app-types'; @@ -17,18 +16,5 @@ interface Args { } export default class SyncHeaderComponent extends Component { - @service declare readonly version: VersionService; @service declare readonly flags: FlagsService; - - get badgeText() { - const isHvdManaged = this.flags.isHvdManaged; - const onLicense = this.version.hasSecretsSync; - const isEnterprise = this.version.isEnterprise; - - if (isHvdManaged) return 'Plus feature'; - if (isEnterprise && !onLicense) return 'Premium feature'; - if (!isEnterprise) return 'Enterprise feature'; - // no badge for Enterprise clusters with Secrets Sync on their license--the only remaining option. - return ''; - } } diff --git a/ui/lib/sync/addon/routes/secrets.ts b/ui/lib/sync/addon/routes/secrets.ts index cd38c7a2521c..0a1a6ec296db 100644 --- a/ui/lib/sync/addon/routes/secrets.ts +++ b/ui/lib/sync/addon/routes/secrets.ts @@ -13,13 +13,8 @@ export default class SyncSecretsRoute extends Route { @service declare readonly router: RouterService; @service declare readonly flags: FlagService; - beforeModel() { - return this.flags.fetchActivatedFlags(); - } - model() { return { - // TODO will modify when we use the persona service. activatedFeatures: this.flags.activatedFlags, }; } diff --git a/ui/types/vault/services/permissions.d.ts b/ui/types/vault/services/permissions.d.ts index 232508240cce..99181ab72258 100644 --- a/ui/types/vault/services/permissions.d.ts +++ b/ui/types/vault/services/permissions.d.ts @@ -16,4 +16,5 @@ export default class PermissionsService extends Service { canViewAll: boolean | null; permissionsBanner: string | null; chrootNamespace: string | null | undefined; + hasNavPermission: (string) => boolean; } From 931a9264647ff5b20c737b72bcba3fb42cc58139 Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Wed, 29 May 2024 10:41:43 -0600 Subject: [PATCH 02/21] client count rename getter to clairfy --- changelog/27262.txt | 3 +++ ui/app/components/clients/page/counts.hbs | 2 +- ui/app/components/clients/page/counts.ts | 2 +- ui/app/routes/application.js | 4 ++-- .../components/clients/counts/nav-bar-test.js | 14 ++++++++------ 5 files changed, 15 insertions(+), 10 deletions(-) create mode 100644 changelog/27262.txt diff --git a/changelog/27262.txt b/changelog/27262.txt new file mode 100644 index 000000000000..93c2fbe3f0d1 --- /dev/null +++ b/changelog/27262.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui/secrets-sync: Hide Secrets Sync from the sidebar nav if user does not have access to the feature. +``` diff --git a/ui/app/components/clients/page/counts.hbs b/ui/app/components/clients/page/counts.hbs index 512d729b9148..8c08e3d354f0 100644 --- a/ui/app/components/clients/page/counts.hbs +++ b/ui/app/components/clients/page/counts.hbs @@ -151,7 +151,7 @@ {{/if}} - + {{! CLIENT COUNT PAGE COMPONENTS RENDER HERE }} {{yield}} diff --git a/ui/app/components/clients/page/counts.ts b/ui/app/components/clients/page/counts.ts index 28fb5ae7b05d..ffc29d8081cf 100644 --- a/ui/app/components/clients/page/counts.ts +++ b/ui/app/components/clients/page/counts.ts @@ -167,7 +167,7 @@ export default class ClientsCountsPageComponent extends Component { return activity?.total; } - get showSecretsSync(): boolean { + get showSecretsSyncClientCounts(): boolean { const { activity } = this.args; // if there is any sync client data, show it if (activity && activity?.total?.secret_syncs > 0) return true; diff --git a/ui/app/routes/application.js b/ui/app/routes/application.js index d61495f5cf74..fef17b2b3d09 100644 --- a/ui/app/routes/application.js +++ b/ui/app/routes/application.js @@ -66,8 +66,8 @@ export default Route.extend({ }, async beforeModel() { - // activatedFlags are called this high up in routing to show/hide Secrets sync on sidebar nav. More deeply nested in routing, this flag determines to hide/show an activation banner for Secrets sync. - // featureFlags are used for all sorts of things but need to be this high in routing because it determines if the cluster isHvdManaged or not. + // activatedFlags are called this high up in routing to return a response used to show/hide Secrets sync on sidebar nav. + // featureFlags are called this high in routing because to determine isHvdManaged things, etc. await this.flagsService.fetchActivatedFlags(); await this.flagsService.fetchFeatureFlags(); }, diff --git a/ui/tests/integration/components/clients/counts/nav-bar-test.js b/ui/tests/integration/components/clients/counts/nav-bar-test.js index eed377b88fe0..a8dcaf979838 100644 --- a/ui/tests/integration/components/clients/counts/nav-bar-test.js +++ b/ui/tests/integration/components/clients/counts/nav-bar-test.js @@ -13,10 +13,12 @@ module('Integration | Component | clients/counts/nav-bar', function (hooks) { setupRenderingTest(hooks); hooks.beforeEach(function () { - this.showSecretsSync = false; + this.showSecretsSyncClientCounts = false; this.renderComponent = async () => { - await render(hbs``); + await render( + hbs`` + ); }; }); @@ -28,15 +30,15 @@ module('Integration | Component | clients/counts/nav-bar', function (hooks) { assert.dom(GENERAL.tab('acme')).hasText('ACME clients'); }); - test('it shows secrets sync tab if showSecretsSync is true', async function (assert) { - this.showSecretsSync = true; + test('it shows secrets sync tab if showSecretsSyncClientCounts is true', async function (assert) { + this.showSecretsSyncClientCounts = true; await this.renderComponent(); assert.dom(GENERAL.tab('sync')).exists(); }); - test('it should not show secrets sync tab if showSecretsSync is false', async function (assert) { - this.showSecretsSync = false; + test('it should not show secrets sync tab if showSecretsSyncClientCounts is false', async function (assert) { + this.showSecretsSyncClientCounts = false; await this.renderComponent(); assert.dom(GENERAL.tab('sync')).doesNotExist(); From ac6b26264f1dce3c21fad4def68724edf3e0e199 Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Wed, 29 May 2024 12:59:51 -0600 Subject: [PATCH 03/21] fix has-permission api-paths --- ui/app/routes/application.js | 4 ++-- ui/app/services/flags.ts | 7 ++++--- ui/app/services/permissions.js | 11 ++++++++--- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/ui/app/routes/application.js b/ui/app/routes/application.js index fef17b2b3d09..0d7bc2823b5d 100644 --- a/ui/app/routes/application.js +++ b/ui/app/routes/application.js @@ -66,8 +66,8 @@ export default Route.extend({ }, async beforeModel() { - // activatedFlags are called this high up in routing to return a response used to show/hide Secrets sync on sidebar nav. - // featureFlags are called this high in routing because to determine isHvdManaged things, etc. + // activatedFlags are called this high in routing to return a response used to show/hide Secrets sync on sidebar nav. + // featureFlags are called this high in routing to determine isHvdManaged things, etc. await this.flagsService.fetchActivatedFlags(); await this.flagsService.fetchFeatureFlags(); }, diff --git a/ui/app/services/flags.ts b/ui/app/services/flags.ts index 629fb2449eb9..67164d0b9d2e 100644 --- a/ui/app/services/flags.ts +++ b/ui/app/services/flags.ts @@ -94,14 +94,15 @@ export default class flagsService extends Service { const isEnterprise = this.version.isEnterprise; const isActivated = this.secretsSyncIsActivated; + if (!isEnterprise) return false; if (isHvdManaged) return true; if (isEnterprise && !onLicense) return false; - if (!isEnterprise) return false; + // only remaining version is Enterprise with Secrets Sync on their license if (isActivated) { - // if activated but the user does not have permissions to do anything on the `sys/sync` endpoint, hide navigation link. + // if the feature is activated but the user does not have permissions on the `sys/sync` endpoint, hide navigation link. return this.permissions.hasNavPermission('sync'); } - // The only remaining option is Enterprise with Secrets Sync on the license but the feature is not activated. In this case, we want to show the upsell page and message about either activating or having an admin activate. + // only remaining option now is Enterprise with Secrets Sync on the license but the feature is not activated. In this case, we want to show the upsell page and message about either activating or having an admin activate. return true; } } diff --git a/ui/app/services/permissions.js b/ui/app/services/permissions.js index b209e78ec9a8..1d6de829aec7 100644 --- a/ui/app/services/permissions.js +++ b/ui/app/services/permissions.js @@ -50,7 +50,8 @@ const API_PATHS = { customMessages: 'sys/config/ui/custom-messages', }, sync: { - sync: 'sys/sync', + destinations: 'sys/sync/destinations', + config: 'sys/sync/config', }, }; @@ -190,7 +191,11 @@ export default class PermissionsService extends Service { return this.hasPermission(API_PATHS[navItem][param], capability); }); } - return Object.values(API_PATHS[navItem]).some((path) => this.hasPermission(path)); + return Object.values(API_PATHS[navItem]).some((path) => { + const test = this.hasPermission(path); + + return test; + }); } navPathParams(navItem) { @@ -216,7 +221,6 @@ export default class PermissionsService extends Service { return true; } const path = this.pathNameWithNamespace(pathName); - return capabilities.every( (capability) => this.hasMatchingExactPath(path, capability) || this.hasMatchingGlobPath(path, capability) @@ -240,6 +244,7 @@ export default class PermissionsService extends Service { hasMatchingGlobPath(pathName, capability) { const globPaths = this.globPaths; + if (globPaths) { const matchingPath = Object.keys(globPaths).find((k) => { return pathName.includes(k) || pathName.includes(k.replace(/\/$/, '')); From b1ad18f6175a2f7271dc0633bdd97ad8be1bb3dc Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Wed, 29 May 2024 14:34:16 -0600 Subject: [PATCH 04/21] wip --- ui/app/components/clients/counts/nav-bar.hbs | 2 +- ui/app/services/permissions.js | 1 + ui/lib/sync/addon/components/sync-header.hbs | 2 +- .../acceptance/clients/counts/sync-test.js | 5 ++--- .../components/sync/sync-header-test.js | 19 ------------------- 5 files changed, 5 insertions(+), 24 deletions(-) diff --git a/ui/app/components/clients/counts/nav-bar.hbs b/ui/app/components/clients/counts/nav-bar.hbs index 04c6e028c22f..3c4b75c3ce6a 100644 --- a/ui/app/components/clients/counts/nav-bar.hbs +++ b/ui/app/components/clients/counts/nav-bar.hbs @@ -15,7 +15,7 @@ Entity/Non-entity clients - {{#if @showSecretsSync}} + {{#if @showSecretsSyncClientCounts}}
  • Secrets sync clients diff --git a/ui/app/services/permissions.js b/ui/app/services/permissions.js index 1d6de829aec7..07f0f724010d 100644 --- a/ui/app/services/permissions.js +++ b/ui/app/services/permissions.js @@ -51,6 +51,7 @@ const API_PATHS = { }, sync: { destinations: 'sys/sync/destinations', + associations: 'sys/sync/associations', config: 'sys/sync/config', }, }; diff --git a/ui/lib/sync/addon/components/sync-header.hbs b/ui/lib/sync/addon/components/sync-header.hbs index b8bb70ff40b1..d75a814c34a6 100644 --- a/ui/lib/sync/addon/components/sync-header.hbs +++ b/ui/lib/sync/addon/components/sync-header.hbs @@ -17,7 +17,7 @@ {{/if}} {{@title}} {{#if this.flags.isHvdManaged}} - + {{/if}} diff --git a/ui/tests/acceptance/clients/counts/sync-test.js b/ui/tests/acceptance/clients/counts/sync-test.js index 6dfb2a823815..55ebf4708bc6 100644 --- a/ui/tests/acceptance/clients/counts/sync-test.js +++ b/ui/tests/acceptance/clients/counts/sync-test.js @@ -45,11 +45,11 @@ module('Acceptance | clients | sync', function (hooks) { }); test('it should show an empty state when secrets sync is not activated', async function (assert) { - assert.expect(3); + assert.expect(2); this.server.get('/sys/activation-flags', () => { assert.true(true, '/sys/activation-flags/ is called to check if secrets-sync is activated'); - + // called once from the higher level application route return { data: { activated: [], @@ -59,7 +59,6 @@ module('Acceptance | clients | sync', function (hooks) { }); assert.dom(GENERAL.emptyStateTitle).exists('Shows empty state when secrets-sync is not activated'); - await click(`${GENERAL.emptyStateActions} .hds-link-standalone`); assert.strictEqual( currentURL(), diff --git a/ui/tests/integration/components/sync/sync-header-test.js b/ui/tests/integration/components/sync/sync-header-test.js index 7f387bcb9ef0..95d1144684c3 100644 --- a/ui/tests/integration/components/sync/sync-header-test.js +++ b/ui/tests/integration/components/sync/sync-header-test.js @@ -44,25 +44,6 @@ module('Integration | Component | sync | SyncHeader', function (hooks) { assert.dom(title).hasText('Secrets Sync'); }); - - test('it should render title and premium badge if license does not have secrets sync feature', async function (assert) { - this.version.features = []; - await this.renderComponent(); - - assert.dom(title).hasText('Secrets Sync Premium feature'); - }); - }); - - module('community', function (hooks) { - hooks.beforeEach(function () { - this.version.type = 'community'; - }); - - test('it should render title and enterprise badge', async function (assert) { - await this.renderComponent(); - - assert.dom(title).hasText('Secrets Sync Enterprise feature'); - }); }); module('managed', function (hooks) { From 09162c4be45ab19fda57422595bdb8306de618d2 Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Wed, 29 May 2024 15:34:01 -0600 Subject: [PATCH 05/21] wip --- ui/app/components/clients/page/counts.hbs | 2 +- ui/app/components/clients/page/counts.ts | 14 +----- ui/app/routes/application.js | 2 +- ui/app/routes/vault/cluster.js | 2 + .../acceptance/sync/secrets/overview-test.js | 43 ++++++++++++++----- 5 files changed, 38 insertions(+), 25 deletions(-) diff --git a/ui/app/components/clients/page/counts.hbs b/ui/app/components/clients/page/counts.hbs index 8c08e3d354f0..59d00e5babf8 100644 --- a/ui/app/components/clients/page/counts.hbs +++ b/ui/app/components/clients/page/counts.hbs @@ -151,7 +151,7 @@ {{/if}} - + {{! CLIENT COUNT PAGE COMPONENTS RENDER HERE }} {{yield}} diff --git a/ui/app/components/clients/page/counts.ts b/ui/app/components/clients/page/counts.ts index ffc29d8081cf..265c90da17db 100644 --- a/ui/app/components/clients/page/counts.ts +++ b/ui/app/components/clients/page/counts.ts @@ -167,20 +167,10 @@ export default class ClientsCountsPageComponent extends Component { return activity?.total; } - get showSecretsSyncClientCounts(): boolean { + get hasSecretsSyncClients(): boolean { const { activity } = this.args; // if there is any sync client data, show it - if (activity && activity?.total?.secret_syncs > 0) return true; - - // otherwise, show the tab based on the cluster type and license - if (this.version.isCommunity) return false; - - const isHvd = this.flags.isHvdManaged; - const onLicense = this.version.hasSecretsSync; - - // we can't tell if HVD clusters have the feature or not, so we show it by default - // if the cluster is not HVD, show the tab if the feature is on the license - return isHvd || onLicense; + return activity && activity?.total?.secret_syncs > 0; } @action diff --git a/ui/app/routes/application.js b/ui/app/routes/application.js index 0d7bc2823b5d..34ba1e2719c7 100644 --- a/ui/app/routes/application.js +++ b/ui/app/routes/application.js @@ -68,7 +68,7 @@ export default Route.extend({ async beforeModel() { // activatedFlags are called this high in routing to return a response used to show/hide Secrets sync on sidebar nav. // featureFlags are called this high in routing to determine isHvdManaged things, etc. - await this.flagsService.fetchActivatedFlags(); + // await this.flagsService.fetchActivatedFlags(); await this.flagsService.fetchFeatureFlags(); }, }); diff --git a/ui/app/routes/vault/cluster.js b/ui/app/routes/vault/cluster.js index 74fc23cc52d6..ed839cbaebd6 100644 --- a/ui/app/routes/vault/cluster.js +++ b/ui/app/routes/vault/cluster.js @@ -55,10 +55,12 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, { }, async beforeModel() { + await this.flagsService.fetchActivatedFlags(); const params = this.paramsFor(this.routeName); let namespace = params.namespaceQueryParam; const currentTokenName = this.auth.currentTokenName; const managedRoot = this.flagsService.hvdManagedNamespaceRoot; + assert( 'Cannot use VAULT_CLOUD_ADMIN_NAMESPACE flag with non-enterprise Vault version', !(managedRoot && this.version.isCommunity) diff --git a/ui/tests/acceptance/sync/secrets/overview-test.js b/ui/tests/acceptance/sync/secrets/overview-test.js index 147c687b5665..2cbd2ca0064a 100644 --- a/ui/tests/acceptance/sync/secrets/overview-test.js +++ b/ui/tests/acceptance/sync/secrets/overview-test.js @@ -19,16 +19,25 @@ module('Acceptance | sync | overview', function (hooks) { setupMirage(hooks); hooks.beforeEach(async function () { - syncHandlers(this.server); this.version = this.owner.lookup('service:version'); this.version.features = ['Secrets Sync']; - await authPage.login(); + // await authPage.login(); }); module('when feature is activated', function (hooks) { hooks.beforeEach(async function () { - syncScenario(this.server); + syncHandlers(this.server); + this.server.get('/sys/activation-flags', () => { + return { + data: { + activated: ['secrets-sync'], + unactivated: [''], + }, + }; + }); + + await authPage.login(); }); test('it fetches destinations and associations', async function (assert) { @@ -47,6 +56,7 @@ module('Acceptance | sync | overview', function (hooks) { module('when there are pre-existing destinations', function (hooks) { hooks.beforeEach(async function () { syncScenario(this.server); + await authPage.login(); }); test('it should transition to correct routes when performing actions', async function (assert) { @@ -71,7 +81,6 @@ module('Acceptance | sync | overview', function (hooks) { module('when feature is not activated', function (hooks) { hooks.beforeEach(async function () { let wasActivatePOSTCalled = false; - // simulate the feature being activated once /secrets-sync/activate has been called this.server.get('/sys/activation-flags', () => { if (wasActivatePOSTCalled) { @@ -95,6 +104,7 @@ module('Acceptance | sync | overview', function (hooks) { wasActivatePOSTCalled = true; return {}; }); + await authPage.login(); }); test('it does not fetch destinations and associations', async function (assert) { @@ -145,6 +155,7 @@ module('Acceptance | sync | overview', function (hooks) { module('enterprise with namespaces', function (hooks) { hooks.beforeEach(async function () { this.version.features = ['Secrets Sync', 'Namespaces']; + await authPage.login(); await runCmd(`write sys/namespaces/admin -f`, false); await authPage.loginNs('admin'); await runCmd(`write sys/namespaces/foo -f`, false); @@ -154,16 +165,27 @@ module('Acceptance | sync | overview', function (hooks) { test('it should make activation-flag requests to correct namespace', async function (assert) { assert.expect(4); // should call GET activation-flags twice because we need an updated response after activating the feature + let wasActivatePOSTCalled = false; this.server.get('/sys/activation-flags', (_, req) => { assert.deepEqual(req.requestHeaders, {}, 'Request is unauthenticated and in root namespace'); - return { - data: { - activated: [''], - unactivated: ['secrets-sync'], - }, - }; + if (wasActivatePOSTCalled) { + return { + data: { + activated: ['secrets-sync'], + unactivated: [''], + }, + }; + } else { + return { + data: { + activated: [''], + unactivated: ['secrets-sync'], + }, + }; + } }); this.server.post('/sys/activation-flags/secrets-sync/activate', (_, req) => { + wasActivatePOSTCalled = true; assert.strictEqual( req.requestHeaders['X-Vault-Namespace'], undefined, @@ -174,7 +196,6 @@ module('Acceptance | sync | overview', function (hooks) { // confirm we're in admin/foo assert.dom('[data-test-badge-namespace]').hasText('foo'); - await click(ts.navLink('Secrets Sync')); await click(ts.overview.optInBanner.enable); await click(ts.overview.activationModal.checkbox); From e67076e150495c122906ea8b6a7a5ef000f40420 Mon Sep 17 00:00:00 2001 From: Noelle Daley Date: Wed, 29 May 2024 15:57:27 -0700 Subject: [PATCH 06/21] fix: explicitly refresh vault.cluster model to re-fetch activatedFeatures after actication --- ui/lib/sync/addon/components/secrets/sync-activation-modal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/lib/sync/addon/components/secrets/sync-activation-modal.ts b/ui/lib/sync/addon/components/secrets/sync-activation-modal.ts index b279e9db5129..70fee2d750ba 100644 --- a/ui/lib/sync/addon/components/secrets/sync-activation-modal.ts +++ b/ui/lib/sync/addon/components/secrets/sync-activation-modal.ts @@ -43,7 +43,7 @@ export default class SyncActivationModal extends Component { .adapterFor('application') .ajax('/v1/sys/activation-flags/secrets-sync/activate', 'POST', { namespace }); // must refresh and not transition because transition does not refresh the model from within a namespace - yield this.router.refresh(); + yield this.router.refresh('vault.cluster'); } catch (error) { this.args.onError(errorMessage(error)); this.flashMessages.danger(`Error enabling feature \n ${errorMessage(error)}`); From e69075b09bce698ea039c77112db4e0ef3050a73 Mon Sep 17 00:00:00 2001 From: Noelle Daley Date: Wed, 29 May 2024 15:57:57 -0700 Subject: [PATCH 07/21] tests: fix # of assertions for verifying that activation was called --- .../acceptance/sync/secrets/overview-test.js | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/ui/tests/acceptance/sync/secrets/overview-test.js b/ui/tests/acceptance/sync/secrets/overview-test.js index 2cbd2ca0064a..8c2067d5fa07 100644 --- a/ui/tests/acceptance/sync/secrets/overview-test.js +++ b/ui/tests/acceptance/sync/secrets/overview-test.js @@ -163,29 +163,18 @@ module('Acceptance | sync | overview', function (hooks) { }); test('it should make activation-flag requests to correct namespace', async function (assert) { - assert.expect(4); - // should call GET activation-flags twice because we need an updated response after activating the feature - let wasActivatePOSTCalled = false; + assert.expect(3); + this.server.get('/sys/activation-flags', (_, req) => { assert.deepEqual(req.requestHeaders, {}, 'Request is unauthenticated and in root namespace'); - if (wasActivatePOSTCalled) { - return { - data: { - activated: ['secrets-sync'], - unactivated: [''], - }, - }; - } else { - return { - data: { - activated: [''], - unactivated: ['secrets-sync'], - }, - }; - } + return { + data: { + activated: [''], + unactivated: ['secrets-sync'], + }, + }; }); this.server.post('/sys/activation-flags/secrets-sync/activate', (_, req) => { - wasActivatePOSTCalled = true; assert.strictEqual( req.requestHeaders['X-Vault-Namespace'], undefined, @@ -203,8 +192,7 @@ module('Acceptance | sync | overview', function (hooks) { }); test('it should make activation-flag requests to correct namespace when managed', async function (assert) { - assert.expect(4); - // should call GET activation-flags twice because we need an updated response after activating the feature + assert.expect(3); this.owner.lookup('service:flags').featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE']; this.server.get('/sys/activation-flags', (_, req) => { From 27df01adedfe12b696087d374e6b1bb3237e7ee4 Mon Sep 17 00:00:00 2001 From: Noelle Daley Date: Wed, 29 May 2024 16:07:15 -0700 Subject: [PATCH 08/21] tests: tidy overview-test --- .../acceptance/sync/secrets/overview-test.js | 135 ++++++++---------- 1 file changed, 62 insertions(+), 73 deletions(-) diff --git a/ui/tests/acceptance/sync/secrets/overview-test.js b/ui/tests/acceptance/sync/secrets/overview-test.js index 8c2067d5fa07..036c752aeaa3 100644 --- a/ui/tests/acceptance/sync/secrets/overview-test.js +++ b/ui/tests/acceptance/sync/secrets/overview-test.js @@ -21,22 +21,11 @@ module('Acceptance | sync | overview', function (hooks) { hooks.beforeEach(async function () { this.version = this.owner.lookup('service:version'); this.version.features = ['Secrets Sync']; - - // await authPage.login(); }); module('when feature is activated', function (hooks) { hooks.beforeEach(async function () { syncHandlers(this.server); - this.server.get('/sys/activation-flags', () => { - return { - data: { - activated: ['secrets-sync'], - unactivated: [''], - }, - }; - }); - await authPage.login(); }); @@ -150,76 +139,76 @@ module('Acceptance | sync | overview', function (hooks) { 'create new destination is available once feature is activated' ); }); - }); - module('enterprise with namespaces', function (hooks) { - hooks.beforeEach(async function () { - this.version.features = ['Secrets Sync', 'Namespaces']; - await authPage.login(); - await runCmd(`write sys/namespaces/admin -f`, false); - await authPage.loginNs('admin'); - await runCmd(`write sys/namespaces/foo -f`, false); - await authPage.loginNs('admin/foo'); - }); + module('enterprise with namespaces', function (hooks) { + hooks.beforeEach(async function () { + this.version.features = ['Secrets Sync', 'Namespaces']; - test('it should make activation-flag requests to correct namespace', async function (assert) { - assert.expect(3); - - this.server.get('/sys/activation-flags', (_, req) => { - assert.deepEqual(req.requestHeaders, {}, 'Request is unauthenticated and in root namespace'); - return { - data: { - activated: [''], - unactivated: ['secrets-sync'], - }, - }; - }); - this.server.post('/sys/activation-flags/secrets-sync/activate', (_, req) => { - assert.strictEqual( - req.requestHeaders['X-Vault-Namespace'], - undefined, - 'Request is made to undefined namespace' - ); - return {}; + await runCmd(`write sys/namespaces/admin -f`, false); + await authPage.loginNs('admin'); + await runCmd(`write sys/namespaces/foo -f`, false); + await authPage.loginNs('admin/foo'); }); - // confirm we're in admin/foo - assert.dom('[data-test-badge-namespace]').hasText('foo'); - await click(ts.navLink('Secrets Sync')); - await click(ts.overview.optInBanner.enable); - await click(ts.overview.activationModal.checkbox); - await click(ts.overview.activationModal.confirm); - }); + test('it should make activation-flag requests to correct namespace', async function (assert) { + assert.expect(3); - test('it should make activation-flag requests to correct namespace when managed', async function (assert) { - assert.expect(3); - this.owner.lookup('service:flags').featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE']; - - this.server.get('/sys/activation-flags', (_, req) => { - assert.deepEqual(req.requestHeaders, {}, 'Request is unauthenticated and in root namespace'); - return { - data: { - activated: [''], - unactivated: ['secrets-sync'], - }, - }; - }); - this.server.post('/sys/activation-flags/secrets-sync/activate', (_, req) => { - assert.strictEqual( - req.requestHeaders['X-Vault-Namespace'], - 'admin', - 'Request is made to the admin namespace' - ); - return {}; + this.server.get('/sys/activation-flags', (_, req) => { + assert.deepEqual(req.requestHeaders, {}, 'Request is unauthenticated and in root namespace'); + return { + data: { + activated: [''], + unactivated: ['secrets-sync'], + }, + }; + }); + this.server.post('/sys/activation-flags/secrets-sync/activate', (_, req) => { + assert.strictEqual( + req.requestHeaders['X-Vault-Namespace'], + undefined, + 'Request is made to undefined namespace' + ); + return {}; + }); + + // confirm we're in admin/foo + assert.dom('[data-test-badge-namespace]').hasText('foo'); + await click(ts.navLink('Secrets Sync')); + await click(ts.overview.optInBanner.enable); + await click(ts.overview.activationModal.checkbox); + await click(ts.overview.activationModal.confirm); }); - // confirm we're in admin/foo - assert.dom('[data-test-badge-namespace]').hasText('foo'); + test('it should make activation-flag requests to correct namespace when managed', async function (assert) { + assert.expect(3); + this.owner.lookup('service:flags').featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE']; - await click(ts.navLink('Secrets Sync')); - await click(ts.overview.optInBanner.enable); - await click(ts.overview.activationModal.checkbox); - await click(ts.overview.activationModal.confirm); + this.server.get('/sys/activation-flags', (_, req) => { + assert.deepEqual(req.requestHeaders, {}, 'Request is unauthenticated and in root namespace'); + return { + data: { + activated: [''], + unactivated: ['secrets-sync'], + }, + }; + }); + this.server.post('/sys/activation-flags/secrets-sync/activate', (_, req) => { + assert.strictEqual( + req.requestHeaders['X-Vault-Namespace'], + 'admin', + 'Request is made to the admin namespace' + ); + return {}; + }); + + // confirm we're in admin/foo + assert.dom('[data-test-badge-namespace]').hasText('foo'); + + await click(ts.navLink('Secrets Sync')); + await click(ts.overview.optInBanner.enable); + await click(ts.overview.activationModal.checkbox); + await click(ts.overview.activationModal.confirm); + }); }); }); }); From 7fc2fd7c50609039b22d7091233576c0d507e073 Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Thu, 30 May 2024 09:41:21 -0600 Subject: [PATCH 09/21] add additional api permission path and move fetch back to application --- ui/app/routes/application.js | 2 +- ui/app/routes/vault/cluster.js | 1 - ui/app/services/permissions.js | 9 +++------ 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/ui/app/routes/application.js b/ui/app/routes/application.js index 34ba1e2719c7..0d7bc2823b5d 100644 --- a/ui/app/routes/application.js +++ b/ui/app/routes/application.js @@ -68,7 +68,7 @@ export default Route.extend({ async beforeModel() { // activatedFlags are called this high in routing to return a response used to show/hide Secrets sync on sidebar nav. // featureFlags are called this high in routing to determine isHvdManaged things, etc. - // await this.flagsService.fetchActivatedFlags(); + await this.flagsService.fetchActivatedFlags(); await this.flagsService.fetchFeatureFlags(); }, }); diff --git a/ui/app/routes/vault/cluster.js b/ui/app/routes/vault/cluster.js index ed839cbaebd6..7d0cbf122432 100644 --- a/ui/app/routes/vault/cluster.js +++ b/ui/app/routes/vault/cluster.js @@ -55,7 +55,6 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, { }, async beforeModel() { - await this.flagsService.fetchActivatedFlags(); const params = this.paramsFor(this.routeName); let namespace = params.namespaceQueryParam; const currentTokenName = this.auth.currentTokenName; diff --git a/ui/app/services/permissions.js b/ui/app/services/permissions.js index 07f0f724010d..f68fb9a0a6fe 100644 --- a/ui/app/services/permissions.js +++ b/ui/app/services/permissions.js @@ -53,6 +53,7 @@ const API_PATHS = { destinations: 'sys/sync/destinations', associations: 'sys/sync/associations', config: 'sys/sync/config', + github: 'sys/sync/github-apps', }, }; @@ -192,11 +193,7 @@ export default class PermissionsService extends Service { return this.hasPermission(API_PATHS[navItem][param], capability); }); } - return Object.values(API_PATHS[navItem]).some((path) => { - const test = this.hasPermission(path); - - return test; - }); + return Object.values(API_PATHS[navItem]).some((path) => this.hasPermission(path)); } navPathParams(navItem) { @@ -222,6 +219,7 @@ export default class PermissionsService extends Service { return true; } const path = this.pathNameWithNamespace(pathName); + return capabilities.every( (capability) => this.hasMatchingExactPath(path, capability) || this.hasMatchingGlobPath(path, capability) @@ -245,7 +243,6 @@ export default class PermissionsService extends Service { hasMatchingGlobPath(pathName, capability) { const globPaths = this.globPaths; - if (globPaths) { const matchingPath = Object.keys(globPaths).find((k) => { return pathName.includes(k) || pathName.includes(k.replace(/\/$/, '')); From 36fbb78b5b680a8e2f0f70d316c3d6dbfe006118 Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Thu, 30 May 2024 11:26:05 -0600 Subject: [PATCH 10/21] add test coverage for the service --- ui/app/routes/application.js | 3 - ui/app/routes/vault/cluster.js | 5 +- ui/app/services/flags.ts | 1 + .../components/sidebar/nav/cluster-test.js | 27 +----- ui/tests/unit/services/flags-test.js | 93 +++++++++++++++++-- 5 files changed, 93 insertions(+), 36 deletions(-) diff --git a/ui/app/routes/application.js b/ui/app/routes/application.js index 0d7bc2823b5d..b3d134e6651b 100644 --- a/ui/app/routes/application.js +++ b/ui/app/routes/application.js @@ -66,9 +66,6 @@ export default Route.extend({ }, async beforeModel() { - // activatedFlags are called this high in routing to return a response used to show/hide Secrets sync on sidebar nav. - // featureFlags are called this high in routing to determine isHvdManaged things, etc. - await this.flagsService.fetchActivatedFlags(); await this.flagsService.fetchFeatureFlags(); }, }); diff --git a/ui/app/routes/vault/cluster.js b/ui/app/routes/vault/cluster.js index 7d0cbf122432..8b153953f0a6 100644 --- a/ui/app/routes/vault/cluster.js +++ b/ui/app/routes/vault/cluster.js @@ -59,11 +59,14 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, { let namespace = params.namespaceQueryParam; const currentTokenName = this.auth.currentTokenName; const managedRoot = this.flagsService.hvdManagedNamespaceRoot; - assert( 'Cannot use VAULT_CLOUD_ADMIN_NAMESPACE flag with non-enterprise Vault version', !(managedRoot && this.version.isCommunity) ); + + // activatedFlags are called this high in routing to return a response used to show/hide Secrets sync on sidebar nav. + await this.flagsService.fetchActivatedFlags(); + if (!namespace && currentTokenName && !Ember.testing) { // if no namespace queryParam and user authenticated, // use user's root namespace to redirect to properly param'd url diff --git a/ui/app/services/flags.ts b/ui/app/services/flags.ts index 67164d0b9d2e..99a7a466bb69 100644 --- a/ui/app/services/flags.ts +++ b/ui/app/services/flags.ts @@ -64,6 +64,7 @@ export default class flagsService extends Service { getActivatedFlags = keepLatestTask(async () => { // Response could change between user sessions. // Fire off endpoint without checking if activated features are already set. + if (this.version.isCommunity) return; try { const response = await this.store .adapterFor('application') diff --git a/ui/tests/integration/components/sidebar/nav/cluster-test.js b/ui/tests/integration/components/sidebar/nav/cluster-test.js index 9e76b280b12e..f36209c96eae 100644 --- a/ui/tests/integration/components/sidebar/nav/cluster-test.js +++ b/ui/tests/integration/components/sidebar/nav/cluster-test.js @@ -52,7 +52,7 @@ module('Integration | Component | sidebar-nav-cluster', function (hooks) { assert .dom('[data-test-sidebar-nav-link]') - .exists({ count: 3 }, 'Nav links are hidden other than secrets, secrets sync and dashboard'); + .exists({ count: 2 }, 'Nav links are hidden other than secrets and dashboard'); assert .dom('[data-test-sidebar-nav-heading]') .exists({ count: 1 }, 'Headings are hidden other than Vault'); @@ -84,31 +84,6 @@ module('Integration | Component | sidebar-nav-cluster', function (hooks) { }); }); - test('it should render badge for promotional links on community version', async function (assert) { - const promotionalLinks = ['Secrets Sync']; - // if no features passed, it defaults to all features and we need to specifically remove Secrets Sync - stubFeaturesAndPermissions(this.owner, false, true, []); - await renderComponent(); - - promotionalLinks.forEach((link) => { - assert - .dom(`[data-test-sidebar-nav-link="${link}"]`) - .hasText(`${link} Enterprise`, `${link} link renders Enterprise badge`); - }); - }); - - test('it should render badge for promotional links on enterprise version', async function (assert) { - const promotionalLinks = ['Secrets Sync']; - stubFeaturesAndPermissions(this.owner, true, true, ['Namespaces']); - await renderComponent(); - - promotionalLinks.forEach((link) => { - assert - .dom(`[data-test-sidebar-nav-link="${link}"]`) - .hasText(`${link} Premium`, `${link} link renders Premium badge`); - }); - }); - test('it should hide enterprise related links in child namespace', async function (assert) { const links = [ 'Disaster Recovery', diff --git a/ui/tests/unit/services/flags-test.js b/ui/tests/unit/services/flags-test.js index 782a3fb34321..78eb153d0f0f 100644 --- a/ui/tests/unit/services/flags-test.js +++ b/ui/tests/unit/services/flags-test.js @@ -6,6 +6,7 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; +import sinon from 'sinon'; const ACTIVATED_FLAGS_RESPONSE = { data: { @@ -24,6 +25,8 @@ module('Unit | Service | flags', function (hooks) { hooks.beforeEach(function () { this.service = this.owner.lookup('service:flags'); + this.version = this.owner.lookup('service:version'); + this.permissions = this.owner.lookup('service:permissions'); }); test('it loads with defaults', function (assert) { @@ -33,7 +36,7 @@ module('Unit | Service | flags', function (hooks) { module('#fetchActivatedFlags', function (hooks) { hooks.beforeEach(function () { - this.owner.lookup('service:version').type = 'enterprise'; + this.version.type = 'enterprise'; }); test('it returns activated flags', async function (assert) { @@ -66,8 +69,12 @@ module('Unit | Service | flags', function (hooks) { assert.deepEqual(this.service.activatedFlags, [], 'Activated flags are empty'); }); - test('it returns an empty array if the cluster is OSS', async function (assert) { - this.owner.lookup('service:version').type = 'community'; + test('it does not call activation-flags endpoint if the cluster is OSS', async function (assert) { + this.version.type = 'community'; + + this.server.get('sys/activation-flags', () => { + assert.true(false, 'activation-flags is not called'); + }); await this.service.fetchActivatedFlags(); assert.deepEqual(this.service.activatedFlags, [], 'Activated flags are empty'); @@ -76,7 +83,7 @@ module('Unit | Service | flags', function (hooks) { module('#fetchFeatureFlags', function (hooks) { hooks.beforeEach(function () { - this.owner.lookup('service:version').type = 'enterprise'; + this.version.type = 'enterprise'; }); test('it returns feature flags', async function (assert) { @@ -119,9 +126,9 @@ module('Unit | Service | flags', function (hooks) { }); }); - module('#secretsSyncActivated', function (hooks) { + module('#secretsSyncIsActivated', function (hooks) { hooks.beforeEach(function () { - this.owner.lookup('service:version').type = 'enterprise'; + this.version.type = 'enterprise'; this.service.activatedFlags = ACTIVATED_FLAGS_RESPONSE.data.activated; }); @@ -134,4 +141,78 @@ module('Unit | Service | flags', function (hooks) { assert.false(this.service.secretsSyncIsActivated); }); }); + + module('#showSecretsSync', function () { + test('it returns false when version is community', function (assert) { + this.version.type = 'community'; + assert.false(this.service.showSecretsSync); + }); + + module('isHvdManaged', function (hooks) { + hooks.beforeEach(function () { + this.version.type = 'enterprise'; + this.service.featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE']; + }); + + test('it returns true when not activated', function (assert) { + this.service.activatedFlags = []; + assert.true(this.service.showSecretsSync); + }); + + test('it returns true when activated', function (assert) { + this.service.activatedFlags = ACTIVATED_FLAGS_RESPONSE.data.activated; + assert.true(this.service.showSecretsSync); + }); + }); + + module('is Enterprise', function (hooks) { + hooks.beforeEach(function () { + this.version.type = 'enterprise'; + }); + + test('it returns false when not on license ', function (assert) { + this.version.features = ['replication']; + assert.false(this.service.showSecretsSync); + }); + + module('no permissions to sys/sync', function (hooks) { + hooks.beforeEach(function () { + this.version.features = ['Secrets Sync']; + const hasNavPermission = sinon.stub(this.permissions, 'hasNavPermission'); + hasNavPermission.returns(false); + }); + + test('it returns false when activated ', function (assert) { + this.service.activatedFlags = ACTIVATED_FLAGS_RESPONSE.data.activated; + assert.false(this.service.showSecretsSync); + }); + + test('it returns true when not activated ', function (assert) { + // the activate endpoint is located at a different path than all sys/sync. + // the expected UX experience is that if the feature is not activated, regardless of permissions + // the user should see the landing page and a banner that tells them to either have an admin activate the feature or activate it themselves + this.service.activatedFlags = []; + assert.true(this.service.showSecretsSync); + }); + }); + + module('user has permissions to sys/sync', function (hooks) { + hooks.beforeEach(function () { + this.version.features = ['Secrets Sync']; + const hasNavPermission = sinon.stub(this.permissions, 'hasNavPermission'); + hasNavPermission.returns(true); + }); + + test('it returns true when activated ', function (assert) { + this.service.activatedFlags = ACTIVATED_FLAGS_RESPONSE.data.activated; + assert.true(this.service.showSecretsSync); + }); + + test('it returns true when not activated ', function (assert) { + this.service.activatedFlags = []; + assert.true(this.service.showSecretsSync); + }); + }); + }); + }); }); From 462d25843ca73d48060326ee9b242ad3f2c995c8 Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Thu, 30 May 2024 11:44:41 -0600 Subject: [PATCH 11/21] cleanup --- ui/app/routes/application.js | 4 ++-- ui/app/services/flags.ts | 3 +-- ui/tests/acceptance/clients/counts/sync-test.js | 1 + ui/tests/unit/services/flags-test.js | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ui/app/routes/application.js b/ui/app/routes/application.js index b3d134e6651b..4916222c4685 100644 --- a/ui/app/routes/application.js +++ b/ui/app/routes/application.js @@ -65,7 +65,7 @@ export default Route.extend({ }, }, - async beforeModel() { - await this.flagsService.fetchFeatureFlags(); + beforeModel() { + return this.flagsService.fetchFeatureFlags(); }, }); diff --git a/ui/app/services/flags.ts b/ui/app/services/flags.ts index 99a7a466bb69..ccacb50d9ed3 100644 --- a/ui/app/services/flags.ts +++ b/ui/app/services/flags.ts @@ -98,12 +98,11 @@ export default class flagsService extends Service { if (!isEnterprise) return false; if (isHvdManaged) return true; if (isEnterprise && !onLicense) return false; - // only remaining version is Enterprise with Secrets Sync on their license if (isActivated) { // if the feature is activated but the user does not have permissions on the `sys/sync` endpoint, hide navigation link. return this.permissions.hasNavPermission('sync'); } - // only remaining option now is Enterprise with Secrets Sync on the license but the feature is not activated. In this case, we want to show the upsell page and message about either activating or having an admin activate. + // only remaining option is Enterprise with Secrets Sync on the license but the feature is not activated. In this case, we want to show the upsell page and message about either activating or having an admin activate. return true; } } diff --git a/ui/tests/acceptance/clients/counts/sync-test.js b/ui/tests/acceptance/clients/counts/sync-test.js index 55ebf4708bc6..2506bb92aafc 100644 --- a/ui/tests/acceptance/clients/counts/sync-test.js +++ b/ui/tests/acceptance/clients/counts/sync-test.js @@ -59,6 +59,7 @@ module('Acceptance | clients | sync', function (hooks) { }); assert.dom(GENERAL.emptyStateTitle).exists('Shows empty state when secrets-sync is not activated'); + await click(`${GENERAL.emptyStateActions} .hds-link-standalone`); assert.strictEqual( currentURL(), diff --git a/ui/tests/unit/services/flags-test.js b/ui/tests/unit/services/flags-test.js index 78eb153d0f0f..9ff52f118492 100644 --- a/ui/tests/unit/services/flags-test.js +++ b/ui/tests/unit/services/flags-test.js @@ -188,8 +188,8 @@ module('Unit | Service | flags', function (hooks) { }); test('it returns true when not activated ', function (assert) { - // the activate endpoint is located at a different path than all sys/sync. - // the expected UX experience is that if the feature is not activated, regardless of permissions + // the activate endpoint is located at a different path than sys/sync. + // the expected UX experience: if the feature is not activated, regardless of permissions // the user should see the landing page and a banner that tells them to either have an admin activate the feature or activate it themselves this.service.activatedFlags = []; assert.true(this.service.showSecretsSync); From 9f1ab0954eead97c3d9570adbaf284e6d6145692 Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Thu, 30 May 2024 12:18:44 -0600 Subject: [PATCH 12/21] remove test that checked for upsell without license or on community --- .../clients/counts/overview-test.js | 2 +- .../sync/secrets/page/overview-test.js | 44 +------------------ 2 files changed, 2 insertions(+), 44 deletions(-) diff --git a/ui/tests/acceptance/clients/counts/overview-test.js b/ui/tests/acceptance/clients/counts/overview-test.js index 3bf134effa73..2251f64db892 100644 --- a/ui/tests/acceptance/clients/counts/overview-test.js +++ b/ui/tests/acceptance/clients/counts/overview-test.js @@ -236,7 +236,7 @@ module('Acceptance | clients | overview | sync in license, activated', function }); test('it should render the correct tabs', async function (assert) { - assert.dom(GENERAL.tab('sync')).exists(); + assert.dom(GENERAL.tab('sync')).exists('shows the sync tab'); }); test('it should show secrets sync stats', async function (assert) { diff --git a/ui/tests/integration/components/sync/secrets/page/overview-test.js b/ui/tests/integration/components/sync/secrets/page/overview-test.js index 8684f662a6e3..e4dddd3fdc2f 100644 --- a/ui/tests/integration/components/sync/secrets/page/overview-test.js +++ b/ui/tests/integration/components/sync/secrets/page/overview-test.js @@ -61,42 +61,12 @@ module('Integration | Component | sync | Page::Overview', function (hooks) { assert.dom(overview.createDestination).hasText('Create new destination', 'Toolbar action renders'); }); - module('community', function (hooks) { - hooks.beforeEach(function () { - this.version.type = 'community'; - this.isActivated = false; - this.licenseHasSecretsSync = false; - this.destinations = []; - }); - - test('it should show an upsell CTA', async function (assert) { - await this.renderComponent(); - - assert - .dom(title) - .hasText('Secrets Sync Enterprise feature', 'page title indicates feature is only for Enterprise'); - assert.dom(cta.button).doesNotExist(); - assert.dom(cta.summary).exists(); - }); - }); - module('ent', function (hooks) { hooks.beforeEach(function () { this.isActivated = false; this.destinations = []; }); - test('it should show an upsell CTA if license does NOT have the secrets sync feature', async function (assert) { - this.version.features = []; - await this.renderComponent(); - - assert - .dom(title) - .hasText('Secrets Sync Premium feature', 'title indicates feature is only for Premium'); - assert.dom(cta.button).doesNotExist(); - assert.dom(cta.summary).exists(); - }); - test('it should show create CTA if license has the secrets sync feature', async function (assert) { this.version.features = ['Secrets Sync']; this.isActivated = true; @@ -182,7 +152,7 @@ module('Integration | Component | sync | Page::Overview', function (hooks) { }); }); - module('secrets sync is not activated and license has secrets sync', function (hooks) { + module('secrets sync is not activated', function (hooks) { hooks.beforeEach(async function () { this.isActivated = false; }); @@ -263,18 +233,6 @@ module('Integration | Component | sync | Page::Overview', function (hooks) { }); }); - module('secrets sync is not activated and license does not have secrets sync', function (hooks) { - hooks.beforeEach(async function () { - this.licenseHasSecretsSync = false; - }); - - test('it should hide the opt-in banner', async function (assert) { - await this.renderComponent(); - - assert.dom(overview.optInBanner.container).doesNotExist(); - }); - }); - module('secrets sync is activated', function () { test('it should hide the opt-in banner', async function (assert) { await this.renderComponent(); From e4e10805cf20c36a6cae23438a2d1b78190bf919 Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Thu, 30 May 2024 12:22:52 -0600 Subject: [PATCH 13/21] small comment change --- ui/tests/acceptance/clients/counts/sync-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/tests/acceptance/clients/counts/sync-test.js b/ui/tests/acceptance/clients/counts/sync-test.js index 2506bb92aafc..8f0f3a8b10ec 100644 --- a/ui/tests/acceptance/clients/counts/sync-test.js +++ b/ui/tests/acceptance/clients/counts/sync-test.js @@ -49,7 +49,7 @@ module('Acceptance | clients | sync', function (hooks) { this.server.get('/sys/activation-flags', () => { assert.true(true, '/sys/activation-flags/ is called to check if secrets-sync is activated'); - // called once from the higher level application route + // called once from the higher level cluster route return { data: { activated: [], From 3ec16462556b86c5a10aa7b801a00a7d2e06135f Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Thu, 30 May 2024 14:46:43 -0600 Subject: [PATCH 14/21] welp missed component getter --- ui/app/components/clients/page/counts.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/components/clients/page/counts.hbs b/ui/app/components/clients/page/counts.hbs index 59d00e5babf8..3c4ce83a4370 100644 --- a/ui/app/components/clients/page/counts.hbs +++ b/ui/app/components/clients/page/counts.hbs @@ -151,7 +151,7 @@ {{/if}} - + {{! CLIENT COUNT PAGE COMPONENTS RENDER HERE }} {{yield}} From 628ea6b85db354600b31db55dc441be4d15b1843 Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Thu, 30 May 2024 15:03:03 -0600 Subject: [PATCH 15/21] flaky test fix --- ui/tests/acceptance/sync/secrets/destination-test.js | 3 ++- ui/tests/acceptance/sync/secrets/destinations-test.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ui/tests/acceptance/sync/secrets/destination-test.js b/ui/tests/acceptance/sync/secrets/destination-test.js index 452e3b457891..0da28a2c0945 100644 --- a/ui/tests/acceptance/sync/secrets/destination-test.js +++ b/ui/tests/acceptance/sync/secrets/destination-test.js @@ -14,7 +14,7 @@ import { settled, click, visit, currentURL, fillIn, currentRouteName } from '@em import { PAGE as ts } from 'vault/tests/helpers/sync/sync-selectors'; // sync is an enterprise feature but since mirage is used the enterprise label has been intentionally omitted from the module name -module('Acceptance | sync | destination', function (hooks) { +module('Acceptance | sync | destination (singular)', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); @@ -35,6 +35,7 @@ module('Acceptance | sync | destination', function (hooks) { }); test('it should transition to correct routes when performing actions', async function (assert) { + await click(ts.navLink('Dashboard')); // add click to Dashboard to avoid race condition where Secrets Sync link does not show yet await click(ts.navLink('Secrets Sync')); await click(ts.tab('Destinations')); await click(ts.listItem); diff --git a/ui/tests/acceptance/sync/secrets/destinations-test.js b/ui/tests/acceptance/sync/secrets/destinations-test.js index 3cca6b7a4355..71b090d15df7 100644 --- a/ui/tests/acceptance/sync/secrets/destinations-test.js +++ b/ui/tests/acceptance/sync/secrets/destinations-test.js @@ -16,7 +16,7 @@ import { syncDestinations } from 'vault/helpers/sync-destinations'; const SYNC_DESTINATIONS = syncDestinations(); // sync is an enterprise feature but since mirage is used the enterprise label has been intentionally omitted from the module name -module('Acceptance | sync | destinations', function (hooks) { +module('Acceptance | sync | destinations (plural)', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); From dccc476e7e02dceddd5837d98065b62b32a4950c Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Thu, 30 May 2024 15:18:22 -0600 Subject: [PATCH 16/21] flaky test --- ui/tests/acceptance/sync/secrets/destination-test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ui/tests/acceptance/sync/secrets/destination-test.js b/ui/tests/acceptance/sync/secrets/destination-test.js index 0da28a2c0945..e0286428f952 100644 --- a/ui/tests/acceptance/sync/secrets/destination-test.js +++ b/ui/tests/acceptance/sync/secrets/destination-test.js @@ -35,8 +35,7 @@ module('Acceptance | sync | destination (singular)', function (hooks) { }); test('it should transition to correct routes when performing actions', async function (assert) { - await click(ts.navLink('Dashboard')); // add click to Dashboard to avoid race condition where Secrets Sync link does not show yet - await click(ts.navLink('Secrets Sync')); + await visit('/vault/sync/secrets/overview'); await click(ts.tab('Destinations')); await click(ts.listItem); assert.dom(ts.tab('Secrets')).hasClass('active', 'Secrets tab is active'); From 22ce68a1d04b8213870562647f5a69571e56b5cb Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Fri, 31 May 2024 10:36:46 -0600 Subject: [PATCH 17/21] small nit changes from pr reviews --- ui/app/components/clients/page/counts.hbs | 2 +- ui/lib/sync/addon/components/sync-header.hbs | 2 +- ui/tests/unit/services/flags-test.js | 10 +++++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/ui/app/components/clients/page/counts.hbs b/ui/app/components/clients/page/counts.hbs index 3c4ce83a4370..91c401bda7b1 100644 --- a/ui/app/components/clients/page/counts.hbs +++ b/ui/app/components/clients/page/counts.hbs @@ -151,7 +151,7 @@ {{/if}} - + {{! CLIENT COUNT PAGE COMPONENTS RENDER HERE }} {{yield}} diff --git a/ui/lib/sync/addon/components/sync-header.hbs b/ui/lib/sync/addon/components/sync-header.hbs index d75a814c34a6..afdcaccb8f41 100644 --- a/ui/lib/sync/addon/components/sync-header.hbs +++ b/ui/lib/sync/addon/components/sync-header.hbs @@ -17,7 +17,7 @@ {{/if}} {{@title}} {{#if this.flags.isHvdManaged}} - + {{/if}} diff --git a/ui/tests/unit/services/flags-test.js b/ui/tests/unit/services/flags-test.js index 9ff52f118492..610c60e8cbc0 100644 --- a/ui/tests/unit/services/flags-test.js +++ b/ui/tests/unit/services/flags-test.js @@ -72,9 +72,13 @@ module('Unit | Service | flags', function (hooks) { test('it does not call activation-flags endpoint if the cluster is OSS', async function (assert) { this.version.type = 'community'; - this.server.get('sys/activation-flags', () => { - assert.true(false, 'activation-flags is not called'); - }); + this.server.get( + 'sys/activation-flags', + () => + new Error( + 'uh oh! a request was made to sys/activation-flags, this should not happen for community versions' + ) + ); await this.service.fetchActivatedFlags(); assert.deepEqual(this.service.activatedFlags, [], 'Activated flags are empty'); From 6fd9b2195f85bff307450684ed477b14f7b98a3e Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Fri, 31 May 2024 12:10:17 -0600 Subject: [PATCH 18/21] add defaults to sync mirage handler --- ui/mirage/handlers/sync.js | 5 ++++- ui/mirage/helpers/modify-passthrough-response.js | 1 + ui/tests/acceptance/sync/secrets/destination-test.js | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ui/mirage/handlers/sync.js b/ui/mirage/handlers/sync.js index cf1eec263935..ae35dd35dcfc 100644 --- a/ui/mirage/handlers/sync.js +++ b/ui/mirage/handlers/sync.js @@ -7,6 +7,7 @@ import { Response } from 'miragejs'; import { camelize } from '@ember/string'; import { findDestination } from 'core/helpers/sync-destinations'; import clientsHandler from './clients'; +import modifyPassthroughResponse from '../helpers/modify-passthrough-response'; export const associationsResponse = (schema, req) => { const { type, name } = req.params; @@ -116,7 +117,9 @@ const createOrUpdateDestination = (schema, req) => { }; export default function (server) { - // default to activated + // default to enterprise with Secrets Sync on the license and activated + server.get('sys/health', (schema, req) => modifyPassthroughResponse(req, { enterprise: true })); + server.get('/sys/license/features', () => ({ features: ['Secrets Sync'] })); server.get('/sys/activation-flags', () => { return { data: { diff --git a/ui/mirage/helpers/modify-passthrough-response.js b/ui/mirage/helpers/modify-passthrough-response.js index c86a23eb07e5..b0ce762277ad 100644 --- a/ui/mirage/helpers/modify-passthrough-response.js +++ b/ui/mirage/helpers/modify-passthrough-response.js @@ -5,6 +5,7 @@ // passthrough request and modify response from server // pass object as second arg of properties in response to override +// ex: server.get('sys/health', (schema, req) => modifyPassthroughResponse(req, { enterprise: true })); export default function (req, props = {}) { return new Promise((resolve) => { const xhr = req.passthrough(); diff --git a/ui/tests/acceptance/sync/secrets/destination-test.js b/ui/tests/acceptance/sync/secrets/destination-test.js index e0286428f952..66404f0d1dd6 100644 --- a/ui/tests/acceptance/sync/secrets/destination-test.js +++ b/ui/tests/acceptance/sync/secrets/destination-test.js @@ -35,7 +35,7 @@ module('Acceptance | sync | destination (singular)', function (hooks) { }); test('it should transition to correct routes when performing actions', async function (assert) { - await visit('/vault/sync/secrets/overview'); + await click(ts.navLink('Secrets Sync')); await click(ts.tab('Destinations')); await click(ts.listItem); assert.dom(ts.tab('Secrets')).hasClass('active', 'Secrets tab is active'); From 51bf477d8b0cb5708c12c997b6eb1fd503a573fe Mon Sep 17 00:00:00 2001 From: Noelle Daley Date: Mon, 3 Jun 2024 10:05:38 -0700 Subject: [PATCH 19/21] Gate sync overview route for users without access (#27320) * routes: add redirect if user does not have access to sync * tests: verify redirect on sync overview page happens * tests: organize tests modules to ensure enterprise is explicitly set up --- ui/lib/sync/addon/routes/secrets/overview.ts | 8 + .../acceptance/sync/secrets/overview-test.js | 409 +++++++++++------- 2 files changed, 259 insertions(+), 158 deletions(-) diff --git a/ui/lib/sync/addon/routes/secrets/overview.ts b/ui/lib/sync/addon/routes/secrets/overview.ts index 52a81d1074bb..75b90ec50263 100644 --- a/ui/lib/sync/addon/routes/secrets/overview.ts +++ b/ui/lib/sync/addon/routes/secrets/overview.ts @@ -8,10 +8,12 @@ import { service } from '@ember/service'; import { hash } from 'rsvp'; import type FlagsService from 'vault/services/flags'; +import type RouterService from '@ember/routing/router-service'; import type StoreService from 'vault/services/store'; import type VersionService from 'vault/services/version'; export default class SyncSecretsOverviewRoute extends Route { + @service declare readonly router: RouterService; @service declare readonly store: StoreService; @service declare readonly flags: FlagsService; @service declare readonly version: VersionService; @@ -34,4 +36,10 @@ export default class SyncSecretsOverviewRoute extends Route { : [], }); } + + redirect() { + if (!this.flags.showSecretsSync) { + this.router.replaceWith('vault.cluster.dashboard'); + } + } } diff --git a/ui/tests/acceptance/sync/secrets/overview-test.js b/ui/tests/acceptance/sync/secrets/overview-test.js index 036c752aeaa3..10a81c88d1a6 100644 --- a/ui/tests/acceptance/sync/secrets/overview-test.js +++ b/ui/tests/acceptance/sync/secrets/overview-test.js @@ -8,6 +8,7 @@ import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; import syncScenario from 'vault/mirage/scenarios/sync'; import syncHandlers from 'vault/mirage/handlers/sync'; +import sinon from 'sinon'; import authPage from 'vault/tests/pages/auth'; import { click, waitFor, visit, currentURL } from '@ember/test-helpers'; import { PAGE as ts } from 'vault/tests/helpers/sync/sync-selectors'; @@ -20,195 +21,287 @@ module('Acceptance | sync | overview', function (hooks) { hooks.beforeEach(async function () { this.version = this.owner.lookup('service:version'); - this.version.features = ['Secrets Sync']; + this.permissions = this.owner.lookup('service:permissions'); }); - module('when feature is activated', function (hooks) { + module('ent', function (hooks) { hooks.beforeEach(async function () { - syncHandlers(this.server); - await authPage.login(); + this.version.type = 'enterprise'; }); - test('it fetches destinations and associations', async function (assert) { - assert.expect(2); - - this.server.get('/sys/sync/destinations', () => { - assert.true(true, 'destinations is called'); - }); - this.server.get('/sys/sync/associations', () => { - assert.true(true, 'associations is called'); + module('sync on license', function (hooks) { + hooks.beforeEach(async function () { + this.version.features = ['Secrets Sync']; }); - await visit('/vault/sync/secrets/overview'); - }); + module('when feature is activated', function (hooks) { + hooks.beforeEach(async function () { + syncHandlers(this.server); + await authPage.login(); + }); - module('when there are pre-existing destinations', function (hooks) { - hooks.beforeEach(async function () { - syncScenario(this.server); - await authPage.login(); - }); + test('it fetches destinations and associations', async function (assert) { + assert.expect(2); - test('it should transition to correct routes when performing actions', async function (assert) { - await click(ts.navLink('Secrets Sync')); - await click(ts.destinations.list.create); - await click(ts.createCancel); - await click(ts.overviewCard.actionLink('Create new')); - await click(ts.createCancel); - await waitFor(ts.overview.table.actionToggle(0)); - await click(ts.overview.table.actionToggle(0)); - await click(ts.overview.table.action('sync')); - await click(ts.destinations.sync.cancel); - await click(ts.breadcrumbLink('Secrets Sync')); - await waitFor(ts.overview.table.actionToggle(0)); - await click(ts.overview.table.actionToggle(0)); - await click(ts.overview.table.action('details')); - assert.dom(ts.tab('Secrets')).hasClass('active', 'Navigates to secrets view for destination'); - }); - }); - }); + this.server.get('/sys/sync/destinations', () => { + assert.true(true, 'destinations is called'); + }); + this.server.get('/sys/sync/associations', () => { + assert.true(true, 'associations is called'); + }); - module('when feature is not activated', function (hooks) { - hooks.beforeEach(async function () { - let wasActivatePOSTCalled = false; - // simulate the feature being activated once /secrets-sync/activate has been called - this.server.get('/sys/activation-flags', () => { - if (wasActivatePOSTCalled) { - return { - data: { - activated: ['secrets-sync'], - unactivated: [''], - }, - }; - } else { - return { - data: { - activated: [''], - unactivated: ['secrets-sync'], - }, - }; - } - }); + await visit('/vault/sync/secrets/overview'); + }); - this.server.post('/sys/activation-flags/secrets-sync/activate', () => { - wasActivatePOSTCalled = true; - return {}; - }); - await authPage.login(); - }); + module('when there are pre-existing destinations', function (hooks) { + hooks.beforeEach(async function () { + syncScenario(this.server); + await authPage.login(); + }); - test('it does not fetch destinations and associations', async function (assert) { - assert.expect(0); + test('it should transition to correct routes when performing actions', async function (assert) { + await click(ts.navLink('Secrets Sync')); + await click(ts.destinations.list.create); + await click(ts.createCancel); + await click(ts.overviewCard.actionLink('Create new')); + await click(ts.createCancel); + await waitFor(ts.overview.table.actionToggle(0)); + await click(ts.overview.table.actionToggle(0)); + await click(ts.overview.table.action('sync')); + await click(ts.destinations.sync.cancel); + await click(ts.breadcrumbLink('Secrets Sync')); + await waitFor(ts.overview.table.actionToggle(0)); + await click(ts.overview.table.actionToggle(0)); + await click(ts.overview.table.action('details')); + assert.dom(ts.tab('Secrets')).hasClass('active', 'Navigates to secrets view for destination'); + }); + }); - this.server.get('/sys/sync/destinations', () => { - assert.true(false, 'destinations is not called'); - }); - this.server.get('/sys/sync/associations', () => { - assert.true(false, 'associations is not called'); - }); + module('permissions', function () { + test('users without permissions - denies access to sync page', async function (assert) { + const hasNavPermission = sinon.stub(this.permissions, 'hasNavPermission'); + hasNavPermission.returns(false); - await visit('/vault/sync/secrets/overview'); - }); + await visit('/vault/sync/secrets/overview'); - test('the activation workflow works', async function (assert) { - await visit('/vault/sync/secrets/overview'); + assert.strictEqual(currentURL(), '/vault/dashboard', 'redirects to cluster dashboard route'); + }); - assert - .dom(ts.cta.button) - .doesNotExist('create first destination is not available until feature has been activated'); - - assert.dom(ts.overview.optInBanner.container).exists(); - await click(ts.overview.optInBanner.enable); - - assert - .dom(ts.overview.activationModal.container) - .exists('modal to opt-in and activate feature is shown'); - await click(ts.overview.activationModal.checkbox); - await click(ts.overview.activationModal.confirm); - - assert - .dom(ts.overview.activationModal.container) - .doesNotExist('modal is gone once activation has been submitted'); - assert - .dom(ts.overview.optInBanner.container) - .doesNotExist('opt-in banner is gone once activation has been submitted'); - - await click(ts.cta.button); - assert.strictEqual( - currentURL(), - '/vault/sync/secrets/destinations/create', - 'create new destination is available once feature is activated' - ); - }); + test('users with permissions - allows access to sync page', async function (assert) { + const hasNavPermission = sinon.stub(this.permissions, 'hasNavPermission'); + hasNavPermission.returns(true); - module('enterprise with namespaces', function (hooks) { - hooks.beforeEach(async function () { - this.version.features = ['Secrets Sync', 'Namespaces']; + await visit('/vault/sync/secrets/overview'); - await runCmd(`write sys/namespaces/admin -f`, false); - await authPage.loginNs('admin'); - await runCmd(`write sys/namespaces/foo -f`, false); - await authPage.loginNs('admin/foo'); + assert.strictEqual( + currentURL(), + '/vault/sync/secrets/overview', + 'stays on the sync overview route' + ); + }); + }); }); - test('it should make activation-flag requests to correct namespace', async function (assert) { - assert.expect(3); - - this.server.get('/sys/activation-flags', (_, req) => { - assert.deepEqual(req.requestHeaders, {}, 'Request is unauthenticated and in root namespace'); - return { - data: { - activated: [''], - unactivated: ['secrets-sync'], - }, - }; + module('when feature is not activated', function (hooks) { + hooks.beforeEach(async function () { + let wasActivatePOSTCalled = false; + // simulate the feature being activated once /secrets-sync/activate has been called + this.server.get('/sys/activation-flags', () => { + if (wasActivatePOSTCalled) { + return { + data: { + activated: ['secrets-sync'], + unactivated: [''], + }, + }; + } else { + return { + data: { + activated: [''], + unactivated: ['secrets-sync'], + }, + }; + } + }); + + this.server.post('/sys/activation-flags/secrets-sync/activate', () => { + wasActivatePOSTCalled = true; + return {}; + }); + await authPage.login(); }); - this.server.post('/sys/activation-flags/secrets-sync/activate', (_, req) => { + + test('it does not fetch destinations and associations', async function (assert) { + assert.expect(0); + + this.server.get('/sys/sync/destinations', () => { + assert.true(false, 'destinations is not called'); + }); + this.server.get('/sys/sync/associations', () => { + assert.true(false, 'associations is not called'); + }); + + await visit('/vault/sync/secrets/overview'); + }); + + test('the activation workflow works', async function (assert) { + await visit('/vault/sync/secrets/overview'); + + assert + .dom(ts.cta.button) + .doesNotExist('create first destination is not available until feature has been activated'); + + assert.dom(ts.overview.optInBanner.container).exists(); + await click(ts.overview.optInBanner.enable); + + assert + .dom(ts.overview.activationModal.container) + .exists('modal to opt-in and activate feature is shown'); + await click(ts.overview.activationModal.checkbox); + await click(ts.overview.activationModal.confirm); + + assert + .dom(ts.overview.activationModal.container) + .doesNotExist('modal is gone once activation has been submitted'); + assert + .dom(ts.overview.optInBanner.container) + .doesNotExist('opt-in banner is gone once activation has been submitted'); + + await click(ts.cta.button); assert.strictEqual( - req.requestHeaders['X-Vault-Namespace'], - undefined, - 'Request is made to undefined namespace' + currentURL(), + '/vault/sync/secrets/destinations/create', + 'create new destination is available once feature is activated' ); - return {}; }); - // confirm we're in admin/foo - assert.dom('[data-test-badge-namespace]').hasText('foo'); - await click(ts.navLink('Secrets Sync')); - await click(ts.overview.optInBanner.enable); - await click(ts.overview.activationModal.checkbox); - await click(ts.overview.activationModal.confirm); - }); + module('enterprise with namespaces', function (hooks) { + hooks.beforeEach(async function () { + this.version.features = ['Secrets Sync', 'Namespaces']; + + await runCmd(`write sys/namespaces/admin -f`, false); + await authPage.loginNs('admin'); + await runCmd(`write sys/namespaces/foo -f`, false); + await authPage.loginNs('admin/foo'); + }); + + test('it should make activation-flag requests to correct namespace', async function (assert) { + assert.expect(3); + + this.server.get('/sys/activation-flags', (_, req) => { + assert.deepEqual(req.requestHeaders, {}, 'Request is unauthenticated and in root namespace'); + return { + data: { + activated: [''], + unactivated: ['secrets-sync'], + }, + }; + }); + this.server.post('/sys/activation-flags/secrets-sync/activate', (_, req) => { + assert.strictEqual( + req.requestHeaders['X-Vault-Namespace'], + undefined, + 'Request is made to undefined namespace' + ); + return {}; + }); + + // confirm we're in admin/foo + assert.dom('[data-test-badge-namespace]').hasText('foo'); + await click(ts.navLink('Secrets Sync')); + await click(ts.overview.optInBanner.enable); + await click(ts.overview.activationModal.checkbox); + await click(ts.overview.activationModal.confirm); + }); + + test('it should make activation-flag requests to correct namespace when managed', async function (assert) { + assert.expect(3); + this.owner.lookup('service:flags').featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE']; - test('it should make activation-flag requests to correct namespace when managed', async function (assert) { - assert.expect(3); - this.owner.lookup('service:flags').featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE']; - - this.server.get('/sys/activation-flags', (_, req) => { - assert.deepEqual(req.requestHeaders, {}, 'Request is unauthenticated and in root namespace'); - return { - data: { - activated: [''], - unactivated: ['secrets-sync'], - }, - }; + this.server.get('/sys/activation-flags', (_, req) => { + assert.deepEqual(req.requestHeaders, {}, 'Request is unauthenticated and in root namespace'); + return { + data: { + activated: [''], + unactivated: ['secrets-sync'], + }, + }; + }); + this.server.post('/sys/activation-flags/secrets-sync/activate', (_, req) => { + assert.strictEqual( + req.requestHeaders['X-Vault-Namespace'], + 'admin', + 'Request is made to the admin namespace' + ); + return {}; + }); + + // confirm we're in admin/foo + assert.dom('[data-test-badge-namespace]').hasText('foo'); + + await click(ts.navLink('Secrets Sync')); + await click(ts.overview.optInBanner.enable); + await click(ts.overview.activationModal.checkbox); + await click(ts.overview.activationModal.confirm); + }); }); - this.server.post('/sys/activation-flags/secrets-sync/activate', (_, req) => { - assert.strictEqual( - req.requestHeaders['X-Vault-Namespace'], - 'admin', - 'Request is made to the admin namespace' - ); - return {}; + + module('permissions', function () { + test('users without permissions - allows access to sync page', async function (assert) { + const hasNavPermission = sinon.stub(this.permissions, 'hasNavPermission'); + hasNavPermission.returns(false); + + await visit('/vault/sync/secrets/overview'); + + assert.strictEqual( + currentURL(), + '/vault/sync/secrets/overview', + 'stays on the sync overview route' + ); + }); + + test('users with permissions - allows access to sync page', async function (assert) { + const hasNavPermission = sinon.stub(this.permissions, 'hasNavPermission'); + hasNavPermission.returns(true); + + await visit('/vault/sync/secrets/overview'); + + assert.strictEqual( + currentURL(), + '/vault/sync/secrets/overview', + 'stays on the sync overview route' + ); + }); }); + }); + }); - // confirm we're in admin/foo - assert.dom('[data-test-badge-namespace]').hasText('foo'); + module('sync NOT on license', function (hooks) { + hooks.beforeEach(async function () { + await authPage.login(); - await click(ts.navLink('Secrets Sync')); - await click(ts.overview.optInBanner.enable); - await click(ts.overview.activationModal.checkbox); - await click(ts.overview.activationModal.confirm); + // reset features *after* login, since the login process will set the initial value according to the actual license + this.version.features = []; }); + + test('it should not allow access to sync page', async function (assert) { + await visit('/vault/sync/secrets/overview'); + + assert.strictEqual(currentURL(), '/vault/dashboard', 'redirects to cluster dashboard route'); + }); + }); + }); + + module('oss', function (hooks) { + hooks.beforeEach(async function () { + this.version.type = 'community'; + await authPage.login(); + }); + + test('it should not allow access to sync page', async function (assert) { + await visit('/vault/sync/secrets/overview'); + + assert.strictEqual(currentURL(), '/vault/dashboard', 'redirects to cluster dashboard route'); }); }); }); From 8f0fed0e11fac8bd0834d85ccc696f3b6076ac96 Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Tue, 4 Jun 2024 10:07:59 -0600 Subject: [PATCH 20/21] add type enterprise required now because we do a check for this first --- ui/tests/acceptance/clients/counts/sync-test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/tests/acceptance/clients/counts/sync-test.js b/ui/tests/acceptance/clients/counts/sync-test.js index 8f0f3a8b10ec..f94dcf241cb5 100644 --- a/ui/tests/acceptance/clients/counts/sync-test.js +++ b/ui/tests/acceptance/clients/counts/sync-test.js @@ -22,6 +22,8 @@ module('Acceptance | clients | sync', function (hooks) { hooks.beforeEach(async function () { sinon.replace(timestamp, 'now', sinon.fake.returns(STATIC_NOW)); syncHandler(this.server); + const version = this.owner.lookup('service:version'); + version.type = 'enterprise'; await authPage.login(); return visit('/vault/clients/counts/sync'); }); From e8bf2c4aa3d9acdca5473dc543a95f1e5e4e2e59 Mon Sep 17 00:00:00 2001 From: Angel Garbarino Date: Mon, 10 Jun 2024 09:29:07 -0600 Subject: [PATCH 21/21] fix oss test --- ui/tests/acceptance/clients/counts/sync-test.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ui/tests/acceptance/clients/counts/sync-test.js b/ui/tests/acceptance/clients/counts/sync-test.js index f94dcf241cb5..82868d79b234 100644 --- a/ui/tests/acceptance/clients/counts/sync-test.js +++ b/ui/tests/acceptance/clients/counts/sync-test.js @@ -36,12 +36,21 @@ module('Acceptance | clients | sync', function (hooks) { }); }); - module('sync not activated', function (hooks) { + module('sync not activated and on license', function (hooks) { hooks.beforeEach(async function () { this.server.get('/sys/internal/counters/config', function () { return CONFIG_RESPONSE; }); sinon.replace(timestamp, 'now', sinon.fake.returns(STATIC_NOW)); + syncHandler(this.server); + server.get('/sys/activation-flags', () => { + return { + data: { + activated: [''], + unactivated: ['secrets-sync'], + }, + }; + }); await authPage.login(); return visit('/vault/clients/counts/sync'); });