use crate::security::access_review::{
AccessReviewEngine, ApiTokenInfo, PrivilegedAccessInfo, ResourceAccessInfo, ReviewFrequency,
UserAccessInfo,
};
use crate::Error;
use chrono::{DateTime, Duration, Utc};
use uuid::Uuid;
#[async_trait::async_trait]
pub trait UserDataProvider: Send + Sync {
async fn get_all_users(&self) -> Result<Vec<UserAccessInfo>, Error>;
async fn get_privileged_users(&self) -> Result<Vec<PrivilegedAccessInfo>, Error>;
async fn get_api_tokens(&self) -> Result<Vec<ApiTokenInfo>, Error>;
async fn get_user(&self, user_id: Uuid) -> Result<Option<UserAccessInfo>, Error>;
async fn get_last_login(&self, user_id: Uuid) -> Result<Option<DateTime<Utc>>, Error>;
async fn revoke_user_access(&self, user_id: Uuid, reason: String) -> Result<(), Error>;
async fn update_user_permissions(
&self,
user_id: Uuid,
roles: Vec<String>,
permissions: Vec<String>,
) -> Result<(), Error>;
}
pub struct AccessReviewService {
engine: AccessReviewEngine,
user_provider: Box<dyn UserDataProvider>,
}
impl AccessReviewService {
pub fn new(engine: AccessReviewEngine, user_provider: Box<dyn UserDataProvider>) -> Self {
Self {
engine,
user_provider,
}
}
pub async fn start_user_access_review(&mut self) -> Result<String, Error> {
let users = self.user_provider.get_all_users().await?;
let review = self.engine.start_user_access_review(users).await?;
Ok(review.review_id)
}
pub async fn start_privileged_access_review(&mut self) -> Result<String, Error> {
let privileged_users = self.user_provider.get_privileged_users().await?;
let users: Vec<UserAccessInfo> = privileged_users
.into_iter()
.map(|p| UserAccessInfo {
user_id: p.user_id,
username: p.username,
email: "".to_string(), roles: p.roles,
permissions: vec![], last_login: p.last_privileged_action,
access_granted: p
.last_privileged_action
.unwrap_or_else(|| Utc::now() - Duration::days(90)),
days_inactive: p.last_privileged_action.map(|d| (Utc::now() - d).num_days() as u64),
is_active: true,
})
.collect();
let review = self.engine.start_user_access_review(users).await?;
Ok(review.review_id)
}
pub async fn start_token_review(&mut self) -> Result<String, Error> {
let tokens = self.user_provider.get_api_tokens().await?;
let review = self.engine.start_api_token_review(tokens).await?;
Ok(review.review_id)
}
pub async fn start_resource_access_review(
&mut self,
resources: Vec<ResourceAccessInfo>,
) -> Result<String, Error> {
let review = self.engine.start_resource_access_review(resources).await?;
Ok(review.review_id)
}
pub async fn approve_user_access(
&mut self,
review_id: &str,
user_id: Uuid,
approved_by: Uuid,
justification: Option<String>,
) -> Result<(), Error> {
self.engine
.approve_user_access(review_id, user_id, approved_by, justification)
.map_err(|e| Error::internal(e.to_string()))
}
pub async fn revoke_user_access(
&mut self,
review_id: &str,
user_id: Uuid,
revoked_by: Uuid,
reason: String,
) -> Result<(), Error> {
self.engine
.revoke_user_access(review_id, user_id, revoked_by, reason.clone())
.map_err(|e| Error::internal(e.to_string()))?;
self.user_provider.revoke_user_access(user_id, reason).await?;
Ok(())
}
pub async fn update_user_permissions(
&mut self,
review_id: &str,
user_id: Uuid,
updated_by: Uuid,
new_roles: Vec<String>,
new_permissions: Vec<String>,
reason: Option<String>,
) -> Result<(), Error> {
self.engine
.update_user_permissions(
review_id,
user_id,
updated_by,
new_roles.clone(),
new_permissions.clone(),
reason.clone(),
)
.map_err(|e| Error::internal(e.to_string()))?;
self.user_provider
.update_user_permissions(user_id, new_roles, new_permissions)
.await?;
Ok(())
}
pub fn get_review(
&self,
review_id: &str,
) -> Option<&crate::security::access_review::AccessReview> {
self.engine.get_review(review_id)
}
pub fn get_all_reviews(&self) -> Vec<&crate::security::access_review::AccessReview> {
self.engine.get_all_reviews()
}
pub async fn check_auto_revocations(&mut self) -> Result<Vec<(String, Uuid)>, Error> {
let revoked = self.engine.check_auto_revocation();
for (review_id, user_id) in &revoked {
if let Some(review_item) =
self.engine.get_review_items(review_id).and_then(|items| items.get(user_id))
{
let reason = review_item
.rejection_reason
.clone()
.unwrap_or_else(|| "Auto-revoked due to missing approval".to_string());
if let Err(e) = self.user_provider.revoke_user_access(*user_id, reason).await {
tracing::error!("Failed to revoke access for user {}: {}", user_id, e);
}
}
}
Ok(revoked)
}
pub fn engine(&self) -> &AccessReviewEngine {
&self.engine
}
pub fn engine_mut(&mut self) -> &mut AccessReviewEngine {
&mut self.engine
}
}
pub fn is_review_due(frequency: ReviewFrequency, last_review_date: Option<DateTime<Utc>>) -> bool {
let now = Utc::now();
if let Some(last_review) = last_review_date {
let next_review = frequency.next_review_date(last_review);
now >= next_review
} else {
true
}
}
pub fn days_since_last_activity(last_activity: Option<DateTime<Utc>>) -> Option<u64> {
last_activity.map(|activity| {
let duration = Utc::now() - activity;
duration.num_days() as u64
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_review_due() {
let frequency = ReviewFrequency::Quarterly;
let last_review = Utc::now() - Duration::days(100);
assert!(is_review_due(frequency, Some(last_review)));
let recent_review = Utc::now() - Duration::days(10);
assert!(!is_review_due(frequency, Some(recent_review)));
assert!(is_review_due(frequency, None));
}
#[test]
fn test_days_since_last_activity() {
let recent = Utc::now() - Duration::days(5);
assert_eq!(days_since_last_activity(Some(recent)), Some(5));
let old = Utc::now() - Duration::days(100);
assert_eq!(days_since_last_activity(Some(old)), Some(100));
assert_eq!(days_since_last_activity(None), None);
}
}