ForcePilot/backend/package/yuxi/channel/secrets/models.py
Kris 3ce713eb9e feat(secrets): 新增完整的密钥管理工具链
新增了包括密钥加解密、配置健康检查、原子写入、审计、解析器以及 scrubber 在内的完整 secrets 模块,实现了明文密钥检测替换、密钥引用解析和配置安全校验能力
2026-05-21 10:32:14 +08:00

234 lines
6.7 KiB
Python

from __future__ import annotations
import re
from dataclasses import dataclass, field
from enum import StrEnum
from typing import Any
DEFAULT_SECRET_PROVIDER_ALIAS = "default"
SINGLE_VALUE_FILE_REF_ID = "value"
PROVIDER_ALIAS_PATTERN = re.compile(r"^[a-z][a-z0-9_-]{0,63}$")
EXEC_SECRET_REF_ID_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$")
EXEC_SECRET_REF_TRAVERSAL_SEGMENTS = frozenset({".", ".."})
_SECRET_FIELD_NAMES = frozenset(
{
"app_secret",
"bot_token",
"signing_secret",
"encrypt_key",
"verification_token",
"api_key",
"api_secret",
"access_token",
"refresh_token",
"webhook_token",
"key",
"secret",
"token",
"password",
}
)
def is_secret_field_name(key: str) -> bool:
return key.lower() in _SECRET_FIELD_NAMES
class SecretSource(StrEnum):
ENV = "env"
FILE = "file"
EXEC = "exec"
SecretExpectedResolvedValue = str
class ExecSecretRefIdValidationReason(StrEnum):
PATTERN = "pattern"
TRAVERSAL_SEGMENT = "traversal-segment"
@dataclass
class ExecSecretRefIdValidationResult:
ok: bool
reason: ExecSecretRefIdValidationReason | None = None
@classmethod
def valid(cls) -> ExecSecretRefIdValidationResult:
return cls(ok=True)
@classmethod
def invalid(cls, reason: ExecSecretRefIdValidationReason) -> ExecSecretRefIdValidationResult:
return cls(ok=False, reason=reason)
@dataclass
class SecretRef:
source: SecretSource
ref: str
provider: str | None = None
@classmethod
def from_dict(cls, data: dict) -> SecretRef:
provider = data.get("provider")
if provider is not None and not isinstance(provider, str):
raise TypeError(f"SecretRef.provider must be a string or None, got {type(provider).__name__}")
ref = data.get("ref")
if not isinstance(ref, str):
raise TypeError(f"SecretRef.ref must be a string, got {type(ref).__name__}")
source_str = data.get("source", "env")
if not isinstance(source_str, str):
raise TypeError(f"SecretRef.source must be a string, got {type(source_str).__name__}")
return cls(
source=SecretSource(source_str),
ref=ref,
provider=provider if provider else None,
)
@property
def key(self) -> str:
return secret_ref_key(self)
def to_dict(self) -> dict[str, str | None]:
d: dict[str, str | None] = {
"source": self.source.value,
"ref": self.ref,
}
if self.provider:
d["provider"] = self.provider
return d
def secret_ref_key(ref: SecretRef) -> str:
provider = ref.provider or DEFAULT_SECRET_PROVIDER_ALIAS
return f"{ref.source.value}:{provider}:{ref.ref}"
def is_valid_provider_alias(value: str) -> bool:
return bool(PROVIDER_ALIAS_PATTERN.match(value))
def is_valid_exec_secret_ref_id(value: str) -> bool:
return validate_exec_secret_ref_id(value).ok
def validate_exec_secret_ref_id(value: str) -> ExecSecretRefIdValidationResult:
if not EXEC_SECRET_REF_ID_PATTERN.match(value):
return ExecSecretRefIdValidationResult.invalid(ExecSecretRefIdValidationReason.PATTERN)
for segment in value.split("/"):
if segment in EXEC_SECRET_REF_TRAVERSAL_SEGMENTS:
return ExecSecretRefIdValidationResult.invalid(ExecSecretRefIdValidationReason.TRAVERSAL_SEGMENT)
return ExecSecretRefIdValidationResult.valid()
def format_exec_secret_ref_id_validation_message() -> str:
pattern = EXEC_SECRET_REF_ID_PATTERN.pattern
return (
f"Exec secret reference id must match {pattern} "
'and must not include "." or ".." path segments '
'(example: "vault/openai/api-key").'
)
@dataclass
class SecretIssue:
path: str
message: str
ref: SecretRef | None = None
severity: str = "error"
def __str__(self) -> str:
return f"[{self.severity}] {self.path}: {self.message}"
@dataclass
class SecretResolveResult:
resolved: dict[str, Any]
issues: list[SecretIssue] = field(default_factory=list)
warnings: list[SecretIssue] = field(default_factory=list)
@property
def ok(self) -> bool:
return not any(i.severity == "error" for i in self.issues)
@dataclass
class SecretsAuditFinding:
code: str
severity: str
file: str
json_path: str
message: str
provider: str | None = None
profile_id: str | None = None
@dataclass
class SecretsAuditReport:
version: int = 1
status: str = "clean"
resolution: dict[str, Any] = field(default_factory=dict)
files_scanned: list[str] = field(default_factory=list)
summary: dict[str, int] = field(default_factory=dict)
findings: list[SecretsAuditFinding] = field(default_factory=list)
_SECRET_MARKER = "$secret"
_ENV_REF_PATTERN = re.compile(r"^[A-Z][A-Z0-9_]{0,127}$")
_FILE_REF_SEGMENT_PATTERN = re.compile(r"^(?:[^~]|~0|~1)*$")
def is_non_empty_string(value: Any) -> bool:
return isinstance(value, str) and len(value.strip()) > 0
def is_record(value: Any) -> bool:
return isinstance(value, dict)
def is_expected_resolved_secret_value(value: Any, expected: str) -> bool:
if expected == "string":
return is_non_empty_string(value)
return is_non_empty_string(value) or is_record(value)
def assert_expected_resolved_secret_value(value: Any, expected: str, error_message: str) -> None:
if not is_expected_resolved_secret_value(value, expected):
raise ValueError(error_message)
def has_configured_plaintext_secret_value(value: Any, expected: str) -> bool:
if expected == "string":
return is_non_empty_string(value)
return is_non_empty_string(value) or (is_record(value) and len(value) > 0)
def is_secret_ref(value: Any) -> bool:
return isinstance(value, dict) and _SECRET_MARKER in value
def parse_secret_ref(value: dict) -> SecretRef | None:
try:
inner = value[_SECRET_MARKER]
return SecretRef.from_dict(inner)
except (KeyError, TypeError, ValueError):
return None
def validate_ref_id(source: SecretSource, ref: str) -> str | None:
if source == SecretSource.ENV:
if not _ENV_REF_PATTERN.match(ref):
return f"Invalid env ref '{ref}': must match /^[A-Z][A-Z0-9_]{{0,127}}$/"
elif source == SecretSource.FILE:
if ref == SINGLE_VALUE_FILE_REF_ID:
return None
if not ref.startswith("/"):
return f"Invalid JSON pointer ref '{ref}': must start with '/'"
segments = ref[1:].split("/")
if not all(_FILE_REF_SEGMENT_PATTERN.match(s) for s in segments):
return f"Invalid JSON pointer ref '{ref}': contains invalid escape sequence"
return None