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.") raise Problem(403, "csrf_failed", "CSRF validation failed.")
return user, scopes, cookie_auth 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( async def audit(
db: AsyncSession, db: AsyncSession,
request: Request, request: Request,
@@ -767,8 +771,9 @@ def create_app(settings: Settings) -> FastAPI:
async def enqueue_execution( async def enqueue_execution(
job_id: str, job_id: str,
db: Annotated[AsyncSession, Depends(session)], 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]: ) -> dict[str, Any]:
enforce_scope(identity[1], "execution:control")
try: try:
execution = await enqueue(db, job_id) execution = await enqueue(db, job_id)
except EnqueueError as error: except EnqueueError as error:
@@ -793,8 +798,9 @@ def create_app(settings: Settings) -> FastAPI:
async def get_execution( async def get_execution(
execution_id: str, execution_id: str,
db: Annotated[AsyncSession, Depends(session)], 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]: ) -> dict[str, object]:
enforce_scope(identity[1], "execution:read")
execution = await db.get(Execution, execution_id) execution = await db.get(Execution, execution_id)
if execution is None: if execution is None:
raise Problem(404, "resource_not_found", "Execution was not found.") raise Problem(404, "resource_not_found", "Execution was not found.")
@@ -804,8 +810,9 @@ def create_app(settings: Settings) -> FastAPI:
async def cancel_execution( async def cancel_execution(
execution_id: str, execution_id: str,
db: Annotated[AsyncSession, Depends(session)], 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]: ) -> dict[str, object]:
enforce_scope(identity[1], "execution:control")
execution = await request_cancellation(db, execution_id) execution = await request_cancellation(db, execution_id)
if execution is None: if execution is None:
raise Problem(409, "cancellation_not_allowed", "Execution cannot be cancelled.") raise Problem(409, "cancellation_not_allowed", "Execution cannot be cancelled.")
@@ -815,8 +822,9 @@ def create_app(settings: Settings) -> FastAPI:
async def retry_execution( async def retry_execution(
execution_id: str, execution_id: str,
db: Annotated[AsyncSession, Depends(session)], 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]: ) -> dict[str, object]:
enforce_scope(identity[1], "execution:control")
try: try:
execution = await retry(db, execution_id) execution = await retry(db, execution_id)
except EnqueueError as error: except EnqueueError as error:
@@ -829,8 +837,9 @@ def create_app(settings: Settings) -> FastAPI:
async def execution_events( async def execution_events(
execution_id: str, execution_id: str,
db: Annotated[AsyncSession, Depends(session)], db: Annotated[AsyncSession, Depends(session)],
_: Annotated[tuple[User, set[str], bool], Depends(actor)], identity: Annotated[tuple[User, set[str], bool], Depends(actor)],
) -> StreamingResponse: ) -> StreamingResponse:
enforce_scope(identity[1], "execution:read")
execution = await db.get(Execution, execution_id) execution = await db.get(Execution, execution_id)
if execution is None: if execution is None:
raise Problem(404, "resource_not_found", "Execution was not found.") 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 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]: def public_event(execution: Execution) -> dict[str, object]:
"""Return redacted progress suitable for polling or SSE.""" """Return redacted progress suitable for polling or SSE."""
progress = redact(execution.progress) progress = _redact_progress(execution.progress)
return { return {
"id": execution.id, "id": execution.id,
"state": execution.state, "state": execution.state,
+1 -3
View File
@@ -115,9 +115,7 @@ async def test_local_source_probe_archive_and_repository_targeted_job(
await client.get(f"/api/v2/executions/{execution_id}", headers=token_headers) await client.get(f"/api/v2/executions/{execution_id}", headers=token_headers)
).status_code == 403 ).status_code == 403
assert ( assert (
await client.post( await client.post(f"/api/v2/executions/{execution_id}/cancel", headers=token_headers)
f"/api/v2/executions/{execution_id}/cancel", headers=token_headers
)
).status_code == 403 ).status_code == 403
async with app.state.sessions() as db: async with app.state.sessions() as db:
assert await claim(db, execution_id, "expired-worker") is not None assert await claim(db, execution_id, "expired-worker") is not None