docbox_management/tenant/
create_tenant.rs

1use docbox_core::tenant::create_tenant::InitTenantError;
2use docbox_database::{
3    DbErr, DbPool, ROOT_DATABASE_NAME,
4    create::{create_database, create_restricted_role},
5    models::tenant::{Tenant, TenantId},
6};
7use docbox_search::SearchIndexFactory;
8use docbox_secrets::AppSecretManager;
9use docbox_storage::StorageLayerFactory;
10use serde::{Deserialize, Serialize};
11use serde_json::json;
12use thiserror::Error;
13
14use crate::{database::DatabaseProvider, password::random_password};
15
16#[derive(Debug, Error)]
17pub enum CreateTenantError {
18    #[error("error connecting to 'postgres' database: {0}")]
19    ConnectPostgres(DbErr),
20
21    #[error("error creating tenant database: {0}")]
22    CreateTenantDatabase(DbErr),
23
24    #[error("error connecting to tenant database: {0}")]
25    ConnectTenantDatabase(DbErr),
26
27    #[error("error connecting to root database: {0}")]
28    ConnectRootDatabase(DbErr),
29
30    #[error("error creating tenant database role: {0}")]
31    CreateTenantRole(DbErr),
32
33    #[error("error serializing tenant secret: {0}")]
34    SerializeSecret(serde_json::Error),
35
36    #[error("failed to create tenant secret: {0}")]
37    CreateTenantSecret(anyhow::Error),
38
39    #[error("failed to init tenant: {0}")]
40    CreateTenant(InitTenantError),
41}
42
43/// Request to create a tenant
44#[derive(Debug, Deserialize, Serialize, Clone)]
45pub struct CreateTenantConfig {
46    /// Unique ID for the tenant
47    pub id: TenantId,
48    /// Name of the tenant
49    pub name: String,
50    /// Environment of the tenant
51    pub env: String,
52
53    /// Database name for the tenant
54    pub db_name: String,
55    /// Database secret credentials name for the tenant
56    /// (Where the username and password will be stored/)
57    pub db_secret_name: String,
58    /// Name for the tenant role
59    pub db_role_name: String,
60
61    /// Name of the tenant s3 bucket
62    pub storage_bucket_name: String,
63    /// CORS Origins for setting up presigned uploads with S3
64    pub storage_cors_origins: Vec<String>,
65    /// ARN for the S3 queue to publish S3 notifications, required
66    /// for presigned uploads
67    pub storage_s3_queue_arn: Option<String>,
68
69    /// Name of the tenant search index
70    pub search_index_name: String,
71
72    /// URL for the SQS event queue
73    pub event_queue_url: Option<String>,
74}
75
76pub async fn create_tenant(
77    db_provider: &impl DatabaseProvider,
78    search_factory: &SearchIndexFactory,
79    storage_factory: &StorageLayerFactory,
80    secrets: &AppSecretManager,
81    config: CreateTenantConfig,
82) -> Result<Tenant, CreateTenantError> {
83    // Create tenant database
84    let tenant_db = initialize_tenant_database(db_provider, &config.db_name).await?;
85    tracing::info!("created tenant database");
86
87    // Generate password for the database role
88    let db_role_password = random_password(30);
89
90    initialize_tenant_db_role(
91        &tenant_db,
92        &config.db_name,
93        &config.db_role_name,
94        &db_role_password,
95    )
96    .await?;
97    tracing::info!("created tenant user");
98
99    initialize_tenant_db_secret(
100        secrets,
101        &config.db_secret_name,
102        &config.db_role_name,
103        &db_role_password,
104    )
105    .await?;
106    tracing::info!("created tenant database secret");
107
108    // Connect to the root database
109    let root_db = db_provider
110        .connect(ROOT_DATABASE_NAME)
111        .await
112        .map_err(CreateTenantError::ConnectRootDatabase)?;
113
114    // Initialize the tenant
115    let tenant = docbox_core::tenant::create_tenant::create_tenant(
116        &root_db,
117        &tenant_db,
118        search_factory,
119        storage_factory,
120        docbox_core::tenant::create_tenant::CreateTenant {
121            id: config.id,
122            name: config.name,
123            db_name: config.db_name,
124            db_secret_name: config.db_secret_name,
125            s3_name: config.storage_bucket_name,
126            os_index_name: config.search_index_name,
127            event_queue_url: config.event_queue_url,
128            origins: config.storage_cors_origins,
129            s3_queue_arn: config.storage_s3_queue_arn,
130            env: config.env,
131        },
132    )
133    .await
134    .map_err(CreateTenantError::CreateTenant)?;
135
136    Ok(tenant)
137}
138
139pub async fn initialize_tenant_database(
140    db_provider: &impl DatabaseProvider,
141    db_name: &str,
142) -> Result<DbPool, CreateTenantError> {
143    // Connect to the "postgres" database to use while creating the tenant database
144    let db_postgres = db_provider
145        .connect("postgres")
146        .await
147        .map_err(CreateTenantError::ConnectPostgres)?;
148
149    // Create the tenant database
150    if let Err(error) = create_database(&db_postgres, db_name).await {
151        if !error
152            .as_database_error()
153            .is_some_and(|err| err.code().is_some_and(|code| code.to_string().eq("42P04")))
154        {
155            return Err(CreateTenantError::CreateTenantDatabase(error));
156        }
157    }
158
159    // Connect to the tenant database
160    let tenant_db = db_provider
161        .connect(db_name)
162        .await
163        .map_err(CreateTenantError::ConnectTenantDatabase)?;
164
165    Ok(tenant_db)
166}
167
168/// Initializes a tenant db role that the docbox API will use when accessing
169/// the tenant databases
170pub async fn initialize_tenant_db_role(
171    db: &DbPool,
172    db_name: &str,
173    role_name: &str,
174    role_password: &str,
175) -> Result<(), CreateTenantError> {
176    // Setup the restricted root db role
177    create_restricted_role(db, db_name, role_name, role_password)
178        .await
179        .map_err(CreateTenantError::CreateTenantRole)?;
180
181    Ok(())
182}
183
184/// Initializes and stores the secret for the tenant database access
185pub async fn initialize_tenant_db_secret(
186    secrets: &AppSecretManager,
187    secret_name: &str,
188    role_name: &str,
189    role_password: &str,
190) -> Result<(), CreateTenantError> {
191    let secret_value = serde_json::to_string(&json!({
192        "username": role_name,
193        "password": role_password
194    }))
195    .map_err(CreateTenantError::SerializeSecret)?;
196
197    secrets
198        .set_secret(secret_name, &secret_value)
199        .await
200        .map_err(CreateTenantError::CreateTenantSecret)?;
201
202    Ok(())
203}