mirror of
https://github.com/langgenius/dify.git
synced 2026-11-11 22:02:23 +08:00
149 lines
3.9 KiB
Python
149 lines
3.9 KiB
Python
"""Audit emission for openapi app-run endpoints.
|
|
|
|
Pattern: logger.info with extra={"audit": True, "event": "app.run.openapi", ...}
|
|
matches the existing oauth_device convention. The EE OTel exporter consults
|
|
its own allowlist to decide whether to ship the line.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from collections.abc import Mapping
|
|
from contextlib import suppress
|
|
from uuid import UUID
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
EVENT_APP_RUN_OPENAPI = "app.run.openapi"
|
|
EVENT_OPENAPI_WRONG_SURFACE_DENIED = "openapi.wrong_surface_denied"
|
|
|
|
# Review this list when adding audited targets; a route parameter name alone
|
|
# must never opt request data into audit logs. Form tokens are intentionally absent.
|
|
PERSONAL_AUDIT_TARGET_FIELDS = frozenset(
|
|
{
|
|
"app_id",
|
|
"workspace_id",
|
|
"run_id",
|
|
"task_id",
|
|
"import_id",
|
|
"member_id",
|
|
"session_id",
|
|
"env_id",
|
|
"version_id",
|
|
"skill_id",
|
|
"knowledge_space_id",
|
|
"credential_id",
|
|
}
|
|
)
|
|
|
|
|
|
def personal_audit_targets(targets: Mapping[str, object]) -> dict[str, str]:
|
|
"""Return only explicitly approved UUID targets; discard other request values."""
|
|
result = {}
|
|
for key in sorted(PERSONAL_AUDIT_TARGET_FIELDS & targets.keys()):
|
|
value = targets[key]
|
|
if isinstance(value, str):
|
|
with suppress(ValueError):
|
|
result[key] = str(UUID(value))
|
|
return result
|
|
|
|
|
|
def emit_app_run(
|
|
*,
|
|
app_id: str,
|
|
tenant_id: str,
|
|
caller_kind: str,
|
|
mode: str,
|
|
surface: str,
|
|
) -> None:
|
|
logger.info(
|
|
"audit: %s app_id=%s tenant_id=%s caller_kind=%s mode=%s surface=%s",
|
|
EVENT_APP_RUN_OPENAPI,
|
|
app_id,
|
|
tenant_id,
|
|
caller_kind,
|
|
mode,
|
|
surface,
|
|
extra={
|
|
"audit": True,
|
|
"event": EVENT_APP_RUN_OPENAPI,
|
|
"app_id": app_id,
|
|
"tenant_id": tenant_id,
|
|
"caller_kind": caller_kind,
|
|
"mode": mode,
|
|
"surface": surface,
|
|
},
|
|
)
|
|
|
|
|
|
def emit_wrong_surface(
|
|
*,
|
|
subject_type: str | None,
|
|
attempted_path: str,
|
|
client_id: str | None,
|
|
token_id: str | None,
|
|
) -> None:
|
|
logger.warning(
|
|
"audit: %s subject_type=%s attempted_path=%s",
|
|
EVENT_OPENAPI_WRONG_SURFACE_DENIED,
|
|
subject_type,
|
|
attempted_path,
|
|
extra={
|
|
"audit": True,
|
|
"event": EVENT_OPENAPI_WRONG_SURFACE_DENIED,
|
|
"subject_type": subject_type,
|
|
"attempted_path": attempted_path,
|
|
"client_id": client_id,
|
|
"token_id": token_id,
|
|
},
|
|
)
|
|
|
|
|
|
def emit_personal_request(
|
|
*,
|
|
token_id: str,
|
|
account_id: str,
|
|
operation: str,
|
|
target_route: str,
|
|
target_ids: dict[str, str],
|
|
http_status: int,
|
|
streaming: bool,
|
|
trace_id: str | None,
|
|
) -> None:
|
|
"""Audit authenticated PAT requests without headers, bodies or exception text.
|
|
|
|
A successful SSE response records admission only, not execution completion.
|
|
trace_id is absent when no server trace was assigned.
|
|
"""
|
|
outcome = (
|
|
"failed"
|
|
if http_status >= 500
|
|
else "denied"
|
|
if http_status >= 400
|
|
else "stream_opened"
|
|
if streaming
|
|
else "succeeded"
|
|
)
|
|
logger.info(
|
|
"audit: pat.request.openapi token_id=%s account_id=%s operation=%s target=%s ids=%s outcome=%s status=%s",
|
|
token_id,
|
|
account_id,
|
|
operation,
|
|
target_route,
|
|
target_ids,
|
|
outcome,
|
|
http_status,
|
|
extra={
|
|
"audit": True,
|
|
"event": "pat.request.openapi",
|
|
"token_id": token_id,
|
|
"account_id": account_id,
|
|
"operation": operation,
|
|
"target_route": target_route,
|
|
"target_ids": target_ids,
|
|
"outcome": outcome,
|
|
"http_status": http_status,
|
|
"trace_id": trace_id,
|
|
},
|
|
)
|