Repository navigation
fix(tests): SSO/JWKS/RBAC-gate security tests — stop cross-file autobot_shared stub leak (#13312) - #13324
Merged
Conversation
Contributor
✅ SSOT Configuration Compliance: Passing🎉 No hardcoded values detected that have SSOT config equivalents! |
mrveiss
force-pushed
the
issue-13312-sso
branch
from
August 2, 2026 22:53
64c86f7 to
425dc82
Compare
… autobot_shared/fastapi stub across the whole suite (#13312) test_rbac_middleware_cache.py and test_rbac_permission_denied_audit.py stub sys.modules["autobot_shared"] with a bare MagicMock (no __path__) then exec_module() rbac_middleware.py directly. Neither file stubbed autobot_shared.logging_manager or autobot_shared.ssot_constants, both of which rbac_middleware.py imports at module level, so exec_module() always raised ModuleNotFoundError before the sys.modules restore block ran. Because that restore lived after the load instead of in a finally, the broken autobot_shared/fastapi stubs leaked into every test collected afterward for the rest of the pytest session -- the root cause behind SSO auth, JWKS, and service-management-gate security tests failing with unrelated "'autobot_shared' is not a package" / MagicMock-attribute errors despite their own code being correct.
…he current token_denylist API (#13312) #12807/#12827 migrated token_denylist.py off the sync-returning get_redis_client(async_client=True) pattern onto get_async_redis_client(), the safe wrapper that always awaits its own coroutine. That migration updated patch targets in six test files but missed this one: patch.object(_dl_mod, "get_redis_client", ...) raised AttributeError since the module no longer has that name, so the test never exercised revoke_jti at all. Production code (services/token_denylist.py) already uses the safe helper -- no security defect, the assertion just targeted a name that no longer exists.
…ndencies (#13312) Two follow-up defects surfaced once the sys.modules leak fix let these files collect and actually execute for the first time: - get_logger was stubbed as a MagicMock returning a MagicMock, so rbac_middleware.py's module-level `logger = get_logger(__name__)` produced a logger whose calls went nowhere -- test_db_failure_logs_error asserted on caplog.records for a log line that could structurally never arrive. Stub it with the real stdlib logging.getLogger instead (rbac_middleware.py used logging.getLogger directly before #13213 switched it to the shared wrapper, and the test passed then). - _make_redis()'s AsyncMock() auto-specced r.pipeline as async too, but redis.asyncio.Redis.pipeline() is synchronous -- it returns a Pipeline object immediately whose .delete() buffers synchronously and only .execute() is awaited. The un-awaited coroutine standing in for the pipeline raised AttributeError on .delete(). Give the stub a MagicMock pipeline with a sync .delete and an AsyncMock .execute, matching the real client's shape.
… 500ing the OAuth callback endpoint (#13312) Two real defects in _build_callback_url (api/sso_auth.py), both on public, pre-auth SSO endpoints: - urlsplit()'s .port property re-parses lazily and raises ValueError for a non-numeric or out-of-range (>65535) port. The access happened AFTER the try/except that was supposed to turn a malformed callback host into a 400, so "localhost:99999" in X-Forwarded-Host escaped as an unhandled 500 instead. Move the .port read inside the try. - X-Forwarded-Proto is attacker-controlled exactly like X-Forwarded-Host on the next line, but reached the returned redirect_uri with no validation at all -- "javascript" as the forwarded proto produced "javascript://host/api/auth/sso/callback". Reject anything outside {http, https} up front. Extracted the repeated log+raise into _reject_callback() to keep the function under the house line-count convention with the two new guards. Added regression tests for both (proven to fail against the pre-fix code). Also stub api/security in test_sso_auth.py's _load_sso_auth_with_stubs(): its absence meant sso_auth.py's `from api.security import create_audit_log` real- imported api/security.py, whose module-level route decorators need fastapi.status.HTTP_201_CREATED -- present on a full MagicMock fastapi but not on the narrower SimpleNamespace stub the host-validation tests pass, so those tests only worked when an earlier test in the same session happened to cache a real api.security first. Discovered while adding the new tests above; same failure mode on the pre-existing invalid-host test in isolation.
mrveiss
force-pushed
the
issue-13312-sso
branch
from
August 2, 2026 22:54
425dc82 to
21dde84
Compare
This was referenced Aug 3, 2026
mrveiss
added a commit
that referenced
this pull request
Aug 4, 2026
#13447) * chore(worktree): claim issue-13361-owners (#13361) * test(infra): stop nine test modules leaking sys.modules stubs (#13361) Every owner here mutates sys.modules at import or test scope for its own benefit and then leaves the mutation live for the rest of the session, so unrelated modules collected afterwards run against a stand-in instead of the real thing — the defect family behind #13320, #13324 and #13337. Removed outright, because the dependency they stood in for is a hard requirement that is always importable: tests/llm_interface_pkg/test_anthropic_provider.py anthropic, xxhash tests/llm_interface_pkg/test_groq_provider.py groq, xxhash tests/knowledge/test_cleanup_endpoint.py llama_index.*, chromadb, redis tests/knowledge/test_facts_dedup.py same, plus three lazy-import stubs tests/knowledge/test_stats_dedup.py same Bracketed by try/finally, because the registration is needed only while the module body executes: services/npu_pipeline/test_dispatcher.py shard_planner, dispatcher tests/services/test_scheduler_registry.py services.scheduler_registry autobot_shared/jina_parser_test.py pop-and-reimport autobot_shared/url_safety_test.py pop-and-reimport Seven baseline lines deleted; the two dedup modules were never listed — they only became visible once test_cleanup_endpoint.py stopped winning the setdefault race, and the guard failed the run until they were fixed. Three of the removed stubs rebound RedisError/Redis/asyncio on the REAL redis module, which no sys.modules guard can see. Two sibling suites were depending on it: the provider chat_completion tests spent 30s each on the cross-worker rate limiter's Redis retry budget without it (fixed with an autouse fixture that takes the limiter's documented Redis-unavailable path), and reranking_staleness_test.py reached the live client because its patch named the sync get_redis_client the code does not call. * style(test-infra): declare the real-load key tuple before its use (#13361)
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Thinking Path
#13312 reported 56 failures + 11 errors from
autobot-slm-backend's first-ever CI run. This PR fixes the security-relevant subset — SSO secret migration, SSO auth callback-URL, JWKS/token verification, and the service-management RBAC gate — plus everything else provably cleared by the same root-cause fix.For each failing test I first checked whether production code or the test was wrong, running each cluster standalone and comparing against git history for the modules under test.
migrations/migrate_sso_secrets_test.py,tests/api/test_sso_auth.py,services/jwks_verifier_test.py,tests/test_service_management_gate.pyall pass standalone — encryption-at-rest, rollback/commit, the callback-host allowlist, RS256/HS256 JWKS verification, and theSERVICE_MANAGEMENTRBAC gate are all correctly implemented.autobot-slm-backendsuite with-n auto --dist loadscope(the invocationci.ymlused before perf(ci): shard python-suite six ways, recombine coverage, CPU-only torch (#10691) #13314's sharding, still what each shard runs internally) reproduced 33 of the 56 tracked failures deterministically, spanning 11 files, all with the identical symptom:ModuleNotFoundError: ... 'autobot_shared' is not a packageor a permission/status object resolving to aMagicMockinstead of its real value.pytest_collectstartprobe plugin (not committed) plus a--continue-on-collection-errorsserial run:tests/test_rbac_middleware_cache.pyandtests/test_rbac_permission_denied_audit.pyeach replacesys.modules["autobot_shared"]with a bareMagicMock()(no__path__) at module scope, thenexec_module()the realrbac_middleware.py.git log -Son that import traces the second dependency it needs (autobot_shared.ssot_constants) to486eb1900(refactor(rbac): converge rbac_middleware onto one implementation, wiring in the unused cache accessors (#12925) #13213, 2026-08-01, an ancestor of the python-suite aborts after the first pytest: autobot-slm-backend tests and the 70% coverage gate never run #13300 baseline run) — before that refactorrbac_middleware.pydidn't import either module and these two test files collected fine. Neither test file's stub list was updated, soexec_module()raisesModuleNotFoundErrorbefore the sys.modules-restore block that follows it — outside anytry/finally. Both files fail to collect even in isolation. Because the restore never runs, the brokenautobot_shared/fastapistubs stay insys.modulesfor the rest of the pytest process, breaking every later realfrom autobot_shared.X import Y— this is what took down the other 9 files.pytest_collection_modifyitems, which runs after collection/module-import — so the leak reaches every shard's collection regardless of which shard a given test lands in. Rebased ontoc4938ec36(post-sharding) and re-verified with the actual--splits 12 --group Ninvocation for the two shards that contain every file this PR touches.tests/api/test_auth_logout.py::test_revoke_called_with_positive_ttlis unrelated to the leak:services/token_denylist.pywas migrated fromget_redis_client(async_client=True)to the safeget_async_redis_client()wrapper in tech-debt(backend): migrate ~47 remaining get_redis_client(async_client=True) call sites to get_async_redis_client() #12807/tech-debt(backend): migrate remaining async-Redis call sites off the legacy API (#12807) #12827, which updated patch targets in six test files but missed this one.get_loggerstub returning aMagicMock(sologger.error(...)never reachedcaplog) and aredis.pipeline()stub that auto-specced as async when the real client'spipeline()is synchronous._build_callback_urlline-by-line for the reconstruction/allowlist logic (needed to diagnose the port question) surfaced two real, if low-severity, gaps:urlsplit().portis accessed outside the try that is supposed to turn a bad host into a 400, andX-Forwarded-Protoreaches the returnedredirect_uriwith no scheme validation despiteX-Forwarded-Hoston the very next line being treated as hostile. Fixed both with regression tests proven to fail pre-fix.What Changed
Test-infrastructure fix (the root cause):
autobot-slm-backend/tests/test_rbac_middleware_cache.py,tests/test_rbac_permission_denied_audit.py: add the missingautobot_shared.logging_manager/autobot_shared.ssot_constantsstubs, wrap theexec_module()call +sys.modulesrestore intry/finallyso a future import added torbac_middleware.pycan't leak stub state again, fixget_logger's stub to be the reallogging.getLogger(not a swallowingMagicMock), and fix the fake Redis client's.pipeline()to match the real synchronous-pipeline/async-.execute()shape.Test-only fix (stale patch target, unrelated to the leak):
autobot-slm-backend/tests/api/test_auth_logout.py: patchget_async_redis_clientinstead of the removedget_redis_client.Production security fix (
autobot-slm-backend/api/sso_auth.py), both on public, pre-auth SSO endpoints, both with new regression tests proven to fail against the pre-fix code:urlsplit().portread inside the existing try/except — it re-parses lazily and raisesValueErrorfor a non-numeric or >65535 port, which previously escaped as an unhandled 500 instead of the intended 400.X-Forwarded-Protooutside{http, https}before it reaches the returnedredirect_uri— it was unvalidated attacker input reaching the response unlikeX-Forwarded-Hoston the next line._reject_callback()to keep the function within the house line-count convention with the two new guards.Test-only fix (
autobot-slm-backend/tests/api/test_sso_auth.py): stubapi.securityin_load_sso_auth_with_stubs()— its absence meantsso_auth.py'sfrom api.security import create_audit_logreal-importedapi/security.py, whose route decorators needfastapi.status.HTTP_201_CREATED, present on a fullMagicMockfastapi but not on the narrower stub some tests pass. Discovered because the pre-existingtest_build_callback_url_invalid_host_rejectedhas the identical failure mode in isolation.Verification
Rebased onto
origin/Dev_new_guiatc4938ec36(#13314's 12-way shard) for the sweeps below, then rebased once more onto93c17474d(#13328, the issue-link gate widening) before the final push — no test-affecting change in that second rebase. All numbers below are from thec4938ec36base.Accounting reconciled against a full sweep on the rebased base, before any commit in this PR (
-n auto --dist loadscope, unsharded — equivalent to any one shard's internal collection since pytest-split deselects after import): 33 failed, 1462 passed, 10 errors. 33 + the 23 below = 56, matching #13312 exactly.33 verified-resolved by this PR, enumerated by file:
tests/api/test_sso_auth.pytests/test_service_management_gate.pyservices/jwks_verifier_test.pyapi/code_source_test.pytests/test_prometheus_registry_endpoint.pytests/test_api_request_counter.pytests/api/test_node_code_status_read_derivation_12428.pytests/api/test_role_migration_extra_vars.pytests/test_code_sync_topology.pytests/api/test_nodes_list_timeout_10913.pytests/api/test_auth_logout.py23 tracked in #13312 with no reproducible local failure and no known cause — not touched, not claimed as resolved:
migrations/migrate_sso_secrets_test.py— 18. Ran standalone, full-suite unsharded, and sharded (--splits 12 --group 1, the shard this file lands in): passes every time, before and after this PR.git log adf289c60..HEADon the migration script and its test is empty. I have no working hypothesis for why CI's original run saw 18 failures here and explicitly do not carry one — an earlier draft of this PR blamed a worker-count-dependent pollution reach, which a-n 4 --covre-run disproved (byte-identical 33/10 failure set): pytest-xdist workers each perform a full collection, so the leak's reach does not vary with worker count, and it isn't the explanation for this cluster either.tests/services/version_checker_test.py— 5. Same: no reproducible failure found in any invocation.Full local sweep after this PR, same unsharded invocation:
1556 passed, 3 skipped, 1 xfailed, 0 failed, 0 errors.Sharded verification matching the actual CI gate (
--splits 12 --group N --durations-path .test_durations_slm, the exact flagsci.yml'spython-shardjob uses): every file this PR touches or that the leak fix clears lands in shard 1 or shard 5 of 12. Both shards, post-fix:Zero failures, zero errors in either shard.
New regression tests, proven to fail against the pre-fix
api/sso_auth.py:Both pass against the fixed code, along with the full existing 15-test file (17 total after the additions).
Collection errors, part of #13312's tracked "11 errors" bucket per the two RBAC files:
test_rbac_middleware_cache.pyandtest_rbac_permission_denied_audit.pyfailed to collect (ModuleNotFoundError) both standalone and in every full-suite run before this PR; both collect and pass fully after.test_redis_service_router.pyandmain_ensure_local_node_test.py(7 sub-tests) also went from erroring to passing locally as a further side effect of the leak fix — not claimed as part of the tracked 11 since that isn't independently confirmed.No production code changed except the two
_build_callback_urlguards inapi/sso_auth.py. flake8/isort/black clean on every touched file. No commit trailers.Refs #13312
Model Used
Sonnet 5