Skip to content

Commit

Permalink
UI: Don't show Resultant-ACL banner when wildcard policy present (#26233
Browse files Browse the repository at this point in the history
)

* 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
  • Loading branch information
hashishaw committed Apr 3, 2024
1 parent 8c515f2 commit c7b1f54
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 4 deletions.
47 changes: 46 additions & 1 deletion ui/app/services/permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -75,7 +96,6 @@ export default Service.extend({
permissionsBanner: null,
chrootNamespace: null,
store: service(),
auth: service(),
namespace: service(),

get baseNs() {
Expand All @@ -101,14 +121,39 @@ 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);
return;
}
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);
},
Expand Down
8 changes: 5 additions & 3 deletions ui/app/templates/vault/cluster.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,6 @@
@autoloaded={{eq this.activeCluster.licenseState "autoloaded"}}
/>
{{/if}}
{{#if this.permissionBanner}}
<ResultantAclBanner @isEnterprise={{this.activeCluster.version.isEnterprise}} @failType={{this.permissionBanner}} />
{{/if}}
</div>
<div class="global-flash">
{{#each this.flashMessages.queue as |flash|}}
Expand Down Expand Up @@ -101,6 +98,11 @@

{{#if this.auth.isActiveSession}}
<TokenExpireWarning @expirationDate={{this.auth.tokenExpirationDate}} @allowingExpiration={{this.auth.allowExpiration}}>
{{#if this.permissionBanner}}
<div class="has-top-margin-m">
<ResultantAclBanner @isEnterprise={{this.activeCluster.version.isEnterprise}} @failType={{this.permissionBanner}} />
</div>
{{/if}}
{{outlet}}
</TokenExpireWarning>
{{else}}
Expand Down
134 changes: 134 additions & 0 deletions ui/tests/unit/services/permissions-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
});

0 comments on commit c7b1f54

Please sign in to comment.