Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI: always send capabilities-self request in user's root namespace #24168

Merged
merged 6 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/24168.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
ui: capabilities-self is always called in the user's root namespace
```
16 changes: 15 additions & 1 deletion ui/app/adapters/capabilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,28 @@
import AdapterError from '@ember-data/adapter/error';
import { set } from '@ember/object';
import ApplicationAdapter from './application';
import { sanitizePath } from 'core/utils/sanitize-path';

export default ApplicationAdapter.extend({
pathForType() {
return 'capabilities-self';
},

formatPaths(path) {
const { relativeNamespace } = this.namespaceService;
if (!relativeNamespace) {
return [path];
}
// ensure original path doesn't have leading slash
return [`${relativeNamespace}/${path.replace(/^\//, '')}`];
},

findRecord(store, type, id) {
return this.ajax(this.buildURL(type), 'POST', { data: { paths: [id] } }).catch((e) => {
const paths = this.formatPaths(id);
return this.ajax(this.buildURL(type), 'POST', {
data: { paths },
namespace: sanitizePath(this.namespaceService.userRootNamespace),
}).catch((e) => {
if (e instanceof AdapterError) {
set(e, 'policyPath', 'sys/capabilities-self');
}
Expand Down
8 changes: 8 additions & 0 deletions ui/app/services/namespace.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { alias, equal } from '@ember/object/computed';
import Service, { inject as service } from '@ember/service';
import { task } from 'ember-concurrency';
import { computed } from '@ember/object';
import { getRelativePath } from 'core/utils/sanitize-path';

const ROOT_NAMESPACE = '';
export default Service.extend({
Expand All @@ -28,6 +29,13 @@ export default Service.extend({
return parts[parts.length - 1];
}),

relativeNamespace: computed('path', 'userRootNamespace', function () {
// relative namespace is the current namespace minus the user's root.
// so if we're in app/staging/group1 but the user's root is app, the
// relative namespace is staging/group
return getRelativePath(this.path, this.userRootNamespace);
}),

setNamespace(path) {
if (!path) {
this.set('path', '');
Expand Down
20 changes: 20 additions & 0 deletions ui/lib/core/addon/utils/sanitize-path.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,30 @@
*/

export function sanitizePath(path) {
if (!path) return '';
//remove whitespace + remove trailing and leading slashes
return path.trim().replace(/^\/+|\/+$/g, '');
}

export function ensureTrailingSlash(path) {
return path.replace(/(\w+[^/]$)/g, '$1/');
}

/**
* getRelativePath is for removing matching segments of a subpath from the front of a full path.
* This method assumes that the full path starts with all of the root path.
* @param {string} fullPath eg apps/prod/app_1/test
* @param {string} rootPath eg apps/prod
* @returns the leftover segment, eg app_1/test
*/
export function getRelativePath(fullPath = '', rootPath = '') {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially had this logic within the namespace computed property, but it was much easier to test as a util

const root = sanitizePath(rootPath);
const full = sanitizePath(fullPath);

if (!root) {
return full;
} else if (root === full) {
return '';
}
return sanitizePath(full.substring(root.length));
}
2 changes: 1 addition & 1 deletion ui/lib/core/app/utils/sanitize-path.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
* SPDX-License-Identifier: BUSL-1.1
*/

export { ensureTrailingSlash, sanitizePath } from 'core/utils/sanitize-path';
export { ensureTrailingSlash, sanitizePath, getRelativePath } from 'core/utils/sanitize-path';
41 changes: 41 additions & 0 deletions ui/tests/unit/adapters/capabilities-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,45 @@ module('Unit | Adapter | capabilities', function (hooks) {
assert.deepEqual({ paths: ['foo'] }, options.data, 'data params OK');
assert.strictEqual(method, 'POST', 'method OK');
});

test('enterprise calls the correct url within namespace when userRoot = root', function (assert) {
const namespaceSvc = this.owner.lookup('service:namespace');
namespaceSvc.setNamespace('admin');

let url, method, options;
const adapter = this.owner.factoryFor('adapter:capabilities').create({
ajax: (...args) => {
[url, method, options] = args;
return resolve();
},
});

adapter.findRecord(null, 'capabilities', 'foo');
assert.strictEqual(url, '/v1/sys/capabilities-self', 'calls the correct URL');
assert.deepEqual({ paths: ['admin/foo'] }, options.data, 'data params prefix paths with namespace');
assert.strictEqual(options.namespace, '', 'sent with root namespace');
assert.strictEqual(method, 'POST', 'method OK');
});

test('enterprise calls the correct url within namespace when userRoot is not root', function (assert) {
const namespaceSvc = this.owner.lookup('service:namespace');
namespaceSvc.setNamespace('admin/bar/baz');
namespaceSvc.reopen({
userRootNamespace: 'admin/bar',
});

let url, method, options;
const adapter = this.owner.factoryFor('adapter:capabilities').create({
ajax: (...args) => {
[url, method, options] = args;
return resolve();
},
});

adapter.findRecord(null, 'capabilities', 'foo');
assert.strictEqual(url, '/v1/sys/capabilities-self', 'calls the correct URL');
assert.deepEqual({ paths: ['baz/foo'] }, options.data, 'data params prefix path with relative namespace');
assert.strictEqual(options.namespace, 'admin/bar', 'sent with root namespace');
assert.strictEqual(method, 'POST', 'method OK');
});
});
11 changes: 10 additions & 1 deletion ui/tests/unit/utils/sanitize-path-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import { module, test } from 'qunit';
import { ensureTrailingSlash, sanitizePath } from 'core/utils/sanitize-path';
import { ensureTrailingSlash, getRelativePath, sanitizePath } from 'core/utils/sanitize-path';

module('Unit | Utility | sanitize-path', function () {
test('it removes spaces and slashes from either side', function (assert) {
Expand All @@ -14,10 +14,19 @@ module('Unit | Utility | sanitize-path', function () {
'removes spaces and slashes on either side'
);
assert.strictEqual(sanitizePath('//foo/bar/baz/'), 'foo/bar/baz', 'removes more than one slash');
assert.strictEqual(sanitizePath(undefined), '', 'handles falsey values');
});

test('#ensureTrailingSlash', function (assert) {
assert.strictEqual(ensureTrailingSlash('foo/bar'), 'foo/bar/', 'adds trailing slash');
assert.strictEqual(ensureTrailingSlash('baz/'), 'baz/', 'keeps trailing slash if there is one');
});

test('#getRelativePath', function (assert) {
assert.strictEqual(getRelativePath('/', undefined), '', 'works with minimal inputs');
assert.strictEqual(getRelativePath('/baz/bar/', undefined), 'baz/bar', 'sanitizes the output');
assert.strictEqual(getRelativePath('recipes/cookies/choc-chip/', 'recipes/'), 'cookies/choc-chip');
assert.strictEqual(getRelativePath('/recipes/cookies/choc-chip/', 'recipes/cookies'), 'choc-chip');
assert.strictEqual(getRelativePath('/admin/bop/boop/admin_foo/baz/', 'admin'), 'bop/boop/admin_foo/baz');
});
});
Loading