diff --git a/changelog/24492.txt b/changelog/24492.txt
new file mode 100644
index 00000000000..d61c901a2c1
--- /dev/null
+++ b/changelog/24492.txt
@@ -0,0 +1,3 @@
+```release-note:bug
+ui: fix navigation items shown to user when chroot_namespace configured
+```
diff --git a/ui/app/components/sidebar/nav/cluster.hbs b/ui/app/components/sidebar/nav/cluster.hbs
index 2b0ecff88dd..33dd4f2e58a 100644
--- a/ui/app/components/sidebar/nav/cluster.hbs
+++ b/ui/app/components/sidebar/nav/cluster.hbs
@@ -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")
)
}}
@@ -61,7 +59,7 @@
{{#if
(and
this.version.isEnterprise
- this.namespace.inRootNamespace
+ this.isRootNamespace
(not this.cluster.replicationRedacted)
(has-permission "status" routeParams="replication")
)
@@ -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"))}}
{{/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))}}
diff --git a/ui/lib/core/addon/utils/sanitize-path.js b/ui/lib/core/addon/utils/sanitize-path.js
index 7ca66c651de..88a7bbc9fe3 100644
--- a/ui/lib/core/addon/utils/sanitize-path.js
+++ b/ui/lib/core/addon/utils/sanitize-path.js
@@ -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/');
}
diff --git a/ui/mirage/handlers/chroot-namespace.js b/ui/mirage/handlers/chroot-namespace.js
index 97a17e10074..6a6fb5d7eca 100644
--- a/ui/mirage/handlers/chroot-namespace.js
+++ b/ui/mirage/handlers/chroot-namespace.js
@@ -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
@@ -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' })
+ );
}
diff --git a/ui/tests/acceptance/chroot-namespace-test.js b/ui/tests/acceptance/chroot-namespace-test.js
index 5bca573f31c..48fe749ef8e 100644
--- a/ui/tests/acceptance/chroot-namespace-test.js
+++ b/ui/tests/acceptance/chroot-namespace-test.js
@@ -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);
@@ -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}`);
+ });
});
diff --git a/ui/tests/unit/services/permissions-test.js b/ui/tests/unit/services/permissions-test.js
index ceb7cf89429..c9483fc365a 100644
--- a/ui/tests/unit/services/permissions-test.js
+++ b/ui/tests/unit/services/permissions-test.js
@@ -53,66 +53,11 @@ module('Unit | Service | permissions', function (hooks) {
assert.deepEqual(this.service.get('globPaths'), PERMISSIONS_RESPONSE.data.glob_paths);
});
- test('returns true if a policy includes access to an exact path', function (assert) {
- this.service.set('exactPaths', PERMISSIONS_RESPONSE.data.exact_paths);
- assert.true(this.service.hasPermission('foo'));
- });
-
- test('returns true if a paths prefix is included in the policys exact paths', function (assert) {
- this.service.set('exactPaths', PERMISSIONS_RESPONSE.data.exact_paths);
- assert.true(this.service.hasPermission('bar'));
- });
-
- test('it returns true if a policy includes access to a glob path', function (assert) {
- this.service.set('globPaths', PERMISSIONS_RESPONSE.data.glob_paths);
- assert.true(this.service.hasPermission('baz/biz/hi'));
- });
-
- test('it returns true if a policy includes access to the * glob path', function (assert) {
- const splatPath = { '': {} };
- this.service.set('globPaths', splatPath);
- assert.true(this.service.hasPermission('hi'));
- });
-
- test('it returns false if the matched path includes the deny capability', function (assert) {
- this.service.set('globPaths', PERMISSIONS_RESPONSE.data.glob_paths);
- assert.false(this.service.hasPermission('boo'));
- });
-
- test('it returns true if passed path does not end in a slash but globPath does', function (assert) {
- this.service.set('globPaths', PERMISSIONS_RESPONSE.data.glob_paths);
- assert.true(this.service.hasPermission('ends/in/slash'), 'matches without slash');
- assert.true(this.service.hasPermission('ends/in/slash/'), 'matches with slash');
- });
-
- test('it returns false if a policy does not includes access to a path', function (assert) {
- assert.false(this.service.hasPermission('danger'));
- });
-
test('sets the root token', function (assert) {
this.service.setPaths({ data: { root: true } });
assert.true(this.service.canViewAll);
});
- test('returns true with the root token', function (assert) {
- this.service.set('canViewAll', true);
- assert.true(this.service.hasPermission('hi'));
- });
-
- test('it returns true if a policy has the specified capabilities on a path', function (assert) {
- this.service.set('exactPaths', PERMISSIONS_RESPONSE.data.exact_paths);
- this.service.set('globPaths', PERMISSIONS_RESPONSE.data.glob_paths);
- assert.true(this.service.hasPermission('bar/bee', ['create', 'list']));
- assert.true(this.service.hasPermission('baz/biz', ['read']));
- });
-
- test('it returns false if a policy does not have the specified capabilities on a path', function (assert) {
- this.service.set('exactPaths', PERMISSIONS_RESPONSE.data.exact_paths);
- this.service.set('globPaths', PERMISSIONS_RESPONSE.data.glob_paths);
- assert.false(this.service.hasPermission('bar/bee', ['create', 'delete']));
- assert.false(this.service.hasPermission('foo', ['create']));
- });
-
test('defaults to show all items when policy cannot be found', async function (assert) {
this.server.get('/v1/sys/internal/ui/resultant-acl', () => {
return [403, { 'Content-Type': 'application/json' }];
@@ -148,77 +93,161 @@ module('Unit | Service | permissions', function (hooks) {
assert.deepEqual(this.service.navPathParams('access'), expected);
});
- test('hasNavPermission returns true if a policy includes the required capabilities for at least one path', function (assert) {
- const accessPaths = {
- 'sys/auth': {
- capabilities: ['deny'],
- },
- 'identity/group/id': {
- capabilities: ['list', 'read'],
- },
- };
- this.service.set('exactPaths', accessPaths);
- assert.true(this.service.hasNavPermission('access', 'groups'));
- });
+ module('hasPermission', function () {
+ test('returns true if a policy includes access to an exact path', function (assert) {
+ this.service.set('exactPaths', PERMISSIONS_RESPONSE.data.exact_paths);
+ assert.true(this.service.hasPermission('foo'));
+ });
- test('hasNavPermission returns false if a policy does not include the required capabilities for at least one path', function (assert) {
- const accessPaths = {
- 'sys/auth': {
- capabilities: ['deny'],
- },
- 'identity/group/id': {
- capabilities: ['read'],
- },
- };
- this.service.set('exactPaths', accessPaths);
- assert.false(this.service.hasNavPermission('access', 'groups'));
+ test('returns true if a paths prefix is included in the policys exact paths', function (assert) {
+ this.service.set('exactPaths', PERMISSIONS_RESPONSE.data.exact_paths);
+ assert.true(this.service.hasPermission('bar'));
+ });
+
+ test('it returns true if a policy includes access to a glob path', function (assert) {
+ this.service.set('globPaths', PERMISSIONS_RESPONSE.data.glob_paths);
+ assert.true(this.service.hasPermission('baz/biz/hi'));
+ });
+
+ test('it returns true if a policy includes access to the * glob path', function (assert) {
+ const splatPath = { '': {} };
+ this.service.set('globPaths', splatPath);
+ assert.true(this.service.hasPermission('hi'));
+ });
+
+ test('it returns false if the matched path includes the deny capability', function (assert) {
+ this.service.set('globPaths', PERMISSIONS_RESPONSE.data.glob_paths);
+ assert.false(this.service.hasPermission('boo'));
+ });
+
+ test('it returns true if passed path does not end in a slash but globPath does', function (assert) {
+ this.service.set('globPaths', PERMISSIONS_RESPONSE.data.glob_paths);
+ assert.true(this.service.hasPermission('ends/in/slash'), 'matches without slash');
+ assert.true(this.service.hasPermission('ends/in/slash/'), 'matches with slash');
+ });
+
+ test('it returns false if a policy does not includes access to a path', function (assert) {
+ assert.false(this.service.hasPermission('danger'));
+ });
+ test('returns true with the root token', function (assert) {
+ this.service.set('canViewAll', true);
+ assert.true(this.service.hasPermission('hi'));
+ });
+ test('it returns true if a policy has the specified capabilities on a path', function (assert) {
+ this.service.set('exactPaths', PERMISSIONS_RESPONSE.data.exact_paths);
+ this.service.set('globPaths', PERMISSIONS_RESPONSE.data.glob_paths);
+ assert.true(this.service.hasPermission('bar/bee', ['create', 'list']));
+ assert.true(this.service.hasPermission('baz/biz', ['read']));
+ });
+
+ test('it returns false if a policy does not have the specified capabilities on a path', function (assert) {
+ this.service.set('exactPaths', PERMISSIONS_RESPONSE.data.exact_paths);
+ this.service.set('globPaths', PERMISSIONS_RESPONSE.data.glob_paths);
+ assert.false(this.service.hasPermission('bar/bee', ['create', 'delete']));
+ assert.false(this.service.hasPermission('foo', ['create']));
+ });
});
- test('hasNavPermission should handle routeParams as array', function (assert) {
- const getPaths = (override) => ({
- 'sys/auth': {
- capabilities: [override || 'read'],
- },
- 'identity/mfa/method': {
- capabilities: [override || 'read'],
- },
- 'identity/oidc/client': {
- capabilities: [override || 'deny'],
- },
+ module('hasNavPermission', function () {
+ test('returns true if a policy includes the required capabilities for at least one path', function (assert) {
+ const accessPaths = {
+ 'sys/auth': {
+ capabilities: ['deny'],
+ },
+ 'identity/group/id': {
+ capabilities: ['list', 'read'],
+ },
+ };
+ this.service.set('exactPaths', accessPaths);
+ assert.true(this.service.hasNavPermission('access', 'groups'));
});
- this.service.set('exactPaths', getPaths());
- assert.true(
- this.service.hasNavPermission('access', ['methods', 'mfa', 'oidc']),
- 'hasNavPermission returns true for array of route params when any route is permitted'
- );
- assert.false(
- this.service.hasNavPermission('access', ['methods', 'mfa', 'oidc'], true),
- 'hasNavPermission returns false for array of route params when any route is not permitted and requireAll is passed'
- );
-
- this.service.set('exactPaths', getPaths('read'));
- assert.true(
- this.service.hasNavPermission('access', ['methods', 'mfa', 'oidc'], true),
- 'hasNavPermission returns true for array of route params when all routes are permitted and requireAll is passed'
- );
-
- this.service.set('exactPaths', getPaths('deny'));
- assert.false(
- this.service.hasNavPermission('access', ['methods', 'mfa', 'oidc']),
- 'hasNavPermission returns false for array of route params when no routes are permitted'
- );
- assert.false(
- this.service.hasNavPermission('access', ['methods', 'mfa', 'oidc'], true),
- 'hasNavPermission returns false for array of route params when no routes are permitted and requireAll is passed'
- );
+ test('returns false if a policy does not include the required capabilities for at least one path', function (assert) {
+ const accessPaths = {
+ 'sys/auth': {
+ capabilities: ['deny'],
+ },
+ 'identity/group/id': {
+ capabilities: ['read'],
+ },
+ };
+ this.service.set('exactPaths', accessPaths);
+ assert.false(this.service.hasNavPermission('access', 'groups'));
+ });
+
+ test('should handle routeParams as array', function (assert) {
+ const getPaths = (override) => ({
+ 'sys/auth': {
+ capabilities: [override || 'read'],
+ },
+ 'identity/mfa/method': {
+ capabilities: [override || 'read'],
+ },
+ 'identity/oidc/client': {
+ capabilities: [override || 'deny'],
+ },
+ });
+
+ this.service.set('exactPaths', getPaths());
+ assert.true(
+ this.service.hasNavPermission('access', ['methods', 'mfa', 'oidc']),
+ 'hasNavPermission returns true for array of route params when any route is permitted'
+ );
+ assert.false(
+ this.service.hasNavPermission('access', ['methods', 'mfa', 'oidc'], true),
+ 'hasNavPermission returns false for array of route params when any route is not permitted and requireAll is passed'
+ );
+
+ this.service.set('exactPaths', getPaths('read'));
+ assert.true(
+ this.service.hasNavPermission('access', ['methods', 'mfa', 'oidc'], true),
+ 'hasNavPermission returns true for array of route params when all routes are permitted and requireAll is passed'
+ );
+
+ this.service.set('exactPaths', getPaths('deny'));
+ assert.false(
+ this.service.hasNavPermission('access', ['methods', 'mfa', 'oidc']),
+ 'hasNavPermission returns false for array of route params when no routes are permitted'
+ );
+ assert.false(
+ this.service.hasNavPermission('access', ['methods', 'mfa', 'oidc'], true),
+ 'hasNavPermission returns false for array of route params when no routes are permitted and requireAll is passed'
+ );
+ });
});
- test('appends the namespace to the path if there is one', function (assert) {
- const namespaceService = Service.extend({
- path: 'marketing',
+ module('pathWithNamespace', function () {
+ test('appends the namespace to the path if there is one', function (assert) {
+ const namespaceService = Service.extend({
+ path: 'marketing',
+ });
+ this.owner.register('service:namespace', namespaceService);
+ assert.strictEqual(this.service.pathNameWithNamespace('sys/auth'), 'marketing/sys/auth');
+ });
+
+ test('appends the chroot and namespace when both present', function (assert) {
+ const namespaceService = Service.extend({
+ path: 'marketing',
+ });
+ this.owner.register('service:namespace', namespaceService);
+ this.service.set('chrootNamespace', 'admin/');
+ assert.strictEqual(this.service.pathNameWithNamespace('sys/auth'), 'admin/marketing/sys/auth');
+ });
+ test('appends the chroot when no namespace', function (assert) {
+ this.service.set('chrootNamespace', 'admin');
+ assert.strictEqual(this.service.pathNameWithNamespace('sys/auth'), 'admin/sys/auth');
+ });
+ test('handles superfluous slashes', function (assert) {
+ const namespaceService = Service.extend({
+ path: '/marketing',
+ });
+ this.owner.register('service:namespace', namespaceService);
+ this.service.set('chrootNamespace', '/admin/');
+ assert.strictEqual(this.service.pathNameWithNamespace('/sys/auth'), 'admin/marketing/sys/auth');
+ assert.strictEqual(
+ this.service.pathNameWithNamespace('/sys/policies/'),
+ 'admin/marketing/sys/policies/'
+ );
});
- this.owner.register('service:namespace', namespaceService);
- assert.strictEqual(this.service.pathNameWithNamespace('sys/auth'), 'marketing/sys/auth');
});
});
diff --git a/ui/tests/unit/utils/sanitize-path-test.js b/ui/tests/unit/utils/sanitize-path-test.js
index 7107d737f2c..fe4fa998403 100644
--- a/ui/tests/unit/utils/sanitize-path-test.js
+++ b/ui/tests/unit/utils/sanitize-path-test.js
@@ -4,7 +4,7 @@
*/
import { module, test } from 'qunit';
-import { ensureTrailingSlash, getRelativePath, sanitizePath } from 'core/utils/sanitize-path';
+import { ensureTrailingSlash, getRelativePath, sanitizePath, sanitizeStart } from 'core/utils/sanitize-path';
module('Unit | Utility | sanitize-path', function () {
test('it removes spaces and slashes from either side', function (assert) {
@@ -29,4 +29,18 @@ module('Unit | Utility | sanitize-path', function () {
assert.strictEqual(getRelativePath('/recipes/cookies/choc-chip/', 'recipes/cookies'), 'choc-chip');
assert.strictEqual(getRelativePath('/admin/bop/boop/admin_foo/baz/', 'admin'), 'bop/boop/admin_foo/baz');
});
+
+ test('#sanitizeStart', function (assert) {
+ assert.strictEqual(
+ sanitizeStart(' /foo/bar/baz/ '),
+ 'foo/bar/baz/',
+ 'trims spaces and removes slashes only from beginning'
+ );
+ assert.strictEqual(
+ sanitizeStart('//foo/bar/baz/'),
+ 'foo/bar/baz/',
+ 'removes more than one slash from start'
+ );
+ assert.strictEqual(sanitizeStart(undefined), '', 'handles falsey values');
+ });
});