docbox_management/root/
initialize.rs

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