docbox_management/tenant/
create_tenant.rs

1use docbox_database::{
2    DbErr, DbPool, DbResult, ROOT_DATABASE_NAME,
3    create::{
4        check_database_exists, check_database_role_exists, create_database, create_restricted_role,
5        delete_database, delete_role,
6    },
7    migrations::apply_tenant_migrations,
8    models::tenant::{Tenant, TenantId},
9    utils::DatabaseErrorExt,
10};
11use docbox_search::{SearchError, SearchIndexFactory, TenantSearchIndex};
12use docbox_secrets::{SecretManager, SecretManagerError};
13use docbox_storage::{
14    CreateBucketOutcome, StorageLayerError, StorageLayerFactory, TenantStorageLayer,
15};
16use serde::{Deserialize, Serialize};
17use serde_json::json;
18use std::ops::DerefMut;
19use thiserror::Error;
20
21use crate::{
22    database::{DatabaseProvider, close_pool_on_drop},
23    password::random_password,
24};
25
26/// Errors that can occur when creating a tenant
27#[derive(Debug, Error)]
28pub enum CreateTenantError {
29    /// Failed to connect to the temporary database
30    #[error("error connecting to 'postgres' database: {0}")]
31    ConnectPostgres(DbErr),
32
33    /// Failed to create the tenant root database
34    #[error("error creating tenant database: {0}")]
35    CreateTenantDatabase(DbErr),
36
37    /// Failed to connect to the created tenant database
38    #[error("error connecting to tenant database: {0}")]
39    ConnectTenantDatabase(DbErr),
40
41    /// Failed to connect to the root database
42    #[error("error connecting to root database: {0}")]
43    ConnectRootDatabase(DbErr),
44
45    /// Failed to create the tenant rol
46    #[error("error creating tenant database role: {0}")]
47    CreateTenantRole(DbErr),
48
49    /// Database error
50    #[error(transparent)]
51    Database(#[from] DbErr),
52
53    /// Failed to serialize the secret
54    #[error("error serializing tenant secret: {0}")]
55    SerializeSecret(serde_json::Error),
56
57    /// The chosen database secret name is already in use
58    #[error("failed to create tenant secret: secret name already exists")]
59    SecretAlreadyExists,
60
61    /// Failed to create the secret
62    #[error("failed to create tenant secret: {0}")]
63    CreateTenantSecret(SecretManagerError),
64
65    /// Tenant already exists
66    #[error("tenant already exists")]
67    TenantAlreadyExist,
68
69    /// Failed to create the storage bucket
70    #[error("failed to create tenant storage bucket: {0}")]
71    CreateStorageBucket(StorageLayerError),
72
73    /// Failed to setup the S3 bucket CORS rules
74    #[error("failed to setup s3 notification rules: {0}")]
75    SetupS3Notifications(StorageLayerError),
76
77    /// Failed to setup the storage bucket CORS rules
78    #[error("failed to setup storage origin rules rules: {0}")]
79    SetupStorageOrigins(StorageLayerError),
80
81    /// Failed to create the search index
82    #[error("failed to create tenant search index: {0}")]
83    CreateSearchIndex(SearchError),
84
85    /// Failed to migrate the search index
86    #[error("failed to migrate tenant search index: {0}")]
87    MigrateSearchIndex(SearchError),
88}
89
90/// Request to create a tenant
91#[derive(Debug, Deserialize, Serialize, Clone)]
92pub struct CreateTenantConfig {
93    /// Unique ID for the tenant
94    pub id: TenantId,
95    /// Name of the tenant
96    pub name: String,
97    /// Environment of the tenant
98    pub env: String,
99
100    /// Database name for the tenant
101    pub db_name: String,
102    /// Database secret credentials name for the tenant
103    /// (Where the username and password will be stored/)
104    pub db_secret_name: String,
105    /// Name for the tenant role
106    pub db_role_name: String,
107
108    /// Name of the tenant storage bucket
109    pub storage_bucket_name: String,
110    /// CORS Origins for setting up presigned uploads with S3
111    pub storage_cors_origins: Vec<String>,
112    /// ARN for the S3 queue to publish S3 notifications, required
113    /// for presigned uploads
114    pub storage_s3_queue_arn: Option<String>,
115
116    /// Name of the tenant search index
117    pub search_index_name: String,
118
119    /// URL for the SQS event queue
120    pub event_queue_url: Option<String>,
121}
122
123/// Data required to rollback the failed creation of a tenant
124#[derive(Default)]
125struct CreateTenantRollbackData {
126    search_index: Option<TenantSearchIndex>,
127    storage: Option<TenantStorageLayer>,
128    secret: Option<(SecretManager, String)>,
129    database: Option<String>,
130    db_role: Option<String>,
131}
132
133impl CreateTenantRollbackData {
134    async fn rollback(&mut self, db_provider: &impl DatabaseProvider) {
135        // Rollback search index
136        if let Some(search_index) = self.search_index.take()
137            && let Err(error) = search_index.delete_index().await
138        {
139            tracing::error!(?error, "failed to rollback created tenant search index");
140        }
141
142        // Rollback storage
143        if let Some(storage) = self.storage.take()
144            && let Err(error) = storage.delete_bucket().await
145        {
146            tracing::error!(?error, "failed to rollback created tenant storage bucket");
147        }
148
149        // Rollback secrets
150        if let Some((secrets, secret_name)) = self.secret.take()
151            && let Err(error) = secrets.delete_secret(&secret_name, true).await
152        {
153            tracing::error!(?error, "failed to rollback tenant secret");
154        }
155
156        // Handle operations requiring a root database
157        let db_name = self.database.take();
158        let db_role_name = self.db_role.take();
159        if db_name.is_some() || db_role_name.is_some() {
160            match db_provider.connect("postgres").await {
161                Ok(db_postgres) => {
162                    // Rollback the database
163                    if let Some(db_name) = db_name
164                        && let Err(error) = delete_database(&db_postgres, &db_name).await
165                    {
166                        tracing::error!(?error, "failed to rollback tenant database");
167                    }
168
169                    // Rollback the database role
170                    if let Some(db_role_name) = db_role_name
171                        && let Err(error) = delete_role(&db_postgres, &db_role_name).await
172                    {
173                        tracing::error!(?error, "failed to rollback tenant db role name");
174                    }
175
176                    db_postgres.close().await;
177                }
178                Err(error) => {
179                    tracing::error!(
180                        ?error,
181                        "failed to rollback tenant database, unable to acquire postgres database"
182                    );
183                }
184            }
185        }
186    }
187}
188
189/// Handles the process of creating a new docbox tenant
190///
191/// Performs:
192/// - Create tenant database
193/// - Create tenant database role
194/// - Store a secret with the tenant database role credentials
195/// - Add the tenant to the docbox database
196/// - Run migrations on the tenant database
197/// - Create and setup the tenant storage bucket
198/// - Create and setup the tenant search index
199/// - Run search index migrations
200///
201/// On failure any created resources will be rolled back
202#[tracing::instrument(skip_all, fields(?config))]
203pub async fn create_tenant(
204    db_provider: &impl DatabaseProvider,
205    search_factory: &SearchIndexFactory,
206    storage_factory: &StorageLayerFactory,
207    secrets: &SecretManager,
208    config: CreateTenantConfig,
209) -> Result<Tenant, CreateTenantError> {
210    let mut rollback = CreateTenantRollbackData::default();
211
212    match create_tenant_inner(
213        db_provider,
214        search_factory,
215        storage_factory,
216        secrets,
217        config,
218        &mut rollback,
219    )
220    .await
221    {
222        Ok(value) => Ok(value),
223        Err(error) => {
224            // Rollback the failure
225            rollback.rollback(db_provider).await;
226            Err(error)
227        }
228    }
229}
230
231#[tracing::instrument(skip_all, fields(?config))]
232async fn create_tenant_inner(
233    db_provider: &impl DatabaseProvider,
234    search_factory: &SearchIndexFactory,
235    storage_factory: &StorageLayerFactory,
236    secrets: &SecretManager,
237    config: CreateTenantConfig,
238    rollback: &mut CreateTenantRollbackData,
239) -> Result<Tenant, CreateTenantError> {
240    let (tenant_db, _tenant_db_guard) = {
241        // Connect to the "postgres" database to use while creating the tenant database
242        let db_postgres = db_provider
243            .connect("postgres")
244            .await
245            .map_err(CreateTenantError::ConnectPostgres)?;
246        let _postgres_guard = close_pool_on_drop(&db_postgres);
247
248        // Create tenant database
249        initialize_tenant_database(&db_postgres, &config.db_name, rollback).await?;
250        tracing::info!("created tenant database");
251
252        // Connect to the tenant database
253        let tenant_db = db_provider
254            .connect(&config.db_name)
255            .await
256            .map_err(CreateTenantError::ConnectTenantDatabase)?;
257
258        let tenant_db_guard = close_pool_on_drop(&tenant_db);
259        (tenant_db, tenant_db_guard)
260    };
261
262    // Generate password for the database role
263    let db_role_password = random_password(30);
264
265    initialize_tenant_db_role(
266        &tenant_db,
267        &config.db_name,
268        &config.db_role_name,
269        &db_role_password,
270        rollback,
271    )
272    .await?;
273    tracing::info!("created tenant user");
274
275    initialize_tenant_db_secret(
276        secrets,
277        &config.db_secret_name,
278        &config.db_role_name,
279        &db_role_password,
280        rollback,
281    )
282    .await?;
283    tracing::info!("created tenant database secret");
284
285    // Connect to the root database
286    let root_db = db_provider
287        .connect(ROOT_DATABASE_NAME)
288        .await
289        .map_err(CreateTenantError::ConnectRootDatabase)?;
290
291    let _guard = close_pool_on_drop(&root_db);
292
293    // Enter a database transaction
294    let mut root_transaction = root_db
295        .begin()
296        .await
297        .inspect_err(|error| tracing::error!(?error, "failed to begin root transaction"))?;
298
299    // Create the tenant
300    let tenant: Tenant = Tenant::create(
301        root_transaction.deref_mut(),
302        docbox_database::models::tenant::CreateTenant {
303            id: config.id,
304            name: config.name,
305            db_name: config.db_name,
306            db_secret_name: config.db_secret_name,
307            s3_name: config.storage_bucket_name,
308            os_index_name: config.search_index_name,
309            event_queue_url: config.event_queue_url,
310            env: config.env,
311        },
312    )
313    .await
314    .map_err(|err| {
315        // Handle attempts at a duplicate tenant creation
316        if err.is_duplicate_record() {
317            CreateTenantError::TenantAlreadyExist
318        } else {
319            CreateTenantError::Database(err)
320        }
321    })
322    .inspect_err(|error| tracing::error!(?error, "failed to create tenant"))?;
323
324    // Enter a database transaction
325    let mut tenant_transaction = tenant_db
326        .begin()
327        .await
328        .inspect_err(|error| tracing::error!(?error, "failed to begin tenant transaction"))?;
329
330    // Setup the tenant database
331    apply_tenant_migrations(
332        &mut root_transaction,
333        &mut tenant_transaction,
334        &tenant,
335        None,
336    )
337    .await
338    .inspect_err(|error| tracing::error!(?error, "failed to create tenant tables"))?;
339
340    // Setup the tenant storage bucket
341    tracing::debug!("creating tenant storage");
342    create_tenant_storage(
343        &tenant,
344        storage_factory,
345        config.storage_s3_queue_arn,
346        config.storage_cors_origins,
347        rollback,
348    )
349    .await?;
350
351    // Setup the tenant search index
352    tracing::debug!("creating tenant search index");
353    let search = create_tenant_search(&tenant, search_factory, rollback).await?;
354
355    // Apply migrations to search
356    search
357        .apply_migrations(
358            &tenant,
359            &mut root_transaction,
360            &mut tenant_transaction,
361            None,
362        )
363        .await
364        .map_err(CreateTenantError::MigrateSearchIndex)?;
365
366    // Commit database changes
367    tenant_transaction
368        .commit()
369        .await
370        .inspect_err(|error| tracing::error!(?error, "failed to commit tenant transaction"))?;
371    root_transaction
372        .commit()
373        .await
374        .inspect_err(|error| tracing::error!(?error, "failed to commit root transaction"))?;
375
376    Ok(tenant)
377}
378
379/// Helper to check if a tenant database already exists
380/// (Used to warn against duplicate creation when performing validation)
381#[tracing::instrument(skip(db_provider))]
382pub async fn is_tenant_database_existing(
383    db_provider: &impl DatabaseProvider,
384    db_name: &str,
385) -> DbResult<bool> {
386    // Connect to the "postgres" database to use while creating the tenant database
387    let db_postgres = db_provider.connect("postgres").await?;
388    let _guard = close_pool_on_drop(&db_postgres);
389
390    check_database_exists(&db_postgres, db_name).await
391}
392
393/// Initializes the creation of a tenant database, if the database
394/// already exists that silently passes. Returns a [DbPool] to the
395/// tenant database
396#[tracing::instrument(skip(db_postgres, rollback))]
397async fn initialize_tenant_database(
398    db_postgres: &DbPool,
399    db_name: &str,
400    rollback: &mut CreateTenantRollbackData,
401) -> Result<(), CreateTenantError> {
402    let already_exists = match create_database(db_postgres, db_name).await {
403        // We created the database
404        Ok(_) => false,
405        // Database already exists
406        Err(error) if error.is_database_exists() => true,
407        // Other database error
408        Err(error) => return Err(CreateTenantError::CreateTenantDatabase(error)),
409    };
410
411    if !already_exists {
412        rollback.database = Some(db_name.to_string());
413    }
414
415    Ok(())
416}
417
418/// Helper to check if a tenant database role already exists
419/// (Used to warn against duplicate creation when performing validation)
420#[tracing::instrument(skip(db_provider))]
421pub async fn is_tenant_database_role_existing(
422    db_provider: &impl DatabaseProvider,
423    role_name: &str,
424) -> DbResult<bool> {
425    // Connect to the "postgres" database to use while creating the tenant database
426    let db_postgres = db_provider.connect("postgres").await?;
427
428    let _guard = close_pool_on_drop(&db_postgres);
429
430    check_database_role_exists(&db_postgres, role_name).await
431}
432
433/// Initializes a tenant db role that the docbox API will use when accessing
434/// the tenant databases
435#[tracing::instrument(skip(db, role_password, rollback))]
436async fn initialize_tenant_db_role(
437    db: &DbPool,
438    db_name: &str,
439    role_name: &str,
440    role_password: &str,
441    rollback: &mut CreateTenantRollbackData,
442) -> Result<(), CreateTenantError> {
443    // Setup the restricted root db role
444    create_restricted_role(db, db_name, role_name, role_password)
445        .await
446        .map_err(CreateTenantError::CreateTenantRole)?;
447
448    rollback.db_role = Some(role_name.to_string());
449
450    Ok(())
451}
452
453/// Helper to check if a tenant database role secret already exists
454/// (Used to warn against duplicate creation when performing validation)
455#[tracing::instrument(skip(secrets))]
456pub async fn is_tenant_database_role_secret_existing(
457    secrets: &SecretManager,
458    secret_name: &str,
459) -> Result<bool, SecretManagerError> {
460    secrets
461        .get_secret(secret_name)
462        .await
463        .map(|value| value.is_some())
464}
465
466/// Initializes and stores the secret for the tenant database access
467#[tracing::instrument(skip(secrets, role_password, rollback))]
468async fn initialize_tenant_db_secret(
469    secrets: &SecretManager,
470    secret_name: &str,
471    role_name: &str,
472    role_password: &str,
473    rollback: &mut CreateTenantRollbackData,
474) -> Result<(), CreateTenantError> {
475    // Ensure the secret does not already exist, we don't want to override it
476    if secrets
477        .has_secret(secret_name)
478        .await
479        .map_err(CreateTenantError::CreateTenantSecret)?
480    {
481        return Err(CreateTenantError::SecretAlreadyExists);
482    }
483
484    let secret_value = serde_json::to_string(&json!({
485        "username": role_name,
486        "password": role_password
487    }))
488    .map_err(CreateTenantError::SerializeSecret)?;
489
490    secrets
491        .set_secret(secret_name, &secret_value)
492        .await
493        .map_err(CreateTenantError::CreateTenantSecret)?;
494
495    rollback.secret = Some((secrets.clone(), secret_name.to_string()));
496
497    Ok(())
498}
499
500/// Create and setup the tenant storage
501#[tracing::instrument(skip(storage, rollback))]
502async fn create_tenant_storage(
503    tenant: &Tenant,
504    storage: &StorageLayerFactory,
505    s3_queue_arn: Option<String>,
506    origins: Vec<String>,
507    rollback: &mut CreateTenantRollbackData,
508) -> Result<(), CreateTenantError> {
509    let storage = storage.create_storage_layer(tenant);
510    let outcome = storage
511        .create_bucket()
512        .await
513        .inspect_err(|error| tracing::error!(?error, "failed to create tenant bucket"))
514        .map_err(CreateTenantError::CreateStorageBucket)?;
515
516    // Mark the storage for rollback if we created it
517    if matches!(outcome, CreateBucketOutcome::New) {
518        rollback.storage = Some(storage.clone());
519    }
520
521    // Connect the S3 bucket for file upload notifications
522    if let Some(s3_queue_arn) = s3_queue_arn {
523        storage
524            .add_bucket_notifications(&s3_queue_arn)
525            .await
526            .inspect_err(|error| {
527                tracing::error!(?error, "failed to add bucket notification configuration")
528            })
529            .map_err(CreateTenantError::SetupS3Notifications)?;
530    }
531
532    // Setup bucket allowed origins for presigned uploads
533    if !origins.is_empty() {
534        storage
535            .set_bucket_cors_origins(origins)
536            .await
537            .inspect_err(|error| tracing::error!(?error, "failed to add bucket cors rules"))
538            .map_err(CreateTenantError::SetupStorageOrigins)?;
539    }
540
541    Ok(())
542}
543
544/// Create and setup the tenant search index
545#[tracing::instrument(skip(search, rollback))]
546async fn create_tenant_search(
547    tenant: &Tenant,
548    search: &SearchIndexFactory,
549    rollback: &mut CreateTenantRollbackData,
550) -> Result<TenantSearchIndex, CreateTenantError> {
551    // Setup the tenant search index
552    let search = search.create_search_index(tenant);
553    search
554        .create_index()
555        .await
556        .map_err(CreateTenantError::CreateSearchIndex)
557        .inspect_err(|error| tracing::error!(?error, "failed to create search index"))?;
558
559    // Index has been created, provide it as rollback state
560    rollback.search_index = Some(search.clone());
561
562    Ok(search)
563}