use core::marker::PhantomData;
use std::time::{Duration, Instant};
use thiserror::Error;
use crate::authority::capability::{CapabilityClass, UserCapability};
use crate::identity::TraceId;
use crate::ingress::{AttributionChain, Requester, RequesterKind};
use crate::oracle::{
AudienceOracleQuery, AudienceState, BlockOracleQuery, BlockState, OracleKind,
OracleQueryKind,
};
use crate::sealed;
use crate::wire::ReceiptVerificationFailure;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DenialReason {
Blocked {
query: BlockOracleQuery,
state: BlockState,
},
NotInAudience {
query: AudienceOracleQuery,
state: AudienceState,
},
OwnershipCheckFailed,
CapabilityPredicateRejected {
detail: &'static str,
},
PredicatePanic,
JwtScopeInsufficient {
required: String,
granted: smallvec::SmallVec<[String; 4]>,
},
JwtVerificationFailed(crate::verification::JwtVerificationError),
AttributionReceiptInvalid {
failing_hop: u8,
reason: ReceiptVerificationFailure,
},
CapabilityDeprecated {
nsid: &'static str,
since_version: SemVer,
successor: Option<&'static str>,
},
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PipelineStage {
DeprecationGate,
RecordState,
BlockConsultation,
AudienceConsultation,
Predicate,
JwtScope,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BindOutcomeRepr {
Success,
TargetMismatch,
ContextMismatch,
Expired {
issued_at: Instant,
max_age: Duration,
},
OracleStale {
oracle: OracleKind,
query: OracleQueryKind,
sync_age: Duration,
},
DeniedAtPipeline {
stage: PipelineStage,
reason: DenialReason,
},
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum BindError {
#[error("bind target mismatch")]
TargetMismatch,
#[error("bind context mismatch")]
ContextMismatch,
#[error("bind: proof expired")]
Expired,
#[error("bind: oracle stale ({oracle:?})")]
OracleStale {
oracle: OracleKind,
query: OracleQueryKind,
},
#[error("bind: audit unavailable")]
AuditUnavailable,
#[error("bind: audit sink panicked")]
AuditPanicked,
#[error("bind: attribution receipt invalid at hop {failing_hop}")]
AttributionReceiptInvalid {
failing_hop: u8,
reason: ReceiptVerificationFailure,
},
#[error("bind denied at {stage:?}: {reason:?}")]
DeniedAtPipeline {
stage: PipelineStage,
reason: DenialReason,
},
}
impl From<crate::audit::CompositeAuditError> for BindError {
fn from(e: crate::audit::CompositeAuditError) -> Self {
use crate::audit::CompositeAuditError;
match e {
CompositeAuditError::SinkCommitFailed { .. }
| CompositeAuditError::RollbackDispatchFailed { .. }
| CompositeAuditError::TrackerFull => BindError::AuditUnavailable,
CompositeAuditError::InconsistencyUnrecoverable => BindError::AuditPanicked,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum BindFailureReason {
#[error("reborrow: proof expired")]
Expired,
#[error("reborrow: oracle stale")]
OracleStale {
oracle: OracleKind,
query: OracleQueryKind,
},
#[error("reborrow: audit unavailable")]
AuditUnavailable,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum AuthDenial {
#[error("issuance rate-limited")]
RateLimited,
#[error("issuance: oracle stale ({oracle:?})")]
OracleStale {
oracle: OracleKind,
query: OracleQueryKind,
},
#[error("issuance denied")]
Denied(DenialReason),
#[error("issuance: audit unavailable")]
AuditUnavailable,
#[error("issuance: predicate panicked")]
PredicatePanic,
#[error(
"issuance: write to deprecated lexicon {nsid} (deprecated since {since_version:?})"
)]
WriteToDeprecatedLexicon {
nsid: &'static str,
since_version: SemVer,
successor: Option<&'static str>,
},
#[error("issuance: scope mismatch")]
ScopeMismatch {
required: String,
granted: smallvec::SmallVec<[String; 4]>,
},
#[error("issuance: requester (kind {found:?}) lacks authority to issue {class:?}-class")]
RequesterLacksAuthority {
class: CapabilityClass,
found: RequesterKind,
},
}
pub use kryphocron_lexicons::SemVer;
pub struct PredicateContext<'a> {
requester: &'a Requester,
trace_id: TraceId,
attribution_chain: &'a AttributionChain,
_no_oracles: PhantomData<()>,
_no_sinks: PhantomData<()>,
_private: PhantomData<sealed::Token>,
}
impl<'a> PredicateContext<'a> {
#[must_use]
pub(crate) fn new(
requester: &'a Requester,
trace_id: TraceId,
attribution_chain: &'a AttributionChain,
) -> Self {
PredicateContext {
requester,
trace_id,
attribution_chain,
_no_oracles: PhantomData,
_no_sinks: PhantomData,
_private: PhantomData,
}
}
#[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
}
}
pub trait IssuancePolicy: UserCapability {
fn capability_predicate(
ctx: &PredicateContext<'_>,
target: &<Self as UserCapability>::Subject,
oracle_results: &<Self as UserCapability>::OracleResults,
) -> Result<(), DenialReason>;
fn required_jwt_scope() -> Option<&'static str> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bind_error_includes_attribution_receipt_invalid() {
let _e = BindError::AttributionReceiptInvalid {
failing_hop: 0,
reason: ReceiptVerificationFailure::Malformed,
};
}
#[test]
fn pipeline_stage_carries_jwt_scope_per_7_2() {
let _s = PipelineStage::JwtScope;
}
#[test]
fn requester_lacks_authority_carries_class_and_found_kind() {
let e = AuthDenial::RequesterLacksAuthority {
class: CapabilityClass::Substrate,
found: RequesterKind::Anonymous,
};
match e {
AuthDenial::RequesterLacksAuthority { class, found } => {
assert_eq!(class, CapabilityClass::Substrate);
assert_eq!(found, RequesterKind::Anonymous);
}
other => panic!("expected RequesterLacksAuthority, got {other:?}"),
}
}
#[test]
fn capability_deprecated_carries_nsid_version_and_successor() {
let r = DenialReason::CapabilityDeprecated {
nsid: "tools.kryphocron.feed.postOld",
since_version: SemVer::new(1, 0, 0),
successor: Some("tools.kryphocron.feed.postPrivate"),
};
match r {
DenialReason::CapabilityDeprecated {
nsid,
since_version,
successor,
} => {
assert_eq!(nsid, "tools.kryphocron.feed.postOld");
assert_eq!(since_version, SemVer::new(1, 0, 0));
assert_eq!(successor, Some("tools.kryphocron.feed.postPrivate"));
}
other => panic!("expected CapabilityDeprecated, got {other:?}"),
}
}
#[test]
fn bind_error_denied_at_pipeline_constructible() {
let e = BindError::DeniedAtPipeline {
stage: PipelineStage::DeprecationGate,
reason: DenialReason::CapabilityDeprecated {
nsid: "tools.kryphocron.feed.postOld",
since_version: SemVer::new(1, 0, 0),
successor: None,
},
};
match e {
BindError::DeniedAtPipeline { stage, reason } => {
assert_eq!(stage, PipelineStage::DeprecationGate);
assert!(matches!(reason, DenialReason::CapabilityDeprecated { .. }));
}
other => panic!("expected DeniedAtPipeline, got {other:?}"),
}
}
}