refac: debt on role backend
This commit is contained in:
1 parent
de142f6c5f
commit
a043f7f41a
130 files changed
+3591
-512
No files matched your search
@@ -18,15 +18,15 @@ func allGrants(include func(Permission) bool) []Grant {
|
||||
|
||||
func writerGrants() []Grant {
|
||||
return []Grant{
|
||||
{Key: "content.read", Scope: ScopeAll},
|
||||
{Key: "content.create", Scope: ScopeAll},
|
||||
{Key: "content.update", Scope: ScopeAll},
|
||||
{Key: "schema.view", Scope: ScopeAll},
|
||||
{Key: "media.view", Scope: ScopeAll},
|
||||
{Key: "media.upload", Scope: ScopeAll},
|
||||
{Key: "authors.view", Scope: ScopeAll},
|
||||
{Key: "authors.edit", Scope: ScopeOwn},
|
||||
{Key: "activity.view", Scope: ScopeOwn},
|
||||
{Key: KeyContentRead, Scope: ScopeAll},
|
||||
{Key: KeyContentCreate, Scope: ScopeAll},
|
||||
{Key: KeyContentUpdate, Scope: ScopeAll},
|
||||
{Key: KeySchemaView, Scope: ScopeAll},
|
||||
{Key: KeyMediaView, Scope: ScopeAll},
|
||||
{Key: KeyMediaUpload, Scope: ScopeAll},
|
||||
{Key: KeyAuthorsView, Scope: ScopeAll},
|
||||
{Key: KeyAuthorsEdit, Scope: ScopeOwn},
|
||||
{Key: KeyActivityView, Scope: ScopeOwn},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ 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")
|
||||
ErrDuplicateGrant = errors.New("permission granted more than once")
|
||||
|
||||
ErrMissingPermission = errors.New("missing permission")
|
||||
)
|
||||
@@ -38,55 +39,6 @@ type Role struct {
|
||||
Grants []Grant `json:"grants"`
|
||||
}
|
||||
|
||||
func (r Role) scopeOf(key string) Scope {
|
||||
for _, grant := range r.Grants {
|
||||
if grant.Key == key {
|
||||
return grant.Scope
|
||||
}
|
||||
}
|
||||
return ScopeNone
|
||||
}
|
||||
|
||||
func (r Role) Allows(key string, owned bool) bool {
|
||||
permission, err := Lookup(key)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if permission.OwnerOnly && !r.Locked {
|
||||
return false
|
||||
}
|
||||
return scopeAllows(permission.Kind, r.scopeOf(key), owned)
|
||||
}
|
||||
|
||||
func (r Role) AllowsFor(key string, typeKey string, owned bool) bool {
|
||||
if typeKey == "" {
|
||||
return r.Allows(key, owned)
|
||||
}
|
||||
composite := key + typeSeparator + typeKey
|
||||
base, _, err := ParseTypedKey(composite)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
permission, _ := Lookup(base)
|
||||
for _, grant := range r.Grants {
|
||||
if grant.Key == composite {
|
||||
return scopeAllows(permission.Kind, grant.Scope, owned)
|
||||
}
|
||||
}
|
||||
return r.Allows(key, owned)
|
||||
}
|
||||
|
||||
func scopeAllows(kind Kind, scope Scope, owned bool) bool {
|
||||
switch scope {
|
||||
case ScopeAll:
|
||||
return true
|
||||
case ScopeOwn:
|
||||
return kind == KindScoped && owned
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func resolveGrantKey(key string) (Permission, error) {
|
||||
if strings.Contains(key, typeSeparator) {
|
||||
base, _, err := ParseTypedKey(key)
|
||||
@@ -99,6 +51,7 @@ func resolveGrantKey(key string) (Permission, error) {
|
||||
}
|
||||
|
||||
func ValidateGrants(locked bool, grants []Grant) error {
|
||||
seen := make(map[string]bool, len(grants))
|
||||
for _, grant := range grants {
|
||||
permission, err := resolveGrantKey(grant.Key)
|
||||
if err != nil {
|
||||
@@ -107,14 +60,25 @@ func ValidateGrants(locked bool, grants []Grant) error {
|
||||
if permission.OwnerOnly && !locked {
|
||||
return ErrOwnerOnlyGrant
|
||||
}
|
||||
if !scopeValidFor(permission.Kind, grant.Scope) {
|
||||
if !scopeValidFor(permission.Kind, grant.Scope, isTypedKey(grant.Key)) {
|
||||
return ErrInvalidScope
|
||||
}
|
||||
if seen[grant.Key] {
|
||||
return ErrDuplicateGrant
|
||||
}
|
||||
seen[grant.Key] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func scopeValidFor(kind Kind, scope Scope) bool {
|
||||
func isTypedKey(key string) bool {
|
||||
return strings.Contains(key, typeSeparator)
|
||||
}
|
||||
|
||||
func scopeValidFor(kind Kind, scope Scope, typed bool) bool {
|
||||
if typed && scope == ScopeNone {
|
||||
return true
|
||||
}
|
||||
if kind == KindToggle {
|
||||
return scope == ScopeAll
|
||||
}
|
||||
|
||||
@@ -12,29 +12,103 @@ 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
|
||||
type GrantSet struct {
|
||||
scopes map[string]Scope
|
||||
}
|
||||
|
||||
func (g GrantSet) Scope(key string) Scope {
|
||||
scope, present := g[key]
|
||||
func (r Role) GrantSet() GrantSet {
|
||||
scopes := make(map[string]Scope, len(r.Grants))
|
||||
for _, grant := range r.Grants {
|
||||
permission, err := resolveGrantKey(grant.Key)
|
||||
if err != nil || permission.OwnerOnly && !r.Locked {
|
||||
continue
|
||||
}
|
||||
scope := effectiveScope(permission.Kind, grant.Scope)
|
||||
if held, present := scopes[grant.Key]; present {
|
||||
scope = mostRestrictive(held, scope)
|
||||
}
|
||||
scopes[grant.Key] = scope
|
||||
}
|
||||
return GrantSet{scopes: scopes}
|
||||
}
|
||||
|
||||
func effectiveScope(kind Kind, scope Scope) Scope {
|
||||
switch {
|
||||
case scope == ScopeAll:
|
||||
return ScopeAll
|
||||
case scope == ScopeOwn && kind == KindScoped:
|
||||
return ScopeOwn
|
||||
default:
|
||||
return ScopeNone
|
||||
}
|
||||
}
|
||||
|
||||
func scopeRank(scope Scope) int {
|
||||
switch scope {
|
||||
case ScopeAll:
|
||||
return 2
|
||||
case ScopeOwn:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func mostRestrictive(left Scope, right Scope) Scope {
|
||||
if scopeRank(left) <= scopeRank(right) {
|
||||
return left
|
||||
}
|
||||
return right
|
||||
}
|
||||
|
||||
func (g GrantSet) ScopeFor(key string, typeKey string) Scope {
|
||||
if typeKey == "" {
|
||||
return g.wildcard(key)
|
||||
}
|
||||
composite := key + typeSeparator + typeKey
|
||||
if _, _, err := ParseTypedKey(composite); err != nil {
|
||||
return ScopeNone
|
||||
}
|
||||
if scope, present := g.scopes[composite]; present {
|
||||
return scope
|
||||
}
|
||||
return g.wildcard(key)
|
||||
}
|
||||
|
||||
func (g GrantSet) wildcard(key string) Scope {
|
||||
if isTypedKey(key) {
|
||||
return ScopeNone
|
||||
}
|
||||
scope, present := g.scopes[key]
|
||||
if !present {
|
||||
return ScopeNone
|
||||
}
|
||||
return scope
|
||||
}
|
||||
|
||||
func (g GrantSet) Permits(key string, typeKey string, callerID uint32, createdBy uint32) bool {
|
||||
return ScopePermits(g.ScopeFor(key, typeKey), callerID, createdBy)
|
||||
}
|
||||
|
||||
func ScopePermits(scope Scope, callerID uint32, createdBy uint32) bool {
|
||||
switch scope {
|
||||
case ScopeAll:
|
||||
return true
|
||||
case ScopeOwn:
|
||||
return callerID != 0 && callerID == createdBy
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (g GrantSet) HoldsFor(key string, typeKey string) bool {
|
||||
return g.ScopeFor(key, typeKey) != ScopeNone
|
||||
}
|
||||
|
||||
func (g GrantSet) Holds(key string) bool {
|
||||
scope := g.Scope(key)
|
||||
return scope == ScopeOwn || scope == ScopeAll
|
||||
return g.HoldsFor(key, "")
|
||||
}
|
||||
|
||||
func (g GrantSet) HoldsAll(key string) bool {
|
||||
return g.Scope(key) == ScopeAll
|
||||
return g.ScopeFor(key, "") == ScopeAll
|
||||
}
|
||||
@@ -1,11 +1,38 @@
|
||||
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"
|
||||
KeyContentRead = "content.read"
|
||||
KeyContentCreate = "content.create"
|
||||
KeyContentUpdate = "content.update"
|
||||
KeyContentDelete = "content.delete"
|
||||
KeyContentPublish = "content.publish"
|
||||
KeySchemaView = "schema.view"
|
||||
KeySchemaEdit = "schema.edit"
|
||||
KeySchemaDelete = "schema.delete"
|
||||
KeyMediaView = "media.view"
|
||||
KeyMediaUpload = "media.upload"
|
||||
KeyMediaDelete = "media.delete"
|
||||
KeyAuthorsView = "authors.view"
|
||||
KeyAuthorsCreate = "authors.create"
|
||||
KeyAuthorsEdit = "authors.edit"
|
||||
KeyAuthorsDelete = "authors.delete"
|
||||
KeyActivityView = "activity.view"
|
||||
KeyActivityExport = "activity.export"
|
||||
KeyUsersView = "users.view"
|
||||
KeyUsersInvite = "users.invite"
|
||||
KeyUsersChangeRole = "users.change_role"
|
||||
KeyUsersRemove = "users.remove"
|
||||
KeySettingsView = "settings.view"
|
||||
KeySettingsEditGeneral = "settings.edit_general"
|
||||
KeySettingsManageSites = "settings.manage_sites"
|
||||
KeySettingsDeleteSite = "settings.delete_site"
|
||||
KeyAPIKeysView = "apikeys.view"
|
||||
KeyAPIKeysCreate = "apikeys.create"
|
||||
KeyAPIKeysRevoke = "apikeys.revoke"
|
||||
KeyRolesView = "roles.view"
|
||||
KeyRolesManage = "roles.manage"
|
||||
KeyRolesEditOwnerOnly = "roles.edit_owner_only"
|
||||
KeySystemApplyUpdates = "system.apply_updates"
|
||||
KeySystemEditRetention = "system.edit_retention"
|
||||
KeySystemTransferOwnership = "system.transfer_ownership"
|
||||
)
|
||||
@@ -2,7 +2,9 @@ package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/builtintype"
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/role"
|
||||
"RenewCMS/internal/domain/site"
|
||||
)
|
||||
|
||||
@@ -32,11 +34,17 @@ func NewCheckArticleSEOUseCase(articleRepository article.Repository, sites site.
|
||||
return &CheckArticleSEOUseCase{articleRepository: articleRepository, sites: sites, images: images}
|
||||
}
|
||||
|
||||
func (u *CheckArticleSEOUseCase) Check(id uint32, command CheckArticleSEOCommand) (article.SEOCheck, error) {
|
||||
func (u *CheckArticleSEOUseCase) Check(id uint32, caller ContentCaller, command CheckArticleSEOCommand) (article.SEOCheck, error) {
|
||||
if err := caller.requireHolding(role.KeyContentRead, builtintype.KeyArticle); err != nil {
|
||||
return article.SEOCheck{}, err
|
||||
}
|
||||
saved, err := u.articleRepository.Get(id)
|
||||
if err != nil {
|
||||
return article.SEOCheck{}, err
|
||||
}
|
||||
if !caller.seesArticle(saved) {
|
||||
return article.SEOCheck{}, article.ErrArticleNotFound
|
||||
}
|
||||
if err := u.validate(command); err != nil {
|
||||
return article.SEOCheck{}, err
|
||||
}
|
||||
|
||||
@@ -14,42 +14,39 @@ 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
|
||||
func (c *CheckPermissionUseCase) Grants(account user.User) (role.GrantSet, error) {
|
||||
resolved, err := resolveRole(c.roles, account)
|
||||
if errors.Is(err, role.ErrRoleNotFound) {
|
||||
return role.GrantSet{}, nil
|
||||
}
|
||||
if !grants.Holds(key) {
|
||||
return nil, role.MissingPermissionError{Key: key}
|
||||
if err != nil {
|
||||
return role.GrantSet{}, err
|
||||
}
|
||||
return resolved.GrantSet(), nil
|
||||
}
|
||||
|
||||
func (c *CheckPermissionUseCase) Require(account user.User, key string) (role.GrantSet, error) {
|
||||
return c.RequireFor(account, key, "")
|
||||
}
|
||||
|
||||
func (c *CheckPermissionUseCase) RequireFor(account user.User, key string, typeKey string) (role.GrantSet, error) {
|
||||
grants, err := c.Grants(account)
|
||||
if err != nil {
|
||||
return role.GrantSet{}, err
|
||||
}
|
||||
if !grants.HoldsFor(key, typeKey) {
|
||||
return role.GrantSet{}, 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)
|
||||
grants, err := c.Grants(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return role.GrantSet{}, err
|
||||
}
|
||||
if !grants.HoldsAll(key) {
|
||||
return nil, role.MissingPermissionError{Key: key}
|
||||
return role.GrantSet{}, 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
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/audit"
|
||||
"RenewCMS/internal/domain/author"
|
||||
"RenewCMS/internal/domain/builtintype"
|
||||
"RenewCMS/internal/domain/role"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"context"
|
||||
"errors"
|
||||
@@ -18,10 +20,10 @@ type CreateArticleUseCase struct {
|
||||
}
|
||||
|
||||
type CreateArticleCommand struct {
|
||||
Title string
|
||||
Body string
|
||||
Format article.BodyFormat
|
||||
CreatedBy uint32
|
||||
Title string
|
||||
Body string
|
||||
Format article.BodyFormat
|
||||
Caller ContentCaller
|
||||
}
|
||||
|
||||
func NewCreateArticleUseCase(articleRepository article.Repository, bodyRenderer article.BodyRenderer, recorder audit.Recorder, sites site.Repository, authors author.Repository) *CreateArticleUseCase {
|
||||
@@ -48,6 +50,9 @@ func (g *CreateArticleUseCase) creatorBylines(userID uint32) ([]author.Byline, e
|
||||
}
|
||||
|
||||
func (g *CreateArticleUseCase) CreateArticle(ctx context.Context, createArticle CreateArticleCommand) (article.Article, error) {
|
||||
if err := createArticle.Caller.requireHolding(role.KeyContentCreate, builtintype.KeyArticle); err != nil {
|
||||
return article.Article{}, err
|
||||
}
|
||||
event := audit.NewEvent(audit.ActionArticleCreate, audit.CategoryArticles)
|
||||
if err := article.ValidateTitle(createArticle.Title); err != nil {
|
||||
recordFailure(ctx, g.recorder, event, err)
|
||||
@@ -60,7 +65,7 @@ func (g *CreateArticleUseCase) CreateArticle(ctx context.Context, createArticle
|
||||
return article.Article{}, err
|
||||
}
|
||||
|
||||
creditedAuthors, err := g.creatorBylines(createArticle.CreatedBy)
|
||||
creditedAuthors, err := g.creatorBylines(createArticle.Caller.UserID)
|
||||
if err != nil {
|
||||
recordFailure(ctx, g.recorder, event, err)
|
||||
return article.Article{}, err
|
||||
@@ -71,7 +76,7 @@ func (g *CreateArticleUseCase) CreateArticle(ctx context.Context, createArticle
|
||||
Body: createArticle.Body,
|
||||
IsOnline: false,
|
||||
SiteID: siteID,
|
||||
CreatedBy: createArticle.CreatedBy,
|
||||
CreatedBy: createArticle.Caller.UserID,
|
||||
Authors: creditedAuthors,
|
||||
SEODismissed: []string{},
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/builtintype"
|
||||
"RenewCMS/internal/domain/contenttype"
|
||||
"errors"
|
||||
"time"
|
||||
@@ -55,6 +56,9 @@ func (u *CreateContentTypeUseCase) Create(command CreateContentTypeCommand) (con
|
||||
}
|
||||
|
||||
func (u *CreateContentTypeUseCase) ensureKeyFree(key string) error {
|
||||
if _, err := builtintype.Find(key); err == nil {
|
||||
return contenttype.ErrKeyTaken
|
||||
}
|
||||
_, err := u.contentTypes.GetByKey(key)
|
||||
if errors.Is(err, contenttype.ErrContentTypeNotFound) {
|
||||
return nil
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"RenewCMS/internal/domain/contenttype"
|
||||
"RenewCMS/internal/domain/entry"
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/role"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -17,7 +18,7 @@ type CreateEntryUseCase struct {
|
||||
type CreateEntryCommand struct {
|
||||
ContentTypeKey string
|
||||
Data map[string]any
|
||||
UserID uint32
|
||||
Caller ContentCaller
|
||||
}
|
||||
|
||||
func NewCreateEntryUseCase(contentTypes contenttype.Repository, entries entry.Repository, images image.Repository, now func() time.Time) *CreateEntryUseCase {
|
||||
@@ -25,6 +26,9 @@ func NewCreateEntryUseCase(contentTypes contenttype.Repository, entries entry.Re
|
||||
}
|
||||
|
||||
func (u *CreateEntryUseCase) Create(command CreateEntryCommand) (TypedEntry, error) {
|
||||
if err := command.Caller.requireHolding(role.KeyContentCreate, command.ContentTypeKey); err != nil {
|
||||
return TypedEntry{}, err
|
||||
}
|
||||
owner, err := u.contentTypes.GetByKey(command.ContentTypeKey)
|
||||
if err != nil {
|
||||
return TypedEntry{}, err
|
||||
@@ -47,8 +51,8 @@ func (u *CreateEntryUseCase) Create(command CreateEntryCommand) (TypedEntry, err
|
||||
stored, err := u.entries.Create(entry.Entry{
|
||||
ContentTypeID: owner.ID,
|
||||
Data: data,
|
||||
CreatedBy: command.UserID,
|
||||
UpdatedBy: command.UserID,
|
||||
CreatedBy: command.Caller.UserID,
|
||||
UpdatedBy: command.Caller.UserID,
|
||||
CreatedAt: created,
|
||||
UpdatedAt: created,
|
||||
})
|
||||
|
||||
@@ -3,6 +3,8 @@ package useCases
|
||||
import (
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/audit"
|
||||
"RenewCMS/internal/domain/builtintype"
|
||||
"RenewCMS/internal/domain/role"
|
||||
"context"
|
||||
)
|
||||
|
||||
@@ -15,11 +17,17 @@ func NewDeleteArticleUseCase(articleRepository article.Repository, recorder audi
|
||||
return &DeleteArticleUseCase{articleRepository, recorder}
|
||||
}
|
||||
|
||||
func (g *DeleteArticleUseCase) DeleteArticle(ctx context.Context, id uint32) error {
|
||||
func (g *DeleteArticleUseCase) DeleteArticle(ctx context.Context, id uint32, caller ContentCaller) error {
|
||||
if err := caller.requireHolding(role.KeyContentDelete, builtintype.KeyArticle); err != nil {
|
||||
return err
|
||||
}
|
||||
target, err := g.articleRepository.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := caller.authorize(role.KeyContentDelete, builtintype.KeyArticle, target.CreatedBy); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
event := audit.NewEvent(audit.ActionArticleDelete, audit.CategoryArticles)
|
||||
event.Target = articleTarget(target)
|
||||
|
||||
@@ -3,6 +3,7 @@ package useCases
|
||||
import (
|
||||
"RenewCMS/internal/domain/contenttype"
|
||||
"RenewCMS/internal/domain/entry"
|
||||
"RenewCMS/internal/domain/role"
|
||||
)
|
||||
|
||||
type DeleteEntryUseCase struct {
|
||||
@@ -14,10 +15,20 @@ func NewDeleteEntryUseCase(contentTypes contenttype.Repository, entries entry.Re
|
||||
return &DeleteEntryUseCase{contentTypes, entries}
|
||||
}
|
||||
|
||||
func (u *DeleteEntryUseCase) Delete(contentTypeKey string, id uint32) error {
|
||||
func (u *DeleteEntryUseCase) Delete(contentTypeKey string, id uint32, caller ContentCaller) error {
|
||||
if err := caller.requireHolding(role.KeyContentDelete, contentTypeKey); err != nil {
|
||||
return err
|
||||
}
|
||||
owner, err := u.contentTypes.GetByKey(contentTypeKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
found, err := u.entries.Get(owner.ID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := caller.authorize(role.KeyContentDelete, contentTypeKey, found.CreatedBy); err != nil {
|
||||
return err
|
||||
}
|
||||
return u.entries.Delete(owner.ID, id)
|
||||
}
|
||||
@@ -11,24 +11,14 @@ func NewGetArticleUseCase(articleRepository article.Repository, bodyRenderer art
|
||||
return &GetArticleUseCase{articleRepository, bodyRenderer}
|
||||
}
|
||||
|
||||
func (u *GetArticleUseCase) GetOnlineArticle(id uint32, format article.BodyFormat) (article.Article, error) {
|
||||
func (u *GetArticleUseCase) GetArticle(id uint32, format article.BodyFormat, caller ContentCaller) (article.Article, error) {
|
||||
art, err := u.articleRepository.Get(id)
|
||||
if err != nil {
|
||||
return article.Article{}, err
|
||||
}
|
||||
|
||||
if !art.IsOnline {
|
||||
if !caller.seesArticle(art) {
|
||||
return article.Article{}, article.ErrArticleNotFound
|
||||
}
|
||||
|
||||
return renderArticle(u.bodyRenderer, art, format)
|
||||
}
|
||||
|
||||
func (u *GetArticleUseCase) GetArticle(id uint32, format article.BodyFormat) (article.Article, error) {
|
||||
art, err := u.articleRepository.Get(id)
|
||||
if err != nil {
|
||||
return article.Article{}, err
|
||||
}
|
||||
return renderArticle(u.bodyRenderer, art, format)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"RenewCMS/internal/domain/contenttype"
|
||||
"RenewCMS/internal/domain/entry"
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/role"
|
||||
)
|
||||
|
||||
type GetEntryUseCase struct {
|
||||
@@ -16,7 +17,10 @@ func NewGetEntryUseCase(contentTypes contenttype.Repository, entries entry.Repos
|
||||
return &GetEntryUseCase{contentTypes, entries, images}
|
||||
}
|
||||
|
||||
func (u *GetEntryUseCase) Get(contentTypeKey string, id uint32) (TypedEntry, error) {
|
||||
func (u *GetEntryUseCase) Get(contentTypeKey string, id uint32, caller ContentCaller) (TypedEntry, error) {
|
||||
if err := caller.requireHolding(role.KeyContentRead, contentTypeKey); err != nil {
|
||||
return TypedEntry{}, err
|
||||
}
|
||||
owner, err := u.contentTypes.GetByKey(contentTypeKey)
|
||||
if err != nil {
|
||||
return TypedEntry{}, err
|
||||
@@ -25,5 +29,8 @@ func (u *GetEntryUseCase) Get(contentTypeKey string, id uint32) (TypedEntry, err
|
||||
if err != nil {
|
||||
return TypedEntry{}, err
|
||||
}
|
||||
if !caller.reads(contentTypeKey, found.CreatedBy) {
|
||||
return TypedEntry{}, entry.ErrEntryNotFound
|
||||
}
|
||||
return typedEntry(owner, found, newEntryResolvers(u.images, u.contentTypes, u.entries))
|
||||
}
|
||||
@@ -46,24 +46,15 @@ func (u *GetMyAccessUseCase) Get(account user.User) (Access, error) {
|
||||
if err != nil {
|
||||
return Access{}, err
|
||||
}
|
||||
granted, err := u.roles.ListGrants(resolved.ID)
|
||||
if err != nil {
|
||||
return Access{}, err
|
||||
}
|
||||
return Access{Role: resolved, Permissions: orderedGrants(granted)}, nil
|
||||
return Access{Role: resolved, Permissions: effectiveGrants(resolved.GrantSet())}, nil
|
||||
}
|
||||
|
||||
func orderedGrants(granted []role.Grant) []role.Grant {
|
||||
scopes := make(map[string]role.Scope, len(granted))
|
||||
for _, grant := range granted {
|
||||
scopes[grant.Key] = grant.Scope
|
||||
}
|
||||
ordered := []role.Grant{}
|
||||
func effectiveGrants(grants role.GrantSet) []role.Grant {
|
||||
effective := []role.Grant{}
|
||||
for _, permission := range role.Catalogue() {
|
||||
scope, present := scopes[permission.Key]
|
||||
if present && scope != role.ScopeNone {
|
||||
ordered = append(ordered, role.Grant{Key: permission.Key, Scope: scope})
|
||||
if scope := grants.ScopeFor(permission.Key, ""); scope != role.ScopeNone {
|
||||
effective = append(effective, role.Grant{Key: permission.Key, Scope: scope})
|
||||
}
|
||||
}
|
||||
return ordered
|
||||
return effective
|
||||
}
|
||||
@@ -27,7 +27,7 @@ func NewListAPIKeysUseCase(apiKeyRepository apikey.Repository, userRepository us
|
||||
}
|
||||
|
||||
func (l *ListAPIKeysUseCase) ListAPIKeys(requesterId uint32, grants role.GrantSet) ([]APIKeyEntry, error) {
|
||||
scope := grants.Scope(role.KeyAPIKeysView)
|
||||
scope := grants.ScopeFor(role.KeyAPIKeysView, "")
|
||||
if scope == role.ScopeNone {
|
||||
return nil, role.MissingPermissionError{Key: role.KeyAPIKeysView}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/builtintype"
|
||||
"RenewCMS/internal/domain/role"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"errors"
|
||||
)
|
||||
@@ -17,15 +19,15 @@ func NewListArticlesUseCase(articleRepository article.Repository, bodyRenderer a
|
||||
}
|
||||
|
||||
type ListArticlesCommand struct {
|
||||
Format article.BodyFormat
|
||||
Verified bool
|
||||
Page int
|
||||
Size int
|
||||
Q string
|
||||
Status string
|
||||
Sort article.Sort
|
||||
SiteKey *string
|
||||
SEO string
|
||||
Format article.BodyFormat
|
||||
Caller ContentCaller
|
||||
Page int
|
||||
Size int
|
||||
Q string
|
||||
Status string
|
||||
Sort article.Sort
|
||||
SiteKey *string
|
||||
SEO string
|
||||
}
|
||||
|
||||
type ArticlePage struct {
|
||||
@@ -38,20 +40,31 @@ type ArticlePage struct {
|
||||
}
|
||||
|
||||
func (u *ListArticlesUseCase) ListArticlesPage(command ListArticlesCommand) (ArticlePage, error) {
|
||||
counts, err := u.articleRepository.CountByVisibility(!command.Verified)
|
||||
switch command.Caller.Grants.ScopeFor(role.KeyContentRead, builtintype.KeyArticle) {
|
||||
case role.ScopeAll:
|
||||
return u.listVisibleByRepository(command, false)
|
||||
case role.ScopeOwn:
|
||||
return u.listOwnedAndOnline(command)
|
||||
default:
|
||||
return u.listVisibleByRepository(command, true)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *ListArticlesUseCase) listVisibleByRepository(command ListArticlesCommand, onlineOnly bool) (ArticlePage, error) {
|
||||
counts, err := u.articleRepository.CountByVisibility(onlineOnly)
|
||||
if err != nil {
|
||||
return ArticlePage{}, err
|
||||
}
|
||||
summaries, err := u.summarizeVisible(!command.Verified)
|
||||
summaries, err := u.summarizeVisible(onlineOnly)
|
||||
if err != nil {
|
||||
return ArticlePage{}, err
|
||||
}
|
||||
counts.Suggestions = countSuggestions(summaries)
|
||||
page := ArticlePage{Items: []article.Article{}, Page: command.Page, Size: command.Size, Counts: counts, SEO: map[uint32]article.SEOSummary{}}
|
||||
if !command.Verified && command.Status == article.StatusDraft {
|
||||
page := emptyArticlePage(command, counts)
|
||||
if onlineOnly && command.Status == article.StatusDraft {
|
||||
return page, nil
|
||||
}
|
||||
filters := pageFilters(command)
|
||||
filters := pageFilters(command, onlineOnly)
|
||||
if command.SiteKey != nil {
|
||||
siteID, known, err := u.siteIDOf(*command.SiteKey)
|
||||
if err != nil {
|
||||
@@ -81,6 +94,99 @@ func (u *ListArticlesUseCase) ListArticlesPage(command ListArticlesCommand) (Art
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func emptyArticlePage(command ListArticlesCommand, counts article.Counts) ArticlePage {
|
||||
return ArticlePage{Items: []article.Article{}, Page: command.Page, Size: command.Size, Counts: counts, SEO: map[uint32]article.SEOSummary{}}
|
||||
}
|
||||
|
||||
func (u *ListArticlesUseCase) listOwnedAndOnline(command ListArticlesCommand) (ArticlePage, error) {
|
||||
everything, err := u.articleRepository.FindPage(article.Filters{Unpaged: true})
|
||||
if err != nil {
|
||||
return ArticlePage{}, err
|
||||
}
|
||||
visible := visibleTo(command.Caller, everything.Items)
|
||||
summaries, err := u.summarize(visible)
|
||||
if err != nil {
|
||||
return ArticlePage{}, err
|
||||
}
|
||||
counts := tally(visible)
|
||||
counts.Suggestions = countSuggestions(summaries)
|
||||
page := emptyArticlePage(command, counts)
|
||||
filters := pageFilters(command, false)
|
||||
filters.Unpaged = true
|
||||
if command.SiteKey != nil {
|
||||
siteID, known, err := u.siteIDOf(*command.SiteKey)
|
||||
if err != nil {
|
||||
return ArticlePage{}, err
|
||||
}
|
||||
if !known {
|
||||
return page, nil
|
||||
}
|
||||
filters.SiteID = &siteID
|
||||
}
|
||||
found, err := u.articleRepository.FindPage(filters)
|
||||
if err != nil {
|
||||
return ArticlePage{}, err
|
||||
}
|
||||
matching := visibleTo(command.Caller, found.Items)
|
||||
if command.SEO != "" {
|
||||
matching = withSEOStatus(matching, summaries, command.SEO)
|
||||
}
|
||||
rendered, err := renderArticles(u.bodyRenderer, pageSlice(matching, command.Page, command.Size), command.Format)
|
||||
if err != nil {
|
||||
return ArticlePage{}, err
|
||||
}
|
||||
page.Items = rendered
|
||||
page.Total = len(matching)
|
||||
page.SEO = summaries
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func visibleTo(caller ContentCaller, items []article.Article) []article.Article {
|
||||
visible := make([]article.Article, 0, len(items))
|
||||
for _, item := range items {
|
||||
if caller.seesArticle(item) {
|
||||
visible = append(visible, item)
|
||||
}
|
||||
}
|
||||
return visible
|
||||
}
|
||||
|
||||
func tally(visible []article.Article) article.Counts {
|
||||
counts := article.Counts{All: len(visible)}
|
||||
for _, item := range visible {
|
||||
if item.IsOnline {
|
||||
counts.Online++
|
||||
} else {
|
||||
counts.Draft++
|
||||
}
|
||||
}
|
||||
return counts
|
||||
}
|
||||
|
||||
func withSEOStatus(items []article.Article, summaries map[uint32]article.SEOSummary, status string) []article.Article {
|
||||
matching := make([]article.Article, 0, len(items))
|
||||
for _, item := range items {
|
||||
if summaries[item.ID].Status == status {
|
||||
matching = append(matching, item)
|
||||
}
|
||||
}
|
||||
return matching
|
||||
}
|
||||
|
||||
func pageSlice[T any](items []T, page int, size int) []T {
|
||||
size = min(max(size, 0), len(items))
|
||||
if size == 0 {
|
||||
return []T{}
|
||||
}
|
||||
pages := (len(items) + size - 1) / size
|
||||
pageIndex := max(page, 1) - 1
|
||||
if pageIndex >= pages {
|
||||
return []T{}
|
||||
}
|
||||
start := pageIndex * size
|
||||
return items[start:min(start+size, len(items))]
|
||||
}
|
||||
|
||||
func (u *ListArticlesUseCase) summarizeVisible(onlineOnly bool) (map[uint32]article.SEOSummary, error) {
|
||||
filters := article.Filters{Unpaged: true}
|
||||
if onlineOnly {
|
||||
@@ -105,24 +211,8 @@ func countSuggestions(summaries map[uint32]article.SEOSummary) int {
|
||||
}
|
||||
|
||||
func pageBySEOStatus(items []article.Article, summaries map[uint32]article.SEOSummary, command ListArticlesCommand) ([]article.Article, int) {
|
||||
matching := make([]article.Article, 0, len(items))
|
||||
for _, item := range items {
|
||||
if summaries[item.ID].Status == command.SEO {
|
||||
matching = append(matching, item)
|
||||
}
|
||||
}
|
||||
size := min(max(command.Size, 0), len(matching))
|
||||
if size == 0 {
|
||||
return []article.Article{}, len(matching)
|
||||
}
|
||||
pages := (len(matching) + size - 1) / size
|
||||
pageIndex := max(command.Page, 1) - 1
|
||||
if pageIndex >= pages {
|
||||
return []article.Article{}, len(matching)
|
||||
}
|
||||
start := pageIndex * size
|
||||
end := min(start+size, len(matching))
|
||||
return matching[start:end], len(matching)
|
||||
matching := withSEOStatus(items, summaries, command.SEO)
|
||||
return pageSlice(matching, command.Page, command.Size), len(matching)
|
||||
}
|
||||
|
||||
func (u *ListArticlesUseCase) summarize(items []article.Article) (map[uint32]article.SEOSummary, error) {
|
||||
@@ -155,9 +245,9 @@ func (u *ListArticlesUseCase) siteIDOf(key string) (uint32, bool, error) {
|
||||
return found.ID, true, nil
|
||||
}
|
||||
|
||||
func pageFilters(command ListArticlesCommand) article.Filters {
|
||||
func pageFilters(command ListArticlesCommand, onlineOnly bool) article.Filters {
|
||||
filters := article.Filters{Page: command.Page, Size: command.Size, Q: command.Q, Status: command.Status, Sort: command.Sort}
|
||||
if !command.Verified {
|
||||
if onlineOnly {
|
||||
online := true
|
||||
filters.IsOnline = &online
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"RenewCMS/internal/domain/contenttype"
|
||||
"RenewCMS/internal/domain/entry"
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/role"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -19,6 +20,7 @@ type ListEntriesCommand struct {
|
||||
Size int
|
||||
Sort entry.Sort
|
||||
Query string
|
||||
Caller ContentCaller
|
||||
}
|
||||
|
||||
type EntryPage struct {
|
||||
@@ -33,6 +35,9 @@ func NewListEntriesUseCase(contentTypes contenttype.Repository, entries entry.Re
|
||||
}
|
||||
|
||||
func (u *ListEntriesUseCase) ListPage(command ListEntriesCommand) (EntryPage, error) {
|
||||
if err := command.Caller.requireHolding(role.KeyContentRead, command.ContentTypeKey); err != nil {
|
||||
return EntryPage{}, err
|
||||
}
|
||||
if err := entry.ValidatePage(command.Page, command.Size); err != nil {
|
||||
return EntryPage{}, err
|
||||
}
|
||||
@@ -61,7 +66,8 @@ func (u *ListEntriesUseCase) ListPage(command ListEntriesCommand) (EntryPage, er
|
||||
|
||||
func (u *ListEntriesUseCase) page(owner contenttype.ContentType, command ListEntriesCommand) (entry.PageResult, error) {
|
||||
query := strings.TrimSpace(command.Query)
|
||||
if query == "" {
|
||||
readScope := command.Caller.Grants.ScopeFor(role.KeyContentRead, command.ContentTypeKey)
|
||||
if query == "" && readScope == role.ScopeAll {
|
||||
return u.entries.ListPage(entry.Filters{ContentTypeID: owner.ID, Page: command.Page, Size: command.Size, Sort: command.Sort})
|
||||
}
|
||||
all, err := u.entries.ListPage(entry.Filters{ContentTypeID: owner.ID, Unpaged: true, Sort: command.Sort})
|
||||
@@ -70,13 +76,9 @@ func (u *ListEntriesUseCase) page(owner contenttype.ContentType, command ListEnt
|
||||
}
|
||||
matching := make([]entry.Entry, 0, len(all.Items))
|
||||
for _, record := range all.Items {
|
||||
if entry.MatchesQuery(owner.Fields, record.Data, query) {
|
||||
if role.ScopePermits(readScope, command.Caller.UserID, record.CreatedBy) && (query == "" || entry.MatchesQuery(owner.Fields, record.Data, query)) {
|
||||
matching = append(matching, record)
|
||||
}
|
||||
}
|
||||
pageNumber := max(command.Page, 1)
|
||||
size := max(command.Size, 0)
|
||||
start := min((pageNumber-1)*size, len(matching))
|
||||
end := min(start+size, len(matching))
|
||||
return entry.PageResult{Items: matching[start:end], Total: len(matching)}, nil
|
||||
return entry.PageResult{Items: pageSlice(matching, command.Page, command.Size), Total: len(matching)}, nil
|
||||
}
|
||||
@@ -32,12 +32,12 @@ func NewResolveArticleSEOUseCase(articleRepository article.Repository, sites sit
|
||||
return &ResolveArticleSEOUseCase{articleRepository: articleRepository, sites: sites, images: images}
|
||||
}
|
||||
|
||||
func (u *ResolveArticleSEOUseCase) Resolve(id uint32, verified bool) (article.ResolvedSEO, error) {
|
||||
func (u *ResolveArticleSEOUseCase) Resolve(id uint32, caller ContentCaller) (article.ResolvedSEO, error) {
|
||||
art, err := u.articleRepository.Get(id)
|
||||
if err != nil {
|
||||
return article.ResolvedSEO{}, err
|
||||
}
|
||||
if !art.IsOnline && !verified {
|
||||
if !caller.seesArticle(art) {
|
||||
return article.ResolvedSEO{}, article.ErrArticleNotFound
|
||||
}
|
||||
owner, err := u.sites.Get(art.SiteID)
|
||||
|
||||
@@ -19,8 +19,7 @@ func NewRevokeAPIKeyUseCase(apiKeyRepository apikey.Repository, now func() time.
|
||||
}
|
||||
|
||||
func (r *RevokeAPIKeyUseCase) RevokeAPIKey(ctx context.Context, requesterId uint32, keyId uint32, grants role.GrantSet) error {
|
||||
scope := grants.Scope(role.KeyAPIKeysRevoke)
|
||||
if scope == role.ScopeNone {
|
||||
if !grants.Holds(role.KeyAPIKeysRevoke) {
|
||||
return role.MissingPermissionError{Key: role.KeyAPIKeysRevoke}
|
||||
}
|
||||
|
||||
@@ -29,7 +28,7 @@ func (r *RevokeAPIKeyUseCase) RevokeAPIKey(ctx context.Context, requesterId uint
|
||||
return err
|
||||
}
|
||||
|
||||
if scope != role.ScopeAll && key.UserID != requesterId {
|
||||
if !grants.Permits(role.KeyAPIKeysRevoke, "", requesterId, key.UserID) {
|
||||
return apikey.ErrAPIKeyNotFound
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/audit"
|
||||
"RenewCMS/internal/domain/author"
|
||||
"RenewCMS/internal/domain/builtintype"
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/role"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"context"
|
||||
"slices"
|
||||
@@ -44,15 +46,21 @@ func NewUpdateArticleUseCase(articleRepository article.Repository, bodyRenderer
|
||||
return &UpdateArticleUseCase{articleRepository: articleRepository, bodyRenderer: bodyRenderer, recorder: recorder, sites: sites, authors: authors, images: images}
|
||||
}
|
||||
|
||||
func (g *UpdateArticleUseCase) UpdateBody(ctx context.Context, id uint32, body string, format article.BodyFormat) (article.Article, error) {
|
||||
return g.Update(ctx, id, UpdateArticleCommand{Body: &body, Format: format})
|
||||
func (g *UpdateArticleUseCase) UpdateBody(ctx context.Context, id uint32, caller ContentCaller, body string, format article.BodyFormat) (article.Article, error) {
|
||||
return g.Update(ctx, id, caller, UpdateArticleCommand{Body: &body, Format: format})
|
||||
}
|
||||
|
||||
func (g *UpdateArticleUseCase) Update(ctx context.Context, id uint32, command UpdateArticleCommand) (article.Article, error) {
|
||||
func (g *UpdateArticleUseCase) Update(ctx context.Context, id uint32, caller ContentCaller, command UpdateArticleCommand) (article.Article, error) {
|
||||
if err := caller.requireHolding(role.KeyContentUpdate, builtintype.KeyArticle); err != nil {
|
||||
return article.Article{}, err
|
||||
}
|
||||
before, err := g.articleRepository.Get(id)
|
||||
if err != nil {
|
||||
return article.Article{}, err
|
||||
}
|
||||
if err := caller.authorize(role.KeyContentUpdate, builtintype.KeyArticle, before.CreatedBy); err != nil {
|
||||
return article.Article{}, err
|
||||
}
|
||||
destination, err := g.destinationSite(command)
|
||||
if err != nil {
|
||||
return article.Article{}, err
|
||||
@@ -247,11 +255,17 @@ func (g *UpdateArticleUseCase) siteName(id uint32) string {
|
||||
return found.Name
|
||||
}
|
||||
|
||||
func (g *UpdateArticleUseCase) UpdateIsOnline(ctx context.Context, id uint32, isOnline bool, format article.BodyFormat) (article.Article, error) {
|
||||
func (g *UpdateArticleUseCase) UpdateIsOnline(ctx context.Context, id uint32, caller ContentCaller, isOnline bool, format article.BodyFormat) (article.Article, error) {
|
||||
if err := caller.requireHolding(role.KeyContentPublish, builtintype.KeyArticle); err != nil {
|
||||
return article.Article{}, err
|
||||
}
|
||||
before, err := g.articleRepository.Get(id)
|
||||
if err != nil {
|
||||
return article.Article{}, err
|
||||
}
|
||||
if err := caller.authorize(role.KeyContentPublish, builtintype.KeyArticle, before.CreatedBy); err != nil {
|
||||
return article.Article{}, err
|
||||
}
|
||||
updated, err := g.articleRepository.UpdateIsOnline(id, isOnline)
|
||||
if err != nil {
|
||||
g.recordFailure(ctx, publishAction(isOnline), before, err)
|
||||
|
||||
@@ -93,10 +93,14 @@ func (u *UpdateAuthorUseCase) prepare(before author.Author, caller AuthorCaller,
|
||||
}
|
||||
|
||||
func authorizeEdit(before author.Author, caller AuthorCaller, patch author.Patch) error {
|
||||
if caller.Grants.HoldsAll(role.KeyAuthorsEdit) {
|
||||
return nil
|
||||
var linkedID uint32
|
||||
if before.UserID != nil {
|
||||
linkedID = *before.UserID
|
||||
}
|
||||
if before.UserID == nil || *before.UserID != caller.UserID || patch.UserID.Set {
|
||||
if !caller.Grants.Permits(role.KeyAuthorsEdit, "", caller.UserID, linkedID) {
|
||||
return author.ErrAuthorRights
|
||||
}
|
||||
if patch.UserID.Set && !caller.Grants.HoldsAll(role.KeyAuthorsEdit) {
|
||||
return author.ErrAuthorRights
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"RenewCMS/internal/domain/contenttype"
|
||||
"RenewCMS/internal/domain/entry"
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/role"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -18,7 +19,7 @@ type UpdateEntryCommand struct {
|
||||
ContentTypeKey string
|
||||
ID uint32
|
||||
Data map[string]any
|
||||
UserID uint32
|
||||
Caller ContentCaller
|
||||
}
|
||||
|
||||
func NewUpdateEntryUseCase(contentTypes contenttype.Repository, entries entry.Repository, images image.Repository, now func() time.Time) *UpdateEntryUseCase {
|
||||
@@ -26,6 +27,9 @@ func NewUpdateEntryUseCase(contentTypes contenttype.Repository, entries entry.Re
|
||||
}
|
||||
|
||||
func (u *UpdateEntryUseCase) Update(command UpdateEntryCommand) (TypedEntry, error) {
|
||||
if err := command.Caller.requireHolding(role.KeyContentUpdate, command.ContentTypeKey); err != nil {
|
||||
return TypedEntry{}, err
|
||||
}
|
||||
owner, err := u.contentTypes.GetByKey(command.ContentTypeKey)
|
||||
if err != nil {
|
||||
return TypedEntry{}, err
|
||||
@@ -34,6 +38,9 @@ func (u *UpdateEntryUseCase) Update(command UpdateEntryCommand) (TypedEntry, err
|
||||
if err != nil {
|
||||
return TypedEntry{}, err
|
||||
}
|
||||
if err := command.Caller.authorize(role.KeyContentUpdate, command.ContentTypeKey, current.CreatedBy); err != nil {
|
||||
return TypedEntry{}, err
|
||||
}
|
||||
if err := entry.Validate(owner.Fields, command.Data); err != nil {
|
||||
return TypedEntry{}, err
|
||||
}
|
||||
@@ -48,7 +55,7 @@ func (u *UpdateEntryUseCase) Update(command UpdateEntryCommand) (TypedEntry, err
|
||||
return TypedEntry{}, err
|
||||
}
|
||||
current.Data = command.Data
|
||||
current.UpdatedBy = command.UserID
|
||||
current.UpdatedBy = command.Caller.UserID
|
||||
current.UpdatedAt = u.now()
|
||||
stored, err := u.entries.Update(current)
|
||||
if err != nil {
|
||||
|
||||
@@ -34,7 +34,7 @@ type ActivityQuery struct {
|
||||
}
|
||||
|
||||
func scopeFilterToViewer(filter *audit.Filter, viewerID uint32, grants role.GrantSet) error {
|
||||
switch grants.Scope(role.KeyActivityView) {
|
||||
switch grants.ScopeFor(role.KeyActivityView, "") {
|
||||
case role.ScopeAll:
|
||||
return nil
|
||||
case role.ScopeOwn:
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/builtintype"
|
||||
"RenewCMS/internal/domain/role"
|
||||
)
|
||||
|
||||
type ContentCaller struct {
|
||||
UserID uint32
|
||||
Grants role.GrantSet
|
||||
}
|
||||
|
||||
func (c ContentCaller) requireHolding(key string, typeKey string) error {
|
||||
if !c.Grants.HoldsFor(key, typeKey) {
|
||||
return role.MissingPermissionError{Key: key}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c ContentCaller) authorize(key string, typeKey string, createdBy uint32) error {
|
||||
if !c.Grants.Permits(key, typeKey, c.UserID, createdBy) {
|
||||
return role.MissingPermissionError{Key: key}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c ContentCaller) reads(typeKey string, createdBy uint32) bool {
|
||||
return c.Grants.Permits(role.KeyContentRead, typeKey, c.UserID, createdBy)
|
||||
}
|
||||
|
||||
func (c ContentCaller) seesArticle(art article.Article) bool {
|
||||
return art.IsOnline || c.reads(builtintype.KeyArticle, art.CreatedBy)
|
||||
}
|
||||
Reference in new issue
Block a user