use std::collections::HashMap;
use bacnet_encoding::constructed::{decode_access_rule, decode_assigned_access_rights};
use bacnet_types::constructed::{
BACnetAccessRule, BACnetAssignedAccessRights, BACnetDeviceObjectPropertyReference,
BACnetDeviceObjectReference,
};
use bacnet_types::enums::{
AccessEvent, AccessRuleLocationSpecifier, AccessRuleTimeRangeSpecifier, AuthorizationExemption,
BinaryPV, ErrorClass, ErrorCode, ObjectType, PropertyIdentifier, ResolvedEnum,
};
use bacnet_types::error::Error;
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
use super::rights::unspecified;
use crate::common;
use crate::database::{LocalDevice, ObjectDatabase};
use crate::device_reference::decode_references;
use crate::traits::BACnetObject;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct AccessRightsEvaluation {
pub decision: AccessRightsDecision,
pub unresolved: Vec<UnresolvedAccessRights>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum AccessRightsDecision {
Exempt,
#[non_exhaustive]
Granted {
rule: AccessRulePosition,
},
#[non_exhaustive]
Denied {
access_event: AccessEvent,
rule: Option<AccessRulePosition>,
},
}
impl AccessRightsDecision {
pub fn allows(&self) -> bool {
matches!(self, Self::Exempt | Self::Granted { .. })
}
pub fn access_event(&self) -> Option<AccessEvent> {
match self {
Self::Denied { access_event, .. } => Some(*access_event),
_ => None,
}
}
pub fn rule(&self) -> Option<AccessRulePosition> {
match self {
Self::Granted { rule } => Some(*rule),
Self::Denied { rule, .. } => *rule,
Self::Exempt => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct AccessRulePosition {
pub access_rights: ObjectIdentifier,
pub kind: AccessRuleKind,
pub index: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum AccessRuleKind {
Positive,
Negative,
}
impl AccessRuleKind {
pub fn property(self) -> PropertyIdentifier {
match self {
Self::Positive => PropertyIdentifier::POSITIVE_ACCESS_RULES,
Self::Negative => PropertyIdentifier::NEGATIVE_ACCESS_RULES,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UnresolvedAccessRights {
pub index: u32,
pub reference: BACnetDeviceObjectReference,
pub reason: UnresolvedReason,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum UnresolvedReason {
Remote,
WildcardDevice,
NotAccessRights,
Missing,
Unreadable,
}
pub fn evaluate_access_rights(
db: &ObjectDatabase,
credential: ObjectIdentifier,
point: ObjectIdentifier,
) -> Result<AccessRightsEvaluation, Error> {
let holder = object_of_type(db, credential, ObjectType::ACCESS_CREDENTIAL)?;
object_of_type(db, point, ObjectType::ACCESS_POINT)?;
if exempt(holder) {
return Ok(AccessRightsEvaluation {
decision: AccessRightsDecision::Exempt,
unresolved: Vec::new(),
});
}
let mut scope = Scope {
db,
local: db.local_device(),
point,
zones: HashMap::new(),
};
let (gathered, unresolved) = scope.gather(&assigned_access_rights(holder)?);
Ok(AccessRightsEvaluation {
decision: scope.decide(&gathered),
unresolved,
})
}
fn object_of_type(
db: &ObjectDatabase,
oid: ObjectIdentifier,
object_type: ObjectType,
) -> Result<&dyn BACnetObject, Error> {
db.get(&oid)
.filter(|_| oid.object_type() == object_type)
.ok_or_else(|| common::protocol_error(ErrorClass::OBJECT, ErrorCode::UNKNOWN_OBJECT))
}
fn exempt(credential: &dyn BACnetObject) -> bool {
let access_rights = PropertyValue::Enumerated(AuthorizationExemption::ACCESS_RIGHTS.to_raw());
matches!(
credential.read_property(PropertyIdentifier::AUTHORIZATION_EXEMPTIONS, None),
Ok(PropertyValue::List(exemptions)) if exemptions.contains(&access_rights)
)
}
fn assigned_access_rights(
credential: &dyn BACnetObject,
) -> Result<Vec<BACnetAssignedAccessRights>, Error> {
let value = credential.read_property(PropertyIdentifier::ASSIGNED_ACCESS_RIGHTS, None)?;
common::decode_elements(
&value,
|tag| tag.is_opening_tag(0),
decode_assigned_access_rights,
)
}
struct Gathered {
oid: ObjectIdentifier,
negative: Vec<BACnetAccessRule>,
positive: Vec<BACnetAccessRule>,
}
impl Gathered {
fn rules(&self, kind: AccessRuleKind) -> &[BACnetAccessRule] {
match kind {
AccessRuleKind::Positive => &self.positive,
AccessRuleKind::Negative => &self.negative,
}
}
}
fn enabled_rules(
gathered: &[Gathered],
kind: AccessRuleKind,
) -> impl Iterator<Item = (AccessRulePosition, &BACnetAccessRule)> {
gathered.iter().flat_map(move |rights| {
(1..)
.zip(rights.rules(kind))
.filter(|(_, rule)| rule.enable)
.map(move |(index, rule)| {
let position = AccessRulePosition {
access_rights: rights.oid,
kind,
index,
};
(position, rule)
})
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Place {
Everywhere,
Point,
Zone,
}
impl Place {
fn denial(self) -> AccessEvent {
match self {
Place::Zone => AccessEvent::DENIED_ZONE_NO_ACCESS_RIGHTS,
Place::Point | Place::Everywhere => AccessEvent::DENIED_POINT_NO_ACCESS_RIGHTS,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TimeRange {
True,
False,
Never,
}
struct Scope<'a> {
db: &'a ObjectDatabase,
local: LocalDevice,
point: ObjectIdentifier,
zones: HashMap<ObjectIdentifier, bool>,
}
impl Scope<'_> {
fn is_local(&self, device: Option<ObjectIdentifier>) -> bool {
self.local.is_local(device)
}
fn gather(
&self,
assigned: &[BACnetAssignedAccessRights],
) -> (Vec<Gathered>, Vec<UnresolvedAccessRights>) {
let mut gathered = Vec::new();
let mut unresolved = Vec::new();
for (index, element) in (1..).zip(assigned) {
let reference = &element.assigned_access_rights;
if !element.enable
|| unspecified(reference.object_identifier, reference.device_identifier)
{
continue;
}
match self.resolve(reference) {
Ok(Some(rights)) => gathered.push(rights),
Ok(None) => {}
Err(reason) => unresolved.push(UnresolvedAccessRights {
index,
reference: reference.clone(),
reason,
}),
}
}
(gathered, unresolved)
}
fn resolve(
&self,
reference: &BACnetDeviceObjectReference,
) -> Result<Option<Gathered>, UnresolvedReason> {
let oid = reference.object_identifier;
if reference.device_identifier.is_some_and(is_wildcard_device) {
return Err(UnresolvedReason::WildcardDevice);
}
if !self.is_local(reference.device_identifier) {
return Err(UnresolvedReason::Remote);
}
if oid.object_type() != ObjectType::ACCESS_RIGHTS {
return Err(UnresolvedReason::NotAccessRights);
}
let object = self.db.get(&oid).ok_or(UnresolvedReason::Missing)?;
match object.read_property(PropertyIdentifier::LOG_ENABLE, None) {
Ok(PropertyValue::Boolean(true)) => {}
Ok(PropertyValue::Boolean(false)) => return Ok(None),
_ => return Err(UnresolvedReason::Unreadable),
}
let rules = |kind: AccessRuleKind| {
object
.read_property(kind.property(), None)
.and_then(|value| {
common::decode_elements(&value, |tag| tag.is_context(0), decode_access_rule)
})
.map_err(|_| UnresolvedReason::Unreadable)
};
Ok(Some(Gathered {
oid,
negative: rules(AccessRuleKind::Negative)?,
positive: rules(AccessRuleKind::Positive)?,
}))
}
fn decide(&mut self, gathered: &[Gathered]) -> AccessRightsDecision {
for (position, rule) in enabled_rules(gathered, AccessRuleKind::Negative) {
if let Some(place) = self.location(rule) {
if self.time_range(rule) == TimeRange::True {
return AccessRightsDecision::Denied {
access_event: place.denial(),
rule: Some(position),
};
}
}
}
let mut out_of_time = None;
for (position, rule) in enabled_rules(gathered, AccessRuleKind::Positive) {
if self.location(rule).is_some() {
match self.time_range(rule) {
TimeRange::True => return AccessRightsDecision::Granted { rule: position },
TimeRange::False => {
out_of_time.get_or_insert(position);
}
TimeRange::Never => {}
}
}
}
match out_of_time {
Some(position) => AccessRightsDecision::Denied {
access_event: AccessEvent::DENIED_OUT_OF_TIME_RANGE,
rule: Some(position),
},
None => AccessRightsDecision::Denied {
access_event: AccessEvent::DENIED_NO_ACCESS_RIGHTS,
rule: None,
},
}
}
fn location(&mut self, rule: &BACnetAccessRule) -> Option<Place> {
match rule.location_specifier {
AccessRuleLocationSpecifier::ALL => Some(Place::Everywhere),
AccessRuleLocationSpecifier::SPECIFIED => {
let reference = rule.location.as_ref()?;
let oid = reference.object_identifier;
if unspecified(oid, reference.device_identifier)
|| !self.is_local(reference.device_identifier)
{
return None;
}
match oid.object_type() {
ObjectType::ACCESS_POINT => (oid == self.point).then_some(Place::Point),
ObjectType::ACCESS_ZONE => self.enters(oid).then_some(Place::Zone),
_ => None,
}
}
_ => None,
}
}
fn enters(&mut self, zone: ObjectIdentifier) -> bool {
if let Some(&enters) = self.zones.get(&zone) {
return enters;
}
let enters = self.read_entry_points(zone);
self.zones.insert(zone, enters);
enters
}
fn read_entry_points(&self, zone: ObjectIdentifier) -> bool {
let Some(zone) = self.db.get(&zone) else {
return false;
};
let Ok(value) = zone.read_property(PropertyIdentifier::ENTRY_POINTS, None) else {
return false;
};
decode_references::<BACnetDeviceObjectReference>(&value).is_ok_and(|entries| {
entries.iter().any(|entry| {
entry.object_identifier == self.point && self.is_local(entry.device_identifier)
})
})
}
fn time_range(&self, rule: &BACnetAccessRule) -> TimeRange {
match rule.time_range_specifier {
AccessRuleTimeRangeSpecifier::ALWAYS => TimeRange::True,
AccessRuleTimeRangeSpecifier::SPECIFIED => rule
.time_range
.as_ref()
.map_or(TimeRange::Never, |reference| {
self.read_time_range(reference)
}),
_ => TimeRange::Never,
}
}
fn read_time_range(&self, reference: &BACnetDeviceObjectPropertyReference) -> TimeRange {
let oid = reference.object_identifier;
if unspecified(oid, reference.device_identifier)
|| !self.is_local(reference.device_identifier)
{
return TimeRange::Never;
}
let Some(object) = self.db.get(&oid) else {
return TimeRange::Never;
};
let property = PropertyIdentifier::from_raw(reference.property_identifier);
let index = reference.property_array_index;
if index == Some(0) || (index.is_some() && !object.is_array_property(property)) {
return TimeRange::Never;
}
match object.read_property(property, index) {
Ok(value) => time_range_value(property, &value),
Err(_) => TimeRange::Never,
}
}
}
fn time_range_value(property: PropertyIdentifier, value: &PropertyValue) -> TimeRange {
let holds = match *value {
PropertyValue::Boolean(value) => value,
PropertyValue::Unsigned(value) => value != 0,
PropertyValue::Signed(value) => value > 0,
PropertyValue::Enumerated(raw) => match ResolvedEnum::from_property(property, raw) {
ResolvedEnum::BinaryPV(state) => state == BinaryPV::ACTIVE,
ResolvedEnum::Unknown(raw) => raw == BinaryPV::ACTIVE.to_raw(),
_ => return TimeRange::Never,
},
_ => return TimeRange::Never,
};
if holds {
TimeRange::True
} else {
TimeRange::False
}
}
fn is_wildcard_device(device: ObjectIdentifier) -> bool {
device.object_type() == ObjectType::DEVICE
&& device.instance_number() == ObjectIdentifier::WILDCARD_INSTANCE
}
#[cfg(test)]
#[path = "rights_evaluation_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "rights_evaluation_term_tests.rs"]
mod term_tests;
#[cfg(test)]
#[path = "rights_evaluation_order_tests.rs"]
mod order_tests;
#[cfg(test)]
#[path = "rights_evaluation_scenario_tests.rs"]
mod scenario_tests;