From 564fa4ab04dcd18831c67eb31c28fa62e141db30 Mon Sep 17 00:00:00 2001 From: Vlad <4941176+crn4@users.noreply.github.com> Date: Thu, 19 Feb 2026 18:34:28 +0100 Subject: [PATCH] [management] fix possible race condition on user role change (#5395) --- management/server/user.go | 13 +++++- management/server/user_test.go | 84 ++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/management/server/user.go b/management/server/user.go index 48005f325..924efc1e4 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -737,6 +737,14 @@ func (am *DefaultAccountManager) processUserUpdate(ctx context.Context, transact return false, nil, nil, nil, status.Errorf(status.InvalidArgument, "provided user update is nil") } + if initiatorUserId != activity.SystemInitiator { + freshInitiator, err := transaction.GetUserByUserID(ctx, store.LockingStrengthUpdate, initiatorUserId) + if err != nil { + return false, nil, nil, nil, fmt.Errorf("failed to re-read initiator user in transaction: %w", err) + } + initiatorUser = freshInitiator + } + oldUser, isNewUser, err := getUserOrCreateIfNotExists(ctx, transaction, accountID, update, addIfNotExists) if err != nil { return false, nil, nil, nil, err @@ -864,7 +872,10 @@ func validateUserUpdate(groupsMap map[string]*types.Group, initiatorUser, oldUse return nil } - // @todo double check these + if !initiatorUser.HasAdminPower() { + return status.Errorf(status.PermissionDenied, "only admins and owners can update users") + } + if initiatorUser.HasAdminPower() && initiatorUser.Id == update.Id && oldUser.Blocked != update.Blocked { return status.Errorf(status.PermissionDenied, "admins can't block or unblock themselves") } diff --git a/management/server/user_test.go b/management/server/user_test.go index 2dd1cea2e..72a19a9a5 100644 --- a/management/server/user_test.go +++ b/management/server/user_test.go @@ -2031,3 +2031,87 @@ func TestUser_Operations_WithEmbeddedIDP(t *testing.T) { t.Logf("Duplicate email error: %v", err) }) } + +func TestValidateUserUpdate_RejectsNonAdminInitiator(t *testing.T) { + groupsMap := map[string]*types.Group{} + + initiator := &types.User{ + Id: "initiator", + Role: types.UserRoleUser, + } + oldUser := &types.User{ + Id: "target", + Role: types.UserRoleUser, + } + update := &types.User{ + Id: "target", + Role: types.UserRoleOwner, + } + + err := validateUserUpdate(groupsMap, initiator, oldUser, update) + require.Error(t, err, "regular user should not be able to promote to owner") + assert.Contains(t, err.Error(), "only admins and owners can update users") +} + +func TestProcessUserUpdate_RejectsStaleInitiatorRole(t *testing.T) { + s, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir()) + require.NoError(t, err) + t.Cleanup(cleanup) + + account := newAccountWithId(context.Background(), "account1", "owner1", "", "", "", false) + + adminID := "admin1" + account.Users[adminID] = types.NewAdminUser(adminID) + + targetID := "target1" + account.Users[targetID] = types.NewRegularUser(targetID, "", "") + + require.NoError(t, s.SaveAccount(context.Background(), account)) + + demotedAdmin, err := s.GetUserByUserID(context.Background(), store.LockingStrengthNone, adminID) + require.NoError(t, err) + demotedAdmin.Role = types.UserRoleUser + require.NoError(t, s.SaveUser(context.Background(), demotedAdmin)) + + staleInitiator := &types.User{ + Id: adminID, + AccountID: account.Id, + Role: types.UserRoleAdmin, + } + + permissionsManager := permissions.NewManager(s) + am := DefaultAccountManager{ + Store: s, + eventStore: &activity.InMemoryEventStore{}, + permissionsManager: permissionsManager, + } + + settings, err := s.GetAccountSettings(context.Background(), store.LockingStrengthNone, account.Id) + require.NoError(t, err) + + groups, err := s.GetAccountGroups(context.Background(), store.LockingStrengthNone, account.Id) + require.NoError(t, err) + groupsMap := make(map[string]*types.Group, len(groups)) + for _, g := range groups { + groupsMap[g.ID] = g + } + + update := &types.User{ + Id: targetID, + Role: types.UserRoleAdmin, + } + + err = s.ExecuteInTransaction(context.Background(), func(tx store.Store) error { + _, _, _, _, txErr := am.processUserUpdate( + context.Background(), tx, groupsMap, account.Id, adminID, staleInitiator, update, false, settings, + ) + return txErr + }) + + require.Error(t, err, "processUserUpdate should reject stale initiator whose role was demoted") + assert.Contains(t, err.Error(), "only admins and owners can update users") + + targetUser, err := s.GetUserByUserID(context.Background(), store.LockingStrengthNone, targetID) + require.NoError(t, err) + assert.Equal(t, types.UserRoleUser, targetUser.Role) +}