diff --git a/server/src/cmds/run/config.rs b/server/src/cmds/run/config.rs index f3d685e..4017a41 100644 --- a/server/src/cmds/run/config.rs +++ b/server/src/cmds/run/config.rs @@ -67,6 +67,11 @@ pub struct BindConfig { /// specify a localhost bind address. #[serde(default)] pub trust_forward_hdrs: bool, + + /// On Unix-domain sockets, treat clients with the Moonfire NVR server's own + /// effective UID as privileged. + #[serde(default)] + pub own_uid_is_privileged: bool, } #[derive(Debug, Deserialize)] diff --git a/server/src/cmds/run/mod.rs b/server/src/cmds/run/mod.rs index 8547338..733c609 100644 --- a/server/src/cmds/run/mod.rs +++ b/server/src/cmds/run/mod.rs @@ -336,6 +336,7 @@ async fn inner( }; // Start the web interface(s). + let own_euid = nix::unistd::Uid::effective(); let web_handles: Result, Error> = config .binds .iter() @@ -349,11 +350,13 @@ async fn inner( .map(Permissions::as_proto), trust_forward_hdrs: b.trust_forward_hdrs, time_zone_name: time_zone_name.clone(), + privileged_unix_uid: b.own_uid_is_privileged.then(|| own_euid), })?); - let make_svc = make_service_fn(move |_conn| { + let make_svc = make_service_fn(move |conn: &crate::web::accept::Conn| { + let conn_data = *conn.data(); futures::future::ok::<_, std::convert::Infallible>(service_fn({ let svc = Arc::clone(&svc); - move |req| Arc::clone(&svc).serve(req) + move |req| Arc::clone(&svc).serve(req, conn_data) })) }); let listener = make_listener(&b.address)?; diff --git a/server/src/web/accept.rs b/server/src/web/accept.rs index eb97251..5959c20 100644 --- a/server/src/web/accept.rs +++ b/server/src/web/accept.rs @@ -28,8 +28,10 @@ impl Accept for Listener { } Some(Ok(Conn { stream: Stream::Tcp(s), - client_unix_uid: None, - client_addr: Some(a), + data: ConnData { + client_unix_uid: None, + client_addr: Some(a), + }, })) }), Listener::Unix(l) => Pin::new(l).poll_accept(cx)?.map(|(s, _a)| { @@ -39,8 +41,10 @@ impl Accept for Listener { }; Some(Ok(Conn { stream: Stream::Unix(s), - client_unix_uid: Some(ucred.uid()), - client_addr: None, + data: ConnData { + client_unix_uid: Some(nix::unistd::Uid::from_raw(ucred.uid())), + client_addr: None, + }, })) }), } @@ -50,19 +54,19 @@ impl Accept for Listener { /// An open connection. pub struct Conn { stream: Stream, - client_unix_uid: Option, - client_addr: Option, + data: ConnData, +} + +/// Extra data associated with a connection. +#[derive(Copy, Clone)] +pub struct ConnData { + pub client_unix_uid: Option, + pub client_addr: Option, } impl Conn { - #[allow(dead_code)] // TODO: feed this onward. - pub fn client_unix_uid(&self) -> Option { - self.client_unix_uid - } - - #[allow(dead_code)] // TODO: feed this onward. - pub fn client_addr(&self) -> Option<&std::net::SocketAddr> { - self.client_addr.as_ref() + pub fn data(&self) -> &ConnData { + &self.data } } diff --git a/server/src/web/mod.rs b/server/src/web/mod.rs index 21e0088..89341e1 100644 --- a/server/src/web/mod.rs +++ b/server/src/web/mod.rs @@ -10,6 +10,7 @@ mod signals; mod static_file; mod view; +use self::accept::ConnData; use self::path::Path; use crate::body::Body; use crate::json; @@ -88,6 +89,7 @@ fn from_base_error(err: base::Error) -> Response { plain_response(status_code, err.to_string()) } +#[derive(Debug)] struct Caller { permissions: db::Permissions, user: Option, @@ -160,6 +162,7 @@ pub struct Config<'a> { pub trust_forward_hdrs: bool, pub time_zone_name: String, pub allow_unauthenticated_permissions: Option, + pub privileged_unix_uid: Option, } pub struct Service { @@ -169,6 +172,7 @@ pub struct Service { time_zone_name: String, allow_unauthenticated_permissions: Option, trust_forward_hdrs: bool, + privileged_unix_uid: Option, } /// Useful HTTP `Cache-Control` values to set on successful (HTTP 200) API responses. @@ -221,6 +225,7 @@ impl Service { allow_unauthenticated_permissions: config.allow_unauthenticated_permissions, trust_forward_hdrs: config.trust_forward_hdrs, time_zone_name: config.time_zone_name, + privileged_unix_uid: config.privileged_unix_uid, }) } @@ -240,7 +245,7 @@ impl Service { self.init_segment(sha1, debug, &req)?, ), Path::TopLevel => (CacheControl::PrivateDynamic, self.top_level(&req, caller)?), - Path::Request => (CacheControl::PrivateDynamic, self.request(&req)?), + Path::Request => (CacheControl::PrivateDynamic, self.request(&req, caller)?), Path::Camera(uuid) => (CacheControl::PrivateDynamic, self.camera(&req, uuid)?), Path::StreamRecordings(uuid, type_) => ( CacheControl::PrivateDynamic, @@ -298,6 +303,7 @@ impl Service { pub async fn serve( self: Arc, req: Request<::hyper::Body>, + conn_data: ConnData, ) -> Result, std::convert::Infallible> { let p = Path::decode(req.uri().path()); let always_allow_unauthenticated = matches!( @@ -305,7 +311,7 @@ impl Service { Path::NotFound | Path::Request | Path::Login | Path::Logout | Path::Static ); debug!("request on: {}: {:?}", req.uri(), p); - let caller = match self.authenticate(&req, always_allow_unauthenticated) { + let caller = match self.authenticate(&req, &conn_data, always_allow_unauthenticated) { Ok(c) => c, Err(e) => return Ok(from_base_error(e)), }; @@ -506,7 +512,7 @@ impl Service { } } - fn request(&self, req: &Request<::hyper::Body>) -> ResponseResult { + fn request(&self, req: &Request<::hyper::Body>, caller: Caller) -> ResponseResult { let authreq = self.authreq(req); let host = req .headers() @@ -523,7 +529,8 @@ impl Service { host: {:?}\n\ addr: {:?}\n\ user_agent: {:?}\n\ - secure: {:?}", + secure: {:?}\n\ + caller: {:?}\n", time::at(time::Timespec { sec: authreq.when_sec.unwrap(), nsec: 0 @@ -534,7 +541,8 @@ impl Service { host.as_deref(), &authreq.addr, agent.as_deref(), - self.is_secure(req) + self.is_secure(req), + &caller, ), )) } @@ -555,11 +563,13 @@ impl Service { /// Authenticates the session (if any) and returns a Caller. /// /// If there's no session, - /// 1. if `allow_unauthenticated_permissions` is configured, returns okay + /// 1. if connected via Unix domain socket from the same effective uid + /// as Moonfire NVR itself, return with all privileges. + /// 2. if `allow_unauthenticated_permissions` is configured, returns okay /// with those permissions. - /// 2. if the caller specifies `unauth_path`, returns okay with no + /// 3. if the caller specifies `unauth_path`, returns okay with no /// permissions. - /// 3. returns `Unauthenticated` error otherwise. + /// 4. returns `Unauthenticated` error otherwise. /// /// Does no authorization. That is, this doesn't check that the returned /// permissions are sufficient for whatever operation the caller is @@ -567,6 +577,7 @@ impl Service { fn authenticate( &self, req: &Request, + conn_data: &ConnData, unauth_path: bool, ) -> Result { if let Some(sid) = extract_sid(req) { @@ -594,6 +605,18 @@ impl Service { }; } + if matches!(conn_data.client_unix_uid, Some(uid) if Some(uid) == self.privileged_unix_uid) { + return Ok(Caller { + permissions: db::Permissions { + view_video: true, + read_camera_configs: true, + update_signals: true, + ..Default::default() + }, + user: None, + }); + } + if let Some(s) = self.allow_unauthenticated_permissions.as_ref() { return Ok(Caller { permissions: s.clone(), @@ -638,13 +661,22 @@ mod tests { allow_unauthenticated_permissions, trust_forward_hdrs: true, time_zone_name: "".to_owned(), + privileged_unix_uid: nix::unistd::Uid::from_raw(!0), }) .unwrap(), ); let make_svc = hyper::service::make_service_fn(move |_conn| { futures::future::ok::<_, std::convert::Infallible>(hyper::service::service_fn({ let s = Arc::clone(&service); - move |req| Arc::clone(&s).serve(req) + move |req| { + Arc::clone(&s).serve( + req, + super::accept::ConnData { + client_unix_uid: None, + client_addr: None, + }, + ) + } })) }); let (tx, rx) = std::sync::mpsc::channel(); @@ -740,13 +772,22 @@ mod bench { allow_unauthenticated_permissions: Some(db::Permissions::default()), trust_forward_hdrs: false, time_zone_name: "".to_owned(), + privileged_unix_uid: nix::unistd::Uid::from_raw(!0), }) .unwrap(), ); let make_svc = hyper::service::make_service_fn(move |_conn| { futures::future::ok::<_, std::convert::Infallible>(hyper::service::service_fn({ let s = Arc::clone(&service); - move |req| Arc::clone(&s).serve(req) + move |req| { + Arc::clone(&s).serve( + req, + super::accept::ConnData { + client_unix_uid: None, + client_addr: None, + }, + ) + } })) }); let rt = tokio::runtime::Runtime::new().unwrap();