"""By-id GET endpoints for IAM users + object-storage buckets. Added so the mobile detail deep-links resolve by id instead of paging the list and ``.find()``-ing (which silently 404s past the first page). These tests pin the SECURITY-sensitive scoping: - IAM user by-id: cloud readers see any row; a plain employee sees ONLY their own (``owner_person_id == self``) — never someone else's (which would leak ARN / policies / groups / MFA). - Object-storage by-id: same ``can_read_cloud`` gate as the list (OSS rows carry no owner; bucket names + ACL flags are admin/auditor-only). """ import uuid import pytest import pytest_asyncio from sqlalchemy.ext.asyncio import AsyncSession from app.domains.cmdb.models import CmdbIamUser, CmdbObjectStorage from app.identity.person_models import IdentityPerson from app.integrations.models import Integration async def _other_person(db_session) -> IdentityPerson: """A real person to own a row in the not-mine / admin cases — ``cmdb_iam_users.owner_person_id`` has a FK to identity_persons, so a random uuid would 1452 at insert.""" p = IdentityPerson( id=uuid.uuid4(), work_email=f"{uuid.uuid4().hex[:8]}@test.com", display_name="Other Owner", employment_status="active", source_system="successfactors", hris_person_id=f"sf-{uuid.uuid4().hex[:8]}", ) db_session.add(p) await db_session.flush() return p @pytest_asyncio.fixture async def provider(db_session: AsyncSession) -> Integration: integ = Integration( id=uuid.uuid4(), kind="cloud", provider="aws", name="pytest-iam-oss-aws", slug=f"pytest-iam-oss-{uuid.uuid4().hex[:8]}", enabled=True, is_primary=False, capabilities=["iam", "object_storage"], ) db_session.add(integ) await db_session.commit() return integ async def _iam(db_session, provider, *, owner_id=None) -> CmdbIamUser: row = CmdbIamUser( integration_id=str(provider.id), user_name=f"u-{uuid.uuid4().hex[:8]}", sub_account_id="", arn=f"arn:aws:iam::123:user/{uuid.uuid4().hex[:6]}", owner_person_id=str(owner_id) if owner_id else None, ) db_session.add(row) await db_session.flush() return row async def _oss(db_session, provider) -> CmdbObjectStorage: row = CmdbObjectStorage( integration_id=str(provider.id), bucket_name=f"bucket-{uuid.uuid4().hex[:8]}", region="us-east-1", status="active", ) db_session.add(row) await db_session.flush() return row @pytest.mark.asyncio async def test_employee_gets_own_iam_user(client, employee_user, db_session, provider): user, token = employee_user row = await _iam(db_session, provider, owner_id=user.id) await db_session.commit() resp = await client.get( f"/api/v1/cmdb/iam-users/{row.id}", headers={"Authorization": f"Bearer {token}"}, ) assert resp.status_code == 200, resp.text assert resp.json()["data"]["id"] == str(row.id) @pytest.mark.asyncio async def test_employee_cannot_see_others_iam_user(client, employee_user, db_session, provider): _user, token = employee_user # Owned by a DIFFERENT (real) person. other = await _other_person(db_session) row = await _iam(db_session, provider, owner_id=other.id) await db_session.commit() resp = await client.get( f"/api/v1/cmdb/iam-users/{row.id}", headers={"Authorization": f"Bearer {token}"}, ) # Scope demotion -> 404 (not 200, not 403): the row must be invisible. assert resp.status_code == 404, resp.text @pytest.mark.asyncio async def test_admin_sees_any_iam_user(client, admin_user, db_session, provider): _user, token = admin_user other = await _other_person(db_session) row = await _iam(db_session, provider, owner_id=other.id) await db_session.commit() resp = await client.get( f"/api/v1/cmdb/iam-users/{row.id}", headers={"Authorization": f"Bearer {token}"}, ) assert resp.status_code == 200, resp.text @pytest.mark.asyncio async def test_object_storage_by_id_reader_gate( client, employee_user, admin_user, db_session, provider, ): row = await _oss(db_session, provider) await db_session.commit() # Plain employee: OSS has no owner -> can_read_cloud gate -> 403. _eu, etoken = employee_user r1 = await client.get( f"/api/v1/cmdb/object-storage/{row.id}", headers={"Authorization": f"Bearer {etoken}"}, ) assert r1.status_code == 403, r1.text # Admin (cloud reader): 200. _au, atoken = admin_user r2 = await client.get( f"/api/v1/cmdb/object-storage/{row.id}", headers={"Authorization": f"Bearer {atoken}"}, ) assert r2.status_code == 200, r2.text assert r2.json()["data"]["id"] == str(row.id)