"""ORM model for ``google_orphan_accounts`` — see migration 103. Every Google Workspace user not matched to an :class:`IdentityPerson` (by ``work_email`` OR any alias) becomes an orphan. Examples: * Shared mailboxes (``finance@``, ``support@``, ``release-bot@``) * Service accounts for CI / monitoring / scheduled scripts * Old contractor accounts that never moved off after the contract ended * External brand-protection registrations (``sales-cn@``) The platform's stance: every Google account that *exists* must have an accountable human. ``owner_user_id`` carries that pointer. The offboarding scan adds an :class:`OffboardingBlocker` of type ``RES_GOOGLE_ORPHAN_OWNER`` for each orphan owned by the leaver, and the leaver process can't reach ``ready`` until the admin re-assigns ownership in the orphan-account UI. Discovery + drift cleanup live in ``app/scheduled_jobs/google_handlers.py:run_google_seat_sync``. """ from __future__ import annotations import uuid from datetime import datetime from sqlalchemy import Boolean, DateTime, ForeignKey, String, Text, UniqueConstraint, func from sqlalchemy.orm import Mapped, mapped_column from app.core.database import Base from app.core.sql_types import GUID as Uuid class GoogleOrphanAccount(Base): """One Google Workspace user not currently bound to a Helix person. Identity: ``primary_email`` is unique within the tenant (Google enforces this); ``google_user_id`` is the stable numeric id used when the email gets renamed. Lifecycle: rows are upserted by ``google.seat_sync`` from the tenant user list. Rows are NEVER hard-deleted by the sync — accounts deleted out-of-band remain here so admins can audit "which orphans disappeared and when". Soft-delete works in two layers: (1) ``last_seen_at`` only advances when the sync observed the row on the current tick; (2) when ``last_seen_at`` falls behind ``now - GRACE_DAYS`` (migration 129, default 3 days), the sync stamps ``deleted_at`` so list queries can hide the row by default while preserving owner + purpose for audit. Admins can still surface deleted rows via ``include_deleted=true``. """ __tablename__ = "google_orphan_accounts" __mapper_args__ = {"eager_defaults": True} __table_args__ = ( UniqueConstraint("primary_email", name="uq_google_orphan_accounts_email"), ) id: Mapped[uuid.UUID] = mapped_column( Uuid, primary_key=True, default=uuid.uuid4, ) primary_email: Mapped[str] = mapped_column(String(255), nullable=False) google_user_id: Mapped[str | None] = mapped_column(String(64)) display_name: Mapped[str | None] = mapped_column(String(255)) org_unit_path: Mapped[str | None] = mapped_column(String(255)) suspended: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) is_admin: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) creation_time: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) last_login_time: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) owner_user_id: Mapped[uuid.UUID | None] = mapped_column( Uuid, ForeignKey("identity_persons.id", ondelete="SET NULL"), ) purpose: Mapped[str | None] = mapped_column(Text) first_seen_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), ) last_seen_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), ) # Set by ``google.seat_sync`` when ``last_seen_at`` falls behind # ``now - GRACE_DAYS`` (migration 129, default 3 days). Owner + # purpose are preserved on the row so admin can audit what # disappeared. List endpoints filter ``deleted_at IS NULL`` by # default; admins opt into the full history via include_deleted. deleted_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now(), ) def __repr__(self) -> str: return f""