Files
inecs f8d3cbdd59 Initial commit: VMware vSphere API simulator scaffold.
Add the FastAPI app, PostgreSQL migrations, Docker/Helm packaging, API
contracts, docs, client examples, and the unit/integration/compatibility
test suite for local client and tooling labs without a real vCenter.
2026-07-18 04:42:11 +03:00

180 lines
6.9 KiB
Python

"""Role / privilege authorization for vSphere REST (and SOAP gates)."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from functools import wraps
from typing import Any
from fastapi import Depends
from app.db.pool import Database
from app.dependencies import get_database
from app.vsphere.errors import unauthorized
from app.vsphere.security.session import SessionInfo, require_session
# Privilege catalog (subset of vSphere privilege ids).
PRIVILEGES: dict[str, str] = {
"System.Anonymous": "Anonymous access",
"System.Read": "Read inventory",
"System.View": "View inventory",
"Global.ManageCustomFields": "Manage custom fields",
"Authorization.ModifyPermissions": "Modify permissions",
"VirtualMachine.Inventory.Create": "Create VM",
"VirtualMachine.Inventory.Delete": "Delete VM",
"VirtualMachine.Inventory.Move": "Move VM",
"VirtualMachine.Interact.PowerOn": "Power on VM",
"VirtualMachine.Interact.PowerOff": "Power off VM",
"VirtualMachine.Interact.Suspend": "Suspend VM",
"VirtualMachine.Interact.Reset": "Reset VM",
"VirtualMachine.Interact.DeviceConnection": "Connect devices",
"VirtualMachine.Interact.ConsoleInteract": "Console",
"VirtualMachine.Config.AddNewDisk": "Add disk",
"VirtualMachine.Config.AddExistingDisk": "Add existing disk",
"VirtualMachine.Config.RemoveDisk": "Remove disk",
"VirtualMachine.Config.CPUCount": "Change CPU",
"VirtualMachine.Config.Memory": "Change memory",
"VirtualMachine.Config.AddRemoveDevice": "Add/remove device",
"VirtualMachine.Config.Rename": "Rename VM",
"VirtualMachine.Provisioning.Clone": "Clone VM",
"VirtualMachine.Provisioning.DeployTemplate": "Deploy template",
"VirtualMachine.Provisioning.MarkAsTemplate": "Mark as template",
"VirtualMachine.State.CreateSnapshot": "Create snapshot",
"VirtualMachine.State.RemoveSnapshot": "Remove snapshot",
"VirtualMachine.State.RevertToSnapshot": "Revert snapshot",
"Datastore.Browse": "Browse datastore",
"Datastore.FileManagement": "Manage datastore files",
"Host.Config.Maintenance": "Host maintenance",
"Folder.Create": "Create folder",
"Folder.Delete": "Delete folder",
"Folder.Rename": "Rename folder",
"Folder.Move": "Move folder",
"Datacenter.Create": "Create datacenter",
"Datacenter.Delete": "Delete datacenter",
"Cluster.Create": "Create cluster",
"Cluster.Delete": "Delete cluster",
"Resource.CreatePool": "Create resource pool",
"Resource.DeletePool": "Delete resource pool",
"Network.Assign": "Assign network",
"ContentLibrary.CreateLocalLibrary": "Create content library",
"ContentLibrary.AddLibraryItem": "Add library item",
"InventoryService.Tagging.CreateCategory": "Create tag category",
"InventoryService.Tagging.CreateTag": "Create tag",
"InventoryService.Tagging.AttachTag": "Attach tag",
}
_ALL = frozenset(PRIVILEGES)
_READ = frozenset({"System.Anonymous", "System.Read", "System.View", "Datastore.Browse"})
_POWER = frozenset(
{
*_READ,
"VirtualMachine.Interact.PowerOn",
"VirtualMachine.Interact.PowerOff",
"VirtualMachine.Interact.Suspend",
"VirtualMachine.Interact.Reset",
"VirtualMachine.Interact.ConsoleInteract",
"VirtualMachine.State.CreateSnapshot",
"VirtualMachine.State.RemoveSnapshot",
"VirtualMachine.State.RevertToSnapshot",
"VirtualMachine.Provisioning.Clone",
}
)
_VM_ADMIN = frozenset(
{
*_POWER,
"VirtualMachine.Inventory.Create",
"VirtualMachine.Inventory.Delete",
"VirtualMachine.Inventory.Move",
"VirtualMachine.Config.AddNewDisk",
"VirtualMachine.Config.AddExistingDisk",
"VirtualMachine.Config.RemoveDisk",
"VirtualMachine.Config.CPUCount",
"VirtualMachine.Config.Memory",
"VirtualMachine.Config.AddRemoveDevice",
"VirtualMachine.Config.Rename",
"VirtualMachine.Provisioning.DeployTemplate",
"VirtualMachine.Provisioning.MarkAsTemplate",
"VirtualMachine.Interact.DeviceConnection",
"Datastore.FileManagement",
"Network.Assign",
"InventoryService.Tagging.CreateCategory",
"InventoryService.Tagging.CreateTag",
"InventoryService.Tagging.AttachTag",
"ContentLibrary.CreateLocalLibrary",
"ContentLibrary.AddLibraryItem",
}
)
ROLE_PRIVILEGES: dict[str, frozenset[str]] = {
"Administrator": _ALL,
"ReadOnly": _READ,
"VirtualMachinePowerUser": _POWER,
"VirtualMachineAdministrator": _VM_ADMIN,
}
def privileges_for_roles(roles: list[str] | tuple[str, ...]) -> frozenset[str]:
granted: set[str] = set()
for role in roles:
granted.update(ROLE_PRIVILEGES.get(role, ()))
return frozenset(granted)
def has_privilege(roles: list[str] | tuple[str, ...], privilege: str) -> bool:
granted = privileges_for_roles(roles)
if privilege in granted:
return True
# Wildcard Administrator already has exact set; keep prefix convenience.
return any(p.endswith(".*") and privilege.startswith(p[:-1]) for p in granted)
async def load_roles(database: Database, username: str) -> list[str]:
pool = database.pool # type: ignore[attr-defined]
async with pool.acquire() as conn:
row = await conn.fetchrow(
"SELECT roles FROM vsphere_credentials WHERE username = $1",
username,
)
if row is None:
return ["Administrator"] if username.endswith("@vsphere.local") else ["ReadOnly"]
roles = list(row["roles"] or [])
return roles or ["ReadOnly"]
def require_privilege(*needed: str) -> Callable[..., Any]:
"""FastAPI dependency factory: session must hold every listed privilege."""
async def _dependency(
session: SessionInfo = Depends(require_session),
database: Database = Depends(get_database),
) -> SessionInfo:
roles = list(session.roles)
if not roles:
roles = await load_roles(database, session.username)
for privilege in needed:
if not has_privilege(roles, privilege):
raise unauthorized(f"Missing privilege: {privilege}")
return session
return _dependency
require_read = require_privilege("System.Read")
require_power = require_privilege("VirtualMachine.Interact.PowerOn")
require_vm_mutate = require_privilege("VirtualMachine.Inventory.Create")
require_admin = require_privilege("Authorization.ModifyPermissions")
def guard(*needed: str) -> Callable[[Callable[..., Awaitable[Any]]], Callable[..., Awaitable[Any]]]:
"""Decorator-style helper for non-FastAPI call sites (SOAP)."""
def decorator(fn: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[Any]]:
@wraps(fn)
async def wrapper(*args: Any, **kwargs: Any) -> Any:
return await fn(*args, **kwargs)
wrapper.__vsphere_privileges__ = needed # type: ignore[attr-defined]
return wrapper
return decorator