Skip to content

Commit 71a2eb7

Browse files
authored
Make the Context immutable (#1372)
2 parents 732347b + 1403b71 commit 71a2eb7

File tree

7 files changed

+57
-101
lines changed

7 files changed

+57
-101
lines changed

src/common/context.rs

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
use std::{env, io};
1+
use std::env;
22

3-
use crate::common::{HARDENED_ENUM_VALUE_0, HARDENED_ENUM_VALUE_1, HARDENED_ENUM_VALUE_2};
4-
use crate::exec::{RunOptions, Umask};
3+
use crate::common::{Error, HARDENED_ENUM_VALUE_0, HARDENED_ENUM_VALUE_1, HARDENED_ENUM_VALUE_2};
4+
use crate::exec::RunOptions;
55
use crate::sudo::{SudoEditOptions, SudoListOptions, SudoRunOptions, SudoValidateOptions};
66
use crate::sudoers::Sudoers;
7+
use crate::sudoers::{DirChange, Restrictions};
78
use crate::system::{audit::sudo_call, Group, Hostname, Process, User};
89

910
use super::{
1011
command::CommandAndArguments,
1112
resolve::{resolve_shell, resolve_target_user_and_group, CurrentUser},
12-
Error, SudoPath,
13+
SudoPath,
1314
};
1415

1516
#[derive(Debug)]
@@ -29,11 +30,6 @@ pub struct Context {
2930
pub hostname: Hostname,
3031
pub current_user: CurrentUser,
3132
pub process: Process,
32-
// policy
33-
pub use_pty: bool,
34-
pub noexec: bool,
35-
pub umask: Umask,
36-
pub noninteractive_auth: bool,
3733
// sudoedit
3834
pub files_to_edit: Vec<Option<SudoPath>>,
3935
}
@@ -101,10 +97,6 @@ impl Context {
10197
prompt,
10298
non_interactive: sudo_options.non_interactive,
10399
process: Process::new(),
104-
use_pty: true,
105-
noexec: false,
106-
umask: Umask::Preserve,
107-
noninteractive_auth: false,
108100
files_to_edit: vec![],
109101
})
110102
}
@@ -172,10 +164,6 @@ impl Context {
172164
prompt: sudo_options.prompt,
173165
non_interactive: sudo_options.non_interactive,
174166
process: Process::new(),
175-
use_pty: true,
176-
noexec: false,
177-
umask: Umask::Preserve,
178-
noninteractive_auth: false,
179167
files_to_edit,
180168
})
181169
}
@@ -199,10 +187,6 @@ impl Context {
199187
prompt: sudo_options.prompt,
200188
non_interactive: sudo_options.non_interactive,
201189
process: Process::new(),
202-
use_pty: true,
203-
noexec: false,
204-
umask: Umask::Preserve,
205-
noninteractive_auth: false,
206190
files_to_edit: vec![],
207191
})
208192
}
@@ -249,31 +233,50 @@ impl Context {
249233
prompt: sudo_options.prompt,
250234
non_interactive: sudo_options.non_interactive,
251235
process: Process::new(),
252-
use_pty: true,
253-
noexec: false,
254-
umask: Umask::Preserve,
255-
noninteractive_auth: false,
256236
files_to_edit: vec![],
257237
})
258238
}
259239

260-
pub(crate) fn try_as_run_options(&self) -> io::Result<RunOptions<'_>> {
240+
pub(crate) fn try_as_run_options(
241+
&self,
242+
controls: &Restrictions,
243+
) -> Result<RunOptions<'_>, Error> {
244+
// see if the chdir flag is permitted
245+
let mut chdir = match controls.chdir {
246+
DirChange::Any => self.chdir.clone(),
247+
DirChange::Strict(optdir) => {
248+
if let Some(chdir) = &self.chdir {
249+
return Err(Error::ChDirNotAllowed {
250+
chdir: chdir.clone(),
251+
command: self.command.command.clone(),
252+
});
253+
} else {
254+
optdir.cloned()
255+
}
256+
}
257+
};
258+
259+
// expand tildes in the path with the users home directory
260+
if let Some(dir) = chdir.take() {
261+
chdir = Some(dir.expand_tilde_in_path(&self.target_user.name)?);
262+
}
263+
261264
Ok(RunOptions {
262265
command: if self.command.resolved {
263266
&self.command.command
264267
} else {
265-
return Err(io::ErrorKind::NotFound.into());
268+
return Err(Error::CommandNotFound(self.command.command.clone()));
266269
},
267270
arguments: &self.command.arguments,
268271
arg0: self.command.arg0.as_deref(),
269-
chdir: self.chdir.as_deref(),
272+
chdir: chdir.as_deref().map(ToOwned::to_owned),
270273
is_login: self.launch == LaunchType::Login,
271274
user: &self.target_user,
272275
group: &self.target_group,
273-
umask: self.umask,
276+
umask: controls.umask,
274277

275-
use_pty: self.use_pty,
276-
noexec: self.noexec,
278+
use_pty: controls.use_pty,
279+
noexec: controls.noexec,
277280
})
278281
}
279282
}

src/exec/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use std::{
1212
io,
1313
os::unix::ffi::OsStrExt,
1414
os::unix::process::CommandExt,
15-
path::Path,
15+
path::{Path, PathBuf},
1616
process::Command,
1717
time::Duration,
1818
};
@@ -65,7 +65,7 @@ pub struct RunOptions<'a> {
6565
pub command: &'a Path,
6666
pub arguments: &'a [String],
6767
pub arg0: Option<&'a Path>,
68-
pub chdir: Option<&'a Path>,
68+
pub chdir: Option<PathBuf>,
6969
pub is_login: bool,
7070
pub user: &'a User,
7171
pub group: &'a Group,
@@ -120,6 +120,7 @@ pub fn run_command(
120120
// Decide if the pwd should be changed. `--chdir` takes precedence over `-i`.
121121
let path = options
122122
.chdir
123+
.as_ref()
123124
.map(|chdir| chdir.to_owned())
124125
.or_else(|| options.is_login.then(|| options.user.home.clone().into()))
125126
.clone();

src/sudo/env/environment.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,6 @@ mod tests {
281281
#[cfg(feature = "apparmor")]
282282
apparmor_profile: None,
283283
noexec: false,
284-
noninteractive_auth: false,
285284
}
286285
),
287286
expected,

src/sudo/env/tests.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use crate::common::resolve::CurrentUser;
22
use crate::common::{CommandAndArguments, Context};
3-
use crate::exec::Umask;
43
use crate::sudo::{
54
cli::{SudoAction, SudoRunOptions},
65
env::environment::{get_target_environment, Environment},
@@ -132,11 +131,7 @@ fn create_test_context(sudo_options: SudoRunOptions) -> Context {
132131
non_interactive: sudo_options.non_interactive,
133132
process: Process::new(),
134133
use_session_records: false,
135-
use_pty: true,
136-
noexec: false,
137134
bell: false,
138-
umask: Umask::Preserve,
139-
noninteractive_auth: false,
140135
files_to_edit: vec![],
141136
}
142137
}
@@ -175,7 +170,6 @@ fn test_environment_variable_filtering() {
175170
chdir: crate::sudoers::DirChange::Strict(None),
176171
trust_environment: false,
177172
umask: crate::exec::Umask::Preserve,
178-
noninteractive_auth: false,
179173
#[cfg(feature = "apparmor")]
180174
apparmor_profile: None,
181175
noexec: false,

src/sudo/pipeline.rs

Lines changed: 14 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@ use crate::log::{auth_info, auth_warn};
1111
use crate::pam::PamContext;
1212
use crate::sudo::env::environment;
1313
use crate::sudo::pam::{attempt_authenticate, init_pam, pre_exec, InitPamArgs};
14-
use crate::sudoers::{
15-
AuthenticatingUser, Authentication, Authorization, DirChange, Judgement, Restrictions, Sudoers,
16-
};
14+
use crate::sudoers::{AuthenticatingUser, Authentication, Authorization, Judgement, Sudoers};
1715
use crate::system::term::current_tty_name;
1816
use crate::system::timestamp::{RecordScope, SessionRecordFile, TouchResult};
1917
use crate::system::{escape_os_str_lossy, Process};
@@ -61,15 +59,14 @@ pub fn run(mut cmd_opts: SudoRunOptions) -> Result<(), Error> {
6159

6260
let user_requested_env_vars = std::mem::take(&mut cmd_opts.env_var_list);
6361

64-
let mut context = Context::from_run_opts(cmd_opts, &mut policy)?;
62+
let context = Context::from_run_opts(cmd_opts, &mut policy)?;
6563

6664
let policy = judge(policy, &context)?;
6765

6866
let Authorization::Allowed(auth, controls) = policy.authorization() else {
6967
return Err(Error::Authorization(context.current_user.name.to_string()));
7068
};
7169

72-
apply_policy_to_context(&mut context, &controls)?;
7370
let mut pam_context = auth_and_update_record_file(&context, auth)?;
7471

7572
// build environment
@@ -96,25 +93,19 @@ pub fn run(mut cmd_opts: SudoRunOptions) -> Result<(), Error> {
9693

9794
// prepare switch of apparmor profile
9895
#[cfg(feature = "apparmor")]
99-
if let Some(profile) = controls.apparmor_profile {
100-
crate::apparmor::set_profile_for_next_exec(&profile)
101-
.map_err(|err| Error::AppArmor(profile, err))?;
96+
if let Some(profile) = &controls.apparmor_profile {
97+
crate::apparmor::set_profile_for_next_exec(profile)
98+
.map_err(|err| Error::AppArmor(profile.clone(), err))?;
10299
}
103100

101+
let options = context.try_as_run_options(&controls)?;
102+
103+
// Log after try_as_run_options to avoid logging if the command is not resolved
104+
log_command_execution(&context);
105+
104106
// run command and return corresponding exit code
105-
let command_exit_reason = if context.command.resolved {
106-
log_command_execution(&context);
107-
108-
crate::exec::run_command(
109-
context
110-
.try_as_run_options()
111-
.map_err(|io_error| Error::Io(Some(context.command.command.clone()), io_error))?,
112-
target_env,
113-
)
114-
.map_err(|io_error| Error::Io(Some(context.command.command), io_error))
115-
} else {
116-
Err(Error::CommandNotFound(context.command.command))
117-
};
107+
let command_exit_reason = crate::exec::run_command(options, target_env)
108+
.map_err(|io_error| Error::Io(Some(context.command.command), io_error));
118109

119110
pam_context.close_session();
120111

@@ -154,6 +145,7 @@ fn auth_and_update_record_file(
154145
password_timeout,
155146
ref credential,
156147
pwfeedback,
148+
noninteractive_auth,
157149
}: Authentication,
158150
) -> Result<PamContext, Error> {
159151
let auth_user = match credential {
@@ -190,7 +182,7 @@ fn auth_and_update_record_file(
190182
hostname: &context.hostname,
191183
})?;
192184
if auth_status.must_authenticate {
193-
if context.non_interactive && !context.noninteractive_auth {
185+
if context.non_interactive && !noninteractive_auth {
194186
return Err(Error::InteractionRequired);
195187
}
196188

@@ -213,40 +205,6 @@ fn auth_and_update_record_file(
213205
Ok(pam_context)
214206
}
215207

216-
fn apply_policy_to_context(
217-
context: &mut Context,
218-
controls: &Restrictions,
219-
) -> Result<(), crate::common::Error> {
220-
// see if the chdir flag is permitted
221-
match controls.chdir {
222-
DirChange::Any => {}
223-
DirChange::Strict(optdir) => {
224-
if let Some(chdir) = &context.chdir {
225-
return Err(Error::ChDirNotAllowed {
226-
chdir: chdir.clone(),
227-
command: context.command.command.clone(),
228-
});
229-
} else {
230-
context.chdir = optdir.cloned();
231-
}
232-
}
233-
}
234-
235-
// expand tildes in the path with the users home directory
236-
if let Some(dir) = context.chdir.take() {
237-
context.chdir = Some(dir.expand_tilde_in_path(&context.target_user.name)?)
238-
}
239-
240-
// in case the user could set these from the commandline, something more fancy
241-
// could be needed, but here we copy these -- perhaps we should split up the Context type
242-
context.use_pty = controls.use_pty;
243-
context.noexec = controls.noexec;
244-
context.umask = controls.umask;
245-
context.noninteractive_auth = controls.noninteractive_auth;
246-
247-
Ok(())
248-
}
249-
250208
/// This should determine what the authentication status for the given record
251209
/// match limit and origin/target user from the context is.
252210
fn determine_auth_status(

src/sudo/pipeline/edit.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,14 @@ use crate::system::audit;
1010
pub fn run_edit(edit_opts: SudoEditOptions) -> Result<(), Error> {
1111
let policy = super::read_sudoers()?;
1212

13-
let mut context = Context::from_edit_opts(edit_opts)?;
13+
let context = Context::from_edit_opts(edit_opts)?;
1414

1515
let policy = super::judge(policy, &context)?;
1616

17-
let Authorization::Allowed(auth, controls) = policy.authorization() else {
17+
let Authorization::Allowed(auth, _controls) = policy.authorization() else {
1818
return Err(Error::Authorization(context.current_user.name.to_string()));
1919
};
2020

21-
super::apply_policy_to_context(&mut context, &controls)?;
2221
let mut pam_context = super::auth_and_update_record_file(&context, auth)?;
2322

2423
let pid = context.process.pid;

src/sudoers/policy.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ pub struct Authentication {
3232
pub prior_validity: Duration,
3333
pub pwfeedback: bool,
3434
pub password_timeout: Option<Duration>,
35+
pub noninteractive_auth: bool,
3536
}
3637

3738
impl super::Settings {
@@ -45,6 +46,7 @@ impl super::Settings {
4546
0 => None,
4647
timeout => Some(Duration::from_secs(timeout)),
4748
},
49+
noninteractive_auth: self.noninteractive_auth(),
4850
credential: if self.rootpw() {
4951
AuthenticatingUser::Root
5052
} else if self.targetpw() {
@@ -67,7 +69,6 @@ pub struct Restrictions<'a> {
6769
pub chdir: DirChange<'a>,
6870
pub path: Option<&'a str>,
6971
pub umask: Umask,
70-
pub noninteractive_auth: bool,
7172
#[cfg(feature = "apparmor")]
7273
pub apparmor_profile: Option<String>,
7374
}
@@ -132,7 +133,6 @@ impl Judgement {
132133
Umask::Extend(mask)
133134
}
134135
},
135-
noninteractive_auth: self.settings.noninteractive_auth(),
136136
#[cfg(feature = "apparmor")]
137137
apparmor_profile: tag
138138
.apparmor_profile
@@ -195,6 +195,7 @@ mod test {
195195
prior_validity: Duration::from_secs(15 * 60),
196196
credential: AuthenticatingUser::InvokingUser,
197197
pwfeedback: false,
198+
noninteractive_auth: false,
198199
password_timeout: Some(Duration::from_secs(300)),
199200
},
200201
);
@@ -212,6 +213,7 @@ mod test {
212213
prior_validity: Duration::from_secs(15 * 60),
213214
credential: AuthenticatingUser::InvokingUser,
214215
pwfeedback: false,
216+
noninteractive_auth: false,
215217
password_timeout: Some(Duration::from_secs(300)),
216218
},
217219
);

0 commit comments

Comments
 (0)