use super::*;
use bacnet_types::constructed::BACnetDeviceObjectReference;
use bacnet_types::enums::{AccessPassbackMode, ObjectType, PropertyIdentifier as P};
use bacnet_types::primitives::PropertyValue;
use core::time::Duration;
type Outcome = Result<AuthorizationCheckOutcome, AuthorizationProblem>;
pub(super) fn check(
db: &ObjectDatabase,
request: &AuthorizationRequest<'_>,
mode: AuthorizationMode,
exemptions: &[AuthorizationExemption],
criterion: AuthorizationCheck,
result: &mut AuthorizationEvaluation,
) -> Outcome {
let exemption = match criterion {
AuthorizationCheck::Occupancy => Some(AuthorizationExemption::OCCUPANCY_CHECK),
AuthorizationCheck::Lockout => Some(AuthorizationExemption::LOCKOUT),
AuthorizationCheck::Mode if mode == AuthorizationMode::DENY_ALL => {
Some(AuthorizationExemption::DENY)
}
_ => None,
};
if let Some(exemption) = exemption.filter(|e| exemptions.contains(e)) {
return Ok(AuthorizationCheckOutcome::Exempt(exemption));
}
match criterion {
AuthorizationCheck::Threat => threat(db, request),
AuthorizationCheck::Occupancy => occupancy(db, request),
AuthorizationCheck::Passback => {
if exemptions.contains(&AuthorizationExemption::PASSBACK) {
if let Err(problem) = passback(db, request, result, true) {
result.passback_reporting_problem = Some(problem);
}
Ok(AuthorizationCheckOutcome::Exempt(
AuthorizationExemption::PASSBACK,
))
} else {
passback(db, request, result, false)
}
}
AuthorizationCheck::Lockout => Ok(denial_if(
read::boolean(
db,
request.original.point,
P::LOCKOUT,
request.fallback_lockout,
"point lockout",
)?,
AccessEvent::DENIED_LOCKOUT,
)),
AuthorizationCheck::Mode => Ok(denial_if(
mode == AuthorizationMode::DENY_ALL,
AccessEvent::DENIED_DENY_ALL,
)),
AuthorizationCheck::Credential | AuthorizationCheck::AccessRights => {
unreachable!("controller dispatches credential and rights separately")
}
}
}
fn denial_if(denied: bool, event: AccessEvent) -> AuthorizationCheckOutcome {
if denied {
AuthorizationCheckOutcome::Denied(event)
} else {
AuthorizationCheckOutcome::Passed
}
}
fn level(
db: &ObjectDatabase,
oid: ObjectIdentifier,
property: P,
fallback: AuthorizationFact<u32>,
) -> Result<u64, AuthorizationProblem> {
let value = match read::optional(db, oid, property)? {
Some(PropertyValue::Unsigned(value)) => value,
Some(_) => {
return Err(read::malformed(
oid,
property,
"expected Unsigned threat value",
));
}
None => match fallback {
AuthorizationFact::Known(value) => u64::from(value),
AuthorizationFact::Unknown => {
return Err(AuthorizationProblem::MissingFact("point threat level"));
}
},
};
if value > 100 {
return Err(AuthorizationProblem::UnsupportedValue {
object: oid,
property,
value,
});
}
Ok(value)
}
fn threat(db: &ObjectDatabase, request: &AuthorizationRequest<'_>) -> Outcome {
let point = level(
db,
request.original.point,
P::THREAT_LEVEL,
request.fallback_threat_level,
)?;
if point == 0 {
return Ok(AuthorizationCheckOutcome::NotApplicable);
}
let authority = level(
db,
request.original.credential,
P::THREAT_AUTHORITY,
AuthorizationFact::Known(0),
)?;
Ok(denial_if(
authority < point,
AccessEvent::DENIED_THREAT_LEVEL,
))
}
fn occupancy(db: &ObjectDatabase, request: &AuthorizationRequest<'_>) -> Outcome {
let zones = match request.occupancy {
AuthorizationOccupancy::NotApplicable => {
return Ok(AuthorizationCheckOutcome::NotApplicable);
}
AuthorizationOccupancy::Unknown => {
return Err(AuthorizationProblem::MissingFact(
"relevant occupancy zones and direction",
));
}
AuthorizationOccupancy::Zones([]) => {
return Err(AuthorizationProblem::Context(
"empty relevant occupancy zone set",
));
}
AuthorizationOccupancy::Zones(zones) => zones,
};
for (index, context) in zones.iter().enumerate() {
if zones[..index]
.iter()
.any(|old| old.zone == context.zone && old.direction == context.direction)
{
return Err(AuthorizationProblem::Context(
"duplicate occupancy zone and direction",
));
}
let outcome = occupancy_zone(db, request.original.point, *context)?;
if matches!(outcome, AuthorizationCheckOutcome::Denied(_)) {
return Ok(outcome);
}
}
Ok(AuthorizationCheckOutcome::Passed)
}
fn occupancy_zone(
db: &ObjectDatabase,
point: ObjectIdentifier,
context: AuthorizationOccupancyZone,
) -> Outcome {
let (enforced, bound, topology, point_zone, event) = match context.direction {
AuthorizationDirection::Entering => (
P::OCCUPANCY_UPPER_LIMIT_ENFORCED,
P::OCCUPANCY_UPPER_LIMIT,
P::ENTRY_POINTS,
P::ZONE_TO,
AccessEvent::DENIED_UPPER_OCCUPANCY_LIMIT,
),
AuthorizationDirection::Leaving => (
P::OCCUPANCY_LOWER_LIMIT_ENFORCED,
P::OCCUPANCY_LOWER_LIMIT,
P::EXIT_POINTS,
P::ZONE_FROM,
AccessEvent::DENIED_LOWER_OCCUPANCY_LIMIT,
),
};
if !read::boolean(
db,
point,
enforced,
context.fallback_enforced,
"occupancy enforcement",
)? {
return Ok(AuthorizationCheckOutcome::NotApplicable);
}
read::object(db, context.zone, ObjectType::ACCESS_ZONE)
.map_err(|e| read::error(context.zone, bound, e))?;
if let Some(value) = read::optional(db, point, point_zone)? {
let reference: BACnetDeviceObjectReference =
crate::device_reference::decode_reference(&value)
.map_err(|e| read::error(point, point_zone, e))?;
if reference.object_identifier != context.zone
|| !db.local_device().is_local(reference.device_identifier)
{
return Err(AuthorizationProblem::Context(
"point zone disagrees with occupancy context",
));
}
}
let references: Vec<BACnetDeviceObjectReference> =
crate::device_reference::decode_references(&read::required(db, context.zone, topology)?)
.map_err(|e| read::error(context.zone, topology, e))?;
if !references.iter().any(|reference| {
reference.object_identifier == point
&& db.local_device().is_local(reference.device_identifier)
}) {
return Err(AuthorizationProblem::Context(
"zone topology does not establish requested direction",
));
}
let limit = read::unsigned(db, context.zone, bound)?;
if limit == 0 {
return Ok(AuthorizationCheckOutcome::NotApplicable);
}
if !read::boolean(
db,
context.zone,
P::OCCUPANCY_COUNT_ENABLE,
AuthorizationFact::Unknown,
"occupancy counting enabled",
)? {
return Err(AuthorizationProblem::MissingFact(
"current occupancy while counting is disabled",
));
}
let count = read::unsigned(db, context.zone, P::OCCUPANCY_COUNT)?;
Ok(denial_if(
match context.direction {
AuthorizationDirection::Entering => count >= limit,
AuthorizationDirection::Leaving => count <= limit,
},
event,
))
}
fn passback(
db: &ObjectDatabase,
request: &AuthorizationRequest<'_>,
result: &mut AuthorizationEvaluation,
report_only: bool,
) -> Outcome {
let (zone, fallback_mode, fallback_timeout, observation) = match request.passback {
AuthorizationPassback::NotApplicable => {
return Ok(AuthorizationCheckOutcome::NotApplicable);
}
AuthorizationPassback::Unknown => {
return Err(AuthorizationProblem::MissingFact("passback applicability"));
}
AuthorizationPassback::Zone {
zone,
fallback_mode,
fallback_timeout_minutes,
observation,
} => (zone, fallback_mode, fallback_timeout_minutes, observation),
};
read::object(db, zone, ObjectType::ACCESS_ZONE)
.map_err(|e| read::error(zone, P::PASSBACK_MODE, e))?;
let mode = match read::optional(db, zone, P::PASSBACK_MODE)? {
Some(PropertyValue::Enumerated(raw)) => AccessPassbackMode::from_raw(raw),
Some(_) => {
return Err(read::malformed(
zone,
P::PASSBACK_MODE,
"expected passback enumeration",
));
}
None => match fallback_mode {
AuthorizationFact::Known(mode) => mode,
AuthorizationFact::Unknown => {
return Err(AuthorizationProblem::MissingFact("passback mode"));
}
},
};
match mode {
AccessPassbackMode::PASSBACK_OFF => return Ok(AuthorizationCheckOutcome::NotApplicable),
AccessPassbackMode::HARD_PASSBACK if report_only => {
return Ok(AuthorizationCheckOutcome::NotApplicable);
}
AccessPassbackMode::HARD_PASSBACK | AccessPassbackMode::SOFT_PASSBACK => (),
_ => {
return Err(AuthorizationProblem::UnsupportedValue {
object: zone,
property: P::PASSBACK_MODE,
value: mode.to_raw().into(),
});
}
}
let observation = match observation {
AuthorizationFact::Known(value) => value,
AuthorizationFact::Unknown => {
return Err(AuthorizationProblem::MissingFact(
"passback violation observation",
));
}
};
if observation.presentation != request.original || observation.zone != zone {
return Err(AuthorizationProblem::Context(
"passback observation belongs to another presentation or zone",
));
}
if report_only && !observation.violation {
return Ok(AuthorizationCheckOutcome::Passed);
}
if observation
.last_successful_entry
.is_some_and(|entry| entry > request.original.at)
{
return Err(AuthorizationProblem::Context(
"successful entry is after the presentation",
));
}
let timeout = match read::optional(db, zone, P::PASSBACK_TIMEOUT)? {
Some(PropertyValue::Unsigned(value)) if u32::try_from(value).is_ok() => value,
Some(PropertyValue::Unsigned(value)) => {
return Err(AuthorizationProblem::UnsupportedValue {
object: zone,
property: P::PASSBACK_TIMEOUT,
value,
});
}
Some(_) => {
return Err(read::malformed(
zone,
P::PASSBACK_TIMEOUT,
"expected Unsigned timeout minutes",
));
}
None => u64::from(fallback_timeout.unwrap_or(0)),
};
if !observation.violation {
return Ok(AuthorizationCheckOutcome::Passed);
}
if timeout > 0 {
let entry = observation
.last_successful_entry
.ok_or(AuthorizationProblem::MissingFact(
"last successful entry for passback expiry",
))?;
if request.now - entry >= Duration::from_secs(timeout * 60) {
return Ok(AuthorizationCheckOutcome::Passed);
}
}
if mode == AccessPassbackMode::HARD_PASSBACK {
Ok(AuthorizationCheckOutcome::Denied(
AccessEvent::DENIED_PASSBACK,
))
} else {
result.event(AccessEvent::PASSBACK_DETECTED, request.original);
Ok(AuthorizationCheckOutcome::Passed)
}
}