From cd96add6aed49a3b26a5d338794be9fb778bcf55 Mon Sep 17 00:00:00 2001 From: euxaristia <25621994+euxaristia@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:03:47 -0400 Subject: [PATCH 1/2] fix(core): honor wildcard capabilities in Ucan::can, matching is_attenuated_by MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ucan::can did strict equality on both resource and action, so a validly delegated wildcard capability (with: "*", or can: "*" / "repo/admin") could never match a concrete runtime request even though is_attenuated_by treats those same values as wildcards during delegation-chain verification. Not a security issue: the mismatch only produces false negatives (denying a capability the delegation chain actually grants), never false positives. Dormant today since no production code path calls can() yet — auth's caller_authorized_to_push is pure did_matches(), with the UCAN-capability check noted as future (Phase 2) work. Makes can() mirror is_attenuated_by's semantics: the stored capability plays the "parent" role, so with == "*" matches any resource and can == "*" or "repo/admin" matches any action. Closes #199 --- crates/gitlawb-core/src/ucan.rs | 53 ++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/crates/gitlawb-core/src/ucan.rs b/crates/gitlawb-core/src/ucan.rs index 5fff7b54..ce7fd64d 100644 --- a/crates/gitlawb-core/src/ucan.rs +++ b/crates/gitlawb-core/src/ucan.rs @@ -187,11 +187,17 @@ impl Ucan { } /// Check if this UCAN grants a specific capability on a resource. + /// + /// Mirrors `Capability::is_attenuated_by`'s wildcard semantics: a stored + /// capability of `with: "*"` or `can: "*"` / `"repo/admin"` covers any + /// requested resource/action, since a valid delegation chain can produce + /// exactly that capability (see `is_attenuated_by`). pub fn can(&self, resource: &str, action: &str) -> bool { - self.payload - .att - .iter() - .any(|cap| cap.with == resource && cap.can == action) + self.payload.att.iter().any(|cap| { + let resource_ok = cap.with == resource || cap.with == "*"; + let action_ok = cap.can == action || cap.can == "*" || cap.can == caps::REPO_ADMIN; + resource_ok && action_ok + }) } /// Encode to a compact JSON string (the wire format). @@ -312,6 +318,45 @@ mod tests { assert!(!ucan.can("gitlawb://repos/test/repo", caps::PR_MERGE)); } + #[test] + fn can_honors_resource_wildcard() { + let issuer = Keypair::generate(); + let audience = Keypair::generate().did(); + + let ucan = Ucan::issue( + &issuer, + audience, + vec![Capability::new("*", caps::GIT_PUSH)], + None, + ) + .unwrap(); + + assert!(ucan.can("gitlawb://repos/test/repo", caps::GIT_PUSH)); + assert!(ucan.can("gitlawb://repos/other/repo", caps::GIT_PUSH)); + assert!(!ucan.can("gitlawb://repos/test/repo", caps::PR_MERGE)); + } + + #[test] + fn can_honors_repo_admin_action_wildcard() { + let issuer = Keypair::generate(); + let audience = Keypair::generate().did(); + + let ucan = Ucan::issue( + &issuer, + audience, + vec![Capability::new( + "gitlawb://repos/test/repo", + caps::REPO_ADMIN, + )], + None, + ) + .unwrap(); + + assert!(ucan.can("gitlawb://repos/test/repo", caps::GIT_PUSH)); + assert!(ucan.can("gitlawb://repos/test/repo", caps::PR_MERGE)); + assert!(!ucan.can("gitlawb://repos/other/repo", caps::GIT_PUSH)); + } + #[test] fn bootstrap_ucan() { let issuer = Keypair::generate(); From f5587e20f42c3f4defc03f623484389cf8570dc0 Mon Sep 17 00:00:00 2001 From: euxaristia <25621994+euxaristia@users.noreply.github.com> Date: Wed, 15 Jul 2026 04:10:31 -0400 Subject: [PATCH 2/2] test: add can_honors_action_wildcard unit test for Ucan::can --- crates/gitlawb-core/src/ucan.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/gitlawb-core/src/ucan.rs b/crates/gitlawb-core/src/ucan.rs index ce7fd64d..86b3dd9f 100644 --- a/crates/gitlawb-core/src/ucan.rs +++ b/crates/gitlawb-core/src/ucan.rs @@ -357,6 +357,24 @@ mod tests { assert!(!ucan.can("gitlawb://repos/other/repo", caps::GIT_PUSH)); } + #[test] + fn can_honors_action_wildcard() { + let issuer = Keypair::generate(); + let audience = Keypair::generate().did(); + + let ucan = Ucan::issue( + &issuer, + audience, + vec![Capability::new("gitlawb://repos/test/repo", "*")], + None, + ) + .unwrap(); + + assert!(ucan.can("gitlawb://repos/test/repo", caps::GIT_PUSH)); + assert!(ucan.can("gitlawb://repos/test/repo", caps::PR_MERGE)); + assert!(!ucan.can("gitlawb://repos/other/repo", caps::GIT_PUSH)); + } + #[test] fn bootstrap_ucan() { let issuer = Keypair::generate();