1use crate::protocol::{Message, WireParam};
2use powdb_auth::{Permission, Role, UserStore};
3use powdb_query::executor::{is_read_only_statement, Engine};
4use powdb_query::parser;
5use powdb_query::result::{QueryError, QueryResult};
6use powdb_query::sql;
7use powdb_storage::types::Value;
8use std::collections::HashMap;
9use std::net::IpAddr;
10use std::sync::{Arc, Mutex, RwLock};
11use std::time::{Duration, Instant};
12use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, BufWriter};
13use tokio::sync::{watch, OwnedSemaphorePermit, Semaphore};
14use tracing::{debug, error, info, warn};
15use zeroize::Zeroizing;
16
17pub type AuthRateLimiter = Arc<Mutex<HashMap<IpAddr, (u32, Instant)>>>;
19
20pub type TxGate = Arc<Semaphore>;
25
26pub fn new_tx_gate() -> TxGate {
28 Arc::new(Semaphore::new(1))
29}
30
31const MAX_QUERY_LENGTH: usize = 1024 * 1024;
33
34const WRITE_TIMEOUT: Duration = Duration::from_secs(30);
37
38const MAX_AUTH_FAILURES: u32 = 5;
40
41const AUTH_FAILURE_WINDOW: Duration = Duration::from_secs(60);
43
44pub fn new_rate_limiter() -> AuthRateLimiter {
46 Arc::new(Mutex::new(HashMap::new()))
47}
48
49fn is_rate_limited(limiter: &AuthRateLimiter, ip: IpAddr) -> bool {
52 let mut map = limiter.lock().unwrap_or_else(|e| e.into_inner());
53 let now = Instant::now();
55 map.retain(|_, (_, ts)| now.duration_since(*ts) < AUTH_FAILURE_WINDOW);
56
57 if let Some((count, _)) = map.get(&ip) {
58 *count >= MAX_AUTH_FAILURES
59 } else {
60 false
61 }
62}
63
64fn record_auth_failure(limiter: &AuthRateLimiter, ip: IpAddr) {
66 let mut map = limiter.lock().unwrap_or_else(|e| e.into_inner());
67 let now = Instant::now();
68 let entry = map.entry(ip).or_insert((0, now));
69 if now.duration_since(entry.1) >= AUTH_FAILURE_WINDOW {
71 *entry = (1, now);
72 } else {
73 entry.0 += 1;
74 }
75}
76
77fn clear_auth_failures(limiter: &AuthRateLimiter, ip: IpAddr) {
79 let mut map = limiter.lock().unwrap_or_else(|e| e.into_inner());
80 map.remove(&ip);
81}
82
83fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
86 use sha2::{Digest, Sha256};
87 let ha = Sha256::digest(a);
88 let hb = Sha256::digest(b);
89 let mut diff = 0u8;
90 for (x, y) in ha.iter().zip(hb.iter()) {
91 diff |= x ^ y;
92 }
93 diff == 0
94}
95
96#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct Principal {
101 pub name: String,
102 pub role: String,
103}
104
105fn is_ddl_statement(stmt: &powdb_query::ast::Statement) -> bool {
111 use powdb_query::ast::Statement;
112 let inner = match stmt {
113 Statement::Explain(inner) => inner.as_ref(),
114 other => other,
115 };
116 matches!(
117 inner,
118 Statement::CreateType(_)
119 | Statement::AlterTable(_)
120 | Statement::DropTable(_)
121 | Statement::CreateView(_)
122 | Statement::DropView(_)
123 )
124}
125
126fn required_permission(stmt: &powdb_query::ast::Statement) -> Permission {
132 if is_read_only_statement(stmt) {
133 Permission::Read
134 } else if is_ddl_statement(stmt) {
135 Permission::Ddl
136 } else {
137 Permission::Write
138 }
139}
140
141fn check_statement_permitted(
153 principal: Option<&Principal>,
154 stmt: &powdb_query::ast::Statement,
155) -> Result<(), QueryError> {
156 let Some(p) = principal else {
157 return Ok(());
160 };
161 if is_read_only_statement(stmt) {
164 return Ok(());
165 }
166 let needed = required_permission(stmt);
167 if Role::builtin(&p.role).is_some_and(|r| r.allows(needed)) {
168 return Ok(());
169 }
170 let kind = if needed == Permission::Ddl {
171 "schema-definition"
172 } else {
173 "write"
174 };
175 Err(QueryError::Execution(format!(
176 "permission denied: role '{}' cannot execute {kind} statements",
177 p.role
178 )))
179}
180
181#[derive(Debug, Clone, PartialEq, Eq)]
183pub enum AuthOutcome {
184 Authenticated { principal: Option<Principal> },
188 Rejected,
191}
192
193pub fn authenticate_connect(
205 users: &UserStore,
206 expected_password: Option<&str>,
207 username: Option<&str>,
208 password: Option<&str>,
209) -> AuthOutcome {
210 if !users.is_empty() {
211 let Some(name) = username else {
213 return AuthOutcome::Rejected;
214 };
215 let Some(candidate) = password else {
216 return AuthOutcome::Rejected;
217 };
218 match users.authenticate(name, candidate) {
219 Some(user) => AuthOutcome::Authenticated {
220 principal: Some(Principal {
221 name: user.name.clone(),
222 role: user.role.clone(),
223 }),
224 },
225 None => AuthOutcome::Rejected,
226 }
227 } else {
228 match expected_password {
230 Some(expected) => {
231 if password.is_some_and(|p| constant_time_eq(p.as_bytes(), expected.as_bytes())) {
232 AuthOutcome::Authenticated { principal: None }
233 } else {
234 AuthOutcome::Rejected
235 }
236 }
237 None => AuthOutcome::Authenticated { principal: None },
238 }
239 }
240}
241
242const SAFE_ERROR_PREFIXES: &[&str] = &[
244 "table not found",
245 "column not found",
246 "parse error",
247 "type mismatch",
248 "unknown table",
249 "unknown column",
250 "unknown function",
251 "syntax error",
252 "expected",
253 "unexpected",
254 "missing",
255 "duplicate",
256 "invalid",
257 "cannot",
258 "no such",
259 "already exists",
260 "permission denied",
261 "row too large",
262 "unique constraint violation",
263 "sort input exceeds",
268 "join result exceeds",
269 "query exceeded memory budget",
270];
271
272fn sanitize_error(e: &str) -> String {
276 let lower = e.to_lowercase();
277 for prefix in SAFE_ERROR_PREFIXES {
278 if lower.starts_with(prefix) {
279 return e.to_string();
280 }
281 }
282 "query execution error".into()
283}
284
285async fn write_msg<W: AsyncWrite + Unpin>(writer: &mut BufWriter<W>, msg: &Message) -> bool {
288 let write_fut = async {
289 if msg.write_to(writer).await.is_err() {
290 return false;
291 }
292 writer.flush().await.is_ok()
293 };
294 tokio::time::timeout(WRITE_TIMEOUT, write_fut)
295 .await
296 .unwrap_or_default()
297}
298
299pub struct ConnOpts<'a> {
302 pub engine: Arc<RwLock<Engine>>,
303 pub tx_gate: TxGate,
304 pub expected_password: Option<Zeroizing<String>>,
307 pub users: Arc<UserStore>,
311 pub shutdown_rx: &'a mut watch::Receiver<bool>,
312 pub idle_timeout: Duration,
313 pub query_timeout: Duration,
314 pub rate_limiter: Option<&'a AuthRateLimiter>,
315 pub peer_addr: Option<std::net::SocketAddr>,
316}
317
318fn dispatch_query(
327 engine: &Arc<RwLock<Engine>>,
328 query: &str,
329 principal: Option<&Principal>,
330) -> Result<QueryResult, QueryError> {
331 let stmt_result = parser::parse(query).map_err(|e| e.to_string());
332
333 if let Ok(stmt) = &stmt_result {
337 check_statement_permitted(principal, stmt)?;
338 }
339
340 let can_try_read = matches!(&stmt_result, Ok(s) if is_read_only_statement(s));
341 if can_try_read {
342 let res = {
343 let eng = engine
344 .read()
345 .map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
346 eng.execute_powql_readonly(query)
347 };
348 match res {
349 Ok(r) => return Ok(r),
350 Err(QueryError::ReadonlyNeedsWrite) => {
351 }
353 Err(e) => return Err(e),
354 }
355 }
356
357 let mut eng = engine
358 .write()
359 .map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
360 eng.execute_powql(query)
361}
362
363fn dispatch_sql_query(
364 engine: &Arc<RwLock<Engine>>,
365 query: &str,
366 principal: Option<&Principal>,
367) -> Result<QueryResult, QueryError> {
368 let stmt_result = sql::parse_sql(query).map_err(|e| e.to_string());
369
370 if let Ok(stmt) = &stmt_result {
371 check_statement_permitted(principal, stmt)?;
372 }
373
374 let can_try_read = matches!(&stmt_result, Ok(s) if is_read_only_statement(s));
375 if can_try_read {
376 let res = {
377 let eng = engine
378 .read()
379 .map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
380 eng.execute_sql_readonly(query)
381 };
382 match res {
383 Ok(r) => return Ok(r),
384 Err(QueryError::ReadonlyNeedsWrite) => {}
385 Err(e) => return Err(e),
386 }
387 }
388
389 let mut eng = engine
390 .write()
391 .map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
392 eng.execute_sql(query)
393}
394
395fn wire_param_to_value(p: &WireParam) -> powdb_query::ast::ParamValue {
398 use powdb_query::ast::ParamValue;
399 match p {
400 WireParam::Null => ParamValue::Null,
401 WireParam::Int(v) => ParamValue::Int(*v),
402 WireParam::Float(v) => ParamValue::Float(*v),
403 WireParam::Bool(v) => ParamValue::Bool(*v),
404 WireParam::Str(s) => ParamValue::Str(s.clone()),
405 }
406}
407
408#[derive(Debug, Clone, Copy, PartialEq, Eq)]
414enum TransactionControl {
415 Begin,
416 Commit,
417 Rollback,
418}
419
420fn transaction_control(stmt: &powdb_query::ast::Statement) -> Option<TransactionControl> {
421 use powdb_query::ast::Statement;
422 match stmt {
423 Statement::Begin => Some(TransactionControl::Begin),
424 Statement::Commit => Some(TransactionControl::Commit),
425 Statement::Rollback => Some(TransactionControl::Rollback),
426 _ => None,
427 }
428}
429
430fn classify_query_transaction_control(query: &str) -> Option<TransactionControl> {
431 parser::parse(query)
432 .ok()
433 .and_then(|stmt| transaction_control(&stmt))
434}
435
436fn classify_sql_transaction_control(query: &str) -> Option<TransactionControl> {
437 sql::parse_sql(query)
438 .ok()
439 .and_then(|stmt| transaction_control(&stmt))
440}
441
442fn classify_params_transaction_control(
443 query: &str,
444 params: &[WireParam],
445) -> Option<TransactionControl> {
446 let bound: Vec<powdb_query::ast::ParamValue> = params.iter().map(wire_param_to_value).collect();
447 parser::parse_with_params(query, &bound)
448 .ok()
449 .and_then(|stmt| transaction_control(&stmt))
450}
451
452fn dispatch_query_with_params(
453 engine: &Arc<RwLock<Engine>>,
454 query: &str,
455 params: &[WireParam],
456 principal: Option<&Principal>,
457) -> Result<QueryResult, QueryError> {
458 let bound: Vec<powdb_query::ast::ParamValue> = params.iter().map(wire_param_to_value).collect();
459
460 let stmt_result = parser::parse_with_params(query, &bound).map_err(|e| e.to_string());
463
464 if let Ok(stmt) = &stmt_result {
465 check_statement_permitted(principal, stmt)?;
466 }
467
468 let can_try_read = matches!(&stmt_result, Ok(s) if is_read_only_statement(s));
469 if can_try_read {
470 let res = {
471 let eng = engine
472 .read()
473 .map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
474 eng.execute_powql_readonly_with_params(query, &bound)
475 };
476 match res {
477 Ok(r) => return Ok(r),
478 Err(QueryError::ReadonlyNeedsWrite) => {
479 }
481 Err(e) => return Err(e),
482 }
483 }
484
485 let mut eng = engine
486 .write()
487 .map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
488 eng.execute_powql_with_params(query, &bound)
489}
490
491async fn execute_wire_query(
492 engine: Arc<RwLock<Engine>>,
493 tx_gate: TxGate,
494 tx_permit: &mut Option<OwnedSemaphorePermit>,
495 query: String,
496 principal: Option<Principal>,
497 query_timeout: Duration,
498) -> Message {
499 match classify_query_transaction_control(&query) {
500 Some(TransactionControl::Begin) => {
501 if tx_permit.is_some() {
502 return Message::Error {
503 message: sanitize_error("transaction already active"),
504 };
505 }
506 let permit = match tx_gate.acquire_owned().await {
507 Ok(permit) => permit,
508 Err(_) => {
509 return Message::Error {
510 message: "query execution error".into(),
511 }
512 }
513 };
514 let response = run_blocking_query(
515 engine,
516 query,
517 principal,
518 query_timeout,
519 |engine, query, principal| dispatch_query(&engine, &query, principal.as_ref()),
520 )
521 .await;
522 if is_success_response(&response) {
523 *tx_permit = Some(permit);
524 }
525 response
526 }
527 Some(TransactionControl::Commit | TransactionControl::Rollback) => {
528 let response = run_blocking_query(
529 engine,
530 query,
531 principal,
532 query_timeout,
533 |engine, query, principal| dispatch_query(&engine, &query, principal.as_ref()),
534 )
535 .await;
536 if is_success_response(&response) {
537 tx_permit.take();
538 }
539 response
540 }
541 None if tx_permit.is_some() => {
542 run_blocking_query(
543 engine,
544 query,
545 principal,
546 query_timeout,
547 |engine, query, principal| dispatch_query(&engine, &query, principal.as_ref()),
548 )
549 .await
550 }
551 None => {
552 let permit = match tx_gate.acquire_owned().await {
553 Ok(permit) => permit,
554 Err(_) => {
555 return Message::Error {
556 message: "query execution error".into(),
557 }
558 }
559 };
560 let response = run_blocking_query(
561 engine,
562 query,
563 principal,
564 query_timeout,
565 |engine, query, principal| dispatch_query(&engine, &query, principal.as_ref()),
566 )
567 .await;
568 drop(permit);
569 response
570 }
571 }
572}
573
574async fn execute_wire_query_sql(
575 engine: Arc<RwLock<Engine>>,
576 tx_gate: TxGate,
577 tx_permit: &mut Option<OwnedSemaphorePermit>,
578 query: String,
579 principal: Option<Principal>,
580 query_timeout: Duration,
581) -> Message {
582 match classify_sql_transaction_control(&query) {
583 Some(TransactionControl::Begin) => {
584 if tx_permit.is_some() {
585 return Message::Error {
586 message: sanitize_error("transaction already active"),
587 };
588 }
589 let permit = match tx_gate.acquire_owned().await {
590 Ok(permit) => permit,
591 Err(_) => {
592 return Message::Error {
593 message: "query execution error".into(),
594 }
595 }
596 };
597 let response = run_blocking_query(
598 engine,
599 query,
600 principal,
601 query_timeout,
602 |engine, query, principal| dispatch_sql_query(&engine, &query, principal.as_ref()),
603 )
604 .await;
605 if is_success_response(&response) {
606 *tx_permit = Some(permit);
607 }
608 response
609 }
610 Some(TransactionControl::Commit | TransactionControl::Rollback) => {
611 let response = run_blocking_query(
612 engine,
613 query,
614 principal,
615 query_timeout,
616 |engine, query, principal| dispatch_sql_query(&engine, &query, principal.as_ref()),
617 )
618 .await;
619 if is_success_response(&response) {
620 tx_permit.take();
621 }
622 response
623 }
624 None if tx_permit.is_some() => {
625 run_blocking_query(
626 engine,
627 query,
628 principal,
629 query_timeout,
630 |engine, query, principal| dispatch_sql_query(&engine, &query, principal.as_ref()),
631 )
632 .await
633 }
634 None => {
635 let permit = match tx_gate.acquire_owned().await {
636 Ok(permit) => permit,
637 Err(_) => {
638 return Message::Error {
639 message: "query execution error".into(),
640 }
641 }
642 };
643 let response = run_blocking_query(
644 engine,
645 query,
646 principal,
647 query_timeout,
648 |engine, query, principal| dispatch_sql_query(&engine, &query, principal.as_ref()),
649 )
650 .await;
651 drop(permit);
652 response
653 }
654 }
655}
656
657async fn execute_wire_query_with_params(
658 engine: Arc<RwLock<Engine>>,
659 tx_gate: TxGate,
660 tx_permit: &mut Option<OwnedSemaphorePermit>,
661 query: String,
662 params: Vec<WireParam>,
663 principal: Option<Principal>,
664 query_timeout: Duration,
665) -> Message {
666 match classify_params_transaction_control(&query, ¶ms) {
667 Some(TransactionControl::Begin) => {
668 if tx_permit.is_some() {
669 return Message::Error {
670 message: sanitize_error("transaction already active"),
671 };
672 }
673 let permit = match tx_gate.acquire_owned().await {
674 Ok(permit) => permit,
675 Err(_) => {
676 return Message::Error {
677 message: "query execution error".into(),
678 }
679 }
680 };
681 let response = run_blocking_query(
682 engine,
683 (query, params),
684 principal,
685 query_timeout,
686 |engine, (query, params), principal| {
687 dispatch_query_with_params(&engine, &query, ¶ms, principal.as_ref())
688 },
689 )
690 .await;
691 if is_success_response(&response) {
692 *tx_permit = Some(permit);
693 }
694 response
695 }
696 Some(TransactionControl::Commit | TransactionControl::Rollback) => {
697 let response = run_blocking_query(
698 engine,
699 (query, params),
700 principal,
701 query_timeout,
702 |engine, (query, params), principal| {
703 dispatch_query_with_params(&engine, &query, ¶ms, principal.as_ref())
704 },
705 )
706 .await;
707 if is_success_response(&response) {
708 tx_permit.take();
709 }
710 response
711 }
712 None if tx_permit.is_some() => {
713 run_blocking_query(
714 engine,
715 (query, params),
716 principal,
717 query_timeout,
718 |engine, (query, params), principal| {
719 dispatch_query_with_params(&engine, &query, ¶ms, principal.as_ref())
720 },
721 )
722 .await
723 }
724 None => {
725 let permit = match tx_gate.acquire_owned().await {
726 Ok(permit) => permit,
727 Err(_) => {
728 return Message::Error {
729 message: "query execution error".into(),
730 }
731 }
732 };
733 let response = run_blocking_query(
734 engine,
735 (query, params),
736 principal,
737 query_timeout,
738 |engine, (query, params), principal| {
739 dispatch_query_with_params(&engine, &query, ¶ms, principal.as_ref())
740 },
741 )
742 .await;
743 drop(permit);
744 response
745 }
746 }
747}
748
749async fn run_blocking_query<T, F>(
750 engine: Arc<RwLock<Engine>>,
751 input: T,
752 principal: Option<Principal>,
753 query_timeout: Duration,
754 f: F,
755) -> Message
756where
757 T: Send + 'static,
758 F: FnOnce(Arc<RwLock<Engine>>, T, Option<Principal>) -> Result<QueryResult, QueryError>
759 + Send
760 + 'static,
761{
762 let handle = tokio::task::spawn_blocking(move || f(engine, input, principal));
763 let abort_handle = handle.abort_handle();
764 match tokio::time::timeout(query_timeout, handle).await {
765 Ok(Ok(Ok(result))) => query_result_to_message(result),
766 Ok(Ok(Err(e))) => Message::Error {
767 message: sanitize_error(&e.to_string()),
768 },
769 Ok(Err(e)) => Message::Error {
770 message: format!("internal error: {e}"),
771 },
772 Err(_) => {
773 abort_handle.abort();
774 Message::Error {
775 message: "query timeout exceeded".into(),
776 }
777 }
778 }
779}
780
781fn is_success_response(msg: &Message) -> bool {
782 matches!(
783 msg,
784 Message::ResultRows { .. }
785 | Message::ResultScalar { .. }
786 | Message::ResultOk { .. }
787 | Message::ResultMessage { .. }
788 )
789}
790
791fn rollback_open_transaction(engine: Arc<RwLock<Engine>>, principal: Option<Principal>) {
792 let _ = dispatch_query(&engine, "rollback", principal.as_ref());
793}
794
795pub async fn handle_connection<S>(stream: S, opts: ConnOpts<'_>)
796where
797 S: AsyncRead + AsyncWrite + Unpin,
798{
799 let ConnOpts {
800 engine,
801 tx_gate,
802 expected_password,
803 users,
804 shutdown_rx,
805 idle_timeout,
806 query_timeout,
807 rate_limiter,
808 peer_addr,
809 } = opts;
810
811 let peer = peer_addr
812 .map(|a| a.to_string())
813 .unwrap_or_else(|| "unknown".into());
814 let peer_ip = peer_addr.map(|a| a.ip());
815
816 let (reader, writer) = tokio::io::split(stream);
817 let mut reader = BufReader::new(reader);
818 let mut writer = BufWriter::new(writer);
819
820 let connect_msg = loop {
825 match tokio::time::timeout(idle_timeout, Message::read_from_preauth(&mut reader)).await {
826 Ok(Ok(Some(Message::Ping))) => {
827 debug!(peer = %peer, "pre-auth ping");
828 if !write_msg(&mut writer, &Message::Pong).await {
829 return;
830 }
831 continue;
832 }
833 Ok(Ok(Some(msg))) => break msg,
834 Ok(Ok(None)) => {
835 debug!(peer = %peer, "client closed before CONNECT");
836 return;
837 }
838 Ok(Err(e)) => {
839 error!(peer = %peer, error = %e, "error reading CONNECT");
840 return;
841 }
842 Err(_) => {
843 warn!(peer = %peer, "idle timeout waiting for CONNECT");
844 return;
845 }
846 }
847 };
848
849 let principal: Option<Principal>;
852 match connect_msg {
853 Message::Connect {
854 db_name,
855 password,
856 username,
857 } => {
858 if let (Some(limiter), Some(ip)) = (rate_limiter, peer_ip) {
860 if is_rate_limited(limiter, ip) {
861 warn!(peer = %peer, "rate limited: too many auth failures");
862 let err = Message::Error {
863 message: "too many auth failures, try again later".into(),
864 };
865 write_msg(&mut writer, &err).await;
866 return;
867 }
868 }
869
870 let outcome = authenticate_connect(
871 &users,
872 expected_password.as_ref().map(|p| p.as_str()),
873 username.as_deref(),
874 password.as_ref().map(|p| p.as_str()),
875 );
876
877 match outcome {
878 AuthOutcome::Rejected => {
879 warn!(peer = %peer, db = %db_name, "auth rejected");
880 if let (Some(limiter), Some(ip)) = (rate_limiter, peer_ip) {
882 record_auth_failure(limiter, ip);
883 }
884 let err = Message::Error {
885 message: "authentication failed".into(),
886 };
887 write_msg(&mut writer, &err).await;
888 return;
889 }
890 AuthOutcome::Authenticated {
891 principal: auth_principal,
892 } => {
893 if let (Some(limiter), Some(ip)) = (rate_limiter, peer_ip) {
895 clear_auth_failures(limiter, ip);
896 }
897 match &auth_principal {
898 Some(p) => {
899 info!(peer = %peer, db = %db_name, user = %p.name, role = %p.role, "authenticated");
900 }
901 None => {
902 info!(peer = %peer, db = %db_name, "client connected");
903 }
904 }
905 principal = auth_principal;
906 }
907 }
908
909 let ok = Message::ConnectOk {
910 version: env!("CARGO_PKG_VERSION").into(),
911 };
912 if !write_msg(&mut writer, &ok).await {
913 return;
914 }
915 }
916 _ => {
917 warn!(peer = %peer, "first message was not CONNECT");
918 let err = Message::Error {
919 message: "expected CONNECT".into(),
920 };
921 write_msg(&mut writer, &err).await;
922 return;
923 }
924 }
925
926 let mut tx_permit: Option<OwnedSemaphorePermit> = None;
927
928 loop {
930 let msg = tokio::select! {
931 result = tokio::time::timeout(idle_timeout, Message::read_from(&mut reader)) => {
933 match result {
934 Ok(Ok(Some(msg))) => msg,
935 Ok(Ok(None)) => break,
936 Ok(Err(e)) => {
937 error!(peer = %peer, error = %e, "read error");
938 break;
939 }
940 Err(_) => {
941 info!(peer = %peer, "idle timeout, closing connection");
942 let err = Message::Error { message: "idle timeout".into() };
943 write_msg(&mut writer, &err).await;
944 break;
945 }
946 }
947 }
948 _ = shutdown_rx.changed() => {
950 if *shutdown_rx.borrow() {
951 info!(peer = %peer, "server shutting down, closing connection");
952 let err = Message::Error { message: "server shutting down".into() };
953 write_msg(&mut writer, &err).await;
954 break;
955 }
956 continue;
957 }
958 };
959
960 let response = match msg {
961 Message::Ping => {
962 debug!(peer = %peer, "ping");
963 Message::Pong
964 }
965 Message::Query { query } => {
966 if query.len() > MAX_QUERY_LENGTH {
967 Message::Error {
968 message: format!(
969 "query too large: {} bytes (max {})",
970 query.len(),
971 MAX_QUERY_LENGTH
972 ),
973 }
974 } else {
975 debug!(peer = %peer, query = %query, "received query");
976 let response = execute_wire_query(
977 engine.clone(),
978 tx_gate.clone(),
979 &mut tx_permit,
980 query.clone(),
981 principal.clone(),
982 query_timeout,
983 )
984 .await;
985 if matches!(&response, Message::Error { message } if message == "query timeout exceeded")
986 {
987 warn!(peer = %peer, query = %query, "query timeout exceeded");
988 if tx_permit.is_some() {
989 let engine = engine.clone();
990 let principal = principal.clone();
991 let _ = tokio::task::spawn_blocking(move || {
992 rollback_open_transaction(engine, principal)
993 })
994 .await;
995 }
996 tx_permit.take();
997 }
998 response
999 }
1000 }
1001 Message::QuerySql { query } => {
1002 if query.len() > MAX_QUERY_LENGTH {
1003 Message::Error {
1004 message: format!(
1005 "query too large: {} bytes (max {})",
1006 query.len(),
1007 MAX_QUERY_LENGTH
1008 ),
1009 }
1010 } else {
1011 debug!(peer = %peer, query = %query, "received SQL query");
1012 let response = execute_wire_query_sql(
1013 engine.clone(),
1014 tx_gate.clone(),
1015 &mut tx_permit,
1016 query.clone(),
1017 principal.clone(),
1018 query_timeout,
1019 )
1020 .await;
1021 if matches!(&response, Message::Error { message } if message == "query timeout exceeded")
1022 {
1023 warn!(peer = %peer, query = %query, "SQL query timeout exceeded");
1024 if tx_permit.is_some() {
1025 let engine = engine.clone();
1026 let principal = principal.clone();
1027 let _ = tokio::task::spawn_blocking(move || {
1028 rollback_open_transaction(engine, principal)
1029 })
1030 .await;
1031 }
1032 tx_permit.take();
1033 }
1034 response
1035 }
1036 }
1037 Message::QueryWithParams { query, params } => {
1038 if query.len() > MAX_QUERY_LENGTH {
1039 Message::Error {
1040 message: format!(
1041 "query too large: {} bytes (max {})",
1042 query.len(),
1043 MAX_QUERY_LENGTH
1044 ),
1045 }
1046 } else {
1047 debug!(peer = %peer, query = %query, n_params = params.len(), "received parameterized query");
1048 let response = execute_wire_query_with_params(
1049 engine.clone(),
1050 tx_gate.clone(),
1051 &mut tx_permit,
1052 query.clone(),
1053 params.clone(),
1054 principal.clone(),
1055 query_timeout,
1056 )
1057 .await;
1058 if matches!(&response, Message::Error { message } if message == "query timeout exceeded")
1059 {
1060 warn!(peer = %peer, query = %query, "query timeout exceeded");
1061 if tx_permit.is_some() {
1062 let engine = engine.clone();
1063 let principal = principal.clone();
1064 let _ = tokio::task::spawn_blocking(move || {
1065 rollback_open_transaction(engine, principal)
1066 })
1067 .await;
1068 }
1069 tx_permit.take();
1070 }
1071 response
1072 }
1073 }
1074 Message::Disconnect => {
1075 debug!(peer = %peer, "received DISCONNECT");
1076 break;
1077 }
1078 _ => Message::Error {
1079 message: "unexpected message type".into(),
1080 },
1081 };
1082
1083 if !write_msg(&mut writer, &response).await {
1084 break;
1085 }
1086 }
1087
1088 if tx_permit.is_some() {
1095 let engine = engine.clone();
1096 let principal = principal.clone();
1097 let _ =
1098 tokio::task::spawn_blocking(move || rollback_open_transaction(engine, principal)).await;
1099 }
1100 tx_permit.take();
1101
1102 info!(peer = %peer, "client disconnected");
1103}
1104
1105fn query_result_to_message(result: QueryResult) -> Message {
1106 match result {
1107 QueryResult::Rows { columns, rows } => {
1108 let str_rows: Vec<Vec<String>> = rows
1109 .iter()
1110 .map(|row| row.iter().map(value_to_display).collect())
1111 .collect();
1112 Message::ResultRows {
1113 columns,
1114 rows: str_rows,
1115 }
1116 }
1117 QueryResult::Scalar(val) => Message::ResultScalar {
1118 value: value_to_display(&val),
1119 },
1120 QueryResult::Modified(n) => Message::ResultOk { affected: n },
1121 QueryResult::Created(name) => Message::ResultMessage {
1122 message: format!("type {name} created"),
1123 },
1124 QueryResult::Executed { message } => Message::ResultMessage { message },
1125 }
1126}
1127
1128fn value_to_display(v: &Value) -> String {
1129 match v {
1130 Value::Int(n) => n.to_string(),
1131 Value::Float(n) => format!("{n}"),
1132 Value::Bool(b) => b.to_string(),
1133 Value::Str(s) => s.clone(),
1134 Value::DateTime(t) => format!("{t}"),
1135 Value::Uuid(u) => format!("{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
1136 u[0], u[1], u[2], u[3], u[4], u[5], u[6], u[7],
1137 u[8], u[9], u[10], u[11], u[12], u[13], u[14], u[15]),
1138 Value::Bytes(b) => format!("<{} bytes>", b.len()),
1139 Value::Empty => "null".into(),
1145 }
1146}
1147
1148#[cfg(test)]
1149mod tests {
1150 use super::*;
1151
1152 #[test]
1155 fn null_serializes_as_null_bareword_on_wire() {
1156 assert_eq!(value_to_display(&Value::Empty), "null");
1157 }
1158
1159 #[test]
1162 fn unique_violation_error_surfaces_to_remote_clients() {
1163 assert_eq!(
1166 sanitize_error("unique constraint violation on User.email"),
1167 "unique constraint violation on User.email"
1168 );
1169 }
1170
1171 #[test]
1172 fn internal_errors_stay_generic() {
1173 assert_eq!(
1174 sanitize_error("some internal io panic detail"),
1175 "query execution error"
1176 );
1177 }
1178
1179 #[test]
1180 fn resource_limit_errors_surface_actionable_hints() {
1181 for msg in [
1186 "sort input exceeds row limit — add a LIMIT clause",
1187 "join result exceeds row limit",
1188 "query exceeded memory budget: requested 100 bytes, limit 50 bytes",
1189 ] {
1190 assert_eq!(sanitize_error(msg), msg, "should pass through verbatim");
1191 }
1192 }
1193
1194 fn parsed(q: &str) -> powdb_query::ast::Statement {
1197 parser::parse(q).unwrap()
1198 }
1199
1200 fn principal(role: &str) -> Option<Principal> {
1201 Some(Principal {
1202 name: "u".into(),
1203 role: role.into(),
1204 })
1205 }
1206
1207 #[test]
1208 fn readonly_can_read_but_not_write() {
1209 let p = principal("readonly");
1210 assert!(check_statement_permitted(p.as_ref(), &parsed("User")).is_ok());
1212 assert!(check_statement_permitted(p.as_ref(), &parsed("count(User)")).is_ok());
1213 assert!(check_statement_permitted(p.as_ref(), &parsed("explain User")).is_ok());
1214 for q in [
1216 r#"insert User { name := "x" }"#,
1217 "User filter .id = 1 update { age := 2 }",
1218 "User filter .id = 1 delete",
1219 "drop User",
1220 "alter User add column c: str",
1221 "type T { required id: int }",
1222 "begin",
1223 "commit",
1224 "rollback",
1225 ] {
1226 let err = check_statement_permitted(p.as_ref(), &parsed(q))
1227 .expect_err(&format!("must deny: {q}"));
1228 assert!(
1229 err.to_string().contains("permission denied"),
1230 "unexpected error for {q}: {err}"
1231 );
1232 }
1233 }
1234
1235 #[test]
1236 fn readwrite_and_admin_have_full_query_access() {
1237 for role in ["readwrite", "admin"] {
1238 let p = principal(role);
1239 assert!(check_statement_permitted(p.as_ref(), &parsed("User")).is_ok());
1240 assert!(check_statement_permitted(
1241 p.as_ref(),
1242 &parsed(r#"insert User { name := "x" }"#)
1243 )
1244 .is_ok());
1245 assert!(check_statement_permitted(p.as_ref(), &parsed("drop User")).is_ok());
1246 }
1247 }
1248
1249 #[test]
1250 fn unknown_role_fails_closed_for_writes() {
1251 let p = principal("mystery");
1252 assert!(check_statement_permitted(p.as_ref(), &parsed("User")).is_ok());
1253 assert!(
1254 check_statement_permitted(p.as_ref(), &parsed(r#"insert User { name := "x" }"#))
1255 .is_err()
1256 );
1257 }
1258
1259 #[test]
1260 fn no_principal_means_full_access() {
1261 assert!(check_statement_permitted(None, &parsed("drop User")).is_ok());
1263 assert!(check_statement_permitted(None, &parsed(r#"insert User { name := "x" }"#)).is_ok());
1264 }
1265
1266 fn store_with_alice() -> UserStore {
1267 let mut s = UserStore::new();
1268 s.create_user("alice", "pw", "readwrite").unwrap();
1269 s
1270 }
1271
1272 #[test]
1275 fn empty_store_no_password_is_open() {
1276 let s = UserStore::new();
1277 assert_eq!(
1278 authenticate_connect(&s, None, None, None),
1279 AuthOutcome::Authenticated { principal: None }
1280 );
1281 assert_eq!(
1283 authenticate_connect(&s, None, Some("x"), Some("y")),
1284 AuthOutcome::Authenticated { principal: None }
1285 );
1286 }
1287
1288 #[test]
1289 fn empty_store_correct_shared_password_succeeds() {
1290 let s = UserStore::new();
1291 assert_eq!(
1292 authenticate_connect(&s, Some("pw"), None, Some("pw")),
1293 AuthOutcome::Authenticated { principal: None }
1294 );
1295 }
1296
1297 #[test]
1298 fn empty_store_wrong_shared_password_rejected() {
1299 let s = UserStore::new();
1300 assert_eq!(
1301 authenticate_connect(&s, Some("pw"), None, Some("bad")),
1302 AuthOutcome::Rejected
1303 );
1304 }
1305
1306 #[test]
1307 fn empty_store_missing_password_rejected_when_expected() {
1308 let s = UserStore::new();
1309 assert_eq!(
1310 authenticate_connect(&s, Some("pw"), None, None),
1311 AuthOutcome::Rejected
1312 );
1313 }
1314
1315 #[test]
1316 fn empty_store_ignores_username_for_shared_password() {
1317 let s = UserStore::new();
1320 assert_eq!(
1321 authenticate_connect(&s, Some("pw"), Some("whoever"), Some("pw")),
1322 AuthOutcome::Authenticated { principal: None }
1323 );
1324 }
1325
1326 #[test]
1329 fn user_auth_success_binds_principal() {
1330 let s = store_with_alice();
1331 assert_eq!(
1332 authenticate_connect(&s, None, Some("alice"), Some("pw")),
1333 AuthOutcome::Authenticated {
1334 principal: Some(Principal {
1335 name: "alice".into(),
1336 role: "readwrite".into(),
1337 })
1338 }
1339 );
1340 }
1341
1342 #[test]
1343 fn user_auth_wrong_password_rejected() {
1344 let s = store_with_alice();
1345 assert_eq!(
1346 authenticate_connect(&s, None, Some("alice"), Some("bad")),
1347 AuthOutcome::Rejected
1348 );
1349 }
1350
1351 #[test]
1352 fn user_auth_unknown_user_rejected() {
1353 let s = store_with_alice();
1354 assert_eq!(
1355 authenticate_connect(&s, None, Some("mallory"), Some("pw")),
1356 AuthOutcome::Rejected
1357 );
1358 }
1359
1360 #[test]
1361 fn user_auth_missing_username_rejected() {
1362 let s = store_with_alice();
1363 assert_eq!(
1364 authenticate_connect(&s, None, None, Some("pw")),
1365 AuthOutcome::Rejected
1366 );
1367 }
1368
1369 #[test]
1370 fn user_auth_missing_password_rejected() {
1371 let s = store_with_alice();
1372 assert_eq!(
1373 authenticate_connect(&s, Some("pw"), Some("alice"), None),
1374 AuthOutcome::Rejected
1375 );
1376 }
1377
1378 #[test]
1379 fn user_auth_ignores_shared_password_when_users_present() {
1380 let s = store_with_alice();
1383 assert_eq!(
1384 authenticate_connect(&s, Some("shared"), None, Some("shared")),
1385 AuthOutcome::Rejected
1386 );
1387 }
1388}