botserver/src/compliance/access_review.rs
Rodrigo Rodriguez (Pragmatismo) 14b7cf70af feat(autotask): Implement AutoTask system with intent classification and app generation
- Add IntentClassifier with 7 intent types (APP_CREATE, TODO, MONITOR, ACTION, SCHEDULE, GOAL, TOOL)
- Add AppGenerator with LLM-powered app structure analysis
- Add DesignerAI for modifying apps through conversation
- Add app_server for serving generated apps with clean URLs
- Add db_api for CRUD operations on bot database tables
- Add ask_later keyword for pending info collection
- Add migration 6.1.1 with tables: pending_info, auto_tasks, execution_plans, task_approvals, task_decisions, safety_audit_log, generated_apps, intent_classifications, designer_changes
- Write apps to S3 drive and sync to SITE_ROOT for serving
- Clean URL structure: /apps/{app_name}/
- Integrate with DriveMonitor for file sync

Based on Chapter 17 - Autonomous Tasks specification
2025-12-27 21:10:09 -03:00

432 lines
12 KiB
Rust

use anyhow::{anyhow, Result};
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum AccessLevel {
Read,
Write,
Admin,
Owner,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ResourceType {
File,
Database,
API,
System,
Application,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccessPermission {
pub id: Uuid,
pub user_id: Uuid,
pub resource_id: String,
pub resource_type: ResourceType,
pub access_level: AccessLevel,
pub granted_at: DateTime<Utc>,
pub granted_by: Uuid,
pub expires_at: Option<DateTime<Utc>>,
pub justification: String,
pub is_active: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccessReviewRequest {
pub id: Uuid,
pub user_id: Uuid,
pub reviewer_id: Uuid,
pub permissions: Vec<AccessPermission>,
pub requested_at: DateTime<Utc>,
pub due_date: DateTime<Utc>,
pub status: ReviewStatus,
pub comments: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ReviewStatus {
Pending,
InProgress,
Approved,
Rejected,
Expired,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccessReviewResult {
pub review_id: Uuid,
pub reviewer_id: Uuid,
pub reviewed_at: DateTime<Utc>,
pub approved_permissions: Vec<Uuid>,
pub revoked_permissions: Vec<Uuid>,
pub modified_permissions: Vec<(Uuid, AccessLevel)>,
pub comments: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccessViolation {
pub id: Uuid,
pub user_id: Uuid,
pub resource_id: String,
pub attempted_action: String,
pub denied_reason: String,
pub occurred_at: DateTime<Utc>,
pub severity: ViolationSeverity,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ViolationSeverity {
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone)]
pub struct AccessReviewService {
permissions: HashMap<Uuid, Vec<AccessPermission>>,
reviews: HashMap<Uuid, AccessReviewRequest>,
violations: Vec<AccessViolation>,
}
impl AccessReviewService {
pub fn new() -> Self {
Self {
permissions: HashMap::new(),
reviews: HashMap::new(),
violations: Vec::new(),
}
}
pub fn grant_permission(
&mut self,
user_id: Uuid,
resource_id: String,
resource_type: ResourceType,
access_level: AccessLevel,
granted_by: Uuid,
justification: String,
expires_in: Option<Duration>,
) -> Result<AccessPermission> {
let permission = AccessPermission {
id: Uuid::new_v4(),
user_id,
resource_id,
resource_type,
access_level,
granted_at: Utc::now(),
granted_by,
expires_at: expires_in.map(|d| Utc::now() + d),
justification,
is_active: true,
};
self.permissions
.entry(user_id)
.or_default()
.push(permission.clone());
log::info!(
"Granted {} access to user {} for resource {}",
serde_json::to_string(&permission.access_level)?,
user_id,
permission.resource_id
);
Ok(permission)
}
pub fn revoke_permission(&mut self, permission_id: Uuid, revoked_by: Uuid) -> Result<()> {
for permissions in self.permissions.values_mut() {
if let Some(perm) = permissions.iter_mut().find(|p| p.id == permission_id) {
perm.is_active = false;
log::info!(
"Revoked permission {} for user {} by {}",
permission_id,
perm.user_id,
revoked_by
);
return Ok(());
}
}
Err(anyhow!("Permission not found"))
}
pub fn check_access(
&mut self,
user_id: Uuid,
resource_id: &str,
required_level: AccessLevel,
) -> Result<bool> {
let user_permissions = self.permissions.get(&user_id);
if let Some(permissions) = user_permissions {
for perm in permissions {
if perm.resource_id == resource_id && perm.is_active {
if let Some(expires) = perm.expires_at {
if expires < Utc::now() {
continue;
}
}
if Self::has_sufficient_access(&perm.access_level, &required_level) {
return Ok(true);
}
}
}
}
let violation = AccessViolation {
id: Uuid::new_v4(),
user_id,
resource_id: resource_id.to_string(),
attempted_action: format!("{:?} access", required_level),
denied_reason: "Insufficient permissions".to_string(),
occurred_at: Utc::now(),
severity: ViolationSeverity::Medium,
};
self.violations.push(violation);
Ok(false)
}
fn has_sufficient_access(user_level: &AccessLevel, required: &AccessLevel) -> bool {
match required {
AccessLevel::Read => true,
AccessLevel::Write => matches!(
user_level,
AccessLevel::Write | AccessLevel::Admin | AccessLevel::Owner
),
AccessLevel::Admin => matches!(user_level, AccessLevel::Admin | AccessLevel::Owner),
AccessLevel::Owner => matches!(user_level, AccessLevel::Owner),
}
}
pub fn create_review_request(
&mut self,
user_id: Uuid,
reviewer_id: Uuid,
days_until_due: i64,
) -> Result<AccessReviewRequest> {
let user_permissions = self.permissions.get(&user_id).cloned().unwrap_or_default();
let review = AccessReviewRequest {
id: Uuid::new_v4(),
user_id,
reviewer_id,
permissions: user_permissions,
requested_at: Utc::now(),
due_date: Utc::now() + Duration::days(days_until_due),
status: ReviewStatus::Pending,
comments: None,
};
self.reviews.insert(review.id, review.clone());
log::info!(
"Created access review {} for user {} assigned to {}",
review.id,
user_id,
reviewer_id
);
Ok(review)
}
pub fn process_review(
&mut self,
review_id: Uuid,
approved: Vec<Uuid>,
revoked: Vec<Uuid>,
modified: Vec<(Uuid, AccessLevel)>,
comments: String,
) -> Result<AccessReviewResult> {
let (reviewer_id, user_id) = {
let review = self
.reviews
.get(&review_id)
.ok_or_else(|| anyhow!("Review not found"))?;
if review.status != ReviewStatus::Pending && review.status != ReviewStatus::InProgress {
return Err(anyhow!("Review already completed"));
}
(review.reviewer_id, review.user_id)
};
for perm_id in &revoked {
self.revoke_permission(*perm_id, reviewer_id)?;
}
for (perm_id, new_level) in &modified {
if let Some(permissions) = self.permissions.get_mut(&user_id) {
if let Some(perm) = permissions.iter_mut().find(|p| p.id == *perm_id) {
perm.access_level = new_level.clone();
}
}
}
if let Some(review) = self.reviews.get_mut(&review_id) {
review.status = ReviewStatus::Approved;
review.comments = Some(comments.clone());
}
let result = AccessReviewResult {
review_id,
reviewer_id,
reviewed_at: Utc::now(),
approved_permissions: approved,
revoked_permissions: revoked,
modified_permissions: modified,
comments,
};
log::info!("Completed access review {} with result", review_id);
Ok(result)
}
pub fn get_expired_permissions(&self) -> Vec<AccessPermission> {
let now = Utc::now();
let mut expired = Vec::new();
for permissions in self.permissions.values() {
for perm in permissions {
if let Some(expires) = perm.expires_at {
if expires < now && perm.is_active {
expired.push(perm.clone());
}
}
}
}
expired
}
pub fn get_user_permissions(&self, user_id: Uuid) -> Vec<AccessPermission> {
self.permissions
.get(&user_id)
.cloned()
.unwrap_or_default()
.into_iter()
.filter(|p| p.is_active)
.collect()
}
pub fn get_pending_reviews(&self, reviewer_id: Option<Uuid>) -> Vec<AccessReviewRequest> {
self.reviews
.values()
.filter(|r| {
r.status == ReviewStatus::Pending
&& reviewer_id.is_none_or(|id| r.reviewer_id == id)
})
.cloned()
.collect()
}
pub fn get_violations(
&self,
user_id: Option<Uuid>,
severity: Option<ViolationSeverity>,
since: Option<DateTime<Utc>>,
) -> Vec<AccessViolation> {
self.violations
.iter()
.filter(|v| {
user_id.is_none_or(|id| v.user_id == id)
&& severity.as_ref().is_none_or(|s| &v.severity == s)
&& since.is_none_or(|d| v.occurred_at >= d)
})
.cloned()
.collect()
}
pub fn generate_compliance_report(&self) -> AccessComplianceReport {
let total_permissions = self.permissions.values().map(|p| p.len()).sum::<usize>();
let active_permissions = self
.permissions
.values()
.flat_map(|p| p.iter())
.filter(|p| p.is_active)
.count();
let expired_permissions = self.get_expired_permissions().len();
let pending_reviews = self
.reviews
.values()
.filter(|r| r.status == ReviewStatus::Pending)
.count();
let violations_last_30_days = self
.violations
.iter()
.filter(|v| v.occurred_at > Utc::now() - Duration::days(30))
.count();
let critical_violations = self
.violations
.iter()
.filter(|v| v.severity == ViolationSeverity::Critical)
.count();
AccessComplianceReport {
generated_at: Utc::now(),
total_permissions,
active_permissions,
expired_permissions,
pending_reviews,
violations_last_30_days,
critical_violations,
compliance_score: self.calculate_compliance_score(),
}
}
fn calculate_compliance_score(&self) -> f64 {
let mut score = 100.0;
let expired = self.get_expired_permissions().len();
score -= expired as f64 * 2.0;
let overdue_reviews = self
.reviews
.values()
.filter(|r| r.status == ReviewStatus::Pending && r.due_date < Utc::now())
.count();
score -= overdue_reviews as f64 * 5.0;
for violation in &self.violations {
match violation.severity {
ViolationSeverity::Low => score -= 1.0,
ViolationSeverity::Medium => score -= 3.0,
ViolationSeverity::High => score -= 5.0,
ViolationSeverity::Critical => score -= 10.0,
}
}
score.clamp(0.0, 100.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccessComplianceReport {
pub generated_at: DateTime<Utc>,
pub total_permissions: usize,
pub active_permissions: usize,
pub expired_permissions: usize,
pub pending_reviews: usize,
pub violations_last_30_days: usize,
pub critical_violations: usize,
pub compliance_score: f64,
}
impl Default for AccessReviewService {
fn default() -> Self {
Self::new()
}
}