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:
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user