From 5fe0da1a658bc62e84b99ca31253c78a9366a12c Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Wed, 29 Oct 2025 22:27:32 +0100 Subject: [PATCH] tenants --- .../down.sql | 2 + .../up.sql | 2 + backend/src/bin/admin.rs | 146 ++++++++++-------- backend/src/tenants.rs | 20 +-- 4 files changed, 83 insertions(+), 87 deletions(-) create mode 100644 backend/migrations/202512010165_drop_tenant_name_unique/down.sql create mode 100644 backend/migrations/202512010165_drop_tenant_name_unique/up.sql diff --git a/backend/migrations/202512010165_drop_tenant_name_unique/down.sql b/backend/migrations/202512010165_drop_tenant_name_unique/down.sql new file mode 100644 index 0000000..4c306fa --- /dev/null +++ b/backend/migrations/202512010165_drop_tenant_name_unique/down.sql @@ -0,0 +1,2 @@ +ALTER TABLE tenants + ADD CONSTRAINT tenants_name_key UNIQUE (name); diff --git a/backend/migrations/202512010165_drop_tenant_name_unique/up.sql b/backend/migrations/202512010165_drop_tenant_name_unique/up.sql new file mode 100644 index 0000000..4dffef4 --- /dev/null +++ b/backend/migrations/202512010165_drop_tenant_name_unique/up.sql @@ -0,0 +1,2 @@ +ALTER TABLE tenants + DROP CONSTRAINT IF EXISTS tenants_name_key; diff --git a/backend/src/bin/admin.rs b/backend/src/bin/admin.rs index d66b533..946fa5a 100644 --- a/backend/src/bin/admin.rs +++ b/backend/src/bin/admin.rs @@ -44,23 +44,23 @@ enum Command { quickwit_index: Option, }, DeleteTenant { - name: String, + tenant_id: Uuid, }, AddUserToTenant { username: String, - name: String, + tenant_id: Uuid, }, RemoveUserFromTenant { username: String, - name: String, + tenant_id: Uuid, }, ReanalyzeDocuments { - name: String, + tenant_id: Uuid, }, ListTenants, - DeleteAssets(String), - QuickwitCreate(String), - QuickwitDelete(String), + DeleteAssets(Uuid), + QuickwitCreate(Uuid), + QuickwitDelete(Uuid), } impl Command { @@ -71,14 +71,19 @@ impl Command { list-users\n\ delete-user \n\ create-tenant [storage_root] [quickwit_index]\n\ - delete-tenant \n\ - add-user-to-tenant \n\ - remove-user-from-tenant \n\ - reanalyze-documents \n\ + delete-tenant \n\ + add-user-to-tenant \n\ + remove-user-from-tenant \n\ + reanalyze-documents \n\ list-tenants\n\ - delete-assets \n\ - quickwit-create-index \n\ - quickwit-delete-index " + delete-assets \n\ + quickwit-create-index \n\ + quickwit-delete-index " + } + + fn parse_tenant_id(arg: Option) -> Result { + let raw = arg.ok_or_else(|| anyhow!("tenant id required"))?; + Uuid::parse_str(&raw).map_err(|_| anyhow!("invalid tenant id: {}", raw)) } fn parse() -> Result { @@ -102,29 +107,27 @@ impl Command { quickwit_index: args.next(), }), Some("delete-tenant") => Ok(Self::DeleteTenant { - name: args.next().ok_or_else(|| anyhow!("tenant name required"))?, + tenant_id: Self::parse_tenant_id(args.next())?, }), Some("add-user-to-tenant") => Ok(Self::AddUserToTenant { username: args.next().ok_or_else(|| anyhow!("username required"))?, - name: args.next().ok_or_else(|| anyhow!("tenant name required"))?, + tenant_id: Self::parse_tenant_id(args.next())?, }), Some("remove-user-from-tenant") => Ok(Self::RemoveUserFromTenant { username: args.next().ok_or_else(|| anyhow!("username required"))?, - name: args.next().ok_or_else(|| anyhow!("tenant name required"))?, + tenant_id: Self::parse_tenant_id(args.next())?, }), Some("reanalyze-documents") => Ok(Self::ReanalyzeDocuments { - name: args.next().ok_or_else(|| anyhow!("tenant name required"))?, + tenant_id: Self::parse_tenant_id(args.next())?, }), Some("list-tenants") => Ok(Self::ListTenants), - Some("delete-assets") => Ok(Self::DeleteAssets( - args.next().ok_or_else(|| anyhow!("tenant name required"))?, - )), - Some("quickwit-create-index") => Ok(Self::QuickwitCreate( - args.next().ok_or_else(|| anyhow!("tenant name required"))?, - )), - Some("quickwit-delete-index") => Ok(Self::QuickwitDelete( - args.next().ok_or_else(|| anyhow!("tenant name required"))?, - )), + Some("delete-assets") => Ok(Self::DeleteAssets(Self::parse_tenant_id(args.next())?)), + Some("quickwit-create-index") => { + Ok(Self::QuickwitCreate(Self::parse_tenant_id(args.next())?)) + } + Some("quickwit-delete-index") => { + Ok(Self::QuickwitDelete(Self::parse_tenant_id(args.next())?)) + } _ => Err(anyhow!(Self::usage())), } } @@ -147,19 +150,25 @@ async fn main() -> Result<()> { storage_root, quickwit_index, } => create_tenant(&pool, &name, storage_root, quickwit_index)?, - Command::DeleteTenant { name } => delete_tenant(&pool, &name)?, - Command::AddUserToTenant { username, name } => add_user_to_tenant(&pool, &username, &name)?, - Command::RemoveUserFromTenant { username, name } => { - remove_user_from_tenant(&pool, &username, &name)? - } - Command::ReanalyzeDocuments { name } => reanalyze_documents(&pool, &name)?, + Command::DeleteTenant { tenant_id } => delete_tenant(&pool, tenant_id)?, + Command::AddUserToTenant { + username, + tenant_id, + } => add_user_to_tenant(&pool, &username, tenant_id)?, + Command::RemoveUserFromTenant { + username, + tenant_id, + } => remove_user_from_tenant(&pool, &username, tenant_id)?, + Command::ReanalyzeDocuments { tenant_id } => reanalyze_documents(&pool, tenant_id)?, Command::ListTenants => list_tenants(&pool)?, - Command::DeleteAssets(name) => delete_assets_for_tenant(&config, &pool, &name).await?, - Command::QuickwitCreate(name) => { - quickwit_index(&config, &pool, &name, Method::POST).await? + Command::DeleteAssets(tenant_id) => { + delete_assets_for_tenant(&config, &pool, tenant_id).await? } - Command::QuickwitDelete(name) => { - quickwit_index(&config, &pool, &name, Method::DELETE).await? + Command::QuickwitCreate(tenant_id) => { + quickwit_index(&config, &pool, tenant_id, Method::POST).await? + } + Command::QuickwitDelete(tenant_id) => { + quickwit_index(&config, &pool, tenant_id, Method::DELETE).await? } } @@ -297,29 +306,29 @@ fn create_tenant( Ok(()) } -fn delete_tenant(pool: &PgPool, name: &str) -> Result<()> { +fn delete_tenant(pool: &PgPool, tenant_id: Uuid) -> Result<()> { let mut conn = pool.get().context("failed to get database connection")?; let tenant: Tenant = tenants::table - .filter(tenants::name.eq(name)) + .find(tenant_id) .first(&mut conn) .optional()? - .ok_or_else(|| anyhow!("tenant '{}' not found", name))?; + .ok_or_else(|| anyhow!("tenant '{}' not found", tenant_id))?; let member_exists: bool = select(exists( user_memberships::table.filter(user_memberships::tenant_id.eq(tenant.id)), )) .get_result(&mut conn)?; if member_exists { - bail!("tenant '{}' still has user memberships", name); + bail!("tenant '{}' still has user memberships", tenant.name); } diesel::delete(tenants::table.filter(tenants::id.eq(tenant.id))).execute(&mut conn)?; - println!("deleted tenant '{}'", name); + println!("deleted tenant '{}'", tenant.name); Ok(()) } -fn add_user_to_tenant(pool: &PgPool, username: &str, name: &str) -> Result<()> { +fn add_user_to_tenant(pool: &PgPool, username: &str, tenant_id: Uuid) -> Result<()> { let mut conn = pool.get().context("failed to get database connection")?; let user: User = users::table @@ -329,10 +338,10 @@ fn add_user_to_tenant(pool: &PgPool, username: &str, name: &str) -> Result<()> { .ok_or_else(|| anyhow!("user '{}' not found", username))?; let tenant: Tenant = tenants::table - .filter(tenants::name.eq(name)) + .find(tenant_id) .first(&mut conn) .optional()? - .ok_or_else(|| anyhow!("tenant '{}' not found", name))?; + .ok_or_else(|| anyhow!("tenant '{}' not found", tenant_id))?; let membership = NewUserMembership { id: Uuid::new_v4(), @@ -346,11 +355,11 @@ fn add_user_to_tenant(pool: &PgPool, username: &str, name: &str) -> Result<()> { .do_nothing() .execute(&mut conn)?; - println!("added user '{}' to tenant '{}'", username, name); + println!("added user '{}' to tenant '{}'", username, tenant.name); Ok(()) } -fn remove_user_from_tenant(pool: &PgPool, username: &str, name: &str) -> Result<()> { +fn remove_user_from_tenant(pool: &PgPool, username: &str, tenant_id: Uuid) -> Result<()> { let mut conn = pool.get().context("failed to get database connection")?; let user: User = users::table @@ -360,10 +369,10 @@ fn remove_user_from_tenant(pool: &PgPool, username: &str, name: &str) -> Result< .ok_or_else(|| anyhow!("user '{}' not found", username))?; let tenant: Tenant = tenants::table - .filter(tenants::name.eq(name)) + .find(tenant_id) .first(&mut conn) .optional()? - .ok_or_else(|| anyhow!("tenant '{}' not found", name))?; + .ok_or_else(|| anyhow!("tenant '{}' not found", tenant_id))?; let removed = diesel::delete( user_memberships::table @@ -373,21 +382,24 @@ fn remove_user_from_tenant(pool: &PgPool, username: &str, name: &str) -> Result< .execute(&mut conn)?; if removed == 0 { - println!("user '{}' was not a member of tenant '{}'", username, name); + println!( + "user '{}' was not a member of tenant '{}'", + username, tenant.name + ); } else { - println!("removed user '{}' from tenant '{}'", username, name); + println!("removed user '{}' from tenant '{}'", username, tenant.name); } Ok(()) } -fn reanalyze_documents(pool: &PgPool, name: &str) -> Result<()> { +fn reanalyze_documents(pool: &PgPool, tenant_id: Uuid) -> Result<()> { let mut conn = pool.get().context("failed to get database connection")?; let tenant: Tenant = tenants::table - .filter(tenants::name.eq(name)) + .find(tenant_id) .first(&mut conn) .optional()? - .ok_or_else(|| anyhow!("tenant '{}' not found", name))?; + .ok_or_else(|| anyhow!("tenant '{}' not found", tenant_id))?; let targets: Vec<(Uuid, Uuid)> = documents::table .filter(documents::tenant_id.eq(tenant.id)) @@ -396,7 +408,7 @@ fn reanalyze_documents(pool: &PgPool, name: &str) -> Result<()> { .load(&mut conn)?; if targets.is_empty() { - println!("tenant '{}' has no active documents", name); + println!("tenant '{}' has no active documents", tenant.name); return Ok(()); } @@ -419,7 +431,7 @@ fn reanalyze_documents(pool: &PgPool, name: &str) -> Result<()> { println!( "queued {} documents for re-analysis in tenant '{}'", - queued, name + queued, tenant.name ); Ok(()) } @@ -437,7 +449,7 @@ fn list_tenants(pool: &PgPool) -> Result<()> { } for tenant in tenants { - println!("{} ({})", tenant.name, tenant.id); + println!("{} {}", tenant.id, tenant.name); } Ok(()) @@ -446,7 +458,7 @@ fn list_tenants(pool: &PgPool) -> Result<()> { async fn delete_assets_for_tenant( config: &AppConfig, pool: &PgPool, - tenant_name: &str, + tenant_id: Uuid, ) -> Result<()> { let s3_client = s3::build_client(config).await?; let storage: Arc = @@ -454,11 +466,10 @@ async fn delete_assets_for_tenant( let mut conn = pool.get().context("failed to get database connection")?; let tenant: Tenant = tenants::table - .filter(tenants::name.eq(tenant_name)) + .find(tenant_id) .first(&mut conn) - .optional() - .context("failed to load tenant")? - .ok_or_else(|| anyhow!("tenant '{}' not found", tenant_name))?; + .optional()? + .ok_or_else(|| anyhow!("tenant '{}' not found", tenant_id))?; let tenant_storage = TenantStorage::new(Arc::clone(&storage), &tenant) .with_context(|| format!("missing storage root for tenant {}", tenant.name))?; @@ -516,7 +527,7 @@ async fn delete_assets_for_tenant( async fn quickwit_index( config: &AppConfig, pool: &PgPool, - name: &str, + tenant_id: Uuid, method: Method, ) -> Result<()> { let endpoint = config @@ -526,11 +537,10 @@ async fn quickwit_index( let mut conn = pool.get().context("failed to get database connection")?; let tenant: Tenant = tenants::table - .filter(tenants::name.eq(name)) + .find(tenant_id) .first(&mut conn) - .optional() - .context("failed to query tenants")? - .ok_or_else(|| anyhow!("tenant '{}' not found", name))?; + .optional()? + .ok_or_else(|| anyhow!("tenant '{}' not found", tenant_id))?; let client = Client::new(); let index_id = format!("documents-{}", tenant.id); diff --git a/backend/src/tenants.rs b/backend/src/tenants.rs index c756cd3..fc97ac7 100644 --- a/backend/src/tenants.rs +++ b/backend/src/tenants.rs @@ -1,9 +1,4 @@ -use diesel::{ - dsl::{exists, select}, - pg::PgConnection, - prelude::*, - sql_types::Text, -}; +use diesel::{pg::PgConnection, prelude::*, sql_types::Text}; use serde_json::json; use uuid::Uuid; @@ -51,10 +46,6 @@ impl TenantService { Ok(tenant) } - pub fn tenant_id_for_name(&self, name: &str) -> AppResult { - Ok(self.get_by_name(name)?.id) - } - pub fn create_tenant( &self, name: &str, @@ -74,15 +65,6 @@ impl TenantService { AppError::internal("database pool error") })?; - let exists: bool = - select(exists(dsl::tenants.filter(dsl::name.eq(name)))).get_result(&mut conn)?; - if exists { - return Err(AppError::conflict(format!( - "tenant '{}' already exists", - name - ))); - } - let id = Uuid::new_v4(); let storage_root = normalize_storage_root(storage_root, id); let quickwit_index = normalize_quickwit_index(quickwit_index, id);