mirror of
https://github.com/block/buzz.git
synced 2026-10-29 21:41:33 +00:00
Freeze IFC publications and simplify session checks
Signed-off-by: Jordan Mecom <jm@squareup.com>
This commit is contained in:
@@ -441,6 +441,11 @@ impl ExecutionDomain {
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the label describing who may receive this domain's output.
|
||||
pub fn audience(&self) -> &ConfidentialityLabel {
|
||||
&self.audience
|
||||
}
|
||||
|
||||
/// Hash every domain field into a stable key for session lookup.
|
||||
///
|
||||
/// Changing any field changes the key. The broker must use this complete key
|
||||
|
||||
@@ -22,10 +22,7 @@ pub use domain::{
|
||||
DomainFacts, DomainKey, ExecutionDomain, MembershipEpoch, OperationEffect,
|
||||
};
|
||||
pub use label::{CommunityId, ConfidentialityLabel, LabelError, Principal, PrincipalError};
|
||||
pub use session::{
|
||||
AuthorizedPublication, IfcError, IfcSession, PublicationRequest, PublicationTarget,
|
||||
ResourceLabel,
|
||||
};
|
||||
pub use session::{AuthorizedPublication, IfcError, IfcSession, ResourceLabel};
|
||||
|
||||
#[cfg(test)]
|
||||
mod session_tests;
|
||||
|
||||
@@ -31,80 +31,53 @@ impl ResourceLabel {
|
||||
}
|
||||
}
|
||||
|
||||
/// The audience receiving a publication.
|
||||
/// An outbound request that passed the session's capability and audience checks.
|
||||
///
|
||||
/// This type describes only the IFC destination. The trusted broker remains
|
||||
/// responsible for ordinary product policy, including resolving and checking
|
||||
/// the channel, message, URL, or other concrete destination named by the
|
||||
/// operation payload.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct PublicationTarget {
|
||||
audience: ConfidentialityLabel,
|
||||
}
|
||||
|
||||
impl PublicationTarget {
|
||||
/// Use an execution domain's authorized audience as a publication target.
|
||||
pub fn from_domain(domain: &ExecutionDomain) -> Self {
|
||||
Self {
|
||||
audience: domain.audience.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A broker request to execute one publication operation with an exact
|
||||
/// payload.
|
||||
pub struct PublicationRequest<T> {
|
||||
operation: String,
|
||||
target: PublicationTarget,
|
||||
payload: T,
|
||||
}
|
||||
|
||||
impl<T> PublicationRequest<T> {
|
||||
/// Construct a request after the broker has resolved its IFC target.
|
||||
pub fn new(operation: impl Into<String>, target: PublicationTarget, payload: T) -> Self {
|
||||
Self {
|
||||
operation: operation.into(),
|
||||
target,
|
||||
payload,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Proof that an exact publication request passed the session's capability
|
||||
/// and information-flow checks.
|
||||
/// The request owns its serialized bytes and destination label. Neither can
|
||||
/// change through a shared reference after authorization. A broker sink should
|
||||
/// accept this type and execute those bytes, without substituting a new payload
|
||||
/// or resolving destination fields from mutable agent state.
|
||||
///
|
||||
/// A broker sink should accept this type rather than [`PublicationRequest`].
|
||||
/// Its fields are private and it is not cloneable, so only
|
||||
/// [`IfcSession::publish`] can create the value that authorizes a sink call.
|
||||
/// Only [`IfcSession::publish`] can construct this value. It cannot be cloned.
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// # fn forge(destination: buzz_ifc::ConfidentialityLabel) {
|
||||
/// let forged = buzz_ifc::AuthorizedPublication {
|
||||
/// operation: "buzz.reply".to_owned(),
|
||||
/// target: todo!(),
|
||||
/// payload: "unchecked",
|
||||
/// destination,
|
||||
/// payload: b"unchecked".to_vec(),
|
||||
/// };
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Payload access is read-only until the sink consumes the authorization:
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// # fn change(mut authorization: buzz_ifc::AuthorizedPublication) {
|
||||
/// authorization.payload()[0] = b'!';
|
||||
/// # }
|
||||
/// ```
|
||||
#[must_use = "the authorization must be consumed by the publication sink"]
|
||||
pub struct AuthorizedPublication<T> {
|
||||
pub struct AuthorizedPublication {
|
||||
operation: String,
|
||||
target: PublicationTarget,
|
||||
payload: T,
|
||||
destination: ConfidentialityLabel,
|
||||
payload: Vec<u8>,
|
||||
}
|
||||
|
||||
impl<T> AuthorizedPublication<T> {
|
||||
impl AuthorizedPublication {
|
||||
/// Return the checked operation name.
|
||||
pub fn operation(&self) -> &str {
|
||||
&self.operation
|
||||
}
|
||||
|
||||
/// Return the exact payload covered by the information-flow decision.
|
||||
pub fn payload(&self) -> &T {
|
||||
pub fn payload(&self) -> &[u8] {
|
||||
&self.payload
|
||||
}
|
||||
|
||||
/// Consume the authorization and return the checked sink inputs.
|
||||
pub fn into_parts(self) -> (String, PublicationTarget, T) {
|
||||
(self.operation, self.target, self.payload)
|
||||
pub fn into_parts(self) -> (String, ConfidentialityLabel, Vec<u8>) {
|
||||
(self.operation, self.destination, self.payload)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,27 +86,26 @@ impl<T> AuthorizedPublication<T> {
|
||||
/// The broker calls [`read`](Self::read) before delivering data to the agent,
|
||||
/// [`call`](Self::call) before executing operations that cannot publish, and
|
||||
/// [`publish`](Self::publish) before handing an exact outbound payload to a
|
||||
/// sink. These are the ordinary-flow checks described in Appendices F and G of
|
||||
/// the [design paper](../../../docs/practical-information-flow-for-buzz-agents.md#appendix-f-reference-monitor-pseudocode).
|
||||
/// 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:
|
||||
///
|
||||
/// ```
|
||||
/// # use buzz_ifc::{AuthorizedPublication, ExecutionDomain, IfcError, IfcSession,
|
||||
/// # PublicationRequest, PublicationTarget, ResourceLabel};
|
||||
/// # fn broker_sink(_: AuthorizedPublication<Vec<u8>>) {}
|
||||
/// # use buzz_ifc::{AuthorizedPublication, ConfidentialityLabel, ExecutionDomain,
|
||||
/// # IfcError, IfcSession, ResourceLabel};
|
||||
/// # fn broker_sink(_: AuthorizedPublication) {}
|
||||
/// # fn run_turn(
|
||||
/// # domain: ExecutionDomain,
|
||||
/// # resource: &ResourceLabel,
|
||||
/// # target: PublicationTarget,
|
||||
/// # destination: &ConfidentialityLabel,
|
||||
/// # request_bytes: Vec<u8>,
|
||||
/// # ) -> Result<(), IfcError> {
|
||||
/// let mut session = IfcSession::enter(domain);
|
||||
/// session.read(resource)?;
|
||||
/// let session = IfcSession::enter(domain);
|
||||
/// session.call("buzz.read.current")?;
|
||||
/// session.read(resource)?;
|
||||
///
|
||||
/// let authorization = session.publish(PublicationRequest::new(
|
||||
/// "buzz.reply",
|
||||
/// target,
|
||||
/// b"hello".to_vec(),
|
||||
/// ))?;
|
||||
/// let authorization = session.publish("buzz.reply", destination, request_bytes)?;
|
||||
/// broker_sink(authorization);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
@@ -161,13 +133,12 @@ impl IfcSession {
|
||||
self.domain.key()
|
||||
}
|
||||
|
||||
/// Check and record a labeled resource before exposing it to the agent.
|
||||
/// Check a labeled resource before exposing it to the agent.
|
||||
///
|
||||
/// A failed read does not taint the session because the broker must not
|
||||
/// deliver the rejected resource. Successful reads are recorded
|
||||
/// monotonically; the domain audience already conservatively bounds output
|
||||
/// in this coarse-grained Buzz model.
|
||||
pub fn read(&mut self, resource: &ResourceLabel) -> Result<(), IfcError> {
|
||||
/// 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.
|
||||
pub fn read(&self, resource: &ResourceLabel) -> Result<(), IfcError> {
|
||||
if !resource.audience.can_flow_to(&self.domain.audience) {
|
||||
return Err(IfcError::ReadAudienceDenied);
|
||||
}
|
||||
@@ -183,15 +154,14 @@ impl IfcSession {
|
||||
return Err(IfcError::StaleResourceEpoch);
|
||||
}
|
||||
|
||||
self.flow.observe(&resource.audience);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Permanently record that unlabeled input reached the agent.
|
||||
///
|
||||
/// Ordinary publication remains blocked for the rest of the session. A
|
||||
/// future declassification API may authorize a specific exceptional flow,
|
||||
/// but this small wrapper deliberately provides no bypass.
|
||||
/// No new publication can be authorized for the rest of the session.
|
||||
/// Already authorized requests still contain only their earlier, frozen
|
||||
/// bytes. They cannot be updated to include the unknown input.
|
||||
pub fn mark_unknown_input(&mut self) {
|
||||
self.flow.mark_unknown();
|
||||
}
|
||||
@@ -206,11 +176,30 @@ impl IfcSession {
|
||||
}
|
||||
|
||||
/// Authorize an exact outbound payload for a checked broker sink.
|
||||
pub fn publish<T>(
|
||||
///
|
||||
/// The broker must serialize the complete request, including its concrete
|
||||
/// destination, and resolve `destination` from that request before calling
|
||||
/// this method. The sink must execute the returned bytes as checked. This
|
||||
/// method does not parse the request or check current destination policy.
|
||||
///
|
||||
/// Owned bytes prevent a caller from changing the payload through a shared
|
||||
/// mutable value after it passes the checks:
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// # use buzz_ifc::{ConfidentialityLabel, IfcSession};
|
||||
/// # use std::{cell::RefCell, rc::Rc};
|
||||
/// # fn publish_shared(session: &IfcSession, destination: &ConfidentialityLabel) {
|
||||
/// let payload = Rc::new(RefCell::new(b"hello".to_vec()));
|
||||
/// session.publish("buzz.reply", destination, payload);
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn publish(
|
||||
&self,
|
||||
request: PublicationRequest<T>,
|
||||
) -> Result<AuthorizedPublication<T>, IfcError> {
|
||||
match self.domain.capabilities.effect(&request.operation) {
|
||||
operation: &str,
|
||||
destination: &ConfidentialityLabel,
|
||||
payload: Vec<u8>,
|
||||
) -> Result<AuthorizedPublication, IfcError> {
|
||||
match self.domain.capabilities.effect(operation) {
|
||||
Some(OperationEffect::Publication) => {}
|
||||
Some(OperationEffect::NonEgressing) => {
|
||||
return Err(IfcError::NonEgressingRequiresCall);
|
||||
@@ -218,11 +207,11 @@ impl IfcSession {
|
||||
None => return Err(IfcError::CapabilityDenied),
|
||||
}
|
||||
|
||||
self.flow.check_egress(&request.target.audience)?;
|
||||
self.flow.check_egress(destination)?;
|
||||
Ok(AuthorizedPublication {
|
||||
operation: request.operation,
|
||||
target: request.target,
|
||||
payload: request.payload,
|
||||
operation: operation.to_owned(),
|
||||
destination: destination.clone(),
|
||||
payload,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -230,7 +219,7 @@ impl IfcSession {
|
||||
/// Why an IFC session refused a broker action.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
|
||||
pub enum IfcError {
|
||||
/// The resource could be read by principals outside the session audience.
|
||||
/// Some session readers are not allowed to read the resource.
|
||||
#[error("resource audience is not safe for this execution domain")]
|
||||
ReadAudienceDenied,
|
||||
/// The resource belongs to retained state this domain may not reuse.
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::rc::Rc;
|
||||
|
||||
use nostr::Keys;
|
||||
use uuid::Uuid;
|
||||
@@ -96,7 +98,7 @@ fn owner_private_domain(community: CommunityId, channel: u128, epoch: &str) -> E
|
||||
}
|
||||
|
||||
fn deliver_to_agent(
|
||||
session: &mut IfcSession,
|
||||
session: &IfcSession,
|
||||
label: &ResourceLabel,
|
||||
value: &str,
|
||||
inbox: &mut Vec<String>,
|
||||
@@ -107,37 +109,78 @@ fn deliver_to_agent(
|
||||
}
|
||||
|
||||
fn execute_publication(
|
||||
authorization: AuthorizedPublication<String>,
|
||||
sink_log: &mut Vec<(String, String)>,
|
||||
authorization: AuthorizedPublication,
|
||||
sink_log: &mut Vec<(String, ConfidentialityLabel, Vec<u8>)>,
|
||||
) {
|
||||
let (operation, _checked_target, payload) = authorization.into_parts();
|
||||
sink_log.push((operation, payload));
|
||||
sink_log.push(authorization.into_parts());
|
||||
}
|
||||
|
||||
/// This is the intended broker-facing happy path. The broker checks a resource
|
||||
/// before delivery and gives its sink only the unforgeable result returned by
|
||||
/// `publish`, rather than executing an unchecked `PublicationRequest`.
|
||||
/// The broker checks a resource before delivery. The sink receives the checked
|
||||
/// operation, audience, and bytes together, including the concrete destination.
|
||||
#[test]
|
||||
fn broker_turn_uses_one_small_checked_surface() {
|
||||
let domain = restricted_domain(community(1), 10, "membership:v1", &[1, 2]);
|
||||
let resource = ResourceLabel::from_domain(&domain);
|
||||
let target = PublicationTarget::from_domain(&domain);
|
||||
let mut session = IfcSession::enter(domain);
|
||||
let destination = domain.audience().clone();
|
||||
let session = IfcSession::enter(domain);
|
||||
let mut inbox = Vec::new();
|
||||
let mut sink_log = Vec::new();
|
||||
let request = br#"{"channel":10,"text":"answer"}"#.to_vec();
|
||||
|
||||
deliver_to_agent(&mut session, &resource, "question", &mut inbox)
|
||||
.expect("broker may deliver the current conversation");
|
||||
session
|
||||
.call(READ)
|
||||
.expect("read operation cannot publish information");
|
||||
deliver_to_agent(&session, &resource, "question", &mut inbox)
|
||||
.expect("broker may deliver the current conversation");
|
||||
let authorization = session
|
||||
.publish(PublicationRequest::new(REPLY, target, "answer".to_owned()))
|
||||
.publish(REPLY, &destination, request.clone())
|
||||
.expect("reply may flow to the current audience");
|
||||
execute_publication(authorization, &mut sink_log);
|
||||
|
||||
assert_eq!(inbox, ["question"]);
|
||||
assert_eq!(sink_log, [(REPLY.to_owned(), "answer".to_owned())]);
|
||||
assert_eq!(sink_log, [(REPLY.to_owned(), destination, request)]);
|
||||
}
|
||||
|
||||
/// Shared request state can change while a publication waits for its sink.
|
||||
/// Those changes must not alter an existing authorization, even if the session
|
||||
/// has since received unknown input and can no longer authorize new requests.
|
||||
#[test]
|
||||
fn publication_keeps_the_checked_bytes_operation_and_destination() {
|
||||
let domain = public_domain(community(1), 10, "community:v1");
|
||||
let mut destination = domain.audience().clone();
|
||||
let checked_destination = destination.clone();
|
||||
let mut session = IfcSession::enter(domain);
|
||||
let mut operation = REPLY.to_owned();
|
||||
let original = br#"{"channel":10,"text":"public answer"}"#.to_vec();
|
||||
let shared_request = Rc::new(RefCell::new(original.clone()));
|
||||
let writer = Rc::clone(&shared_request);
|
||||
let authorization = session
|
||||
.publish(&operation, &destination, shared_request.borrow().clone())
|
||||
.expect("authorize an owned snapshot of the request");
|
||||
|
||||
session.mark_unknown_input();
|
||||
*writer.borrow_mut() = br#"{"channel":20,"text":"unknown secret"}"#.to_vec();
|
||||
operation.clear();
|
||||
destination = ConfidentialityLabel::public(community(2));
|
||||
|
||||
assert_eq!(
|
||||
session
|
||||
.publish(REPLY, &checked_destination, shared_request.borrow().clone())
|
||||
.err(),
|
||||
Some(IfcError::InformationFlow(
|
||||
ifc_core::EgressError::UnresolvedInput
|
||||
))
|
||||
);
|
||||
assert_ne!(destination, checked_destination);
|
||||
assert_ne!(operation, authorization.operation());
|
||||
assert_eq!(authorization.operation(), REPLY);
|
||||
assert_eq!(authorization.payload(), original);
|
||||
let mut sink_log = Vec::new();
|
||||
execute_publication(authorization, &mut sink_log);
|
||||
assert_eq!(
|
||||
sink_log,
|
||||
[(REPLY.to_owned(), checked_destination, original)]
|
||||
);
|
||||
}
|
||||
|
||||
/// A rejected read must never reach the agent and must not taint the session.
|
||||
@@ -148,17 +191,17 @@ fn broker_does_not_deliver_a_resource_with_a_narrower_audience() {
|
||||
let group = restricted_domain(community(1), 10, "membership:v1", &[1, 2]);
|
||||
let alice_only = restricted_domain(community(1), 20, "membership:v1", &[1]);
|
||||
let resource = ResourceLabel::from_domain(&alice_only);
|
||||
let target = PublicationTarget::from_domain(&group);
|
||||
let mut session = IfcSession::enter(group);
|
||||
let destination = group.audience().clone();
|
||||
let session = IfcSession::enter(group);
|
||||
let mut inbox = Vec::new();
|
||||
|
||||
assert_eq!(
|
||||
deliver_to_agent(&mut session, &resource, "alice secret", &mut inbox),
|
||||
deliver_to_agent(&session, &resource, "alice secret", &mut inbox),
|
||||
Err(IfcError::ReadAudienceDenied)
|
||||
);
|
||||
assert!(inbox.is_empty());
|
||||
assert!(session
|
||||
.publish(PublicationRequest::new(REPLY, target, "safe"))
|
||||
.publish(REPLY, &destination, b"safe".to_vec())
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
@@ -179,11 +222,11 @@ fn egressing_operation_cannot_use_the_call_path() {
|
||||
#[test]
|
||||
fn non_egressing_operation_cannot_use_the_publish_path() {
|
||||
let domain = public_domain(community(1), 10, "community:v1");
|
||||
let target = PublicationTarget::from_domain(&domain);
|
||||
let destination = domain.audience().clone();
|
||||
let session = IfcSession::enter(domain);
|
||||
|
||||
assert!(matches!(
|
||||
session.publish(PublicationRequest::new(READ, target, "payload")),
|
||||
session.publish(READ, &destination, b"payload".to_vec()),
|
||||
Err(IfcError::NonEgressingRequiresCall)
|
||||
));
|
||||
}
|
||||
@@ -194,12 +237,12 @@ fn non_egressing_operation_cannot_use_the_publish_path() {
|
||||
#[test]
|
||||
fn operation_absent_from_the_domain_is_denied() {
|
||||
let domain = public_domain(community(1), 10, "community:v1");
|
||||
let target = PublicationTarget::from_domain(&domain);
|
||||
let destination = domain.audience().clone();
|
||||
let session = IfcSession::enter(domain);
|
||||
|
||||
assert_eq!(session.call("email.send"), Err(IfcError::CapabilityDenied));
|
||||
assert!(matches!(
|
||||
session.publish(PublicationRequest::new("email.send", target, "payload")),
|
||||
session.publish("email.send", &destination, b"payload".to_vec()),
|
||||
Err(IfcError::CapabilityDenied)
|
||||
));
|
||||
}
|
||||
@@ -210,12 +253,12 @@ fn operation_absent_from_the_domain_is_denied() {
|
||||
fn private_session_cannot_publish_to_a_public_audience() {
|
||||
let private = restricted_domain(community(1), 10, "membership:v1", &[1, 2]);
|
||||
let public = public_domain(community(1), 20, "community:v1");
|
||||
let target = PublicationTarget::from_domain(&public);
|
||||
let destination = public.audience();
|
||||
let session = IfcSession::enter(private);
|
||||
|
||||
assert_eq!(
|
||||
session
|
||||
.publish(PublicationRequest::new(REPLY, target, "secret"))
|
||||
.publish(REPLY, destination, b"secret".to_vec())
|
||||
.err(),
|
||||
Some(IfcError::InformationFlow(
|
||||
ifc_core::EgressError::DestinationWidensReaders
|
||||
@@ -229,12 +272,12 @@ fn private_session_cannot_publish_to_a_public_audience() {
|
||||
#[test]
|
||||
fn public_session_may_publish_to_a_private_audience() {
|
||||
let public = public_domain(community(1), 10, "community:v1");
|
||||
let private = restricted_domain(community(1), 20, "membership:v1", &[1]);
|
||||
let target = PublicationTarget::from_domain(&private);
|
||||
// A destination needs an audience, not an agent execution domain.
|
||||
let destination = ConfidentialityLabel::restricted_to(community(1), principal(1));
|
||||
let session = IfcSession::enter(public);
|
||||
|
||||
assert!(session
|
||||
.publish(PublicationRequest::new(REPLY, target, "public data"))
|
||||
.publish(REPLY, &destination, b"public data".to_vec())
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
@@ -244,7 +287,7 @@ fn public_session_may_publish_to_a_private_audience() {
|
||||
fn unknown_input_permanently_blocks_publication() {
|
||||
let domain = public_domain(community(1), 10, "community:v1");
|
||||
let resource = ResourceLabel::from_domain(&domain);
|
||||
let target = PublicationTarget::from_domain(&domain);
|
||||
let destination = domain.audience().clone();
|
||||
let mut session = IfcSession::enter(domain);
|
||||
|
||||
session.mark_unknown_input();
|
||||
@@ -253,7 +296,7 @@ fn unknown_input_permanently_blocks_publication() {
|
||||
.expect("a later labeled read is still admissible");
|
||||
assert_eq!(
|
||||
session
|
||||
.publish(PublicationRequest::new(REPLY, target, "output"))
|
||||
.publish(REPLY, &destination, b"output".to_vec())
|
||||
.err(),
|
||||
Some(IfcError::InformationFlow(
|
||||
ifc_core::EgressError::UnresolvedInput
|
||||
@@ -268,7 +311,7 @@ fn same_conversation_rejects_a_stale_membership_epoch() {
|
||||
let old = restricted_domain(community(1), 10, "membership:v1", &[1, 2]);
|
||||
let current = restricted_domain(community(1), 10, "membership:v2", &[1, 2]);
|
||||
let resource = ResourceLabel::from_domain(&old);
|
||||
let mut session = IfcSession::enter(current);
|
||||
let session = IfcSession::enter(current);
|
||||
|
||||
assert_eq!(session.read(&resource), Err(IfcError::StaleResourceEpoch));
|
||||
}
|
||||
@@ -277,14 +320,27 @@ fn same_conversation_rejects_a_stale_membership_epoch() {
|
||||
/// so a restricted session may read it without comparing unrelated epochs.
|
||||
/// This catches the earlier design bug where public data inherited an epoch
|
||||
/// and was rejected by every private domain with a different epoch.
|
||||
/// Reading that data must not make the session's private state public.
|
||||
#[test]
|
||||
fn private_session_may_read_public_data_from_its_community() {
|
||||
let public = public_domain(community(1), 20, "community:v7");
|
||||
let private = restricted_domain(community(1), 10, "membership:v2", &[1, 2]);
|
||||
let resource = ResourceLabel::from_domain(&public);
|
||||
let mut session = IfcSession::enter(private);
|
||||
let private_audience = private.audience().clone();
|
||||
let session = IfcSession::enter(private);
|
||||
|
||||
assert_eq!(session.read(&resource), Ok(()));
|
||||
assert_eq!(
|
||||
session
|
||||
.publish(REPLY, public.audience(), b"private state".to_vec())
|
||||
.err(),
|
||||
Some(IfcError::InformationFlow(
|
||||
ifc_core::EgressError::DestinationWidensReaders
|
||||
))
|
||||
);
|
||||
assert!(session
|
||||
.publish(REPLY, &private_audience, b"private state".to_vec())
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
/// Owner-private work may explicitly import conversation data when the owner
|
||||
@@ -295,7 +351,7 @@ fn owner_private_session_may_read_conversation_data_safe_for_its_owner() {
|
||||
let conversation = restricted_domain(community(1), 20, "membership:v7", &[1, 2]);
|
||||
let owner_private = owner_private_domain(community(1), 10, "membership:v2");
|
||||
let resource = ResourceLabel::from_domain(&conversation);
|
||||
let mut session = IfcSession::enter(owner_private);
|
||||
let session = IfcSession::enter(owner_private);
|
||||
|
||||
assert_eq!(session.read(&resource), Ok(()));
|
||||
}
|
||||
@@ -307,7 +363,7 @@ fn equal_audiences_do_not_merge_restricted_conversation_contexts() {
|
||||
let source = restricted_domain(community(1), 10, "membership:v1", &[1, 2]);
|
||||
let destination = restricted_domain(community(1), 20, "membership:v1", &[1, 2]);
|
||||
let resource = ResourceLabel::from_domain(&source);
|
||||
let mut session = IfcSession::enter(destination);
|
||||
let session = IfcSession::enter(destination);
|
||||
|
||||
assert_eq!(session.read(&resource), Err(IfcError::ReadContextDenied));
|
||||
}
|
||||
@@ -318,13 +374,16 @@ fn equal_audiences_do_not_merge_restricted_conversation_contexts() {
|
||||
fn publication_cannot_cross_communities() {
|
||||
let source = public_domain(community(1), 10, "community:v1");
|
||||
let destination = public_domain(community(2), 10, "community:v1");
|
||||
let target = PublicationTarget::from_domain(&destination);
|
||||
let session = IfcSession::enter(source);
|
||||
|
||||
assert!(matches!(
|
||||
session.publish(PublicationRequest::new(REPLY, target, "output")),
|
||||
Err(IfcError::InformationFlow(_))
|
||||
));
|
||||
assert_eq!(
|
||||
session
|
||||
.publish(REPLY, destination.audience(), b"output".to_vec())
|
||||
.err(),
|
||||
Some(IfcError::InformationFlow(
|
||||
ifc_core::EgressError::DestinationUniverseMismatch
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/// This models the broker's retained-session pool. Public channels share one
|
||||
|
||||
Reference in New Issue
Block a user