""" Tenant resolution — maps channel workspace IDs to Konstruct tenant IDs. This is the ONE pre-RLS query in the system. Tenant resolution must work across all tenants because we don't know which tenant owns a message until after we resolve it. The query bypasses RLS by using the admin/superuser connection for this specific lookup only. Design: - Query `channel_connections` for matching workspace_id + channel_type - Returns the tenant_id UUID as a string, or None if not found - Uses a raw SELECT without RLS context (intentional — pre-resolution) """ from __future__ import annotations import logging from sqlalchemy import select, text from sqlalchemy.ext.asyncio import AsyncSession from shared.models.message import ChannelType from shared.models.tenant import ChannelConnection, ChannelTypeEnum logger = logging.getLogger(__name__) # Map ChannelType (StrEnum from message.py) to ChannelTypeEnum (ORM enum from tenant.py) _CHANNEL_TYPE_MAP: dict[str, ChannelTypeEnum] = { "slack": ChannelTypeEnum.SLACK, "whatsapp": ChannelTypeEnum.WHATSAPP, "mattermost": ChannelTypeEnum.MATTERMOST, "rocketchat": ChannelTypeEnum.ROCKETCHAT, "teams": ChannelTypeEnum.TEAMS, "telegram": ChannelTypeEnum.TELEGRAM, "signal": ChannelTypeEnum.SIGNAL, } async def resolve_tenant( workspace_id: str, channel_type: ChannelType | str, session: AsyncSession, ) -> str | None: """ Resolve a channel workspace ID to a Konstruct tenant ID. This is deliberately a RLS-bypass query — we cannot know which tenant to set in `app.current_tenant` until after we resolve the tenant. The session passed here should use the admin connection (postgres superuser) or the konstruct_app role with RLS disabled for this specific query. In practice, for this single lookup, we disable the RLS SET LOCAL by temporarily not setting `current_tenant_id` — the ContextVar defaults to None, so the RLS hook does not inject SET LOCAL, and the query sees all rows in `channel_connections`. Args: workspace_id: Channel-native workspace identifier (e.g. Slack T12345). channel_type: Channel type as ChannelType enum or string. session: Async SQLAlchemy session. Returns: Tenant ID as a string (UUID), or None if no matching connection found. """ channel_str = str(channel_type).lower() orm_channel = _CHANNEL_TYPE_MAP.get(channel_str) if orm_channel is None: logger.warning("resolve_tenant: unknown channel_type=%r", channel_type) return None try: # Bypass RLS for this query — disable RLS row filtering at the session level # by setting app.current_tenant to empty (no policy match = all rows visible # to konstruct_app for SELECT on channel_connections). # We use a raw SET LOCAL here to ensure the tenant policy is not applied. await session.execute(text("SET LOCAL app.current_tenant = ''")) stmt = ( select(ChannelConnection.tenant_id) .where(ChannelConnection.channel_type == orm_channel) .where(ChannelConnection.workspace_id == workspace_id) .limit(1) ) result = await session.execute(stmt) row = result.scalar_one_or_none() except Exception: logger.exception( "resolve_tenant: DB error workspace_id=%r channel=%r", workspace_id, channel_type, ) return None if row is None: logger.debug( "resolve_tenant: no match workspace_id=%r channel=%r", workspace_id, channel_type, ) return None return str(row)