Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions crates/gitlawb-node/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ mod did_tests {
assert!(!did_matches("did:key:zABC", "did:key:zXYZ"));
assert!(!did_matches("zABC", "zXYZ"));
}

#[test]
fn empty_did_matches() {
assert!(did_matches("", ""));
assert!(did_matches("", "did:key:"));
}
}

/// Drift guard (plan 002 §Gate-type table, Step 5). Every in-scope mutation
Expand Down
2 changes: 1 addition & 1 deletion crates/gitlawb-node/src/api/profiles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ pub async fn set_profile(
&state.http_client,
&state.config.pinata_upload_url,
&state.config.pinata_jwt,
&format!("profile-{}", &did),
&format!("profile-{}", did),
&profile_json,
)
.await
Expand Down
43 changes: 34 additions & 9 deletions crates/gitlawb-node/src/visibility.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,12 @@ fn nfc(s: &str) -> String {
s.nfc().collect()
}

/// True if `caller` is the repo owner. Matches exactly or against the
/// did:key short form — a non-key DID (did:gitlawb:/did:web:) in `owner_did`
/// never matches a bare short segment, and a non-key DID in `caller` never
/// matches a did:key owner's short form. Uses `normalize_owner_key` (not
/// `did_matches`) so the did:key-only rule is stricter: cross-method matching
/// would let a non-key canonical row bypass the #124 visibility gate.
/// True if `caller` is the repo owner. Uses [`crate::api::did_matches`] so the
/// owner check is representation-agnostic within `did:key` (full `did:key:z...`
/// matches bare `z...` stored by mirror rows) while still denying cross-method
/// collisions (`did:gitlawb:z...` vs bare `z...`).
fn is_owner(owner_did: &str, caller: &str) -> bool {
let owner_short = crate::db::normalize_owner_key(owner_did);
let caller_short = crate::db::normalize_owner_key(caller);
owner_short == caller_short
crate::api::did_matches(owner_did, caller)
}

/// The match prefix for a glob: "/" stays "/", "/secret/**" becomes "/secret".
Expand Down Expand Up @@ -429,6 +425,35 @@ mod tests {
);
}

// #153 forward guard: is_owner must match across bare-key / full-did:key
// representations should someone revert to single-side normalize_owner_key.
#[test]
fn bare_owner_full_caller_allows_owner() {
let rules = [rule("/", VisibilityMode::A, &[])];
assert_eq!(
visibility_check(&rules, false, "z6MkOwner", Some("did:key:z6MkOwner"), "/"),
Decision::Allow,
"bare owner + full did:key caller must match (guard against single-side revert)"
);
}

// #153 regression: cross-method DID must still be denied even when the
// trailing segment collides with a bare owner key.
#[test]
fn cross_method_did_denied_with_bare_owner() {
let rules = [rule("/", VisibilityMode::A, &[])];
assert_eq!(
visibility_check(&rules, false, "z6MkFoo", Some("did:gitlawb:z6MkFoo"), "/"),
Decision::Deny,
"cross-method DID must not match a bare did:key owner"
);
assert_eq!(
visibility_check(&rules, false, "z6MkFoo", Some("did:web:z6MkFoo"), "/"),
Decision::Deny,
"did:web must not match a bare did:key owner"
);
}

#[test]
fn root_rule_allows_listed_reader() {
let rules = [rule("/", VisibilityMode::A, &["did:key:z6MkFriend"])];
Expand Down
Loading