- 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
432 lines
12 KiB
Rust
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()
|
|
}
|
|
}
|