Files
pvemanager/backend/app/rbac/permissions.py
Dilshod Rasulov bddbf15652
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Failing after 1m22s
Deploy to Production / Trigger production update (push) Failing after 11s
feat(rbac): split setting:view into panel settings and a permission-free profile
setting:view used to gate the whole /settings page, which mixed panel-wide
configuration with a user's own account. Revoking it to keep a standard user
out of the panel settings also took away their profile, password, 2FA and SSH
keys, so the permission could not be used for what it names.

The account half now lives at /profile: its endpoints are self-scoped and only
require a session, so the page carries no permission at all and is reached from
the user menu (and Ctrl+K) rather than a sidebar group. /settings keeps the
panel-wide tabs behind setting:view, with saving gated on setting:update,
security on setting:manage and the SMTP/Telegram channels on is_admin, matching
what each endpoint checks.

The shipped "user" role no longer gets setting:view. Migration 42 drops it from
roles holding it without setting:update/manage, since those grants only existed
to reach the profile page; it runs once (marker in panel_settings) so a later
deliberate grant is not reverted on the next start.
2026-08-11 20:07:34 +05:00

849 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Permission Registry - Central definition of all permissions
Format: resource:action[:scope]
Resources: cluster, node, vm, lxc, storage, network, backup, template, user, role, log, setting, ipam
Actions: view, list, create, update, delete, start, stop, restart, console, migrate, manage
Scopes: global, organization, workspace, instance (optional, defaults to global)
"""
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Set
from enum import Enum
class ResourceType(str, Enum):
"""Resource types in the system"""
CLUSTER = "cluster"
NODE = "node"
SERVER = "server" # Proxmox server
VM = "vm"
LXC = "lxc"
STORAGE = "storage"
NETWORK = "network"
BACKUP = "backup"
TEMPLATE = "template"
USER = "user"
ROLE = "role"
LOG = "log"
SETTING = "setting"
IPAM = "ipam"
NOTIFICATION = "notification"
DASHBOARD = "dashboard"
class ActionType(str, Enum):
"""Action types"""
VIEW = "view"
LIST = "list"
CREATE = "create"
UPDATE = "update"
DELETE = "delete"
START = "start"
STOP = "stop"
RESTART = "restart"
CONSOLE = "console"
MIGRATE = "migrate"
MANAGE = "manage"
EXPORT = "export"
EXECUTE = "execute" # For running commands
class ScopeType(str, Enum):
"""Permission scope levels"""
GLOBAL = "global" # Platform-wide access
ORGANIZATION = "org" # Within organization
WORKSPACE = "workspace" # Within workspace
INSTANCE = "instance" # Specific resource only
@dataclass
class Permission:
"""
Atomic permission definition.
Format: resource:action or resource:action:scope
Examples:
- vm:view (global scope)
- vm:create:workspace (workspace scope)
- vm:delete:instance (instance scope)
"""
resource: str
action: str
scope: ScopeType = ScopeType.GLOBAL
display_name: str = ""
description: str = ""
category: str = ""
requires: List[str] = field(default_factory=list) # Dependencies
@property
def code(self) -> str:
"""Get permission code"""
if self.scope == ScopeType.GLOBAL:
return f"{self.resource}:{self.action}"
return f"{self.resource}:{self.action}:{self.scope.value}"
@property
def legacy_code(self) -> str:
"""Get legacy permission code for backwards compatibility"""
# Map to old format: resource.action
return f"{self.resource}.{self.action}"
def __hash__(self):
return hash(self.code)
def __eq__(self, other):
if isinstance(other, Permission):
return self.code == other.code
return False
class PermissionRegistry:
"""
Central registry of all permissions.
Provides validation, lookup, and category grouping.
"""
_instance = None
_permissions: Dict[str, Permission] = {}
_by_category: Dict[str, List[Permission]] = {}
_by_resource: Dict[str, List[Permission]] = {}
_legacy_map: Dict[str, str] = {} # legacy code -> new code
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
if self._initialized:
return
self._initialized = True
self._permissions = {}
self._by_category = {}
self._by_resource = {}
self._legacy_map = {}
def register(self, permission: Permission) -> None:
"""Register a permission"""
self._permissions[permission.code] = permission
# Index by category
if permission.category not in self._by_category:
self._by_category[permission.category] = []
self._by_category[permission.category].append(permission)
# Index by resource
if permission.resource not in self._by_resource:
self._by_resource[permission.resource] = []
self._by_resource[permission.resource].append(permission)
# Legacy mapping
self._legacy_map[permission.legacy_code] = permission.code
def get(self, code: str) -> Optional[Permission]:
"""Get permission by code"""
# Try direct lookup
if code in self._permissions:
return self._permissions[code]
# Try legacy format
if code in self._legacy_map:
return self._permissions[self._legacy_map[code]]
return None
def exists(self, code: str) -> bool:
"""Check if permission exists"""
return code in self._permissions or code in self._legacy_map
def resolve(self, code: str) -> Optional[str]:
"""Resolve legacy code to new code"""
if code in self._permissions:
return code
return self._legacy_map.get(code)
def all(self) -> List[Permission]:
"""Get all permissions"""
return list(self._permissions.values())
def by_category(self, category: str) -> List[Permission]:
"""Get permissions by category"""
return self._by_category.get(category, [])
def by_resource(self, resource: str) -> List[Permission]:
"""Get permissions by resource"""
return self._by_resource.get(resource, [])
def categories(self) -> List[str]:
"""Get all categories"""
return list(self._by_category.keys())
def resources(self) -> List[str]:
"""Get all resources"""
return list(self._by_resource.keys())
def validate(self, codes: List[str]) -> tuple[List[str], List[str]]:
"""Validate permission codes, returns (valid, invalid)"""
valid = []
invalid = []
for code in codes:
if self.exists(code):
valid.append(self.resolve(code) or code)
else:
invalid.append(code)
return valid, invalid
# ==================== Permission Definitions ====================
def _create_permissions() -> PermissionRegistry:
"""Create and populate the permission registry"""
registry = PermissionRegistry()
# Dashboard permissions
registry.register(Permission(
resource="dashboard", action="view",
display_name="View Dashboard",
description="Access to dashboard and overview",
category="Dashboard"
))
# Server (Proxmox) permissions
registry.register(Permission(
resource="server", action="view",
display_name="View Servers",
description="View Proxmox server list and status",
category="Proxmox Servers"
))
registry.register(Permission(
resource="server", action="create",
display_name="Add Server",
description="Add new Proxmox servers",
category="Proxmox Servers"
))
registry.register(Permission(
resource="server", action="update",
display_name="Edit Server",
description="Edit Proxmox server settings",
category="Proxmox Servers"
))
registry.register(Permission(
resource="server", action="delete",
display_name="Delete Server",
description="Remove Proxmox servers",
category="Proxmox Servers"
))
registry.register(Permission(
resource="server", action="manage",
display_name="Manage Servers",
description="Full server management including cluster operations",
category="Proxmox Servers",
requires=["server:view"]
))
registry.register(Permission(
resource="cluster", action="manage",
display_name="Manage Cluster Topology",
description="Create clusters, join nodes, eject nodes from cluster",
category="Proxmox Servers",
requires=["server:view"]
))
# VM permissions
registry.register(Permission(
resource="vm", action="view",
display_name="View VMs",
description="View virtual machines list and details",
category="Virtual Machines"
))
registry.register(Permission(
resource="vm", action="create",
display_name="Create VMs",
description="Create new virtual machines",
category="Virtual Machines",
requires=["vm:view", "template:view"]
))
registry.register(Permission(
resource="vm", action="update",
display_name="Update VMs",
description="Modify VM configuration",
category="Virtual Machines",
requires=["vm:view"]
))
registry.register(Permission(
resource="vm", action="delete",
display_name="Delete VMs",
description="Delete virtual machines",
category="Virtual Machines",
requires=["vm:view"]
))
registry.register(Permission(
resource="vm", action="start",
display_name="Start VMs",
description="Start virtual machines",
category="Virtual Machines",
requires=["vm:view"]
))
registry.register(Permission(
resource="vm", action="stop",
display_name="Stop VMs",
description="Stop virtual machines",
category="Virtual Machines",
requires=["vm:view"]
))
registry.register(Permission(
resource="vm", action="restart",
display_name="Restart VMs",
description="Restart virtual machines",
category="Virtual Machines",
requires=["vm:view"]
))
registry.register(Permission(
resource="vm", action="console",
display_name="VM Console",
description="Access VM console (noVNC/xterm.js)",
category="Virtual Machines",
requires=["vm:view"]
))
registry.register(Permission(
resource="vm", action="migrate",
display_name="Migrate VMs",
description="Migrate VMs between nodes",
category="Virtual Machines",
requires=["vm:view"]
))
registry.register(Permission(
resource="vm", action="remote_migrate",
display_name="Remote Migrate VMs",
description="Migrate VMs to a different, independently registered Proxmox cluster",
category="Virtual Machines",
requires=["vm:migrate"]
))
registry.register(Permission(
resource="vm", action="execute",
display_name="Execute Commands",
description="Execute commands on VMs via QEMU agent",
category="Virtual Machines",
requires=["vm:console"]
))
registry.register(Permission(
resource="vm", action="manage",
display_name="Manage VMs",
description="Full VM management including bulk operations",
category="Virtual Machines",
requires=["vm:view"]
))
# LXC (Container) permissions - same structure as VM
registry.register(Permission(
resource="lxc", action="view",
display_name="View Containers",
description="View LXC containers list and details",
category="Containers"
))
registry.register(Permission(
resource="lxc", action="create",
display_name="Create Containers",
description="Create new LXC containers",
category="Containers",
requires=["lxc:view", "template:view"]
))
registry.register(Permission(
resource="lxc", action="update",
display_name="Update Containers",
description="Modify container configuration",
category="Containers",
requires=["lxc:view"]
))
registry.register(Permission(
resource="lxc", action="delete",
display_name="Delete Containers",
description="Delete LXC containers",
category="Containers",
requires=["lxc:view"]
))
registry.register(Permission(
resource="lxc", action="start",
display_name="Start Containers",
description="Start LXC containers",
category="Containers",
requires=["lxc:view"]
))
registry.register(Permission(
resource="lxc", action="stop",
display_name="Stop Containers",
description="Stop LXC containers",
category="Containers",
requires=["lxc:view"]
))
registry.register(Permission(
resource="lxc", action="restart",
display_name="Restart Containers",
description="Restart LXC containers",
category="Containers",
requires=["lxc:view"]
))
registry.register(Permission(
resource="lxc", action="console",
display_name="Container Console",
description="Access container console",
category="Containers",
requires=["lxc:view"]
))
registry.register(Permission(
resource="lxc", action="migrate",
display_name="Migrate Containers",
description="Migrate containers between nodes",
category="Containers",
requires=["lxc:view"]
))
registry.register(Permission(
resource="lxc", action="remote_migrate",
display_name="Remote Migrate Containers",
description="Migrate containers to a different, independently registered Proxmox cluster",
category="Containers",
requires=["lxc:migrate"]
))
# Template permissions
registry.register(Permission(
resource="template", action="view",
display_name="View Templates",
description="View OS templates",
category="Templates"
))
registry.register(Permission(
resource="template", action="create",
display_name="Create Templates",
description="Create new templates",
category="Templates",
requires=["template:view"]
))
registry.register(Permission(
resource="template", action="update",
display_name="Update Templates",
description="Edit template settings",
category="Templates",
requires=["template:view"]
))
registry.register(Permission(
resource="template", action="delete",
display_name="Delete Templates",
description="Delete templates",
category="Templates",
requires=["template:view"]
))
registry.register(Permission(
resource="template", action="manage",
display_name="Manage Templates",
description="Full template management including download",
category="Templates",
requires=["template:view"]
))
# Storage permissions
registry.register(Permission(
resource="storage", action="view",
display_name="View Storage",
description="View storage pools and usage",
category="Storage"
))
registry.register(Permission(
resource="storage", action="manage",
display_name="Manage Storage",
description="Manage storage pools and volumes",
category="Storage",
requires=["storage:view"]
))
# Backup permissions
registry.register(Permission(
resource="backup", action="view",
display_name="View Backups",
description="View backup list",
category="Backups"
))
registry.register(Permission(
resource="backup", action="create",
display_name="Create Backups",
description="Create new backups",
category="Backups",
requires=["backup:view"]
))
registry.register(Permission(
resource="backup", action="restore",
display_name="Restore Backups",
description="Restore VM/LXC from backups",
category="Backups",
requires=["backup:view"]
))
registry.register(Permission(
resource="backup", action="delete",
display_name="Delete Backups",
description="Delete backups",
category="Backups",
requires=["backup:view"]
))
registry.register(Permission(
resource="backup", action="manage",
display_name="Manage Backups",
description="Manage storages and scheduled backup jobs",
category="Backups",
requires=["backup:view"]
))
# IPAM permissions
registry.register(Permission(
resource="ipam", action="view",
display_name="View IPAM",
description="View IP address management",
category="IPAM"
))
registry.register(Permission(
resource="ipam", action="manage",
display_name="Manage IPAM",
description="Manage networks and IP allocations",
category="IPAM",
requires=["ipam:view"]
))
# User management permissions
registry.register(Permission(
resource="user", action="view",
display_name="View Users",
description="View user list",
category="User Management"
))
registry.register(Permission(
resource="user", action="create",
display_name="Create Users",
description="Create new users",
category="User Management",
requires=["user:view"]
))
registry.register(Permission(
resource="user", action="update",
display_name="Update Users",
description="Edit user profiles",
category="User Management",
requires=["user:view"]
))
registry.register(Permission(
resource="user", action="delete",
display_name="Delete Users",
description="Delete users",
category="User Management",
requires=["user:view"]
))
registry.register(Permission(
resource="user", action="manage",
display_name="Manage Users",
description="Full user administration including managing other users' SSH keys",
category="User Management",
requires=["user:view"]
))
# Quota permissions
registry.register(Permission(
resource="quota", action="view",
display_name="View Quotas",
description="View user resource quotas",
category="User Management",
requires=["user:view"]
))
registry.register(Permission(
resource="quota", action="manage",
display_name="Manage Quotas",
description="Set user resource quotas",
category="User Management",
requires=["user:view"]
))
# Role management permissions
registry.register(Permission(
resource="role", action="view",
display_name="View Roles",
description="View roles and permissions",
category="Role Management"
))
registry.register(Permission(
resource="role", action="create",
display_name="Create Roles",
description="Create new roles",
category="Role Management",
requires=["role:view"]
))
registry.register(Permission(
resource="role", action="update",
display_name="Update Roles",
description="Edit role permissions",
category="Role Management",
requires=["role:view"]
))
registry.register(Permission(
resource="role", action="delete",
display_name="Delete Roles",
description="Delete roles",
category="Role Management",
requires=["role:view"]
))
registry.register(Permission(
resource="role", action="manage",
display_name="Manage Roles",
description="Full role management including assignment",
category="Role Management",
requires=["role:view", "user:view"]
))
# Log permissions
registry.register(Permission(
resource="log", action="view",
display_name="View Logs",
description="View audit logs",
category="Logs"
))
registry.register(Permission(
resource="log", action="export",
display_name="Export Logs",
description="Export log data",
category="Logs",
requires=["log:view"]
))
registry.register(Permission(
resource="log", action="delete",
display_name="Delete Logs",
description="Delete log entries",
category="Logs",
requires=["log:view"]
))
# Settings permissions. These cover panel-wide configuration only — a user's
# own account (profile, password, 2FA, SSH keys, notification preferences)
# is self-scoped and needs no permission at all.
registry.register(Permission(
resource="setting", action="view",
display_name="View Panel Settings",
description="View panel-wide settings (not needed for one's own profile)",
category="Settings"
))
registry.register(Permission(
resource="setting", action="update",
display_name="Update Panel Settings",
description="Modify panel-wide settings",
category="Settings",
requires=["setting:view"]
))
registry.register(Permission(
resource="setting", action="manage",
display_name="Manage Security Settings",
description="Manage security and advanced settings",
category="Settings",
requires=["setting:view"]
))
# Notification permissions
registry.register(Permission(
resource="notification", action="view",
display_name="View Notifications",
description="View notifications",
category="Notifications"
))
registry.register(Permission(
resource="notification", action="manage",
display_name="Manage Notifications",
description="Manage notification settings",
category="Notifications",
requires=["notification:view"]
))
# Firewall permissions (datacenter / node / VM firewall)
registry.register(Permission(
resource="firewall", action="view",
display_name="View Firewall",
description="View firewall rules, groups and options",
category="Firewall"
))
registry.register(Permission(
resource="firewall", action="manage",
display_name="Manage Firewall",
description="Manage firewall rules, security groups, IP sets and options",
category="Firewall",
requires=["firewall:view"]
))
# Networking permissions (node interfaces + SDN)
registry.register(Permission(
resource="network", action="view",
display_name="View Networking",
description="View node network interfaces and SDN configuration",
category="Networking"
))
registry.register(Permission(
resource="network", action="manage",
display_name="Manage Networking",
description="Manage node network interfaces, bridges and SDN (zones, vnets, subnets)",
category="Networking",
requires=["network:view"]
))
# Node administration permissions (services, APT, upgrade)
registry.register(Permission(
resource="node", action="view",
display_name="View Node Administration",
description="View node services, APT updates and repositories",
category="Node Administration"
))
registry.register(Permission(
resource="node", action="manage",
display_name="Manage Node Administration",
description="Control systemd services and manage APT repositories/refresh",
category="Node Administration",
requires=["node:view"]
))
registry.register(Permission(
resource="node", action="upgrade",
display_name="Upgrade Node Packages",
description="Run apt dist-upgrade via the node console (root shell)",
category="Node Administration",
requires=["node:view"]
))
registry.register(Permission(
resource="node", action="power",
display_name="Power Control Node",
description="Reboot or shut down the physical node",
category="Node Administration",
requires=["node:view"]
))
# Resource pool permissions
registry.register(Permission(
resource="pool", action="view",
display_name="View Pools",
description="View resource pools and their members",
category="Resource Pools"
))
registry.register(Permission(
resource="pool", action="manage",
display_name="Manage Pools",
description="Create/delete pools and manage pool membership",
category="Resource Pools",
requires=["pool:view"]
))
# App Store permissions (каталог self-hosted приложений)
registry.register(Permission(
resource="app", action="view",
display_name="View App Store",
description="Просмотр каталога приложений и установленных приложений",
category="App Store"
))
registry.register(Permission(
resource="app", action="install",
display_name="Install Apps",
description="Установка приложений из каталога (создание LXC)",
category="App Store",
requires=["app:view"]
))
registry.register(Permission(
resource="app", action="manage",
display_name="Manage App Store",
description="Синхронизация каталога, обновление/удаление приложений",
category="App Store",
requires=["app:view"]
))
# Script catalog permissions (bash-скрипты на нодах/гостях)
registry.register(Permission(
resource="script", action="view",
display_name="View Scripts",
description="Просмотр каталога скриптов и истории запусков",
category="Scripts"
))
registry.register(Permission(
resource="script", action="execute",
display_name="Execute Scripts",
description="Запуск скриптов из каталога на нодах/ВМ/LXC",
category="Scripts",
requires=["script:view"]
))
registry.register(Permission(
resource="script", action="manage",
display_name="Manage Scripts",
description="Создание/редактирование скриптов и git-источников каталога",
category="Scripts",
requires=["script:view"]
))
# PVE access permissions (authentication realms & API tokens)
registry.register(Permission(
resource="access", action="view",
display_name="View PVE Access",
description="View Proxmox authentication realms and API tokens",
category="PVE Access"
))
registry.register(Permission(
resource="access", action="manage",
display_name="Manage PVE Access",
description="Manage Proxmox authentication realms and API tokens",
category="PVE Access",
requires=["access:view"]
))
return registry
# Global registry instance
PERMISSIONS = _create_permissions()
# ==================== Legacy Permission Mapping ====================
LEGACY_PERMISSION_MAP = {
# Old format -> New format
"dashboard.view": "dashboard:view",
"proxmox.view": "server:view",
"proxmox.manage": "server:manage",
"proxmox.servers.add": "server:create",
"proxmox.servers.edit": "server:update",
"proxmox.servers.delete": "server:delete",
"proxmox.cluster.manage": "cluster:manage",
"proxmox.vm.manage": "vm:manage",
"vms.view": "vm:view",
"vms.create": "vm:create",
"vms.start": "vm:start",
"vms.stop": "vm:stop",
"vms.restart": "vm:restart",
"vms.delete": "vm:delete",
"vms.console": "vm:console",
"vms.migrate": "vm:migrate",
"templates.view": "template:view",
"templates.manage": "template:manage",
"ipam.view": "ipam:view",
"ipam.manage": "ipam:manage",
"logs.view": "log:view",
"logs.export": "log:export",
"logs.delete": "log:delete",
"settings.view": "setting:view",
"settings.panel": "setting:update",
"settings.security": "setting:manage",
"settings.manage": "setting:manage",
"users.view": "user:view",
"users.create": "user:create",
"users.edit": "user:update",
"users.delete": "user:delete",
"users.manage": "user:manage",
"roles.view": "role:view",
"roles.manage": "role:manage",
"notifications.manage": "notification:manage",
}
def resolve_permission(code: str) -> str:
"""Resolve legacy permission code to new format"""
return LEGACY_PERMISSION_MAP.get(code, code)
def get_permission_info(code: str) -> Optional[Permission]:
"""Get permission object by code (supports legacy)"""
resolved = resolve_permission(code)
return PERMISSIONS.get(resolved)