//! Organization Management Module //! //! Provides organization creation, role management, group management, //! and access control for multi-tenant deployments. use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use uuid::Uuid; use crate::shared::utils::DbPool; // ============================================================================ // Organization Types // ============================================================================ /// Organization entity #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Organization { pub id: Uuid, pub name: String, pub slug: String, pub description: Option, pub logo_url: Option, pub website: Option, pub plan_id: String, pub owner_id: Uuid, pub settings: OrganizationSettings, pub metadata: HashMap, pub created_at: DateTime, pub updated_at: DateTime, pub deleted_at: Option>, } impl Organization { pub fn new(name: String, owner_id: Uuid) -> Self { let slug = slugify(&name); let now = Utc::now(); Self { id: Uuid::new_v4(), name, slug, description: None, logo_url: None, website: None, plan_id: "free".to_string(), owner_id, settings: OrganizationSettings::default(), metadata: HashMap::new(), created_at: now, updated_at: now, deleted_at: None, } } pub fn with_plan(mut self, plan_id: String) -> Self { self.plan_id = plan_id; self } pub fn with_description(mut self, description: String) -> Self { self.description = Some(description); self } pub fn is_deleted(&self) -> bool { self.deleted_at.is_some() } } /// Organization settings #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OrganizationSettings { pub allow_public_bots: bool, pub require_2fa: bool, pub allowed_email_domains: Vec, pub default_user_role: String, pub max_members: Option, pub sso_enabled: bool, pub sso_provider: Option, pub audit_log_retention_days: u32, pub ip_whitelist: Vec, pub custom_branding: Option, } impl Default for OrganizationSettings { fn default() -> Self { Self { allow_public_bots: false, require_2fa: false, allowed_email_domains: Vec::new(), default_user_role: "member".to_string(), max_members: None, sso_enabled: false, sso_provider: None, audit_log_retention_days: 90, ip_whitelist: Vec::new(), custom_branding: None, } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CustomBranding { pub primary_color: String, pub secondary_color: Option, pub logo_url: Option, pub favicon_url: Option, pub custom_css: Option, } // ============================================================================ // Organization Member // ============================================================================ /// Organization member entity #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OrganizationMember { pub id: Uuid, pub organization_id: Uuid, pub user_id: Uuid, pub role: String, pub status: MemberStatus, pub invited_by: Option, pub invited_at: Option>, pub joined_at: Option>, pub last_active_at: Option>, pub created_at: DateTime, pub updated_at: DateTime, } impl OrganizationMember { pub fn new(organization_id: Uuid, user_id: Uuid, role: &str) -> Self { let now = Utc::now(); Self { id: Uuid::new_v4(), organization_id, user_id, role: role.to_string(), status: MemberStatus::Active, invited_by: None, invited_at: None, joined_at: Some(now), last_active_at: Some(now), created_at: now, updated_at: now, } } pub fn as_invited(mut self, invited_by: Uuid) -> Self { self.status = MemberStatus::Invited; self.invited_by = Some(invited_by); self.invited_at = Some(Utc::now()); self.joined_at = None; self } pub fn accept_invitation(&mut self) { self.status = MemberStatus::Active; self.joined_at = Some(Utc::now()); self.last_active_at = Some(Utc::now()); } pub fn is_active(&self) -> bool { self.status == MemberStatus::Active } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum MemberStatus { Active, Invited, Suspended, Deactivated, } // ============================================================================ // Roles // ============================================================================ /// Role definition #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Role { pub id: Uuid, pub organization_id: Option, // None for system roles pub name: String, pub display_name: String, pub description: Option, pub permissions: Vec, pub is_system: bool, pub is_default: bool, pub hierarchy_level: u32, // Lower = more powerful pub created_at: DateTime, pub updated_at: DateTime, } impl Role { pub fn new(name: &str, display_name: &str, permissions: Vec) -> Self { let now = Utc::now(); Self { id: Uuid::new_v4(), organization_id: None, name: name.to_string(), display_name: display_name.to_string(), description: None, permissions, is_system: false, is_default: false, hierarchy_level: 100, created_at: now, updated_at: now, } } pub fn system_role(name: &str, display_name: &str, permissions: Vec, level: u32) -> Self { Self { is_system: true, hierarchy_level: level, ..Self::new(name, display_name, permissions) } } pub fn has_permission(&self, permission: &str) -> bool { self.permissions.contains(&permission.to_string()) || self.permissions.contains(&"*".to_string()) } pub fn can_manage(&self, other: &Role) -> bool { self.hierarchy_level < other.hierarchy_level } } /// Default system roles pub fn default_roles() -> Vec { vec![ Role::system_role( "owner", "Owner", vec!["*".to_string()], 0, ), Role::system_role( "admin", "Administrator", vec![ "org:manage".to_string(), "org:members".to_string(), "org:settings".to_string(), "bot:*".to_string(), "kb:*".to_string(), "app:*".to_string(), "analytics:*".to_string(), ], 10, ), Role::system_role( "manager", "Manager", vec![ "org:members:view".to_string(), "bot:create".to_string(), "bot:edit".to_string(), "bot:delete".to_string(), "bot:publish".to_string(), "kb:read".to_string(), "kb:write".to_string(), "app:create".to_string(), "app:edit".to_string(), "app:delete".to_string(), "analytics:view".to_string(), "analytics:export".to_string(), ], 20, ), Role::system_role( "member", "Member", vec![ "bot:create".to_string(), "bot:edit".to_string(), "kb:read".to_string(), "kb:write".to_string(), "app:create".to_string(), "app:edit".to_string(), "analytics:view".to_string(), ], 50, ), Role::system_role( "viewer", "Viewer", vec![ "bot:view".to_string(), "kb:read".to_string(), "app:view".to_string(), "analytics:view".to_string(), ], 80, ), Role::system_role( "guest", "Guest", vec![ "kb:read".to_string(), ], 90, ), ] } // ============================================================================ // Groups // ============================================================================ /// Group definition #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Group { pub id: Uuid, pub organization_id: Uuid, pub name: String, pub display_name: String, pub description: Option, pub permissions: Vec, pub parent_group_id: Option, pub is_system: bool, pub created_at: DateTime, pub updated_at: DateTime, } impl Group { pub fn new(organization_id: Uuid, name: &str, display_name: &str) -> Self { let now = Utc::now(); Self { id: Uuid::new_v4(), organization_id, name: name.to_string(), display_name: display_name.to_string(), description: None, permissions: Vec::new(), parent_group_id: None, is_system: false, created_at: now, updated_at: now, } } pub fn with_permissions(mut self, permissions: Vec) -> Self { self.permissions = permissions; self } pub fn with_description(mut self, description: String) -> Self { self.description = Some(description); self } pub fn as_system(mut self) -> Self { self.is_system = true; self } pub fn has_permission(&self, permission: &str) -> bool { self.permissions.contains(&permission.to_string()) } } /// Group member #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GroupMember { pub id: Uuid, pub group_id: Uuid, pub user_id: Uuid, pub added_by: Option, pub created_at: DateTime, } impl GroupMember { pub fn new(group_id: Uuid, user_id: Uuid, added_by: Option) -> Self { Self { id: Uuid::new_v4(), group_id, user_id, added_by, created_at: Utc::now(), } } } /// Default groups for an organization pub fn default_groups(organization_id: Uuid) -> Vec { vec![ Group::new(organization_id, "everyone", "Everyone") .with_description("All members of the organization".to_string()) .with_permissions(vec!["kb:read:public".to_string()]) .as_system(), Group::new(organization_id, "developers", "Developers") .with_description("Development team members".to_string()) .with_permissions(vec![ "bot:create".to_string(), "bot:edit".to_string(), "kb:write".to_string(), "app:create".to_string(), ]), Group::new(organization_id, "content_managers", "Content Managers") .with_description("Content management team".to_string()) .with_permissions(vec![ "kb:read".to_string(), "kb:write".to_string(), ]), Group::new(organization_id, "support", "Support Team") .with_description("Customer support team".to_string()) .with_permissions(vec![ "bot:view".to_string(), "kb:read".to_string(), "analytics:view".to_string(), ]), ] } // ============================================================================ // User Role Assignment // ============================================================================ /// User role within an organization #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UserRole { pub id: Uuid, pub user_id: Uuid, pub organization_id: Uuid, pub role_id: Uuid, pub role_name: String, pub assigned_by: Option, pub assigned_at: DateTime, pub expires_at: Option>, } impl UserRole { pub fn new( user_id: Uuid, organization_id: Uuid, role_id: Uuid, role_name: &str, assigned_by: Option, ) -> Self { Self { id: Uuid::new_v4(), user_id, organization_id, role_id, role_name: role_name.to_string(), assigned_by, assigned_at: Utc::now(), expires_at: None, } } pub fn is_expired(&self) -> bool { self.expires_at .map(|exp| exp < Utc::now()) .unwrap_or(false) } } // ============================================================================ // Bot Access Control // ============================================================================ /// Bot access configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BotAccessConfig { pub bot_id: Uuid, pub organization_id: Uuid, pub visibility: BotVisibility, pub allowed_roles: Vec, pub allowed_groups: Vec, pub allowed_users: Vec, pub denied_users: Vec, pub requires_authentication: bool, pub ip_restrictions: Vec, pub rate_limit_per_user: Option, pub created_at: DateTime, pub updated_at: DateTime, } impl BotAccessConfig { pub fn new(bot_id: Uuid, organization_id: Uuid) -> Self { let now = Utc::now(); Self { bot_id, organization_id, visibility: BotVisibility::Private, allowed_roles: Vec::new(), allowed_groups: Vec::new(), allowed_users: Vec::new(), denied_users: Vec::new(), requires_authentication: true, ip_restrictions: Vec::new(), rate_limit_per_user: None, created_at: now, updated_at: now, } } pub fn public() -> Self { Self { visibility: BotVisibility::Public, requires_authentication: false, ..Self::new(Uuid::nil(), Uuid::nil()) } } pub fn check_access(&self, user: &UserAccessContext) -> AccessCheckResult { // Check if user is explicitly denied if self.denied_users.contains(&user.user_id) { return AccessCheckResult::Denied("User explicitly denied".to_string()); } // Public bots if self.visibility == BotVisibility::Public { if self.requires_authentication && !user.is_authenticated { return AccessCheckResult::Denied("Authentication required".to_string()); } return AccessCheckResult::Allowed; } // Must be authenticated for non-public bots if !user.is_authenticated { return AccessCheckResult::Denied("Authentication required".to_string()); } // Check if user is explicitly allowed if self.allowed_users.contains(&user.user_id) { return AccessCheckResult::Allowed; } // Check role access if !self.allowed_roles.is_empty() { for role in &user.roles { if self.allowed_roles.contains(role) { return AccessCheckResult::Allowed; } } } // Check group access if !self.allowed_groups.is_empty() { for group in &user.groups { if self.allowed_groups.contains(group) { return AccessCheckResult::Allowed; } } } // Organization-wide access if self.visibility == BotVisibility::Organization { if user.organization_id == Some(self.organization_id) { return AccessCheckResult::Allowed; } } AccessCheckResult::Denied("Access not granted".to_string()) } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum BotVisibility { Private, // Only explicit users/roles/groups Organization, // All org members Public, // Anyone (optionally with auth) } // ============================================================================ // App Access Control // ============================================================================ /// App access configuration (Forms, Sites, etc.) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppAccessConfig { pub app_id: Uuid, pub app_type: AppType, pub organization_id: Uuid, pub visibility: AppVisibility, pub allowed_roles: Vec, pub allowed_groups: Vec, pub allowed_users: Vec, pub requires_authentication: bool, pub submission_requires_auth: bool, // For forms pub created_at: DateTime, pub updated_at: DateTime, } impl AppAccessConfig { pub fn new(app_id: Uuid, app_type: AppType, organization_id: Uuid) -> Self { let now = Utc::now(); Self { app_id, app_type, organization_id, visibility: AppVisibility::Private, allowed_roles: Vec::new(), allowed_groups: Vec::new(), allowed_users: Vec::new(), requires_authentication: true, submission_requires_auth: false, created_at: now, updated_at: now, } } pub fn check_access(&self, user: &UserAccessContext, action: AppAction) -> AccessCheckResult { match action { AppAction::View => self.check_view_access(user), AppAction::Edit => self.check_edit_access(user), AppAction::Submit => self.check_submit_access(user), AppAction::Admin => self.check_admin_access(user), } } fn check_view_access(&self, user: &UserAccessContext) -> AccessCheckResult { if self.visibility == AppVisibility::Public { if self.requires_authentication && !user.is_authenticated { return AccessCheckResult::Denied("Authentication required".to_string()); } return AccessCheckResult::Allowed; } if !user.is_authenticated { return AccessCheckResult::Denied("Authentication required".to_string()); } self.check_membership(user) } fn check_edit_access(&self, user: &UserAccessContext) -> AccessCheckResult { if !user.is_authenticated { return AccessCheckResult::Denied("Authentication required".to_string()); } // Check for edit permission in roles if user.has_permission("app:edit") { return self.check_membership(user); } AccessCheckResult::Denied("Edit permission required".to_string()) } fn check_submit_access(&self, user: &UserAccessContext) -> AccessCheckResult { if self.submission_requires_auth && !user.is_authenticated { return AccessCheckResult::Denied("Authentication required for submission".to_string()); } // Public apps allow submissions if self.visibility == AppVisibility::Public { return AccessCheckResult::Allowed; } self.check_membership(user) } fn check_admin_access(&self, user: &UserAccessContext) -> AccessCheckResult { if !user.is_authenticated { return AccessCheckResult::Denied("Authentication required".to_string()); } if user.has_permission("app:admin") || user.has_permission("*") { return AccessCheckResult::Allowed; } AccessCheckResult::Denied("Admin permission required".to_string()) } fn check_membership(&self, user: &UserAccessContext) -> AccessCheckResult { // Explicit user access if self.allowed_users.contains(&user.user_id) { return AccessCheckResult::Allowed; } // Role access if !self.allowed_roles.is_empty() { for role in &user.roles { if self.allowed_roles.contains(role) { return AccessCheckResult::Allowed; } } } // Group access if !self.allowed_groups.is_empty() { for group in &user.groups { if self.allowed_groups.contains(group) { return AccessCheckResult::Allowed; } } } // Organization-wide if self.visibility == AppVisibility::Organization { if user.organization_id == Some(self.organization_id) { return AccessCheckResult::Allowed; } } AccessCheckResult::Denied("Access not granted".to_string()) } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum AppType { Form, Site, Dashboard, Report, Workflow, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum AppVisibility { Private, Organization, Public, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AppAction { View, Edit, Submit, Admin, } // ============================================================================ // Access Check Types // ============================================================================ /// User context for access checks #[derive(Debug, Clone)] pub struct UserAccessContext { pub user_id: Uuid, pub is_authenticated: bool, pub organization_id: Option, pub roles: Vec, pub groups: Vec, pub permissions: Vec, } impl UserAccessContext { pub fn anonymous() -> Self { Self { user_id: Uuid::nil(), is_authenticated: false, organization_id: None, roles: Vec::new(), groups: Vec::new(), permissions: Vec::new(), } } pub fn authenticated(user_id: Uuid, org_id: Option) -> Self { Self { user_id, is_authenticated: true, organization_id: org_id, roles: Vec::new(), groups: Vec::new(), permissions: Vec::new(), } } pub fn with_roles(mut self, roles: Vec) -> Self { self.roles = roles; self } pub fn with_groups(mut self, groups: Vec) -> Self { self.groups = groups; self } pub fn with_permissions(mut self, permissions: Vec) -> Self { self.permissions = permissions; self } pub fn has_permission(&self, permission: &str) -> bool { self.permissions.contains(&permission.to_string()) || self.permissions.contains(&"*".to_string()) } pub fn has_role(&self, role: &str) -> bool { self.roles.contains(&role.to_string()) } pub fn has_group(&self, group: &str) -> bool { self.groups.contains(&group.to_string()) } } /// Access check result #[derive(Debug, Clone)] pub enum AccessCheckResult { Allowed, Denied(String), RequiresElevation(String), } impl AccessCheckResult { pub fn is_allowed(&self) -> bool { matches!(self, Self::Allowed) } pub fn reason(&self) -> Option<&str> { match self { Self::Allowed => None, Self::Denied(r) => Some(r), Self::RequiresElevation(r) => Some(r), } } } // ============================================================================ // Organization Service // ============================================================================ /// Organization management service pub struct OrganizationService { /// Database connection pool for organization operations _db_pool: DbPool, } impl OrganizationService { pub fn new(db_pool: DbPool) -> Self { Self { _db_pool: db_pool } } /// Get a database connection from the pool fn _get_conn(&self) -> Result>, OrganizationError> { self._db_pool.get().map_err(|e| { OrganizationError::DatabaseError(format!("Failed to get database connection: {}", e)) }) } /// Create a new organization with default roles and groups pub async fn create_organization( &self, name: String, owner_id: Uuid, plan_id: Option, ) -> Result { // Create organization let mut org = Organization::new(name, owner_id); if let Some(plan) = plan_id { org = org.with_plan(plan); } // Create default roles for org (custom roles in addition to system roles) let roles = default_roles(); // Create default groups let groups = default_groups(org.id); // Create owner membership let owner_member = OrganizationMember::new(org.id, owner_id, "owner"); // Assign owner role let owner_role = roles.iter().find(|r| r.name == "owner").unwrap(); let owner_role_assignment = UserRole::new( owner_id, org.id, owner_role.id, &owner_role.name, None, ); Ok(OrganizationCreationResult { organization: org, roles, groups, owner_member, owner_role: owner_role_assignment, }) } /// Invite a user to an organization pub fn create_invitation( &self, organization_id: Uuid, user_id: Uuid, role: &str, invited_by: Uuid, ) -> OrganizationMember { OrganizationMember::new(organization_id, user_id, role) .as_invited(invited_by) } /// Check if user has permission in organization pub fn check_permission( &self, user: &UserAccessContext, permission: &str, ) -> bool { user.has_permission(permission) } /// Get effective permissions for a user (from roles + groups) pub fn get_effective_permissions( &self, roles: &[Role], groups: &[Group], ) -> Vec { let mut permissions = Vec::new(); for role in roles { for perm in &role.permissions { if !permissions.contains(perm) { permissions.push(perm.clone()); } } } for group in groups { for perm in &group.permissions { if !permissions.contains(perm) { permissions.push(perm.clone()); } } } permissions } } /// Result of organization creation #[derive(Debug)] pub struct OrganizationCreationResult { pub organization: Organization, pub roles: Vec, pub groups: Vec, pub owner_member: OrganizationMember, pub owner_role: UserRole, } // ============================================================================ // Errors // ============================================================================ #[derive(Debug, Clone)] pub enum OrganizationError { NotFound, AlreadyExists, InvalidName(String), PermissionDenied(String), MemberLimitReached, InvalidRole(String), InvalidGroup(String), DatabaseError(String), } impl std::fmt::Display for OrganizationError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::NotFound => write!(f, "Organization not found"), Self::AlreadyExists => write!(f, "Organization already exists"), Self::InvalidName(n) => write!(f, "Invalid organization name: {}", n), Self::PermissionDenied(p) => write!(f, "Permission denied: {}", p), Self::MemberLimitReached => write!(f, "Member limit reached"), Self::InvalidRole(r) => write!(f, "Invalid role: {}", r), Self::InvalidGroup(g) => write!(f, "Invalid group: {}", g), Self::DatabaseError(e) => write!(f, "Database error: {}", e), } } } impl std::error::Error for OrganizationError {} // ============================================================================ // Helper Functions // ============================================================================ /// Generate a URL-safe slug from a name fn slugify(name: &str) -> String { name.to_lowercase() .chars() .map(|c| { if c.is_alphanumeric() { c } else if c.is_whitespace() || c == '-' || c == '_' { '-' } else { '\0' } }) .filter(|c| *c != '\0') .collect::() .split('-') .filter(|s| !s.is_empty()) .collect::>() .join("-") } // ============================================================================ // Permission Constants // ============================================================================ pub mod permissions { pub const ORG_MANAGE: &str = "org:manage"; pub const ORG_BILLING: &str = "org:billing"; pub const ORG_MEMBERS: &str = "org:members"; pub const ORG_MEMBERS_VIEW: &str = "org:members:view"; pub const ORG_SETTINGS: &str = "org:settings"; pub const ORG_DELETE: &str = "org:delete"; pub const BOT_CREATE: &str = "bot:create"; pub const BOT_VIEW: &str = "bot:view"; pub const BOT_EDIT: &str = "bot:edit"; pub const BOT_DELETE: &str = "bot:delete"; pub const BOT_PUBLISH: &str = "bot:publish"; pub const BOT_ALL: &str = "bot:*"; pub const KB_READ: &str = "kb:read"; pub const KB_WRITE: &str = "kb:write"; pub const KB_ADMIN: &str = "kb:admin"; pub const KB_ALL: &str = "kb:*"; pub const APP_CREATE: &str = "app:create"; pub const APP_VIEW: &str = "app:view"; pub const APP_EDIT: &str = "app:edit"; pub const APP_DELETE: &str = "app:delete"; pub const APP_ADMIN: &str = "app:admin"; pub const APP_ALL: &str = "app:*"; pub const ANALYTICS_VIEW: &str = "analytics:view"; pub const ANALYTICS_EXPORT: &str = "analytics:export"; pub const ANALYTICS_ALL: &str = "analytics:*"; pub const WILDCARD: &str = "*"; }