"""API tests for the expiry-alert acknowledge / snooze endpoints (authenticated + one-click action-token magic link), plus the one-click cloud-grant renewal magic link that reuses the same action-token primitive.""" from __future__ import annotations import uuid from datetime import UTC, datetime, timedelta import pytest from httpx import AsyncClient from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.security import create_action_token from app.domains.cmdb.models import ( CERT_SOURCE_PROBED, GRANT_STATUS_ACTIVE, SCOPE_STATUS_ACTIVE, CmdbCertificate, CmdbCloudAccessGrant, CmdbCloudScope, CmdbDnsRecord, CmdbDomain, CmdbExpiryAlertState, ) from app.identity.person_models import IdentityPerson from app.integrations.models import Integration async def _owned_cert(db, owner_id) -> str: """Create a probed cert whose host's DNS record is owned by ``owner_id`` so the live resolver attributes it to them. Returns the cert id.""" integ = Integration( id=uuid.uuid4(), kind="cloud", provider="aliyun", name="pytest-ack", slug=f"ack-{uuid.uuid4().hex[:8]}", enabled=True, is_primary=False, capabilities=["dns_records"], ) db.add(integ) await db.flush() db.add(CmdbDomain( id=uuid.uuid4(), integration_id=integ.id, domain_name="ack.example.com", owner_person_id=None, )) db.add(CmdbDnsRecord( integration_id=integ.id, zone_name="ack.example.com", record_id="r1", name="app", record_type="A", value="1.2.3.4", status="enable", owner_person_id=str(owner_id), )) cert = CmdbCertificate( id=uuid.uuid4(), dedup_key="probed:app.ack.example.com:443", source=CERT_SOURCE_PROBED, probe_host="app.ack.example.com", common_name="app.ack.example.com", not_after=datetime.now(UTC) + timedelta(days=5), status="expiring", first_seen_at=datetime.now(UTC), ) db.add(cert) await db.commit() return str(cert.id) @pytest.mark.asyncio async def test_owner_can_acknowledge( client: AsyncClient, db_session: AsyncSession, employee_user, employee_headers, ): emp, _ = employee_user cert_id = await _owned_cert(db_session, emp.id) resp = await client.post( f"/api/v1/cmdb/expiry/certificate/{cert_id}/acknowledge?days=10", headers=employee_headers, ) assert resp.status_code == 200, resp.text until = resp.json()["data"]["acknowledged_until"] assert until # a date string row = (await db_session.execute( select(CmdbExpiryAlertState).where( CmdbExpiryAlertState.resource_id == cert_id, ) )).scalar_one() assert row.acknowledged_until is not None assert str(row.acknowledged_by_person_id) == str(emp.id) @pytest.mark.asyncio async def test_non_owner_forbidden( client: AsyncClient, db_session: AsyncSession, employee_user, employee_headers, ): # A cert owned by SOMEONE ELSE → the employee may not ack it. other = uuid.uuid4() from app.identity.person_models import IdentityPerson db_session.add(IdentityPerson( id=other, source_system="successfactors", hris_person_id=f"sf-{uuid.uuid4().hex[:8]}", display_name="Other", work_email=f"other-{uuid.uuid4().hex[:6]}@pingcap.com", employment_status="active", )) await db_session.flush() cert_id = await _owned_cert(db_session, other) resp = await client.post( f"/api/v1/cmdb/expiry/certificate/{cert_id}/acknowledge", headers=employee_headers, ) assert resp.status_code == 403 @pytest.mark.asyncio async def test_action_token_one_click_ack( client: AsyncClient, db_session: AsyncSession, employee_user, ): emp, _ = employee_user cert_id = await _owned_cert(db_session, emp.id) token = create_action_token( purpose="expiry_ack", subject_id=str(emp.id), resource_id=f"certificate:{cert_id}", ) # No auth header — the signed token authorises the action. resp = await client.post(f"/api/v1/cmdb/expiry/ack?token={token}") assert resp.status_code == 200, resp.text assert resp.json()["data"]["resource_type"] == "certificate" # A garbage token is rejected. bad = await client.post("/api/v1/cmdb/expiry/ack?token=not-a-real-token") assert bad.status_code == 403 # ── One-click cloud-grant renewal magic link ───────────────────────── async def _make_grant( db: AsyncSession, holder_id, *, expires_in_days: int = 14, environment: str = "dev", ) -> CmdbCloudAccessGrant: """An active scope grant expiring soon, held by ``holder_id``.""" integ = Integration( id=uuid.uuid4(), kind="cloud", provider="azure", name=f"azure-{uuid.uuid4().hex[:6]}", slug=f"azure-{uuid.uuid4().hex[:6]}", enabled=True, is_primary=False, capabilities=["servers"], ) db.add(integ) await db.flush() scope = CmdbCloudScope( id=uuid.uuid4(), integration_id=str(integ.id), scope_type="azure_subscription", external_id=uuid.uuid4().hex[:16], display_name=f"sub-{uuid.uuid4().hex[:6]}", status=SCOPE_STATUS_ACTIVE, first_seen_at=datetime.now(UTC), last_synced_at=datetime.now(UTC), environment=environment, ) db.add(scope) await db.flush() grant = CmdbCloudAccessGrant( id=uuid.uuid4(), scope_id=str(scope.id), person_id=str(holder_id), external_principal_email=f"g-{uuid.uuid4().hex[:6]}@pingcap.com", role_key="reader", role_display_name="Reader", status=GRANT_STATUS_ACTIVE, granted_at=datetime.now(UTC), expires_at=datetime.now(UTC).replace(tzinfo=None) + timedelta(days=expires_in_days), external_ref=f"ra-{uuid.uuid4().hex[:8]}", ) db.add(grant) await db.commit() return grant @pytest.mark.asyncio async def test_grant_renew_token_one_click_files_a_request( client: AsyncClient, db_session: AsyncSession, employee_user, ): """Holder clicks the magic link → a renewal REQUEST is filed, no auth header required. The click does NOT extend anything by itself. Behaviour change 2026-07-29: this endpoint used to apply the renewal on click. Renewal now needs the scope owner's approval — 763 renewals in three months had been executed by holders alone, so the 90-day expiry cycle was a control nobody exercised. The token still only authorises the holder to act on their OWN access, which is exactly what "ask for an extension" is, so the no-login property is unchanged. """ from app.domains.cmdb.models import ( RENEWAL_STATUS_PENDING_OWNER, CmdbCloudGrantRenewalRequest, ) emp, _ = employee_user grant = await _make_grant(db_session, emp.id, expires_in_days=3) grant_id = str(grant.id) expiry_before = grant.expires_at token = create_action_token( purpose="cloud_grant_renew", subject_id=str(emp.id), resource_id=grant_id, ) resp = await client.post(f"/api/v1/cmdb/grants/renew-by-token?token={token}") assert resp.status_code == 200, resp.text data = resp.json()["data"] assert data["renewal_requested"] is True assert data["request_group_id"] assert data["scope_display_name"] db_session.expire_all() row = await db_session.get(CmdbCloudAccessGrant, uuid.UUID(grant_id)) assert row.status == GRANT_STATUS_ACTIVE # Untouched: the click asked, it did not grant. Second granularity — # MySQL DATETIME drops sub-second precision on round-trip. before_naive = ( expiry_before.astimezone(UTC).replace(tzinfo=None) if expiry_before.tzinfo else expiry_before ) assert abs((row.expires_at - before_naive).total_seconds()) < 2 reqs = (await db_session.execute( select(CmdbCloudGrantRenewalRequest).where( CmdbCloudGrantRenewalRequest.grant_id == grant_id, ) )).scalars().all() assert len(reqs) == 1 assert reqs[0].status == RENEWAL_STATUS_PENDING_OWNER # Scoped to the ONE grant the card named — a card is minted per grant, # so widening it to every role on the scope would be an escalation. assert str(reqs[0].grant_id) == grant_id @pytest.mark.asyncio async def test_grant_renew_token_prod_scope_reassures_not_errors( client: AsyncClient, db_session: AsyncSession, employee_user, ): """Clicking a renew link for a PROD scope must NOT hit the prod write lock and surface the scary "link invalid/expired" page. Prod access is never auto-recycled and prod writes are default- locked, so a renew would always 403 on the lock. The endpoint short-circuits to a reassuring ``prod_no_renew`` response and leaves the grant untouched (still active, expiry unchanged). Regression for the 2026-07-16 ``dbaas-prod-clinic`` feedback.""" emp, _ = employee_user grant = await _make_grant( db_session, emp.id, expires_in_days=14, environment="prod", ) grant_id = str(grant.id) token = create_action_token( purpose="cloud_grant_renew", subject_id=str(emp.id), resource_id=grant_id, ) resp = await client.post(f"/api/v1/cmdb/grants/renew-by-token?token={token}") assert resp.status_code == 200, resp.text data = resp.json()["data"] assert data["prod_no_renew"] is True assert data["already_renewed"] is True db_session.expire_all() row = await db_session.get(CmdbCloudAccessGrant, uuid.UUID(grant_id)) # Grant untouched — no renewal, no lock error, access retained. The # short-circuit never renews, so expiry stays near-term (was +14d) # and is NOT pushed out to +90d, and last_renewed_at stays NULL. assert row.status == GRANT_STATUS_ACTIVE assert row.last_renewed_at is None assert row.expires_at < datetime.now(UTC).replace(tzinfo=None) + timedelta( days=30, ) @pytest.mark.asyncio async def test_grant_renew_token_single_use( client: AsyncClient, db_session: AsyncSession, employee_user, ): """The magic link is single-use: the first click renews, a second click of the SAME link is an idempotent no-op (``already_renewed``) — it must NOT re-pin expiry to today+90 again, and must NOT surface the scary "link invalid/expired" error page (regression: double-tap / webview re-open previously either re-renewed or raced the per-grant lock into a 409).""" emp, _ = employee_user grant = await _make_grant(db_session, emp.id, expires_in_days=3) grant_id = str(grant.id) token = create_action_token( purpose="cloud_grant_renew", subject_id=str(emp.id), resource_id=grant_id, ) first = await client.post(f"/api/v1/cmdb/grants/renew-by-token?token={token}") assert first.status_code == 200, first.text assert first.json()["data"]["already_renewed"] is False db_session.expire_all() row = await db_session.get(CmdbCloudAccessGrant, uuid.UUID(grant_id)) first_expiry = row.expires_at first_renewed_at = row.last_renewed_at # Second click of the same link → idempotent no-op. second = await client.post(f"/api/v1/cmdb/grants/renew-by-token?token={token}") assert second.status_code == 200, second.text body = second.json()["data"] assert body["already_renewed"] is True assert body["expires_at"] is not None db_session.expire_all() row2 = await db_session.get(CmdbCloudAccessGrant, uuid.UUID(grant_id)) # Expiry and last-renewed timestamp are untouched by the replay. assert row2.expires_at == first_expiry assert row2.last_renewed_at == first_renewed_at @pytest.mark.asyncio async def test_grant_renew_token_wrong_subject_forbidden( client: AsyncClient, db_session: AsyncSession, employee_user, ): """A token whose subject is not the grant holder is refused — the magic link never lets one person extend someone else's access.""" emp, _ = employee_user other = IdentityPerson( id=uuid.uuid4(), source_system="successfactors", hris_person_id=f"sf-{uuid.uuid4().hex[:8]}", display_name="Other", work_email=f"other-{uuid.uuid4().hex[:6]}@pingcap.com", employment_status="active", ) db_session.add(other) await db_session.flush() grant = await _make_grant(db_session, emp.id) token = create_action_token( purpose="cloud_grant_renew", subject_id=str(other.id), resource_id=str(grant.id), ) resp = await client.post(f"/api/v1/cmdb/grants/renew-by-token?token={token}") assert resp.status_code == 403 @pytest.mark.asyncio async def test_grant_renew_token_leaver_forbidden( client: AsyncClient, db_session: AsyncSession, ): """A leaver clicking a stale renew link is refused even while the token signature is still valid.""" leaver = IdentityPerson( id=uuid.uuid4(), source_system="successfactors", hris_person_id=f"sf-{uuid.uuid4().hex[:8]}", display_name="Leaver", work_email=f"leaver-{uuid.uuid4().hex[:6]}@pingcap.com", employment_status="left", ) db_session.add(leaver) await db_session.flush() grant = await _make_grant(db_session, leaver.id) token = create_action_token( purpose="cloud_grant_renew", subject_id=str(leaver.id), resource_id=str(grant.id), ) resp = await client.post(f"/api/v1/cmdb/grants/renew-by-token?token={token}") assert resp.status_code == 403 @pytest.mark.asyncio async def test_grant_renew_token_garbage_and_wrong_purpose( client: AsyncClient, db_session: AsyncSession, employee_user, ): """Garbage tokens and cross-purpose tokens (e.g. an expiry_ack token replayed against the renew endpoint) are both refused.""" emp, _ = employee_user grant = await _make_grant(db_session, emp.id) bad = await client.post( "/api/v1/cmdb/grants/renew-by-token?token=not-a-real-token", ) assert bad.status_code == 403 cross = create_action_token( purpose="expiry_ack", subject_id=str(emp.id), resource_id=str(grant.id), ) resp = await client.post(f"/api/v1/cmdb/grants/renew-by-token?token={cross}") assert resp.status_code == 403