fix(auth): bind capability snapshots to enrollment policy

Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com>
This commit is contained in:
Cea Stapleton Cordasco
2026-08-03 17:03:52 -05:00
parent 23f98df72d
commit 4bd64092bc
2 changed files with 443 additions and 35 deletions
+109 -16
View File
@@ -12,8 +12,8 @@ use thiserror::Error;
use uuid::Uuid;
use crate::context::{
AuthMethod, AuthTransport, BindingVersion, FederatedPrincipal, VerifiedFederatedAssertion,
VerifiedNostrProof, VersionedBindingRef,
AuthMethod, AuthTransport, AuthoritativeBindingEvidence, BindingVersion, FederatedPolicyStamp,
FederatedPrincipal, ResolvedFederatedPolicy, VerifiedFederatedAssertion, VerifiedNostrProof,
};
const MAX_OPAQUE_ID_BYTES: usize = 256;
@@ -133,10 +133,12 @@ impl fmt::Debug for AuthorizationProfileId {
}
}
/// Opaque, equality-comparable policy version returned by a provider.
/// Opaque, equality-comparable capability-policy version returned by a provider.
///
/// This is the typed policy-change seam that later lease and invalidation code
/// can use without assuming a provider-specific numeric ordering.
/// can use without assuming a provider-specific numeric ordering. It is a
/// distinct namespace from [`FederatedPolicyStamp::epoch`] and must never be
/// used as enrollment-policy currency evidence.
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct PolicyVersion(String);
@@ -222,6 +224,7 @@ pub struct AuthorizationRequest {
proof_method: AuthMethod,
authority: AuthorizationAuthority,
principal: FederatedPrincipal,
federated_policy: FederatedPolicyStamp,
profile_id: AuthorizationProfileId,
requested_capabilities: CapabilitySet,
correlation_id: Uuid,
@@ -239,6 +242,7 @@ impl AuthorizationRequest {
pub fn direct(
proof: &VerifiedNostrProof,
assertion: &VerifiedFederatedAssertion,
federated_policy: &ResolvedFederatedPolicy,
profile_id: AuthorizationProfileId,
requested_capabilities: CapabilitySet,
correlation_id: Uuid,
@@ -247,6 +251,12 @@ impl AuthorizationRequest {
if correlation_id.is_nil() {
return Err(ProviderContractError::InvalidCorrelationId);
}
validate_federated_policy(
federated_policy,
proof.authorization_domain(),
correlation_id,
now_unix_seconds,
)?;
if proof.verified_delegation().is_some() {
return Err(ProviderContractError::DirectRequestHasOwner);
}
@@ -278,11 +288,17 @@ impl AuthorizationRequest {
proof_method: proof.proof_method(),
authority: AuthorizationAuthority::Direct,
principal: assertion.principal().clone(),
federated_policy: federated_policy.stamp().clone(),
profile_id,
requested_capabilities,
correlation_id,
decision_source: DecisionSource::DirectAssertion,
evidence_valid_until: Some(assertion.expires_at().unix_seconds()),
evidence_valid_until: Some(
assertion
.expires_at()
.unix_seconds()
.min(federated_policy.stamp().effective_until()),
),
})
}
@@ -293,7 +309,8 @@ impl AuthorizationRequest {
/// `now_unix_seconds` must come from the server clock.
pub fn delegated(
proof: &VerifiedNostrProof,
owner: &VersionedBindingRef,
owner: &AuthoritativeBindingEvidence,
federated_policy: &ResolvedFederatedPolicy,
profile_id: AuthorizationProfileId,
requested_capabilities: CapabilitySet,
correlation_id: Uuid,
@@ -302,6 +319,12 @@ impl AuthorizationRequest {
if correlation_id.is_nil() {
return Err(ProviderContractError::InvalidCorrelationId);
}
validate_federated_policy(
federated_policy,
proof.authorization_domain(),
correlation_id,
now_unix_seconds,
)?;
if proof.authorization_domain() != owner.authorization_domain() {
return Err(ProviderContractError::AuthorizationDomainMismatch);
}
@@ -323,14 +346,13 @@ impl AuthorizationRequest {
{
return Err(ProviderContractError::BindingExpired);
}
let evidence_valid_until = match (delegation.expires_at(), owner.expires_at()) {
(Some(delegation), Some(binding)) => {
Some(delegation.unix_seconds().min(binding.unix_seconds()))
}
(Some(delegation), None) => Some(delegation.unix_seconds()),
(None, Some(binding)) => Some(binding.unix_seconds()),
(None, None) => None,
};
let mut evidence_valid_until = federated_policy.stamp().effective_until();
if let Some(delegation) = delegation.expires_at() {
evidence_valid_until = evidence_valid_until.min(delegation.unix_seconds());
}
if let Some(binding) = owner.expires_at() {
evidence_valid_until = evidence_valid_until.min(binding.unix_seconds());
}
Ok(Self {
authorization_domain: proof.authorization_domain(),
transport: proof.authorized_transport(),
@@ -342,11 +364,12 @@ impl AuthorizationRequest {
binding_version: owner.binding_version(),
},
principal: owner.principal().clone(),
federated_policy: federated_policy.stamp().clone(),
profile_id,
requested_capabilities,
correlation_id,
decision_source: DecisionSource::DelegatedOwnerBinding,
evidence_valid_until,
evidence_valid_until: Some(evidence_valid_until),
})
}
@@ -380,6 +403,11 @@ impl AuthorizationRequest {
&self.principal
}
/// Exact authoritative enrollment-policy lineage bound to this request.
pub const fn federated_policy(&self) -> &FederatedPolicyStamp {
&self.federated_policy
}
/// Server-resolved provider profile.
pub const fn profile_id(&self) -> &AuthorizationProfileId {
&self.profile_id
@@ -401,7 +429,7 @@ impl AuthorizationRequest {
}
/// Earliest validity bound supplied by verified assertion, owner-binding,
/// or delegation evidence.
/// delegation, or authoritative enrollment-policy evidence.
pub const fn evidence_valid_until(&self) -> Option<u64> {
self.evidence_valid_until
}
@@ -417,6 +445,7 @@ impl fmt::Debug for AuthorizationRequest {
.field("proof_method", &"[redacted]")
.field("authority", &"[redacted]")
.field("principal", &"[redacted]")
.field("federated_policy", &"[redacted]")
.field("profile_id", &"[redacted]")
.field("requested_capabilities", &"[redacted]")
.field("correlation_id", &"[redacted]")
@@ -505,6 +534,8 @@ pub enum AuthorizationDenialReason {
FutureDecision,
/// Verified identity evidence expired before the decision became effective.
IdentityEvidenceExpired,
/// The bound federated enrollment policy was not current after provider I/O.
FederatedPolicyNotCurrent,
}
impl AuthorizationDenialReason {
@@ -519,6 +550,7 @@ impl AuthorizationDenialReason {
Self::FutureDecision => "authorization_provider_deny_006",
Self::IdentityEvidenceExpired => "authorization_provider_deny_007",
Self::AuthorizationProfileMismatch => "authorization_provider_deny_008",
Self::FederatedPolicyNotCurrent => "authorization_provider_deny_009",
}
}
}
@@ -779,6 +811,7 @@ pub struct CapabilitySnapshot {
binding_version: Option<BindingVersion>,
proof_method: AuthMethod,
principal: FederatedPrincipal,
federated_policy: FederatedPolicyStamp,
profile_id: AuthorizationProfileId,
capabilities: CapabilitySet,
policy_version: PolicyVersion,
@@ -834,6 +867,19 @@ impl CapabilitySnapshot {
&self.principal
}
/// Exact authoritative enrollment-policy lineage bound to this decision.
pub const fn federated_policy(&self) -> &FederatedPolicyStamp {
&self.federated_policy
}
/// Whether a freshly resolved O3 policy is exactly the policy used here.
///
/// O3 must additionally compare this stamp with current authoritative state
/// and use its epoch as an atomic enrollment precondition.
pub fn is_bound_to_federated_policy(&self, policy: &ResolvedFederatedPolicy) -> bool {
self.federated_policy == *policy.stamp()
}
/// Server-resolved authorization profile for this decision.
pub const fn profile_id(&self) -> &AuthorizationProfileId {
&self.profile_id
@@ -892,6 +938,7 @@ impl fmt::Debug for CapabilitySnapshot {
.field("binding_version", &"[redacted]")
.field("proof_method", &"[redacted]")
.field("principal", &"[redacted]")
.field("federated_policy", &"[redacted]")
.field("profile_id", &"[redacted]")
.field("capabilities", &"[redacted]")
.field("policy_version", &"[redacted]")
@@ -964,6 +1011,14 @@ pub async fn resolve_authorization(
));
};
if request
.federated_policy
.is_not_yet_effective_at(now_unix_seconds)
|| request.federated_policy.is_expired_at(now_unix_seconds)
{
return deny(AuthorizationDenialReason::FederatedPolicyNotCurrent);
}
if allow.authorization_domain != request.authorization_domain {
return deny(AuthorizationDenialReason::AuthorizationDomainMismatch);
}
@@ -1013,6 +1068,7 @@ pub async fn resolve_authorization(
},
proof_method: request.proof_method,
principal: allow.principal,
federated_policy: request.federated_policy.clone(),
profile_id: allow.profile_id,
capabilities: request.requested_capabilities.clone(),
policy_version: allow.policy_version,
@@ -1099,6 +1155,18 @@ pub enum ProviderContractError {
/// Owner binding was expired at server time.
#[error("delegated provider request owner binding has expired")]
BindingExpired,
/// Enrollment policy belonged to another authorization domain.
#[error("provider request enrollment policy does not match the authorization domain")]
FederatedPolicyDomainMismatch,
/// Enrollment policy belonged to another correlated decision.
#[error("provider request enrollment policy does not match the correlation identifier")]
FederatedPolicyCorrelationMismatch,
/// Enrollment policy was not yet effective at server time.
#[error("provider request enrollment policy is not yet effective")]
FederatedPolicyNotYetEffective,
/// Enrollment policy was expired at server time.
#[error("provider request enrollment policy has expired")]
FederatedPolicyExpired,
}
impl ProviderContractError {
@@ -1127,9 +1195,34 @@ impl ProviderContractError {
Self::MissingKeyAttestation => "authorization_provider_contract_020",
Self::FreshnessWindowTooLong => "authorization_provider_contract_021",
Self::BindingExpired => "authorization_provider_contract_022",
Self::FederatedPolicyDomainMismatch => "authorization_provider_contract_023",
Self::FederatedPolicyCorrelationMismatch => "authorization_provider_contract_024",
Self::FederatedPolicyNotYetEffective => "authorization_provider_contract_025",
Self::FederatedPolicyExpired => "authorization_provider_contract_026",
}
}
}
fn validate_federated_policy(
policy: &ResolvedFederatedPolicy,
authorization_domain: CommunityId,
correlation_id: Uuid,
now_unix_seconds: u64,
) -> Result<(), ProviderContractError> {
if policy.authorization_domain() != authorization_domain {
return Err(ProviderContractError::FederatedPolicyDomainMismatch);
}
if policy.stamp().correlation_id() != correlation_id {
return Err(ProviderContractError::FederatedPolicyCorrelationMismatch);
}
if policy.stamp().is_not_yet_effective_at(now_unix_seconds) {
return Err(ProviderContractError::FederatedPolicyNotYetEffective);
}
if policy.stamp().is_expired_at(now_unix_seconds) {
return Err(ProviderContractError::FederatedPolicyExpired);
}
Ok(())
}
#[cfg(test)]
mod tests;
+334 -19
View File
@@ -11,9 +11,10 @@ use nostr::Keys;
use super::*;
use crate::context::{
AssertionExpiry, AssertionNotBefore, AssertionTransport, AuthTransport, BindingExpiry,
BindingSource, BindingVersion, DelegationExpiry, VerifiedKeyAttestation,
VerifiedTransportDelegation,
AssertionExpiry, AssertionNotBefore, AssertionTransport, AuthTransport,
AuthoritativeBindingEvidence, BindingExpiry, BindingSource, BindingVersion, DelegationExpiry,
EnrollmentMode, FederatedIdentityRequirement, FederatedPolicyStamp, ResolvedFederatedPolicy,
VerifiedKeyAttestation, VerifiedTransportDelegation,
};
const NOW: u64 = 100;
@@ -35,6 +36,39 @@ fn policy_version(value: &str) -> PolicyVersion {
PolicyVersion::new(value).expect("synthetic policy version is valid")
}
fn federated_policy_with(
domain_value: u128,
correlation_id: Uuid,
epoch: u64,
enrollment_mode: EnrollmentMode,
effective_from: u64,
effective_until: u64,
) -> ResolvedFederatedPolicy {
ResolvedFederatedPolicy::from_authoritative_resolution(
FederatedPolicyStamp::from_authoritative_state(
domain(domain_value),
Uuid::from_u128(40),
epoch,
correlation_id,
FederatedIdentityRequirement::Required(enrollment_mode),
effective_from,
effective_until,
)
.expect("synthetic federated policy lineage is valid"),
)
}
fn federated_policy() -> ResolvedFederatedPolicy {
federated_policy_with(
1,
Uuid::from_u128(20),
1,
EnrollmentMode::Provisioned,
1,
200,
)
}
fn provider_timeout() -> ProviderTimeout {
ProviderTimeout::new(Duration::from_secs(1)).expect("synthetic timeout is finite")
}
@@ -131,7 +165,7 @@ fn proof_method_for_transport(transport: AuthTransport) -> AuthMethod {
}
}
fn all_contract_errors() -> [ProviderContractError; 22] {
fn all_contract_errors() -> [ProviderContractError; 26] {
[
ProviderContractError::EmptyCapabilitySet,
ProviderContractError::EmptyProfileId,
@@ -155,6 +189,10 @@ fn all_contract_errors() -> [ProviderContractError; 22] {
ProviderContractError::DelegatedOwnerMismatch,
ProviderContractError::DelegationExpired,
ProviderContractError::BindingExpired,
ProviderContractError::FederatedPolicyDomainMismatch,
ProviderContractError::FederatedPolicyCorrelationMismatch,
ProviderContractError::FederatedPolicyNotYetEffective,
ProviderContractError::FederatedPolicyExpired,
]
}
@@ -181,6 +219,7 @@ fn direct_request_for_transport(
AuthorizationRequest::direct(
&proof,
&assertion,
&federated_policy(),
profile(),
requested,
Uuid::from_u128(20),
@@ -212,11 +251,11 @@ fn direct_request(actor: &Keys) -> AuthorizationRequest {
)
}
fn existing_binding(owner: &Keys) -> VersionedBindingRef {
fn existing_binding(owner: &Keys) -> AuthoritativeBindingEvidence {
existing_binding_in(1, owner)
}
fn existing_binding_in(domain_value: u128, owner: &Keys) -> VersionedBindingRef {
fn existing_binding_in(domain_value: u128, owner: &Keys) -> AuthoritativeBindingEvidence {
existing_binding_with_expiry_in(domain_value, owner, None)
}
@@ -224,8 +263,8 @@ fn existing_binding_with_expiry_in(
domain_value: u128,
owner: &Keys,
expires_at: Option<u64>,
) -> VersionedBindingRef {
VersionedBindingRef::new_existing_active_for_test(
) -> AuthoritativeBindingEvidence {
AuthoritativeBindingEvidence::new(
domain(domain_value),
Uuid::from_u128(10),
principal(),
@@ -260,6 +299,7 @@ fn delegated_request(actor: &Keys, owner: &Keys, expiry: u64) -> AuthorizationRe
AuthorizationRequest::delegated(
&proof,
&existing_binding(owner),
&federated_policy(),
profile(),
capabilities(&[AuthorizationCapability::CommunityRead]),
Uuid::from_u128(20),
@@ -567,6 +607,186 @@ async fn provider_freshness_is_evaluated_after_async_io() {
assert_eq!(clock.reads(), 1);
}
#[tokio::test]
async fn federated_policy_expiry_is_evaluated_after_async_io() {
let actor = Keys::generate();
let proof = VerifiedNostrProof::new(
domain(1),
AuthTransport::RelayWebSocket,
actor.public_key(),
AuthMethod::Nip42,
None,
)
.expect("synthetic proof is valid");
let assertion = VerifiedFederatedAssertion::new(
domain(1),
AuthTransport::RelayWebSocket,
principal(),
Some(VerifiedKeyAttestation::new(actor.public_key())),
AssertionTransport::TrustedProxy,
None,
AssertionExpiry::new(180).expect("synthetic assertion expiry is valid"),
);
let policy = federated_policy_with(
1,
Uuid::from_u128(20),
7,
EnrollmentMode::Provisioned,
1,
105,
);
let request = AuthorizationRequest::direct(
&proof,
&assertion,
&policy,
profile(),
capabilities(&[AuthorizationCapability::CommunityRead]),
Uuid::from_u128(20),
NOW,
)
.expect("federated policy is current when provider I/O begins");
let clock = TestClock::at(NOW);
let provider = AdvancingProvider::returning_at(
allow_for(
&request,
request.requested_capabilities().clone(),
"capability-policy-v1",
NOW,
180,
),
clock.clone(),
105,
);
let AuthorizationOutcome::Deny(denial) =
resolve_authorization(&provider, &request, &clock, provider_timeout()).await
else {
panic!("federated enrollment policy expired after I/O must deny");
};
assert_eq!(
denial.reason(),
AuthorizationDenialReason::FederatedPolicyNotCurrent
);
assert_eq!(clock.reads(), 1);
}
#[tokio::test]
async fn snapshot_requires_exact_enrollment_policy_lineage() {
let actor = Keys::generate();
let proof = VerifiedNostrProof::new(
domain(1),
AuthTransport::RelayWebSocket,
actor.public_key(),
AuthMethod::Nip42,
None,
)
.expect("synthetic proof is valid");
let assertion = VerifiedFederatedAssertion::new(
domain(1),
AuthTransport::RelayWebSocket,
principal(),
Some(VerifiedKeyAttestation::new(actor.public_key())),
AssertionTransport::TrustedProxy,
None,
AssertionExpiry::new(180).expect("synthetic assertion expiry is valid"),
);
let current_policy = federated_policy_with(
1,
Uuid::from_u128(20),
7,
EnrollmentMode::Provisioned,
1,
160,
);
let request = AuthorizationRequest::direct(
&proof,
&assertion,
&current_policy,
profile(),
capabilities(&[AuthorizationCapability::CommunityRead]),
Uuid::from_u128(20),
NOW,
)
.expect("current policy can enter provider evaluation");
let provider = FakeProvider::returning(allow_for(
&request,
request.requested_capabilities().clone(),
"6",
90,
180,
));
let AuthorizationOutcome::Allow(snapshot) =
resolve_at(&provider, &request, NOW, provider_timeout()).await
else {
panic!("current provider and enrollment policy must allow");
};
let stale_tofu_policy =
federated_policy_with(1, Uuid::from_u128(20), 6, EnrollmentMode::Tofu, 1, 160);
assert!(snapshot.is_bound_to_federated_policy(&current_policy));
assert!(!snapshot.is_bound_to_federated_policy(&stale_tofu_policy));
assert_eq!(snapshot.policy_version().as_str(), "6");
assert_ne!(
snapshot.policy_version().as_str(),
snapshot.federated_policy().epoch().to_string()
);
}
#[tokio::test]
async fn enrollment_policy_bounds_snapshot_effective_interval() {
let actor = Keys::generate();
let proof = VerifiedNostrProof::new(
domain(1),
AuthTransport::RelayWebSocket,
actor.public_key(),
AuthMethod::Nip42,
None,
)
.expect("synthetic proof is valid");
let assertion = VerifiedFederatedAssertion::new(
domain(1),
AuthTransport::RelayWebSocket,
principal(),
Some(VerifiedKeyAttestation::new(actor.public_key())),
AssertionTransport::TrustedProxy,
None,
AssertionExpiry::new(180).expect("synthetic assertion expiry is valid"),
);
let policy = federated_policy_with(
1,
Uuid::from_u128(20),
7,
EnrollmentMode::Provisioned,
1,
150,
);
let request = AuthorizationRequest::direct(
&proof,
&assertion,
&policy,
profile(),
capabilities(&[AuthorizationCapability::CommunityRead]),
Uuid::from_u128(20),
NOW,
)
.expect("current policy can enter provider evaluation");
let provider = FakeProvider::returning(allow_for(
&request,
request.requested_capabilities().clone(),
"capability-policy-v1",
90,
170,
));
let AuthorizationOutcome::Allow(snapshot) =
resolve_at(&provider, &request, NOW, provider_timeout()).await
else {
panic!("current bounded policy must allow");
};
assert_eq!(request.evidence_valid_until(), Some(150));
assert_eq!(snapshot.effective_until(), 150);
}
#[tokio::test]
async fn identity_evidence_is_evaluated_after_async_io() {
let actor = Keys::generate();
@@ -608,6 +828,7 @@ async fn owner_binding_expiry_is_evaluated_after_async_io() {
let request = AuthorizationRequest::delegated(
&proof,
&binding,
&federated_policy(),
profile(),
capabilities(&[AuthorizationCapability::CommunityRead]),
Uuid::from_u128(20),
@@ -649,6 +870,7 @@ fn delegated_request_rejects_owner_binding_at_exact_expiry() {
let error = AuthorizationRequest::delegated(
&proof,
&binding,
&federated_policy(),
profile(),
capabilities(&[AuthorizationCapability::CommunityRead]),
Uuid::from_u128(20),
@@ -1230,6 +1452,7 @@ fn request_construction_rechecks_verified_bounds_and_relationships() {
AuthorizationRequest::direct(
&proof,
&expired,
&federated_policy(),
profile(),
capabilities(&[AuthorizationCapability::CommunityRead]),
Uuid::nil(),
@@ -1241,6 +1464,7 @@ fn request_construction_rechecks_verified_bounds_and_relationships() {
AuthorizationRequest::direct(
&proof,
&expired,
&federated_policy(),
profile(),
capabilities(&[AuthorizationCapability::CommunityRead]),
Uuid::from_u128(20),
@@ -1262,6 +1486,7 @@ fn request_construction_rechecks_verified_bounds_and_relationships() {
AuthorizationRequest::direct(
&proof,
&future,
&federated_policy(),
profile(),
capabilities(&[AuthorizationCapability::CommunityRead]),
Uuid::from_u128(20),
@@ -1271,6 +1496,88 @@ fn request_construction_rechecks_verified_bounds_and_relationships() {
);
}
#[test]
fn request_construction_rejects_non_current_or_mismatched_federated_policy() {
let actor = Keys::generate();
let proof = VerifiedNostrProof::new(
domain(1),
AuthTransport::RelayWebSocket,
actor.public_key(),
AuthMethod::Nip42,
None,
)
.expect("synthetic proof is valid");
let assertion = VerifiedFederatedAssertion::new(
domain(1),
AuthTransport::RelayWebSocket,
principal(),
Some(VerifiedKeyAttestation::new(actor.public_key())),
AssertionTransport::TrustedProxy,
None,
AssertionExpiry::new(180).expect("synthetic assertion expiry is valid"),
);
let request_with = |policy: &ResolvedFederatedPolicy| {
AuthorizationRequest::direct(
&proof,
&assertion,
policy,
profile(),
capabilities(&[AuthorizationCapability::CommunityRead]),
Uuid::from_u128(20),
NOW,
)
};
let wrong_domain = federated_policy_with(
2,
Uuid::from_u128(20),
1,
EnrollmentMode::Provisioned,
1,
180,
);
assert_eq!(
request_with(&wrong_domain),
Err(ProviderContractError::FederatedPolicyDomainMismatch)
);
let wrong_correlation = federated_policy_with(
1,
Uuid::from_u128(21),
1,
EnrollmentMode::Provisioned,
1,
180,
);
assert_eq!(
request_with(&wrong_correlation),
Err(ProviderContractError::FederatedPolicyCorrelationMismatch)
);
let future = federated_policy_with(
1,
Uuid::from_u128(20),
1,
EnrollmentMode::Provisioned,
NOW + 1,
180,
);
assert_eq!(
request_with(&future),
Err(ProviderContractError::FederatedPolicyNotYetEffective)
);
let expired = federated_policy_with(
1,
Uuid::from_u128(20),
1,
EnrollmentMode::Provisioned,
1,
NOW,
);
assert_eq!(
request_with(&expired),
Err(ProviderContractError::FederatedPolicyExpired)
);
}
#[test]
fn request_construction_rejects_mismatched_verified_evidence() {
let actor = Keys::generate();
@@ -1301,6 +1608,7 @@ fn request_construction_rejects_mismatched_verified_evidence() {
AuthorizationRequest::direct(
proof,
assertion,
&federated_policy(),
profile(),
capabilities(&[AuthorizationCapability::CommunityRead]),
Uuid::from_u128(20),
@@ -1359,20 +1667,23 @@ fn request_construction_rejects_mismatched_verified_evidence() {
Err(ProviderContractError::DirectRequestHasOwner)
);
let delegated_request_from = |proof: &VerifiedNostrProof, binding: &VersionedBindingRef| {
AuthorizationRequest::delegated(
proof,
binding,
profile(),
capabilities(&[AuthorizationCapability::CommunityRead]),
Uuid::from_u128(20),
NOW,
)
};
let delegated_request_from =
|proof: &VerifiedNostrProof, binding: &AuthoritativeBindingEvidence| {
AuthorizationRequest::delegated(
proof,
binding,
&federated_policy(),
profile(),
capabilities(&[AuthorizationCapability::CommunityRead]),
Uuid::from_u128(20),
NOW,
)
};
assert_eq!(
AuthorizationRequest::delegated(
&delegated_proof,
&existing_binding(&owner),
&federated_policy(),
profile(),
capabilities(&[AuthorizationCapability::CommunityRead]),
Uuid::nil(),
@@ -1422,6 +1733,7 @@ async fn request_decision_snapshot_and_errors_are_redaction_safe() {
"transport: \"[redacted]\", actor_pubkey: \"[redacted]\", ",
"proof_method: \"[redacted]\", ",
"authority: \"[redacted]\", principal: \"[redacted]\", ",
"federated_policy: \"[redacted]\", ",
"profile_id: \"[redacted]\", requested_capabilities: \"[redacted]\", ",
"correlation_id: \"[redacted]\", decision_source: \"[redacted]\", ",
"evidence_valid_until: \"[redacted]\" }"
@@ -1489,6 +1801,7 @@ async fn request_decision_snapshot_and_errors_are_redaction_safe() {
"owner_pubkey: \"[redacted]\", binding_id: \"[redacted]\", ",
"binding_version: \"[redacted]\", proof_method: \"[redacted]\", ",
"principal: \"[redacted]\", ",
"federated_policy: \"[redacted]\", ",
"profile_id: \"[redacted]\", capabilities: \"[redacted]\", ",
"policy_version: \"[redacted]\", issued_at: \"[redacted]\", ",
"fresh_until: \"[redacted]\", effective_until: \"[redacted]\", ",
@@ -1584,6 +1897,7 @@ async fn request_decision_snapshot_and_errors_are_redaction_safe() {
AuthorizationDenialReason::StaleDecision,
AuthorizationDenialReason::FutureDecision,
AuthorizationDenialReason::IdentityEvidenceExpired,
AuthorizationDenialReason::FederatedPolicyNotCurrent,
] {
assert_eq!(
format!("{reason:?}"),
@@ -1641,13 +1955,14 @@ fn provider_trait_is_object_safe_and_codes_are_unique() {
AuthorizationDenialReason::StaleDecision.code(),
AuthorizationDenialReason::FutureDecision.code(),
AuthorizationDenialReason::IdentityEvidenceExpired.code(),
AuthorizationDenialReason::FederatedPolicyNotCurrent.code(),
ProviderUnavailableReason::TemporarilyUnavailable.code(),
ProviderUnavailableReason::Timeout.code(),
ProviderUnavailableReason::DependencyUnavailable.code(),
];
codes.sort_unstable();
codes.dedup();
assert_eq!(codes.len(), 12);
assert_eq!(codes.len(), 13);
let contract_errors = all_contract_errors();
let mut contract_codes = contract_errors