Skip to content

Commit 3abca8e

Browse files
committed
fix(node): prefer canonical repo row over mirror row in get_repo (#124)
get_repo matched both mirror rows (bare short-DID, is_public=true, no visibility rules) and canonical rows (full did:key: DID, correct rules), returning one nondeterministically. When the mirror row won, the visibility gate saw is_public=true + empty rules, allowing unauthenticated read of private/withheld content. Add ORDER BY to prefer canonical rows (UUID id, no slash) over mirror rows (slash-form id), matching the ranking logic already used by DEDUP_CTE and dedupe_canonical_repos.
1 parent 0b7c494 commit 3abca8e

1 file changed

Lines changed: 74 additions & 1 deletion

File tree

  • crates/gitlawb-node/src/db

‎crates/gitlawb-node/src/db/mod.rs‎

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -891,7 +891,13 @@ impl Db {
891891
"SELECT id, name, owner_did, description, is_public, default_branch,
892892
created_at, updated_at, disk_path, forked_from, machine_id
893893
FROM repos
894-
WHERE (owner_did = $1 OR owner_did LIKE '%:' || $1 || '%') AND name = $2",
894+
WHERE (owner_did = $1 OR owner_did LIKE '%:' || $1 || '%') AND name = $2
895+
-- Prefer canonical rows (UUID id, no slash) over mirror rows (slash id).
896+
-- Mirror rows are inserted by upsert_mirror_repo with is_public=true and
897+
-- no visibility rules; using them for visibility checks would bypass the
898+
-- canonical row's gate (issue #124).
899+
ORDER BY CASE WHEN position('/' in id) > 0 THEN 1 ELSE 0 END,
900+
created_at ASC, id ASC",
895901
)
896902
.bind(owner_did)
897903
.bind(name)
@@ -3700,6 +3706,73 @@ mod dedup_db_tests {
37003706
assert_eq!(list_len, 2);
37013707
assert_eq!(count, list_len, "count must equal the deduped list length");
37023708
}
3709+
3710+
/// get_repo must prefer the canonical row over the mirror row when both match,
3711+
/// so the visibility gate keys off the canonical row's rules and is_public flag
3712+
/// rather than the mirror's hardcoded public-with-no-rules (issue #124).
3713+
#[sqlx::test]
3714+
async fn get_repo_prefers_canonical_over_mirror(pool: PgPool) {
3715+
let db = db(pool).await;
3716+
let short = "z6Mkwbud";
3717+
let owner_did = "did:key:z6Mkwbud";
3718+
3719+
// Canonical row with is_public=false and a visibility rule.
3720+
let canonical = RepoRecord {
3721+
id: uuid::Uuid::new_v4().to_string(),
3722+
name: "secret-repo".into(),
3723+
owner_did: owner_did.to_string(),
3724+
description: None,
3725+
is_public: false,
3726+
default_branch: "main".into(),
3727+
created_at: ts("2026-01-01T00:00:00Z"),
3728+
updated_at: ts("2026-01-01T00:00:00Z"),
3729+
disk_path: "/srv/secret".into(),
3730+
forked_from: None,
3731+
machine_id: None,
3732+
};
3733+
db.create_repo(&canonical).await.unwrap();
3734+
3735+
// Mirror row — hardcoded is_public=true, no visibility rules.
3736+
db.upsert_mirror_repo(short, "secret-repo", "/srv/mirror", None, false)
3737+
.await
3738+
.unwrap();
3739+
3740+
// Querying with bare short DID should return the canonical row.
3741+
let got = db
3742+
.get_repo(short, "secret-repo")
3743+
.await
3744+
.unwrap()
3745+
.expect("get_repo should find the repo");
3746+
3747+
assert_eq!(
3748+
got.owner_did, owner_did,
3749+
"canonical row (did:key: form) must win over mirror row (bare short DID)"
3750+
);
3751+
assert!(
3752+
!got.id.contains('/'),
3753+
"canonical row id must not contain a slash"
3754+
);
3755+
assert!(!got.is_public, "canonical row's is_public must be preserved");
3756+
}
3757+
3758+
/// get_repo still returns the mirror row when no canonical row exists
3759+
/// (mirror-only group), so sync and read paths remain functional.
3760+
#[sqlx::test]
3761+
async fn get_repo_returns_mirror_when_no_canonical(pool: PgPool) {
3762+
let db = db(pool).await;
3763+
db.upsert_mirror_repo("z6Lonely", "orphan", "/srv/m", None, false)
3764+
.await
3765+
.unwrap();
3766+
3767+
let got = db
3768+
.get_repo("z6Lonely", "orphan")
3769+
.await
3770+
.unwrap()
3771+
.expect("get_repo should find the mirror");
3772+
3773+
assert_eq!(got.id, "z6Lonely/orphan", "mirror row is returned");
3774+
assert!(got.is_public, "mirror row's is_public should be true");
3775+
}
37033776
}
37043777

37053778
/// Exercises the iCaptcha single-use proof ledger (`icaptcha_consumed_proofs`),

0 commit comments

Comments
 (0)