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