use std::{
cmp::Ordering,
collections::HashMap,
fs,
io::BufReader,
path::{Path, PathBuf},
};
use api::{Api, ApiEvent};
use bon::Builder;
use de::{ConfigFinderDeserializer, DConfigFinder, DLinkedCommand, DLinkedRole, DLinkedTask};
use log::{debug, error, warn};
use options::BorrowedOptStack;
use rootasrole_core::{
Cred,
database::{
actor::{DGroupType, DGroups},
options::{SAuthentication, SBounding, SPrivileged, STimeout, SUMask},
score::{ActorMatchMin, CmdMin, CmdOrder, HardenedBool, Score, hardened_bool_from_bool},
workdir::ResolvedWorkdir,
},
util::{StorageMethod, check_immutable, check_others_permissions, read_with_privileges},
};
use serde::de::DeserializeSeed;
use crate::{
Cli,
error::{SrError, SrResult},
finder::de::{cred::CredOwnedData, settings::read_storage},
};
pub mod api;
pub mod cmd;
use cmd::resolve_any_command;
pub mod de;
mod options;
#[derive(Debug, Default, Clone, Builder)]
pub struct BestExecSettings {
#[builder(default)]
pub score: Score,
#[builder(default)]
pub final_path: PathBuf,
#[builder(default)]
pub cred: CredOwnedData,
pub task: Option<String>,
#[builder(default)]
pub role: String,
#[builder(default)]
pub env: HashMap<String, String>,
#[builder(default)]
pub env_path: Vec<String>,
pub workdir: Option<PathBuf>,
#[builder(default)]
pub bounding: SBounding,
#[builder(default)]
pub timeout: STimeout,
#[builder(default)]
pub auth: SAuthentication,
#[builder(default)]
pub root: SPrivileged,
#[builder(default)]
pub umask: SUMask,
#[builder(skip)]
best_task: Option<usize>,
#[builder(skip)]
tie: Option<String>,
}
pub fn find_best_exec_settings<'de: 'a, 'a, P>(
cli: &'a Cli,
cred: &'a Cred,
rar_cfg_path: P,
rar_cfg_data_path: P,
rar_cfg_type: StorageMethod,
env_vars: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
env_path: &[&str],
) -> SrResult<BestExecSettings>
where
P: AsRef<Path>,
{
let env_vars: Vec<(String, String)> = env_vars
.into_iter()
.map(|(key, value)| (key.into(), value.into()))
.collect();
let settings_file = read_storage(rar_cfg_path, rar_cfg_type)?;
let storage_settings = settings_file.data.data.settings.unwrap_or_default();
let immutable = storage_settings.immutable.unwrap_or(false);
let file_path = storage_settings
.path
.unwrap_or_else(|| rar_cfg_data_path.as_ref().to_path_buf());
let parse_file = |file_path: &Path| -> SrResult<BestExecSettings> {
let config_finder_deserializer = ConfigFinderDeserializer { cli, cred };
let file = read_with_privileges(file_path)?;
check_others_permissions(file_path, &file)?;
if immutable {
check_immutable(file_path, &file)?;
}
let reader = BufReader::new(file);
match settings_file.data.data.method {
StorageMethod::CBOR => {
let mut io_reader = cbor4ii::core::utils::IoReader::new(reader);
Ok(BestExecSettings::evaluate(
cli,
cred,
&config_finder_deserializer
.deserialize(&mut cbor4ii::serde::Deserializer::new(&mut io_reader))
.map_err(|e| {
debug!("Error deserializing CBOR: {e}");
SrError::ConfigurationError
})?,
env_vars.iter().cloned(),
env_path,
)?)
}
StorageMethod::JSON => {
let io_reader = serde_json::de::IoRead::new(reader);
Ok(BestExecSettings::evaluate(
cli,
cred,
&config_finder_deserializer
.deserialize(&mut serde_json::Deserializer::new(io_reader))
.map_err(|e| {
debug!("Error deserializing JSON: {e}");
SrError::ConfigurationError
})?,
env_vars.iter().cloned(),
env_path,
)?)
}
}
};
if file_path.is_dir() {
let mut entries: Vec<PathBuf> = fs::read_dir(&file_path)
.map_err(|e| {
debug!(
"Failed to read policy directory {}: {e}",
file_path.display()
);
SrError::ConfigurationError
})?
.flatten()
.map(|entry| entry.path())
.filter(|path| path.is_file())
.collect();
entries.sort();
let mut best: Option<BestExecSettings> = None;
let mut parsed_any = false;
for entry in entries {
let result = parse_file(&entry);
match result {
Ok(settings) => {
parsed_any = true;
BestExecSettings::keep_best(&mut best, settings);
}
Err(SrError::PermissionDenied) => {
parsed_any = true;
}
Err(err) => {
debug!("Skipping policy file {}: {err}", entry.display());
}
}
}
return best
.ok_or({
if parsed_any {
SrError::PermissionDenied
} else {
SrError::ConfigurationError
}
})
.and_then(BestExecSettings::refuse_tie);
}
parse_file(&file_path).and_then(BestExecSettings::refuse_tie)
}
impl BestExecSettings {
pub(crate) fn retrieve_settings<'a>(
cli: &'a Cli,
cred: &'a Cred,
data: &'a DConfigFinder<'a>,
env_vars: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
env_path: &[&str],
) -> SrResult<Self> {
Self::evaluate(cli, cred, data, env_vars, env_path).and_then(Self::refuse_tie)
}
fn refuse_tie(self) -> SrResult<Self> {
if let Some(other) = &self.tie {
warn!(
"Permission denied: the tasks {} and {other} match the command equally, with the same privileges; the policy must tell them apart",
self.task_name()
);
return Err(SrError::PermissionDenied);
}
Ok(self)
}
fn keep_best(best: &mut Option<Self>, settings: Self) {
match best {
Some(best) if !settings.score.better_fully(&best.score) => {
if settings.score.cmp(&best.score) == Ordering::Equal {
best.tie.get_or_insert_with(|| settings.task_name());
}
}
_ => *best = Some(settings),
}
}
fn task_name(&self) -> String {
format!("{}/{}", self.role, self.task.as_deref().unwrap_or_default())
}
fn evaluate<'a>(
cli: &'a Cli,
cred: &'a Cred,
data: &'a DConfigFinder<'a>,
env_vars: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
env_path: &[&str],
) -> SrResult<Self> {
let mut result = Self::default();
let mut matching = false;
let mut opt_stack = BorrowedOptStack::new(data.options.as_ref());
for role in data.roles() {
if !result.actors_settings(&role)? {
continue;
}
matching |= result.role_tasks_settings(cli, cred, &role, &mut opt_stack, env_path)?;
Api::notify(ApiEvent::BestRoleSettingsFound(
cli,
cred,
&role,
&mut opt_stack,
&env_path,
&mut result,
&mut matching,
))?;
}
if !matching {
return Err(SrError::PermissionDenied);
}
result.env = opt_stack
.calc_temp_env(opt_stack.calc_override_behavior(), cli.opt_filter.as_ref())
.final_env(
env_vars,
opt_stack.calc_path(env_path),
cred,
result.cred.setuid.as_ref(),
format!(
"{}{}",
cli.cmd_path.display(),
if cli.cmd_args.is_empty() {
String::new()
} else {
format!(" {}", cli.cmd_args.join(" "))
}
),
)
.ok_or_else(|| {
error!("Internal Error with environment behavior");
SrError::ConfigurationError
})?;
result.auth = opt_stack.calc_authentication();
result.bounding = opt_stack.calc_bounding();
result.timeout = opt_stack.calc_timeout();
result.root = opt_stack.calc_privileged();
result.umask = opt_stack.calc_umask();
result.workdir = opt_stack.calc_workdir();
Ok(result)
}
#[cfg(test)]
pub fn role_settings<'c, 'a>(
&mut self,
cli: &'c Cli,
cred: &'c Cred,
data: &DLinkedRole<'c, 'a>,
opt_stack: &mut BorrowedOptStack<'c, 'a>,
env_path: &[&str],
) -> SrResult<bool> {
debug!("role_settings: {:?}", data.role().role);
if !self.actors_settings(data)? {
return Ok(false);
}
self.role_tasks_settings(cli, cred, data, opt_stack, env_path)
}
fn role_tasks_settings<'c, 'a>(
&mut self,
cli: &'c Cli,
cred: &'c Cred,
data: &DLinkedRole<'c, 'a>,
opt_stack: &mut BorrowedOptStack<'c, 'a>,
env_path: &[&str],
) -> SrResult<bool> {
let mut res = false;
for task in data.tasks() {
res |= self.task_settings(cli, cred, &task, opt_stack, env_path)?;
}
Ok(res)
}
pub fn actors_settings(&mut self, data: &DLinkedRole<'_, '_>) -> SrResult<bool> {
let mut res = !data.role().user_min.is_no_match();
Api::notify(ApiEvent::ActorMatching(data, self, &mut res))?;
Ok(res)
}
pub fn task_settings<'t, 'c, 'a>(
&mut self,
cli: &'t Cli,
cred: &'t Cred,
data: &DLinkedTask<'t, 'c, 'a>,
opt_stack: &mut BorrowedOptStack<'c, 'a>,
env_path: &[&str],
) -> SrResult<bool> {
let held_through = data.role().role().user_min;
self.task_settings_as(cli, cred, data, held_through, opt_stack, env_path)
}
#[allow(clippy::too_many_lines)]
pub fn task_settings_as<'t, 'c, 'a>(
&mut self,
cli: &'t Cli,
cred: &'t Cred,
data: &DLinkedTask<'t, 'c, 'a>,
held_through: ActorMatchMin,
opt_stack: &mut BorrowedOptStack<'c, 'a>,
env_path: &[&str],
) -> SrResult<bool> {
debug!("task_settings: {:?}", data.id);
let temp_opt_stack = BorrowedOptStack::from_task(data);
if let Some(entry) = temp_opt_stack.invalid_env_entry() {
error!(
"task_settings: deny task {:?}: invalid environment entry `{entry}`, it must be a variable name where `*` matches any characters",
data.id
);
return Ok(false);
}
let mut task_env_path = None;
if cli
.opt_filter
.as_ref()
.is_some_and(|f| f.env_behavior.is_some() && !temp_opt_stack.calc_override_behavior())
{
debug!(
"task_settings: deny task due to inherited from role or config env_override requirement"
);
return Ok(false);
}
let workdir = temp_opt_stack.get_workdir_temp();
let requested = cli.opt_filter.as_ref().and_then(|f| f.workdir.as_ref());
if let Some(requested) = requested {
if Self::allowed_workdir(&workdir, requested).is_false() {
debug!(
"task_settings: deny task due to the user wanted to execute to a specific folder, which do not match to settings"
);
return Ok(false);
}
} else if workdir.fallback.is_none()
&& Self::allowed_workdir(&workdir, cred.curdir.as_os_str().to_string_lossy().as_ref())
.is_false()
{
debug!(
"task_settings: deny task due to the current directory do not match to allowed settings"
);
return Ok(false);
}
if cli.info && temp_opt_stack.calc_info().is_hide() {
debug!("task_settings: deny task due to inherited from role or config info hide");
return Ok(false);
}
let mut task_cmd_min = CmdMin::empty();
let mut task_final_path = None;
if let Some(commands) = data.commands() {
let t_env_path = temp_opt_stack.calc_path(env_path);
let t_env_path_ref: Vec<&str> = t_env_path.iter().map(String::as_str).collect();
for command in commands.del() {
if Self::match_command(&t_env_path_ref, cli, &command)?.is_some() {
debug!("task_settings: deny task, the command is in its deny list");
return Ok(false);
}
}
if commands.default_behavior.is_some_and(|b| b.is_all()) {
debug!("default behavior is all");
debug!("{}", &cli.cmd_path.display());
task_final_path = Some(
resolve_any_command(&cli.cmd_path, &t_env_path_ref)
.ok_or(SrError::ExecutionFailed)?,
);
task_cmd_min = CmdMin::builder()
.matching()
.order(CmdOrder::FullWildcardPath | CmdOrder::FullRegexArgs)
.build();
} else {
for command in commands.add() {
if let Some((cmd_min, final_path)) =
Self::match_command(&t_env_path_ref, cli, &command)?
&& cmd_min.better(task_cmd_min)
{
task_cmd_min = cmd_min;
task_final_path = Some(final_path);
}
}
}
task_env_path = Some(t_env_path);
}
let found = task_cmd_min.matching();
let mut score = data.score(task_cmd_min, temp_opt_stack.calc_security_min());
score.set_role_score(&held_through);
Api::notify(ApiEvent::BestTaskSettingsFound(
cli, data, opt_stack, self, &mut score,
))?;
let task_address = std::ptr::from_ref(data.task).addr();
if found
&& task_final_path.is_some()
&& score.fully_matching()
&& !score.better_fully(&self.score)
&& score.cmp(&self.score) == Ordering::Equal
&& self.best_task != Some(task_address)
{
debug!("task {:?} is as good as the best task", data.id);
self.tie
.get_or_insert_with(|| format!("{}/{}", data.role().role().role, data.id));
}
if found
&& let Some(final_path) = task_final_path
&& score.better_fully(&self.score)
{
debug!("found better task settings");
self.best_task = Some(task_address);
self.tie = None;
self.final_path = final_path;
self.role = data.role().role().role.to_string();
self.task = Some(data.id.to_string());
self.env_path = task_env_path.unwrap_or_else(|| temp_opt_stack.calc_path(env_path));
self.score = score;
self.cred.setuid = data.cred.setuid.clone().and_then(|u| u.fetch_user());
self.cred.setgroups = data.cred.setgroups.clone().map(|g| match g {
DGroups::Single(g) => vec![g.fetch_group()].into_iter().flatten().collect(),
DGroups::Multiple(g) => g.iter().filter_map(DGroupType::fetch_group).collect(),
});
self.cred.caps = data.cred.caps;
opt_stack.set_role(data);
opt_stack.set_task(data);
debug!("resulting settings: {self:?}");
}
Ok(found)
}
pub fn match_command<'d>(
env_path: &[&str],
cli: &'d Cli,
data: &DLinkedCommand<'d, '_, '_, '_, '_>,
) -> SrResult<Option<(CmdMin, PathBuf)>> {
debug!("env_path: {env_path:?}");
let mut final_path = None;
let cmd_min = match &**data {
de::DCommand::Simple(role_cmd) => cmd::evaluate_command_match(
env_path,
&cli.cmd_path,
&cli.cmd_args,
role_cmd,
CmdMin::empty(),
&mut final_path,
),
de::DCommand::Complex(value) => {
let mut cmd_min = CmdMin::empty();
Api::notify(ApiEvent::ProcessComplexCommand(
value,
env_path,
&cli.cmd_path,
&cli.cmd_args,
&mut cmd_min,
&mut final_path,
))?;
cmd_min
}
};
Ok(final_path
.filter(|_| cmd_min.matching())
.map(|final_path| (cmd_min, final_path)))
}
fn allowed_workdir(result: &ResolvedWorkdir<'_>, workdir: &str) -> HardenedBool {
hardened_bool_from_bool(result.allows(workdir))
}
}
#[cfg(test)]
mod tests {
use super::de::{DCommand, DCommandList, DRoleFinder, DTaskFinder, IdTask};
use super::*;
use capctl::CapSet;
use rootasrole_core::database::FilterMatcher;
use rootasrole_core::database::options::{EnvBehavior, Level, SInfo};
use rootasrole_core::database::score::{ActorMatchMin, CmdMin};
use rootasrole_core::database::structs::SetBehavior;
use serde_json::Value;
use std::path::PathBuf;
use crate::Cli;
use crate::finder::de::cred::CredData;
use crate::finder::options::{DEnvOptions, DPathOptions, Opt};
use rootasrole_core::Cred;
use test_log::test;
fn dummy_cli() -> Cli {
Cli::builder()
.cmd_path("/usr/bin/ls".to_string())
.cmd_args(vec!["-l".to_string()])
.build()
}
fn dummy_cred() -> Cred {
Cred::builder()
.curdir()
.unwrap()
.groups()
.unwrap()
.user()
.unwrap()
.build()
}
fn dummy_dconfigfinder<'a>() -> DConfigFinder<'a> {
DConfigFinder::builder()
.roles(vec![
DRoleFinder::builder()
.user_min(ActorMatchMin::UserMatch)
.role("test")
.tasks(vec![
DTaskFinder::builder()
.id(IdTask::Number(0))
.cred(CredData::builder().caps(!CapSet::empty()).build())
.commands(
DCommandList::builder(SetBehavior::None)
.add(vec![DCommand::simple("/usr/bin/ls -l")])
.build(),
)
.options(Opt::builder(Level::Task).execinfo(SInfo::Hide).build())
.build(),
DTaskFinder::builder()
.id(IdTask::Number(1))
.cred(CredData::builder().caps(CapSet::empty()).build())
.commands(
DCommandList::builder(SetBehavior::None)
.add(vec![
DCommand::simple("/usr/bin/ls ^.*$"),
DCommand::complex(Value::Object(
std::iter::once((
"key".to_string(),
Value::String("value".into()),
))
.collect::<serde_json::Map<String, Value>>(),
)),
])
.build(),
)
.options(
Opt::builder(Level::Task)
.execinfo(SInfo::Show)
.env(
DEnvOptions::builder(EnvBehavior::Delete)
.override_behavior(true)
.build(),
)
.build(),
)
.build(),
])
.build(),
DRoleFinder::builder()
.user_min(ActorMatchMin::UserMatch)
.role("test2")
.tasks(vec![
DTaskFinder::builder()
.id(IdTask::Number(0))
.cred(CredData::builder().caps(!CapSet::empty()).build())
.commands(
DCommandList::builder(SetBehavior::None)
.add(vec![DCommand::simple("/usr/bin/ls -l")])
.build(),
)
.options(
Opt::builder(Level::Task)
.execinfo(SInfo::Show)
.env(
DEnvOptions::builder(EnvBehavior::Delete)
.override_behavior(true)
.build(),
)
.build(),
)
.build(),
DTaskFinder::builder()
.id(IdTask::Number(1))
.cred(CredData::builder().caps(CapSet::empty()).build())
.commands(
DCommandList::builder(SetBehavior::None)
.add(vec![DCommand::simple("/usr/bin/ls ^.*$")])
.build(),
)
.options(Opt::builder(Level::Task).execinfo(SInfo::Hide).build())
.build(),
])
.build(),
])
.build()
}
#[test]
fn test_retrieve_settings_no_matching_role() {
let cli = Cli::builder().cmd_path("/usr/bin/cat".to_string()).build();
let cred = dummy_cred();
let data = dummy_dconfigfinder();
let env_vars = vec![("KEY", "VALUE")];
let env_path = &["/bin"];
let result = BestExecSettings::retrieve_settings(&cli, &cred, &data, env_vars, env_path);
assert!(result.is_err());
}
#[test]
fn test_retrieve_settings_with_matching_role() {
let cli = dummy_cli();
let cred = dummy_cred();
let data = dummy_dconfigfinder();
let env_vars = vec![("KEY", "VALUE")];
let env_path = &["/UNWANTED"];
let result = BestExecSettings::retrieve_settings(&cli, &cred, &data, env_vars, env_path);
assert!(result.is_ok());
let settings = result.unwrap();
assert_eq!(settings.final_path, PathBuf::from("/usr/bin/ls"));
assert_eq!(settings.role, "test");
assert_eq!(settings.task, Some("0".to_string()));
assert!(settings.cred.setuid.is_none());
assert!(settings.cred.setgroups.is_none());
assert!(settings.cred.caps.is_some());
assert!(!settings.env.is_empty());
assert!(!settings.env_path.is_empty());
assert!(settings.env_path.iter().all(|p| p != "/UNWANTED"));
}
#[test]
fn test_commands_are_resolved_with_the_path_of_their_task() {
let path = |dir: &'static str| {
Opt::builder(Level::Task)
.path(
DPathOptions::builder(rootasrole_core::database::options::PathBehavior::Delete)
.add([dir])
.build(),
)
.build()
};
let data = DConfigFinder::builder()
.options(path("/nonexistent"))
.roles(vec![
DRoleFinder::builder()
.user_min(ActorMatchMin::UserMatch)
.role("test")
.tasks(vec![
DTaskFinder::builder()
.id(IdTask::Number(0))
.cred(CredData::builder().build())
.commands(DCommandList::builder(SetBehavior::All).build())
.options(path("/usr/bin"))
.build(),
])
.build(),
])
.build();
let cli = Cli::builder().cmd_path("ls".to_string()).build();
let settings = BestExecSettings::retrieve_settings(
&cli,
&dummy_cred(),
&data,
vec![("KEY", "VALUE")],
&["/bin"],
)
.expect("the task allows every command in its PATH");
assert_eq!(settings.final_path, PathBuf::from("/usr/bin/ls"));
assert_eq!(settings.env_path, ["/usr/bin"]);
}
fn task_json(name: &str, caps: &[&str], commands: &str, keep: &str, path: &str) -> String {
format!(
r#"{{"name":"{name}","cred":{{"caps":{caps}}},"commands":{commands},"options":{{"env":{{"default":"delete","keep":["{keep}"]}},"path":{{"default":"delete","add":["{path}"]}}}}}}"#,
caps = serde_json::to_string(caps).unwrap()
)
}
fn best_task(
tasks: &[&String],
cmd: &str,
) -> SrResult<(String, PathBuf, Vec<String>, Vec<String>)> {
best_task_for(tasks, &Cli::builder().cmd_path(cmd.to_string()).build())
}
fn best_task_for(
tasks: &[&String],
cli: &Cli,
) -> SrResult<(String, PathBuf, Vec<String>, Vec<String>)> {
use serde::de::DeserializeSeed;
let policy = format!(
r#"{{"roles":[{{"name":"r","actors":[{{"type":"user","id":{}}}],"tasks":[{}]}}]}}"#,
nix::unistd::getuid(),
tasks
.iter()
.map(|task| task.as_str())
.collect::<Vec<_>>()
.join(",")
);
let cred = dummy_cred();
let env_path = ["/usr/bin"];
let data = super::de::ConfigFinderDeserializer { cli, cred: &cred }
.deserialize(&mut serde_json::Deserializer::from_str(&policy))
.expect("valid policy");
let user_env = vec![("A_VAR", "a"), ("B_VAR", "b"), ("C_VAR", "c")];
let settings = BestExecSettings::retrieve_settings(cli, &cred, &data, user_env, &env_path)?;
let kept = ["A_VAR", "B_VAR", "C_VAR"]
.into_iter()
.filter(|var| settings.env.contains_key(*var))
.map(ToString::to_string)
.collect();
Ok((
settings.task.unwrap_or_default(),
settings.final_path,
settings.env_path,
kept,
))
}
fn orders<'t>(tasks: &[&'t String]) -> Vec<Vec<&'t String>> {
if tasks.len() <= 1 {
return vec![tasks.to_vec()];
}
(0..tasks.len())
.flat_map(|first| {
let mut rest = tasks.to_vec();
let first = rest.remove(first);
orders(&rest).into_iter().map(move |mut order| {
order.insert(0, first);
order
})
})
.collect()
}
#[test]
fn test_best_task_needs_the_fewest_privileges_in_any_order() {
let a = task_json(
"a",
&["CAP_NET_RAW"],
r#"["/usr/bin/ls"]"#,
"A_VAR",
"/usr/bin",
);
let b = task_json("b", &[], r#"["/usr/bin/ls"]"#, "B_VAR", "/usr/bin");
for tasks in orders(&[&a, &b]) {
let (task, _, _, kept) = best_task(&tasks, "/usr/bin/ls").unwrap();
assert_eq!(task, "b", "{tasks:?}");
assert_eq!(kept, ["B_VAR"], "{tasks:?}");
}
}
#[test]
fn test_best_task_needs_no_critical_capability_in_any_order() {
let a = task_json(
"a",
&["CAP_NET_BIND_SERVICE"],
r#"["/usr/bin/ls"]"#,
"A_VAR",
"/usr/bin",
);
let b = task_json(
"b",
&["CAP_SYS_ADMIN"],
r#"["/usr/bin/ls"]"#,
"B_VAR",
"/usr/bin",
);
let c = task_json("c", &[], r#"["/usr/bin/ls"]"#, "C_VAR", "/usr/bin")
.replace(r#""caps":[]"#, r#""caps":"all""#);
for tasks in orders(&[&a, &b, &c]) {
let (task, _, _, kept) = best_task(&tasks, "/usr/bin/ls").unwrap();
assert_eq!(task, "a", "{tasks:?}");
assert_eq!(kept, ["A_VAR"], "{tasks:?}");
}
}
#[test]
fn test_tie_between_the_best_tasks_is_refused_in_any_order() {
let a = task_json("a", &[], r#"["/usr/bin/ls"]"#, "A_VAR", "/usr/bin");
let b = task_json("b", &[], r#"["/usr/bin/ls"]"#, "B_VAR", "/usr/bin");
let c = task_json(
"c",
&["CAP_NET_RAW"],
r#"["/usr/bin/ls"]"#,
"C_VAR",
"/usr/bin",
);
for tasks in orders(&[&a, &b, &c]) {
assert!(
matches!(
best_task(&tasks, "/usr/bin/ls"),
Err(SrError::PermissionDenied)
),
"{tasks:?}"
);
}
let cli = Cli::builder()
.cmd_path("/usr/bin/ls".to_string())
.opt_filter(
FilterMatcher::builder()
.role("r".to_string())
.task("b".to_string())
.build(),
)
.build();
for tasks in orders(&[&a, &b, &c]) {
let (task, _, _, kept) = best_task_for(&tasks, &cli).unwrap();
assert_eq!(task, "b", "{tasks:?}");
assert_eq!(kept, ["B_VAR"], "{tasks:?}");
}
}
#[test]
fn test_tie_between_worse_tasks_does_not_matter() {
let a = task_json(
"a",
&["CAP_NET_RAW"],
r#"["/usr/bin/ls"]"#,
"A_VAR",
"/usr/bin",
);
let b = task_json(
"b",
&["CAP_NET_RAW"],
r#"["/usr/bin/ls"]"#,
"B_VAR",
"/usr/bin",
);
let c = task_json("c", &[], r#"["/usr/bin/ls"]"#, "C_VAR", "/usr/bin");
for tasks in orders(&[&a, &b, &c]) {
let (task, _, _, kept) = best_task(&tasks, "/usr/bin/ls").unwrap();
assert_eq!(task, "c", "{tasks:?}");
assert_eq!(kept, ["C_VAR"], "{tasks:?}");
}
}
#[test]
fn test_losing_task_does_not_leak_into_the_result() {
let a = task_json("a", &[], r#"["/usr/bin/ls"]"#, "A_VAR", "/usr/bin");
let b = task_json(
"b",
&["CAP_NET_RAW"],
r#"{"default":"all"}"#,
"B_VAR",
"/bin",
);
for tasks in orders(&[&a, &b]) {
let (task, final_path, env_path, kept) = best_task(&tasks, "ls").unwrap();
assert_eq!(task, "a", "{tasks:?}");
assert_eq!(final_path, PathBuf::from("/usr/bin/ls"), "{tasks:?}");
assert_eq!(env_path, ["/usr/bin"], "{tasks:?}");
assert_eq!(kept, ["A_VAR"], "{tasks:?}");
}
}
#[test]
fn test_losing_task_does_not_degrade_the_best_score() {
let a = task_json("a", &[], r#"["/usr/bin/ls -l"]"#, "A_VAR", "/usr/bin");
let b = task_json(
"b",
&["CAP_NET_RAW"],
r#"{"default":"all"}"#,
"B_VAR",
"/usr/bin",
);
let c = task_json("c", &[], r#"["/usr/bin/ls ^.*$"]"#, "C_VAR", "/usr/bin");
let cli = crate::getopt(["dosr", "ls", "-l"]).unwrap();
for tasks in orders(&[&a, &b, &c]) {
let (task, _, _, kept) = best_task_for(&tasks, &cli).unwrap();
assert_eq!(task, "a", "{tasks:?}");
assert_eq!(kept, ["A_VAR"], "{tasks:?}");
}
}
#[test]
fn test_command_without_arguments_allows_none() {
let none = task_json("none", &[], r#"["/usr/bin/ls"]"#, "A_VAR", "/usr/bin");
let any = task_json("any", &[], r#"["/usr/bin/ls ^.*$"]"#, "B_VAR", "/usr/bin");
let with_args = crate::getopt(["dosr", "ls", "-l"]).unwrap();
let without_args = crate::getopt(["dosr", "ls"]).unwrap();
assert!(best_task_for(&[&none], &with_args).is_err());
for tasks in orders(&[&none, &any]) {
assert_eq!(
best_task_for(&tasks, &with_args).unwrap().0,
"any",
"{tasks:?}"
);
assert_eq!(
best_task_for(&tasks, &without_args).unwrap().0,
"none",
"{tasks:?}"
);
}
}
#[test]
fn test_denied_command_only_denies_its_own_task() {
let d = task_json(
"d",
&["CAP_NET_RAW"],
r#"{"default":"all","del":["/usr/bin/ls"]}"#,
"B_VAR",
"/usr/bin",
);
let c = task_json("c", &[], r#"["/usr/bin/ls ^.*$"]"#, "C_VAR", "/usr/bin");
for tasks in orders(&[&d, &c]) {
let (task, final_path, _, _) = best_task(&tasks, "ls").unwrap();
assert_eq!(task, "c", "{tasks:?}");
assert_eq!(final_path, PathBuf::from("/usr/bin/ls"), "{tasks:?}");
}
assert!(best_task(&[&d], "ls").is_err());
}
#[test]
fn test_forced_workdir_is_not_checked_against_the_current_directory() {
let task =
r#"{"name":"w","cred":{},"commands":["/usr/bin/ls"],"options":{"workdir":"/srv"}}"#
.to_string();
let (task, final_path, _, _) = best_task(&[&task], "/usr/bin/ls").unwrap();
assert_eq!(task, "w");
assert_eq!(final_path, PathBuf::from("/usr/bin/ls"));
let task = r#"{"name":"w","cred":{},"commands":["/usr/bin/ls"],"options":{"workdir":{"default":"none","add":["/srv"]}}}"#
.to_string();
assert!(best_task(&[&task], "/usr/bin/ls").is_err());
}
#[test]
fn test_any_binary_pattern() {
let task = r#"{"name":"any","cred":{},"commands":["**"]}"#.to_string();
let (name, final_path, _, _) = best_task(&[&task], "/usr/bin/ls").unwrap();
assert_eq!(name, "any");
assert_eq!(final_path, PathBuf::from("/usr/bin/ls"));
let (_, final_path, env_path, _) = best_task(&[&task], "ls").unwrap();
assert!(
env_path
.iter()
.any(|dir| final_path == Path::new(dir).join("ls"))
);
assert!(best_task(&[&task], "rar-no-such-command").is_err());
let exact = task_json("exact", &[], r#"["/usr/bin/ls"]"#, "A_VAR", "/usr/bin");
for tasks in orders(&[&task, &exact]) {
assert_eq!(best_task(&tasks, "ls").unwrap().0, "exact");
}
}
struct PolicyDir(PathBuf);
impl PolicyDir {
fn new(name: &str) -> Self {
let dir =
std::env::temp_dir().join(format!("rar-finder-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
set_mode(&dir, 0o700);
Self(dir)
}
}
impl Drop for PolicyDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn set_mode(path: &Path, mode: u32) {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode)).unwrap();
}
fn write_file(path: &Path, content: &Value, method: StorageMethod, mode: u32) {
let bytes = match method {
StorageMethod::JSON => serde_json::to_vec(content).unwrap(),
StorageMethod::CBOR => cbor4ii::serde::to_vec(Vec::new(), content).unwrap(),
};
std::fs::write(path, bytes).unwrap();
set_mode(path, mode);
}
fn root_settings(method: StorageMethod, data: &Path, roles: Value) -> Value {
let mut settings = serde_json::json!({
"version": rootasrole_core::PACKAGE_VERSION.to_string(),
"storage": {
"method": method,
"settings": {"path": data, "immutable": false}
}
});
settings["roles"] = roles;
settings
}
fn policy(roles: Value) -> Value {
let mut policy =
serde_json::json!({ "version": rootasrole_core::PACKAGE_VERSION.to_string() });
policy["roles"] = roles;
policy
}
fn user_role(task: &str, command: &str, caps: &[&str]) -> Value {
serde_json::json!([{
"name": "r",
"actors": [{"type": "user", "id": nix::unistd::getuid().as_raw()}],
"tasks": [{"name": task, "cred": {"caps": caps}, "commands": [command]}]
}])
}
fn find(root: &Path, method: StorageMethod, cmd: &str) -> SrResult<String> {
let cli = Cli::builder().cmd_path(cmd.to_string()).build();
find_best_exec_settings(
&cli,
&dummy_cred(),
root,
root,
method,
vec![("A", "B")],
&["/usr/bin"],
)
.map(|settings| settings.task.unwrap_or_default())
}
#[test]
fn test_find_best_exec_settings_single_file() {
for method in [StorageMethod::JSON, StorageMethod::CBOR] {
let dir = PolicyDir::new(&format!("single-{method}"));
let root = dir.0.join("rootasrole");
let roles = user_role("t", "/usr/bin/ls", &[]);
write_file(&root, &root_settings(method, &root, roles), method, 0o600);
assert_eq!(find(&root, method, "/usr/bin/ls").unwrap(), "t", "{method}");
assert!(matches!(
find(&root, method, "/usr/bin/cat"),
Err(SrError::PermissionDenied)
));
}
}
#[test]
fn test_immutable_policy_refuses_a_mutable_file() {
let dir = PolicyDir::new("immutable");
let policies = dir.0.join("policies");
std::fs::create_dir(&policies).unwrap();
set_mode(&policies, 0o700);
let planted = policy(user_role("planted", "/usr/bin/ls", &[]));
write_file(
&policies.join("planted.json"),
&planted,
StorageMethod::JSON,
0o600,
);
let single = dir.0.join("policy.json");
write_file(&single, &planted, StorageMethod::JSON, 0o600);
let root = dir.0.join("rootasrole.json");
for data in [&policies, &single] {
let mut settings = root_settings(StorageMethod::JSON, data, serde_json::json!([]));
write_file(&root, &settings, StorageMethod::JSON, 0o600);
assert_eq!(
find(&root, StorageMethod::JSON, "/usr/bin/ls").unwrap(),
"planted",
"{}",
data.display()
);
settings["storage"]["settings"]["immutable"] = serde_json::json!(true);
write_file(&root, &settings, StorageMethod::JSON, 0o600);
assert!(
find(&root, StorageMethod::JSON, "/usr/bin/ls").is_err(),
"{}",
data.display()
);
}
}
#[test]
fn test_find_best_exec_settings_policy_directory() {
let dir = PolicyDir::new("directory");
let policies = dir.0.join("policies");
std::fs::create_dir(&policies).unwrap();
set_mode(&policies, 0o700);
let root = dir.0.join("rootasrole.json");
let settings = root_settings(StorageMethod::JSON, &policies, serde_json::json!([]));
write_file(&root, &settings, StorageMethod::JSON, 0o600);
let admin = policy(user_role("admin", "/usr/bin/ls", &["CAP_NET_RAW"]));
write_file(&policies.join("a.json"), &admin, StorageMethod::JSON, 0o600);
let least = policy(user_role("least", "/usr/bin/ls", &[]));
write_file(&policies.join("b.json"), &least, StorageMethod::JSON, 0o600);
std::fs::write(policies.join("c.json"), "not a policy").unwrap();
set_mode(&policies.join("c.json"), 0o600);
assert_eq!(
find(&root, StorageMethod::JSON, "/usr/bin/ls").unwrap(),
"least"
);
let tie = policy(user_role("tie", "/usr/bin/ls", &[]));
write_file(&policies.join("b2.json"), &tie, StorageMethod::JSON, 0o600);
assert!(matches!(
find(&root, StorageMethod::JSON, "/usr/bin/ls"),
Err(SrError::PermissionDenied)
));
std::fs::remove_file(policies.join("b2.json")).unwrap();
let admin2 = policy(user_role("admin2", "/usr/bin/ls", &["CAP_NET_RAW"]));
write_file(
&policies.join("a2.json"),
&admin2,
StorageMethod::JSON,
0o600,
);
assert_eq!(
find(&root, StorageMethod::JSON, "/usr/bin/ls").unwrap(),
"least"
);
let writable = policy(user_role("writable", "/usr/bin/cat", &[]));
write_file(
&policies.join("d.json"),
&writable,
StorageMethod::JSON,
0o646,
);
assert!(matches!(
find(&root, StorageMethod::JSON, "/usr/bin/cat"),
Err(SrError::PermissionDenied)
));
}
#[test]
fn test_find_best_exec_settings_refuses_files_writable_by_others() {
let dir = PolicyDir::new("writable");
let root = dir.0.join("rootasrole.json");
let settings = root_settings(
StorageMethod::JSON,
&root,
user_role("t", "/usr/bin/ls", &[]),
);
write_file(&root, &settings, StorageMethod::JSON, 0o646);
assert!(matches!(
find(&root, StorageMethod::JSON, "/usr/bin/ls"),
Err(SrError::PermissionDenied)
));
set_mode(&root, 0o644);
assert_eq!(
find(&root, StorageMethod::JSON, "/usr/bin/ls").unwrap(),
"t"
);
set_mode(&dir.0, 0o757);
let result = find(&root, StorageMethod::JSON, "/usr/bin/ls");
set_mode(&dir.0, 0o700);
assert!(matches!(result, Err(SrError::PermissionDenied)));
let data = dir.0.join("policy.json");
let settings = root_settings(StorageMethod::JSON, &data, serde_json::json!([]));
write_file(&root, &settings, StorageMethod::JSON, 0o600);
let roles = policy(user_role("t", "/usr/bin/ls", &[]));
write_file(&data, &roles, StorageMethod::JSON, 0o646);
assert!(matches!(
find(&root, StorageMethod::JSON, "/usr/bin/ls"),
Err(SrError::PermissionDenied)
));
}
#[test]
fn test_find_best_exec_settings_invalid_files() {
let dir = PolicyDir::new("invalid");
let root = dir.0.join("rootasrole.json");
assert!(find(&root, StorageMethod::JSON, "/usr/bin/ls").is_err());
let no_storage = policy(user_role("t", "/usr/bin/ls", &[]));
write_file(&root, &no_storage, StorageMethod::JSON, 0o600);
assert!(find(&root, StorageMethod::JSON, "/usr/bin/ls").is_err());
let data = dir.0.join("policy.json");
let settings = root_settings(StorageMethod::JSON, &data, serde_json::json!([]));
write_file(&root, &settings, StorageMethod::JSON, 0o600);
std::fs::write(&data, "not a policy").unwrap();
set_mode(&data, 0o600);
assert!(matches!(
find(&root, StorageMethod::JSON, "/usr/bin/ls"),
Err(SrError::ConfigurationError)
));
}
fn choose(roles: &Value, cmd: &str) -> SrResult<(String, Option<String>, PathBuf)> {
use serde::de::DeserializeSeed;
api::register_plugins();
let policy = serde_json::json!({ "roles": roles }).to_string();
let cli = Cli::builder().cmd_path(cmd.to_string()).build();
let cred = dummy_cred();
let env_path = ["/usr/bin"];
let data = super::de::ConfigFinderDeserializer {
cli: &cli,
cred: &cred,
}
.deserialize(&mut serde_json::Deserializer::from_str(&policy))
.expect("valid policy");
let settings =
BestExecSettings::retrieve_settings(&cli, &cred, &data, vec![("A", "B")], &env_path)?;
Ok((settings.role, settings.task, settings.final_path))
}
#[test]
fn test_actors_in_short_form() {
let uid = nix::unistd::getuid().as_raw();
let mut cases = vec![
(serde_json::json!([{"user": uid}]), true),
(serde_json::json!([{"user": uid + 54_321}]), false),
];
if let Some(gid) = nix::unistd::getgroups().unwrap().first() {
let gid = gid.as_raw();
cases.push((serde_json::json!([{"group": gid}]), true));
cases.push((serde_json::json!([{"group": [gid]}]), true));
cases.push((serde_json::json!([{"user": uid, "group": gid}]), false));
}
for (actors, allowed) in cases {
let roles = serde_json::json!([role_json(
"r",
true,
Some("/usr/bin/ls"),
&serde_json::json!({ "actors": actors })
)]);
assert_eq!(choose(&roles, "/usr/bin/ls").is_ok(), allowed, "{actors}");
}
}
fn role_json(name: &str, member: bool, command: Option<&str>, extra: &Value) -> Value {
let uid = nix::unistd::getuid().as_raw();
let actor = if member { uid } else { uid + 54_321 };
let mut role = serde_json::json!({
"name": name,
"actors": [{"type": "user", "id": actor}],
"tasks": command.map_or_else(Vec::new, |command| {
vec![serde_json::json!({"name": format!("{name}_task"), "cred": {}, "commands": [command]})]
}),
});
if let (Some(role), Some(extra)) = (role.as_object_mut(), extra.as_object()) {
role.extend(extra.clone());
}
role
}
#[cfg(feature = "ssd")]
#[test]
fn test_ssd_plugin() {
let ssd = |value: Value| serde_json::json!({ "ssd": value });
let none = serde_json::json!({});
for value in [serde_json::json!(["auditor"]), serde_json::json!("auditor")] {
let roles = serde_json::json!([
role_json("dev", true, Some("/usr/bin/ls"), &ssd(value)),
role_json("auditor", true, None, &none),
]);
assert!(matches!(
choose(&roles, "/usr/bin/ls"),
Err(SrError::PermissionDenied)
));
}
let roles = serde_json::json!([
role_json(
"dev",
true,
Some("/usr/bin/ls"),
&ssd(serde_json::json!(["auditor"]))
),
role_json("auditor", false, None, &none),
]);
assert_eq!(choose(&roles, "/usr/bin/ls").unwrap().0, "dev");
let roles = serde_json::json!([
role_json(
"a",
false,
Some("/usr/bin/ls"),
&ssd(serde_json::json!(["b"]))
),
role_json("b", false, None, &ssd(serde_json::json!(["a"]))),
role_json("c", true, Some("/usr/bin/ls"), &none),
]);
assert_eq!(choose(&roles, "/usr/bin/ls").unwrap().0, "c");
let roles = serde_json::json!([role_json(
"dev",
true,
Some("/usr/bin/ls"),
&ssd(serde_json::json!(3))
)]);
assert!(matches!(
choose(&roles, "/usr/bin/ls"),
Err(SrError::ConfigurationError)
));
}
#[test]
fn test_default_policy_runs_as_the_requested_user() {
api::register_plugins();
let policy = include_str!("../../../resources/rootasrole.json");
let root = Cred {
user: nix::unistd::User::from_uid(0.into()).unwrap().unwrap(),
groups: Vec::new(),
tty: None,
ppid: nix::unistd::Pid::from_raw(1),
curdir: PathBuf::from("/"),
};
let nobody = nix::unistd::User::from_name("nobody").unwrap().unwrap();
let group = nix::unistd::Group::from_gid(nix::unistd::getgid())
.unwrap()
.unwrap();
let cli = crate::getopt([
"dosr",
"-u",
"nobody",
"-g",
group.name.as_str(),
"/usr/bin/id",
])
.unwrap();
let data = de::ConfigFinderDeserializer {
cli: &cli,
cred: &root,
}
.deserialize(&mut serde_json::Deserializer::from_str(policy))
.unwrap();
let settings = BestExecSettings::retrieve_settings(
&cli,
&root,
&data,
Vec::<(String, String)>::new(),
&["/usr/bin"],
)
.unwrap();
assert_eq!(settings.task.as_deref(), Some("t_root"));
assert_eq!(settings.cred.setuid.map(|user| user.uid), Some(nobody.uid));
assert_eq!(
settings
.cred
.setgroups
.map(|groups| groups.iter().map(|g| g.gid).collect::<Vec<_>>()),
Some(vec![group.gid])
);
}
#[cfg(feature = "hierarchy")]
#[test]
fn test_hierarchy_plugin() {
let parents = |value: Value| serde_json::json!({ "parents": value });
let none = serde_json::json!({});
for value in [serde_json::json!(["junior"]), serde_json::json!("junior")] {
let roles = serde_json::json!([
role_json("senior", true, None, &parents(value)),
role_json("junior", false, Some("/usr/bin/ls"), &none),
]);
let (_, task, final_path) = choose(&roles, "/usr/bin/ls").unwrap();
assert_eq!(task.as_deref(), Some("junior_task"));
assert_eq!(final_path, PathBuf::from("/usr/bin/ls"));
}
let roles = serde_json::json!([
role_json(
"senior",
true,
None,
&parents(serde_json::json!(["junior"]))
),
role_json("other", true, None, &parents(serde_json::json!(["junior"]))),
role_json("junior", true, Some("/usr/bin/ls"), &none),
]);
let (_, task, _) = choose(&roles, "/usr/bin/ls").unwrap();
assert_eq!(task.as_deref(), Some("junior_task"));
let roles = serde_json::json!([
role_json(
"senior",
false,
None,
&parents(serde_json::json!(["junior"]))
),
role_json("junior", false, Some("/usr/bin/ls"), &none),
]);
assert!(matches!(
choose(&roles, "/usr/bin/ls"),
Err(SrError::PermissionDenied)
));
let roles = serde_json::json!([role_json(
"senior",
true,
None,
&parents(serde_json::json!(3))
)]);
assert!(matches!(
choose(&roles, "/usr/bin/ls"),
Err(SrError::ConfigurationError)
));
}
#[cfg(all(feature = "hierarchy", feature = "ssd"))]
#[test]
fn test_role_excluded_by_ssd_gives_no_task_of_its_parents() {
let none = serde_json::json!({});
let roles = serde_json::json!([
role_json(
"senior",
true,
None,
&serde_json::json!({"parents": ["junior"], "ssd": ["auditor"]})
),
role_json("junior", false, Some("/usr/bin/ls"), &none),
role_json("auditor", true, None, &none),
]);
assert!(matches!(
choose(&roles, "/usr/bin/ls"),
Err(SrError::PermissionDenied)
));
}
#[cfg(all(feature = "hierarchy", feature = "ssd"))]
#[test]
fn test_ssd_counts_the_roles_held_through_parents() {
let none = serde_json::json!({});
let roles = serde_json::json!([
role_json(
"senior",
true,
None,
&serde_json::json!({"parents": ["junior"]})
),
role_json("junior", false, Some("/usr/bin/ls"), &none),
role_json(
"auditor",
true,
Some("/usr/bin/cat"),
&serde_json::json!({"ssd": ["junior"]})
),
]);
assert!(matches!(
choose(&roles, "/usr/bin/cat"),
Err(SrError::PermissionDenied)
));
assert!(choose(&roles, "/usr/bin/ls").is_ok());
}
#[cfg(all(feature = "hierarchy", feature = "ssd"))]
#[test]
fn test_conflicting_parents_give_no_conflicting_task() {
let none = serde_json::json!({});
let ssd = |other: &str| serde_json::json!({ "ssd": [other] });
let senior = role_json(
"senior",
true,
None,
&serde_json::json!({"parents": ["a", "b"]}),
);
let roles = serde_json::json!([
senior,
role_json("a", false, Some("/usr/bin/ls"), &ssd("b")),
role_json("b", false, Some("/usr/bin/cat"), &none),
]);
assert!(matches!(
choose(&roles, "/usr/bin/ls"),
Err(SrError::PermissionDenied)
));
assert!(choose(&roles, "/usr/bin/cat").is_ok());
let roles = serde_json::json!([
senior,
role_json("a", false, Some("/usr/bin/ls"), &ssd("b")),
role_json("b", false, Some("/usr/bin/cat"), &ssd("a")),
]);
for cmd in ["/usr/bin/ls", "/usr/bin/cat"] {
assert!(
matches!(choose(&roles, cmd), Err(SrError::PermissionDenied)),
"{cmd}"
);
}
}
#[test]
fn test_role_settings_calls_actors_and_tasks() {
let mut best = BestExecSettings::default();
let cli = dummy_cli();
let cred = dummy_cred();
let binding = dummy_dconfigfinder();
let data = binding.roles().next().unwrap();
let mut opt_stack = BorrowedOptStack::new(None);
let env_path = &["/bin"];
let result = best.role_settings(&cli, &cred, &data, &mut opt_stack, env_path);
assert!(result.is_ok());
}
#[test]
fn test_actors_settings_returns_bool() {
let mut best = BestExecSettings::default();
let binding = dummy_dconfigfinder();
let data = binding.roles().next().unwrap();
let result = best.actors_settings(&data);
assert!(result.is_ok());
assert!(result.is_ok());
}
#[test]
fn test_task_settings_sets_fields_on_found() {
let mut best = BestExecSettings::default();
let cli = dummy_cli();
let cred = dummy_cred();
let binding = dummy_dconfigfinder();
let binding = binding.roles().next().unwrap();
let data = binding.tasks().next().unwrap();
let mut opt_stack = BorrowedOptStack::new(data.role().config().options.as_ref());
let env_path = &["/bin"];
let result = best.task_settings(&cli, &cred, &data, &mut opt_stack, env_path);
assert!(result.is_ok_and(|r| r));
assert!(*best.final_path == *"/usr/bin/ls");
assert!(best.role == "test");
assert!(best.task == Some("0".to_string()));
assert!(best.cred.caps.is_some());
assert!(best.score.cmd_min == CmdMin::MATCH);
}
#[cfg(feature = "regex")]
#[test]
fn test_command_settings_simple_and_complex() {
let cli = dummy_cli();
let env_path = &["/usr/bin"];
let binding = dummy_dconfigfinder();
let binding = binding.roles().next().unwrap();
let binding = binding.tasks().nth(1).unwrap();
let binding = binding.commands().unwrap();
let data = binding.add().next().unwrap();
let result = BestExecSettings::match_command(env_path, &cli, &data);
assert!(result.as_ref().is_ok_and(|m| {
m.as_ref()
.is_some_and(|(_, path)| path == Path::new("/usr/bin/ls"))
}));
let data = binding.add().nth(1).unwrap();
let result = BestExecSettings::match_command(env_path, &cli, &data);
assert!(
result.as_ref().is_ok_and(Option::is_none),
"Failed to process complex command : {}",
result.unwrap_err()
);
}
#[test]
fn test_info_denied_due_to_inherited_hide_one() {
let mut best = BestExecSettings::default();
let cli = Cli::builder()
.cmd_path("/usr/bin/ls".to_string())
.cmd_args(vec!["-l".to_string()])
.info()
.build();
let cred = dummy_cred();
let binding = dummy_dconfigfinder();
let binding = binding.roles().next().unwrap();
let binding = binding.tasks().next().unwrap();
let data = binding;
let mut opt_stack = BorrowedOptStack::new(data.role().config().options.as_ref());
let env_path = &["/usr/bin"];
let result = best.task_settings(&cli, &cred, &data, &mut opt_stack, env_path);
assert!(result.is_ok());
assert!(!result.unwrap());
}
#[test]
fn test_info_denied_due_to_inherited_hide_two() {
let mut best = BestExecSettings::default();
let cli = Cli::builder()
.cmd_path("/usr/bin/ls".to_string())
.cmd_args(vec!["-l".to_string()])
.info()
.build();
let cred = dummy_cred();
let binding = dummy_dconfigfinder();
let binding = binding.roles().next().unwrap();
let binding = binding.tasks().nth(1).unwrap();
let data = binding;
let mut opt_stack = BorrowedOptStack::new(data.role().config().options.as_ref());
let env_path = &["/usr/bin"];
let result = best.task_settings(&cli, &cred, &data, &mut opt_stack, env_path);
assert!(result.is_ok());
assert!(result.unwrap());
}
#[test]
fn test_info_denied_due_to_inherited_hide_three() {
let mut best = BestExecSettings::default();
let cli = Cli::builder()
.cmd_path("/usr/bin/ls".to_string())
.cmd_args(vec!["-l".to_string()])
.info()
.build();
let cred = dummy_cred();
let binding = dummy_dconfigfinder();
let binding = binding.roles().next().unwrap();
let data = binding.tasks().nth(1).unwrap();
let mut opt_stack = BorrowedOptStack::new(data.role().config().options.as_ref());
let env_path = &["/usr/bin"];
let result = best.role_settings(&cli, &cred, &binding, &mut opt_stack, env_path);
assert!(result.is_ok());
assert!(result.unwrap());
assert!(*best.final_path == *"/usr/bin/ls");
assert!(best.role == "test", "role was {}", best.role);
assert!(best.task == Some("1".to_string()));
}
#[test]
fn test_info_denied_due_to_inherited_hide_four() {
let mut best = BestExecSettings::default();
let cli = Cli::builder()
.cmd_path("/usr/bin/ls".to_string())
.cmd_args(vec!["-l".to_string()])
.info()
.build();
let cred = dummy_cred();
let binding = dummy_dconfigfinder();
let binding = binding.roles().nth(1).unwrap();
let data = binding.tasks().next().unwrap();
let mut opt_stack = BorrowedOptStack::new(data.role().config().options.as_ref());
let env_path = &["/usr/bin"];
let result = best.role_settings(&cli, &cred, &binding, &mut opt_stack, env_path);
assert!(result.is_ok());
assert!(result.unwrap());
assert!(*best.final_path == *"/usr/bin/ls");
assert!(best.role == "test2", "role was {}", best.role);
assert!(best.task == Some("0".to_string()));
}
#[test]
fn test_info_denied_due_to_inherited_env_override_one() {
let mut best = BestExecSettings::default();
let cli = Cli::builder()
.cmd_path("/usr/bin/ls".to_string())
.cmd_args(vec!["-l".to_string()])
.opt_filter(
FilterMatcher::builder()
.env_behavior(EnvBehavior::Keep)
.build(),
)
.build();
let cred = dummy_cred();
let binding = dummy_dconfigfinder();
let binding = binding.roles().next().unwrap();
let binding = binding.tasks().next().unwrap();
let data = binding;
let mut opt_stack = BorrowedOptStack::new(data.role().config().options.as_ref());
let env_path = &["/usr/bin"];
let result = best.task_settings(&cli, &cred, &data, &mut opt_stack, env_path);
assert!(result.is_ok());
assert!(!result.unwrap());
}
#[test]
fn test_info_denied_due_to_inherited_env_override_two() {
let mut best = BestExecSettings::default();
let cli = Cli::builder()
.cmd_path("/usr/bin/ls".to_string())
.cmd_args(vec!["-l".to_string()])
.opt_filter(
FilterMatcher::builder()
.env_behavior(EnvBehavior::Keep)
.build(),
)
.build();
let cred = dummy_cred();
let binding = dummy_dconfigfinder();
let binding = binding.roles().next().unwrap();
let binding = binding.tasks().nth(1).unwrap();
let data = binding;
let mut opt_stack = BorrowedOptStack::new(data.role().config().options.as_ref());
let env_path = &["/usr/bin"];
let result = best.task_settings(&cli, &cred, &data, &mut opt_stack, env_path);
assert!(result.is_ok());
assert!(result.unwrap());
}
#[test]
fn test_info_denied_due_to_inherited_env_override_three() {
let mut best = BestExecSettings::default();
let cli = Cli::builder()
.cmd_path("/usr/bin/ls".to_string())
.cmd_args(vec!["-l".to_string()])
.opt_filter(
FilterMatcher::builder()
.env_behavior(EnvBehavior::Keep)
.build(),
)
.build();
let cred = dummy_cred();
let binding = dummy_dconfigfinder();
let binding = binding.roles().next().unwrap();
let data = binding.tasks().nth(1).unwrap();
let mut opt_stack = BorrowedOptStack::new(data.role().config().options.as_ref());
let env_path = &["/usr/bin"];
let result = best.role_settings(&cli, &cred, &binding, &mut opt_stack, env_path);
assert!(result.is_ok());
assert!(result.unwrap());
assert!(*best.final_path == *"/usr/bin/ls");
assert!(best.role == "test", "role was {}", best.role);
assert!(best.task == Some("1".to_string()));
}
#[test]
fn test_info_denied_due_to_inherited_env_override_four() {
let mut best = BestExecSettings::default();
let cli = Cli::builder()
.cmd_path("/usr/bin/ls".to_string())
.cmd_args(vec!["-l".to_string()])
.opt_filter(
FilterMatcher::builder()
.env_behavior(EnvBehavior::Keep)
.build(),
)
.build();
let cred = dummy_cred();
let binding = dummy_dconfigfinder();
let binding = binding.roles().nth(1).unwrap();
let data = binding.tasks().next().unwrap();
let mut opt_stack = BorrowedOptStack::new(data.role().config().options.as_ref());
let env_path = &["/usr/bin"];
let result = best.role_settings(&cli, &cred, &binding, &mut opt_stack, env_path);
assert!(result.is_ok());
assert!(result.unwrap());
assert!(*best.final_path == *"/usr/bin/ls");
assert!(best.role == "test2", "role was {}", best.role);
assert!(best.task == Some("0".to_string()));
}
#[test]
fn test_resolve_any_command_ignores_current_directory() {
assert!(Path::new("Cargo.toml").is_file());
assert_eq!(resolve_any_command(Path::new("Cargo.toml"), &[]), None);
assert_eq!(
resolve_any_command(Path::new("Cargo.toml"), &[".", ""]),
None
);
let dir = std::env::current_dir().unwrap();
let dir = dir.to_str().unwrap();
assert_eq!(
resolve_any_command(Path::new("Cargo.toml"), &["/nonexistent", dir]),
Some(Path::new(dir).join("Cargo.toml"))
);
}
#[test]
fn test_resolve_any_command_keeps_the_given_name() {
let dir =
std::env::temp_dir().join(format!("rar_resolve_any_command_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let link = dir.join("pkill");
std::os::unix::fs::symlink("/usr/bin/pgrep", &link).unwrap();
assert_eq!(resolve_any_command(&link, &[]), Some(link.clone()));
assert_eq!(
resolve_any_command(Path::new("./Cargo.toml"), &[]),
Some(std::env::current_dir().unwrap().join("./Cargo.toml"))
);
std::fs::remove_dir_all(&dir).unwrap();
}
}