Skip to content

fix(node): gate fork_repo on per-caller path-scoped visibility (#98)#109

Merged
kevincodex1 merged 2 commits into
mainfrom
fix/fork-repo-path-scope-bypass
Jun 28, 2026
Merged

fix(node): gate fork_repo on per-caller path-scoped visibility (#98)#109
kevincodex1 merged 2 commits into
mainfrom
fix/fork-repo-path-scope-bypass

Conversation

@beardthelion

@beardthelion beardthelion commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

Closes #98.

fork_repo authorized the caller only at the repo root / and then git clone --mirrord the whole source, discarding the visibility rules it had just fetched. A non-owner with root read but a denied path-scoped subtree could fork the full mirror and obtain blobs the filtered git_upload_pack path withholds.

Fix

Refuse the fork when the caller has any withheld path in the source, the same per-caller decision the read path makes:

  • New pure helper fork_withheld_blocks(rules, is_public, owner_did, caller) returns true (refuse) iff withheld_globs(...) is non-empty for that caller. It delegates to the existing visibility_check/withheld_globs seam, so the owner bypass (full and short DID) and reader_dids grants are inherited from the read path rather than reimplemented, and the two cannot drift.
  • The gate runs before repo_store.acquire, so no withheld object is materialized on disk, and fails closed with RepoNotFound (mirroring authorize_repo_reads Deny) so a withheld subtrees existence is not leaked.

Owners, fully-authorized readers, and forks of repos with no path-scoped withholding for the caller are unaffected.

The predicate is a conservative over-approximation of the read paths object-level withholding: never weaker (so fork cannot leak), and marginally stricter only in the duplicate/co-located-blob case. A filtered fork projection (serve readers a fork minus the withheld subtree) is the larger follow-up and is intentionally out of scope.

Tests

  • 8 unit tests on the pure predicate: owner full/short DID, a non-owner denied a subtree (the core regression), a non-owner who is a listed reader_did of that subtree (must still be allowed to fork), root-only rule, no rules, mixed root+subtree, and the partial-reader case.
  • One #[sqlx::test] integration test that drives the handler: a public repo with a /secret/** rule excluding the caller returns 404 and creates no fork row. Because the gate precedes acquire, it needs no on-disk source.

Scope note

While mapping every content-egress path during review, I found a separate, pre-existing leak in GET /ipfs/{cid} (api/ipfs.rs get_by_cid): it returns any git object by raw SHA-256 across all repos with no visibility check, which is the same bypass class on a different surface. It is out of scope here and filed separately.

Summary by CodeRabbit

  • Bug Fixes
    • Forking a repository now correctly refuses requests when any path-scoped content would be hidden from the caller.
    • Improved privacy handling so restricted subtrees do not reveal whether they exist, returning a generic not-found response instead.
    • Added regression coverage to verify fork requests are denied for partially restricted repositories and no fork is created after a failed request.

fork_repo authorized the caller only at the repo root "/" and then
git clone --mirror'd the full source, discarding the visibility rules.
A non-owner with root read but denied a path-scoped subtree could fork
the full mirror and obtain blobs the filtered git_upload_pack path
withholds.

Refuse the fork when the caller has any withheld glob (per-caller
withheld_globs non-empty), the same decision the read path makes;
owners and authorized subtree readers are unaffected. Fail closed with
a not-found response, before repo_store.acquire, so no withheld object
is materialized. Add unit coverage for the policy and a direct-mount
integration test pinning the gate wiring.
…globs

Address code-review findings on the #98 fork gate:
- Document that fork_withheld_blocks's prefix-probe equivalence with the
  serve path depends on validate_path_glob keeping "/" the only whole-repo
  scope, mirroring has_path_scoped_rule's caveat (latent-coupling guard).
- Import withheld_globs and call it unqualified, consistent with the
  sibling visibility_check import in the same module.
@coderabbitai

coderabbitai Bot commented Jun 27, 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: 08e99dac-d24c-4ffd-a66e-72b47d3ee376

📥 Commits

Reviewing files that changed from the base of the PR and between 6809201 and b2c83ea.

📒 Files selected for processing (2)
  • crates/gitlawb-node/src/api/repos.rs
  • crates/gitlawb-node/src/test_support.rs

📝 Walkthrough

Walkthrough

fork_repo now retains the visibility rules returned by authorize_repo_read and passes them through a new fork_withheld_blocks helper. When that helper finds any path-scoped subtree would be withheld from the caller, the fork is refused with RepoNotFound before any on-disk clone occurs. Unit tests and one SQLx integration test validate the new gate.

Fork withheld-subtree gate

Layer / File(s) Summary
fork_withheld_blocks helper and fork_repo enforcement
crates/gitlawb-node/src/api/repos.rs
Adds withheld_globs to imports, introduces the private fork_withheld_blocks boolean helper (delegating to withheld_globs), and updates fork_repo to retain fetched visibility rules and return RepoNotFound when the helper signals any path-scoped block would be withheld.
Unit and integration tests
crates/gitlawb-node/src/api/repos.rs, crates/gitlawb-node/src/test_support.rs
Unit tests cover owner vs. non-owner, path-scoped allow/deny, whole-repo / rule exclusion, empty rules, mixed root+denied, and partial-reader cases. The SQLx integration test seeds a /secret/** rule, calls the fork endpoint as a stranger, asserts 404, and confirms no fork row is written.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant fork_repo
  participant authorize_repo_read
  participant fork_withheld_blocks
  participant withheld_globs

  Caller->>fork_repo: POST /api/v1/repos/{owner}/fork-repo/fork
  fork_repo->>authorize_repo_read: caller DID, repo path "/"
  authorize_repo_read-->>fork_repo: (source_repo, visibility_rules)
  fork_repo->>fork_withheld_blocks: (caller_did, visibility_rules)
  fork_withheld_blocks->>withheld_globs: evaluate path-scoped rules
  withheld_globs-->>fork_withheld_blocks: non-empty withheld set
  fork_withheld_blocks-->>fork_repo: true
  fork_repo-->>Caller: 404 RepoNotFound (no clone performed)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Gitlawb/node#33: Introduces withheld_globs that fork_withheld_blocks directly delegates to.
  • Gitlawb/node#52: Establishes the authorize_repo_read/RepoNotFound pattern that the fork gate extends.
  • Gitlawb/node#84: Tightens the withheld-blob/path computation in visibility_pack.rs that underpins the same withheld_globs call.

Suggested labels

sev:high, crate:node, subsystem:visibility, kind:bug

Suggested reviewers

  • kevincodex1
  • jatmn

Poem

A mirror once copied all secrets within,
Now withheld globs say "stranger, you cannot begin."
The fork gate stands firm with a 404 wall,
No subtree for strangers — fail-closed for all.
🐇 The rabbit guards paths with a resolute hop!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: gating fork_repo on per-caller path-scoped visibility for issue #98.
Description check ✅ Passed The description covers the problem, fix, tests, and scope notes, though it doesn't fully follow the template headings.
Linked Issues check ✅ Passed The changes implement the required fail-closed fork gate, preserve owner/reader access, and add tests for the regression.
Out of Scope Changes check ✅ Passed The code changes stay focused on the fork visibility gate and its tests, with no unrelated behavior changes evident.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ 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 fix/fork-repo-path-scope-bypass

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

@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 contribution. I do not see any actionable issues from my review.

@kevincodex1 LGTM

@kevincodex1 kevincodex1 merged commit 6ae316c into main Jun 28, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fork_repo clones a full mirror after only a root (/) visibility check, bypassing path-scoped withholding

3 participants