Skip to content

Commit

Permalink
Enabling Secrets Sync for HVD (#26841)
Browse files Browse the repository at this point in the history
* Allow Managed clusters to see Secrets Sync Overview and Sidebar nav (#26649)

* update badge text and allow hvd on secrets sync views

* update logic in Secrets Sync overview and cta for hvd.

* spacing

* rearrange based on pr feedback

* fix return on badgeText and cluster nav test

* fix landing cta tests

* update test to reflect new changes

* moved call to feature-flags from application route to the service to match patterns

* add managed test coverage on overview component test and remove premium feature so cta message appplies to both managed and non-managed clusters

* missed service name and unskip admin test

* clean up

* fix tests

* flags test fix

* Rename isManaged and managedNamespaceRoot (#26697)

* renames

* lowercase HVD to match

* missed some

* test failure

* [Secrets Sync] enable access to Sync clients page for HVD clusters (#26713)

* feat: split client counts navbar into separate component

* acceptance/clients/counts/overview-test: remove tests now covered by int tests

* clients counts route: rename isSecretsSyncActivated to showSecretsSync

* sync clients page: show unactivated state unless sync client history or feature is activated

* client counts navbar: show sync tab only if client history or is /able to be/ activated

* clients overview page: only show sync charts if activated

* fix: rename isManaged to isHvd

* acceptance/counts/overview-test: add HVD tests

* acceptance/counts/overview-test: clean up unused cruft

* aceptance/clients/counts/overview-test: ensure we dont get false negatives

* chore: move Clients::Error to Clients::Counts::Error

* chore: calculate showSecretSync in page component instead of route

* chore: add copyright headers

* acceptance/clients/counts/overview-test: stub activated flags to fix test

* [Secrets sync] update sync test selectors (#26824)

* acceptance/clients/counts/overview-test: use imported test selectors

* general-selectors: add missing emptyStateSubtitle property

* acceptance/clients/counts/sync: nest tests in top level module for easier test runs

* Add permissions check to show/hide activate button (#26840)

* add permissions check to flags service and consume in overview template

* add back missing refresh

* fix test failures

* add test coverage

* clean up

* address flaky test

* grr

* address test failures

* add changelog

* try to fix test failure only on gh

* fix fetch to match previous implementation of feature-flags

* fix failing test

* update comment

---------

Co-authored-by: Noelle Daley <[email protected]>
Co-authored-by: [email protected] <[email protected]>
  • Loading branch information
3 people authored May 9, 2024
1 parent a7b6f34 commit 077c70f
Show file tree
Hide file tree
Showing 48 changed files with 696 additions and 514 deletions.
3 changes: 3 additions & 0 deletions changelog/26841.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
ui (enterprise): Allow HVD users to access Secrets Sync.
```
2 changes: 1 addition & 1 deletion ui/app/components/auth-jwt.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export default Component.extend({
// The namespace can be either be passed as a query parameter, or be embedded
// in the state param in the format `<state_id>,ns=<namespace>`. So if
// `namespace` is empty, check for namespace in state as well.
if (namespace === '' || this.flagsService.managedNamespaceRoot) {
if (namespace === '' || this.flagsService.hvdManagedNamespaceRoot) {
const i = state.indexOf(',ns=');
if (i >= 0) {
// ",ns=" is 4 characters
Expand Down
1 change: 0 additions & 1 deletion ui/app/components/clients/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import type {
} from 'core/utils/client-count-utils';

interface Args {
isSecretsSyncActivated?: boolean;
activity: ClientsActivityModel;
versionHistory: ClientsVersionHistoryModel[];
startTimestamp: number;
Expand Down
1 change: 1 addition & 0 deletions ui/app/components/clients/attribution.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { format, isSameMonth } from 'date-fns';
* @param {string} responseTimestamp - ISO timestamp created in serializer to timestamp the response, renders in bottom left corner below attribution chart
* @param {boolean} isHistoricalMonth - when true data is from a single, historical month so side-by-side charts should display for attribution data
* @param {array} upgradesDuringActivity - array of objects containing version history upgrade data
* @param {boolean} isSecretsSyncActivated - boolean to determine if secrets sync is activated
*/

export default class Attribution extends Component {
Expand Down
File renamed without changes.
31 changes: 31 additions & 0 deletions ui/app/components/clients/counts/nav-bar.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}

<nav class="tabs has-bottom-margin-s" aria-label="navigation for managing client counts">
<ul>
<li>
<LinkTo @route="vault.cluster.clients.counts.overview" data-test-tab="overview">
Overview
</LinkTo>
</li>
<li>
<LinkTo @route="vault.cluster.clients.counts.token" data-test-tab="token">
Entity/Non-entity clients
</LinkTo>
</li>
{{#if @showSecretsSync}}
<li>
<LinkTo @route="vault.cluster.clients.counts.sync" data-test-tab="sync">
Secrets sync clients
</LinkTo>
</li>
{{/if}}
<li>
<LinkTo @route="vault.cluster.clients.counts.acme" data-test-tab="acme">
ACME clients
</LinkTo>
</li>
</ul>
</nav>
29 changes: 2 additions & 27 deletions ui/app/components/clients/page/counts.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
{{#if (eq @activity.id "no-data")}}
<Clients::NoData @config={{@config}} @dateRangeMessage={{this.dateRangeMessage}} />
{{else if @activityError}}
<Clients::Error @error={{@activityError}} />
<Clients::Counts::Error @error={{@activityError}} />
{{else}}
{{#if (eq @config.enabled "Off")}}
<Hds::Alert @type="inline" @color="warning" class="has-bottom-margin-s" as |A|>
Expand Down Expand Up @@ -151,32 +151,7 @@
</Hds::Alert>
{{/if}}

<nav class="tabs has-bottom-margin-s" aria-label="navigation for managing client counts">
<ul>
<li>
<LinkTo @route="vault.cluster.clients.counts.overview" data-test-tab="overview">
Overview
</LinkTo>
</li>
<li>
<LinkTo @route="vault.cluster.clients.counts.token" data-test-tab="token">
Entity/Non-entity clients
</LinkTo>
</li>
{{#if this.version.hasSecretsSync}}
<li>
<LinkTo @route="vault.cluster.clients.counts.sync" data-test-tab="sync">
Secrets sync clients
</LinkTo>
</li>
{{/if}}
<li>
<LinkTo @route="vault.cluster.clients.counts.acme" data-test-tab="acme">
ACME clients
</LinkTo>
</li>
</ul>
</nav>
<Clients::Counts::NavBar @showSecretsSync={{this.showSecretsSync}} />

{{! CLIENT COUNT PAGE COMPONENTS RENDER HERE }}
{{yield}}
Expand Down
18 changes: 18 additions & 0 deletions ui/app/components/clients/page/counts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { filterVersionHistory, formatDateObject } from 'core/utils/client-count-
import timestamp from 'core/utils/timestamp';

import type AdapterError from '@ember-data/adapter';
import type FlagsService from 'vault/services/flags';
import type StoreService from 'vault/services/store';
import type VersionService from 'vault/services/version';
import type ClientsActivityModel from 'vault/models/clients/activity';
Expand All @@ -30,6 +31,7 @@ interface Args {
}

export default class ClientsCountsPageComponent extends Component<Args> {
@service declare readonly flags: FlagsService;
@service declare readonly version: VersionService;
@service declare readonly store: StoreService;

Expand Down Expand Up @@ -165,6 +167,22 @@ export default class ClientsCountsPageComponent extends Component<Args> {
return activity?.total;
}

get showSecretsSync(): 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;
}

@action
onDateChange(dateObject: { dateType: string; monthIdx: number; year: number }) {
const { dateType, monthIdx, year } = dateObject;
Expand Down
4 changes: 2 additions & 2 deletions ui/app/components/clients/page/overview.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
~}}

<Clients::RunningTotal
@isSecretsSyncActivated={{@isSecretsSyncActivated}}
@isSecretsSyncActivated={{this.flags.secretsSyncIsActivated}}
@byMonthActivityData={{this.byMonthActivityData}}
@isHistoricalMonth={{and (not this.isCurrentMonth) (not this.isDateRange)}}
@isCurrentMonth={{this.isCurrentMonth}}
Expand All @@ -15,7 +15,7 @@
/>
{{#if this.hasAttributionData}}
<Clients::Attribution
@isSecretsSyncActivated={{@isSecretsSyncActivated}}
@isSecretsSyncActivated={{this.flags.secretsSyncIsActivated}}
@totalUsageCounts={{this.totalUsageCounts}}
@newUsageCounts={{this.newClientCounts}}
@totalClientAttribution={{this.totalClientAttribution}}
Expand Down
6 changes: 5 additions & 1 deletion ui/app/components/clients/page/overview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,9 @@
*/

import ActivityComponent from '../activity';
import { service } from '@ember/service';
import type FlagsService from 'vault/services/flags';

export default class ClientsOverviewPageComponent extends ActivityComponent {}
export default class ClientsOverviewPageComponent extends ActivityComponent {
@service declare readonly flags: FlagsService;
}
2 changes: 1 addition & 1 deletion ui/app/components/clients/page/sync.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
{{#if @isSecretsSyncActivated}}
{{#if this.flags.secretsSyncIsActivated}}
{{#if (not this.byMonthActivityData)}}
{{! byMonthActivityData is an empty array if there is no monthly data (monthly breakdown was added in 1.11)
this means the user has queried dates before sync clients existed. we render an empty state instead of
Expand Down
5 changes: 4 additions & 1 deletion ui/app/components/clients/page/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
*/

import ActivityComponent from '../activity';

import { service } from '@ember/service';
import type FlagsService from 'vault/services/flags';
export default class SyncComponent extends ActivityComponent {
@service declare readonly flags: FlagsService;

title = 'Secrets sync usage';
description =
'This data can be used to understand how many secrets sync clients have been used for this date range. Each Vault secret that is synced to at least one destination counts as one Vault client.';
Expand Down
14 changes: 6 additions & 8 deletions ui/app/components/sidebar/nav/cluster.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,12 @@
@text="Secrets Engines"
data-test-sidebar-nav-link="Secrets Engines"
/>
{{#if this.showSync}}
<Nav.Link
@route="vault.cluster.sync"
@text="Secrets Sync"
@badge={{this.syncBadge}}
data-test-sidebar-nav-link="Secrets Sync"
/>
{{/if}}
<Nav.Link
@route="vault.cluster.sync"
@text="Secrets Sync"
@badge={{this.badgeText}}
data-test-sidebar-nav-link="Secrets Sync"
/>
{{#if (has-permission "access")}}
<Nav.Link
@route={{get (route-params-for "access") "route"}}
Expand Down
17 changes: 9 additions & 8 deletions ui/app/components/sidebar/nav/cluster.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ export default class SidebarNavClusterComponent extends Component {
return this.namespace.inRootNamespace && !this.cluster?.hasChrootNamespace;
}

get showSync() {
// Only show sync if cluster is not managed
return this.flags.managedNamespaceRoot === null;
}
get badgeText() {
const isHvdManaged = this.flags.isHvdManaged;
const onLicense = this.version.hasSecretsSync;
const isEnterprise = this.version.isEnterprise;

get syncBadge() {
if (this.version.isCommunity) return 'Enterprise';
if (!this.version.hasSecretsSync) return 'Premium';
return undefined;
if (isHvdManaged) return 'Plus';
if (isEnterprise && !onLicense) return 'Premium';
if (!isEnterprise) return 'Enterprise';
// no badge for Enterprise clusters with Secrets Sync on their license--the only remaining option.
return '';
}
}
8 changes: 4 additions & 4 deletions ui/app/controllers/vault/cluster/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ export default Controller.extend({
namespaceQueryParam: alias('clusterController.namespaceQueryParam'),
wrappedToken: alias('vaultController.wrappedToken'),
redirectTo: alias('vaultController.redirectTo'),
managedNamespaceRoot: alias('flagsService.managedNamespaceRoot'),
hvdManagedNamespaceRoot: alias('flagsService.hvdManagedNamespaceRoot'),
authMethod: '',
oidcProvider: '',

get namespaceInput() {
const namespaceQP = this.clusterController.namespaceQueryParam;
if (this.managedNamespaceRoot) {
if (this.hvdManagedNamespaceRoot) {
// When managed, the user isn't allowed to edit the prefix `admin/` for their nested namespace
const split = namespaceQP.split('/');
if (split.length > 1) {
Expand All @@ -42,8 +42,8 @@ export default Controller.extend({

fullNamespaceFromInput(value) {
const strippedNs = sanitizePath(value);
if (this.managedNamespaceRoot) {
return `${this.managedNamespaceRoot}/${strippedNs}`;
if (this.hvdManagedNamespaceRoot) {
return `${this.hvdManagedNamespaceRoot}/${strippedNs}`;
}
return strippedNs;
},
Expand Down
11 changes: 2 additions & 9 deletions ui/app/routes/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,7 @@ export default Route.extend({
},
},

async beforeModel() {
const result = await fetch('/v1/sys/internal/ui/feature-flags', {
method: 'GET',
});
if (result.status === 200) {
const body = await result.json();
const flags = body.feature_flags || [];
this.flagsService.setFeatureFlags(flags);
}
beforeModel() {
return this.flagsService.fetchFeatureFlags();
},
});
2 changes: 1 addition & 1 deletion ui/app/routes/vault/cluster.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
const params = this.paramsFor(this.routeName);
let namespace = params.namespaceQueryParam;
const currentTokenName = this.auth.currentTokenName;
const managedRoot = this.flagsService.managedNamespaceRoot;
const managedRoot = this.flagsService.hvdManagedNamespaceRoot;
assert(
'Cannot use VAULT_CLOUD_ADMIN_NAMESPACE flag with non-enterprise Vault version',
!(managedRoot && this.version.isCommunity)
Expand Down
43 changes: 7 additions & 36 deletions ui/app/routes/vault/cluster/clients/counts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@

import Route from '@ember/routing/route';
import { service } from '@ember/service';
import { DEBUG } from '@glimmer/env';
import timestamp from 'core/utils/timestamp';
import { getUnixTime } from 'date-fns';

import type FlagsService from 'vault/services/flags';
import type StoreService from 'vault/services/store';
import type VersionService from 'vault/services/version';

import type { ModelFrom } from 'vault/vault/route';
import type ClientsRoute from '../clients';
import type ClientsActivityModel from 'vault/models/clients/activity';
import type ClientsConfigModel from 'vault/models/clients/config';
import type ClientsCountsController from 'vault/controllers/vault/cluster/clients/counts';
import { setStartTimeQuery } from 'core/utils/client-count-utils';

Expand All @@ -25,14 +25,8 @@ export interface ClientsCountsRouteParams {
mountPath?: string | undefined;
}

interface ActivationFlagsResponse {
data: {
activated: Array<string>;
unactivated: Array<string>;
};
}

export default class ClientsCountsRoute extends Route {
@service declare readonly flags: FlagsService;
@service declare readonly store: StoreService;
@service declare readonly version: VersionService;

Expand All @@ -43,6 +37,10 @@ export default class ClientsCountsRoute extends Route {
mountPath: { refreshModel: false, replace: true },
};

beforeModel() {
return this.flags.fetchActivatedFlags();
}

async getActivity(start_time: number | null, end_time: number) {
let activity, activityError;
// if there is no start_time we want the user to manually choose a date
Expand All @@ -60,30 +58,6 @@ export default class ClientsCountsRoute extends Route {
return { activity, activityError };
}

async getActivatedFeatures() {
try {
const resp: ActivationFlagsResponse = await this.store
.adapterFor('application')
.ajax('/v1/sys/activation-flags', 'GET', { unauthenticated: true, namespace: null });
return resp.data?.activated;
} catch (error) {
if (DEBUG) console.error(error); // eslint-disable-line no-console
return [];
}
}

async isSecretsSyncActivated(activity: ClientsActivityModel | undefined) {
// if there are secrets, the feature is activated
if (activity && activity.total?.secret_syncs > 0) return true;

// if feature is not in license, it's definitely not activated
if (!this.version.hasSecretsSync) return false;

// otherwise check explicitly if the feature has been activated
const activatedFeatures = await this.getActivatedFeatures();
return activatedFeatures.includes('secrets-sync');
}

async model(params: ClientsCountsRouteParams) {
const { config, versionHistory } = this.modelFor('vault.cluster.clients') as ModelFrom<ClientsRoute>;
// only enterprise versions will have a relevant billing start date, if null users must select initial start time
Expand All @@ -93,14 +67,11 @@ export default class ClientsCountsRoute extends Route {
const endTimestamp = Number(params.end_time) || getUnixTime(timestamp.now());
const { activity, activityError } = await this.getActivity(startTimestamp, endTimestamp);

const isSecretsSyncActivated = await this.isSecretsSyncActivated(activity);

return {
activity,
activityError,
config,
endTimestamp,
isSecretsSyncActivated,
startTimestamp,
versionHistory,
};
Expand Down
Loading

0 comments on commit 077c70f

Please sign in to comment.