Demo (not for merge): is_public enforcement on the git read path — grounds the #18 design decisions#24
Conversation
…e-read) `repos.is_public` is stored on every repo but never checked when serving clone/fetch, so private repos are world-readable over git smart-HTTP. This wires read enforcement for the whole-repo (scope=`/`) case — the literal "Implement private-read enforcement" short-term roadmap item. node: - api/repos.rs: add `authorize_read()`. Public repos stay open; private repos require the caller's authenticated DID to match the owner. Returns 404 (not 403) on denial so a private repo's existence does not leak. Mirrors the owner-match idiom in api/protect.rs. Gates `git_upload_pack` and the `git-upload-pack` branch of `git_info_refs`; the receive-pack (push) handshake is left untouched (authorized separately on the POST). - server.rs: move `info/refs` into `git_read_routes` and layer `optional_signature`, so an `AuthenticatedDid` is available on reads when a signature is present, without breaking anonymous clone of public repos. client (git-remote-gitlawb): - main.rs: sign the GET ref-advertisement, and broaden POST signing from push-only to fetch too, so a `git clone` of a private repo can authenticate. Out of scope (deliberately): path/package-scoped visibility, the gossip/sync and GraphQL/IPFS read surfaces, and the never-replicate-vs-fetch-denied question. See proposal discussion. Verification: `cargo check --workspace` clean (0 errors, 0 warnings); git-remote-gitlawb unit tests 6/6 pass.
Six unit tests over the full visibility matrix for `authorize_read`:
public → allow (anonymous and any DID); private → allow owner; private → deny
anonymous; private → deny non-owner. Plus a no-leak contract test asserting the
denial payload is byte-identical to a missing repo (`RepoNotFound("owner/secret")`),
so a private repo's existence cannot be inferred from the response.
Red-green verified: disabling the enforcement check fails exactly the three
denial tests and leaves the three allow tests green.
|
copy |
|
No rush on the build-out. Just the one fork when you have a sec: for gated content, should a peer lacking authorization (a) never receive the objects in gossip/IPFS at all, or (b) receive hashes/metadata and get capability-required on content fetch? Even a one-letter answer unblocks the path-scoping work. Also fine to hear if it is already claimed. |
|
Superseded by #25, which is the real, mergeable Phase 1 of the path-scoped visibility feature. This demo did its job: it grounded the design decisions by proving the HTTP read-path enforcement end to end (anon and wrong-DID get a 404 byte-identical to a missing repo, owner gets 200). #25 builds the actual path-scoped model on top (visibility_rules with path globs, reader-DID allow-lists, owner-only management API, gl visibility CLI), with whole-repo rules fully enforced. Happy to close this in favor of #25 whenever you like. The one open question from the thread (the a/b replication semantics) still gates the gossip/IPFS phase, but Phase 1 stands on its own regardless. |
|
Closing this out. Phase 1 landed in #25 (merged into main as 6abaf1d), so the whole-repo enforcement slice this PR demoed is now the real thing. Thanks for the look, @kevincodex1. |
Not a merge candidate — a working demonstration to make the open design decisions in #18 concrete instead of theoretical. It wires the
is_publicflag the node already stores but never checks on the git read path, and runs end to end against a local node. Please read the "What this does NOT do" section before reviewing as a feature.What works
authorize_readgates the git smart-HTTP read path (info/refs?service=git-upload-packandgit-upload-pack) on the DID that signed the request (verified upstream byoptional_signature). Unauthorized reads return 404, not 403, byte-identical to a missing repo.404(identical to missing repo)200404200(unchanged)A 6-case truth-table unit test pins this, including an assertion that the private-denial payload is indistinguishable from a missing repo. (
cargo fmt/clippyclean, rebased onmain, no conflicts.)What this does NOT do (why it is a demo, not a feature)
These are deliberate, and several are genuinely maintainer decisions, not things I should pick unilaterally:
authorize_readis wired into the two HTTP handlers only. The IPFS read path (api/ipfs.rs,ipfs_pin.rs) and gossip replication still serve the objects ungated. So a repo marked private here is still readable through those paths. This is the single biggest reason it must not be mistaken for real privacy, and closing those surfaces is the same decision as the replication question below.caller_did == owner_did. There is no way to grant another DID read access, so this expresses "private to me alone," not the capability-based reader sets Path/package-scoped visibility — make "private" a property of a subtree, not a whole repo #18 is actually about.is_publicboolean; it does not add a path-scoped data model. Path/package-scoped visibility — make "private" a property of a subtree, not a whole repo #18 argues the data model should carry path scope from day one to avoid a later migration — this demo deliberately does not, pending your call on that.gl. The create API acceptsis_public:false, but everyglpath hard-codestrueand there is no endpoint to change visibility after creation. The demo was exercised by hand-crafting the create call.The decision that unblocks the real feature
@kevincodex1 the fork that gates everything (path-scoping, and surfaces #1 above) is replication of gated content: should a peer lacking authorization (a) never receive the objects in gossip/IPFS at all, or (b) receive hashes/metadata and get capability-required on content fetch? (a) is stricter; (b) is friendlier to the durability goal but leaks existence. The byte-identical-404 in this demo is one expression of (a) on the HTTP path; if you want (b), even this behavior changes. Pick a direction and I will build the path-scoping layer on top.
Also: is private-read / authorization enforcement already claimed or in progress? I would rather build on it than collide.