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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
//! Executing an inbound WriteGroup request (Clause 15.11, #1151).
//!
//! A WriteGroup names a control group, a write priority and a change list of
//! channel numbers, each with a BACnetChannelValue and maybe a priority of its
//! own. The server writes each value to the Present_Value of every local
//! Channel whose Channel_Number is that channel and whose Control_Groups holds
//! the group, at the entry's priority when it has one and at the request's
//! otherwise. Membership is read Channel by Channel: Clause 12.53.15 makes a
//! Channel, not its device, the member of a group, so a Channel outside the
//! group is left alone even when another Channel here is in it. Group 0 means
//! no assignment and never reaches here: the codec refuses it.
//!
//! Each value goes through the same [`LocalWriter`] path as `write_local` and
//! a Command's writes, so the Channel's own checks hold (BUSY while it
//! distributes, the value and priority checks, Out_Of_Service), and the
//! distribution the write queues starts through the server's
//! [`CommandRunner`]: coercion, Write_Status, the run chain, delays
//! and COV all behave as for a WriteProperty. One refused write is logged at
//! debug and the rest still go (Clause 15.11.2). The service is unconfirmed:
//! nothing goes back, and a request the codec refuses is dropped.
//!
//! The plan is made under a read guard and each write takes the write guard
//! afterwards, so another write can change a Channel in between.
//! [`LocalWriter`] checks the Channel again under its write guard: one that
//! has left the group or taken another Channel_Number since the plan is
//! skipped, logged at debug, and Allow_Group_Delay_Inhibit is read there too.
//!
//! A channel number listed twice in one change list reaches the same
//! Channels twice, in list order. The later value is refused as BUSY while
//! the distribution the earlier one queued is still running and taken once
//! it has finished, so which value stays depends on timing.
//!
//! The Inhibit Delay flag zeroes the delays of a Channel's distribution only
//! when that Channel's Allow_Group_Delay_Inhibit is TRUE (Clause 12.53.13);
//! otherwise each member waits its Execution_Delay as usual.
//!
//! Local mutation policy decides each Channel write on its own, just before
//! it is made and with no database guard held (#1319):
//! [`MutationPolicy::DenyAll`] denies it, and an installed policy sees a
//! [`MutationTarget::WriteGroup`] naming the Channel, the entry and the
//! priority, with the requester's addresses and provenance, no invoke ID and
//! the unconfirmed service. A denied write is skipped with nothing sent back,
//! since the service is unconfirmed, and counted in the WriteGroup mutation
//! decision counters; the other Channels are decided on their own.
//!
//! Each write that goes ahead is audited as a WRITE, the operation Table 19-5
//! gives WriteGroup, of that Channel's Present_Value at the priority used:
//! one record per Channel written, as Clause 19.6.5 asks of a service with
//! several targets. The requester is the source (the Device bound to its
//! address, when the audit profile knows one), with no invoke ID, which an
//! unconfirmed request doesn't have (Table 19-4). A denied write makes no
//! record (#1318). Under DCC's DISABLE_INITIATION a WriteGroup still runs: it
//! initiates nothing.
use super::audit_reporter::WriteAudit;
use super::command_runs::CommandRunner;
use super::local_writes::{LocalWrite, LocalWriter};
use super::request_services::UnconfirmedServices;
use super::*;
use crate::command_lists::TakenRuns;
use crate::mutation::{
MutationAuthorizationContext, MutationDecision, MutationDecisions, MutationPolicy,
MutationService, MutationTarget, MutationTrust, WriteGroupTarget,
};
use bacnet_network::layer::ReceivedApdu;
use bacnet_objects::command::RunPlan;
use bacnet_objects::traits::BACnetObject;
use bacnet_services::write_group::WriteGroupRequest;
use bacnet_transport::port::TransportProvenance;
use bacnet_types::constructed::BACnetRecipient;
const WRITE_GROUP: MutationService =
MutationService::Unconfirmed(UnconfirmedServiceChoice::WRITE_GROUP);
/// One Channel Present_Value write a WriteGroup asks for.
#[derive(Debug, Clone, PartialEq)]
struct GroupWrite<'r> {
channel: ObjectIdentifier,
/// The change-list entry's channel number, the Channel's Channel_Number
/// when planned.
number: u16,
/// The entry's BACnetChannelValue, as the codec checked it.
value: &'r [u8],
priority: u8,
}
/// Who sent a WriteGroup, and the policy its Channel writes are made under.
struct Requester<'a> {
config: &'a ServerConfig,
decisions: &'a MutationDecisions,
source_mac: &'a [u8],
source_network: Option<&'a NpduAddress>,
provenance: TransportProvenance,
/// The source the writes' Audit records name.
audit_source: BACnetRecipient,
}
impl Requester<'_> {
/// Whether local policy lets `write`, one Channel write of `request`, go
/// ahead; the decision is counted. An installed policy is asked off the
/// request task, as for the confirmed services (#2032).
async fn admits(&self, request: &WriteGroupRequest, write: &GroupWrite<'_>) -> bool {
let decision = if self.config.mutation_policy == MutationPolicy::DenyAll {
MutationDecision::PolicyDeny
} else if let Some(policy) = self.config.mutation_decider() {
let context = MutationAuthorizationContext {
source_mac: MacAddr::from_slice(self.source_mac),
source_network: self.source_network.cloned(),
provenance: self.provenance.clone(),
trust: MutationTrust::from_provenance(&self.provenance),
invoke_id: None,
service_choice: WRITE_GROUP,
target: MutationTarget::WriteGroup(WriteGroupTarget {
channel: write.channel,
group_number: request.group_number.get(),
channel_number: write.number,
priority: write.priority,
value: write.value.to_vec(),
inhibit_delay: request.inhibit_delay == Some(true),
}),
};
// A callback that panics denies, as for the confirmed services.
if super::requests::ask_mutation_policy(policy, vec![context]).await == [true] {
MutationDecision::Allow
} else {
MutationDecision::Deny
}
} else {
MutationDecision::Allow
};
self.decisions.record(WRITE_GROUP, decision);
matches!(decision, MutationDecision::Allow)
}
}
/// The Channel_Number `object` serves, if it serves one that fits.
fn channel_number(object: &dyn BACnetObject) -> Option<u16> {
match object.read_property(PropertyIdentifier::CHANNEL_NUMBER, None) {
Ok(PropertyValue::Unsigned(number)) => u16::try_from(number).ok(),
_ => None,
}
}
/// Whether `object`'s Control_Groups holds `group`.
fn in_group(object: &dyn BACnetObject, group: u32) -> bool {
let wanted = PropertyValue::Unsigned(group.into());
match object.read_property(PropertyIdentifier::CONTROL_GROUPS, None) {
Ok(PropertyValue::List(groups)) => groups.contains(&wanted),
Ok(single) => single == wanted,
Err(_) => false,
}
}
/// Whether `object` still takes a WriteGroup value for `group` and channel
/// `number`: its Control_Groups holds the group and its Channel_Number is the
/// number.
pub(super) fn qualifies(object: &dyn BACnetObject, group: u32, number: u16) -> bool {
in_group(object, group) && channel_number(object) == Some(number)
}
/// Whether `object`'s Allow_Group_Delay_Inhibit is present and TRUE.
pub(super) fn allows_delay_inhibit(object: &dyn BACnetObject) -> bool {
matches!(
object.read_property(PropertyIdentifier::ALLOW_GROUP_DELAY_INHIBIT, None),
Ok(PropertyValue::Boolean(true))
)
}
/// Zero the member delays of the distribution `channel` queued in `runs`.
pub(super) fn skip_delays(runs: &mut TakenRuns, channel: ObjectIdentifier) {
for run in runs.iter_mut().filter(|run| run.source == channel) {
if let RunPlan::Channel(distribution) = &mut run.plan {
for member in &mut distribution.members {
member.delay_ms = 0;
}
}
}
}
/// The writes `request` asks of `db`'s Channels: change-list order, and
/// Channels by instance within one entry.
fn plan<'r>(db: &ObjectDatabase, request: &'r WriteGroupRequest) -> Vec<GroupWrite<'r>> {
let group = request.group_number.get();
let mut members: Vec<(ObjectIdentifier, u16)> = db
.find_by_type(ObjectType::CHANNEL)
.into_iter()
.filter_map(|oid| {
let object = db.get(&oid)?;
if !in_group(object, group) {
return None;
}
Some((oid, channel_number(object)?))
})
.collect();
members.sort_by_key(|(oid, _)| oid.instance_number());
request
.change_list
.iter()
.flat_map(|entry| {
let priority = entry.override_priority.unwrap_or(request.write_priority);
members
.iter()
.filter(move |(_, number)| *number == entry.channel)
.map(move |&(channel, number)| GroupWrite {
channel,
number,
value: &entry.value,
priority,
})
})
.collect()
}
/// Write one value to its Channel on behalf of `requester`; the runs that
/// queued. [`LocalWriter`] checks the Channel again and applies the inhibit
/// under its write guard.
async fn write_channel<T: TransportPort + 'static>(
writer: &LocalWriter<'_, T>,
request: &WriteGroupRequest,
write: &GroupWrite<'_>,
requester: &BACnetRecipient,
) -> Result<TakenRuns, Error> {
// A Channel's Present_Value is one value, never a list.
let value = handlers::decode_write_property_value(
PropertyIdentifier::PRESENT_VALUE,
None,
false,
write.value,
)?;
let write_group = LocalWrite::WriteGroup {
group: request.group_number.get(),
number: write.number,
priority: write.priority,
inhibit_delay: request.inhibit_delay == Some(true),
requester,
};
writer.write(&write.channel, write_group, value, None).await
}
impl<T: TransportPort + 'static> BACnetServer<T> {
/// Execute an inbound WriteGroup's change list on the local Channels.
pub(super) async fn execute_write_group(
services: &UnconfirmedServices<T>,
service_request: &[u8],
received: &ReceivedApdu,
) {
let request = match WriteGroupRequest::decode(service_request) {
Ok(request) => request,
Err(error) => {
debug!(%error, "Ignoring malformed WriteGroup");
return;
}
};
let writes = plan(&*services.db.read().await, &request);
if writes.is_empty() {
return;
}
let audit_source = WriteAudit::<T>::requester(
&services.config,
&services.notification_transactions,
&services.device_bindings,
&received.source_mac,
received.source_network.as_ref(),
services.network.local_network_number().get(),
)
.await;
let requester = Requester {
config: &services.config,
decisions: &services.mutation_decisions,
source_mac: &received.source_mac,
source_network: received.source_network.as_ref(),
provenance: received.provenance.clone(),
audit_source,
};
let runner = CommandRunner::for_unconfirmed(services);
apply(&runner, &requester, &request, &writes).await;
}
}
/// Make `writes` in order, each one `requester`'s policy admits, and start
/// the runs they queue. A denied or refused write is logged and the rest
/// still go.
async fn apply<T: TransportPort + 'static>(
runner: &CommandRunner<T>,
requester: &Requester<'_>,
request: &WriteGroupRequest,
writes: &[GroupWrite<'_>],
) {
let writer = runner.writer();
for write in writes {
if !requester.admits(request, write).await {
debug!(
channel = %write.channel,
priority = write.priority,
"WriteGroup value denied by local mutation policy"
);
continue;
}
match write_channel(&writer, request, write, &requester.audit_source).await {
Ok(runs) => runner.start(runs),
Err(error) => debug!(
channel = %write.channel,
priority = write.priority,
%error,
"WriteGroup value refused by its Channel"
),
}
}
}
#[cfg(test)]
#[path = "write_group_tests.rs"]
mod tests;