@@ -0,0 +1,202 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user