From fe4485037a25426446ced95050e9498f477bf71d Mon Sep 17 00:00:00 2001 From: h44z Date: Tue, 24 Feb 2026 22:32:37 +0100 Subject: [PATCH] Merge commit from fork --- internal/app/api/v0/backend/user_service.go | 11 +++++ internal/app/users/user_manager.go | 45 ++++++++++++++++++++- internal/domain/user.go | 25 ++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/internal/app/api/v0/backend/user_service.go b/internal/app/api/v0/backend/user_service.go index 7a0279d..35d2fc5 100644 --- a/internal/app/api/v0/backend/user_service.go +++ b/internal/app/api/v0/backend/user_service.go @@ -53,6 +53,17 @@ func (u UserService) GetAllUsers(ctx context.Context) ([]domain.User, error) { } func (u UserService) UpdateUser(ctx context.Context, user *domain.User) (*domain.User, error) { + sessionUser := domain.GetUserInfo(ctx) + currentUser, err := u.users.GetUser(ctx, user.Identifier) + if err != nil { + return nil, err + } + + // if this endpoint is used by non-admins, make sure that the user can only modify a specific subset of attributes + if !sessionUser.IsAdmin { + user.CopyAdminAttributes(currentUser, u.cfg.Advanced.ApiAdminOnly) + } + return u.users.UpdateUser(ctx, user) } diff --git a/internal/app/users/user_manager.go b/internal/app/users/user_manager.go index e15f8b4..65dab2f 100644 --- a/internal/app/users/user_manager.go +++ b/internal/app/users/user_manager.go @@ -352,8 +352,9 @@ func (m Manager) DeactivateApi(ctx context.Context, id domain.UserIdentifier) (* func (m Manager) validateModifications(ctx context.Context, old, new *domain.User) error { currentUser := domain.GetUserInfo(ctx) - if currentUser.Id != new.Identifier && !currentUser.IsAdmin { - return fmt.Errorf("insufficient permissions") + adminErrors := m.validateAdminModifications(ctx, old, new) + if adminErrors != nil { + return adminErrors } if err := old.EditAllowed(new); err != nil && currentUser.Id != domain.SystemAdminContextUserInfo().Id { @@ -387,6 +388,42 @@ func (m Manager) validateModifications(ctx context.Context, old, new *domain.Use return nil } +func (m Manager) validateAdminModifications(ctx context.Context, old, new *domain.User) error { + currentUser := domain.GetUserInfo(ctx) + + if currentUser.IsAdmin { + if currentUser.Id == old.Identifier && !new.IsAdmin { + return fmt.Errorf("cannot remove own admin rights: %w", domain.ErrInvalidData) + } + + return nil // admins can do (almost) everything + } + + // non-admins can only modify very their own profile data + + if currentUser.Id != new.Identifier { + return fmt.Errorf("insufficient permissions: %w", domain.ErrInvalidData) + } + + if new.IsAdmin { + return fmt.Errorf("cannot grant admin rights: %w", domain.ErrInvalidData) + } + + if new.Notes != old.Notes { + return fmt.Errorf("cannot update notes: %w", domain.ErrInvalidData) + } + + if old.Locked != new.Locked || old.LockedReason != new.LockedReason { + return fmt.Errorf("cannot change lock state: %w", domain.ErrInvalidData) + } + + if old.Disabled != new.Disabled || old.DisabledReason != new.DisabledReason { + return fmt.Errorf("cannot change disabled state: %w", domain.ErrInvalidData) + } + + return nil +} + func (m Manager) validateCreation(ctx context.Context, new *domain.User) error { currentUser := domain.GetUserInfo(ctx) @@ -453,6 +490,10 @@ func (m Manager) validateDeletion(ctx context.Context, del *domain.User) error { func (m Manager) validateApiChange(ctx context.Context, user *domain.User) error { currentUser := domain.GetUserInfo(ctx) + if !currentUser.IsAdmin && m.cfg.Advanced.ApiAdminOnly { + return fmt.Errorf("insufficient permissions to change API access: %w", domain.ErrNoPermission) + } + if currentUser.Id != user.Identifier { return fmt.Errorf("cannot change API access of user: %w", domain.ErrNoPermission) } diff --git a/internal/domain/user.go b/internal/domain/user.go index ee5f371..3bcec71 100644 --- a/internal/domain/user.go +++ b/internal/domain/user.go @@ -185,6 +185,31 @@ func (u *User) CopyCalculatedAttributes(src *User) { u.LinkedPeerCount = src.LinkedPeerCount } +// CopyAdminAttributes copies all attributes from the given user except password, passkey and +// api-token if apiAdminOnly is false. +func (u *User) CopyAdminAttributes(src *User, apiAdminOnly bool) { + u.BaseModel = src.BaseModel + u.Identifier = src.Identifier + u.Email = src.Email + u.Source = src.Source + u.ProviderName = src.ProviderName + u.IsAdmin = src.IsAdmin + u.Firstname = src.Firstname + u.Lastname = src.Lastname + u.Phone = src.Phone + u.Department = src.Department + u.Notes = src.Notes + u.Disabled = src.Disabled + u.DisabledReason = src.DisabledReason + u.Locked = src.Locked + u.LockedReason = src.LockedReason + u.LinkedPeerCount = src.LinkedPeerCount + if apiAdminOnly { + u.ApiToken = src.ApiToken + u.ApiTokenCreated = src.ApiTokenCreated + } +} + // DisplayName returns the display name of the user. // The display name is the first and last name, or the email address of the user. // If none of these fields are set, the user identifier is returned.