203 lines
9.7 KiB
Go
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
|
|
}
|