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_storage::types::Value;
7use std::collections::HashMap;
8use std::net::IpAddr;
9use std::sync::{Arc, Mutex, RwLock};
10use std::time::{Duration, Instant};
11use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, BufWriter};
12use tokio::sync::watch;
13use tracing::{debug, error, info, warn};
14use zeroize::Zeroizing;
15
16pub type AuthRateLimiter = Arc<Mutex<HashMap<IpAddr, (u32, Instant)>>>;
18
19const MAX_QUERY_LENGTH: usize = 1024 * 1024;
21
22const WRITE_TIMEOUT: Duration = Duration::from_secs(30);
25
26const MAX_AUTH_FAILURES: u32 = 5;
28
29const AUTH_FAILURE_WINDOW: Duration = Duration::from_secs(60);
31
32pub fn new_rate_limiter() -> AuthRateLimiter {
34 Arc::new(Mutex::new(HashMap::new()))
35}
36
37fn is_rate_limited(limiter: &AuthRateLimiter, ip: IpAddr) -> bool {
40 let mut map = limiter.lock().unwrap_or_else(|e| e.into_inner());
41 let now = Instant::now();
43 map.retain(|_, (_, ts)| now.duration_since(*ts) < AUTH_FAILURE_WINDOW);
44
45 if let Some((count, _)) = map.get(&ip) {
46 *count >= MAX_AUTH_FAILURES
47 } else {
48 false
49 }
50}
51
52fn record_auth_failure(limiter: &AuthRateLimiter, ip: IpAddr) {
54 let mut map = limiter.lock().unwrap_or_else(|e| e.into_inner());
55 let now = Instant::now();
56 let entry = map.entry(ip).or_insert((0, now));
57 if now.duration_since(entry.1) >= AUTH_FAILURE_WINDOW {
59 *entry = (1, now);
60 } else {
61 entry.0 += 1;
62 }
63}
64
65fn clear_auth_failures(limiter: &AuthRateLimiter, ip: IpAddr) {
67 let mut map = limiter.lock().unwrap_or_else(|e| e.into_inner());
68 map.remove(&ip);
69}
70
71fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
74 use sha2::{Digest, Sha256};
75 let ha = Sha256::digest(a);
76 let hb = Sha256::digest(b);
77 let mut diff = 0u8;
78 for (x, y) in ha.iter().zip(hb.iter()) {
79 diff |= x ^ y;
80 }
81 diff == 0
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct Principal {
89 pub name: String,
90 pub role: String,
91}
92
93fn role_can_write(role: &str) -> bool {
97 Role::builtin(role).is_some_and(|r| r.allows(Permission::Write))
98}
99
100fn check_statement_permitted(
109 principal: Option<&Principal>,
110 stmt: &powdb_query::ast::Statement,
111) -> Result<(), QueryError> {
112 let Some(p) = principal else {
113 return Ok(());
116 };
117 if is_read_only_statement(stmt) || role_can_write(&p.role) {
118 return Ok(());
119 }
120 Err(QueryError::Execution(format!(
121 "permission denied: role '{}' cannot execute write statements",
122 p.role
123 )))
124}
125
126#[derive(Debug, Clone, PartialEq, Eq)]
128pub enum AuthOutcome {
129 Authenticated { principal: Option<Principal> },
133 Rejected,
136}
137
138pub fn authenticate_connect(
150 users: &UserStore,
151 expected_password: Option<&str>,
152 username: Option<&str>,
153 password: Option<&str>,
154) -> AuthOutcome {
155 if !users.is_empty() {
156 let Some(name) = username else {
158 return AuthOutcome::Rejected;
159 };
160 let Some(candidate) = password else {
161 return AuthOutcome::Rejected;
162 };
163 match users.authenticate(name, candidate) {
164 Some(user) => AuthOutcome::Authenticated {
165 principal: Some(Principal {
166 name: user.name.clone(),
167 role: user.role.clone(),
168 }),
169 },
170 None => AuthOutcome::Rejected,
171 }
172 } else {
173 match expected_password {
175 Some(expected) => {
176 if password.is_some_and(|p| constant_time_eq(p.as_bytes(), expected.as_bytes())) {
177 AuthOutcome::Authenticated { principal: None }
178 } else {
179 AuthOutcome::Rejected
180 }
181 }
182 None => AuthOutcome::Authenticated { principal: None },
183 }
184 }
185}
186
187const SAFE_ERROR_PREFIXES: &[&str] = &[
189 "table not found",
190 "column not found",
191 "parse error",
192 "type mismatch",
193 "unknown table",
194 "unknown column",
195 "unknown function",
196 "syntax error",
197 "expected",
198 "unexpected",
199 "missing",
200 "duplicate",
201 "invalid",
202 "cannot",
203 "no such",
204 "already exists",
205 "permission denied",
206 "row too large",
207 "unique constraint violation",
208];
209
210fn sanitize_error(e: &str) -> String {
214 let lower = e.to_lowercase();
215 for prefix in SAFE_ERROR_PREFIXES {
216 if lower.starts_with(prefix) {
217 return e.to_string();
218 }
219 }
220 "query execution error".into()
221}
222
223async fn write_msg<W: AsyncWrite + Unpin>(writer: &mut BufWriter<W>, msg: &Message) -> bool {
226 let write_fut = async {
227 if msg.write_to(writer).await.is_err() {
228 return false;
229 }
230 writer.flush().await.is_ok()
231 };
232 tokio::time::timeout(WRITE_TIMEOUT, write_fut)
233 .await
234 .unwrap_or_default()
235}
236
237pub struct ConnOpts<'a> {
240 pub engine: Arc<RwLock<Engine>>,
241 pub expected_password: Option<Zeroizing<String>>,
244 pub users: Arc<UserStore>,
248 pub shutdown_rx: &'a mut watch::Receiver<bool>,
249 pub idle_timeout: Duration,
250 pub query_timeout: Duration,
251 pub rate_limiter: Option<&'a AuthRateLimiter>,
252 pub peer_addr: Option<std::net::SocketAddr>,
253}
254
255fn dispatch_query(
264 engine: &Arc<RwLock<Engine>>,
265 query: &str,
266 principal: Option<&Principal>,
267) -> Result<QueryResult, QueryError> {
268 let stmt_result = parser::parse(query).map_err(|e| e.to_string());
269
270 if let Ok(stmt) = &stmt_result {
274 check_statement_permitted(principal, stmt)?;
275 }
276
277 let can_try_read = matches!(&stmt_result, Ok(s) if is_read_only_statement(s));
278 if can_try_read {
279 let res = {
280 let eng = engine
281 .read()
282 .map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
283 eng.execute_powql_readonly(query)
284 };
285 match res {
286 Ok(r) => return Ok(r),
287 Err(QueryError::ReadonlyNeedsWrite) => {
288 }
290 Err(e) => return Err(e),
291 }
292 }
293
294 let mut eng = engine
295 .write()
296 .map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
297 eng.execute_powql(query)
298}
299
300fn wire_param_to_value(p: &WireParam) -> powdb_query::ast::ParamValue {
303 use powdb_query::ast::ParamValue;
304 match p {
305 WireParam::Null => ParamValue::Null,
306 WireParam::Int(v) => ParamValue::Int(*v),
307 WireParam::Float(v) => ParamValue::Float(*v),
308 WireParam::Bool(v) => ParamValue::Bool(*v),
309 WireParam::Str(s) => ParamValue::Str(s.clone()),
310 }
311}
312
313fn dispatch_query_with_params(
319 engine: &Arc<RwLock<Engine>>,
320 query: &str,
321 params: &[WireParam],
322 principal: Option<&Principal>,
323) -> Result<QueryResult, QueryError> {
324 let bound: Vec<powdb_query::ast::ParamValue> = params.iter().map(wire_param_to_value).collect();
325
326 let stmt_result = parser::parse_with_params(query, &bound).map_err(|e| e.to_string());
329
330 if let Ok(stmt) = &stmt_result {
331 check_statement_permitted(principal, stmt)?;
332 }
333
334 let can_try_read = matches!(&stmt_result, Ok(s) if is_read_only_statement(s));
335 if can_try_read {
336 let res = {
337 let eng = engine
338 .read()
339 .map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
340 eng.execute_powql_readonly_with_params(query, &bound)
341 };
342 match res {
343 Ok(r) => return Ok(r),
344 Err(QueryError::ReadonlyNeedsWrite) => {
345 }
347 Err(e) => return Err(e),
348 }
349 }
350
351 let mut eng = engine
352 .write()
353 .map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
354 eng.execute_powql_with_params(query, &bound)
355}
356
357pub async fn handle_connection<S>(stream: S, opts: ConnOpts<'_>)
358where
359 S: AsyncRead + AsyncWrite + Unpin,
360{
361 let ConnOpts {
362 engine,
363 expected_password,
364 users,
365 shutdown_rx,
366 idle_timeout,
367 query_timeout,
368 rate_limiter,
369 peer_addr,
370 } = opts;
371
372 let peer = peer_addr
373 .map(|a| a.to_string())
374 .unwrap_or_else(|| "unknown".into());
375 let peer_ip = peer_addr.map(|a| a.ip());
376
377 let (reader, writer) = tokio::io::split(stream);
378 let mut reader = BufReader::new(reader);
379 let mut writer = BufWriter::new(writer);
380
381 let connect_msg = loop {
386 match tokio::time::timeout(idle_timeout, Message::read_from_preauth(&mut reader)).await {
387 Ok(Ok(Some(Message::Ping))) => {
388 debug!(peer = %peer, "pre-auth ping");
389 if !write_msg(&mut writer, &Message::Pong).await {
390 return;
391 }
392 continue;
393 }
394 Ok(Ok(Some(msg))) => break msg,
395 Ok(Ok(None)) => {
396 debug!(peer = %peer, "client closed before CONNECT");
397 return;
398 }
399 Ok(Err(e)) => {
400 error!(peer = %peer, error = %e, "error reading CONNECT");
401 return;
402 }
403 Err(_) => {
404 warn!(peer = %peer, "idle timeout waiting for CONNECT");
405 return;
406 }
407 }
408 };
409
410 let principal: Option<Principal>;
413 match connect_msg {
414 Message::Connect {
415 db_name,
416 password,
417 username,
418 } => {
419 if let (Some(limiter), Some(ip)) = (rate_limiter, peer_ip) {
421 if is_rate_limited(limiter, ip) {
422 warn!(peer = %peer, "rate limited: too many auth failures");
423 let err = Message::Error {
424 message: "too many auth failures, try again later".into(),
425 };
426 write_msg(&mut writer, &err).await;
427 return;
428 }
429 }
430
431 let outcome = authenticate_connect(
432 &users,
433 expected_password.as_ref().map(|p| p.as_str()),
434 username.as_deref(),
435 password.as_ref().map(|p| p.as_str()),
436 );
437
438 match outcome {
439 AuthOutcome::Rejected => {
440 warn!(peer = %peer, db = %db_name, "auth rejected");
441 if let (Some(limiter), Some(ip)) = (rate_limiter, peer_ip) {
443 record_auth_failure(limiter, ip);
444 }
445 let err = Message::Error {
446 message: "authentication failed".into(),
447 };
448 write_msg(&mut writer, &err).await;
449 return;
450 }
451 AuthOutcome::Authenticated {
452 principal: auth_principal,
453 } => {
454 if let (Some(limiter), Some(ip)) = (rate_limiter, peer_ip) {
456 clear_auth_failures(limiter, ip);
457 }
458 match &auth_principal {
459 Some(p) => {
460 info!(peer = %peer, db = %db_name, user = %p.name, role = %p.role, "authenticated");
461 }
462 None => {
463 info!(peer = %peer, db = %db_name, "client connected");
464 }
465 }
466 principal = auth_principal;
467 }
468 }
469
470 let ok = Message::ConnectOk {
471 version: env!("CARGO_PKG_VERSION").into(),
472 };
473 if !write_msg(&mut writer, &ok).await {
474 return;
475 }
476 }
477 _ => {
478 warn!(peer = %peer, "first message was not CONNECT");
479 let err = Message::Error {
480 message: "expected CONNECT".into(),
481 };
482 write_msg(&mut writer, &err).await;
483 return;
484 }
485 }
486
487 loop {
489 let msg = tokio::select! {
490 result = tokio::time::timeout(idle_timeout, Message::read_from(&mut reader)) => {
492 match result {
493 Ok(Ok(Some(msg))) => msg,
494 Ok(Ok(None)) => break,
495 Ok(Err(e)) => {
496 error!(peer = %peer, error = %e, "read error");
497 break;
498 }
499 Err(_) => {
500 info!(peer = %peer, "idle timeout, closing connection");
501 let err = Message::Error { message: "idle timeout".into() };
502 write_msg(&mut writer, &err).await;
503 break;
504 }
505 }
506 }
507 _ = shutdown_rx.changed() => {
509 if *shutdown_rx.borrow() {
510 info!(peer = %peer, "server shutting down, closing connection");
511 let err = Message::Error { message: "server shutting down".into() };
512 write_msg(&mut writer, &err).await;
513 break;
514 }
515 continue;
516 }
517 };
518
519 let response = match msg {
520 Message::Ping => {
521 debug!(peer = %peer, "ping");
522 Message::Pong
523 }
524 Message::Query { query } => {
525 if query.len() > MAX_QUERY_LENGTH {
526 Message::Error {
527 message: format!(
528 "query too large: {} bytes (max {})",
529 query.len(),
530 MAX_QUERY_LENGTH
531 ),
532 }
533 } else {
534 debug!(peer = %peer, query = %query, "received query");
535 let handle = tokio::task::spawn_blocking({
536 let engine = engine.clone();
537 let query = query.clone();
538 let principal = principal.clone();
539 move || dispatch_query(&engine, &query, principal.as_ref())
540 });
541 let abort_handle = handle.abort_handle();
542 match tokio::time::timeout(query_timeout, handle).await {
543 Ok(Ok(Ok(result))) => query_result_to_message(result),
544 Ok(Ok(Err(e))) => Message::Error {
545 message: sanitize_error(&e.to_string()),
546 },
547 Ok(Err(e)) => Message::Error {
548 message: format!("internal error: {e}"),
549 },
550 Err(_) => {
551 abort_handle.abort();
552 warn!(peer = %peer, query = %query, "query timeout exceeded");
553 Message::Error {
554 message: "query timeout exceeded".into(),
555 }
556 }
557 }
558 }
559 }
560 Message::QueryWithParams { query, params } => {
561 if query.len() > MAX_QUERY_LENGTH {
562 Message::Error {
563 message: format!(
564 "query too large: {} bytes (max {})",
565 query.len(),
566 MAX_QUERY_LENGTH
567 ),
568 }
569 } else {
570 debug!(peer = %peer, query = %query, n_params = params.len(), "received parameterized query");
571 let handle = tokio::task::spawn_blocking({
572 let engine = engine.clone();
573 let query = query.clone();
574 let params = params.clone();
575 let principal = principal.clone();
576 move || {
577 dispatch_query_with_params(&engine, &query, ¶ms, principal.as_ref())
578 }
579 });
580 let abort_handle = handle.abort_handle();
581 match tokio::time::timeout(query_timeout, handle).await {
582 Ok(Ok(Ok(result))) => query_result_to_message(result),
583 Ok(Ok(Err(e))) => Message::Error {
584 message: sanitize_error(&e.to_string()),
585 },
586 Ok(Err(e)) => Message::Error {
587 message: format!("internal error: {e}"),
588 },
589 Err(_) => {
590 abort_handle.abort();
591 warn!(peer = %peer, query = %query, "query timeout exceeded");
592 Message::Error {
593 message: "query timeout exceeded".into(),
594 }
595 }
596 }
597 }
598 }
599 Message::Disconnect => {
600 debug!(peer = %peer, "received DISCONNECT");
601 break;
602 }
603 _ => Message::Error {
604 message: "unexpected message type".into(),
605 },
606 };
607
608 if !write_msg(&mut writer, &response).await {
609 break;
610 }
611 }
612
613 info!(peer = %peer, "client disconnected");
614}
615
616fn query_result_to_message(result: QueryResult) -> Message {
617 match result {
618 QueryResult::Rows { columns, rows } => {
619 let str_rows: Vec<Vec<String>> = rows
620 .iter()
621 .map(|row| row.iter().map(value_to_display).collect())
622 .collect();
623 Message::ResultRows {
624 columns,
625 rows: str_rows,
626 }
627 }
628 QueryResult::Scalar(val) => Message::ResultScalar {
629 value: value_to_display(&val),
630 },
631 QueryResult::Modified(n) => Message::ResultOk { affected: n },
632 QueryResult::Created(name) => Message::ResultMessage {
633 message: format!("type {name} created"),
634 },
635 QueryResult::Executed { message } => Message::ResultMessage { message },
636 }
637}
638
639fn value_to_display(v: &Value) -> String {
640 match v {
641 Value::Int(n) => n.to_string(),
642 Value::Float(n) => format!("{n}"),
643 Value::Bool(b) => b.to_string(),
644 Value::Str(s) => s.clone(),
645 Value::DateTime(t) => format!("{t}"),
646 Value::Uuid(u) => format!("{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
647 u[0], u[1], u[2], u[3], u[4], u[5], u[6], u[7],
648 u[8], u[9], u[10], u[11], u[12], u[13], u[14], u[15]),
649 Value::Bytes(b) => format!("<{} bytes>", b.len()),
650 Value::Empty => "null".into(),
656 }
657}
658
659#[cfg(test)]
660mod tests {
661 use super::*;
662
663 #[test]
666 fn null_serializes_as_null_bareword_on_wire() {
667 assert_eq!(value_to_display(&Value::Empty), "null");
668 }
669
670 #[test]
673 fn unique_violation_error_surfaces_to_remote_clients() {
674 assert_eq!(
677 sanitize_error("unique constraint violation on User.email"),
678 "unique constraint violation on User.email"
679 );
680 }
681
682 #[test]
683 fn internal_errors_stay_generic() {
684 assert_eq!(
685 sanitize_error("some internal io panic detail"),
686 "query execution error"
687 );
688 }
689
690 fn parsed(q: &str) -> powdb_query::ast::Statement {
693 parser::parse(q).unwrap()
694 }
695
696 fn principal(role: &str) -> Option<Principal> {
697 Some(Principal {
698 name: "u".into(),
699 role: role.into(),
700 })
701 }
702
703 #[test]
704 fn readonly_can_read_but_not_write() {
705 let p = principal("readonly");
706 assert!(check_statement_permitted(p.as_ref(), &parsed("User")).is_ok());
708 assert!(check_statement_permitted(p.as_ref(), &parsed("count(User)")).is_ok());
709 assert!(check_statement_permitted(p.as_ref(), &parsed("explain User")).is_ok());
710 for q in [
712 r#"insert User { name := "x" }"#,
713 "User filter .id = 1 update { age := 2 }",
714 "User filter .id = 1 delete",
715 "drop User",
716 "alter User add column c: str",
717 "type T { required id: int }",
718 "begin",
719 "commit",
720 "rollback",
721 ] {
722 let err = check_statement_permitted(p.as_ref(), &parsed(q))
723 .expect_err(&format!("must deny: {q}"));
724 assert!(
725 err.to_string().contains("permission denied"),
726 "unexpected error for {q}: {err}"
727 );
728 }
729 }
730
731 #[test]
732 fn readwrite_and_admin_have_full_query_access() {
733 for role in ["readwrite", "admin"] {
734 let p = principal(role);
735 assert!(check_statement_permitted(p.as_ref(), &parsed("User")).is_ok());
736 assert!(check_statement_permitted(
737 p.as_ref(),
738 &parsed(r#"insert User { name := "x" }"#)
739 )
740 .is_ok());
741 assert!(check_statement_permitted(p.as_ref(), &parsed("drop User")).is_ok());
742 }
743 }
744
745 #[test]
746 fn unknown_role_fails_closed_for_writes() {
747 let p = principal("mystery");
748 assert!(check_statement_permitted(p.as_ref(), &parsed("User")).is_ok());
749 assert!(
750 check_statement_permitted(p.as_ref(), &parsed(r#"insert User { name := "x" }"#))
751 .is_err()
752 );
753 }
754
755 #[test]
756 fn no_principal_means_full_access() {
757 assert!(check_statement_permitted(None, &parsed("drop User")).is_ok());
759 assert!(check_statement_permitted(None, &parsed(r#"insert User { name := "x" }"#)).is_ok());
760 }
761
762 fn store_with_alice() -> UserStore {
763 let mut s = UserStore::new();
764 s.create_user("alice", "pw", "readwrite").unwrap();
765 s
766 }
767
768 #[test]
771 fn empty_store_no_password_is_open() {
772 let s = UserStore::new();
773 assert_eq!(
774 authenticate_connect(&s, None, None, None),
775 AuthOutcome::Authenticated { principal: None }
776 );
777 assert_eq!(
779 authenticate_connect(&s, None, Some("x"), Some("y")),
780 AuthOutcome::Authenticated { principal: None }
781 );
782 }
783
784 #[test]
785 fn empty_store_correct_shared_password_succeeds() {
786 let s = UserStore::new();
787 assert_eq!(
788 authenticate_connect(&s, Some("pw"), None, Some("pw")),
789 AuthOutcome::Authenticated { principal: None }
790 );
791 }
792
793 #[test]
794 fn empty_store_wrong_shared_password_rejected() {
795 let s = UserStore::new();
796 assert_eq!(
797 authenticate_connect(&s, Some("pw"), None, Some("bad")),
798 AuthOutcome::Rejected
799 );
800 }
801
802 #[test]
803 fn empty_store_missing_password_rejected_when_expected() {
804 let s = UserStore::new();
805 assert_eq!(
806 authenticate_connect(&s, Some("pw"), None, None),
807 AuthOutcome::Rejected
808 );
809 }
810
811 #[test]
812 fn empty_store_ignores_username_for_shared_password() {
813 let s = UserStore::new();
816 assert_eq!(
817 authenticate_connect(&s, Some("pw"), Some("whoever"), Some("pw")),
818 AuthOutcome::Authenticated { principal: None }
819 );
820 }
821
822 #[test]
825 fn user_auth_success_binds_principal() {
826 let s = store_with_alice();
827 assert_eq!(
828 authenticate_connect(&s, None, Some("alice"), Some("pw")),
829 AuthOutcome::Authenticated {
830 principal: Some(Principal {
831 name: "alice".into(),
832 role: "readwrite".into(),
833 })
834 }
835 );
836 }
837
838 #[test]
839 fn user_auth_wrong_password_rejected() {
840 let s = store_with_alice();
841 assert_eq!(
842 authenticate_connect(&s, None, Some("alice"), Some("bad")),
843 AuthOutcome::Rejected
844 );
845 }
846
847 #[test]
848 fn user_auth_unknown_user_rejected() {
849 let s = store_with_alice();
850 assert_eq!(
851 authenticate_connect(&s, None, Some("mallory"), Some("pw")),
852 AuthOutcome::Rejected
853 );
854 }
855
856 #[test]
857 fn user_auth_missing_username_rejected() {
858 let s = store_with_alice();
859 assert_eq!(
860 authenticate_connect(&s, None, None, Some("pw")),
861 AuthOutcome::Rejected
862 );
863 }
864
865 #[test]
866 fn user_auth_missing_password_rejected() {
867 let s = store_with_alice();
868 assert_eq!(
869 authenticate_connect(&s, Some("pw"), Some("alice"), None),
870 AuthOutcome::Rejected
871 );
872 }
873
874 #[test]
875 fn user_auth_ignores_shared_password_when_users_present() {
876 let s = store_with_alice();
879 assert_eq!(
880 authenticate_connect(&s, Some("shared"), None, Some("shared")),
881 AuthOutcome::Rejected
882 );
883 }
884}