diff --git a/changelog/24168.txt b/changelog/24168.txt new file mode 100644 index 000000000000..09f34ce8621c --- /dev/null +++ b/changelog/24168.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: capabilities-self is always called in the user's root namespace +``` \ No newline at end of file diff --git a/ui/app/adapters/capabilities.js b/ui/app/adapters/capabilities.js index de54824bbb0f..8eee53c3e27b 100644 --- a/ui/app/adapters/capabilities.js +++ b/ui/app/adapters/capabilities.js @@ -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'); } diff --git a/ui/app/services/namespace.js b/ui/app/services/namespace.js index 07802320a031..cd8e65e67e05 100644 --- a/ui/app/services/namespace.js +++ b/ui/app/services/namespace.js @@ -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({ @@ -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', ''); diff --git a/ui/lib/core/addon/utils/sanitize-path.js b/ui/lib/core/addon/utils/sanitize-path.js index bfd200a362d5..7ca66c651de6 100644 --- a/ui/lib/core/addon/utils/sanitize-path.js +++ b/ui/lib/core/addon/utils/sanitize-path.js @@ -4,6 +4,7 @@ */ export function sanitizePath(path) { + if (!path) return ''; //remove whitespace + remove trailing and leading slashes return path.trim().replace(/^\/+|\/+$/g, ''); } @@ -11,3 +12,22 @@ export function sanitizePath(path) { 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 = '') { + const root = sanitizePath(rootPath); + const full = sanitizePath(fullPath); + + if (!root) { + return full; + } else if (root === full) { + return ''; + } + return sanitizePath(full.substring(root.length)); +} diff --git a/ui/lib/core/app/utils/sanitize-path.js b/ui/lib/core/app/utils/sanitize-path.js index 58a60b33d422..27a0db30e1c6 100644 --- a/ui/lib/core/app/utils/sanitize-path.js +++ b/ui/lib/core/app/utils/sanitize-path.js @@ -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'; diff --git a/ui/tests/unit/adapters/capabilities-test.js b/ui/tests/unit/adapters/capabilities-test.js index bcd1f055b551..2e0bb0f33092 100644 --- a/ui/tests/unit/adapters/capabilities-test.js +++ b/ui/tests/unit/adapters/capabilities-test.js @@ -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'); + }); }); diff --git a/ui/tests/unit/utils/sanitize-path-test.js b/ui/tests/unit/utils/sanitize-path-test.js index cdde888100c9..7107d737f2c5 100644 --- a/ui/tests/unit/utils/sanitize-path-test.js +++ b/ui/tests/unit/utils/sanitize-path-test.js @@ -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) { @@ -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'); + }); });