From 9b7571f42415b3092a52aeef7a7665d357ce3500 Mon Sep 17 00:00:00 2001 From: Chelsea Shaw Date: Mon, 1 Apr 2024 15:10:59 -0500 Subject: [PATCH 1/6] Add wildcard calc helpers to permissions service with tests --- ui/app/services/permissions.js | 21 ++++ ui/tests/unit/services/permissions-test.js | 134 +++++++++++++++++++++ 2 files changed, 155 insertions(+) diff --git a/ui/app/services/permissions.js b/ui/app/services/permissions.js index c5e8064025d9..a10a5d090c99 100644 --- a/ui/app/services/permissions.js +++ b/ui/app/services/permissions.js @@ -117,6 +117,27 @@ export default class PermissionsService extends Service { } } + get wildcardPath() { + const ns = [sanitizePath(this.chrootNamespace), sanitizePath(this.namespace.userRootNamespace)].join('/'); + // wildcard path comes back from root namespace as empty string, + // while within a namespace it's the namespace itself ending with a slash + return ns === '/' ? '' : `${sanitizePath(ns)}/`; + } + + /** + * + * @param {object} globPaths key is path, value is object with capabilities + * @returns {boolean} whether the user's policy includes wildcard access to NS + */ + hasWildcardAccess(globPaths = {}) { + // First check if the wildcard path is in the globPaths object + if (!Object.keys(globPaths).includes(this.wildcardPath)) return false; + + // if so, make sure the current namespace is a child of the wildcard path + return this.namespace.path.startsWith(this.wildcardPath); + } + + // This method is called to recalculate whether to show the permissionsBanner when the namespace changes calcNsAccess() { if (this.canViewAll) { this.permissionsBanner = null; diff --git a/ui/tests/unit/services/permissions-test.js b/ui/tests/unit/services/permissions-test.js index c9483fc365ac..ebab76f02cb3 100644 --- a/ui/tests/unit/services/permissions-test.js +++ b/ui/tests/unit/services/permissions-test.js @@ -250,4 +250,138 @@ module('Unit | Service | permissions', function (hooks) { ); }); }); + + module('wildcardPath calculates correctly', function () { + [ + { + scenario: 'no user root or chroot', + userRoot: '', + chroot: null, + expectedPath: '', + }, + { + scenario: 'user root = child ns and no chroot', + userRoot: 'bar', + chroot: null, + expectedPath: 'bar/', + }, + { + scenario: 'user root = child ns and chroot set', + userRoot: 'bar', + chroot: 'admin/', + expectedPath: 'admin/bar/', + }, + { + scenario: 'no user root and chroot set', + userRoot: '', + chroot: 'admin/', + expectedPath: 'admin/', + }, + ].forEach((testCase) => { + test(`when ${testCase.scenario}`, function (assert) { + const namespaceService = Service.extend({ + userRootNamespace: testCase.userRoot, + path: 'current/path/does/not/matter', + }); + this.owner.register('service:namespace', namespaceService); + this.service.set('chrootNamespace', testCase.chroot); + assert.strictEqual(this.service.wildcardPath, testCase.expectedPath); + }); + }); + test('when user root =child ns and chroot set', function (assert) { + const namespaceService = Service.extend({ + path: 'bar/baz', + userRootNamespace: 'bar', + }); + this.owner.register('service:namespace', namespaceService); + this.service.set('chrootNamespace', 'admin/'); + assert.strictEqual(this.service.wildcardPath, 'admin/bar/'); + }); + }); + + module('hasWildcardAccess calculates correctly', function () { + // The resultant-acl endpoint returns paths with chroot and + // relative root prefixed on all paths. + [ + { + scenario: 'when root wildcard in root namespace', + chroot: null, + userRoot: '', + currentNs: 'foo/bar', + globs: { + '': { capabilities: ['read'] }, + }, + expectedAccess: true, + }, + { + scenario: 'when root wildcard in chroot ns', + chroot: 'admin/', + userRoot: '', + currentNs: 'admin/child', + globs: { + 'admin/': { capabilities: ['read'] }, + }, + expectedAccess: true, + }, + { + scenario: 'when namespace wildcard in child ns', + chroot: null, + userRoot: 'bar', + currentNs: 'bar/baz', + globs: { + 'bar/': { capabilities: ['read'] }, + }, + expectedAccess: true, + }, + { + scenario: 'when namespace wildcard in child ns & chroot', + chroot: 'foo/', + userRoot: 'bar', + currentNs: 'foo/bar/baz', + globs: { + 'foo/bar/': { capabilities: ['read'] }, + }, + expectedAccess: true, + }, + { + scenario: 'when namespace wildcard in different ns with chroot & user root', + chroot: 'foo/', + userRoot: 'bar', + currentNs: 'foo/bing', + globs: { + 'foo/bar/': { capabilities: ['read'] }, + }, + expectedAccess: false, + }, + { + scenario: 'when namespace wildcard in different ns without chroot', + chroot: null, + userRoot: 'bar', + currentNs: 'foo/bing', + globs: { + 'bar/': { capabilities: ['read'] }, + }, + expectedAccess: false, + }, + { + scenario: 'when globs is empty', + chroot: 'foo/', + userRoot: 'bar', + currentNs: 'foo/bing', + globs: {}, + expectedAccess: false, + }, + ].forEach((testCase) => { + test(`when ${testCase.scenario}`, function (assert) { + const namespaceService = Service.extend({ + path: testCase.currentNs, + userRootNamespace: testCase.userRoot, + }); + this.owner.register('service:namespace', namespaceService); + this.service.set('chrootNamespace', testCase.chroot); + const result = this.service.hasWildcardAccess(testCase.globs); + assert.strictEqual(result, testCase.expectedAccess); + }); + }); + }); }); From 32272374609b5cf0fcaf2fff3a829269f6612889 Mon Sep 17 00:00:00 2001 From: Chelsea Shaw Date: Mon, 1 Apr 2024 15:11:45 -0500 Subject: [PATCH 2/6] Check for wildcard access when calculating permissionsBanner --- ui/app/services/permissions.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ui/app/services/permissions.js b/ui/app/services/permissions.js index a10a5d090c99..8d62e125c055 100644 --- a/ui/app/services/permissions.js +++ b/ui/app/services/permissions.js @@ -82,6 +82,12 @@ const API_PATHS_TO_ROUTE_PARAMS = { root: boolean; chroot_namespace?: string; }; + There are a couple nuances to be aware of about this response. When a + chroot_namespace is set, all of the paths in the response will be prefixed + with that namespace. Additionally, this endpoint is only added to the default + policy in the user's root namespace, so we make the call to the user's root + namespace (the namespace where the user's auth method is mounted) no matter what + the current namespace is. */ export default class PermissionsService extends Service { @@ -91,7 +97,6 @@ export default class PermissionsService extends Service { @tracked permissionsBanner = null; @tracked chrootNamespace = null; @service store; - @service auth; @service namespace; get baseNs() { @@ -145,7 +150,11 @@ export default class PermissionsService extends Service { } const namespace = this.baseNs; const allowed = + // check if the user has wildcard access to the relative root namespace + this.hasWildcardAccess(this.globPaths) || + // or if any of their glob paths start with the namespace Object.keys(this.globPaths).any((k) => k.startsWith(namespace)) || + // or if any of their exact paths start with the namespace Object.keys(this.exactPaths).any((k) => k.startsWith(namespace)); this.permissionsBanner = allowed ? null : PERMISSIONS_BANNER_STATES.noAccess; } From 3f4eced4296a370cb8afb21fcd624339fd7746af Mon Sep 17 00:00:00 2001 From: Chelsea Shaw Date: Mon, 1 Apr 2024 15:14:14 -0500 Subject: [PATCH 3/6] Move resultant-acl banner within TokenExpireWarning so it's mutually exclusive with token expired banner --- ui/app/templates/vault/cluster.hbs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/app/templates/vault/cluster.hbs b/ui/app/templates/vault/cluster.hbs index 49d7c35978b6..9ce6868776af 100644 --- a/ui/app/templates/vault/cluster.hbs +++ b/ui/app/templates/vault/cluster.hbs @@ -70,9 +70,6 @@ @autoloaded={{eq this.activeCluster.licenseState "autoloaded"}} /> {{/if}} - {{#if this.permissionBanner}} - - {{/if}}
{{#each this.flashMessages.queue as |flash|}} @@ -84,6 +81,9 @@ {{#if this.auth.isActiveSession}} + {{#if (this.permissionBanner)}} + + {{/if}} {{outlet}} {{else}} From 201e49a0d1a258fc3f573cca559d912413ff38cb Mon Sep 17 00:00:00 2001 From: Chelsea Shaw Date: Mon, 1 Apr 2024 16:09:18 -0500 Subject: [PATCH 4/6] fix permissions banner if statement --- ui/app/templates/vault/cluster.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/app/templates/vault/cluster.hbs b/ui/app/templates/vault/cluster.hbs index 9ce6868776af..8ef29378187b 100644 --- a/ui/app/templates/vault/cluster.hbs +++ b/ui/app/templates/vault/cluster.hbs @@ -81,7 +81,7 @@ {{#if this.auth.isActiveSession}} - {{#if (this.permissionBanner)}} + {{#if this.permissionBanner}} {{/if}} {{outlet}} From 9b87e9bace84576af0e624c140942b5c48d50774 Mon Sep 17 00:00:00 2001 From: Chelsea Shaw Date: Mon, 1 Apr 2024 16:17:00 -0500 Subject: [PATCH 5/6] Add margin to resultant-acl --- ui/app/templates/vault/cluster.hbs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/app/templates/vault/cluster.hbs b/ui/app/templates/vault/cluster.hbs index 8ef29378187b..4fa86a248db8 100644 --- a/ui/app/templates/vault/cluster.hbs +++ b/ui/app/templates/vault/cluster.hbs @@ -82,7 +82,9 @@ {{#if this.auth.isActiveSession}} {{#if this.permissionBanner}} - +
+ +
{{/if}} {{outlet}}
From 015880386ad36bf1488f6f19815fd67ff64345bf Mon Sep 17 00:00:00 2001 From: Chelsea Shaw Date: Mon, 1 Apr 2024 16:26:35 -0500 Subject: [PATCH 6/6] cleanup comments --- ui/app/services/permissions.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/app/services/permissions.js b/ui/app/services/permissions.js index 8d62e125c055..3233d095996b 100644 --- a/ui/app/services/permissions.js +++ b/ui/app/services/permissions.js @@ -86,8 +86,8 @@ const API_PATHS_TO_ROUTE_PARAMS = { chroot_namespace is set, all of the paths in the response will be prefixed with that namespace. Additionally, this endpoint is only added to the default policy in the user's root namespace, so we make the call to the user's root - namespace (the namespace where the user's auth method is mounted) no matter what - the current namespace is. + namespace (the namespace where the user's auth method is mounted) no matter + what the current namespace is. */ export default class PermissionsService extends Service { @@ -125,12 +125,12 @@ export default class PermissionsService extends Service { get wildcardPath() { const ns = [sanitizePath(this.chrootNamespace), sanitizePath(this.namespace.userRootNamespace)].join('/'); // wildcard path comes back from root namespace as empty string, - // while within a namespace it's the namespace itself ending with a slash + // but within a namespace it's the namespace itself ending with a slash return ns === '/' ? '' : `${sanitizePath(ns)}/`; } /** - * + * hasWildcardAccess checks if the user has a wildcard policy * @param {object} globPaths key is path, value is object with capabilities * @returns {boolean} whether the user's policy includes wildcard access to NS */