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,247 @@
|
||||
"""API-token lifecycle handler tests without external services."""
|
||||
|
||||
import json
|
||||
from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI, Request
|
||||
|
||||
from app.api.errors import ApiError
|
||||
from app.api.registry import HandlerRegistry
|
||||
from app.db.pool import AsyncpgDatabase
|
||||
from app.handlers.access import register_access_handlers
|
||||
|
||||
|
||||
class TokenPool:
|
||||
def __init__(self) -> None:
|
||||
self.token: dict[str, Any] | None = None
|
||||
|
||||
async def fetch(self, _query: str, _userid: str) -> list[dict[str, Any]]:
|
||||
return [] if self.token is None else [{"token_id": "test", **self.token}]
|
||||
|
||||
async def fetchrow(self, query: str, *arguments: object) -> dict[str, Any] | None:
|
||||
if "INSERT INTO" in query:
|
||||
self.token = {
|
||||
"comment": arguments[3],
|
||||
"privilege_separation": arguments[5],
|
||||
"expire": arguments[4],
|
||||
}
|
||||
return self.token
|
||||
if "UPDATE api_tokens" in query:
|
||||
if self.token is None:
|
||||
return None
|
||||
self.token["comment"] = arguments[2]
|
||||
self.token["privilege_separation"] = arguments[4]
|
||||
return self.token
|
||||
return self.token
|
||||
|
||||
async def fetchval(self, _query: str, _userid: str) -> bool:
|
||||
return True
|
||||
|
||||
async def execute(self, _query: str, _userid: str, _tokenid: str) -> str:
|
||||
if self.token is None:
|
||||
return "DELETE 0"
|
||||
self.token = None
|
||||
return "DELETE 1"
|
||||
|
||||
|
||||
class RealmPool:
|
||||
def __init__(self) -> None:
|
||||
self.realms: dict[str, dict[str, Any]] = {
|
||||
"pam": {
|
||||
"kind": "pam",
|
||||
"config": {"comment": "Linux PAM standard authentication"},
|
||||
},
|
||||
"pve": {
|
||||
"kind": "pve",
|
||||
"config": {"comment": "Proxmox VE authentication server"},
|
||||
},
|
||||
}
|
||||
self.principals: dict[str, str] = {"root@pam": "pam"}
|
||||
|
||||
async def fetch(self, query: str, *arguments: object) -> list[dict[str, Any]]:
|
||||
del arguments
|
||||
if "FROM realms ORDER BY name" in query:
|
||||
return [
|
||||
{"name": name, "kind": data["kind"], "config": dict(data["config"])}
|
||||
for name, data in sorted(self.realms.items())
|
||||
]
|
||||
raise AssertionError(query)
|
||||
|
||||
async def fetchrow(self, query: str, *arguments: object) -> dict[str, Any] | None:
|
||||
if "FROM realms WHERE name" in query:
|
||||
realm = str(arguments[0])
|
||||
data = self.realms.get(realm)
|
||||
if data is None:
|
||||
return None
|
||||
return {"name": realm, "kind": data["kind"], "config": dict(data["config"])}
|
||||
raise AssertionError(query)
|
||||
|
||||
async def fetchval(self, query: str, *arguments: object) -> bool:
|
||||
realm = str(arguments[0])
|
||||
if "EXISTS(SELECT 1 FROM realms" in query:
|
||||
return realm in self.realms
|
||||
if "EXISTS(SELECT 1 FROM principals" in query:
|
||||
return any(value == realm for value in self.principals.values())
|
||||
raise AssertionError(query)
|
||||
|
||||
async def execute(self, query: str, *arguments: object) -> str:
|
||||
if "INSERT INTO realms" in query:
|
||||
self.realms[str(arguments[0])] = {
|
||||
"kind": str(arguments[1]),
|
||||
"config": json.loads(str(arguments[2])),
|
||||
}
|
||||
return "INSERT 0 1"
|
||||
if "UPDATE realms SET config=$2" in query:
|
||||
realm = str(arguments[0])
|
||||
self.realms[realm]["config"] = json.loads(str(arguments[1]))
|
||||
return "UPDATE 1"
|
||||
if "SET config = config - 'default'" in query:
|
||||
skip = str(arguments[0]) if arguments else None
|
||||
for name, data in self.realms.items():
|
||||
if skip is not None and name == skip:
|
||||
continue
|
||||
data["config"].pop("default", None)
|
||||
return "UPDATE 0"
|
||||
if "DELETE FROM realms" in query:
|
||||
realm = str(arguments[0])
|
||||
if realm not in self.realms:
|
||||
return "DELETE 0"
|
||||
del self.realms[realm]
|
||||
return "DELETE 1"
|
||||
raise AssertionError(query)
|
||||
|
||||
|
||||
class FakeDatabase:
|
||||
def __init__(self, pool: TokenPool | RealmPool) -> None:
|
||||
self.pool = pool
|
||||
|
||||
|
||||
def request(pool: TokenPool | RealmPool, principal: str = "root@pam") -> Request:
|
||||
app = FastAPI()
|
||||
app.state.database = cast(AsyncpgDatabase, FakeDatabase(pool))
|
||||
result = Request(
|
||||
{
|
||||
"type": "http",
|
||||
"app": app,
|
||||
"method": "POST",
|
||||
"path": "/",
|
||||
"headers": [],
|
||||
"query_string": b"",
|
||||
"server": ("test", 80),
|
||||
"client": ("test", 123),
|
||||
"scheme": "http",
|
||||
}
|
||||
)
|
||||
result.state.principal = principal
|
||||
return result
|
||||
|
||||
|
||||
def values(**items: object) -> dict[str, Any]:
|
||||
return {"values": items, "provided": frozenset(items)}
|
||||
|
||||
|
||||
async def test_token_lifecycle_returns_secret_once_and_persists_metadata() -> None:
|
||||
registry = HandlerRegistry()
|
||||
register_access_handlers(registry)
|
||||
pool = TokenPool()
|
||||
http_request = request(pool)
|
||||
create = registry.get("/access/users/{userid}/token/{tokenid}", "POST")
|
||||
get = registry.get("/access/users/{userid}/token/{tokenid}", "GET")
|
||||
update = registry.get("/access/users/{userid}/token/{tokenid}", "PUT")
|
||||
delete = registry.get("/access/users/{userid}/token/{tokenid}", "DELETE")
|
||||
list_tokens = registry.get("/access/users/{userid}/token", "GET")
|
||||
assert create and get and update and delete and list_tokens
|
||||
|
||||
created = await create(
|
||||
http_request,
|
||||
values(userid="root@pam", tokenid="test", comment="first", privsep=True),
|
||||
)
|
||||
assert created["full-tokenid"] == "root@pam!test"
|
||||
assert created["value"]
|
||||
assert "value" not in await get(http_request, values(userid="root@pam", tokenid="test"))
|
||||
assert await list_tokens(http_request, values(userid="root@pam"))
|
||||
|
||||
updated = await update(
|
||||
http_request,
|
||||
values(userid="root@pam", tokenid="test", comment="second", privsep=False),
|
||||
)
|
||||
assert updated["comment"] == "second"
|
||||
await delete(http_request, values(userid="root@pam", tokenid="test"))
|
||||
with pytest.raises(ApiError) as missing:
|
||||
await get(http_request, values(userid="root@pam", tokenid="test"))
|
||||
assert missing.value.status_code == 404
|
||||
|
||||
|
||||
async def test_token_lifecycle_rejects_non_owner() -> None:
|
||||
registry = HandlerRegistry()
|
||||
register_access_handlers(registry)
|
||||
handler = registry.get("/access/users/{userid}/token", "GET")
|
||||
assert handler
|
||||
with pytest.raises(ApiError) as denied:
|
||||
await handler(request(TokenPool(), "auditor@pve"), values(userid="other@pve"))
|
||||
assert denied.value.status_code == 403
|
||||
|
||||
|
||||
async def test_domain_lifecycle_persists_realm_config() -> None:
|
||||
registry = HandlerRegistry()
|
||||
register_access_handlers(registry)
|
||||
pool = RealmPool()
|
||||
http_request = request(pool)
|
||||
create = registry.get("/access/domains", "POST")
|
||||
listing = registry.get("/access/domains", "GET")
|
||||
get = registry.get("/access/domains/{realm}", "GET")
|
||||
update = registry.get("/access/domains/{realm}", "PUT")
|
||||
delete = registry.get("/access/domains/{realm}", "DELETE")
|
||||
assert create and listing and get and update and delete
|
||||
|
||||
await create(
|
||||
http_request,
|
||||
values(
|
||||
realm="corp",
|
||||
type="ldap",
|
||||
comment="Corporate LDAP",
|
||||
server1="ldap.example.com",
|
||||
password="secret",
|
||||
default=1,
|
||||
),
|
||||
)
|
||||
listed = await listing(http_request, values())
|
||||
assert any(item["realm"] == "corp" and item["type"] == "ldap" for item in listed)
|
||||
created = await get(http_request, values(realm="corp"))
|
||||
assert created["comment"] == "Corporate LDAP"
|
||||
assert created["server1"] == "ldap.example.com"
|
||||
assert created["default"] == 1
|
||||
assert "password" not in created
|
||||
|
||||
await update(
|
||||
http_request,
|
||||
values(realm="corp", comment="Updated LDAP", delete="default"),
|
||||
)
|
||||
updated = await get(http_request, values(realm="corp"))
|
||||
assert updated["comment"] == "Updated LDAP"
|
||||
assert "default" not in updated
|
||||
|
||||
await delete(http_request, values(realm="corp"))
|
||||
with pytest.raises(ApiError) as missing:
|
||||
await get(http_request, values(realm="corp"))
|
||||
assert missing.value.status_code == 404
|
||||
|
||||
|
||||
async def test_domain_delete_rejects_builtin_and_in_use_realms() -> None:
|
||||
registry = HandlerRegistry()
|
||||
register_access_handlers(registry)
|
||||
pool = RealmPool()
|
||||
http_request = request(pool)
|
||||
delete = registry.get("/access/domains/{realm}", "DELETE")
|
||||
assert delete
|
||||
|
||||
with pytest.raises(ApiError) as builtin:
|
||||
await delete(http_request, values(realm="pam"))
|
||||
assert builtin.value.status_code == 400
|
||||
|
||||
pool.realms["corp"] = {"kind": "ldap", "config": {}}
|
||||
pool.principals["alice@corp"] = "corp"
|
||||
with pytest.raises(ApiError) as in_use:
|
||||
await delete(http_request, values(realm="corp"))
|
||||
assert in_use.value.status_code == 400
|
||||
Reference in New Issue
Block a user