feat: complete Phase 2.6 - Security Hardening
- Implement session management with device tracking - Implement audit logging system - Implement account lockout for brute-force protection - Add security headers middleware - Add rate limiting middleware (stub) - Integrate security services into main application Build Status: Compiles successfully Phase: 2.6 of 8 (75% complete)
This commit is contained in:
parent
be49d9d674
commit
4627903999
17 changed files with 910 additions and 61 deletions
128
backend/src/security/account_lockout.rs
Normal file
128
backend/src/security/account_lockout.rs
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
use mongodb::bson::{doc, DateTime};
|
||||
use mongodb::Collection;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use anyhow::Result;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AccountLockout {
|
||||
user_collection: Arc<RwLock<Collection<mongodb::bson::Document>>>,
|
||||
max_attempts: u32,
|
||||
base_duration_minutes: u32,
|
||||
max_duration_minutes: u32,
|
||||
}
|
||||
|
||||
impl AccountLockout {
|
||||
pub fn new(
|
||||
user_collection: Collection<mongodb::bson::Document>,
|
||||
max_attempts: u32,
|
||||
base_duration_minutes: u32,
|
||||
max_duration_minutes: u32,
|
||||
) -> Self {
|
||||
Self {
|
||||
user_collection: Arc::new(RwLock::new(user_collection)),
|
||||
max_attempts,
|
||||
base_duration_minutes,
|
||||
max_duration_minutes,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn check_lockout(&self, email: &str) -> Result<bool> {
|
||||
let collection = self.user_collection.read().await;
|
||||
let user = collection
|
||||
.find_one(
|
||||
doc! { "email": email },
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(user_doc) = user {
|
||||
if let Some(locked_until_val) = user_doc.get("locked_until") {
|
||||
if let Some(dt) = locked_until_val.as_datetime() {
|
||||
let now = DateTime::now();
|
||||
if dt.timestamp_millis() > now.timestamp_millis() {
|
||||
return Ok(true); // Account is locked
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false) // Account is not locked
|
||||
}
|
||||
|
||||
pub async fn record_failed_attempt(&self, email: &str) -> Result<bool> {
|
||||
let collection = self.user_collection.write().await;
|
||||
|
||||
// Get current failed attempts
|
||||
let user = collection
|
||||
.find_one(
|
||||
doc! { "email": email },
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let current_attempts = if let Some(user_doc) = user {
|
||||
user_doc.get("failed_login_attempts")
|
||||
.and_then(|v| v.as_i64())
|
||||
.unwrap_or(0) as u32
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let new_attempts = current_attempts + 1;
|
||||
let should_lock = new_attempts >= self.max_attempts;
|
||||
|
||||
// Calculate lockout duration
|
||||
let lock_duration = if should_lock {
|
||||
let multiplier = (new_attempts as u32).saturating_sub(self.max_attempts) + 1;
|
||||
let duration = self.base_duration_minutes * multiplier;
|
||||
std::cmp::min(duration, self.max_duration_minutes)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let locked_until = if lock_duration > 0 {
|
||||
let now = DateTime::now();
|
||||
let duration_millis = lock_duration as u64 * 60 * 1000;
|
||||
DateTime::from_millis(now.timestamp_millis() + duration_millis as i64)
|
||||
} else {
|
||||
DateTime::now()
|
||||
};
|
||||
|
||||
// Update user
|
||||
collection
|
||||
.update_one(
|
||||
doc! { "email": email },
|
||||
doc! {
|
||||
"$set": {
|
||||
"failed_login_attempts": new_attempts as i32,
|
||||
"last_failed_login": DateTime::now(),
|
||||
"locked_until": locked_until,
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(should_lock)
|
||||
}
|
||||
|
||||
pub async fn reset_attempts(&self, email: &str) -> Result<()> {
|
||||
let collection = self.user_collection.write().await;
|
||||
|
||||
collection
|
||||
.update_one(
|
||||
doc! { "email": email },
|
||||
doc! {
|
||||
"$set": {
|
||||
"failed_login_attempts": 0,
|
||||
"locked_until": null,
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
34
backend/src/security/audit_logger.rs
Normal file
34
backend/src/security/audit_logger.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
use anyhow::Result;
|
||||
use mongodb::bson::oid::ObjectId;
|
||||
use crate::models::audit_log::{AuditLog, AuditLogRepository, AuditEventType};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AuditLogger {
|
||||
repository: AuditLogRepository,
|
||||
}
|
||||
|
||||
impl AuditLogger {
|
||||
pub fn new(db: &mongodb::Database) -> Self {
|
||||
Self {
|
||||
repository: AuditLogRepository::new(db),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn log_event(
|
||||
&self,
|
||||
event_type: AuditEventType,
|
||||
user_id: Option<ObjectId>,
|
||||
email: Option<String>,
|
||||
ip_address: String,
|
||||
resource_type: Option<String>,
|
||||
resource_id: Option<String>,
|
||||
) -> Result<ObjectId> {
|
||||
self.repository
|
||||
.log(event_type, user_id, email, ip_address, resource_type, resource_id)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_user_audit_logs(&self, user_id: &ObjectId) -> Result<Vec<AuditLog>> {
|
||||
self.repository.find_by_user(user_id).await
|
||||
}
|
||||
}
|
||||
7
backend/src/security/mod.rs
Normal file
7
backend/src/security/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
pub mod audit_logger;
|
||||
pub mod session_manager;
|
||||
pub mod account_lockout;
|
||||
|
||||
pub use audit_logger::AuditLogger;
|
||||
pub use session_manager::SessionManager;
|
||||
pub use account_lockout::AccountLockout;
|
||||
42
backend/src/security/session_manager.rs
Normal file
42
backend/src/security/session_manager.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
use anyhow::Result;
|
||||
use crate::models::session::{Session, SessionRepository, DeviceInfo};
|
||||
use mongodb::bson::oid::ObjectId;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SessionManager {
|
||||
repository: SessionRepository,
|
||||
}
|
||||
|
||||
impl SessionManager {
|
||||
pub fn new(db: &mongodb::Database) -> Self {
|
||||
Self {
|
||||
repository: SessionRepository::new(db),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_session(
|
||||
&self,
|
||||
user_id: ObjectId,
|
||||
device_info: DeviceInfo,
|
||||
token_hash: String,
|
||||
duration_hours: i64,
|
||||
) -> Result<ObjectId> {
|
||||
self.repository.create(user_id, device_info, token_hash, duration_hours).await
|
||||
}
|
||||
|
||||
pub async fn get_user_sessions(&self, user_id: &ObjectId) -> Result<Vec<Session>> {
|
||||
self.repository.find_by_user(user_id).await
|
||||
}
|
||||
|
||||
pub async fn revoke_session(&self, session_id: &ObjectId) -> Result<()> {
|
||||
self.repository.revoke(session_id).await
|
||||
}
|
||||
|
||||
pub async fn revoke_all_user_sessions(&self, user_id: &ObjectId) -> Result<()> {
|
||||
self.repository.revoke_all_for_user(user_id).await
|
||||
}
|
||||
|
||||
pub async fn cleanup_expired(&self) -> Result<u64> {
|
||||
self.repository.cleanup_expired().await
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue