Skip to content
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/24492.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
ui: fix navigation items shown to user when chroot_namespace configured
```
10 changes: 4 additions & 6 deletions ui/app/components/sidebar/nav/cluster.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,7 @@

{{#if
(or
(and
this.namespace.inRootNamespace (has-permission "status" routeParams=(array "replication" "raft" "license" "seal"))
)
(and this.isRootNamespace (has-permission "status" routeParams=(array "replication" "raft" "license" "seal")))
(has-permission "clients" routeParams="activity")
)
}}
Expand All @@ -81,7 +79,7 @@
{{#if (and this.version.isEnterprise this.namespace.inRootNamespace (has-permission "status" routeParams="replication"))}}
<Nav.Link @route="vault.cluster.replication.index" @text="Replication" data-test-sidebar-nav-link="Replication" />
{{/if}}
{{#if (and this.cluster.usingRaft this.namespace.inRootNamespace (has-permission "status" routeParams="raft"))}}
{{#if (and this.cluster.usingRaft this.isRootNamespace (has-permission "status" routeParams="raft"))}}
<Nav.Link
@route="vault.cluster.storage"
@model={{this.cluster.name}}
Expand All @@ -95,7 +93,7 @@
{{#if
(and
this.version.features
this.namespace.inRootNamespace
this.isRootNamespace
(has-permission "status" routeParams="license")
(not this.cluster.dr.isSecondary)
)
Expand All @@ -107,7 +105,7 @@
data-test-sidebar-nav-link="License"
/>
{{/if}}
{{#if (and this.namespace.inRootNamespace (has-permission "status" routeParams="seal") (not this.cluster.dr.isSecondary))}}
{{#if (and this.isRootNamespace (has-permission "status" routeParams="seal") (not this.cluster.dr.isSecondary))}}
<Nav.Link
@route="vault.cluster.settings.seal"
@model={{this.cluster.name}}
Expand Down
5 changes: 5 additions & 0 deletions ui/app/components/sidebar/nav/cluster.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,9 @@ export default class SidebarNavClusterComponent extends Component {
get cluster() {
return this.currentCluster.cluster;
}

get isRootNamespace() {
// should only return true if we're in the true root namespace
return this.namespace.inRootNamespace && !this.currentCluster.hasChrootNamespace;
}
}
13 changes: 9 additions & 4 deletions ui/app/services/permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import Service, { inject as service } from '@ember/service';
import { sanitizePath, sanitizeStart } from 'core/utils/sanitize-path';
import { task } from 'ember-concurrency';

const API_PATHS = {
Expand Down Expand Up @@ -65,6 +66,7 @@ export default Service.extend({
globPaths: null,
canViewAll: null,
readFailed: false,
chrootNamespace: null,
store: service(),
auth: service(),
namespace: service(),
Expand All @@ -89,6 +91,7 @@ export default Service.extend({
this.set('exactPaths', resp.data.exact_paths);
this.set('globPaths', resp.data.glob_paths);
this.set('canViewAll', resp.data.root);
this.set('chrootNamespace', resp.data.chroot_namespace);
this.set('readFailed', false);
},

Expand All @@ -97,6 +100,7 @@ export default Service.extend({
this.set('globPaths', null);
this.set('canViewAll', null);
this.set('readFailed', false);
this.set('chrootNamespace', null);
},

hasNavPermission(navItem, routeParams, requireAll) {
Expand Down Expand Up @@ -124,20 +128,21 @@ export default Service.extend({
},

pathNameWithNamespace(pathName) {
const namespace = this.namespace.path;
const namespace = this.chrootNamespace
? `${sanitizePath(this.chrootNamespace)}/${sanitizePath(this.namespace.path)}`
: sanitizePath(this.namespace.path);
if (namespace) {
return `${namespace}/${pathName}`;
return `${sanitizePath(namespace)}/${sanitizeStart(pathName)}`;
} else {
return pathName;
}
},

hasPermission(pathName, capabilities = [null]) {
const path = this.pathNameWithNamespace(pathName);

if (this.canViewAll) {
return true;
}
const path = this.pathNameWithNamespace(pathName);

return capabilities.every(
(capability) =>
Expand Down
6 changes: 6 additions & 0 deletions ui/lib/core/addon/utils/sanitize-path.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ export function sanitizePath(path) {
return path.trim().replace(/^\/+|\/+$/g, '');
}

export function sanitizeStart(path) {
if (!path) return '';
//remove leading slashes
return path.trim().replace(/^\/+/, '');
}

export function ensureTrailingSlash(path) {
return path.replace(/(\w+[^/]$)/g, '$1/');
}
Expand Down
4 changes: 4 additions & 0 deletions ui/mirage/handlers/chroot-namespace.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import { Response } from 'miragejs';
import modifyPassthroughResponse from '../helpers/modify-passthrough-response';

/*
These are mocked responses to mimic what we get from the server
Expand All @@ -12,4 +13,7 @@ import { Response } from 'miragejs';
export default function (server) {
server.get('sys/health', () => new Response(400, {}, { errors: ['unsupported path'] }));
server.get('sys/replication/status', () => new Response(400, {}, { errors: ['unsupported path'] }));
server.get('sys/internal/ui/resultant-acl', (schema, req) =>
modifyPassthroughResponse(req, { chroot_namespace: 'my-ns' })
);
}
98 changes: 98 additions & 0 deletions ui/tests/acceptance/chroot-namespace-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import { currentRouteName } from '@ember/test-helpers';
import authPage from 'vault/tests/pages/auth';
import { setupMirage } from 'ember-cli-mirage/test-support';
import ENV from 'vault/config/environment';
import { createTokenCmd, runCmd, tokenWithPolicyCmd } from '../helpers/commands';

const navLink = (title) => `[data-test-sidebar-nav-link="${title}"]`;
// Matches the chroot namespace on the mirage handler
const namespace = 'my-ns';

module('Acceptance | chroot-namespace enterprise ui', function (hooks) {
setupApplicationTest(hooks);
Expand All @@ -26,4 +31,97 @@ module('Acceptance | chroot-namespace enterprise ui', function (hooks) {
assert.strictEqual(currentRouteName(), 'vault.cluster.dashboard', 'goes to dashboard page');
assert.dom('[data-test-badge-namespace]').includesText('root', 'Shows root namespace badge');
});

test('a user with default policy should see nav items', async function (assert) {
await authPage.login();
// Create namespace
await runCmd(`write sys/namespaces/${namespace} -f`, false);
// Create user within the namespace
await authPage.loginNs(namespace);
const userDefault = await runCmd(createTokenCmd());

await authPage.loginNs(namespace, userDefault);
['Dashboard', 'Secrets Engines', 'Access', 'Tools'].forEach((nav) => {
assert.dom(navLink(nav)).exists(`Shows ${nav} nav item for user with default policy`);
});
['Policies', 'Client Count', 'Replication', 'Raft Storage', 'License', 'Seal Vault'].forEach((nav) => {
assert.dom(navLink(nav)).doesNotExist(`Does not show ${nav} nav item for user with default policy`);
});

// cleanup namespace
await authPage.login();
await runCmd(`delete sys/namespaces/${namespace}`);
});

test('a user with read policy should see nav items', async function (assert) {
await authPage.login();
// Create namespace
await runCmd(`write sys/namespaces/${namespace} -f`, false);
// Create user within the namespace
await authPage.loginNs(namespace);
const reader = await runCmd(
tokenWithPolicyCmd(
'read-all',
`
path "sys/*" {
capabilities = ["read"]
}
`
)
);

await authPage.loginNs(namespace, reader);
['Dashboard', 'Secrets Engines', 'Access', 'Policies', 'Tools', 'Client Count'].forEach((nav) => {
assert.dom(navLink(nav)).exists(`Shows ${nav} nav item for user with read access policy`);
});
['Replication', 'Raft Storage', 'License', 'Seal Vault'].forEach((nav) => {
assert.dom(navLink(nav)).doesNotExist(`Does not show ${nav} nav item for user with read access policy`);
});

// cleanup namespace
await authPage.login();
await runCmd(`delete sys/namespaces/${namespace}`);
});

test('it works within a child namespace', async function (assert) {
await authPage.login();
// Create namespace
await runCmd(`write sys/namespaces/${namespace} -f`, false);
// Create user within the namespace
await authPage.loginNs(namespace);
const childReader = await runCmd(
tokenWithPolicyCmd(
'read-child',
`
path "child/sys/*" {
capabilities = ["read"]
}
`
)
);
// Create child namespace
await runCmd(`write sys/namespaces/child -f`, false);

await authPage.loginNs(namespace, childReader);
['Dashboard', 'Secrets Engines', 'Access', 'Tools'].forEach((nav) => {
assert.dom(navLink(nav)).exists(`Shows ${nav} nav item`);
});
['Policies', 'Client Count', 'Replication', 'Raft Storage', 'License', 'Seal Vault'].forEach((nav) => {
assert.dom(navLink(nav)).doesNotExist(`Does not show ${nav} nav item`);
});

await authPage.loginNs(`${namespace}/child`, childReader);
['Dashboard', 'Secrets Engines', 'Access', 'Policies', 'Tools', 'Client Count'].forEach((nav) => {
assert.dom(navLink(nav)).exists(`Shows ${nav} nav item within child namespace`);
});
['Replication', 'Raft Storage', 'License', 'Seal Vault'].forEach((nav) => {
assert.dom(navLink(nav)).doesNotExist(`Does not show ${nav} nav item within child namespace`);
});

// cleanup namespaces
await authPage.loginNs(namespace);
await runCmd(`delete sys/namespaces/child`);
await authPage.login();
await runCmd(`delete sys/namespaces/${namespace}`);
});
});
4 changes: 2 additions & 2 deletions ui/tests/pages/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default create({
await this.usernameInput(username);
return this.passwordInput(password).submit();
},
loginNs: async function (ns) {
loginNs: async function (ns, token = rootToken) {
// make sure we're always logged out and logged back in
await this.logout();
await settled();
Expand All @@ -55,7 +55,7 @@ export default create({
await settled();
await this.namespaceInput(ns);
await settled();
await this.tokenInput(rootToken).submit();
await this.tokenInput(token).submit();
return;
},
clickLogout: async function (clearNamespace = false) {
Expand Down
Loading