mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-01 19:17:58 +00:00
UI: Fix token renewal breaking policy checks (#29416)
* set namespace_path in renewal method * add tests * add changelog
This commit is contained in:
3
changelog/29416.txt
Normal file
3
changelog/29416.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
```release-note:bug
|
||||
ui (enterprise): Fixes token renewal to ensure capability checks are performed in the relevant namespace, resolving 'Not authorized' errors for resources that users have permission to access.
|
||||
```
|
||||
@@ -246,15 +246,14 @@ export default Service.extend({
|
||||
// haven't set a value yet
|
||||
// all of the typeof checks are necessary because the root namespace is ''
|
||||
let userRootNamespace = namespace_path && namespace_path.replace(/\/$/, '');
|
||||
// if we're logging in with token and there's no namespace_path, we can assume
|
||||
// renew-self does not return namespace_path, so we manually setting in renew().
|
||||
// so if we're logging in with token and there's no namespace_path, we can assume
|
||||
// that the token belongs to the root namespace
|
||||
if (backend === 'token' && !userRootNamespace) {
|
||||
userRootNamespace = '';
|
||||
}
|
||||
if (typeof userRootNamespace === 'undefined') {
|
||||
if (this.authData) {
|
||||
userRootNamespace = this.authData.userRootNamespace;
|
||||
}
|
||||
if (typeof userRootNamespace === 'undefined' && this.authData) {
|
||||
userRootNamespace = this.authData.userRootNamespace;
|
||||
}
|
||||
if (typeof userRootNamespace === 'undefined') {
|
||||
userRootNamespace = currentNamespace;
|
||||
@@ -374,7 +373,13 @@ export default Service.extend({
|
||||
return this.renewCurrentToken().then(
|
||||
(resp) => {
|
||||
this.isRenewing = false;
|
||||
return this.persistAuthData(tokenName, resp.data || resp.auth);
|
||||
const namespacePath = this.namespaceService.path;
|
||||
const response = resp.data || resp.auth;
|
||||
// renew-self does not return namespace_path, so manually add it if it exists
|
||||
if (!response?.namespace_path && namespacePath) {
|
||||
response.namespace_path = namespacePath;
|
||||
}
|
||||
return this.persistAuthData(tokenName, response);
|
||||
},
|
||||
(e) => {
|
||||
this.isRenewing = false;
|
||||
|
||||
@@ -9,12 +9,18 @@ import { click, currentURL, visit, waitUntil, find, fillIn } from '@ember/test-h
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { allSupportedAuthBackends, supportedAuthBackends } from 'vault/helpers/supported-auth-backends';
|
||||
import VAULT_KEYS from 'vault/tests/helpers/vault-keys';
|
||||
import {
|
||||
createNS,
|
||||
createPolicyCmd,
|
||||
mountAuthCmd,
|
||||
mountEngineCmd,
|
||||
runCmd,
|
||||
} from 'vault/tests/helpers/commands';
|
||||
import { login, loginMethod, loginNs, logout } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { GENERAL } from '../helpers/general-selectors';
|
||||
|
||||
const AUTH_FORM = {
|
||||
method: '[data-test-select=auth-method]',
|
||||
token: '[data-test-token]',
|
||||
login: '[data-test-auth-submit]',
|
||||
};
|
||||
const ENT_AUTH_METHODS = ['saml'];
|
||||
const { rootToken } = VAULT_KEYS;
|
||||
|
||||
@@ -39,10 +45,10 @@ module('Acceptance | auth', function (hooks) {
|
||||
|
||||
test('it clears token when changing selected auth method', async function (assert) {
|
||||
await visit('/vault/auth');
|
||||
await fillIn(AUTH_FORM.token, 'token');
|
||||
await fillIn(AUTH_FORM.input('token'), 'token');
|
||||
await fillIn(AUTH_FORM.method, 'github');
|
||||
await fillIn(AUTH_FORM.method, 'token');
|
||||
assert.dom(AUTH_FORM.token).hasNoValue('it clears the token value when toggling methods');
|
||||
assert.dom(AUTH_FORM.input('token')).hasNoValue('it clears the token value when toggling methods');
|
||||
});
|
||||
|
||||
module('it sends the right payload when authenticating', function (hooks) {
|
||||
@@ -202,10 +208,85 @@ module('Acceptance | auth', function (hooks) {
|
||||
() => new Error('should not call renew-self directly after logging in')
|
||||
);
|
||||
|
||||
await visit('/vault/auth');
|
||||
await fillIn(AUTH_FORM.method, 'token');
|
||||
await fillIn(AUTH_FORM.token, rootToken);
|
||||
await click('[data-test-auth-submit]');
|
||||
await login(rootToken);
|
||||
assert.strictEqual(currentURL(), '/vault/dashboard');
|
||||
});
|
||||
|
||||
module('Enterprise', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
const uid = uuidv4();
|
||||
this.ns = `admin-${uid}`;
|
||||
// log in to root to create namespace
|
||||
await login();
|
||||
await runCmd(createNS(this.ns), false);
|
||||
// login to namespace, mount userpass, create policy and user
|
||||
await loginNs(this.ns);
|
||||
this.db = `database-${uid}`;
|
||||
this.userpass = `userpass-${uid}`;
|
||||
this.user = 'bob';
|
||||
this.policyName = `policy-${this.userpass}`;
|
||||
this.policy = `
|
||||
path "${this.db}/" {
|
||||
capabilities = ["list"]
|
||||
}
|
||||
path "${this.db}/roles" {
|
||||
capabilities = ["read","list"]
|
||||
}
|
||||
`;
|
||||
await runCmd([
|
||||
mountAuthCmd('userpass', this.userpass),
|
||||
mountEngineCmd('database', this.db),
|
||||
createPolicyCmd(this.policyName, this.policy),
|
||||
`write auth/${this.userpass}/users/${this.user} password=${this.user} token_policies=${this.policyName}`,
|
||||
]);
|
||||
return await logout();
|
||||
});
|
||||
|
||||
hooks.afterEach(async function () {
|
||||
await visit(`/vault/logout?namespace=${this.ns}`);
|
||||
await fillIn(AUTH_FORM.namespaceInput, ''); // clear login form namespace input
|
||||
await login();
|
||||
await runCmd([`delete sys/namespaces/${this.ns}`], false);
|
||||
});
|
||||
|
||||
// this test is specifically to cover a token renewal bug within namespaces
|
||||
// namespace_path isn't returned by the renew-self response and so the auth service was
|
||||
// incorrectly setting userRootNamespace to '' (which denotes 'root')
|
||||
// making subsequent capability checks fail because they would not be queried with the appropriate namespace header
|
||||
// if this test fails because a POST /v1/sys/capabilities-self returns a 403, then we have a problem!
|
||||
test('it sets namespace when renewing token', async function (assert) {
|
||||
await login();
|
||||
await runCmd([
|
||||
mountAuthCmd('userpass', this.userpass),
|
||||
mountEngineCmd('database', this.db),
|
||||
createPolicyCmd(this.policyName, this.policy),
|
||||
`write auth/${this.userpass}/users/${this.user} password=${this.user} token_policies=${this.policyName}`,
|
||||
]);
|
||||
|
||||
const options = { username: this.user, password: this.user, 'auth-form-mount-path': this.userpass };
|
||||
|
||||
// login as user just to get token (this is the only way to generate a token in the UI right now..)
|
||||
await loginMethod('userpass', options, { toggleOptions: true, ns: this.ns });
|
||||
await click('[data-test-user-menu-trigger=""]');
|
||||
const token = find('[data-test-copy-button]').getAttribute('data-test-copy-button');
|
||||
|
||||
// login with token to reproduce bug
|
||||
await loginNs(this.ns, token);
|
||||
await visit(`/vault/secrets/${this.db}/overview?namespace=${this.ns}`);
|
||||
assert
|
||||
.dom('[data-test-overview-card="Roles"]')
|
||||
.hasText('Roles Create new', 'database overview renders');
|
||||
// renew token
|
||||
await click('[data-test-user-menu-trigger=""]');
|
||||
await click('[data-test-user-menu-item="renew token"]');
|
||||
// navigate out and back to overview tab to re-request capabilities
|
||||
await click(GENERAL.secretTab('Roles'));
|
||||
await click(GENERAL.tab('overview'));
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/vault/secrets/${this.db}/overview?namespace=${this.ns}`,
|
||||
'it navigates to database overview'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
export const AUTH_FORM = {
|
||||
method: '[data-test-select=auth-method]',
|
||||
form: '[data-test-auth-form]',
|
||||
login: '[data-test-auth-submit]',
|
||||
tabs: (method: string) => (method ? `[data-test-auth-method="${method}"]` : '[data-test-auth-method]'),
|
||||
|
||||
@@ -9,6 +9,7 @@ import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
|
||||
|
||||
const { rootToken } = VAULT_KEYS;
|
||||
|
||||
// LOGIN WITH TOKEN
|
||||
export const login = async (token = rootToken) => {
|
||||
// make sure we're always logged out and logged back in
|
||||
await logout();
|
||||
@@ -26,6 +27,30 @@ export const loginNs = async (ns: string, token = rootToken) => {
|
||||
return click(AUTH_FORM.login);
|
||||
};
|
||||
|
||||
// LOGIN WITH NON-TOKEN methods
|
||||
/*
|
||||
inputValues are for filling in the form values
|
||||
the key completes to the input's test selector and fills it in with the corresponding value
|
||||
for example: { username: 'bob', password: 'my-password', 'auth-form-mount-path': 'userpasss1' };
|
||||
*/
|
||||
export const loginMethod = async (
|
||||
methodType: string,
|
||||
inputValues: object,
|
||||
{ toggleOptions = false, ns = '' }
|
||||
) => {
|
||||
// make sure we're always logged out and logged back in
|
||||
await logout();
|
||||
await visit(`/vault/auth?with=${methodType}`);
|
||||
|
||||
if (ns) await fillIn(AUTH_FORM.namespaceInput, ns);
|
||||
if (toggleOptions) await click(AUTH_FORM.moreOptions);
|
||||
|
||||
for (const [input, value] of Object.entries(inputValues)) {
|
||||
await fillIn(AUTH_FORM.input(input), value);
|
||||
}
|
||||
return click(AUTH_FORM.login);
|
||||
};
|
||||
|
||||
export const logout = async () => {
|
||||
// make sure we're always logged out and logged back in
|
||||
await visit('/vault/logout');
|
||||
|
||||
Reference in New Issue
Block a user