Skip to main content

repo/daemon/
mount_auth.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Fail-closed authorization for mount-daemon RPCs.
3//!
4//! Localhost TCP is not an authz boundary. A client-supplied
5//! `mount_path` is honored only when the peer has the same uid the
6//! UDS `SO_PEERCRED` path proves. Unauthenticated transports
7//! (including the historical `127.0.0.1` listener) must refuse.
8
9use std::path::Path;
10
11use objects::error::HeddleError;
12
13use super::mount_proto::MountDaemonRequest;
14
15/// How the daemon authenticated the caller.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum MountClientAuth {
18    /// `SO_PEERCRED` / `getpeereid` proved the peer uid matches this process.
19    SameUid,
20    /// No peer identity. Localhost TCP is this. Fail closed.
21    Unauthenticated,
22}
23
24/// Why a mount-daemon request was refused.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct MountAuthDenied {
27    pub code: &'static str,
28    pub message: String,
29}
30
31impl MountAuthDenied {
32    pub fn unauthorized(message: impl Into<String>) -> Self {
33        Self {
34            code: super::mount_proto::ERR_UNAUTHORIZED,
35            message: message.into(),
36        }
37    }
38}
39
40impl From<MountAuthDenied> for HeddleError {
41    fn from(denied: MountAuthDenied) -> Self {
42        HeddleError::Config(format!("{}: {}", denied.code, denied.message))
43    }
44}
45
46/// Honor a client-supplied `mount_path` only for same-uid peers.
47pub fn trusted_mount_path(
48    auth: MountClientAuth,
49    supplied: &Path,
50) -> Result<&Path, MountAuthDenied> {
51    match auth {
52        MountClientAuth::SameUid => Ok(supplied),
53        MountClientAuth::Unauthenticated => Err(MountAuthDenied::unauthorized(
54            "refusing client-supplied mount_path over an unauthenticated transport; same-uid UDS is required",
55        )),
56    }
57}
58
59/// Every mount-daemon verb needs same-uid proof. Health and list leak
60/// mount paths; mount/unmount/shutdown are filesystem primitives.
61pub fn authorize_mount_request(
62    auth: MountClientAuth,
63    request: &MountDaemonRequest,
64) -> Result<(), MountAuthDenied> {
65    match auth {
66        MountClientAuth::SameUid => Ok(()),
67        MountClientAuth::Unauthenticated => Err(MountAuthDenied::unauthorized(format!(
68            "refusing {verb} over an unauthenticated transport; same-uid UDS is required",
69            verb = request_verb(request)
70        ))),
71    }
72}
73
74fn request_verb(request: &MountDaemonRequest) -> &'static str {
75    match request {
76        MountDaemonRequest::Mount { .. } => "mount",
77        MountDaemonRequest::Unmount { .. } => "unmount",
78        MountDaemonRequest::ListMounts {} => "list_mounts",
79        MountDaemonRequest::Health {} => "health",
80        MountDaemonRequest::Shutdown {} => "shutdown",
81        MountDaemonRequest::Unknown => "unknown",
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use std::path::{Path, PathBuf};
88
89    use super::{MountClientAuth, authorize_mount_request, trusted_mount_path};
90    use crate::daemon::MountDaemonRequest;
91
92    fn mount_request() -> MountDaemonRequest {
93        MountDaemonRequest::Mount {
94            thread_id: "agent-7".to_string(),
95            mount_path: PathBuf::from("/tmp/evil"),
96            repo_root: PathBuf::from("/tmp/repo"),
97        }
98    }
99
100    #[test]
101    fn unauthenticated_tcp_must_not_honor_client_mount_path() {
102        let denied = trusted_mount_path(MountClientAuth::Unauthenticated, Path::new("/tmp/evil"));
103        assert!(
104            denied.is_err(),
105            "localhost TCP must not honor a client-supplied mount_path"
106        );
107    }
108
109    #[test]
110    fn same_uid_peer_may_supply_mount_path() {
111        let path = Path::new("/tmp/ok");
112        let accepted = trusted_mount_path(MountClientAuth::SameUid, path)
113            .expect("same-uid UDS may honor mount_path");
114        assert_eq!(accepted, path);
115    }
116
117    #[test]
118    fn unauthenticated_tcp_must_not_drive_mount_or_unmount() {
119        let mount = authorize_mount_request(MountClientAuth::Unauthenticated, &mount_request());
120        assert!(mount.is_err(), "unauthenticated mount must fail closed");
121
122        let unmount = authorize_mount_request(
123            MountClientAuth::Unauthenticated,
124            &MountDaemonRequest::Unmount {
125                thread_id: "agent-7".to_string(),
126            },
127        );
128        assert!(unmount.is_err(), "unauthenticated unmount must fail closed");
129    }
130
131    #[test]
132    fn same_uid_peer_is_authorized() {
133        authorize_mount_request(MountClientAuth::SameUid, &mount_request())
134            .expect("same-uid mount is allowed");
135    }
136}