admin.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. """api.v1.admin — Tenant management (admin only).
  2. SEC-01: All endpoints require authentication + admin scope.
  3. """
  4. from __future__ import annotations
  5. import json
  6. import secrets
  7. from fastapi import APIRouter, Depends, HTTPException
  8. from pydantic import BaseModel, Field
  9. from agentpaas.api.deps import get_database, get_tenant
  10. from agentpaas.api.middleware.auth import hash_key, TenantContext
  11. from agentpaas.db.models import Database, gen_id, now_utc
  12. router = APIRouter(prefix="/admin", tags=["admin"])
  13. class CreateTenantRequest(BaseModel):
  14. name: str
  15. plan: str = "free"
  16. admin_email: str = ""
  17. class SetQuotaRequest(BaseModel):
  18. tokens_monthly: int = 1000000
  19. concurrency: int = 5
  20. agents: int = 20
  21. def _require_admin(tenant: TenantContext):
  22. """SEC-01: Verify caller has admin scope."""
  23. if not any(s in ("admin:*", "admin:create_tenant") for s in tenant.scopes):
  24. raise HTTPException(403, {"error": {"code": "FORBIDDEN", "message": "Admin scope required"}})
  25. @router.post("/tenants", status_code=201)
  26. async def create_tenant(
  27. req: CreateTenantRequest,
  28. tenant: TenantContext = Depends(get_tenant),
  29. db: Database = Depends(get_database),
  30. ):
  31. """Create tenant + admin user + initial API key. Returns the API key."""
  32. _require_admin(tenant)
  33. tenant_id = gen_id("tn_")
  34. user_id = gen_id("usr_")
  35. now = now_utc()
  36. db.execute("INSERT INTO tenants (id, name, plan, status, created_at) VALUES (?, ?, ?, 'active', ?)",
  37. (tenant_id, req.name, req.plan, now))
  38. db.execute("INSERT INTO users (id, tenant_id, email, role, created_at) VALUES (?, ?, ?, 'admin', ?)",
  39. (user_id, tenant_id, req.admin_email, now))
  40. raw_key = f"ap_{secrets.token_hex(16)}"
  41. key_id = gen_id("key_")
  42. db.execute(
  43. "INSERT INTO api_keys (id, tenant_id, user_id, key_hash, key_prefix, name, scopes, rate_limit, status, created_at) "
  44. "VALUES (?, ?, ?, ?, ?, 'admin-key', ?, 600, 'active', ?)",
  45. (key_id, tenant_id, user_id, hash_key(raw_key), raw_key[:8],
  46. json.dumps(["agents:*", "keys:*", "billing:read", "admin:*"]), now)
  47. )
  48. db.commit()
  49. return {"tenant_id": tenant_id, "user_id": user_id, "api_key": raw_key,
  50. "warning": "Store this API key securely."}
  51. @router.get("/tenants/{tenant_id}")
  52. async def get_tenant_info(
  53. tenant_id: str,
  54. tenant: TenantContext = Depends(get_tenant),
  55. db: Database = Depends(get_database),
  56. ):
  57. _require_admin(tenant)
  58. t = db.fetchone("SELECT * FROM tenants WHERE id = ?", (tenant_id,))
  59. if not t:
  60. raise HTTPException(404, {"error": {"code": "TENANT_NOT_FOUND"}})
  61. return t
  62. @router.put("/tenants/{tenant_id}/quota")
  63. async def set_quota(
  64. tenant_id: str,
  65. req: SetQuotaRequest,
  66. tenant: TenantContext = Depends(get_tenant),
  67. db: Database = Depends(get_database),
  68. ):
  69. _require_admin(tenant)
  70. db.execute("UPDATE tenants SET quota_tokens_monthly=?, quota_concurrency=?, quota_agents=? WHERE id=?",
  71. (req.tokens_monthly, req.concurrency, req.agents, tenant_id))
  72. db.commit()
  73. return {"message": "Quota updated"}
  74. @router.get("/tenants/{tenant_id}/usage")
  75. async def get_usage(
  76. tenant_id: str,
  77. tenant: TenantContext = Depends(get_tenant),
  78. db: Database = Depends(get_database),
  79. ):
  80. _require_admin(tenant)
  81. usage = db.fetchone(
  82. "SELECT COUNT(*) as runs, COALESCE(SUM(input_tokens),0) as input_tokens, "
  83. "COALESCE(SUM(output_tokens),0) as output_tokens, COALESCE(SUM(duration_ms),0) as total_duration_ms "
  84. "FROM runs WHERE tenant_id = ?", (tenant_id,))
  85. return usage