diff --git a/backend/app/api/agents.py b/backend/app/api/agents.py index fdf6f32f3..e7da1a80d 100644 --- a/backend/app/api/agents.py +++ b/backend/app/api/agents.py @@ -194,22 +194,13 @@ async def _agents_to_out( @router.get("/", response_model=list[AgentOut]) async def list_agents( - tenant_id: uuid.UUID | None = None, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """List all agents the current user has access to.""" - if tenant_id and tenant_id != current_user.tenant_id: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Can only list agents in your own company", - ) - - requested_tenant_id = current_user.tenant_id - stmt = build_visible_agents_query( current_user, - tenant_id=requested_tenant_id, + tenant_id=current_user.tenant_id, ).order_by(Agent.created_at.desc()) result = await db.execute(stmt) diff --git a/backend/app/api/plaza.py b/backend/app/api/plaza.py index fe6b53c01..7696ea4e1 100644 --- a/backend/app/api/plaza.py +++ b/backend/app/api/plaza.py @@ -35,7 +35,6 @@ class PostCreate(BaseModel): author_id: uuid.UUID author_type: str = "human" # "agent" or "human" author_name: str - tenant_id: uuid.UUID | None = None class CommentCreate(BaseModel): diff --git a/backend/app/dao/agent_access_dao.py b/backend/app/dao/agent_access_dao.py index 4dc596787..c0cddfc8b 100644 --- a/backend/app/dao/agent_access_dao.py +++ b/backend/app/dao/agent_access_dao.py @@ -4,13 +4,13 @@ from sqlalchemy import select -from app.dao.base import BaseDAO +from app.dao.base import TenantScopedBaseDAO from app.models.agent import Agent, AgentPermission from app.models.org import AgentRelationship, OrgMember from app.models.user import User -class AgentAccessDAO(BaseDAO[Agent]): +class AgentAccessDAO(TenantScopedBaseDAO[Agent]): """Read access patterns used by permission checks.""" def __init__(self) -> None: @@ -18,7 +18,7 @@ def __init__(self) -> None: async def get_agent(self, agent_id: Any) -> Agent | None: """Fetch a single agent by id.""" - return await self.get(agent_id) + return await self.get_scoped(agent_id) async def get_user(self, user_id: Any) -> User | None: """Fetch a single user by id.""" @@ -38,15 +38,15 @@ async def list_permissions(self, agent_id: Any) -> Sequence[AgentPermission]: result = await db.execute(select(AgentPermission).where(AgentPermission.agent_id == agent_id)) return result.scalars().all() - async def list_active_user_ids_by_tenant(self, tenant_id: Any) -> list[Any]: + async def list_active_user_ids_by_tenant(self, tenant_id: Any = None) -> list[Any]: """Return active user ids in a tenant.""" + ctx_tenant = self._require_tenant_id() + tid = ctx_tenant if ctx_tenant is not None else tenant_id async with self.session(readonly=True) as db: - result = await db.execute( - select(User.id).where( - User.tenant_id == tenant_id, - User.is_active == True, # noqa: E712 - ) - ) + stmt = select(User.id).where(User.is_active == True) # noqa: E712 + if tid is not None: + stmt = stmt.where(User.tenant_id == tid) + result = await db.execute(stmt) return [row[0] for row in result.fetchall()] async def list_custom_permission_user_ids(self, agent_id: Any) -> list[Any]: @@ -61,53 +61,61 @@ async def list_custom_permission_user_ids(self, agent_id: Any) -> list[Any]: ) return [row[0] for row in result.fetchall() if row[0]] - async def list_active_admin_user_ids_by_tenant(self, tenant_id: Any) -> list[Any]: + async def list_active_admin_user_ids_by_tenant(self, tenant_id: Any = None) -> list[Any]: """Return active tenant admin user ids.""" + ctx_tenant = self._require_tenant_id() + tid = ctx_tenant if ctx_tenant is not None else tenant_id async with self.session(readonly=True) as db: - result = await db.execute( - select(User.id).where( - User.tenant_id == tenant_id, - User.is_active == True, # noqa: E712 - User.role.in_(["platform_admin", "org_admin"]), - ) + stmt = select(User.id).where( + User.is_active == True, # noqa: E712 + User.role.in_(["platform_admin", "org_admin"]), ) + if tid is not None: + stmt = stmt.where(User.tenant_id == tid) + result = await db.execute(stmt) return [row[0] for row in result.fetchall()] async def list_active_relationship_user_ids( self, *, agent_id: Any, - tenant_id: Any, user_ids: set[Any], + tenant_id: Any = None, ) -> set[Any]: """Return active org-member user ids already linked to an agent.""" if not user_ids: return set() + ctx_tenant = self._require_tenant_id() + tid = ctx_tenant if ctx_tenant is not None else tenant_id async with self.session(readonly=True) as db: - result = await db.execute( + stmt = ( select(OrgMember.user_id) .join(AgentRelationship, AgentRelationship.member_id == OrgMember.id) .where( AgentRelationship.agent_id == agent_id, - OrgMember.tenant_id == tenant_id, OrgMember.status == "active", OrgMember.user_id.in_(user_ids), ) ) + if tid is not None: + stmt = stmt.where(OrgMember.tenant_id == tid) + result = await db.execute(stmt) return {row[0] for row in result.fetchall() if row[0]} - async def list_active_users_by_ids(self, *, user_ids: set[Any], tenant_id: Any) -> Sequence[User]: + async def list_active_users_by_ids(self, *, user_ids: set[Any], tenant_id: Any = None) -> Sequence[User]: """Return active users by ids under one tenant.""" if not user_ids: return [] + ctx_tenant = self._require_tenant_id() + tid = ctx_tenant if ctx_tenant is not None else tenant_id async with self.session(readonly=True) as db: - result = await db.execute( - select(User).where( - User.id.in_(user_ids), - User.tenant_id == tenant_id, - User.is_active.is_(True), - ) + stmt = select(User).where( + User.id.in_(user_ids), + User.is_active.is_(True), ) + if tid is not None: + stmt = stmt.where(User.tenant_id == tid) + result = await db.execute(stmt) return result.scalars().all() diff --git a/backend/app/dao/user_dao.py b/backend/app/dao/user_dao.py index dbf483cd7..2b61468f0 100644 --- a/backend/app/dao/user_dao.py +++ b/backend/app/dao/user_dao.py @@ -104,13 +104,21 @@ async def get_representative_user_for_identity(self, identity_id: Any) -> User | return result.scalar_one_or_none() - async def list_admin_users(self, tenant_id: Any) -> Sequence[User]: - """Fetch all active org/platform admin users in a tenant.""" - if not tenant_id: + async def list_admin_users(self, tenant_id: Any = None) -> Sequence[User]: + """Fetch all active org/platform admin users in a tenant. + + If active tenant context exists in _tenant_ctx, enforces active tenant scope + to prevent cross-tenant queries by org_admin. + """ + from app.dao.base import _tenant_ctx + + active_tenant = _tenant_ctx.get() + tid = active_tenant if active_tenant is not None else tenant_id + if not tid: return [] async with self.session(readonly=True) as db: query = select(User).where( - User.tenant_id == tenant_id, + User.tenant_id == tid, User.is_active == True, # noqa: E712 User.role.in_(["platform_admin", "org_admin"]), )