mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 17:52:32 +00:00
UI: Don't show resultant-ACL banner in nested namespace if ancestor wildcard policy (#27263)
* Refactor hasWildcardAccess to check for ancestors of current namespace in globPaths * Add extra test coverage * remove tests for removed getter * changelog + test update * update test coverage to also check resulting permissionsBanner state * rename hasWildcardAccess for clarity, add isDenied check on namespace access check * Update changelog/27263.txt Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com> * Remove redundant check --------- Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>
This commit is contained in:
3
changelog/27263.txt
Normal file
3
changelog/27263.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
```release-note:bug
|
||||
ui: Do not show resultant-ACL banner when ancestor namespace grants wildcard access.
|
||||
```
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,58 +247,9 @@ 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.
|
||||
module('permissions banner calculates correctly', function () {
|
||||
[
|
||||
// First set: no chroot or user root
|
||||
{
|
||||
scenario: 'when root wildcard in root namespace',
|
||||
chroot: null,
|
||||
@@ -306,18 +258,140 @@ module('Unit | Service | permissions', function (hooks) {
|
||||
globs: {
|
||||
'': { capabilities: ['read'] },
|
||||
},
|
||||
expectedAccess: true,
|
||||
expected: {
|
||||
wildcard: true,
|
||||
banner: null,
|
||||
fullNs: 'foo/bar',
|
||||
},
|
||||
},
|
||||
{
|
||||
scenario: 'when nested access granted in root namespace',
|
||||
chroot: null,
|
||||
userRoot: '',
|
||||
currentNs: 'foo/bing',
|
||||
globs: {
|
||||
'foo/': { capabilities: ['read'] },
|
||||
},
|
||||
expected: {
|
||||
wildcard: true,
|
||||
banner: null,
|
||||
fullNs: 'foo/bing',
|
||||
},
|
||||
},
|
||||
{
|
||||
scenario: 'when engine access granted',
|
||||
chroot: null,
|
||||
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: 'when namespace wildcard in child ns & chroot',
|
||||
chroot: 'foo/',
|
||||
userRoot: 'bar',
|
||||
currentNs: 'bar/baz',
|
||||
globs: {
|
||||
'foo/bar/': { capabilities: ['read'] },
|
||||
},
|
||||
expected: {
|
||||
wildcard: true,
|
||||
banner: null,
|
||||
fullNs: 'foo/bar/baz',
|
||||
},
|
||||
},
|
||||
{
|
||||
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',
|
||||
},
|
||||
},
|
||||
{
|
||||
scenario: 'when engine access granted with chroot and user root',
|
||||
chroot: 'foo/',
|
||||
userRoot: 'bing',
|
||||
currentNs: 'bing',
|
||||
globs: {
|
||||
'foo/bing/kv/data/': { capabilities: ['read'] },
|
||||
},
|
||||
expected: {
|
||||
wildcard: false,
|
||||
banner: null,
|
||||
fullNs: 'foo/bing',
|
||||
},
|
||||
},
|
||||
// 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,
|
||||
},
|
||||
{
|
||||
scenario: 'when namespace wildcard in child ns & chroot',
|
||||
chroot: 'foo/',
|
||||
userRoot: 'bar',
|
||||
currentNs: 'foo/bar/baz',
|
||||
globs: {
|
||||
'foo/bar/': { capabilities: ['read'] },
|
||||
expected: {
|
||||
wildcard: true,
|
||||
banner: null,
|
||||
fullNs: 'bar/baz',
|
||||
},
|
||||
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',
|
||||
scenario: 'when namespace wildcard in different ns',
|
||||
chroot: null,
|
||||
userRoot: 'bar',
|
||||
currentNs: 'foo/bing',
|
||||
globs: {
|
||||
'bar/': { capabilities: ['read'] },
|
||||
},
|
||||
expected: {
|
||||
wildcard: false,
|
||||
banner: PERMISSIONS_BANNER_STATES.noAccess,
|
||||
fullNs: 'foo/bing',
|
||||
},
|
||||
expectedAccess: false,
|
||||
},
|
||||
{
|
||||
scenario: 'when globs is empty',
|
||||
chroot: 'foo/',
|
||||
scenario: 'when access granted via parent namespace in child ns',
|
||||
chroot: null,
|
||||
userRoot: 'foo',
|
||||
currentNs: 'foo/bing/baz',
|
||||
globs: {
|
||||
'foo/bing/': { capabilities: ['read'] },
|
||||
},
|
||||
expected: {
|
||||
wildcard: true,
|
||||
banner: null,
|
||||
fullNs: 'foo/bing/baz',
|
||||
},
|
||||
},
|
||||
{
|
||||
scenario: 'when namespace access denied for child ns',
|
||||
chroot: null,
|
||||
userRoot: 'bar',
|
||||
currentNs: 'bar/baz/bin',
|
||||
globs: {
|
||||
'bar/': { capabilities: ['read'] },
|
||||
'bar/baz/': { capabilities: ['deny'] },
|
||||
},
|
||||
expected: {
|
||||
wildcard: false,
|
||||
banner: PERMISSIONS_BANNER_STATES.noAccess,
|
||||
fullNs: 'bar/baz/bin',
|
||||
},
|
||||
},
|
||||
{
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user