fix(node): carry full owner DID on ref-update wire event (#144)#145
fix(node): carry full owner DID on ref-update wire event (#144)#145Gravirei wants to merge 11 commits into
Conversation
|
Thanks for the contribution. A couple of things will help us review this faster:
See CONTRIBUTING.md. Update the PR and these notes will clear automatically. |
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds an optional ChangesOwner DID propagation
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
crates/gitlawb-node/src/test_support.rs (1)
1969-2043: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider a shared builder for
ReceivedRefUpdatetest fixtures.Both tests construct near-identical
ReceivedRefUpdateliterals inline. A shared helper (similar to theupdate(...)fixture already used in db/mod.rs tests) would reduce duplication and make future field additions (likeowner_did) less error-prone to keep in sync across test files.🤖 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 1969 - 2043, The `events_returns_inserted_ref_updates` and `events_limit_respects_limit_param` tests duplicate the same `crate::db::ReceivedRefUpdate` setup inline, so add a shared test fixture/builder for `ReceivedRefUpdate` in `test_support.rs` and use it in both cases. Model it after the existing `update(...)` helper used in the db tests, and make sure the helper accepts overrides for fields like `repo`, `owner_did`, `new_sha`, and timestamps so future schema changes stay consistent across tests.crates/gitlawb-node/src/db/mod.rs (1)
3919-4077: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAssert
owner_didin the filtered-by-repo test too.
list_ref_updates_filtered_by_repo(lines 4027-4058) inserts records with distinctowner_didvalues but only assertsid/count, notowner_did, unlike the siblinglist_repo_ref_updates_filters_by_repotest which does check it. Adding that assertion would close a small gap in coverage for the exact field this PR introduces.♻️ Proposed test strengthening
let filtered = db .list_ref_updates_filtered(Some("ownerA/proj"), 100) .await .unwrap(); assert_eq!(filtered.len(), 1); assert_eq!(filtered[0].id, "u5"); + assert_eq!(filtered[0].owner_did.as_deref(), Some("did:key:zA"));🤖 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/db/mod.rs` around lines 3919 - 4077, The list_ref_updates_filtered_by_repo test is missing coverage for the new owner_did behavior. Update ref_update_db_tests::list_ref_updates_filtered_by_repo to assert the returned row’s owner_did matches the inserted value, similar to list_repo_ref_updates_filters_by_repo, so the filtered path verifies both repo filtering and owner_did preservation.
🤖 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/test_support.rs`:
- Around line 1969-2007: The ref-update events payload returned by the events
API is still missing owner_did, so the current test only verifies persistence in
Postgres. Update the ref-update feed serialization in the events response path
to include owner_did, using the relevant event model/handler that backs
events_router and GET /api/v1/events/ref-updates, and then extend
events_returns_inserted_ref_updates to assert events[0]["owner_did"] matches the
inserted value.
---
Nitpick comments:
In `@crates/gitlawb-node/src/db/mod.rs`:
- Around line 3919-4077: The list_ref_updates_filtered_by_repo test is missing
coverage for the new owner_did behavior. Update
ref_update_db_tests::list_ref_updates_filtered_by_repo to assert the returned
row’s owner_did matches the inserted value, similar to
list_repo_ref_updates_filters_by_repo, so the filtered path verifies both repo
filtering and owner_did preservation.
In `@crates/gitlawb-node/src/test_support.rs`:
- Around line 1969-2043: The `events_returns_inserted_ref_updates` and
`events_limit_respects_limit_param` tests duplicate the same
`crate::db::ReceivedRefUpdate` setup inline, so add a shared test
fixture/builder for `ReceivedRefUpdate` in `test_support.rs` and use it in both
cases. Model it after the existing `update(...)` helper used in the db tests,
and make sure the helper accepts overrides for fields like `repo`, `owner_did`,
`new_sha`, and timestamps so future schema changes stay consistent across tests.
🪄 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: 6d0fa77a-014d-4a04-af46-9ef99e20fe27
📒 Files selected for processing (5)
crates/gitlawb-node/src/api/peers.rscrates/gitlawb-node/src/api/repos.rscrates/gitlawb-node/src/db/mod.rscrates/gitlawb-node/src/p2p/mod.rscrates/gitlawb-node/src/test_support.rs
beardthelion
left a comment
There was a problem hiding this comment.
The write-side plumbing is right: the full DID comes from the local repo record, it's threaded through both the gossip and HTTP-notify paths, and the #[serde(default)] / nullable column keeps older peers compatible. CodeRabbit's feed-serialization ask is genuinely addressed too. Two things block merge, and the first is a hard one: the schema change breaks every existing node on upgrade, and the PR closes #144 without fixing #144's symptom.
Findings
-
[P1] Ship
owner_didas a new migration version, not appended to v1
crates/gitlawb-node/src/db/mod.rs:542
TheALTER TABLE ... ADD COLUMN owner_didandidx_ref_updates_ownerwere added insideMigration { version: 1 }, butrun_migrationsskips any version already recorded inschema_migrations, and the catalogue comment right above the array says new schema must be a new version (v2, v3, ...). Every existing node has v1 recorded, so on upgrade the column is never created;insert_ref_update(binds$12) and all threelist_*SELECTs then reference a missing column and error, which stops ref-update ingestion on both the gossip and notify paths and 500s the events feed. Fresh databases pass because they run v1 from scratch, which is why CI is green. I reproduced it against the real migration runner: an existing node (v1..v9 recorded, noowner_did) upgraded to this build leaves the column absent and the insert/select fail; moving both statements into a newMigration { version: 10 }makes the same upgrade round-trip cleanly. -
[P2] Don't close #144 on this PR; nothing consumes
owner_didyet
crates/gitlawb-node/src/api/events.rs:38
#144 has two halves: carry the full owner DID (done here) and make the feed gate use it to stop the over-drop. Nothing in this diff readsowner_didfor a visibility decision; it's stored and echoed in the feed JSON only, andidx_ref_updates_owneris an index no query uses. The gate that over-drops isn't in this branch, so merging this auto-closes #144 while the reported symptom persists. Keep the plumbing, but change "Closes #144" to "Refs #144" and land the gate consumption (the did:key-aware match #144 describes) in the follow-up that sits on top of the gate. -
[P3] Populate
owner_didon the other read surfaces, or hold the JSON change
crates/gitlawb-node/src/graphql/query.rs:61,crates/gitlawb-node/src/api/events.rs:117
The field lands on the global REST feed but the GraphQLref_updatesresolver drops it (RefUpdateTypehas no such field) and the repo-scopedlist_repo_eventsgossip block omits it. No consumer reads it yet so there's no impact today, but the asymmetry is the same drift the duplicated four-copy SELECT column list invites, and it becomes a trap once something depends on the field. Either populate all three surfaces or defer the JSON exposure until the gate needs it. While here, add a migration upgrade-path test (seedschema_migrationsat v1, run migrations, assert the column round-trips) so the v1-append class can't recur.
The direction is right and the receive/publish wiring is sound; once the migration moves to its own version this is close.
P1 migration break resolved on 72ccb28; re-reviewed the new head, posting a fresh review.
beardthelion
left a comment
There was a problem hiding this comment.
Re-reviewed on 72ccb28d. The P1 migration break is resolved and I verified it by execution: the owner_did DDL now lives in a new Migration{version:10}, and on a simulated existing node (schema_migrations at v1..v9, received_ref_updates without the column) migrate() adds the column and index and records v10. Putting the DDL back inside the already-applied v1 makes that same check fail (the column is never re-added), so the guard is load-bearing. Wire compat (#[serde(default)] Option<String>) and the DB plumbing are sound, and "Refs #144" is correct. Four items to fold in.
Findings
-
[P2] Drop the premature
idx_ref_updates_ownerindex, or defer it to the gate PR
crates/gitlawb-node/src/db/mod.rs:833
Nothing readsreceived_ref_updatesbyowner_didyet: every reference is a projection, no WHERE/JOIN/ORDER. Migrations run inside a transaction soCREATE INDEX CONCURRENTLYisn't available here (mod.rs:426), which means this index builds under a write-blocking lock at startup on exactly the populated nodes this PR targets, for no current benefit. Ship the column now and add the index in #143 next to the query that reads it. -
[P2] Expose owner_did on the repo-scoped events feed
crates/gitlawb-node/src/api/events.rs:116
list_ref_updatesreturnsowner_didnow, but thegossip_eventsblock inlist_repo_eventsomits it, soGET /api/v1/repos/{owner}/{repo}/eventssilently drops the field. Add"owner_did": u.owner_didto thatjson!block to match line 38. -
[P2] Carry owner_did through the GraphQL query and subscription
crates/gitlawb-node/src/graphql/types.rs:52
RefUpdateTypeandRefUpdateBroadcast(state.rs:12) have noowner_did, so theref_updatesquery (query.rs:61) and the subscription drop it while REST exposes it. Addowner_did: Option<String>to both structs and populate it at query.rs, subscription.rs, and the broadcast send in repos.rs. -
[P2] Add an existing-node upgrade test for v10
crates/gitlawb-node/src/db/mod.rs:3200
migration_v10_creates_owner_did_columnruns only a fresh v1..v10 chain, which can't catch DDL appended to an already-applied version. Seedschema_migrationswith v1..v9 and areceived_ref_updateswithout the column, thenmigrate()and assert the column, index, and v10 row appear. I ran exactly this against the head and it passes; the same body fails when the DDL sits back in v1, so it is the missing regression guard, not a live bug.
One forward note for #143, not a blocker here: the wire owner_did is attacker-controlled (the peer signature authenticates the relaying node, not this field) and is stored verbatim. Every authorization path today reads the trusted record.owner_did from the local repo row, so nothing consumes the wire value yet, but the gate must cross-check it against a trusted source and must not fall back to the lossy last-segment / did_matches matcher the full DID exists to avoid.
@Gravirei migration fix verified, thanks. The above are plumbing-consistency and deploy-window items.
There was a problem hiding this comment.
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 (2)
crates/gitlawb-node/src/db/mod.rs (2)
3207-3218: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAssert the column is actually nullable.
The tests fetch
is_nullablebut only assert the name/type. Since older peers can omitowner_did, please assert"YES"so a futureNOT NULLchange is caught.Proposed fix
assert_eq!(col.0, "owner_did"); assert_eq!(col.1, "text"); + assert_eq!(col.2, "YES");Apply the same assertion in both migration tests.
Also applies to: 3274-3284
🤖 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/db/mod.rs` around lines 3207 - 3218, The migration tests for received_ref_updates currently verify owner_did exists and is text, but they ignore the fetched is_nullable value. Update both migration test blocks that query information_schema.columns in db::mod to assert the third tuple field from sqlx::query_as is "YES" alongside the existing name/type checks, so changes to nullable behavior are caught.
2103-2107: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winPreserve later
owner_didenrichment on duplicate ref-update IDs.With
ON CONFLICT(id) DO NOTHING, a mixed-version network can store the olderNonepayload first and ignore a later duplicate carrying the full owner DID, leaving the feed unable to disambiguate that event.Proposed fix
(id, node_did, pusher_did, repo, ref_name, old_sha, new_sha, timestamp, cert_id, received_at, from_peer, owner_did) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) - ON CONFLICT(id) DO NOTHING", + ON CONFLICT(id) DO UPDATE + SET owner_did = COALESCE(received_ref_updates.owner_did, EXCLUDED.owner_did) + WHERE received_ref_updates.owner_did IS NULL + AND EXCLUDED.owner_did IS NOT NULL",🤖 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/db/mod.rs` around lines 2103 - 2107, The INSERT into received_ref_updates currently uses ON CONFLICT(id) DO NOTHING, which prevents a later duplicate from enriching an earlier row with owner_did. Update the upsert logic in the received ref-update insert path so duplicates with the same id can fill in a missing owner_did while still avoiding overwriting existing non-null data. Refer to the received_ref_updates insert statement and the surrounding db write function that persists ref updates.
🧹 Nitpick comments (1)
crates/gitlawb-node/src/db/mod.rs (1)
4096-4129: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a same-slug owner collision regression.
This test uses different
repovalues, so it does not cover the PR’s core case: two updates with the same trailing repo slug but different fullowner_didvalues. Add a row pair likealice/myrepo+did:web:host:aliceandalice/myrepo+did:gitlawb:alice.🤖 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/db/mod.rs` around lines 4096 - 4129, The current `list_repo_ref_updates_filters_by_repo` test only verifies filtering by full repo name, not the same-slug owner collision case. Update this test in `db` to insert two ref updates with the same trailing repo slug but different full owner identifiers, using the existing `insert_ref_update`, `update`, and `list_repo_ref_updates` helpers, and assert the query returns the correct row for each full repo owner path. Keep the original repo-filter assertions if helpful, but add coverage for the `owner_did` collision scenario the PR is targeting.
🤖 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`:
- Line 130: The event payload in list_repo_events is only populating owner_did
for gossipsub events, so local cert events in the same feed remain inconsistent.
Update the local event mapping in events.rs to also set owner_did from
record.owner_did, keeping the repo-scoped payload shape consistent with the
existing gossipsub path and preserving consumers that dedup or gate on the full
owner DID.
---
Outside diff comments:
In `@crates/gitlawb-node/src/db/mod.rs`:
- Around line 3207-3218: The migration tests for received_ref_updates currently
verify owner_did exists and is text, but they ignore the fetched is_nullable
value. Update both migration test blocks that query information_schema.columns
in db::mod to assert the third tuple field from sqlx::query_as is "YES"
alongside the existing name/type checks, so changes to nullable behavior are
caught.
- Around line 2103-2107: The INSERT into received_ref_updates currently uses ON
CONFLICT(id) DO NOTHING, which prevents a later duplicate from enriching an
earlier row with owner_did. Update the upsert logic in the received ref-update
insert path so duplicates with the same id can fill in a missing owner_did while
still avoiding overwriting existing non-null data. Refer to the
received_ref_updates insert statement and the surrounding db write function that
persists ref updates.
---
Nitpick comments:
In `@crates/gitlawb-node/src/db/mod.rs`:
- Around line 4096-4129: The current `list_repo_ref_updates_filters_by_repo`
test only verifies filtering by full repo name, not the same-slug owner
collision case. Update this test in `db` to insert two ref updates with the same
trailing repo slug but different full owner identifiers, using the existing
`insert_ref_update`, `update`, and `list_repo_ref_updates` helpers, and assert
the query returns the correct row for each full repo owner path. Keep the
original repo-filter assertions if helpful, but add coverage for the `owner_did`
collision scenario the PR is targeting.
🪄 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: 9f70fb07-7129-4c96-9e77-7b2943106a56
📒 Files selected for processing (7)
crates/gitlawb-node/src/api/events.rscrates/gitlawb-node/src/api/repos.rscrates/gitlawb-node/src/db/mod.rscrates/gitlawb-node/src/graphql/query.rscrates/gitlawb-node/src/graphql/subscription.rscrates/gitlawb-node/src/graphql/types.rscrates/gitlawb-node/src/state.rs
🚧 Files skipped from review as they are similar to previous changes (1)
- crates/gitlawb-node/src/api/repos.rs
beardthelion
left a comment
There was a problem hiding this comment.
Re-reviewed on 63ab6015. All four items from my last pass are in, CodeRabbit's local-cert-events fix landed, and I verified the security- and deploy-relevant paths by execution:
-
Migration, both ways: on a simulated existing node (v1..v9 recorded, no
owner_didcolumn),migrate()adds the column and records v10; moving that DDL back into the already-applied v1 makesmigration_v10_existing_node_upgradefail (RowNotFound) while the fresh-DB test still passes, so the upgrade guard is load-bearing. A pre-upgrade row (owner_didNULL, since the ALTER has no default) round-trips through the feed as JSON null, no 500. The index is correctly deferred to #143. -
Feed provenance, executed: the repo-scoped feed's local block carries the trusted
record.owner_did, and the gossip block echoes the stored wire value verbatim, which I confirmed with a spoofedowner_didsurfacing unchanged (display-only, nothing filters on it). All fourreceived_ref_updatesSELECTs projectowner_did, so the shared row mapper never hits a missing column. The global feed and GraphQL (query, subscription, broadcast) carry the field through as a passthrough. -
[P3] The stored
received_ref_updates.owner_didis attacker-controlled (the peer signature authenticates the relaying node, not this field, andnotify_syncaccepts it unsigned by default) and is echoed verbatim, as the spoof test shows. It is display-only and nothing gates on it, so not a break. A short comment at the two ingest sites (p2p/mod.rs,api/peers.rs:398) marking the column untrusted would keep #143's DID-aware feed gate from consuming it as trusted; that gate must reconcile it against the repo slug or a verified cert.
@Gravirei the P2s and the migration all check out, thanks. @kevincodex1 this is ready.
|
hello @Gravirei kindly rebase to main and fix conflicts |
63ab601 to
b46005b
Compare
Stale after the rebase to b46005b; re-reviewing the new head.
beardthelion
left a comment
There was a problem hiding this comment.
Re-reviewed on b46005bf. The owner_did work (#144) is solid: owner_did is carried on all four emit paths, the shared received_ref_updates mapper and migration v10 line up, and the feed gate never trusts the stored value. But the rebase onto 0.5.0 pulled api/ipfs.rs backward relative to main and reintroduces an unauthenticated availability regression, so I can't approve this head.
Findings
-
[P1] Restore the pre-walk object-existence check on
GET /ipfs/{cid}
crates/gitlawb-node/src/api/ipfs.rs(get_by_cid)
Current main (via #133,466a550) runsstore::object_typeandcontinues before the reachability walk, so a random CID can't trigger a full-history walk on repos that don't hold the object. This branch removes that guard and runsallowed_blob_set_for_caller(git rev-list --all+ a per-commitls-tree) beforeread_object. The route isoptional_signatureand unthrottled, andget_by_ciditerates the fulllist_all_repos(), so one anonymous request with an absent CID walks every public path-scoped repo's history. I reproduced it: an anon request for an object not in the repo fires the walk on this head and fires zero on main. Restore main'sobject_typepre-filter, or gate the walk behind an indexed proof the object is present. -
[P2] Drop the
/ipfs/{cid}change from this PR
crates/gitlawb-node/src/api/ipfs.rs
d995c4deis a ~150-line ipfs gate revision unrelated to #144, and main already carries #133. Rebasing onto current main to drop it (keeping main'sipfs.rs) removes both the P1 and the scope creep in one move. -
[P2] Correct the docstring claiming owner_did feeds the feed gate
crates/gitlawb-node/src/p2p/mod.rs:42
The comment says owner_did was added "so the feed gate can distinguish different DID methods that share the same trailing segment," butvisibility::ref_update_row_names_repomatches onrow_repoplus the local record and collapses both via.split(':').next_back(), never reading the wire owner_did. The field is additive display/storage today. Either wire owner_did into the matcher or fix the doc so it doesn't imply that collision is closed. -
[P3] Exercise the real v9 to v10 upgrade over populated data
crates/gitlawb-node/src/db/mod.rs(migration_v10_creates_owner_did_column)
The test runs the full v1..v10 chain on a fresh pool; nothing seedsschema_migrationsat v9 with existingreceived_ref_updatesrows before applying v10. The migration is safe (ADD COLUMN IF NOT EXISTS, nullable, Option readers), so this is coverage rather than a defect. A test that migrates to v9, inserts a row without owner_did, then applies v10 and assertsowner_did IS NULLpins the actual upgrade path. -
[P3] Don't render peer-supplied owner_did as authoritative
crates/gitlawb-node/src/api/events.rs:261
Gossip and notify rows store owner_did verbatim off the wire with no signature binding it to the owner's key, and the feed echoes it. It isn't an authz break (the gate uses the matched local record, never the stored value) and it's the same spoofable class aspusher_did/node_did, but rendering the display owner from the local record for locally-known rows would close it.
The owner_did core is ready; the ipfs regression is the only merge blocker. @Gravirei recommend rebasing onto current main to drop d995c4de and pick up the #133 ipfs gate.
b46005b to
1ddbc5b
Compare
jatmn
left a comment
There was a problem hiding this comment.
I found a couple of review-feedback/test issues that need to be addressed before this is ready.
Findings
-
[P2] Make the v10 migration test actually start from a v9 schema
crates/gitlawb-node/src/db/mod.rs:3320
This test says it simulates an existing v9 node, but it first runsdb.migrate(), which applies v10 and createsreceived_ref_updates.owner_didbefore the test deletes and reseedsschema_migrationsat v9. The later insert withoutowner_didtherefore runs against a table that already has the new column, so the test would still pass if the upgrade path being claimed here was not actually exercised. Please build the pre-v10 schema without applying migration 10 first, or explicitly drop the new column/index before the reseed, then apply v10 and assert the old populated row survives withowner_did IS NULL. -
[P3] Complete CodeRabbit's owner_did feed assertion
crates/gitlawb-node/src/test_support.rs:2432
CodeRabbit's request to exposeowner_didin/api/v1/events/ref-updatesis marked addressed, and the handler now serializes the field, but the end-to-end test still only asserts the returnedrepo. Since the row inserted above hasowner_did: Some(owner.into()), this test would pass even if the API regressed to omitting the new field again. Please complete that review item by assertingevents[0]["owner_did"]matches the inserted owner DID.
jatmn
left a comment
There was a problem hiding this comment.
Thanks for the update. I found one remaining issue that needs to be addressed before this is ready.
Findings
- [P2] Defer the unused owner_did index until the gate query lands
crates/gitlawb-node/src/db/mod.rs:834
Migration v10 createsidx_ref_updates_owner, but this PR only stores and projectsreceived_ref_updates.owner_did; no current query filters, joins, or orders that table byowner_did. Since migrations run inside a transaction here, this index cannot be built concurrently, so existing nodes with populated ref-update tables take avoidable write-blocking startup work for no current read path. Please ship just the nullable column in this plumbing PR, then add the index in the follow-up that actually consumesowner_didfor the feed gate.
beardthelion
left a comment
There was a problem hiding this comment.
Approving on 093a3c0d. My earlier request-changes was on a stale base; after the rebase, ipfs.rs and the feed gate (visibility.rs) are byte-identical to main, and the owner_did work verifies clean by execution.
What I checked:
- The wire
owner_didis display/storage only. I drove a private repo's ref-update withowner_didspoofed to an attacker's own DID and read the feed authenticated as that DID: the row stays denied, because the gate matches on the local record, not the wire field. The owner still sees the row. - The GraphQL subscription broadcast stays
announce-gated (private repos do not announce), so a private push does not leak ref metadata to anonymous subscribers, and the broadcastowner_didis the local record's. - Migration v10 is a new highest version, idempotent, nullable, no index. The upgrade test genuinely migrates a v9 node with a pre-existing row and asserts
owner_did IS NULL. A NULL row reads back through the shared mapper asNonewith no panic, and old peers (missing field or explicit null) deserialize toNone.
Findings
- [P3] Show the local owner on the global feed for locally-known repos.
crates/gitlawb-node/src/api/events.rs:155. The cross-repo feed echoes the peer-suppliedowner_did, so a locally-hosted repo's row can display whatever owner a peer claimed. The per-repo feed already does the right thing atevents.rs:261(it emits the matchedrecord.owner_did). Sinceref_update_row_visiblealready resolves the matching local record, use itsowner_didhere too and keep the wire value only for rows with no local match. - [P3] Assert the nullable invariant in the v10 migration test.
crates/gitlawb-node/src/db/mod.rs.migration_v10_creates_owner_did_columnselectsis_nullableinto the tuple and comments "nullable TEXT" but never asserts it; addassert_eq!(col.2, "YES")so a future accidental NOT NULL is caught. - [P3] Add an
owner_didecho assertion on the GraphQLrefUpdatessurface (and onlist_repo_events). The REST cross-repo feed has one; the others do not, so a dropped or nulled field there would pass CI.
Core is sound and this is display/test polish only. @kevincodex1 LGTM.
jatmn
left a comment
There was a problem hiding this comment.
I found one issue to address.
Findings
- [P2] Do not resolve
owner_didfrom the lossy repo slug alone
crates/gitlawb-node/src/api/events.rs:145
The global REST feed overrides a row's storedowner_didwheneverref_update_row_names_repo(record, u.repo)matches a local repo, but that matcher intentionally collapses both owners to the trailing DID segment. If this node hosts a readabledid:web:host:alice/widgetand receives a remote row fordid:gitlawb:alice/widgetwith the legacyreposlugalice/widget, the visibility gate keeps the row because the local repo is public, then the response rewrites itsowner_didtodid:web:host:alice. That loses the full owner DID this PR is adding for same-slug disambiguation. GraphQL has the opposite drift (query.rs:86still returns the stored wire value directly), so the two global feeds can disagree for the same row. Please include the newowner_didin the local-record resolution before replacing it withrecord.owner_did, and share or mirror that projection in GraphQL with an assertion for the exactownerDidvalue.
jatmn
left a comment
There was a problem hiding this comment.
Thanks for the update. I rechecked the previously discussed owner_did, migration, REST, and GraphQL paths and do not see any remaining actionable code issues from my side.
GitHub still reports this PR as blocked even though it is mergeable and the visible checks are passing, so there may be a remaining branch-protection or review-state item for maintainers to clear.
@kevincodex1 LGTM
beardthelion
left a comment
There was a problem hiding this comment.
Re-reviewed the new head 1074aa4; refreshing my earlier approval, which sat on 093a3c0.
The change since then resolves the owner_did display divergence: both the REST feed and GraphQL now prefer the stored wire owner_did and fall back to a local lookup only for legacy rows with no stored owner, so the two surfaces agree. I verified the security-relevant behavior by execution against a local Postgres, not by reading:
- The wire owner_did is display-only, never a visibility input. A ref-update row carrying a spoofed owner_did is dropped for anonymous callers and for a caller authenticated as the spoofed DID; only the real owner sees it, with the spoofable value merely displayed. Reverting the gate's fail-closed arm flips that to a leak, so the gate is load-bearing.
- The local fallback cannot surface a private owner. A slug that collides on the loose owner matcher fails closed when the collision partner is a private repo the caller cannot read (verified the cross-method did:gitlawb vs did:key case drops the row for anon), and where the collision is the same key in two DID spellings the dedup resolves to the owner's public repo, so the displayed owner is always one the caller can already list.
- The migration nullability assertion from last round is in.
A few non-blocking follow-ups, none of which need to hold this:
- The feed handlers load list_all_repos_deduped() twice per request (once in the collector, once for the owner_did projection) on both REST and GraphQL. Threading the collector's set through would drop the redundant query on the hot feed path.
- The fallback resolves by first match over that query, whose outer ORDER BY has no id tiebreaker, and REST and GraphQL query it separately. For legacy no-owner rows under a tied updated_at plus a name collision the two surfaces can still pick different owners. A stable tiebreaker (and sharing the set, per the point above) closes it.
- A peer's empty or malformed wire owner_did is shown verbatim, since the fallback only applies to a missing value. Treating an empty string as missing would recover the local owner.
@kevincodex1 good to merge.
|
hello @Gravirei please rebase to main and fix conflicts |
…mprove migration test, use local owner_did
…omplete owner_did API assertion
…ion test, add owner_did echo assertions
…ion; mirror projection in GraphQL
1074aa4 to
4d37b11
Compare
beardthelion
left a comment
There was a problem hiding this comment.
Re-reviewed on dbfb0a1. Approving. I verified the security-critical properties by execution on this head.
The wire owner_did is display-only and the gate does not trust it: I seeded a private local repo and a received_ref_updates row naming it with a forged owner_did, drove the global feed as anon, and the private row stays dropped. A public repo's row with the same forged owner_did is shown with the forged value echoed. So a peer cannot use owner_did to unlock a private row, only to mislabel a public one on the feed. The shared row mapper reads owner_did as Option, so a backward-compat NULL row round-trips as None with no panic (insert_and_list_without_owner_did covers it). The v10 migration is a new highest-version block, nullable and idempotent, and its upgrade-path test genuinely starts from a v9 schema. All four emit paths carry owner_did, and I confirmed the HTTP-notify path stores it end-to-end.
Two non-blocking notes:
- The feed echoes the wire owner_did verbatim even on rows that name a local repo this node hosts, where the true owner is already known from the record. It is display-only and joins the feed's existing untrusted peer fields (pusher_did, node_did, from_peer), so it is not a gate issue, but for local-matching rows preferring record.owner_did would avoid displaying a peer's claim over a fact the node holds. This reflects the prefer-wire resolution from an earlier round, so treat it as a judgment call rather than a required change.
- Thin emit-path coverage: the notify_sync tests do not assert owner_did is stored, so a revert of the binding would pass all of them, and the REST None-fallback branch and the subscription projection are untested. The behavior is correct, but a regression in those paths would not be caught.
@kevincodex1 good to merge.
Stale approval on an earlier head; superseded by my re-review on dbfb0a1.
Summary
Carry the full owner DID on the ref-update wire event so the feed gate can distinguish different DID methods that share the same trailing segment (e.g.
did:web:host:alicevsdid:gitlawb:alice).Motivation & context
Refs #144
The ref-update wire slug was previously constructed from the last colon-segmented component of the owner DID (
{owner_did.split(":").last}/{name}), which is lossy. Two owners with different DID methods but the same trailing segment would produce identical slugs, causing the feed gate to over-drop remote rows for anonymous callers.This PR is the plumbing half of #144: it carries the full owner DID on the wire and stores it in the database so a follow-up can teach the feed gate to use
did_matchesfor proper disambiguation. The gate itself (the second half of #144) will land in a separate change on top of this.Kind of change
What changed
crates/gitlawb-node (gitlawb-node):
p2p/mod.rs: Addedowner_did: Option<String>toRefUpdateEventwith#[serde(default)]for backward compat with older peers that do not include the fielddb/mod.rs: Addedowner_did: Option<String>toReceivedRefUpdate, added migration v10 to addowner_did TEXTcolumn andidx_ref_updates_ownerindex to thereceived_ref_updatestable, updated all SELECT/INSERT queries to include the columnapi/repos.rs: Setowner_did: Some(record.owner_did)when publishing ref-update events over gossip; addedowner_didto the HTTP sync notify bodyapi/peers.rs: Addedowner_didtoNotifyRequestand wired it through toReceivedRefUpdateHow a reviewer can verify
DATABASE_URL=postgresql://gitlawb:changeme@172.19.0.2:5432/gitlawb cargo test -p gitlawb-node cargo fmt --check cargo clippy --workspace --all-targets -- -D warningsBefore you request review
cargo test --workspacepasses locally (327/327)cargo fmt --allandcargo clippy --workspace --all-targets -- -D warningsare cleanfix(...),test(...),style(...)).env.exampleupdated if behavior or config changed (or N/A)Protocol & signing impact
This is a backward-compatible wire-format addition: the new
owner_didfield is#[serde(default)], so events from older peers deserialize without it (owner_did: None). Therepofield format is unchanged. Migration v10 adds the column to existing databases;ADD COLUMN IF NOT EXISTSis idempotent for re-runs.did:key, Ed25519 / RFC 9421 signatures, UCAN, ref certs, or P2P wire formatsSummary by CodeRabbit
New Features
owner_didwhen available.owner_didas optional (omittable).Bug Fixes
owner_didis missing or null in incoming notifications.