refac: DELETE /v1/user/{id}?expected_role_id=N, user.remove audit event, 409 target_role_not_found, GetAll returns its error, Own-scope visibility filter moved into repos, JSON tags removed from use-case types
This commit is contained in:
1 parent
5d6f378fce
commit
b39a41be88
124 files changed
+8816
-523
No files matched your search
@@ -105,6 +105,7 @@ const (
|
||||
ActionUserUpdateProfile = "user.update_profile"
|
||||
ActionUserChangeRole = "user.change_role"
|
||||
ActionUserTransferOwnership = "user.transfer_ownership"
|
||||
ActionUserRemove = "user.remove"
|
||||
ActionInviteCreate = "invite.create"
|
||||
ActionInviteCancel = "invite.cancel"
|
||||
ActionInviteRenew = "invite.renew"
|
||||
@@ -135,6 +136,7 @@ const (
|
||||
ParamRemoved = "removed"
|
||||
ParamFrom = "from"
|
||||
ParamTo = "to"
|
||||
ParamToName = "to_name"
|
||||
ParamMaxGBFrom = "max_gb_from"
|
||||
ParamMaxGBTo = "max_gb_to"
|
||||
ParamMerged = "merged"
|
||||
|
||||
@@ -16,18 +16,19 @@ var (
|
||||
|
||||
ErrMissingPermission = errors.New("missing permission")
|
||||
|
||||
ErrRoleHasMembers = errors.New("role still has members")
|
||||
ErrRoleNameTaken = errors.New("role name already used")
|
||||
ErrRoleLocked = errors.New("role is locked")
|
||||
ErrBuiltInRole = errors.New("role is built in")
|
||||
ErrBuiltInName = errors.New("built-in role cannot be renamed")
|
||||
ErrOwnRole = errors.New("cannot change the role you hold")
|
||||
ErrInvalidRoleName = errors.New("invalid role name")
|
||||
ErrUnknownContentType = errors.New("unknown content type")
|
||||
ErrNotBuiltIn = errors.New("role is not built in")
|
||||
ErrInvalidReassign = errors.New("invalid reassign target")
|
||||
ErrEscalation = errors.New("grants more than the caller holds")
|
||||
ErrUnknownRole = errors.New("unknown role")
|
||||
ErrRoleHasMembers = errors.New("role still has members")
|
||||
ErrRoleNameTaken = errors.New("role name already used")
|
||||
ErrRoleLocked = errors.New("role is locked")
|
||||
ErrBuiltInRole = errors.New("role is built in")
|
||||
ErrBuiltInName = errors.New("built-in role cannot be renamed")
|
||||
ErrOwnRole = errors.New("cannot change the role you hold")
|
||||
ErrInvalidRoleName = errors.New("invalid role name")
|
||||
ErrUnknownContentType = errors.New("unknown content type")
|
||||
ErrNotBuiltIn = errors.New("role is not built in")
|
||||
ErrInvalidReassign = errors.New("invalid reassign target")
|
||||
ErrReassignTargetNotFound = errors.New("reassign target not found")
|
||||
ErrEscalation = errors.New("grants more than the caller holds")
|
||||
ErrUnknownRole = errors.New("unknown role")
|
||||
)
|
||||
|
||||
type Scope string
|
||||
|
||||
@@ -10,6 +10,7 @@ type Repository interface {
|
||||
FindByIDs(ids []uint32) ([]User, error)
|
||||
Create(user User) (User, error)
|
||||
Delete(id uint32) error
|
||||
DeleteIfRole(id uint32, roleID uint32) error
|
||||
UpdateVerificationStatus(userId uint32, isVerified bool) (User, error)
|
||||
UpdatePassword(userId uint32, password string) (User, error)
|
||||
UpdatePasswordResetCode(userId uint32, code string, expiration time.Time) (User, error)
|
||||
|
||||
@@ -52,6 +52,26 @@ func (u *UserRepository) Delete(id uint32) error {
|
||||
return deleteOrNotFound(result, domainUser.ErrUserNotFound)
|
||||
}
|
||||
|
||||
func (u *UserRepository) DeleteIfRole(id uint32, roleID uint32) error {
|
||||
return u.db.Transaction(func(tx *gorm.DB) error {
|
||||
result := tx.Where("id = ? AND role_id = ?", id, roleID).Delete(&entity.User{})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected > 0 {
|
||||
return nil
|
||||
}
|
||||
var users int64
|
||||
if err := tx.Model(&entity.User{}).Where("id = ?", id).Count(&users).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if users == 0 {
|
||||
return domainUser.ErrUserNotFound
|
||||
}
|
||||
return domainUser.ErrRoleChanged
|
||||
})
|
||||
}
|
||||
|
||||
func (u *UserRepository) GetAll() ([]domainUser.User, error) {
|
||||
var users []entity.User
|
||||
if err := u.db.Model(&entity.User{}).Find(&users).Error; err != nil {
|
||||
|
||||
@@ -34,7 +34,8 @@ func (u *ReassignRoleMembersUseCase) Reassign(ctx context.Context, command Reass
|
||||
if err := u.checkSource(command.Caller, source); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := u.checkTarget(command, source); err != nil {
|
||||
target, err := u.checkTarget(command, source)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@@ -42,6 +43,7 @@ func (u *ReassignRoleMembersUseCase) Reassign(ctx context.Context, command Reass
|
||||
event.Target = roleTarget(source)
|
||||
event.Params[audit.ParamFrom] = source.ID
|
||||
event.Params[audit.ParamTo] = command.TargetID
|
||||
event.Params[audit.ParamToName] = target.Name
|
||||
event.Params[audit.ParamMoved] = 0
|
||||
moved, err := u.roles.ReassignMembers(source.ID, command.TargetID)
|
||||
if err != nil {
|
||||
@@ -68,19 +70,22 @@ func (u *ReassignRoleMembersUseCase) checkSource(caller ContentCaller, source ro
|
||||
return ensureCovered(caller, u.contentTypes, source)
|
||||
}
|
||||
|
||||
func (u *ReassignRoleMembersUseCase) checkTarget(command ReassignRoleMembersCommand, source role.Role) error {
|
||||
func (u *ReassignRoleMembersUseCase) checkTarget(command ReassignRoleMembersCommand, source role.Role) (role.Role, error) {
|
||||
if command.TargetID == 0 || command.TargetID == source.ID {
|
||||
return role.ErrInvalidReassign
|
||||
return role.Role{}, role.ErrInvalidReassign
|
||||
}
|
||||
target, err := u.roles.Get(command.TargetID)
|
||||
if errors.Is(err, role.ErrRoleNotFound) {
|
||||
return role.ErrInvalidReassign
|
||||
return role.Role{}, role.ErrReassignTargetNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
return role.Role{}, err
|
||||
}
|
||||
if target.Locked {
|
||||
return role.ErrInvalidReassign
|
||||
return role.Role{}, role.ErrInvalidReassign
|
||||
}
|
||||
return ensureCovered(command.Caller, u.contentTypes, target)
|
||||
if err := ensureCovered(command.Caller, u.contentTypes, target); err != nil {
|
||||
return role.Role{}, err
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/audit"
|
||||
"RenewCMS/internal/domain/contenttype"
|
||||
"RenewCMS/internal/domain/role"
|
||||
"RenewCMS/internal/domain/user"
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type RemoveUserUseCase struct {
|
||||
users user.Repository
|
||||
roles role.Repository
|
||||
contentTypes contenttype.Repository
|
||||
recorder audit.Recorder
|
||||
}
|
||||
|
||||
type RemoveUserCommand struct {
|
||||
Caller ContentCaller
|
||||
TargetID uint32
|
||||
ExpectedRoleID uint32
|
||||
}
|
||||
|
||||
func NewRemoveUserUseCase(users user.Repository, roles role.Repository, contentTypes contenttype.Repository, recorder audit.Recorder) *RemoveUserUseCase {
|
||||
return &RemoveUserUseCase{users: users, roles: roles, contentTypes: contentTypes, recorder: recorder}
|
||||
}
|
||||
|
||||
func (u *RemoveUserUseCase) Remove(ctx context.Context, command RemoveUserCommand) error {
|
||||
if command.ExpectedRoleID == 0 {
|
||||
return role.ErrUnknownRole
|
||||
}
|
||||
target, err := u.users.Get(command.TargetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := u.check(command, target); err != nil {
|
||||
return err
|
||||
}
|
||||
event := audit.NewEvent(audit.ActionUserRemove, audit.CategoryUsers)
|
||||
event.Target = audit.NewTarget(audit.TargetUser, target.ID, target.Username)
|
||||
if err := u.users.DeleteIfRole(target.ID, command.ExpectedRoleID); err != nil {
|
||||
u.recordWriteFailure(ctx, event, err)
|
||||
return err
|
||||
}
|
||||
u.recorder.Record(ctx, event)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *RemoveUserUseCase) check(command RemoveUserCommand, target user.User) error {
|
||||
if target.ID == command.Caller.UserID {
|
||||
return role.ErrOwnRole
|
||||
}
|
||||
if target.RoleID != command.ExpectedRoleID {
|
||||
return user.ErrRoleChanged
|
||||
}
|
||||
held, err := resolveRole(u.roles, target)
|
||||
if errors.Is(err, role.ErrRoleNotFound) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if held.Locked {
|
||||
return role.ErrRoleLocked
|
||||
}
|
||||
typeKeys, err := allowedTypeKeys(u.contentTypes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ensureCoveredBy(command.Caller, typeKeys, held)
|
||||
}
|
||||
|
||||
func (u *RemoveUserUseCase) recordWriteFailure(ctx context.Context, event audit.Event, err error) {
|
||||
if errors.Is(err, user.ErrRoleChanged) || errors.Is(err, user.ErrUserNotFound) {
|
||||
return
|
||||
}
|
||||
recordFailure(ctx, u.recorder, event, err)
|
||||
}
|
||||
Reference in new issue
Block a user