use core::marker::PhantomData;
use std::time::SystemTime;
use smallvec::SmallVec;
use thiserror::Error;
use crate::authority::capability::CapabilitySet;
use crate::authority::moderation::InspectionNotificationQueueImpl;
use crate::audit::{
ChannelAuditSink, FallbackAuditSink, ModerationAuditSink, SubstrateAuditSink,
UserAuditSink,
};
use crate::identity::{CorrelationKey, ServiceIdentity, TraceId};
use crate::oracle::{AudienceOracle, BlockOracle, MuteOracle};
use crate::proto::Did;
use crate::sealed;
pub const MAX_CHAIN_DEPTH: usize = 8;
pub struct AuthContext<'a> {
requester: Requester,
trace_id: TraceId,
audit: AuditSinks<'a>,
oracles: OracleSet<'a>,
attribution_chain: AttributionChain,
capabilities: CapabilitySet,
_no_clone: PhantomData<*const ()>,
}
impl<'a> AuthContext<'a> {
#[must_use]
pub(crate) fn new_internal(
requester: Requester,
trace_id: TraceId,
audit: AuditSinks<'a>,
oracles: OracleSet<'a>,
attribution_chain: AttributionChain,
capabilities: CapabilitySet,
) -> Self {
AuthContext {
requester,
trace_id,
audit,
oracles,
attribution_chain,
capabilities,
_no_clone: PhantomData,
}
}
#[cfg(feature = "test-support")]
#[must_use]
pub fn new_for_test(
requester: Requester,
trace_id: TraceId,
audit: AuditSinks<'a>,
oracles: OracleSet<'a>,
attribution_chain: AttributionChain,
capabilities: CapabilitySet,
) -> Self {
Self::new_internal(
requester,
trace_id,
audit,
oracles,
attribution_chain,
capabilities,
)
}
#[must_use]
pub fn capabilities(&self) -> &CapabilitySet {
&self.capabilities
}
#[must_use]
pub fn requester(&self) -> &Requester {
&self.requester
}
#[must_use]
pub fn trace_id(&self) -> TraceId {
self.trace_id
}
#[must_use]
pub fn attribution_chain(&self) -> &AttributionChain {
&self.attribution_chain
}
#[must_use]
pub fn audit(&self) -> &AuditSinks<'a> {
&self.audit
}
#[must_use]
pub fn oracles(&self) -> &OracleSet<'a> {
&self.oracles
}
pub fn derive_for<N: Narrowing + 'static>(
&self,
narrowing: N,
) -> Result<AuthContext<'_>, DeriveError> {
let now = SystemTime::now();
let narrowing_any = &narrowing as &dyn std::any::Any;
let (new_requester, derivation_reason, narrowing_kind, new_capabilities) =
if narrowing_any.is::<ToAnonymous>() {
(
Requester::Anonymous,
DerivationReason::DropPrivilegeToAnonymous,
crate::audit::NarrowingKind::ToAnonymous,
CapabilitySet::empty(),
)
} else if let Some(narrow) = narrowing_any.downcast_ref::<NarrowCapabilities>() {
if !self.capabilities.is_superset_of(&narrow.drop) {
emit_derived_context(
self,
self.requester.clone(),
crate::audit::NarrowingKind::NarrowCapabilities,
crate::audit::DerivationOutcome::NarrowingExceedsAuthority,
now,
);
return Err(DeriveError::NarrowingExceedsAuthority);
}
(
self.requester.clone(),
DerivationReason::NarrowCapabilities {
dropped: narrow.drop.clone(),
},
crate::audit::NarrowingKind::NarrowCapabilities,
self.capabilities.without(&narrow.drop),
)
} else if let Some(svc_to_svc) =
narrowing_any.downcast_ref::<ServiceToService>()
{
let current_svc = match &self.requester {
Requester::Service(s) => s.clone(),
_ => {
let to = Requester::Service(svc_to_svc.target.clone());
emit_derived_context(
self,
to,
crate::audit::NarrowingKind::ServiceToService,
crate::audit::DerivationOutcome::IllegalNarrowing,
now,
);
return Err(DeriveError::IllegalNarrowing);
}
};
if ¤t_svc != svc_to_svc.trust_declaration.from_service() {
let to = Requester::Service(svc_to_svc.target.clone());
emit_derived_context(
self,
to,
crate::audit::NarrowingKind::ServiceToService,
crate::audit::DerivationOutcome::IllegalNarrowing,
now,
);
return Err(DeriveError::IllegalNarrowing);
}
if &svc_to_svc.target != svc_to_svc.trust_declaration.to_service() {
let to = Requester::Service(svc_to_svc.target.clone());
emit_derived_context(
self,
to,
crate::audit::NarrowingKind::ServiceToService,
crate::audit::DerivationOutcome::IllegalNarrowing,
now,
);
return Err(DeriveError::IllegalNarrowing);
}
if now < svc_to_svc.trust_declaration.issued_at()
|| now >= svc_to_svc.trust_declaration.expires_at()
{
let to = Requester::Service(svc_to_svc.target.clone());
emit_derived_context(
self,
to,
crate::audit::NarrowingKind::ServiceToService,
crate::audit::DerivationOutcome::UndeclaredServiceTrust,
now,
);
return Err(DeriveError::UndeclaredServiceTrust);
}
(
Requester::Service(svc_to_svc.target.clone()),
DerivationReason::ServiceToServiceDelegation {
trust_declaration_id: *svc_to_svc
.trust_declaration
.declaration_id(),
},
crate::audit::NarrowingKind::ServiceToService,
svc_to_svc.trust_declaration.capabilities().clone(),
)
} else {
unreachable!(
"Narrowing is sealed; only ToAnonymous / NarrowCapabilities / ServiceToService impl it"
)
};
let mut new_chain = self.attribution_chain.clone();
if let Err(e) = new_chain.try_push(AttributionEntry {
requester: self.requester.clone(),
derivation_reason,
derived_at: now,
key_id_used: None,
}) {
emit_derived_context(
self,
new_requester,
narrowing_kind,
crate::audit::DerivationOutcome::ChainTooDeep,
now,
);
return Err(e);
}
emit_derived_context(
self,
new_requester.clone(),
narrowing_kind,
crate::audit::DerivationOutcome::Success,
now,
);
Ok(AuthContext::new_internal(
new_requester,
self.trace_id,
self.audit,
self.oracles,
new_chain,
new_capabilities,
))
}
}
fn emit_derived_context(
ctx: &AuthContext<'_>,
to: Requester,
narrowing_kind: crate::audit::NarrowingKind,
outcome: crate::audit::DerivationOutcome,
at: SystemTime,
) {
let event = crate::audit::UserAuditEvent::DerivedContext {
trace_id: ctx.trace_id,
from: ctx.requester.clone(),
to,
narrowing_kind,
outcome,
at,
};
let _ = ctx.audit.user.record(event);
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Requester {
Did(Did),
Service(ServiceIdentity),
Anonymous,
}
impl Requester {
#[must_use]
pub fn kind(&self) -> RequesterKind {
match self {
Requester::Did(_) => RequesterKind::Did,
Requester::Service(_) => RequesterKind::Service,
Requester::Anonymous => RequesterKind::Anonymous,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RequesterKind {
Did,
Service,
Anonymous,
}
#[derive(Copy, Clone)]
#[non_exhaustive]
pub struct AuditSinks<'a> {
pub user: &'a dyn UserAuditSink,
pub channel: &'a dyn ChannelAuditSink,
pub substrate: &'a dyn SubstrateAuditSink,
pub moderation: &'a dyn ModerationAuditSink,
pub fallback: &'a dyn FallbackAuditSink,
pub inspection_queue: &'a dyn InspectionNotificationQueueImpl,
pub correlation_key: &'a CorrelationKey,
}
impl<'a> AuditSinks<'a> {
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn new(
user: &'a dyn UserAuditSink,
channel: &'a dyn ChannelAuditSink,
substrate: &'a dyn SubstrateAuditSink,
moderation: &'a dyn ModerationAuditSink,
fallback: &'a dyn FallbackAuditSink,
inspection_queue: &'a dyn InspectionNotificationQueueImpl,
correlation_key: &'a CorrelationKey,
) -> Self {
AuditSinks {
user,
channel,
substrate,
moderation,
fallback,
inspection_queue,
correlation_key,
}
}
}
#[derive(Copy, Clone)]
#[non_exhaustive]
pub struct OracleSet<'a> {
pub block: &'a dyn BlockOracle,
pub audience: &'a dyn AudienceOracle,
pub mute: &'a dyn MuteOracle,
}
impl<'a> OracleSet<'a> {
#[must_use]
pub fn new(
block: &'a dyn BlockOracle,
audience: &'a dyn AudienceOracle,
mute: &'a dyn MuteOracle,
) -> Self {
OracleSet { block, audience, mute }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct AttributionChain {
entries: SmallVec<[AttributionEntry; MAX_CHAIN_DEPTH]>,
}
impl AttributionChain {
#[must_use]
pub fn empty() -> Self {
AttributionChain::default()
}
#[must_use]
pub fn entries(&self) -> &[AttributionEntry] {
&self.entries
}
pub(crate) fn try_push(&mut self, entry: AttributionEntry) -> Result<(), DeriveError> {
if self.entries.len() >= MAX_CHAIN_DEPTH {
return Err(DeriveError::ChainTooDeep);
}
self.entries.push(entry);
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct AttributionEntry {
pub requester: Requester,
pub derivation_reason: DerivationReason,
pub derived_at: SystemTime,
pub key_id_used: Option<crate::identity::KeyId>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DerivationReason {
DropPrivilegeToAnonymous,
NarrowCapabilities {
dropped: CapabilitySet,
},
ServiceToServiceDelegation {
trust_declaration_id: TrustDeclarationId,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TrustDeclarationId([u8; 16]);
impl TrustDeclarationId {
#[must_use]
pub const fn from_bytes(bytes: [u8; 16]) -> Self {
TrustDeclarationId(bytes)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 16] {
&self.0
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum DeriveError {
#[error("attribution chain too deep")]
ChainTooDeep,
#[error("illegal narrowing")]
IllegalNarrowing,
#[error("undeclared service trust")]
UndeclaredServiceTrust,
#[error("narrowing exceeds authority")]
NarrowingExceedsAuthority,
}
pub trait Narrowing: sealed::Sealed {}
#[derive(Debug, Clone, Copy)]
pub struct ToAnonymous;
impl sealed::Sealed for ToAnonymous {}
impl Narrowing for ToAnonymous {}
#[derive(Debug, Clone)]
pub struct NarrowCapabilities {
pub drop: CapabilitySet,
}
impl sealed::Sealed for NarrowCapabilities {}
impl Narrowing for NarrowCapabilities {}
#[derive(Debug, Clone)]
pub struct ServiceToService {
pub target: ServiceIdentity,
pub trust_declaration: ServiceTrustDeclaration,
}
impl sealed::Sealed for ServiceToService {}
impl Narrowing for ServiceToService {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ServiceTrustDeclaration {
pub(crate) declaration_id: TrustDeclarationId,
pub(crate) from_service: ServiceIdentity,
pub(crate) to_service: ServiceIdentity,
pub(crate) capabilities: CapabilitySet,
pub(crate) resource_scope: crate::wire::ResourceScope,
pub(crate) issued_at: SystemTime,
pub(crate) expires_at: SystemTime,
pub(crate) trust_root: crate::trust::TrustRootIdentity,
pub(crate) signature: crate::trust::TrustRootSignature,
pub(crate) _private: PhantomData<sealed::Token>,
}
impl ServiceTrustDeclaration {
#[must_use]
pub fn declaration_id(&self) -> &TrustDeclarationId {
&self.declaration_id
}
#[must_use]
pub fn from_service(&self) -> &ServiceIdentity {
&self.from_service
}
#[must_use]
pub fn to_service(&self) -> &ServiceIdentity {
&self.to_service
}
#[must_use]
pub fn capabilities(&self) -> &CapabilitySet {
&self.capabilities
}
#[must_use]
pub fn resource_scope(&self) -> &crate::wire::ResourceScope {
&self.resource_scope
}
#[must_use]
pub fn issued_at(&self) -> SystemTime {
self.issued_at
}
#[must_use]
pub fn expires_at(&self) -> SystemTime {
self.expires_at
}
#[must_use]
pub fn trust_root(&self) -> &crate::trust::TrustRootIdentity {
&self.trust_root
}
#[must_use]
pub fn signature(&self) -> &crate::trust::TrustRootSignature {
&self.signature
}
}
#[must_use]
pub fn from_xrpc_request<'a>(
evidence: crate::verification::VerifiedJwt,
trace_id: TraceId,
sinks: AuditSinks<'a>,
oracles: OracleSet<'a>,
) -> AuthContext<'a> {
AuthContext::new_internal(
Requester::Did(evidence.issuer().clone()),
trace_id,
sinks,
oracles,
AttributionChain::empty(),
CapabilitySet::empty(),
)
}
#[must_use]
pub fn from_service_request<'a>(
evidence: crate::verification::VerifiedCapabilityClaim,
trace_id: TraceId,
sinks: AuditSinks<'a>,
oracles: OracleSet<'a>,
) -> AuthContext<'a> {
let issuer = evidence.issuer().clone();
let chain = evidence.chain().cloned().unwrap_or_else(AttributionChain::empty);
let capabilities =
CapabilitySet::from_kinds(evidence.capabilities().iter().copied());
AuthContext::new_internal(
Requester::Service(issuer),
trace_id,
sinks,
oracles,
chain,
capabilities,
)
}
#[must_use]
pub fn from_sync_channel_message<'a>(
evidence: crate::verification::VerifiedSyncMessage,
trace_id: TraceId,
sinks: AuditSinks<'a>,
oracles: OracleSet<'a>,
) -> AuthContext<'a> {
let session_identity = evidence.session_identity().clone();
let chain = evidence
.payload()
.chain()
.cloned()
.unwrap_or_else(AttributionChain::empty);
let capabilities =
CapabilitySet::from_kinds(evidence.payload().capabilities().iter().copied());
AuthContext::new_internal(
Requester::Service(session_identity),
trace_id,
sinks,
oracles,
chain,
capabilities,
)
}
#[must_use]
pub fn anonymous_for_public_read<'a>(
trace_id: TraceId,
sinks: AuditSinks<'a>,
oracles: OracleSet<'a>,
) -> AuthContext<'a> {
AuthContext::new_internal(
Requester::Anonymous,
trace_id,
sinks,
oracles,
AttributionChain::empty(),
CapabilitySet::empty(),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn max_chain_depth_pinned_at_8() {
assert_eq!(MAX_CHAIN_DEPTH, 8);
}
#[test]
fn requester_kind_discriminant_matches_variant() {
assert_eq!(
Requester::Did(Did::new("did:plc:example").unwrap()).kind(),
RequesterKind::Did
);
assert_eq!(Requester::Anonymous.kind(), RequesterKind::Anonymous);
}
#[test]
fn attribution_chain_rejects_overdepth() {
let mut chain = AttributionChain::empty();
for _ in 0..MAX_CHAIN_DEPTH {
chain
.try_push(AttributionEntry {
requester: Requester::Anonymous,
derivation_reason: DerivationReason::DropPrivilegeToAnonymous,
derived_at: SystemTime::UNIX_EPOCH,
key_id_used: None,
})
.unwrap();
}
let r = chain.try_push(AttributionEntry {
requester: Requester::Anonymous,
derivation_reason: DerivationReason::DropPrivilegeToAnonymous,
derived_at: SystemTime::UNIX_EPOCH,
key_id_used: None,
});
assert!(matches!(r, Err(DeriveError::ChainTooDeep)));
}
mod derive_for_fixture {
use crate::audit::*;
use crate::authority::moderation::InspectionNotificationQueueImpl;
use crate::oracle::*;
use std::sync::Mutex;
use std::time::{Duration, SystemTime};
pub(super) struct CapturingUserSink {
captured: Mutex<Vec<UserAuditEvent>>,
}
impl CapturingUserSink {
pub fn new() -> Self {
CapturingUserSink {
captured: Mutex::new(Vec::new()),
}
}
pub fn captured(&self) -> Vec<UserAuditEvent> {
self.captured.lock().unwrap().clone()
}
}
impl UserAuditSink for CapturingUserSink {
fn record(&self, event: UserAuditEvent) -> Result<(), AuditError> {
self.captured.lock().unwrap().push(event);
Ok(())
}
}
pub(super) struct NoSink;
impl ChannelAuditSink for NoSink {
fn record(&self, _: ChannelAuditEvent) -> Result<(), AuditError> {
Ok(())
}
}
impl SubstrateAuditSink for NoSink {
fn record(&self, _: SubstrateAuditEvent) -> Result<(), AuditError> {
Ok(())
}
}
impl ModerationAuditSink for NoSink {
fn record(&self, _: ModerationAuditEvent) -> Result<(), AuditError> {
Ok(())
}
}
impl FallbackAuditSink for NoSink {
fn record_panic(
&self,
_: SinkKind,
_: crate::identity::TraceId,
_: crate::authority::CapabilityKind,
_: SystemTime,
) {
}
fn record_composite_failure(
&self,
_: crate::identity::TraceId,
_: CompositeOpId,
_: &[SinkKind],
_: &[SinkKind],
_: SystemTime,
) {
}
fn record_event(&self, _: FallbackAuditEvent) {}
}
impl InspectionNotificationQueueImpl for NoSink {
fn enqueue(
&self,
_: &crate::proto::Did,
_: crate::authority::InspectionNotification,
) {
}
}
pub(super) struct FailingUserSink;
impl UserAuditSink for FailingUserSink {
fn record(&self, _: UserAuditEvent) -> Result<(), AuditError> {
Err(AuditError::Unavailable)
}
}
pub(super) struct NoOracle;
impl BlockOracle for NoOracle {
fn block_state(
&self,
_: &crate::proto::Did,
_: &crate::proto::Did,
) -> BlockState {
BlockState::None
}
fn last_synced_at(&self) -> SystemTime {
SystemTime::UNIX_EPOCH
}
fn data_freshness_bound(&self) -> Duration {
Duration::from_secs(60)
}
fn worst_case_latency_for(&self, _: BlockOracleQuery) -> Duration {
Duration::ZERO
}
}
impl AudienceOracle for NoOracle {
fn audience_state(
&self,
_: &crate::proto::Did,
_: &crate::authority::ResourceId,
) -> AudienceState {
AudienceState::NoAudienceConfigured
}
fn last_synced_at(&self) -> SystemTime {
SystemTime::UNIX_EPOCH
}
fn data_freshness_bound(&self) -> Duration {
Duration::from_secs(60)
}
fn worst_case_latency_for(&self, _: AudienceOracleQuery) -> Duration {
Duration::ZERO
}
}
impl MuteOracle for NoOracle {
fn mute_state(
&self,
_: &crate::proto::Did,
_: &crate::proto::Did,
) -> MuteState {
MuteState::None
}
fn last_synced_at(&self) -> SystemTime {
SystemTime::UNIX_EPOCH
}
fn data_freshness_bound(&self) -> Duration {
Duration::from_secs(60)
}
fn worst_case_latency_for(&self, _: MuteOracleQuery) -> Duration {
Duration::ZERO
}
}
}
use derive_for_fixture::*;
fn sample_did() -> Did {
Did::new("did:plc:phase7e-derive").unwrap()
}
fn sample_service() -> crate::identity::ServiceIdentity {
crate::identity::ServiceIdentity::new_internal(
sample_did(),
crate::identity::KeyId::from_bytes([0u8; 32]),
crate::identity::PublicKey {
algorithm: crate::identity::SignatureAlgorithm::Ed25519,
bytes: [0u8; 32],
},
None,
)
}
fn build_ctx<'a>(
user_sink: &'a dyn UserAuditSink,
no_sink: &'a NoSink,
no_oracle: &'a NoOracle,
correlation_key: &'a crate::identity::CorrelationKey,
requester: Requester,
) -> AuthContext<'a> {
build_ctx_with_caps(
user_sink,
no_sink,
no_oracle,
correlation_key,
requester,
crate::authority::capability::CapabilitySet::empty(),
)
}
fn build_ctx_with_caps<'a>(
user_sink: &'a dyn UserAuditSink,
no_sink: &'a NoSink,
no_oracle: &'a NoOracle,
correlation_key: &'a crate::identity::CorrelationKey,
requester: Requester,
capabilities: crate::authority::capability::CapabilitySet,
) -> AuthContext<'a> {
AuthContext::new_internal(
requester,
crate::identity::TraceId::from_bytes([0xEE; 16]),
AuditSinks {
user: user_sink,
channel: no_sink,
substrate: no_sink,
moderation: no_sink,
fallback: no_sink,
inspection_queue: no_sink,
correlation_key,
},
OracleSet {
block: no_oracle,
audience: no_oracle,
mute: no_oracle,
},
AttributionChain::empty(),
capabilities,
)
}
#[test]
fn derive_for_to_anonymous_from_did() {
let user = CapturingUserSink::new();
let no_sink = NoSink;
let no_oracle = NoOracle;
let ck = crate::identity::CorrelationKey::from_bytes([0u8; 32]);
let ctx = build_ctx(
&user,
&no_sink,
&no_oracle,
&ck,
Requester::Did(sample_did()),
);
let derived = ctx.derive_for(ToAnonymous).expect("ToAnonymous should succeed");
assert!(matches!(derived.requester(), Requester::Anonymous));
assert_eq!(derived.attribution_chain().entries().len(), 1);
match &derived.attribution_chain().entries()[0].derivation_reason {
DerivationReason::DropPrivilegeToAnonymous => {}
other => panic!("expected DropPrivilegeToAnonymous, got {other:?}"),
}
}
#[test]
fn derive_for_to_anonymous_from_service() {
let user = CapturingUserSink::new();
let no_sink = NoSink;
let no_oracle = NoOracle;
let ck = crate::identity::CorrelationKey::from_bytes([0u8; 32]);
let svc = sample_service();
let ctx = build_ctx(
&user,
&no_sink,
&no_oracle,
&ck,
Requester::Service(svc.clone()),
);
let derived = ctx.derive_for(ToAnonymous).unwrap();
assert!(matches!(derived.requester(), Requester::Anonymous));
let entries = derived.attribution_chain().entries();
assert_eq!(entries.len(), 1);
assert!(matches!(&entries[0].requester, Requester::Service(_)));
}
#[test]
fn derive_for_to_anonymous_from_anonymous() {
let user = CapturingUserSink::new();
let no_sink = NoSink;
let no_oracle = NoOracle;
let ck = crate::identity::CorrelationKey::from_bytes([0u8; 32]);
let ctx = build_ctx(&user, &no_sink, &no_oracle, &ck, Requester::Anonymous);
let derived = ctx.derive_for(ToAnonymous).unwrap();
assert!(matches!(derived.requester(), Requester::Anonymous));
assert_eq!(derived.attribution_chain().entries().len(), 1);
}
#[test]
fn derive_for_narrow_capabilities() {
use crate::authority::capability::{CapabilityKind, CapabilitySet};
let user = CapturingUserSink::new();
let no_sink = NoSink;
let no_oracle = NoOracle;
let ck = crate::identity::CorrelationKey::from_bytes([0u8; 32]);
let did = sample_did();
let initial = CapabilitySet::from_kinds([
CapabilityKind::ViewPrivate,
CapabilityKind::EditPrivatePost,
]);
let ctx = build_ctx_with_caps(
&user,
&no_sink,
&no_oracle,
&ck,
Requester::Did(did.clone()),
initial.clone(),
);
let dropped = CapabilitySet::from_kinds([CapabilityKind::EditPrivatePost]);
let derived = ctx
.derive_for(NarrowCapabilities {
drop: dropped.clone(),
})
.unwrap();
match derived.requester() {
Requester::Did(d) => assert_eq!(d, &did),
other => panic!("expected Did(unchanged), got {other:?}"),
}
let entries = derived.attribution_chain().entries();
assert_eq!(entries.len(), 1);
match &entries[0].derivation_reason {
DerivationReason::NarrowCapabilities { dropped: d } => {
assert_eq!(d, &dropped);
}
other => panic!("expected NarrowCapabilities, got {other:?}"),
}
let expected_after =
CapabilitySet::from_kinds([CapabilityKind::ViewPrivate]);
assert_eq!(derived.capabilities(), &expected_after);
}
#[test]
fn derive_for_narrow_capabilities_rejects_superset() {
use crate::authority::capability::{CapabilityKind, CapabilitySet};
let user = CapturingUserSink::new();
let no_sink = NoSink;
let no_oracle = NoOracle;
let ck = crate::identity::CorrelationKey::from_bytes([0u8; 32]);
let initial = CapabilitySet::from_kinds([CapabilityKind::ViewPrivate]);
let ctx = build_ctx_with_caps(
&user,
&no_sink,
&no_oracle,
&ck,
Requester::Did(sample_did()),
initial,
);
let exceeding = CapabilitySet::from_kinds([
CapabilityKind::ViewPrivate,
CapabilityKind::EditPrivatePost,
]);
let result = ctx.derive_for(NarrowCapabilities { drop: exceeding });
assert!(matches!(result, Err(DeriveError::NarrowingExceedsAuthority)));
}
#[test]
fn derive_for_narrow_capabilities_empty_drop_is_noop() {
use crate::authority::capability::{CapabilityKind, CapabilitySet};
let user = CapturingUserSink::new();
let no_sink = NoSink;
let no_oracle = NoOracle;
let ck = crate::identity::CorrelationKey::from_bytes([0u8; 32]);
let initial = CapabilitySet::from_kinds([CapabilityKind::ViewPrivate]);
let ctx = build_ctx_with_caps(
&user,
&no_sink,
&no_oracle,
&ck,
Requester::Did(sample_did()),
initial.clone(),
);
let derived = ctx
.derive_for(NarrowCapabilities {
drop: CapabilitySet::empty(),
})
.unwrap();
assert_eq!(derived.capabilities(), &initial);
let entries = derived.attribution_chain().entries();
assert_eq!(entries.len(), 1);
match &entries[0].derivation_reason {
DerivationReason::NarrowCapabilities { dropped } => {
assert!(dropped.is_empty());
}
other => panic!("expected NarrowCapabilities, got {other:?}"),
}
}
#[test]
fn derive_for_narrow_capabilities_failed_superset_emits_audit() {
use crate::authority::capability::{CapabilityKind, CapabilitySet};
let user = CapturingUserSink::new();
let no_sink = NoSink;
let no_oracle = NoOracle;
let ck = crate::identity::CorrelationKey::from_bytes([0u8; 32]);
let ctx = build_ctx_with_caps(
&user,
&no_sink,
&no_oracle,
&ck,
Requester::Did(sample_did()),
CapabilitySet::empty(),
);
let exceeding =
CapabilitySet::from_kinds([CapabilityKind::ViewPrivate]);
let _ = ctx.derive_for(NarrowCapabilities { drop: exceeding });
let events = user.captured();
assert_eq!(events.len(), 1);
match &events[0] {
crate::audit::UserAuditEvent::DerivedContext {
narrowing_kind,
outcome,
..
} => {
assert_eq!(
*narrowing_kind,
crate::audit::NarrowingKind::NarrowCapabilities
);
assert_eq!(
*outcome,
crate::audit::DerivationOutcome::NarrowingExceedsAuthority
);
}
other => panic!(
"expected DerivedContext with NarrowingExceedsAuthority, got {other:?}"
),
}
}
#[test]
fn derive_for_preserves_attribution_chain() {
let user = CapturingUserSink::new();
let no_sink = NoSink;
let no_oracle = NoOracle;
let ck = crate::identity::CorrelationKey::from_bytes([0u8; 32]);
let ctx_a = build_ctx(
&user,
&no_sink,
&no_oracle,
&ck,
Requester::Did(sample_did()),
);
let ctx_b = ctx_a.derive_for(ToAnonymous).unwrap();
let ctx_c = ctx_b.derive_for(ToAnonymous).unwrap();
let entries = ctx_c.attribution_chain().entries();
assert_eq!(entries.len(), 2, "chain extends, doesn't replace");
assert!(matches!(
entries[0].derivation_reason,
DerivationReason::DropPrivilegeToAnonymous
));
assert!(matches!(
entries[1].derivation_reason,
DerivationReason::DropPrivilegeToAnonymous
));
assert!(matches!(entries[0].requester, Requester::Did(_)));
assert!(matches!(entries[1].requester, Requester::Anonymous));
}
#[test]
fn derive_for_returns_chain_too_deep_at_max() {
let user = CapturingUserSink::new();
let no_sink = NoSink;
let no_oracle = NoOracle;
let ck = crate::identity::CorrelationKey::from_bytes([0u8; 32]);
let mut chain = AttributionChain::empty();
for _ in 0..MAX_CHAIN_DEPTH {
chain
.try_push(AttributionEntry {
requester: Requester::Anonymous,
derivation_reason: DerivationReason::DropPrivilegeToAnonymous,
derived_at: SystemTime::UNIX_EPOCH,
key_id_used: None,
})
.unwrap();
}
let ctx = AuthContext::new_internal(
Requester::Did(sample_did()),
crate::identity::TraceId::from_bytes([0u8; 16]),
AuditSinks {
user: &user,
channel: &no_sink,
substrate: &no_sink,
moderation: &no_sink,
fallback: &no_sink,
inspection_queue: &no_sink,
correlation_key: &ck,
},
OracleSet {
block: &no_oracle,
audience: &no_oracle,
mute: &no_oracle,
},
chain,
crate::authority::capability::CapabilitySet::empty(),
);
let r = ctx.derive_for(ToAnonymous);
assert!(matches!(r, Err(DeriveError::ChainTooDeep)));
}
fn make_service(did_str: &str) -> crate::identity::ServiceIdentity {
crate::identity::ServiceIdentity::new_internal(
Did::new(did_str).unwrap(),
crate::identity::KeyId::from_bytes([0u8; 32]),
crate::identity::PublicKey {
algorithm: crate::identity::SignatureAlgorithm::Ed25519,
bytes: [0u8; 32],
},
None,
)
}
fn make_trust_declaration(
from: crate::identity::ServiceIdentity,
to: crate::identity::ServiceIdentity,
issued_at: SystemTime,
expires_at: SystemTime,
) -> ServiceTrustDeclaration {
ServiceTrustDeclaration {
declaration_id: TrustDeclarationId::from_bytes([0xAB; 16]),
from_service: from,
to_service: to,
capabilities: crate::authority::capability::CapabilitySet::empty(),
resource_scope: crate::wire::ResourceScope::Resource(
crate::authority::ResourceId::new(
sample_did(),
crate::Nsid::new("tools.kryphocron.feed.postPrivate").unwrap(),
crate::proto::Rkey::new("3jzfcijpj2z2a").unwrap(),
),
),
issued_at,
expires_at,
trust_root: crate::trust::TrustRootIdentity {
root_key_id: crate::identity::KeyId::from_bytes([0u8; 32]),
root_key: crate::identity::PublicKey {
algorithm: crate::identity::SignatureAlgorithm::Ed25519,
bytes: [0u8; 32],
},
},
signature: crate::trust::TrustRootSignature {
algorithm: crate::identity::SignatureAlgorithm::Ed25519,
bytes: [0u8; 64],
},
_private: PhantomData,
}
}
#[test]
fn derive_for_service_to_service_happy() {
let user = CapturingUserSink::new();
let no_sink = NoSink;
let no_oracle = NoOracle;
let ck = crate::identity::CorrelationKey::from_bytes([0u8; 32]);
let svc_a = make_service("did:plc:phase7e-svc-a");
let svc_b = make_service("did:plc:phase7e-svc-b");
let ctx = build_ctx(
&user,
&no_sink,
&no_oracle,
&ck,
Requester::Service(svc_a.clone()),
);
let now = SystemTime::now();
let decl = make_trust_declaration(
svc_a.clone(),
svc_b.clone(),
now - std::time::Duration::from_secs(60),
now + std::time::Duration::from_secs(86400),
);
let sts = ServiceToService {
target: svc_b.clone(),
trust_declaration: decl,
};
let derived = ctx.derive_for(sts).expect("happy path should succeed");
match derived.requester() {
Requester::Service(s) => assert_eq!(s, &svc_b),
other => panic!("expected Service(B), got {other:?}"),
}
let entries = derived.attribution_chain().entries();
assert_eq!(entries.len(), 1);
match &entries[0].derivation_reason {
DerivationReason::ServiceToServiceDelegation { trust_declaration_id } => {
assert_eq!(trust_declaration_id.as_bytes(), &[0xAB; 16]);
}
other => panic!("expected ServiceToServiceDelegation, got {other:?}"),
}
}
#[test]
fn derive_for_service_to_service_from_did_rejected() {
let user = CapturingUserSink::new();
let no_sink = NoSink;
let no_oracle = NoOracle;
let ck = crate::identity::CorrelationKey::from_bytes([0u8; 32]);
let svc_a = make_service("did:plc:phase7e-svc-a");
let svc_b = make_service("did:plc:phase7e-svc-b");
let ctx = build_ctx(
&user,
&no_sink,
&no_oracle,
&ck,
Requester::Did(sample_did()),
);
let now = SystemTime::now();
let decl = make_trust_declaration(
svc_a,
svc_b.clone(),
now - std::time::Duration::from_secs(60),
now + std::time::Duration::from_secs(86400),
);
let sts = ServiceToService {
target: svc_b,
trust_declaration: decl,
};
assert!(matches!(
ctx.derive_for(sts),
Err(DeriveError::IllegalNarrowing)
));
}
#[test]
fn derive_for_service_to_service_from_service_mismatch_rejected() {
let user = CapturingUserSink::new();
let no_sink = NoSink;
let no_oracle = NoOracle;
let ck = crate::identity::CorrelationKey::from_bytes([0u8; 32]);
let svc_a = make_service("did:plc:phase7e-svc-a");
let svc_b = make_service("did:plc:phase7e-svc-b");
let svc_c = make_service("did:plc:phase7e-svc-c");
let ctx = build_ctx(
&user,
&no_sink,
&no_oracle,
&ck,
Requester::Service(svc_a),
);
let now = SystemTime::now();
let decl = make_trust_declaration(
svc_b, svc_c.clone(),
now - std::time::Duration::from_secs(60),
now + std::time::Duration::from_secs(86400),
);
let sts = ServiceToService {
target: svc_c,
trust_declaration: decl,
};
assert!(matches!(
ctx.derive_for(sts),
Err(DeriveError::IllegalNarrowing)
));
}
#[test]
fn derive_for_service_to_service_expired_declaration_rejected() {
let user = CapturingUserSink::new();
let no_sink = NoSink;
let no_oracle = NoOracle;
let ck = crate::identity::CorrelationKey::from_bytes([0u8; 32]);
let svc_a = make_service("did:plc:phase7e-svc-a");
let svc_b = make_service("did:plc:phase7e-svc-b");
let ctx = build_ctx(
&user,
&no_sink,
&no_oracle,
&ck,
Requester::Service(svc_a.clone()),
);
let now = SystemTime::now();
let decl = make_trust_declaration(
svc_a,
svc_b.clone(),
now - std::time::Duration::from_secs(7200),
now - std::time::Duration::from_secs(3600),
);
let sts = ServiceToService {
target: svc_b,
trust_declaration: decl,
};
assert!(matches!(
ctx.derive_for(sts),
Err(DeriveError::UndeclaredServiceTrust)
));
}
#[test]
fn derive_for_emits_derived_context_on_success() {
use crate::audit::{DerivationOutcome, NarrowingKind, UserAuditEvent};
let user = CapturingUserSink::new();
let no_sink = NoSink;
let no_oracle = NoOracle;
let ck = crate::identity::CorrelationKey::from_bytes([0u8; 32]);
let svc_a = make_service("did:plc:phase7e-emit-a");
let svc_b = make_service("did:plc:phase7e-emit-b");
let ctx = build_ctx(
&user,
&no_sink,
&no_oracle,
&ck,
Requester::Service(svc_a.clone()),
);
let _ = ctx.derive_for(ToAnonymous).unwrap();
let _ = ctx
.derive_for(NarrowCapabilities {
drop: crate::authority::capability::CapabilitySet::empty(),
})
.unwrap();
let now = SystemTime::now();
let decl = make_trust_declaration(
svc_a,
svc_b.clone(),
now - std::time::Duration::from_secs(60),
now + std::time::Duration::from_secs(86400),
);
let _ = ctx
.derive_for(ServiceToService {
target: svc_b,
trust_declaration: decl,
})
.unwrap();
let captured = user.captured();
assert_eq!(captured.len(), 3, "one DerivedContext per derive_for call");
let kinds: Vec<NarrowingKind> = captured
.iter()
.map(|e| match e {
UserAuditEvent::DerivedContext { narrowing_kind, .. } => *narrowing_kind,
other => panic!("expected DerivedContext, got {other:?}"),
})
.collect();
assert_eq!(
kinds,
vec![
NarrowingKind::ToAnonymous,
NarrowingKind::NarrowCapabilities,
NarrowingKind::ServiceToService,
],
);
for event in &captured {
match event {
UserAuditEvent::DerivedContext { outcome, .. } => {
assert_eq!(*outcome, DerivationOutcome::Success);
}
_ => unreachable!(),
}
}
}
#[test]
fn derive_for_emits_derived_context_on_failure() {
use crate::audit::{DerivationOutcome, UserAuditEvent};
let user = CapturingUserSink::new();
let no_sink = NoSink;
let no_oracle = NoOracle;
let ck = crate::identity::CorrelationKey::from_bytes([0u8; 32]);
let svc_a = make_service("did:plc:phase7e-fail-a");
let svc_b = make_service("did:plc:phase7e-fail-b");
let ctx = build_ctx(&user, &no_sink, &no_oracle, &ck, Requester::Anonymous);
let now = SystemTime::now();
let decl = make_trust_declaration(
svc_a,
svc_b.clone(),
now - std::time::Duration::from_secs(60),
now + std::time::Duration::from_secs(86400),
);
let _ = ctx.derive_for(ServiceToService {
target: svc_b,
trust_declaration: decl,
});
let captured = user.captured();
assert_eq!(captured.len(), 1);
match &captured[0] {
UserAuditEvent::DerivedContext { outcome, .. } => {
assert_eq!(*outcome, DerivationOutcome::IllegalNarrowing);
}
other => panic!("expected DerivedContext, got {other:?}"),
}
}
#[test]
fn derive_for_audit_emit_failure_does_not_block_derivation() {
let failing_user = FailingUserSink;
let no_sink = NoSink;
let no_oracle = NoOracle;
let ck = crate::identity::CorrelationKey::from_bytes([0u8; 32]);
let ctx = build_ctx(
&failing_user,
&no_sink,
&no_oracle,
&ck,
Requester::Did(sample_did()),
);
let derived = ctx.derive_for(ToAnonymous);
assert!(
derived.is_ok(),
"audit emit failure should not block derivation"
);
let derived = derived.unwrap();
assert!(matches!(derived.requester(), Requester::Anonymous));
}
#[test]
fn anonymous_for_public_read_constructs_anonymous_context() {
let user = CapturingUserSink::new();
let no_sink = NoSink;
let no_oracle = NoOracle;
let ck = crate::identity::CorrelationKey::from_bytes([0u8; 32]);
let trace_id = crate::identity::TraceId::from_bytes([0xAA; 16]);
let ctx = anonymous_for_public_read(
trace_id,
AuditSinks {
user: &user,
channel: &no_sink,
substrate: &no_sink,
moderation: &no_sink,
fallback: &no_sink,
inspection_queue: &no_sink,
correlation_key: &ck,
},
OracleSet {
block: &no_oracle,
audience: &no_oracle,
mute: &no_oracle,
},
);
assert!(matches!(ctx.requester(), Requester::Anonymous));
assert_eq!(ctx.trace_id(), trace_id);
assert_eq!(
ctx.attribution_chain().entries().len(),
0,
"anonymous context starts with empty chain"
);
assert_eq!(user.captured().len(), 0);
}
}