"""Identity-consumer periodic re-certification (access-cert reuse). Pins: * the snapshot builder makes one item per active consumer, reviewer hint = owner, with a 90-day usage signal (dormant flag) in raw_payload * a ``revoke`` decision disables the consumer (kills PII egress) * a ``keep`` decision stamps last_certified_at, and re-enables a consumer that an earlier lapse had auto-disabled * the lapse sweep auto-disables consumers whose recert item is pending past deadline+grace, sets recert_lapsed_at, and is idempotent """ from __future__ import annotations import uuid from datetime import UTC, datetime, timedelta import pytest from app.domains.auditor import access_certification_service as acs from app.domains.auditor.models import AccessCertificationItem from app.identity.consumers.models import ( CONSUMER_STATUS_ACTIVE, CONSUMER_STATUS_DISABLED, IdentityConsumer, IdentityConsumerUsageDaily, ) from app.identity.person_models import IdentityPerson pytestmark = pytest.mark.asyncio async def _owner(db) -> IdentityPerson: p = IdentityPerson( id=uuid.uuid4(), work_email=f"o-{uuid.uuid4().hex[:8]}@pingcap.com", display_name="Owner", employment_status="active", source_system="successfactors", hris_person_id=f"sf-{uuid.uuid4().hex[:10]}", ) db.add(p) await db.flush() return p async def _consumer(db, owner, *, slug: str | None = None) -> IdentityConsumer: c = IdentityConsumer( name="Puller", slug=slug or f"c-{uuid.uuid4().hex[:6]}", owner_person_id=owner.id, scopes=["directory.persons:read"], granted_fields=["id", "updated_at", "work_email"], filter_config={}, status=CONSUMER_STATUS_ACTIVE, ) db.add(c) await db.flush() return c async def _open_campaign(db, *, due_days_from_now: int = 30): return await acs.create_campaign( db, label=f"recert-{uuid.uuid4().hex[:6]}", campaign_type="identity_consumer", scope_config={}, due_at=datetime.now(UTC) + timedelta(days=due_days_from_now), actor_person_id=None, ) async def _item_for(db, cert, consumer) -> AccessCertificationItem: from sqlalchemy import select return (await db.execute( select(AccessCertificationItem).where( AccessCertificationItem.certification_id == cert.id, AccessCertificationItem.access_ref == str(consumer.id), ) )).scalar_one() async def test_snapshot_builds_items_with_usage_signal(db_session): owner = await _owner(db_session) active = await _consumer(db_session, owner) # one day of usage in the last 90d → not dormant db_session.add(IdentityConsumerUsageDaily( id=uuid.uuid4(), consumer_id=active.id, usage_date=datetime.now(UTC).date(), request_count=42, )) dormant = await _consumer(db_session, owner) await db_session.commit() cert = await _open_campaign(db_session) assert cert.snapshot_item_count == 2 item = await _item_for(db_session, cert, active) assert item.assigned_reviewer_person_id == owner.id assert item.access_type == "identity_consumer" assert item.raw_payload["requests_90d"] == 42 assert item.raw_payload["dormant"] is False ditem = await _item_for(db_session, cert, dormant) assert ditem.raw_payload["dormant"] is True async def test_revoke_disables_consumer(db_session): owner = await _owner(db_session) consumer = await _consumer(db_session, owner) await db_session.commit() cert = await _open_campaign(db_session) item = await _item_for(db_session, cert, consumer) await acs.decide_item( db_session, item_id=item.id, decision="revoke", note="not needed", actor_person_id=owner.id, ) await db_session.flush() refreshed = await db_session.get(IdentityConsumer, consumer.id) assert refreshed.status == CONSUMER_STATUS_DISABLED async def test_keep_stamps_certified(db_session): owner = await _owner(db_session) consumer = await _consumer(db_session, owner) await db_session.commit() cert = await _open_campaign(db_session) item = await _item_for(db_session, cert, consumer) await acs.decide_item( db_session, item_id=item.id, decision="keep", note="still used", actor_person_id=owner.id, ) await db_session.flush() refreshed = await db_session.get(IdentityConsumer, consumer.id) assert refreshed.last_certified_at is not None assert refreshed.status == CONSUMER_STATUS_ACTIVE async def test_lapse_disables_then_keep_reenables(db_session): owner = await _owner(db_session) consumer = await _consumer(db_session, owner) await db_session.commit() # Campaign already 20 days past its deadline. cert = await _open_campaign(db_session, due_days_from_now=-20) item = await _item_for(db_session, cert, consumer) # Lapse sweep auto-disables + flags. res = await acs.lapse_disable_identity_consumers(db_session, trace_id="t") assert res["disabled"] == 1 await db_session.flush() refreshed = await db_session.get(IdentityConsumer, consumer.id) assert refreshed.status == CONSUMER_STATUS_DISABLED assert refreshed.recert_lapsed_at is not None # Idempotent: a second sweep doesn't re-disable. res2 = await acs.lapse_disable_identity_consumers(db_session, trace_id="t") assert res2["disabled"] == 0 # Auditor later keeps → consumer re-enabled + lapse flag cleared. await acs.decide_item( db_session, item_id=item.id, decision="keep", note="confirmed", actor_person_id=owner.id, ) await db_session.flush() refreshed2 = await db_session.get(IdentityConsumer, consumer.id) assert refreshed2.status == CONSUMER_STATUS_ACTIVE assert refreshed2.recert_lapsed_at is None assert refreshed2.last_certified_at is not None async def test_lapse_leaves_admin_disabled_untouched(db_session): """A consumer an admin disabled (recert_lapsed_at NULL) is not a lapse target, and a keep must not silently re-enable it.""" owner = await _owner(db_session) consumer = await _consumer(db_session, owner) consumer.status = CONSUMER_STATUS_DISABLED # admin kill switch await db_session.commit() cert = await _open_campaign(db_session) # disabled consumers aren't in the active snapshot, so build a manual item item = AccessCertificationItem( id=uuid.uuid4(), certification_id=cert.id, person_id=owner.id, access_type="identity_consumer", access_ref=str(consumer.id), access_label=consumer.name, assigned_reviewer_person_id=owner.id, decided_at_deadline=cert.due_at, decision="pending", snapshot_at=datetime.now(UTC).replace(tzinfo=None), ) db_session.add(item) await db_session.flush() await acs.decide_item( db_session, item_id=item.id, decision="keep", note="x", actor_person_id=owner.id, ) await db_session.flush() refreshed = await db_session.get(IdentityConsumer, consumer.id) # last_certified_at stamped, but NOT re-enabled (admin intent preserved). assert refreshed.last_certified_at is not None assert refreshed.status == CONSUMER_STATUS_DISABLED