"""``data_access.grant_lifecycle`` — the daily DAX grant lifecycle tick (migration 606). Two-transaction posture (same as ``apikey.idle_check``): the sweep (stamps, reminders, revokes, events) commits BEFORE the IT digest goes out, so a notification-provider failure cannot roll back a day's lifecycle transitions and make tomorrow's reminder arithmetic lie. The sweep never raises on an individual binding (``summary["errors"]`` counts them); it raises only when the settings row itself is unreadable — a systemic condition the operator must see as a failed run. """ from __future__ import annotations import logging import uuid from datetime import UTC, datetime from typing import Any from zoneinfo import ZoneInfo from sqlalchemy.ext.asyncio import AsyncSession from app.scheduled_jobs.registry import register_scheduled_job logger = logging.getLogger(__name__) DATA_ACCESS_GRANT_LIFECYCLE_HANDLER = "data_access.grant_lifecycle" @register_scheduled_job( key=DATA_ACCESS_GRANT_LIFECYCLE_HANDLER, display_name="DAX 授权 · 到期 / 续期 / 负责人确认", description=( "每日 09:30(Asia/Shanghai)。① 给缺到期日的个人 DAX 授权补 expires_at;② 按阶梯" "(默认 T-14 / T-7 / T-1)提醒持有人续期 —— 员工推送由 data_access.grant_lifecycle." "employee_push_enabled 控制,默认关,关着时只进 IT 汇总;③ 到期:auto_revoke_on_expiry" " 开着才回收(走 DAX 自己的 revoke → apply-absent reconciler),关着只在 IT 汇总里" "标「已过期待处理」;④ 服务账号授权的半年度负责人确认:到期开确认、每 7 天催、" "逾期超过宽限且 auto_revoke_on_confirmation_lapse 开着才回收。每天一张 IT 汇总卡。" ), ) async def run_data_access_grant_lifecycle( db: AsyncSession, config: dict[str, Any], ) -> dict[str, Any]: del config from app.core.database import async_session_factory # noqa: PLC0415 from app.domains.data_access.lifecycle import ( # noqa: PLC0415 DataAccessLifecycleService, load_lifecycle_settings, send_it_digest, ) trace_id = f"data_access_grant_lifecycle:{uuid.uuid4().hex}" now = datetime.now(UTC) day = datetime.now(ZoneInfo("Asia/Shanghai")).date().isoformat() async with async_session_factory() as session: settings = await load_lifecycle_settings(session) summary = await DataAccessLifecycleService(session).run_daily( now=now, settings=settings, trace_id=trace_id, ) await session.commit() digest = summary.get("digest") or {} async with async_session_factory() as digest_session: summary["it_digest_recipients"] = await send_it_digest( digest_session, day=day, summary=summary, settings=settings, trace_id=trace_id, ) await digest_session.commit() summary["digest_counts"] = {k: len(v) for k, v in digest.items()} summary.pop("digest", None) summary["settings"] = { "employee_push_enabled": settings.employee_push_enabled, "auto_revoke_on_expiry": settings.auto_revoke_on_expiry, "owner_confirmation_push_enabled": settings.owner_confirmation_push_enabled, "auto_revoke_on_confirmation_lapse": settings.auto_revoke_on_confirmation_lapse, } return summary