84e06ca396
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
82 lines
2.9 KiB
Python
82 lines
2.9 KiB
Python
"""Service account management endpoints (Stage 1 PR4).
|
|
|
|
Owner/admin self-service: create / list / suspend service accounts in
|
|
the caller's current workspace. All operations are workspace-scoped;
|
|
cross-workspace targets return 404 (existence hidden).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Literal
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
from pydantic import BaseModel, Field
|
|
|
|
from app.gateway.authz import require_workspace_admin
|
|
from deerflow.persistence.service_account import ServiceAccountRepository
|
|
from deerflow.runtime.workspace_context import get_current_workspace
|
|
|
|
router = APIRouter(prefix="/api/v1/service-accounts", tags=["service-accounts"])
|
|
|
|
|
|
class CreateServiceAccountRequest(BaseModel):
|
|
name: str = Field(..., min_length=1, max_length=64)
|
|
# Constrained to Stage-1-supported values; widen these Literals in
|
|
# 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):
|
|
status: str = Field(..., pattern="^(active|suspended|deleted)$")
|
|
|
|
|
|
def get_service_account_repo() -> ServiceAccountRepository:
|
|
from deerflow.persistence.engine import get_session_factory
|
|
|
|
sf = get_session_factory()
|
|
if sf is None:
|
|
raise HTTPException(status_code=503, detail="persistence backend not available")
|
|
return ServiceAccountRepository(sf)
|
|
|
|
|
|
def _current_workspace_id() -> str:
|
|
ws = get_current_workspace()
|
|
if ws is None:
|
|
raise HTTPException(status_code=403, detail="no workspace in context")
|
|
return str(ws.id)
|
|
|
|
|
|
@router.post("", status_code=201, dependencies=[Depends(require_workspace_admin)])
|
|
async def create_service_account(
|
|
body: CreateServiceAccountRequest,
|
|
request: Request,
|
|
repo: ServiceAccountRepository = Depends(get_service_account_repo),
|
|
):
|
|
return await repo.create(
|
|
workspace_id=_current_workspace_id(),
|
|
name=body.name,
|
|
created_by=str(request.state.user.id),
|
|
role=body.role,
|
|
identity_mode=body.identity_mode,
|
|
)
|
|
|
|
|
|
@router.get("", dependencies=[Depends(require_workspace_admin)])
|
|
async def list_service_accounts(repo: ServiceAccountRepository = Depends(get_service_account_repo)):
|
|
return await repo.list_by_workspace(_current_workspace_id())
|
|
|
|
|
|
@router.patch("/{sa_id}", dependencies=[Depends(require_workspace_admin)])
|
|
async def update_service_account(
|
|
sa_id: str,
|
|
body: UpdateServiceAccountRequest,
|
|
repo: ServiceAccountRepository = Depends(get_service_account_repo),
|
|
):
|
|
sa = await repo.get(sa_id)
|
|
if sa is None or sa["workspace_id"] != _current_workspace_id():
|
|
raise HTTPException(status_code=404, detail="service account not found")
|
|
await repo.update_status(sa_id, body.status)
|
|
return await repo.get(sa_id)
|