Skip to content

fix(node): gate /hooks and sibling read surfaces leaking private repo metadata (#94)#113

Open
beardthelion wants to merge 20 commits into
mainfrom
fix/gate-repo-read-surfaces
Open

fix(node): gate /hooks and sibling read surfaces leaking private repo metadata (#94)#113
beardthelion wants to merge 20 commits into
mainfrom
fix/gate-repo-read-surfaces

Conversation

@beardthelion

@beardthelion beardthelion commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator

Closes #94, and gates the three sibling read surfaces that share its pattern.

Problem

Four GET handlers on the node sat on the optional_signature read group with no ownership or visibility check, so an unauthenticated caller who knew a repo's owner/name could read data that should be private:

Fix

Gating model follows data sensitivity, not a blanket rule:

  • list_webhooks is owner-restricted but layered read-visibility-then-owner: headerless -> 401, non-reader of a private repo -> 404 (existence hidden, uniform with the siblings), non-owner of a public repo -> 403. Webhook callback URLs are owner-secret config.
  • list_replicas, list_protected_branches, list_repo_events gate on read visibility (authorize_repo_read at /, the INV-2 root-listing exception). Public repos stay anonymously readable; private repos return an opaque 404. replicas is documented as a public mirror-discovery surface, so read-visibility (not owner-only) preserves that for public repos.
  • list_repo_events only gates the locally-hosted branch: when the node holds the repo it runs visibility_check on the loaded record before serving either the cert or gossip events; a repo known only via gossip (no local row) keeps its existing public federation-feed behavior.

Each new gate is pinned into the existing authz_guard drift table so it cannot silently regress.

CLI/MCP

Tightening the node broke gl/MCP read commands that sent unsigned requests. Fixed the callers of the newly gated endpoints: gl webhook list and the MCP webhook_list tool now sign (get_signed); gl protect list, gl repo replicas, and the replica/branch sub-fetches in gl repo info/gl repo owner sign when an identity is present via a new NodeClient::get_maybe_signed (anonymous otherwise, so public repos still work without auth). Added --dir to gl repo replicas.

Tests

Test-first throughout. Coverage includes the full per-surface matrix (owner / non-owner / reader / stranger / headerless / anonymous / absent), the no-existence-oracle property (headerless on an existing-private vs an absent repo both 401), the gossip-only vs local-private split for events, both branches of get_maybe_signed, an end-to-end test that runs a real sign_request output through the node's optional_signature middleware, and signature-header assertions on the CLI/MCP signing paths.

Deferred (separate issues)

  • The global GET /api/v1/events/ref-updates feed is ungated for private-repo ref metadata (the REST analog of GraphQL refUpdates serves private-repo ref metadata to anonymous callers (visibility bypass) #112). Out of scope here; kept to the per-repo surfaces.
  • Several other gl/MCP read commands (gl repo commits, gl pr *, MCP repo_get/pr_*) hit endpoints that were already visibility-gated before this change and send unsigned requests, so they are broken for private repos independently of this PR. Worth a follow-up to sign all gl/MCP read commands.

Summary by CodeRabbit

  • New Features
    • Added optional identity-aware HTTP request signing for CLI read operations; reads are signed when credentials are available.
    • Added --dir support for gl repo replicas and gl webhook list to enable signed, owner-gated listing.
    • MCP webhook listing now uses signed requests.
  • Bug Fixes
    • Enforced read-visibility and owner-gating for replicas, protected branches, webhooks, repo events, and labels; unauthenticated/non-owner access returns consistent 401/403/404 and private details are redacted.
    • Improved fail-closed behavior for visibility gating and aligned owner identity matching.
  • Tests
    • Expanded HTTP/MCP and end-to-end RFC-9421 verification coverage for signing, gating, and secure event redaction.

@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Node read endpoints now enforce visibility or ownership checks for webhooks, replicas, protected branches, labels, and repo events. CLI read paths for webhooks, repo info, replicas, protected branches, labels, and MCP webhook listing now conditionally sign GET requests when an identity keypair is available.

Changes

Node-side endpoint authorization

Layer / File(s) Summary
Endpoint authorization changes
crates/gitlawb-node/src/api/webhooks.rs, crates/gitlawb-node/src/api/replicas.rs, crates/gitlawb-node/src/api/protect.rs, crates/gitlawb-node/src/api/events.rs, crates/gitlawb-node/src/api/labels.rs, crates/gitlawb-node/src/visibility.rs
list_webhooks, list_replicas, list_protected_branches, list_labels, and list_repo_events now accept authenticated caller context and apply visibility or ownership checks before returning data. is_owner now delegates to shared DID matching logic.
Gate markers and integration tests
crates/gitlawb-node/src/api/mod.rs, crates/gitlawb-node/src/test_support.rs
The authz guard fixture list includes events.rs, the marker expectations cover the updated handlers, and integration tests validate webhook, replica, protected-branch, label, and event visibility behavior.

CLI signed request support

Layer / File(s) Summary
Conditional signing helper
crates/gl/src/http.rs
NodeClient gains get_maybe_signed, which signs GET requests with RFC 9421 headers when a keypair exists and otherwise sends an anonymous GET.
Webhook list signing
crates/gl/src/webhook.rs
gl webhook accepts --dir, resolves repo ownership from the loaded identity, and uses a signed request for the hooks list endpoint.
Repo read signing
crates/gl/src/repo.rs
gl repo adds --dir for replicas, loads identities for repo info, replicas, and labels, and updates tests to cover signed and unsigned request paths.
Protected branches and MCP signing
crates/gl/src/protect.rs, crates/gl/src/mcp.rs
gl protect list switches to conditional signing, and the MCP webhook_list tool uses a signed request with matching tests.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • Gitlawb/node#25: Introduced the visibility primitives now used to gate repo-event reads.
  • Gitlawb/node#52: Added authorize_repo_read, which this PR applies to multiple read endpoints.
  • Gitlawb/node#87: Covers the per-route authorization enforcement pattern extended here.

Suggested reviewers

  • kevincodex1
  • jatmn

Poem

🐇 I hop through gates with a signed little grin,
Webhooks, labels, and branches tucked in.
Events stay hidden when visibility says no,
But trusted callers still get the show.
A carrot for auth, and a signature cheer—
The rabbit approves: safe reads are here!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: gating webhook and related private-repo read surfaces.
Description check ✅ Passed The description covers the problem, fix, tests, and follow-ups, though it doesn't mirror the full template.
Linked Issues check ✅ Passed The PR addresses #94 by gating webhook listing and the related private-repo read surfaces as requested.
Out of Scope Changes check ✅ Passed The additional CLI, MCP, and test updates support the same authorization fix and don't appear unrelated.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/gate-repo-read-surfaces

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/gl/src/webhook.rs (1)

157-164: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Check the webhook-list HTTP status before reading webhooks.

A 401/403/404 JSON error body has no webhooks field, so this currently prints “No webhooks” and exits successfully instead of surfacing the authorization failure.

Proposed fix
-    let resp: Value = client
+    let resp = client
         .get_signed(&format!("/api/v1/repos/{owner}/{repo}/hooks"))
-        .await?
-        .json()
-        .await
-        .context("invalid JSON")?;
+        .await?;
+    let status = resp.status();
+    let resp: Value = resp.json().await.unwrap_or_default();
+    if !status.is_success() {
+        let msg = resp["message"].as_str().unwrap_or("unknown error");
+        anyhow::bail!("webhook list failed ({status}): {msg}");
+    }
 
     let hooks = resp["webhooks"].as_array().cloned().unwrap_or_default();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/gl/src/webhook.rs` around lines 157 - 164, The webhook listing flow in
`list_webhooks` is treating error responses as empty results because it parses
JSON and reads `resp["webhooks"]` without checking the HTTP status first. Update
the `client.get_signed(...).await?` handling to inspect the response status
before deserializing or accessing `webhooks`, and surface 401/403/404 as errors
instead of falling back to `No webhooks`. Keep the fix localized around the
`list_webhooks` response handling and the `webhooks` extraction logic.
🧹 Nitpick comments (1)
crates/gitlawb-node/src/api/mod.rs (1)

177-181: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Guard both halves of list_webhooks authorization.

This row only enforces the owner gate. Add a second marker for authorize_repo_read( so the drift test also catches regressions that reintroduce private-repo existence leaks before require_repo_owner.

🧪 Proposed guard hardening
             // Bucket A/B hybrid — list_webhooks is read-visibility THEN owner:
             // authorize_repo_read 404s a non-reader of a private repo, then
             // require_repo_owner 403s a non-owner of a public one. The
             // require_repo_owner marker guards the owner half.
+            (webhooks, "list_webhooks", "authorize_repo_read("),
             (webhooks, "list_webhooks", "require_repo_owner("),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/gitlawb-node/src/api/mod.rs` around lines 177 - 181, The
`list_webhooks` drift-test row in `api::mod` only checks the owner gate, so it
can miss regressions in the read-visibility half. Update the marker set for
`list_webhooks` to include both `authorize_repo_read(` and `require_repo_owner(`
so the test covers the private-repo existence check as well as the owner check.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/gitlawb-node/src/api/events.rs`:
- Around line 66-80: The repo visibility gate in events handling is failing open
because `repo_record` is built with `.ok().flatten()`, so a database lookup
error is indistinguishable from a missing local repo and skips the
`visibility_check` path. Update the `events` flow to preserve and propagate the
lookup error from the local repo fetch, and only allow the ungated gossip-only
behavior when the lookup successfully returns `Ok(None)`. Keep the existing
`visibility_check`/`RepoNotFound` logic in the `repo_record`-based branch so
`api::events` fails closed for local private repos.

---

Outside diff comments:
In `@crates/gl/src/webhook.rs`:
- Around line 157-164: The webhook listing flow in `list_webhooks` is treating
error responses as empty results because it parses JSON and reads
`resp["webhooks"]` without checking the HTTP status first. Update the
`client.get_signed(...).await?` handling to inspect the response status before
deserializing or accessing `webhooks`, and surface 401/403/404 as errors instead
of falling back to `No webhooks`. Keep the fix localized around the
`list_webhooks` response handling and the `webhooks` extraction logic.

---

Nitpick comments:
In `@crates/gitlawb-node/src/api/mod.rs`:
- Around line 177-181: The `list_webhooks` drift-test row in `api::mod` only
checks the owner gate, so it can miss regressions in the read-visibility half.
Update the marker set for `list_webhooks` to include both `authorize_repo_read(`
and `require_repo_owner(` so the test covers the private-repo existence check as
well as the owner check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ee75b2eb-a906-4a01-b6ce-0fd597b7eec6

📥 Commits

Reviewing files that changed from the base of the PR and between 6ae316c and e2ba8a8.

📒 Files selected for processing (11)
  • crates/gitlawb-node/src/api/events.rs
  • crates/gitlawb-node/src/api/mod.rs
  • crates/gitlawb-node/src/api/protect.rs
  • crates/gitlawb-node/src/api/replicas.rs
  • crates/gitlawb-node/src/api/webhooks.rs
  • crates/gitlawb-node/src/test_support.rs
  • crates/gl/src/http.rs
  • crates/gl/src/mcp.rs
  • crates/gl/src/protect.rs
  • crates/gl/src/repo.rs
  • crates/gl/src/webhook.rs

Comment thread crates/gitlawb-node/src/api/events.rs

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

[P2] Fail closed when the local repo lookup errors
crates/gitlawb-node/src/api/events.rs:64
CodeRabbit's inline review item is still valid: get_repo errors are currently collapsed with .ok().flatten(), so a database error is indistinguishable from Ok(None) and the handler takes the ungated gossip-only path. That means the new private-repo visibility gate only runs when the lookup succeeds; on a lookup error, the handler can continue into the public event feed path instead of failing closed. Please complete that review request by propagating the lookup error and only using the gossip-only behavior for an actual Ok(None).

[P2] Surface webhook-list authorization failures instead of printing an empty list
crates/gl/src/webhook.rs:157
CodeRabbit's outside-diff review item is still valid: gl webhook list signs the request now, but it immediately deserializes the response and reads resp["webhooks"] without checking the HTTP status. A 401/403/404 JSON error body has no webhooks field, so the command prints No webhooks for and exits successfully instead of telling the user that the node rejected the request. Please complete that review request by checking resp.status() before extracting webhooks, matching the status handling already used by the neighboring list commands.

[P3] Pin both halves of the list_webhooks authorization gate
crates/gitlawb-node/src/api/mod.rs:181
CodeRabbit's drift-guard comment is still valid: the authz_guard row for list_webhooks only checks for require_repo_owner(, even though this route depends on authorize_repo_read( first to hide private-repo existence before the owner-only check runs. The behavior tests cover the current implementation, but the static guard can still pass if the read-visibility half is later removed. Please add the authorize_repo_read( marker for list_webhooks too, so the regression guard matches the layered security boundary this PR is adding.

@beardthelion beardthelion added kind:security Vulnerability fix or hardening crate:node gitlawb-node — the serving node and REST API subsystem:api Node REST API request/response surface subsystem:visibility Path-scoped visibility and content withholding sev:high Major break or real security/trust risk, no easy workaround labels Jun 29, 2026
@beardthelion beardthelion requested a review from jatmn June 29, 2026 01:36

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/gitlawb-node/src/api/events.rs (1)

55-87: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use did_matches(...) for the owner fast-path behind this new gate.

This handler now relies on visibility_check to admit the repo owner, but that helper still treats ownership as caller == owner_did || caller == owner_short. If a local repo row is stored in bare form, a signed did:key:... owner is denied here and gets the opaque 404 reserved for non-readers.

Based on learnings, owner DID comparisons must normalize bare and full forms with did_matches(...).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/gitlawb-node/src/api/events.rs` around lines 55 - 87, The new
visibility gate in the events handler is still relying on raw owner DID
equality, which can wrongly deny the repo owner when the stored owner is in bare
form and the caller is a full DID. Update the owner fast-path in the events flow
by using did_matches(...) for the ownership check, and apply it consistently in
the visibility_check path around the repo_record handling so bare and full DID
forms both match correctly.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@crates/gitlawb-node/src/api/events.rs`:
- Around line 55-87: The new visibility gate in the events handler is still
relying on raw owner DID equality, which can wrongly deny the repo owner when
the stored owner is in bare form and the caller is a full DID. Update the owner
fast-path in the events flow by using did_matches(...) for the ownership check,
and apply it consistently in the visibility_check path around the repo_record
handling so bare and full DID forms both match correctly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dc377dcf-7169-4355-96cd-8ce371715b57

📥 Commits

Reviewing files that changed from the base of the PR and between e2ba8a8 and 8453814.

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

@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 changed paths and found a few issues that still need to be addressed.

Findings

  • [P2] Resolve webhook-list repos from the signing identity
    crates/gl/src/webhook.rs:153
    The new --dir support loads and signs with the user's keypair, but cmd_list still calls resolve_owner(&client), which gets the owner segment from the node root DID rather than from the keypair or an owner/repo argument. For a normal repo created by the local identity on a public node, gl webhook list my-repo --dir ... therefore signs as the repo owner but requests /api/v1/repos/<node-did>/my-repo/hooks, so the newly owner-gated route returns not found/forbidden instead of listing the user's hooks. Please resolve the owner the same way the other repo-scoped commands do: accept owner/repo when provided, and otherwise derive the owner from --dir before falling back to the node DID.

  • [P2] Complete the webhook-list status handling request
    crates/gl/src/webhook.rs:157
    CodeRabbit's outside-diff request, and the later jatmn change request based on it, is still valid in the current patch. gl webhook list signs the request now, but it immediately deserializes the response and reads resp["webhooks"] without checking the HTTP status. A 401/403/404 JSON error from the owner-gated endpoint has no webhooks field, so the command prints No webhooks for <repo> and exits successfully instead of surfacing the authorization failure. Please complete that request by keeping the Response, checking status.is_success() before extracting webhooks, and returning an error on rejected responses, like the neighboring list commands already do.

  • [P2] Use DID-normalized owner matching in the visibility gate
    crates/gitlawb-node/src/visibility.rs:17
    CodeRabbit's current owner-normalization request is still valid. The newly added event gate, and the new authorize_repo_read callers for replicas/protected branches/webhooks, rely on visibility_check to admit the repo owner, but visibility_check's is_owner helper only checks caller == owner_did || caller == owner_short. If a repo row stores the owner in bare form, as mirror rows do, a signed full did:key:... owner is denied by the new gate and gets the opaque 404 path reserved for non-readers. Please switch this helper to the same did_matches semantics used by require_repo_owner, and add coverage for the bare-owner/full-caller case on the newly gated read path.

  • [P3] Complete the list_webhooks authz-guard request
    crates/gitlawb-node/src/api/mod.rs:181
    The previous CodeRabbit/jatmn drift-guard request is still only half addressed. The implementation of list_webhooks intentionally depends on authorize_repo_read first and require_repo_owner second, but the authz_guard row only pins the owner half. That means the static guard can still pass if the private-repo existence-hiding gate is later removed. Please add the authorize_repo_read( marker for list_webhooks too, so the guard matches the layered authorization boundary this PR is adding.

beardthelion added a commit that referenced this pull request Jun 29, 2026
Address jatmn's review feedback on the read-surface gate:

- gl webhook: resolve owner from the signing identity, not the node DID.
  cmd_create/list/delete all routed through the local resolve_owner helper,
  which only ever returned the node root DID, so for a repo owned by the
  local identity the owner-gated /hooks route returned 404/403. Drop that
  helper and reuse repo::resolve_owner_repo_pair (accepts owner/repo, else
  derives from --dir/keypair). Fixed across all three subcommands so
  create-writes and list/delete-reads stay on the same owner.

- gl webhook list: check the HTTP status before reading `webhooks`. A
  401/403/404 body has no webhooks field and previously printed
  "No webhooks" and exited 0, hiding the rejection; now bails with the
  server message like cmd_create.

- visibility::is_owner: delegate to api::did_matches so the read gate and
  require_repo_owner share one normalization. Fixes the under-match (bare
  owner stored, full did:key caller, as mirror rows) that denied the owner
  with the opaque 404, and the cross-method over-match.

- authz_guard: pin both halves of list_webhooks (authorize_repo_read and
  require_repo_owner) so removing the existence-hiding gate fails the drift
  test.

cargo fmt/clippy clean; 644 workspace tests pass.
@beardthelion

Copy link
Copy Markdown
Collaborator Author

All four addressed, pushed to 60ef427.

  • Owner resolution (webhook.rs): dropped the node-DID-only resolve_owner and routed create/list/delete through repo::resolve_owner_repo_pair, so the owner comes from the signing keypair (or an explicit owner/repo). Fixed all three together so create-writes and list/delete-reads stay on the same owner.
  • Webhook-list status (webhook.rs): cmd_list now checks status.is_success() and bails with the server message before reading webhooks, matching the neighboring list commands.
  • Visibility gate (visibility.rs): is_owner now delegates to did_matches, sharing one normalization with require_repo_owner. Added coverage on the gated read path for the bare-owner/full-caller case and a cross-method non-match.
  • authz_guard (api/mod.rs): added the authorize_repo_read( marker row for list_webhooks, so both halves are pinned and removing either gate fails the drift test.

Two follow-ups in the later commits while I was in here: a list_repo_events listed-reader test (the events handler was the only gated read surface missing the non-owner-reader case its siblings already cover), and gl repo label-list switched to get_maybe_signed for parity with the other read commands.

fmt/clippy clean, 646 tests pass.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/gl/src/webhook.rs`:
- Line 159: The webhook success-path parsing in `webhook.rs` is too permissive
because `hooks` defaults to an empty array when `body["webhooks"]` is missing or
malformed. Update the response handling around the `webhooks` extraction so that
2xx responses without a valid `webhooks` array are treated as an error instead
of “No webhooks”, and keep the existing success flow only when the `webhooks`
field is present and is an array. Use the `webhooks` parsing logic in the
webhook response code to locate and tighten this validation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 761abeb7-1bd7-4e17-b0d4-d241f941a155

📥 Commits

Reviewing files that changed from the base of the PR and between 8453814 and 60ef427.

📒 Files selected for processing (5)
  • crates/gitlawb-node/src/api/mod.rs
  • crates/gitlawb-node/src/test_support.rs
  • crates/gitlawb-node/src/visibility.rs
  • crates/gl/src/repo.rs
  • crates/gl/src/webhook.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/gitlawb-node/src/api/mod.rs
  • crates/gl/src/repo.rs
  • crates/gitlawb-node/src/test_support.rs

Comment thread crates/gl/src/webhook.rs Outdated
beardthelion added a commit that referenced this pull request Jun 29, 2026
)

A 2xx /hooks response without a `webhooks` array could hide a node/API
contract regression. cmd_list now errors with context instead of mapping
it to "No webhooks". An empty-but-present array stays a valid no-webhooks
result. Adds a test for the missing-array case.

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

This PR gates the list_webhooks, list_replicas, list_protected_branches, and list_repo_events read surfaces on the node, adds NodeClient::get_maybe_signed so CLI read commands can sign when an identity is present, and updates the gl/MCP callers that hit those endpoints. I found two issues that still need to be addressed before this is ready.

Findings

  • [P2] Fix MCP webhook_list default owner resolution
    crates/gl/src/mcp.rs:935
    The webhook_list tool now uses get_signed, but when the owner argument is omitted it still calls resolve_owner, which falls back to the node DID. In a normal MCP server started with a public node and the user's --dir, the request is signed by the user's keypair but the path uses the node owner's DID, so the owner-gated list_webhooks endpoint returns 403 (or 404 for a private repo). Default the owner to the signing keypair's short DID, keeping the explicit owner argument as an override, so the MCP tool matches the behavior you already fixed for gl webhook list.

  • [P3] Decide whether list_labels is read-visibility-gated
    crates/gl/src/repo.rs:655 and crates/gitlawb-node/src/api/labels.rs:74
    cmd_label_list was switched to get_maybe_signed with a comment saying labels are "Read-visibility-gated like the sibling read surfaces", but the node's labels::list_labels handler still serves labels without calling authorize_repo_read or checking the caller. That makes the signature unnecessary and leaves private-repo labels public. Either gate the server endpoint with authorize_repo_read (and add the marker to authz_guard), or update the client comment to explain that the signing is only future-proofing and remove the claim that labels are already gated.

beardthelion added a commit that referenced this pull request Jun 29, 2026
…ls on read visibility (#113)

- MCP webhook_list: default owner to the signing keypair's short DID (explicit
  owner arg still overrides) instead of the node root DID, so the signed,
  owner-gated /hooks request resolves to the caller's own repo. Mirrors the
  gl webhook list fix. resolve_owner and webhook_create/delete left untouched.
- list_labels: gate on authorize_repo_read so a private repo's label names 404
  for non-readers (public repos stay anonymously listable), matching the sibling
  read surfaces and making the existing client comment true. Added the
  authz_guard marker and a read-visibility integration test.
- gl label list: check HTTP status before reading the body so the now-gated
  404 surfaces as an error instead of printing "No labels" and exiting 0,
  matching the sibling list clients (replicas, protected branches).
@beardthelion

Copy link
Copy Markdown
Collaborator Author

Both addressed in 80cfb13.

MCP webhook_list now defaults the owner to the signing keypair's short DID, with an explicit owner arg still overriding, and errors when no identity is loaded instead of falling back to the node DID. That matches the gl webhook list behavior.

On list_labels: gated the endpoint. It now runs authorize_repo_read like the sibling read surfaces, so a private repo's label names 404 for non-readers while public repos stay anonymously listable. Added the authz_guard marker and a read-visibility test (owner and listed reader 200, non-reader and anon 404, absent repo 404), and fixed gl label list to surface that 404 as an error instead of printing an empty list.

@beardthelion beardthelion requested a review from jatmn June 29, 2026 22:02

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
crates/gitlawb-node/src/test_support.rs (1)

1252-1267: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Assert the anonymous 404 body does not leak labels.

This path only checks the status. Since anonymous callers are part of the private-label leak threat model, read the body here and assert !leaks(&bytes) like the signed non-reader case.

Proposed test hardening
         assert_eq!(
             resp.status(),
             StatusCode::NOT_FOUND,
             "anon is denied the private labels"
         );
+        let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
+            .await
+            .unwrap();
+        assert!(!leaks(&bytes), "anonymous 404 must not leak the label name");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/gitlawb-node/src/test_support.rs` around lines 1252 - 1267, The
anonymous private-repo 404 test only verifies StatusCode in the router().oneshot
flow and does not check for label leakage. Update this test in test_support.rs
to read the response body bytes after the NOT_FOUND assertion and verify they do
not leak labels using the existing leaks(...) helper, matching the signed
non-reader coverage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/gitlawb-node/src/test_support.rs`:
- Around line 1252-1267: The anonymous private-repo 404 test only verifies
StatusCode in the router().oneshot flow and does not check for label leakage.
Update this test in test_support.rs to read the response body bytes after the
NOT_FOUND assertion and verify they do not leak labels using the existing
leaks(...) helper, matching the signed non-reader coverage.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 177b8fc7-25a3-4bed-9ac8-e2954a160e93

📥 Commits

Reviewing files that changed from the base of the PR and between 281a7b2 and 80cfb13.

📒 Files selected for processing (5)
  • crates/gitlawb-node/src/api/labels.rs
  • crates/gitlawb-node/src/api/mod.rs
  • crates/gitlawb-node/src/test_support.rs
  • crates/gl/src/mcp.rs
  • crates/gl/src/repo.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/gitlawb-node/src/api/mod.rs
  • crates/gl/src/repo.rs

@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 changed paths and found a couple of issues that still need to be addressed.

Findings

  • [P2] Surface denied repo-info reads instead of printing fake repo details
    crates/gl/src/repo.rs:348
    This PR changes gl repo info to use get_maybe_signed for the visibility-gated GET /api/v1/repos/{owner}/{name} path, but the command still parses the response body without checking resp.status(). When the node returns the expected JSON 404 for a private repo the caller cannot read, the command treats that error JSON as a repo, falls back to owner, ?, true, and main, prints a plausible-looking repository summary, and exits successfully. Please keep the Response, check status.is_success() before reading repo fields, and return the node's error message on 401/403/404, like the other newly gated list commands do.

  • [P3] Complete CodeRabbit's anonymous label-leak assertion request
    crates/gitlawb-node/src/test_support.rs:1252
    CodeRabbit's latest test-hardening request is still valid in the current patch. The signed non-reader label path reads the 404 body and asserts that it does not contain the private label name, and the sibling replica/protected-branch anonymous-denial cases make the same no-leak assertion. The anonymous private-label branch, though, only checks the 404 status and never reads the body, so this PR's regression coverage can still miss the exact anonymous label-name leak it is trying to close if an error body regresses later. Please complete that request by reading the anonymous 404 body here and asserting !leaks(&bytes).

@kevincodex1 kevincodex1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

beardthelion added a commit that referenced this pull request Jun 30, 2026
…#113)

cmd_info parsed the GET /api/v1/repos/{owner}/{name} body without checking
the HTTP status, so a private-repo JSON 404 was rendered as a fake repo
summary (owner / ? / public / main) and exited 0. Capture the status and
bail on non-2xx, matching the sibling list commands.

Also assert the anonymous 404 label-list body does not leak the label name,
closing the last unread-body branch in the read-visibility gate test.
@beardthelion beardthelion requested a review from jatmn June 30, 2026 01:46

@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 changed paths and found issues that still need to be addressed.

Findings

  • [P1] Rebase on main and resolve the merge conflicts first
    GitHub mergeability
    GitHub currently reports this branch as CONFLICTING with merge state DIRTY, so the PR cannot be merged in its current state. Please rebase on the latest main branch and resolve those conflicts before addressing the review finding below, so any follow-up fix applies to the code that will actually land.

  • [P2] Surface denied MCP webhook-list responses as tool errors
    crates/gl/src/mcp.rs:949
    The MCP webhook_list path now signs the owner-gated /hooks request, but it still immediately deserializes the response body and returns it as a successful tool result without checking resp.status(). If the node returns the expected 401/403/404 JSON error for a missing identity, wrong owner, private repo, or deleted repo, the MCP tool reports success with that error body instead of failing the tool call, unlike the fixed gl webhook list path which checks status.is_success() before reading webhooks. Please keep the Response, check the HTTP status, and return an error with the node message for non-2xx responses before serializing the successful webhook list.

kevincodex1 pushed a commit that referenced this pull request Jun 30, 2026
…122)

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

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.

* test(node): don't let a service-conditional gate satisfy the egress guard (#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.

* fix(review): harden gate_runs_unconditionally against unclosed blocks (#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.
list_webhooks sat on the optional_signature read group with no auth or
ownership check, so any unauthenticated caller who knew a repo's owner/name
could enumerate its webhooks (target URL, created_by_did, events; only the
secret was redacted).

Gate it read-visibility-then-owner, mirroring create_webhook/delete_webhook:
headerless -> 401, non-reader of a private repo -> 404 (existence hidden,
uniform with the sibling read surfaces), non-owner of a public repo -> 403.
Add a list_webhooks row to the authz_guard drift table so the gate can't
silently regress.
list_replicas sat on optional_signature with no visibility check, so a
private repo's replica URLs were enumerable by anyone who knew owner/name.
Replica lists are a documented public mirror-discovery surface, so gate on
read visibility (authorize_repo_read) rather than owner-only: a public repo
stays anonymously listable, a private repo returns 404 to anyone who cannot
read it.

register_replica registers non-owner DIDs, and a replica operator is not a
visibility reader, so a non-owner replica operator of a private repo gets
404 — the intended contract, pinned by the new test. Add the authz_guard
row.
list_protected_branches sat on optional_signature with no visibility check,
leaking a private repo's branch names to any unauthenticated caller. Gate on
read visibility (authorize_repo_read at the root, INV-2 root-listing
exception): public repos stay anonymously listable, private repos 404 to a
non-reader. Add the authz_guard row.
…-only (#94)

list_repo_events served a private repo's ref certificates and gossip ref
metadata (ref names, SHAs) to any unauthenticated caller. The handler also
tolerates a repo it knows only via gossip (no local row) and legitimately
serves its events, so a blanket get_repo->authorize_repo_read swap would have
404'd that federation path.

Gate only the locally-hosted branch: when get_repo returns a record, run
visibility_check on it at the root and 404 on deny before fetching either the
cert or gossip events. A gossip-only repo (no local row) keeps its existing
public federation-feed behavior; the global /api/v1/events/ref-updates feed
remains a separately tracked deferral. Add the authz_guard row (visibility_check
marker, since the gate is direct to avoid a second get_repo).
…ated (#94)

`gl webhook list` built its NodeClient with no keypair, so the GET went out
anonymously. With the node now owner-gating GET /hooks, that request returns
401. Thread a --dir through the List subcommand and load the keypair, matching
`gl webhook create`/`delete`, so the listing is signed as the owner.

(Replica and protected-branch listings stay anonymous for public repos under
read-visibility gating, so their gl clients need no change.)
Code review caught that NodeClient::get() never attaches the RFC 9421
signature even when a keypair is loaded — only get_signed/post/put/delete
sign. The earlier gl webhook-list fix loaded the keypair but still called
get(), so `gl webhook list` (and the MCP webhook_list tool) 401'd against
the now-owner-gated endpoint. Switch both to get_signed.

Harden the two cmd_list tests to require a Signature/Signature-Input header on
the mock (verified: they fail against plain get(), pass with get_signed) — the
missing assertion that let the bug through. Add the untested owner-of-private-
repo webhooks path (200 + redaction). Fix the stale module doc and the
authz_guard bucket comment for list_webhooks.
…eir own repos (#94)

The #94 server change read-visibility-gates GET /replicas and
/branches/protected: public repos stay anonymous, private repos require the
owner/reader to authenticate. Four gl read commands sent unsigned GETs and so
broke (404) or silently degraded (empty output) for a private repo's owner:
gl protect list, gl repo owner (silent empty protected list), gl repo info
(silent missing replica count), gl repo replicas (and it had no --dir flag).

Add NodeClient::get_maybe_signed — signs when an identity is present, stays
anonymous otherwise (mirrors the conditional signing of post/put/delete) —
and route the four call sites through it. Add --dir to `gl repo replicas`.
Harden the protect-list tests to assert the request is signed (verified: they
fail on plain get()). Public-repo anonymous reads are unchanged.
Round-2 review caught that cmd_info and cmd_owner loaded a keypair and signed
their replica/protected-branch sub-fetch but left the PRIMARY GET
/api/v1/repos/{owner}/{name} on plain get(). get_repo is read-visibility-gated,
so for a private repo that primary fetch 404'd first and the command bailed
before reaching the signed sub-fetch — the half-fix never actually let an owner
inspect their own private repo. Route both primary fetches through
get_maybe_signed.

Add the missing signing guards: harden test_cmd_owner_is_owner to require a
signature on both mocks, and add tests for cmd_replicas and cmd_info (both were
untested) asserting the request is signed when an identity is present.

Out of scope (pre-existing, gated before #94, untouched here): gl repo commits,
gl pr list/view/diff, and the MCP repo_get/repo_commits/pr_* tools call other
already-gated read endpoints unsigned and are broken for private repos
independently of this change.
Close the vetting gaps from the review:
- node: a listed reader (non-owner) passes the read gate but is still refused
  the webhook list (403), and headerless on an existing-private vs an absent
  repo both 401 — proving the 401 fires before any lookup, so it is not an
  existence oracle.
- node: the read-visibility surfaces (replicas, protected branches) admit a
  listed reader who is not the owner (the allow-list branch of visibility_check),
  while a non-reader stranger still 404s.
- gl: get_maybe_signed's anonymous fallback (no identity -> unsigned GET) is now
  exercised, RED-proven by flipping it to get_signed (which signs and fails the
  no-signature-header assertion).
)

Close the last two reasoned-only paths by execution:

- MCP webhook_list: a call_tool("webhook_list", ...) dispatch test against a
  mock node asserts the /hooks request carries a signature (get_signed).
  RED-proven: flipping the arm back to get() fails the header assertion.

- gl->node end-to-end: a real RFC-9421 signature produced exactly as the gl
  client's get_signed does (gitlawb_core::http_sig::sign_request over GET +
  empty body) is run through the node's actual optional_signature middleware,
  which verifies it and authorizes the owner (200 + webhook body). RED-proven:
  signing over POST while sending GET makes the node reject it. This stitches
  the gl signing side and the node verifying side in one test rather than
  mocking one end.
…nts (#94)

`get_repo(...).await.ok().flatten()` collapsed a lookup Err into None, so a
transient DB failure skipped the read-visibility gate and the handler served a
private repo's gossip ref-update events. Propagate with `?` instead: an error
now maps to AppError::Internal (500) and fails closed, while a genuine Ok(None)
(repo not hosted locally) stays the intentional ungated pass-through.

Add a regression test that forces a get_repo error (drop the column its SELECT
reads) and asserts 500 with no secret in the body. The injection is
self-verifying: it asserts the lookup succeeds pre-drop and errors post-drop, so
the test can't pass vacuously if get_repo's projection changes later.
Address jatmn's review feedback on the read-surface gate:

- gl webhook: resolve owner from the signing identity, not the node DID.
  cmd_create/list/delete all routed through the local resolve_owner helper,
  which only ever returned the node root DID, so for a repo owned by the
  local identity the owner-gated /hooks route returned 404/403. Drop that
  helper and reuse repo::resolve_owner_repo_pair (accepts owner/repo, else
  derives from --dir/keypair). Fixed across all three subcommands so
  create-writes and list/delete-reads stay on the same owner.

- gl webhook list: check the HTTP status before reading `webhooks`. A
  401/403/404 body has no webhooks field and previously printed
  "No webhooks" and exited 0, hiding the rejection; now bails with the
  server message like cmd_create.

- visibility::is_owner: delegate to api::did_matches so the read gate and
  require_repo_owner share one normalization. Fixes the under-match (bare
  owner stored, full did:key caller, as mirror rows) that denied the owner
  with the opaque 404, and the cross-method over-match.

- authz_guard: pin both halves of list_webhooks (authorize_repo_read and
  require_repo_owner) so removing the existence-hiding gate fails the drift
  test.

cargo fmt/clippy clean; 644 workspace tests pass.
…t signing

Self-review follow-ups on the read-gate work:

- list_repo_events: add list_repo_events_admits_listed_reader, pinning the
  allow-list branch of the events gate (a listed non-owner reader gets 200
  with the cert; a non-reader stranger 404s with no leak). Mirrors the
  read_visibility_admits_listed_reader coverage the sibling replica and
  protected-branch surfaces already had; the events handler was the only
  gated read surface missing it.

- gl repo: cmd_label_list now loads an identity and uses get_maybe_signed
  instead of a bare anonymous get(), matching cmd_replicas/cmd_info. Public
  repos stay anonymously listable; a private-repo owner can read their own
  labels once /labels is read-visibility-gated, with no silent client drift.

fmt/clippy clean; 645 workspace tests pass.
Final-review hardening of the prior two follow-ups:

- gl repo label-list tests: require the signature/signature-input headers on
  the signed-branch mocks, and add an anonymous (no-identity) test asserting
  the header is absent. Without the header assertion the tests passed even
  with a bare get(), so the signing change was not actually pinned
  (RED-verified: reverting to get() now fails both signed-branch tests).

- list_repo_events_admits_listed_reader: also assert the cert sha (not just
  the ref name) is absent from the stranger's 404 body, matching the
  list_repo_events_is_read_visibility_gated leak-check convention.

fmt/clippy clean; 646 workspace tests pass.
)

A 2xx /hooks response without a `webhooks` array could hide a node/API
contract regression. cmd_list now errors with context instead of mapping
it to "No webhooks". An empty-but-present array stays a valid no-webhooks
result. Adds a test for the missing-array case.
…ls on read visibility (#113)

- MCP webhook_list: default owner to the signing keypair's short DID (explicit
  owner arg still overrides) instead of the node root DID, so the signed,
  owner-gated /hooks request resolves to the caller's own repo. Mirrors the
  gl webhook list fix. resolve_owner and webhook_create/delete left untouched.
- list_labels: gate on authorize_repo_read so a private repo's label names 404
  for non-readers (public repos stay anonymously listable), matching the sibling
  read surfaces and making the existing client comment true. Added the
  authz_guard marker and a read-visibility integration test.
- gl label list: check HTTP status before reading the body so the now-gated
  404 surfaces as an error instead of printing "No labels" and exiting 0,
  matching the sibling list clients (replicas, protected branches).
…#113)

cmd_info parsed the GET /api/v1/repos/{owner}/{name} body without checking
the HTTP status, so a private-repo JSON 404 was rendered as a fake repo
summary (owner / ? / public / main) and exited 0. Capture the status and
bail on non-2xx, matching the sibling list commands.

Also assert the anonymous 404 label-list body does not leak the label name,
closing the last unread-body branch in the read-visibility gate test.
…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 MCP webhook_list path deserialized the response body and returned it as a
successful tool result without checking the HTTP status, so a 401/403/404 JSON
error (missing identity, wrong owner, private or deleted repo) was reported as
success with the error body as the result. Check status().is_success() before
deserializing and surface the node's message as a tool error, matching the
`gl webhook list` path. Covered by a non-2xx test (RED without the check).
@beardthelion beardthelion force-pushed the fix/gate-repo-read-surfaces branch from da8be46 to dbf3bdf Compare June 30, 2026 14:26

@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 changed paths and found one issue that still needs to be addressed.

Findings

  • [P2] Parse the protected-branch response in gl repo owner
    crates/gl/src/repo.rs:725
    This PR switches the gl repo owner protected-branch sub-fetch to get_maybe_signed, so a private-repo owner can now successfully reach /api/v1/repos/{owner}/{name}/branches/protected. However, that command still only reads a bare array or body["branches"], while the node endpoint returns {"protected_branches":[...],"count":...} and gl protect list already parses that key. As a result, gl repo owner --dir ... reports Protected: (none) even when the signed request returns protected branches, and the current tests miss it because they mock a bare array instead of the real server response shape. Please parse protected_branches here and update the owner tests to use the endpoint's actual response schema.

gl repo owner parsed the protected-branch response as a bare array or a
"branches" key, but the node returns {"protected_branches":[...],"count":N},
so it always printed "Protected: (none)" even when branches were protected.
Extract the parse into parse_protected_branches matching the gl protect list
shape, and update the owner tests to mock the real schema instead of bare
arrays (which is why the bug went uncaught).
@beardthelion

Copy link
Copy Markdown
Collaborator Author

@jatmn fixed in 8592621. gl repo owner now reads the protected_branches key the way gl protect list does, instead of a bare array or a branches key, so it lists the protected branches rather than always printing (none). I also reshaped the three owner-test mocks to the real {"protected_branches":[...],"count":N} response shape, which is what let the bug through. Ready for another look.

@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

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:security Vulnerability fix or hardening sev:high Major break or real security/trust risk, no easy workaround 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.

Unauthenticated GET /repos/:owner/:repo/hooks leaks webhook target URLs for any repo

3 participants