zerodds_security_permissions/
plugin.rs1use alloc::collections::BTreeMap;
7use alloc::string::String;
8use core::sync::atomic::{AtomicU64, Ordering};
9
10use zerodds_security::access_control::{AccessControlPlugin, AccessDecision, PermissionsHandle};
11use zerodds_security::authentication::IdentityHandle;
12use zerodds_security::error::{SecurityError, SecurityErrorKind, SecurityResult};
13use zerodds_security::properties::PropertyList;
14
15use crate::topic_match::topic_match;
16use crate::xml::{Grant, Permissions, parse_permissions_xml};
17
18pub const PROP_PERMISSIONS_XML: &str = "dds.sec.access.permissions";
20pub const PROP_SUBJECT_NAME: &str = "dds.sec.access.subject_name";
24
25pub struct PermissionsAccessControl {
28 next_handle: AtomicU64,
29 slots: BTreeMap<PermissionsHandle, Slot>,
30}
31
32struct Slot {
33 subject_name: String,
34 permissions: Permissions,
35}
36
37impl Default for PermissionsAccessControl {
38 fn default() -> Self {
39 Self::new()
40 }
41}
42
43impl PermissionsAccessControl {
44 #[must_use]
46 pub fn new() -> Self {
47 Self {
48 next_handle: AtomicU64::new(0),
49 slots: BTreeMap::new(),
50 }
51 }
52
53 fn next_id(&self) -> u64 {
54 self.next_handle.fetch_add(1, Ordering::Relaxed) + 1
55 }
56
57 pub fn register(
60 &mut self,
61 subject_name: String,
62 permissions: Permissions,
63 ) -> PermissionsHandle {
64 let handle = PermissionsHandle(self.next_id());
65 self.slots.insert(
66 handle,
67 Slot {
68 subject_name,
69 permissions,
70 },
71 );
72 handle
73 }
74
75 fn grant(&self, handle: PermissionsHandle) -> Option<(&str, Option<&Grant>)> {
76 let slot = self.slots.get(&handle)?;
77 let g = slot.permissions.find_grant(&slot.subject_name);
78 Some((slot.subject_name.as_str(), g))
79 }
80}
81
82fn topics_allow(patterns: &[String], topic: &str) -> bool {
83 patterns.iter().any(|p| topic_match(p, topic))
84}
85
86fn decide(grant: Option<&Grant>, topic: &str, is_publish: bool) -> AccessDecision {
87 match grant {
88 None => AccessDecision::Deny,
89 Some(g) => {
90 let hit = if is_publish {
91 topics_allow(&g.allow_publish_topics, topic)
92 } else {
93 topics_allow(&g.allow_subscribe_topics, topic)
94 };
95 if hit {
96 AccessDecision::Permit
97 } else if g.default_deny {
98 AccessDecision::Deny
99 } else {
100 AccessDecision::Permit
102 }
103 }
104 }
105}
106
107impl AccessControlPlugin for PermissionsAccessControl {
108 fn validate_local_permissions(
109 &mut self,
110 _local: IdentityHandle,
111 _participant_guid: [u8; 16],
112 props: &PropertyList,
113 ) -> SecurityResult<PermissionsHandle> {
114 let xml = props.get(PROP_PERMISSIONS_XML).ok_or_else(|| {
115 SecurityError::new(
116 SecurityErrorKind::InvalidConfiguration,
117 "permissions: fehlt dds.sec.access.permissions",
118 )
119 })?;
120 let subject = props.get(PROP_SUBJECT_NAME).ok_or_else(|| {
121 SecurityError::new(
122 SecurityErrorKind::InvalidConfiguration,
123 "permissions: fehlt dds.sec.access.subject_name",
124 )
125 })?;
126 let perms = parse_permissions_xml(xml).map_err(|e| {
127 SecurityError::new(
128 SecurityErrorKind::InvalidConfiguration,
129 alloc::format!("permissions: {e}"),
130 )
131 })?;
132 Ok(self.register(subject.to_string(), perms))
133 }
134
135 fn validate_remote_permissions(
136 &mut self,
137 _local: IdentityHandle,
138 _remote: IdentityHandle,
139 remote_permissions_token: &[u8],
140 _remote_credential: &[u8],
141 ) -> SecurityResult<PermissionsHandle> {
142 let xml = core::str::from_utf8(remote_permissions_token).map_err(|_| {
146 SecurityError::new(
147 SecurityErrorKind::BadArgument,
148 "permissions: remote_permissions_token ist kein UTF-8",
149 )
150 })?;
151 let perms = parse_permissions_xml(xml).map_err(|e| {
152 SecurityError::new(
153 SecurityErrorKind::BadArgument,
154 alloc::format!("permissions: {e}"),
155 )
156 })?;
157 let subject = perms
159 .grants
160 .first()
161 .map(|g| g.subject_name.clone())
162 .unwrap_or_default();
163 Ok(self.register(subject, perms))
164 }
165
166 fn check_create_datawriter(
167 &self,
168 perms: PermissionsHandle,
169 topic_name: &str,
170 ) -> SecurityResult<AccessDecision> {
171 let (_, g) = self.grant(perms).ok_or_else(|| {
172 SecurityError::new(
173 SecurityErrorKind::BadArgument,
174 "permissions: unbekannter PermissionsHandle",
175 )
176 })?;
177 Ok(decide(g, topic_name, true))
178 }
179
180 fn check_create_datareader(
181 &self,
182 perms: PermissionsHandle,
183 topic_name: &str,
184 ) -> SecurityResult<AccessDecision> {
185 let (_, g) = self.grant(perms).ok_or_else(|| {
186 SecurityError::new(
187 SecurityErrorKind::BadArgument,
188 "permissions: unbekannter PermissionsHandle",
189 )
190 })?;
191 Ok(decide(g, topic_name, false))
192 }
193
194 fn check_remote_datawriter_match(
195 &self,
196 _local: PermissionsHandle,
197 remote: PermissionsHandle,
198 topic_name: &str,
199 ) -> SecurityResult<AccessDecision> {
200 self.check_create_datawriter(remote, topic_name)
203 }
204
205 fn check_remote_datareader_match(
206 &self,
207 _local: PermissionsHandle,
208 remote: PermissionsHandle,
209 topic_name: &str,
210 ) -> SecurityResult<AccessDecision> {
211 self.check_create_datareader(remote, topic_name)
212 }
213
214 fn plugin_class_id(&self) -> &str {
215 "DDS:Access:Permissions:1.2"
216 }
217}
218
219#[cfg(test)]
220#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
221mod tests {
222 use super::*;
223 use zerodds_security::properties::Property;
224
225 const ALICE_XML: &str = r#"
226<permissions>
227 <grant><subject_name>CN=alice</subject_name>
228 <allow_rule>
229 <publish><topic>Chatter</topic><topic>sensor_*</topic></publish>
230 <subscribe><topic>Echo</topic></subscribe>
231 </allow_rule>
232 <default>DENY</default>
233 </grant>
234</permissions>
235"#;
236
237 fn build_alice() -> (PermissionsAccessControl, PermissionsHandle) {
238 let mut ac = PermissionsAccessControl::new();
239 let perms = parse_permissions_xml(ALICE_XML).unwrap();
240 let h = ac.register("CN=alice".into(), perms);
241 (ac, h)
242 }
243
244 #[test]
245 fn permit_on_exact_match() {
246 let (ac, h) = build_alice();
247 let d = ac.check_create_datawriter(h, "Chatter").unwrap();
248 assert_eq!(d, AccessDecision::Permit);
249 }
250
251 #[test]
252 fn permit_on_wildcard_match() {
253 let (ac, h) = build_alice();
254 let d = ac.check_create_datawriter(h, "sensor_temp").unwrap();
255 assert_eq!(d, AccessDecision::Permit);
256 }
257
258 #[test]
259 fn deny_on_non_matching_topic() {
260 let (ac, h) = build_alice();
261 let d = ac.check_create_datawriter(h, "actuator_x").unwrap();
262 assert_eq!(d, AccessDecision::Deny);
263 }
264
265 #[test]
266 fn deny_writer_when_only_subscribe_granted() {
267 let (ac, h) = build_alice();
268 let d = ac.check_create_datawriter(h, "Echo").unwrap();
269 assert_eq!(d, AccessDecision::Deny);
270 }
271
272 #[test]
273 fn permit_reader_on_subscribe_allowed_topic() {
274 let (ac, h) = build_alice();
275 let d = ac.check_create_datareader(h, "Echo").unwrap();
276 assert_eq!(d, AccessDecision::Permit);
277 }
278
279 #[test]
280 fn plugin_class_id_matches_spec() {
281 let ac = PermissionsAccessControl::new();
282 assert_eq!(ac.plugin_class_id(), "DDS:Access:Permissions:1.2");
283 }
284
285 #[test]
286 fn property_list_driver_loads_permissions() {
287 let mut ac = PermissionsAccessControl::new();
288 let props = PropertyList::new()
289 .with(Property::local(PROP_PERMISSIONS_XML, ALICE_XML.to_string()))
290 .with(Property::local(PROP_SUBJECT_NAME, "CN=alice"));
291 let h = ac
292 .validate_local_permissions(IdentityHandle(1), [0xAA; 16], &props)
293 .expect("validate via props");
294 assert_eq!(
295 ac.check_create_datawriter(h, "Chatter").unwrap(),
296 AccessDecision::Permit,
297 );
298 }
299
300 #[test]
301 fn missing_permissions_property_is_invalid_configuration() {
302 let mut ac = PermissionsAccessControl::new();
303 let props = PropertyList::new();
304 let err = ac
305 .validate_local_permissions(IdentityHandle(1), [0xAA; 16], &props)
306 .unwrap_err();
307 assert_eq!(err.kind, SecurityErrorKind::InvalidConfiguration);
308 }
309}