From 565ad62c399013a35e1ad930e8cac49109c19ee7 Mon Sep 17 00:00:00 2001 From: patrick-motsch Date: Sun, 1 Mar 2026 13:01:35 +0100 Subject: [PATCH] feat(teamsbot): RBAC session isolation -- users see only their own sessions Made-with: Cursor --- .../teamsbot/interfaceFeatureTeamsbot.py | 9 +++++--- .../features/teamsbot/routeFeatureTeamsbot.py | 22 +++++++++++++++++-- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/modules/features/teamsbot/interfaceFeatureTeamsbot.py b/modules/features/teamsbot/interfaceFeatureTeamsbot.py index f230a024..9be96393 100644 --- a/modules/features/teamsbot/interfaceFeatureTeamsbot.py +++ b/modules/features/teamsbot/interfaceFeatureTeamsbot.py @@ -69,11 +69,14 @@ class TeamsbotObjects: # Sessions # ========================================================================= - def getSessions(self, instanceId: str, includeEnded: bool = True) -> List[Dict[str, Any]]: - """Get all sessions for a feature instance.""" + def getSessions(self, instanceId: str, includeEnded: bool = True, userId: str | None = None) -> List[Dict[str, Any]]: + """Get sessions for a feature instance, optionally filtered by owner.""" + recordFilter = {"instanceId": instanceId} + if userId: + recordFilter["startedByUserId"] = userId records = self.db.getRecordset( TeamsbotSession, - recordFilter={"instanceId": instanceId}, + recordFilter=recordFilter, ) if not includeEnded: records = [r for r in records if r.get("status") != TeamsbotSessionStatus.ENDED.value] diff --git a/modules/features/teamsbot/routeFeatureTeamsbot.py b/modules/features/teamsbot/routeFeatureTeamsbot.py index 9d84f854..fef66eb9 100644 --- a/modules/features/teamsbot/routeFeatureTeamsbot.py +++ b/modules/features/teamsbot/routeFeatureTeamsbot.py @@ -134,6 +134,14 @@ def _validateInstanceAccess(instanceId: str, context: RequestContext) -> str: return str(mandateId) +def _validateSessionOwnership(session: dict, context: RequestContext) -> None: + """Raise 404 if the user does not own this session (sysAdmin bypasses).""" + if context.hasSysAdminRole: + return + if session.get("startedByUserId") != str(context.user.id): + raise HTTPException(status_code=404, detail=f"Session '{session.get('id')}' not found") + + def _getInstanceConfig(instanceId: str) -> TeamsbotConfig: """Load TeamsbotConfig from FeatureInstance.config JSONB field.""" rootInterface = getRootInterface() @@ -306,10 +314,11 @@ async def listSessions( includeEnded: bool = Query(True, description="Include ended sessions"), context: RequestContext = Depends(getRequestContext), ): - """List all sessions for a feature instance.""" + """List sessions for a feature instance (filtered to own sessions unless sysAdmin).""" _validateInstanceAccess(instanceId, context) interface = _getInterface(context, instanceId) - sessions = interface.getSessions(instanceId, includeEnded=includeEnded) + userId = None if context.hasSysAdminRole else str(context.user.id) + sessions = interface.getSessions(instanceId, includeEnded=includeEnded, userId=userId) return {"sessions": sessions} @@ -330,6 +339,7 @@ async def getSession( session = interface.getSession(sessionId) if not session: raise HTTPException(status_code=404, detail=f"Session '{sessionId}' not found") + _validateSessionOwnership(session, context) result = {"session": session} @@ -362,6 +372,7 @@ async def streamSession( session = interface.getSession(sessionId) if not session: raise HTTPException(status_code=404, detail=f"Session '{sessionId}' not found") + _validateSessionOwnership(session, context) async def _eventGenerator(): """Generate SSE events from the session event queue.""" @@ -418,6 +429,7 @@ async def stopSession( session = interface.getSession(sessionId) if not session: raise HTTPException(status_code=404, detail=f"Session '{sessionId}' not found") + _validateSessionOwnership(session, context) currentStatus = session.get("status") if currentStatus in [TeamsbotSessionStatus.ENDED.value, TeamsbotSessionStatus.ERROR.value]: @@ -446,6 +458,7 @@ async def deleteSession( session = interface.getSession(sessionId) if not session: raise HTTPException(status_code=404, detail=f"Session '{sessionId}' not found") + _validateSessionOwnership(session, context) # Don't delete active sessions currentStatus = session.get("status") @@ -798,6 +811,11 @@ async def submitMfaCode( ): """Submit MFA code/confirmation from the frontend to the active bot session.""" _validateInstanceAccess(instanceId, context) + interface = _getInterface(context, instanceId) + session = interface.getSession(sessionId) + if not session: + raise HTTPException(status_code=404, detail=f"Session '{sessionId}' not found") + _validateSessionOwnership(session, context) body = await request.json() mfaCode = body.get("code", "") mfaAction = body.get("action", "code")