Repository navigation
fix(agent-terminal): refuse binding a conversation the creator does not own (#17422) - #17426
Conversation
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. 📝 WalkthroughWalkthroughAgent-terminal session creation now verifies that the caller owns the supplied conversation. The API returns the missing-session 404 response when ownership verification fails. Tests cover rejected and permitted requests, including unreadable and unowned conversations. ChangesConversation ownership enforcement
Priority: ➖ Normal Severity of issue fixed: Medium Merge Risk: 🔵 Low · up to Conversation ownership checks appear intact. Tightening the refusal tests would improve regression detection, but the remaining gap does not prevent merging. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The change strengthens a cross-tenant ownership check before a terminal session is created. No new security weakness was established, though the behavior of existing sessions during an ownership transfer remains uncertain. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
Full details: Linked Issues checkExplanation The implementation meets the other coding requirements in
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
✅ SSOT Configuration Compliance: Passing🎉 No new hardcoded values of either class — Known backlog in |
c305201 to
54f0d02
Compare
|
The job fails while building the SLM backend, which this diff does not touch: SQLAlchemy 2.1.0 was just released, and
The same fresh install would also break the SLM backend at runtime, not only CI. Proposed fix (one line, its own PR, not folded in here): --- a/autobot-slm-backend/requirements.txt
-sqlalchemy>=2.0.54
+sqlalchemy[asyncio]>=2.0.54No fix PR exists yet; I searched open PRs for Generated by Claude Code |
|
The backend container from this PR came up healthy ( Generated by Claude Code |
|
This PR now carries the Generated by Claude Code |
d8bcd61 to
54f0d02
Compare
|
I dropped d8bcd61 (the On the four metadata gates ( On 54f0d02 the only failures were the three caused by the Generated by Claude Code |
|
Its path-detection job was cancelled, most likely because my force-pushes (d8bcd61, then back to 54f0d02) superseded the run. This PR changes no migrations, and the same gate passed on 54f0d02 in the earlier run ( Generated by Claude Code |
…ot own (#17422) POST /api/agent-terminal/sessions took conversation_id from the request body and passed the JWT username as an explicit owner, which skipped the only place the conversation's owner was resolved (#17053's `owner is None` branch). Any signed-in user could bind another user's conversation, be recorded as the session owner, and have its pending approval restored into their session. SessionManager.create_session now resolves the conversation's owner on every call and raises ConversationNotOwnedError when an explicit owner does not own it, or when the conversation has no resolvable owner. The route maps that to the same 404 as a missing session.
54f0d02 to
800010f
Compare
|
This was a network reset on the runner, not a failure in this PR's change. The same check passed on 800010f at 06:17Z, and cc4403d only adds the main merge. I tried to re-run the failed job and got Generated by Claude Code |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository: mrveiss/AutoBot-AI/.coderabbit.yaml
Review profile: ASSERTIVE
Plan: Advanced
Run ID: c1417088-e63c-4d03-8d82-1197b0e40e9a
📒 Files selected for processing (4)
autobot-backend/api/agent_terminal.pyautobot-backend/api/agent_terminal_access.pyautobot-backend/services/agent_terminal/conversation_owner.pyautobot-backend/services/agent_terminal/session_manager.py
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.
…, never falls to the grant (#17422) verified_conversation_owner() backs the explicit-owner check: when the session file exists but cannot be read, it refuses instead of falling through to the Redis grant. The chat ownership gate grants a conversation it cannot read to its next caller, so that grant proves nothing about who owns it. The implicit (chat workflow) path keeps conversation_owner()'s existing fallback. Found in review on #17426.
|
CI on c3909d1 (the unreadable-owner fix) was cancelled, not failed. Between 10:49Z and 10:52Z nearly every job in the batch finished as Nothing has run against this commit yet, so there is no verdict on it. Locally: Generated by Claude Code |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository: mrveiss/AutoBot-AI/.coderabbit.yaml
Review profile: ASSERTIVE
Plan: Advanced
Run ID: a0e04147-0a5d-4e50-9191-518cd1f69712
📒 Files selected for processing (5)
autobot-backend/api/agent_terminal.pyautobot-backend/api/agent_terminal_access.pyautobot-backend/services/agent_terminal/conversation_owner.pyautobot-backend/services/agent_terminal/session_manager.pyautobot-backend/tests/security/test_agent_terminal_conversation_binding_17422.py
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.
Thinking Path
POST /api/agent-terminal/sessionstakesconversation_idfrom the request body. The route passed the JWT username as an explicitowner, andSessionManager.create_sessiononly resolved the conversation's owner whenowner is None(#17053). The REST path never takes that branch. So nothing compared the conversation to the caller. The session record then named the caller as owner, which means every #17053 check passed. An authenticated user could bind another tenant's conversation, and_restore_pending_approvalwould load that conversation's pending approval into their session (CWE-639).The comment above the call ("owner and tenant_id both come from the JWT only") was accurate but beside the point: the untrusted input was
conversation_id.The fix is in the service layer, so no caller can skip it.
create_sessionnow resolves the conversation's owner on every call. An explicitownermust match it, and it narrows authority rather than replacing the check. The route maps the refusal to the same 404 a missing session gets (THREAT_MODEL.md §2: a response never confirms who owns what).Decisions made on purpose, where the brief left them open:
Noneinconversation_owner(), so this path fails closed.SessionManagerhas no role, andAgentTerminalServiceis at its file-size ceiling (951 lines), so it couldn't take a new parameter without a split. An admin who creates a session on someone else's conversation would be stamped as that session's owner, which misattributes it anyway. If an admin flow needs this, it is a follow-up. I found no such flow: the only frontend caller (useTerminalStore.createAgentTerminalSession) binds the user's current chat conversation.terminal_tool) pass no owner, so the session is stamped with the conversation's own owner. There is no caller identity that could differ from it, so there is nothing to refuse. The check is not on theowner is Nonebranch. The test pins the implicit path to the conversation's owner.Discoverability of another tenant's
conversation_id(severity): partly established. Server-minted IDs are UUID4 (utils/chat_utils.generate_chat_session_id), andChatHistoryManager.create_sessionfalls back tochat-{ms}-{8 hex}. Neither can be guessed. Client-minted IDs pass a format check only (validate_chat_session_id). I did not measure whether conversation IDs leak through other channels (logs, shared sessions, org indexes, WebSocket channel names), so moderate vs. severe is still undetermined.What Changed
services/agent_terminal/session_manager.py: new_owner_for(conversation_id, owner). It always resolves the recorded owner, returns it whenowneris None, and otherwise raisesConversationNotOwnedErrorunless the recorded owner equalsowner.create_sessioncalls it in place of theowner is Nonebranch, before PTY setup, session storage and_restore_pending_approval.services/agent_terminal/conversation_owner.py:ConversationNotOwnedError(PermissionError). The same error covers someone else's conversation and an unowned one.api/agent_terminal_access.py:create_session_for_caller(service, user, **fields)stamps owner and tenant from the JWT and maps the refusal to404 ERR_SESSION_NOT_FOUND.api/agent_terminal.py: the create route calls the helper. No net line change, because the file is at itsKNOWN_LARGEceiling (773).tests/security/test_agent_terminal_conversation_binding_17422.py(new, 8 tests): the real route runs over a realSessionManager. Only PTY setup and_restore_pending_approvalare mocked.session_manager_conversation_owner_17053_test.py:test_an_explicit_creator_is_never_replacedasserted the defect (carol bound alice's conversation). It now covers an explicit creator with no conversation.changelog/unreleased/17422-agent-terminal-conversation-binding.md.Verification
Mapped to the acceptance criteria:
test_binding_a_conversation_you_do_not_own_is_404_and_nothing_is_createdasserts== 404anddetail == ERR_SESSION_NOT_FOUNDfor someone else's, unowned, unknown, and admin-on-someone-else's conversations._owner_for, off theowner is Nonebranch.test_an_explicit_owner_is_checked_against_the_conversationcovers the service directly, andtest_an_implicit_owner_is_the_conversations_own_so_it_cannot_name_someone_elsepins the implicit path._restore_pending_approvalnot reached:assert_not_awaited()in the refusal tests, andassert_awaited_once_with(session, "conv-alice")for the owner.Targeted run (new tests plus every test file that mentions the agent terminal, 24 files):
The skips come from the local environment (optional deps absent; the conftest reports it).
tests/security/test_agent_terminal_ownership_17052.pyruns in full: 92 passed.Mutation: I removed the two lines
if conversation_id and recorded != owner: raise ConversationNotOwnedError(conversation_id)and ran the new file:After restoring the check:
8 passed.pre-commit run --files <changed>passed with exit 0, andcheck_python_file_size.pypassed on every changed file.Risks
legacy_migration), and Agent-terminal session routes have no ownership check: any signed-in user can list, read, execute in or delete another user's session #17053 made the same conversations admin-only./api/wschannel scoping) and security(events): captcha timeout/resolved publish the target URL to the 'global' channel, and the #17354 guard cannot see it #17414 (globalpublishes in the captcha path).Rollback: revert this single commit.
Model Used
Claude (Anthropic), via Claude Code
Issue Link
Closes #17422
Single-issue rationale: a security fix to the agent-terminal ownership path; the adjacent issues (#14822, #17414) are different code paths with different risk, and the brief scopes this to one defect.
Changelog fragment
changelog/unreleased/17422-agent-terminal-conversation-binding.mdChecklist
CLAUDE.mdmain(notrelease)Summary by CodeRabbit