use std::ffi::OsStr;
use std::time::Duration;
use super::cli::{SudoRunOptions, SudoValidateOptions};
use super::diagnostic;
use crate::common::resolve::{AuthUser, CurrentUser};
use crate::common::{Context, Error};
use crate::log::{auth_info, auth_warn};
use crate::pam::PamContext;
use crate::sudo::env::environment;
use crate::sudo::pam::{InitPamArgs, attempt_authenticate, init_pam, pre_exec};
use crate::sudoers::{AuthenticatingUser, Authentication, Authorization, Judgement, Sudoers};
use crate::system::term::current_tty_name;
use crate::system::timestamp::{RecordScope, SessionRecordFile, TouchResult};
use crate::system::{Process, escape_os_str_lossy};
mod list;
pub(super) use list::run_list;
mod edit;
pub(super) use edit::run_edit;
fn read_sudoers() -> Result<Sudoers, Error> {
let sudoers_path = &super::candidate_sudoers_file();
let (sudoers, syntax_errors) = Sudoers::open(sudoers_path).map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
Error::Configuration(xlat!(
"sudoers file not found: {path}\n\
\n\
The sudoers file is required for sudo-rs to function. Please ensure:\n\
- The file exists at the expected location\n\
- You have the necessary permissions to read it\n\
- If setting up sudo-rs for the first time, create a sudoers file with appropriate permissions\n\
\n\
For more information, see the sudo-rs documentation.",
path = sudoers_path.display()
))
} else {
Error::Configuration(xlat!("invalid configuration: {error}", error = e))
}
})?;
for crate::sudoers::Error {
source,
location,
message,
} in syntax_errors
{
let path = source.as_deref().unwrap_or(sudoers_path);
diagnostic::diagnostic!("{message}", path @ location);
}
Ok(sudoers)
}
fn judge(mut policy: Sudoers, context: &Context) -> Result<Judgement, Error> {
Ok(policy.check(
&*context.current_user,
&context.hostname,
crate::sudoers::Request {
user: &context.target_user,
group: &context.target_group,
command: &context.command.command,
arguments: &context.command.arguments,
},
))
}
pub fn run(mut cmd_opts: SudoRunOptions) -> Result<(), Error> {
let mut policy = read_sudoers()?;
let user_requested_env_vars = std::mem::take(&mut cmd_opts.env_var_list);
let context = Context::from_run_opts(cmd_opts, &mut policy)?;
let policy = judge(policy, &context)?;
let Authorization::Allowed(auth, controls) = policy.authorization() else {
return Err(Error::Authorization(context.current_user.name.to_string()));
};
let mut pam_context = auth_and_update_record_file(&context, auth)?;
let additional_env = pre_exec(&mut pam_context, &context.target_user.name)?;
let current_env = environment::system_environment();
let (checked_vars, trusted_vars) = if controls.trust_environment {
(vec![], user_requested_env_vars)
} else {
(user_requested_env_vars, vec![])
};
let mut target_env = environment::get_target_environment(
current_env,
additional_env,
checked_vars,
&context,
&controls,
)?;
environment::dangerous_extend(&mut target_env, trusted_vars);
#[cfg(feature = "apparmor")]
if let Some(profile) = &controls.apparmor_profile {
crate::apparmor::set_profile_for_next_exec(profile)
.map_err(|err| Error::AppArmor(profile.clone(), err))?;
}
let options = context.try_as_run_options(&controls)?;
log_command_execution(&context);
let command_exit_reason = crate::exec::run_command(options, target_env)
.map_err(|io_error| Error::Io(Some(context.command.command), io_error));
pam_context.close_session();
match command_exit_reason?.exit_process()? {}
}
pub fn run_validate(cmd_opts: SudoValidateOptions) -> Result<(), Error> {
let mut policy = read_sudoers()?;
let context = Context::from_validate_opts(cmd_opts)?;
match policy.check_validate_permission(&*context.current_user, &context.hostname) {
Authorization::Forbidden => {
return Err(Error::Authorization(context.current_user.name.to_string()));
}
Authorization::Allowed(auth, ()) => {
auth_and_update_record_file(&context, auth)?;
}
}
Ok(())
}
fn auth_and_update_record_file(
context: &Context,
Authentication {
must_authenticate,
prior_validity,
allowed_attempts,
password_timeout,
ref credential,
pwfeedback,
noninteractive_auth,
}: Authentication,
) -> Result<PamContext, Error> {
let auth_user = match credential {
AuthenticatingUser::InvokingUser => {
AuthUser::from_current_user(context.current_user.clone())
}
AuthenticatingUser::Root => AuthUser::resolve_root_for_rootpw()?,
AuthenticatingUser::TargetUser => {
AuthUser::from_user_for_targetpw(context.target_user.clone())
}
};
let scope = RecordScope::for_process(&Process::new());
let mut auth_status = determine_auth_status(
must_authenticate,
context.use_session_records,
scope,
&context.current_user,
&auth_user,
prior_validity,
);
let mut pam_context = init_pam(InitPamArgs {
launch: context.launch,
use_askpass: context.askpass,
use_stdin: context.stdin,
bell: context.bell,
non_interactive: context.non_interactive,
password_feedback: pwfeedback,
password_timeout,
auth_prompt: context.prompt.clone(),
auth_user: &auth_user.name,
requesting_user: &context.current_user.name,
target_user: &context.target_user.name,
hostname: &context.hostname,
})?;
if auth_status.must_authenticate {
if context.non_interactive && !noninteractive_auth {
return Err(Error::InteractionRequired);
}
attempt_authenticate(
&mut pam_context,
&auth_user.name,
context.non_interactive,
allowed_attempts,
)?;
if let (Some(record_file), Some(scope)) = (&mut auth_status.record_file, scope) {
match record_file.create(scope, &auth_user) {
Ok(_) => (),
Err(e) => {
auth_warn!("Could not update session record file with new record: {e}");
}
}
}
}
Ok(pam_context)
}
fn determine_auth_status(
must_policy_authenticate: bool,
use_session_records: bool,
record_for: Option<RecordScope>,
current_user: &CurrentUser,
auth_user: &AuthUser,
prior_validity: Duration,
) -> AuthStatus {
if !must_policy_authenticate {
AuthStatus::new(false, None)
} else if let (true, Some(record_for)) = (use_session_records, record_for) {
match SessionRecordFile::open_for_user(current_user, prior_validity) {
Ok(mut sr) => {
match sr.touch(record_for, auth_user) {
Ok(TouchResult::Updated { .. }) => AuthStatus::new(false, Some(sr)),
Ok(TouchResult::NotFound | TouchResult::Outdated { .. }) => {
AuthStatus::new(true, Some(sr))
}
Err(e) => {
auth_warn!("Unexpected error while reading session information: {e}");
AuthStatus::new(true, None)
}
}
}
Err(e) => {
auth_warn!("Could not use session information: {e}");
AuthStatus::new(true, None)
}
}
} else {
AuthStatus::new(true, None)
}
}
struct AuthStatus {
must_authenticate: bool,
record_file: Option<SessionRecordFile>,
}
impl AuthStatus {
fn new(must_authenticate: bool, record_file: Option<SessionRecordFile>) -> AuthStatus {
AuthStatus {
must_authenticate,
record_file,
}
}
}
fn log_command_execution(context: &Context) {
let tty_info = if let Ok(tty_name) = current_tty_name() {
format!("TTY={} ;", escape_os_str_lossy(&tty_name))
} else {
String::from("")
};
let pwd = escape_os_str_lossy(
std::env::current_dir()
.as_ref()
.map(|s| s.as_os_str())
.unwrap_or_else(|_| OsStr::new("unknown")),
);
let user = context.target_user.name.escape_debug().collect::<String>();
auth_info!(
"{} : {} PWD={} ; USER={} ; COMMAND={}",
&context.current_user.name,
tty_info,
pwd,
user,
&context.command
);
}