generalbots/src/core/directory/provisioning.rs

370 lines
12 KiB
Rust
Raw Normal View History

2025-11-29 16:29:28 -03:00
use anyhow::Result;
#[cfg(feature = "drive")]
2025-11-29 16:29:28 -03:00
use aws_sdk_s3::Client as S3Client;
2025-12-02 21:09:43 -03:00
use diesel::r2d2::{ConnectionManager, Pool};
2025-11-29 16:29:28 -03:00
use diesel::PgConnection;
use serde::{Deserialize, Serialize};
2025-12-02 21:09:43 -03:00
2025-11-29 16:29:28 -03:00
use std::sync::Arc;
2025-12-02 21:09:43 -03:00
use uuid::Uuid;
pub type DbPool = Pool<ConnectionManager<PgConnection>>;
2025-11-29 16:29:28 -03:00
pub struct UserProvisioningService {
2025-12-02 21:09:43 -03:00
db_pool: DbPool,
#[cfg(feature = "drive")]
2025-12-02 21:09:43 -03:00
s3_client: Option<Arc<S3Client>>,
#[cfg(not(feature = "drive"))]
s3_client: Option<Arc<()>>,
2025-11-29 16:29:28 -03:00
base_url: String,
}
2025-12-02 21:09:43 -03:00
impl std::fmt::Debug for UserProvisioningService {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UserProvisioningService")
.field("base_url", &self.base_url)
.field("has_s3_client", &self.s3_client.is_some())
.finish_non_exhaustive()
}
}
2025-11-29 16:29:28 -03:00
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserAccount {
pub username: String,
pub email: String,
pub first_name: String,
pub last_name: String,
pub organization: String,
pub is_admin: bool,
pub bots: Vec<BotAccess>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BotAccess {
pub bot_id: String,
pub bot_name: String,
pub role: UserRole,
pub home_path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum UserRole {
Admin,
User,
ReadOnly,
}
impl UserProvisioningService {
#[cfg(feature = "drive")]
2025-12-02 21:09:43 -03:00
pub fn new(db_pool: DbPool, s3_client: Option<Arc<S3Client>>, base_url: String) -> Self {
2025-11-29 16:29:28 -03:00
Self {
2025-12-02 21:09:43 -03:00
db_pool,
2025-11-29 16:29:28 -03:00
s3_client,
base_url,
}
}
#[cfg(not(feature = "drive"))]
pub fn new(db_pool: DbPool, _s3_client: Option<Arc<()>>, base_url: String) -> Self {
Self {
db_pool,
s3_client: None,
base_url,
}
}
2025-12-02 21:09:43 -03:00
pub fn get_base_url(&self) -> &str {
&self.base_url
}
pub fn build_profile_url(&self, username: &str) -> String {
format!("{}/users/{}/profile", self.base_url, username)
}
2025-11-29 16:29:28 -03:00
pub async fn provision_user(&self, account: &UserAccount) -> Result<()> {
2025-12-02 21:09:43 -03:00
log::info!(
"Provisioning user: {} via directory at {}",
account.username,
self.base_url
);
2025-11-29 16:29:28 -03:00
let user_id = self.create_database_user(account)?;
2025-11-29 16:29:28 -03:00
for bot_access in &account.bots {
self.create_s3_home(account, bot_access).await?;
}
if let Err(e) = self.setup_email_account(account) {
2025-11-29 16:29:28 -03:00
log::warn!("Email account creation failed: {}", e);
}
self.setup_oauth_config(&user_id, account)?;
2025-11-29 16:29:28 -03:00
2025-12-02 21:09:43 -03:00
let profile_url = self.build_profile_url(&account.username);
log::info!(
"User {} provisioned successfully. Profile: {}",
account.username,
profile_url
);
2025-11-29 16:29:28 -03:00
Ok(())
}
fn create_database_user(&self, account: &UserAccount) -> Result<String> {
2025-11-29 16:29:28 -03:00
use crate::shared::models::schema::users;
2025-12-02 21:09:43 -03:00
use argon2::{
password_hash::{rand_core::OsRng, SaltString},
Argon2, PasswordHasher,
};
2025-11-29 16:29:28 -03:00
use diesel::prelude::*;
2025-12-02 21:09:43 -03:00
let user_id = Uuid::new_v4();
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let password_hash = argon2
.hash_password(Uuid::new_v4().to_string().as_bytes(), &salt)
.map_err(|e| anyhow::anyhow!("Password hashing failed: {}", e))?
.to_string();
2025-11-29 16:29:28 -03:00
2025-12-02 21:09:43 -03:00
let mut conn = self
.db_pool
2025-12-02 21:09:43 -03:00
.get()
.map_err(|e| anyhow::anyhow!("Failed to get database connection: {}", e))?;
2025-11-29 16:29:28 -03:00
diesel::insert_into(users::table)
.values((
2025-12-02 21:09:43 -03:00
users::id.eq(user_id),
2025-11-29 16:29:28 -03:00
users::username.eq(&account.username),
users::email.eq(&account.email),
users::password_hash.eq(&password_hash),
users::is_admin.eq(account.is_admin),
users::created_at.eq(chrono::Utc::now()),
))
2025-12-02 21:09:43 -03:00
.execute(&mut conn)?;
2025-11-29 16:29:28 -03:00
2025-12-02 21:09:43 -03:00
Ok(user_id.to_string())
2025-11-29 16:29:28 -03:00
}
async fn create_s3_home(&self, account: &UserAccount, bot_access: &BotAccess) -> Result<()> {
#[cfg(feature = "drive")]
{
let Some(s3_client) = &self.s3_client else {
log::warn!("S3 client not configured, skipping S3 home creation");
return Ok(());
};
2025-12-02 21:09:43 -03:00
let bucket_name = format!("{}.gbdrive", bot_access.bot_name);
let home_path = format!("home/{}/", account.username);
2025-11-29 16:29:28 -03:00
if s3_client
.head_bucket()
.bucket(&bucket_name)
.send()
.await
.is_err()
{
s3_client
.create_bucket()
.bucket(&bucket_name)
.send()
.await?;
}
2025-11-29 16:29:28 -03:00
2025-12-02 21:09:43 -03:00
s3_client
2025-11-29 16:29:28 -03:00
.put_object()
.bucket(&bucket_name)
.key(&home_path)
2025-11-29 16:29:28 -03:00
.body(aws_sdk_s3::primitives::ByteStream::from(vec![]))
.send()
.await?;
for folder in &["documents", "projects", "shared"] {
let folder_key = format!("{}{}/", home_path, folder);
s3_client
.put_object()
.bucket(&bucket_name)
.key(&folder_key)
.body(aws_sdk_s3::primitives::ByteStream::from(vec![]))
.send()
.await?;
}
log::info!(
"Created S3 home for {} in {}",
account.username,
bucket_name
);
}
#[cfg(not(feature = "drive"))]
{
let _ = account;
let _ = bot_access;
log::debug!("Drive feature not enabled, skipping S3 home creation");
2025-11-29 16:29:28 -03:00
}
Ok(())
}
#[cfg(feature = "mail")]
fn setup_email_account(&self, account: &UserAccount) -> Result<()> {
2025-11-29 16:29:28 -03:00
use crate::shared::models::schema::user_email_accounts;
use diesel::prelude::*;
2025-12-02 21:09:43 -03:00
let mut conn = self
.db_pool
2025-12-02 21:09:43 -03:00
.get()
.map_err(|e| anyhow::anyhow!("Failed to get database connection: {}", e))?;
let user_uuid = Uuid::new_v4();
2025-11-29 16:29:28 -03:00
diesel::insert_into(user_email_accounts::table)
.values((
2025-12-02 21:09:43 -03:00
user_email_accounts::user_id.eq(user_uuid),
2025-11-29 16:29:28 -03:00
user_email_accounts::email.eq(&account.email),
user_email_accounts::imap_server.eq("localhost"),
user_email_accounts::imap_port.eq(993),
user_email_accounts::smtp_server.eq("localhost"),
user_email_accounts::smtp_port.eq(465),
user_email_accounts::username.eq(&account.username),
user_email_accounts::password_encrypted.eq("oauth"),
user_email_accounts::is_active.eq(true),
))
2025-12-02 21:09:43 -03:00
.execute(&mut conn)?;
2025-11-29 16:29:28 -03:00
log::info!("Setup email configuration for: {}", account.email);
Ok(())
}
#[cfg(not(feature = "mail"))]
fn setup_email_account(&self, _account: &UserAccount) -> Result<()> {
log::debug!("Email feature not enabled, skipping email account setup");
Ok(())
}
fn setup_oauth_config(&self, _user_id: &str, account: &UserAccount) -> Result<()> {
use crate::shared::models::schema::bot_configuration;
2025-11-29 16:29:28 -03:00
use diesel::prelude::*;
let services = vec![
("oauth-drive-enabled", "true"),
("oauth-email-enabled", "true"),
("oauth-git-enabled", "true"),
("oauth-provider", "zitadel"),
];
2025-12-02 21:09:43 -03:00
let mut conn = self
.db_pool
2025-12-02 21:09:43 -03:00
.get()
.map_err(|e| anyhow::anyhow!("Failed to get database connection: {}", e))?;
2025-11-29 16:29:28 -03:00
for (key, value) in services {
diesel::insert_into(bot_configuration::table)
2025-11-29 16:29:28 -03:00
.values((
2025-12-02 21:09:43 -03:00
bot_configuration::bot_id.eq(Uuid::nil()),
bot_configuration::config_key.eq(key),
bot_configuration::config_value.eq(value),
bot_configuration::is_encrypted.eq(false),
bot_configuration::config_type.eq("string"),
bot_configuration::created_at.eq(chrono::Utc::now()),
bot_configuration::updated_at.eq(chrono::Utc::now()),
2025-11-29 16:29:28 -03:00
))
.on_conflict((bot_configuration::bot_id, bot_configuration::config_key))
2025-11-29 16:29:28 -03:00
.do_update()
.set(bot_configuration::config_value.eq(value))
2025-12-02 21:09:43 -03:00
.execute(&mut conn)?;
2025-11-29 16:29:28 -03:00
}
log::info!("Setup OAuth configuration for user: {}", account.username);
Ok(())
}
pub async fn deprovision_user(&self, username: &str) -> Result<()> {
log::info!("Deprovisioning user: {}", username);
self.remove_s3_data(username).await?;
self.remove_email_config(username)?;
self.remove_user_from_db(username)?;
2025-11-29 16:29:28 -03:00
log::info!("User {} deprovisioned successfully", username);
Ok(())
}
fn remove_user_from_db(&self, username: &str) -> Result<()> {
2025-11-29 16:29:28 -03:00
use crate::shared::models::schema::users;
use diesel::prelude::*;
2025-12-02 21:09:43 -03:00
let mut conn = self
.db_pool
2025-12-02 21:09:43 -03:00
.get()
.map_err(|e| anyhow::anyhow!("Failed to get database connection: {}", e))?;
diesel::delete(users::table.filter(users::username.eq(username))).execute(&mut conn)?;
2025-11-29 16:29:28 -03:00
Ok(())
}
async fn remove_s3_data(&self, username: &str) -> Result<()> {
#[cfg(feature = "drive")]
2025-12-02 21:09:43 -03:00
if let Some(s3_client) = &self.s3_client {
let buckets_result = s3_client.list_buckets().send().await?;
if let Some(buckets) = buckets_result.buckets {
for bucket in buckets {
if let Some(name) = bucket.name {
if name.ends_with(".gbdrive") {
let prefix = format!("home/{}/", username);
let objects = s3_client
.list_objects_v2()
.bucket(&name)
.prefix(&prefix)
.send()
.await?;
if let Some(contents) = objects.contents {
for object in contents {
if let Some(key) = object.key {
s3_client
.delete_object()
.bucket(&name)
.key(&key)
.send()
.await?;
}
2025-11-29 16:29:28 -03:00
}
}
}
}
}
}
}
#[cfg(not(feature = "drive"))]
{
let _ = username;
log::debug!("Drive feature not enabled, bypassing S3 data removal");
}
2025-11-29 16:29:28 -03:00
Ok(())
}
#[cfg(feature = "mail")]
fn remove_email_config(&self, username: &str) -> Result<()> {
2025-11-29 16:29:28 -03:00
use crate::shared::models::schema::user_email_accounts;
use diesel::prelude::*;
2025-12-02 21:09:43 -03:00
let mut conn = self
.db_pool
2025-12-02 21:09:43 -03:00
.get()
.map_err(|e| anyhow::anyhow!("Failed to get database connection: {}", e))?;
2025-11-29 16:29:28 -03:00
diesel::delete(
user_email_accounts::table.filter(user_email_accounts::username.eq(username)),
)
2025-12-02 21:09:43 -03:00
.execute(&mut conn)?;
2025-11-29 16:29:28 -03:00
Ok(())
}
#[cfg(not(feature = "mail"))]
fn remove_email_config(&self, _username: &str) -> Result<()> {
log::debug!("Email feature not enabled, skipping email config removal");
Ok(())
}
2025-11-29 16:29:28 -03:00
}