Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 1 addition & 10 deletions backend/app/api/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 0 additions & 1 deletion backend/app/api/plaza.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
62 changes: 35 additions & 27 deletions backend/app/dao/agent_access_dao.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@

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:
super().__init__(Agent)

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."""
Expand All @@ -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]:
Expand All @@ -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()


Expand Down
16 changes: 12 additions & 4 deletions backend/app/dao/user_dao.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]),
)
Expand Down