feat(quotas): per-user resource limits for VM/LXC deployment
Add per-user quotas (max instances, vCPU, RAM, disk) enforced at deploy time and surfaced to users self-service. Backend: - UserQuota model (one-to-one, NULL = unlimited) + migration #28 - quota_service: usage from vm_instances cache, check_quota raises 429 - enforce in VM (templates.py) and LXC (lxc.py) deploy paths - RBAC quota:view / quota:manage, granted to admin - admin API GET/PUT /admin/api/users/{id}/quota - self-service GET /settings/api/quota - tighten VM list isolation via can_view_all_instances Frontend: - admin quota dialog in UsersPage with usage bars - quota card on Settings page (own limits) - remaining-quota hint in CreateInstanceWizard config step - useUserQuota/useSetUserQuota, useMyQuota hooks; RU/EN locales Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -20,43 +20,46 @@ from ...ipam_service import IPAMService
|
||||
|
||||
# ==================== Helper Functions for User Isolation ====================
|
||||
|
||||
def can_view_all_instances(current_user: User) -> bool:
|
||||
"""
|
||||
Whether the user may see/manage *every* instance, not just their own.
|
||||
|
||||
Only administrators — or roles that explicitly grant ``vm:manage`` — get the
|
||||
global view. Every other authenticated user (including users with no role,
|
||||
custom roles, or the stock ``user`` role) is scoped to instances they own.
|
||||
|
||||
Note: ``vm:view`` is deliberately NOT treated as a global view permission.
|
||||
The stock ``user`` role carries ``vm:view`` simply so the instances feature
|
||||
is usable at all; granting it would let any tenant see every VM.
|
||||
"""
|
||||
if getattr(current_user, 'is_admin', False):
|
||||
return True
|
||||
role = current_user.role
|
||||
if role and role.name == 'admin':
|
||||
return True
|
||||
perms = (role.permissions if role else None) or {}
|
||||
return bool(perms.get('vm:manage', False))
|
||||
|
||||
|
||||
def check_vm_access(db: Session, current_user: User, server_id: int, vmid: int) -> bool:
|
||||
"""
|
||||
Check if user has access to a specific VM instance.
|
||||
|
||||
Admins and users with 'vms:view' permission can access all VMs.
|
||||
Users with 'user' role can only access VMs where they are the owner.
|
||||
Admins (and roles granting ``vm:manage``) can access all VMs. Everyone else
|
||||
can only access VMs where they are the owner.
|
||||
|
||||
Returns True if access is allowed, False otherwise.
|
||||
"""
|
||||
# Admin role has full access
|
||||
if current_user.role and current_user.role.name == 'admin':
|
||||
if can_view_all_instances(current_user):
|
||||
return True
|
||||
|
||||
# Check permissions
|
||||
if current_user.role and current_user.role.permissions:
|
||||
perms = current_user.role.permissions
|
||||
# If user has full vms:view, they can access all VMs
|
||||
has_full_view = perms.get('vm:view', False) or perms.get('vms:view', False)
|
||||
has_own_only = perms.get('vms:view:own', False) or perms.get('vms.view.own', False)
|
||||
instance = db.query(VMInstance).filter(
|
||||
VMInstance.server_id == server_id,
|
||||
VMInstance.vmid == vmid,
|
||||
VMInstance.deleted_at.is_(None)
|
||||
).first()
|
||||
|
||||
if has_full_view and not has_own_only:
|
||||
return True
|
||||
|
||||
# For limited users, check ownership
|
||||
if current_user.role and current_user.role.name == 'user':
|
||||
instance = db.query(VMInstance).filter(
|
||||
VMInstance.server_id == server_id,
|
||||
VMInstance.vmid == vmid,
|
||||
VMInstance.deleted_at.is_(None)
|
||||
).first()
|
||||
|
||||
if instance and instance.owner_id == current_user.id:
|
||||
return True
|
||||
return False
|
||||
|
||||
# Default: allow access (for backwards compatibility)
|
||||
return True
|
||||
return bool(instance and instance.owner_id == current_user.id)
|
||||
|
||||
|
||||
def require_vm_access(db: Session, current_user: User, server_id: int, vmid: int):
|
||||
|
||||
@@ -448,6 +448,10 @@ async def deploy_lxc_from_template(
|
||||
raise HTTPException(status_code=404, detail="Owner user not found")
|
||||
instance_owner_id = owner.id
|
||||
|
||||
# Enforce per-user resource quota (no-op if owner has no quota set)
|
||||
from ...services.quota_service import check_quota
|
||||
check_quota(db, instance_owner_id, add_cores=req.cores, add_memory_mb=req.memory, add_disk_gb=req.disk)
|
||||
|
||||
if req.ssh_key_ids:
|
||||
from ..ssh_keys import resolve_ssh_keys_for_deploy
|
||||
resolved = resolve_ssh_keys_for_deploy(db, current_user, req.ssh_key_ids, instance_owner_id)
|
||||
|
||||
@@ -20,7 +20,8 @@ from ...logging_service import LoggingService
|
||||
from ...ipam_service import IPAMService
|
||||
from ._helpers import (check_vm_access, require_vm_access, _get_proxmox_client,
|
||||
get_next_vmid, archive_and_delete_snapshots,
|
||||
save_vm_instance, get_vm_instance, soft_delete_vm_instance)
|
||||
save_vm_instance, get_vm_instance, soft_delete_vm_instance,
|
||||
can_view_all_instances)
|
||||
from ...services.metrics_history import query_instance_metrics, timeframe_to_range
|
||||
|
||||
router = APIRouter()
|
||||
@@ -146,21 +147,11 @@ def get_all_virtual_machines(
|
||||
if workspace_server_ids is not None:
|
||||
query = query.filter(VMInstance.server_id.in_(workspace_server_ids))
|
||||
|
||||
# VPS-style user isolation: users with 'user' role see only their own instances
|
||||
# Check if user has 'vm:view' but not 'vms.view:all' (or is user role)
|
||||
user_role = current_user.role.name if current_user.role else None
|
||||
is_limited_user = user_role == 'user'
|
||||
|
||||
# Also check permission-based isolation
|
||||
if hasattr(current_user, 'role') and current_user.role:
|
||||
perms = current_user.role.permissions or {}
|
||||
# If user has vms:view:own but not vms:view (full), filter by owner
|
||||
has_view_own = perms.get('vms:view:own', False) or perms.get('vms.view.own', False)
|
||||
has_view_all = perms.get('vms:view', False) or perms.get('vm:view', False)
|
||||
|
||||
# Limited users can only see their own VMs
|
||||
if is_limited_user or (has_view_own and not has_view_all):
|
||||
query = query.filter(VMInstance.owner_id == current_user.id)
|
||||
# VPS-style user isolation: only admins (or roles granting vm:manage) see
|
||||
# every instance. Everyone else — including users with no role or a custom
|
||||
# role — is restricted to instances they own. See can_view_all_instances.
|
||||
if not can_view_all_instances(current_user):
|
||||
query = query.filter(VMInstance.owner_id == current_user.id)
|
||||
|
||||
cached_vms = query.order_by(VMInstance.name).all()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user