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