feat: more role backend and admin/writer roles additions and adjustments

This commit is contained in:
Florian Sylvain committed 2026-10-26 18:15:26 +01:00
1 parent 8c6c6456da
commit de142f6c5f
131 files changed
+2663 -435

No files matched your search

@@ -0,0 +1,55 @@
package useCases
import (
"RenewCMS/internal/domain/role"
"RenewCMS/internal/domain/user"
"errors"
)
type CheckPermissionUseCase struct {
roles role.Repository
}
func NewCheckPermissionUseCase(roles role.Repository) *CheckPermissionUseCase {
return &CheckPermissionUseCase{roles: roles}
}
func (c *CheckPermissionUseCase) Require(account user.User, key string) (role.GrantSet, error) {
grants, err := c.grantsOf(account, key)
if err != nil {
return nil, err
}
if !grants.Holds(key) {
return nil, role.MissingPermissionError{Key: key}
}
return grants, nil
}
func (c *CheckPermissionUseCase) RequireAll(account user.User, key string) (role.GrantSet, error) {
grants, err := c.grantsOf(account, key)
if err != nil {
return nil, err
}
if !grants.HoldsAll(key) {
return nil, role.MissingPermissionError{Key: key}
}
return grants, nil
}
func (c *CheckPermissionUseCase) grantsOf(account user.User, key string) (role.GrantSet, error) {
resolved, err := resolveRole(c.roles, account)
if errors.Is(err, role.ErrRoleNotFound) {
return nil, role.MissingPermissionError{Key: key}
}
if err != nil {
return nil, err
}
granted, err := c.roles.ListGrants(resolved.ID)
if errors.Is(err, role.ErrRoleNotFound) {
return nil, role.MissingPermissionError{Key: key}
}
if err != nil {
return nil, err
}
return role.NewGrantSet(granted), nil
}
@@ -2,6 +2,7 @@ package useCases
import (
"RenewCMS/internal/domain/audit"
"RenewCMS/internal/domain/role"
"encoding/csv"
"io"
"strings"
@@ -29,11 +30,20 @@ func NewExportActivityUseCase(events audit.Repository, now func() time.Time) *Ex
return &ExportActivityUseCase{events, now}
}
func (e *ExportActivityUseCase) Prepare(query ActivityQuery) (*ActivityExport, error) {
filter, err := buildActivityFilter(query, e.now())
type ExportActivityCommand struct {
ViewerID uint32
Grants role.GrantSet
Query ActivityQuery
}
func (e *ExportActivityUseCase) Prepare(command ExportActivityCommand) (*ActivityExport, error) {
filter, err := buildActivityFilter(command.Query, e.now())
if err != nil {
return nil, err
}
if err := scopeFilterToViewer(&filter, command.ViewerID, command.Grants); err != nil {
return nil, err
}
return &ActivityExport{events: e.events, filter: filter}, nil
}
@@ -23,15 +23,15 @@ var legacyBuiltInNames = map[user.Role]string{
user.RoleAdmin: role.AdminName,
}
func (u *GetMyAccessUseCase) resolveRole(account user.User) (role.Role, error) {
func resolveRole(roles role.Repository, account user.User) (role.Role, error) {
if account.RoleID != 0 {
return u.roles.Get(account.RoleID)
return roles.Get(account.RoleID)
}
name, known := legacyBuiltInNames[account.Role]
if !known {
return role.Role{}, role.ErrRoleNotFound
}
found, err := u.roles.GetByName(name)
found, err := roles.GetByName(name)
if err != nil {
return role.Role{}, err
}
@@ -42,7 +42,7 @@ func (u *GetMyAccessUseCase) resolveRole(account user.User) (role.Role, error) {
}
func (u *GetMyAccessUseCase) Get(account user.User) (Access, error) {
resolved, err := u.resolveRole(account)
resolved, err := resolveRole(u.roles, account)
if err != nil {
return Access{}, err
}
@@ -19,8 +19,8 @@ func NewGetUpdateStatusUseCase(store update.StatusStore, jobs update.JobStore, h
}
}
func (g *GetUpdateStatusUseCase) GetStatus(isOwner bool) update.UpdateStatus {
status := g.status.build(isOwner && g.helper.Installed())
func (g *GetUpdateStatusUseCase) GetStatus(canApply bool) update.UpdateStatus {
status := g.status.build(canApply && g.helper.Installed())
status.Container = g.container
return status
}
@@ -2,6 +2,7 @@ package useCases
import (
"RenewCMS/internal/domain/apikey"
"RenewCMS/internal/domain/role"
"RenewCMS/internal/domain/user"
"errors"
"sort"
@@ -25,13 +26,17 @@ func NewListAPIKeysUseCase(apiKeyRepository apikey.Repository, userRepository us
return &ListAPIKeysUseCase{apiKeyRepository, userRepository, now}
}
func (l *ListAPIKeysUseCase) ListAPIKeys(requesterId uint32) ([]APIKeyEntry, error) {
func (l *ListAPIKeysUseCase) ListAPIKeys(requesterId uint32, grants role.GrantSet) ([]APIKeyEntry, error) {
scope := grants.Scope(role.KeyAPIKeysView)
if scope == role.ScopeNone {
return nil, role.MissingPermissionError{Key: role.KeyAPIKeysView}
}
requester, err := l.userRepository.Get(requesterId)
if err != nil {
return nil, err
}
keys, err := l.keysVisibleTo(requester)
keys, err := l.keysVisibleTo(requester, scope)
if err != nil {
return nil, err
}
@@ -43,7 +48,7 @@ func (l *ListAPIKeysUseCase) ListAPIKeys(requesterId uint32) ([]APIKeyEntry, err
return keys[i].CreatedAt.After(keys[j].CreatedAt)
})
creatorNames, err := l.creatorNamesFor(requester, keys)
creatorNames, err := l.creatorNamesFor(requester, scope, keys)
if err != nil {
return nil, err
}
@@ -51,14 +56,14 @@ func (l *ListAPIKeysUseCase) ListAPIKeys(requesterId uint32) ([]APIKeyEntry, err
now := l.now()
entries := make([]APIKeyEntry, 0, len(keys))
for _, key := range keys {
entries = append(entries, APIKeyEntry{Key: key, Status: key.StatusAt(now), CreatorName: creatorNames[key.UserID], ListsCreator: requester.Role == user.RoleOwner})
entries = append(entries, APIKeyEntry{Key: key, Status: key.StatusAt(now), CreatorName: creatorNames[key.UserID], ListsCreator: scope == role.ScopeAll})
}
return entries, nil
}
func (l *ListAPIKeysUseCase) creatorNamesFor(requester user.User, keys []apikey.APIKey) (map[uint32]string, error) {
func (l *ListAPIKeysUseCase) creatorNamesFor(requester user.User, scope role.Scope, keys []apikey.APIKey) (map[uint32]string, error) {
names := map[uint32]string{}
if requester.Role != user.RoleOwner {
if scope != role.ScopeAll {
return names, nil
}
names[requester.ID] = requester.NameOrUsername()
@@ -79,8 +84,8 @@ func (l *ListAPIKeysUseCase) creatorNamesFor(requester user.User, keys []apikey.
return names, nil
}
func (l *ListAPIKeysUseCase) keysVisibleTo(requester user.User) ([]apikey.APIKey, error) {
if requester.Role == user.RoleOwner {
func (l *ListAPIKeysUseCase) keysVisibleTo(requester user.User, scope role.Scope) ([]apikey.APIKey, error) {
if scope == role.ScopeAll {
return l.apiKeyRepository.ListAll()
}
return l.apiKeyRepository.ListByUser(requester.ID)
@@ -6,6 +6,7 @@ import (
"RenewCMS/internal/domain/audit"
"RenewCMS/internal/domain/author"
"RenewCMS/internal/domain/image"
"RenewCMS/internal/domain/role"
"RenewCMS/internal/domain/site"
"RenewCMS/internal/domain/user"
"errors"
@@ -26,6 +27,7 @@ type ListActivityUseCase struct {
type ListActivityCommand struct {
ViewerID uint32
Grants role.GrantSet
Query ActivityQuery
}
@@ -53,7 +55,7 @@ func (l *ListActivityUseCase) ListActivity(command ListActivityCommand) (Activit
if filter.Page, filter.Size, err = resolveActivityPaging(command.Query); err != nil {
return ActivityPage{}, err
}
if err := l.scopeToViewer(&filter, command.ViewerID); err != nil {
if err := scopeFilterToViewer(&filter, command.ViewerID, command.Grants); err != nil {
return ActivityPage{}, err
}
@@ -68,22 +70,6 @@ func (l *ListActivityUseCase) ListActivity(command ListActivityCommand) (Activit
return ActivityPage{Items: entries, Total: found.Total, Page: filter.Page, Size: filter.Size}, nil
}
func (l *ListActivityUseCase) scopeToViewer(filter *audit.Filter, viewerID uint32) error {
viewer, err := l.users.Get(viewerID)
if err != nil {
return err
}
if viewer.Role == user.RoleOwner || viewer.Role == user.RoleAdmin {
return nil
}
own := audit.ActorRef{Type: audit.ActorUser, ID: &viewer.ID}
if filter.Actor != nil && !sameActor(*filter.Actor, own) {
return audit.ErrActorNotAllowed
}
filter.Actor = &own
return nil
}
func sameActor(a audit.ActorRef, b audit.ActorRef) bool {
return a.Type == b.Type && a.ID != nil && b.ID != nil && *a.ID == *b.ID
}
@@ -2,6 +2,7 @@ package useCases
import (
"RenewCMS/internal/domain/author"
"RenewCMS/internal/domain/role"
"RenewCMS/internal/domain/user"
)
@@ -11,10 +12,10 @@ type ListAuthorsUseCase struct {
}
type ListAuthorsCommand struct {
Q string
Page int
Size int
CallerRole user.Role
Q string
Page int
Size int
Grants role.GrantSet
}
type AuthorPage struct {
@@ -35,7 +36,7 @@ func (u *ListAuthorsUseCase) List(command ListAuthorsCommand) (AuthorPage, error
}
items := make([]author.Author, 0, len(found.Items))
for _, item := range found.Items {
if isAdminRole(command.CallerRole) {
if command.Grants.Holds(role.KeyUsersView) {
item = withUserEmail(u.users, item)
}
items = append(items, item)
@@ -3,34 +3,33 @@ package useCases
import (
"RenewCMS/internal/domain/apikey"
"RenewCMS/internal/domain/audit"
"RenewCMS/internal/domain/user"
"RenewCMS/internal/domain/role"
"context"
"time"
)
type RevokeAPIKeyUseCase struct {
apiKeyRepository apikey.Repository
userRepository user.Repository
now func() time.Time
recorder audit.Recorder
}
func NewRevokeAPIKeyUseCase(apiKeyRepository apikey.Repository, userRepository user.Repository, now func() time.Time, recorder audit.Recorder) *RevokeAPIKeyUseCase {
return &RevokeAPIKeyUseCase{apiKeyRepository, userRepository, now, recorder}
func NewRevokeAPIKeyUseCase(apiKeyRepository apikey.Repository, now func() time.Time, recorder audit.Recorder) *RevokeAPIKeyUseCase {
return &RevokeAPIKeyUseCase{apiKeyRepository, now, recorder}
}
func (r *RevokeAPIKeyUseCase) RevokeAPIKey(ctx context.Context, requesterId uint32, keyId uint32) error {
func (r *RevokeAPIKeyUseCase) RevokeAPIKey(ctx context.Context, requesterId uint32, keyId uint32, grants role.GrantSet) error {
scope := grants.Scope(role.KeyAPIKeysRevoke)
if scope == role.ScopeNone {
return role.MissingPermissionError{Key: role.KeyAPIKeysRevoke}
}
key, err := r.apiKeyRepository.Get(keyId)
if err != nil {
return err
}
requester, err := r.userRepository.Get(requesterId)
if err != nil {
return err
}
if requester.Role != user.RoleOwner && key.UserID != requester.ID {
if scope != role.ScopeAll && key.UserID != requesterId {
return apikey.ErrAPIKeyNotFound
}
@@ -4,6 +4,7 @@ import (
"RenewCMS/internal/domain/audit"
"RenewCMS/internal/domain/author"
"RenewCMS/internal/domain/image"
"RenewCMS/internal/domain/role"
"RenewCMS/internal/domain/user"
"context"
"strings"
@@ -21,6 +22,9 @@ func NewUpdateAuthorUseCase(authors author.Repository, users user.Repository, im
}
func (u *UpdateAuthorUseCase) Update(ctx context.Context, id uint32, caller AuthorCaller, patch author.Patch) (author.Author, error) {
if !caller.Grants.Holds(role.KeyAuthorsEdit) {
return author.Author{}, role.MissingPermissionError{Key: role.KeyAuthorsEdit}
}
before, err := u.authors.Get(id)
if err != nil {
return author.Author{}, err
@@ -50,7 +54,7 @@ func (u *UpdateAuthorUseCase) Update(ctx context.Context, id uint32, caller Auth
}
func (u *UpdateAuthorUseCase) present(a author.Author, caller AuthorCaller) author.Author {
if caller.isAdmin() {
if caller.seesUserEmail() {
return withUserEmail(u.users, a)
}
a.UserEmail = nil
@@ -89,7 +93,7 @@ func (u *UpdateAuthorUseCase) prepare(before author.Author, caller AuthorCaller,
}
func authorizeEdit(before author.Author, caller AuthorCaller, patch author.Patch) error {
if caller.isAdmin() {
if caller.Grants.HoldsAll(role.KeyAuthorsEdit) {
return nil
}
if before.UserID == nil || *before.UserID != caller.UserID || patch.UserID.Set {
@@ -2,6 +2,7 @@ package useCases
import (
"RenewCMS/internal/domain/audit"
"RenewCMS/internal/domain/role"
"time"
)
@@ -32,6 +33,22 @@ type ActivityQuery struct {
Size int
}
func scopeFilterToViewer(filter *audit.Filter, viewerID uint32, grants role.GrantSet) error {
switch grants.Scope(role.KeyActivityView) {
case role.ScopeAll:
return nil
case role.ScopeOwn:
own := audit.ActorRef{Type: audit.ActorUser, ID: &viewerID}
if filter.Actor != nil && !sameActor(*filter.Actor, own) {
return audit.ErrActorNotAllowed
}
filter.Actor = &own
return nil
default:
return role.MissingPermissionError{Key: role.KeyActivityView}
}
}
type activityWindow struct {
from time.Time
to time.Time
@@ -4,6 +4,7 @@ import (
"RenewCMS/internal/domain/audit"
"RenewCMS/internal/domain/author"
"RenewCMS/internal/domain/image"
"RenewCMS/internal/domain/role"
"RenewCMS/internal/domain/user"
"errors"
"strconv"
@@ -14,15 +15,11 @@ const authorChangeSeparator = ", "
type AuthorCaller struct {
UserID uint32
Role user.Role
Grants role.GrantSet
}
func isAdminRole(role user.Role) bool {
return role == user.RoleOwner || role == user.RoleAdmin
}
func (c AuthorCaller) isAdmin() bool {
return isAdminRole(c.Role)
func (c AuthorCaller) seesUserEmail() bool {
return c.Grants.Holds(role.KeyUsersView)
}
func authorTarget(a author.Author) audit.Target {