refac: debt on role backend

This commit is contained in:
Florian Sylvain committed 2026-10-26 23:34:35 +01:00
1 parent de142f6c5f
commit a043f7f41a
130 files changed
+3591 -512

No files matched your search

+9 -9
View File
@@ -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},
}
}
+15 -51
View File
@@ -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
}
+87 -13
View File
@@ -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
}
+34 -7
View File
@@ -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"
)