"""Scheduled-job handlers for the identity domain. ``identity.data_quality_scan`` — the continuous reconciliation sweep of the Identity Hub. Detects departed-but-can-still-log-in bindings, stale HRIS tombstones, departed managers, missing managers, and persons stuck failing downstream sync; upserts findings (deduped) and auto-resolves ones that disappeared. New high-severity findings page IT admins (dedup-aware) from inside the scan. """ from __future__ import annotations from typing import Any from sqlalchemy.ext.asyncio import AsyncSession from app.scheduled_jobs.registry import register_scheduled_job @register_scheduled_job( key="identity.data_quality_scan", display_name="身份数据质量扫描", description=( "每日扫描身份图谱的数据质量问题:离职/已删除但登录绑定仍可用、" "HRIS 已清除却仍可登录、汇报对象已离职、缺汇报对象、下游同步持续失败。" "发现即 upsert 留痕(按 finding_type+dedup_key 去重),消失即自动解决;" "新增高危发现去重推送 IT 管理员。结果写「身份数据质量」面板。" ), ) async def run_identity_data_quality_scan( db: AsyncSession, config: dict[str, Any], ) -> dict[str, Any]: from app.identity.data_quality_service import IdentityDataQualityService return await IdentityDataQualityService(db).run_scan(trigger_source="cron") @register_scheduled_job( key="identity.department_role_reconcile", display_name="部门角色自动派生对账", description=( "每日对账「部门 → 平台角色」自动派生:Finance BU 成员自动持有 finance " "角色、HR 成员自动持有 hr 角色。只增删 grant_source='derived' 的派生授权," "管理员手动授予的角色永不受影响。正常路径在 HRIS 同步落库时实时派生," "本 cron 仅作兜底并完成存量补齐。" ), ) async def run_department_role_reconcile( db: AsyncSession, config: dict[str, Any], ) -> dict[str, Any]: from app.identity.department_role_sync import reconcile_department_roles summary = await reconcile_department_roles(db) await db.commit() # Per ``feedback_handler_status_must_raise``: surface a missing role # row as a raise so the failure_notifier fires instead of landing a # silent ``status='ok'`` no-op. If finance/hr are missing from # ``auth_roles`` the derivation can never grant them. if summary.get("role_missing"): raise RuntimeError( "identity.department_role_reconcile: role rows missing from " f"auth_roles: {summary['role_missing']} — derivation is a no-op " "until they are re-seeded via the canonical role bootstrap." ) return summary