mod error;
mod finder;
pub mod pam;
mod pre_exec;
#[cfg(feature = "timeout")]
mod timeout;
use bon::Builder;
use capctl::{Cap, CapState};
use const_format::formatcp;
use nix::{
sys::stat,
unistd::{Uid, User, isatty},
};
use rar_common::database::options::SBounding;
use rar_common::util::{escape_parser_string, initialize_capabilities, with_privileges};
use rar_common::{
Cred,
database::{
FilterMatcher,
actor::{SGroupType, SGroups, SUserType},
options::EnvBehavior,
},
};
use log::{debug, error};
use pam::PAM_PROMPT;
use std::{io, process::Command};
use std::{io::stdout, path::PathBuf};
use crate::pre_exec::{PRE_EXEC_ORCHESTRATOR, configure_pre_exec};
use rar_common::util::{BOLD, RST, UNDERLINE, drop_effective, subsribe};
use crate::error::SrError;
use crate::error::SrResult;
use crate::finder::de::cred::CredOwnedData;
#[cfg(not(test))]
pub const RAR_CFG_PATH: &str = env!("RAR_CFG_PATH");
#[cfg(test)]
pub const RAR_CFG_PATH: &str = "target/rootasrole.json";
#[cfg(not(test))]
pub const RAR_CFG_DATA_PATH: &str = env!("RAR_CFG_DATA_PATH");
#[cfg(test)]
pub const RAR_CFG_DATA_PATH: &str = "target/rootasrole.json";
#[allow(clippy::needless_raw_string_hashes)]
const USAGE: &str = formatcp!(
r#"{UNDERLINE}{BOLD}Usage:{RST} {BOLD}dosr{RST} [OPTIONS] [COMMAND]...
{UNDERLINE}{BOLD}Arguments:{RST}
[COMMAND]...
Command to execute
{UNDERLINE}{BOLD}Options:{RST}
{BOLD}-r, --role <ROLE>{RST}
Role option allows you to select a specific role to use
{BOLD}-t, --task <TASK>{RST}
Task option allows you to select a specific task to use in the selected role. Note: You must specify a role to designate a task
{BOLD}-E, --preserve-env{RST}
Preserve environment variables if allowed by a matching task
{BOLD}-D, --chdir <WORKDIR>{RST}
Workdir option allows you to specify the working directory for the command
{BOLD}-p, --prompt <PROMPT>{RST}
Prompt option allows you to override the default password prompt and use a custom one
[default: "Password: "]
{BOLD}-u, --user <USER>{RST}
Specify the user to execute the command as
{BOLD}-g --group <GROUP>(,<GROUP>...){RST}
Specify the group to execute the command as
{BOLD}-i, --info{RST}
Display rights of executor
{BOLD}-v, --version{RST}
Print dosr version
{BOLD}-h, --help{RST}
Print help (see a summary with '-h')"#
);
#[derive(Debug, Builder)]
#[allow(clippy::struct_excessive_bools)]
struct Cli {
opt_filter: Option<FilterMatcher>,
#[builder(into)]
prompt: Option<String>,
#[builder(default, with = || true)]
info: bool,
#[builder(default, with = || true)]
help: bool,
#[builder(default, into)]
cmd_path: PathBuf,
#[builder(default, with = |i : impl IntoIterator<Item = impl Into<String>> | i.into_iter().map(std::convert::Into::into).collect())]
cmd_args: Vec<String>,
#[builder(default, with = || true)]
stdin: bool,
#[builder(default, with = || false)]
del_ts: bool,
}
impl Default for Cli {
fn default() -> Self {
Self::builder().prompt(PAM_PROMPT).build()
}
}
const CAPABILITIES_ERROR: &str =
"You need at least dac_read_search or dac_override, setpcap and setuid capabilities to run sr";
fn cap_effective_error(caplist: &str) -> String {
format!("Unable to toggle {caplist} privilege. {CAPABILITIES_ERROR}")
}
fn getopt<S, I>(s: I) -> SrResult<Cli>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut args = Cli::default();
let mut iter = s.into_iter().skip(1);
let mut role = None;
let mut task = None;
let mut workdir = None;
let mut user: Option<SUserType> = None;
let mut group: Option<SGroups> = None;
let mut env = None;
while let Some(arg) = iter.next() {
match arg.as_ref() {
"-u" | "--user" => {
user = iter.next().map(|s| escape_parser_string(s).as_str().into());
}
"-g" | "--group" => {
group = iter.next().map(|s| {
SGroups::Multiple(
s.as_ref()
.split(',')
.map(std::convert::Into::into)
.collect::<Vec<SGroupType>>(),
)
});
}
"-S" | "--stdin" => {
args.stdin = true;
}
"-r" | "--role" => {
role = iter.next().map(|s| escape_parser_string(s));
}
"-t" | "--task" => {
task = iter.next().map(|s| escape_parser_string(s));
}
"-E" | "--preserve-env" => {
env.replace(EnvBehavior::Keep);
}
"-D" | "--chdir" => {
workdir = iter.next().map(|s| escape_parser_string(s));
}
#[cfg(feature = "timeout")]
"-K" | "--remove-timestamp" => {
args.del_ts = true;
}
"-p" | "--prompt" => {
args.prompt = Some(
iter.next()
.map(|s| escape_parser_string(s))
.expect("Missing prompt for -p option"),
);
}
"-i" | "--info" => {
args.info = true;
}
"-h" | "--help" => {
args.help = true;
}
"-v" | "--version" => {
println!("dosr: version {}", env!("CARGO_PKG_VERSION"));
std::process::exit(0);
}
_ => {
if arg.as_ref().starts_with('-') {
error!("Unknown option: {}", arg.as_ref());
return Err(SrError::InvalidAgruments);
}
args.cmd_path = arg.as_ref().into();
break;
}
}
}
args.opt_filter = Some(
FilterMatcher::builder()
.maybe_role(role)
.maybe_task(task)
.maybe_workdir(workdir)
.maybe_env_behavior(env)
.maybe_user(user)
.map_err(|e| {
error!("Error parsing user: {e}");
SrError::InvalidAgruments
})?
.maybe_group(group)
.map_err(|e| {
error!("Error parsing group: {e}");
SrError::InvalidAgruments
})?
.build(),
);
for arg in iter {
args.cmd_args.push(escape_parser_string(arg));
}
Ok(args)
}
#[cfg(not(tarpaulin_include))]
fn main() {
if let Err(e) = subsribe("sr") {
eprintln!("dosr: Failed to initialize logging: {e}");
std::process::exit(1);
}
if let Err(e) = main_inner() {
eprintln!("dosr: {e}");
if e == SrError::InsufficientPrivileges {
error!("Insufficient privileges to run sr. {CAPABILITIES_ERROR}");
} else if e == SrError::AuthenticationFailed {
error!(
"Authentication failed for user '{}', when trying running '''{}'''",
User::from_uid(Uid::current())
.and_then(|u| u.map(|u| u.name).ok_or(nix::errno::Errno::EAGAIN))
.unwrap_or_else(|_| Uid::current().to_string()),
std::env::args().skip(1).collect::<Vec<_>>().join(" ")
);
eprintln!("This incident is reported.");
} else {
error!(
"User '{}' got a '{}' when trying running '''{}'''",
User::from_uid(Uid::current())
.and_then(|u| u.map(|u| u.name).ok_or(nix::errno::Errno::EAGAIN))
.unwrap_or_else(|_| Uid::current().to_string()),
e,
std::env::args().skip(1).collect::<Vec<_>>().join(" ")
);
}
std::process::exit(1);
}
}
#[cfg(not(tarpaulin_include))]
#[allow(clippy::too_many_lines)]
fn main_inner() -> SrResult<()> {
use std::env;
use crate::{
RAR_CFG_PATH,
finder::api::{Api, register_plugins},
pam::start_session,
};
use finder::find_best_exec_settings;
use rar_common::util::RAR_CFG_TYPE;
debug!("Started with capabilities: {:?}", CapState::get_current()?);
drop_effective()?;
let args = std::env::args();
if args.len() < 2 {
println!("{USAGE}");
return Ok(());
}
let args = getopt(args)?;
if args.help {
println!("{USAGE}");
return Ok(());
}
let user = make_cred().map_err(|_| SrError::SystemError)?;
if args.del_ts {
#[cfg(not(feature = "timeout"))]
{
error!("The timeout feature is not enabled, cannot delete timestamp cookie");
return Err(SrError::InvalidAgruments);
}
#[cfg(feature = "timeout")]
{
timeout::clear_cookies(&user).map_err(|e| {
error!("Failed to clear timestamp cookies: {e}");
SrError::InsufficientPrivileges
})?;
if args.cmd_path.as_os_str().is_empty() {
return Ok(());
}
}
}
register_plugins();
let execcfg = find_best_exec_settings(
&args,
&user,
RAR_CFG_PATH,
RAR_CFG_DATA_PATH,
RAR_CFG_TYPE,
env::vars(),
env::var("PATH")
.unwrap_or_default()
.split(':')
.collect::<Vec<_>>()
.as_slice(),
)?;
debug!("Best exec settings: {execcfg:?}");
let session = start_session(execcfg.auth, &execcfg.timeout, &user, &args)?;
if !execcfg.score.fully_matching() {
println!("You are not allowed to execute this command, this incident will be reported.");
error!(
"User {} tried to execute command : {} {:?} without the permission.",
&user.user.name,
args.cmd_path.display(),
args.cmd_args
);
std::process::exit(1);
}
if args.info {
use capctl::CapSet;
println!(
"Role: {}",
if execcfg.role.is_empty() {
"None"
} else {
&execcfg.role
}
);
println!("Task: {}", execcfg.task.as_ref().map_or("None", |v| v));
print!(
"Execute as user: {}",
if let Some(u) = execcfg.cred.setuid {
format!("{} ({})", u.name, u.uid)
} else {
"Your current user".to_string()
}
);
if let Some(gids) = execcfg.cred.setgroups.as_ref() {
print!(" and group(s): ");
let groups = gids
.iter()
.map(|g| format!("{} ({})", g.name, g.gid))
.collect::<Vec<_>>()
.join(", ");
println!("{groups}");
} else {
println!(" your current group(s)");
}
println!(
"With capabilities: {}",
execcfg.cred.caps.map_or_else(
|| "None".to_string(),
|caps| if caps == !CapSet::empty() {
"All capabilities".to_string()
} else {
caps.into_iter()
.fold(String::new(), |acc, cap| acc + &cap.to_string() + " ")
.trim_end()
.to_string()
}
)
);
println!(
"Command: {} {:?}",
execcfg.final_path.display(),
args.cmd_args
);
std::process::exit(0);
}
let mut command_builder = Command::new(&execcfg.final_path);
configure_pre_exec(
execcfg.umask.0,
execcfg.cred.clone(),
execcfg.bounding,
execcfg.root.is_user(),
);
debug!("setuid : {:?}", execcfg.cred.setuid);
let cargs = args.cmd_args.clone();
let cfinal_path = execcfg.final_path.clone();
let cfinal_env = execcfg.clone().env;
Api::notify(finder::api::ApiEvent::PreExec(&args, &execcfg))?;
if let Some(workdir) = args.opt_filter.as_ref().and_then(|f| f.workdir.as_ref()) {
command_builder.current_dir(workdir);
} else if let Some(workdir) = execcfg.workdir.as_ref() {
command_builder.current_dir(workdir);
}
command_builder.args(cargs.iter());
command_builder.env_clear();
command_builder.envs(cfinal_env);
let logger = rar_exec::runner::SimpleFileLogger::new("/tmp/dosr-session.log")
.ok()
.map(|l| Box::new(l) as Box<dyn rar_exec::pipe::IoLogger>);
debug!(
"Command (via Runner): {} {:?}",
cfinal_path.display(),
cargs.join(" ")
);
let status = match rar_exec::runner::run(command_builder, PRE_EXEC_ORCHESTRATOR, logger) {
Ok(status) => status,
Err(e) => {
error!("{e}");
eprintln!("dosr: {} : {}", cfinal_path.display(), e);
std::process::exit(1);
}
};
drop(session);
std::process::exit(status.code().unwrap_or(1));
}
fn make_cred() -> std::io::Result<Cred> {
Ok(Cred::builder()
.user()?
.groups()?
.curdir()?
.maybe_tty(stat::fstat(stdout()).ok().and_then(|s| {
if isatty(stdout()).ok().unwrap_or(false) {
Some(s.st_rdev)
} else {
None
}
}))
.build())
}
fn set_capabilities(cred: &CredOwnedData, bounding: SBounding) -> SrResult<()> {
let caps = cred.caps.unwrap_or_default();
let sbounding = capctl::bounding::probe();
if sbounding & caps != caps {
error!(
"Unable to setup the execution environment: There are more capabilities in this task than the current bounding set! You may are in a container or already in a RootAsRole session."
);
return Err(SrError::InsufficientPrivileges);
}
initialize_capabilities(&[Cap::SETPCAP])
.inspect_err(|_| error!("{}", cap_effective_error("setpcap")))?;
let mut capstate = CapState::empty();
if bounding.is_strict() {
for cap in (!caps).iter() {
capctl::bounding::drop(cap).expect("Failed to set bounding cap");
}
}
capstate.effective.clear();
capstate.permitted = caps;
capstate.inheritable = caps;
debug!("caps : {caps:?}");
capstate.set_current().expect("Failed to set current cap");
for cap in caps.iter() {
capctl::ambient::raise(cap).expect("Failed to set ambiant cap");
}
Ok(())
}
fn setuid_setgid(cred: &CredOwnedData) -> SrResult<()> {
let gid = cred
.setgroups
.as_ref()
.and_then(|g| g.first().cloned())
.map(|g| g.gid.as_raw());
with_privileges(&[Cap::SETUID, Cap::SETGID], || {
capctl::cap_set_ids(
cred.setuid.as_ref().map(|u| u.uid.as_raw()),
gid,
cred.setgroups
.as_ref()
.map(|g| g.iter().map(|g| g.gid.as_raw()).collect::<Vec<_>>())
.as_deref(),
)?;
Ok(())
})
.map_err(|e| {
error!("{}", cap_effective_error("setuid/setgid"));
e.into()
})
}
fn close_restrictive_fds() -> io::Result<()> {
debug!("Closing restrictive FDs");
let mut fds = Vec::new();
let read_dir = std::fs::read_dir("/proc/self/fd")?;
let mut error = false;
for entry in read_dir.flatten() {
if let Ok(fd) = entry.file_name().to_string_lossy().parse::<i32>() {
if fd > 2 {
fds.push(fd);
} else {
error!("Failed to read fd entry: {entry:?}");
error = true;
}
}
}
for fd in fds {
let _ = nix::unistd::close(fd);
}
if error {
Err(io::Error::other("Failed to read some fd entries"))
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::fs;
use crate::finder::BestExecSettings;
use super::finder::de::cred::CredOwnedData;
use capctl::{Cap, CapSet};
use libc::getgid;
use nix::unistd::{Group, Pid, User, getgroups, getuid};
use rar_common::database::options::SBounding;
use super::*;
fn get_non_root_uid(nth: usize) -> Option<u32> {
let passwd = fs::read_to_string("/etc/passwd").unwrap();
let passwd: Vec<&str> = passwd.split('\n').collect();
passwd
.iter()
.map(|line| {
let line: Vec<&str> = line.split(':').collect();
line[2].parse::<u32>().unwrap()
})
.filter(|uid| *uid != 0)
.nth(nth)
}
fn get_non_root_gid(nth: usize) -> Option<u32> {
let passwd = fs::read_to_string("/etc/group").unwrap();
let passwd: Vec<&str> = passwd.split('\n').collect();
passwd
.iter()
.map(|line| {
let line: Vec<&str> = line.split(':').collect();
line[2].parse::<u32>().unwrap()
})
.filter(|uid| *uid != 0)
.nth(nth)
}
#[test]
fn test_getopt() {
let args = getopt(vec![
"sr",
"-u",
"root",
"-g",
"root,root",
"-r",
"role1",
"-t",
"task1",
"-p",
"prompt",
"-E",
"-i",
"-h",
"ls",
"-l",
])
.unwrap();
let opt_filter = args.opt_filter.as_ref().unwrap();
assert_eq!(opt_filter.user, Some(0));
assert_eq!(opt_filter.group, Some(vec![0, 0]));
assert_eq!(opt_filter.role.as_deref(), Some("role1"));
assert_eq!(opt_filter.task.as_deref(), Some("task1"));
assert_eq!(args.prompt.unwrap(), "prompt");
assert!(args.info);
assert!(args.help);
assert_eq!(args.cmd_path, PathBuf::from("ls"));
assert_eq!(args.cmd_args, vec!["-l".to_string()]);
}
#[test]
fn test_make_cred() {
let user = make_cred().unwrap();
assert_eq!(user.user.uid, getuid());
let groups = getgroups()
.unwrap()
.iter()
.map(|g| Group::from_gid(*g).unwrap().unwrap())
.collect::<Vec<_>>();
assert!(!user.groups.is_empty());
assert_eq!(
user.groups
.iter()
.map(|e| e.left().unwrap().clone())
.collect::<Vec<_>>(),
groups
);
assert_eq!(user.ppid, Pid::parent());
}
#[test]
fn test_setuid_setgid() {
let mut capset = CapState::get_current().unwrap();
if capset.permitted.has(Cap::SETUID) && capset.permitted.has(Cap::SETGID) {
println!("setuid and setgid are available");
capset.effective.add(Cap::SETUID);
capset.effective.add(Cap::SETGID);
capset.set_current().unwrap();
let execcfg = BestExecSettings::builder()
.cred(
CredOwnedData::builder()
.setuid(
User::from_uid(get_non_root_uid(0).unwrap().into())
.unwrap()
.unwrap(),
)
.setgroups(vec![
Group::from_gid(get_non_root_gid(0).unwrap().into())
.unwrap()
.unwrap(),
])
.build(),
)
.build();
setuid_setgid(&execcfg.cred).unwrap();
assert_eq!(getuid(), execcfg.cred.setuid.unwrap().uid);
if let Some(gid) = execcfg.cred.setgroups.as_ref().and_then(|g| g.first()) {
assert_eq!(unsafe { getgid() }, gid.gid.as_raw());
}
capset.effective.clear();
capset.set_current().unwrap();
}
}
#[test]
fn test_set_capabilities() {
let mut capset = CapState::get_current().unwrap();
if capset.permitted.has(Cap::SETPCAP)
&& capset.permitted.has(Cap::SETUID)
&& capset.permitted.has(Cap::SETGID)
{
capset.effective.add(Cap::SETPCAP);
capset.set_current().unwrap();
let mut execcfg = BestExecSettings::default();
let mut capset = CapSet::empty();
capset.add(Cap::SETUID);
capset.add(Cap::SETGID);
capset.add(Cap::SETPCAP);
execcfg.cred.caps = Some(capset);
set_capabilities(&execcfg.cred, execcfg.bounding).unwrap();
let capset = CapState::get_current().unwrap();
assert!(capset.permitted.has(Cap::SETUID));
assert!(capset.permitted.has(Cap::SETGID));
assert!(capset.permitted.has(Cap::SETPCAP));
assert!(capset.inheritable.has(Cap::SETUID));
assert!(capset.inheritable.has(Cap::SETGID));
assert!(capset.inheritable.has(Cap::SETPCAP));
assert!(capctl::bounding::probe().has(Cap::SETUID));
assert!(capctl::bounding::probe().has(Cap::SETGID));
assert!(capctl::bounding::probe().has(Cap::SETPCAP));
assert!(capctl::ambient::probe().unwrap().has(Cap::SETUID));
assert!(capctl::ambient::probe().unwrap().has(Cap::SETGID));
assert!(capctl::ambient::probe().unwrap().has(Cap::SETPCAP));
execcfg.cred.caps = None;
execcfg.bounding = SBounding::Strict;
set_capabilities(&execcfg.cred, execcfg.bounding).unwrap();
let capset = CapState::get_current().unwrap();
assert!(!capset.permitted.has(Cap::SETUID));
}
}
#[test]
fn test_setuid_setgid_coverage() {
let execcfg = BestExecSettings::builder()
.cred(
CredOwnedData::builder()
.setuid(
User::from_uid(get_non_root_uid(0).unwrap().into())
.unwrap()
.unwrap(),
)
.setgroups(vec![
Group::from_gid(get_non_root_gid(0).unwrap().into())
.unwrap()
.unwrap(),
])
.build(),
)
.build();
let _ = setuid_setgid(&execcfg.cred);
}
#[test]
fn test_set_capabilities_coverage() {
let mut execcfg = BestExecSettings::default();
let mut capset = CapSet::empty();
capset.add(Cap::SETUID);
execcfg.cred.caps = Some(capset);
let _ = set_capabilities(&execcfg.cred, execcfg.bounding);
}
}