feat: 接入远程订阅策略门禁 T-705
Tests / Python 3.11 / Windows (push) Has been cancelled

This commit is contained in:
chengma
2026-07-28 15:42:19 +08:00
parent 593c6f77cb
commit df9da63841
15 changed files with 925 additions and 73 deletions
+205
View File
@@ -0,0 +1,205 @@
"""Remote client-policy parsing and non-sensitive local caching."""
from __future__ import annotations
import json
import os
import tempfile
from dataclasses import dataclass, replace
from datetime import datetime, timezone
from . import appconfig
POLICY_VERSION = 1
CACHE_VERSION = 1
CACHE_FILENAME = "client_policy.json"
class ClientPolicyError(ValueError):
"""Raised when a remote or cached client policy is malformed."""
@dataclass(frozen=True)
class ClientPolicy:
policy_version: int
subscription_check_enabled: bool
subscription_enforcement_enabled: bool
updated_at: str
source: str = "remote"
warning: str = ""
@property
def mode(self) -> str:
if not self.subscription_check_enabled:
return "off"
if self.subscription_enforcement_enabled:
return "enforce"
return "observe"
def to_contract_dict(self) -> dict:
return {
"policy_version": self.policy_version,
"subscription_check_enabled": self.subscription_check_enabled,
"subscription_enforcement_enabled": (
self.subscription_enforcement_enabled
),
"updated_at": self.updated_at,
}
def observation_policy(*, source="fallback", warning="") -> ClientPolicy:
"""Return the fail-open client fallback that still observes entitlement."""
return ClientPolicy(
policy_version=POLICY_VERSION,
subscription_check_enabled=True,
subscription_enforcement_enabled=False,
updated_at="",
source=source,
warning=warning,
)
def parse_client_policy(value, *, source="remote") -> ClientPolicy:
if not isinstance(value, dict):
raise ClientPolicyError("客户端策略不是 JSON 对象")
policy_version = value.get("policy_version")
if type(policy_version) is not int or policy_version != POLICY_VERSION:
raise ClientPolicyError("客户端策略版本不受支持")
check_enabled = value.get("subscription_check_enabled")
enforcement_enabled = value.get("subscription_enforcement_enabled")
if type(check_enabled) is not bool or type(enforcement_enabled) is not bool:
raise ClientPolicyError("客户端订阅策略开关必须是布尔值")
updated_at = _parse_timezone_timestamp(
value.get("updated_at"),
field_name="客户端策略更新时间",
)
warning = ""
if not check_enabled and enforcement_enabled:
enforcement_enabled = False
warning = "客户端订阅策略组合非法,已按关闭模式处理"
return ClientPolicy(
policy_version=policy_version,
subscription_check_enabled=check_enabled,
subscription_enforcement_enabled=enforcement_enabled,
updated_at=updated_at,
source=source,
warning=warning,
)
def policy_cache_path(config=None) -> str:
return appconfig.data_path("config", CACHE_FILENAME, config=config)
def save_cached_policy(
policy: ClientPolicy,
*,
config=None,
path=None,
cached_at=None,
) -> str:
destination = os.path.abspath(path or policy_cache_path(config))
os.makedirs(os.path.dirname(destination), exist_ok=True)
acquired_at = _parse_timezone_timestamp(
cached_at or _now_iso(),
field_name="客户端策略缓存时间",
)
payload = {
"cache_version": CACHE_VERSION,
"cached_at": acquired_at,
"client_policy": policy.to_contract_dict(),
}
temporary_path = ""
try:
with tempfile.NamedTemporaryFile(
mode="w",
encoding="utf-8",
dir=os.path.dirname(destination),
prefix="client-policy-",
suffix=".tmp",
delete=False,
) as handle:
temporary_path = handle.name
json.dump(payload, handle, ensure_ascii=False, indent=2)
handle.write("\n")
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary_path, destination)
finally:
if temporary_path and os.path.exists(temporary_path):
try:
os.remove(temporary_path)
except OSError:
pass
return destination
def load_cached_policy(*, config=None, path=None) -> ClientPolicy:
source_path = os.path.abspath(path or policy_cache_path(config))
try:
with open(source_path, "r", encoding="utf-8") as handle:
payload = json.load(handle)
except (OSError, ValueError) as exc:
raise ClientPolicyError("客户端策略缓存不可用") from exc
cache_version = payload.get("cache_version") if isinstance(payload, dict) else None
if type(cache_version) is not int or cache_version != CACHE_VERSION:
raise ClientPolicyError("客户端策略缓存版本不受支持")
_parse_timezone_timestamp(
payload.get("cached_at"),
field_name="客户端策略缓存时间",
)
return parse_client_policy(payload.get("client_policy"), source="cache")
def resolve_client_policy(
remote_policy,
*,
config=None,
path=None,
) -> ClientPolicy:
"""Prefer a valid live policy, then cache, then observation mode."""
if isinstance(remote_policy, ClientPolicy):
try:
save_cached_policy(remote_policy, config=config, path=path)
except (OSError, ClientPolicyError) as exc:
warning = _join_warning(
remote_policy.warning,
"客户端策略缓存写入失败:%s" % exc,
)
return replace(remote_policy, source="remote", warning=warning)
return replace(remote_policy, source="remote")
try:
return load_cached_policy(config=config, path=path)
except ClientPolicyError as exc:
return observation_policy(
warning="未取得有效远程策略或本地缓存,已使用观察模式:%s" % exc
)
def _parse_timezone_timestamp(value, *, field_name) -> str:
text = str(value or "").strip()
if not text:
raise ClientPolicyError("%s不能为空" % field_name)
try:
parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
except ValueError as exc:
raise ClientPolicyError("%s格式不正确" % field_name) from exc
if parsed.tzinfo is None or parsed.utcoffset() is None:
raise ClientPolicyError("%s必须包含时区" % field_name)
return text
def _now_iso() -> str:
return datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds")
def _join_warning(*messages) -> str:
return ";".join(str(message).strip() for message in messages if str(message).strip())