fix(v2): scope and redact execution events

This commit is contained in:
2026-07-27 22:26:40 +02:00
parent c97dd8cfb2
commit c729f5db80
3 changed files with 32 additions and 9 deletions
+14 -5
View File
@@ -244,6 +244,10 @@ def create_app(settings: Settings) -> FastAPI:
raise Problem(403, "csrf_failed", "CSRF validation failed.")
return user, scopes, cookie_auth
def enforce_scope(scopes: set[str], required: str) -> None:
if "*" not in scopes and required not in scopes:
raise Problem(403, "insufficient_scope", "Required scope is missing.")
async def audit(
db: AsyncSession,
request: Request,
@@ -767,8 +771,9 @@ def create_app(settings: Settings) -> FastAPI:
async def enqueue_execution(
job_id: str,
db: Annotated[AsyncSession, Depends(session)],
_: Annotated[tuple[User, set[str], bool], Depends(require)],
identity: Annotated[tuple[User, set[str], bool], Depends(require)],
) -> dict[str, Any]:
enforce_scope(identity[1], "execution:control")
try:
execution = await enqueue(db, job_id)
except EnqueueError as error:
@@ -793,8 +798,9 @@ def create_app(settings: Settings) -> FastAPI:
async def get_execution(
execution_id: str,
db: Annotated[AsyncSession, Depends(session)],
_: Annotated[tuple[User, set[str], bool], Depends(actor)],
identity: Annotated[tuple[User, set[str], bool], Depends(actor)],
) -> dict[str, object]:
enforce_scope(identity[1], "execution:read")
execution = await db.get(Execution, execution_id)
if execution is None:
raise Problem(404, "resource_not_found", "Execution was not found.")
@@ -804,8 +810,9 @@ def create_app(settings: Settings) -> FastAPI:
async def cancel_execution(
execution_id: str,
db: Annotated[AsyncSession, Depends(session)],
_: Annotated[tuple[User, set[str], bool], Depends(require)],
identity: Annotated[tuple[User, set[str], bool], Depends(require)],
) -> dict[str, object]:
enforce_scope(identity[1], "execution:control")
execution = await request_cancellation(db, execution_id)
if execution is None:
raise Problem(409, "cancellation_not_allowed", "Execution cannot be cancelled.")
@@ -815,8 +822,9 @@ def create_app(settings: Settings) -> FastAPI:
async def retry_execution(
execution_id: str,
db: Annotated[AsyncSession, Depends(session)],
_: Annotated[tuple[User, set[str], bool], Depends(require)],
identity: Annotated[tuple[User, set[str], bool], Depends(require)],
) -> dict[str, object]:
enforce_scope(identity[1], "execution:control")
try:
execution = await retry(db, execution_id)
except EnqueueError as error:
@@ -829,8 +837,9 @@ def create_app(settings: Settings) -> FastAPI:
async def execution_events(
execution_id: str,
db: Annotated[AsyncSession, Depends(session)],
_: Annotated[tuple[User, set[str], bool], Depends(actor)],
identity: Annotated[tuple[User, set[str], bool], Depends(actor)],
) -> StreamingResponse:
enforce_scope(identity[1], "execution:read")
execution = await db.get(Execution, execution_id)
if execution is None:
raise Problem(404, "resource_not_found", "Execution was not found.")
+17 -1
View File
@@ -169,9 +169,25 @@ async def retry(db: AsyncSession, execution_id: str) -> Execution | None:
return execution
def _redact_progress(value: object) -> object:
"""Apply common redaction plus execution-specific path redaction recursively."""
if isinstance(value, Mapping):
safe: dict[str, object] = {}
for key, item in value.items():
normalized = str(key).lower()
if any(marker in normalized for marker in ("path", "secret", "token", "password")):
safe[str(key)] = "[REDACTED]"
else:
safe[str(key)] = _redact_progress(item)
return safe
if isinstance(value, list):
return [_redact_progress(item) for item in value]
return redact(value)
def public_event(execution: Execution) -> dict[str, object]:
"""Return redacted progress suitable for polling or SSE."""
progress = redact(execution.progress)
progress = _redact_progress(execution.progress)
return {
"id": execution.id,
"state": execution.state,