mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-12 08:06:59 +02:00
Merge remote-tracking branch 'remotes/from/ce/main'
This commit is contained in:
commit
0490a964f9
@ -49,6 +49,7 @@ var sudoPaths = map[string]*regexp.Regexp{
|
||||
"/sys/rotate": regexp.MustCompile(`^/sys/rotate$`),
|
||||
"/sys/seal": regexp.MustCompile(`^/sys/seal$`),
|
||||
"/sys/step-down": regexp.MustCompile(`^/sys/step-down$`),
|
||||
"/identity/entity/merge": regexp.MustCompile(`^/identity/entity/merge/?$`),
|
||||
|
||||
// enterprise-only paths
|
||||
"/sys/replication/dr/primary/secondary-token": regexp.MustCompile(`^/sys/replication/dr/primary/secondary-token$`),
|
||||
|
||||
3
changelog/_14436.txt
Normal file
3
changelog/_14436.txt
Normal file
@ -0,0 +1,3 @@
|
||||
```release-note:change
|
||||
identity: Require `sudo` capability to invoke the identity entity merge API endpoint (`identity/entity/merge`).
|
||||
```
|
||||
@ -27,7 +27,7 @@
|
||||
<LinkTo
|
||||
class={{if @authenticated "active"}}
|
||||
@route="messages"
|
||||
@query={{hash authenticated=true page=1}}
|
||||
@query={{hash authenticated=true page=1 pageFilter=null status=null type=null}}
|
||||
data-test-custom-messages-tab="After user logs in"
|
||||
>
|
||||
After user logs in
|
||||
@ -37,7 +37,7 @@
|
||||
<LinkTo
|
||||
class={{unless @authenticated "active"}}
|
||||
@route="messages"
|
||||
@query={{hash authenticated=false page=1}}
|
||||
@query={{hash authenticated=false page=1 pageFilter=null status=null type=null}}
|
||||
data-test-custom-messages-tab="On login page"
|
||||
>
|
||||
On login page
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { click, visit, fillIn, findAll, waitFor } from '@ember/test-helpers';
|
||||
import { click, visit, fillIn, findAll, waitFor, currentURL } from '@ember/test-helpers';
|
||||
import { login } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import { runCmd } from 'vault/tests/helpers/commands';
|
||||
import { format, addDays, startOfDay } from 'date-fns';
|
||||
@ -205,6 +205,64 @@ module('Acceptance | Enterprise | config-ui/message', function (hooks) {
|
||||
await this.deleteMessages();
|
||||
});
|
||||
|
||||
test('it should clear filter params when switching between tabs', async function (assert) {
|
||||
await this.createMessageRepl({ title: 'tab-switch-test-1', type: 'banner', authenticated: true });
|
||||
await this.createMessageRepl({ title: 'tab-switch-test-2', type: 'modal', authenticated: false });
|
||||
|
||||
// Start on authenticated tab with filters applied
|
||||
await visit('vault/config-ui/messages?authenticated=true&pageFilter=test&status=active&type=banner');
|
||||
|
||||
// Verify filters are applied
|
||||
assert.dom(GENERAL.filter('pageFilter')).hasValue('test', 'pageFilter is set');
|
||||
assert.dom(GENERAL.filter('status')).hasValue('active', 'status filter is set');
|
||||
assert.dom(GENERAL.filter('type')).hasValue('banner', 'type filter is set');
|
||||
|
||||
// Switch to unauthenticated tab
|
||||
await click(CUSTOM_MESSAGES.tab('On login page'));
|
||||
|
||||
// Verify filters are cleared after tab switch
|
||||
assert.dom(GENERAL.filter('pageFilter')).hasValue('', 'pageFilter is cleared after tab switch');
|
||||
assert.dom(GENERAL.filter('status')).hasValue('', 'status filter is cleared after tab switch');
|
||||
assert.dom(GENERAL.filter('type')).hasValue('', 'type filter is cleared after tab switch');
|
||||
|
||||
// Verify URL params are cleared (except authenticated and page)
|
||||
const unauthenticatedUrl = currentURL();
|
||||
assert.true(unauthenticatedUrl.includes('authenticated=false'), 'authenticated param is set to false');
|
||||
assert.false(unauthenticatedUrl.includes('pageFilter'), 'pageFilter param is not in URL');
|
||||
assert.false(unauthenticatedUrl.includes('status'), 'status param is not in URL');
|
||||
assert.false(unauthenticatedUrl.includes('type'), 'type param is not in URL');
|
||||
|
||||
// Apply filters on unauthenticated tab
|
||||
await fillIn(GENERAL.filter('pageFilter'), 'modal-test');
|
||||
await fillIn(GENERAL.filter('type'), 'modal');
|
||||
await click(GENERAL.submitButton);
|
||||
|
||||
// Verify filters are applied
|
||||
assert
|
||||
.dom(GENERAL.filter('pageFilter'))
|
||||
.hasValue('modal-test', 'pageFilter is set on unauthenticated tab');
|
||||
assert.dom(GENERAL.filter('type')).hasValue('modal', 'type filter is set on unauthenticated tab');
|
||||
|
||||
// Switch back to authenticated tab
|
||||
await click(CUSTOM_MESSAGES.tab('After user logs in'));
|
||||
|
||||
// Verify filters are cleared again
|
||||
assert.dom(GENERAL.filter('pageFilter')).hasValue('', 'pageFilter is cleared when switching back');
|
||||
assert.dom(GENERAL.filter('type')).hasValue('', 'type filter is cleared when switching back');
|
||||
|
||||
// Verify URL params are cleared
|
||||
const authenticated = currentURL();
|
||||
const authenticatedParam =
|
||||
authenticated.includes('authenticated=true') || !authenticated.includes('authenticated=false');
|
||||
assert.true(authenticatedParam, 'authenticated param is set to true or uses default');
|
||||
assert.false(authenticated.includes('pageFilter'), 'pageFilter param is not in URL after switching back');
|
||||
assert.false(authenticated.includes('status'), 'status param is not in URL after switching back');
|
||||
assert.false(authenticated.includes('type'), 'type param is not in URL after switching back');
|
||||
|
||||
// delete the created messages
|
||||
await this.deleteMessages();
|
||||
});
|
||||
|
||||
test('it should display preview a message when all required fields are filled out', async function (assert) {
|
||||
await click(GENERAL.navLink('Operational tools'));
|
||||
await click(CUSTOM_MESSAGES.navLink);
|
||||
|
||||
@ -11,6 +11,7 @@ import (
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/helper/testhelpers/minimal"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIdentityStore_EntityDisabled(t *testing.T) {
|
||||
@ -353,3 +354,79 @@ func TestIdentityStore_EntityPoliciesInInitialAuth(t *testing.T) {
|
||||
t.Fatalf("policy mismatch, got policies: %v", policies)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIdentity_EntityMerge_RequiresSudo verifies the identity entity merge API endpoint requires the sudo capability (in addition to update) when called via the HTTP API.
|
||||
func TestIdentity_EntityMerge_RequiresSudo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cluster := minimal.NewTestSoloCluster(t, nil)
|
||||
client := cluster.Cores[0].Client
|
||||
rootToken := client.Token()
|
||||
|
||||
// Create two entities as root to merge.
|
||||
toEnt, err := client.Logical().Write("identity/entity", nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, toEnt)
|
||||
toID, ok := toEnt.Data["id"].(string)
|
||||
require.True(t, ok)
|
||||
require.NotEmpty(t, toID)
|
||||
|
||||
fromEnt, err := client.Logical().Write("identity/entity", nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, fromEnt)
|
||||
fromID, ok := fromEnt.Data["id"].(string)
|
||||
require.True(t, ok)
|
||||
require.NotEmpty(t, fromID)
|
||||
|
||||
// Token with update but without sudo should be denied.
|
||||
require.NoError(t, client.Sys().PutPolicy("identity-merge-no-sudo", `
|
||||
path "identity/entity/merge" {
|
||||
capabilities = ["update"]
|
||||
}
|
||||
`))
|
||||
|
||||
noSudoTok, err := client.Auth().Token().Create(&api.TokenCreateRequest{
|
||||
Policies: []string{"identity-merge-no-sudo"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, noSudoTok)
|
||||
require.NotNil(t, noSudoTok.Auth)
|
||||
require.NotEmpty(t, noSudoTok.Auth.ClientToken)
|
||||
|
||||
client.SetToken(noSudoTok.Auth.ClientToken)
|
||||
_, err = client.Logical().Write("identity/entity/merge", map[string]interface{}{
|
||||
"to_entity_id": toID,
|
||||
"from_entity_ids": []string{fromID},
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, logical.ErrPermissionDenied.Error())
|
||||
|
||||
// Token with update+sudo should succeed.
|
||||
client.SetToken(rootToken)
|
||||
require.NoError(t, client.Sys().PutPolicy("identity-merge-with-sudo", `
|
||||
path "identity/entity/merge" {
|
||||
capabilities = ["update", "sudo"]
|
||||
}
|
||||
`))
|
||||
|
||||
sudoTok, err := client.Auth().Token().Create(&api.TokenCreateRequest{
|
||||
Policies: []string{"identity-merge-with-sudo"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, sudoTok)
|
||||
require.NotNil(t, sudoTok.Auth)
|
||||
require.NotEmpty(t, sudoTok.Auth.ClientToken)
|
||||
|
||||
client.SetToken(sudoTok.Auth.ClientToken)
|
||||
_, err = client.Logical().Write("identity/entity/merge", map[string]interface{}{
|
||||
"to_entity_id": toID,
|
||||
"from_entity_ids": []string{fromID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the merge happened by checking the from entity was deleted.
|
||||
client.SetToken(rootToken)
|
||||
deleted, err := client.Logical().Read("identity/entity/id/" + fromID)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, deleted)
|
||||
}
|
||||
|
||||
@ -133,6 +133,11 @@ func NewIdentityStore(ctx context.Context, core *Core, config *logical.BackendCo
|
||||
InitializeFunc: iStore.initialize,
|
||||
ActivationFunc: iStore.activate,
|
||||
PathsSpecial: &logical.Paths{
|
||||
// Root paths require the token have sudo capability.
|
||||
Root: []string{
|
||||
// Entity merge is destructive and can operate on every entity, so requires a higher privilege as a result
|
||||
"entity/merge*",
|
||||
},
|
||||
Unauthenticated: unauthenticatedPaths,
|
||||
LocalStorage: []string{
|
||||
localAliasesBucketsPrefix,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user