"""Submit-time shape pre-flight wired into the access-request flow. Regression for the 2026-06-15 incident: a stale RAM-shape catalog row (``System:ReadOnlyAccess``) on a CloudSSO-managed AliCloud tenant passed ``submit_access_request`` (the static governance classifier can't see the tenant's CloudSSO surface), routed through the scope owner's approval, and only failed at grant time — so the *approver* (Tennix), not the requester, saw the confusing "role catalog is stale" toast. ``CloudLifecycleService.submit_access_request`` / ``owner_add_member`` now call ``driver.validate_request_role`` before persisting, so the requester is rejected up front and the bad request never enters the queue. """ from __future__ import annotations import uuid from unittest.mock import AsyncMock, patch import pytest from sqlalchemy.ext.asyncio import AsyncSession # register the driver in the registry import app.integrations.drivers.alicloud_intl_cloud # noqa: F401 from app.core.exceptions import AppException from app.domains.cmdb.lifecycle_service import CloudLifecycleService from app.domains.cmdb.models import SCOPE_STATUS_ACTIVE, CmdbCloudScope from app.identity.person_models import IdentityPerson from app.integrations.drivers.alicloud_intl_cloud import AlicloudIntlCloudDriver from app.integrations.models import Integration async def _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 _intl_scope(db: AsyncSession, owner: IdentityPerson) -> CmdbCloudScope: integ = Integration( id=uuid.uuid4(), kind="cloud", provider="alicloud_intl", name="AliCloud (海外)", slug=f"intl-{uuid.uuid4().hex[:6]}", enabled=True, is_primary=False, capabilities=["servers"], ) db.add(integ) await db.commit() scope = CmdbCloudScope( integration_id=str(integ.id), scope_type="aliyun_account", external_id=f"acct-{uuid.uuid4().hex[:6]}", display_name="intl-test", owner_person_id=str(owner.id), status=SCOPE_STATUS_ACTIVE, ) db.add(scope) await db.commit() return scope @pytest.mark.asyncio async def test_submit_rejects_ram_shape_role_on_cloudsso_tenant(db_session: AsyncSession): owner = await _person(db_session, "owner@pingcap.com") requester = await _person(db_session, "req@pingcap.com") scope = await _intl_scope(db_session, owner) svc = CloudLifecycleService(db_session) # CloudSSO directory present → RAM-shape key must be refused at submit. with patch.object( AlicloudIntlCloudDriver, "_sso_directory_id", new=AsyncMock(return_value="d-test"), ): with pytest.raises(AppException) as ei: await svc.submit_access_request( scope_id=str(scope.id), person_id=str(requester.id), role_key="System:ReadOnlyAccess", role_display_name="System:ReadOnlyAccess", reason="t", ttl_days=30, requester_id=requester.id, trace_id="t", actor_role_names=["employee"], ) assert ei.value.status_code == 422 assert "stale" in (ei.value.message or "").lower() @pytest.mark.asyncio async def test_submit_allows_cloudsso_permission_set_name(db_session: AsyncSession): owner = await _person(db_session, "owner2@pingcap.com") requester = await _person(db_session, "req2@pingcap.com") scope = await _intl_scope(db_session, owner) svc = CloudLifecycleService(db_session) # Bare CloudSSO permission-set name is the correct shape → submit # persists a pending grant (the pre-flight is a no-op here). with patch.object( AlicloudIntlCloudDriver, "_sso_directory_id", new=AsyncMock(return_value="d-test"), ): grant = await svc.submit_access_request( scope_id=str(scope.id), person_id=str(requester.id), role_key="DBaaS-DevUser-Role", role_display_name="DBaaS-DevUser-Role", reason="t", ttl_days=30, requester_id=requester.id, trace_id="t", actor_role_names=["employee"], ) assert grant.status == "pending" assert grant.role_key == "DBaaS-DevUser-Role"