Skip to main content

zerodds_security_runtime/
bundle.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! One-stop security configuration facade for a participant.
5//!
6//! [`SecurityBundle`] groups the two things a participant needs to run with
7//! DDS-Security observability — the access-control/crypto [`SecurityProfile`]
8//! and the security-event [`LoggingPlugin`] — behind a single builder. It is
9//! handed to the runtime via `RuntimeConfig::with_security_bundle`, which wires
10//! the profile into `RuntimeConfig.security` and the logger into
11//! `RuntimeConfig.security_logger`.
12//!
13//! ```
14//! use zerodds_security_runtime::SecurityBundle;
15//! use zerodds_security_logging::StderrLoggingPlugin;
16//! use zerodds_security_runtime::LogLevel;
17//!
18//! let bundle = SecurityBundle::builder()
19//!     .logging_plugin(Box::new(StderrLoggingPlugin::with_level(LogLevel::Warning)))
20//!     .build();
21//! assert!(bundle.has_logging());
22//! ```
23
24// The security-event logger is a runtime plug-in boundary; `Arc/Box<dyn LoggingPlugin>` is the intended heterogeneous sink type (mirrors RuntimeConfig.security_logger).
25// zerodds-lint: allow no_dyn_in_safe
26
27use alloc::boxed::Box;
28use alloc::sync::Arc;
29
30use crate::profile::SecurityProfile;
31use zerodds_security::logging::LoggingPlugin;
32
33/// Bundles the security configuration a participant needs — the
34/// access-control/crypto [`SecurityProfile`] and the security-event
35/// [`LoggingPlugin`] — behind one builder, ready to hand to a runtime config.
36///
37/// Build it with [`SecurityBundle::builder`] and apply it via
38/// `RuntimeConfig::with_security_bundle` (in `zerodds-dcps`).
39#[derive(Clone, Default)]
40pub struct SecurityBundle {
41    logging_plugin: Option<Arc<dyn LoggingPlugin>>,
42    profile: Option<Arc<SecurityProfile>>,
43}
44
45impl SecurityBundle {
46    /// Start building a bundle.
47    #[must_use]
48    pub fn builder() -> SecurityBundleBuilder {
49        SecurityBundleBuilder::default()
50    }
51
52    /// The security-event logger, if one was configured. The returned `Arc`
53    /// is exactly what `RuntimeConfig.security_logger` expects.
54    #[must_use]
55    pub fn logging_plugin(&self) -> Option<Arc<dyn LoggingPlugin>> {
56        self.logging_plugin.clone()
57    }
58
59    /// The access-control/crypto profile, if one was configured.
60    #[must_use]
61    pub fn security_profile(&self) -> Option<Arc<SecurityProfile>> {
62        self.profile.clone()
63    }
64
65    /// `true` if a security-event logger is configured.
66    #[must_use]
67    pub fn has_logging(&self) -> bool {
68        self.logging_plugin.is_some()
69    }
70
71    /// `true` if an access-control/crypto profile is configured.
72    #[must_use]
73    pub fn has_profile(&self) -> bool {
74        self.profile.is_some()
75    }
76}
77
78/// Builder for [`SecurityBundle`]. Every setter is optional and chainable.
79#[derive(Default)]
80pub struct SecurityBundleBuilder {
81    logging_plugin: Option<Arc<dyn LoggingPlugin>>,
82    profile: Option<Arc<SecurityProfile>>,
83}
84
85impl SecurityBundleBuilder {
86    /// Set the security-event logger. Accepts any boxed [`LoggingPlugin`] —
87    /// e.g. a `FanOutLoggingPlugin` fanning out to stderr plus an audit file.
88    #[must_use]
89    pub fn logging_plugin(mut self, plugin: Box<dyn LoggingPlugin>) -> Self {
90        self.logging_plugin = Some(Arc::from(plugin));
91        self
92    }
93
94    /// Set the access-control/crypto profile, e.g. from
95    /// [`SecurityProfile::from_files`].
96    #[must_use]
97    pub fn security_profile(mut self, profile: SecurityProfile) -> Self {
98        self.profile = Some(Arc::new(profile));
99        self
100    }
101
102    /// Finalize the bundle.
103    #[must_use]
104    pub fn build(self) -> SecurityBundle {
105        SecurityBundle {
106            logging_plugin: self.logging_plugin,
107            profile: self.profile,
108        }
109    }
110}
111
112#[cfg(test)]
113#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
114mod tests {
115    use super::*;
116    use alloc::sync::Arc;
117    use core::sync::atomic::{AtomicUsize, Ordering};
118    use zerodds_security::logging::LogLevel;
119
120    #[derive(Default)]
121    struct CountingLogger {
122        calls: Arc<AtomicUsize>,
123    }
124    impl LoggingPlugin for CountingLogger {
125        fn log(&self, _level: LogLevel, _participant: [u8; 16], _category: &str, _message: &str) {
126            self.calls.fetch_add(1, Ordering::SeqCst);
127        }
128        fn plugin_class_id(&self) -> &str {
129            "test:counting"
130        }
131    }
132
133    #[test]
134    fn empty_bundle_has_no_logging_or_profile() {
135        let b = SecurityBundle::builder().build();
136        assert!(!b.has_logging());
137        assert!(!b.has_profile());
138        assert!(b.logging_plugin().is_none());
139    }
140
141    #[test]
142    fn bundle_carries_the_logger_and_forwards_log_calls() {
143        let calls = Arc::new(AtomicUsize::new(0));
144        let logger = CountingLogger {
145            calls: Arc::clone(&calls),
146        };
147        let bundle = SecurityBundle::builder()
148            .logging_plugin(Box::new(logger))
149            .build();
150
151        assert!(bundle.has_logging());
152        let plugin = bundle.logging_plugin().expect("logger present");
153        plugin.log(LogLevel::Warning, [0u8; 16], "auth", "test event");
154        assert_eq!(calls.load(Ordering::SeqCst), 1);
155    }
156
157    #[test]
158    fn bundle_is_cloneable_and_shares_the_same_logger() {
159        let calls = Arc::new(AtomicUsize::new(0));
160        let bundle = SecurityBundle::builder()
161            .logging_plugin(Box::new(CountingLogger {
162                calls: Arc::clone(&calls),
163            }))
164            .build();
165        let clone = bundle.clone();
166        bundle
167            .logging_plugin()
168            .unwrap()
169            .log(LogLevel::Error, [0u8; 16], "crypto", "a");
170        clone
171            .logging_plugin()
172            .unwrap()
173            .log(LogLevel::Error, [0u8; 16], "crypto", "b");
174        // Both handles point at the same underlying logger.
175        assert_eq!(calls.load(Ordering::SeqCst), 2);
176    }
177}