From c7b1f54ec4b468f74f2b619d647020e49fa17c46 Mon Sep 17 00:00:00 2001 From: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com> Date: Wed, 3 Apr 2024 10:14:16 -0500 Subject: [PATCH] UI: Don't show Resultant-ACL banner when wildcard policy present (#26233) * Add wildcard calc helpers to permissions service with tests * Check for wildcard access when calculating permissionsBanner * Move resultant-acl banner within TokenExpireWarning so it's mutually exclusive with token expired banner * fix permissions banner if statement * Add margin to resultant-acl * cleanup comments --- ui/app/services/permissions.js | 47 +++++++- ui/app/templates/vault/cluster.hbs | 8 +- ui/tests/unit/services/permissions-test.js | 134 +++++++++++++++++++++ 3 files changed, 185 insertions(+), 4 deletions(-) diff --git a/ui/app/services/permissions.js b/ui/app/services/permissions.js index 3e417d6d691d..c6896c727225 100644 --- a/ui/app/services/permissions.js +++ b/ui/app/services/permissions.js @@ -66,6 +66,27 @@ const API_PATHS_TO_ROUTE_PARAMS = { It fetches a users' policy from the resultant-acl endpoint and stores their allowed exact and glob paths as state. It also has methods for checking whether a user has permission for a given path. + The data from the resultant-acl endpoint has the following shape: + { + exact_paths: { + [key: string]: { + capabilities: string[]; + }; + }; + glob_paths: { + [key: string]: { + capabilities: string[]; + }; + }; + 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 Service.extend({ @@ -75,7 +96,6 @@ export default Service.extend({ permissionsBanner: null, chrootNamespace: null, store: service(), - auth: service(), namespace: service(), get baseNs() { @@ -101,6 +121,27 @@ export default Service.extend({ } }), + get wildcardPath() { + const ns = [sanitizePath(this.chrootNamespace), sanitizePath(this.namespace.userRootNamespace)].join('/'); + // wildcard path comes back from root namespace as empty string, + // 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 + */ + 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.set('permissionsBanner', null); @@ -108,7 +149,11 @@ export default Service.extend({ } 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.set('permissionsBanner', allowed ? null : PERMISSIONS_BANNER_STATES.noAccess); }, diff --git a/ui/app/templates/vault/cluster.hbs b/ui/app/templates/vault/cluster.hbs index 026479172947..bc7993df30bc 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|}} @@ -101,6 +98,11 @@ {{#if this.auth.isActiveSession}} + {{#if this.permissionBanner}} +
+ +
+ {{/if}} {{outlet}}
{{else}} 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); + }); + }); + }); });