aimdb_core/remote/
config.rs

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