test(node): guard that every repo-scoped handler is visibility-gated#122
Conversation
Mirrors the existing every_in_scope_mutation_has_its_gate source-string CI test on the egress side: scrapes every repo-scoped handler (any pub/pub(crate) async fn taking Path<(String,String..)>) and asserts it carries an authz marker, either a read gate (authorize_repo_read / visibility_check) or a write/owner/self gate (require_repo_owner / require_owner / did_matches / &auth.0), or is listed in KNOWN_UNGATED with its tracking issue. Verb-agnostic on purpose: a repo-scoped read named outside list_/get_ (git_info_refs, replicate_encrypted_blobs, withheld_paths) is still checked, so a new ungated egress of any name fails CI instead of shipping as the next leak. A completeness scan over src/api/ asserts every module declaring a repo-scoped handler is wired into the guard, so a whole new file cannot add an ungated handler the scrape never looks at. KNOWN_UNGATED is the live gap list (certs/issues/labels/ bounties/stars per #120, plus events/webhooks/replicas/protected pending PR #113); the staleness assert forces each entry out the moment its gate lands. A count floor trips if the scrape silently breaks, and unit tests pin handler_names and is_repo_scoped. Verified by execution: removing an allowlist entry trips the missing-gate assert; listing a gated handler trips the staleness assert; dropping a source file trips the completeness scan; breaking the parser trips the count floor; and a non-list/get handler (git_info_refs) is confirmed scraped and gate-checked.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds source-scraping authz coverage checks in ChangesAuthz Gate Coverage Enforcement
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
jatmn
left a comment
There was a problem hiding this comment.
I found an issue that needs to be addressed before this is ready.
Findings
- [P2] Do not let a partial gate satisfy the egress guard
crates/gitlawb-node/src/api/mod.rs:388
The new guard treats a handler as gated when any marker string appears anywhere in its body, sogit_info_refscurrently passes because it containsvisibility_check(. In the actual handler, though, that check only runs underif service == "git-upload-pack";service=git-receive-packskips it and is the live private-ref advertisement gap being fixed separately in #119. That means this PR would merge a guard that says every repo-scoped handler is gated while the knowngit_info_refsegress is neither fully gated nor inKNOWN_UNGATED, and the same pattern would miss future partially-gated handlers. Please make the guard distinguish fully-applied gates from conditional/partial ones, or explicitly trackgit_info_refsas a known ungated gap until the #119 fix lands.
…uard (#122) The guard counted a handler as gated on bare substring presence of a gate marker, so git_info_refs false-passed on visibility_check( even though that check only runs under `if service == "git-upload-pack"` and leaves the git-receive-pack advertisement ungated. Add gate_runs_unconditionally, which ignores markers that sit only inside an `if service ==` block, and track git_info_refs in KNOWN_UNGATED until #119 makes the gate unconditional. When that lands, the staleness assert forces the entry's removal.
…#122) Code review of the egress-guard hardening: - Clamp the span-advance index so an unclosed `if service ==` block can't slice past the body end and panic (latent: real source is balanced, but the scraper should not crash on a pathological future body). Keep span-to-EOF as the fail-safe direction. - Document that only `if service ==` is detected, not `match service`, so a future match-arm gate isn't silently treated as full. - Add unit cases: gate inside both service blocks (conditional), gate inside and outside (full), and the unclosed-block no-panic regression.
|
Addressed in cbde4fd, with a follow-up hardening in 93b842b. The guard counted a handler as gated on bare substring presence, so
|
jatmn
left a comment
There was a problem hiding this comment.
Thanks for the update. I rechecked the previously discussed paths and do not see any remaining actionable issues from my side.
@kevincodex1 LGTM
The repo-scoped egress guard's allowlist (added in #122, merged to main after this branch was opened) still listed git_info_refs as ungated. This branch makes the info/refs visibility gate unconditional for both git-upload-pack and git-receive-pack, so the handler is now gated and the staleness assert requires removing it from the allowlist.
…e to 0.4.0 The repo-scoped egress guard's allowlist (added in #122, merged after this branch was opened) still listed the read surfaces this branch gates. Now that list_labels, list_repo_events, list_webhooks, list_replicas, and list_protected_branches are read-visibility gated, the staleness assert requires removing them from the allowlist. The #120 surfaces (certs, issues, bounties, stars) stay listed; git_info_refs stays until #119 lands. Lockfile versions follow the 0.4.0 release the rebase landed on.
The repo-scoped egress guard's allowlist (added in #122, merged to main after this branch was opened) still listed git_info_refs as ungated. This branch makes the info/refs visibility gate unconditional for both git-upload-pack and git-receive-pack, so the handler is now gated and the staleness assert requires removing it from the allowlist.
Every PR that gates one read surface keeps revealing two more (A1/#116, #110, #112, #114, #120, #121). The root cause is structural: the visibility invariant is re-applied by hand on each egress path, and nothing fails when a new path forgets it. The codebase already proves the fix shape works for mutations (
authz_guard::every_in_scope_mutation_has_its_gate); this adds the read/egress equivalent.every_repo_scoped_handler_is_gatedscrapes every repo-scoped handler (anypub/pub(crate) async fntakingPath<(String, String..)>) and asserts it carries an authz marker, either a read gate (authorize_repo_read/visibility_check) or a write/owner gate (require_repo_owner/require_owner/did_matches/ a&auth.0self-binding), or is listed inKNOWN_UNGATEDwith its tracking issue. It is verb-agnostic on purpose, so a repo-scoped read named outsidelist_*/get_*(the git transport handlers,replicate_encrypted_blobs,withheld_paths) is still checked. A completeness scan oversrc/api/trips for a whole new module that adds a repo-scoped handler without being wired in. So a new ungated egress of any name fails CI instead of shipping as the next leak.KNOWN_UNGATEDis the live gap list, currently thecerts/issues/labels/bounties/starsreads (#120) plus theevents/webhooks/replicas/protectedreads being closed in #113. A staleness assert forces each entry out the moment its gate lands, so the allowlist cannot outlive the bug it tracks.Scope: this is a source scrape (paired with a runtime route guard for behaviour). It proves a gate is called, not that it runs on the requested subtree, and it sees the owner+repo tuple
Pathshape. Those limits are documented in the test and pinned by the helper unit tests.Verified by execution: it covers ~54 handlers today, and removing an allowlist entry, listing a gated handler, dropping a source file, breaking the parser, and confirming a non-list/get handler is scraped each trip the guard.
Summary by CodeRabbit