Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enabling Secrets Sync for HVD #26841

Merged
merged 17 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make sure I understand correctly -- HVD vault clusters will show the "Plus" badge next to this nav item, whether or not their cluster is plus tier?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. The UI will cannot tell if the hvd cluster is plus or not, so we're basically upselling it to 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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

much cleaner 👏

},
});
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
Loading