| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596 |
- """api.v1.admin — Tenant management (admin only).
- SEC-01: All endpoints require authentication + admin scope.
- """
- from __future__ import annotations
- import json
- import secrets
- from fastapi import APIRouter, Depends, HTTPException
- from pydantic import BaseModel, Field
- from agentpaas.api.deps import get_database, get_tenant
- from agentpaas.api.middleware.auth import hash_key, TenantContext
- from agentpaas.db.models import Database, gen_id, now_utc
- router = APIRouter(prefix="/admin", tags=["admin"])
- class CreateTenantRequest(BaseModel):
- name: str
- plan: str = "free"
- admin_email: str = ""
- class SetQuotaRequest(BaseModel):
- tokens_monthly: int = 1000000
- concurrency: int = 5
- agents: int = 20
- def _require_admin(tenant: TenantContext):
- """SEC-01: Verify caller has admin scope."""
- if not any(s in ("admin:*", "admin:create_tenant") for s in tenant.scopes):
- raise HTTPException(403, {"error": {"code": "FORBIDDEN", "message": "Admin scope required"}})
- @router.post("/tenants", status_code=201)
- async def create_tenant(
- req: CreateTenantRequest,
- tenant: TenantContext = Depends(get_tenant),
- db: Database = Depends(get_database),
- ):
- """Create tenant + admin user + initial API key. Returns the API key."""
- _require_admin(tenant)
- tenant_id = gen_id("tn_")
- user_id = gen_id("usr_")
- now = now_utc()
- db.execute("INSERT INTO tenants (id, name, plan, status, created_at) VALUES (?, ?, ?, 'active', ?)",
- (tenant_id, req.name, req.plan, now))
- db.execute("INSERT INTO users (id, tenant_id, email, role, created_at) VALUES (?, ?, ?, 'admin', ?)",
- (user_id, tenant_id, req.admin_email, now))
- raw_key = f"ap_{secrets.token_hex(16)}"
- key_id = gen_id("key_")
- db.execute(
- "INSERT INTO api_keys (id, tenant_id, user_id, key_hash, key_prefix, name, scopes, rate_limit, status, created_at) "
- "VALUES (?, ?, ?, ?, ?, 'admin-key', ?, 600, 'active', ?)",
- (key_id, tenant_id, user_id, hash_key(raw_key), raw_key[:8],
- json.dumps(["agents:*", "keys:*", "billing:read", "admin:*"]), now)
- )
- db.commit()
- return {"tenant_id": tenant_id, "user_id": user_id, "api_key": raw_key,
- "warning": "Store this API key securely."}
- @router.get("/tenants/{tenant_id}")
- async def get_tenant_info(
- tenant_id: str,
- tenant: TenantContext = Depends(get_tenant),
- db: Database = Depends(get_database),
- ):
- _require_admin(tenant)
- t = db.fetchone("SELECT * FROM tenants WHERE id = ?", (tenant_id,))
- if not t:
- raise HTTPException(404, {"error": {"code": "TENANT_NOT_FOUND"}})
- return t
- @router.put("/tenants/{tenant_id}/quota")
- async def set_quota(
- tenant_id: str,
- req: SetQuotaRequest,
- tenant: TenantContext = Depends(get_tenant),
- db: Database = Depends(get_database),
- ):
- _require_admin(tenant)
- db.execute("UPDATE tenants SET quota_tokens_monthly=?, quota_concurrency=?, quota_agents=? WHERE id=?",
- (req.tokens_monthly, req.concurrency, req.agents, tenant_id))
- db.commit()
- return {"message": "Quota updated"}
- @router.get("/tenants/{tenant_id}/usage")
- async def get_usage(
- tenant_id: str,
- tenant: TenantContext = Depends(get_tenant),
- db: Database = Depends(get_database),
- ):
- _require_admin(tenant)
- usage = db.fetchone(
- "SELECT COUNT(*) as runs, COALESCE(SUM(input_tokens),0) as input_tokens, "
- "COALESCE(SUM(output_tokens),0) as output_tokens, COALESCE(SUM(duration_ms),0) as total_duration_ms "
- "FROM runs WHERE tenant_id = ?", (tenant_id,))
- return usage
|