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
18pub type AuthRateLimiter = Arc<Mutex<HashMap<IpAddr, (u32, Instant)>>>;
20
21pub type TxGate = Arc<Semaphore>;
26
27pub fn new_tx_gate() -> TxGate {
29 Arc::new(Semaphore::new(1))
30}
31
32const MAX_QUERY_LENGTH: usize = 1024 * 1024;
34
35const WRITE_TIMEOUT: Duration = Duration::from_secs(30);
38
39const MAX_AUTH_FAILURES: u32 = 5;
41
42const AUTH_FAILURE_WINDOW: Duration = Duration::from_secs(60);
44
45pub fn new_rate_limiter() -> AuthRateLimiter {
47 Arc::new(Mutex::new(HashMap::new()))
48}
49
50fn is_rate_limited(limiter: &AuthRateLimiter, ip: IpAddr) -> bool {
53 let mut map = limiter.lock().unwrap_or_else(|e| e.into_inner());
54 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
65fn 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 if now.duration_since(entry.1) >= AUTH_FAILURE_WINDOW {
72 *entry = (1, now);
73 } else {
74 entry.0 += 1;
75 }
76}
77
78fn 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
84fn 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#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct Principal {
102 pub name: String,
103 pub role: String,
104}
105
106fn 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
127fn 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
142fn check_statement_permitted(
154 principal: Option<&Principal>,
155 stmt: &powdb_query::ast::Statement,
156) -> Result<(), QueryError> {
157 let Some(p) = principal else {
158 return Ok(());
161 };
162 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#[derive(Debug, Clone, PartialEq, Eq)]
184pub enum AuthOutcome {
185 Authenticated { principal: Option<Principal> },
189 Rejected,
192}
193
194pub 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 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 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
243const 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 "sort input exceeds",
269 "join result exceeds",
270 "query exceeded memory budget",
271];
272
273fn 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
286async 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
300pub struct ConnOpts<'a> {
303 pub engine: Arc<RwLock<Engine>>,
304 pub tx_gate: TxGate,
305 pub expected_password: Option<Zeroizing<String>>,
308 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 pub metrics: Arc<Metrics>,
319}
320
321fn 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 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 }
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
398fn 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#[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 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 }
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#[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, ¶ms) {
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, ¶ms, 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, ¶ms, 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, ¶ms, 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, ¶ms, 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 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 let principal: Option<Principal>;
896 match connect_msg {
897 Message::Connect {
898 db_name,
899 password,
900 username,
901 } => {
902 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 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 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 loop {
975 let msg = tokio::select! {
976 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 _ = 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 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 Value::Empty => "null".into(),
1193 }
1194}
1195
1196#[cfg(test)]
1197mod tests {
1198 use super::*;
1199
1200 #[test]
1203 fn null_serializes_as_null_bareword_on_wire() {
1204 assert_eq!(value_to_display(&Value::Empty), "null");
1205 }
1206
1207 #[test]
1210 fn unique_violation_error_surfaces_to_remote_clients() {
1211 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 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 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 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 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 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 #[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 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 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 #[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 let s = store_with_alice();
1431 assert_eq!(
1432 authenticate_connect(&s, Some("shared"), None, Some("shared")),
1433 AuthOutcome::Rejected
1434 );
1435 }
1436}