"""When the per-driver ``list_scope_grants`` returns nothing — broken auth scope (AWS SSO mode without Identity Center configured), CAM ListAttachedUserAllPolicies silently returning [] (Tencent in prod), RAM master-only roles (AliCloud sub-account RAM users live in sub-accounts the master AK can't read directly) — we still want to populate ``cmdb_cloud_access_grants`` from the IAM-user table that's already kept fresh by the IAM resource sync. Every test below sets up the SAME shape: * 1 integration with 1 active scope. * 1 ``cmdb_iam_users`` row whose ``user_name`` resolves to a Helix person via :func:`_email_from_iam_user` and whose ``attached_policies`` is non-empty. * A ``_FakeDriver`` whose ``list_scope_grants`` returns ``[]`` — proving the fallback (NOT the driver) is what produced the rows. """ from __future__ import annotations import uuid from contextlib import asynccontextmanager from unittest.mock import patch import pytest from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.domains.cmdb.lifecycle_service import ( _email_from_iam_user, sync_grants_for_integration, ) from app.domains.cmdb.models import ( GRANT_STATUS_ACTIVE, SCOPE_STATUS_ACTIVE, CmdbCloudAccessGrant, CmdbCloudScope, CmdbIamUser, ) from app.identity.person_models import IdentityPerson from app.integrations.driver_base import CloudScopeRef from app.integrations.models import Integration async def _make_person(db: AsyncSession, email: str) -> IdentityPerson: p = IdentityPerson( id=uuid.uuid4(), work_email=email, display_name=email.split("@")[0], employment_status="active", source_system="successfactors", hris_person_id=f"sf-{uuid.uuid4().hex[:8]}", ) db.add(p) await db.commit() return p async def _make_integration(db: AsyncSession, provider: str) -> Integration: row = Integration( id=uuid.uuid4(), kind="cloud", provider=provider, name=f"{provider}-iam-fallback", slug=f"{provider}-iam-fallback-{uuid.uuid4().hex[:6]}", enabled=True, is_primary=False, capabilities=["servers"], ) db.add(row) await db.commit() return row async def _make_scope( db: AsyncSession, integration: Integration, ) -> CmdbCloudScope: s = CmdbCloudScope( id=uuid.uuid4(), integration_id=str(integration.id), scope_type="aws_account", external_id="123456789012", display_name="test-scope", status=SCOPE_STATUS_ACTIVE, ) db.add(s) await db.commit() return s async def _make_iam_user( db: AsyncSession, integration: Integration, *, user_name: str, tags: dict | None = None, attached_policies: list[dict] | None = None, ) -> CmdbIamUser: u = CmdbIamUser( id=uuid.uuid4(), integration_id=str(integration.id), user_name=user_name, arn=f"arn:aws:iam::123456789012:user/{user_name}", user_id_external=f"AIDA{uuid.uuid4().hex[:16].upper()}", attached_policies=attached_policies or [], groups=[], mfa_enabled=False, tags=tags or {}, raw_data={}, ) db.add(u) await db.commit() return u def _patch_session_factory(db_session: AsyncSession): @asynccontextmanager async def _factory(): yield db_session return patch( "app.domains.cmdb.lifecycle_service.async_session_factory", new=_factory, ) class _EmptyDriver: """A driver whose ``list_scope_grants`` always returns ``[]`` — simulates Tencent / AWS-without-SSO / RAM-master-only behavior. The fallback should kick in when this driver yields nothing.""" provider = "aws" grants_scope_wide = True owner_role_keys = frozenset() async def list_scope_grants(self, scope: CloudScopeRef): return [] async def close(self) -> None: pass def _patch_driver(driver): async def _loader(session, integration): return driver return patch( "app.domains.cmdb.lifecycle_service._load_cloud_driver", side_effect=_loader, ) # ── Email-resolution unit tests (no DB needed) ─────────────────── def test_email_from_user_name_with_at() -> None: assert _email_from_iam_user("shuai.feng@pingcap.com", None) == "shuai.feng@pingcap.com" # Lowercased. assert _email_from_iam_user("Shuai.Feng@PingCAP.COM", None) == "shuai.feng@pingcap.com" def test_email_from_owner_tag_when_username_lacks_at() -> None: """Production AWS truth: half the IAM users are bare names like ``li.da`` / ``breezewish`` / ``mem9`` and their email lives only in the ``Owner`` tag (capital O).""" assert _email_from_iam_user("li.da", {"Owner": "li.da@pingcap.com"}) == "li.da@pingcap.com" def test_email_tag_takes_precedence_over_owner_tag() -> None: """When both ``email`` and ``owner`` exist, ``email`` is the explicit/canonical one; ``Owner`` is the fallback/legacy.""" tags = {"email": "primary@x.com", "Owner": "secondary@x.com"} assert _email_from_iam_user("u", tags) == "primary@x.com" def test_owner_tag_skipped_when_not_email() -> None: """``Owner: BedrockAPIKey-d6l8`` is what AWS service-account-style IAM users have — it's a label, not an email. We must NOT synthesise a fake email from it (creating a grant that resolves to nobody).""" assert _email_from_iam_user("svc-key", {"Owner": "BedrockAPIKey-d6l8"}) is None def test_no_email_no_at_no_tag_returns_none() -> None: assert _email_from_iam_user("breezewish", {}) is None assert _email_from_iam_user("breezewish", None) is None # ── Service-level fallback tests ───────────────────────────────── @pytest.mark.asyncio async def test_fallback_creates_grants_when_driver_returns_empty( db_session: AsyncSession, ) -> None: """The full path: driver returns nothing, fallback walks ``cmdb_iam_users``, synthesises grants, applies them. Grant rows end up with ``source='discovered'``.""" person = await _make_person(db_session, "shuai.feng@pingcap.com") integration = await _make_integration(db_session, "aws") scope = await _make_scope(db_session, integration) await _make_iam_user( db_session, integration, user_name="shuai.feng@pingcap.com", attached_policies=[ {"name": "AdministratorAccess", "arn": "arn:aws:iam::aws:policy/AdministratorAccess"}, {"name": "ReadOnlyAccess", "arn": "arn:aws:iam::aws:policy/ReadOnlyAccess"}, ], ) with _patch_session_factory(db_session), _patch_driver(_EmptyDriver()): result = await sync_grants_for_integration(str(integration.id)) # Fallback contributed 2 grants. assert result["upserted"] == 2 assert result["status"] == "success" grants = (await db_session.execute( select(CmdbCloudAccessGrant).where( CmdbCloudAccessGrant.scope_id == str(scope.id), ) )).scalars().all() assert len(grants) == 2 assert all(g.person_id == str(person.id) for g in grants) assert all(g.status == GRANT_STATUS_ACTIVE for g in grants) assert all(g.source == "discovered" for g in grants) role_keys = sorted(g.role_key for g in grants) # ARN-style keys preferred when present. assert role_keys == [ "arn:aws:iam::aws:policy/AdministratorAccess", "arn:aws:iam::aws:policy/ReadOnlyAccess", ] @pytest.mark.asyncio async def test_fallback_resolves_owner_tag_email( db_session: AsyncSession, ) -> None: """The exact prod-AWS shape: user_name doesn't have ``@``, but ``tags.Owner`` does. Without this fallback the grant would never resolve to the Helix person.""" person = await _make_person(db_session, "li.da@pingcap.com") integration = await _make_integration(db_session, "aws") _scope = await _make_scope(db_session, integration) await _make_iam_user( db_session, integration, user_name="li.da", # no @ tags={"Owner": "li.da@pingcap.com"}, attached_policies=[ {"name": "PowerUserAccess", "arn": "arn:aws:iam::aws:policy/PowerUserAccess"}, ], ) with _patch_session_factory(db_session), _patch_driver(_EmptyDriver()): result = await sync_grants_for_integration(str(integration.id)) assert result["upserted"] == 1 grants = (await db_session.execute( select(CmdbCloudAccessGrant) )).scalars().all() assert len(grants) == 1 assert grants[0].person_id == str(person.id) @pytest.mark.asyncio async def test_fallback_skipped_when_driver_returned_grants( db_session: AsyncSession, ) -> None: """The fallback is a *fallback* — when the driver already populated grants, we must NOT also synthesize from IAM users (that would double-count or shadow the driver's authoritative payload).""" from app.integrations.driver_base import DiscoveredGrant _person = await _make_person(db_session, "shuai.feng@pingcap.com") integration = await _make_integration(db_session, "aws") _scope = await _make_scope(db_session, integration) await _make_iam_user( db_session, integration, user_name="shuai.feng@pingcap.com", attached_policies=[ {"name": "AdministratorAccess", "arn": "arn:aws:iam::aws:policy/AdministratorAccess"}, {"name": "ReadOnlyAccess", "arn": "arn:aws:iam::aws:policy/ReadOnlyAccess"}, ], ) class _DriverWithOneGrant(_EmptyDriver): async def list_scope_grants(self, scope: CloudScopeRef): return [DiscoveredGrant( principal_email="shuai.feng@pingcap.com", principal_type="user", role_key="arn:aws:iam::aws:policy/AdministratorAccess", role_display_name="AdministratorAccess", external_ref="driver-source", scope=scope, granted_at=None, raw={}, )] with _patch_session_factory(db_session), \ _patch_driver(_DriverWithOneGrant()): result = await sync_grants_for_integration(str(integration.id)) # Driver returned 1; fallback NOT triggered. Result: 1 grant only. assert result["upserted"] == 1 grants = (await db_session.execute( select(CmdbCloudAccessGrant) )).scalars().all() assert len(grants) == 1 # Exactly the driver-provided one, not the IAM-table-derived one. assert grants[0].external_ref == "driver-source" @pytest.mark.asyncio async def test_fallback_no_iam_users_no_grants( db_session: AsyncSession, ) -> None: """No IAM users + empty driver = honestly 0 grants. The fallback can't conjure data from nothing — and shouldn't error.""" integration = await _make_integration(db_session, "aws") await _make_scope(db_session, integration) with _patch_session_factory(db_session), _patch_driver(_EmptyDriver()): result = await sync_grants_for_integration(str(integration.id)) assert result["upserted"] == 0 assert result["status"] == "success" @pytest.mark.asyncio async def test_fallback_skips_iam_users_with_no_email( db_session: AsyncSession, ) -> None: """An IAM user whose user_name has no ``@`` AND no Owner email tag (e.g. an automation key like ``BedrockAPIKey-d6l8``) is legitimately unmappable to a Helix person — we skip it rather than inventing a fake email.""" integration = await _make_integration(db_session, "aws") await _make_scope(db_session, integration) await _make_iam_user( db_session, integration, user_name="BedrockAPIKey-d6l8", tags={"Owner": "BedrockAPIKey-d6l8"}, # not an email attached_policies=[{"name": "X", "arn": "arn:aws:iam::aws:policy/X"}], ) with _patch_session_factory(db_session), _patch_driver(_EmptyDriver()): result = await sync_grants_for_integration(str(integration.id)) assert result["upserted"] == 0 grants = (await db_session.execute( select(CmdbCloudAccessGrant) )).scalars().all() assert grants == [] @pytest.mark.asyncio async def test_fallback_unresolved_email_is_counted( db_session: AsyncSession, ) -> None: """An IAM user whose email is well-formed but doesn't match any Helix person (left the company before HRIS sync, contractor account, …) gets counted in ``skipped_unresolved`` so the metric reflects reality. The grant is NOT created.""" integration = await _make_integration(db_session, "aws") await _make_scope(db_session, integration) # No Helix person matches this email. await _make_iam_user( db_session, integration, user_name="someone-not-in-helix@pingcap.com", attached_policies=[ {"name": "X", "arn": "arn:aws:iam::aws:policy/X"}, ], ) with _patch_session_factory(db_session), _patch_driver(_EmptyDriver()): result = await sync_grants_for_integration(str(integration.id)) assert result["upserted"] == 0 assert result["skipped_unresolved"] == 1