use parlov_core::{Applicability, BlockFamily, DifferentialSet, ProbeExchange, Technique};
use super::auth_equivalence::auth_gate_decision;
use super::auth_types::{AuthGateDecision, CredentialBlockKind};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthBlockLayer {
Origin,
Proxy,
Network,
LoginRedirect,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PreconditionBlock {
AuthGateBeforeTechnique {
credential_state: CredentialBlockKind,
layer: AuthBlockLayer,
},
MethodGateBeforeResource,
BlockedByParser,
ApplicabilityMarkerMissing,
SurfaceMismatch,
MutationDestroyedControl,
}
impl PreconditionBlock {
#[must_use]
pub fn block_family(self) -> BlockFamily {
match self {
Self::AuthGateBeforeTechnique { .. } => BlockFamily::Authorization,
Self::MethodGateBeforeResource => BlockFamily::Method,
Self::BlockedByParser => BlockFamily::Parser,
Self::ApplicabilityMarkerMissing | Self::MutationDestroyedControl => {
BlockFamily::TechniqueLocal
}
Self::SurfaceMismatch => BlockFamily::Surface,
}
}
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::AuthGateBeforeTechnique {
credential_state,
layer,
} => auth_block_reason(credential_state, layer),
Self::MethodGateBeforeResource => "method-level rejection before resource lookup",
Self::BlockedByParser => "parser/validator rejection before technique evaluated",
Self::ApplicabilityMarkerMissing => "technique applicability marker not observed",
Self::SurfaceMismatch => {
"differential observed on a surface this technique does not test"
}
Self::MutationDestroyedControl => {
"mutation broke baseline route — control reference destroyed"
}
}
}
}
#[must_use]
fn auth_block_reason(credential_state: CredentialBlockKind, layer: AuthBlockLayer) -> &'static str {
match (credential_state, layer) {
(CredentialBlockKind::NoCredential, AuthBlockLayer::Origin) => {
"auth gate fired before technique (no credential provided)"
}
(CredentialBlockKind::CredentialRejected, AuthBlockLayer::Origin) => {
"auth gate fired before technique (credential rejected — token invalid/expired)"
}
(CredentialBlockKind::InsufficientScope, AuthBlockLayer::Origin) => {
"auth gate fired before technique (credential lacks required scope)"
}
(CredentialBlockKind::UnknownAuthFailure, AuthBlockLayer::Origin) => {
"auth gate fired before technique (specific failure not identified)"
}
(CredentialBlockKind::NotApplicable, AuthBlockLayer::Origin) => {
"auth gate fired before technique reached oracle layer"
}
(_, AuthBlockLayer::Proxy) => "proxy auth required before technique reached origin",
(_, AuthBlockLayer::Network) => "network/captive-portal auth required",
(_, AuthBlockLayer::LoginRedirect) => "login-redirect fired before technique",
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PreconditionDecision {
Reached(f64),
Blocked(PreconditionBlock),
}
impl PreconditionDecision {
#[must_use]
pub fn confidence(self) -> f64 {
match self {
Self::Reached(c) => c,
Self::Blocked(_) => 0.0,
}
}
#[must_use]
pub fn block_reason(self) -> Option<PreconditionBlock> {
match self {
Self::Blocked(reason) => Some(reason),
Self::Reached(_) => None,
}
}
}
fn same_method_gate(differential: &DifferentialSet) -> bool {
let Some((b, p)) = first_pair(differential) else {
return false;
};
b.response.status.as_u16() == 405 && p.response.status.as_u16() == 405
}
fn same_parser_failure(differential: &DifferentialSet) -> bool {
let Some((b, p)) = first_pair(differential) else {
return false;
};
let bs = b.response.status.as_u16();
let ps = p.response.status.as_u16();
matches!(bs, 400 | 422) && bs == ps
}
fn first_pair(differential: &DifferentialSet) -> Option<(&ProbeExchange, &ProbeExchange)> {
let b = differential.baseline.first()?;
let p = differential.probe.first()?;
Some((b, p))
}
#[must_use]
pub fn precondition_confidence(
technique: &Technique,
differential: &DifferentialSet,
) -> PreconditionDecision {
match auth_gate_decision(differential) {
AuthGateDecision::Gate(reason) => return PreconditionDecision::Blocked(reason),
AuthGateDecision::DoNotGate => return PreconditionDecision::Reached(1.0),
AuthGateDecision::NoAuthInvolvement => {}
}
if same_method_gate(differential) && !technique.method_relevant {
return PreconditionDecision::Blocked(PreconditionBlock::MethodGateBeforeResource);
}
if same_parser_failure(differential) && !technique.parser_relevant {
return PreconditionDecision::Blocked(PreconditionBlock::BlockedByParser);
}
let Some((b, p)) = first_pair(differential) else {
return PreconditionDecision::Reached(1.0);
};
let app = (technique.applicability)(&b.response, &p.response);
if app == Applicability::Missing {
return PreconditionDecision::Blocked(PreconditionBlock::ApplicabilityMarkerMissing);
}
PreconditionDecision::Reached(app.confidence())
}
#[cfg(test)]
#[path = "precondition_tests.rs"]
mod tests;