Skip to main content

bacnet_server/server/
local_authorization.rs

1//! Local completed-attempt authorization and its recorded event evidence.
2
3use super::cov_fanout::CovFanout;
4use super::local_writes::local_runtime;
5use super::*;
6use bacnet_objects::access_control::{
7    AccessEventReport, AuthorizationEvaluation, AuthorizationOutcome, AuthorizationRequest,
8    evaluate_authorization,
9};
10use bacnet_types::enums::Reliability;
11use tokio::sync::{OwnedRwLockReadGuard, OwnedRwLockWriteGuard};
12
13/// Complete evaluation and the event records installed by one local attempt.
14#[derive(Debug, Clone, PartialEq)]
15pub struct LocalAuthorizationResult {
16    /// The full evaluator result, including diagnostic and intermediate intents.
17    pub evaluation: AuthorizationEvaluation,
18    /// Records actually installed, in intent order, with their assigned timestamps.
19    /// Empty for pending, unresolved, and no-authorization evaluations.
20    pub committed_events: Vec<AccessEventReport>,
21}
22
23impl<T: TransportPort + 'static> BACnetServer<T> {
24    /// Evaluate an authenticated local presentation and record a terminal result.
25    ///
26    /// A grant or denial installs all its event intents in order; pending,
27    /// unresolved and no-authorization results publish nothing, including their
28    /// intermediate intents. The full request supplies every evaluation policy
29    /// and operational fact. The caller owns retries, tag progression and replay
30    /// handling: repeating a terminal attempt is a new publication, even when its
31    /// tag is unchanged. Evaluation and event installation share one DB guard.
32    ///
33    /// Each event names its associated credential (the companion for BY) and is
34    /// stamped by the point. No physical factor is supplied, so the absent-factor
35    /// UNDEFINED representation applies (Clauses 12.31.27–31). Equal/backward
36    /// clocks advance the event trigger, and an exhausted timestamp representation
37    /// refuses the entire batch. This route performs no physical door action.
38    ///
39    /// Runtime, stopped-network, unknown/wrong object, unsupported batch, malformed
40    /// admission and out-of-service/fault errors leave state unchanged. Contention
41    /// retries release all guards before waiting and observe fresh state; progress
42    /// is cooperative once contention quiesces, not a starvation/latency guarantee.
43    ///
44    /// A committed attempt retains snapshots for ordered COV fanout through the
45    /// existing notification lifecycle. Caller cancellation after commit does not
46    /// discard owned work. DCC, budgets, send failures and shutdown still apply;
47    /// success is not remote acknowledgment or durable delivery. Independent
48    /// transactions have no global notification-order guarantee.
49    pub async fn authorize_access_local(
50        &self,
51        request: AuthorizationRequest<'_>,
52        tag: u64,
53    ) -> Result<LocalAuthorizationResult, Error> {
54        let runtime = local_runtime()?;
55        self.active_network()?;
56        let oid = request.original.point;
57        let (mut db, table) = self.authorization_guards().await;
58        admit(&db, &oid)?;
59        let evaluation = evaluate_authorization(&db, request)?;
60        if !matches!(
61            evaluation.outcome,
62            AuthorizationOutcome::Granted | AuthorizationOutcome::Denied { .. }
63        ) {
64            return Ok(LocalAuthorizationResult {
65                evaluation,
66                committed_events: Vec::new(),
67            });
68        }
69        let reports: Vec<_> = evaluation
70            .events
71            .iter()
72            .map(|intent| AccessEventReport {
73                credential: Some(intent.presentation.credential.into()),
74                ..AccessEventReport::new(intent.access_event, tag)
75            })
76            .collect();
77        let committed = db
78            .get_mut(&oid)
79            .expect("admitted point remains under the same database guard")
80            .record_access_event_batch_internal(&reports)?;
81        let records = committed.iter().map(|event| event.report.clone()).collect();
82        for event in &committed {
83            table
84                .timed_capture(oid)
85                .run_from_snapshot(event.snapshot.as_ref(), event.clock_frame);
86        }
87        // No await after the atomic install and capture until post-commit work
88        // belongs to the server's task set. The COV read guard excludes admission
89        // through capture, and the database guard is released before fanout awaits.
90        drop(table);
91        let fanout = CovFanout::new(&self.local_writer().cov_context(), &self.event_suppressions);
92        let routers = Arc::clone(&self.learned_routers);
93        let bindings = Arc::clone(&self.device_bindings);
94        self.finish_in_task(&runtime, async move {
95            drop(db);
96            Self::fire_event_notifications_with_bindings(
97                &fanout.event_delivery(&routers, &bindings),
98                &fanout.cov_table,
99                &oid,
100            )
101            .await;
102            for event in committed {
103                Self::fire_cov_notifications_from_snapshot(
104                    &fanout.notify_context(),
105                    &oid,
106                    event.snapshot.as_ref(),
107                )
108                .await;
109            }
110        })
111        .await?;
112        Ok(LocalAuthorizationResult {
113            evaluation,
114            committed_events: records,
115        })
116    }
117
118    /// Acquire DB before COV without retaining a lock across either queued wait.
119    /// A failed try-read performs no evaluation and retains no subscription data.
120    async fn authorization_guards(
121        &self,
122    ) -> (
123        OwnedRwLockWriteGuard<ObjectDatabase>,
124        OwnedRwLockReadGuard<CovSubscriptionTable>,
125    ) {
126        loop {
127            let db = Arc::clone(&self.db).write_owned().await;
128            if let Ok(table) = Arc::clone(&self.cov_table).try_read_owned() {
129                return (db, table);
130            }
131            drop(db);
132            drop(self.cov_table.read().await);
133            tokio::task::yield_now().await;
134        }
135    }
136}
137
138/// Reject unhealthy or malformed admission before invoking the pure evaluator.
139fn admit(db: &ObjectDatabase, oid: &ObjectIdentifier) -> Result<(), Error> {
140    let object = db.get(oid).ok_or(Error::protocol(
141        ErrorClass::OBJECT,
142        ErrorCode::UNKNOWN_OBJECT,
143    ))?;
144    if oid.object_type() != ObjectType::ACCESS_POINT {
145        return Err(Error::protocol(
146            ErrorClass::OBJECT,
147            ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED,
148        ));
149    }
150    let healthy = match (
151        object.read_property(PropertyIdentifier::OUT_OF_SERVICE, None)?,
152        object.read_property(PropertyIdentifier::RELIABILITY, None)?,
153    ) {
154        (PropertyValue::Boolean(out_of_service), PropertyValue::Enumerated(reliability)) => {
155            !out_of_service && reliability == Reliability::NO_FAULT_DETECTED.to_raw()
156        }
157        _ => {
158            return Err(Error::protocol(
159                ErrorClass::PROPERTY,
160                ErrorCode::INVALID_DATA_TYPE,
161            ));
162        }
163    };
164    if !healthy {
165        return Err(Error::protocol(
166            ErrorClass::PROPERTY,
167            ErrorCode::WRITE_ACCESS_DENIED,
168        ));
169    }
170    if !object.supports_access_event_batch_internal() {
171        return Err(Error::protocol(
172            ErrorClass::OBJECT,
173            ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED,
174        ));
175    }
176    Ok(())
177}