botserver/src/core/organization.rs

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 = "*";
}