harden(gateway): block API key mint on inactive SA; cover cross-workspace IDOR (Stage 1 PR4)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1445043649
2026-06-28 19:25:17 +08:00
parent 950964e0ef
commit c3c4e57416
2 changed files with 38 additions and 3 deletions
+4 -3
View File
@@ -11,7 +11,7 @@ from __future__ import annotations
from datetime import datetime
from typing import Literal
from fastapi import APIRouter, Depends, HTTPException, Request, Response
from fastapi import APIRouter, Depends, HTTPException, Response
from pydantic import BaseModel, Field
from app.gateway.authz import require_workspace_admin
@@ -66,11 +66,12 @@ async def _require_sa_in_workspace(sa_id: str, sa_repo: ServiceAccountRepository
@router.post("", status_code=201, dependencies=[Depends(require_workspace_admin)])
async def create_api_key(
body: CreateApiKeyRequest,
request: Request,
key_repo: ApiKeyRepository = Depends(get_api_key_repo),
sa_repo: ServiceAccountRepository = Depends(get_service_account_repo),
):
await _require_sa_in_workspace(body.service_account_id, sa_repo)
sa = await _require_sa_in_workspace(body.service_account_id, sa_repo)
if sa["status"] != "active":
raise HTTPException(status_code=409, detail="service account is not active")
gen = generate_api_key(body.env)
created = await key_repo.create(
service_account_id=body.service_account_id,
+34
View File
@@ -123,3 +123,37 @@ async def test_member_cannot_create_key(tmp_path):
assert r.status_code == 403
finally:
await _cleanup()
async def test_revoke_other_workspace_key_404(tmp_path):
await _init_db_with_sa(tmp_path, sa_id="sa-1", workspace_id="w-1")
try:
client_a = TestClient(_make_app(workspace_id="w-1"))
created = client_a.post("/api/v1/api-keys", json={"service_account_id": "sa-1", "name": "k", "scopes": ""}).json()
client_b = TestClient(_make_app(workspace_id="w-2"))
assert client_b.delete(f"/api/v1/api-keys/{created['id']}").status_code == 404
finally:
await _cleanup()
async def test_list_other_workspace_sa_404(tmp_path):
await _init_db_with_sa(tmp_path, sa_id="sa-1", workspace_id="w-1")
try:
client_b = TestClient(_make_app(workspace_id="w-2"))
assert client_b.get("/api/v1/api-keys", params={"service_account_id": "sa-1"}).status_code == 404
finally:
await _cleanup()
async def test_create_for_suspended_sa_409(tmp_path):
await _init_db_with_sa(tmp_path)
try:
from deerflow.persistence.engine import get_session_factory
from deerflow.persistence.service_account import ServiceAccountRepository
await ServiceAccountRepository(get_session_factory()).update_status("sa-1", "suspended")
client = TestClient(_make_app())
r = client.post("/api/v1/api-keys", json={"service_account_id": "sa-1", "name": "x", "scopes": ""})
assert r.status_code == 409
finally:
await _cleanup()