use std::{collections::HashMap, error::Error, str::FromStr};
use capctl::Cap;
use indexmap::IndexSet;
use jiff::SignedDuration;
use log::{debug, warn};
use pest::iterators::Pair;
use crate::cli::data::{Convertion, RoleType, TaskType};
use rootasrole_core::{
database::{
actor::{SActor, SGroupType},
options::{EnvBehavior, OptType, PathBehavior, TimestampType, WorkdirBehavior},
structs::{IdTask, SetBehavior},
},
util::StorageMethod,
};
use super::data::{InputAction, Inputs, Rule, SetListType, TimeoutOpt};
type MatchingFunction = dyn Fn(&Pair<Rule>, &mut Inputs) -> Result<(), Box<dyn Error>>;
fn recurse_pair_with_action(
pair: Pair<Rule>,
inputs: &mut Inputs,
do_matching: &MatchingFunction,
) -> Result<(), Box<dyn Error>> {
for inner_pair in pair.into_inner() {
do_matching(&inner_pair, inputs)?;
recurse_pair(inner_pair, inputs)?;
}
Ok(())
}
pub fn recurse_pair(pair: Pair<Rule>, inputs: &mut Inputs) -> Result<(), Box<dyn Error>> {
recurse_pair_with_action(pair, inputs, &match_pair)
}
#[allow(clippy::too_many_lines)]
fn match_pair(pair: &Pair<Rule>, inputs: &mut Inputs) -> Result<(), Box<dyn Error>> {
match pair.as_rule() {
Rule::help => {
inputs.action = InputAction::Help;
}
Rule::list => {
inputs.action = InputAction::List;
}
Rule::set => {
inputs.action = InputAction::Set;
}
Rule::add | Rule::grant => {
inputs.action = InputAction::Add;
}
Rule::del | Rule::revoke => {
inputs.action = InputAction::Del;
}
Rule::purge => {
inputs.action = InputAction::Purge;
}
Rule::whitelist => {
inputs.setlist_type = Some(SetListType::White);
}
Rule::blacklist => {
inputs.setlist_type = Some(SetListType::Black);
}
Rule::checklist => {
inputs.setlist_type = Some(SetListType::Check);
}
Rule::setlist => {
inputs.setlist_type = Some(SetListType::Set);
}
Rule::convert => {
inputs.action = InputAction::Convert;
}
Rule::to => {
let mut inner = pair.clone().into_inner();
let temp_convertion = Convertion::default();
let convertion = inputs.convertion.get_or_insert(temp_convertion);
convertion.to_type = inner
.next()
.ok_or("No type specified for convertion")?
.as_str()
.to_lowercase()
.parse()
.inspect_err(|e| {
warn!(
"Unknown type {}, types available : {}",
e,
StorageMethod::VARIANTS.join(", ")
);
})?;
convertion.to = inner.next().ok_or("to_value not found")?.as_str().into();
}
Rule::from => {
let mut inner = pair.clone().into_inner();
let temp_convertion = Convertion::default();
let convertion = inputs.convertion.get_or_insert(temp_convertion);
convertion.from_type = Some(
inner
.next()
.expect("from_type not found")
.as_str()
.to_lowercase()
.parse()
.inspect_err(|e| {
warn!(
"Unknown type {}, types available : {}",
e,
StorageMethod::VARIANTS.join(", ")
);
})?,
);
convertion.from = Some(inner.next().expect("from_value not found").as_str().into());
}
Rule::convert_reconfigure => {
inputs.convert_reconfigure = true;
}
Rule::cmd_policy => {
inputs.action = InputAction::Set;
if pair.as_str() == "deny-all" {
inputs.cmd_policy = Some(SetBehavior::None);
} else if pair.as_str() == "allow-all" {
inputs.cmd_policy = Some(SetBehavior::All);
} else {
unreachable!("Unknown cmd policy: {}", pair.as_str())
}
}
Rule::caps_policy => {
inputs.action = InputAction::Set;
if pair.as_str() == "deny-all" {
inputs.cred_policy = Some(SetBehavior::None);
} else if pair.as_str() == "allow-all" {
inputs.cred_policy = Some(SetBehavior::All);
} else {
unreachable!("Unknown caps policy: {}", pair.as_str())
}
}
Rule::path_policy => {
inputs.action = InputAction::Set;
if pair.as_str() == "delete-all" {
inputs.options_path_policy = Some(PathBehavior::Delete);
} else if pair.as_str() == "keep-safe" {
inputs.options_path_policy = Some(PathBehavior::KeepSafe);
} else if pair.as_str() == "keep-unsafe" {
inputs.options_path_policy = Some(PathBehavior::KeepUnsafe);
} else if pair.as_str() == "inherit" {
inputs.options_path_policy = Some(PathBehavior::Inherit);
} else {
unreachable!("Unknown path policy: {}", pair.as_str())
}
}
Rule::env_policy => {
inputs.action = InputAction::Set;
if pair.as_str() == "delete-all" {
inputs.options_env_policy = Some(EnvBehavior::Delete);
} else if pair.as_str() == "keep-all" {
inputs.options_env_policy = Some(EnvBehavior::Keep);
} else if pair.as_str() == "inherit" {
inputs.options_env_policy = Some(EnvBehavior::Inherit);
} else {
unreachable!("Unknown env policy: {}", pair.as_str())
}
}
Rule::workdir_policy => {
inputs.action = InputAction::Set;
inputs.options_workdir_policy = Some(WorkdirBehavior::const_parse(pair.as_str()));
}
Rule::time => {
let mut reversed = pair.as_str().split(':').rev();
let mut duration: SignedDuration = SignedDuration::from_secs(
reversed
.next()
.ok_or("No seconds in time")?
.parse::<i64>()
.unwrap_or(0),
);
if let Some(mins) = reversed.next() {
duration = duration
.checked_add(
SignedDuration::try_from_mins(mins.parse::<i64>().unwrap_or(0))
.unwrap_or_default(),
)
.expect("Invalid minutes");
if let Some(hours) = reversed.next() {
duration = duration
.checked_add(
SignedDuration::try_from_hours(hours.parse::<i64>().unwrap_or(0))
.unwrap_or_default(),
)
.expect("Invalid hours");
}
}
inputs.timeout_duration = Some(duration);
}
Rule::opt_timeout_type => {
if pair.as_str() == "tty" {
inputs.timeout_type = Some(TimestampType::TTY);
} else if pair.as_str() == "ppid" {
inputs.timeout_type = Some(TimestampType::PPID);
} else if pair.as_str() == "uid" {
inputs.timeout_type = Some(TimestampType::UID);
} else {
unreachable!("Unknown timeout type: {}", pair.as_str())
}
}
Rule::opt_timeout_t_arg => {
let mut timeout_arg = inputs.timeout_arg.unwrap_or_default();
timeout_arg[TimeoutOpt::Type as usize] = true;
inputs.timeout_arg.replace(timeout_arg);
}
Rule::opt_timeout_d_arg => {
let mut timeout_arg = inputs.timeout_arg.unwrap_or_default();
timeout_arg[TimeoutOpt::Duration as usize] = true;
inputs.timeout_arg.replace(timeout_arg);
}
Rule::opt_timeout_m_arg => {
let mut timeout_arg = inputs.timeout_arg.unwrap_or_default();
timeout_arg[TimeoutOpt::MaxUsage as usize] = true;
inputs.timeout_arg.replace(timeout_arg);
}
Rule::opt_timeout_max_usage => {
inputs.timeout_max_usage = Some(pair.as_str().parse::<u64>()?);
}
Rule::file => {
inputs.policy = true;
}
Rule::policy_path => {
inputs.policy_path = Some(pair.as_str().to_string());
}
Rule::role_id => {
inputs.role_id = Some(pair.as_str().to_string());
}
Rule::role_type_arg => {
if pair.as_str() == "all" {
inputs.role_type = Some(RoleType::All);
} else if pair.as_str() == "actors" {
inputs.role_type = Some(RoleType::Actors);
} else if pair.as_str() == "tasks" {
inputs.role_type = Some(RoleType::Tasks);
} else {
unreachable!("Unknown role type: {}", pair.as_str())
}
}
Rule::user => {
for pair in pair.clone().into_inner() {
debug!("user {:?}", pair.as_str());
inputs
.actors
.get_or_insert_default()
.push(SActor::user(pair.as_str()).build());
}
}
Rule::group => {
fn inner_recurse(pair: &Pair<Rule>, vec: &mut Vec<String>) {
for pair in pair.clone().into_inner() {
if pair.as_rule() == Rule::actor_name {
vec.push(pair.as_str().into());
}
inner_recurse(&pair, vec);
}
}
let mut vec: Vec<String> = Vec::new();
for pair in pair.clone().into_inner() {
inner_recurse(&pair, &mut vec);
}
if vec.is_empty() {
warn!("No group specified");
} else if vec.len() == 1 {
inputs
.actors
.get_or_insert_default()
.push(SActor::group(vec[0].as_str()).build());
} else {
inputs.actors.get_or_insert_default().push(
SActor::group(
vec.iter()
.map(|s| SGroupType::from(s.as_str()))
.collect::<Vec<_>>(),
)
.build(),
);
}
debug!("actors: {:?}", inputs.actors);
}
Rule::task_id => {
inputs.task_id = Some(IdTask::Name(pair.as_str().to_string()));
}
Rule::task_type_arg => {
if pair.as_str() == "all" {
inputs.task_type = Some(TaskType::All);
} else if pair.as_str() == "commands" || pair.as_str() == "cmd" {
inputs.task_type = Some(TaskType::Commands);
} else if pair.as_str().starts_with("cred") {
inputs.task_type = Some(TaskType::Credentials);
} else {
unreachable!("Unknown task type: {}", pair.as_str());
}
}
Rule::cmd => {
inputs.cmd_id = Some(shell_words::split(pair.as_str())?);
}
Rule::capability => {
if let Ok(cap) = Cap::from_str(pair.as_str()) {
inputs.cred_caps.get_or_insert_default().add(cap);
} else {
warn!("Unknown capability: {}", pair.as_str());
}
}
Rule::cred_u => {
inputs.cred_setuid = Some(
pair.as_str()
.chars()
.skip(9)
.collect::<String>()
.as_str()
.into(),
);
}
Rule::cred_g => {
let mut vec: Vec<SGroupType> = Vec::new();
for pair in pair.clone().into_inner() {
if pair.as_rule() == Rule::actor_name {
vec.push(pair.as_str().into());
}
}
if vec.is_empty() {
warn!("No group specified");
}
inputs.cred_setgid = Some(vec.into());
}
Rule::options_operations => {
inputs.options = true;
}
Rule::opt_env => {
inputs.options_type = Some(OptType::Env);
}
Rule::opt_env_listing => {
inputs.options_key_env = Some(IndexSet::new());
}
Rule::opt_env_setlisting => {
inputs.setlist_type = Some(SetListType::Set);
inputs.options_env_values = Some(HashMap::new());
inputs.options_key_env = Some(IndexSet::new());
}
Rule::opt_env_keep => {
inputs.action = InputAction::Set;
inputs.options_env_policy = Some(EnvBehavior::Delete);
inputs.options_key_env = Some(IndexSet::new());
}
Rule::opt_env_delete => {
inputs.action = InputAction::Set;
inputs.options_env_policy = Some(EnvBehavior::Keep);
inputs.options_key_env = Some(IndexSet::new());
}
Rule::opt_env_set => {
inputs.action = InputAction::Set;
inputs.setlist_type = Some(SetListType::Set);
inputs.options_env_values = Some(HashMap::new());
}
Rule::opt_path => {
inputs.options_type = Some(OptType::Path);
}
Rule::opt_workdir => {
inputs.options_type = Some(OptType::Workdir);
}
Rule::opt_show_arg => {
if pair.as_str() == "all" {
inputs.options_type = None;
} else if pair.as_str() == "path" {
inputs.options_type = Some(OptType::Path);
} else if pair.as_str() == "env" {
inputs.options_type = Some(OptType::Env);
} else if pair.as_str() == "root" {
inputs.options_type = Some(OptType::Root);
} else if pair.as_str() == "bounding" {
inputs.options_type = Some(OptType::Bounding);
} else if pair.as_str() == "timeout" {
inputs.options_type = Some(OptType::Timeout);
} else if pair.as_str() == "authentication" {
inputs.options_type = Some(OptType::Authentication);
} else if pair.as_str() == "execinfo" {
inputs.options_type = Some(OptType::ExecInfo);
} else if pair.as_str() == "umask" {
inputs.options_type = Some(OptType::UMask);
} else {
unreachable!("Unknown option type: {}", pair.as_str())
}
}
Rule::path => {
inputs.options_path = Some(pair.as_str().to_string());
}
Rule::env_key_value => {
if let Some(options_env_values) = inputs.options_env_values.as_mut() {
let mut inner = pair.clone().into_inner();
let key = inner
.next()
.expect("env_key_value parsing must have a key, implementation error")
.as_str()
.to_string();
let value = inner
.next()
.expect("env_key_value parsing must have a value, implementation error")
.into_inner()
.next()
.expect("env_key_value parsing must have a value, implementation error")
.as_str()
.to_string();
debug!("env_key_value: {key}={value}");
options_env_values.insert(key, value);
}
}
Rule::env_key => {
if let Some(options_env) = inputs.options_key_env.as_mut() {
options_env.insert(pair.as_str().into());
}
}
Rule::opt_root_args => {
inputs.action = InputAction::Set; inputs.options_root = Some(pair.as_str().parse().unwrap_or_default());
}
Rule::opt_bounding_args => {
inputs.action = InputAction::Set; inputs.options_bounding = Some(pair.as_str().parse().unwrap_or_default());
}
Rule::opt_skip_auth_args => {
inputs.action = InputAction::Set; inputs.options_auth = Some(pair.as_str().parse().unwrap_or_default());
}
Rule::opt_execinfo_args => {
inputs.action = InputAction::Set; inputs.options_execinfo = Some(pair.as_str().parse().unwrap_or_default());
}
Rule::opt_mask_args => {
inputs.action = InputAction::Set; inputs.options_umask = Some(pair.as_str().parse().unwrap_or_default());
}
Rule::all => {
if inputs.role_id.is_some() && inputs.task_id.is_none() {
inputs.role_type = Some(RoleType::All);
} else if inputs.task_id.is_some() {
inputs.task_type = Some(TaskType::All);
}
}
Rule::editor => {
debug!("Editor mode enabled");
inputs.editor = true;
}
Rule::editor_type => {
let mut inner = pair.clone().into_inner();
inputs.editor_type = Some(
inner
.next()
.expect("from_type not found")
.as_str()
.to_lowercase()
.parse()
.inspect_err(|e| {
warn!(
"Unknown type {}, types available : {}",
e,
StorageMethod::VARIANTS.join(", ")
);
})?,
);
}
Rule::editor_path => {
inputs.editor_path = Some(pair.as_str().into());
}
_ => {
debug!("Unmatched rule: {:?}", pair.as_rule());
}
}
Ok(())
}
#[cfg(test)]
mod test {
use crate::{
cli::{
data::{Cli, RoleType},
pair::{InputAction, Rule, recurse_pair},
},
util::underline,
};
use pest::Parser;
use rootasrole_core::{
database::actor::SActor,
util::{BOLD, RED, RST},
};
use super::Inputs;
fn make_args(args: &str) -> String {
shell_words::join(shell_words::split(args).expect("Failed to split args"))
}
fn get_inputs(args: &str) -> Inputs {
let binding = make_args(args);
let args = Cli::parse(Rule::cli, &binding);
let args = match args {
Ok(v) => v,
Err(e) => {
println!(
"{RED}{BOLD}Unrecognized command line:\n| {RST}{}{RED}{BOLD}\n| {}\n= {}{RST}",
e.line(),
underline(&e),
e.variant.message(),
RED = RED,
BOLD = BOLD,
RST = RST
);
panic!("Error parsing args");
}
};
let mut inputs = Inputs::default();
for pair in args {
assert!(recurse_pair(pair, &mut inputs).is_ok());
}
inputs
}
#[test]
fn test_grant() {
let inputs = get_inputs("role r1 grant -u u1 -u u2 -g g1,g2");
assert_eq!(inputs.role_id, Some("r1".to_string()));
assert_eq!(inputs.action, InputAction::Add);
assert_eq!(
inputs.actors,
Some(vec![
SActor::user("u1").build(),
SActor::user("u2").build(),
SActor::group(["g1", "g2"]).build()
])
);
}
#[test]
fn test_list_roles() {
let inputs = get_inputs("list");
assert_eq!(inputs.action, InputAction::List);
}
#[test]
fn test_list_role() {
let inputs = get_inputs("role r1 show");
assert_eq!(inputs.action, InputAction::List);
assert_eq!(inputs.role_id, Some("r1".to_string()));
}
#[test]
fn test_list_role_actors() {
let inputs = get_inputs("r r1 l actors");
assert_eq!(inputs.action, InputAction::List);
assert_eq!(inputs.role_id, Some("r1".to_string()));
assert_eq!(inputs.role_type, Some(RoleType::Actors));
}
#[test]
fn test_list_role_tasks() {
let inputs = get_inputs("r r1 l tasks");
assert_eq!(inputs.action, InputAction::List);
assert_eq!(inputs.role_id, Some("r1".to_string()));
assert_eq!(inputs.role_type, Some(RoleType::Tasks));
}
#[test]
fn test_list_role_all() {
let inputs = get_inputs("r r1 l all");
assert_eq!(inputs.action, InputAction::List);
assert_eq!(inputs.role_id, Some("r1".to_string()));
assert_eq!(inputs.role_type, Some(RoleType::All));
}
}