Files
proxmox-api-simulator/tests/unit/test_api_auth_boundary.py
T
2026-07-13 01:35:01 +03:00

107 lines
3.3 KiB
Python

"""HTTP-boundary API-token and contract permission tests."""
from typing import Any, cast
import pytest
from fastapi import FastAPI, Request
from app.api.errors import ApiError
from app.api.registry import _authenticate
from app.config import Settings
from app.contracts.model import Method, Permissions, Schema
from app.db.pool import AsyncpgDatabase
from app.security.auth import hash_secret
class FakePool:
def __init__(self, secret: str, token_privileges: list[str]) -> None:
self.secret_hash = hash_secret(secret, salt=b"boundary-token-v1")
self.token_privileges = token_privileges
async def fetchrow(self, _query: str, principal: str, token_id: str) -> dict[str, Any] | None:
if principal != "operator@pve" or token_id != "api":
return None
return {
"name": principal,
"secret_hash": self.secret_hash,
"privileges": self.token_privileges,
"privilege_separation": True,
}
async def fetch(self, _query: str, principal: str) -> list[dict[str, Any]]:
return [
{
"path": "/vms",
"propagate": True,
"privileges": ["VM.Audit", "VM.PowerMgmt"],
"principal": principal,
}
]
class FakeDatabase:
def __init__(self, pool: FakePool) -> None:
self.pool = pool
def token_request(secret: str, token_privileges: list[str]) -> Request:
app = FastAPI()
app.state.settings = Settings()
app.state.database = cast(AsyncpgDatabase, FakeDatabase(FakePool("valid", token_privileges)))
return Request(
{
"type": "http",
"app": app,
"method": "POST",
"path": "/api2/json/nodes/pve1/qemu/101/status/start",
"headers": [(b"authorization", f"PVEAPIToken=operator@pve!api={secret}".encode())],
"query_string": b"",
"server": ("test", 80),
"client": ("test", 123),
"scheme": "http",
}
)
def power_method() -> Method:
return Method(
verb="POST",
name="start",
returns=Schema(type="string"),
permissions=Permissions(expression={"check": ["perm", "/vms/{vmid}", ["VM.PowerMgmt"]]}),
checksum="1" * 64,
)
async def test_api_token_skips_csrf_but_honors_separated_privileges() -> None:
allowed = token_request("valid", ["VM.PowerMgmt"])
await _authenticate(
allowed,
"/nodes/{node}/qemu/{vmid}/status/start",
power_method(),
{"values": {"node": "pve1", "vmid": 101}},
)
assert allowed.state.principal == "operator@pve"
denied = token_request("valid", ["VM.Audit"])
with pytest.raises(ApiError) as error:
await _authenticate(
denied,
"/nodes/{node}/qemu/{vmid}/status/start",
power_method(),
{"values": {"node": "pve1", "vmid": 101}},
)
assert error.value.status_code == 403
async def test_api_token_rejects_unknown_or_wrong_secret() -> None:
request = token_request("wrong", ["VM.PowerMgmt"])
with pytest.raises(ApiError) as error:
await _authenticate(
request,
"/nodes/{node}/qemu/{vmid}/status/start",
power_method(),
{"values": {"node": "pve1", "vmid": 101}},
)
assert error.value.status_code == 401