Skip to content

Commit 9ff09f7

Browse files
authored
fix(security): enforce SQL authorization across transports (#177)
* test(security): cover SQL transport authorization * ci: annotate ephemeral Calvin deadlines * chore: ignore subagent runtime artifacts * feat(security): centralize SQL task authorization * feat(security): enforce HTTP and pgwire SQL authorization * feat(security): authorize websocket and native SQL
1 parent d6d2b69 commit 9ff09f7

64 files changed

Lines changed: 4419 additions & 1831 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,4 @@ dmypy.json
103103

104104
.cargo/
105105
.claude/prep/
106+
.pi-subagents

Cargo.lock

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nodedb-test-support/src/native_harness/frames.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ use tokio::net::TcpStream;
1111
use nodedb_types::protocol::request_fields::RequestFields;
1212
use nodedb_types::protocol::text_fields::TextFields;
1313
use nodedb_types::protocol::{
14-
FRAME_HEADER_LEN, HELLO_ACK_MAGIC, HELLO_ERROR_MAGIC_U32, HelloAckFrame, HelloErrorFrame,
15-
HelloFrame, NativeRequest, NativeResponse, OpCode,
14+
AuthMethod, FRAME_HEADER_LEN, HELLO_ACK_MAGIC, HELLO_ERROR_MAGIC_U32, HelloAckFrame,
15+
HelloErrorFrame, HelloFrame, NativeRequest, NativeResponse, OpCode,
1616
};
1717

1818
/// Perform the handshake with a custom `HelloFrame`.
@@ -127,6 +127,21 @@ pub async fn send_request(
127127
sonic_rs::from_slice(&response_payload).expect("json decode NativeResponse")
128128
}
129129

130+
/// Authenticate a fresh native connection with an API key. The JSON Auth
131+
/// request also selects JSON framing for the rest of the session.
132+
pub async fn send_api_key_auth(stream: &mut TcpStream, seq: u64, token: String) -> NativeResponse {
133+
send_request(
134+
stream,
135+
seq,
136+
OpCode::Auth,
137+
TextFields {
138+
auth: Some(AuthMethod::ApiKey { token }),
139+
..Default::default()
140+
},
141+
)
142+
.await
143+
}
144+
130145
/// Send a `SHOW`/SQL statement over an established JSON-encoding session and
131146
/// decode the `NativeResponse`. Assumes the session's first frame already
132147
/// selected JSON (see `json_request_gets_json_response`) — callers that open

nodedb-test-support/src/native_harness/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,7 @@
99
mod frames;
1010
mod server;
1111

12-
pub use frames::{do_handshake, read_frame, send_request, send_sql, write_frame};
12+
pub use frames::{
13+
do_handshake, read_frame, send_api_key_auth, send_request, send_sql, write_frame,
14+
};
1315
pub use server::NativeTestServer;

nodedb-test-support/src/native_harness/server.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,8 @@ use nodedb::wal::WalManager;
1717
/// A running native-protocol test server.
1818
pub struct NativeTestServer {
1919
pub addr: std::net::SocketAddr,
20-
/// Shared Control-Plane state, exposed so tests can read metrics (e.g.
21-
/// `shared.system_metrics.active_txn_overlays`) that the Data-Plane
22-
/// core also updates via the same `Arc<SystemMetrics>`.
20+
/// Shared Control-Plane state, exposed so tests can inspect the same
21+
/// metrics and authorization stores used by the running server.
2322
pub shared: Arc<SharedState>,
2423
pub(super) shutdown_bus: nodedb::control::shutdown::ShutdownBus,
2524
pub(super) poller_shutdown_tx: tokio::sync::watch::Sender<bool>,
@@ -35,6 +34,15 @@ impl NativeTestServer {
3534
/// Spawn a single-core NodeDB server with the native listener bound to
3635
/// an ephemeral `127.0.0.1` port (trust-mode auth).
3736
pub async fn start() -> Self {
37+
Self::start_with_auth_mode(AuthMode::Trust).await
38+
}
39+
40+
/// Spawn a single-core server that requires an explicit native Auth frame.
41+
pub async fn start_authenticated() -> Self {
42+
Self::start_with_auth_mode(AuthMode::Password).await
43+
}
44+
45+
async fn start_with_auth_mode(auth_mode: AuthMode) -> Self {
3846
let dir = tempfile::tempdir().expect("tempdir");
3947
let wal_path = dir.path().join("test.wal");
4048
let wal = Arc::new(WalManager::open_for_testing(&wal_path).expect("open wal"));
@@ -69,7 +77,6 @@ impl NativeTestServer {
6977
let core_dir = dir.path().to_path_buf();
7078
let event_producer = event_producers.into_iter().next().expect("event producer");
7179
let core_array_catalog = shared.array_catalog.clone();
72-
let core_metrics = shared.system_metrics.clone();
7380
let (core_stop_tx, core_stop_rx) = std::sync::mpsc::channel::<()>();
7481
let _core_handle = tokio::task::spawn_blocking(move || {
7582
let mut core = CoreLoop::open_with_array_catalog(
@@ -82,9 +89,6 @@ impl NativeTestServer {
8289
)
8390
.expect("open core");
8491
core.set_event_producer(event_producer);
85-
if let Some(m) = core_metrics {
86-
core.set_metrics(m);
87-
}
8892
while matches!(
8993
core_stop_rx.try_recv(),
9094
Err(std::sync::mpsc::TryRecvError::Empty)
@@ -136,7 +140,7 @@ impl NativeTestServer {
136140
listener
137141
.run(nodedb::control::server::listener::ListenerRunParams {
138142
state: shared_listener,
139-
auth_mode: AuthMode::Trust,
143+
auth_mode,
140144
tls_acceptor: None,
141145
conn_semaphore: Arc::new(tokio::sync::Semaphore::new(128)),
142146
startup_gate: test_startup_gate,

nodedb-test-support/src/pgwire_harness/query.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,24 @@ impl TestServer {
123123
&self,
124124
user: &str,
125125
password: &str,
126+
) -> Result<(tokio_postgres::Client, tokio::task::JoinHandle<()>), String> {
127+
self.connect_as_database(user, password, "nodedb").await
128+
}
129+
130+
/// Open a second pgwire connection under a user-selected database.
131+
pub async fn connect_as_database(
132+
&self,
133+
user: &str,
134+
password: &str,
135+
database: &str,
126136
) -> Result<(tokio_postgres::Client, tokio::task::JoinHandle<()>), String> {
127137
let conn_str = format!(
128-
"host=127.0.0.1 port={} user={} password={} dbname=nodedb",
129-
self.pg_port, user, password
138+
"host=127.0.0.1 port={} user={} password={} dbname={}",
139+
self.pg_port, user, password, database
130140
);
131141
let (client, connection) = tokio_postgres::connect(&conn_str, tokio_postgres::NoTls)
132142
.await
133-
.map_err(|e| pg_error_detail(&e))?;
143+
.map_err(|error| pg_error_detail(&error))?;
134144
let handle = tokio::spawn(async move {
135145
let _ = connection.await;
136146
});

nodedb/src/control/cluster/calvin/scheduler/driver/core/request.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ impl Scheduler {
3131
database_id: DatabaseId::DEFAULT,
3232
vshard_id: VShardId::new(self.vshard_id),
3333
plan,
34+
// no-determinism: scheduler deadline controls waiting, not ordered state.
3435
deadline: Instant::now()
3536
+ Duration::from_millis(
3637
self.config.epoch_duration_ms * u64::from(self.config.txn_deadline_multiplier),

0 commit comments

Comments
 (0)