Skip to main content
Glama

CodeGraph CLI MCP Server

by Jakedismo
security.rs•15.6 kB
use secrecy::{ExposeSecret, Secret, Zeroize}; use serde::{Deserialize, Serialize}; use std::fmt; use thiserror::Error; use uuid::Uuid; use chrono::{DateTime, Utc}; use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier, password_hash::{SaltString}}; use rand::rngs::OsRng; use rand::RngCore; use dashmap::DashMap; #[derive(Error, Debug)] pub enum SecurityError { #[error("Invalid credentials")] InvalidCredentials, #[error("Token generation failed: {0}")] TokenGeneration(String), #[error("Key derivation failed: {0}")] KeyDerivation(String), #[error("Cryptographic operation failed: {0}")] CryptographicFailure(String), } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] pub enum Permission { READ_CODE, READ_GRAPH, READ_METRICS, READ_ANALYSIS, WRITE_ANNOTATIONS, MANAGE_CACHE, ADMIN_SYSTEM, SUBSCRIBE_UPDATES, MANAGE_API_KEYS, } impl fmt::Display for Permission { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Permission::READ_CODE => write!(f, "read:code"), Permission::READ_GRAPH => write!(f, "read:graph"), Permission::READ_METRICS => write!(f, "read:metrics"), Permission::READ_ANALYSIS => write!(f, "read:analysis"), Permission::WRITE_ANNOTATIONS => write!(f, "write:annotations"), Permission::MANAGE_CACHE => write!(f, "manage:cache"), Permission::ADMIN_SYSTEM => write!(f, "admin:system"), Permission::SUBSCRIBE_UPDATES => write!(f, "subscribe:updates"), Permission::MANAGE_API_KEYS => write!(f, "manage:api_keys"), } } } #[derive(Debug, Clone)] pub struct AuthContext { pub user_id: Uuid, pub username: String, pub permissions: Vec<Permission>, pub roles: Vec<String>, pub organization_id: Option<String>, pub project_access: Vec<String>, pub session_id: String, pub issued_at: DateTime<Utc>, pub expires_at: DateTime<Utc>, } #[derive(Debug, Clone)] pub struct ApiKey { pub id: Uuid, pub key_hash: String, pub name: String, pub permissions: Vec<Permission>, pub created_at: DateTime<Utc>, pub expires_at: Option<DateTime<Utc>>, pub last_used: Option<DateTime<Utc>>, pub is_active: bool, pub created_by: Uuid, } #[derive(Debug, Clone, Deserialize)] pub struct AuthConfig { pub jwt_secret: Secret<String>, pub jwt_expiry_hours: u64, pub api_key_prefix: String, } impl AuthConfig { pub fn from_env() -> Result<Self, SecurityError> { let jwt_secret = std::env::var("JWT_SECRET") .map_err(|_| SecurityError::KeyDerivation("JWT_SECRET environment variable not found".to_string()))?; // Validate JWT secret strength if jwt_secret.len() < 32 { return Err(SecurityError::KeyDerivation("JWT_SECRET must be at least 32 characters".to_string())); } Ok(Self { jwt_secret: Secret::new(jwt_secret), jwt_expiry_hours: std::env::var("JWT_EXPIRY_HOURS") .unwrap_or_else(|_| "24".to_string()) .parse() .unwrap_or(24), api_key_prefix: std::env::var("API_KEY_PREFIX") .unwrap_or_else(|_| "cgk".to_string()), }) } pub fn jwt_secret(&self) -> &str { self.jwt_secret.expose_secret() } } pub struct ApiKeyManager { keys: DashMap<String, ApiKey>, argon2: Argon2<'static>, } impl ApiKeyManager { pub fn new() -> Self { Self { keys: DashMap::new(), argon2: Argon2::default(), } } pub fn create_key( &self, name: String, permissions: Vec<Permission>, created_by: Uuid, prefix: &str, ) -> Result<(String, Uuid), SecurityError> { // Generate cryptographically secure key let key_value = self.generate_secure_key(prefix)?; let key_hash = self.hash_api_key(&key_value)?; let api_key = ApiKey { id: Uuid::new_v4(), key_hash: key_hash.clone(), name, permissions, created_at: Utc::now(), expires_at: None, last_used: None, is_active: true, created_by, }; self.keys.insert(key_hash, api_key.clone()); Ok((key_value, api_key.id)) } pub fn validate_key(&self, key: &str) -> Option<ApiKey> { let key_hash = self.hash_api_key(key).ok()?; self.keys.get_mut(&key_hash).and_then(|mut entry| { let api_key = entry.value_mut(); // Check if key is active and not expired if !api_key.is_active { return None; } if let Some(expires_at) = api_key.expires_at { if expires_at <= Utc::now() { return None; } } // Update last used timestamp api_key.last_used = Some(Utc::now()); Some(api_key.clone()) }) } pub fn revoke_key(&self, key_id: &Uuid) -> bool { for mut entry in self.keys.iter_mut() { if entry.value().id == *key_id { entry.value_mut().is_active = false; return true; } } false } pub fn list_keys_for_user(&self, user_id: &Uuid) -> Vec<ApiKey> { self.keys .iter() .filter(|entry| entry.value().created_by == *user_id) .map(|entry| { let mut key = entry.value().clone(); // Don't expose the hash key.key_hash = "***".to_string(); key }) .collect() } fn generate_secure_key(&self, prefix: &str) -> Result<String, SecurityError> { use base64::{Engine as _, engine::general_purpose}; let mut key_bytes = [0u8; 32]; getrandom::getrandom(&mut key_bytes) .map_err(|e| SecurityError::TokenGeneration(e.to_string()))?; Ok(format!("{}_{}", prefix, general_purpose::URL_SAFE_NO_PAD.encode(&key_bytes))) } fn hash_api_key(&self, key: &str) -> Result<String, SecurityError> { let mut rng = OsRng; // rand_core trait impl is available via rand; SaltString expects rand_core::CryptoRng + RngCore let salt = SaltString::generate(&mut rng); let password_hash = self.argon2 .hash_password(key.as_bytes(), &salt) .map_err(|e| SecurityError::CryptographicFailure(e.to_string()))?; Ok(password_hash.to_string()) } fn verify_api_key(&self, key: &str, hash: &str) -> Result<bool, SecurityError> { let parsed_hash = PasswordHash::new(hash) .map_err(|e| SecurityError::CryptographicFailure(e.to_string()))?; match self.argon2.verify_password(key.as_bytes(), &parsed_hash) { Ok(()) => Ok(true), Err(_) => Ok(false), } } } /// Secure JWT token manager pub struct JwtManager { config: AuthConfig, } impl JwtManager { pub fn new(config: AuthConfig) -> Self { Self { config } } pub fn create_token(&self, auth_context: &AuthContext) -> Result<String, SecurityError> { use jsonwebtoken::{encode, Header, EncodingKey, Algorithm}; use serde_json::json; let claims = json!({ "sub": auth_context.user_id.to_string(), "username": auth_context.username, "permissions": auth_context.permissions, "roles": auth_context.roles, "org": auth_context.organization_id, "projects": auth_context.project_access, "session_id": auth_context.session_id, "iat": auth_context.issued_at.timestamp(), "exp": auth_context.expires_at.timestamp(), }); let header = Header::new(Algorithm::HS256); let key = EncodingKey::from_secret(self.config.jwt_secret().as_bytes()); encode(&header, &claims, &key) .map_err(|e| SecurityError::TokenGeneration(e.to_string())) } pub fn validate_token(&self, token: &str) -> Result<AuthContext, SecurityError> { use jsonwebtoken::{decode, Validation, DecodingKey, Algorithm}; use serde_json::Value; let key = DecodingKey::from_secret(self.config.jwt_secret().as_bytes()); let mut validation = Validation::new(Algorithm::HS256); validation.validate_exp = true; let token_data = decode::<Value>(token, &key, &validation) .map_err(|e| SecurityError::InvalidCredentials)?; let claims = &token_data.claims; Ok(AuthContext { user_id: Uuid::parse_str(claims["sub"].as_str().unwrap_or("")) .map_err(|_| SecurityError::InvalidCredentials)?, username: claims["username"].as_str().unwrap_or("").to_string(), permissions: serde_json::from_value(claims["permissions"].clone()) .unwrap_or_default(), roles: serde_json::from_value(claims["roles"].clone()) .unwrap_or_default(), organization_id: claims["org"].as_str().map(|s| s.to_string()), project_access: serde_json::from_value(claims["projects"].clone()) .unwrap_or_default(), session_id: claims["session_id"].as_str().unwrap_or("").to_string(), issued_at: DateTime::from_timestamp(claims["iat"].as_i64().unwrap_or(0), 0) .unwrap_or_default(), expires_at: DateTime::from_timestamp(claims["exp"].as_i64().unwrap_or(0), 0) .unwrap_or_default(), }) } } /// Authorization engine for permission checks pub struct AuthorizationEngine; impl AuthorizationEngine { pub fn has_permission(context: &AuthContext, required: &[Permission]) -> bool { // Admin system permission grants all other permissions if context.permissions.contains(&Permission::ADMIN_SYSTEM) { return true; } // Check specific permissions required.iter().all(|perm| context.permissions.contains(perm)) } pub fn has_role(context: &AuthContext, required_roles: &[&str]) -> bool { required_roles.iter().any(|role| context.roles.contains(&role.to_string())) } pub fn can_access_project(context: &AuthContext, project_id: &str) -> bool { // Admin can access any project if context.permissions.contains(&Permission::ADMIN_SYSTEM) { return true; } // Check project-specific access context.project_access.contains(&project_id.to_string()) } } /// Security event logging #[derive(Debug, Clone)] pub enum SecurityEvent { AuthenticationFailure { username: String, ip_address: String, reason: String, }, AuthenticationSuccess { user_id: Uuid, ip_address: String, method: String, }, PermissionDenied { user_id: Uuid, resource: String, required_permission: String, }, AdminAccess { user_id: Uuid, action: String, resource: String, }, ApiKeyCreated { key_id: Uuid, created_by: Uuid, permissions: Vec<Permission>, }, ApiKeyRevoked { key_id: Uuid, revoked_by: Uuid, }, SuspiciousActivity { user_id: Option<Uuid>, ip_address: String, description: String, }, } pub struct SecurityLogger; impl SecurityLogger { pub fn log_event(event: SecurityEvent) { use tracing::{warn, info, error}; match event { SecurityEvent::AuthenticationFailure { username, ip_address, reason } => { warn!( username = %username, ip_address = %ip_address, reason = %reason, "Authentication failure" ); } SecurityEvent::AuthenticationSuccess { user_id, ip_address, method } => { info!( user_id = %user_id, ip_address = %ip_address, method = %method, "Authentication success" ); } SecurityEvent::PermissionDenied { user_id, resource, required_permission } => { warn!( user_id = %user_id, resource = %resource, required_permission = %required_permission, "Permission denied" ); } SecurityEvent::AdminAccess { user_id, action, resource } => { info!( user_id = %user_id, action = %action, resource = %resource, "Admin access" ); } SecurityEvent::ApiKeyCreated { key_id, created_by, permissions } => { info!( key_id = %key_id, created_by = %created_by, permissions = ?permissions, "API key created" ); } SecurityEvent::ApiKeyRevoked { key_id, revoked_by } => { info!( key_id = %key_id, revoked_by = %revoked_by, "API key revoked" ); } SecurityEvent::SuspiciousActivity { user_id, ip_address, description } => { error!( user_id = ?user_id, ip_address = %ip_address, description = %description, "Suspicious activity detected" ); } } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_api_key_generation() { let manager = ApiKeyManager::new(); let user_id = Uuid::new_v4(); let result = manager.create_key( "test-key".to_string(), vec![Permission::READ_CODE], user_id, "test" ); assert!(result.is_ok()); let (key, key_id) = result.unwrap(); assert!(key.starts_with("test_")); assert!(key.len() > 40); // Should be long enough } #[test] fn test_permission_hierarchy() { let mut context = AuthContext { user_id: Uuid::new_v4(), username: "test".to_string(), permissions: vec![Permission::ADMIN_SYSTEM], roles: vec![], organization_id: None, project_access: vec![], session_id: "session".to_string(), issued_at: Utc::now(), expires_at: Utc::now() + chrono::Duration::hours(1), }; // Admin should have all permissions assert!(AuthorizationEngine::has_permission(&context, &[Permission::READ_CODE])); assert!(AuthorizationEngine::has_permission(&context, &[Permission::MANAGE_CACHE])); // Non-admin should only have specific permissions context.permissions = vec![Permission::READ_CODE]; assert!(AuthorizationEngine::has_permission(&context, &[Permission::READ_CODE])); assert!(!AuthorizationEngine::has_permission(&context, &[Permission::ADMIN_SYSTEM])); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Jakedismo/codegraph-rust'

If you have feedback or need assistance with the MCP directory API, please join our Discord server