Skip to content

Commit

Permalink
Implement the rootpw option (#947)
Browse files Browse the repository at this point in the history
  • Loading branch information
squell authored Jan 16, 2025
2 parents c1871ba + 8d61ef8 commit 44229de
Show file tree
Hide file tree
Showing 10 changed files with 158 additions and 5 deletions.
17 changes: 16 additions & 1 deletion src/common/context.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -61,9 +64,14 @@ impl Context {
pub fn build_from_options(
sudo_options: OptionsForContext,
path: String,
auth_user: AuthenticatingUser,
) -> Result<Context, Error> {
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, &current_user)?;
let (launch, shell) = resolve_launch_and_shell(&sudo_options, &current_user, &target_user);
Expand All @@ -81,6 +89,7 @@ impl Context {
hostname,
command,
current_user,
auth_user,
target_user,
target_group,
use_session_records: !sudo_options.reset_timestamp,
Expand All @@ -99,6 +108,7 @@ impl Context {
mod tests {
use crate::{
sudo::SudoAction,
sudoers::AuthenticatingUser,
system::{interface::UserId, Hostname},
};
use std::collections::HashMap;
Expand All @@ -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());
Expand Down
24 changes: 24 additions & 0 deletions src/common/resolve.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::system::interface::UserId;
use crate::system::{Group, User};
use core::fmt;
use std::{
Expand Down Expand Up @@ -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<Self, Error> {
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<PathBuf>;

pub(super) fn resolve_launch_and_shell(
Expand Down
1 change: 1 addition & 0 deletions src/defaults/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ defaults! {
visiblepw = false #ignored
pwfeedback = false
env_editor = true
rootpw = false

passwd_tries = 3 [0..=1000]

Expand Down
5 changes: 4 additions & 1 deletion src/sudo/env/tests.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand Down Expand Up @@ -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()),
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion src/sudo/pam.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ impl PamAuthenticator<CLIConverser> {
context.stdin,
context.non_interactive,
context.password_feedback,
&context.current_user.name,
&context.auth_user.name,
&context.current_user.name,
)
})
Expand Down
3 changes: 2 additions & 1 deletion src/sudo/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion src/sudoers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
16 changes: 16 additions & 0 deletions src/sudoers/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -99,6 +106,7 @@ impl Policy for Judgement {

pub trait PreJudgementPolicy {
fn secure_path(&self) -> Option<String>;
fn authenticate_as(&self) -> AuthenticatingUser;
fn validate_authorization(&self) -> Authorization;
}

Expand All @@ -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,
Expand Down
54 changes: 54 additions & 0 deletions test-framework/sudo-compliance-tests/src/sudo/misc.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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(())
}
37 changes: 37 additions & 0 deletions test-framework/sudo-test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ pub struct EnvBuilder {
groups: HashMap<Groupname, Group>,
hostname: Option<String>,
users: HashMap<Username, User>,
user_passwords: HashMap<String, String>,
}

impl EnvBuilder {
Expand Down Expand Up @@ -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<str>) -> &mut Self {
self.hostname = Some(hostname.as_ref().to_string());
Expand Down Expand Up @@ -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)?;
}
Expand Down

0 comments on commit 44229de

Please sign in to comment.