aimdb_core/remote/
config.rs

1//! Configuration types for AimX remote access
2
3use std::{collections::HashSet, path::PathBuf, string::String, vec::Vec};
4
5use crate::record_id::StringKey;
6
7/// Configuration for AimX remote access
8///
9/// Defines how the remote access layer behaves, including socket path,
10/// security policy, connection limits, and subscription queue sizes.
11#[derive(Debug, Clone)]
12pub struct AimxConfig {
13    /// Path to Unix domain socket
14    pub socket_path: PathBuf,
15
16    /// Security policy (read-only or read-write)
17    pub security_policy: SecurityPolicy,
18
19    /// Maximum number of concurrent connections
20    pub max_connections: usize,
21
22    /// Subscription queue size per client per subscription
23    pub subscription_queue_size: usize,
24
25    /// Optional authentication token
26    pub auth_token: Option<String>,
27
28    /// File permissions for the socket (Unix only)
29    /// Format: octal mode (e.g., 0o600 for owner-only)
30    pub socket_permissions: Option<u32>,
31}
32
33impl AimxConfig {
34    /// Creates a default UDS configuration
35    ///
36    /// # Defaults
37    /// - Socket path: `/tmp/aimdb.sock`
38    /// - Security policy: Read-only
39    /// - Max connections: 16
40    /// - Subscription queue size: 100
41    /// - No auth token
42    /// - Socket permissions: 0o600 (owner-only)
43    pub fn uds_default() -> Self {
44        Self {
45            socket_path: PathBuf::from("/tmp/aimdb.sock"),
46            security_policy: SecurityPolicy::ReadOnly,
47            max_connections: 16,
48            subscription_queue_size: 100,
49            auth_token: None,
50            socket_permissions: Some(0o600),
51        }
52    }
53
54    /// Sets the socket path
55    pub fn socket_path(mut self, path: impl Into<PathBuf>) -> Self {
56        self.socket_path = path.into();
57        self
58    }
59
60    /// Sets the security policy
61    pub fn security_policy(mut self, policy: SecurityPolicy) -> Self {
62        self.security_policy = policy;
63        self
64    }
65
66    /// Sets the maximum number of concurrent connections
67    pub fn max_connections(mut self, max: usize) -> Self {
68        self.max_connections = max;
69        self
70    }
71
72    /// Sets the subscription queue size per client
73    pub fn subscription_queue_size(mut self, size: usize) -> Self {
74        self.subscription_queue_size = size;
75        self
76    }
77
78    /// Sets an authentication token
79    pub fn auth_token(mut self, token: impl Into<String>) -> Self {
80        self.auth_token = Some(token.into());
81        self
82    }
83
84    /// Sets the socket file permissions (Unix only)
85    ///
86    /// # Example
87    /// ```rust,ignore
88    /// config.socket_permissions(0o600)  // Owner only
89    /// config.socket_permissions(0o660)  // Owner + group
90    /// ```
91    pub fn socket_permissions(mut self, mode: u32) -> Self {
92        self.socket_permissions = Some(mode);
93        self
94    }
95}
96
97/// Security policy for remote access
98///
99/// Defines which operations are permitted and for which records.
100#[derive(Debug, Clone)]
101pub enum SecurityPolicy {
102    /// Read-only access (list, get, subscribe)
103    ///
104    /// This is the default and recommended policy for most deployments.
105    /// No write operations are permitted.
106    ReadOnly,
107
108    /// Read-write access with explicit per-record opt-in
109    ///
110    /// Write operations (`record.set`) are only allowed for records
111    /// whose RecordKey is in the `writable_records` set.
112    ReadWrite {
113        /// Set of RecordKeys that allow write operations
114        writable_records: HashSet<String>,
115    },
116}
117
118impl SecurityPolicy {
119    /// Creates a read-only policy
120    pub fn read_only() -> Self {
121        Self::ReadOnly
122    }
123
124    /// Creates a read-write policy with no writable records initially
125    pub fn read_write() -> Self {
126        Self::ReadWrite {
127            writable_records: HashSet::new(),
128        }
129    }
130
131    /// Adds a record key to the writable set
132    ///
133    /// Only has effect for ReadWrite policies. Panics if policy is ReadOnly.
134    pub fn allow_write_key(&mut self, key: impl Into<String>) {
135        match self {
136            Self::ReadWrite { writable_records } => {
137                writable_records.insert(key.into());
138            }
139            Self::ReadOnly => {
140                panic!("Cannot allow writes in ReadOnly security policy");
141            }
142        }
143    }
144
145    /// Adds a record key to the writable set (builder pattern)
146    ///
147    /// # Panics
148    /// Panics if called on a ReadOnly policy
149    pub fn with_writable_key(mut self, key: impl Into<String>) -> Self {
150        match self {
151            Self::ReadWrite {
152                ref mut writable_records,
153            } => {
154                writable_records.insert(key.into());
155                self
156            }
157            Self::ReadOnly => {
158                panic!("Cannot allow writes in ReadOnly security policy");
159            }
160        }
161    }
162
163    /// Checks if a record key is writable
164    pub fn is_writable_key(&self, key: &str) -> bool {
165        match self {
166            Self::ReadOnly => false,
167            Self::ReadWrite { writable_records } => writable_records.contains(key),
168        }
169    }
170
171    /// Returns the list of granted permissions
172    pub fn permissions(&self) -> &[&str] {
173        match self {
174            Self::ReadOnly => &["read", "subscribe"],
175            Self::ReadWrite { .. } => &["read", "subscribe", "write"],
176        }
177    }
178
179    /// Returns the list of writable record keys (for ReadWrite policy)
180    pub fn writable_records(&self) -> Vec<String> {
181        match self {
182            Self::ReadOnly => Vec::new(),
183            Self::ReadWrite { writable_records } => writable_records.iter().cloned().collect(),
184        }
185    }
186
187    /// Returns the list of writable record keys as StringKeys
188    pub fn writable_record_keys(&self) -> Vec<StringKey> {
189        match self {
190            Self::ReadOnly => Vec::new(),
191            Self::ReadWrite { writable_records } => writable_records
192                .iter()
193                .map(|s| StringKey::from_dynamic(s.as_str()))
194                .collect(),
195        }
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    #[test]
204    #[cfg(feature = "std")]
205    fn test_default_config() {
206        let config = AimxConfig::uds_default();
207        assert_eq!(config.socket_path, PathBuf::from("/tmp/aimdb.sock"));
208        assert_eq!(config.max_connections, 16);
209        assert_eq!(config.subscription_queue_size, 100);
210        assert!(matches!(config.security_policy, SecurityPolicy::ReadOnly));
211        assert!(config.auth_token.is_none());
212    }
213
214    #[test]
215    #[cfg(feature = "std")]
216    fn test_config_builder() {
217        let config = AimxConfig::uds_default()
218            .socket_path("/var/run/aimdb.sock")
219            .max_connections(32)
220            .subscription_queue_size(200)
221            .auth_token("secret-token")
222            .socket_permissions(0o660);
223
224        assert_eq!(config.socket_path, PathBuf::from("/var/run/aimdb.sock"));
225        assert_eq!(config.max_connections, 32);
226        assert_eq!(config.subscription_queue_size, 200);
227        assert_eq!(config.auth_token, Some("secret-token".to_string()));
228        assert_eq!(config.socket_permissions, Some(0o660));
229    }
230
231    #[test]
232    fn test_security_policy_read_only() {
233        let policy = SecurityPolicy::read_only();
234        assert!(!policy.is_writable_key("test.record"));
235        assert_eq!(policy.permissions(), &["read", "subscribe"]);
236    }
237
238    #[test]
239    fn test_security_policy_read_write() {
240        let mut policy = SecurityPolicy::read_write();
241        assert!(!policy.is_writable_key("test.record"));
242
243        policy.allow_write_key("test.record");
244        assert!(policy.is_writable_key("test.record"));
245        assert!(!policy.is_writable_key("other.record"));
246        assert_eq!(policy.permissions(), &["read", "subscribe", "write"]);
247    }
248
249    #[test]
250    #[should_panic(expected = "Cannot allow writes in ReadOnly security policy")]
251    fn test_security_policy_read_only_panic() {
252        let mut policy = SecurityPolicy::read_only();
253        policy.allow_write_key("test.record");
254    }
255
256    #[test]
257    #[should_panic(expected = "Cannot allow writes in ReadOnly security policy")]
258    fn test_security_policy_read_only_builder_panic() {
259        let _policy = SecurityPolicy::read_only().with_writable_key("test.record");
260    }
261
262    #[test]
263    fn test_security_policy_builder() {
264        let policy = SecurityPolicy::read_write()
265            .with_writable_key("sensor.temperature")
266            .with_writable_key("config.app");
267
268        assert!(policy.is_writable_key("sensor.temperature"));
269        assert!(policy.is_writable_key("config.app"));
270        assert!(!policy.is_writable_key("other.record"));
271    }
272}