Skip to content

fix(gl): resolve webhook owner from caller identity, not node DID#159

Open
carlitos973 wants to merge 3 commits into
Gitlawb:mainfrom
carlitos973:fix/webhook-owner-resolves-to-node-did
Open

fix(gl): resolve webhook owner from caller identity, not node DID#159
carlitos973 wants to merge 3 commits into
Gitlawb:mainfrom
carlitos973:fix/webhook-owner-resolves-to-node-did

Conversation

@carlitos973

@carlitos973 carlitos973 commented Jul 6, 2026

Copy link
Copy Markdown

Summary

  • resolve_owner() in crates/gl/src/webhook.rs unconditionally fetched the node's own root DID (via GET /) and used it as the repo owner for gl webhook create/list/delete, ignoring both an explicit owner/repo argument and the caller's own loaded identity.
  • In practice this makes webhooks unusable for any repo not owned by the node operator itself -- every call resolves to the wrong owner and 404s server-side.
  • crates/gl/src/cert.rs's resolve_repo already implements the correct pattern (check for an explicit slash first, prefer the caller's own keypair, fall back to the node DID only if no local identity exists). This PR applies that same proven pattern to webhook create/list/delete.

Test plan

  • Existing webhook test suite passes unchanged (8/8, cargo test -p gl webhook)
  • Verified against a live self-hosted node (create/list/delete all resolve the correct owner now, previously 404d)
  • No changes to server-side routes or other crates

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved webhook handling so repository targeting works reliably for both owner/repo and plain repo inputs.
    • Webhook operations now prefer your local account identity when available, falling back to the server identity when needed.
    • Create, list, and delete webhook actions behave consistently across repository input formats, and gl webhook create now prints the suggested delete command as gl webhook delete owner/repo <id> for clarity.

resolve_owner() in webhook.rs unconditionally fetched the node's own
root DID via GET / and used it as the repo owner, ignoring both an
explicit owner/repo argument and the caller's own loaded identity.
This made gl webhook create/list/delete effectively unusable for any
repo not owned by the node operator itself -- every call resolved to
the wrong owner and 404d.

cert.rs's resolve_repo already has the correct pattern: check for an
explicit slash first, then prefer the caller's own keypair, falling
back to the node DID only if no local identity is available. This
applies that same pattern to webhook create/list/delete.

Verified against a live self-hosted node: webhook create/list/delete
all resolve the correct owner now. Existing webhook test suite passes
unchanged (8/8).
Copilot AI review requested due to automatic review settings July 6, 2026 13:50
@github-actions github-actions Bot added needs-issue PR has no linked issue needs-tests Source changed without accompanying tests (advisory) labels Jul 6, 2026
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Thanks for the contribution. A couple of things will help us review this faster:

  • Link the issue this addresses (Closes #123). For protocol changes, open an issue first.
  • This changes Rust source but no tests changed. Tests are required for fixes and strongly encouraged for features.

See CONTRIBUTING.md. Update the PR and these notes will clear automatically.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

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: f97c416f-66e4-48db-86c7-70ee412c021a

📥 Commits

Reviewing files that changed from the base of the PR and between d42a809 and e5b1b0c.

📒 Files selected for processing (1)
  • crates/gl/src/webhook.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/gl/src/webhook.rs

📝 Walkthrough

Walkthrough

crates/gl/src/webhook.rs now resolves webhook repository ownership as (owner, repo) instead of a single owner string. It accepts owner/repo input directly, can derive the caller identity from a local keypair directory, falls back to the node root DID, and threads the new shape through create, list, delete, and tests.

Changes

Webhook Owner Resolution

Layer / File(s) Summary
New resolve_owner helper
crates/gl/src/webhook.rs
resolve_owner now returns (owner, repo), parsing owner/repo input directly, preferring the caller's local identity DID from a keypair directory, and falling back to the node root DID via GET / when unavailable.
Command handlers and tests
crates/gl/src/webhook.rs
WebhookCmd::List carries dir, the dispatcher passes it through, cmd_create and cmd_delete build hook paths from (owner, repo), cmd_list updates its messages and empty state for {owner}/{repo}, and tests cover the new signature and bare-repo identity resolution.

Sequence Diagram(s)

sequenceDiagram
  participant WebhookCmd
  participant resolve_owner
  participant KeypairDir
  participant Client
  participant Node

  WebhookCmd->>resolve_owner: resolve_owner(repo, dir, client)
  resolve_owner->>resolve_owner: parse repo as owner/repo or bare repo
  alt dir provided and keypair loads
    resolve_owner->>KeypairDir: load caller keypair
    KeypairDir-->>resolve_owner: DID short
  else local identity unavailable
    resolve_owner->>Client: GET /
    Client->>Node: request root DID
    Node-->>Client: root DID
    Client-->>resolve_owner: root DID
  end
  resolve_owner-->>WebhookCmd: (owner, repo)
Loading

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested labels: kind:bug

Suggested reviewers: jatmn, beardthelion, kevincodex1

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description only covers Summary and Test plan; it omits the required template sections and review checklist. Fill in Motivation & context, Kind of change, What changed, How a reviewer can verify, and the remaining checklist/notes sections from the template.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: webhook owner resolution now prefers caller identity over node DID.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copilot AI 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.

Pull request overview

This PR fixes gl webhook owner resolution so webhook operations target the caller’s identity (or an explicit owner/repo), rather than always defaulting to the node operator’s DID—making webhooks usable for repos not owned by the node itself.

Changes:

  • Reworked webhook owner/repo resolution to honor owner/repo inputs and prefer the caller’s local identity, falling back to the node DID only when needed.
  • Updated webhook create/list/delete to use the new resolution logic.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/gl/src/webhook.rs
Comment on lines 115 to 118
let keypair = load_keypair_from_dir(dir.as_deref())?;
let client = NodeClient::new(&node, Some(keypair));
let owner = resolve_owner(&client).await?;
let (owner, repo) = resolve_owner(&repo, dir.as_deref(), &client).await?;

Comment thread crates/gl/src/webhook.rs Outdated
Comment on lines 165 to 170
async fn cmd_list(repo: String, node: String) -> Result<()> {
let client = NodeClient::new(&node, None);
let owner = resolve_owner(&client).await?;
let (owner, repo) = resolve_owner(&repo, None, &client).await?;

let resp: Value = client
.get(&format!("/api/v1/repos/{owner}/{repo}/hooks"))
Comment thread crates/gl/src/webhook.rs Outdated
Comment on lines +79 to +84
/// Resolve "repo" into (owner, name). If repo already contains a slash
/// (owner/repo form), use it directly. Otherwise, prefer the caller's own
/// identity (matching the pattern used elsewhere, e.g. cert.rs's
/// resolve_repo) so webhooks are created/listed against the DID that's
/// actually calling, not the node's own operator DID -- falls back to the
/// node's root DID only if no local keypair is available at all.

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

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 117: The `resolve_owner` destructuring in `webhook.rs` is shadowing
`repo`, so the later delete hint loses the original owner and prints an
incorrect command. Update the `resolve_owner` result binding in the webhook flow
to keep the resolved repository name in a different variable from the input
`repo`, and use that value consistently when constructing the delete hint.
Ensure the hint in the webhook command path prints the fully qualified
owner/repo form rather than only the name.
🪄 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: 8215879e-68b1-4844-b415-ff7f8a5455ef

📥 Commits

Reviewing files that changed from the base of the PR and between 6cff528 and 4335535.

📒 Files selected for processing (1)
  • crates/gl/src/webhook.rs

Comment thread crates/gl/src/webhook.rs
let keypair = load_keypair_from_dir(dir.as_deref())?;
let client = NodeClient::new(&node, Some(keypair));
let owner = resolve_owner(&client).await?;
let (owner, repo) = resolve_owner(&repo, dir.as_deref(), &client).await?;

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Shadowing repo corrupts the delete-command hint printed later.

Destructuring into (owner, repo) reassigns repo to the name-only portion. The delete hint at Line 161 (gl webhook delete {repo} {id}) then drops the owner whenever the caller passed owner/repo, so the suggested command re-resolves to the local/node DID instead of the intended owner — reproducing the very mismatch this PR fixes.

Preserve the fully-qualified form for the hint (e.g. bind the resolved name to a new variable, or print {owner}/{repo}).

🐛 Proposed fix
-    let (owner, repo) = resolve_owner(&repo, dir.as_deref(), &client).await?;
+    let (owner, name) = resolve_owner(&repo, dir.as_deref(), &client).await?;

Then update the path and hint to use name and print the qualified form:

-    println!("\n  Delete: gl webhook delete {repo} {id}");
+    println!("\n  Delete: gl webhook delete {owner}/{name} {id}");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let (owner, repo) = resolve_owner(&repo, dir.as_deref(), &client).await?;
let (owner, name) = resolve_owner(&repo, dir.as_deref(), &client).await?;
🤖 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` at line 117, The `resolve_owner` destructuring in
`webhook.rs` is shadowing `repo`, so the later delete hint loses the original
owner and prints an incorrect command. Update the `resolve_owner` result binding
in the webhook flow to keep the resolved repository name in a different variable
from the input `repo`, and use that value consistently when constructing the
delete hint. Ensure the hint in the webhook command path prints the fully
qualified owner/repo form rather than only the name.

@beardthelion beardthelion added crate:gl gl — the contributor CLI kind:bug Defect fix — wrong or unsafe behavior labels Jul 6, 2026
@kevincodex1

Copy link
Copy Markdown
Contributor

hello @carlitos973 thank you so much for your contribution, please address comments from coderabbit and github bot

Per CodeRabbit review on Gitlawb#159: the resolve_owner destructuring shadowed
repo with the bare name, so the printed delete hint lost the owner
qualification -- only correct by coincidence if the caller runs it
under the same identity used at create time. Print owner/repo
explicitly so the hint always works regardless of default identity.
@carlitos973

Copy link
Copy Markdown
Author

Good catch, fixed in d42a809 -- the delete hint now prints the fully-qualified owner/repo instead of relying on the caller's default identity matching. Tests still 8/8.

@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 issues that need to be addressed before this is ready.

Findings

  • [P2] Make gl webhook list <repo> resolve against the caller identity
    crates/gl/src/webhook.rs:165
    The List subcommand still has only repo and node, and cmd_list calls resolve_owner(&repo, None, &client), so a bare repo name can never load the caller's keypair. In that path the new helper always falls back to GET / and uses the node DID, which is the same failure mode this PR says it fixes for create/list/delete; only create and delete pass an identity directory. Please thread the identity selection through gl webhook list as well, and add a test that asserts the requested /api/v1/repos/{owner}/{repo}/hooks path uses the local identity for a bare repo name.

  • [P2] Do not turn list failures into an empty webhook list
    crates/gl/src/webhook.rs:169
    cmd_list never checks the HTTP status before reading resp["webhooks"], so a node error body like {"message":"repo not found"} or {"message":"only the repo owner..."} has no webhooks array and is treated as []. That means the wrong-owner 404 class this PR is trying to fix can still exit successfully as No webhooks for ..., hiding a broken resolution or authorization failure from the caller. Please mirror the create/delete paths by checking status.is_success() before rendering, and error with the node's message on non-2xx responses.

  • [P3] Complete Copilot's request to preserve the qualified repo in list output
    crates/gl/src/webhook.rs:167
    Copilot's list-output thread is still valid: after let (owner, repo) = ..., the original argument is shadowed, so gl webhook list alice/project prints No webhooks for project or Webhooks for project instead of the repo the user supplied. Please keep the resolved name in a separate variable, or print {owner}/{name}, so the command output stays unambiguous for explicit owner/repo input.

…ok list

Apply the same fix already applied to create/delete to the list
subcommand, per review feedback on this PR:

- gl webhook list now accepts --dir and resolves the caller identity
  the same way create/delete do, instead of always falling back to
  the node root DID for bare repo names
- cmd_list checks the HTTP status before parsing, so a non-2xx error
  (e.g. repo not found, not authorized) surfaces as an error instead
  of silently rendering as an empty webhook list
- list output now prints the fully-qualified owner/repo, matching the
  fix already applied to the create/delete hints

Also tightens resolve_owners doc comment: it falls back to the node
DID on any keypair-load failure, not only when no keypair exists.
@carlitos973

Copy link
Copy Markdown
Author

Thanks for the review — all three points addressed in e5b1b0c:

  • P2 (identity resolution): gl webhook list now takes --dir and calls resolve_owner with it, exactly like create/delete. Added test_list_webhooks_uses_caller_identity_for_bare_repo_name, which mocks a different DID than the node-root fallback and asserts the request goes to the identity-resolved path — it would fail against the root-DID path if the fix regressed.
  • P2 (silent empty list on error): cmd_list now checks status.is_success() before reading webhooks, and bails with the node's message on non-2xx, matching create/delete. Added test_list_webhooks_error_status_propagates (mocks a 404 with a message body, asserts the error surfaces).
  • P3 (qualified name in output): list output now prints {owner}/{repo} in both the "No webhooks for..." and "Webhooks for... (N total)" lines.

Also tightened the resolve_owner doc comment per Copilot's earlier note — it now says it falls back on any keypair-load failure, not just "no keypair available."

Full suite: 10/10 passing (8 previous + 2 new). Also manually verified against the live node: gl webhook list <bare-repo> now resolves to the caller's own DID and prints it qualified, and listing a nonexistent repo now errors instead of printing "No webhooks for ...".

@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] Run rustfmt on the changed webhook tests
    crates/gl/src/webhook.rs:377
    cargo fmt --all -- --check currently fails on this file, including the cmd_list(...).await.unwrap() calls at lines 377 and 393. The repository's PR workflow runs that exact fmt check before clippy, so this branch will fail the required formatting gate even though the webhook tests themselves pass. Please run cargo fmt --all and push the formatting-only result.

  • [P2] Apply the owner-resolution fix to the MCP webhook tools too
    crates/gl/src/mcp.rs:919
    The CLI gl webhook path now resolves bare repo names through the caller identity and accepts owner/repo, but gl mcp serve still leaves webhook_create, webhook_list, and webhook_delete on the old behavior. Those handlers read repo unchanged, call the MCP resolve_owner() helper, and that helper only uses an explicit owner argument or falls back to GET /, so a bare repo name still targets the node operator DID and an owner/repo string is sent as an extra path segment under the wrong owner. Please thread the same (owner, repo) resolution through the MCP webhook handlers so agents using the MCP tools get the same fix as the CLI commands.

beardthelion
beardthelion previously approved these changes Jul 6, 2026

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

Verified this against the current head, each check by execution.

  • cmd_list now reads the response status and bails with the node's message on a non-2xx, so a wrong-owner 404 no longer renders as an empty list. Removing that check makes test_list_webhooks_error_status_propagates fail, so it is load-bearing.
  • gl webhook list takes --dir and resolves against the caller identity like create and delete; forcing the node-DID branch makes test_list_webhooks_uses_caller_identity_for_bare_repo_name fail, so it is load-bearing too.
  • List output prints the fully-qualified owner/repo, and a corrupt keypair falls back to the node DID as the doc now states.

One optional hardening, not a blocker:

Findings

  • [P3] Validate the resolved owner and repo as single path segments
    crates/gl/src/webhook.rs:93
    split_once('/') lets extra path content through, and the segments are interpolated unescaped into /api/v1/repos/{owner}/{repo}/hooks. gl webhook list 'victim/repo/../../other/target' normalizes on the wire to /api/v1/repos/other/target/hooks, so the command reads a different repo than the one it prints. The server still gates by owner, so this is not an escalation, but rejecting / and .. in the resolved segments (or percent-encoding them) keeps the request and the displayed target in sync.

Core change is correct and complete. @jatmn your three items all landed on this head.

@jatmn

jatmn commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Core change is correct and complete. @jatmn your three items all landed on this head.

sigh my pr's take too long to run lol

@beardthelion beardthelion dismissed their stale review July 7, 2026 00:19

Pulling my earlier approval. cargo fmt --all --check still fails on this head (e5b1b0c) at crates/gl/src/webhook.rs:374 and :390, where the two new cmd_list tests keep .await.unwrap() on one line. That's the rustfmt P1 jatmn already flagged, so this head isn't mergeable and my approve shouldn't stand on it. The functional change is correct (status-checked cmd_list/cmd_create, owner-qualified delete hint and list output), so re-request once rustfmt is applied. The open Copilot/CodeRabbit shadowing threads were filed on the first head (4335535) and are already addressed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

crate:gl gl — the contributor CLI kind:bug Defect fix — wrong or unsafe behavior needs-issue PR has no linked issue needs-tests Source changed without accompanying tests (advisory)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants