harden(gateway): constrain service-account role/identity_mode at API boundary (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:15:40 +08:00
parent d16e294185
commit 84e06ca396
2 changed files with 30 additions and 2 deletions
@@ -7,6 +7,8 @@ cross-workspace targets return 404 (existence hidden).
from __future__ import annotations from __future__ import annotations
from typing import Literal
from fastapi import APIRouter, Depends, HTTPException, Request from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@@ -19,8 +21,11 @@ router = APIRouter(prefix="/api/v1/service-accounts", tags=["service-accounts"])
class CreateServiceAccountRequest(BaseModel): class CreateServiceAccountRequest(BaseModel):
name: str = Field(..., min_length=1, max_length=64) name: str = Field(..., min_length=1, max_length=64)
role: str = Field(default="member") # Constrained to Stage-1-supported values; widen these Literals in
identity_mode: str = Field(default="collapsed") # future stages alongside the behavior (Stage 2 RBAC opens role
# admin/viewer; the passthrough PR opens identity_mode).
role: Literal["member"] = "member"
identity_mode: Literal["collapsed"] = "collapsed"
class UpdateServiceAccountRequest(BaseModel): class UpdateServiceAccountRequest(BaseModel):
@@ -121,3 +121,26 @@ async def test_patch_other_workspace_sa_404(tmp_path):
assert r.status_code == 404 assert r.status_code == 404
finally: finally:
await _cleanup() await _cleanup()
async def test_list_is_scoped_to_current_workspace(tmp_path):
await _init_db(tmp_path)
try:
w1 = TestClient(_make_app(role="owner", workspace_id="w-1"))
w1.post("/api/v1/service-accounts", json={"name": "bot-w1"})
# A caller in w-2 must NOT see w-1's service accounts.
w2 = TestClient(_make_app(role="owner", workspace_id="w-2"))
rows = w2.get("/api/v1/service-accounts").json()
assert rows == []
finally:
await _cleanup()
async def test_create_rejects_unknown_role(tmp_path):
await _init_db(tmp_path)
try:
client = TestClient(_make_app(role="owner"))
r = client.post("/api/v1/service-accounts", json={"name": "x", "role": "superadmin"})
assert r.status_code == 422
finally:
await _cleanup()