rmcp_memex/security/
mod.rs1use anyhow::{Result, anyhow};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::path::Path;
11use std::sync::Arc;
12use tokio::sync::RwLock;
13use tracing::{debug, info, warn};
14use uuid::Uuid;
15
16const TOKEN_PREFIX: &str = "ns_";
18
19#[derive(Debug, Clone, Default, Serialize, Deserialize)]
21pub struct NamespaceSecurityConfig {
22 #[serde(default)]
24 pub enabled: bool,
25 #[serde(default)]
27 pub token_store_path: Option<String>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct NamespaceToken {
33 pub namespace: String,
35 pub token: String,
37 pub created_at: u64,
39 pub description: Option<String>,
41}
42
43#[derive(Debug)]
45pub struct TokenStore {
46 tokens: Arc<RwLock<HashMap<String, NamespaceToken>>>,
48 store_path: Option<String>,
50}
51
52impl TokenStore {
53 pub fn new(store_path: Option<String>) -> Self {
55 Self {
56 tokens: Arc::new(RwLock::new(HashMap::new())),
57 store_path,
58 }
59 }
60
61 pub async fn load(&self) -> Result<()> {
63 if let Some(path) = &self.store_path {
64 let expanded = shellexpand::tilde(path).to_string();
65 let path = Path::new(&expanded);
66
67 if path.exists() {
68 let contents = tokio::fs::read_to_string(path).await?;
69 let loaded: HashMap<String, NamespaceToken> = serde_json::from_str(&contents)?;
70 let mut tokens = self.tokens.write().await;
71 *tokens = loaded;
72 info!("Loaded {} namespace tokens from {}", tokens.len(), expanded);
73 }
74 }
75 Ok(())
76 }
77
78 pub async fn save(&self) -> Result<()> {
80 if let Some(path) = &self.store_path {
81 let expanded = shellexpand::tilde(path).to_string();
82 let path = Path::new(&expanded);
83
84 if let Some(parent) = path.parent() {
86 tokio::fs::create_dir_all(parent).await?;
87 }
88
89 let tokens = self.tokens.read().await;
90 let contents = serde_json::to_string_pretty(&*tokens)?;
91 tokio::fs::write(path, contents).await?;
92 debug!("Saved {} namespace tokens to {}", tokens.len(), expanded);
93 }
94 Ok(())
95 }
96
97 pub fn generate_token() -> String {
99 format!(
100 "{}{}",
101 TOKEN_PREFIX,
102 Uuid::new_v4().to_string().replace("-", "")
103 )
104 }
105
106 pub async fn create_token(
108 &self,
109 namespace: &str,
110 description: Option<String>,
111 ) -> Result<String> {
112 let token = Self::generate_token();
113 let namespace_token = NamespaceToken {
114 namespace: namespace.to_string(),
115 token: token.clone(),
116 created_at: std::time::SystemTime::now()
117 .duration_since(std::time::UNIX_EPOCH)
118 .unwrap_or_default()
119 .as_secs(),
120 description,
121 };
122
123 {
124 let mut tokens = self.tokens.write().await;
125 tokens.insert(namespace.to_string(), namespace_token);
126 }
127
128 self.save().await?;
129 info!("Created token for namespace '{}'", namespace);
130 Ok(token)
131 }
132
133 pub async fn verify_token(&self, namespace: &str, token: &str) -> bool {
135 let tokens = self.tokens.read().await;
136 if let Some(stored) = tokens.get(namespace) {
137 stored.token == token
138 } else {
139 true
142 }
143 }
144
145 pub async fn has_token(&self, namespace: &str) -> bool {
147 let tokens = self.tokens.read().await;
148 tokens.contains_key(namespace)
149 }
150
151 pub async fn get_token_info(&self, namespace: &str) -> Option<(u64, Option<String>)> {
153 let tokens = self.tokens.read().await;
154 tokens
155 .get(namespace)
156 .map(|t| (t.created_at, t.description.clone()))
157 }
158
159 pub async fn revoke_token(&self, namespace: &str) -> Result<bool> {
161 let removed = {
162 let mut tokens = self.tokens.write().await;
163 tokens.remove(namespace).is_some()
164 };
165
166 if removed {
167 self.save().await?;
168 info!("Revoked token for namespace '{}'", namespace);
169 }
170
171 Ok(removed)
172 }
173
174 pub async fn list_protected_namespaces(&self) -> Vec<(String, u64, Option<String>)> {
176 let tokens = self.tokens.read().await;
177 tokens
178 .values()
179 .map(|t| (t.namespace.clone(), t.created_at, t.description.clone()))
180 .collect()
181 }
182}
183
184#[derive(Debug)]
186pub struct NamespaceAccessManager {
187 token_store: TokenStore,
189 enabled: bool,
191}
192
193impl NamespaceAccessManager {
194 pub fn new(config: NamespaceSecurityConfig) -> Self {
196 let store_path = config.token_store_path.or_else(|| {
197 if config.enabled {
198 Some("~/.rmcp-servers/rmcp-memex/tokens.json".to_string())
199 } else {
200 None
201 }
202 });
203
204 Self {
205 token_store: TokenStore::new(store_path),
206 enabled: config.enabled,
207 }
208 }
209
210 pub async fn init(&self) -> Result<()> {
212 if self.enabled {
213 self.token_store.load().await?;
214 }
215 Ok(())
216 }
217
218 pub fn is_enabled(&self) -> bool {
220 self.enabled
221 }
222
223 pub async fn verify_access(&self, namespace: &str, token: Option<&str>) -> Result<()> {
226 if !self.enabled {
227 return Ok(());
228 }
229
230 if !self.token_store.has_token(namespace).await {
232 return Ok(());
234 }
235
236 match token {
238 Some(t) => {
239 if self.token_store.verify_token(namespace, t).await {
240 Ok(())
241 } else {
242 warn!("Invalid token provided for namespace '{}'", namespace);
243 Err(anyhow!(
244 "Access denied: invalid token for namespace '{}'",
245 namespace
246 ))
247 }
248 }
249 None => {
250 warn!("No token provided for protected namespace '{}'", namespace);
251 Err(anyhow!(
252 "Access denied: namespace '{}' requires a token. Use namespace_create_token to generate one.",
253 namespace
254 ))
255 }
256 }
257 }
258
259 pub async fn create_token(
261 &self,
262 namespace: &str,
263 description: Option<String>,
264 ) -> Result<String> {
265 self.token_store.create_token(namespace, description).await
266 }
267
268 pub async fn revoke_token(&self, namespace: &str) -> Result<bool> {
270 self.token_store.revoke_token(namespace).await
271 }
272
273 pub async fn list_protected_namespaces(&self) -> Vec<(String, u64, Option<String>)> {
275 self.token_store.list_protected_namespaces().await
276 }
277
278 pub async fn get_token_info(&self, namespace: &str) -> Option<(u64, Option<String>)> {
280 self.token_store.get_token_info(namespace).await
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[tokio::test]
289 async fn test_token_generation() {
290 let token = TokenStore::generate_token();
291 assert!(token.starts_with(TOKEN_PREFIX));
292 assert!(token.len() > TOKEN_PREFIX.len());
293 }
294
295 #[tokio::test]
296 async fn test_token_store_create_and_verify() {
297 let store = TokenStore::new(None);
298
299 let token = store
300 .create_token("test_namespace", Some("Test token".to_string()))
301 .await
302 .unwrap();
303
304 assert!(store.verify_token("test_namespace", &token).await);
305 assert!(!store.verify_token("test_namespace", "wrong_token").await);
306 assert!(store.verify_token("other_namespace", "any_token").await); }
308
309 #[tokio::test]
310 async fn test_access_manager_disabled() {
311 let config = NamespaceSecurityConfig::default();
312 let manager = NamespaceAccessManager::new(config);
313
314 assert!(manager.verify_access("any_namespace", None).await.is_ok());
316 }
317
318 #[tokio::test]
319 async fn test_access_manager_enabled() {
320 let config = NamespaceSecurityConfig {
321 enabled: true,
322 token_store_path: None,
323 };
324 let manager = NamespaceAccessManager::new(config);
325
326 let token = manager
328 .create_token("protected", Some("Test".to_string()))
329 .await
330 .unwrap();
331
332 assert!(manager.verify_access("protected", None).await.is_err());
334
335 assert!(
337 manager
338 .verify_access("protected", Some("wrong"))
339 .await
340 .is_err()
341 );
342
343 assert!(
345 manager
346 .verify_access("protected", Some(&token))
347 .await
348 .is_ok()
349 );
350
351 assert!(manager.verify_access("unprotected", None).await.is_ok());
353 }
354}