528 lines
20 KiB
Python
528 lines
20 KiB
Python
|
|
"""领域模型(dataclass) ↔ Output DTO(Pydantic) 转换 + 集中脱敏。
|
|||
|
|
|
|||
|
|
规范 §6.1/§7.3:
|
|||
|
|
- persistence 返回领域模型 dataclass,service 返回 Output DTO
|
|||
|
|
- 脱敏集中在 mapper(调 crypto_service.mask_sensitive_fields)
|
|||
|
|
- use_cases 层不调用 encrypt / decrypt
|
|||
|
|
|
|||
|
|
脱敏策略:
|
|||
|
|
- dict 型敏感字段(``connection_config`` / ``auth_config`` / ``secret_refs`` /
|
|||
|
|
``adapter_config``)调用 ``crypto_service.mask_sensitive_fields`` 递归脱敏
|
|||
|
|
- 字符串型敏感字段(``access_token`` / ``refresh_token`` / ``secret``)
|
|||
|
|
非空时替换为 ``"***"``
|
|||
|
|
|
|||
|
|
审计字段(``created_by`` / ``updated_by``)由 service 在调用 mapper 前补充,
|
|||
|
|
mapper 仅做字段映射。
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
from yuxi.external_systems.core.models import (
|
|||
|
|
AdapterAsset,
|
|||
|
|
Alert,
|
|||
|
|
AuditLog,
|
|||
|
|
Environment,
|
|||
|
|
Execution,
|
|||
|
|
ExecutionTrace,
|
|||
|
|
ExternalSystem,
|
|||
|
|
ExternalTool,
|
|||
|
|
NotificationChannel,
|
|||
|
|
QuotaUsage,
|
|||
|
|
SecretRotationPolicy,
|
|||
|
|
SystemToken,
|
|||
|
|
ToolAccessRule,
|
|||
|
|
ToolTestCase,
|
|||
|
|
WebhookEvent,
|
|||
|
|
WebhookSubscription,
|
|||
|
|
)
|
|||
|
|
from yuxi.external_systems.framework.runtime import crypto_service
|
|||
|
|
from yuxi.external_systems.use_cases.dto.access_rule import AccessRuleOutput
|
|||
|
|
from yuxi.external_systems.use_cases.dto.alert import AlertOutput
|
|||
|
|
from yuxi.external_systems.use_cases.dto.asset import AssetOutput
|
|||
|
|
from yuxi.external_systems.use_cases.dto.audit_log import AuditLogOutput
|
|||
|
|
from yuxi.external_systems.use_cases.dto.environment import EnvironmentOutput
|
|||
|
|
from yuxi.external_systems.use_cases.dto.execution import (
|
|||
|
|
ExecutionDetailOutput,
|
|||
|
|
ExecutionOutput,
|
|||
|
|
ExecutionTraceOutput,
|
|||
|
|
)
|
|||
|
|
from yuxi.external_systems.use_cases.dto.notification_channel import (
|
|||
|
|
NotificationChannelOutput,
|
|||
|
|
)
|
|||
|
|
from yuxi.external_systems.use_cases.dto.quota import QuotaOutput
|
|||
|
|
from yuxi.external_systems.use_cases.dto.secret_rotation import (
|
|||
|
|
SecretRotationPolicyOutput,
|
|||
|
|
)
|
|||
|
|
from yuxi.external_systems.use_cases.dto.system import (
|
|||
|
|
GovernanceConfigOutput,
|
|||
|
|
SystemOutput,
|
|||
|
|
)
|
|||
|
|
from yuxi.external_systems.use_cases.dto.test_case import TestCaseOutput
|
|||
|
|
from yuxi.external_systems.use_cases.dto.token import TokenOutput
|
|||
|
|
from yuxi.external_systems.use_cases.dto.tool import ToolOutput
|
|||
|
|
from yuxi.external_systems.use_cases.dto.webhook import (
|
|||
|
|
WebhookEventOutput,
|
|||
|
|
WebhookSubscriptionOutput,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 敏感字符串字段的统一脱敏占位符
|
|||
|
|
_MASKED_VALUE = "***"
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|||
|
|
# 含敏感字段的子域(mask: bool = True)
|
|||
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
|
|||
|
|
def to_system_output(system: ExternalSystem, *, mask: bool = True) -> SystemOutput:
|
|||
|
|
"""ExternalSystem dataclass → SystemOutput DTO(含脱敏)。
|
|||
|
|
|
|||
|
|
dict 型敏感字段:``connection_config`` / ``auth_config`` / ``secret_refs``。
|
|||
|
|
"""
|
|||
|
|
connection_config = system.connection_config
|
|||
|
|
auth_config = system.auth_config
|
|||
|
|
secret_refs = system.secret_refs
|
|||
|
|
if mask:
|
|||
|
|
connection_config = crypto_service.mask_sensitive_fields(connection_config)
|
|||
|
|
auth_config = crypto_service.mask_sensitive_fields(auth_config)
|
|||
|
|
secret_refs = crypto_service.mask_sensitive_fields(secret_refs)
|
|||
|
|
return SystemOutput(
|
|||
|
|
id=system.id,
|
|||
|
|
slug=system.slug,
|
|||
|
|
name=system.name,
|
|||
|
|
description=system.description,
|
|||
|
|
category=system.category,
|
|||
|
|
adapter_type=system.adapter_type,
|
|||
|
|
source_type=system.source_type,
|
|||
|
|
enabled=system.enabled,
|
|||
|
|
connection_config=connection_config,
|
|||
|
|
auth_type=system.auth_type,
|
|||
|
|
auth_config=auth_config,
|
|||
|
|
secret_refs=secret_refs,
|
|||
|
|
rate_limit=system.rate_limit,
|
|||
|
|
circuit_breaker=system.circuit_breaker,
|
|||
|
|
pool_config=system.pool_config,
|
|||
|
|
observability=system.observability,
|
|||
|
|
timeout=system.timeout,
|
|||
|
|
retry_policy=system.retry_policy,
|
|||
|
|
created_by=system.created_by,
|
|||
|
|
updated_by=system.updated_by,
|
|||
|
|
env_count=system.env_count,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def to_tool_output(tool: ExternalTool, *, mask: bool = True) -> ToolOutput:
|
|||
|
|
"""ExternalTool dataclass → ToolOutput DTO(含脱敏)。
|
|||
|
|
|
|||
|
|
dict 型敏感字段:``adapter_config`` / ``auth_config``。
|
|||
|
|
"""
|
|||
|
|
adapter_config = tool.adapter_config
|
|||
|
|
auth_config = tool.auth_config
|
|||
|
|
if mask:
|
|||
|
|
adapter_config = crypto_service.mask_sensitive_fields(adapter_config)
|
|||
|
|
auth_config = crypto_service.mask_sensitive_fields(auth_config)
|
|||
|
|
return ToolOutput(
|
|||
|
|
id=tool.id,
|
|||
|
|
slug=tool.slug,
|
|||
|
|
name=tool.name,
|
|||
|
|
description=tool.description,
|
|||
|
|
category=tool.category,
|
|||
|
|
adapter_type=tool.adapter_type,
|
|||
|
|
auth_type=tool.auth_type,
|
|||
|
|
adapter_config=adapter_config,
|
|||
|
|
auth_config=auth_config,
|
|||
|
|
timeout=tool.timeout,
|
|||
|
|
retry_policy=tool.retry_policy,
|
|||
|
|
enabled=tool.enabled,
|
|||
|
|
system_id=tool.system_id,
|
|||
|
|
created_by=tool.created_by,
|
|||
|
|
updated_by=tool.updated_by,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def to_environment_output(env: Environment, *, mask: bool = True) -> EnvironmentOutput:
|
|||
|
|
"""Environment dataclass → EnvironmentOutput DTO(含脱敏)。
|
|||
|
|
|
|||
|
|
dict 型敏感字段:``connection_config`` / ``auth_config``。
|
|||
|
|
整合旧 ``environment_service._mask_secrets`` 的脱敏职责(非 decrypt 部分)。
|
|||
|
|
"""
|
|||
|
|
connection_config = env.connection_config
|
|||
|
|
auth_config = env.auth_config
|
|||
|
|
if mask:
|
|||
|
|
connection_config = crypto_service.mask_sensitive_fields(connection_config)
|
|||
|
|
auth_config = crypto_service.mask_sensitive_fields(auth_config)
|
|||
|
|
return EnvironmentOutput(
|
|||
|
|
id=env.id,
|
|||
|
|
system_id=env.system_id,
|
|||
|
|
env_key=env.env_key,
|
|||
|
|
name=env.name,
|
|||
|
|
connection_config=connection_config,
|
|||
|
|
auth_config=auth_config,
|
|||
|
|
is_default=env.is_default,
|
|||
|
|
enabled=env.enabled,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def to_token_output(token: SystemToken, *, mask: bool = True) -> TokenOutput:
|
|||
|
|
"""SystemToken dataclass → TokenOutput DTO(含脱敏)。
|
|||
|
|
|
|||
|
|
字符串型敏感字段:``access_token`` / ``refresh_token``,非空时替换为 ``"***"``。
|
|||
|
|
"""
|
|||
|
|
access_token = token.access_token
|
|||
|
|
refresh_token = token.refresh_token
|
|||
|
|
if mask:
|
|||
|
|
access_token = _MASKED_VALUE if access_token else access_token
|
|||
|
|
refresh_token = _MASKED_VALUE if refresh_token else refresh_token
|
|||
|
|
return TokenOutput(
|
|||
|
|
id=token.id,
|
|||
|
|
system_id=token.system_id,
|
|||
|
|
env_key=token.env_key,
|
|||
|
|
token_type=token.token_type,
|
|||
|
|
access_token=access_token,
|
|||
|
|
refresh_token=refresh_token,
|
|||
|
|
expires_at=token.expires_at,
|
|||
|
|
extra=token.extra,
|
|||
|
|
is_invalidated=token.is_invalidated,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def to_webhook_subscription_output(
|
|||
|
|
subscription: WebhookSubscription,
|
|||
|
|
*,
|
|||
|
|
mask: bool = True,
|
|||
|
|
) -> WebhookSubscriptionOutput:
|
|||
|
|
"""WebhookSubscription dataclass → WebhookSubscriptionOutput DTO(含脱敏)。
|
|||
|
|
|
|||
|
|
字符串型敏感字段:``secret``,非空时替换为 ``"***"``。
|
|||
|
|
"""
|
|||
|
|
secret = subscription.secret
|
|||
|
|
if mask:
|
|||
|
|
secret = _MASKED_VALUE if secret else secret
|
|||
|
|
return WebhookSubscriptionOutput(
|
|||
|
|
id=subscription.id,
|
|||
|
|
slug=subscription.slug,
|
|||
|
|
name=subscription.name,
|
|||
|
|
system_id=subscription.system_id,
|
|||
|
|
source_event_type=subscription.source_event_type,
|
|||
|
|
target_handler_type=subscription.target_handler_type,
|
|||
|
|
callback_path=subscription.callback_path,
|
|||
|
|
description=subscription.description,
|
|||
|
|
env_key=subscription.env_key,
|
|||
|
|
subscription_id=subscription.subscription_id,
|
|||
|
|
target_handler_id=subscription.target_handler_id,
|
|||
|
|
secret=secret,
|
|||
|
|
secret_algorithm=subscription.secret_algorithm,
|
|||
|
|
filter_rules=subscription.filter_rules,
|
|||
|
|
transform_rules=subscription.transform_rules,
|
|||
|
|
status=subscription.status,
|
|||
|
|
enabled=subscription.enabled,
|
|||
|
|
expires_at=subscription.expires_at,
|
|||
|
|
auto_renew=subscription.auto_renew,
|
|||
|
|
last_renewed_at=subscription.last_renewed_at,
|
|||
|
|
last_renew_error=subscription.last_renew_error,
|
|||
|
|
renewal_attempts=subscription.renewal_attempts,
|
|||
|
|
last_received_at=subscription.last_received_at,
|
|||
|
|
received_count=subscription.received_count,
|
|||
|
|
retention_days=subscription.retention_days,
|
|||
|
|
created_by=subscription.created_by,
|
|||
|
|
updated_by=subscription.updated_by,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|||
|
|
# 无敏感字段的子域(直接字段映射)
|
|||
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
|
|||
|
|
def to_webhook_event_output(event: WebhookEvent) -> WebhookEventOutput:
|
|||
|
|
"""将 WebhookEvent dataclass 转换为 WebhookEventOutput DTO。"""
|
|||
|
|
return WebhookEventOutput(
|
|||
|
|
id=event.id,
|
|||
|
|
subscription_id=event.subscription_id,
|
|||
|
|
subscription_slug=event.subscription_slug,
|
|||
|
|
external_event_id=event.external_event_id,
|
|||
|
|
event_type=event.event_type,
|
|||
|
|
payload=event.payload,
|
|||
|
|
payload_hash=event.payload_hash,
|
|||
|
|
headers=event.headers,
|
|||
|
|
signature=event.signature,
|
|||
|
|
signature_verified=event.signature_verified,
|
|||
|
|
received_at=event.received_at,
|
|||
|
|
source_ip=event.source_ip,
|
|||
|
|
processing_status=event.processing_status,
|
|||
|
|
processing_attempts=event.processing_attempts,
|
|||
|
|
last_processed_at=event.last_processed_at,
|
|||
|
|
processing_error=event.processing_error,
|
|||
|
|
correlation_id=event.correlation_id,
|
|||
|
|
triggered_handler_type=event.triggered_handler_type,
|
|||
|
|
triggered_handler_id=event.triggered_handler_id,
|
|||
|
|
triggered_execution_id=event.triggered_execution_id,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def to_execution_output(execution: Execution) -> ExecutionOutput:
|
|||
|
|
"""Execution dataclass → ExecutionOutput DTO(无敏感字段,直接映射)。"""
|
|||
|
|
return ExecutionOutput(
|
|||
|
|
id=execution.id,
|
|||
|
|
execution_id=execution.execution_id,
|
|||
|
|
trace_id=execution.trace_id,
|
|||
|
|
system_id=execution.system_id,
|
|||
|
|
env_key=execution.env_key,
|
|||
|
|
tool_slug=execution.tool_slug,
|
|||
|
|
caller=execution.caller,
|
|||
|
|
caller_id=execution.caller_id,
|
|||
|
|
status=execution.status,
|
|||
|
|
started_at=execution.started_at,
|
|||
|
|
operation=execution.operation,
|
|||
|
|
request_summary=execution.request_summary,
|
|||
|
|
response_summary=execution.response_summary,
|
|||
|
|
error=execution.error,
|
|||
|
|
ended_at=execution.ended_at,
|
|||
|
|
duration_ms=execution.duration_ms,
|
|||
|
|
correlation_id=execution.correlation_id,
|
|||
|
|
retry_of=execution.retry_of,
|
|||
|
|
retry_count=execution.retry_count,
|
|||
|
|
tags=execution.tags,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def to_execution_detail_output(
|
|||
|
|
execution: Execution,
|
|||
|
|
traces: list[ExecutionTrace],
|
|||
|
|
) -> ExecutionDetailOutput:
|
|||
|
|
"""Execution + traces → ExecutionDetailOutput DTO。
|
|||
|
|
|
|||
|
|
注意:当前 ``ExecutionDetailOutput`` 未包含 traces 字段,``traces`` 参数
|
|||
|
|
预留用于后续扩展(service 层可单独调用 ``to_execution_trace_output``
|
|||
|
|
处理 traces)。``system_slug`` / ``system_name`` 由 service 层补充。
|
|||
|
|
"""
|
|||
|
|
_ = traces # 预留参数,当前 DTO 未包含 traces 字段
|
|||
|
|
return ExecutionDetailOutput(
|
|||
|
|
execution=to_execution_output(execution),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def to_execution_trace_output(trace: ExecutionTrace) -> ExecutionTraceOutput:
|
|||
|
|
"""ExecutionTrace dataclass → ExecutionTraceOutput DTO(无敏感字段,直接映射)。"""
|
|||
|
|
return ExecutionTraceOutput(
|
|||
|
|
id=trace.id,
|
|||
|
|
execution_id=trace.execution_id,
|
|||
|
|
trace_id=trace.trace_id,
|
|||
|
|
spans=trace.spans,
|
|||
|
|
total_duration_ms=trace.total_duration_ms,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def to_audit_log_output(audit_log: AuditLog) -> AuditLogOutput:
|
|||
|
|
"""AuditLog dataclass → AuditLogOutput DTO(无敏感字段,直接映射)。"""
|
|||
|
|
return AuditLogOutput(
|
|||
|
|
id=audit_log.id,
|
|||
|
|
system_id=audit_log.system_id,
|
|||
|
|
env_key=audit_log.env_key,
|
|||
|
|
event_type=audit_log.event_type,
|
|||
|
|
action_type=audit_log.action_type,
|
|||
|
|
resource=audit_log.resource,
|
|||
|
|
user=audit_log.user,
|
|||
|
|
detail=audit_log.detail,
|
|||
|
|
snapshot_before=audit_log.snapshot_before,
|
|||
|
|
snapshot_after=audit_log.snapshot_after,
|
|||
|
|
description=audit_log.description,
|
|||
|
|
created_at=audit_log.created_at,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def to_asset_output(asset: AdapterAsset) -> AssetOutput:
|
|||
|
|
"""AdapterAsset dataclass → AssetOutput DTO(不含二进制 content)。"""
|
|||
|
|
return AssetOutput(
|
|||
|
|
id=asset.id,
|
|||
|
|
adapter_type=asset.adapter_type,
|
|||
|
|
asset_type=asset.asset_type,
|
|||
|
|
name=asset.name,
|
|||
|
|
size=asset.size,
|
|||
|
|
checksum=asset.checksum,
|
|||
|
|
status=asset.status,
|
|||
|
|
status_message=asset.status_message,
|
|||
|
|
created_by=asset.created_by,
|
|||
|
|
updated_by=asset.updated_by,
|
|||
|
|
created_at=asset.created_at,
|
|||
|
|
updated_at=asset.updated_at,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def to_quota_output(quota: QuotaUsage) -> QuotaOutput:
|
|||
|
|
"""QuotaUsage dataclass → QuotaOutput DTO(无敏感字段,直接映射)。"""
|
|||
|
|
return QuotaOutput(
|
|||
|
|
id=quota.id,
|
|||
|
|
system_id=quota.system_id,
|
|||
|
|
quota_key=quota.quota_key,
|
|||
|
|
quota_name=quota.quota_name,
|
|||
|
|
quota_window=quota.quota_window,
|
|||
|
|
env_key=quota.env_key,
|
|||
|
|
window_start=quota.window_start,
|
|||
|
|
window_end=quota.window_end,
|
|||
|
|
limit_value=quota.limit_value,
|
|||
|
|
used_value=quota.used_value,
|
|||
|
|
remaining_value=quota.remaining_value,
|
|||
|
|
usage_ratio=quota.usage_ratio,
|
|||
|
|
is_warning=quota.is_warning,
|
|||
|
|
is_critical=quota.is_critical,
|
|||
|
|
unit=quota.unit,
|
|||
|
|
source=quota.source,
|
|||
|
|
source_header_name=quota.source_header_name,
|
|||
|
|
warning_threshold=quota.warning_threshold,
|
|||
|
|
critical_threshold=quota.critical_threshold,
|
|||
|
|
last_updated_at=quota.last_updated_at,
|
|||
|
|
last_response_at=quota.last_response_at,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def to_alert_output(alert: Alert) -> AlertOutput:
|
|||
|
|
"""Alert dataclass → AlertOutput DTO(无敏感字段,直接映射)。"""
|
|||
|
|
return AlertOutput(
|
|||
|
|
id=alert.id,
|
|||
|
|
system_id=alert.system_id,
|
|||
|
|
alert_type=alert.alert_type,
|
|||
|
|
title=alert.title,
|
|||
|
|
triggered_at=alert.triggered_at,
|
|||
|
|
env_key=alert.env_key,
|
|||
|
|
severity=alert.severity,
|
|||
|
|
status=alert.status,
|
|||
|
|
description=alert.description,
|
|||
|
|
detail=alert.detail,
|
|||
|
|
resource_type=alert.resource_type,
|
|||
|
|
resource_id=alert.resource_id,
|
|||
|
|
related_execution_id=alert.related_execution_id,
|
|||
|
|
related_trace_id=alert.related_trace_id,
|
|||
|
|
metric_snapshot=alert.metric_snapshot,
|
|||
|
|
dedup_key=alert.dedup_key,
|
|||
|
|
resolved_at=alert.resolved_at,
|
|||
|
|
acknowledged_by=alert.acknowledged_by,
|
|||
|
|
acknowledged_at=alert.acknowledged_at,
|
|||
|
|
resolution_note=alert.resolution_note,
|
|||
|
|
notification_status=alert.notification_status,
|
|||
|
|
notification_sent_at=alert.notification_sent_at,
|
|||
|
|
notification_channels=alert.notification_channels,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def to_secret_rotation_policy_output(
|
|||
|
|
policy: SecretRotationPolicy,
|
|||
|
|
) -> SecretRotationPolicyOutput:
|
|||
|
|
"""SecretRotationPolicy dataclass → SecretRotationPolicyOutput DTO(无敏感字段,直接映射)。
|
|||
|
|
|
|||
|
|
注意:``secret_ref`` 仅是引用 key(指向 ``ExternalSystem.secret_refs`` 中的
|
|||
|
|
具名 secret),非敏感值本身,不脱敏。
|
|||
|
|
"""
|
|||
|
|
return SecretRotationPolicyOutput(
|
|||
|
|
id=policy.id,
|
|||
|
|
system_id=policy.system_id,
|
|||
|
|
secret_type=policy.secret_type,
|
|||
|
|
secret_name=policy.secret_name,
|
|||
|
|
secret_ref=policy.secret_ref,
|
|||
|
|
env_key=policy.env_key,
|
|||
|
|
rotation_mode=policy.rotation_mode,
|
|||
|
|
rotation_interval_days=policy.rotation_interval_days,
|
|||
|
|
notify_before_days=policy.notify_before_days,
|
|||
|
|
notify_channels=policy.notify_channels,
|
|||
|
|
last_rotated_at=policy.last_rotated_at,
|
|||
|
|
next_rotation_at=policy.next_rotation_at,
|
|||
|
|
last_rotation_at=policy.last_rotation_at,
|
|||
|
|
last_rotation_status=policy.last_rotation_status,
|
|||
|
|
last_rotation_error=policy.last_rotation_error,
|
|||
|
|
rotation_count=policy.rotation_count,
|
|||
|
|
max_rotation_retries=policy.max_rotation_retries,
|
|||
|
|
status=policy.status,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def to_access_rule_output(rule: ToolAccessRule) -> AccessRuleOutput:
|
|||
|
|
"""ToolAccessRule dataclass → AccessRuleOutput DTO(无敏感字段,直接映射)。"""
|
|||
|
|
return AccessRuleOutput(
|
|||
|
|
id=rule.id,
|
|||
|
|
principal_type=rule.principal_type,
|
|||
|
|
principal_id=rule.principal_id,
|
|||
|
|
created_by=rule.created_by,
|
|||
|
|
tool_slug=rule.tool_slug,
|
|||
|
|
system_id=rule.system_id,
|
|||
|
|
principal_name=rule.principal_name,
|
|||
|
|
env_key=rule.env_key,
|
|||
|
|
effect=rule.effect,
|
|||
|
|
priority=rule.priority,
|
|||
|
|
conditions=rule.conditions,
|
|||
|
|
expires_at=rule.expires_at,
|
|||
|
|
enabled=rule.enabled,
|
|||
|
|
description=rule.description,
|
|||
|
|
updated_by=rule.updated_by,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def to_test_case_output(test_case: ToolTestCase) -> TestCaseOutput:
|
|||
|
|
"""ToolTestCase dataclass → TestCaseOutput DTO(无敏感字段,直接映射)。"""
|
|||
|
|
return TestCaseOutput(
|
|||
|
|
id=test_case.id,
|
|||
|
|
tool_slug=test_case.tool_slug,
|
|||
|
|
name=test_case.name,
|
|||
|
|
request_fixture=test_case.request_fixture,
|
|||
|
|
created_by=test_case.created_by,
|
|||
|
|
description=test_case.description,
|
|||
|
|
env_key=test_case.env_key,
|
|||
|
|
expected_status=test_case.expected_status,
|
|||
|
|
assertions=test_case.assertions,
|
|||
|
|
timeout=test_case.timeout,
|
|||
|
|
enabled=test_case.enabled,
|
|||
|
|
schedule_cron=test_case.schedule_cron,
|
|||
|
|
alert_on_failure=test_case.alert_on_failure,
|
|||
|
|
alert_threshold=test_case.alert_threshold,
|
|||
|
|
last_run_at=test_case.last_run_at,
|
|||
|
|
last_run_status=test_case.last_run_status,
|
|||
|
|
last_run_duration_ms=test_case.last_run_duration_ms,
|
|||
|
|
last_run_response=test_case.last_run_response,
|
|||
|
|
last_run_error=test_case.last_run_error,
|
|||
|
|
last_run_assertions_result=test_case.last_run_assertions_result,
|
|||
|
|
consecutive_failures=test_case.consecutive_failures,
|
|||
|
|
updated_by=test_case.updated_by,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def to_notification_channel_output(
|
|||
|
|
channel: NotificationChannel,
|
|||
|
|
*,
|
|||
|
|
mask: bool = True,
|
|||
|
|
) -> NotificationChannelOutput:
|
|||
|
|
"""NotificationChannel dataclass → NotificationChannelOutput DTO(含脱敏)。
|
|||
|
|
|
|||
|
|
dict 型敏感字段:``config``(可能含 webhook URL、SMTP 密码等)。
|
|||
|
|
"""
|
|||
|
|
config = channel.config
|
|||
|
|
if mask:
|
|||
|
|
config = crypto_service.mask_sensitive_fields(config)
|
|||
|
|
return NotificationChannelOutput(
|
|||
|
|
id=channel.id,
|
|||
|
|
channel_type=channel.channel_type,
|
|||
|
|
name=channel.name,
|
|||
|
|
config=config,
|
|||
|
|
enabled=channel.enabled,
|
|||
|
|
description=channel.description,
|
|||
|
|
created_by=channel.created_by,
|
|||
|
|
updated_by=channel.updated_by,
|
|||
|
|
created_at=channel.created_at,
|
|||
|
|
updated_at=channel.updated_at,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def to_secret_refs_governance_output(
|
|||
|
|
system: ExternalSystem,
|
|||
|
|
*,
|
|||
|
|
mask: bool = True,
|
|||
|
|
) -> GovernanceConfigOutput:
|
|||
|
|
"""ExternalSystem.secret_refs → GovernanceConfigOutput DTO(含脱敏)。
|
|||
|
|
|
|||
|
|
dict 型敏感字段:``secret_refs``,调用 ``crypto_service.mask_sensitive_fields``
|
|||
|
|
递归脱敏。整合旧 ``system_service.get_secret_refs`` / ``update_secret_refs``
|
|||
|
|
的脱敏职责,使 service 层不再直接调用 crypto_service。
|
|||
|
|
"""
|
|||
|
|
secret_refs = system.secret_refs
|
|||
|
|
if mask:
|
|||
|
|
secret_refs = crypto_service.mask_sensitive_fields(secret_refs)
|
|||
|
|
return GovernanceConfigOutput(system_id=system.id, config=secret_refs)
|