feat(auth): make upload review a separate grant instead of an admin privilege
Reviewing the upload queue is now a per-user flag (reviews_uploads), not part of the admin role: several people can hold it, a moderator can have it, and an admin may not. Only admins grant or revoke it; existing admins keep it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
066ba24834
commit
74da457362
27 files changed
+503
-319
No files matched your search
@@ -75,9 +75,10 @@ func (s *Service) ListUsers(ctx context.Context, p Principal, query string, role
|
||||
|
||||
// CreateUserInput — форма создания пользователя.
|
||||
type CreateUserInput struct {
|
||||
Username string
|
||||
DisplayName string
|
||||
Role Role
|
||||
Username string
|
||||
DisplayName string
|
||||
Role Role
|
||||
ReviewsUploads bool
|
||||
}
|
||||
|
||||
// CreateUser создаёт пользователя с временным паролем, который возвращается один раз.
|
||||
@@ -88,6 +89,9 @@ func (s *Service) CreateUser(ctx context.Context, p Principal, in CreateUserInpu
|
||||
if !CanAssignRole(p.Role, in.Role) || !CanManageUser(actorOf(p), Actor{Role: in.Role}, UserCreate) {
|
||||
return User{}, "", forbidden()
|
||||
}
|
||||
if in.ReviewsUploads && p.Role != RoleAdmin {
|
||||
return User{}, "", forbidden()
|
||||
}
|
||||
username := NormalizeUsername(in.Username)
|
||||
if err := ValidateUsername("username", username); err != nil {
|
||||
return User{}, "", err
|
||||
@@ -109,7 +113,8 @@ func (s *Service) CreateUser(ctx context.Context, p Principal, in CreateUserInpu
|
||||
locale = s.cfg.SupportedLocales[0]
|
||||
}
|
||||
u := User{
|
||||
ID: s.ids.New(), Username: username, DisplayName: name, PasswordHash: hash, Role: in.Role, Locale: locale,
|
||||
ID: s.ids.New(), Username: username, DisplayName: name, PasswordHash: hash, Role: in.Role,
|
||||
ReviewsUploads: in.ReviewsUploads, Locale: locale,
|
||||
MustChangePassword: true, CreatedAt: s.clock.Now(),
|
||||
}
|
||||
err = s.store.WithTx(ctx, func(tx Store) error {
|
||||
@@ -118,7 +123,7 @@ func (s *Service) CreateUser(ctx context.Context, p Principal, in CreateUserInpu
|
||||
}
|
||||
return tx.InsertAudit(ctx, s.ids.New(), AuditEntry{
|
||||
ActorID: &p.UserID, Action: "user.create", TargetType: "user", TargetID: &u.ID,
|
||||
Payload: map[string]any{"username": username, "role": string(in.Role)},
|
||||
Payload: map[string]any{"username": username, "role": string(in.Role), "reviews_uploads": in.ReviewsUploads},
|
||||
})
|
||||
})
|
||||
if errors.Is(err, ErrUsernameTaken) {
|
||||
@@ -145,9 +150,10 @@ func displayName(s string) (*string, error) {
|
||||
|
||||
// UserPatch — изменения пользователя из админки. nil — не менять.
|
||||
type UserPatch struct {
|
||||
DisplayName *string
|
||||
Role *Role
|
||||
IsBlocked *bool
|
||||
DisplayName *string
|
||||
Role *Role
|
||||
IsBlocked *bool
|
||||
ReviewsUploads *bool
|
||||
}
|
||||
|
||||
// UpdateUser меняет имя, роль и блокировку с проверкой прав по каждому изменению.
|
||||
@@ -191,6 +197,13 @@ func (s *Service) UpdateUser(ctx context.Context, p Principal, id uuid.UUID, pat
|
||||
upd.Role, upd.RevokeTokens = patch.Role, true
|
||||
audit("user.role_change", map[string]any{"from": string(target.Role), "to": string(*patch.Role)})
|
||||
}
|
||||
if patch.ReviewsUploads != nil && *patch.ReviewsUploads != target.ReviewsUploads {
|
||||
if p.Role != RoleAdmin {
|
||||
return forbidden()
|
||||
}
|
||||
upd.ReviewsUploads = patch.ReviewsUploads
|
||||
audit("user.reviews_uploads", map[string]any{"reviews_uploads": *patch.ReviewsUploads})
|
||||
}
|
||||
blockChange := patch.IsBlocked != nil && *patch.IsBlocked != target.IsBlocked
|
||||
if blockChange {
|
||||
if !CanManageUser(actor, tgt, UserBlock) {
|
||||
|
||||
Reference in new issue
Block a user