1
//! One-time global seeding of the admin database.
2
//!
3
//! The seed data (admin user, infra/system config, signing keys) used to live
4
//! in a migration (`9999_default_config.sql`). It was moved out so the shared
5
//! migration set is DDL-only and can be applied to per-user databases without
6
//! replicating secrets. The seed now runs here, against the admin database
7
//! only.
8
//!
9
//! All correctness guarantees live inside `seed.sql` itself: it is a single
10
//! guarded `DO` block that takes a transaction-scoped advisory lock (serializing
11
//! concurrent boots), skips if the `seed_complete` marker is already present
12
//! (idempotent), and runs atomically (a crash rolls back rather than leaving a
13
//! partial seed). So this function only has to ship that script to the admin
14
//! pool — which keeps the future `Send` (no `&mut PgConnection` held across an
15
//! await, unlike an in-Rust transaction) so `boot()` can be spawned.
16

            
17
use crate::db::{DBError, execute_raw, get_connection};
18
use rust_i18n::t;
19
use sqlx::Connection;
20

            
21
const SEED_SQL: &str = include_str!("../seed.sql");
22

            
23
/// The seeded admin user's id (see `seed.sql`).
24
/// The seeded administrator.
25
///
26
/// Exported because the fleet migrator must skip this row: `seed()` points it at
27
/// the global database, so treating it as a tenant would migrate the global
28
/// schema through the tenant path.
29
pub const ADMIN_USER_ID: &str = "22d36bde-d987-4927-b070-842efdcf9c59";
30

            
31
/// Seeds the admin database (idempotent, concurrency-safe, atomic — see the
32
/// module docs and `seed.sql`).
33
///
34
/// After seeding, points the admin user's `database_name` at this deployment's admin
35
/// `DATABASE_URL` (the seed leaves it empty rather than hardcoding a
36
/// machine-specific DSN). The update is **unconditional** (compare-and-update on
37
/// every boot) so that if the deployment's DSN later changes while reusing the
38
/// same admin database, the stored value tracks it rather than going stale. The
39
/// `WHERE database_name <> $1` keeps it a no-op write when already correct.
40
///
41
/// # Errors
42
///
43
/// Returns [`DBError`] on a connection failure or if the seed transaction fails.
44
156
pub async fn seed() -> Result<(), DBError> {
45
156
    execute_raw(SEED_SQL).await?;
46

            
47
    // The admin's data lives in the admin database itself, so its
48
    // `database_name` is this database. `current_database()` avoids parsing a
49
    // name out of DATABASE_URL and cannot disagree with where we are connected.
50
156
    let mut conn = get_connection().await?;
51
    // Every tenant DSN is composed by `dsn_for`, which refuses a name outside
52
    // `[a-z0-9_]`. Seeding one it will later reject would leave the admin
53
    // unable to reach its own database, so fail here where the cause is plain.
54
156
    let name: String = sqlx::query_scalar("SELECT current_database()")
55
156
        .fetch_one(&mut *conn)
56
156
        .await?;
57
156
    crate::provision::check_database_name(&name)?;
58
156
    sqlx::query(
59
156
        "UPDATE users SET database_name = $2 \
60
156
         WHERE id = $1::uuid AND database_name <> $2",
61
156
    )
62
156
    .bind(ADMIN_USER_ID)
63
156
    .bind(&name)
64
156
    .execute(&mut *conn)
65
156
    .await?;
66

            
67
156
    backfill_admin_keypair(&mut conn).await?;
68

            
69
156
    log::debug!("{}", t!("Admin database seed checked"));
70
156
    Ok(())
71
156
}
72

            
73
/// Advisory-lock key serializing admin-keypair backfill across concurrent boots.
74
const ADMIN_KEY_LOCK: i64 = 0x6E6F_6D69_5F61_6B31;
75

            
76
/// Generates and stores a signing keypair for the seeded admin user if it has
77
/// none yet. The admin's data lives in the admin DB itself, so both halves are
78
/// written here: the public key into `users.jwt_public_key`, the private key
79
/// into the admin DB's `user_auth_keys`.
80
///
81
/// Concurrency- and partial-failure-safe:
82
/// - A transaction-scoped advisory lock serializes concurrent boots.
83
/// - Idempotency checks BOTH halves — the public key on the admin row AND a
84
///   matching private key in `user_auth_keys` — so a prior run that wrote only
85
///   the public key (then crashed) is repaired rather than treated as done.
86
/// - Both writes happen in one transaction; the private insert replaces any
87
///   prior rows so the stored private key always matches the public key, and a
88
///   crash rolls back rather than leaving a half-written keypair.
89
156
async fn backfill_admin_keypair(conn: &mut sqlx::PgConnection) -> Result<(), DBError> {
90
156
    let mut tx = conn.begin().await?;
91
156
    sqlx::query("SELECT pg_advisory_xact_lock($1)")
92
156
        .bind(ADMIN_KEY_LOCK)
93
156
        .execute(&mut *tx)
94
156
        .await?;
95

            
96
156
    let public_key: Option<String> =
97
156
        sqlx::query_scalar("SELECT jwt_public_key FROM users WHERE id = $1::uuid")
98
156
            .bind(ADMIN_USER_ID)
99
156
            .fetch_optional(&mut *tx)
100
156
            .await?
101
156
            .flatten();
102
156
    let private_count: i64 = sqlx::query_scalar("SELECT count(*) FROM user_auth_keys")
103
156
        .fetch_one(&mut *tx)
104
156
        .await?;
105
    // Healthy ONLY when both halves are present and there is exactly one private
106
    // key. Anything else (missing public, missing private, or a duplicated row
107
    // left by a pre-fix race) is repaired by rewriting the keypair below.
108
156
    if public_key.is_some() && private_count == 1 {
109
78
        tx.commit().await?;
110
78
        return Ok(());
111
78
    }
112

            
113
78
    let keypair = crate::auth_keys::generate().await?;
114
78
    sqlx::query("UPDATE users SET jwt_public_key = $1 WHERE id = $2::uuid")
115
78
        .bind(&keypair.public_pem_b64)
116
78
        .bind(ADMIN_USER_ID)
117
78
        .execute(&mut *tx)
118
78
        .await?;
119
    // Clear any prior rows (incl. a duplicated pre-fix state) then insert the
120
    // one key matching the public key just written. The singleton constraint
121
    // (0005) also guarantees at most one row going forward.
122
78
    sqlx::query("DELETE FROM user_auth_keys")
123
78
        .execute(&mut *tx)
124
78
        .await?;
125
78
    sqlx::query("INSERT INTO user_auth_keys (singleton, private_key) VALUES (TRUE, $1)")
126
78
        .bind(&keypair.private_pem_b64)
127
78
        .execute(&mut *tx)
128
78
        .await?;
129
78
    tx.commit().await?;
130
78
    Ok(())
131
156
}