Skip to content

test(node): guard that every repo-scoped handler is visibility-gated#122

Merged
kevincodex1 merged 3 commits into
mainfrom
test/egress-read-gate-guard
Jun 30, 2026
Merged

test(node): guard that every repo-scoped handler is visibility-gated#122
kevincodex1 merged 3 commits into
mainfrom
test/egress-read-gate-guard

Conversation

@beardthelion

@beardthelion beardthelion commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

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_gated 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 gate (require_repo_owner / require_owner / did_matches / a &auth.0 self-binding), or is listed in KNOWN_UNGATED with its tracking issue. It is verb-agnostic on purpose, so a repo-scoped read named outside list_*/get_* (the git transport handlers, replicate_encrypted_blobs, withheld_paths) is still checked. A completeness scan over src/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_UNGATED is the live gap list, currently the certs/issues/labels/bounties/stars reads (#120) plus the events/webhooks/replicas/protected reads 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 Path shape. 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

  • Bug Fixes
    • Strengthened authorization enforcement verification to ensure repo-scoped API handlers are always protected by the proper access control gate.
    • Added safeguards to flag newly added or reorganized API modules that introduce repo-scoped handlers without required protection.
  • Tests
    • Expanded automated coverage for detecting routed async handlers, identifying repo-scoped signatures, and confirming gate application behavior—reducing the chance of missed authorization gaps.

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.
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b815620c-2fb1-40bc-8ec2-203ea965f6a8

📥 Commits

Reviewing files that changed from the base of the PR and between 92dbdfa and 93b842b.

📒 Files selected for processing (1)
  • crates/gitlawb-node/src/api/mod.rs

📝 Walkthrough

Walkthrough

Adds source-scraping authz coverage checks in crates/gitlawb-node/src/api/mod.rs that detect repo-scoped handlers, classify unconditional gate markers, and fail when repo-scoped API handlers lack a gate or approved exception.

Changes

Authz Gate Coverage Enforcement

Layer / File(s) Summary
Handler scraping and repo-scoped detection
crates/gitlawb-node/src/api/mod.rs
Adds handler_names to collect async handler names from source text and is_repo_scoped to detect owner+repo Path signatures, with unit tests covering both helper boundaries.
Conditional gate detection
crates/gitlawb-node/src/api/mod.rs
Adds gate_runs_unconditionally to treat markers inside if service == ... blocks as conditional only, with unit tests covering conditional, mixed, and unclosed block cases.
Repo-scoped gate enforcement test
crates/gitlawb-node/src/api/mod.rs
Expands every_repo_scoped_handler_is_gated to scan listed and unlisted src/api modules, verify allowlisted names, require an unconditional gate or tracked exception for each repo-scoped handler, and enforce a minimum discovered handler count.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

  • Gitlawb/node#52: Adds the authz plumbing whose gate markers are now scraped and validated by this PR.

Suggested reviewers

  • jatmn
  • kevincodex1

🐇 I hopped through source and counted gates,
Sniffed out paths and guarded states.
If a handler wanders bare,
The bunny finds it այնտեղ—fair and square,
And keeps the authz carrots safe. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the change, but it does not follow the required template sections or include verification steps and checklist items. Rewrite the PR description using the repository template: add Summary, Motivation & context, Kind of change, What changed, and How a reviewer can verify sections.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: a new guard for repo-scoped handler visibility gating.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test/egress-read-gate-guard

Comment @coderabbitai help to get the list of available commands.

@beardthelion beardthelion added kind:test Test coverage or harness crate:node gitlawb-node — the serving node and REST API subsystem:visibility Path-scoped visibility and content withholding subsystem:api Node REST API request/response surface sev:low Cosmetic, cleanup, or nice-to-have labels Jun 29, 2026

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, so git_info_refs currently passes because it contains visibility_check(. In the actual handler, though, that check only runs under if service == "git-upload-pack"; service=git-receive-pack skips 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 known git_info_refs egress is neither fully gated nor in KNOWN_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 track git_info_refs as 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.
@beardthelion

Copy link
Copy Markdown
Collaborator Author

Addressed in cbde4fd, with a follow-up hardening in 93b842b.

The guard counted a handler as gated on bare substring presence, so git_info_refs false-passed on visibility_check( even though that call sits under if service == "git-upload-pack" and leaves the git-receive-pack advertisement ungated. Added gate_runs_unconditionally: a marker that appears only inside an if service == block no longer counts as a full gate. git_info_refs is now tracked in KNOWN_UNGATED against #119, and the staleness assert forces its removal the moment #119 makes the gate unconditional (verified by simulating that change: the assert trips its now gated staleness check).

git_info_refs is the only handler with an if service == discriminator, so no other handler's classification changes. 93b842b clamps the span scan against an unclosed block (no panic; fail-safe to over-flag) and documents that match service is not detected.

@beardthelion beardthelion requested a review from jatmn June 29, 2026 18:11

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update. I rechecked the previously discussed paths and do not see any remaining actionable issues from my side.

@kevincodex1 LGTM

@kevincodex1 kevincodex1 merged commit 86341d1 into main Jun 30, 2026
14 checks passed
beardthelion added a commit that referenced this pull request Jun 30, 2026
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.
beardthelion added a commit that referenced this pull request Jun 30, 2026
…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.
beardthelion added a commit that referenced this pull request Jul 3, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

crate:node gitlawb-node — the serving node and REST API kind:test Test coverage or harness sev:low Cosmetic, cleanup, or nice-to-have subsystem:api Node REST API request/response surface subsystem:visibility Path-scoped visibility and content withholding

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants