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