zerodds_security/access_control.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Access control plugin SPI (OMG DDS-Security 1.1 §8.4).
5//!
6//! Decides per topic / per operation whether an authenticated
7//! participant may perform the action. Inputs:
8//! * Governance XML — topic-level rules (discovery, publishing,
9//! subscribing, encrypt flag).
10//! * Permissions XML — who may do what (signed with the permissions CA).
11//!
12//! The XML files are passed as properties (paths). The
13//! plugin parses + validates + caches internally.
14//!
15//! zerodds-lint: allow no_dyn_in_safe
16//! (The plugin SPI needs `Box<dyn AccessControlPlugin>`.)
17
18extern crate alloc;
19
20use alloc::boxed::Box;
21
22use crate::authentication::IdentityHandle;
23use crate::error::SecurityResult;
24use crate::properties::PropertyList;
25
26/// Opaque handle for validated permissions. Created by
27/// [`AccessControlPlugin::validate_local_permissions`] or
28/// `validate_remote_permissions`.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
30pub struct PermissionsHandle(pub u64);
31
32/// Whether an action may be performed. Deliberately sparse — no
33/// reason string, because the reason must not reveal anything to the
34/// remote (logging via [`crate::LoggingPlugin`]).
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum AccessDecision {
37 /// Allowed.
38 Permit,
39 /// Denied.
40 Deny,
41}
42
43impl AccessDecision {
44 /// `true` if `Permit`.
45 #[must_use]
46 pub fn is_permitted(self) -> bool {
47 matches!(self, Self::Permit)
48 }
49}
50
51/// Access control plugin trait (spec §8.4.2.9).
52pub trait AccessControlPlugin: Send + Sync {
53 /// Validates local permissions (Governance.xml + Permissions.xml
54 /// + signature check against the permissions CA).
55 ///
56 /// # Spec §8.4.2.9.1
57 fn validate_local_permissions(
58 &mut self,
59 local: IdentityHandle,
60 participant_guid: [u8; 16],
61 props: &PropertyList,
62 ) -> SecurityResult<PermissionsHandle>;
63
64 /// Validates remote permissions from the SEDP handshake.
65 ///
66 /// # Spec §8.4.2.9.2
67 fn validate_remote_permissions(
68 &mut self,
69 local: IdentityHandle,
70 remote: IdentityHandle,
71 remote_permissions_token: &[u8],
72 remote_credential: &[u8],
73 ) -> SecurityResult<PermissionsHandle>;
74
75 /// May this participant create a DataWriter on this topic?
76 ///
77 /// # Spec §8.4.2.9.4 `check_create_datawriter`.
78 fn check_create_datawriter(
79 &self,
80 perms: PermissionsHandle,
81 topic_name: &str,
82 ) -> SecurityResult<AccessDecision>;
83
84 /// May this participant create a DataReader on this topic?
85 ///
86 /// # Spec §8.4.2.9.5 `check_create_datareader`.
87 fn check_create_datareader(
88 &self,
89 perms: PermissionsHandle,
90 topic_name: &str,
91 ) -> SecurityResult<AccessDecision>;
92
93 /// May the local reader match the remote's publication?
94 ///
95 /// # Spec §8.4.2.9.17 `check_remote_datawriter_match`.
96 fn check_remote_datawriter_match(
97 &self,
98 local_perms: PermissionsHandle,
99 remote_perms: PermissionsHandle,
100 topic_name: &str,
101 ) -> SecurityResult<AccessDecision>;
102
103 /// Mirror image: may a remote reader match our writer?
104 fn check_remote_datareader_match(
105 &self,
106 local_perms: PermissionsHandle,
107 remote_perms: PermissionsHandle,
108 topic_name: &str,
109 ) -> SecurityResult<AccessDecision>;
110
111 /// Plugin class id (e.g. "DDS:Access:Permissions:1.2") for SPDP
112 /// announcing.
113 fn plugin_class_id(&self) -> &str;
114
115 /// Spec §9.4.2.5: `check_create_participant`. Default: permit
116 /// (no plugin-specific filtering).
117 ///
118 /// # Errors
119 /// Implementation-specific.
120 fn check_create_participant(
121 &self,
122 _local_perms: PermissionsHandle,
123 _domain_id: u32,
124 ) -> SecurityResult<AccessDecision> {
125 Ok(AccessDecision::Permit)
126 }
127
128 /// Spec §9.4.2.6: `check_remote_participant` — may the remote
129 /// participant join our domain? Default: permit.
130 ///
131 /// # Errors
132 /// Implementation-specific.
133 fn check_remote_participant(
134 &self,
135 _local_perms: PermissionsHandle,
136 _remote_perms: PermissionsHandle,
137 _domain_id: u32,
138 ) -> SecurityResult<AccessDecision> {
139 Ok(AccessDecision::Permit)
140 }
141
142 /// Spec §9.4.2.10: `check_create_topic` — may the local
143 /// subject create a topic with that name? Default: permit.
144 ///
145 /// # Errors
146 /// Implementation-specific.
147 fn check_create_topic(
148 &self,
149 _local_perms: PermissionsHandle,
150 _topic_name: &str,
151 ) -> SecurityResult<AccessDecision> {
152 Ok(AccessDecision::Permit)
153 }
154
155 /// Spec §9.4.2.13: `get_permissions_token` — opaque
156 /// permissions token for SPDP announcing
157 /// (`PID_PERMISSIONS_TOKEN` 0x1002). Default: empty.
158 ///
159 /// # Errors
160 /// Implementation-specific.
161 fn get_permissions_token(
162 &self,
163 _local_perms: PermissionsHandle,
164 ) -> SecurityResult<alloc::vec::Vec<u8>> {
165 Ok(alloc::vec::Vec::new())
166 }
167
168 /// Spec §9.4.2.14: `get_permissions_credential_token` — opaque
169 /// credential passed on in the authentication plugin via
170 /// `set_permissions_credential_and_token`.
171 /// Default: empty.
172 ///
173 /// # Errors
174 /// Implementation-specific.
175 fn get_permissions_credential_token(
176 &self,
177 _local_perms: PermissionsHandle,
178 ) -> SecurityResult<alloc::vec::Vec<u8>> {
179 Ok(alloc::vec::Vec::new())
180 }
181}
182
183/// Factory alias.
184pub type AccessControlBox = Box<dyn AccessControlPlugin>;
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn access_decision_helper() {
192 assert!(AccessDecision::Permit.is_permitted());
193 assert!(!AccessDecision::Deny.is_permitted());
194 }
195}