use std::collections::HashMap;
use ring::rand::{SecureRandom, SystemRandom};
use security_core::identity::AuthenticatedIdentity;
use security_core::types::{ActorId, TenantId};
use time::OffsetDateTime;
use tokio::sync::Mutex;
use crate::error::IdentityError;
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Session {
pub id: String,
pub actor_id: ActorId,
pub tenant_id: Option<TenantId>,
pub roles: Vec<String>,
pub created_at: OffsetDateTime,
pub expires_at: OffsetDateTime,
pub last_accessed: OffsetDateTime,
}
#[allow(async_fn_in_trait)]
pub trait SessionManager {
async fn create_session(
&self,
identity: &AuthenticatedIdentity,
lifetime_secs: u64,
) -> Result<Session, IdentityError>;
async fn validate_session(&self, id: &str) -> Result<Session, IdentityError>;
async fn refresh_session(&self, id: &str, extra_secs: u64) -> Result<Session, IdentityError>;
async fn revoke_session(&self, id: &str) -> Result<(), IdentityError>;
}
fn generate_session_id() -> Result<String, IdentityError> {
let rng = SystemRandom::new();
let mut bytes = [0u8; 16];
rng.fill(&mut bytes)
.map_err(|_| IdentityError::ProviderUnavailable)?;
Ok(bytes.iter().map(|b| format!("{b:02x}")).collect())
}
pub struct InMemorySessionManager {
sessions: Mutex<HashMap<String, Session>>,
}
impl InMemorySessionManager {
#[must_use]
pub fn new() -> Self {
Self {
sessions: Mutex::new(HashMap::new()),
}
}
}
impl Default for InMemorySessionManager {
fn default() -> Self {
Self::new()
}
}
impl SessionManager for InMemorySessionManager {
async fn create_session(
&self,
identity: &AuthenticatedIdentity,
lifetime_secs: u64,
) -> Result<Session, IdentityError> {
let id = generate_session_id()?;
let now = OffsetDateTime::now_utc();
#[allow(clippy::cast_possible_truncation)]
let expires_at = now + time::Duration::seconds(lifetime_secs as i64);
let session = Session {
id: id.clone(),
actor_id: identity.actor_id.clone(),
tenant_id: identity.tenant_id.clone(),
roles: identity.roles.clone(),
created_at: now,
expires_at,
last_accessed: now,
};
self.sessions.lock().await.insert(id, session.clone());
Ok(session)
}
async fn validate_session(&self, id: &str) -> Result<Session, IdentityError> {
let now = OffsetDateTime::now_utc();
let mut guard = self.sessions.lock().await;
let session = guard
.get(id)
.cloned()
.ok_or(IdentityError::SessionExpired)?;
if now > session.expires_at {
guard.remove(id);
return Err(IdentityError::SessionExpired);
}
let mut session = session;
session.last_accessed = now;
guard.insert(id.to_owned(), session.clone());
Ok(session)
}
async fn refresh_session(&self, id: &str, extra_secs: u64) -> Result<Session, IdentityError> {
let now = OffsetDateTime::now_utc();
let mut guard = self.sessions.lock().await;
let session = guard.get_mut(id).ok_or(IdentityError::SessionExpired)?;
if now > session.expires_at {
return Err(IdentityError::SessionExpired);
}
#[allow(clippy::cast_possible_truncation)]
let extra = time::Duration::seconds(extra_secs as i64);
session.expires_at += extra;
session.last_accessed = now;
Ok(session.clone())
}
async fn revoke_session(&self, id: &str) -> Result<(), IdentityError> {
self.sessions.lock().await.remove(id);
Ok(())
}
}