feat: v1.1.8 — user-server assignment, VM/LXC ownership, smart login redirect, RBAC migrations

- Migration 20: user_servers association table (User ↔ ProxmoxServer)
- API: GET/PUT /api/users/{id}/servers — assign servers to users with workspace-conflict validation
- API: GET /api/users/{id}/server-assignments — enriched server list (assigned + compatible flags)
- Non-admin server filter: intersection of assigned_servers and workspace servers
- Migration 21: dashboard:view = false for user role (new key format)
- Migration 22: proxmox.cluster.manage → cluster:manage for admin role
- API: GET/PUT /api/{server_id}/vm/{vmid}/owner — VM/LXC owner assignment (admin-only)
- Login: smart redirect based on user permissions after authentication
- Login: returnUrl param support for post-login redirect
- Roles API: user_count field per role in GET /api/roles
- node_uptime added to server detail API response
- Remove redundant PermissionChecker from GET /users HTML route
This commit is contained in:
markmorado
2026-03-16 08:27:36 +01:00
parent c09c0c5b63
commit 57575d58ce
22 changed files with 1436 additions and 314 deletions

View File

@@ -92,12 +92,50 @@ def list_proxmox_servers(
):
"""Получить список всех Proxmox серверов (фильтруется по активному workspace)"""
from ...api.workspaces import get_workspace_server_ids
server_ids = get_workspace_server_ids(request, db, current_user)
query = db.query(ProxmoxServer)
if server_ids is not None:
query = query.filter(ProxmoxServer.id.in_(server_ids))
servers = query.all()
return servers
from ...models import WorkspaceUser, WorkspaceServer
is_privileged = current_user.is_admin or (
current_user.role and current_user.role.name in ('admin', 'moderator')
)
if is_privileged:
# Privileged: use workspace filter only
server_ids = get_workspace_server_ids(request, db, current_user)
query = db.query(ProxmoxServer)
if server_ids is not None:
query = query.filter(ProxmoxServer.id.in_(server_ids))
return query.all()
# Non-privileged user: must be directly assigned AND server must be in one of their workspaces
assigned_ids = {s.id for s in current_user.assigned_servers}
# Collect server IDs accessible through user's workspaces
user_ws_ids = {
r.workspace_id for r in
db.query(WorkspaceUser.workspace_id).filter(WorkspaceUser.user_id == current_user.id).all()
}
if user_ws_ids:
ws_server_ids = {
r.server_id for r in
db.query(WorkspaceServer.server_id).filter(
WorkspaceServer.workspace_id.in_(user_ws_ids)
).all()
}
else:
ws_server_ids = set()
# Intersect: directly assigned AND within user's workspaces
effective_ids = assigned_ids & ws_server_ids
# Also apply active-workspace header filter on top
ws_filter = get_workspace_server_ids(request, db, current_user)
if ws_filter is not None:
effective_ids = effective_ids & set(ws_filter)
if not effective_ids:
return []
return db.query(ProxmoxServer).filter(ProxmoxServer.id.in_(effective_ids)).all()
@router.post("/api/servers", response_model=ProxmoxServerResponse, status_code=status.HTTP_201_CREATED)
@@ -601,6 +639,22 @@ def get_server_resources(
vms = [vm for vm in resources.get('vms', []) if not vm.get('template', 0)]
containers = [ct for ct in resources.get('containers', []) if not ct.get('template', 0)]
# Получаем uptime ноды
node_uptime = None
try:
client = _get_proxmox_client(server)
nodes = client.get_nodes()
if nodes:
target_node = node_filter or (nodes[0].get('node') if nodes else None)
for n in nodes:
if target_node and n.get('node') == target_node:
node_uptime = n.get('uptime')
break
if node_uptime is None and nodes:
node_uptime = nodes[0].get('uptime')
except Exception:
pass
server.update_status(True)
db.commit()
@@ -608,7 +662,8 @@ def get_server_resources(
"server_id": server_id,
"server_name": server.name,
"vms": vms,
"containers": containers
"containers": containers,
"node_uptime": node_uptime
})
except Exception as e:
server.update_status(False, str(e))

View File

@@ -45,6 +45,10 @@ class BulkOperationRequest(BaseModel):
items: TypingList[BulkOperationItem]
class VMOwnerUpdate(BaseModel):
user_id: Optional[int] = None
@router.post("/api/sync-vms")
def sync_vms_now(
db: Session = Depends(get_db),
@@ -2793,3 +2797,73 @@ def create_bulk_operation(
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
# ===================== VM Owner Management =====================
@router.get("/api/{server_id}/vm/{vmid}/owner")
def get_vm_owner(
server_id: int,
vmid: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get current owner of a VM/LXC. Admin only."""
if not current_user.is_admin:
raise HTTPException(status_code=403, detail="Admin required")
instance = db.query(VMInstance).filter(
VMInstance.server_id == server_id,
VMInstance.vmid == vmid,
VMInstance.deleted_at.is_(None)
).first()
owner = None
if instance and instance.owner_id:
owner_user = db.query(User).filter(User.id == instance.owner_id).first()
if owner_user:
owner = {"id": owner_user.id, "username": owner_user.username, "full_name": owner_user.full_name}
users = (
db.query(User)
.filter(User.is_active == True, User.is_admin == False)
.order_by(User.username)
.all()
)
return {
"owner_id": instance.owner_id if instance else None,
"owner": owner,
"users": [{"id": u.id, "username": u.username, "full_name": u.full_name} for u in users]
}
@router.put("/api/{server_id}/vm/{vmid}/owner")
def set_vm_owner(
server_id: int,
vmid: int,
body: VMOwnerUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Set (or clear) the owner of a VM/LXC. Admin only."""
if not current_user.is_admin:
raise HTTPException(status_code=403, detail="Admin required")
instance = db.query(VMInstance).filter(
VMInstance.server_id == server_id,
VMInstance.vmid == vmid,
VMInstance.deleted_at.is_(None)
).first()
if not instance:
raise HTTPException(status_code=404, detail="VM not found in cache")
if body.user_id is not None:
target_user = db.query(User).filter(User.id == body.user_id, User.is_active == True).first()
if not target_user:
raise HTTPException(status_code=404, detail="User not found")
instance.owner_id = body.user_id
db.commit()
logger.info(f"Admin {current_user.username} set owner of VM {vmid} (server {server_id}) to user_id={body.user_id}")
return {"ok": True, "owner_id": body.user_id}