use anyhow::{Result, anyhow};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{debug, info, warn};
use uuid::Uuid;
const TOKEN_PREFIX: &str = "ns_";
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct NamespaceSecurityConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub token_store_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NamespaceToken {
pub namespace: String,
pub token: String,
pub created_at: u64,
pub description: Option<String>,
}
#[derive(Debug)]
pub struct TokenStore {
tokens: Arc<RwLock<HashMap<String, NamespaceToken>>>,
store_path: Option<String>,
}
impl TokenStore {
pub fn new(store_path: Option<String>) -> Self {
Self {
tokens: Arc::new(RwLock::new(HashMap::new())),
store_path,
}
}
pub async fn load(&self) -> Result<()> {
if let Some(path) = &self.store_path {
let expanded = shellexpand::tilde(path).to_string();
let path = Path::new(&expanded);
if path.exists() {
let contents = tokio::fs::read_to_string(path).await?;
let loaded: HashMap<String, NamespaceToken> = serde_json::from_str(&contents)?;
let mut tokens = self.tokens.write().await;
*tokens = loaded;
info!("Loaded {} namespace tokens from {}", tokens.len(), expanded);
}
}
Ok(())
}
pub async fn save(&self) -> Result<()> {
if let Some(path) = &self.store_path {
let expanded = shellexpand::tilde(path).to_string();
let path = Path::new(&expanded);
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent).await?;
}
let tokens = self.tokens.read().await;
let contents = serde_json::to_string_pretty(&*tokens)?;
tokio::fs::write(path, contents).await?;
debug!("Saved {} namespace tokens to {}", tokens.len(), expanded);
}
Ok(())
}
pub fn generate_token() -> String {
format!(
"{}{}",
TOKEN_PREFIX,
Uuid::new_v4().to_string().replace("-", "")
)
}
pub async fn create_token(
&self,
namespace: &str,
description: Option<String>,
) -> Result<String> {
let token = Self::generate_token();
let namespace_token = NamespaceToken {
namespace: namespace.to_string(),
token: token.clone(),
created_at: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
description,
};
{
let mut tokens = self.tokens.write().await;
tokens.insert(namespace.to_string(), namespace_token);
}
self.save().await?;
info!("Created token for namespace '{}'", namespace);
Ok(token)
}
pub async fn verify_token(&self, namespace: &str, token: &str) -> bool {
let tokens = self.tokens.read().await;
if let Some(stored) = tokens.get(namespace) {
stored.token == token
} else {
true
}
}
pub async fn has_token(&self, namespace: &str) -> bool {
let tokens = self.tokens.read().await;
tokens.contains_key(namespace)
}
pub async fn get_token_info(&self, namespace: &str) -> Option<(u64, Option<String>)> {
let tokens = self.tokens.read().await;
tokens
.get(namespace)
.map(|t| (t.created_at, t.description.clone()))
}
pub async fn revoke_token(&self, namespace: &str) -> Result<bool> {
let removed = {
let mut tokens = self.tokens.write().await;
tokens.remove(namespace).is_some()
};
if removed {
self.save().await?;
info!("Revoked token for namespace '{}'", namespace);
}
Ok(removed)
}
pub async fn list_protected_namespaces(&self) -> Vec<(String, u64, Option<String>)> {
let tokens = self.tokens.read().await;
tokens
.values()
.map(|t| (t.namespace.clone(), t.created_at, t.description.clone()))
.collect()
}
}
#[derive(Debug)]
pub struct NamespaceAccessManager {
token_store: TokenStore,
enabled: bool,
}
impl NamespaceAccessManager {
pub fn new(config: NamespaceSecurityConfig) -> Self {
let store_path = config.token_store_path.or_else(|| {
if config.enabled {
Some("~/.rmcp-servers/rmcp-memex/tokens.json".to_string())
} else {
None
}
});
Self {
token_store: TokenStore::new(store_path),
enabled: config.enabled,
}
}
pub async fn init(&self) -> Result<()> {
if self.enabled {
self.token_store.load().await?;
}
Ok(())
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub async fn verify_access(&self, namespace: &str, token: Option<&str>) -> Result<()> {
if !self.enabled {
return Ok(());
}
if !self.token_store.has_token(namespace).await {
return Ok(());
}
match token {
Some(t) => {
if self.token_store.verify_token(namespace, t).await {
Ok(())
} else {
warn!("Invalid token provided for namespace '{}'", namespace);
Err(anyhow!(
"Access denied: invalid token for namespace '{}'",
namespace
))
}
}
None => {
warn!("No token provided for protected namespace '{}'", namespace);
Err(anyhow!(
"Access denied: namespace '{}' requires a token. Use namespace_create_token to generate one.",
namespace
))
}
}
}
pub async fn create_token(
&self,
namespace: &str,
description: Option<String>,
) -> Result<String> {
self.token_store.create_token(namespace, description).await
}
pub async fn revoke_token(&self, namespace: &str) -> Result<bool> {
self.token_store.revoke_token(namespace).await
}
pub async fn list_protected_namespaces(&self) -> Vec<(String, u64, Option<String>)> {
self.token_store.list_protected_namespaces().await
}
pub async fn get_token_info(&self, namespace: &str) -> Option<(u64, Option<String>)> {
self.token_store.get_token_info(namespace).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_token_generation() {
let token = TokenStore::generate_token();
assert!(token.starts_with(TOKEN_PREFIX));
assert!(token.len() > TOKEN_PREFIX.len());
}
#[tokio::test]
async fn test_token_store_create_and_verify() {
let store = TokenStore::new(None);
let token = store
.create_token("test_namespace", Some("Test token".to_string()))
.await
.unwrap();
assert!(store.verify_token("test_namespace", &token).await);
assert!(!store.verify_token("test_namespace", "wrong_token").await);
assert!(store.verify_token("other_namespace", "any_token").await); }
#[tokio::test]
async fn test_access_manager_disabled() {
let config = NamespaceSecurityConfig::default();
let manager = NamespaceAccessManager::new(config);
assert!(manager.verify_access("any_namespace", None).await.is_ok());
}
#[tokio::test]
async fn test_access_manager_enabled() {
let config = NamespaceSecurityConfig {
enabled: true,
token_store_path: None,
};
let manager = NamespaceAccessManager::new(config);
let token = manager
.create_token("protected", Some("Test".to_string()))
.await
.unwrap();
assert!(manager.verify_access("protected", None).await.is_err());
assert!(
manager
.verify_access("protected", Some("wrong"))
.await
.is_err()
);
assert!(
manager
.verify_access("protected", Some(&token))
.await
.is_ok()
);
assert!(manager.verify_access("unprotected", None).await.is_ok());
}
}