use super::accompaniment_types::*;
use super::{AccessRightsDecision, evaluate_access_rights};
use crate::database::ObjectDatabase;
use crate::prelude::*;
use bacnet_types::constructed::BACnetDeviceObjectReference;
use bacnet_types::enums::{AccessEvent, ObjectType};
use bacnet_types::error::Error;
use bacnet_types::primitives::ObjectIdentifier;
use core::time::Duration;
#[path = "accompaniment_read.rs"]
mod read;
#[path = "accompaniment_timing.rs"]
mod timing;
pub fn evaluate_accompaniment(
db: &ObjectDatabase,
request: AccompanimentRequest<'_>,
) -> Result<AccompanimentEvaluation, Error> {
let original_rights =
evaluate_access_rights(db, request.original.credential, request.original.point)?;
Ok(evaluate_with_rights(db, request, original_rights))
}
pub(super) fn evaluate_with_rights(
db: &ObjectDatabase,
request: AccompanimentRequest<'_>,
original_rights: super::AccessRightsEvaluation,
) -> AccompanimentEvaluation {
let mut result = AccompanimentEvaluation {
original: request.original,
original_rights,
requirement: None,
companions: Vec::new(),
outcome: AccompanimentOutcome::NotEvaluated,
events: Vec::new(),
};
let selected = match result.original_rights.decision {
AccessRightsDecision::Denied { .. } => return result,
AccessRightsDecision::Exempt => {
result.outcome = AccompanimentOutcome::NotRequired;
return result;
}
AccessRightsDecision::Granted { rule } => rule.access_rights,
};
result.outcome = match follow(db, &request, &mut result, selected) {
Ok(outcome) => outcome,
Err(problem) => AccompanimentOutcome::Unresolved(problem),
};
result
}
fn witness(
request: &AccompanimentRequest<'_>,
sequence: u64,
) -> Result<AccompanimentEvidence, AccompanimentProblem> {
let mut entries = request
.witnesses
.iter()
.filter(|witness| witness.original == sequence);
let evidence = entries
.next()
.ok_or(AccompanimentProblem::MissingWitness {
presentation: sequence,
})?
.evidence;
if entries.next().is_some() {
return Err(AccompanimentProblem::AmbiguousWitness {
presentation: sequence,
});
}
Ok(evidence)
}
fn deny(
result: &mut AccompanimentEvaluation,
current: CredentialPresentation,
missing: bool,
) -> AccompanimentOutcome {
let missing_original = missing && current == result.original;
let event = if missing_original {
AccessEvent::DENIED_NO_ACCOMPANIMENT
} else {
AccessEvent::DENIED_INCORRECT_ACCOMPANIMENT
};
result.events.push(AccompanimentEventIntent {
original: result.original,
access_event: event,
presentation: result.original,
});
if missing_original {
AccompanimentOutcome::DeniedNoCompanion
} else {
AccompanimentOutcome::DeniedIncorrectCompanion {
presentation: current.sequence,
}
}
}
fn follow(
db: &ObjectDatabase,
request: &AccompanimentRequest<'_>,
result: &mut AccompanimentEvaluation,
mut selected: ObjectIdentifier,
) -> Result<AccompanimentOutcome, AccompanimentProblem> {
let mut current = request.original;
let mut path = vec![current];
let mut period = None;
let mut links = 0;
loop {
let requirement = read::requirement(db, selected)?;
if let Some(child) = result.companions.last_mut() {
child.requirement = requirement.clone();
} else {
result.requirement = requirement.clone();
}
let Some(requirement) = requirement else {
return Ok(if links == 0 {
AccompanimentOutcome::NotRequired
} else {
AccompanimentOutcome::Satisfied
});
};
if links >= request.max_links {
return Err(AccompanimentProblem::LinkLimit {
max_links: request.max_links,
});
}
let period = match period {
Some(period) => period,
None => {
let value = read::period(db, current.point, request.timing.fallback_period)?;
period = Some(value);
value
}
};
let expired = timing::expired(current, request.now, period, request.timing.deadline)?;
let candidate = match witness(request, current.sequence)? {
AccompanimentEvidence::Waiting if !expired => {
read::available(db, &requirement)?;
return Ok(AccompanimentOutcome::Waiting {
presentation: current.sequence,
});
}
AccompanimentEvidence::DeadlineReached if !expired => {
return Err(timing::problem(
current.sequence,
AccompanimentPresentationFailure::PrematureDeadline,
));
}
AccompanimentEvidence::Waiting | AccompanimentEvidence::DeadlineReached => {
read::available(db, &requirement)?;
return Ok(deny(result, current, true));
}
AccompanimentEvidence::Presented(candidate) => candidate,
};
if let Some(previous) = path
.iter()
.find(|previous| previous.sequence == candidate.sequence)
&& *previous != candidate
{
return Err(timing::problem(
candidate.sequence,
AccompanimentPresentationFailure::ReusedSequence,
));
}
if !timing::eligible(current, candidate, request, period, &path)? {
read::available(db, &requirement)?;
return Ok(deny(result, current, true));
}
if candidate.credential.object_type() != ObjectType::ACCESS_CREDENTIAL {
return Err(AccompanimentProblem::Reference {
reference: candidate.credential.into(),
reason: AccompanimentReferenceFailure::WrongType,
});
}
if links == 0 {
result.events.push(AccompanimentEventIntent {
original: result.original,
access_event: AccessEvent::ACCOMPANIMENT_BY,
presentation: candidate,
});
}
read::available(db, &requirement)?;
if candidate.credential == current.credential {
return Ok(deny(result, current, false));
}
if !read::matches(db, &requirement, candidate.credential)? {
return Ok(deny(result, current, false));
}
if path.iter().any(|previous| {
previous.credential == candidate.credential || previous.sequence == candidate.sequence
}) {
path.push(candidate);
return Err(AccompanimentProblem::CyclicRequirement { path });
}
if !timing::ordered(current, candidate, request.timing.order) {
return Ok(deny(result, current, false));
}
let rights =
evaluate_access_rights(db, candidate.credential, current.point).map_err(|error| {
AccompanimentProblem::Rights {
presentation: candidate.sequence,
detail: error.to_string(),
}
})?;
let decision = rights.decision;
result.companions.push(AccompanimentCompanion {
presentation: candidate,
rights,
requirement: None,
});
match decision {
AccessRightsDecision::Denied { .. } => return Ok(deny(result, current, false)),
AccessRightsDecision::Exempt => return Ok(AccompanimentOutcome::Satisfied),
AccessRightsDecision::Granted { rule } => selected = rule.access_rights,
}
path.push(candidate);
current = candidate;
links += 1;
}
}