Skip to content

Commit b7e9dfa

Browse files
committed
feat(constraint): add Verify trait for individual constraint verification
1 parent ece171d commit b7e9dfa

6 files changed

Lines changed: 1533 additions & 160 deletions

File tree

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
//! Constraint verification abstraction.
2+
//!
3+
//! The [`Verify`] trait provides a uniform interface for checking individual
4+
//! warrant constraints against an [`AuthorizationContext`](crate::warrant::AuthorizationContext).
5+
//!
6+
//! # Design Pattern: Trait-Based Polymorphism
7+
//!
8+
//! Each constraint type implements `Verify` independently, so adding a new
9+
//! constraint variant only requires adding an `impl Verify for NewConstraint`
10+
//! — no changes to the verification loop. This replaces the monolithic
11+
//! `match` + free-function pattern in `warrant.rs` with an open/closed design.
12+
13+
use crate::{
14+
error::Result,
15+
warrant::{
16+
AuthorizationContext, Constraint, MerchantConstraint, PaymentConstraint,
17+
ResourceConstraint, SponsorshipConstraint, ToolConstraint,
18+
},
19+
};
20+
21+
/// Verifies a single constraint against an authorization context.
22+
///
23+
/// Implementations return `Ok(())` when the constraint is satisfied, or an
24+
/// appropriate [`AuthorizationError`](crate::error::AuthorizationError)
25+
/// variant when it is not.
26+
pub trait Verify {
27+
/// Checks this constraint against the given context.
28+
fn verify(&self, context: &AuthorizationContext) -> Result<()>;
29+
}
30+
31+
// ---------------------------------------------------------------------------
32+
// Constraint-level blanket dispatch
33+
// ---------------------------------------------------------------------------
34+
35+
impl Verify for Constraint {
36+
fn verify(&self, context: &AuthorizationContext) -> Result<()> {
37+
match self {
38+
Self::Merchant(c) => c.verify(context),
39+
Self::Resource(c) => c.verify(context),
40+
Self::Tool(c) => c.verify(context),
41+
Self::Payment(c) => c.verify(context),
42+
Self::Sponsorship(c) => c.verify(context),
43+
}
44+
}
45+
}
46+
47+
// ---------------------------------------------------------------------------
48+
// Individual constraint implementations
49+
// ---------------------------------------------------------------------------
50+
51+
impl Verify for MerchantConstraint {
52+
fn verify(&self, context: &AuthorizationContext) -> Result<()> {
53+
if !self.merchant_ids.is_empty() &&
54+
!self.merchant_ids.iter().any(|id| id == &context.merchant_id)
55+
{
56+
return Err(crate::error::AuthorizationError::MerchantNotAllowed {
57+
merchant_id: context.merchant_id.clone(),
58+
});
59+
}
60+
if !self.host_suffixes.is_empty() &&
61+
!self.host_suffixes.iter().any(|suffix| context.merchant_host.ends_with(suffix))
62+
{
63+
return Err(crate::error::AuthorizationError::MerchantNotAllowed {
64+
merchant_id: context.merchant_id.clone(),
65+
});
66+
}
67+
Ok(())
68+
}
69+
}
70+
71+
impl Verify for ResourceConstraint {
72+
fn verify(&self, context: &AuthorizationContext) -> Result<()> {
73+
let method = context.http_method.to_uppercase();
74+
if !self.http_methods.is_empty() &&
75+
!self.http_methods.iter().any(|m| m.eq_ignore_ascii_case(&method))
76+
{
77+
return Err(crate::error::AuthorizationError::HttpMethodNotAllowed { method });
78+
}
79+
if !self.path_prefixes.is_empty() &&
80+
!self.path_prefixes.iter().any(|p| context.path_and_query.starts_with(p))
81+
{
82+
return Err(crate::error::AuthorizationError::ResourcePathNotAllowed {
83+
path: context.path_and_query.clone(),
84+
});
85+
}
86+
Ok(())
87+
}
88+
}
89+
90+
impl Verify for ToolConstraint {
91+
fn verify(&self, context: &AuthorizationContext) -> Result<()> {
92+
if !self.tool_names.is_empty() &&
93+
!self.tool_names.iter().any(|name| name == &context.tool_name)
94+
{
95+
return Err(crate::error::AuthorizationError::ToolNotAllowed {
96+
tool_name: context.tool_name.clone(),
97+
});
98+
}
99+
if !self.model_providers.is_empty() &&
100+
!self.model_providers.iter().any(|p| p == &context.model_provider)
101+
{
102+
return Err(crate::error::AuthorizationError::ModelProviderNotAllowed {
103+
model_provider: context.model_provider.clone(),
104+
});
105+
}
106+
if !self.action_labels.is_empty() &&
107+
!self.action_labels.iter().any(|label| label == &context.action_label)
108+
{
109+
return Err(crate::error::AuthorizationError::ActionLabelNotAllowed {
110+
action_label: context.action_label.clone(),
111+
});
112+
}
113+
Ok(())
114+
}
115+
}
116+
117+
impl Verify for PaymentConstraint {
118+
fn verify(&self, context: &AuthorizationContext) -> Result<()> {
119+
if context.selected_quote_amount > self.max_per_request.amount {
120+
return Err(crate::error::AuthorizationError::PaymentAmountExceeded {
121+
amount: context.selected_quote_amount,
122+
limit: self.max_per_request.amount,
123+
});
124+
}
125+
if !self.allowed_assets.is_empty() &&
126+
!self.allowed_assets.iter().any(|a| a.asset == context.asset)
127+
{
128+
return Err(crate::error::AuthorizationError::AssetNotAllowed {
129+
asset: context.asset.clone(),
130+
});
131+
}
132+
if !self.allowed_schemes.is_empty() &&
133+
!self.allowed_schemes.iter().any(|s| s == &context.scheme)
134+
{
135+
return Err(crate::error::AuthorizationError::SchemeNotAllowed {
136+
scheme: context.scheme.clone(),
137+
});
138+
}
139+
if !self.allowed_rails.is_empty() && !self.allowed_rails.iter().any(|r| r == &context.rail)
140+
{
141+
return Err(crate::error::AuthorizationError::RailNotAllowed { rail: context.rail });
142+
}
143+
if !self.payee_ids.is_empty() && !self.payee_ids.iter().any(|p| p == &context.payee_id) {
144+
return Err(crate::error::AuthorizationError::PayeeNotAllowed {
145+
payee_id: context.payee_id.clone(),
146+
});
147+
}
148+
Ok(())
149+
}
150+
}
151+
152+
impl Verify for SponsorshipConstraint {
153+
fn verify(&self, _context: &AuthorizationContext) -> Result<()> {
154+
if !self.allow_sponsored_execution && !self.sponsor_ids.is_empty() {
155+
return Err(crate::error::AuthorizationError::SponsorshipNotAllowed);
156+
}
157+
Ok(())
158+
}
159+
}
160+
161+
// ---------------------------------------------------------------------------
162+
// Convenience: verify a slice of constraints
163+
// ---------------------------------------------------------------------------
164+
165+
/// Verifies every constraint in a slice against the context.
166+
///
167+
/// Short-circuits on the first failure.
168+
pub fn verify_all(constraints: &[Constraint], context: &AuthorizationContext) -> Result<()> {
169+
for constraint in constraints {
170+
constraint.verify(context)?;
171+
}
172+
Ok(())
173+
}

crates/ledgerflow-core/src/lib.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,24 @@
22
33
#![allow(missing_docs)]
44

5+
pub mod constraint;
56
pub mod error;
7+
pub mod proof_builder;
8+
pub mod typestate;
9+
pub mod verification;
610
pub mod warrant;
711

812
pub use crate::{
13+
constraint::{Verify, verify_all as verify_all_constraints},
914
error::{AuthorizationError, Result, WireError, WireResult},
15+
proof_builder::ProofBuilder,
16+
typestate::WarrantBuilder,
17+
verification::{
18+
Digestible, FullyVerified, ProofExt, VerificationPipeline, VerifiedProof, VerifiedWarrant,
19+
WarrantExt,
20+
},
1021
warrant::{
11-
AmountLimit, AssetRef, AudienceScope, AuthorizationContext, Constraint,
22+
AmountLimit, AssetRef, AudienceScope, AuthorizationContext, CborCodec, Constraint,
1223
DEFAULT_PROOF_FRESHNESS_MS, DelegationPolicy, MAX_WARRANT_CBOR_BYTES, MerchantConstraint,
1324
PaymentConstraint, PaymentRail, PaymentSubjectKind, PaymentSubjectRef, PeriodLimit, Proof,
1425
ResourceConstraint, SignatureEnvelope, SignerRef, SigningAlgorithm, SigningKeyPair,

0 commit comments

Comments
 (0)