mirror of
https://github.com/block/buzz.git
synced 2026-11-04 00:12:02 +00:00
Compare IFC domains using current policy state
Compare complete domain structures and derive restricted audiences from normalized membership. Remove event-ID epochs from execution domains and resource labels while preserving retained session restrictions. Signed-off-by: Jordan Mecom <jm@squareup.com>
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -1274,11 +1274,9 @@ name = "buzz-ifc"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"buzz-core",
|
||||
"hex",
|
||||
"ifc-core",
|
||||
"nostr 0.44.7",
|
||||
"serde",
|
||||
"sha2 0.11.0",
|
||||
"thiserror 2.0.18",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -9,10 +9,8 @@ description = "Buzz execution domains and broker-side information-flow checks"
|
||||
|
||||
[dependencies]
|
||||
buzz-core = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
ifc-core = { workspace = true }
|
||||
nostr = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use serde::Serialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::label::{CommunityId, ConfidentialityLabel, Principal, ReaderSet};
|
||||
@@ -13,9 +12,9 @@ use crate::label::{CommunityId, ConfidentialityLabel, Principal, ReaderSet};
|
||||
/// its owner has a separate owner-private context.
|
||||
///
|
||||
/// A matching context is not enough to reuse state. The broker must compare the
|
||||
/// full [`DomainKey`], so a change in agent, owner, audience, membership version,
|
||||
/// or capabilities also prevents reuse.
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
/// full [`ExecutionDomain`], so a change in agent, owner, audience, or
|
||||
/// capabilities also prevents reuse.
|
||||
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub(crate) enum DomainContext {
|
||||
/// Shared state for public conversations in one community.
|
||||
CommunityPublic(CommunityId),
|
||||
@@ -64,28 +63,6 @@ impl DomainContext {
|
||||
Self::OwnerPrivate { .. } => self == source,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn stable_hash(&self, hasher: &mut Sha256) {
|
||||
match self {
|
||||
Self::CommunityPublic(community) => {
|
||||
hash_field(hasher, b"community-public");
|
||||
hash_field(hasher, community.as_uuid().as_bytes());
|
||||
}
|
||||
Self::Conversation {
|
||||
community,
|
||||
channel_id,
|
||||
} => {
|
||||
hash_field(hasher, b"conversation");
|
||||
hash_field(hasher, community.as_uuid().as_bytes());
|
||||
hash_field(hasher, channel_id.as_bytes());
|
||||
}
|
||||
Self::OwnerPrivate { community, owner } => {
|
||||
hash_field(hasher, b"owner-private");
|
||||
hash_field(hasher, community.as_uuid().as_bytes());
|
||||
hash_field(hasher, &owner.to_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether an operation can send information out of the agent's environment.
|
||||
@@ -94,7 +71,7 @@ impl DomainContext {
|
||||
/// For a publication, permission to call the operation is not enough: the broker
|
||||
/// must use [`crate::IfcSession::publish`] to check whether the information may
|
||||
/// flow to the destination's readers.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum OperationEffect {
|
||||
/// Does not expose information outside the agent's environment.
|
||||
@@ -110,13 +87,6 @@ impl OperationEffect {
|
||||
(Self::Publication, _) | (_, Self::Publication) => Self::Publication,
|
||||
}
|
||||
}
|
||||
|
||||
fn stable_hash(self, hasher: &mut Sha256) {
|
||||
match self {
|
||||
Self::NonEgressing => hash_field(hasher, b"non-egressing"),
|
||||
Self::Publication => hash_field(hasher, b"publication"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Operations a policy allows, with each operation's publication behavior.
|
||||
@@ -129,7 +99,7 @@ impl OperationEffect {
|
||||
/// let capabilities = buzz_ifc::CapabilitySet::default();
|
||||
/// let _ = capabilities.contains("buzz.post");
|
||||
/// ```
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
|
||||
pub struct CapabilitySet(BTreeMap<String, OperationEffect>);
|
||||
|
||||
impl CapabilitySet {
|
||||
@@ -177,14 +147,6 @@ impl CapabilitySet {
|
||||
pub(crate) fn effect(&self, operation: &str) -> Option<OperationEffect> {
|
||||
self.0.get(operation).copied()
|
||||
}
|
||||
|
||||
fn stable_hash(&self, hasher: &mut Sha256) {
|
||||
hash_field(hasher, &(self.0.len() as u64).to_be_bytes());
|
||||
for (name, effect) in &self.0 {
|
||||
hash_field(hasher, name.as_bytes());
|
||||
effect.stable_hash(hasher);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Limits on the operations available to an agent.
|
||||
@@ -207,22 +169,6 @@ impl CapabilityPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
/// Version of the membership or access policy used to derive a domain.
|
||||
///
|
||||
/// The broker must change this value when the relevant membership or policy
|
||||
/// changes. It becomes part of the domain key, preventing the broker from
|
||||
/// selecting old state with the new key. This crate does not check freshness.
|
||||
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct MembershipEpoch(String);
|
||||
|
||||
impl MembershipEpoch {
|
||||
/// Use a broker-verified version, such as a membership event ID.
|
||||
pub fn new(value: impl Into<String>) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Conversation type read from metadata the broker has verified.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ConversationKind {
|
||||
@@ -248,8 +194,6 @@ pub struct DomainFacts {
|
||||
pub channel_id: Uuid,
|
||||
/// Conversation type from verified metadata.
|
||||
pub kind: ConversationKind,
|
||||
/// Current membership or community policy version supplied by the relay.
|
||||
pub epoch: MembershipEpoch,
|
||||
/// Complete verified member list, including the agent. Ignored for public
|
||||
/// channels, whose audience is the whole community.
|
||||
pub members: BTreeSet<Principal>,
|
||||
@@ -297,8 +241,8 @@ pub enum DerivationError {
|
||||
/// and conversation policies, even if the owner made the request.
|
||||
///
|
||||
/// The broker must supply verified [`DomainFacts`] and use the resulting
|
||||
/// [`DomainKey`] to select saved state. This function does not load or clear
|
||||
/// sessions itself.
|
||||
/// [`ExecutionDomain`] to select saved state. This function does not load or
|
||||
/// clear sessions itself.
|
||||
pub fn derive_execution_domain(
|
||||
facts: DomainFacts,
|
||||
policy: &CapabilityPolicy,
|
||||
@@ -352,7 +296,6 @@ pub fn derive_execution_domain(
|
||||
facts.owner,
|
||||
audience,
|
||||
context,
|
||||
facts.epoch,
|
||||
capabilities,
|
||||
)
|
||||
.map_err(|_| DerivationError::InvalidDomain)
|
||||
@@ -380,38 +323,31 @@ fn effective_capabilities(
|
||||
CapabilitySet::effective(&policy.bot, requester, domain)
|
||||
}
|
||||
|
||||
/// Key the broker uses to select an agent's saved state.
|
||||
///
|
||||
/// Compare the full key when deciding whether to reuse a session. Matching only
|
||||
/// a channel or member list would miss changes in owner, policy, or capabilities.
|
||||
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct DomainKey(String);
|
||||
|
||||
impl DomainKey {
|
||||
/// Return the key as a hexadecimal string for storage or lookup.
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// The identity, readers, context, and policy for an agent invocation.
|
||||
///
|
||||
/// The audience says who may receive information. The context says which
|
||||
/// conversations may share saved state. The epoch records the membership or
|
||||
/// policy version, and the capabilities list the allowed operations. Agent and
|
||||
/// owner identities keep different agents and ownership relationships separate.
|
||||
/// Conceptually, `D = (community, agent, owner, audience, context, capabilities)`.
|
||||
/// The community is encoded in both the audience and context; the constructor
|
||||
/// requires them to agree. The audience says who may receive information, the
|
||||
/// context says which conversations may share saved state, and capabilities
|
||||
/// list the allowed operations. Agent and owner identities keep different
|
||||
/// agents and ownership relationships separate.
|
||||
///
|
||||
/// This extends the domain model in
|
||||
/// Restricted audiences contain the verified member set minus the executing
|
||||
/// agent. Public audiences represent everyone in the community without listing
|
||||
/// members. Event IDs, timestamps, names, and topics do not participate in
|
||||
/// equality. Compare the complete structure before reusing retained state.
|
||||
///
|
||||
/// This represents current policy state. Unlike the temporal epochs in
|
||||
/// [Appendix B of the design paper](../../../docs/practical-information-flow-for-buzz-agents.md#appendix-b-formal-execution-domains)
|
||||
/// by including the owner and capabilities in the key as well.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
/// it does not distinguish two occurrences of the same policy separated by a
|
||||
/// membership change. The broker must verify current access separately and
|
||||
/// preserve session restrictions when reusing retained state.
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct ExecutionDomain {
|
||||
agent: Principal,
|
||||
owner: Option<Principal>,
|
||||
pub(crate) audience: ConfidentialityLabel,
|
||||
pub(crate) context: DomainContext,
|
||||
pub(crate) epoch: MembershipEpoch,
|
||||
pub(crate) capabilities: CapabilitySet,
|
||||
}
|
||||
|
||||
@@ -422,13 +358,12 @@ impl ExecutionDomain {
|
||||
owner: Option<Principal>,
|
||||
audience: ConfidentialityLabel,
|
||||
context: DomainContext,
|
||||
epoch: MembershipEpoch,
|
||||
capabilities: CapabilitySet,
|
||||
) -> Result<Self, DomainError> {
|
||||
if *audience.universe() != context.community() {
|
||||
return Err(DomainError::ContextCommunityMismatch);
|
||||
}
|
||||
if !audience_context_shape_matches(&audience, &context) {
|
||||
if !audience_matches_context(&audience, &context) {
|
||||
return Err(DomainError::AudienceContextMismatch);
|
||||
}
|
||||
Ok(Self {
|
||||
@@ -436,7 +371,6 @@ impl ExecutionDomain {
|
||||
owner,
|
||||
audience,
|
||||
context,
|
||||
epoch,
|
||||
capabilities,
|
||||
})
|
||||
}
|
||||
@@ -445,54 +379,9 @@ impl ExecutionDomain {
|
||||
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
|
||||
/// so it cannot reuse a session that has seen data under a different policy.
|
||||
pub fn key(&self) -> DomainKey {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(b"buzz-ifc-domain-v6");
|
||||
hash_field(&mut hasher, &self.agent.to_bytes());
|
||||
match &self.owner {
|
||||
Some(owner) => {
|
||||
hash_field(&mut hasher, b"owner");
|
||||
hash_field(&mut hasher, &owner.to_bytes());
|
||||
}
|
||||
None => hash_field(&mut hasher, b"no-owner"),
|
||||
}
|
||||
hash_field(&mut hasher, self.audience.universe().as_uuid().as_bytes());
|
||||
hash_reader_set(self.audience.reader_set(), &mut hasher);
|
||||
self.context.stable_hash(&mut hasher);
|
||||
hash_field(&mut hasher, self.epoch.0.as_bytes());
|
||||
self.capabilities.stable_hash(&mut hasher);
|
||||
DomainKey(hex::encode(hasher.finalize()))
|
||||
}
|
||||
}
|
||||
|
||||
fn hash_reader_set(readers: &ReaderSet, hasher: &mut Sha256) {
|
||||
match readers {
|
||||
ReaderSet::Everyone => hash_field(hasher, b"everyone"),
|
||||
ReaderSet::Only(readers) => {
|
||||
hash_field(hasher, b"only");
|
||||
hash_field(hasher, &(readers.len() as u64).to_be_bytes());
|
||||
for reader in readers {
|
||||
hash_field(hasher, &reader.to_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn hash_field(hasher: &mut Sha256, value: &[u8]) {
|
||||
let length = u64::try_from(value.len()).unwrap_or(u64::MAX);
|
||||
hasher.update(length.to_be_bytes());
|
||||
hasher.update(value);
|
||||
}
|
||||
|
||||
fn audience_context_shape_matches(
|
||||
audience: &ConfidentialityLabel,
|
||||
context: &DomainContext,
|
||||
) -> bool {
|
||||
fn audience_matches_context(audience: &ConfidentialityLabel, context: &DomainContext) -> bool {
|
||||
match (audience.reader_set(), context) {
|
||||
(ReaderSet::Everyone, DomainContext::CommunityPublic(_)) => true,
|
||||
(ReaderSet::Only(readers), DomainContext::Conversation { .. }) => !readers.is_empty(),
|
||||
|
||||
@@ -6,8 +6,8 @@ use serde::Serialize;
|
||||
/// A person, agent, or relay identified by a valid Nostr public key.
|
||||
///
|
||||
/// The key is validated and stored in binary form. Hexadecimal case does not
|
||||
/// affect equality or domain keys.
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
/// affect equality of principals or execution domains.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct Principal(XOnlyPublicKey);
|
||||
|
||||
@@ -35,10 +35,6 @@ impl Principal {
|
||||
pub fn to_hex(&self) -> String {
|
||||
self.0.to_string()
|
||||
}
|
||||
|
||||
pub(crate) fn to_bytes(self) -> [u8; 32] {
|
||||
self.0.serialize()
|
||||
}
|
||||
}
|
||||
|
||||
/// Invalid Nostr public-key input.
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
//!
|
||||
//! An [`ExecutionDomain`] records which agent is running, who may receive its
|
||||
//! output, which conversations may share its saved state, and which operations
|
||||
//! it may use. Its [`DomainKey`] includes all of those decisions, plus the owner
|
||||
//! and membership version, so a change produces a different key.
|
||||
//! it may use. Structural equality compares all of those decisions, plus the
|
||||
//! owner and community. Membership determines restricted audiences; reissuing
|
||||
//! the same policy in a different event does not change the domain.
|
||||
//!
|
||||
//! The broker must verify events and membership before supplying [`DomainFacts`].
|
||||
//! It uses the resulting key to select both the agent's saved state and its
|
||||
//! It uses the complete domain 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.
|
||||
//!
|
||||
@@ -23,7 +24,7 @@ mod session;
|
||||
|
||||
pub use domain::{
|
||||
derive_execution_domain, CapabilityPolicy, CapabilitySet, ConversationKind, DerivationError,
|
||||
DomainFacts, DomainKey, ExecutionDomain, MembershipEpoch, OperationEffect,
|
||||
DomainFacts, ExecutionDomain, OperationEffect,
|
||||
};
|
||||
pub use label::{CommunityId, ConfidentialityLabel, LabelError, Principal, PrincipalError};
|
||||
pub use session::{AuthorizedPublication, IfcError, IfcSession, ResourceLabel};
|
||||
|
||||
@@ -1,32 +1,27 @@
|
||||
use ifc_core::{EgressError, FlowState};
|
||||
|
||||
use crate::domain::{DomainContext, DomainKey, ExecutionDomain, MembershipEpoch, OperationEffect};
|
||||
use crate::domain::{DomainContext, ExecutionDomain, OperationEffect};
|
||||
use crate::label::{CommunityId, ConfidentialityLabel, Principal};
|
||||
|
||||
/// The security metadata the broker checks before exposing a resource to an
|
||||
/// agent session.
|
||||
///
|
||||
/// Constructing this from the domain where the resource originated keeps its
|
||||
/// audience, retained-state context, and membership epoch together. Public
|
||||
/// community data does not carry an epoch because any context in that
|
||||
/// community may read it. Restricted data retains the epoch under which its
|
||||
/// source domain was authorized.
|
||||
/// audience and retained-state context together. These describe which readers
|
||||
/// and contexts may receive the resource; the broker checks current access
|
||||
/// separately.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ResourceLabel {
|
||||
audience: ConfidentialityLabel,
|
||||
context: DomainContext,
|
||||
epoch: Option<MembershipEpoch>,
|
||||
}
|
||||
|
||||
impl ResourceLabel {
|
||||
/// Label a resource with the security metadata of its source domain.
|
||||
pub fn from_domain(domain: &ExecutionDomain) -> Self {
|
||||
let epoch = (!matches!(domain.context, DomainContext::CommunityPublic(_)))
|
||||
.then(|| domain.epoch.clone());
|
||||
Self {
|
||||
audience: domain.audience.clone(),
|
||||
context: domain.context.clone(),
|
||||
epoch,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,23 +85,23 @@ impl AuthorizedPublication {
|
||||
/// [Appendix G of the design paper](../../../docs/practical-information-flow-for-buzz-agents.md#appendix-g-security-labels-as-a-lattice).
|
||||
///
|
||||
/// 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
|
||||
/// files, or other state. The same domain 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,
|
||||
/// # DomainKey, IfcError, IfcSession, ResourceLabel};
|
||||
/// # IfcError, IfcSession, ResourceLabel};
|
||||
/// # fn broker_sink(_: AuthorizedPublication) {}
|
||||
/// # fn run_turn(
|
||||
/// # sessions: &mut HashMap<DomainKey, IfcSession>,
|
||||
/// # sessions: &mut HashMap<ExecutionDomain, IfcSession>,
|
||||
/// # domain: ExecutionDomain,
|
||||
/// # resource: &ResourceLabel,
|
||||
/// # destination: &ConfidentialityLabel,
|
||||
/// # request_bytes: Vec<u8>,
|
||||
/// # ) -> Result<(), IfcError> {
|
||||
/// let session = sessions
|
||||
/// .entry(domain.key())
|
||||
/// .entry(domain.clone())
|
||||
/// .or_insert_with(|| IfcSession::enter(domain));
|
||||
/// session.call("buzz.read.current")?;
|
||||
/// session.read(resource)?;
|
||||
@@ -137,9 +132,9 @@ impl IfcSession {
|
||||
Self { domain, flow }
|
||||
}
|
||||
|
||||
/// Return the key the broker uses to route later turns to this session.
|
||||
pub fn domain_key(&self) -> DomainKey {
|
||||
self.domain.key()
|
||||
/// Return the complete domain the broker compares before reusing this session.
|
||||
pub fn domain(&self) -> &ExecutionDomain {
|
||||
&self.domain
|
||||
}
|
||||
|
||||
/// Check a labeled resource before exposing it to the agent.
|
||||
@@ -148,7 +143,7 @@ impl IfcSession {
|
||||
/// 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.
|
||||
/// audience and context 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);
|
||||
@@ -156,15 +151,6 @@ impl IfcSession {
|
||||
if !self.domain.context.permits(&resource.context) {
|
||||
return Err(IfcError::ReadContextDenied);
|
||||
}
|
||||
if resource.context == self.domain.context
|
||||
&& resource
|
||||
.epoch
|
||||
.as_ref()
|
||||
.is_some_and(|epoch| epoch != &self.domain.epoch)
|
||||
{
|
||||
return Err(IfcError::StaleResourceEpoch);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -236,9 +222,6 @@ pub enum IfcError {
|
||||
/// The resource belongs to retained state this domain may not reuse.
|
||||
#[error("resource belongs to a different retained-state context")]
|
||||
ReadContextDenied,
|
||||
/// Restricted state was created under a different membership epoch.
|
||||
#[error("resource membership epoch does not match the execution domain")]
|
||||
StaleResourceEpoch,
|
||||
/// The execution domain does not admit the requested operation.
|
||||
#[error("operation is not admitted by this execution domain")]
|
||||
CapabilityDenied,
|
||||
|
||||
@@ -29,13 +29,12 @@ fn policy() -> CapabilityPolicy {
|
||||
CapabilityPolicy::new(operations(), operations())
|
||||
}
|
||||
|
||||
fn public_domain(community: CommunityId, channel: u128, epoch: &str) -> ExecutionDomain {
|
||||
fn public_domain(community: CommunityId, channel: u128) -> ExecutionDomain {
|
||||
derive_execution_domain(
|
||||
DomainFacts {
|
||||
community,
|
||||
channel_id: Uuid::from_u128(channel),
|
||||
kind: ConversationKind::Public,
|
||||
epoch: MembershipEpoch::new(epoch),
|
||||
members: BTreeSet::new(),
|
||||
executing_agent: principal(9),
|
||||
requesters: BTreeSet::from([principal(1)]),
|
||||
@@ -47,12 +46,7 @@ fn public_domain(community: CommunityId, channel: u128, epoch: &str) -> Executio
|
||||
.expect("valid public domain")
|
||||
}
|
||||
|
||||
fn restricted_domain(
|
||||
community: CommunityId,
|
||||
channel: u128,
|
||||
epoch: &str,
|
||||
readers: &[u8],
|
||||
) -> ExecutionDomain {
|
||||
fn restricted_domain(community: CommunityId, channel: u128, readers: &[u8]) -> ExecutionDomain {
|
||||
let agent = principal(9);
|
||||
let members = readers
|
||||
.iter()
|
||||
@@ -65,7 +59,6 @@ fn restricted_domain(
|
||||
community,
|
||||
channel_id: Uuid::from_u128(channel),
|
||||
kind: ConversationKind::Restricted,
|
||||
epoch: MembershipEpoch::new(epoch),
|
||||
members,
|
||||
executing_agent: agent,
|
||||
requesters: BTreeSet::from([principal(readers[0])]),
|
||||
@@ -77,7 +70,7 @@ fn restricted_domain(
|
||||
.expect("valid restricted domain")
|
||||
}
|
||||
|
||||
fn owner_private_domain(community: CommunityId, channel: u128, epoch: &str) -> ExecutionDomain {
|
||||
fn owner_private_domain(community: CommunityId, channel: u128) -> ExecutionDomain {
|
||||
let owner = principal(1);
|
||||
let agent = principal(9);
|
||||
derive_execution_domain(
|
||||
@@ -85,7 +78,6 @@ fn owner_private_domain(community: CommunityId, channel: u128, epoch: &str) -> E
|
||||
community,
|
||||
channel_id: Uuid::from_u128(channel),
|
||||
kind: ConversationKind::DirectMessage,
|
||||
epoch: MembershipEpoch::new(epoch),
|
||||
members: BTreeSet::from([agent, owner]),
|
||||
executing_agent: agent,
|
||||
requesters: BTreeSet::from([owner]),
|
||||
@@ -119,7 +111,7 @@ fn execute_publication(
|
||||
/// 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 domain = restricted_domain(community(1), 10, &[1, 2]);
|
||||
let resource = ResourceLabel::from_domain(&domain);
|
||||
let destination = domain.audience().clone();
|
||||
let session = IfcSession::enter(domain);
|
||||
@@ -146,7 +138,7 @@ fn broker_turn_uses_one_small_checked_surface() {
|
||||
/// 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 domain = public_domain(community(1), 10);
|
||||
let mut destination = domain.audience().clone();
|
||||
let checked_destination = destination.clone();
|
||||
let mut session = IfcSession::enter(domain);
|
||||
@@ -188,8 +180,8 @@ fn publication_keeps_the_checked_bytes_operation_and_destination() {
|
||||
/// appended to the simulated agent inbox.
|
||||
#[test]
|
||||
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 group = restricted_domain(community(1), 10, &[1, 2]);
|
||||
let alice_only = restricted_domain(community(1), 20, &[1]);
|
||||
let resource = ResourceLabel::from_domain(&alice_only);
|
||||
let destination = group.audience().clone();
|
||||
let session = IfcSession::enter(group);
|
||||
@@ -209,7 +201,7 @@ fn broker_does_not_deliver_a_resource_with_a_narrower_audience() {
|
||||
/// destination-flow check. Misclassifying this path would bypass IFC entirely.
|
||||
#[test]
|
||||
fn egressing_operation_cannot_use_the_call_path() {
|
||||
let session = IfcSession::enter(public_domain(community(1), 10, "community:v1"));
|
||||
let session = IfcSession::enter(public_domain(community(1), 10));
|
||||
|
||||
assert_eq!(
|
||||
session.call(REPLY),
|
||||
@@ -221,7 +213,7 @@ fn egressing_operation_cannot_use_the_call_path() {
|
||||
/// broker API and policy cannot silently change how a call is executed.
|
||||
#[test]
|
||||
fn non_egressing_operation_cannot_use_the_publish_path() {
|
||||
let domain = public_domain(community(1), 10, "community:v1");
|
||||
let domain = public_domain(community(1), 10);
|
||||
let destination = domain.audience().clone();
|
||||
let session = IfcSession::enter(domain);
|
||||
|
||||
@@ -236,7 +228,7 @@ fn non_egressing_operation_cannot_use_the_publish_path() {
|
||||
/// how to execute it.
|
||||
#[test]
|
||||
fn operation_absent_from_the_domain_is_denied() {
|
||||
let domain = public_domain(community(1), 10, "community:v1");
|
||||
let domain = public_domain(community(1), 10);
|
||||
let destination = domain.audience().clone();
|
||||
let session = IfcSession::enter(domain);
|
||||
|
||||
@@ -251,8 +243,8 @@ fn operation_absent_from_the_domain_is_denied() {
|
||||
/// the central no-write-down confidentiality invariant at the checked sink.
|
||||
#[test]
|
||||
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 private = restricted_domain(community(1), 10, &[1, 2]);
|
||||
let public = public_domain(community(1), 20);
|
||||
let destination = public.audience();
|
||||
let session = IfcSession::enter(private);
|
||||
|
||||
@@ -271,7 +263,7 @@ fn private_session_cannot_publish_to_a_public_audience() {
|
||||
/// narrowing while potentially allowing the unsafe direction above.
|
||||
#[test]
|
||||
fn public_session_may_publish_to_a_private_audience() {
|
||||
let public = public_domain(community(1), 10, "community:v1");
|
||||
let public = public_domain(community(1), 10);
|
||||
// A destination needs an audience, not an agent execution domain.
|
||||
let destination = ConfidentialityLabel::restricted_to(community(1), principal(1));
|
||||
let session = IfcSession::enter(public);
|
||||
@@ -285,7 +277,7 @@ fn public_session_may_publish_to_a_private_audience() {
|
||||
/// must not reset the flag and accidentally launder unknown data.
|
||||
#[test]
|
||||
fn unknown_input_permanently_blocks_publication() {
|
||||
let domain = public_domain(community(1), 10, "community:v1");
|
||||
let domain = public_domain(community(1), 10);
|
||||
let resource = ResourceLabel::from_domain(&domain);
|
||||
let destination = domain.audience().clone();
|
||||
let mut session = IfcSession::enter(domain);
|
||||
@@ -309,18 +301,18 @@ fn unknown_input_permanently_blocks_publication() {
|
||||
/// 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 first_turn = public_domain(community(1), 10);
|
||||
let next_turn = public_domain(community(1), 20);
|
||||
let resource = ResourceLabel::from_domain(&next_turn);
|
||||
let destination = next_turn.audience().clone();
|
||||
let mut pool = HashMap::new();
|
||||
|
||||
pool.entry(first_turn.key())
|
||||
pool.entry(first_turn.clone())
|
||||
.or_insert_with(|| IfcSession::enter(first_turn))
|
||||
.mark_unknown_input();
|
||||
|
||||
let session = pool
|
||||
.entry(next_turn.key())
|
||||
.entry(next_turn.clone())
|
||||
.or_insert_with(|| IfcSession::enter(next_turn));
|
||||
session
|
||||
.call(READ)
|
||||
@@ -337,27 +329,25 @@ fn unknown_input_survives_reusing_a_session_in_another_public_channel() {
|
||||
assert_eq!(pool.len(), 1);
|
||||
}
|
||||
|
||||
/// Retained restricted state from an older membership snapshot cannot enter a
|
||||
/// newly routed session for the same conversation.
|
||||
/// Adding a member does not make older restricted state readable by them.
|
||||
/// Its original audience must cover everyone in the new session's audience.
|
||||
#[test]
|
||||
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]);
|
||||
fn same_conversation_rejects_a_resource_with_a_narrower_audience() {
|
||||
let old = restricted_domain(community(1), 10, &[1, 2]);
|
||||
let current = restricted_domain(community(1), 10, &[1, 2, 3]);
|
||||
let resource = ResourceLabel::from_domain(&old);
|
||||
let session = IfcSession::enter(current);
|
||||
|
||||
assert_eq!(session.read(&resource), Err(IfcError::StaleResourceEpoch));
|
||||
assert_eq!(session.read(&resource), Err(IfcError::ReadAudienceDenied));
|
||||
}
|
||||
|
||||
/// Public community data intentionally has no conversation 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.
|
||||
/// Public community data is readable by the restricted session's audience.
|
||||
/// Its public context may enter any retained-state context in that community.
|
||||
/// 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 public = public_domain(community(1), 20);
|
||||
let private = restricted_domain(community(1), 10, &[1, 2]);
|
||||
let resource = ResourceLabel::from_domain(&public);
|
||||
let private_audience = private.audience().clone();
|
||||
let session = IfcSession::enter(private);
|
||||
@@ -381,8 +371,8 @@ fn private_session_may_read_public_data_from_its_community() {
|
||||
/// owner's private state remains protected by the exact-context rule.
|
||||
#[test]
|
||||
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 conversation = restricted_domain(community(1), 20, &[1, 2]);
|
||||
let owner_private = owner_private_domain(community(1), 10);
|
||||
let resource = ResourceLabel::from_domain(&conversation);
|
||||
let session = IfcSession::enter(owner_private);
|
||||
|
||||
@@ -393,8 +383,8 @@ fn owner_private_session_may_read_conversation_data_safe_for_its_owner() {
|
||||
/// context. Otherwise one private channel could inject history into another.
|
||||
#[test]
|
||||
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 source = restricted_domain(community(1), 10, &[1, 2]);
|
||||
let destination = restricted_domain(community(1), 20, &[1, 2]);
|
||||
let resource = ResourceLabel::from_domain(&source);
|
||||
let session = IfcSession::enter(destination);
|
||||
|
||||
@@ -406,8 +396,8 @@ fn equal_audiences_do_not_merge_restricted_conversation_contexts() {
|
||||
/// 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]);
|
||||
let owner_private = owner_private_domain(community(1), 10);
|
||||
let conversation = restricted_domain(community(1), 20, &[1]);
|
||||
assert_eq!(owner_private.audience(), conversation.audience());
|
||||
let resource = ResourceLabel::from_domain(&owner_private);
|
||||
let session = IfcSession::enter(conversation);
|
||||
@@ -419,8 +409,8 @@ fn owner_private_state_cannot_enter_a_conversation_with_the_same_audience() {
|
||||
/// 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 source = public_domain(community(1), 10);
|
||||
let destination = public_domain(community(2), 10);
|
||||
let resource = ResourceLabel::from_domain(&source);
|
||||
let session = IfcSession::enter(destination);
|
||||
let mut inbox = Vec::new();
|
||||
@@ -436,8 +426,8 @@ fn public_resource_cannot_cross_communities() {
|
||||
/// same principals. A target in another community is never a valid IFC sink.
|
||||
#[test]
|
||||
fn publication_cannot_cross_communities() {
|
||||
let source = public_domain(community(1), 10, "community:v1");
|
||||
let destination = public_domain(community(2), 10, "community:v1");
|
||||
let source = public_domain(community(1), 10);
|
||||
let destination = public_domain(community(2), 10);
|
||||
let session = IfcSession::enter(source);
|
||||
|
||||
assert_eq!(
|
||||
@@ -451,33 +441,33 @@ fn publication_cannot_cross_communities() {
|
||||
}
|
||||
|
||||
/// This models the broker's retained-session pool. Public channels share one
|
||||
/// community domain, while restricted channel identity and membership epoch
|
||||
/// community domain, while restricted channel identity and audience
|
||||
/// each select different state.
|
||||
#[test]
|
||||
fn broker_routes_retained_sessions_by_complete_domain_key() {
|
||||
let public_a = public_domain(community(1), 10, "community:v1");
|
||||
let public_b = public_domain(community(1), 20, "community:v1");
|
||||
let restricted_a = restricted_domain(community(1), 10, "membership:v1", &[1, 2]);
|
||||
let restricted_b = restricted_domain(community(1), 20, "membership:v1", &[1, 2]);
|
||||
let restricted_new_epoch = restricted_domain(community(1), 10, "membership:v2", &[1, 2]);
|
||||
let public_key = public_a.key();
|
||||
fn broker_routes_retained_sessions_by_complete_domain_structure() {
|
||||
let public_a = public_domain(community(1), 10);
|
||||
let public_b = public_domain(community(1), 20);
|
||||
let restricted_a = restricted_domain(community(1), 10, &[1, 2]);
|
||||
let restricted_b = restricted_domain(community(1), 20, &[1, 2]);
|
||||
let restricted_new_audience = restricted_domain(community(1), 10, &[1, 2, 3]);
|
||||
let public_domain = public_a.clone();
|
||||
let domains = [
|
||||
public_a,
|
||||
public_b,
|
||||
restricted_a,
|
||||
restricted_b,
|
||||
restricted_new_epoch,
|
||||
restricted_new_audience,
|
||||
];
|
||||
let mut pool = HashMap::new();
|
||||
|
||||
for domain in domains {
|
||||
pool.entry(domain.key())
|
||||
pool.entry(domain.clone())
|
||||
.or_insert_with(|| IfcSession::enter(domain));
|
||||
}
|
||||
|
||||
assert_eq!(pool.len(), 4);
|
||||
assert_eq!(
|
||||
pool.get(&public_key).map(IfcSession::domain_key),
|
||||
Some(public_key)
|
||||
pool.get(&public_domain).map(IfcSession::domain),
|
||||
Some(&public_domain)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ fn conversation_capabilities() -> CapabilitySet {
|
||||
])
|
||||
}
|
||||
|
||||
fn conversation(values: &[u8], channel_id: Uuid, epoch: &str) -> ExecutionDomain {
|
||||
fn conversation(values: &[u8], channel_id: Uuid) -> ExecutionDomain {
|
||||
ExecutionDomain::new(
|
||||
principal(9),
|
||||
Some(principal(1)),
|
||||
@@ -48,7 +48,6 @@ fn conversation(values: &[u8], channel_id: Uuid, epoch: &str) -> ExecutionDomain
|
||||
community: community(),
|
||||
channel_id,
|
||||
},
|
||||
MembershipEpoch::new(epoch),
|
||||
conversation_capabilities(),
|
||||
)
|
||||
.expect("coherent conversation domain")
|
||||
@@ -128,7 +127,6 @@ fn derivation_grants_personal_capabilities_only_to_owner_private_work() {
|
||||
community: community(),
|
||||
channel_id: Uuid::from_u128(1),
|
||||
kind: ConversationKind::DirectMessage,
|
||||
epoch: MembershipEpoch::new("membership:v1"),
|
||||
members: BTreeSet::from([agent, owner]),
|
||||
executing_agent: agent,
|
||||
requesters: BTreeSet::from([owner]),
|
||||
@@ -156,7 +154,6 @@ fn derivation_grants_personal_capabilities_only_to_owner_private_work() {
|
||||
community: community(),
|
||||
channel_id: Uuid::from_u128(2),
|
||||
kind: ConversationKind::Public,
|
||||
epoch: MembershipEpoch::new("community:v1"),
|
||||
members: BTreeSet::new(),
|
||||
executing_agent: agent,
|
||||
requesters: BTreeSet::from([owner]),
|
||||
@@ -189,7 +186,6 @@ fn restricted_derivation_requires_verified_membership() {
|
||||
community: community(),
|
||||
channel_id: Uuid::from_u128(1),
|
||||
kind: ConversationKind::Restricted,
|
||||
epoch: MembershipEpoch::new("membership:v1"),
|
||||
members,
|
||||
executing_agent: agent,
|
||||
requesters,
|
||||
@@ -217,96 +213,96 @@ fn restricted_derivation_requires_verified_membership() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Change each of the six domain fields separately and require a different key.
|
||||
/// Omitting one from the hash could reuse history from a different agent, owner,
|
||||
/// audience, conversation, membership version, or capability set.
|
||||
/// Change each domain component separately and require a different domain.
|
||||
/// Omitting one from equality could reuse history from a different agent, owner,
|
||||
/// audience, conversation, community, or capability set.
|
||||
#[test]
|
||||
fn every_domain_component_changes_the_routing_key() {
|
||||
let base = conversation(&[1, 2], Uuid::from_u128(1), "v1");
|
||||
let build = |agent, owner, audience, channel_id, epoch, capabilities| {
|
||||
fn structural_domain_equality_compares_every_component() {
|
||||
let base = conversation(&[1, 2], Uuid::from_u128(1));
|
||||
let build = |agent, owner, audience: ConfidentialityLabel, channel_id, capabilities| {
|
||||
let community = *audience.universe();
|
||||
ExecutionDomain::new(
|
||||
agent,
|
||||
owner,
|
||||
audience,
|
||||
DomainContext::Conversation {
|
||||
community: community(),
|
||||
community,
|
||||
channel_id,
|
||||
},
|
||||
MembershipEpoch::new(epoch),
|
||||
capabilities,
|
||||
)
|
||||
.expect("coherent domain")
|
||||
};
|
||||
let ids = [
|
||||
base.key(),
|
||||
let domains = [
|
||||
base,
|
||||
build(
|
||||
principal(8),
|
||||
Some(principal(1)),
|
||||
label(&[1, 2]),
|
||||
Uuid::from_u128(1),
|
||||
"v1",
|
||||
conversation_capabilities(),
|
||||
)
|
||||
.key(),
|
||||
),
|
||||
build(
|
||||
principal(9),
|
||||
None,
|
||||
label(&[1, 2]),
|
||||
Uuid::from_u128(1),
|
||||
"v1",
|
||||
conversation_capabilities(),
|
||||
)
|
||||
.key(),
|
||||
),
|
||||
build(
|
||||
principal(9),
|
||||
Some(principal(1)),
|
||||
label(&[1, 3]),
|
||||
Uuid::from_u128(1),
|
||||
"v1",
|
||||
conversation_capabilities(),
|
||||
)
|
||||
.key(),
|
||||
),
|
||||
build(
|
||||
principal(9),
|
||||
Some(principal(1)),
|
||||
label(&[1, 2]),
|
||||
Uuid::from_u128(2),
|
||||
"v1",
|
||||
conversation_capabilities(),
|
||||
)
|
||||
.key(),
|
||||
),
|
||||
build(
|
||||
principal(9),
|
||||
Some(principal(1)),
|
||||
ConfidentialityLabel::restricted(other_community(), readers(&[1, 2]))
|
||||
.expect("non-empty readers"),
|
||||
Uuid::from_u128(1),
|
||||
conversation_capabilities(),
|
||||
),
|
||||
build(
|
||||
principal(9),
|
||||
Some(principal(1)),
|
||||
label(&[1, 2]),
|
||||
Uuid::from_u128(1),
|
||||
"v2",
|
||||
conversation_capabilities(),
|
||||
)
|
||||
.key(),
|
||||
build(
|
||||
principal(9),
|
||||
Some(principal(1)),
|
||||
label(&[1, 2]),
|
||||
Uuid::from_u128(1),
|
||||
"v1",
|
||||
non_egressing(["buzz.read.current"]),
|
||||
)
|
||||
.key(),
|
||||
),
|
||||
];
|
||||
|
||||
assert_eq!(ids.iter().collect::<BTreeSet<_>>().len(), ids.len());
|
||||
for (index, domain) in domains.iter().enumerate() {
|
||||
for previous in &domains[..index] {
|
||||
assert_ne!(domain, previous);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep the same key for the same domain across code changes. An accidental
|
||||
/// encoding change would break lookups of state saved under the old key.
|
||||
/// Reader and operation ordering, and duplicate entries, do not change a domain.
|
||||
/// Equivalent structures must also select the same entry in a retained-state pool.
|
||||
#[test]
|
||||
fn domain_key_has_a_canonical_golden_value() {
|
||||
let domain = conversation(&[1, 2], Uuid::from_u128(1), "membership:event-1");
|
||||
assert_eq!(
|
||||
domain.key().as_str(),
|
||||
"c83fb1c188109526fca87b73297eb00f9bf0b6545e36c6f0b8d3fb2771ce7962"
|
||||
);
|
||||
fn equivalent_domains_ignore_reader_and_capability_ordering() {
|
||||
let domain = conversation(&[1, 2], Uuid::from_u128(1));
|
||||
let mut reordered = conversation(&[2, 1, 2], Uuid::from_u128(1));
|
||||
reordered.capabilities = CapabilitySet::from_operations([
|
||||
("buzz.reply", OperationEffect::Publication),
|
||||
("buzz.post", OperationEffect::Publication),
|
||||
("buzz.read.current", OperationEffect::NonEgressing),
|
||||
("buzz.reply", OperationEffect::Publication),
|
||||
]);
|
||||
|
||||
assert_eq!(domain, reordered);
|
||||
let pool = std::collections::HashMap::from([(domain, "retained state")]);
|
||||
assert_eq!(pool.get(&reordered), Some(&"retained state"));
|
||||
}
|
||||
|
||||
/// A public audience cannot be paired with another community's context or with
|
||||
@@ -319,7 +315,6 @@ fn domains_reject_incoherent_audience_context_pairs() {
|
||||
Some(principal(1)),
|
||||
ConfidentialityLabel::public(community()),
|
||||
DomainContext::CommunityPublic(other_community()),
|
||||
MembershipEpoch::new("v1"),
|
||||
CapabilitySet::default(),
|
||||
),
|
||||
Err(DomainError::ContextCommunityMismatch)
|
||||
@@ -333,7 +328,6 @@ fn domains_reject_incoherent_audience_context_pairs() {
|
||||
community: community(),
|
||||
channel_id: Uuid::from_u128(2),
|
||||
},
|
||||
MembershipEpoch::new("v1"),
|
||||
CapabilitySet::default(),
|
||||
),
|
||||
Err(DomainError::AudienceContextMismatch)
|
||||
|
||||
@@ -40,7 +40,7 @@ use std::fmt::{Display, Formatter};
|
||||
///
|
||||
/// When a computation combines inputs, their reader sets are intersected so
|
||||
/// its output is restricted to principals allowed to read every input.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum ReaderSet<Principal> {
|
||||
/// Every principal in the universe may read the value.
|
||||
Everyone,
|
||||
@@ -103,7 +103,7 @@ impl<Principal: Ord> ReaderSet<Principal> {
|
||||
}
|
||||
|
||||
/// A reader-set confidentiality label inside one isolated universe.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct ConfidentialityLabel<Universe, Principal> {
|
||||
universe: Universe,
|
||||
readers: ReaderSet<Principal>,
|
||||
|
||||
Reference in New Issue
Block a user