本次提交包含大量功能完善与修复: 1. 新增多类操作的操作人审计字段与环境校验逻辑 2. 修复多集成的认证头生成逻辑,支持动态token类型 3. 新增通知派发适配器框架与多渠道实现 4. 完善健康检查返回结果与指标监控项 5. 优化JQL校验逻辑与资产导入规则 6. 补充各类DTO字段与持久层映射关系 7. 修复并优化测试用例、配额、密钥轮转等服务逻辑
532 lines
20 KiB
Python
532 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,
|
||
created_at=trace.created_at,
|
||
updated_at=trace.updated_at,
|
||
)
|
||
|
||
|
||
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,
|
||
source_header_strategy=quota.source_header_strategy,
|
||
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,
|
||
current_retry_count=policy.current_retry_count,
|
||
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)
|