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

Add permissions check to show/hide activate button #26840

Merged
Show file tree
Hide file tree
Changes from 4 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
12 changes: 11 additions & 1 deletion ui/app/services/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Service, { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { keepLatestTask } from 'ember-concurrency';
import { DEBUG } from '@glimmer/env';
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
import type StoreService from 'vault/services/store';
import type VersionService from 'vault/services/version';

Expand All @@ -28,7 +29,7 @@ export default class flagsService extends Service {
@tracked featureFlags: string[] = [];

get isHvdManaged(): boolean {
return this.featureFlags.includes(FLAGS.vaultCloudNamespace);
return this.featureFlags?.includes(FLAGS.vaultCloudNamespace);
}

get hvdManagedNamespaceRoot(): string | null {
Expand Down Expand Up @@ -76,4 +77,13 @@ export default class flagsService extends Service {
fetchActivatedFlags() {
return this.getActivatedFlags.perform();
}

@lazyCapabilities(apiPath`sys/activation-flags/secrets-sync/activate`) secretsSyncActivatePath;

get canActivateSecretsSync() {
return (
this.secretsSyncActivatePath.get('canCreate') !== false ||
this.secretsSyncActivatePath.get('canUpdate') !== false
);
}
}
27 changes: 17 additions & 10 deletions ui/lib/sync/addon/components/secrets/page/overview.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,33 @@
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}

{{#unless @isActivated}}
{{#if (or @licenseHasSecretsSync @isHvdManaged)}}
{{#unless this.hideOptIn}}
{{! Allows users to dismiss activation banner if they have permissions to activate. }}
<Hds::Alert
@type="inline"
@color="warning"
@onDismiss={{fn (mut this.hideOptIn) true}}
@onDismiss={{if this.flags.canActivateSecretsSync (fn (mut this.hideOptIn) true) undefined}}
data-test-secrets-sync-opt-in-banner
as |A|
>
<A.Title>Enable Secrets Sync feature</A.Title>
<A.Description>To use this feature, specific activation is required. Please review the feature documentation and
enable it. If you're upgrading from beta, your previous data will be accessible after activation.</A.Description>
<A.Button
@text="Enable"
@color="secondary"
{{on "click" (fn (mut this.showActivateSecretsSyncModal) true)}}
data-test-secrets-sync-opt-in-banner-enable
/>
<A.Description data-test-secrets-sync-opt-in-banner-description>To use this feature, specific activation is required.
{{if
this.flags.canActivateSecretsSync
"Please review the feature documentation and
enable it. If you're upgrading from beta, your previous data will be accessible after activation."
"Please contact your administrator to activate."
}}</A.Description>
{{#if this.flags.canActivateSecretsSync}}
<A.Button
@text="Enable"
@color="secondary"
{{on "click" (fn (mut this.showActivateSecretsSyncModal) true)}}
data-test-secrets-sync-opt-in-banner-enable
/>
{{/if}}
</Hds::Alert>
{{/unless}}
{{/if}}
Expand Down
13 changes: 10 additions & 3 deletions ui/lib/sync/addon/components/secrets/page/overview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,24 @@ import type FlashMessageService from 'vault/services/flash-messages';
import type StoreService from 'vault/services/store';
import type RouterService from '@ember/routing/router-service';
import type VersionService from 'vault/services/version';
import type FlagsService from 'vault/services/flags';
import type { SyncDestinationAssociationMetrics } from 'vault/vault/adapters/sync/association';
import type SyncDestinationModel from 'vault/vault/models/sync/destination';

interface Args {
destinations: Array<SyncDestinationModel>;
totalVaultSecrets: number;
activatedFeatures: Array<string>;
isActivated: boolean;
licenseHasSecretsSync: boolean;
isHvdManaged: boolean;
}

export default class SyncSecretsDestinationsPageComponent extends Component<Args> {
@service declare readonly flashMessages: FlashMessageService;
@service declare readonly store: StoreService;
@service declare readonly router: RouterService;
@service declare readonly version: VersionService;
@service declare readonly flags: FlagsService;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

flag service is used in the hbs file


@tracked destinationMetrics: SyncDestinationAssociationMetrics[] = [];
@tracked page = 1;
Expand Down Expand Up @@ -71,11 +75,14 @@ export default class SyncSecretsDestinationsPageComponent extends Component<Args
@task
@waitFor
*onFeatureConfirm() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These changes below are a mix of issues from merging in main and a miss on my end of another PR into this side branch that mis-handled the namespace header in this post. The changes restore to what it should be, however we will do extensive testing with the backend to make sure the contract on namespaces is correct.

// must return null instead of root for non managed cluster.
const namespace = this.args.isHvdManaged ? 'admin' : null;
try {
yield this.store
.adapterFor('application')
.ajax('/v1/sys/activation-flags/secrets-sync/activate', 'POST');
this.router.transitionTo('vault.cluster.sync.secrets.overview');
.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();
} catch (error) {
this.error = errorMessage(error);
this.flashMessages.danger(`Error enabling feature \n ${errorMessage(error)}`);
Expand Down
7 changes: 4 additions & 3 deletions ui/tests/acceptance/sync/secrets/overview-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,8 @@ module('Acceptance | sync | overview', function (hooks) {
this.server.post('/sys/activation-flags/secrets-sync/activate', (_, req) => {
assert.strictEqual(
req.requestHeaders['X-Vault-Namespace'],
'admin/foo',
'Request is made to admin/foo namespace'
undefined,
'Request is made to undefined namespace'
);
return {};
});
Expand All @@ -178,7 +178,8 @@ module('Acceptance | sync | overview', function (hooks) {
});

test('it should make activation-flag requests to correct namespace when managed', async function (assert) {
assert.expect(3);
assert.expect(4);
// should call GET activation-flags twice because we need an updated response after activating the feature
this.owner.lookup('service:flags').featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE'];

this.server.get('/sys/activation-flags', (_, req) => {
Expand Down
2 changes: 2 additions & 0 deletions ui/tests/helpers/sync/sync-selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export const PAGE = {
overview: {
optInBanner: '[data-test-secrets-sync-opt-in-banner]',
optInBannerEnable: '[data-test-secrets-sync-opt-in-banner-enable]',
optInBannerDescription: '[data-test-secrets-sync-opt-in-banner-description]',
optInDismiss: '[data-test-secrets-sync-opt-in-banner] [data-test-icon="x"]',
optInModal: '[data-test-secrets-sync-opt-in-modal]',
optInCheck: '[data-test-opt-in-check]',
optInConfirm: '[data-test-opt-in-confirm]',
Expand Down
47 changes: 44 additions & 3 deletions ui/tests/integration/components/sync/secrets/page/overview-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import syncHandlers from 'vault/mirage/handlers/sync';
import { PAGE } from 'vault/tests/helpers/sync/sync-selectors';
import { Response } from 'miragejs';
import { dateFormat } from 'core/helpers/date-format';
import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';

const { title, tab, overviewCard, cta, overview, pagination, emptyStateTitle, emptyStateMessage } = PAGE;

Expand All @@ -24,6 +25,8 @@ module('Integration | Component | sync | Page::Overview', function (hooks) {
setupMirage(hooks);

hooks.beforeEach(async function () {
// allow capabilities as root by default to allow users to POST to the secrets-sync/activate endpoint
this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub());
this.version = this.owner.lookup('service:version');
this.store = this.owner.lookup('service:store');
this.version.type = 'enterprise';
Expand Down Expand Up @@ -61,15 +64,17 @@ module('Integration | Component | sync | Page::Overview', 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();
});
});

Expand Down Expand Up @@ -122,15 +127,51 @@ module('Integration | Component | sync | Page::Overview', function (hooks) {
});
});

module('secrets sync is not activated and license has secrets sync', function (hooks) {
module('user does not have post permissions to activate', function (hooks) {
hooks.beforeEach(function () {
this.isActivated = false;
this.destinations = [];
this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub(['read']));
});

test('it should show the opt-in banner without the ability to activate', async function (assert) {
await this.renderComponent();

assert
.dom(overview.optInBannerDescription)
.hasText(
'To use this feature, specific activation is required. Please contact your administrator to activate.'
);
assert.dom(overview.optInBannerEnable).doesNotExist('Opt-in enable button does not show');
});

test('it should not show allow the user to dismiss the opt-in banner', async function (assert) {
await this.renderComponent();

assert.dom(overview.optInDismiss).doesNotExist('dismiss opt-in banner does not show');
});
});

module('secrets sync is not activated and license has secrets sync meep', function (hooks) {
hooks.beforeEach(async function () {
this.isActivated = false;
});

test('it should show the opt-in banner', async function (assert) {
test('it should show the opt-in banner with activate description', async function (assert) {
await this.renderComponent();

assert.dom(overview.optInBanner).exists('Opt-in banner is shown');
assert
.dom(overview.optInBannerDescription)
.hasText(
"To use this feature, specific activation is required. Please review the feature documentation and enable it. If you're upgrading from beta, your previous data will be accessible after activation."
);
});

test('it should show dismiss banner', async function (assert) {
await this.renderComponent();

assert.dom(overview.optInDismiss).exists('dismiss opt-in banner shows');
});

test('it should navigate to the opt-in modal', async function (assert) {
Expand Down
Loading