support treating own effective uid as privileged

I intend this to be an easy bootstrapping mechanism for web auth.
This commit is contained in:
Scott Lamb 2022-03-11 11:10:26 -08:00
parent 4ce3e511b5
commit 7c453b5f9d
4 changed files with 79 additions and 26 deletions

View File

@ -67,6 +67,11 @@ pub struct BindConfig {
/// specify a localhost bind address. /// specify a localhost bind address.
#[serde(default)] #[serde(default)]
pub trust_forward_hdrs: bool, 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)] #[derive(Debug, Deserialize)]

View File

@ -336,6 +336,7 @@ async fn inner(
}; };
// Start the web interface(s). // Start the web interface(s).
let own_euid = nix::unistd::Uid::effective();
let web_handles: Result<Vec<_>, Error> = config let web_handles: Result<Vec<_>, Error> = config
.binds .binds
.iter() .iter()
@ -349,11 +350,13 @@ async fn inner(
.map(Permissions::as_proto), .map(Permissions::as_proto),
trust_forward_hdrs: b.trust_forward_hdrs, trust_forward_hdrs: b.trust_forward_hdrs,
time_zone_name: time_zone_name.clone(), 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({ futures::future::ok::<_, std::convert::Infallible>(service_fn({
let svc = Arc::clone(&svc); 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)?; let listener = make_listener(&b.address)?;

View File

@ -28,8 +28,10 @@ impl Accept for Listener {
} }
Some(Ok(Conn { Some(Ok(Conn {
stream: Stream::Tcp(s), stream: Stream::Tcp(s),
client_unix_uid: None, data: ConnData {
client_addr: Some(a), client_unix_uid: None,
client_addr: Some(a),
},
})) }))
}), }),
Listener::Unix(l) => Pin::new(l).poll_accept(cx)?.map(|(s, _a)| { Listener::Unix(l) => Pin::new(l).poll_accept(cx)?.map(|(s, _a)| {
@ -39,8 +41,10 @@ impl Accept for Listener {
}; };
Some(Ok(Conn { Some(Ok(Conn {
stream: Stream::Unix(s), stream: Stream::Unix(s),
client_unix_uid: Some(ucred.uid()), data: ConnData {
client_addr: None, 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. /// An open connection.
pub struct Conn { pub struct Conn {
stream: Stream, stream: Stream,
client_unix_uid: Option<libc::uid_t>, data: ConnData,
client_addr: Option<std::net::SocketAddr>, }
/// Extra data associated with a connection.
#[derive(Copy, Clone)]
pub struct ConnData {
pub client_unix_uid: Option<nix::unistd::Uid>,
pub client_addr: Option<std::net::SocketAddr>,
} }
impl Conn { impl Conn {
#[allow(dead_code)] // TODO: feed this onward. pub fn data(&self) -> &ConnData {
pub fn client_unix_uid(&self) -> Option<libc::uid_t> { &self.data
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()
} }
} }

View File

@ -10,6 +10,7 @@ mod signals;
mod static_file; mod static_file;
mod view; mod view;
use self::accept::ConnData;
use self::path::Path; use self::path::Path;
use crate::body::Body; use crate::body::Body;
use crate::json; use crate::json;
@ -88,6 +89,7 @@ fn from_base_error(err: base::Error) -> Response<Body> {
plain_response(status_code, err.to_string()) plain_response(status_code, err.to_string())
} }
#[derive(Debug)]
struct Caller { struct Caller {
permissions: db::Permissions, permissions: db::Permissions,
user: Option<json::ToplevelUser>, user: Option<json::ToplevelUser>,
@ -160,6 +162,7 @@ pub struct Config<'a> {
pub trust_forward_hdrs: bool, pub trust_forward_hdrs: bool,
pub time_zone_name: String, pub time_zone_name: String,
pub allow_unauthenticated_permissions: Option<db::Permissions>, pub allow_unauthenticated_permissions: Option<db::Permissions>,
pub privileged_unix_uid: Option<nix::unistd::Uid>,
} }
pub struct Service { pub struct Service {
@ -169,6 +172,7 @@ pub struct Service {
time_zone_name: String, time_zone_name: String,
allow_unauthenticated_permissions: Option<db::Permissions>, allow_unauthenticated_permissions: Option<db::Permissions>,
trust_forward_hdrs: bool, trust_forward_hdrs: bool,
privileged_unix_uid: Option<nix::unistd::Uid>,
} }
/// Useful HTTP `Cache-Control` values to set on successful (HTTP 200) API responses. /// 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, allow_unauthenticated_permissions: config.allow_unauthenticated_permissions,
trust_forward_hdrs: config.trust_forward_hdrs, trust_forward_hdrs: config.trust_forward_hdrs,
time_zone_name: config.time_zone_name, 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)?, self.init_segment(sha1, debug, &req)?,
), ),
Path::TopLevel => (CacheControl::PrivateDynamic, self.top_level(&req, caller)?), 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::Camera(uuid) => (CacheControl::PrivateDynamic, self.camera(&req, uuid)?),
Path::StreamRecordings(uuid, type_) => ( Path::StreamRecordings(uuid, type_) => (
CacheControl::PrivateDynamic, CacheControl::PrivateDynamic,
@ -298,6 +303,7 @@ impl Service {
pub async fn serve( pub async fn serve(
self: Arc<Self>, self: Arc<Self>,
req: Request<::hyper::Body>, req: Request<::hyper::Body>,
conn_data: ConnData,
) -> Result<Response<Body>, std::convert::Infallible> { ) -> Result<Response<Body>, std::convert::Infallible> {
let p = Path::decode(req.uri().path()); let p = Path::decode(req.uri().path());
let always_allow_unauthenticated = matches!( let always_allow_unauthenticated = matches!(
@ -305,7 +311,7 @@ impl Service {
Path::NotFound | Path::Request | Path::Login | Path::Logout | Path::Static Path::NotFound | Path::Request | Path::Login | Path::Logout | Path::Static
); );
debug!("request on: {}: {:?}", req.uri(), p); 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, Ok(c) => c,
Err(e) => return Ok(from_base_error(e)), 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 authreq = self.authreq(req);
let host = req let host = req
.headers() .headers()
@ -523,7 +529,8 @@ impl Service {
host: {:?}\n\ host: {:?}\n\
addr: {:?}\n\ addr: {:?}\n\
user_agent: {:?}\n\ user_agent: {:?}\n\
secure: {:?}", secure: {:?}\n\
caller: {:?}\n",
time::at(time::Timespec { time::at(time::Timespec {
sec: authreq.when_sec.unwrap(), sec: authreq.when_sec.unwrap(),
nsec: 0 nsec: 0
@ -534,7 +541,8 @@ impl Service {
host.as_deref(), host.as_deref(),
&authreq.addr, &authreq.addr,
agent.as_deref(), 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. /// Authenticates the session (if any) and returns a Caller.
/// ///
/// If there's no session, /// 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. /// 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. /// permissions.
/// 3. returns `Unauthenticated` error otherwise. /// 4. returns `Unauthenticated` error otherwise.
/// ///
/// Does no authorization. That is, this doesn't check that the returned /// Does no authorization. That is, this doesn't check that the returned
/// permissions are sufficient for whatever operation the caller is /// permissions are sufficient for whatever operation the caller is
@ -567,6 +577,7 @@ impl Service {
fn authenticate( fn authenticate(
&self, &self,
req: &Request<hyper::Body>, req: &Request<hyper::Body>,
conn_data: &ConnData,
unauth_path: bool, unauth_path: bool,
) -> Result<Caller, base::Error> { ) -> Result<Caller, base::Error> {
if let Some(sid) = extract_sid(req) { 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() { if let Some(s) = self.allow_unauthenticated_permissions.as_ref() {
return Ok(Caller { return Ok(Caller {
permissions: s.clone(), permissions: s.clone(),
@ -638,13 +661,22 @@ mod tests {
allow_unauthenticated_permissions, allow_unauthenticated_permissions,
trust_forward_hdrs: true, trust_forward_hdrs: true,
time_zone_name: "".to_owned(), time_zone_name: "".to_owned(),
privileged_unix_uid: nix::unistd::Uid::from_raw(!0),
}) })
.unwrap(), .unwrap(),
); );
let make_svc = hyper::service::make_service_fn(move |_conn| { let make_svc = hyper::service::make_service_fn(move |_conn| {
futures::future::ok::<_, std::convert::Infallible>(hyper::service::service_fn({ futures::future::ok::<_, std::convert::Infallible>(hyper::service::service_fn({
let s = Arc::clone(&service); 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(); let (tx, rx) = std::sync::mpsc::channel();
@ -740,13 +772,22 @@ mod bench {
allow_unauthenticated_permissions: Some(db::Permissions::default()), allow_unauthenticated_permissions: Some(db::Permissions::default()),
trust_forward_hdrs: false, trust_forward_hdrs: false,
time_zone_name: "".to_owned(), time_zone_name: "".to_owned(),
privileged_unix_uid: nix::unistd::Uid::from_raw(!0),
}) })
.unwrap(), .unwrap(),
); );
let make_svc = hyper::service::make_service_fn(move |_conn| { let make_svc = hyper::service::make_service_fn(move |_conn| {
futures::future::ok::<_, std::convert::Infallible>(hyper::service::service_fn({ futures::future::ok::<_, std::convert::Infallible>(hyper::service::service_fn({
let s = Arc::clone(&service); 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(); let rt = tokio::runtime::Runtime::new().unwrap();