feat(server): refuse local edits to hq-sourced users

The API is the boundary; hiding the control in web/ is a courtesy. A role
editable in two places is a role with two answers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mrhid6
2026-07-26 16:31:27 +01:00
co-authored by Claude Opus 5
parent 14b855fa9f
commit a05a74cf4d
2 changed files with 19 additions and 1 deletions
+16
View File
@@ -16,6 +16,16 @@ import (
var ErrLastOwner = errors.New("this is the organization's last owner promote another member to owner first")
// ErrHQManaged is returned when a caller tries to change a user this instance
// does not own.
//
// An hq-sourced row is projected from a Vantage HQ account: HQ owns its role,
// its password and its existence. A role editable in two places is a role with
// two answers, and the loser is whichever writer ran first. Refusing here
// rather than merely hiding the control in web/ is the point — the API is the
// boundary, the UI is a courtesy.
var ErrHQManaged = errors.New("this member is managed in Vantage HQ; change their role or remove them from the HQ portal")
func CountUsers() (int64, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
@@ -122,6 +132,9 @@ func UpdateUserRole(instanceID, userID, role string) error {
if err != nil {
return fmt.Errorf("user not found")
}
if target.AuthSource == models.AuthHQ {
return ErrHQManaged
}
if target.Role == models.RoleOwner && role != models.RoleOwner {
others, err := countOtherOwners(instanceID, userID)
@@ -146,6 +159,9 @@ func DeleteUser(instanceID, userID string) error {
if err != nil {
return fmt.Errorf("user not found")
}
if target.AuthSource == models.AuthHQ {
return ErrHQManaged
}
if target.Role == models.RoleOwner {
others, err := countOtherOwners(instanceID, userID)
if err != nil {