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 is_ddl_statement(stmt: &powdb_query::ast::Statement) -> bool {
99 use powdb_query::ast::Statement;
100 let inner = match stmt {
101 Statement::Explain(inner) => inner.as_ref(),
102 other => other,
103 };
104 matches!(
105 inner,
106 Statement::CreateType(_)
107 | Statement::AlterTable(_)
108 | Statement::DropTable(_)
109 | Statement::CreateView(_)
110 | Statement::DropView(_)
111 )
112}
113
114fn required_permission(stmt: &powdb_query::ast::Statement) -> Permission {
120 if is_read_only_statement(stmt) {
121 Permission::Read
122 } else if is_ddl_statement(stmt) {
123 Permission::Ddl
124 } else {
125 Permission::Write
126 }
127}
128
129fn check_statement_permitted(
141 principal: Option<&Principal>,
142 stmt: &powdb_query::ast::Statement,
143) -> Result<(), QueryError> {
144 let Some(p) = principal else {
145 return Ok(());
148 };
149 if is_read_only_statement(stmt) {
152 return Ok(());
153 }
154 let needed = required_permission(stmt);
155 if Role::builtin(&p.role).is_some_and(|r| r.allows(needed)) {
156 return Ok(());
157 }
158 let kind = if needed == Permission::Ddl {
159 "schema-definition"
160 } else {
161 "write"
162 };
163 Err(QueryError::Execution(format!(
164 "permission denied: role '{}' cannot execute {kind} statements",
165 p.role
166 )))
167}
168
169#[derive(Debug, Clone, PartialEq, Eq)]
171pub enum AuthOutcome {
172 Authenticated { principal: Option<Principal> },
176 Rejected,
179}
180
181pub fn authenticate_connect(
193 users: &UserStore,
194 expected_password: Option<&str>,
195 username: Option<&str>,
196 password: Option<&str>,
197) -> AuthOutcome {
198 if !users.is_empty() {
199 let Some(name) = username else {
201 return AuthOutcome::Rejected;
202 };
203 let Some(candidate) = password else {
204 return AuthOutcome::Rejected;
205 };
206 match users.authenticate(name, candidate) {
207 Some(user) => AuthOutcome::Authenticated {
208 principal: Some(Principal {
209 name: user.name.clone(),
210 role: user.role.clone(),
211 }),
212 },
213 None => AuthOutcome::Rejected,
214 }
215 } else {
216 match expected_password {
218 Some(expected) => {
219 if password.is_some_and(|p| constant_time_eq(p.as_bytes(), expected.as_bytes())) {
220 AuthOutcome::Authenticated { principal: None }
221 } else {
222 AuthOutcome::Rejected
223 }
224 }
225 None => AuthOutcome::Authenticated { principal: None },
226 }
227 }
228}
229
230const SAFE_ERROR_PREFIXES: &[&str] = &[
232 "table not found",
233 "column not found",
234 "parse error",
235 "type mismatch",
236 "unknown table",
237 "unknown column",
238 "unknown function",
239 "syntax error",
240 "expected",
241 "unexpected",
242 "missing",
243 "duplicate",
244 "invalid",
245 "cannot",
246 "no such",
247 "already exists",
248 "permission denied",
249 "row too large",
250 "unique constraint violation",
251 "sort input exceeds",
256 "join result exceeds",
257 "query exceeded memory budget",
258];
259
260fn sanitize_error(e: &str) -> String {
264 let lower = e.to_lowercase();
265 for prefix in SAFE_ERROR_PREFIXES {
266 if lower.starts_with(prefix) {
267 return e.to_string();
268 }
269 }
270 "query execution error".into()
271}
272
273async fn write_msg<W: AsyncWrite + Unpin>(writer: &mut BufWriter<W>, msg: &Message) -> bool {
276 let write_fut = async {
277 if msg.write_to(writer).await.is_err() {
278 return false;
279 }
280 writer.flush().await.is_ok()
281 };
282 tokio::time::timeout(WRITE_TIMEOUT, write_fut)
283 .await
284 .unwrap_or_default()
285}
286
287pub struct ConnOpts<'a> {
290 pub engine: Arc<RwLock<Engine>>,
291 pub expected_password: Option<Zeroizing<String>>,
294 pub users: Arc<UserStore>,
298 pub shutdown_rx: &'a mut watch::Receiver<bool>,
299 pub idle_timeout: Duration,
300 pub query_timeout: Duration,
301 pub rate_limiter: Option<&'a AuthRateLimiter>,
302 pub peer_addr: Option<std::net::SocketAddr>,
303}
304
305fn dispatch_query(
314 engine: &Arc<RwLock<Engine>>,
315 query: &str,
316 principal: Option<&Principal>,
317) -> Result<QueryResult, QueryError> {
318 let stmt_result = parser::parse(query).map_err(|e| e.to_string());
319
320 if let Ok(stmt) = &stmt_result {
324 check_statement_permitted(principal, stmt)?;
325 }
326
327 let can_try_read = matches!(&stmt_result, Ok(s) if is_read_only_statement(s));
328 if can_try_read {
329 let res = {
330 let eng = engine
331 .read()
332 .map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
333 eng.execute_powql_readonly(query)
334 };
335 match res {
336 Ok(r) => return Ok(r),
337 Err(QueryError::ReadonlyNeedsWrite) => {
338 }
340 Err(e) => return Err(e),
341 }
342 }
343
344 let mut eng = engine
345 .write()
346 .map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
347 eng.execute_powql(query)
348}
349
350fn wire_param_to_value(p: &WireParam) -> powdb_query::ast::ParamValue {
353 use powdb_query::ast::ParamValue;
354 match p {
355 WireParam::Null => ParamValue::Null,
356 WireParam::Int(v) => ParamValue::Int(*v),
357 WireParam::Float(v) => ParamValue::Float(*v),
358 WireParam::Bool(v) => ParamValue::Bool(*v),
359 WireParam::Str(s) => ParamValue::Str(s.clone()),
360 }
361}
362
363fn dispatch_query_with_params(
369 engine: &Arc<RwLock<Engine>>,
370 query: &str,
371 params: &[WireParam],
372 principal: Option<&Principal>,
373) -> Result<QueryResult, QueryError> {
374 let bound: Vec<powdb_query::ast::ParamValue> = params.iter().map(wire_param_to_value).collect();
375
376 let stmt_result = parser::parse_with_params(query, &bound).map_err(|e| e.to_string());
379
380 if let Ok(stmt) = &stmt_result {
381 check_statement_permitted(principal, stmt)?;
382 }
383
384 let can_try_read = matches!(&stmt_result, Ok(s) if is_read_only_statement(s));
385 if can_try_read {
386 let res = {
387 let eng = engine
388 .read()
389 .map_err(|e| QueryError::Execution(format!("lock poisoned: {e}")))?;
390 eng.execute_powql_readonly_with_params(query, &bound)
391 };
392 match res {
393 Ok(r) => return Ok(r),
394 Err(QueryError::ReadonlyNeedsWrite) => {
395 }
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_powql_with_params(query, &bound)
405}
406
407pub async fn handle_connection<S>(stream: S, opts: ConnOpts<'_>)
408where
409 S: AsyncRead + AsyncWrite + Unpin,
410{
411 let ConnOpts {
412 engine,
413 expected_password,
414 users,
415 shutdown_rx,
416 idle_timeout,
417 query_timeout,
418 rate_limiter,
419 peer_addr,
420 } = opts;
421
422 let peer = peer_addr
423 .map(|a| a.to_string())
424 .unwrap_or_else(|| "unknown".into());
425 let peer_ip = peer_addr.map(|a| a.ip());
426
427 let (reader, writer) = tokio::io::split(stream);
428 let mut reader = BufReader::new(reader);
429 let mut writer = BufWriter::new(writer);
430
431 let connect_msg = loop {
436 match tokio::time::timeout(idle_timeout, Message::read_from_preauth(&mut reader)).await {
437 Ok(Ok(Some(Message::Ping))) => {
438 debug!(peer = %peer, "pre-auth ping");
439 if !write_msg(&mut writer, &Message::Pong).await {
440 return;
441 }
442 continue;
443 }
444 Ok(Ok(Some(msg))) => break msg,
445 Ok(Ok(None)) => {
446 debug!(peer = %peer, "client closed before CONNECT");
447 return;
448 }
449 Ok(Err(e)) => {
450 error!(peer = %peer, error = %e, "error reading CONNECT");
451 return;
452 }
453 Err(_) => {
454 warn!(peer = %peer, "idle timeout waiting for CONNECT");
455 return;
456 }
457 }
458 };
459
460 let principal: Option<Principal>;
463 match connect_msg {
464 Message::Connect {
465 db_name,
466 password,
467 username,
468 } => {
469 if let (Some(limiter), Some(ip)) = (rate_limiter, peer_ip) {
471 if is_rate_limited(limiter, ip) {
472 warn!(peer = %peer, "rate limited: too many auth failures");
473 let err = Message::Error {
474 message: "too many auth failures, try again later".into(),
475 };
476 write_msg(&mut writer, &err).await;
477 return;
478 }
479 }
480
481 let outcome = authenticate_connect(
482 &users,
483 expected_password.as_ref().map(|p| p.as_str()),
484 username.as_deref(),
485 password.as_ref().map(|p| p.as_str()),
486 );
487
488 match outcome {
489 AuthOutcome::Rejected => {
490 warn!(peer = %peer, db = %db_name, "auth rejected");
491 if let (Some(limiter), Some(ip)) = (rate_limiter, peer_ip) {
493 record_auth_failure(limiter, ip);
494 }
495 let err = Message::Error {
496 message: "authentication failed".into(),
497 };
498 write_msg(&mut writer, &err).await;
499 return;
500 }
501 AuthOutcome::Authenticated {
502 principal: auth_principal,
503 } => {
504 if let (Some(limiter), Some(ip)) = (rate_limiter, peer_ip) {
506 clear_auth_failures(limiter, ip);
507 }
508 match &auth_principal {
509 Some(p) => {
510 info!(peer = %peer, db = %db_name, user = %p.name, role = %p.role, "authenticated");
511 }
512 None => {
513 info!(peer = %peer, db = %db_name, "client connected");
514 }
515 }
516 principal = auth_principal;
517 }
518 }
519
520 let ok = Message::ConnectOk {
521 version: env!("CARGO_PKG_VERSION").into(),
522 };
523 if !write_msg(&mut writer, &ok).await {
524 return;
525 }
526 }
527 _ => {
528 warn!(peer = %peer, "first message was not CONNECT");
529 let err = Message::Error {
530 message: "expected CONNECT".into(),
531 };
532 write_msg(&mut writer, &err).await;
533 return;
534 }
535 }
536
537 loop {
539 let msg = tokio::select! {
540 result = tokio::time::timeout(idle_timeout, Message::read_from(&mut reader)) => {
542 match result {
543 Ok(Ok(Some(msg))) => msg,
544 Ok(Ok(None)) => break,
545 Ok(Err(e)) => {
546 error!(peer = %peer, error = %e, "read error");
547 break;
548 }
549 Err(_) => {
550 info!(peer = %peer, "idle timeout, closing connection");
551 let err = Message::Error { message: "idle timeout".into() };
552 write_msg(&mut writer, &err).await;
553 break;
554 }
555 }
556 }
557 _ = shutdown_rx.changed() => {
559 if *shutdown_rx.borrow() {
560 info!(peer = %peer, "server shutting down, closing connection");
561 let err = Message::Error { message: "server shutting down".into() };
562 write_msg(&mut writer, &err).await;
563 break;
564 }
565 continue;
566 }
567 };
568
569 let response = match msg {
570 Message::Ping => {
571 debug!(peer = %peer, "ping");
572 Message::Pong
573 }
574 Message::Query { query } => {
575 if query.len() > MAX_QUERY_LENGTH {
576 Message::Error {
577 message: format!(
578 "query too large: {} bytes (max {})",
579 query.len(),
580 MAX_QUERY_LENGTH
581 ),
582 }
583 } else {
584 debug!(peer = %peer, query = %query, "received query");
585 let handle = tokio::task::spawn_blocking({
586 let engine = engine.clone();
587 let query = query.clone();
588 let principal = principal.clone();
589 move || dispatch_query(&engine, &query, principal.as_ref())
590 });
591 let abort_handle = handle.abort_handle();
592 match tokio::time::timeout(query_timeout, handle).await {
593 Ok(Ok(Ok(result))) => query_result_to_message(result),
594 Ok(Ok(Err(e))) => Message::Error {
595 message: sanitize_error(&e.to_string()),
596 },
597 Ok(Err(e)) => Message::Error {
598 message: format!("internal error: {e}"),
599 },
600 Err(_) => {
601 abort_handle.abort();
602 warn!(peer = %peer, query = %query, "query timeout exceeded");
603 Message::Error {
604 message: "query timeout exceeded".into(),
605 }
606 }
607 }
608 }
609 }
610 Message::QueryWithParams { query, params } => {
611 if query.len() > MAX_QUERY_LENGTH {
612 Message::Error {
613 message: format!(
614 "query too large: {} bytes (max {})",
615 query.len(),
616 MAX_QUERY_LENGTH
617 ),
618 }
619 } else {
620 debug!(peer = %peer, query = %query, n_params = params.len(), "received parameterized query");
621 let handle = tokio::task::spawn_blocking({
622 let engine = engine.clone();
623 let query = query.clone();
624 let params = params.clone();
625 let principal = principal.clone();
626 move || {
627 dispatch_query_with_params(&engine, &query, ¶ms, principal.as_ref())
628 }
629 });
630 let abort_handle = handle.abort_handle();
631 match tokio::time::timeout(query_timeout, handle).await {
632 Ok(Ok(Ok(result))) => query_result_to_message(result),
633 Ok(Ok(Err(e))) => Message::Error {
634 message: sanitize_error(&e.to_string()),
635 },
636 Ok(Err(e)) => Message::Error {
637 message: format!("internal error: {e}"),
638 },
639 Err(_) => {
640 abort_handle.abort();
641 warn!(peer = %peer, query = %query, "query timeout exceeded");
642 Message::Error {
643 message: "query timeout exceeded".into(),
644 }
645 }
646 }
647 }
648 }
649 Message::Disconnect => {
650 debug!(peer = %peer, "received DISCONNECT");
651 break;
652 }
653 _ => Message::Error {
654 message: "unexpected message type".into(),
655 },
656 };
657
658 if !write_msg(&mut writer, &response).await {
659 break;
660 }
661 }
662
663 info!(peer = %peer, "client disconnected");
664}
665
666fn query_result_to_message(result: QueryResult) -> Message {
667 match result {
668 QueryResult::Rows { columns, rows } => {
669 let str_rows: Vec<Vec<String>> = rows
670 .iter()
671 .map(|row| row.iter().map(value_to_display).collect())
672 .collect();
673 Message::ResultRows {
674 columns,
675 rows: str_rows,
676 }
677 }
678 QueryResult::Scalar(val) => Message::ResultScalar {
679 value: value_to_display(&val),
680 },
681 QueryResult::Modified(n) => Message::ResultOk { affected: n },
682 QueryResult::Created(name) => Message::ResultMessage {
683 message: format!("type {name} created"),
684 },
685 QueryResult::Executed { message } => Message::ResultMessage { message },
686 }
687}
688
689fn value_to_display(v: &Value) -> String {
690 match v {
691 Value::Int(n) => n.to_string(),
692 Value::Float(n) => format!("{n}"),
693 Value::Bool(b) => b.to_string(),
694 Value::Str(s) => s.clone(),
695 Value::DateTime(t) => format!("{t}"),
696 Value::Uuid(u) => format!("{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
697 u[0], u[1], u[2], u[3], u[4], u[5], u[6], u[7],
698 u[8], u[9], u[10], u[11], u[12], u[13], u[14], u[15]),
699 Value::Bytes(b) => format!("<{} bytes>", b.len()),
700 Value::Empty => "null".into(),
706 }
707}
708
709#[cfg(test)]
710mod tests {
711 use super::*;
712
713 #[test]
716 fn null_serializes_as_null_bareword_on_wire() {
717 assert_eq!(value_to_display(&Value::Empty), "null");
718 }
719
720 #[test]
723 fn unique_violation_error_surfaces_to_remote_clients() {
724 assert_eq!(
727 sanitize_error("unique constraint violation on User.email"),
728 "unique constraint violation on User.email"
729 );
730 }
731
732 #[test]
733 fn internal_errors_stay_generic() {
734 assert_eq!(
735 sanitize_error("some internal io panic detail"),
736 "query execution error"
737 );
738 }
739
740 #[test]
741 fn resource_limit_errors_surface_actionable_hints() {
742 for msg in [
747 "sort input exceeds row limit — add a LIMIT clause",
748 "join result exceeds row limit",
749 "query exceeded memory budget: requested 100 bytes, limit 50 bytes",
750 ] {
751 assert_eq!(sanitize_error(msg), msg, "should pass through verbatim");
752 }
753 }
754
755 fn parsed(q: &str) -> powdb_query::ast::Statement {
758 parser::parse(q).unwrap()
759 }
760
761 fn principal(role: &str) -> Option<Principal> {
762 Some(Principal {
763 name: "u".into(),
764 role: role.into(),
765 })
766 }
767
768 #[test]
769 fn readonly_can_read_but_not_write() {
770 let p = principal("readonly");
771 assert!(check_statement_permitted(p.as_ref(), &parsed("User")).is_ok());
773 assert!(check_statement_permitted(p.as_ref(), &parsed("count(User)")).is_ok());
774 assert!(check_statement_permitted(p.as_ref(), &parsed("explain User")).is_ok());
775 for q in [
777 r#"insert User { name := "x" }"#,
778 "User filter .id = 1 update { age := 2 }",
779 "User filter .id = 1 delete",
780 "drop User",
781 "alter User add column c: str",
782 "type T { required id: int }",
783 "begin",
784 "commit",
785 "rollback",
786 ] {
787 let err = check_statement_permitted(p.as_ref(), &parsed(q))
788 .expect_err(&format!("must deny: {q}"));
789 assert!(
790 err.to_string().contains("permission denied"),
791 "unexpected error for {q}: {err}"
792 );
793 }
794 }
795
796 #[test]
797 fn readwrite_and_admin_have_full_query_access() {
798 for role in ["readwrite", "admin"] {
799 let p = principal(role);
800 assert!(check_statement_permitted(p.as_ref(), &parsed("User")).is_ok());
801 assert!(check_statement_permitted(
802 p.as_ref(),
803 &parsed(r#"insert User { name := "x" }"#)
804 )
805 .is_ok());
806 assert!(check_statement_permitted(p.as_ref(), &parsed("drop User")).is_ok());
807 }
808 }
809
810 #[test]
811 fn unknown_role_fails_closed_for_writes() {
812 let p = principal("mystery");
813 assert!(check_statement_permitted(p.as_ref(), &parsed("User")).is_ok());
814 assert!(
815 check_statement_permitted(p.as_ref(), &parsed(r#"insert User { name := "x" }"#))
816 .is_err()
817 );
818 }
819
820 #[test]
821 fn no_principal_means_full_access() {
822 assert!(check_statement_permitted(None, &parsed("drop User")).is_ok());
824 assert!(check_statement_permitted(None, &parsed(r#"insert User { name := "x" }"#)).is_ok());
825 }
826
827 fn store_with_alice() -> UserStore {
828 let mut s = UserStore::new();
829 s.create_user("alice", "pw", "readwrite").unwrap();
830 s
831 }
832
833 #[test]
836 fn empty_store_no_password_is_open() {
837 let s = UserStore::new();
838 assert_eq!(
839 authenticate_connect(&s, None, None, None),
840 AuthOutcome::Authenticated { principal: None }
841 );
842 assert_eq!(
844 authenticate_connect(&s, None, Some("x"), Some("y")),
845 AuthOutcome::Authenticated { principal: None }
846 );
847 }
848
849 #[test]
850 fn empty_store_correct_shared_password_succeeds() {
851 let s = UserStore::new();
852 assert_eq!(
853 authenticate_connect(&s, Some("pw"), None, Some("pw")),
854 AuthOutcome::Authenticated { principal: None }
855 );
856 }
857
858 #[test]
859 fn empty_store_wrong_shared_password_rejected() {
860 let s = UserStore::new();
861 assert_eq!(
862 authenticate_connect(&s, Some("pw"), None, Some("bad")),
863 AuthOutcome::Rejected
864 );
865 }
866
867 #[test]
868 fn empty_store_missing_password_rejected_when_expected() {
869 let s = UserStore::new();
870 assert_eq!(
871 authenticate_connect(&s, Some("pw"), None, None),
872 AuthOutcome::Rejected
873 );
874 }
875
876 #[test]
877 fn empty_store_ignores_username_for_shared_password() {
878 let s = UserStore::new();
881 assert_eq!(
882 authenticate_connect(&s, Some("pw"), Some("whoever"), Some("pw")),
883 AuthOutcome::Authenticated { principal: None }
884 );
885 }
886
887 #[test]
890 fn user_auth_success_binds_principal() {
891 let s = store_with_alice();
892 assert_eq!(
893 authenticate_connect(&s, None, Some("alice"), Some("pw")),
894 AuthOutcome::Authenticated {
895 principal: Some(Principal {
896 name: "alice".into(),
897 role: "readwrite".into(),
898 })
899 }
900 );
901 }
902
903 #[test]
904 fn user_auth_wrong_password_rejected() {
905 let s = store_with_alice();
906 assert_eq!(
907 authenticate_connect(&s, None, Some("alice"), Some("bad")),
908 AuthOutcome::Rejected
909 );
910 }
911
912 #[test]
913 fn user_auth_unknown_user_rejected() {
914 let s = store_with_alice();
915 assert_eq!(
916 authenticate_connect(&s, None, Some("mallory"), Some("pw")),
917 AuthOutcome::Rejected
918 );
919 }
920
921 #[test]
922 fn user_auth_missing_username_rejected() {
923 let s = store_with_alice();
924 assert_eq!(
925 authenticate_connect(&s, None, None, Some("pw")),
926 AuthOutcome::Rejected
927 );
928 }
929
930 #[test]
931 fn user_auth_missing_password_rejected() {
932 let s = store_with_alice();
933 assert_eq!(
934 authenticate_connect(&s, Some("pw"), Some("alice"), None),
935 AuthOutcome::Rejected
936 );
937 }
938
939 #[test]
940 fn user_auth_ignores_shared_password_when_users_present() {
941 let s = store_with_alice();
944 assert_eq!(
945 authenticate_connect(&s, Some("shared"), None, Some("shared")),
946 AuthOutcome::Rejected
947 );
948 }
949}