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

* Add getRelativePath helper and use to calculate relativeNamespace

* Always request capabilities-self on users root ns and prefix body with relative path

* Update capabilities adapter with test

* add changelog

* Simplify getRelativePath logic

* test update
This commit is contained in:
Chelsea Shaw
2023-11-21 10:03:06 -06:00
committed by GitHub
parent 13e1c4240f
commit 638a194580
7 changed files with 104 additions and 3 deletions

3
changelog/24168.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:improvement
ui: capabilities-self is always called in the user's root namespace
```

View File

@@ -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');
}

View File

@@ -6,6 +6,8 @@
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({
@@ -20,6 +22,13 @@ export default Service.extend({
inRootNamespace: equal('path', ROOT_NAMESPACE),
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', '');

View File

@@ -1,4 +1,5 @@
export function sanitizePath(path) {
if (!path) return '';
//remove whitespace + remove trailing and leading slashes
return path.trim().replace(/^\/+|\/+$/g, '');
}
@@ -6,3 +7,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));
}

View File

@@ -1 +1,6 @@
export { ensureTrailingSlash, sanitizePath } from 'core/utils/sanitize-path';
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
export { ensureTrailingSlash, sanitizePath, getRelativePath } from 'core/utils/sanitize-path';

View File

@@ -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');
});
});

View File

@@ -1,5 +1,5 @@
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) {
@@ -9,10 +9,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');
});
});