nodedb 0.4.0

Local-first, real-time, edge-to-cloud hybrid database for multi-modal workloads
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
// SPDX-License-Identifier: BUSL-1.1

//! End-to-end TCP roundtrip: real pgwire connection executes DDL and
//! observes both state mutation and SHOW SESSION results.

mod common;

use std::sync::Arc;

use common::{pgwire_auth_helpers::make_state, pgwire_harness::TestServer};
use nodedb::config::auth::AuthMode;
use nodedb::control::security::identity::Role;
use nodedb::types::TenantId;
use nodedb::{ServerConfig, bootstrap};
use nodedb_types::DatabaseId;
use tokio_postgres::SimpleQueryMessage;

async fn connect_empty_store_trust(
    server: &TestServer,
    username: &str,
) -> (tokio_postgres::Client, tokio::task::JoinHandle<()>) {
    let mut config = tokio_postgres::Config::new();
    config
        .host("127.0.0.1")
        .port(server.pg_port)
        .user(username)
        .dbname("default");
    let (client, connection) = config
        .connect(tokio_postgres::NoTls)
        .await
        .expect("trust mode must accept the stored configured identity");
    let connection_handle = tokio::spawn(async move {
        let _ = connection.await;
    });
    (client, connection_handle)
}

fn trust_config() -> ServerConfig {
    let mut config = ServerConfig::default();
    config.auth.mode = AuthMode::Trust;
    config.auth.superuser_name = "nodedb".to_owned();
    config
}

fn bootstrap_trust_superuser(server: &TestServer) {
    bootstrap::credentials::bootstrap_superuser(&server.shared, &trust_config())
        .expect("trust-mode superuser bootstrap must succeed");
}

async fn assert_configured_trust_superuser_survives(sql: &str) {
    let server = TestServer::start_empty_store_trust().await;
    bootstrap_trust_superuser(&server);
    let username = "nodedb";
    let (client, connection_handle) = connect_empty_store_trust(&server, username).await;

    client
        .simple_query(sql)
        .await
        .unwrap_or_else(|error| panic!("trusted superuser DDL failed for {sql}: {error}"));

    let (reconnected, reconnect_handle) = connect_empty_store_trust(&server, username).await;
    let messages = reconnected
        .simple_query("SHOW SESSION")
        .await
        .expect("credential mutation must not revoke the configured trust identity");
    let session_username = messages.iter().find_map(|message| match message {
        SimpleQueryMessage::Row(row) => row.get(0).map(str::to_owned),
        _ => None,
    });
    assert_eq!(session_username, Some(username.to_owned()));

    drop(reconnected);
    reconnect_handle.abort();
    let _ = reconnect_handle.await;
    drop(client);
    connection_handle.abort();
    let _ = connection_handle.await;
    server.graceful_shutdown().await;
}

#[tokio::test]
async fn pgwire_ddl_roundtrip() {
    let state = make_state();
    bootstrap::credentials::bootstrap_superuser(&state, &trust_config())
        .expect("materialize configured trust superuser");

    let pg_listener =
        nodedb::control::server::pgwire::listener::PgListener::bind("127.0.0.1:0".parse().unwrap())
            .await
            .unwrap();
    let port = pg_listener.local_addr().port();

    let (shutdown_bus, _) =
        nodedb::control::shutdown::ShutdownBus::new(Arc::clone(&state.shutdown));
    let shared_pg = Arc::clone(&state);
    let test_startup_gate = Arc::clone(&state.startup);
    let bus_pg = shutdown_bus.clone();
    let listener_handle = tokio::spawn(async move {
        pg_listener
            .run(
                shared_pg,
                nodedb::config::auth::AuthMode::Trust,
                None,
                Arc::new(tokio::sync::Semaphore::new(128)),
                test_startup_gate,
                bus_pg,
            )
            .await
    });

    tokio::time::sleep(std::time::Duration::from_millis(30)).await;

    let conn_str = format!("host=127.0.0.1 port={port} user=nodedb dbname=nodedb");
    let (client, connection) = tokio_postgres::connect(&conn_str, tokio_postgres::NoTls)
        .await
        .unwrap();
    let connection_handle = tokio::spawn(async move {
        let _ = connection.await;
    });

    client
        .simple_query("CREATE USER wire_test WITH PASSWORD 'pass'")
        .await
        .unwrap();

    let msgs = client.simple_query("SHOW SESSION").await.unwrap();
    let username = msgs.iter().find_map(|m| match m {
        SimpleQueryMessage::Row(row) => row.get(0).map(|s| s.to_string()),
        _ => None,
    });
    assert_eq!(username, Some("nodedb".to_string()));

    assert!(state.credentials.get_user("wire_test").is_some());

    drop(client);
    connection_handle.abort();
    let _ = connection_handle.await;
    let _shutdown = shutdown_bus.initiate();
    listener_handle
        .await
        .expect("pgwire listener task must not panic")
        .expect("pgwire listener must shut down cleanly");
}

#[tokio::test]
async fn trust_bootstrap_materializes_configured_superuser() {
    let server = TestServer::start_empty_store_trust().await;
    bootstrap_trust_superuser(&server);

    let user = server
        .shared
        .credentials
        .get_user("nodedb")
        .expect("configured trust superuser must have a durable catalog identity");
    assert!(user.is_superuser);
    assert_eq!(user.tenant_id, TenantId::new(1));

    server.graceful_shutdown().await;
}

#[tokio::test]
async fn trust_configured_superuser_survives_tenant_creation() {
    assert_configured_trust_superuser_survives("CREATE TENANT alpha").await;
}

#[tokio::test]
async fn trust_configured_superuser_survives_user_creation() {
    assert_configured_trust_superuser_survives(
        "CREATE USER alice WITH PASSWORD 'strong-secret' ROLE readonly",
    )
    .await;
}

#[tokio::test]
async fn trust_configured_superuser_survives_service_account_creation() {
    assert_configured_trust_superuser_survives("CREATE SERVICE ACCOUNT batch_processor").await;
}

#[tokio::test]
async fn trust_catalog_owners_remain_valid_across_restart() {
    let server = TestServer::start_empty_store_trust().await;
    bootstrap_trust_superuser(&server);
    let (client, connection_handle) = connect_empty_store_trust(&server, "nodedb").await;

    client
        .simple_query("CREATE COLLECTION trust_owned_records")
        .await
        .expect("trusted superuser must create an owned collection");
    client
        .simple_query("CREATE TENANT alpha")
        .await
        .expect("trusted superuser must create a tenant");

    drop(client);
    connection_handle.abort();
    let _ = connection_handle.await;
    let (server, data_dir) = server.take_dir();
    server.graceful_shutdown().await;

    let (reopened, _data_dir) = TestServer::open_on_path_empty_store_trust(data_dir).await;
    bootstrap_trust_superuser(&reopened);
    let report = nodedb::control::cluster::recovery_check::verify_and_repair(&reopened.shared)
        .await
        .expect("catalog sanity check must complete");
    assert!(
        report.is_acceptable(),
        "configured trust ownership must remain startup-safe: {report}"
    );
    assert_eq!(
        report.integrity_repaired, 0,
        "valid configured-superuser ownership must not require startup repair"
    );

    let collection = reopened
        .shared
        .credentials
        .catalog()
        .get_collection(DatabaseId::DEFAULT, 1, "trust_owned_records")
        .expect("collection catalog lookup")
        .expect("owned collection must survive restart");
    assert_eq!(collection.owner, "nodedb");

    reopened.graceful_shutdown().await;
}

#[tokio::test]
async fn trust_mode_rejects_unmaterialized_identity() {
    let server = TestServer::start_empty_store_trust().await;

    let result = server
        .connect_as_database("unmaterialized_identity", "ignored", "default")
        .await;

    assert!(
        result.is_err(),
        "trust mode must skip password verification without fabricating an identity"
    );
    server.graceful_shutdown().await;
}

#[tokio::test]
async fn trust_superuser_identity_survives_password_mode_restart() {
    let server = TestServer::start_empty_store_trust().await;
    bootstrap_trust_superuser(&server);
    let original_user_id = server
        .shared
        .credentials
        .get_user("nodedb")
        .expect("trust superuser")
        .user_id;
    let (server, data_dir) = server.take_dir();
    server.graceful_shutdown().await;

    let (reopened, _data_dir) = TestServer::open_on_path_empty_store_password(data_dir).await;
    reopened
        .shared
        .credentials
        .bootstrap_superuser("nodedb", "operator-password")
        .expect("replace internal trust credential");
    let password_user = reopened
        .shared
        .credentials
        .get_user("nodedb")
        .expect("password superuser");
    assert_eq!(password_user.user_id, original_user_id);

    let (client, connection_handle) = reopened
        .connect_as_database("nodedb", "operator-password", "default")
        .await
        .expect("password bootstrap must authenticate the durable identity");
    client
        .simple_query("SELECT 1")
        .await
        .expect("password-authenticated query");

    drop(client);
    connection_handle.abort();
    let _ = connection_handle.await;
    reopened.graceful_shutdown().await;
}

#[tokio::test]
async fn trust_configured_identity_survives_discard_all() {
    let server = TestServer::start_empty_store_trust().await;
    bootstrap_trust_superuser(&server);
    let username = "nodedb";
    let (client, connection_handle) = connect_empty_store_trust(&server, username).await;

    client
        .simple_query("SET nodedb.consistency = eventual")
        .await
        .expect("SET must establish mutable session state before DISCARD ALL");
    client
        .simple_query("SET TENANT = 99")
        .await
        .expect("SET TENANT must establish a temporary tenant overlay");
    client
        .simple_query("DISCARD ALL")
        .await
        .expect("DISCARD ALL must retain the authenticated trust identity");
    let messages = client
        .simple_query("SHOW SESSION")
        .await
        .expect("trusted connection must remain authenticated after DISCARD ALL");
    let session_username = messages.iter().find_map(|message| match message {
        SimpleQueryMessage::Row(row) => row.get(0).map(str::to_owned),
        _ => None,
    });

    assert_eq!(session_username, Some(username.to_owned()));

    let tenant_messages = client
        .simple_query("SHOW TENANT")
        .await
        .expect("DISCARD ALL must clear the tenant overlay");
    let effective_tenant = tenant_messages.iter().find_map(|message| match message {
        SimpleQueryMessage::Row(row) => row.get(0).map(str::to_owned),
        _ => None,
    });
    assert_eq!(effective_tenant, Some("1".to_owned()));

    let consistency_messages = client
        .simple_query("SHOW nodedb.consistency")
        .await
        .expect("DISCARD ALL must reset session parameters");
    let consistency = consistency_messages
        .iter()
        .find_map(|message| match message {
            SimpleQueryMessage::Row(row) => row.get(0).map(str::to_owned),
            _ => None,
        });
    assert_eq!(consistency, Some("strong".to_owned()));
    assert!(
        server.shared.credentials.get_user(username).is_some(),
        "DISCARD ALL must retain the durable configured trust identity"
    );

    drop(client);
    connection_handle.abort();
    let _ = connection_handle.await;
    server.graceful_shutdown().await;
}

#[tokio::test]
async fn trust_known_user_role_downgrade_takes_effect() {
    let server = TestServer::start().await;
    let username = "known_trust_role_downgrade";
    server
        .shared
        .credentials
        .create_user(
            username,
            "unused-in-trust-mode",
            TenantId::new(1),
            vec![Role::Superuser],
        )
        .expect("create known Trust user");

    let (client, connection_handle) = server
        .connect_as(username, "ignored")
        .await
        .expect("known Trust user must authenticate");
    client
        .simple_query("SHOW SESSION")
        .await
        .expect("known Trust user must issue an initial query");

    server
        .shared
        .credentials
        .update_roles(username, vec![Role::ReadOnly])
        .expect("downgrade known Trust user");
    let role_downgrade = client
        .simple_query("CREATE USER stale_trust_role_probe WITH PASSWORD 'x'")
        .await;
    assert!(
        role_downgrade.is_err(),
        "a known Trust connection must not retain a stale superuser identity after role removal"
    );
    assert!(
        server
            .shared
            .credentials
            .get_user("stale_trust_role_probe")
            .is_none(),
        "stale Trust roles must not authorize DDL"
    );

    drop(client);
    connection_handle.abort();
    let _ = connection_handle.await;
    server.graceful_shutdown().await;
}

#[tokio::test]
async fn trust_known_user_drop_fails_closed() {
    let server = TestServer::start().await;
    let username = "known_trust_drop";
    server
        .shared
        .credentials
        .create_user(
            username,
            "unused-in-trust-mode",
            TenantId::new(1),
            vec![Role::Superuser],
        )
        .expect("create known Trust user");

    let (client, connection_handle) = server
        .connect_as(username, "ignored")
        .await
        .expect("known Trust user must authenticate");
    client
        .simple_query("SHOW SESSION")
        .await
        .expect("known Trust user must issue an initial query");

    server
        .shared
        .credentials
        .drop_user(username)
        .expect("drop known Trust user");
    assert!(
        client.simple_query("SHOW SESSION").await.is_err(),
        "a dropped known Trust user must fail closed on its next request"
    );

    drop(client);
    connection_handle.abort();
    let _ = connection_handle.await;
    server.graceful_shutdown().await;
}