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
```
12 changes: 5 additions & 7 deletions ui/app/components/sidebar/nav/cluster.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,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 @@ -61,7 +59,7 @@
{{#if
(and
this.version.isEnterprise
this.namespace.inRootNamespace
this.isRootNamespace
(not this.cluster.replicationRedacted)
(has-permission "status" routeParams="replication")
)
Expand All @@ -73,7 +71,7 @@
@hasSubItems={{true}}
/>
{{/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 @@ -87,7 +85,7 @@
{{#if
(and
this.version.features
this.namespace.inRootNamespace
this.isRootNamespace
(has-permission "status" routeParams="license")
(not this.cluster.dr.isSecondary)
)
Expand All @@ -99,7 +97,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
Copy link
Contributor

Choose a reason for hiding this comment

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

ty for the comment!

Copy link
Contributor

@hellobontempo hellobontempo Dec 13, 2023

Choose a reason for hiding this comment

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

I'm still getting up to speed with the chroot situation - does true "root" mean root root?

Does this.currentCluster.hasChrootNamespace mean the user has a configured root namespace and so they will not have root as the base/parent namespace? So therefore configuring chroot and having root as a namespace are mutually exclusive?

Just looking for clarification so I understand 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hasChrootNamespace means the Vault operator has set chroot_namespace to some value on the config of the given listener, and so the user does not have access to "true root" (which yes, is root namespace). So, even if the UI is in its top-most namespace, if chroot_namespace is configured it is not the true root.

Does that answer your question?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes! I think so - I wanted to clarify that setting the chroot_namespace means that you are configuring the top namespace and so therefore root will NOT be a possible/accessible.

Thank you!

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);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved this so we get a little better efficiency, skipping the method if canViewAll


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 @@ -9,6 +9,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}`);
});
});
Loading