1use std::collections::{HashMap, HashSet};
7use std::fs::{File, OpenOptions, TryLockError};
8use std::os::unix::fs::{FileTypeExt, MetadataExt, OpenOptionsExt, PermissionsExt};
9use std::path::{Component, Path, PathBuf};
10use std::sync::Arc;
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::time::{Duration, Instant};
13
14use tokio::net::UnixListener;
15use tokio::sync::{mpsc, watch};
16use tokio::task::JoinSet;
17
18use crate::Instance;
19use crate::auth::crypto::{PublicKey, generate_challenge, verify_challenge_response};
20use crate::auth::errors::AuthError;
21use crate::auth::types::{Permission, SigKey};
22use crate::auth::validation::permissions::resolve_identity_permission;
23use crate::backend::{
24 CacheScope, StagingToken, StoreStateLifecycle, StoreStateRequest, VerificationStatus,
25};
26use crate::database::Database;
27use crate::entry::ID;
28use crate::instance::{CallbackId, WriteSource};
29use crate::service::error::ServiceError;
30use crate::service::protocol::{
31 AuthenticatedDbRequest, DatabaseOp, HandshakeAck, MergeState, Notification, PROTOCOL_VERSION,
32 ServerFrame, ServiceRequest, ServiceResponse, read_frame, write_frame,
33};
34use crate::user::system_databases::lookup_user_record;
35
36type ConnectionId = u64;
41
42const TOKEN_IDLE_TTL: Duration = Duration::from_secs(5 * 60);
45
46struct SessionStaging {
50 backend: StagingToken,
51 user_uuid: String,
52 database: ID,
53 published: bool,
54 chunks: HashMap<u64, Vec<u8>>,
55 last_used: Instant,
56}
57
58struct ConnectionContext {
76 conn_id: ConnectionId,
77 tx: mpsc::UnboundedSender<ServerFrame>,
78 instance: Instance,
79 subscribed: std::sync::Mutex<HashMap<ID, CallbackId>>,
80 staging: std::sync::Mutex<HashMap<String, SessionStaging>>,
81 token_idle_ttl: Duration,
82}
83
84impl ConnectionContext {
85 fn subscribed_lock(&self) -> std::sync::MutexGuard<'_, HashMap<ID, CallbackId>> {
86 self.subscribed
87 .lock()
88 .unwrap_or_else(|poisoned| poisoned.into_inner())
89 }
90
91 fn cleanup_expired(&self) -> Vec<StagingToken> {
94 let now = Instant::now();
95 let mut expired = Vec::new();
96 self.staging.lock().unwrap().retain(|_, value| {
97 let keep = now.duration_since(value.last_used) <= self.token_idle_ttl;
98 if !keep && !value.published {
99 expired.push(value.backend.clone());
100 }
101 keep
102 });
103 expired
104 }
105}
106
107struct ConnectionGuard {
113 ctx: Arc<ConnectionContext>,
114}
115
116impl Drop for ConnectionGuard {
117 fn drop(&mut self) {
118 let subs = self.ctx.subscribed_lock();
119 if !subs.is_empty() {
120 tracing::debug!(
121 conn_id = self.ctx.conn_id,
122 "Unregistering {} subscriptions on disconnect",
123 subs.len()
124 );
125 }
126 for (tree_id, id) in subs.iter() {
127 self.ctx.instance.remove_write_callback(tree_id, *id);
128 }
129 drop(subs);
130 let staging = std::mem::take(&mut *self.ctx.staging.lock().unwrap())
131 .into_values()
132 .filter(|value| !value.published)
133 .map(|value| value.backend)
134 .collect::<Vec<_>>();
135 if !staging.is_empty() {
136 let backend = self.ctx.instance.backend().clone();
137 tokio::spawn(async move {
138 for token in staging {
139 let _ = backend.abort_store_state(token).await;
140 }
141 });
142 }
143 }
144}
145
146#[derive(Debug, Clone)]
158enum ConnectionState {
159 PreAuth,
161 AwaitingProof {
163 username: String,
164 user_uuid: String,
165 challenge: Vec<u8>,
166 expected_pubkey: PublicKey,
167 },
168 #[allow(dead_code)] Authenticated {
185 username: String,
186 user_uuid: String,
187 login_pubkey: PublicKey,
188 session_keyset: HashSet<PublicKey>,
189 pending_key_challenges: HashMap<PublicKey, Vec<u8>>,
190 },
191}
192
193pub struct ServiceServer {
203 instance: Instance,
204 socket_path: PathBuf,
205 listener: UnixListener,
206 socket_identity: SocketIdentity,
207 token_idle_ttl: Duration,
208 _lock_file: File,
209}
210
211#[derive(Clone, Copy)]
212struct SocketIdentity {
213 device: u64,
214 inode: u64,
215}
216
217impl SocketIdentity {
218 fn from_metadata(metadata: &std::fs::Metadata) -> Self {
219 Self {
220 device: metadata.dev(),
221 inode: metadata.ino(),
222 }
223 }
224
225 fn matches(self, metadata: &std::fs::Metadata) -> bool {
226 self.device == metadata.dev() && self.inode == metadata.ino()
227 }
228}
229
230impl ServiceServer {
231 pub async fn bind(instance: Instance, socket_path: impl Into<PathBuf>) -> crate::Result<Self> {
237 Self::bind_inner(instance, socket_path.into(), TOKEN_IDLE_TTL).await
238 }
239
240 #[cfg(feature = "testing")]
242 pub async fn bind_with_token_idle_ttl_for_test(
243 instance: Instance,
244 socket_path: impl Into<PathBuf>,
245 ttl: Duration,
246 ) -> crate::Result<Self> {
247 Self::bind_inner(instance, socket_path.into(), ttl).await
248 }
249
250 async fn bind_inner(
251 instance: Instance,
252 socket_path: PathBuf,
253 token_idle_ttl: Duration,
254 ) -> crate::Result<Self> {
255 let socket_path = absolute_socket_path(socket_path)?;
256 prepare_parent(&socket_path).await?;
257 let lock_file = acquire_endpoint_lock(&socket_path)?;
258 recover_stale_socket(&socket_path).await?;
259
260 let listener = UnixListener::bind(&socket_path)?;
261 let socket_identity = match prepare_bound_socket(&socket_path) {
262 Ok(identity) => identity,
263 Err(error) => {
264 remove_socket_if_owned(&socket_path, None);
265 return Err(error);
266 }
267 };
268
269 Ok(Self {
270 instance,
271 socket_path,
272 listener,
273 socket_identity,
274 token_idle_ttl,
275 _lock_file: lock_file,
276 })
277 }
278
279 pub fn socket_path(&self) -> &Path {
281 &self.socket_path
282 }
283
284 pub async fn run(self, mut shutdown: watch::Receiver<()>) -> crate::Result<()> {
289 tracing::info!("Service server listening on {}", self.socket_path.display());
290
291 let next_conn_id = Arc::new(AtomicU64::new(1));
296
297 let mut handlers = JoinSet::new();
303
304 let result = loop {
305 tokio::select! {
306 accept_result = self.listener.accept() => {
307 match accept_result {
308 Ok((stream, _addr)) => {
309 let instance = self.instance.clone();
310 let token_idle_ttl = self.token_idle_ttl;
311 let conn_id = next_conn_id.fetch_add(1, Ordering::Relaxed);
312 handlers.spawn(async move {
313 if let Err(e) = handle_connection(stream, instance, conn_id, token_idle_ttl).await {
314 tracing::debug!(conn_id, "Connection handler error: {e}");
315 }
316 });
317 }
318 Err(e) => break Err(e.into()),
319 }
320 }
321 _ = handlers.join_next(), if !handlers.is_empty() => {}
326 _ = shutdown.changed() => {
327 tracing::info!("Service server shutting down");
328 break Ok(());
329 }
330 }
331 };
332
333 handlers.abort_all();
338 while handlers.join_next().await.is_some() {}
342
343 result
344 }
345}
346
347const PARENT_MODE: u32 = 0o700;
348const SOCKET_MODE: u32 = 0o660;
349const STALE_PROBE_TIMEOUT: Duration = Duration::from_millis(250);
350
351fn effective_uid() -> u32 {
352 unsafe { libc::geteuid() }
353}
354
355fn absolute_socket_path(path: PathBuf) -> std::io::Result<PathBuf> {
356 let path = if path.is_absolute() {
357 path
358 } else {
359 std::env::current_dir()?.join(path)
360 };
361 if path.file_name().is_none() || path.parent().is_none() {
362 return Err(std::io::Error::new(
363 std::io::ErrorKind::InvalidInput,
364 format!(
365 "service endpoint {} has no parent or file name",
366 path.display()
367 ),
368 ));
369 }
370 Ok(path)
371}
372
373async fn prepare_parent(socket_path: &Path) -> crate::Result<()> {
374 let parent = socket_path
375 .parent()
376 .expect("absolute socket path has a parent");
377 let mut current = PathBuf::new();
378
379 for component in parent.components() {
380 match component {
381 Component::RootDir | Component::Prefix(_) => {
382 current.push(component.as_os_str());
383 continue;
384 }
385 Component::CurDir => continue,
386 Component::ParentDir => {
387 return Err(std::io::Error::new(
388 std::io::ErrorKind::InvalidInput,
389 format!("service endpoint parent {} contains '..'", parent.display()),
390 )
391 .into());
392 }
393 Component::Normal(name) => current.push(name),
394 }
395
396 match std::fs::symlink_metadata(¤t) {
397 Ok(metadata) => validate_directory_component(¤t, &metadata)?,
398 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
399 let mut builder = tokio::fs::DirBuilder::new();
400 builder.mode(PARENT_MODE);
401 match builder.create(¤t).await {
402 Ok(()) => {
403 tokio::fs::set_permissions(
404 ¤t,
405 std::fs::Permissions::from_mode(PARENT_MODE),
406 )
407 .await?;
408 }
409 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
410 Err(error) => return Err(error.into()),
411 }
412 let metadata = std::fs::symlink_metadata(¤t)?;
413 validate_directory_component(¤t, &metadata)?;
414 }
415 Err(error) => return Err(error.into()),
416 }
417 }
418
419 let metadata = std::fs::symlink_metadata(parent)?;
420 let mode = metadata.permissions().mode();
421 if metadata.uid() != effective_uid() || mode & 0o022 != 0 {
422 return Err(std::io::Error::new(
423 std::io::ErrorKind::PermissionDenied,
424 format!(
425 "service endpoint parent {} must be owned by the daemon user and not writable by group or others",
426 parent.display()
427 ),
428 )
429 .into());
430 }
431 Ok(())
432}
433
434fn validate_directory_component(path: &Path, metadata: &std::fs::Metadata) -> crate::Result<()> {
435 if !metadata.file_type().is_dir() {
436 return Err(std::io::Error::new(
437 std::io::ErrorKind::PermissionDenied,
438 format!(
439 "service endpoint path component {} must be a directory, not a symlink or file",
440 path.display()
441 ),
442 )
443 .into());
444 }
445 Ok(())
446}
447
448fn lock_path(socket_path: &Path) -> PathBuf {
449 let mut path = socket_path.as_os_str().to_owned();
450 path.push(".lock");
451 PathBuf::from(path)
452}
453
454fn acquire_endpoint_lock(socket_path: &Path) -> crate::Result<File> {
455 let path = lock_path(socket_path);
456 let file = OpenOptions::new()
457 .read(true)
458 .write(true)
459 .create(true)
460 .truncate(false)
461 .mode(0o600)
462 .open(&path)?;
463 if file.metadata()?.permissions().mode() & 0o777 != 0o600 {
464 file.set_permissions(std::fs::Permissions::from_mode(0o600))?;
465 }
466 file.try_lock().map_err(|error| match error {
467 TryLockError::WouldBlock => std::io::Error::new(
468 std::io::ErrorKind::AddrInUse,
469 format!(
470 "service endpoint {} is already owned",
471 socket_path.display()
472 ),
473 ),
474 TryLockError::Error(error) => error,
475 })?;
476 Ok(file)
477}
478
479async fn recover_stale_socket(socket_path: &Path) -> crate::Result<()> {
480 let metadata = match tokio::fs::symlink_metadata(socket_path).await {
481 Ok(metadata) => metadata,
482 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
483 Err(error) => return Err(error.into()),
484 };
485 if !metadata.file_type().is_socket() {
486 return Err(std::io::Error::new(
487 std::io::ErrorKind::AddrInUse,
488 format!(
489 "refusing to replace non-socket service endpoint {}",
490 socket_path.display()
491 ),
492 )
493 .into());
494 }
495
496 match tokio::time::timeout(
497 STALE_PROBE_TIMEOUT,
498 tokio::net::UnixStream::connect(socket_path),
499 )
500 .await
501 {
502 Err(_) => Err(std::io::Error::new(
503 std::io::ErrorKind::TimedOut,
504 format!(
505 "timed out probing service endpoint {}",
506 socket_path.display()
507 ),
508 )
509 .into()),
510 Ok(Ok(_)) => Err(std::io::Error::new(
511 std::io::ErrorKind::AddrInUse,
512 format!("service endpoint {} is live", socket_path.display()),
513 )
514 .into()),
515 Ok(Err(error))
516 if matches!(
517 error.kind(),
518 std::io::ErrorKind::ConnectionRefused | std::io::ErrorKind::NotFound
519 ) =>
520 {
521 tokio::fs::remove_file(socket_path).await?;
522 Ok(())
523 }
524 Ok(Err(error)) => Err(error.into()),
525 }
526}
527
528fn prepare_bound_socket(socket_path: &Path) -> crate::Result<SocketIdentity> {
529 std::fs::set_permissions(socket_path, std::fs::Permissions::from_mode(SOCKET_MODE))?;
530 let metadata = std::fs::symlink_metadata(socket_path)?;
531 if !metadata.file_type().is_socket()
532 || metadata.uid() != effective_uid()
533 || metadata.permissions().mode() & 0o777 != SOCKET_MODE
534 {
535 return Err(std::io::Error::other(format!(
536 "bound service endpoint has incorrect type, owner, or permissions: {}",
537 socket_path.display()
538 ))
539 .into());
540 }
541 Ok(SocketIdentity::from_metadata(&metadata))
542}
543
544fn remove_socket_if_owned(socket_path: &Path, identity: Option<SocketIdentity>) {
545 let Ok(metadata) = std::fs::symlink_metadata(socket_path) else {
546 return;
547 };
548 if metadata.file_type().is_socket()
549 && identity.is_none_or(|expected| expected.matches(&metadata))
550 {
551 let _ = std::fs::remove_file(socket_path);
552 }
553}
554
555impl Drop for ServiceServer {
556 fn drop(&mut self) {
557 remove_socket_if_owned(&self.socket_path, Some(self.socket_identity));
558 }
559}
560
561async fn handle_connection(
581 stream: tokio::net::UnixStream,
582 instance: Instance,
583 conn_id: ConnectionId,
584 token_idle_ttl: Duration,
585) -> crate::Result<()> {
586 let (mut reader, mut writer) = tokio::io::split(stream);
587
588 let handshake: crate::service::protocol::Handshake = match read_frame(&mut reader).await? {
590 Some(h) => h,
591 None => return Ok(()), };
593
594 if handshake.protocol_version != PROTOCOL_VERSION {
595 let ack = HandshakeAck {
597 protocol_version: PROTOCOL_VERSION,
598 };
599 write_frame(&mut writer, &ack).await?;
600 return Err(crate::Error::Io(std::io::Error::new(
601 std::io::ErrorKind::InvalidData,
602 format!(
603 "Protocol version mismatch: client={}, server={}",
604 handshake.protocol_version, PROTOCOL_VERSION
605 ),
606 )));
607 }
608
609 let ack = HandshakeAck {
611 protocol_version: PROTOCOL_VERSION,
612 };
613 write_frame(&mut writer, &ack).await?;
614
615 let (frame_tx, mut frame_rx) = mpsc::unbounded_channel::<ServerFrame>();
644 let writer_task = tokio::spawn(async move {
645 while let Some(frame) = frame_rx.recv().await {
646 if let Err(e) = write_frame(&mut writer, &frame).await {
647 tracing::debug!(conn_id, "Connection writer error: {e}");
648 break;
649 }
650 }
651 });
652
653 let ctx = Arc::new(ConnectionContext {
654 conn_id,
655 tx: frame_tx.clone(),
656 instance: instance.clone(),
657 subscribed: std::sync::Mutex::new(HashMap::new()),
658 staging: std::sync::Mutex::new(HashMap::new()),
659 token_idle_ttl,
660 });
661 let guard = ConnectionGuard { ctx: ctx.clone() };
662
663 let mut state = ConnectionState::PreAuth;
666 let loop_result: crate::Result<()> = async {
667 loop {
668 let request: ServiceRequest = match read_frame(&mut reader).await? {
669 Some(req) => req,
670 None => break, };
672
673 let response = dispatch(&instance, &mut state, &ctx, request).await;
674 if frame_tx
675 .send(ServerFrame::Response(Box::new(response)))
676 .is_err()
677 {
678 break;
680 }
681 }
682 Ok(())
683 }
684 .await;
685
686 drop(frame_tx);
708 drop(guard);
709 drop(ctx);
710 let _ = writer_task.await;
711
712 loop_result
720}
721
722async fn dispatch(
724 instance: &Instance,
725 state: &mut ConnectionState,
726 ctx: &ConnectionContext,
727 request: ServiceRequest,
728) -> ServiceResponse {
729 match dispatch_inner(instance, state, ctx, request).await {
730 Ok(resp) => resp,
731 Err(e) => ServiceResponse::Error(ServiceError::from(&e)),
732 }
733}
734
735async fn dispatch_inner(
737 instance: &Instance,
738 state: &mut ConnectionState,
739 ctx: &ConnectionContext,
740 request: ServiceRequest,
741) -> crate::Result<ServiceResponse> {
742 for token in ctx.cleanup_expired() {
743 let _ = instance.backend().abort_store_state(token).await;
744 }
745 match request {
746 ServiceRequest::TrustedLoginUser { username } => {
748 handle_trusted_login_user(instance, state, username).await
749 }
750 ServiceRequest::TrustedLoginProve { signature } => {
751 handle_trusted_login_prove(state, &signature)
752 }
753
754 ServiceRequest::GetInstanceMetadata => {
756 let metadata = instance.backend().get_instance_metadata().await?;
757 Ok(ServiceResponse::InstanceMetadata(metadata))
758 }
759
760 ServiceRequest::SessionKeyChallenge { pubkey } => {
762 handle_session_key_challenge(state, pubkey)
763 }
764 ServiceRequest::SessionKeyRegister { pubkey, signature } => {
765 handle_session_key_register(state, pubkey, &signature)
766 }
767
768 ServiceRequest::AuthenticatedDb(inner) => {
777 let (login_pubkey, keyset_snapshot, session_user_uuid) = match state {
778 ConnectionState::Authenticated {
779 login_pubkey,
780 session_keyset,
781 user_uuid,
782 ..
783 } => (
784 login_pubkey.clone(),
785 session_keyset.clone(),
786 user_uuid.clone(),
787 ),
788 _ => {
789 return Err(crate::Error::Auth(Box::new(
790 AuthError::InvalidAuthConfiguration {
791 reason: format!(
792 "database operation requires an authenticated connection; complete TrustedLogin* first (op: {:?})",
793 inner.op
794 ),
795 },
796 )));
797 }
798 };
799
800 let AuthenticatedDbRequest {
801 root_id,
802 identity,
803 op,
804 } = *inner;
805
806 let is_submit = matches!(op, DatabaseOp::SubmitSignedEntry { .. });
825
826 let is_set_metadata = matches!(op, DatabaseOp::SetInstanceMetadata { .. });
836
837 let acting_pubkey = if is_submit {
841 identity
845 .hint()
846 .pubkey
847 .clone()
848 .unwrap_or_else(|| login_pubkey.clone())
849 } else {
850 resolve_acting_pubkey(&identity, &login_pubkey, &keyset_snapshot)?
851 };
852
853 if is_set_metadata {
859 gate_tree_permission(
860 instance,
861 &acting_pubkey,
862 &identity,
863 instance.databases_db_id(),
864 Permission::Admin(0),
865 true,
866 )
867 .await?;
868 } else if !is_submit {
869 gate_tree_permission(
870 instance,
871 &acting_pubkey,
872 &identity,
873 &root_id,
874 op.required_permission(),
875 false,
876 )
877 .await?;
878 }
879
880 dispatch_database_op(
881 instance,
882 ctx,
883 &acting_pubkey,
884 &identity,
885 &session_user_uuid,
886 root_id,
887 op,
888 )
889 .await
890 }
891 }
892}
893
894fn resolve_acting_pubkey(
905 identity: &SigKey,
906 login_pubkey: &PublicKey,
907 session_keyset: &HashSet<PublicKey>,
908) -> crate::Result<PublicKey> {
909 match &identity.hint().pubkey {
910 Some(claimed) if session_keyset.contains(claimed) => Ok(claimed.clone()),
911 Some(claimed) => Err(crate::Error::Auth(Box::new(
912 AuthError::SigningKeyMismatch {
913 reason: format!(
914 "request identity claims pubkey '{claimed}' but it is not in the session keyset; \
915 register it first via SessionKeyChallenge/SessionKeyRegister"
916 ),
917 },
918 ))),
919 None => Ok(login_pubkey.clone()),
920 }
921}
922
923async fn dispatch_database_op(
930 instance: &Instance,
931 ctx: &ConnectionContext,
932 acting_pubkey: &PublicKey,
933 identity: &SigKey,
934 user_uuid: &str,
935 root_id: ID,
936 op: DatabaseOp,
937) -> crate::Result<ServiceResponse> {
938 match op {
939 DatabaseOp::ResolveStoreState { request } => {
940 let request = session_store_state_request(request, user_uuid, &root_id)?;
941 let view = match instance.backend().resolve_store_state(&request).await? {
942 Some(view) => Some(view),
943 None => {
947 let shared = StoreStateRequest {
948 scope: CacheScope::Shared,
949 ..request.clone()
950 };
951 instance.backend().resolve_store_state(&shared).await?
952 }
953 };
954 let token = view.map(|view| {
955 let token = random_token();
956 ctx.staging.lock().unwrap().insert(
957 token.clone(),
958 SessionStaging {
959 backend: StagingToken {
960 namespace_id: view.namespace_id,
961 target: request.clone(),
962 },
963 user_uuid: user_uuid.to_string(),
964 database: request.database.clone(),
965 published: true,
966 chunks: HashMap::new(),
967 last_used: Instant::now(),
968 },
969 );
970 token
971 });
972 Ok(ServiceResponse::RecordView(token))
973 }
974 DatabaseOp::BeginStoreStateStaging { request } => {
975 let request = session_store_state_request(request, user_uuid, &root_id)?;
976 let backend = instance
977 .backend()
978 .begin_store_state_staging(request.clone())
979 .await?;
980 let token = random_token();
981 ctx.staging.lock().unwrap().insert(
982 token.clone(),
983 SessionStaging {
984 backend,
985 user_uuid: user_uuid.to_string(),
986 database: request.database,
987 published: false,
988 chunks: HashMap::new(),
989 last_used: Instant::now(),
990 },
991 );
992 Ok(ServiceResponse::Token(token))
993 }
994 DatabaseOp::StageStoreStateRecords {
995 token,
996 chunk_id,
997 records,
998 } => {
999 let digest = serde_json::to_vec(&records)?;
1000 if digest.len() > crate::service::protocol::MAX_RECORD_CHUNK_BYTES as usize {
1001 return Err(crate::backend::BackendError::RecordTooLarge {
1002 encoded_bytes: digest.len(),
1003 }
1004 .into());
1005 }
1006 let records = records
1007 .into_iter()
1008 .collect::<crate::backend::RecordMutations>();
1009 let (backend, duplicate) =
1010 session_staging(ctx, user_uuid, &root_id, &token, chunk_id, &digest)?;
1011 if duplicate {
1012 return Ok(ServiceResponse::Ok);
1013 }
1014 if let Err(error) = instance
1015 .backend()
1016 .stage_store_state_records(&backend, records)
1017 .await
1018 {
1019 if let Some(value) = ctx.staging.lock().unwrap().get_mut(&token) {
1020 value.chunks.remove(&chunk_id);
1021 }
1022 return Err(error);
1023 }
1024 Ok(ServiceResponse::Ok)
1025 }
1026 DatabaseOp::PublishStoreState { token } => {
1027 let staged = ctx
1028 .staging
1029 .lock()
1030 .unwrap()
1031 .remove(&token)
1032 .filter(|value| value.user_uuid == user_uuid && value.database == root_id)
1033 .ok_or(crate::backend::BackendError::InvalidStoreStateStagingToken)?;
1034 let backend_token = staged.backend;
1035 let view = match instance
1036 .backend()
1037 .publish_store_state(backend_token.clone())
1038 .await
1039 {
1040 Ok(view) => view,
1041 Err(error) => {
1042 let _ = instance.backend().abort_store_state(backend_token).await;
1043 return Err(error);
1044 }
1045 };
1046 let view_token = random_token();
1047 let database = root_id.clone();
1048 ctx.staging.lock().unwrap().insert(
1049 view_token.clone(),
1050 SessionStaging {
1051 backend: StagingToken {
1052 namespace_id: view.namespace_id,
1053 target: StoreStateRequest {
1054 database: database.clone(),
1055 store: String::new(),
1056 lifecycle: StoreStateLifecycle::Derived,
1057 scope: CacheScope::User(user_uuid.to_string()),
1058 projection: crate::backend::ProjectionDescriptor {
1059 name: String::new(),
1060 version: 0,
1061 },
1062 source_key: Vec::new(),
1063 },
1064 },
1065 user_uuid: user_uuid.to_string(),
1066 database,
1067 published: true,
1068 chunks: HashMap::new(),
1069 last_used: Instant::now(),
1070 },
1071 );
1072 Ok(ServiceResponse::RecordView(Some(view_token)))
1073 }
1074 DatabaseOp::AbortStoreState { token } => {
1075 let staged = {
1076 let mut staging = ctx.staging.lock().unwrap();
1077 if staging
1078 .get(&token)
1079 .is_some_and(|value| value.user_uuid != user_uuid || value.database != root_id)
1080 {
1081 return Err(crate::backend::BackendError::InvalidStoreStateStagingToken.into());
1082 }
1083 staging.remove(&token)
1084 };
1085 if let Some(staged) = staged {
1086 instance.backend().abort_store_state(staged.backend).await?;
1087 }
1088 Ok(ServiceResponse::Ok)
1089 }
1090 DatabaseOp::StoreStateRecordGet { view, key } => {
1091 let namespace_id = {
1092 let mut staged = ctx.staging.lock().unwrap();
1093 staged
1094 .get_mut(&view)
1095 .filter(|value| {
1096 value.user_uuid == user_uuid && value.database == root_id && value.published
1097 })
1098 .map(|value| {
1099 value.last_used = Instant::now();
1100 value.backend.namespace_id.clone()
1101 })
1102 .ok_or(crate::backend::BackendError::InvalidStoreStateView)?
1103 };
1104 let record = instance
1105 .backend()
1106 .store_state_record_get(&crate::backend::RecordView { namespace_id }, &key)
1107 .await?;
1108 ensure_record_fits(record.as_ref())?;
1109 Ok(ServiceResponse::Record(record))
1110 }
1111 DatabaseOp::StoreStateRecordScan {
1112 view,
1113 range,
1114 after,
1115 max_records,
1116 max_encoded_bytes,
1117 } => {
1118 let backend = {
1119 let mut staged = ctx.staging.lock().unwrap();
1120 staged
1121 .get_mut(&view)
1122 .filter(|value| {
1123 value.user_uuid == user_uuid && value.database == root_id && value.published
1124 })
1125 .map(|value| {
1126 value.last_used = Instant::now();
1127 value.backend.clone()
1128 })
1129 .ok_or(crate::backend::BackendError::InvalidStoreStateView)?
1130 };
1131 let page = bounded_page(
1132 instance
1133 .backend()
1134 .store_state_record_scan(
1135 &crate::backend::RecordView {
1136 namespace_id: backend.namespace_id,
1137 },
1138 &range,
1139 after.as_deref(),
1140 max_records as usize,
1141 )
1142 .await?,
1143 max_encoded_bytes,
1144 )?;
1145 Ok(ServiceResponse::RecordPage(page))
1146 }
1147
1148 DatabaseOp::GetEntry { id } => {
1149 let entry = instance.backend().get(&id).await?;
1150 gate_entry_read(instance, acting_pubkey, identity, &entry).await?;
1154 Ok(ServiceResponse::Entry(entry))
1155 }
1156
1157 DatabaseOp::GetVerifiedTips => {
1158 let db = Database::open(instance, &root_id).await?;
1162 let snapshot = db.snapshot().await?;
1163 Ok(ServiceResponse::Ids(snapshot))
1164 }
1165
1166 DatabaseOp::SubmitSignedEntry { entry } => {
1167 let previous_tips = instance.snapshot(&root_id).await?;
1168 instance
1184 .put_entry(
1185 &root_id,
1186 VerificationStatus::Unverified,
1187 *entry,
1188 WriteSource::Local,
1189 )
1190 .await?;
1191 Database::open(instance, &root_id)
1192 .await?
1193 .verify_with_source(WriteSource::Local, Some(previous_tips))
1194 .await?;
1195 Ok(ServiceResponse::Ok)
1196 }
1197
1198 DatabaseOp::BeginTransaction { stores, scope } => {
1199 let db = Database::open(instance, &root_id).await?;
1203 let ctx = db.transaction_context(&stores, scope).await?;
1204 Ok(ServiceResponse::TransactionContext(ctx))
1205 }
1206
1207 DatabaseOp::GetStoreState { store } => {
1208 let db = Database::open(instance, &root_id).await?;
1213 let value = db.get_store_state(&store).await?;
1214 Ok(ServiceResponse::CrdtValue(value))
1215 }
1216
1217 DatabaseOp::GetStoreEntries { store, tips, scope } => {
1218 let db = Database::open(instance, &root_id).await?;
1222 let entries = db.get_store_entries(&store, &tips, scope).await?;
1223 Ok(ServiceResponse::Entries(entries))
1224 }
1225
1226 DatabaseOp::GetStoreTipsUpToEntries { store, up_to } => {
1227 let db = Database::open(instance, &root_id).await?;
1228 let boundary = crate::Snapshot::from(up_to);
1229 let snapshot = db
1230 .ops()
1231 .store_snapshot_at(&root_id, &store, &boundary)
1232 .await?;
1233 Ok(ServiceResponse::Ids(snapshot))
1234 }
1235
1236 DatabaseOp::ComputeMergeState { store, entry_ids } => {
1237 let db = Database::open(instance, &root_id).await?;
1238 let slice = db
1239 .ops()
1240 .compute_merge_state(&root_id, &store, &entry_ids)
1241 .await?;
1242 Ok(ServiceResponse::MergeState(MergeState {
1243 merge_base: slice.merge_base,
1244 path: slice.path,
1245 }))
1246 }
1247
1248 DatabaseOp::SetInstanceMetadata { metadata } => {
1249 instance.backend().set_instance_metadata(&metadata).await?;
1252 Ok(ServiceResponse::Ok)
1253 }
1254
1255 DatabaseOp::SubscribeWrites { tips } => {
1256 {
1283 let subs = ctx.subscribed_lock();
1284 if subs.contains_key(&root_id) {
1285 return Ok(ServiceResponse::Ok);
1286 }
1287 }
1288 let tx = ctx.tx.clone();
1289 let tree_lock = instance.tree_lock(&root_id);
1300 let tree_guard = tree_lock.lock().await;
1301 let initial_tips = if tips.is_empty() {
1306 instance.snapshot(&root_id).await?
1310 } else {
1311 tips
1312 };
1313 let id = instance.register_write_callback(
1314 root_id.clone(),
1315 initial_tips,
1316 move |event, db| {
1317 let frame = ServerFrame::Notification(Notification::DatabaseWrite {
1350 root_id: db.root_id().clone(),
1351 previous_tips: event.previous_tips().clone(),
1352 post_tips: event.post_tips().clone(),
1353 source: event.source(),
1354 });
1355 let _ = tx.send(frame);
1356 async move { Ok(()) }
1357 },
1358 );
1359 drop(tree_guard);
1362 use std::collections::hash_map::Entry as MapEntry;
1372 let mut subs = ctx.subscribed_lock();
1373 match subs.entry(root_id.clone()) {
1374 MapEntry::Vacant(slot) => {
1375 slot.insert(id);
1376 }
1377 MapEntry::Occupied(_) => {
1378 drop(subs);
1379 instance.remove_write_callback(&root_id, id);
1380 }
1381 }
1382 Ok(ServiceResponse::Ok)
1383 }
1384
1385 DatabaseOp::UnsubscribeWrites => {
1386 let removed = ctx.subscribed_lock().remove(&root_id);
1389 if let Some(id) = removed {
1390 instance.remove_write_callback(&root_id, id);
1391 }
1392 Ok(ServiceResponse::Ok)
1393 }
1394
1395 DatabaseOp::CreateTicket => Ok(ServiceResponse::DatabaseTicket(
1396 crate::user::ticket_locator(instance, &root_id).await?,
1397 )),
1398 }
1399}
1400
1401fn random_token() -> String {
1402 uuid::Uuid::new_v4().to_string()
1403}
1404
1405fn ensure_record_fits(record: Option<&Vec<u8>>) -> crate::Result<()> {
1407 if let Some(record) = record {
1408 let encoded = serde_json::to_vec(record)?.len();
1409 if encoded > crate::service::protocol::MAX_RECORD_CHUNK_BYTES as usize {
1410 return Err(crate::backend::BackendError::RecordTooLarge {
1411 encoded_bytes: encoded,
1412 }
1413 .into());
1414 }
1415 }
1416 Ok(())
1417}
1418
1419fn bounded_page(
1423 mut page: crate::backend::RecordPage,
1424 max_encoded_bytes: u32,
1425) -> crate::Result<crate::backend::RecordPage> {
1426 let limit = max_encoded_bytes.min(crate::service::protocol::MAX_RECORD_CHUNK_BYTES) as usize;
1427 let mut size = 2usize;
1428 let mut keep = 0usize;
1429 for record in &page.records {
1430 let record_size = serde_json::to_vec(record)?.len();
1431 if record_size > limit {
1432 return Err(crate::backend::BackendError::RecordTooLarge {
1433 encoded_bytes: record_size,
1434 }
1435 .into());
1436 }
1437 if size + record_size > limit {
1438 break;
1439 }
1440 size += record_size;
1441 keep += 1;
1442 }
1443 if keep < page.records.len() {
1444 page.records.truncate(keep);
1445 page.next = page.records.last().map(|(key, _)| key.clone());
1446 }
1447 Ok(page)
1448}
1449
1450fn session_store_state_request(
1458 mut request: StoreStateRequest,
1459 user_uuid: &str,
1460 database: &ID,
1461) -> crate::Result<StoreStateRequest> {
1462 if request.database != *database || request.lifecycle != StoreStateLifecycle::Derived {
1463 return Err(crate::backend::BackendError::InvalidStoreStateStagingToken.into());
1464 }
1465 match &request.scope {
1466 CacheScope::Shared => request.scope = CacheScope::User(user_uuid.to_string()),
1467 CacheScope::User(uuid) if uuid == user_uuid => {}
1468 CacheScope::User(_) => {
1469 return Err(crate::backend::BackendError::InvalidStoreStateStagingToken.into());
1470 }
1471 }
1472 Ok(request)
1473}
1474
1475fn session_staging(
1478 ctx: &ConnectionContext,
1479 user_uuid: &str,
1480 database: &ID,
1481 token: &str,
1482 chunk_id: u64,
1483 digest: &[u8],
1484) -> crate::Result<(StagingToken, bool)> {
1485 let mut staging = ctx.staging.lock().unwrap();
1486 let value = staging
1487 .get_mut(token)
1488 .filter(|value| value.user_uuid == user_uuid && value.database == *database)
1489 .ok_or(crate::backend::BackendError::InvalidStoreStateStagingToken)?;
1490 if let Some(existing) = value.chunks.get(&chunk_id) {
1491 if existing != digest {
1492 return Err(crate::backend::BackendError::InvalidStoreStateStagingToken.into());
1493 }
1494 return Ok((value.backend.clone(), true));
1495 }
1496 value.chunks.insert(chunk_id, digest.to_vec());
1497 value.last_used = Instant::now();
1498 Ok((value.backend.clone(), false))
1499}
1500
1501async fn handle_trusted_login_user(
1504 instance: &Instance,
1505 state: &mut ConnectionState,
1506 username: String,
1507) -> crate::Result<ServiceResponse> {
1508 let users_db = instance.users_db().await?;
1516 let (user_uuid, user_info) = match lookup_user_record(&users_db, &username).await {
1517 Ok(v) => v,
1518 Err(e) => {
1519 *state = ConnectionState::PreAuth;
1520 return Err(e);
1521 }
1522 };
1523
1524 let expected_pubkey = user_info.credentials.root_key_id.clone();
1525 let challenge = generate_challenge();
1526 *state = ConnectionState::AwaitingProof {
1527 username,
1528 user_uuid: user_uuid.clone(),
1529 challenge: challenge.clone(),
1530 expected_pubkey,
1531 };
1532 Ok(ServiceResponse::TrustedLoginChallenge {
1533 challenge,
1534 user_uuid,
1535 user_info,
1536 })
1537}
1538
1539fn handle_trusted_login_prove(
1543 state: &mut ConnectionState,
1544 signature: &[u8],
1545) -> crate::Result<ServiceResponse> {
1546 let (username, user_uuid, challenge, expected_pubkey) =
1547 match std::mem::replace(state, ConnectionState::PreAuth) {
1548 ConnectionState::AwaitingProof {
1549 username,
1550 user_uuid,
1551 challenge,
1552 expected_pubkey,
1553 } => (username, user_uuid, challenge, expected_pubkey),
1554 other => {
1555 *state = other;
1558 return Err(crate::Error::Io(std::io::Error::new(
1559 std::io::ErrorKind::InvalidData,
1560 "TrustedLoginProve received outside of AwaitingProof state",
1561 )));
1562 }
1563 };
1564
1565 match verify_challenge_response(&challenge, signature, &expected_pubkey) {
1566 Ok(()) => {
1567 let mut session_keyset = HashSet::new();
1568 session_keyset.insert(expected_pubkey.clone());
1569 *state = ConnectionState::Authenticated {
1570 username,
1571 user_uuid,
1572 login_pubkey: expected_pubkey,
1573 session_keyset,
1574 pending_key_challenges: HashMap::new(),
1575 };
1576 Ok(ServiceResponse::TrustedLoginOk)
1577 }
1578 Err(e) => {
1579 Err(crate::Error::Auth(Box::new(e)))
1581 }
1582 }
1583}
1584
1585fn handle_session_key_challenge(
1592 state: &mut ConnectionState,
1593 pubkey: PublicKey,
1594) -> crate::Result<ServiceResponse> {
1595 match state {
1596 ConnectionState::Authenticated {
1597 pending_key_challenges,
1598 ..
1599 } => {
1600 let challenge = generate_challenge();
1601 pending_key_challenges.insert(pubkey, challenge.clone());
1602 Ok(ServiceResponse::SessionKeyChallenge { challenge })
1603 }
1604 _ => Err(crate::Error::Auth(Box::new(
1605 AuthError::InvalidAuthConfiguration {
1606 reason: "SessionKeyChallenge requires an authenticated connection; \
1607 complete TrustedLogin* first"
1608 .to_string(),
1609 },
1610 ))),
1611 }
1612}
1613
1614fn handle_session_key_register(
1621 state: &mut ConnectionState,
1622 pubkey: PublicKey,
1623 signature: &[u8],
1624) -> crate::Result<ServiceResponse> {
1625 match state {
1626 ConnectionState::Authenticated {
1627 session_keyset,
1628 pending_key_challenges,
1629 ..
1630 } => {
1631 let challenge = pending_key_challenges.remove(&pubkey).ok_or_else(|| {
1632 crate::Error::Auth(Box::new(AuthError::InvalidAuthConfiguration {
1633 reason: format!(
1634 "no outstanding SessionKeyChallenge for pubkey '{pubkey}'; \
1635 issue the challenge before registering"
1636 ),
1637 }))
1638 })?;
1639 verify_challenge_response(&challenge, signature, &pubkey)
1640 .map_err(|e| crate::Error::Auth(Box::new(e)))?;
1641 session_keyset.insert(pubkey);
1642 Ok(ServiceResponse::Ok)
1643 }
1644 _ => Err(crate::Error::Auth(Box::new(
1645 AuthError::InvalidAuthConfiguration {
1646 reason: "SessionKeyRegister requires an authenticated connection; \
1647 complete TrustedLogin* first"
1648 .to_string(),
1649 },
1650 ))),
1651 }
1652}
1653
1654async fn gate_tree_permission(
1689 instance: &Instance,
1690 pubkey: &PublicKey,
1691 identity: &SigKey,
1692 tree_id: &ID,
1693 required: Permission,
1694 require_existing: bool,
1695) -> crate::Result<()> {
1696 let denied = || {
1697 crate::Error::Auth(Box::new(AuthError::PermissionDenied {
1698 reason: format!("tree {tree_id}: pubkey {pubkey} not permitted for {required:?}"),
1699 }))
1700 };
1701
1702 if !instance.has_database(tree_id).await {
1703 return if require_existing {
1704 Err(denied())
1705 } else {
1706 Ok(())
1707 };
1708 }
1709
1710 let database = Database::open(instance, tree_id).await?;
1711 let settings_store = database.get_settings().await?;
1712 let auth_settings = settings_store.auth_snapshot().await?;
1713
1714 let resolved =
1715 match resolve_identity_permission(pubkey, identity, &auth_settings, Some(instance)).await {
1716 Ok(p) => p,
1717 Err(_) => return Err(denied()),
1721 };
1722
1723 let allowed = match required {
1724 Permission::Read => true,
1725 Permission::Write(_) => resolved.can_write(),
1726 Permission::Admin(_) => resolved.can_admin(),
1727 };
1728
1729 if !allowed {
1730 return Err(denied());
1731 }
1732
1733 Ok(())
1734}
1735
1736async fn gate_entry_read(
1748 instance: &Instance,
1749 pubkey: &PublicKey,
1750 identity: &SigKey,
1751 entry: &crate::entry::Entry,
1752) -> crate::Result<()> {
1753 let owning_tree = entry.root().unwrap_or_else(|| entry.id());
1754 gate_tree_permission(
1758 instance,
1759 pubkey,
1760 identity,
1761 &owning_tree,
1762 Permission::Read,
1763 false,
1764 )
1765 .await
1766}
1767
1768#[cfg(test)]
1769mod tests {
1770 use super::*;
1771 use crate::backend::database::InMemory;
1772 use crate::service::protocol::{Handshake, write_frame};
1773
1774 fn private_tempdir() -> tempfile::TempDir {
1775 let dir = tempfile::tempdir().unwrap();
1776 std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(PARENT_MODE)).unwrap();
1777 dir
1778 }
1779
1780 async fn start_test_server() -> (
1782 PathBuf,
1783 watch::Sender<()>,
1784 tokio::task::JoinHandle<crate::Result<()>>,
1785 Instance,
1786 ) {
1787 let dir = private_tempdir();
1788 let socket_path = dir.keep().join("test.sock");
1789 let (instance, _admin) = Instance::create_backend(
1790 Box::new(InMemory::new()),
1791 crate::NewUser::passwordless("admin"),
1792 )
1793 .await
1794 .unwrap();
1795 let (tx, rx) = watch::channel(());
1796 let server = ServiceServer::bind(instance.clone(), socket_path.clone())
1797 .await
1798 .unwrap();
1799 let task = tokio::spawn(server.run(rx));
1800 (socket_path, tx, task, instance)
1801 }
1802
1803 #[tokio::test]
1804 async fn test_server_starts_and_shuts_down() {
1805 let (socket_path, tx, task, _instance) = start_test_server().await;
1806 assert!(socket_path.exists());
1807 drop(tx);
1808 task.await.unwrap().unwrap();
1809 assert!(!socket_path.exists());
1810 }
1811
1812 #[tokio::test]
1813 async fn test_bind_creates_private_nested_parents_and_group_socket() {
1814 let dir = private_tempdir();
1815 let parents = [dir.path().join("one"), dir.path().join("one/two")];
1816 let socket_path = parents[1].join("test.sock");
1817 let (instance, _admin) = Instance::create_backend(
1818 Box::new(InMemory::new()),
1819 crate::NewUser::passwordless("admin"),
1820 )
1821 .await
1822 .unwrap();
1823
1824 let server = ServiceServer::bind(instance, &socket_path).await.unwrap();
1825
1826 for parent in parents {
1827 assert_eq!(
1828 std::fs::metadata(parent).unwrap().permissions().mode() & 0o777,
1829 PARENT_MODE
1830 );
1831 }
1832 assert_eq!(
1833 std::fs::symlink_metadata(&socket_path)
1834 .unwrap()
1835 .permissions()
1836 .mode()
1837 & 0o777,
1838 SOCKET_MODE
1839 );
1840 drop(server);
1841 assert!(!socket_path.exists());
1842 }
1843
1844 #[tokio::test]
1845 async fn test_existing_group_traversable_parent_is_preserved() {
1846 let dir = private_tempdir();
1847 let parent = dir.path().join("shared");
1848 tokio::fs::create_dir(&parent).await.unwrap();
1849 tokio::fs::set_permissions(&parent, std::fs::Permissions::from_mode(0o750))
1850 .await
1851 .unwrap();
1852 let socket_path = parent.join("test.sock");
1853 let (instance, _admin) = Instance::create_backend(
1854 Box::new(InMemory::new()),
1855 crate::NewUser::passwordless("admin"),
1856 )
1857 .await
1858 .unwrap();
1859
1860 let server = ServiceServer::bind(instance, &socket_path).await.unwrap();
1861 let parent_metadata = std::fs::metadata(&parent).unwrap();
1862 let socket_metadata = std::fs::symlink_metadata(&socket_path).unwrap();
1863
1864 assert_eq!(parent_metadata.permissions().mode() & 0o777, 0o750);
1865 assert_eq!(socket_metadata.permissions().mode() & 0o777, 0o660);
1866 assert_eq!(socket_metadata.gid(), parent_metadata.gid());
1867 let lock_metadata = std::fs::symlink_metadata(lock_path(&socket_path)).unwrap();
1868 assert_eq!(
1869 lock_metadata.permissions().mode() & 0o777,
1870 0o600,
1871 "endpoint lock must remain owner-only"
1872 );
1873 drop(server);
1874 }
1875
1876 #[tokio::test]
1877 async fn test_existing_endpoint_lock_is_restricted() {
1878 let dir = private_tempdir();
1879 let socket_path = dir.path().join("test.sock");
1880 let lock_path = lock_path(&socket_path);
1881 tokio::fs::write(&lock_path, b"").await.unwrap();
1882 tokio::fs::set_permissions(&lock_path, std::fs::Permissions::from_mode(0o660))
1883 .await
1884 .unwrap();
1885 let (instance, _admin) = Instance::create_backend(
1886 Box::new(InMemory::new()),
1887 crate::NewUser::passwordless("admin"),
1888 )
1889 .await
1890 .unwrap();
1891
1892 let server = ServiceServer::bind(instance, &socket_path).await.unwrap();
1893 assert_eq!(
1894 std::fs::symlink_metadata(lock_path)
1895 .unwrap()
1896 .permissions()
1897 .mode()
1898 & 0o777,
1899 0o600,
1900 "existing endpoint lock must be restricted"
1901 );
1902 drop(server);
1903 }
1904
1905 #[tokio::test]
1906 async fn test_group_or_other_writable_parent_is_refused() {
1907 let dir = private_tempdir();
1908 for mode in [0o720, 0o702] {
1909 let parent = dir.path().join(format!("unsafe-{mode:o}"));
1910 tokio::fs::create_dir(&parent).await.unwrap();
1911 tokio::fs::set_permissions(&parent, std::fs::Permissions::from_mode(mode))
1912 .await
1913 .unwrap();
1914 let socket_path = parent.join("test.sock");
1915 let (instance, _admin) = Instance::create_backend(
1916 Box::new(InMemory::new()),
1917 crate::NewUser::passwordless("admin"),
1918 )
1919 .await
1920 .unwrap();
1921
1922 assert!(
1923 ServiceServer::bind(instance, &socket_path).await.is_err(),
1924 "mode {mode:o} must be refused"
1925 );
1926 assert!(!socket_path.exists());
1927 }
1928 }
1929
1930 #[tokio::test]
1931 async fn test_symlinked_parent_component_is_refused() {
1932 let dir = private_tempdir();
1933 let target = dir.path().join("target");
1934 tokio::fs::create_dir(&target).await.unwrap();
1935 let link = dir.path().join("link");
1936 std::os::unix::fs::symlink(&target, &link).unwrap();
1937 let socket_path = link.join("nested/test.sock");
1938 let (instance, _admin) = Instance::create_backend(
1939 Box::new(InMemory::new()),
1940 crate::NewUser::passwordless("admin"),
1941 )
1942 .await
1943 .unwrap();
1944
1945 assert!(ServiceServer::bind(instance, &socket_path).await.is_err());
1946 assert!(!target.join("nested").exists());
1947 }
1948
1949 #[test]
1950 fn test_permissive_umask_does_not_widen_created_parents() {
1951 let output = std::process::Command::new(std::env::current_exe().unwrap())
1952 .args([
1953 "--exact",
1954 "service::server::tests::permissive_umask_bind_helper",
1955 "--nocapture",
1956 ])
1957 .env("EIDETICA_TEST_PERMISSIVE_UMASK", "1")
1958 .output()
1959 .unwrap();
1960 assert!(
1961 output.status.success(),
1962 "child failed:\nstdout:\n{}\nstderr:\n{}",
1963 String::from_utf8_lossy(&output.stdout),
1964 String::from_utf8_lossy(&output.stderr)
1965 );
1966 }
1967
1968 #[tokio::test]
1969 async fn permissive_umask_bind_helper() {
1970 if std::env::var_os("EIDETICA_TEST_PERMISSIVE_UMASK").is_none() {
1971 return;
1972 }
1973
1974 unsafe { libc::umask(0) };
1975 let dir = private_tempdir();
1976 let parents = [dir.path().join("one"), dir.path().join("one/two")];
1977 let socket_path = parents[1].join("test.sock");
1978 let (instance, _admin) = Instance::create_backend(
1979 Box::new(InMemory::new()),
1980 crate::NewUser::passwordless("admin"),
1981 )
1982 .await
1983 .unwrap();
1984
1985 let server = ServiceServer::bind(instance, &socket_path).await.unwrap();
1986 for parent in parents {
1987 assert_eq!(
1988 std::fs::metadata(parent).unwrap().permissions().mode() & 0o777,
1989 PARENT_MODE
1990 );
1991 }
1992 drop(server);
1993 }
1994
1995 #[tokio::test]
1996 async fn test_wrong_protocol_version() {
1997 let (socket_path, _tx, _task, _instance) = start_test_server().await;
1998
1999 let stream = tokio::net::UnixStream::connect(&socket_path).await.unwrap();
2000 let (mut reader, mut writer) = tokio::io::split(stream);
2001
2002 let handshake = Handshake {
2004 protocol_version: PROTOCOL_VERSION.saturating_add(1),
2005 };
2006 write_frame(&mut writer, &handshake).await.unwrap();
2007
2008 let ack: Option<HandshakeAck> = read_frame(&mut reader).await.unwrap();
2010 let ack = ack.unwrap();
2011 assert_eq!(ack.protocol_version, PROTOCOL_VERSION);
2012
2013 let result: crate::Result<Option<ServiceResponse>> = read_frame(&mut reader).await;
2016 assert!(result.unwrap().is_none());
2017 }
2018
2019 #[test]
2032 fn connection_state_never_holds_private_key() {
2033 use crate::auth::crypto::{PrivateKey, generate_keypair};
2034 use std::any::TypeId;
2035
2036 fn assert_not_private_key<T: 'static>(_value: &T, label: &str) {
2037 assert_ne!(
2038 TypeId::of::<T>(),
2039 TypeId::of::<PrivateKey>(),
2040 "ConnectionState field `{label}` is PrivateKey — daemon must not hold plaintext keys"
2041 );
2042 }
2043
2044 let (_signing, pubkey) = generate_keypair();
2045 let states = [
2046 ConnectionState::PreAuth,
2047 ConnectionState::AwaitingProof {
2048 username: "u".to_string(),
2049 user_uuid: "uu".to_string(),
2050 challenge: vec![1, 2, 3],
2051 expected_pubkey: pubkey.clone(),
2052 },
2053 ConnectionState::Authenticated {
2054 username: "u".to_string(),
2055 user_uuid: "uu".to_string(),
2056 login_pubkey: pubkey.clone(),
2057 session_keyset: {
2058 let mut s = HashSet::new();
2059 s.insert(pubkey);
2060 s
2061 },
2062 pending_key_challenges: HashMap::new(),
2063 },
2064 ];
2065
2066 for state in &states {
2067 match state {
2068 ConnectionState::PreAuth => {}
2069 ConnectionState::AwaitingProof {
2070 username,
2071 user_uuid,
2072 challenge,
2073 expected_pubkey,
2074 } => {
2075 assert_not_private_key(username, "AwaitingProof::username");
2076 assert_not_private_key(user_uuid, "AwaitingProof::user_uuid");
2077 assert_not_private_key(challenge, "AwaitingProof::challenge");
2078 assert_not_private_key(expected_pubkey, "AwaitingProof::expected_pubkey");
2079 }
2080 ConnectionState::Authenticated {
2081 username,
2082 user_uuid,
2083 login_pubkey,
2084 session_keyset,
2085 pending_key_challenges,
2086 } => {
2087 assert_not_private_key(username, "Authenticated::username");
2088 assert_not_private_key(user_uuid, "Authenticated::user_uuid");
2089 assert_not_private_key(login_pubkey, "Authenticated::login_pubkey");
2090 for k in session_keyset {
2091 assert_not_private_key(k, "Authenticated::session_keyset entry");
2092 }
2093 for (k, ch) in pending_key_challenges {
2094 assert_not_private_key(k, "Authenticated::pending_key_challenges key");
2095 assert_not_private_key(
2096 ch,
2097 "Authenticated::pending_key_challenges challenge",
2098 );
2099 }
2100 }
2101 }
2102 }
2103 }
2104
2105 #[tokio::test]
2106 async fn test_authenticated_request_rejected_without_login() {
2107 let (socket_path, _tx, _task, _instance) = start_test_server().await;
2112
2113 let stream = tokio::net::UnixStream::connect(&socket_path).await.unwrap();
2114 let (mut reader, mut writer) = tokio::io::split(stream);
2115
2116 write_frame(
2117 &mut writer,
2118 &Handshake {
2119 protocol_version: PROTOCOL_VERSION,
2120 },
2121 )
2122 .await
2123 .unwrap();
2124 let _ack: Option<HandshakeAck> = read_frame(&mut reader).await.unwrap();
2125
2126 write_frame(
2128 &mut writer,
2129 &ServiceRequest::AuthenticatedDb(Box::new(AuthenticatedDbRequest {
2130 root_id: crate::entry::ID::default(),
2131 identity: crate::auth::types::SigKey::default(),
2132 op: DatabaseOp::GetEntry {
2133 id: crate::entry::ID::from_bytes("nonexistent"),
2134 },
2135 })),
2136 )
2137 .await
2138 .unwrap();
2139
2140 let frame: Option<ServerFrame> = read_frame(&mut reader).await.unwrap();
2141 let resp = match frame.unwrap() {
2142 ServerFrame::Response(r) => *r,
2143 other => panic!("Expected Response frame, got {other:?}"),
2144 };
2145 match resp {
2146 ServiceResponse::Error(e) => {
2147 assert_eq!(
2148 e.module, "auth",
2149 "expected an auth-module error from the gate; got {e:?}"
2150 );
2151 }
2152 other => panic!("Expected gate Error, got {other:?}"),
2153 }
2154 }
2155
2156 #[tokio::test]
2157 async fn test_get_instance_metadata() {
2158 let (socket_path, _tx, _task, _instance) = start_test_server().await;
2159
2160 let stream = tokio::net::UnixStream::connect(&socket_path).await.unwrap();
2161 let (mut reader, mut writer) = tokio::io::split(stream);
2162
2163 write_frame(
2165 &mut writer,
2166 &Handshake {
2167 protocol_version: PROTOCOL_VERSION,
2168 },
2169 )
2170 .await
2171 .unwrap();
2172 let _ack: Option<HandshakeAck> = read_frame(&mut reader).await.unwrap();
2173
2174 write_frame(&mut writer, &ServiceRequest::GetInstanceMetadata)
2176 .await
2177 .unwrap();
2178
2179 let frame: Option<ServerFrame> = read_frame(&mut reader).await.unwrap();
2180 let resp = match frame.unwrap() {
2181 ServerFrame::Response(r) => *r,
2182 other => panic!("Expected Response frame, got {other:?}"),
2183 };
2184 match resp {
2185 ServiceResponse::InstanceMetadata(Some(_meta)) => {
2186 }
2188 other => panic!("Expected InstanceMetadata(Some), got {other:?}"),
2189 }
2190 }
2191
2192 #[tokio::test]
2193 async fn test_stale_socket_cleanup() {
2194 let dir = private_tempdir();
2195 let socket_path = dir.path().join("test.sock");
2196 drop(UnixListener::bind(&socket_path).unwrap());
2197 let (instance, _admin) = Instance::create_backend(
2198 Box::new(InMemory::new()),
2199 crate::NewUser::passwordless("admin"),
2200 )
2201 .await
2202 .unwrap();
2203
2204 let server = ServiceServer::bind(instance, &socket_path).await.unwrap();
2205 assert!(tokio::net::UnixStream::connect(&socket_path).await.is_ok());
2206 drop(server);
2207 assert!(!socket_path.exists());
2208 }
2209
2210 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2211 async fn test_simultaneous_bind_refuses_one_and_winner_serves_protocol() {
2212 let dir = private_tempdir();
2213 let socket_path = dir.path().join("test.sock");
2214 let (instance, _admin) = Instance::create_backend(
2215 Box::new(InMemory::new()),
2216 crate::NewUser::passwordless("admin"),
2217 )
2218 .await
2219 .unwrap();
2220 let expected_id = instance.id();
2221 let barrier = Arc::new(tokio::sync::Barrier::new(3));
2222
2223 let contender = |instance: Instance| {
2224 let barrier = barrier.clone();
2225 let socket_path = socket_path.clone();
2226 tokio::spawn(async move {
2227 barrier.wait().await;
2228 ServiceServer::bind(instance, socket_path).await
2229 })
2230 };
2231 let first = contender(instance.clone());
2232 let second = contender(instance);
2233 barrier.wait().await;
2234 let (first, second) = tokio::time::timeout(Duration::from_secs(1), async {
2235 (first.await.unwrap(), second.await.unwrap())
2236 })
2237 .await
2238 .expect("simultaneous binds must finish within a bound");
2239
2240 let (winner, error) = match (first, second) {
2241 (Ok(winner), Err(error)) | (Err(error), Ok(winner)) => (winner, error),
2242 (Ok(_), Ok(_)) => panic!("only one simultaneous bind may succeed"),
2243 (Err(first), Err(second)) => {
2244 panic!("one simultaneous bind must succeed; got {first:?} and {second:?}")
2245 }
2246 };
2247 assert!(
2248 matches!(&error, crate::Error::Io(error) if error.kind() == std::io::ErrorKind::AddrInUse),
2249 "expected AddrInUse, got {error:?}"
2250 );
2251
2252 let (shutdown, rx) = watch::channel(());
2253 let task = tokio::spawn(winner.run(rx));
2254 let client = tokio::time::timeout(
2255 Duration::from_secs(1),
2256 Instance::connect(format!("unix://{}", socket_path.display())),
2257 )
2258 .await
2259 .expect("the winning server must answer within a bound")
2260 .expect("the winning server must complete a protocol request");
2261 assert_eq!(client.id(), expected_id);
2262
2263 drop(client);
2264 drop(shutdown);
2265 task.await.unwrap().unwrap();
2266 assert!(!socket_path.exists());
2267 }
2268
2269 #[tokio::test]
2270 async fn test_existing_regular_file_is_refused_and_preserved() {
2271 let dir = private_tempdir();
2272 let socket_path = dir.path().join("test.sock");
2273 tokio::fs::write(&socket_path, b"keep me").await.unwrap();
2274 let (instance, _admin) = Instance::create_backend(
2275 Box::new(InMemory::new()),
2276 crate::NewUser::passwordless("admin"),
2277 )
2278 .await
2279 .unwrap();
2280
2281 assert!(ServiceServer::bind(instance, &socket_path).await.is_err());
2282 assert_eq!(tokio::fs::read(&socket_path).await.unwrap(), b"keep me");
2283 }
2284
2285 #[tokio::test]
2286 async fn test_dropping_bound_server_cleans_owned_socket_only() {
2287 let dir = private_tempdir();
2288 let socket_path = dir.path().join("test.sock");
2289 let (instance, _admin) = Instance::create_backend(
2290 Box::new(InMemory::new()),
2291 crate::NewUser::passwordless("admin"),
2292 )
2293 .await
2294 .unwrap();
2295
2296 let server = ServiceServer::bind(instance, &socket_path).await.unwrap();
2297 tokio::fs::remove_file(&socket_path).await.unwrap();
2298 tokio::fs::write(&socket_path, b"replacement")
2299 .await
2300 .unwrap();
2301 drop(server);
2302 assert_eq!(tokio::fs::read(&socket_path).await.unwrap(), b"replacement");
2303 }
2304
2305 #[tokio::test]
2306 async fn test_bind_failure_is_returned_before_server_is_ready() {
2307 let dir = private_tempdir();
2308 let parent = dir.path().join("not-a-directory");
2309 let socket_path = parent.join("test.sock");
2310 tokio::fs::write(&parent, "file").await.unwrap();
2311 let (instance, _admin) = Instance::create_backend(
2312 Box::new(InMemory::new()),
2313 crate::NewUser::passwordless("admin"),
2314 )
2315 .await
2316 .unwrap();
2317
2318 assert!(ServiceServer::bind(instance, &socket_path).await.is_err());
2319 assert!(!socket_path.exists());
2320 }
2321
2322 #[tokio::test]
2328 async fn test_gate_require_existing_fails_closed_on_absent_db() {
2329 use crate::auth::crypto::generate_keypair;
2330
2331 let (instance, _admin) = Instance::create_backend(
2332 Box::new(InMemory::new()),
2333 crate::NewUser::passwordless("admin"),
2334 )
2335 .await
2336 .unwrap();
2337 let (_sk, pubkey) = generate_keypair();
2338 let absent = ID::from_bytes("no-such-tree");
2339
2340 gate_tree_permission(
2341 &instance,
2342 &pubkey,
2343 &SigKey::default(),
2344 &absent,
2345 Permission::Admin(0),
2346 false,
2347 )
2348 .await
2349 .expect("create-flow passthrough must wave an absent tree through");
2350
2351 let err = gate_tree_permission(
2352 &instance,
2353 &pubkey,
2354 &SigKey::default(),
2355 &absent,
2356 Permission::Admin(0),
2357 true,
2358 )
2359 .await
2360 .expect_err("require_existing must deny an absent tree");
2361 assert!(
2362 matches!(&err, crate::Error::Auth(b) if matches!(**b, AuthError::PermissionDenied { .. })),
2363 "expected PermissionDenied, got: {err:?}",
2364 );
2365 }
2366}