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 }