Skip to content
Draft
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
47 changes: 47 additions & 0 deletions crates/originweave-policy/src/sensitive_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ pub enum HandleUseDecision {
Authorized,
/// The authoritative in-process handle state was revoked before this use.
Revoked,
/// The supplied tracked-use identity is not outstanding in this exact state.
ReservationNotOutstanding,
/// Tenant, task, field, purpose, destination, or classification did not match the handle scope.
ScopeMismatch,
/// The caller audience was invalid or did not match the handle's non-transferable audience.
Expand Down Expand Up @@ -405,6 +407,51 @@ impl SensitiveHandleUseState {
Ok(SensitiveHandleUseReservation { identity })
}

/// Recheck the exact outstanding tracked reservation immediately before disclosure.
///
/// This does not reserve another use and does not mutate settlement state. The
/// trusted broker must supply authenticated audience, trusted time, and the exact
/// authority that applies at disclosure time, and must call this inside the same
/// transaction or locking boundary that guards value disclosure. Revocation is
/// checked before reservation membership and request detail so a revoked state
/// does not disclose whether a foreign or stale reservation would otherwise match.
/// An outstanding reservation also proves that this immutable state scope passed
/// authority and audience validation when it was admitted; recheck therefore
/// validates the caller-supplied authority and audience without duplicating
/// unreachable scope-validation branches. A use-limit check is intentionally
/// omitted because the outstanding reservation already consumed its bounded use
/// capacity.
#[must_use]
pub fn recheck_reservation(
&self,
reservation: &SensitiveHandleUseReservation,
authority: SensitiveDataAuthority,
audience_id: &str,
now_epoch_seconds: u64,
) -> HandleUseDecision {
if self.revocation_reason.is_some() {
return HandleUseDecision::Revoked;
}
if !self
.outstanding_reservations
.iter()
.any(|candidate| candidate == reservation)
{
return HandleUseDecision::ReservationNotOutstanding;
}
if !authority.is_complete() || authority != self.scope.authority {
HandleUseDecision::ScopeMismatch
} else if !authority_identifier_is_valid(audience_id)
|| audience_id != self.scope.audience_id
{
HandleUseDecision::AudienceMismatch
} else if now_epoch_seconds >= self.scope.expires_at_epoch_seconds {
HandleUseDecision::Expired
} else {
HandleUseDecision::Authorized
}
}

/// Mark one exact tracked reservation as a completed, permanently consumed use.
///
/// This method records settlement only; it does not authorize disclosure. A
Expand Down
144 changes: 144 additions & 0 deletions crates/originweave-policy/tests/sensitive_handle_recheck.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#![allow(clippy::expect_used)]

use originweave_core::Origin;
use originweave_policy::{
DataClassification, HandleRevocationReason, HandleUseDecision, SensitiveDataAuthority,
SensitiveHandleUseState, SensitiveValueHandleScope,
};

const TENANT: &str = "tenant_alpha";
const TASK: &str = "task_ship_order";
const FIELD: &str = "shipping_address";
const PURPOSE: &str = "fulfill_order";
const DESTINATION: &str = "https://shipping.example";
const AUDIENCE: &str = "trusted_browser_adapter";

fn authority(destination: &str) -> SensitiveDataAuthority {
SensitiveDataAuthority::new(
TENANT,
TASK,
FIELD,
PURPOSE,
Origin::parse(destination).expect("test origin must be valid"),
DataClassification::PersonalData,
)
}

fn authority_with_tenant(tenant_id: &str) -> SensitiveDataAuthority {
SensitiveDataAuthority::new(
tenant_id,
TASK,
FIELD,
PURPOSE,
Origin::parse(DESTINATION).expect("test origin must be valid"),
DataClassification::PersonalData,
)
}

fn scope(max_uses: u32) -> SensitiveValueHandleScope {
SensitiveValueHandleScope::new(authority(DESTINATION), AUDIENCE, 2_000, max_uses)
}

#[test]
fn outstanding_reservation_can_be_rechecked_without_consuming_another_use() {
let mut state = SensitiveHandleUseState::new(scope(1));
let reservation = state
.reserve_tracked_use(authority(DESTINATION), AUDIENCE, 1_900)
.expect("reservation must be authorized");

assert_eq!(state.reserved_uses(), 1);
assert_eq!(
state.recheck_reservation(&reservation, authority(DESTINATION), AUDIENCE, 1_999),
HandleUseDecision::Authorized
);
assert_eq!(state.reserved_uses(), 1);
assert_eq!(state.outstanding_reservations(), 1);
}

#[test]
fn foreign_or_settled_reservation_cannot_be_rechecked() {
let mut first_state = SensitiveHandleUseState::new(scope(2));
let mut second_state = SensitiveHandleUseState::new(scope(2));
let first = first_state
.reserve_tracked_use(authority(DESTINATION), AUDIENCE, 1_900)
.expect("first reservation must be authorized");
let second = second_state
.reserve_tracked_use(authority(DESTINATION), AUDIENCE, 1_900)
.expect("second reservation must be authorized");

assert_eq!(
second_state.recheck_reservation(&first, authority(DESTINATION), AUDIENCE, 1_999),
HandleUseDecision::ReservationNotOutstanding
);
assert!(first_state.compensate_reservation(&first));
assert_eq!(
first_state.recheck_reservation(&first, authority(DESTINATION), AUDIENCE, 1_999),
HandleUseDecision::ReservationNotOutstanding
);
assert!(second_state.commit_reservation(&second));
assert_eq!(
second_state.recheck_reservation(&second, authority(DESTINATION), AUDIENCE, 1_999),
HandleUseDecision::ReservationNotOutstanding
);
}

#[test]
fn recheck_revalidates_scope_audience_and_expiry() {
let mut state = SensitiveHandleUseState::new(scope(1));
let reservation = state
.reserve_tracked_use(authority(DESTINATION), AUDIENCE, 1_900)
.expect("reservation must be authorized");

assert_eq!(
state.recheck_reservation(
&reservation,
authority("https://other.example"),
AUDIENCE,
1_999,
),
HandleUseDecision::ScopeMismatch
);
assert_eq!(
state.recheck_reservation(&reservation, authority_with_tenant(""), AUDIENCE, 1_999),
HandleUseDecision::ScopeMismatch
);
assert_eq!(
state.recheck_reservation(
&reservation,
authority(DESTINATION),
"other_browser_adapter",
1_999,
),
HandleUseDecision::AudienceMismatch
);
assert_eq!(
state.recheck_reservation(&reservation, authority(DESTINATION), "", 1_999),
HandleUseDecision::AudienceMismatch
);
assert_eq!(
state.recheck_reservation(&reservation, authority(DESTINATION), AUDIENCE, 2_000),
HandleUseDecision::Expired
);
assert_eq!(state.reserved_uses(), 1);
assert_eq!(state.outstanding_reservations(), 1);
}

#[test]
fn revocation_precedes_reservation_and_request_details_on_recheck() {
let mut active_state = SensitiveHandleUseState::new(scope(1));
let foreign = active_state
.reserve_tracked_use(authority(DESTINATION), AUDIENCE, 1_900)
.expect("foreign reservation must be authorized");
let mut revoked_state = SensitiveHandleUseState::new(scope(1));
assert!(revoked_state.revoke(HandleRevocationReason::PolicyChanged));

assert_eq!(
revoked_state.recheck_reservation(
&foreign,
authority("https://other.example"),
"other_browser_adapter",
2_001,
),
HandleUseDecision::Revoked
);
}
Loading