feat: more role backend and admin/writer roles additions and adjustments
This commit is contained in:
1 parent
8c6c6456da
commit
de142f6c5f
131 files changed
+2663
-435
No files matched your search
@@ -11,6 +11,8 @@ var (
|
||||
ErrRoleConflict = errors.New("role was modified concurrently")
|
||||
ErrInvalidScope = errors.New("invalid scope for permission")
|
||||
ErrOwnerOnlyGrant = errors.New("owner-only permission granted to a non-owner role")
|
||||
|
||||
ErrMissingPermission = errors.New("missing permission")
|
||||
)
|
||||
|
||||
type Scope string
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package role
|
||||
|
||||
type MissingPermissionError struct {
|
||||
Key string
|
||||
}
|
||||
|
||||
func (e MissingPermissionError) Error() string {
|
||||
return ErrMissingPermission.Error() + ": " + e.Key
|
||||
}
|
||||
|
||||
func (e MissingPermissionError) Is(target error) bool {
|
||||
return target == ErrMissingPermission
|
||||
}
|
||||
|
||||
type GrantSet map[string]Scope
|
||||
|
||||
func NewGrantSet(grants []Grant) GrantSet {
|
||||
set := make(GrantSet, len(grants))
|
||||
for _, grant := range grants {
|
||||
set[grant.Key] = grant.Scope
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
func (g GrantSet) Scope(key string) Scope {
|
||||
scope, present := g[key]
|
||||
if !present {
|
||||
return ScopeNone
|
||||
}
|
||||
return scope
|
||||
}
|
||||
|
||||
func (g GrantSet) Holds(key string) bool {
|
||||
scope := g.Scope(key)
|
||||
return scope == ScopeOwn || scope == ScopeAll
|
||||
}
|
||||
|
||||
func (g GrantSet) HoldsAll(key string) bool {
|
||||
return g.Scope(key) == ScopeAll
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package role
|
||||
|
||||
const (
|
||||
KeyActivityView = "activity.view"
|
||||
KeyActivityExport = "activity.export"
|
||||
KeyAPIKeysView = "apikeys.view"
|
||||
KeyAPIKeysRevoke = "apikeys.revoke"
|
||||
KeyAuthorsEdit = "authors.edit"
|
||||
KeyUsersView = "users.view"
|
||||
KeyApplyUpdates = "system.apply_updates"
|
||||
)
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user