1use crate::{
2 database::{DatabaseProvider, close_pool_on_drop},
3 password::random_password,
4 tenant::migrate_tenant_storage::{MigrateTenantStorageError, migrate_tenant_storage_inner},
5};
6use docbox_core::{
7 database::{
8 DbErr, DbPool, DbResult, ROOT_DATABASE_NAME,
9 create::{
10 check_database_exists, check_database_role_exists, create_database,
11 create_restricted_role, create_restricted_role_aws_iam, delete_database, delete_role,
12 },
13 migrations::apply_tenant_migrations,
14 models::tenant::{Tenant, TenantId},
15 utils::DatabaseErrorExt,
16 },
17 search::{SearchError, SearchIndexFactory, TenantSearchIndex},
18 secrets::{SecretManager, SecretManagerError},
19 storage::{CreateBucketOutcome, StorageLayer, StorageLayerError, StorageLayerFactory},
20 tenant::tenant_options_ext::TenantOptionsExt,
21};
22use serde::{Deserialize, Serialize};
23use serde_json::json;
24use std::ops::DerefMut;
25use thiserror::Error;
26
27#[derive(Debug, Error)]
29pub enum CreateTenantError {
30 #[error("error connecting to 'postgres' database: {0}")]
32 ConnectPostgres(DbErr),
33
34 #[error("error creating tenant database: {0}")]
36 CreateTenantDatabase(DbErr),
37
38 #[error("error connecting to tenant database: {0}")]
40 ConnectTenantDatabase(DbErr),
41
42 #[error("error connecting to root database: {0}")]
44 ConnectRootDatabase(DbErr),
45
46 #[error("error creating tenant database role: {0}")]
48 CreateTenantRole(DbErr),
49
50 #[error(transparent)]
52 Database(#[from] DbErr),
53
54 #[error("error serializing tenant secret: {0}")]
56 SerializeSecret(serde_json::Error),
57
58 #[error("failed to create tenant secret: secret name already exists")]
60 SecretAlreadyExists,
61
62 #[error("failed to create tenant secret: {0}")]
64 CreateTenantSecret(SecretManagerError),
65
66 #[error("tenant already exists")]
68 TenantAlreadyExist,
69
70 #[error("failed to create tenant storage bucket: {0}")]
72 CreateStorageBucket(StorageLayerError),
73
74 #[error("failed to setup s3 notification rules: {0}")]
76 SetupS3Notifications(StorageLayerError),
77
78 #[error("failed to setup storage origin rules rules: {0}")]
80 SetupStorageOrigins(StorageLayerError),
81
82 #[error("failed to create tenant search index: {0}")]
84 CreateSearchIndex(SearchError),
85
86 #[error("failed to migrate tenant search index: {0}")]
88 MigrateSearchIndex(SearchError),
89
90 #[error("failed to migrate tenant storage: {0}")]
92 MigrateStorage(MigrateTenantStorageError),
93
94 #[error("when not using db_iam_user the db_secret_name must be specified")]
96 MissingDatabaseSecretName,
97}
98
99#[derive(Debug, Deserialize, Serialize, Clone)]
101pub struct CreateTenantConfig {
102 pub id: TenantId,
104 pub name: String,
106 pub env: String,
108
109 pub db_name: String,
111 pub db_role_name: String,
113
114 pub db_secret_name: Option<String>,
117
118 #[serde(default)]
121 pub db_iam_user: bool,
122
123 pub storage_bucket_name: String,
125 pub storage_cors_origins: Vec<String>,
127 pub storage_s3_queue_arn: Option<String>,
130
131 pub search_index_name: String,
133
134 pub event_queue_url: Option<String>,
136}
137
138#[derive(Default)]
140struct CreateTenantRollbackData {
141 search_index: Option<TenantSearchIndex>,
142 storage: Option<StorageLayer>,
143 secret: Option<(SecretManager, String)>,
144 database: Option<String>,
145 db_role: Option<String>,
146}
147
148impl CreateTenantRollbackData {
149 async fn rollback(&mut self, db_provider: &impl DatabaseProvider) {
150 if let Some(search_index) = self.search_index.take()
152 && let Err(error) = search_index.delete_index().await
153 {
154 tracing::error!(?error, "failed to rollback created tenant search index");
155 }
156
157 if let Some(storage) = self.storage.take()
159 && let Err(error) = storage.delete_bucket().await
160 {
161 tracing::error!(?error, "failed to rollback created tenant storage bucket");
162 }
163
164 if let Some((secrets, secret_name)) = self.secret.take()
166 && let Err(error) = secrets.delete_secret(&secret_name, true).await
167 {
168 tracing::error!(?error, "failed to rollback tenant secret");
169 }
170
171 let db_name = self.database.take();
173 let db_role_name = self.db_role.take();
174 if db_name.is_some() || db_role_name.is_some() {
175 match db_provider.connect("postgres").await {
176 Ok(db_postgres) => {
177 if let Some(db_name) = db_name
179 && let Err(error) = delete_database(&db_postgres, &db_name).await
180 {
181 tracing::error!(?error, "failed to rollback tenant database");
182 }
183
184 if let Some(db_role_name) = db_role_name
186 && let Err(error) = delete_role(&db_postgres, &db_role_name).await
187 {
188 tracing::error!(?error, "failed to rollback tenant db role name");
189 }
190
191 db_postgres.close().await;
192 }
193 Err(error) => {
194 tracing::error!(
195 ?error,
196 "failed to rollback tenant database, unable to acquire postgres database"
197 );
198 }
199 }
200 }
201 }
202}
203
204#[tracing::instrument(skip_all, fields(?config))]
218pub async fn create_tenant(
219 db_provider: &impl DatabaseProvider,
220 search_factory: &SearchIndexFactory,
221 storage_factory: &StorageLayerFactory,
222 secrets: &SecretManager,
223 config: CreateTenantConfig,
224) -> Result<Tenant, CreateTenantError> {
225 let mut rollback = CreateTenantRollbackData::default();
226
227 match create_tenant_inner(
228 db_provider,
229 search_factory,
230 storage_factory,
231 secrets,
232 config,
233 &mut rollback,
234 )
235 .await
236 {
237 Ok(value) => Ok(value),
238 Err(error) => {
239 rollback.rollback(db_provider).await;
241 Err(error)
242 }
243 }
244}
245
246#[tracing::instrument(skip_all, fields(?config))]
247async fn create_tenant_inner(
248 db_provider: &impl DatabaseProvider,
249 search_factory: &SearchIndexFactory,
250 storage_factory: &StorageLayerFactory,
251 secrets: &SecretManager,
252 config: CreateTenantConfig,
253 rollback: &mut CreateTenantRollbackData,
254) -> Result<Tenant, CreateTenantError> {
255 let (tenant_db, _tenant_db_guard) = {
256 let db_postgres = db_provider
258 .connect("postgres")
259 .await
260 .map_err(CreateTenantError::ConnectPostgres)?;
261 let _postgres_guard = close_pool_on_drop(&db_postgres);
262
263 initialize_tenant_database(&db_postgres, &config.db_name, rollback).await?;
265 tracing::info!("created tenant database");
266
267 let tenant_db = db_provider
269 .connect(&config.db_name)
270 .await
271 .map_err(CreateTenantError::ConnectTenantDatabase)?;
272
273 let tenant_db_guard = close_pool_on_drop(&tenant_db);
274 (tenant_db, tenant_db_guard)
275 };
276
277 if config.db_iam_user {
278 initialize_tenant_db_role_aws_iam(
279 &tenant_db,
280 &config.db_name,
281 &config.db_role_name,
282 rollback,
283 )
284 .await?;
285
286 tracing::info!("created tenant user (iam)");
287 } else {
288 let db_secret_name = config
289 .db_secret_name
290 .as_ref()
291 .ok_or(CreateTenantError::MissingDatabaseSecretName)?;
292
293 let db_role_password = random_password(30);
295
296 initialize_tenant_db_role(
297 &tenant_db,
298 &config.db_name,
299 &config.db_role_name,
300 &db_role_password,
301 rollback,
302 )
303 .await?;
304 tracing::info!("created tenant user");
305
306 initialize_tenant_db_secret(
307 secrets,
308 db_secret_name,
309 &config.db_role_name,
310 &db_role_password,
311 rollback,
312 )
313 .await?;
314 tracing::info!("created tenant database secret");
315 }
316
317 let root_db = db_provider
319 .connect(ROOT_DATABASE_NAME)
320 .await
321 .map_err(CreateTenantError::ConnectRootDatabase)?;
322
323 let _guard = close_pool_on_drop(&root_db);
324
325 let mut root_transaction = root_db
327 .begin()
328 .await
329 .inspect_err(|error| tracing::error!(?error, "failed to begin root transaction"))?;
330
331 let tenant: Tenant = Tenant::create(
333 root_transaction.deref_mut(),
334 docbox_core::database::models::tenant::CreateTenant {
335 id: config.id,
336 name: config.name,
337 db_name: config.db_name,
338 db_iam_user_name: if config.db_iam_user {
339 Some(config.db_role_name)
340 } else {
341 None
342 },
343 db_secret_name: config.db_secret_name,
344 s3_name: config.storage_bucket_name,
345 os_index_name: config.search_index_name,
346 event_queue_url: config.event_queue_url,
347 env: config.env,
348 },
349 )
350 .await
351 .map_err(|err| {
352 if err.is_duplicate_record() {
354 CreateTenantError::TenantAlreadyExist
355 } else {
356 CreateTenantError::Database(err)
357 }
358 })
359 .inspect_err(|error| tracing::error!(?error, "failed to create tenant"))?;
360
361 let mut tenant_transaction = tenant_db
363 .begin()
364 .await
365 .inspect_err(|error| tracing::error!(?error, "failed to begin tenant transaction"))?;
366
367 apply_tenant_migrations(
369 &mut root_transaction,
370 &mut tenant_transaction,
371 &tenant,
372 None,
373 )
374 .await
375 .inspect_err(|error| tracing::error!(?error, "failed to create tenant tables"))?;
376
377 tracing::debug!("creating tenant storage");
379 let storage = create_tenant_storage(
380 &tenant,
381 storage_factory,
382 config.storage_s3_queue_arn,
383 config.storage_cors_origins,
384 rollback,
385 )
386 .await?;
387
388 migrate_tenant_storage_inner(&storage, &mut root_transaction, &tenant, None)
390 .await
391 .map_err(CreateTenantError::MigrateStorage)?;
392
393 tracing::debug!("creating tenant search index");
395 let search = create_tenant_search(&tenant, search_factory, rollback).await?;
396
397 search
399 .apply_migrations(
400 &tenant,
401 &mut root_transaction,
402 &mut tenant_transaction,
403 None,
404 )
405 .await
406 .map_err(CreateTenantError::MigrateSearchIndex)?;
407
408 tenant_transaction
410 .commit()
411 .await
412 .inspect_err(|error| tracing::error!(?error, "failed to commit tenant transaction"))?;
413 root_transaction
414 .commit()
415 .await
416 .inspect_err(|error| tracing::error!(?error, "failed to commit root transaction"))?;
417
418 Ok(tenant)
419}
420
421#[tracing::instrument(skip(db_provider))]
424pub async fn is_tenant_database_existing(
425 db_provider: &impl DatabaseProvider,
426 db_name: &str,
427) -> DbResult<bool> {
428 let db_postgres = db_provider.connect("postgres").await?;
430 let _guard = close_pool_on_drop(&db_postgres);
431
432 check_database_exists(&db_postgres, db_name).await
433}
434
435#[tracing::instrument(skip(db_postgres, rollback))]
439async fn initialize_tenant_database(
440 db_postgres: &DbPool,
441 db_name: &str,
442 rollback: &mut CreateTenantRollbackData,
443) -> Result<(), CreateTenantError> {
444 let already_exists = match create_database(db_postgres, db_name).await {
445 Ok(_) => false,
447 Err(error) if error.is_database_exists() => true,
449 Err(error) => return Err(CreateTenantError::CreateTenantDatabase(error)),
451 };
452
453 if !already_exists {
454 rollback.database = Some(db_name.to_string());
455 }
456
457 Ok(())
458}
459
460#[tracing::instrument(skip(db_provider))]
463pub async fn is_tenant_database_role_existing(
464 db_provider: &impl DatabaseProvider,
465 role_name: &str,
466) -> DbResult<bool> {
467 let db_postgres = db_provider.connect("postgres").await?;
469
470 let _guard = close_pool_on_drop(&db_postgres);
471
472 check_database_role_exists(&db_postgres, role_name).await
473}
474
475#[tracing::instrument(skip(db, role_password, rollback))]
478async fn initialize_tenant_db_role(
479 db: &DbPool,
480 db_name: &str,
481 role_name: &str,
482 role_password: &str,
483 rollback: &mut CreateTenantRollbackData,
484) -> Result<(), CreateTenantError> {
485 create_restricted_role(db, db_name, role_name, role_password)
487 .await
488 .map_err(CreateTenantError::CreateTenantRole)?;
489
490 rollback.db_role = Some(role_name.to_string());
491
492 Ok(())
493}
494
495#[tracing::instrument(skip(db, rollback))]
498async fn initialize_tenant_db_role_aws_iam(
499 db: &DbPool,
500 db_name: &str,
501 role_name: &str,
502 rollback: &mut CreateTenantRollbackData,
503) -> Result<(), CreateTenantError> {
504 create_restricted_role_aws_iam(db, db_name, role_name)
506 .await
507 .map_err(CreateTenantError::CreateTenantRole)?;
508
509 rollback.db_role = Some(role_name.to_string());
510
511 Ok(())
512}
513
514#[tracing::instrument(skip(secrets))]
517pub async fn is_tenant_database_role_secret_existing(
518 secrets: &SecretManager,
519 secret_name: &str,
520) -> Result<bool, SecretManagerError> {
521 secrets
522 .get_secret(secret_name)
523 .await
524 .map(|value| value.is_some())
525}
526
527#[tracing::instrument(skip(secrets, role_password, rollback))]
529async fn initialize_tenant_db_secret(
530 secrets: &SecretManager,
531 secret_name: &str,
532 role_name: &str,
533 role_password: &str,
534 rollback: &mut CreateTenantRollbackData,
535) -> Result<(), CreateTenantError> {
536 if secrets
538 .has_secret(secret_name)
539 .await
540 .map_err(CreateTenantError::CreateTenantSecret)?
541 {
542 return Err(CreateTenantError::SecretAlreadyExists);
543 }
544
545 let secret_value = serde_json::to_string(&json!({
546 "username": role_name,
547 "password": role_password
548 }))
549 .map_err(CreateTenantError::SerializeSecret)?;
550
551 secrets
552 .set_secret(secret_name, &secret_value)
553 .await
554 .map_err(CreateTenantError::CreateTenantSecret)?;
555
556 rollback.secret = Some((secrets.clone(), secret_name.to_string()));
557
558 Ok(())
559}
560
561#[tracing::instrument(skip(storage, rollback))]
563async fn create_tenant_storage(
564 tenant: &Tenant,
565 storage: &StorageLayerFactory,
566 s3_queue_arn: Option<String>,
567 origins: Vec<String>,
568 rollback: &mut CreateTenantRollbackData,
569) -> Result<StorageLayer, CreateTenantError> {
570 let storage = storage.create_layer(tenant.storage_layer_options());
571 let outcome = storage
572 .create_bucket()
573 .await
574 .inspect_err(|error| tracing::error!(?error, "failed to create tenant bucket"))
575 .map_err(CreateTenantError::CreateStorageBucket)?;
576
577 if matches!(outcome, CreateBucketOutcome::New) {
579 rollback.storage = Some(storage.clone());
580 }
581
582 if let Some(s3_queue_arn) = s3_queue_arn {
584 storage
585 .add_bucket_notifications(&s3_queue_arn)
586 .await
587 .inspect_err(|error| {
588 tracing::error!(?error, "failed to add bucket notification configuration")
589 })
590 .map_err(CreateTenantError::SetupS3Notifications)?;
591 }
592
593 if !origins.is_empty() {
595 storage
596 .set_bucket_cors_origins(origins)
597 .await
598 .inspect_err(|error| tracing::error!(?error, "failed to add bucket cors rules"))
599 .map_err(CreateTenantError::SetupStorageOrigins)?;
600 }
601
602 Ok(storage)
603}
604
605#[tracing::instrument(skip(search, rollback))]
607async fn create_tenant_search(
608 tenant: &Tenant,
609 search: &SearchIndexFactory,
610 rollback: &mut CreateTenantRollbackData,
611) -> Result<TenantSearchIndex, CreateTenantError> {
612 let search = search.create_search_index(tenant);
614 search
615 .create_index()
616 .await
617 .map_err(CreateTenantError::CreateSearchIndex)
618 .inspect_err(|error| tracing::error!(?error, "failed to create search index"))?;
619
620 rollback.search_index = Some(search.clone());
622
623 Ok(search)
624}