diff --git a/changelog/27263.txt b/changelog/27263.txt new file mode 100644 index 000000000000..cb008c59faee --- /dev/null +++ b/changelog/27263.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: Do not show resultant-ACL banner when ancestor namespace grants wildcard access. +``` diff --git a/ui/app/services/permissions.js b/ui/app/services/permissions.js index 3233d095996b..24c987bad26e 100644 --- a/ui/app/services/permissions.js +++ b/ui/app/services/permissions.js @@ -99,7 +99,7 @@ export default class PermissionsService extends Service { @service store; @service namespace; - get baseNs() { + get fullCurrentNamespace() { const currentNs = this.namespace.path; return this.chrootNamespace ? `${sanitizePath(this.chrootNamespace)}/${sanitizePath(currentNs)}` @@ -122,24 +122,37 @@ 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, - // 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 + * hasWildcardNsAccess checks if the user has a wildcard access to target namespace + * via full glob path or any ancestors of the target namespace + * @param {string} targetNs is the current/target namespace that we are checking access for * @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; + hasWildcardNsAccess(targetNs, globPaths = {}) { + const nsParts = sanitizePath(targetNs).split('/'); + let matchKey = null; + // For each section of the namespace, check if there is a matching wildcard path + while (nsParts.length > 0) { + // glob paths always end in a slash + const test = `${nsParts.join('/')}/`; + if (Object.keys(globPaths).includes(test)) { + matchKey = test; + break; + } + nsParts.pop(); + } + // Finally, check if user has wildcard access to the root namespace + // which is represented by an empty string + if (!matchKey && Object.keys(globPaths).includes('')) { + matchKey = ''; + } + if (null === matchKey) { + return false; + } - // if so, make sure the current namespace is a child of the wildcard path - return this.namespace.path.startsWith(this.wildcardPath); + // if there is a match make sure the capabilities do not include deny + return !this.isDenied(globPaths[matchKey]); } // This method is called to recalculate whether to show the permissionsBanner when the namespace changes @@ -148,14 +161,14 @@ export default class PermissionsService extends Service { this.permissionsBanner = null; return; } - const namespace = this.baseNs; + const namespace = this.fullCurrentNamespace; const allowed = // check if the user has wildcard access to the relative root namespace - this.hasWildcardAccess(this.globPaths) || + this.hasWildcardNsAccess(namespace, this.globPaths) || // or if any of their glob paths start with the namespace - Object.keys(this.globPaths).any((k) => k.startsWith(namespace)) || + Object.keys(this.globPaths).any((k) => k.startsWith(namespace) && !this.isDenied(this.globPaths[k])) || // or if any of their exact paths start with the namespace - Object.keys(this.exactPaths).any((k) => k.startsWith(namespace)); + Object.keys(this.exactPaths).any((k) => k.startsWith(namespace) && !this.isDenied(this.exactPaths[k])); this.permissionsBanner = allowed ? null : PERMISSIONS_BANNER_STATES.noAccess; } @@ -200,7 +213,7 @@ export default class PermissionsService extends Service { } pathNameWithNamespace(pathName) { - const namespace = this.baseNs; + const namespace = this.fullCurrentNamespace; if (namespace) { return `${sanitizePath(namespace)}/${sanitizeStart(pathName)}`; } else { diff --git a/ui/tests/unit/services/permissions-test.js b/ui/tests/unit/services/permissions-test.js index c00223e53cd4..a3eab058cba5 100644 --- a/ui/tests/unit/services/permissions-test.js +++ b/ui/tests/unit/services/permissions-test.js @@ -8,6 +8,7 @@ import { setupTest } from 'ember-qunit'; import Service from '@ember/service'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { overrideResponse } from 'vault/tests/helpers/stubs'; +import { PERMISSIONS_BANNER_STATES } from 'vault/services/permissions'; const PERMISSIONS_RESPONSE = { data: { @@ -246,78 +247,151 @@ module('Unit | Service | permissions', function (hooks) { }); }); - module('wildcardPath calculates correctly', function () { + module('permissions banner calculates correctly', function () { [ + // First set: no chroot or user root { - scenario: 'no user root or chroot', + scenario: 'when root wildcard in root namespace', + chroot: null, userRoot: '', + currentNs: 'foo/bar', + globs: { + '': { capabilities: ['read'] }, + }, + expected: { + wildcard: true, + banner: null, + fullNs: 'foo/bar', + }, + }, + { + scenario: 'when nested access granted in root namespace', chroot: null, - expectedPath: '', + userRoot: '', + currentNs: 'foo/bing', + globs: { + 'foo/': { capabilities: ['read'] }, + }, + expected: { + wildcard: true, + banner: null, + fullNs: 'foo/bing', + }, }, { - scenario: 'user root = child ns and no chroot', - userRoot: 'bar', + scenario: 'when engine access granted', chroot: null, - expectedPath: 'bar/', + userRoot: '', + currentNs: 'foo/bing', + globs: { + 'foo/bing/kv/data/': { capabilities: ['read'] }, + }, + expected: { + wildcard: false, + banner: null, + fullNs: 'foo/bing', + }, }, + // Second set: chroot and user root (currentNs excludes chroot) { - scenario: 'user root = child ns and chroot set', + scenario: 'when namespace wildcard in child ns & chroot', + chroot: 'foo/', userRoot: 'bar', - chroot: 'admin/', - expectedPath: 'admin/bar/', + currentNs: 'bar/baz', + globs: { + 'foo/bar/': { capabilities: ['read'] }, + }, + expected: { + wildcard: true, + banner: null, + fullNs: 'foo/bar/baz', + }, }, { - scenario: 'no user root and chroot set', - userRoot: '', - chroot: 'admin/', - expectedPath: 'admin/', + scenario: 'when namespace wildcard in different ns than user root', + chroot: 'foo/', + userRoot: 'bar', + currentNs: 'bing', + globs: { + 'foo/bar/': { capabilities: ['read'] }, + }, + expected: { + wildcard: false, + banner: PERMISSIONS_BANNER_STATES.noAccess, + fullNs: 'foo/bing', + }, }, - ].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', + scenario: 'when engine access granted with chroot and user root', + chroot: 'foo/', + userRoot: 'bing', + currentNs: 'bing', globs: { - '': { capabilities: ['read'] }, + 'foo/bing/kv/data/': { capabilities: ['read'] }, + }, + expected: { + wildcard: false, + banner: null, + fullNs: 'foo/bing', }, - expectedAccess: true, }, + // Third set: chroot only (currentNs excludes chroot) { scenario: 'when root wildcard in chroot ns', chroot: 'admin/', userRoot: '', - currentNs: 'admin/child', + currentNs: 'child', globs: { 'admin/': { capabilities: ['read'] }, }, + expected: { + wildcard: true, + banner: null, + fullNs: 'admin/child', + }, expectedAccess: true, }, + { + scenario: 'when nested access granted in root namespace and chroot', + chroot: 'foo/', + userRoot: '', + currentNs: 'bing/baz', + globs: { + 'foo/bing/': { capabilities: ['read'] }, + }, + expected: { + wildcard: true, + banner: null, + fullNs: 'foo/bing/baz', + }, + }, + { + scenario: 'when engine access granted with chroot', + chroot: 'foo/', + userRoot: '', + currentNs: 'bing', + globs: { + 'foo/bing/kv/data/': { capabilities: ['read'] }, + }, + expected: { + wildcard: false, + banner: null, + fullNs: 'foo/bing', + }, + }, + // Fourth set: user root, no chroot + { + scenario: 'when globs is empty', + chroot: null, + userRoot: 'foo', + currentNs: 'foo/bing', + globs: {}, + expected: { + wildcard: false, + banner: PERMISSIONS_BANNER_STATES.noAccess, + fullNs: 'foo/bing', + }, + }, { scenario: 'when namespace wildcard in child ns', chroot: null, @@ -326,56 +400,89 @@ module('Unit | Service | permissions', function (hooks) { globs: { 'bar/': { capabilities: ['read'] }, }, - expectedAccess: true, + expected: { + wildcard: true, + banner: null, + fullNs: 'bar/baz', + }, }, { - scenario: 'when namespace wildcard in child ns & chroot', - chroot: 'foo/', + scenario: 'when namespace wildcard in different ns', + chroot: null, userRoot: 'bar', - currentNs: 'foo/bar/baz', + currentNs: 'foo/bing', globs: { - 'foo/bar/': { capabilities: ['read'] }, + 'bar/': { capabilities: ['read'] }, }, - expectedAccess: true, + expected: { + wildcard: false, + banner: PERMISSIONS_BANNER_STATES.noAccess, + fullNs: 'foo/bing', + }, + expectedAccess: false, }, { - scenario: 'when namespace wildcard in different ns with chroot & user root', - chroot: 'foo/', - userRoot: 'bar', - currentNs: 'foo/bing', + scenario: 'when access granted via parent namespace in child ns', + chroot: null, + userRoot: 'foo', + currentNs: 'foo/bing/baz', globs: { - 'foo/bar/': { capabilities: ['read'] }, + 'foo/bing/': { capabilities: ['read'] }, + }, + expected: { + wildcard: true, + banner: null, + fullNs: 'foo/bing/baz', }, - expectedAccess: false, }, { - scenario: 'when namespace wildcard in different ns without chroot', + scenario: 'when namespace access denied for child ns', chroot: null, userRoot: 'bar', - currentNs: 'foo/bing', + currentNs: 'bar/baz/bin', globs: { 'bar/': { capabilities: ['read'] }, + 'bar/baz/': { capabilities: ['deny'] }, + }, + expected: { + wildcard: false, + banner: PERMISSIONS_BANNER_STATES.noAccess, + fullNs: 'bar/baz/bin', }, - expectedAccess: false, }, { - scenario: 'when globs is empty', - chroot: 'foo/', - userRoot: 'bar', + scenario: 'when engine access granted with user root', + chroot: null, + userRoot: 'foo', currentNs: 'foo/bing', - globs: {}, - expectedAccess: false, + globs: { + 'foo/bing/kv/data/': { capabilities: ['read'] }, + }, + expected: { + wildcard: false, + banner: null, + fullNs: 'foo/bing', + }, }, ].forEach((testCase) => { - test(`when ${testCase.scenario}`, function (assert) { + test(`${testCase.scenario}`, async function (assert) { const namespaceService = Service.extend({ - path: testCase.currentNs, userRootNamespace: testCase.userRoot, + path: testCase.currentNs, }); this.owner.register('service:namespace', namespaceService); - this.service.set('chrootNamespace', testCase.chroot); - const result = this.service.hasWildcardAccess(testCase.globs); - assert.strictEqual(result, testCase.expectedAccess); + this.service.setPaths({ + data: { + glob_paths: testCase.globs, + exact_paths: {}, + chroot_namespace: testCase.chroot, + }, + }); + const fullNamespace = this.service.fullCurrentNamespace; + assert.strictEqual(fullNamespace, testCase.expected.fullNs); + const wildcardResult = this.service.hasWildcardNsAccess(fullNamespace, testCase.globs); + assert.strictEqual(wildcardResult, testCase.expected.wildcard); + assert.strictEqual(this.service.permissionsBanner, testCase.expected.banner); }); }); });