1046 lines
30 KiB
Rust
1046 lines
30 KiB
Rust
//! 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<String>,
|
|
pub logo_url: Option<String>,
|
|
pub website: Option<String>,
|
|
pub plan_id: String,
|
|
pub owner_id: Uuid,
|
|
pub settings: OrganizationSettings,
|
|
pub metadata: HashMap<String, serde_json::Value>,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
pub deleted_at: Option<DateTime<Utc>>,
|
|
}
|
|
|
|
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<String>,
|
|
pub default_user_role: String,
|
|
pub max_members: Option<u32>,
|
|
pub sso_enabled: bool,
|
|
pub sso_provider: Option<String>,
|
|
pub audit_log_retention_days: u32,
|
|
pub ip_whitelist: Vec<String>,
|
|
pub custom_branding: Option<CustomBranding>,
|
|
}
|
|
|
|
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<String>,
|
|
pub logo_url: Option<String>,
|
|
pub favicon_url: Option<String>,
|
|
pub custom_css: Option<String>,
|
|
}
|
|
|
|
// ============================================================================
|
|
// 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<Uuid>,
|
|
pub invited_at: Option<DateTime<Utc>>,
|
|
pub joined_at: Option<DateTime<Utc>>,
|
|
pub last_active_at: Option<DateTime<Utc>>,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
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<Uuid>, // None for system roles
|
|
pub name: String,
|
|
pub display_name: String,
|
|
pub description: Option<String>,
|
|
pub permissions: Vec<String>,
|
|
pub is_system: bool,
|
|
pub is_default: bool,
|
|
pub hierarchy_level: u32, // Lower = more powerful
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
impl Role {
|
|
pub fn new(name: &str, display_name: &str, permissions: Vec<String>) -> 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<String>, 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<Role> {
|
|
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<String>,
|
|
pub permissions: Vec<String>,
|
|
pub parent_group_id: Option<Uuid>,
|
|
pub is_system: bool,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
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<String>) -> 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<Uuid>,
|
|
pub created_at: DateTime<Utc>,
|
|
}
|
|
|
|
impl GroupMember {
|
|
pub fn new(group_id: Uuid, user_id: Uuid, added_by: Option<Uuid>) -> 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<Group> {
|
|
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<Uuid>,
|
|
pub assigned_at: DateTime<Utc>,
|
|
pub expires_at: Option<DateTime<Utc>>,
|
|
}
|
|
|
|
impl UserRole {
|
|
pub fn new(
|
|
user_id: Uuid,
|
|
organization_id: Uuid,
|
|
role_id: Uuid,
|
|
role_name: &str,
|
|
assigned_by: Option<Uuid>,
|
|
) -> 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<String>,
|
|
pub allowed_groups: Vec<String>,
|
|
pub allowed_users: Vec<Uuid>,
|
|
pub denied_users: Vec<Uuid>,
|
|
pub requires_authentication: bool,
|
|
pub ip_restrictions: Vec<String>,
|
|
pub rate_limit_per_user: Option<u32>,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
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
|
|
&& 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<String>,
|
|
pub allowed_groups: Vec<String>,
|
|
pub allowed_users: Vec<Uuid>,
|
|
pub requires_authentication: bool,
|
|
pub submission_requires_auth: bool, // For forms
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
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
|
|
&& 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<Uuid>,
|
|
pub roles: Vec<String>,
|
|
pub groups: Vec<String>,
|
|
pub permissions: Vec<String>,
|
|
}
|
|
|
|
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<Uuid>) -> 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<String>) -> Self {
|
|
self.roles = roles;
|
|
self
|
|
}
|
|
|
|
pub fn with_groups(mut self, groups: Vec<String>) -> Self {
|
|
self.groups = groups;
|
|
self
|
|
}
|
|
|
|
pub fn with_permissions(mut self, permissions: Vec<String>) -> 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<diesel::r2d2::PooledConnection<diesel::r2d2::ConnectionManager<diesel::PgConnection>>, 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<String>,
|
|
) -> Result<OrganizationCreationResult, OrganizationError> {
|
|
// 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<String> {
|
|
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<Role>,
|
|
pub groups: Vec<Group>,
|
|
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::<String>()
|
|
.split('-')
|
|
.filter(|s| !s.is_empty())
|
|
.collect::<Vec<_>>()
|
|
.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 = "*";
|
|
}
|