Files
kleiax 904d14b64c
CI / test (push) Canceled after 0s
Initial commit
2026-09-12 22:22:17 +02:00

203 lines
9.7 KiB
Go

package storage
import (
"slices"
"time"
)
// GardenRole identifies a member's authorization level within one garden.
type GardenRole string
// GardenPermission identifies one garden-scoped capability.
type GardenPermission string
// Garden permission constants identify capabilities resolved by the API's
// garden authorization middleware.
const (
GardenPermissionGardenRead GardenPermission = "garden:read"
GardenPermissionGardenUpdate GardenPermission = "garden:update"
GardenPermissionGardenDelete GardenPermission = "garden:delete"
// GardenPermissionContentWrite is retained for garden content which has not
// yet been split into object-specific permissions (journal and assignments).
GardenPermissionContentWrite GardenPermission = "content:write"
GardenPermissionMembersWrite GardenPermission = "members:write"
GardenPermissionSpeciesWrite GardenPermission = "species:write"
GardenPermissionPlantCreate GardenPermission = "plants:create"
GardenPermissionPlantReadOwn GardenPermission = "plants:read:own"
GardenPermissionPlantReadOther GardenPermission = "plants:read:other"
GardenPermissionPlantUpdateOwn GardenPermission = "plants:update:own"
GardenPermissionPlantUpdateOther GardenPermission = "plants:update:other"
GardenPermissionPlantDeleteOwn GardenPermission = "plants:delete:own"
GardenPermissionPlantDeleteOther GardenPermission = "plants:delete:other"
GardenPermissionLocationCreate GardenPermission = "locations:create"
GardenPermissionLocationReadOwn GardenPermission = "locations:read:own"
GardenPermissionLocationReadOther GardenPermission = "locations:read:other"
GardenPermissionLocationUpdateOwn GardenPermission = "locations:update:own"
GardenPermissionLocationUpdateOther GardenPermission = "locations:update:other"
GardenPermissionLocationDeleteOwn GardenPermission = "locations:delete:own"
GardenPermissionLocationDeleteOther GardenPermission = "locations:delete:other"
GardenPermissionTaskCreate GardenPermission = "tasks:create"
GardenPermissionTaskReadOwn GardenPermission = "tasks:read:own"
GardenPermissionTaskReadOther GardenPermission = "tasks:read:other"
GardenPermissionTaskUpdateOwn GardenPermission = "tasks:update:own"
GardenPermissionTaskUpdateOther GardenPermission = "tasks:update:other"
GardenPermissionTaskDeleteOwn GardenPermission = "tasks:delete:own"
GardenPermissionTaskDeleteOther GardenPermission = "tasks:delete:other"
GardenPermissionTaskCompleteOwn GardenPermission = "tasks:complete:own"
GardenPermissionTaskCompleteOther GardenPermission = "tasks:complete:other"
)
const (
// GardenRoleOwner grants full control over a garden.
GardenRoleOwner GardenRole = "owner"
// GardenRoleAdmin grants administrative access without ownership.
GardenRoleAdmin GardenRole = "admin"
// GardenRoleMember grants ordinary editing access.
GardenRoleMember GardenRole = "member"
// GardenRoleViewer grants read-only access.
GardenRoleViewer GardenRole = "viewer"
// GardenRoleWorker may read and complete tasks, but cannot otherwise edit content.
GardenRoleWorker GardenRole = "worker"
)
// Can reports whether a role grants permission.
func (role GardenRole) Can(permission GardenPermission) bool {
switch role {
case GardenRoleOwner:
return validGardenPermission(permission)
case GardenRoleAdmin:
return validGardenPermission(permission) && permission != GardenPermissionGardenDelete
case GardenRoleMember:
switch permission {
case GardenPermissionGardenRead,
GardenPermissionContentWrite,
GardenPermissionPlantCreate, GardenPermissionPlantReadOwn, GardenPermissionPlantReadOther, GardenPermissionPlantUpdateOwn, GardenPermissionPlantDeleteOwn,
GardenPermissionLocationCreate, GardenPermissionLocationReadOwn, GardenPermissionLocationReadOther, GardenPermissionLocationUpdateOwn, GardenPermissionLocationDeleteOwn,
GardenPermissionTaskCreate, GardenPermissionTaskReadOwn, GardenPermissionTaskReadOther, GardenPermissionTaskUpdateOwn, GardenPermissionTaskDeleteOwn, GardenPermissionTaskCompleteOwn, GardenPermissionTaskCompleteOther:
return true
}
return false
case GardenRoleViewer:
return permission == GardenPermissionGardenRead ||
permission == GardenPermissionPlantReadOwn || permission == GardenPermissionPlantReadOther ||
permission == GardenPermissionLocationReadOwn || permission == GardenPermissionLocationReadOther ||
permission == GardenPermissionTaskReadOwn || permission == GardenPermissionTaskReadOther
case GardenRoleWorker:
return permission == GardenPermissionGardenRead ||
permission == GardenPermissionTaskReadOwn || permission == GardenPermissionTaskReadOther ||
permission == GardenPermissionTaskCompleteOwn || permission == GardenPermissionTaskCompleteOther
default:
return false
}
}
func validGardenPermission(permission GardenPermission) bool {
switch permission {
case GardenPermissionGardenRead, GardenPermissionGardenUpdate, GardenPermissionGardenDelete,
GardenPermissionContentWrite, GardenPermissionMembersWrite, GardenPermissionSpeciesWrite,
GardenPermissionPlantCreate, GardenPermissionPlantReadOwn, GardenPermissionPlantReadOther, GardenPermissionPlantUpdateOwn, GardenPermissionPlantUpdateOther, GardenPermissionPlantDeleteOwn, GardenPermissionPlantDeleteOther,
GardenPermissionLocationCreate, GardenPermissionLocationReadOwn, GardenPermissionLocationReadOther, GardenPermissionLocationUpdateOwn, GardenPermissionLocationUpdateOther, GardenPermissionLocationDeleteOwn, GardenPermissionLocationDeleteOther,
GardenPermissionTaskCreate, GardenPermissionTaskReadOwn, GardenPermissionTaskReadOther, GardenPermissionTaskUpdateOwn, GardenPermissionTaskUpdateOther, GardenPermissionTaskDeleteOwn, GardenPermissionTaskDeleteOther, GardenPermissionTaskCompleteOwn, GardenPermissionTaskCompleteOther:
return true
}
return false
}
// ValidGardenPermission reports whether permission is a known garden-scoped
// permission or wildcard.
func ValidGardenPermission(permission string) bool {
return permission == "*" || permission == "garden:*" || validGardenPermission(GardenPermission(permission))
}
// ResolveGardenPermissions applies garden-specific grants and revocations to a
// role's base permissions. The result is deduplicated and stable-sorted.
func ResolveGardenPermissions(base []string, overrides []GardenRolePermissionOverride) []GardenPermission {
permissions := make(map[GardenPermission]bool)
apply := func(permission string, granted bool) {
if permission == "*" || permission == "garden:*" {
for _, concrete := range AllGardenPermissions() {
if permission == "garden:*" && concrete == GardenPermissionGardenDelete {
continue
}
permissions[concrete] = granted
}
return
}
permissions[GardenPermission(permission)] = granted
}
for _, permission := range base {
apply(permission, true)
}
for _, override := range overrides {
apply(override.Permission, override.Granted)
}
result := make([]GardenPermission, 0, len(permissions))
for permission, granted := range permissions {
if granted {
result = append(result, permission)
}
}
slices.Sort(result)
return result
}
// Permissions returns the concrete capabilities bundled into a garden role.
func (role GardenRole) Permissions() []GardenPermission {
permissions := []GardenPermission{}
for _, permission := range AllGardenPermissions() {
if role.Can(permission) {
permissions = append(permissions, permission)
}
}
return permissions
}
// AllGardenPermissions returns every concrete garden-scoped permission.
func AllGardenPermissions() []GardenPermission {
return []GardenPermission{
GardenPermissionGardenRead, GardenPermissionGardenUpdate, GardenPermissionGardenDelete,
GardenPermissionContentWrite, GardenPermissionMembersWrite, GardenPermissionSpeciesWrite,
GardenPermissionPlantCreate, GardenPermissionPlantReadOwn, GardenPermissionPlantReadOther, GardenPermissionPlantUpdateOwn, GardenPermissionPlantUpdateOther, GardenPermissionPlantDeleteOwn, GardenPermissionPlantDeleteOther,
GardenPermissionLocationCreate, GardenPermissionLocationReadOwn, GardenPermissionLocationReadOther, GardenPermissionLocationUpdateOwn, GardenPermissionLocationUpdateOther, GardenPermissionLocationDeleteOwn, GardenPermissionLocationDeleteOther,
GardenPermissionTaskCreate, GardenPermissionTaskReadOwn, GardenPermissionTaskReadOther, GardenPermissionTaskUpdateOwn, GardenPermissionTaskUpdateOther, GardenPermissionTaskDeleteOwn, GardenPermissionTaskDeleteOther, GardenPermissionTaskCompleteOwn, GardenPermissionTaskCompleteOther,
}
}
// GardenMemberModelInterface persists garden membership and role assignments.
type GardenMemberModelInterface interface {
Insert(member GardenMember) (GardenMember, error)
Get(gardenID, userID int) (GardenMember, error)
GetAllForGarden(gardenID int) ([]GardenMember, error)
Update(member GardenMember) (GardenMember, error)
Delete(gardenID, userID int) error
TransferOwnership(gardenID, fromUserID, toUserID int) error
}
// GardenMember links a user to a garden with a role.
type GardenMember struct {
GardenID int `json:"garden_id"`
UserID int `json:"user_id"`
Role GardenRole `json:"role"`
JoinedAt time.Time `json:"joined_at"`
Name string `json:"name,omitempty"`
Email string `json:"email,omitempty"`
Permissions []GardenPermission `json:"permissions,omitempty"`
}
// Can uses persisted permissions when present, retaining built-in roles for
// compatibility with in-memory tests and pre-migration callers.
func (member GardenMember) Can(permission GardenPermission) bool {
if member.Permissions == nil {
return member.Role.Can(permission)
}
for _, granted := range member.Permissions {
if granted == permission || granted == "*" || granted == "garden:*" && permission != GardenPermissionGardenDelete {
return true
}
}
return false
}