security: fullstack hardening — task ownership, mapping validation, API-key scoping, test fixes
Backend: - Add validate_mapping_database_ownership() to verify source/target UUIDs belong to declared environments before persisting mappings (mappings.py) - Add API-key environment scoping to get_mappings (filter) and suggest_mappings_api (enforce) (mappings.py) - Add user_id Column to TaskRecord model + Alembic migration (task.py) - Persist task.user_id on save, restore on load (persistence.py) - Wire current_user.id into migrate_dashboards + backup_dashboards task creation (_action_routes.py) - Fix test_migration_routes.py: module-level patch leak → autouse fixture, SupersetClient→AsyncSupersetClient, AsyncMock for sync_environment/run - Fix 7 Pydantic serializer warnings: 'PENDING'→TaskStatus.PENDING in test_tasks.py + import TaskStatus Frontend: - Deepen isDryRunResult(): validate selection field, risk.items entries (all 5 fields), and diff object uuids individually (ExecutorModel.svelte.ts) Prior work included: task password redaction, resume ownership checks, canonical dry-run DTO alignment, migration UI callback fixes, credential exposure reduction, assistant dry-run await fix.
This commit is contained in:
@@ -21,6 +21,7 @@ from contextlib import asynccontextmanager
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
# project_root is used for static files mounting
|
||||
project_root = Path(__file__).resolve().parent.parent.parent
|
||||
@@ -38,6 +39,7 @@ from .api.routes import (
|
||||
admin,
|
||||
admin_api_keys,
|
||||
agent_conversations,
|
||||
agent_lifecycle,
|
||||
agent_status,
|
||||
agent_superset,
|
||||
agent_superset_explore,
|
||||
@@ -65,7 +67,7 @@ from .api.routes import (
|
||||
)
|
||||
from .api.routes.validation_tasks import router as validation_tasks
|
||||
from .core.auth.security import get_password_hash
|
||||
from ss_tools.shared.cot_logger import get_trace_id, seed_trace_id
|
||||
from ss_tools.shared.cot_logger import get_trace_id, seed_trace_id, set_trace_id
|
||||
from .core.database import AuthSessionLocal, init_db
|
||||
from .core.encryption_key import ensure_encryption_key
|
||||
from .core.logger import belief_scope, logger
|
||||
@@ -476,6 +478,7 @@ app.include_router(reports.router)
|
||||
app.include_router(assistant.router, prefix="/api/assistant", tags=["Assistant"])
|
||||
app.include_router(agent_conversations.agent_router, tags=["Agent"])
|
||||
app.include_router(agent_conversations.router, tags=["Assistant"])
|
||||
app.include_router(agent_lifecycle.router)
|
||||
app.include_router(agent_status.router)
|
||||
app.include_router(agent_superset.router, tags=["Agent Superset"])
|
||||
app.include_router(agent_superset_explore.router, tags=["Agent Superset"])
|
||||
@@ -564,6 +567,32 @@ async def _authenticate_websocket(websocket: WebSocket, endpoint_name: str) -> b
|
||||
# #endregion _authenticate_websocket
|
||||
|
||||
|
||||
# #region _set_websocket_trace_id [C:2] [TYPE Function] [SEMANTICS websocket,trace,context]
|
||||
# @ingroup Module
|
||||
# @BRIEF Apply a valid UUID4 x-trace-id query parameter to the current WebSocket context.
|
||||
# @PRE websocket is a live WebSocket whose query params may include x-trace-id.
|
||||
# @POST Valid UUID4 query value is propagated; invalid or missing values receive a new trace ID.
|
||||
# @SIDE_EFFECT Sets the shared trace ContextVar for this WebSocket task.
|
||||
# @RATIONALE Browser WebSocket clients cannot set arbitrary request headers, so trace
|
||||
# propagation uses a query parameter alongside the authentication token.
|
||||
# @REJECTED Requiring a custom WebSocket header was rejected — browser WebSocket APIs do not
|
||||
# permit application-defined headers during the opening handshake.
|
||||
def _set_websocket_trace_id(websocket: WebSocket) -> str:
|
||||
incoming = websocket.query_params.get("x-trace-id", "")
|
||||
if incoming:
|
||||
try:
|
||||
parsed = uuid.UUID(hex=incoming)
|
||||
if parsed.version == 4:
|
||||
set_trace_id(incoming)
|
||||
return incoming
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
return seed_trace_id()
|
||||
|
||||
|
||||
# #endregion _set_websocket_trace_id
|
||||
|
||||
|
||||
# #region websocket_endpoint [C:5] [TYPE Function]
|
||||
# @ingroup Module
|
||||
# @BRIEF Provides a WebSocket endpoint for real-time log streaming of a task with server-side filtering.
|
||||
@@ -613,7 +642,7 @@ async def websocket_endpoint(websocket: WebSocket, task_id: str, source: str = N
|
||||
level: Filter logs by minimum level (DEBUG, INFO, WARNING, ERROR)
|
||||
token: JWT or API key for authentication (required, see [SEC:C-3])
|
||||
"""
|
||||
seed_trace_id()
|
||||
_set_websocket_trace_id(websocket)
|
||||
with belief_scope("websocket_endpoint", f"task_id={task_id}"):
|
||||
# ── WebSocket authentication (see [SEC:C-3]) ──
|
||||
if not await _authenticate_websocket(websocket, "ws/logs"):
|
||||
@@ -815,7 +844,7 @@ async def task_events_websocket(websocket: WebSocket):
|
||||
Query Parameters:
|
||||
token: JWT or API key for authentication (required)
|
||||
"""
|
||||
seed_trace_id()
|
||||
_set_websocket_trace_id(websocket)
|
||||
with belief_scope("task_events_websocket"):
|
||||
if not await _authenticate_websocket(websocket, "ws/task-events"):
|
||||
await websocket.close(code=4001, reason="Authentication required")
|
||||
@@ -872,7 +901,7 @@ async def maintenance_events_websocket(websocket: WebSocket):
|
||||
Query Parameters:
|
||||
token: JWT or API key for authentication (required)
|
||||
"""
|
||||
seed_trace_id()
|
||||
_set_websocket_trace_id(websocket)
|
||||
with belief_scope("maintenance_events_websocket"):
|
||||
if not await _authenticate_websocket(websocket, "ws/maintenance/events"):
|
||||
await websocket.close(code=4001, reason="Authentication required")
|
||||
@@ -918,7 +947,7 @@ async def maintenance_events_websocket(websocket: WebSocket):
|
||||
# @SIDE_EFFECT Subscribes to dataset event queue in task manager lifecycle.
|
||||
@app.websocket("/ws/datasets/{env_id}")
|
||||
async def dataset_websocket_endpoint(websocket: WebSocket, env_id: str):
|
||||
seed_trace_id()
|
||||
_set_websocket_trace_id(websocket)
|
||||
with belief_scope("dataset_websocket_endpoint", f"env_id={env_id}"):
|
||||
# ── WebSocket authentication (see [SEC:C-3]) ──
|
||||
if not await _authenticate_websocket(websocket, "ws/datasets"):
|
||||
@@ -954,7 +983,7 @@ async def dataset_websocket_endpoint(websocket: WebSocket, env_id: str):
|
||||
# @UX_FEEDBACK Client receives {status, total_records, successful_records, failed_records, progressPct, ...}
|
||||
@app.websocket("/ws/translate/run/{run_id}")
|
||||
async def translate_run_websocket(websocket: WebSocket, run_id: str):
|
||||
seed_trace_id()
|
||||
_set_websocket_trace_id(websocket)
|
||||
if not await _authenticate_websocket(websocket, "ws/translate/run"):
|
||||
await websocket.close(code=4001, reason="Authentication required")
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user