feat: /v1/role list, get, create, update and delete. Reassign members on delete. Write-time validation (ValidateGrants, duplicates, typed keys against real content types). Name uniqueness (role_name_taken), role_locked, role_has_members, role_conflict on version mismatch. No-escalation: nobody grants a permission they don't hold, and roles.edit_owner_only guards the Owner role. Activity events for role changes. Typed-key exposure in /auth/me. Cleanup of typed rows when a content type is deleted. | Backend only. Will carry the deferred ListGrants decision.
This commit is contained in:
1 parent
a043f7f41a
commit
094df63a7f
51 files changed
+4822
-115
No files matched your search
@@ -0,0 +1,117 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/audit"
|
||||
"RenewCMS/internal/domain/builtintype"
|
||||
"RenewCMS/internal/domain/contenttype"
|
||||
"RenewCMS/internal/domain/role"
|
||||
"RenewCMS/internal/domain/user"
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
)
|
||||
|
||||
func roleTarget(found role.Role) audit.Target {
|
||||
return audit.NewTarget(audit.TargetRole, found.ID, found.Name)
|
||||
}
|
||||
|
||||
func allowedTypeKeys(contentTypes contenttype.Repository) ([]string, error) {
|
||||
types, err := contentTypes.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys := []string{builtintype.KeyArticle}
|
||||
for _, contentType := range types {
|
||||
keys = append(keys, contentType.Key)
|
||||
}
|
||||
slices.Sort(keys)
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func ensureCovered(caller ContentCaller, contentTypes contenttype.Repository, candidate role.Role) error {
|
||||
typeKeys, err := allowedTypeKeys(contentTypes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return caller.Grants.Covers(candidate.GrantSet(), typeKeys)
|
||||
}
|
||||
|
||||
func ensureNotOwnRole(roles role.Repository, users user.Repository, caller ContentCaller, target role.Role) error {
|
||||
account, err := users.Get(caller.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
own, err := resolveRole(roles, account)
|
||||
if errors.Is(err, role.ErrRoleNotFound) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if own.ID == target.ID {
|
||||
return role.ErrOwnRole
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureEditable(target role.Role) error {
|
||||
if target.Locked {
|
||||
return role.ErrRoleLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureNotBuiltIn(target role.Role) error {
|
||||
if target.BuiltIn {
|
||||
return role.ErrBuiltInRole
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureNameFree(roles role.Repository, name string, excludedID uint32) error {
|
||||
existing, err := roles.List()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, other := range existing {
|
||||
if other.ID != excludedID && role.SameName(other.Name, name) {
|
||||
return role.ErrRoleNameTaken
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRoleGrants(contentTypes contenttype.Repository, grants []role.Grant) error {
|
||||
if err := role.ValidateGrants(false, grants); err != nil {
|
||||
return err
|
||||
}
|
||||
typeKeys, err := allowedTypeKeys(contentTypes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return role.ValidateGrantTypes(grants, typeKeys)
|
||||
}
|
||||
|
||||
func normalizedGrants(grants []role.Grant) []role.Grant {
|
||||
return role.Role{Grants: grants}.EffectiveGrants()
|
||||
}
|
||||
|
||||
var writeRefusals = []error{role.ErrRoleConflict, role.ErrRoleHasMembers, role.ErrRoleNotFound, role.ErrRoleNameTaken, role.ErrInvalidReassign}
|
||||
|
||||
func recordRoleWriteFailure(ctx context.Context, recorder audit.Recorder, event audit.Event, err error) {
|
||||
for _, refusal := range writeRefusals {
|
||||
if errors.Is(err, refusal) {
|
||||
return
|
||||
}
|
||||
}
|
||||
recordFailure(ctx, recorder, event, err)
|
||||
}
|
||||
|
||||
func grantsParams(diff role.GrantDiff, members int) map[string]any {
|
||||
return map[string]any{
|
||||
audit.ParamAdded: diff.Added,
|
||||
audit.ParamRemoved: diff.Removed,
|
||||
audit.ParamChanged: diff.Changed,
|
||||
audit.ParamMembers: members,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user