from __future__ import annotations import pytest from app.domains.data_access.service import ( DataAccessRequesterAuthorizationError, DataAccessService, ) from app.domains.email_group.models import EmailGroup, EmailGroupOwner def _group( *, email: str, status: str = "active", google_group_id: str, aliases: list[str] | None = None, ) -> EmailGroup: return EmailGroup( email=email, name=email.split("@", 1)[0], status=status, google_group_id=google_group_id, aliases=aliases, ) async def _add_group(db_session, **kwargs) -> EmailGroup: group = _group(**kwargs) db_session.add(group) await db_session.flush() return group async def _validated_principal(db_session, group: EmailGroup): return await DataAccessService().validate_google_group_principal( db_session, group.id, ) async def _assert_denied(db_session, requester, group: EmailGroup) -> None: principal = await _validated_principal(db_session, group) with pytest.raises(DataAccessRequesterAuthorizationError) as exc_info: await DataAccessService().authorize_requester_for_google_group_principal( db_session, requester, principal, ) exc = exc_info.value assert exc.code == "DATA_ACCESS_REQUESTER_NOT_AUTHORIZED" assert exc.status_code == 403 assert exc.field == "principal" assert exc.requester_person_id == requester.id assert exc.principal_email_group_id == principal.principal_email_group_id async def test_group_owner_is_authorized_via_resolved_email_group_id( db_session, employee_user, ) -> None: requester, _ = employee_user group = await _add_group( db_session, email="analytics-readers@example.com", google_group_id="google-group-analytics-authz", aliases=["analytics-alias@example.com"], ) db_session.add(EmailGroupOwner(email_group_id=group.id, person_id=requester.id)) await db_session.flush() principal = await DataAccessService().validate_google_group_principal( db_session, "analytics-alias@example.com", ) result = await DataAccessService().authorize_requester_for_google_group_principal( db_session, requester, principal, ) assert result.requester_person_id == requester.id assert result.principal_email_group_id == group.id assert result.authorized_as == "group_owner" async def test_non_owner_is_denied(db_session, employee_user) -> None: requester, _ = employee_user group = await _add_group( db_session, email="finance-readers@example.com", google_group_id="google-group-finance-authz", ) await _assert_denied(db_session, requester, group) async def test_group_member_is_authorized_via_live_membership_override( db_session, employee_user, ) -> None: requester, _ = employee_user group = await _add_group( db_session, email="finance-readers@example.com", google_group_id="google-group-finance-member-authz", ) requester.__dict__["_data_access_member_group_ids_override"] = [group.id] principal = await _validated_principal(db_session, group) result = await DataAccessService().authorize_requester_for_google_group_principal( db_session, requester, principal, ) assert result.requester_person_id == requester.id assert result.principal_email_group_id == group.id assert result.authorized_as == "group_member" async def test_member_lookup_google_failure_degrades_to_denial_not_500( db_session, employee_user, monkeypatch, ) -> None: """Regression: the live Google-membership lookup is an *additive* enrichment on top of group-ownership. A flaky/over-quota Google Directory call must NOT 500 the authorization path — it degrades to owner-only, so a non-owner is cleanly DENIED (403) rather than crashing (and a real owner above is unaffected).""" from app.core.exceptions import AppException from app.domains.email_group.service import EmailGroupService requester, _ = employee_user group = await _add_group( db_session, email="finance-readers@example.com", google_group_id="google-group-degrade-authz", ) async def _boom(self, **kwargs): # noqa: ANN001, ANN003 raise AppException( code="GOOGLE_DIRECTORY_UNAVAILABLE", message="down", status_code=502) monkeypatch.setattr( EmailGroupService, "list_member_groups_for_person", _boom) # Non-owner + failing membership lookup → 403 (not the raw 502). await _assert_denied(db_session, requester, group) async def test_global_admin_is_authorized_without_group_ownership( db_session, admin_user, ) -> None: admin, _ = admin_user group = await _add_group( db_session, email="security-readers@example.com", google_group_id="google-group-security-authz", ) principal = await _validated_principal(db_session, group) result = await DataAccessService().authorize_requester_for_google_group_principal( db_session, admin, principal, ) assert result.requester_person_id == admin.id assert result.principal_email_group_id == group.id assert result.authorized_as == "global_admin" async def test_view_role_narrowing_removes_admin_escape_hatch( db_session, admin_user, ) -> None: admin, _ = admin_user group = await _add_group( db_session, email="platform-readers@example.com", google_group_id="google-group-platform-authz", ) admin.__dict__["_role_names_override"] = ["employee"] await _assert_denied(db_session, admin, group) async def test_primary_email_or_alias_match_does_not_authorize_requester( db_session, employee_user, ) -> None: requester, _ = employee_user group = await _add_group( db_session, email=requester.work_email, google_group_id="google-group-email-match-authz", aliases=["employee-alias@example.com"], ) await _assert_denied(db_session, requester, group) async def test_deleted_row_owner_does_not_authorize_rename_bridge( db_session, employee_user, ) -> None: requester, _ = employee_user deleted = await _add_group( db_session, email="analytics-old@example.com", status="deleted", google_group_id="google-group-rename-authz", ) active = await _add_group( db_session, email="analytics-new@example.com", google_group_id="google-group-rename-authz", ) db_session.add(EmailGroupOwner(email_group_id=deleted.id, person_id=requester.id)) await db_session.flush() principal = await DataAccessService().validate_google_group_principal( db_session, deleted.id, ) assert principal.principal_email_group_id == active.id with pytest.raises(DataAccessRequesterAuthorizationError): await DataAccessService().authorize_requester_for_google_group_principal( db_session, requester, principal, ) async def test_active_bridge_owner_is_authorized_after_rename( db_session, employee_user, ) -> None: requester, _ = employee_user deleted = await _add_group( db_session, email="bridge-old@example.com", status="deleted", google_group_id="google-group-bridge-authz", ) active = await _add_group( db_session, email="bridge-new@example.com", google_group_id="google-group-bridge-authz", ) db_session.add(EmailGroupOwner(email_group_id=active.id, person_id=requester.id)) await db_session.flush() principal = await DataAccessService().validate_google_group_principal( db_session, deleted.id, ) result = await DataAccessService().authorize_requester_for_google_group_principal( db_session, requester, principal, ) assert result.principal_email_group_id == active.id assert result.authorized_as == "group_owner"