Skip to main content

powdb_server/
handler.rs

1use crate::metrics::{Metrics, QueryOutcome};
2use crate::protocol::{Message, WireParam};
3use powdb_auth::{Permission, Role, UserStore};
4use powdb_query::executor::{is_read_only_statement, Engine};
5use powdb_query::parser;
6use powdb_query::result::{QueryError, QueryResult};
7use powdb_query::sql;
8use powdb_storage::types::Value;
9use std::collections::HashMap;
10use std::net::IpAddr;
11use std::sync::{Arc, Mutex, RwLock};
12use std::time::{Duration, Instant};
13use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, BufWriter};
14use tokio::sync::{watch, OwnedSemaphorePermit, Semaphore};
15use tracing::{debug, error, info, warn};
16use zeroize::Zeroizing;
17
18/// Tracks per-IP authentication failure counts for rate limiting.
19pub type AuthRateLimiter = Arc<Mutex<HashMap<IpAddr, (u32, Instant)>>>;
20
21/// Gate that serializes wire-protocol statements while an explicit
22/// transaction is open on any connection. The connection that runs `begin`
23/// keeps an owned permit until `commit`, `rollback`, disconnect, or timeout,
24/// preventing other connections from observing or joining uncommitted state.
25pub type TxGate = Arc<Semaphore>;
26
27/// Create a transaction gate for a shared engine.
28pub fn new_tx_gate() -> TxGate {
29    Arc::new(Semaphore::new(1))
30}
31
32/// Maximum query text length accepted from the wire (1 MB).
33const MAX_QUERY_LENGTH: usize = 1024 * 1024;
34
35/// Timeout for writing a response to the client. Prevents slow-drain
36/// clients from blocking the handler indefinitely.
37const WRITE_TIMEOUT: Duration = Duration::from_secs(30);
38
39/// Maximum number of auth failures per IP within the rate-limit window.
40const MAX_AUTH_FAILURES: u32 = 5;
41
42/// Window during which auth failures are counted (60 seconds).
43const AUTH_FAILURE_WINDOW: Duration = Duration::from_secs(60);
44
45/// Create a new shared rate limiter.
46pub fn new_rate_limiter() -> AuthRateLimiter {
47    Arc::new(Mutex::new(HashMap::new()))
48}
49
50/// Check whether an IP is rate-limited and record a failure if requested.
51/// Returns `true` if the IP should be rejected.
52fn is_rate_limited(limiter: &AuthRateLimiter, ip: IpAddr) -> bool {
53    let mut map = limiter.lock().unwrap_or_else(|e| e.into_inner());
54    // Clean up stale entries while we have the lock.
55    let now = Instant::now();
56    map.retain(|_, (_, ts)| now.duration_since(*ts) < AUTH_FAILURE_WINDOW);
57
58    if let Some((count, _)) = map.get(&ip) {
59        *count >= MAX_AUTH_FAILURES
60    } else {
61        false
62    }
63}
64
65/// Record an auth failure for the given IP.
66fn record_auth_failure(limiter: &AuthRateLimiter, ip: IpAddr) {
67    let mut map = limiter.lock().unwrap_or_else(|e| e.into_inner());
68    let now = Instant::now();
69    let entry = map.entry(ip).or_insert((0, now));
70    // Reset counter if the window has elapsed.
71    if now.duration_since(entry.1) >= AUTH_FAILURE_WINDOW {
72        *entry = (1, now);
73    } else {
74        entry.0 += 1;
75    }
76}
77
78/// Clear the failure counter on successful auth.
79fn clear_auth_failures(limiter: &AuthRateLimiter, ip: IpAddr) {
80    let mut map = limiter.lock().unwrap_or_else(|e| e.into_inner());
81    map.remove(&ip);
82}
83
84/// Constant-time password comparison. Hashes both inputs to fixed-size
85/// SHA-256 digests so neither length nor content leaks through timing.
86fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
87    use sha2::{Digest, Sha256};
88    let ha = Sha256::digest(a);
89    let hb = Sha256::digest(b);
90    let mut diff = 0u8;
91    for (x, y) in ha.iter().zip(hb.iter()) {
92        diff |= x ^ y;
93    }
94    diff == 0
95}
96
97/// An authenticated connection's identity. Bound at connect time and consulted
98/// on every query by [`dispatch_query`] to enforce the user's role: a
99/// `readonly` principal may only execute read statements.
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct Principal {
102    pub name: String,
103    pub role: String,
104}
105
106/// Whether a parsed statement is data-definition (schema) work: creating,
107/// altering, or dropping a type or view. `explain <ddl>` is classified by its
108/// inner statement so `explain drop User` needs the same permission as
109/// `drop User`. Mutations that change *rows* (insert/update/delete/upsert/
110/// refresh) and transaction control are NOT DDL — they fall under `Write`.
111fn is_ddl_statement(stmt: &powdb_query::ast::Statement) -> bool {
112    use powdb_query::ast::Statement;
113    let inner = match stmt {
114        Statement::Explain(inner) => inner.as_ref(),
115        other => other,
116    };
117    matches!(
118        inner,
119        Statement::CreateType(_)
120            | Statement::AlterTable(_)
121            | Statement::DropTable(_)
122            | Statement::CreateView(_)
123            | Statement::DropView(_)
124    )
125}
126
127/// The capability a parsed statement requires under the RBAC lattice
128/// (`crates/auth/src/role.rs`). Reads need [`Permission::Read`]; schema
129/// definition needs [`Permission::Ddl`]; every other mutation needs
130/// [`Permission::Write`]. [`Permission::Admin`] is reserved for user/role
131/// management, which is CLI-only today and never reaches this wire path.
132fn required_permission(stmt: &powdb_query::ast::Statement) -> Permission {
133    if is_read_only_statement(stmt) {
134        Permission::Read
135    } else if is_ddl_statement(stmt) {
136        Permission::Ddl
137    } else {
138        Permission::Write
139    }
140}
141
142/// Enforce the principal's role against a parsed statement using the full
143/// permission lattice. Reads are always permitted (any authenticated role can
144/// read — unknown role names still read but fail closed on any mutation).
145/// Mutations require the specific capability the statement maps to: row
146/// mutations need `Write`, schema changes need `Ddl`. Unknown role names
147/// resolve to no builtin and therefore grant nothing beyond reads.
148///
149/// Classification uses the parsed AST via
150/// [`powdb_query::executor::is_read_only_statement`] — the exact same
151/// classifier the RwLock read/write split relies on — so the permission
152/// boundary and the concurrency boundary can never disagree.
153fn check_statement_permitted(
154    principal: Option<&Principal>,
155    stmt: &powdb_query::ast::Statement,
156) -> Result<(), QueryError> {
157    let Some(p) = principal else {
158        // No per-user identity (shared-password or open mode): full access,
159        // byte-identical to the pre-RBAC behavior.
160        return Ok(());
161    };
162    // Reads are permitted for every authenticated principal (preserves the
163    // pre-lattice contract that any connected role may run read-only queries).
164    if is_read_only_statement(stmt) {
165        return Ok(());
166    }
167    let needed = required_permission(stmt);
168    if Role::builtin(&p.role).is_some_and(|r| r.allows(needed)) {
169        return Ok(());
170    }
171    let kind = if needed == Permission::Ddl {
172        "schema-definition"
173    } else {
174        "write"
175    };
176    Err(QueryError::Execution(format!(
177        "permission denied: role '{}' cannot execute {kind} statements",
178        p.role
179    )))
180}
181
182/// Result of the connect-time authentication decision.
183#[derive(Debug, Clone, PartialEq, Eq)]
184pub enum AuthOutcome {
185    /// Authenticated. `principal` is `Some` when a named user authenticated via
186    /// the UserStore, and `None` for the legacy shared-password / open paths
187    /// where there is no per-user identity.
188    Authenticated { principal: Option<Principal> },
189    /// Rejected. The caller sends a generic "authentication failed" error and
190    /// records a rate-limit failure — it must not reveal which check failed.
191    Rejected,
192}
193
194/// Pure, exhaustively-testable authentication decision for a CONNECT handshake.
195///
196/// Policy:
197/// - If `users` has at least one user, multi-user auth is in force: a
198///   `username` is required and `users.authenticate(username, password)` must
199///   succeed. Unknown user, wrong password, or a missing username all reject
200///   with an indistinguishable `Rejected` (no user-vs-password leak).
201/// - If `users` is empty, fall back verbatim to the legacy behavior: when
202///   `expected_password` is `Some`, the candidate must match it (constant time);
203///   when `None`, no auth is required (open). The `username` is ignored here so
204///   that a new client talking to a shared-password server still connects.
205pub fn authenticate_connect(
206    users: &UserStore,
207    expected_password: Option<&str>,
208    username: Option<&str>,
209    password: Option<&str>,
210) -> AuthOutcome {
211    if !users.is_empty() {
212        // Multi-user mode: a username is mandatory.
213        let Some(name) = username else {
214            return AuthOutcome::Rejected;
215        };
216        let Some(candidate) = password else {
217            return AuthOutcome::Rejected;
218        };
219        match users.authenticate(name, candidate) {
220            Some(user) => AuthOutcome::Authenticated {
221                principal: Some(Principal {
222                    name: user.name.clone(),
223                    role: user.role.clone(),
224                }),
225            },
226            None => AuthOutcome::Rejected,
227        }
228    } else {
229        // Legacy shared-password fallback (byte-identical to prior behavior).
230        match expected_password {
231            Some(expected) => {
232                if password.is_some_and(|p| constant_time_eq(p.as_bytes(), expected.as_bytes())) {
233                    AuthOutcome::Authenticated { principal: None }
234                } else {
235                    AuthOutcome::Rejected
236                }
237            }
238            None => AuthOutcome::Authenticated { principal: None },
239        }
240    }
241}
242
243/// Error messages that are safe to forward to the client verbatim.
244const SAFE_ERROR_PREFIXES: &[&str] = &[
245    "table not found",
246    "column not found",
247    "parse error",
248    "type mismatch",
249    "unknown table",
250    "unknown column",
251    "unknown function",
252    "syntax error",
253    "expected",
254    "unexpected",
255    "missing",
256    "duplicate",
257    "invalid",
258    "cannot",
259    "no such",
260    "already exists",
261    "permission denied",
262    "row too large",
263    "unique constraint violation",
264    // Resource-limit errors carry actionable guidance (e.g. "add a LIMIT
265    // clause") and leak no internal state, so surface them verbatim instead
266    // of masking them to the generic message. See QueryError::{SortLimit,
267    // JoinLimit,MemoryLimit}Exceeded in crates/query/src/result.rs.
268    "sort input exceeds",
269    "join result exceeds",
270    "query exceeded memory budget",
271];
272
273/// Sanitize an error message before sending it to the client.
274/// Known safe errors are passed through; everything else is replaced
275/// with a generic message to avoid leaking internal details.
276fn sanitize_error(e: &str) -> String {
277    let lower = e.to_lowercase();
278    for prefix in SAFE_ERROR_PREFIXES {
279        if lower.starts_with(prefix) {
280            return e.to_string();
281        }
282    }
283    "query execution error".into()
284}
285
286/// Write a message to the client with a timeout. Returns false if the
287/// write failed or timed out (caller should close the connection).
288async fn write_msg<W: AsyncWrite + Unpin>(writer: &mut BufWriter<W>, msg: &Message) -> bool {
289    let write_fut = async {
290        if msg.write_to(writer).await.is_err() {
291            return false;
292        }
293        writer.flush().await.is_ok()
294    };
295    tokio::time::timeout(WRITE_TIMEOUT, write_fut)
296        .await
297        .unwrap_or_default()
298}
299
300/// Options for a single connection, bundled to keep `handle_connection`'s
301/// argument list short.
302pub struct ConnOpts<'a> {
303    pub engine: Arc<RwLock<Engine>>,
304    pub tx_gate: TxGate,
305    /// Expected client password. Wrapped in `Zeroizing` so the secret is wiped
306    /// from memory on drop (defends against leaking via a core dump).
307    pub expected_password: Option<Zeroizing<String>>,
308    /// Multi-user store loaded from the data dir at startup. When it has users,
309    /// the handshake authenticates `(username, password)` against it; when empty
310    /// the server falls back to `expected_password`. Shared across connections.
311    pub users: Arc<UserStore>,
312    pub shutdown_rx: &'a mut watch::Receiver<bool>,
313    pub idle_timeout: Duration,
314    pub query_timeout: Duration,
315    pub rate_limiter: Option<&'a AuthRateLimiter>,
316    pub peer_addr: Option<std::net::SocketAddr>,
317    /// Shared server metrics. Always present; tests pass `Arc::new(Metrics::new())`.
318    pub metrics: Arc<Metrics>,
319}
320
321/// Execute a query against the engine under the RwLock. Read-only
322/// statements acquire `.read()` so concurrent SELECTs can scan in
323/// parallel; mutations acquire `.write()`.
324///
325/// When `principal` is `Some`, the user's role is enforced first: a role
326/// without the `Write` permission (i.e. `readonly`) gets a clean
327/// "permission denied" error for any non-read statement, before any lock
328/// is taken or any engine state is touched.
329fn dispatch_query(
330    engine: &Arc<RwLock<Engine>>,
331    query: &str,
332    principal: Option<&Principal>,
333) -> Result<QueryResult, QueryError> {
334    let stmt_result = parser::parse(query).map_err(|e| e.to_string());
335
336    // Role enforcement happens on the parsed AST. Statements that fail to
337    // parse fall through — the engine returns the parse error itself and
338    // can never execute anything for them.
339    if let Ok(stmt) = &stmt_result {
340        check_statement_permitted(principal, stmt)?;
341    }
342
343    let can_try_read = matches!(&stmt_result, Ok(s) if is_read_only_statement(s));
344    if can_try_read {
345        let res = {
346            let eng = engine
347                .read()
348                .map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
349            eng.execute_powql_readonly(query)
350        };
351        match res {
352            Ok(r) => return Ok(r),
353            Err(QueryError::ReadonlyNeedsWrite) => {
354                // Escalate: fall through to the write path below.
355            }
356            Err(e) => return Err(e),
357        }
358    }
359
360    let mut eng = engine
361        .write()
362        .map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
363    eng.execute_powql(query)
364}
365
366fn dispatch_sql_query(
367    engine: &Arc<RwLock<Engine>>,
368    query: &str,
369    principal: Option<&Principal>,
370) -> Result<QueryResult, QueryError> {
371    let stmt_result = sql::parse_sql(query).map_err(|e| e.to_string());
372
373    if let Ok(stmt) = &stmt_result {
374        check_statement_permitted(principal, stmt)?;
375    }
376
377    let can_try_read = matches!(&stmt_result, Ok(s) if is_read_only_statement(s));
378    if can_try_read {
379        let res = {
380            let eng = engine
381                .read()
382                .map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
383            eng.execute_sql_readonly(query)
384        };
385        match res {
386            Ok(r) => return Ok(r),
387            Err(QueryError::ReadonlyNeedsWrite) => {}
388            Err(e) => return Err(e),
389        }
390    }
391
392    let mut eng = engine
393        .write()
394        .map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
395    eng.execute_sql(query)
396}
397
398/// Convert a wire parameter into the query-crate [`ParamValue`] used for
399/// token-level binding.
400fn wire_param_to_value(p: &WireParam) -> powdb_query::ast::ParamValue {
401    use powdb_query::ast::ParamValue;
402    match p {
403        WireParam::Null => ParamValue::Null,
404        WireParam::Int(v) => ParamValue::Int(*v),
405        WireParam::Float(v) => ParamValue::Float(*v),
406        WireParam::Bool(v) => ParamValue::Bool(*v),
407        WireParam::Str(s) => ParamValue::Str(s.clone()),
408    }
409}
410
411/// Parameterized counterpart of [`dispatch_query`]. Routes through the exact
412/// same role-enforcement and read/write escalation logic, but binds the
413/// `$N` placeholders at the token level via the query crate's
414/// `parse_with_params` path. A string parameter can never change the query's
415/// shape — it is substituted as a literal token, not interpolated text.
416#[derive(Debug, Clone, Copy, PartialEq, Eq)]
417enum TransactionControl {
418    Begin,
419    Commit,
420    Rollback,
421}
422
423fn transaction_control(stmt: &powdb_query::ast::Statement) -> Option<TransactionControl> {
424    use powdb_query::ast::Statement;
425    match stmt {
426        Statement::Begin => Some(TransactionControl::Begin),
427        Statement::Commit => Some(TransactionControl::Commit),
428        Statement::Rollback => Some(TransactionControl::Rollback),
429        _ => None,
430    }
431}
432
433fn classify_query_transaction_control(query: &str) -> Option<TransactionControl> {
434    parser::parse(query)
435        .ok()
436        .and_then(|stmt| transaction_control(&stmt))
437}
438
439fn classify_sql_transaction_control(query: &str) -> Option<TransactionControl> {
440    sql::parse_sql(query)
441        .ok()
442        .and_then(|stmt| transaction_control(&stmt))
443}
444
445fn classify_params_transaction_control(
446    query: &str,
447    params: &[WireParam],
448) -> Option<TransactionControl> {
449    let bound: Vec<powdb_query::ast::ParamValue> = params.iter().map(wire_param_to_value).collect();
450    parser::parse_with_params(query, &bound)
451        .ok()
452        .and_then(|stmt| transaction_control(&stmt))
453}
454
455fn dispatch_query_with_params(
456    engine: &Arc<RwLock<Engine>>,
457    query: &str,
458    params: &[WireParam],
459    principal: Option<&Principal>,
460) -> Result<QueryResult, QueryError> {
461    let bound: Vec<powdb_query::ast::ParamValue> = params.iter().map(wire_param_to_value).collect();
462
463    // Parse once (with params bound) so role enforcement and read/write
464    // classification see exactly the statement that will execute.
465    let stmt_result = parser::parse_with_params(query, &bound).map_err(|e| e.to_string());
466
467    if let Ok(stmt) = &stmt_result {
468        check_statement_permitted(principal, stmt)?;
469    }
470
471    let can_try_read = matches!(&stmt_result, Ok(s) if is_read_only_statement(s));
472    if can_try_read {
473        let res = {
474            let eng = engine
475                .read()
476                .map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
477            eng.execute_powql_readonly_with_params(query, &bound)
478        };
479        match res {
480            Ok(r) => return Ok(r),
481            Err(QueryError::ReadonlyNeedsWrite) => {
482                // Escalate to the write path below.
483            }
484            Err(e) => return Err(e),
485        }
486    }
487
488    let mut eng = engine
489        .write()
490        .map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
491    eng.execute_powql_with_params(query, &bound)
492}
493
494async fn execute_wire_query(
495    engine: Arc<RwLock<Engine>>,
496    tx_gate: TxGate,
497    tx_permit: &mut Option<OwnedSemaphorePermit>,
498    query: String,
499    principal: Option<Principal>,
500    query_timeout: Duration,
501    metrics: &Arc<Metrics>,
502) -> Message {
503    match classify_query_transaction_control(&query) {
504        Some(TransactionControl::Begin) => {
505            if tx_permit.is_some() {
506                return Message::Error {
507                    message: sanitize_error("transaction already active"),
508                };
509            }
510            let permit = match tx_gate.acquire_owned().await {
511                Ok(permit) => permit,
512                Err(_) => {
513                    return Message::Error {
514                        message: "query execution error".into(),
515                    }
516                }
517            };
518            let response = run_blocking_query(
519                engine,
520                query,
521                principal,
522                query_timeout,
523                metrics,
524                |engine, query, principal| dispatch_query(&engine, &query, principal.as_ref()),
525            )
526            .await;
527            if is_success_response(&response) {
528                *tx_permit = Some(permit);
529            }
530            response
531        }
532        Some(TransactionControl::Commit | TransactionControl::Rollback) => {
533            let response = run_blocking_query(
534                engine,
535                query,
536                principal,
537                query_timeout,
538                metrics,
539                |engine, query, principal| dispatch_query(&engine, &query, principal.as_ref()),
540            )
541            .await;
542            if is_success_response(&response) {
543                tx_permit.take();
544            }
545            response
546        }
547        None if tx_permit.is_some() => {
548            run_blocking_query(
549                engine,
550                query,
551                principal,
552                query_timeout,
553                metrics,
554                |engine, query, principal| dispatch_query(&engine, &query, principal.as_ref()),
555            )
556            .await
557        }
558        None => {
559            let permit = match tx_gate.acquire_owned().await {
560                Ok(permit) => permit,
561                Err(_) => {
562                    return Message::Error {
563                        message: "query execution error".into(),
564                    }
565                }
566            };
567            let response = run_blocking_query(
568                engine,
569                query,
570                principal,
571                query_timeout,
572                metrics,
573                |engine, query, principal| dispatch_query(&engine, &query, principal.as_ref()),
574            )
575            .await;
576            drop(permit);
577            response
578        }
579    }
580}
581
582async fn execute_wire_query_sql(
583    engine: Arc<RwLock<Engine>>,
584    tx_gate: TxGate,
585    tx_permit: &mut Option<OwnedSemaphorePermit>,
586    query: String,
587    principal: Option<Principal>,
588    query_timeout: Duration,
589    metrics: &Arc<Metrics>,
590) -> Message {
591    match classify_sql_transaction_control(&query) {
592        Some(TransactionControl::Begin) => {
593            if tx_permit.is_some() {
594                return Message::Error {
595                    message: sanitize_error("transaction already active"),
596                };
597            }
598            let permit = match tx_gate.acquire_owned().await {
599                Ok(permit) => permit,
600                Err(_) => {
601                    return Message::Error {
602                        message: "query execution error".into(),
603                    }
604                }
605            };
606            let response = run_blocking_query(
607                engine,
608                query,
609                principal,
610                query_timeout,
611                metrics,
612                |engine, query, principal| dispatch_sql_query(&engine, &query, principal.as_ref()),
613            )
614            .await;
615            if is_success_response(&response) {
616                *tx_permit = Some(permit);
617            }
618            response
619        }
620        Some(TransactionControl::Commit | TransactionControl::Rollback) => {
621            let response = run_blocking_query(
622                engine,
623                query,
624                principal,
625                query_timeout,
626                metrics,
627                |engine, query, principal| dispatch_sql_query(&engine, &query, principal.as_ref()),
628            )
629            .await;
630            if is_success_response(&response) {
631                tx_permit.take();
632            }
633            response
634        }
635        None if tx_permit.is_some() => {
636            run_blocking_query(
637                engine,
638                query,
639                principal,
640                query_timeout,
641                metrics,
642                |engine, query, principal| dispatch_sql_query(&engine, &query, principal.as_ref()),
643            )
644            .await
645        }
646        None => {
647            let permit = match tx_gate.acquire_owned().await {
648                Ok(permit) => permit,
649                Err(_) => {
650                    return Message::Error {
651                        message: "query execution error".into(),
652                    }
653                }
654            };
655            let response = run_blocking_query(
656                engine,
657                query,
658                principal,
659                query_timeout,
660                metrics,
661                |engine, query, principal| dispatch_sql_query(&engine, &query, principal.as_ref()),
662            )
663            .await;
664            drop(permit);
665            response
666        }
667    }
668}
669
670// One over clippy's default arg limit: the metrics handle was threaded through
671// to instrument the typed query result. Bundling these into a struct would add
672// more noise than it removes for an internal dispatcher.
673#[allow(clippy::too_many_arguments)]
674async fn execute_wire_query_with_params(
675    engine: Arc<RwLock<Engine>>,
676    tx_gate: TxGate,
677    tx_permit: &mut Option<OwnedSemaphorePermit>,
678    query: String,
679    params: Vec<WireParam>,
680    principal: Option<Principal>,
681    query_timeout: Duration,
682    metrics: &Arc<Metrics>,
683) -> Message {
684    match classify_params_transaction_control(&query, &params) {
685        Some(TransactionControl::Begin) => {
686            if tx_permit.is_some() {
687                return Message::Error {
688                    message: sanitize_error("transaction already active"),
689                };
690            }
691            let permit = match tx_gate.acquire_owned().await {
692                Ok(permit) => permit,
693                Err(_) => {
694                    return Message::Error {
695                        message: "query execution error".into(),
696                    }
697                }
698            };
699            let response = run_blocking_query(
700                engine,
701                (query, params),
702                principal,
703                query_timeout,
704                metrics,
705                |engine, (query, params), principal| {
706                    dispatch_query_with_params(&engine, &query, &params, principal.as_ref())
707                },
708            )
709            .await;
710            if is_success_response(&response) {
711                *tx_permit = Some(permit);
712            }
713            response
714        }
715        Some(TransactionControl::Commit | TransactionControl::Rollback) => {
716            let response = run_blocking_query(
717                engine,
718                (query, params),
719                principal,
720                query_timeout,
721                metrics,
722                |engine, (query, params), principal| {
723                    dispatch_query_with_params(&engine, &query, &params, principal.as_ref())
724                },
725            )
726            .await;
727            if is_success_response(&response) {
728                tx_permit.take();
729            }
730            response
731        }
732        None if tx_permit.is_some() => {
733            run_blocking_query(
734                engine,
735                (query, params),
736                principal,
737                query_timeout,
738                metrics,
739                |engine, (query, params), principal| {
740                    dispatch_query_with_params(&engine, &query, &params, principal.as_ref())
741                },
742            )
743            .await
744        }
745        None => {
746            let permit = match tx_gate.acquire_owned().await {
747                Ok(permit) => permit,
748                Err(_) => {
749                    return Message::Error {
750                        message: "query execution error".into(),
751                    }
752                }
753            };
754            let response = run_blocking_query(
755                engine,
756                (query, params),
757                principal,
758                query_timeout,
759                metrics,
760                |engine, (query, params), principal| {
761                    dispatch_query_with_params(&engine, &query, &params, principal.as_ref())
762                },
763            )
764            .await;
765            drop(permit);
766            response
767        }
768    }
769}
770
771async fn run_blocking_query<T, F>(
772    engine: Arc<RwLock<Engine>>,
773    input: T,
774    principal: Option<Principal>,
775    query_timeout: Duration,
776    metrics: &Arc<Metrics>,
777    f: F,
778) -> Message
779where
780    T: Send + 'static,
781    F: FnOnce(Arc<RwLock<Engine>>, T, Option<Principal>) -> Result<QueryResult, QueryError>
782        + Send
783        + 'static,
784{
785    let _in_flight = metrics.in_flight_guard();
786    let start = Instant::now();
787    let handle = tokio::task::spawn_blocking(move || f(engine, input, principal));
788    let abort_handle = handle.abort_handle();
789    let (message, outcome) = match tokio::time::timeout(query_timeout, handle).await {
790        Ok(Ok(Ok(result))) => (query_result_to_message(result), QueryOutcome::Ok),
791        Ok(Ok(Err(e))) => {
792            let outcome = if matches!(e, QueryError::MemoryLimitExceeded { .. }) {
793                QueryOutcome::MemoryLimit
794            } else {
795                QueryOutcome::Error
796            };
797            (
798                Message::Error {
799                    message: sanitize_error(&e.to_string()),
800                },
801                outcome,
802            )
803        }
804        Ok(Err(e)) => (
805            Message::Error {
806                message: format!("internal error: {e}"),
807            },
808            QueryOutcome::Error,
809        ),
810        Err(_) => {
811            abort_handle.abort();
812            (
813                Message::Error {
814                    message: "query timeout exceeded".into(),
815                },
816                QueryOutcome::Timeout,
817            )
818        }
819    };
820    metrics.record_query(start.elapsed(), outcome);
821    message
822}
823
824fn is_success_response(msg: &Message) -> bool {
825    matches!(
826        msg,
827        Message::ResultRows { .. }
828            | Message::ResultScalar { .. }
829            | Message::ResultOk { .. }
830            | Message::ResultMessage { .. }
831    )
832}
833
834fn rollback_open_transaction(engine: Arc<RwLock<Engine>>, principal: Option<Principal>) {
835    let _ = dispatch_query(&engine, "rollback", principal.as_ref());
836}
837
838pub async fn handle_connection<S>(stream: S, opts: ConnOpts<'_>)
839where
840    S: AsyncRead + AsyncWrite + Unpin,
841{
842    let ConnOpts {
843        engine,
844        tx_gate,
845        expected_password,
846        users,
847        shutdown_rx,
848        idle_timeout,
849        query_timeout,
850        rate_limiter,
851        peer_addr,
852        metrics,
853    } = opts;
854
855    let peer = peer_addr
856        .map(|a| a.to_string())
857        .unwrap_or_else(|| "unknown".into());
858    let peer_ip = peer_addr.map(|a| a.ip());
859
860    let (reader, writer) = tokio::io::split(stream);
861    let mut reader = BufReader::new(reader);
862    let mut writer = BufWriter::new(writer);
863
864    // Wait for Connect message (with idle timeout).
865    // Accept Ping messages before authentication so load balancers can
866    // health-check without completing a full CONNECT handshake.
867    // Uses the smaller pre-auth payload limit (4 KB) to prevent memory abuse.
868    let connect_msg = loop {
869        match tokio::time::timeout(idle_timeout, Message::read_from_preauth(&mut reader)).await {
870            Ok(Ok(Some(Message::Ping))) => {
871                debug!(peer = %peer, "pre-auth ping");
872                if !write_msg(&mut writer, &Message::Pong).await {
873                    return;
874                }
875                continue;
876            }
877            Ok(Ok(Some(msg))) => break msg,
878            Ok(Ok(None)) => {
879                debug!(peer = %peer, "client closed before CONNECT");
880                return;
881            }
882            Ok(Err(e)) => {
883                error!(peer = %peer, error = %e, "error reading CONNECT");
884                return;
885            }
886            Err(_) => {
887                warn!(peer = %peer, "idle timeout waiting for CONNECT");
888                return;
889            }
890        }
891    };
892
893    // The authenticated identity for this connection. Bound at connect time
894    // and enforced on every query by `dispatch_query`.
895    let principal: Option<Principal>;
896    match connect_msg {
897        Message::Connect {
898            db_name,
899            password,
900            username,
901        } => {
902            // Check rate limiting before verifying credentials.
903            if let (Some(limiter), Some(ip)) = (rate_limiter, peer_ip) {
904                if is_rate_limited(limiter, ip) {
905                    warn!(peer = %peer, "rate limited: too many auth failures");
906                    let err = Message::Error {
907                        message: "too many auth failures, try again later".into(),
908                    };
909                    write_msg(&mut writer, &err).await;
910                    return;
911                }
912            }
913
914            let outcome = authenticate_connect(
915                &users,
916                expected_password.as_ref().map(|p| p.as_str()),
917                username.as_deref(),
918                password.as_ref().map(|p| p.as_str()),
919            );
920
921            match outcome {
922                AuthOutcome::Rejected => {
923                    warn!(peer = %peer, db = %db_name, "auth rejected");
924                    metrics.inc_auth_failure();
925                    // Record the failure for rate limiting.
926                    if let (Some(limiter), Some(ip)) = (rate_limiter, peer_ip) {
927                        record_auth_failure(limiter, ip);
928                    }
929                    let err = Message::Error {
930                        message: "authentication failed".into(),
931                    };
932                    write_msg(&mut writer, &err).await;
933                    return;
934                }
935                AuthOutcome::Authenticated {
936                    principal: auth_principal,
937                } => {
938                    // Auth succeeded — clear any prior failure count.
939                    if let (Some(limiter), Some(ip)) = (rate_limiter, peer_ip) {
940                        clear_auth_failures(limiter, ip);
941                    }
942                    match &auth_principal {
943                        Some(p) => {
944                            info!(peer = %peer, db = %db_name, user = %p.name, role = %p.role, "authenticated");
945                        }
946                        None => {
947                            info!(peer = %peer, db = %db_name, "client connected");
948                        }
949                    }
950                    principal = auth_principal;
951                }
952            }
953
954            let ok = Message::ConnectOk {
955                version: env!("CARGO_PKG_VERSION").into(),
956            };
957            if !write_msg(&mut writer, &ok).await {
958                return;
959            }
960        }
961        _ => {
962            warn!(peer = %peer, "first message was not CONNECT");
963            let err = Message::Error {
964                message: "expected CONNECT".into(),
965            };
966            write_msg(&mut writer, &err).await;
967            return;
968        }
969    }
970
971    let mut tx_permit: Option<OwnedSemaphorePermit> = None;
972
973    // Main query loop with idle timeout and shutdown awareness.
974    loop {
975        let msg = tokio::select! {
976            // Read next message with idle timeout.
977            result = tokio::time::timeout(idle_timeout, Message::read_from(&mut reader)) => {
978                match result {
979                    Ok(Ok(Some(msg))) => msg,
980                    Ok(Ok(None)) => break,
981                    Ok(Err(e)) => {
982                        error!(peer = %peer, error = %e, "read error");
983                        break;
984                    }
985                    Err(_) => {
986                        info!(peer = %peer, "idle timeout, closing connection");
987                        let err = Message::Error { message: "idle timeout".into() };
988                        write_msg(&mut writer, &err).await;
989                        break;
990                    }
991                }
992            }
993            // If server is shutting down, notify client and close.
994            _ = shutdown_rx.changed() => {
995                if *shutdown_rx.borrow() {
996                    info!(peer = %peer, "server shutting down, closing connection");
997                    let err = Message::Error { message: "server shutting down".into() };
998                    write_msg(&mut writer, &err).await;
999                    break;
1000                }
1001                continue;
1002            }
1003        };
1004
1005        let response = match msg {
1006            Message::Ping => {
1007                debug!(peer = %peer, "ping");
1008                Message::Pong
1009            }
1010            Message::Query { query } => {
1011                if query.len() > MAX_QUERY_LENGTH {
1012                    Message::Error {
1013                        message: format!(
1014                            "query too large: {} bytes (max {})",
1015                            query.len(),
1016                            MAX_QUERY_LENGTH
1017                        ),
1018                    }
1019                } else {
1020                    debug!(peer = %peer, query = %query, "received query");
1021                    let response = execute_wire_query(
1022                        engine.clone(),
1023                        tx_gate.clone(),
1024                        &mut tx_permit,
1025                        query.clone(),
1026                        principal.clone(),
1027                        query_timeout,
1028                        &metrics,
1029                    )
1030                    .await;
1031                    if matches!(&response, Message::Error { message } if message == "query timeout exceeded")
1032                    {
1033                        warn!(peer = %peer, query = %query, "query timeout exceeded");
1034                        if tx_permit.is_some() {
1035                            let engine = engine.clone();
1036                            let principal = principal.clone();
1037                            let _ = tokio::task::spawn_blocking(move || {
1038                                rollback_open_transaction(engine, principal)
1039                            })
1040                            .await;
1041                        }
1042                        tx_permit.take();
1043                    }
1044                    response
1045                }
1046            }
1047            Message::QuerySql { query } => {
1048                if query.len() > MAX_QUERY_LENGTH {
1049                    Message::Error {
1050                        message: format!(
1051                            "query too large: {} bytes (max {})",
1052                            query.len(),
1053                            MAX_QUERY_LENGTH
1054                        ),
1055                    }
1056                } else {
1057                    debug!(peer = %peer, query = %query, "received SQL query");
1058                    let response = execute_wire_query_sql(
1059                        engine.clone(),
1060                        tx_gate.clone(),
1061                        &mut tx_permit,
1062                        query.clone(),
1063                        principal.clone(),
1064                        query_timeout,
1065                        &metrics,
1066                    )
1067                    .await;
1068                    if matches!(&response, Message::Error { message } if message == "query timeout exceeded")
1069                    {
1070                        warn!(peer = %peer, query = %query, "SQL query timeout exceeded");
1071                        if tx_permit.is_some() {
1072                            let engine = engine.clone();
1073                            let principal = principal.clone();
1074                            let _ = tokio::task::spawn_blocking(move || {
1075                                rollback_open_transaction(engine, principal)
1076                            })
1077                            .await;
1078                        }
1079                        tx_permit.take();
1080                    }
1081                    response
1082                }
1083            }
1084            Message::QueryWithParams { query, params } => {
1085                if query.len() > MAX_QUERY_LENGTH {
1086                    Message::Error {
1087                        message: format!(
1088                            "query too large: {} bytes (max {})",
1089                            query.len(),
1090                            MAX_QUERY_LENGTH
1091                        ),
1092                    }
1093                } else {
1094                    debug!(peer = %peer, query = %query, n_params = params.len(), "received parameterized query");
1095                    let response = execute_wire_query_with_params(
1096                        engine.clone(),
1097                        tx_gate.clone(),
1098                        &mut tx_permit,
1099                        query.clone(),
1100                        params.clone(),
1101                        principal.clone(),
1102                        query_timeout,
1103                        &metrics,
1104                    )
1105                    .await;
1106                    if matches!(&response, Message::Error { message } if message == "query timeout exceeded")
1107                    {
1108                        warn!(peer = %peer, query = %query, "query timeout exceeded");
1109                        if tx_permit.is_some() {
1110                            let engine = engine.clone();
1111                            let principal = principal.clone();
1112                            let _ = tokio::task::spawn_blocking(move || {
1113                                rollback_open_transaction(engine, principal)
1114                            })
1115                            .await;
1116                        }
1117                        tx_permit.take();
1118                    }
1119                    response
1120                }
1121            }
1122            Message::Disconnect => {
1123                debug!(peer = %peer, "received DISCONNECT");
1124                break;
1125            }
1126            _ => Message::Error {
1127                message: "unexpected message type".into(),
1128            },
1129        };
1130
1131        if !write_msg(&mut writer, &response).await {
1132            break;
1133        }
1134    }
1135
1136    // Roll back any open transaction the client left behind on disconnect.
1137    // The permit must stay alive in `tx_permit` for the duration of the awaited
1138    // rollback and be released only afterwards — mirroring the query-timeout
1139    // path above. Using `tx_permit.take().is_some()` here would drop the permit
1140    // (freeing the TxGate) *before* the rollback runs, letting another
1141    // connection BEGIN a transaction that this stale rollback would then clobber.
1142    if tx_permit.is_some() {
1143        let engine = engine.clone();
1144        let principal = principal.clone();
1145        let _ =
1146            tokio::task::spawn_blocking(move || rollback_open_transaction(engine, principal)).await;
1147    }
1148    tx_permit.take();
1149
1150    info!(peer = %peer, "client disconnected");
1151}
1152
1153fn query_result_to_message(result: QueryResult) -> Message {
1154    match result {
1155        QueryResult::Rows { columns, rows } => {
1156            let str_rows: Vec<Vec<String>> = rows
1157                .iter()
1158                .map(|row| row.iter().map(value_to_display).collect())
1159                .collect();
1160            Message::ResultRows {
1161                columns,
1162                rows: str_rows,
1163            }
1164        }
1165        QueryResult::Scalar(val) => Message::ResultScalar {
1166            value: value_to_display(&val),
1167        },
1168        QueryResult::Modified(n) => Message::ResultOk { affected: n },
1169        QueryResult::Created(name) => Message::ResultMessage {
1170            message: format!("type {name} created"),
1171        },
1172        QueryResult::Executed { message } => Message::ResultMessage { message },
1173    }
1174}
1175
1176fn value_to_display(v: &Value) -> String {
1177    match v {
1178        Value::Int(n)      => n.to_string(),
1179        Value::Float(n)    => format!("{n}"),
1180        Value::Bool(b)     => b.to_string(),
1181        Value::Str(s)      => s.clone(),
1182        Value::DateTime(t) => format!("{t}"),
1183        Value::Uuid(u)     => format!("{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
1184            u[0], u[1], u[2], u[3], u[4], u[5], u[6], u[7],
1185            u[8], u[9], u[10], u[11], u[12], u[13], u[14], u[15]),
1186        Value::Bytes(b)    => format!("<{} bytes>", b.len()),
1187        // NULL is serialized as the bareword "null" on the wire. This is the
1188        // sentinel the TypeScript client's typed-row decoder already
1189        // documents and matches (`coerceValue` treats the exact token
1190        // "null" as NULL for non-str columns); the previous "{}" rendering
1191        // was a bug that neither the TS client nor the CLI recognized.
1192        Value::Empty       => "null".into(),
1193    }
1194}
1195
1196#[cfg(test)]
1197mod tests {
1198    use super::*;
1199
1200    // ---- Wire NULL rendering (Fix: remote protocol rendered NULL as `{}`) ----
1201
1202    #[test]
1203    fn null_serializes_as_null_bareword_on_wire() {
1204        assert_eq!(value_to_display(&Value::Empty), "null");
1205    }
1206
1207    // ---- Error sanitization allowlist ----
1208
1209    #[test]
1210    fn unique_violation_error_surfaces_to_remote_clients() {
1211        // The storage layer reports the actionable message; the server must
1212        // not replace it with the generic "query execution error".
1213        assert_eq!(
1214            sanitize_error("unique constraint violation on User.email"),
1215            "unique constraint violation on User.email"
1216        );
1217    }
1218
1219    #[test]
1220    fn internal_errors_stay_generic() {
1221        assert_eq!(
1222            sanitize_error("some internal io panic detail"),
1223            "query execution error"
1224        );
1225    }
1226
1227    #[test]
1228    fn resource_limit_errors_surface_actionable_hints() {
1229        // These carry user-actionable guidance and leak no internal state, so
1230        // they must reach the client verbatim — not be masked to the generic
1231        // message. The exact strings come from QueryError's Display impl
1232        // (crates/query/src/result.rs).
1233        for msg in [
1234            "sort input exceeds row limit — add a LIMIT clause",
1235            "join result exceeds row limit",
1236            "query exceeded memory budget: requested 100 bytes, limit 50 bytes",
1237        ] {
1238            assert_eq!(sanitize_error(msg), msg, "should pass through verbatim");
1239        }
1240    }
1241
1242    // ---- Role enforcement (Fix: readonly role was not enforced) ----
1243
1244    fn parsed(q: &str) -> powdb_query::ast::Statement {
1245        parser::parse(q).unwrap()
1246    }
1247
1248    fn principal(role: &str) -> Option<Principal> {
1249        Some(Principal {
1250            name: "u".into(),
1251            role: role.into(),
1252        })
1253    }
1254
1255    #[test]
1256    fn readonly_can_read_but_not_write() {
1257        let p = principal("readonly");
1258        // Reads pass.
1259        assert!(check_statement_permitted(p.as_ref(), &parsed("User")).is_ok());
1260        assert!(check_statement_permitted(p.as_ref(), &parsed("count(User)")).is_ok());
1261        assert!(check_statement_permitted(p.as_ref(), &parsed("explain User")).is_ok());
1262        // Writes, DDL, and transaction control are denied.
1263        for q in [
1264            r#"insert User { name := "x" }"#,
1265            "User filter .id = 1 update { age := 2 }",
1266            "User filter .id = 1 delete",
1267            "drop User",
1268            "alter User add column c: str",
1269            "type T { required id: int }",
1270            "begin",
1271            "commit",
1272            "rollback",
1273        ] {
1274            let err = check_statement_permitted(p.as_ref(), &parsed(q))
1275                .expect_err(&format!("must deny: {q}"));
1276            assert!(
1277                err.to_string().contains("permission denied"),
1278                "unexpected error for {q}: {err}"
1279            );
1280        }
1281    }
1282
1283    #[test]
1284    fn readwrite_and_admin_have_full_query_access() {
1285        for role in ["readwrite", "admin"] {
1286            let p = principal(role);
1287            assert!(check_statement_permitted(p.as_ref(), &parsed("User")).is_ok());
1288            assert!(check_statement_permitted(
1289                p.as_ref(),
1290                &parsed(r#"insert User { name := "x" }"#)
1291            )
1292            .is_ok());
1293            assert!(check_statement_permitted(p.as_ref(), &parsed("drop User")).is_ok());
1294        }
1295    }
1296
1297    #[test]
1298    fn unknown_role_fails_closed_for_writes() {
1299        let p = principal("mystery");
1300        assert!(check_statement_permitted(p.as_ref(), &parsed("User")).is_ok());
1301        assert!(
1302            check_statement_permitted(p.as_ref(), &parsed(r#"insert User { name := "x" }"#))
1303                .is_err()
1304        );
1305    }
1306
1307    #[test]
1308    fn no_principal_means_full_access() {
1309        // Shared-password / open mode: no per-user identity, no restriction.
1310        assert!(check_statement_permitted(None, &parsed("drop User")).is_ok());
1311        assert!(check_statement_permitted(None, &parsed(r#"insert User { name := "x" }"#)).is_ok());
1312    }
1313
1314    fn store_with_alice() -> UserStore {
1315        let mut s = UserStore::new();
1316        s.create_user("alice", "pw", "readwrite").unwrap();
1317        s
1318    }
1319
1320    // ---- Empty store: legacy shared-password fallback ----
1321
1322    #[test]
1323    fn empty_store_no_password_is_open() {
1324        let s = UserStore::new();
1325        assert_eq!(
1326            authenticate_connect(&s, None, None, None),
1327            AuthOutcome::Authenticated { principal: None }
1328        );
1329        // Even a stray username/password is accepted (legacy open behavior).
1330        assert_eq!(
1331            authenticate_connect(&s, None, Some("x"), Some("y")),
1332            AuthOutcome::Authenticated { principal: None }
1333        );
1334    }
1335
1336    #[test]
1337    fn empty_store_correct_shared_password_succeeds() {
1338        let s = UserStore::new();
1339        assert_eq!(
1340            authenticate_connect(&s, Some("pw"), None, Some("pw")),
1341            AuthOutcome::Authenticated { principal: None }
1342        );
1343    }
1344
1345    #[test]
1346    fn empty_store_wrong_shared_password_rejected() {
1347        let s = UserStore::new();
1348        assert_eq!(
1349            authenticate_connect(&s, Some("pw"), None, Some("bad")),
1350            AuthOutcome::Rejected
1351        );
1352    }
1353
1354    #[test]
1355    fn empty_store_missing_password_rejected_when_expected() {
1356        let s = UserStore::new();
1357        assert_eq!(
1358            authenticate_connect(&s, Some("pw"), None, None),
1359            AuthOutcome::Rejected
1360        );
1361    }
1362
1363    #[test]
1364    fn empty_store_ignores_username_for_shared_password() {
1365        // A new client may send a username even against a shared-password
1366        // server; the username is ignored and the password still governs.
1367        let s = UserStore::new();
1368        assert_eq!(
1369            authenticate_connect(&s, Some("pw"), Some("whoever"), Some("pw")),
1370            AuthOutcome::Authenticated { principal: None }
1371        );
1372    }
1373
1374    // ---- Populated store: multi-user auth ----
1375
1376    #[test]
1377    fn user_auth_success_binds_principal() {
1378        let s = store_with_alice();
1379        assert_eq!(
1380            authenticate_connect(&s, None, Some("alice"), Some("pw")),
1381            AuthOutcome::Authenticated {
1382                principal: Some(Principal {
1383                    name: "alice".into(),
1384                    role: "readwrite".into(),
1385                })
1386            }
1387        );
1388    }
1389
1390    #[test]
1391    fn user_auth_wrong_password_rejected() {
1392        let s = store_with_alice();
1393        assert_eq!(
1394            authenticate_connect(&s, None, Some("alice"), Some("bad")),
1395            AuthOutcome::Rejected
1396        );
1397    }
1398
1399    #[test]
1400    fn user_auth_unknown_user_rejected() {
1401        let s = store_with_alice();
1402        assert_eq!(
1403            authenticate_connect(&s, None, Some("mallory"), Some("pw")),
1404            AuthOutcome::Rejected
1405        );
1406    }
1407
1408    #[test]
1409    fn user_auth_missing_username_rejected() {
1410        let s = store_with_alice();
1411        assert_eq!(
1412            authenticate_connect(&s, None, None, Some("pw")),
1413            AuthOutcome::Rejected
1414        );
1415    }
1416
1417    #[test]
1418    fn user_auth_missing_password_rejected() {
1419        let s = store_with_alice();
1420        assert_eq!(
1421            authenticate_connect(&s, Some("pw"), Some("alice"), None),
1422            AuthOutcome::Rejected
1423        );
1424    }
1425
1426    #[test]
1427    fn user_auth_ignores_shared_password_when_users_present() {
1428        // With users present, the shared password is irrelevant: supplying it as
1429        // the password without a valid user must NOT authenticate.
1430        let s = store_with_alice();
1431        assert_eq!(
1432            authenticate_connect(&s, Some("shared"), None, Some("shared")),
1433            AuthOutcome::Rejected
1434        );
1435    }
1436}