diff --git a/src/common/context.rs b/src/common/context.rs index 0bf5ab71c..a0d5d8cc9 100644 --- a/src/common/context.rs +++ b/src/common/context.rs @@ -1,4 +1,6 @@ +use crate::common::resolve::AuthUser; use crate::common::{HARDENED_ENUM_VALUE_0, HARDENED_ENUM_VALUE_1, HARDENED_ENUM_VALUE_2}; +use crate::sudoers::AuthenticatingUser; use crate::system::{Group, Hostname, Process, User}; use super::resolve::CurrentUser; @@ -43,6 +45,7 @@ pub struct Context { // system pub hostname: Hostname, pub current_user: CurrentUser, + pub auth_user: AuthUser, pub process: Process, // policy pub use_pty: bool, @@ -61,9 +64,14 @@ impl Context { pub fn build_from_options( sudo_options: OptionsForContext, path: String, + auth_user: AuthenticatingUser, ) -> Result { let hostname = Hostname::resolve(); let current_user = CurrentUser::resolve()?; + let auth_user = match auth_user { + AuthenticatingUser::InvokingUser => AuthUser::from_current_user(current_user.clone()), + AuthenticatingUser::Root => AuthUser::resolve_root_for_rootpw()?, + }; let (target_user, target_group) = resolve_target_user_and_group(&sudo_options.user, &sudo_options.group, ¤t_user)?; let (launch, shell) = resolve_launch_and_shell(&sudo_options, ¤t_user, &target_user); @@ -81,6 +89,7 @@ impl Context { hostname, command, current_user, + auth_user, target_user, target_group, use_session_records: !sudo_options.reset_timestamp, @@ -99,6 +108,7 @@ impl Context { mod tests { use crate::{ sudo::SudoAction, + sudoers::AuthenticatingUser, system::{interface::UserId, Hostname}, }; use std::collections::HashMap; @@ -114,7 +124,12 @@ mod tests { .unwrap(); let path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; let (ctx_opts, _pipe_opts) = options.into(); - let context = Context::build_from_options(ctx_opts, path.to_string()).unwrap(); + let context = Context::build_from_options( + ctx_opts, + path.to_string(), + AuthenticatingUser::InvokingUser, + ) + .unwrap(); let mut target_environment = HashMap::new(); target_environment.insert("SUDO_USER".to_string(), context.current_user.name.clone()); diff --git a/src/common/resolve.rs b/src/common/resolve.rs index 6ff651c4f..cfde086f1 100644 --- a/src/common/resolve.rs +++ b/src/common/resolve.rs @@ -1,3 +1,4 @@ +use crate::system::interface::UserId; use crate::system::{Group, User}; use core::fmt; use std::{ @@ -71,6 +72,29 @@ impl CurrentUser { } } +#[derive(Clone, Debug)] +pub struct AuthUser(User); + +impl AuthUser { + pub fn from_current_user(user: CurrentUser) -> Self { + Self(user.inner) + } + + pub fn resolve_root_for_rootpw() -> Result { + Ok(Self( + User::from_uid(UserId::ROOT)?.ok_or(Error::UserNotFound("root".to_string()))?, + )) + } +} + +impl ops::Deref for AuthUser { + type Target = User; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + type Shell = Option; pub(super) fn resolve_launch_and_shell( diff --git a/src/defaults/mod.rs b/src/defaults/mod.rs index 3bbe63150..103201c7e 100644 --- a/src/defaults/mod.rs +++ b/src/defaults/mod.rs @@ -31,6 +31,7 @@ defaults! { visiblepw = false #ignored pwfeedback = false env_editor = true + rootpw = false passwd_tries = 3 [0..=1000] diff --git a/src/sudo/env/tests.rs b/src/sudo/env/tests.rs index 3d5265d74..4362ca24b 100644 --- a/src/sudo/env/tests.rs +++ b/src/sudo/env/tests.rs @@ -1,4 +1,4 @@ -use crate::common::resolve::CurrentUser; +use crate::common::resolve::{AuthUser, CurrentUser}; use crate::common::{CommandAndArguments, Context}; use crate::sudo::{ cli::{SudoAction, SudoRunOptions}, @@ -94,6 +94,8 @@ fn create_test_context(sudo_options: &SudoRunOptions) -> Context { groups: vec![], }); + let auth_user = AuthUser::from_current_user(current_user.clone()); + let current_group = Group { gid: GroupId::new(1000), name: Some("test".to_string()), @@ -119,6 +121,7 @@ fn create_test_context(sudo_options: &SudoRunOptions) -> Context { hostname: Hostname::fake("test-ubuntu"), command, current_user: current_user.clone(), + auth_user, target_user: if sudo_options.user.as_deref() == Some("test") { current_user.into() } else { diff --git a/src/sudo/pam.rs b/src/sudo/pam.rs index 4632f325a..03da62628 100644 --- a/src/sudo/pam.rs +++ b/src/sudo/pam.rs @@ -35,7 +35,7 @@ impl PamAuthenticator { context.stdin, context.non_interactive, context.password_feedback, - &context.current_user.name, + &context.auth_user.name, &context.current_user.name, ) }) diff --git a/src/sudo/pipeline.rs b/src/sudo/pipeline.rs index 216b9d234..582f09fc7 100644 --- a/src/sudo/pipeline.rs +++ b/src/sudo/pipeline.rs @@ -212,7 +212,8 @@ fn build_context( let secure_path: String = pre .secure_path() .unwrap_or_else(|| std::env::var("PATH").unwrap_or_default()); - Context::build_from_options(cmd_opts, secure_path) + let auth_user = pre.authenticate_as(); + Context::build_from_options(cmd_opts, secure_path, auth_user) } /// This should determine what the authentication status for the given record diff --git a/src/sudoers/mod.rs b/src/sudoers/mod.rs index 4f28a70e5..77efc23b8 100644 --- a/src/sudoers/mod.rs +++ b/src/sudoers/mod.rs @@ -61,7 +61,9 @@ pub struct Judgement { mod policy; -pub use policy::{Authorization, AuthorizationAllowed, DirChange, Policy, PreJudgementPolicy}; +pub use policy::{ + AuthenticatingUser, Authorization, AuthorizationAllowed, DirChange, Policy, PreJudgementPolicy, +}; pub use self::entry::Entry; diff --git a/src/sudoers/policy.rs b/src/sudoers/policy.rs index 84604697d..ad68226e6 100644 --- a/src/sudoers/policy.rs +++ b/src/sudoers/policy.rs @@ -52,6 +52,13 @@ pub enum DirChange<'a> { Any = HARDENED_ENUM_VALUE_1, } +#[cfg_attr(test, derive(Debug, PartialEq))] +#[repr(u32)] +pub enum AuthenticatingUser { + InvokingUser = HARDENED_ENUM_VALUE_0, + Root = HARDENED_ENUM_VALUE_1, +} + impl Policy for Judgement { fn authorization(&self) -> Authorization { if let Some(tag) = &self.flags { @@ -99,6 +106,7 @@ impl Policy for Judgement { pub trait PreJudgementPolicy { fn secure_path(&self) -> Option; + fn authenticate_as(&self) -> AuthenticatingUser; fn validate_authorization(&self) -> Authorization; } @@ -107,6 +115,14 @@ impl PreJudgementPolicy for Sudoers { self.settings.secure_path().as_ref().map(|s| s.to_string()) } + fn authenticate_as(&self) -> AuthenticatingUser { + if self.settings.rootpw() { + AuthenticatingUser::Root + } else { + AuthenticatingUser::InvokingUser + } + } + fn validate_authorization(&self) -> Authorization { Authorization::Allowed(AuthorizationAllowed { must_authenticate: true, diff --git a/test-framework/sudo-compliance-tests/src/sudo/misc.rs b/test-framework/sudo-compliance-tests/src/sudo/misc.rs index 133f6ef1f..135e04a24 100644 --- a/test-framework/sudo-compliance-tests/src/sudo/misc.rs +++ b/test-framework/sudo-compliance-tests/src/sudo/misc.rs @@ -1,3 +1,4 @@ +use sudo_test::User; use sudo_test::{helpers::assert_ls_output, Command, Env, BIN_SUDO}; use crate::{Result, PANIC_EXIT_CODE, SUDOERS_ALL_ALL_NOPASSWD, USERNAME}; @@ -259,3 +260,56 @@ fn missing_primary_group() -> Result<()> { .output(&env)? .assert_success() } + +#[test] +fn rootpw_option_works() -> Result<()> { + const PASSWORD: &str = "passw0rd"; + const ROOT_PASSWORD: &str = "r00t"; + + let env = Env(format!( + "Defaults rootpw\nDefaults passwd_tries=1\n{USERNAME} ALL=(ALL:ALL) ALL" + )) + .user_password("root", ROOT_PASSWORD) + .user(User(USERNAME).password(PASSWORD)) + .build()?; + + // User password is not accepted when rootpw is enabled + let output = Command::new("sh") + .arg("-c") + .arg(format!("echo {PASSWORD} | sudo -S true")) + .as_user(USERNAME) + .output(&env)?; + assert!(!output.status().success()); + + // Root password is accepted when rootpw is enabled + let output = Command::new("sh") + .arg("-c") + .arg(format!("echo {ROOT_PASSWORD} | sudo -S true")) + .as_user(USERNAME) + .output(&env)?; + assert!(output.status().success()); + + Ok(()) +} + +#[test] +fn rootpw_option_doesnt_affect_authorization() -> Result<()> { + const PASSWORD: &str = "passw0rd"; + const ROOT_PASSWORD: &str = "r00t"; + + let env = Env("Defaults rootpw\nroot ALL=(ALL:ALL) ALL") + .user_password("root", ROOT_PASSWORD) + .user(User(USERNAME).password(PASSWORD)) + .build()?; + + // Even though we accept the root password when rootpw is enabled, we still check that the + // actual invoking user is authorized to run the command. + let output = Command::new("sh") + .arg("-c") + .arg(format!("echo {ROOT_PASSWORD} | sudo -S true")) + .as_user(USERNAME) + .output(&env)?; + assert!(!output.status().success()); + + Ok(()) +} diff --git a/test-framework/sudo-test/src/lib.rs b/test-framework/sudo-test/src/lib.rs index 2b41e0c23..5b0b3ca19 100644 --- a/test-framework/sudo-test/src/lib.rs +++ b/test-framework/sudo-test/src/lib.rs @@ -119,6 +119,7 @@ pub struct EnvBuilder { groups: HashMap, hostname: Option, users: HashMap, + user_passwords: HashMap, } impl EnvBuilder { @@ -194,6 +195,24 @@ impl EnvBuilder { self } + /// Sets the password for the specified `user` to the test environment + pub fn user_password(&mut self, username: &str, password: &str) -> &mut Self { + assert!( + !self.user_passwords.contains_key(username), + "password for user {} has already been declared", + username + ); + assert!( + !self.users.contains_key(username), + "password for user {} should be set as part of the .user() call", + username + ); + self.user_passwords + .insert(username.to_string(), password.to_string()); + + self + } + /// Sets the hostname of the container to the specified string pub fn hostname(&mut self, hostname: impl AsRef) -> &mut Self { self.hostname = Some(hostname.as_ref().to_string()); @@ -272,6 +291,24 @@ impl EnvBuilder { usernames.insert(user.name.to_string()); } + for (username, password) in &self.user_passwords { + if cfg!(target_os = "freebsd") { + container + .output( + Command::new("pw") + .args(["usermod", "-n", username, "-h", "0"]) + .stdin(password), + )? + .assert_success()?; + } else if cfg!(target_os = "linux") { + container + .output(Command::new("chpasswd").stdin(format!("{username}:{password}")))? + .assert_success()?; + } else { + todo!(); + } + } + for directory in self.directories.values() { directory.create(&container)?; }