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.
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user