use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccessJustification {
pub user_id: Uuid,
pub justification: String,
pub business_need: Option<String>,
pub requested_by: Option<Uuid>,
pub approved_by: Option<Uuid>,
pub approved_at: Option<DateTime<Utc>>,
pub expires_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl AccessJustification {
pub fn new(
user_id: Uuid,
justification: String,
business_need: Option<String>,
requested_by: Option<Uuid>,
expires_at: Option<DateTime<Utc>>,
) -> Self {
let now = Utc::now();
Self {
user_id,
justification,
business_need,
requested_by,
approved_by: None,
approved_at: None,
expires_at,
created_at: now,
updated_at: now,
}
}
pub fn is_expired(&self) -> bool {
self.expires_at.map(|exp| Utc::now() > exp).unwrap_or(false)
}
pub fn is_approved(&self) -> bool {
self.approved_by.is_some() && self.approved_at.is_some()
}
}
#[async_trait::async_trait]
pub trait JustificationStorage: Send + Sync {
async fn get_justification(
&self,
user_id: Uuid,
) -> Result<Option<AccessJustification>, crate::Error>;
async fn set_justification(
&self,
justification: AccessJustification,
) -> Result<(), crate::Error>;
async fn get_all_justifications(&self) -> Result<Vec<AccessJustification>, crate::Error>;
async fn get_expired_justifications(&self) -> Result<Vec<AccessJustification>, crate::Error>;
async fn delete_justification(&self, user_id: Uuid) -> Result<(), crate::Error>;
}
pub struct InMemoryJustificationStorage {
justifications: Arc<RwLock<HashMap<Uuid, AccessJustification>>>,
}
impl InMemoryJustificationStorage {
pub fn new() -> Self {
Self {
justifications: Arc::new(RwLock::new(HashMap::new())),
}
}
}
impl Default for InMemoryJustificationStorage {
fn default() -> Self {
Self::new()
}
}
#[async_trait::async_trait]
impl JustificationStorage for InMemoryJustificationStorage {
async fn get_justification(
&self,
user_id: Uuid,
) -> Result<Option<AccessJustification>, crate::Error> {
let justifications = self.justifications.read().await;
Ok(justifications.get(&user_id).cloned())
}
async fn set_justification(
&self,
justification: AccessJustification,
) -> Result<(), crate::Error> {
let mut justifications = self.justifications.write().await;
justifications.insert(justification.user_id, justification);
Ok(())
}
async fn get_all_justifications(&self) -> Result<Vec<AccessJustification>, crate::Error> {
let justifications = self.justifications.read().await;
Ok(justifications.values().cloned().collect())
}
async fn get_expired_justifications(&self) -> Result<Vec<AccessJustification>, crate::Error> {
let justifications = self.justifications.read().await;
Ok(justifications.values().filter(|j| j.is_expired()).cloned().collect())
}
async fn delete_justification(&self, user_id: Uuid) -> Result<(), crate::Error> {
let mut justifications = self.justifications.write().await;
justifications.remove(&user_id);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_justification_storage() {
let storage = InMemoryJustificationStorage::new();
let user_id = Uuid::new_v4();
let justification = AccessJustification::new(
user_id,
"Required for system administration".to_string(),
Some("Manage production infrastructure".to_string()),
Some(Uuid::new_v4()),
Some(Utc::now() + chrono::Duration::days(365)),
);
storage.set_justification(justification.clone()).await.unwrap();
let retrieved = storage.get_justification(user_id).await.unwrap();
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().justification, "Required for system administration");
}
#[test]
fn test_justification_expiration() {
let justification = AccessJustification::new(
Uuid::new_v4(),
"Test".to_string(),
None,
None,
Some(Utc::now() - chrono::Duration::days(1)), );
assert!(justification.is_expired());
}
}