mirror of
https://github.com/hashicorp/vault.git
synced 2026-02-18 18:38:08 -05:00
UI: Fix namespace context in capabilities service (#31276)
* test coverage for namespace capabilities checks; * set namespace header with appropriate context * add more test coverage, restore stub * add changelog
This commit is contained in:
parent
8f522a2bca
commit
e29c6e3496
3 changed files with 158 additions and 11 deletions
3
changelog/31276.txt
Normal file
3
changelog/31276.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:bug
|
||||
ui: Fix regression in 1.20.0 to properly set namespace context for capabilities checks
|
||||
```
|
||||
|
|
@ -34,7 +34,7 @@ export default class CapabilitiesService extends Service {
|
|||
Users don't always have access to the capabilities-self endpoint in the current namespace.
|
||||
This can happen when logging in to a namespace and then navigating to a child namespace.
|
||||
The "relativeNamespace" refers to the namespace the user is currently in and attempting to access capabilities for.
|
||||
Prepending "relativeNamespace" to the path while making the request to the "userRootNamespace"
|
||||
Prepending "relativeNamespace" to the path while making the request in the "userRootNamespace" context (meaning, "userRootNamespace" is the namespace header)
|
||||
ensures we are querying capabilities-self where the user is most likely to have their policy/permissions.
|
||||
*/
|
||||
relativeNamespacePath(path: string) {
|
||||
|
|
@ -83,16 +83,13 @@ export default class CapabilitiesService extends Service {
|
|||
}
|
||||
|
||||
async fetch(paths: string[]): Promise<CapabilitiesMap> {
|
||||
const payload = {
|
||||
paths: paths.map((path) => this.relativeNamespacePath(path)),
|
||||
namespace: sanitizePath(this.namespace.userRootNamespace),
|
||||
};
|
||||
if (!payload.namespace) {
|
||||
delete payload.namespace;
|
||||
}
|
||||
const payload = { paths: paths.map((path) => this.relativeNamespacePath(path)) };
|
||||
|
||||
try {
|
||||
const { data } = await this.api.sys.queryTokenSelfCapabilities(payload);
|
||||
const { data } = await this.api.sys.queryTokenSelfCapabilities(
|
||||
payload,
|
||||
this.api.buildHeaders({ namespace: sanitizePath(this.namespace.userRootNamespace) })
|
||||
);
|
||||
return this.mapCapabilities(paths, data as CapabilitiesData);
|
||||
} catch (e) {
|
||||
// default to true if there is a problem fetching the model
|
||||
|
|
|
|||
|
|
@ -254,9 +254,9 @@ module('Unit | Service | capabilities', function (hooks) {
|
|||
});
|
||||
});
|
||||
|
||||
module('within namespace', function (hooks) {
|
||||
module('within a namespace', function (hooks) {
|
||||
// capabilities within namespaces are queried at the user's root namespace with a path that includes
|
||||
// the relative namespace. The capabilities record is saved at the path without the namespace.
|
||||
// the relative namespace.
|
||||
hooks.beforeEach(function () {
|
||||
this.nsSvc = this.owner.lookup('service:namespace');
|
||||
this.nsSvc.path = 'ns1';
|
||||
|
|
@ -344,5 +344,152 @@ module('Unit | Service | capabilities', function (hooks) {
|
|||
};
|
||||
assert.propEqual(actual, expected, 'method returns expected response');
|
||||
});
|
||||
|
||||
/*
|
||||
The setup in this test simulates a user whose auth method is mounted in the "root" namespace
|
||||
but their policy only grants access to paths in the context of the "ns1" namespace.
|
||||
|
||||
* ~Example policy paths~ *
|
||||
# explicitly grants access to read "my-secret" in the kv engine mounted in the "ns1" namespace
|
||||
path "ns1/kv/data/my-secret" {
|
||||
capabilities = ["read", "delete"]
|
||||
}
|
||||
|
||||
# alternatively, their policy could grant access to read everything within the "ns1" namespace
|
||||
path "ns1/*" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
*/
|
||||
test(`if the user's root namespace is "root" and the resource is in a child namespace`, async function (assert) {
|
||||
assert.expect(2);
|
||||
const ns = this.nsSvc.path;
|
||||
const paths = ['my/api/path', '/another/api/path'];
|
||||
const expectedPayload = [`${ns}/my/api/path`, `${ns}/another/api/path`];
|
||||
|
||||
this.server.post('/sys/capabilities-self', (schema, req) => {
|
||||
const nsHeader = req.requestHeaders['x-vault-namespace'];
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.strictEqual(nsHeader, '', 'request is made in the context of the "root" namespace');
|
||||
assert.propEqual(payload.paths, expectedPayload, `paths include the relative namespace`);
|
||||
return req.passthrough();
|
||||
});
|
||||
await this.capabilities.fetch(paths);
|
||||
});
|
||||
|
||||
/*
|
||||
The setup in this test simulates a user whose root namespace is "root" and
|
||||
they are accessing a resource at a nested namespace: "ns1/child".
|
||||
*/
|
||||
test(`if the user's root namespace is "root" and the resource is in a grandchild`, async function (assert) {
|
||||
assert.expect(2);
|
||||
// the path in the namespace service is always the FULL namespace path of the current context
|
||||
this.nsSvc.path = 'ns1/child';
|
||||
|
||||
const paths = ['my/api/path', '/another/api/path'];
|
||||
const expectedPaths = ['ns1/child/my/api/path', 'ns1/child/another/api/path'];
|
||||
|
||||
this.server.post('/sys/capabilities-self', (schema, req) => {
|
||||
const nsHeader = req.requestHeaders['x-vault-namespace'];
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.strictEqual(nsHeader, '', 'request is made in the context of the "root" namespace');
|
||||
assert.propEqual(payload.paths, expectedPaths, `paths include the relative namespace`);
|
||||
return req.passthrough();
|
||||
});
|
||||
|
||||
await this.capabilities.fetch(paths);
|
||||
});
|
||||
|
||||
/*
|
||||
The setup in this test simulates a user whose auth method is mounted in the "ns1" namespace and so cannot log in directly to "root" at all.
|
||||
Since this user's context (along with their policy) is exclusively "ns1" the paths do not include the namespace.
|
||||
|
||||
* ~Example policy paths~ *
|
||||
path "kv/data/my-secret" {
|
||||
capabilities = ["read", "delete"]
|
||||
}
|
||||
*/
|
||||
test(`if the user's root namespace is an immediate child of "root" and they are accessing resources in the same namespace context`, async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const ns = this.nsSvc.path;
|
||||
const authService = this.owner.lookup('service:auth');
|
||||
const authStub = sinon.stub(authService, 'authData').value({ userRootNamespace: ns });
|
||||
|
||||
const paths = ['my/api/path', '/another/api/path'];
|
||||
|
||||
this.server.post('/sys/capabilities-self', (schema, req) => {
|
||||
const nsHeader = req.requestHeaders['x-vault-namespace'];
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.strictEqual(nsHeader, 'ns1', 'request is made in the context of the "ns1" namespace');
|
||||
assert.propEqual(
|
||||
payload.paths,
|
||||
paths,
|
||||
'paths do not include the namespace because request header manages context'
|
||||
);
|
||||
return req.passthrough();
|
||||
});
|
||||
|
||||
await this.capabilities.fetch(paths);
|
||||
authStub.restore();
|
||||
});
|
||||
|
||||
/*
|
||||
The setup in this test simulates a user whose root namespace is "ns1" and
|
||||
they are accessing a resource at a namespace one level deeper in "ns1/child".
|
||||
*/
|
||||
test(`if the user's root namespace is a child of "root" and the resource is nested one more level`, async function (assert) {
|
||||
assert.expect(2);
|
||||
// the path in the namespace service is always the FULL namespace path of the current context
|
||||
this.nsSvc.path = 'ns1/child';
|
||||
const authService = this.owner.lookup('service:auth');
|
||||
const authStub = sinon.stub(authService, 'authData').value({ userRootNamespace: 'ns1' });
|
||||
|
||||
const paths = ['my/api/path', '/another/api/path'];
|
||||
const expectedPaths = ['child/my/api/path', 'child/another/api/path'];
|
||||
|
||||
this.server.post('/sys/capabilities-self', (schema, req) => {
|
||||
const nsHeader = req.requestHeaders['x-vault-namespace'];
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.strictEqual(nsHeader, 'ns1', 'request is made in the context of the "ns1" namespace');
|
||||
assert.propEqual(payload.paths, expectedPaths, 'paths include the relative namespace');
|
||||
return req.passthrough();
|
||||
});
|
||||
|
||||
await this.capabilities.fetch(paths);
|
||||
authStub.restore();
|
||||
});
|
||||
|
||||
/*
|
||||
The setup in this test simulates a user whose root namespace is "ns1/child" and
|
||||
they are accessing a resource in the same context.
|
||||
*/
|
||||
test(`if the user's root namespace is a grandchild of "root" and the resource is in the same context`, async function (assert) {
|
||||
assert.expect(2);
|
||||
// the path in the namespace service is always the FULL namespace path of the current context
|
||||
this.nsSvc.path = 'ns1/child';
|
||||
const authService = this.owner.lookup('service:auth');
|
||||
const authStub = sinon.stub(authService, 'authData').value({ userRootNamespace: 'ns1/child' });
|
||||
|
||||
const paths = ['my/api/path', '/another/api/path'];
|
||||
|
||||
this.server.post('/sys/capabilities-self', (schema, req) => {
|
||||
const nsHeader = req.requestHeaders['x-vault-namespace'];
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.strictEqual(
|
||||
nsHeader,
|
||||
'ns1/child',
|
||||
'request is made in the context of the "ns1/child" namespace'
|
||||
);
|
||||
assert.propEqual(
|
||||
payload.paths,
|
||||
paths,
|
||||
'paths do not include namespace because header manages namespace context'
|
||||
);
|
||||
return req.passthrough();
|
||||
});
|
||||
|
||||
await this.capabilities.fetch(paths);
|
||||
authStub.restore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue