state: update policy manager when deleting users

Make DeleteUser call updatePolicyManagerUsers() to refresh the policy
manager's cached user list after user deletion. This ensures consistency
with CreateUser, UpdateUser, and RenameUser which all update the policy
manager.

Previously, DeleteUser only removed the user from the database without
updating the policy manager. This could leave stale user references in
the cached user list, potentially causing issues when policy is
re-evaluated.

The gRPC handler now uses the change returned from DeleteUser instead of
manually constructing change.UserRemoved().

Fixes #2967
This commit is contained in:
Kristoffer Dalby 2026-01-09 15:31:59 +00:00
parent 98c0817b95
commit 4be13baf3f
3 changed files with 224 additions and 6 deletions

View file

@ -28,7 +28,6 @@ import (
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/juanfont/headscale/hscontrol/state"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/types/change"
"github.com/juanfont/headscale/hscontrol/util"
)
@ -99,13 +98,13 @@ func (api headscaleV1APIServer) DeleteUser(
return nil, err
}
err = api.h.state.DeleteUser(types.UserID(user.ID))
policyChanged, err := api.h.state.DeleteUser(types.UserID(user.ID))
if err != nil {
return nil, err
}
// User deletion may affect policy, trigger a full policy re-evaluation.
api.h.Change(change.UserRemoved())
// Use the change returned from DeleteUser which includes proper policy updates
api.h.Change(policyChanged)
return &v1.DeleteUserResponse{}, nil
}

View file

@ -362,8 +362,29 @@ func (s *State) UpdateUser(userID types.UserID, updateFn func(*types.User) error
// DeleteUser permanently removes a user and all associated data (nodes, API keys, etc).
// This operation is irreversible.
func (s *State) DeleteUser(userID types.UserID) error {
return s.db.DestroyUser(userID)
// It also updates the policy manager to ensure ACL policies referencing the deleted
// user are re-evaluated immediately, fixing issue #2967.
func (s *State) DeleteUser(userID types.UserID) (change.Change, error) {
err := s.db.DestroyUser(userID)
if err != nil {
return change.Change{}, err
}
// Update policy manager with the new user list (without the deleted user)
// This ensures that if the policy references the deleted user, it gets
// re-evaluated immediately rather than when some other operation triggers it.
c, err := s.updatePolicyManagerUsers()
if err != nil {
return change.Change{}, fmt.Errorf("updating policy after user deletion: %w", err)
}
// If the policy manager doesn't detect changes, still return UserRemoved
// to ensure peer lists are refreshed
if c.IsEmpty() {
c = change.UserRemoved()
}
return c, nil
}
// RenameUser changes a user's name. The new name must be unique.