feat: roles system backend
This commit is contained in:
1 parent
44cca46d5e
commit
8c6c6456da
50 files changed
+2578
-22
No files matched your search
@@ -0,0 +1,48 @@
|
||||
package role
|
||||
|
||||
const (
|
||||
OwnerName = "Owner"
|
||||
AdminName = "Admin"
|
||||
WriterName = "Writer"
|
||||
)
|
||||
|
||||
func allGrants(include func(Permission) bool) []Grant {
|
||||
grants := []Grant{}
|
||||
for _, permission := range catalogue {
|
||||
if include(permission) {
|
||||
grants = append(grants, Grant{Key: permission.Key, Scope: ScopeAll})
|
||||
}
|
||||
}
|
||||
return grants
|
||||
}
|
||||
|
||||
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},
|
||||
}
|
||||
}
|
||||
|
||||
func BuiltInRoles() []Role {
|
||||
return []Role{
|
||||
{
|
||||
Name: OwnerName, BuiltIn: true, Locked: true, Version: 1,
|
||||
Grants: allGrants(func(Permission) bool { return true }),
|
||||
},
|
||||
{
|
||||
Name: AdminName, BuiltIn: true, Version: 1,
|
||||
Grants: allGrants(func(p Permission) bool { return !p.OwnerOnly }),
|
||||
},
|
||||
{
|
||||
Name: WriterName, BuiltIn: true, Version: 1,
|
||||
Grants: writerGrants(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package role
|
||||
|
||||
import "strings"
|
||||
|
||||
type Kind string
|
||||
|
||||
const (
|
||||
KindScoped Kind = "scoped"
|
||||
KindToggle Kind = "toggle"
|
||||
)
|
||||
|
||||
const contentGroup = "content"
|
||||
|
||||
type Permission struct {
|
||||
Key string
|
||||
Group string
|
||||
Kind Kind
|
||||
OwnerOnly bool
|
||||
}
|
||||
|
||||
func (p Permission) PerType() bool {
|
||||
return strings.HasPrefix(p.Key, contentGroup+".")
|
||||
}
|
||||
|
||||
func scoped(group, key string) Permission {
|
||||
return Permission{Key: key, Group: group, Kind: KindScoped}
|
||||
}
|
||||
|
||||
func toggle(group, key string) Permission {
|
||||
return Permission{Key: key, Group: group, Kind: KindToggle}
|
||||
}
|
||||
|
||||
func ownerOnly(group, key string) Permission {
|
||||
return Permission{Key: key, Group: group, Kind: KindToggle, OwnerOnly: true}
|
||||
}
|
||||
|
||||
var catalogue = []Permission{
|
||||
scoped("content", "content.read"),
|
||||
toggle("content", "content.create"),
|
||||
scoped("content", "content.update"),
|
||||
scoped("content", "content.delete"),
|
||||
scoped("content", "content.publish"),
|
||||
toggle("schema", "schema.view"),
|
||||
toggle("schema", "schema.edit"),
|
||||
toggle("schema", "schema.delete"),
|
||||
toggle("media", "media.view"),
|
||||
toggle("media", "media.upload"),
|
||||
scoped("media", "media.delete"),
|
||||
toggle("authors", "authors.view"),
|
||||
toggle("authors", "authors.create"),
|
||||
scoped("authors", "authors.edit"),
|
||||
toggle("authors", "authors.delete"),
|
||||
scoped("activity", "activity.view"),
|
||||
toggle("activity", "activity.export"),
|
||||
toggle("users", "users.view"),
|
||||
toggle("users", "users.invite"),
|
||||
toggle("users", "users.change_role"),
|
||||
toggle("users", "users.remove"),
|
||||
toggle("settings", "settings.view"),
|
||||
toggle("settings", "settings.edit_general"),
|
||||
toggle("settings", "settings.manage_sites"),
|
||||
ownerOnly("settings", "settings.delete_site"),
|
||||
scoped("settings", "apikeys.view"),
|
||||
toggle("settings", "apikeys.create"),
|
||||
scoped("settings", "apikeys.revoke"),
|
||||
toggle("roles", "roles.view"),
|
||||
toggle("roles", "roles.manage"),
|
||||
ownerOnly("roles", "roles.edit_owner_only"),
|
||||
ownerOnly("system", "system.apply_updates"),
|
||||
ownerOnly("system", "system.edit_retention"),
|
||||
ownerOnly("system", "system.transfer_ownership"),
|
||||
}
|
||||
|
||||
func Catalogue() []Permission {
|
||||
return append([]Permission(nil), catalogue...)
|
||||
}
|
||||
|
||||
func Lookup(key string) (Permission, error) {
|
||||
for _, permission := range catalogue {
|
||||
if permission.Key == key {
|
||||
return permission, nil
|
||||
}
|
||||
}
|
||||
return Permission{}, ErrUnknownPermission
|
||||
}
|
||||
|
||||
func Groups() []string {
|
||||
groups := []string{}
|
||||
for _, permission := range catalogue {
|
||||
if len(groups) == 0 || groups[len(groups)-1] != permission.Group {
|
||||
groups = append(groups, permission.Group)
|
||||
}
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
const (
|
||||
typeSeparator = ":"
|
||||
maxKeyLength = 100
|
||||
typeKeyAlphabet = "abcdefghijklmnopqrstuvwxyz0123456789_-"
|
||||
)
|
||||
|
||||
func ParseTypedKey(composite string) (string, string, error) {
|
||||
base, typeKey, found := strings.Cut(composite, typeSeparator)
|
||||
if !found || len(composite) > maxKeyLength || typeKey == "" {
|
||||
return "", "", ErrUnknownPermission
|
||||
}
|
||||
permission, err := Lookup(base)
|
||||
if err != nil || !permission.PerType() {
|
||||
return "", "", ErrUnknownPermission
|
||||
}
|
||||
for _, char := range typeKey {
|
||||
if !strings.ContainsRune(typeKeyAlphabet, char) {
|
||||
return "", "", ErrUnknownPermission
|
||||
}
|
||||
}
|
||||
return base, typeKey, nil
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package role
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrRoleNotFound = errors.New("role not found")
|
||||
ErrUnknownPermission = errors.New("unknown permission")
|
||||
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")
|
||||
)
|
||||
|
||||
type Scope string
|
||||
|
||||
const (
|
||||
ScopeNone Scope = "none"
|
||||
ScopeOwn Scope = "own"
|
||||
ScopeAll Scope = "all"
|
||||
)
|
||||
|
||||
type Grant struct {
|
||||
Key string `json:"key"`
|
||||
Scope Scope `json:"scope"`
|
||||
}
|
||||
|
||||
type Role struct {
|
||||
ID uint32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
BuiltIn bool `json:"built_in"`
|
||||
Locked bool `json:"locked"`
|
||||
Version uint32 `json:"version"`
|
||||
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)
|
||||
if err != nil {
|
||||
return Permission{}, err
|
||||
}
|
||||
return Lookup(base)
|
||||
}
|
||||
return Lookup(key)
|
||||
}
|
||||
|
||||
func ValidateGrants(locked bool, grants []Grant) error {
|
||||
for _, grant := range grants {
|
||||
permission, err := resolveGrantKey(grant.Key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if permission.OwnerOnly && !locked {
|
||||
return ErrOwnerOnlyGrant
|
||||
}
|
||||
if !scopeValidFor(permission.Kind, grant.Scope) {
|
||||
return ErrInvalidScope
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func scopeValidFor(kind Kind, scope Scope) bool {
|
||||
if kind == KindToggle {
|
||||
return scope == ScopeAll
|
||||
}
|
||||
return scope == ScopeOwn || scope == ScopeAll
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package role
|
||||
|
||||
type Repository interface {
|
||||
List() ([]Role, error)
|
||||
Get(id uint32) (Role, error)
|
||||
GetByName(name string) (Role, error)
|
||||
ListGrants(roleID uint32) ([]Grant, error)
|
||||
Create(created Role) (Role, error)
|
||||
Save(saved Role) (Role, error)
|
||||
}
|
||||
Reference in new issue
Block a user