mirror of
https://github.com/block/buzz.git
synced 2026-11-12 12:22:06 +00:00
Clarify IFC session reuse and test retained-state boundaries
Signed-off-by: Jordan Mecom <jm@squareup.com>
This commit is contained in:
@@ -6,10 +6,14 @@
|
||||
//! and membership version, so a change produces a different key.
|
||||
//!
|
||||
//! The broker must verify events and membership before supplying [`DomainFacts`].
|
||||
//! It uses the resulting key to select an [`IfcSession`], then checks reads and
|
||||
//! calls before executing them. Publications must pass [`IfcSession::publish`]
|
||||
//! before reaching a sink. This crate does not verify signatures, load saved
|
||||
//! sessions, or execute operations itself.
|
||||
//! It uses the resulting key to select both the agent's saved state and its
|
||||
//! [`IfcSession`]. Keep that session across turns: recreating it would forget
|
||||
//! whether unlabeled input had reached the agent.
|
||||
//!
|
||||
//! Check reads before delivering data, calls before executing them, and
|
||||
//! publications with [`IfcSession::publish`] before sending them to a sink.
|
||||
//! This crate does not verify signatures, check live membership, load saved
|
||||
//! sessions, isolate agent processes, or execute operations itself.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
|
||||
@@ -89,19 +89,25 @@ impl AuthorizedPublication {
|
||||
/// sink. The audience checks follow the flow ordering in
|
||||
/// [Appendix G of the design paper](../../../docs/practical-information-flow-for-buzz-agents.md#appendix-g-security-labels-as-a-lattice).
|
||||
///
|
||||
/// The broker supplies the labels and serialized outbound request:
|
||||
/// The broker keeps this value for as long as it keeps the agent's history,
|
||||
/// files, or other state. The same domain key selects both. This example uses a
|
||||
/// broker-owned pool so a later turn cannot reset the session's restrictions:
|
||||
///
|
||||
/// ```
|
||||
/// # use std::collections::HashMap;
|
||||
/// # use buzz_ifc::{AuthorizedPublication, ConfidentialityLabel, ExecutionDomain,
|
||||
/// # IfcError, IfcSession, ResourceLabel};
|
||||
/// # DomainKey, IfcError, IfcSession, ResourceLabel};
|
||||
/// # fn broker_sink(_: AuthorizedPublication) {}
|
||||
/// # fn run_turn(
|
||||
/// # sessions: &mut HashMap<DomainKey, IfcSession>,
|
||||
/// # domain: ExecutionDomain,
|
||||
/// # resource: &ResourceLabel,
|
||||
/// # destination: &ConfidentialityLabel,
|
||||
/// # request_bytes: Vec<u8>,
|
||||
/// # ) -> Result<(), IfcError> {
|
||||
/// let session = IfcSession::enter(domain);
|
||||
/// let session = sessions
|
||||
/// .entry(domain.key())
|
||||
/// .or_insert_with(|| IfcSession::enter(domain));
|
||||
/// session.call("buzz.read.current")?;
|
||||
/// session.read(resource)?;
|
||||
///
|
||||
@@ -122,6 +128,9 @@ impl IfcSession {
|
||||
/// The domain's audience is observed immediately because retained agent
|
||||
/// state and broker-provided instructions may already influence the next
|
||||
/// output before the first explicit resource read.
|
||||
///
|
||||
/// Do not replace an existing session while retaining the agent's state.
|
||||
/// A fresh session would forget any earlier [`Self::mark_unknown_input`].
|
||||
pub fn enter(domain: ExecutionDomain) -> Self {
|
||||
let mut flow = FlowState::default();
|
||||
flow.observe(&domain.audience);
|
||||
@@ -138,6 +147,8 @@ impl IfcSession {
|
||||
/// The broker must not deliver a rejected resource. Every admitted resource
|
||||
/// is readable by the domain's entire audience, so reading it does not
|
||||
/// further restrict output: `enter` already applied that audience.
|
||||
/// The broker must check current resource permissions separately; matching
|
||||
/// stored membership epochs does not prove that membership is still current.
|
||||
pub fn read(&self, resource: &ResourceLabel) -> Result<(), IfcError> {
|
||||
if !resource.audience.can_flow_to(&self.domain.audience) {
|
||||
return Err(IfcError::ReadAudienceDenied);
|
||||
|
||||
@@ -304,6 +304,39 @@ fn unknown_input_permanently_blocks_publication() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Reusing agent history also reuses its IFC state. Two public channels share
|
||||
/// a domain, so routing the next turn through another channel must not clear
|
||||
/// the unknown-input flag. This exercises the pool lookup used in the example.
|
||||
#[test]
|
||||
fn unknown_input_survives_reusing_a_session_in_another_public_channel() {
|
||||
let first_turn = public_domain(community(1), 10, "community:v1");
|
||||
let next_turn = public_domain(community(1), 20, "community:v1");
|
||||
let resource = ResourceLabel::from_domain(&next_turn);
|
||||
let destination = next_turn.audience().clone();
|
||||
let mut pool = HashMap::new();
|
||||
|
||||
pool.entry(first_turn.key())
|
||||
.or_insert_with(|| IfcSession::enter(first_turn))
|
||||
.mark_unknown_input();
|
||||
|
||||
let session = pool
|
||||
.entry(next_turn.key())
|
||||
.or_insert_with(|| IfcSession::enter(next_turn));
|
||||
session
|
||||
.call(READ)
|
||||
.expect("the read operation remains allowed");
|
||||
session.read(&resource).expect("public input is admissible");
|
||||
assert_eq!(
|
||||
session
|
||||
.publish(REPLY, &destination, b"output".to_vec())
|
||||
.err(),
|
||||
Some(IfcError::InformationFlow(
|
||||
ifc_core::EgressError::UnresolvedInput
|
||||
))
|
||||
);
|
||||
assert_eq!(pool.len(), 1);
|
||||
}
|
||||
|
||||
/// Retained restricted state from an older membership snapshot cannot enter a
|
||||
/// newly routed session for the same conversation.
|
||||
#[test]
|
||||
@@ -368,6 +401,37 @@ fn equal_audiences_do_not_merge_restricted_conversation_contexts() {
|
||||
assert_eq!(session.read(&resource), Err(IfcError::ReadContextDenied));
|
||||
}
|
||||
|
||||
/// The owner may bring conversation data into owner-private work, but not the
|
||||
/// reverse. Equal audiences must not let an ordinary conversation import the
|
||||
/// owner's private history or memory.
|
||||
#[test]
|
||||
fn owner_private_state_cannot_enter_a_conversation_with_the_same_audience() {
|
||||
let owner_private = owner_private_domain(community(1), 10, "membership:v1");
|
||||
let conversation = restricted_domain(community(1), 20, "membership:v1", &[1]);
|
||||
assert_eq!(owner_private.audience(), conversation.audience());
|
||||
let resource = ResourceLabel::from_domain(&owner_private);
|
||||
let session = IfcSession::enter(conversation);
|
||||
|
||||
assert_eq!(session.read(&resource), Err(IfcError::ReadContextDenied));
|
||||
}
|
||||
|
||||
/// Public means public within one community, not readable across all of them.
|
||||
/// A cross-community resource must be rejected before any data is delivered.
|
||||
#[test]
|
||||
fn public_resource_cannot_cross_communities() {
|
||||
let source = public_domain(community(1), 10, "community:v1");
|
||||
let destination = public_domain(community(2), 10, "community:v1");
|
||||
let resource = ResourceLabel::from_domain(&source);
|
||||
let session = IfcSession::enter(destination);
|
||||
let mut inbox = Vec::new();
|
||||
|
||||
assert_eq!(
|
||||
deliver_to_agent(&session, &resource, "other community", &mut inbox),
|
||||
Err(IfcError::ReadAudienceDenied)
|
||||
);
|
||||
assert!(inbox.is_empty());
|
||||
}
|
||||
|
||||
/// Universes are isolated even when two communities happen to contain the
|
||||
/// same principals. A target in another community is never a valid IFC sink.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user