Skip to main content

docbox_management/tenant/
create_tenant.rs

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/// Errors that can occur when creating a tenant
28#[derive(Debug, Error)]
29pub enum CreateTenantError {
30    /// Failed to connect to the temporary database
31    #[error("error connecting to 'postgres' database: {0}")]
32    ConnectPostgres(DbErr),
33
34    /// Failed to create the tenant root database
35    #[error("error creating tenant database: {0}")]
36    CreateTenantDatabase(DbErr),
37
38    /// Failed to connect to the created tenant database
39    #[error("error connecting to tenant database: {0}")]
40    ConnectTenantDatabase(DbErr),
41
42    /// Failed to connect to the root database
43    #[error("error connecting to root database: {0}")]
44    ConnectRootDatabase(DbErr),
45
46    /// Failed to create the tenant rol
47    #[error("error creating tenant database role: {0}")]
48    CreateTenantRole(DbErr),
49
50    /// Database error
51    #[error(transparent)]
52    Database(#[from] DbErr),
53
54    /// Failed to serialize the secret
55    #[error("error serializing tenant secret: {0}")]
56    SerializeSecret(serde_json::Error),
57
58    /// The chosen database secret name is already in use
59    #[error("failed to create tenant secret: secret name already exists")]
60    SecretAlreadyExists,
61
62    /// Failed to create the secret
63    #[error("failed to create tenant secret: {0}")]
64    CreateTenantSecret(SecretManagerError),
65
66    /// Tenant already exists
67    #[error("tenant already exists")]
68    TenantAlreadyExist,
69
70    /// Failed to create the storage bucket
71    #[error("failed to create tenant storage bucket: {0}")]
72    CreateStorageBucket(StorageLayerError),
73
74    /// Failed to setup the S3 bucket CORS rules
75    #[error("failed to setup s3 notification rules: {0}")]
76    SetupS3Notifications(StorageLayerError),
77
78    /// Failed to setup the storage bucket CORS rules
79    #[error("failed to setup storage origin rules rules: {0}")]
80    SetupStorageOrigins(StorageLayerError),
81
82    /// Failed to create the search index
83    #[error("failed to create tenant search index: {0}")]
84    CreateSearchIndex(SearchError),
85
86    /// Failed to migrate the search index
87    #[error("failed to migrate tenant search index: {0}")]
88    MigrateSearchIndex(SearchError),
89
90    /// Failed to migrate the storage
91    #[error("failed to migrate tenant storage: {0}")]
92    MigrateStorage(MigrateTenantStorageError),
93
94    /// Missing db_secret_name when not using IAM authentication
95    #[error("when not using db_iam_user the db_secret_name must be specified")]
96    MissingDatabaseSecretName,
97}
98
99/// Request to create a tenant
100#[derive(Debug, Deserialize, Serialize, Clone)]
101pub struct CreateTenantConfig {
102    /// Unique ID for the tenant
103    pub id: TenantId,
104    /// Name of the tenant
105    pub name: String,
106    /// Environment of the tenant
107    pub env: String,
108
109    /// Database name for the tenant
110    pub db_name: String,
111    /// Name for the tenant role
112    pub db_role_name: String,
113
114    /// Database secret credentials name for the tenant
115    /// (Where the username and password will be stored)
116    pub db_secret_name: Option<String>,
117
118    /// Whether to use IAM for role authorization instead
119    /// of a database secret
120    #[serde(default)]
121    pub db_iam_user: bool,
122
123    /// Name of the tenant storage bucket
124    pub storage_bucket_name: String,
125    /// CORS Origins for setting up presigned uploads with S3
126    pub storage_cors_origins: Vec<String>,
127    /// ARN for the S3 queue to publish S3 notifications, required
128    /// for presigned uploads
129    pub storage_s3_queue_arn: Option<String>,
130
131    /// Name of the tenant search index
132    pub search_index_name: String,
133
134    /// URL for the SQS event queue
135    pub event_queue_url: Option<String>,
136}
137
138/// Data required to rollback the failed creation of a tenant
139#[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        // Rollback search index
151        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        // Rollback storage
158        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        // Rollback secrets
165        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        // Handle operations requiring a root database
172        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                    // Rollback the database
178                    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                    // Rollback the database role
185                    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/// Handles the process of creating a new docbox tenant
205///
206/// Performs:
207/// - Create tenant database
208/// - Create tenant database role
209/// - Store a secret with the tenant database role credentials
210/// - Add the tenant to the docbox database
211/// - Run migrations on the tenant database
212/// - Create and setup the tenant storage bucket
213/// - Create and setup the tenant search index
214/// - Run search index migrations
215///
216/// On failure any created resources will be rolled back
217#[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 the failure
240            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        // Connect to the "postgres" database to use while creating the tenant database
257        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        // Create tenant database
264        initialize_tenant_database(&db_postgres, &config.db_name, rollback).await?;
265        tracing::info!("created tenant database");
266
267        // Connect to the tenant database
268        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        // Generate password for the database role
294        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    // Connect to the root database
318    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    // Enter a database transaction
326    let mut root_transaction = root_db
327        .begin()
328        .await
329        .inspect_err(|error| tracing::error!(?error, "failed to begin root transaction"))?;
330
331    // Create the tenant
332    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        // Handle attempts at a duplicate tenant creation
353        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    // Enter a database transaction
362    let mut tenant_transaction = tenant_db
363        .begin()
364        .await
365        .inspect_err(|error| tracing::error!(?error, "failed to begin tenant transaction"))?;
366
367    // Setup the tenant database
368    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    // Setup the tenant storage bucket
378    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    // Apply storage migrations
389    migrate_tenant_storage_inner(&storage, &mut root_transaction, &tenant, None)
390        .await
391        .map_err(CreateTenantError::MigrateStorage)?;
392
393    // Setup the tenant search index
394    tracing::debug!("creating tenant search index");
395    let search = create_tenant_search(&tenant, search_factory, rollback).await?;
396
397    // Apply migrations to search
398    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    // Commit database changes
409    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/// Helper to check if a tenant database already exists
422/// (Used to warn against duplicate creation when performing validation)
423#[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    // Connect to the "postgres" database to use while creating the tenant database
429    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/// Initializes the creation of a tenant database, if the database
436/// already exists that silently passes. Returns a [DbPool] to the
437/// tenant database
438#[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        // We created the database
446        Ok(_) => false,
447        // Database already exists
448        Err(error) if error.is_database_exists() => true,
449        // Other database error
450        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/// Helper to check if a tenant database role already exists
461/// (Used to warn against duplicate creation when performing validation)
462#[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    // Connect to the "postgres" database to use while creating the tenant database
468    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/// Initializes a tenant db role that the docbox API will use when accessing
476/// the tenant databases using secrets for authentication
477#[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    // Setup the restricted root db role
486    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/// Initializes a tenant db role that the docbox API will use when accessing
496/// the tenant databases using IAM for authentication
497#[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    // Setup the restricted root db role
505    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/// Helper to check if a tenant database role secret already exists
515/// (Used to warn against duplicate creation when performing validation)
516#[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/// Initializes and stores the secret for the tenant database access
528#[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    // Ensure the secret does not already exist, we don't want to override it
537    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/// Create and setup the tenant storage
562#[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    // Mark the storage for rollback if we created it
578    if matches!(outcome, CreateBucketOutcome::New) {
579        rollback.storage = Some(storage.clone());
580    }
581
582    // Connect the S3 bucket for file upload notifications
583    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    // Setup bucket allowed origins for presigned uploads
594    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/// Create and setup the tenant search index
606#[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    // Setup the tenant search index
613    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    // Index has been created, provide it as rollback state
621    rollback.search_index = Some(search.clone());
622
623    Ok(search)
624}