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
//! Pre-start execution profile and authority for the endpoint Device responder.
use super::*;
use bacnet_server::mutation::MutationAuthorizer;
use bacnet_types::enums::ServiceSupported;
const SERVICES: &[ServiceSupported] = &[
ServiceSupported::READ_PROPERTY,
ServiceSupported::WRITE_PROPERTY,
];
impl<T: TransportPort + 'static> EndpointSession<T> {
/// Enables authorized writes to the local Device's `Description` and installed Audit recipient.
///
/// The callback is mandatory and receives the existing redacted mutation
/// context. A refusal or panic denies the request before mutation. It must
/// be fast, nonblocking and side-effect-free; see [`MutationAuthorizer`].
/// Other objects/properties and WritePropertyMultiple remain unsupported.
/// An authorized NULL relinquishment succeeds without changing Description.
/// Recipient writes require the complete source runtime installed by source selection.
///
/// Startup requires a server role and a concrete built-in local Device in
/// the attached database ([`ObjectDatabase::local_device`]: the lowest
/// instance when it holds several). Writes reach only that Device; any
/// other Device is out of scope. A composed identity must match it and
/// contain only ReadProperty/WriteProperty service bits. Validation errors
/// precede configuration mutation and transport startup, allowing correction
/// and retry. The enabled Device and identity advertise exactly those two
/// services; the default session remains ReadProperty-only.
///
/// # Panics
/// Panics if startup has already consumed the session configuration.
pub fn with_device_writes(mut self, authorizer: MutationAuthorizer) -> Self {
self.assert_configurable();
self.device_write_authorizer = Some(authorizer);
self
}
pub(super) fn validate_device_execution(&mut self) -> Result<Option<ObjectIdentifier>, Error> {
let writes = self.device_write_authorizer.is_some();
if self.role == SessionRole::ClientOnly {
return if writes {
Err(Error::Encoding(
"Device writes require a server role".into(),
))
} else {
Ok(None)
};
}
let allowed = if writes {
SERVICES
} else {
&[ServiceSupported::READ_PROPERTY]
};
if self.identity.as_ref().is_some_and(|identity| {
!identity
.services()
.contains(&ServiceSupported::READ_PROPERTY)
|| identity
.services()
.iter()
.any(|service| !allowed.contains(service))
}) {
return Err(Error::Encoding(
"Endpoint identity services do not match the RP[/WP] responder".into(),
));
}
if !writes {
return Ok(None);
}
let db = self.database.as_mut().ok_or_else(|| {
Error::Encoding("Device writes require an attached local database".into())
})?;
let db = Arc::get_mut(db)
.expect("database is unshared before startup")
.get_mut();
// The write target is the selected Device, the one wildcard reads
// resolve to, and it must be concrete.
let oid = db.local_device().identifier().ok_or_else(|| {
Error::Encoding("Device writes require a concrete local Device".into())
})?;
if self
.identity
.as_ref()
.is_some_and(|identity| identity.device_oid() != oid)
{
return Err(Error::Encoding(
"Device writes local Device does not match session identity".into(),
));
}
if !db
.get_mut(&oid)
.expect("Device exists")
.device_authority_internal()
.is_some_and(|device| device.object_identifier() == oid)
{
return Err(Error::Encoding(
"Device writes require the built-in Device authority".into(),
));
}
Ok(Some(oid))
}
// All fallible configuration validation (including source ownership) precedes
// this commit. No await/callback or second copy of Device state is involved.
pub(super) fn commit_device_write_profile(&mut self, target: Option<ObjectIdentifier>) {
let Some(oid) = target else { return };
let db = Arc::get_mut(self.database.as_mut().expect("validated database"))
.expect("database is unshared before startup")
.get_mut();
db.get_mut(&oid)
.expect("validated Device")
.device_authority_internal()
.expect("validated Device authority")
.set_services_supported(SERVICES);
if let Some(identity) = self.identity.take() {
self.identity = Some(identity.with_services(SERVICES));
}
}
}