botserver/src/security/redis_session_store.rs
Rodrigo Rodriguez e143968179 feat: Add JWT secret rotation and health verification
SEC-02: Implement credential rotation security improvements

- Add JWT secret rotation to rotate-secret command
- Generate 64-character HS512-compatible secrets
- Automatic .env backup with timestamp
- Atomic file updates via temp+rename pattern
- Add health verification for rotated credentials
- Route rotate-secret, rotate-secrets, vault commands in CLI
- Add verification attempts for database and JWT endpoints

Security improvements:
- JWT_SECRET now rotatable (previously impossible)
- Automatic rollback via backup files
- Health checks catch configuration errors
- Clear warnings about token invalidation

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-19 19:42:41 +00:00

185 lines
5.8 KiB
Rust

use anyhow::{anyhow, Result};
use std::sync::Arc;
use super::session::{Session, SessionStore};
const SESSION_KEY_PREFIX: &str = "session:";
#[derive(Debug, Clone)]
pub struct RedisSessionStore {
client: Arc<redis::Client>,
}
impl RedisSessionStore {
pub async fn new(redis_url: &str) -> Result<Self> {
let client = redis::Client::open(redis_url)
.map_err(|e| anyhow!("Failed to create Redis client: {}", e))?;
let _ = client
.get_multiplexed_async_connection()
.await
.map_err(|e| anyhow!("Redis connection error: {}", e))?;
Ok(Self {
client: Arc::new(client),
})
}
fn session_key(&self, session_id: &str) -> String {
format!("{}{}", SESSION_KEY_PREFIX, session_id)
}
}
impl SessionStore for RedisSessionStore {
fn create(&self, session: Session) -> impl std::future::Future<Output = Result<()>> + Send {
let client = self.client.clone();
let key = self.session_key(&session.id);
let ttl = session.time_until_expiry();
let ttl_secs = ttl.num_seconds().max(0) as usize;
async move {
let mut conn = client
.get_multiplexed_async_connection()
.await
.map_err(|e| anyhow!("Redis connection error: {}", e))?;
let value = serde_json::to_string(&session)?;
redis::cmd("SETEX")
.arg(&key)
.arg(ttl_secs)
.arg(&value)
.query_async::<()>(&mut conn)
.await
.map_err(|e| anyhow!("Failed to create session: {}", e))?;
Ok(())
}
}
fn get(&self, session_id: &str) -> impl std::future::Future<Output = Result<Option<Session>>> + Send {
let client = self.client.clone();
let key = self.session_key(session_id);
async move {
let mut conn = client
.get_multiplexed_async_connection()
.await
.map_err(|e| anyhow!("Redis connection error: {}", e))?;
let value: Option<String> = redis::cmd("GET")
.arg(&key)
.query_async(&mut conn)
.await
.map_err(|e| anyhow!("Failed to get session: {}", e))?;
match value {
Some(v) => {
let session: Session = serde_json::from_str(&v)
.map_err(|e| anyhow!("Failed to deserialize session: {}", e))?;
Ok(Some(session))
}
None => Ok(None),
}
}
}
fn update(&self, session: &Session) -> impl std::future::Future<Output = Result<()>> + Send {
let client = self.client.clone();
let key = self.session_key(&session.id);
let session = session.clone();
let ttl = session.time_until_expiry();
let ttl_secs = ttl.num_seconds().max(0) as usize;
async move {
let mut conn = client
.get_multiplexed_async_connection()
.await
.map_err(|e| anyhow!("Redis connection error: {}", e))?;
let value = serde_json::to_string(&session)?;
redis::cmd("SETEX")
.arg(&key)
.arg(ttl_secs)
.arg(&value)
.query_async::<()>(&mut conn)
.await
.map_err(|e| anyhow!("Failed to update session: {}", e))?;
Ok(())
}
}
fn delete(&self, session_id: &str) -> impl std::future::Future<Output = Result<()>> + Send {
let client = self.client.clone();
let key = self.session_key(session_id);
async move {
let mut conn = client
.get_multiplexed_async_connection()
.await
.map_err(|e| anyhow!("Redis connection error: {}", e))?;
redis::cmd("DEL")
.arg(&key)
.query_async::<()>(&mut conn)
.await
.map_err(|e| anyhow!("Failed to delete session: {}", e))?;
Ok(())
}
}
fn get_user_sessions(&self, user_id: uuid::Uuid) -> impl std::future::Future<Output = Result<Vec<Session>>> + Send {
let client = self.client.clone();
let prefix = SESSION_KEY_PREFIX.to_string();
async move {
let mut conn = client
.get_multiplexed_async_connection()
.await
.map_err(|e| anyhow!("Redis connection error: {}", e))?;
let pattern = format!("{}*", prefix);
let keys: Vec<String> = redis::cmd("KEYS")
.arg(&pattern)
.query_async(&mut conn)
.await
.map_err(|e| anyhow!("Failed to list sessions: {}", e))?;
let mut sessions = Vec::new();
for key in keys {
let session_id = key.trim_start_matches(&prefix);
let store = Self { client: client.clone() };
if let Ok(Some(session)) = store.get(session_id).await {
if session.user_id == user_id && session.is_valid() {
sessions.push(session);
}
}
}
Ok(sessions)
}
}
fn delete_user_sessions(&self, user_id: uuid::Uuid) -> impl std::future::Future<Output = Result<usize>> + Send {
let client = self.client.clone();
async move {
let sessions = Self { client: client.clone() }.get_user_sessions(user_id).await?;
let count = sessions.len();
for session in sessions {
Self { client: client.clone() }.delete(&session.id).await?;
}
Ok(count)
}
}
async fn cleanup_expired(&self) -> Result<usize> {
Ok(0)
}
}