docbox_management/root/
initialize.rs

1use crate::{
2    database::{DatabaseProvider, close_pool_on_drop},
3    password::random_password,
4    root::migrate_root::{MigrateRootError, migrate_root},
5};
6use docbox_database::{
7    DbErr, DbPool, DbResult, ROOT_DATABASE_NAME,
8    create::{create_database, create_restricted_role},
9    models::tenant::Tenant,
10    sqlx::types::Uuid,
11    utils::DatabaseErrorExt,
12};
13use docbox_secrets::{SecretManager, SecretManagerError};
14use serde_json::json;
15use thiserror::Error;
16
17/// Temporary database to connect to while setting up the other databases
18const TEMP_SETUP_DATABASE: &str = "postgres";
19
20#[derive(Debug, Error)]
21pub enum InitializeError {
22    #[error("error connecting to 'postgres' database: {0}")]
23    ConnectPostgres(DbErr),
24
25    #[error("error creating root database: {0}")]
26    CreateRootDatabase(DbErr),
27
28    #[error("error connecting to root database: {0}")]
29    ConnectRootDatabase(DbErr),
30
31    #[error("error migrating root database: {0}")]
32    MigrateRoot(MigrateRootError),
33
34    #[error("error creating root database role: {0}")]
35    CreateRootRole(DbErr),
36
37    #[error("error serializing root secret: {0}")]
38    SerializeSecret(serde_json::Error),
39
40    #[error("failed to create root secret: {0}")]
41    CreateRootSecret(SecretManagerError),
42
43    #[error("error creating tenants table: {0}")]
44    CreateTenantsTable(DbErr),
45}
46
47/// Check if the root database is initialized
48#[tracing::instrument(skip(db_provider))]
49pub async fn is_initialized(db_provider: &impl DatabaseProvider) -> DbResult<bool> {
50    // First check if the root database exists
51    let db = match db_provider.connect(ROOT_DATABASE_NAME).await {
52        Ok(value) => value,
53        Err(error) => {
54            if error.is_database_does_not_exist() {
55                // Database is not setup, server is not initialized
56                return Ok(false);
57            }
58
59            return Err(error);
60        }
61    };
62
63    tracing::debug!("root is initialized");
64
65    let _guard = close_pool_on_drop(&db);
66
67    // Then query the table for a non-existent tenant to make sure its setup correctly
68    if let Err(error) = Tenant::find_by_id(&db, Uuid::nil(), "__DO_NOT_USE").await {
69        if error.is_table_does_not_exist() {
70            // Database is not setup, server is not initialized
71            return Ok(false);
72        }
73
74        return Err(error);
75    }
76
77    tracing::debug!("tenant table is setup");
78
79    Ok(true)
80}
81
82/// Initializes the root database of provida
83#[tracing::instrument(skip(db_provider, secrets))]
84pub async fn initialize(
85    db_provider: &impl DatabaseProvider,
86    secrets: &SecretManager,
87    root_secret_name: &str,
88) -> Result<(), InitializeError> {
89    let db_docbox = initialize_root_database(db_provider).await?;
90    let _guard = close_pool_on_drop(&db_docbox);
91
92    let root_role_name = "docbox_config_api";
93    let root_password = random_password(30);
94
95    // Setup the restricted root db role
96    initialize_root_role(&db_docbox, root_role_name, &root_password).await?;
97    tracing::info!("created root user");
98
99    // Setup the secret to store the role credentials
100    initialize_root_secret(secrets, root_secret_name, root_role_name, &root_password).await?;
101    tracing::info!("created database secret");
102
103    // Migrate the root database
104    migrate_root(db_provider, None)
105        .await
106        .map_err(InitializeError::MigrateRoot)?;
107
108    Ok(())
109}
110
111/// Initializes the root database used by docbox
112#[tracing::instrument(skip(db_provider))]
113pub async fn initialize_root_database(
114    db_provider: &impl DatabaseProvider,
115) -> Result<DbPool, InitializeError> {
116    // Connect to the root postgres database
117    let db_root = db_provider
118        .connect(TEMP_SETUP_DATABASE)
119        .await
120        .map_err(InitializeError::ConnectPostgres)?;
121
122    let _guard = close_pool_on_drop(&db_root);
123
124    // Create the tenant database
125    if let Err(err) = create_database(&db_root, ROOT_DATABASE_NAME).await
126        && !err
127            .as_database_error()
128            .is_some_and(|err| err.code().is_some_and(|code| code.to_string().eq("42P04")))
129    {
130        return Err(InitializeError::CreateRootDatabase(err));
131    }
132
133    // Connect to the docbox database
134    let db_docbox = db_provider
135        .connect(ROOT_DATABASE_NAME)
136        .await
137        .map_err(InitializeError::ConnectRootDatabase)?;
138
139    Ok(db_docbox)
140}
141
142/// Initializes a root role that the docbox API will use when accessing
143/// the tenants table
144#[tracing::instrument(skip(db, root_role_password))]
145pub async fn initialize_root_role(
146    db: &DbPool,
147    root_role_name: &str,
148    root_role_password: &str,
149) -> Result<(), InitializeError> {
150    // Setup the restricted root db role
151    create_restricted_role(db, ROOT_DATABASE_NAME, root_role_name, root_role_password)
152        .await
153        .map_err(InitializeError::CreateRootRole)?;
154
155    Ok(())
156}
157
158/// Initializes and stores the secret for the root database access
159#[tracing::instrument(skip(secrets, root_role_password))]
160pub async fn initialize_root_secret(
161    secrets: &SecretManager,
162    root_secret_name: &str,
163    root_role_name: &str,
164    root_role_password: &str,
165) -> Result<(), InitializeError> {
166    let secret_value = serde_json::to_string(&json!({
167        "username": root_role_name,
168        "password": root_role_password
169    }))
170    .map_err(InitializeError::SerializeSecret)?;
171
172    secrets
173        .set_secret(root_secret_name, &secret_value)
174        .await
175        .map_err(InitializeError::CreateRootSecret)?;
176
177    Ok(())
178}