fix: Forward X-Org-Id during automation auth - #403
Conversation
Co-authored-by: openhands <openhands@all-hands.dev>
|
👋 This PR needs a couple of things fixed before OpenHands can review it:
Push an update once this is addressed and this check re-runs automatically. This is an automated check - no AI was used to generate this comment. |
1 similar comment
|
👋 This PR needs a couple of things fixed before OpenHands can review it:
Push an update once this is addressed and this check re-runs automatically. This is an automated check - no AI was used to generate this comment. |
enyst
left a comment
There was a problem hiding this comment.
Taste Rating: 🟡 Acceptable — clean, focused fix for a real bug; one small taste wart.
Review by OpenHands-DeepSeek-Pro (AI agent), on behalf of the user.
[IMPROVEMENT OPPORTUNITIES]
- [openhands/automation/auth.py, line 197]
_request_has_header— see inline comment. Thetry/except TypeErrorexists only to accommodate theMagicMocktest fixture, not any realRequestbehavior (StarletteHeaders.__contains__never raisesTypeError, andget(name, default)already returns the default for a missing header). Production code shouldn't carry a special case only a mock needs. Cleaner path: give themock_requestfixture a realstarlette.dataclasses.Headers(orRequest), which collapses_extract_x_org_idto a singlerequest.headers.get(X_ORG_ID_HEADER, "").strip()and deletes the_request_has_headerhelper plus the now-redundantif not header_valueguard.
Correct parts (no action needed):
- Keying the cache by
(auth_method, normalized_org, credential)is right, and incidentally fixes a latent collision where an API key and a cookie sharing the same token string would map to the same cache entry. str(uuid.UUID(...))normalization is good taste — it canonicalizesurn:uuid:, braced, and hex forms so the same org can't fragment the cache.- The new tests exercise the real
authenticate_requestpath and assert on forwarded headers and resolvedorg_id/call_count, not just mock-call counts. The mocked HTTP boundary is the legitimate seam here.
[TESTING GAPS]
- Minor: no assertion that
X-Org-Idis absent from outbound headers when the incoming request omits it. The existingtest_authenticate_valid_api_keywould still pass if someone removed theif x_org_id:guard and unconditionally attached the header. One extraassert "X-Org-Id" not in headerswould lock the guard in.
[RISK ASSESSMENT]
- [Overall PR]
⚠️ ✅ LOW
The behavior change is additive (only acts when X-Org-Id is present), the cache-key change is a pure in-memory format change, and the only call site of _credential_cache_key is this file. The one thing a human should confirm is the security boundary: the fix relies on OpenHands /api/v1/users/me rejecting a request when the supplied X-Org-Id disagrees with the credential's bound org (api_key_org_id). That is the property that stops a user from passing another org's UUID to read its automations. The frontend client.ts comment in the OpenHands repo confirms this rejection exists upstream, so I'm treating it as verified rather than blocking — but it is the load-bearing invariant this PR rests on.
VERDICT: ✅ Worth merging.
KEY INSIGHT: The real contribution isn't the header forwarding — it's recognizing that the auth cache key must encode the full authentication scope (method + org), not just the credential, or org scoping silently leaks across requests.
Improve this review? If any feedback above seems incorrect or irrelevant to this repository, you can teach the reviewer to do better:
- Add a
.agents/skills/custom-codereview-guide.mdfile to your branch (or edit it if one already exists) with the/codereviewtrigger and the context the reviewer is missing. See the customization docs for the required frontmatter format.- Re-request a review — the reviewer reads guidelines from the PR branch, so your changes take effect immediately.
- When your PR is merged, the guideline file goes through normal code review by repository maintainers.
Resolve with AI? Install the iterate skill in your agent and run
/iterateto automatically drive this PR through CI, review, and QA until it's merge-ready.Was this review helpful? React with 👍 or 👎 to give feedback.
| return hashlib.sha256(cache_material.encode()).hexdigest() | ||
|
|
||
|
|
||
| def _request_has_header(request: Request, name: str) -> bool: |
There was a problem hiding this comment.
This try/except TypeError exists only so the MagicMock fixture works, not because any real Request raises it. In production request.headers is a Starlette Headers, whose __contains__ returns a real bool and whose get(name, default) already returns the default for a missing header — so the whole helper is mock-specific machinery leaking into production code.
Cleaner: give the mock_request fixture a real starlette.dataclasses.Headers (or a real Request), then collapse _extract_x_org_id to a single request.headers.get(X_ORG_ID_HEADER, "").strip(), drop _request_has_header entirely, and the if not header_value guard becomes redundant. That removes a helper, an exception handler, and a null-check — a net deletion that also makes the tests exercise the same header semantics as prod.
Summary
X-Org-Idto OpenHands/api/v1/users/meduring automation API authentication.X-Org-Idforwarding, invalid org header handling, and cache separation by org.Fixes #402
Testing
uv run pytest tests/test_auth.py -q -k 'not TestAuthIntegration'— passed (50 passed, 4 deselected).uv run pre-commit run --files openhands/automation/auth.py tests/test_auth.py --show-diff-on-failure— passed.uv run pytest tests/test_auth.py -q— unit tests passed, but Docker-backedTestAuthIntegrationsetup failed because this environment has no Docker socket.This PR was created by an AI agent (OpenHands) on behalf of the user.
@malhotra5 can click here to continue refining the PR