Files
group_fqcd_jr/app/service/advisor_rollout_service.py
T

64 lines
2.5 KiB
Python
Raw Normal View History

"""投顾灰度发布闸门。
灰度配置使用环境变量,避免为发布控制改动冻结的数据库基线。白名单只允许客户
身份进入投顾业务;管理员保留审核、发布和故障处置权限。未命中时统一复用权限拒绝
错误,不向客户暴露灰度配置细节。
"""
from datetime import UTC, datetime
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.dependencies.auth import build_request_context
from app.api.dependencies.database import get_session
from app.core.config import get_settings
from app.core.contracts import RequestContext
from app.core.errors import ForbiddenAgentError
from app.model.audit import InteractionAudit
class AdvisorRolloutService:
ADMIN_ROLES = frozenset({"admin", "super_admin"})
def __init__(self, session: AsyncSession | None = None) -> None:
self.session = session
@staticmethod
def customer_ids(raw: str) -> frozenset[str]:
return frozenset(item.strip() for item in raw.split(",") if item.strip())
@classmethod
def is_allowed(cls, context: RequestContext) -> bool:
settings = get_settings()
if not settings.advisor_rollout_enabled:
return True
if cls.ADMIN_ROLES.intersection(context.roles):
return True
allowed = cls.customer_ids(settings.advisor_rollout_customer_ids)
identities = {context.user_id, *context.customer_ids}
return bool(allowed.intersection(identities))
async def ensure_allowed(self, context: RequestContext) -> None:
if self.is_allowed(context):
return
if self.session is not None:
self.session.add(InteractionAudit(
actor_type="user",
actor_id=int(context.user_id),
portal=context.portal,
action_type="advisor.rollout_denied",
detail={"trace_id": context.trace_id, "reason": "not_in_rollout"},
created_at=datetime.now(UTC).replace(tzinfo=None),
))
await self.session.commit()
raise ForbiddenAgentError("当前投顾功能尚未对该账号开放")
async def enforce_advisor_rollout(
context: RequestContext = Depends(build_request_context), # noqa: B008
session: AsyncSession = Depends(get_session), # noqa: B008
) -> None:
"""FastAPI 依赖:保护投顾业务路由,认证依赖先完成身份解析。"""
await AdvisorRolloutService(session).ensure_allowed(context)