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 banner for when resultant-acl check fails #23503

Merged
merged 12 commits into from
Oct 18, 2023
3 changes: 3 additions & 0 deletions changelog/23503.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
ui: show banner when resultant-acl check fails due to permissions or wrong namespace.
```
19 changes: 19 additions & 0 deletions ui/app/components/resultant-acl-banner.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Hds::Alert @type="inline" @color="critical" data-test-resultant-acl-banner as |A|>
<A.Title>Resultant ACL check failed</A.Title>
<A.Description>
{{if
@isEnterprise
"You may be in the wrong namespace, so links might be shown that you don't have access to."
"Links might be shown that you don't have access to. Contact your administrator to update your policy."
}}
</A.Description>
{{#if @isEnterprise}}
<A.Link::Standalone
@icon="arrow-right"
@iconPosition="trailing"
@text={{concat "Log into " this.ns " namespace"}}
@route="vault.cluster.logout"
data-test-resultant-acl-reauthenticate
/>
{{/if}}
</Hds::Alert>
10 changes: 10 additions & 0 deletions ui/app/components/resultant-acl-banner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { service } from '@ember/service';
import Component from '@glimmer/component';

export default class ResultantAclBannerComponent extends Component {
@service namespace;

get ns() {
return this.namespace.path || 'root';
}
}
2 changes: 2 additions & 0 deletions ui/app/controllers/vault/cluster.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export default Controller.extend({
consoleOpen: alias('console.isOpen'),
activeCluster: alias('auth.activeCluster'),

permissionReadFailed: alias('permissions.readFailed'),

actions: {
toggleConsole() {
this.toggleProperty('consoleOpen');
Expand Down
9 changes: 5 additions & 4 deletions ui/app/routes/vault/cluster.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import getStorage from '../../lib/token-storage';
import localStorage from 'vault/lib/local-storage';
import ClusterRoute from 'vault/mixins/cluster-route';
import ModelBoundaryRoute from 'vault/mixins/model-boundary-route';
import { assert } from '@ember/debug';

const POLL_INTERVAL_MS = 10000;

Expand Down Expand Up @@ -55,10 +56,10 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
let namespace = params.namespaceQueryParam;
const currentTokenName = this.auth.get('currentTokenName');
const managedRoot = this.featureFlagService.managedNamespaceRoot;
if (managedRoot && this.version.isOSS) {
// eslint-disable-next-line no-console
console.error('Cannot use Cloud Admin Namespace flag with OSS Vault');
}
assert(
'Cannot use VAULT_CLOUD_ADMIN_NAMESPACE flag with non-enterprise Vault version',
!(managedRoot && this.version.isOSS)
);
if (!namespace && currentTokenName && !Ember.testing) {
// if no namespace queryParam and user authenticated,
// use user's root namespace to redirect to properly param'd url
Expand Down
4 changes: 4 additions & 0 deletions ui/app/services/permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export default Service.extend({
exactPaths: null,
globPaths: null,
canViewAll: null,
readFailed: false,
store: service(),
auth: service(),
namespace: service(),
Expand All @@ -80,19 +81,22 @@ export default Service.extend({
} catch (err) {
// If no policy can be found, default to showing all nav items.
this.set('canViewAll', true);
this.set('readFailed', true);
}
}),

setPaths(resp) {
this.set('exactPaths', resp.data.exact_paths);
this.set('globPaths', resp.data.glob_paths);
this.set('canViewAll', resp.data.root);
this.set('readFailed', false);
},

reset() {
this.set('exactPaths', null);
this.set('globPaths', null);
this.set('canViewAll', null);
this.set('readFailed', false);
},

hasNavPermission(navItem, routeParams, requireAll) {
Expand Down
18 changes: 18 additions & 0 deletions ui/app/styles/components/cluster-banners.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

.cluster-banners-wrapper {
width: 100%;
max-width: 1344px;
margin: 0 auto;
padding: 0 1.5rem;

> div {
margin-top: $spacing-l;
&:last-of-type {
margin-bottom: $spacing-l;
}
}
}
11 changes: 0 additions & 11 deletions ui/app/styles/components/license-banners.scss

This file was deleted.

2 changes: 1 addition & 1 deletion ui/app/styles/core.scss
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
@import './components/box-label';
@import './components/box-radio';
@import './components/calendar-widget';
@import './components/cluster-banners';
@import './components/codemirror';
@import './components/confirm';
@import './components/console-ui-panel';
Expand All @@ -74,7 +75,6 @@
@import './components/info-table-row';
@import './components/kmip-role-edit';
@import './components/known-secondaries-card.scss';
@import './components/license-banners';
@import './components/linked-block';
@import './components/list-item-row';
@import './components/loader';
Expand Down
98 changes: 47 additions & 51 deletions ui/app/templates/components/license-banners.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -4,56 +4,52 @@
~}}

{{#if (and this.licenseExpired (not this.expiredDismissed))}}
<div class="license-banner-wrapper">
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed wrapping div and moved it to outside all the banners (in cluster.hbs)

<Hds::Alert
@type="inline"
@color="critical"
@onDismiss={{fn this.dismissBanner "expired"}}
data-test-license-banner-expired
as |A|
>
<A.Title>License expired</A.Title>
<A.Description>
Your Vault license expired on
{{date-format @expiry "MMM d, yyyy"}}. Add a new license to your configuration and restart Vault.
</A.Description>
<A.Description class="has-top-margin-xs">
<DocLink @path="/vault/tutorials/enterprise/hashicorp-enterprise-license">
Read documentation
<Icon @name="learn-link" />
</DocLink>
</A.Description>
</Hds::Alert>
</div>
<Hds::Alert
@type="inline"
@color="critical"
@onDismiss={{fn this.dismissBanner "expired"}}
data-test-license-banner-expired
as |A|
>
<A.Title>License expired</A.Title>
<A.Description>
Your Vault license expired on
{{date-format @expiry "MMM d, yyyy"}}. Add a new license to your configuration and restart Vault.
</A.Description>
<A.Description class="has-top-margin-xs">
<DocLink @path="/vault/tutorials/enterprise/hashicorp-enterprise-license">
Read documentation
<Icon @name="learn-link" />
</DocLink>
</A.Description>
</Hds::Alert>
{{else if (and (lte this.licenseExpiringInDays 30) (not this.warningDismissed))}}
<div class="license-banner-wrapper">
<Hds::Alert
@type="inline"
@color="warning"
@onDismiss={{fn this.dismissBanner "warning"}}
data-test-license-banner-warning
as |A|
>
<A.Title>Vault license expiring</A.Title>
<A.Description>
Your Vault license will expire in
{{this.licenseExpiringInDays}}
days at
{{date-format @expiry "hh:mm:ss a"}}
on
{{date-format @expiry "MMM d, yyyy"}}.
{{if
@autoloaded
"Add a new license to your configuration."
"Keep in mind that your next license will need to be autoloaded."
}}
</A.Description>
<A.Description class="has-top-margin-xs">
<DocLink @path="/vault/tutorials/enterprise/hashicorp-enterprise-license">
Read documentation
<Icon @name="learn-link" />
</DocLink>
</A.Description>
</Hds::Alert>
</div>
<Hds::Alert
@type="inline"
@color="warning"
@onDismiss={{fn this.dismissBanner "warning"}}
data-test-license-banner-warning
as |A|
>
<A.Title>Vault license expiring</A.Title>
<A.Description>
Your Vault license will expire in
{{this.licenseExpiringInDays}}
days at
{{date-format @expiry "hh:mm:ss a"}}
on
{{date-format @expiry "MMM d, yyyy"}}.
{{if
@autoloaded
"Add a new license to your configuration."
"Keep in mind that your next license will need to be autoloaded."
}}
</A.Description>
<A.Description class="has-top-margin-xs">
<DocLink @path="/vault/tutorials/enterprise/hashicorp-enterprise-license">
Read documentation
<Icon @name="learn-link" />
</DocLink>
</A.Description>
</Hds::Alert>
{{/if}}
18 changes: 11 additions & 7 deletions ui/app/templates/vault/cluster.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
SPDX-License-Identifier: BUSL-1.1
~}}
<Sidebar::Nav::Cluster />
{{! Only show license banners for Enterprise }}
{{#if this.activeCluster.version.isEnterprise}}
<LicenseBanners
@expiry={{this.activeCluster.licenseExpiry}}
@autoloaded={{eq this.activeCluster.licenseState "autoloaded"}}
/>
{{/if}}
<div class="cluster-banners-wrapper">
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This class automatically adds margins to the child divs so that spacing is consistent

{{#if this.activeCluster.version.isEnterprise}}
<LicenseBanners
@expiry={{this.activeCluster.licenseExpiry}}
@autoloaded={{eq this.activeCluster.licenseState "autoloaded"}}
/>
{{/if}}
{{#if this.permissionReadFailed}}
<ResultantAclBanner @isEnterprise={{this.activeCluster.version.isEnterprise}} />
{{/if}}
</div>
<div class="global-flash">
{{#each this.flashMessages.queue as |flash|}}
<FlashMessage data-test-flash-message={{true}} @flash={{flash}} as |customComponent flash close|>
Expand Down
22 changes: 21 additions & 1 deletion ui/tests/acceptance/cluster-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import { create } from 'ember-cli-page-object';
import { settled, click, visit } from '@ember/test-helpers';
import { settled, click, visit, currentRouteName } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { v4 as uuidv4 } from 'uuid';
Expand Down Expand Up @@ -82,4 +82,24 @@ module('Acceptance | cluster', function (hooks) {

assert.dom('[data-test-sidebar-nav-link="Policies"]').hasAttribute('href', '/ui/vault/policies/rgp');
});

test('shows error banner if resultant-acl check fails', async function (assert) {
const login_only = `
path "auth/token/lookup-self" {
capabilities = ["read"]
},
`;
await consoleComponent.runCommands([
`write sys/policies/acl/login-only policy=${btoa(login_only)}`,
`write -field=client_token auth/token/create no_default_policy=true policies="login-only"`,
]);
const noDefaultPolicyUser = consoleComponent.lastLogOutput;
assert.dom('[data-test-resultant-acl-banner]').doesNotExist('Resultant ACL banner does not show as root');
await logout.visit();
assert.dom('[data-test-resultant-acl-banner]').doesNotExist('Does not show on login page');
await authPage.login(noDefaultPolicyUser);
assert.dom('[data-test-resultant-acl-banner]').includesText('Resultant ACL check failed');
await click('[data-test-resultant-acl-reauthenticate]');
assert.strictEqual(currentRouteName(), 'vault.cluster.auth', 'Reauth link goes to login page');
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🎉

});
47 changes: 47 additions & 0 deletions ui/tests/integration/components/resultant-acl-banner-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'vault/tests/helpers';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';

module('Integration | Component | resultant-acl-banner', function (hooks) {
setupRenderingTest(hooks);

test('it renders correctly by default', async function (assert) {
await render(hbs`<ResultantAclBanner />`);

assert.dom('[data-test-resultant-acl-banner] .hds-alert__title').hasText('Resultant ACL check failed');
assert
.dom('[data-test-resultant-acl-banner] .hds-alert__description')
.hasText(
"Links might be shown that you don't have access to. Contact your administrator to update your policy."
);
assert.dom('[data-test-resultant-acl-reauthenticate]').doesNotExist('Does not show reauth link');
});

test('it renders correctly with set namespace', async function (assert) {
const nsService = this.owner.lookup('service:namespace');
nsService.setNamespace('my-ns');

await render(hbs`<ResultantAclBanner @isEnterprise={{true}} />`);

assert.dom('[data-test-resultant-acl-banner] .hds-alert__title').hasText('Resultant ACL check failed');
assert
.dom('[data-test-resultant-acl-banner] .hds-alert__description')
.hasText("You may be in the wrong namespace, so links might be shown that you don't have access to.");
assert
.dom('[data-test-resultant-acl-reauthenticate]')
.hasText('Log into my-ns namespace', 'Shows reauth link with given namespace');
});

test('it renders correctly with default namespace', async function (assert) {
await render(hbs`<ResultantAclBanner @isEnterprise={{true}} />`);

assert.dom('[data-test-resultant-acl-banner] .hds-alert__title').hasText('Resultant ACL check failed');
assert
.dom('[data-test-resultant-acl-banner] .hds-alert__description')
.hasText("You may be in the wrong namespace, so links might be shown that you don't have access to.");
assert
.dom('[data-test-resultant-acl-reauthenticate]')
.hasText('Log into root namespace', 'Shows reauth link with default namespace');
});
});