use std::ffi::CString;
use std::process::Command;
use std::sync::Arc;
#[cfg(unix)]
use std::os::unix::process::CommandExt;
use limes_proto::{LimesEvent, SessionHandle, SessionSpec};
use limes_common::{EventBus, LimesError, Result};
pub trait SessionBackend: Send + Sync {
fn start(&self, spec: &SessionSpec) -> Result<SessionHandle>;
fn wait(&self, handle: &SessionHandle) -> Result<i32>;
fn terminate(&self, handle: &SessionHandle) -> Result<()>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct LocalSessionBackend;
impl SessionBackend for LocalSessionBackend {
fn start(&self, spec: &SessionSpec) -> Result<SessionHandle> {
let (program, args) = spec
.command
.split_first()
.ok_or_else(|| LimesError::Session("session command is empty".to_owned()))?;
let mut command = Command::new(program);
command.args(args);
command.env_clear();
command.envs(spec.env.iter().map(|(key, value)| (key, value)));
if let Some(working_dir) = &spec.working_dir {
command.current_dir(working_dir);
}
#[cfg(unix)]
{
let euid = unsafe { libc::geteuid() };
if euid != 0 && euid != spec.uid {
return Err(LimesError::Session(format!(
"cannot start session for {} without root privileges: effective uid {euid} cannot switch to uid {}",
spec.username, spec.uid
)));
}
let username = CString::new(spec.username.clone()).map_err(|error| {
LimesError::Session(format!("invalid session username for initgroups: {error}"))
})?;
let uid = spec.uid;
let gid = spec.gid;
unsafe {
command.pre_exec(move || {
if libc::geteuid() != 0 {
return Ok(());
}
if libc::initgroups(username.as_ptr(), gid) != 0 {
return Err(std::io::Error::last_os_error());
}
if libc::setgid(gid) != 0 {
return Err(std::io::Error::last_os_error());
}
if libc::setuid(uid) != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
}
let child = command.spawn().map_err(|error| {
LimesError::Session(format!("failed to spawn session `{program}`: {error}"))
})?;
Ok(SessionHandle {
pid: child.id(),
username: spec.username.clone(),
command: spec.command.clone(),
auth_session_id: spec.auth_session_id.clone(),
})
}
fn wait(&self, handle: &SessionHandle) -> Result<i32> {
#[cfg(unix)]
{
let mut status = 0;
let pid = unsafe { libc::waitpid(handle.pid as libc::pid_t, &mut status, 0) };
if pid < 0 {
return Err(LimesError::Session(format!(
"failed waiting for session pid {}: {}",
handle.pid,
std::io::Error::last_os_error()
)));
}
Ok(status)
}
#[cfg(not(unix))]
{
let _ = handle;
Err(LimesError::Unsupported(
"session waiting is only implemented on unix".to_owned(),
))
}
}
fn terminate(&self, handle: &SessionHandle) -> Result<()> {
#[cfg(unix)]
{
if unsafe { libc::kill(handle.pid as libc::pid_t, libc::SIGTERM) } != 0 {
return Err(LimesError::Session(format!(
"failed to terminate session pid {}: {}",
handle.pid,
std::io::Error::last_os_error()
)));
}
}
#[cfg(not(unix))]
{
let _ = handle;
return Err(LimesError::Unsupported(
"session termination is only stubbed on unix".to_owned(),
));
}
Ok(())
}
}
pub struct SessionManager {
backend: Arc<dyn SessionBackend>,
events: EventBus,
}
impl SessionManager {
#[must_use]
pub fn new(backend: Arc<dyn SessionBackend>, events: EventBus) -> Self {
Self { backend, events }
}
pub fn start(&self, spec: &SessionSpec) -> Result<SessionHandle> {
let handle = self.backend.start(spec)?;
self.events.emit(LimesEvent::SessionStarted {
username: handle.username.clone(),
pid: handle.pid,
});
Ok(handle)
}
pub fn wait(&self, handle: &SessionHandle) -> Result<i32> {
self.backend.wait(handle)
}
pub fn terminate(&self, handle: &SessionHandle) -> Result<()> {
self.backend.terminate(handle)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
#[test]
fn local_session_backend_rejects_user_switch_without_root() {
if unsafe { libc::geteuid() } == 0 {
return;
}
let current_uid = unsafe { libc::geteuid() } as u32;
let spec = SessionSpec::new(
"target-user",
current_uid.saturating_add(1),
current_uid.saturating_add(1),
vec!["/bin/true".to_owned()],
);
let error = LocalSessionBackend.start(&spec).unwrap_err();
assert!(error.to_string().contains("without root privileges"));
}
}