fix(security): enforce single-trust-domain; block shared credentials in hosted mode (#718) - #789
Conversation
…in hosted mode (#718) Machine-wide credentials + no self-hosted workspace-owner check meant a multi-tenant deployment shared/leaked one LLM key set and one GitHub PAT. Hosted workspace isolation already ships (#655, path-namespace per user); this closes the credential side and formalizes the trust model: - forbid_shared_credentials_in_hosted_mode(): credential + GitHub-PAT mutation endpoints (settings /keys PUT+DELETE, github connect/disconnect) fail closed (403) in hosted mode; tenants supply keys via per-instance env vars - persist owner_user_id from auth on workspace registration (defense-in-depth; owner-scoped list/delete enforcement is #720) - SECURITY.md documents the self-hosted single-trust-domain model + hosted-mode credential blocking Per-user credential scoping deferred to a follow-up. Closes #718
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 30 minutes Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (6)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Code Review — PR #789: Single-trust-domain enforcement (#718)Overall: Solid, well-scoped security fix. The fail-closed approach is correct, the SECURITY.md documentation is clear, and Issues1. Missing test coverage for GitHub credential endpoints
def test_connect_blocked_in_hosted_mode(self, monkeypatch):
monkeypatch.setenv("CODEFRAME_DEPLOYMENT_MODE", "hosted")
r = self._client().post("/api/v2/integrations/github/connect", json={"pat": "ghp_x", ...})
assert r.status_code == 403
def test_disconnect_blocked_in_hosted_mode(self, monkeypatch):
monkeypatch.setenv("CODEFRAME_DEPLOYMENT_MODE", "hosted")
r = self._client().delete("/api/v2/integrations/github/disconnect")
assert r.status_code == 4032. Obscure double-negation assertion
# current - passes on any non-(403 + hosted detail) response, including 500
assert not (r.status_code == 403 and "hosted mode" in r.json().get("detail", "").lower())
# clearer
assert r.status_code != 403, f"Unexpected hosted-mode 403 in self-hosted: {r.json()}"
# or assert r.status_code in {200, 400} # 400 = value-format rejection3. Undocumented deferred import The def forbid_shared_credentials_in_hosted_mode() -> None:
# Deferred: codeframe.ui.server imports from dependencies at module level.
from codeframe.ui.server import is_hosted_modeConfirmed correct (no action needed)
The two test gaps (GitHub PAT endpoints + assertion clarity) are worth fixing before merge. Everything else looks good. |
…d import (#718 review)
|
Thanks — applied all three:
|
|
Follow-up review: all previous issues resolved. The three items from my last review are addressed in the updated diff:
One optional addition (not blocking): TestCredentialEndpointHostedBlock covers mutation endpoints in hosted mode, but there is no regression guard ensuring GET endpoints (GET /api/v2/settings/keys, GET /api/v2/integrations/github/status) remain accessible in hosted mode. A small negative test would catch a future accidental Depends(forbid_...) on a read path. Not required before merge; the guard is intentionally read-permissive by omission and that is documented in SECURITY.md. This is ready to merge. The fail-closed guard is correct, the scope is tight, the documentation is clear, and test coverage now spans all four guarded endpoints. |
What & why
CodeFRAME's credential store is machine-wide (one LLM key set + one GitHub PAT per host), and self-hosted mode had no workspace-owner check. In a multi-tenant deployment every user could view (last-4), overwrite, or delete another's secrets, and — with no
WORKSPACE_ROOT— read/write another's workspace.Fixes #718 [P0.7]. Approach: enforce + document a single-trust-domain model (the AC's sanctioned alternative to a full per-user credential rework; confirmed with the maintainer).
Context — what already existed
Hosted-mode workspace isolation shipped in #655:
WORKSPACE_ROOTis mandatory in hosted mode (fails closed if unset) and each user is confined to<root>/<user_id>. This PR closes the credential side + formalizes the model.Changes
forbid_shared_credentials_in_hosted_mode()(ui/dependencies.py): credential + GitHub-PAT mutation endpoints fail closed (403) in hosted mode —PUT/DELETE /api/v2/settings/keys/*,POST /connect,DELETE /disconnect. GET status stays readable. Self-hosted (single trust domain) is unaffected.workspace_v2.py):_register_workspacenow recordsauth["user_id"](was hardcodedNone). Defense-in-depth; owner-scoped list/delete enforcement is [P0.9] Owner-scope the workspace registry (list/deletecurrently unrestricted) #720.SECURITY.md: documents the self-hosted single-trust-domain model and the hosted-mode credential block.Tests (
tests/ui/test_hosted_credential_block.py)Guard unit (hosted→raises, self-hosted→no-op);
PUT/DELETE /keys→ 403 in hosted, not-hosted-403 in self-hosted;_register_workspaceforwards owner_user_id.124 passedacross settings/github/workspace/registry/scope suites.ruff+mypyclean.Demo (auth-off to isolate the guard; a real hosted tenant is authenticated then hits the same 403)
Acceptance criteria
WORKSPACE_ROOT(fail closed in hosted mode) — shipped in [P7.0.1] Workspace path allowlist — prevent authenticated cross-tenant RCE (M1) #655, documented hereowner_user_idfromauth["user_id"](owner-scoped enforcement is [P0.9] Owner-scope the workspace registry (list/deletecurrently unrestricted) #720)Known limitation / follow-up
Per-user credential scoping (so hosted tenants can store their own keys through the API) is deferred to the follow-up issue below.