Skip to content

Commit ec58eb7

Browse files
committed
feat: -K for deleting timestamps
fix: finally, it was a real bug
1 parent 92ca898 commit ec58eb7

File tree

8 files changed

+160
-46
lines changed

8 files changed

+160
-46
lines changed

.github/workflows/tests.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,19 @@ jobs:
2525
- name: Install sudo
2626
run: apt install sudo -y
2727

28+
- name: Configure PAM
29+
run: |
30+
sudo bash -c 'echo "#%PAM-1.0
31+
auth [success=1 default=ignore] pam_permit.so
32+
auth requisite pam_permit.so
33+
auth required pam_permit.so
34+
account [success=1 default=ignore] pam_permit.so
35+
account requisite pam_permit.so
36+
account required pam_permit.so
37+
session [success=1 default=ignore] pam_permit.so
38+
session requisite pam_permit.so
39+
session required pam_permit.so" | tee /etc/pam.d/dosr'
40+
2841
- name: Install Dependencies
2942
run: cargo xtask dependencies -dip sudo
3043

src/sr/finder/cmd.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,20 @@ fn match_path(
2727
return min;
2828
} else {
2929
debug!("match_path: user relative path");
30+
let mut curmin = CmdMin::empty();
3031
all_paths_from_env(env_path, user_path)
3132
.iter()
3233
.find_map(|cmd_path| {
3334
let min = match_single_path(cmd_path, role_path);
34-
if min.better(&previous_min) {
35+
if min.better(&previous_min) && min.better(&curmin) {
3536
*final_path = Some(cmd_path.clone());
37+
curmin = min;
3638
Some(min)
3739
} else {
3840
None
3941
}
4042
})
43+
.inspect(|m| debug!("match_path: found better match {:?} with {}", m, final_path.as_ref().unwrap().display()))
4144
.unwrap_or_default()
4245
}
4346
}

src/sr/main.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ struct Cli {
108108
#[builder(default, with = || true)]
109109
/// Use stdin for password prompt
110110
stdin: bool,
111+
112+
#[builder(default, with = || false)]
113+
/// Delete timestamp cookie after successful authentication
114+
del_ts: bool,
111115
}
112116

113117
impl Default for Cli {
@@ -169,6 +173,9 @@ where
169173
"-E" | "--preserve-env" => {
170174
env.replace(EnvBehavior::Keep);
171175
}
176+
"-K" | "--remove-timestamp" => {
177+
args.del_ts = true;
178+
}
172179
"-p" | "--prompt" => {
173180
args.prompt = Some(
174181
iter.next()
@@ -280,6 +287,12 @@ fn main_inner() -> SrResult<()> {
280287
return Ok(());
281288
}
282289
let user = make_cred();
290+
if args.del_ts {
291+
timeout::clear_cookies(&user).map_err(|e| {
292+
error!("Failed to clear timestamp cookies: {}", e);
293+
SrError::InsufficientPrivileges
294+
})?;
295+
}
283296
let execcfg = find_best_exec_settings(
284297
&args,
285298
&user,

src/sr/pam/mod.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,7 @@ mod rpassword;
2525
#[allow(dead_code, reason = "This file is part of sudo-rs.")]
2626
mod securemem;
2727

28-
#[cfg(not(test))]
2928
const PAM_SERVICE: &str = "dosr";
30-
#[cfg(test)]
31-
const PAM_SERVICE: &str = "dosr_test";
32-
3329
pub(crate) const PAM_PROMPT: &str = "Password: ";
3430

3531
struct SrConversationHandler {

src/sr/timeout.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,14 @@ pub(crate) fn update_cookie(
282282
Ok(())
283283
}
284284

285+
pub(crate) fn clear_cookies(user: &Cred) -> Result<(), Box<dyn Error>> {
286+
let path = Path::new(TS_LOCATION).join(user.user.uid.as_raw().to_string());
287+
if path.exists() {
288+
remove_with_privileges(&path)?;
289+
}
290+
Ok(())
291+
}
292+
285293
#[cfg(test)]
286294
mod test {
287295
use nix::unistd::{Pid, User};
@@ -307,6 +315,7 @@ mod test {
307315
tty: None,
308316
ppid: Pid::parent(),
309317
};
318+
clear_cookies(&cred).unwrap();
310319
let constraint = STimeout {
311320
type_field: Some(TimestampType::TTY),
312321
duration: Some(chrono::Duration::seconds(10)),

tests/fixtures/perform_auth.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"version": "3.2.0",
3+
"options": {
4+
"authentication": "perform"
5+
},
6+
"roles": [
7+
{
8+
"name": "r_auth",
9+
"actors": [
10+
{
11+
"type": "user",
12+
"name": "root"
13+
}
14+
],
15+
"tasks": [
16+
{
17+
"name": "t_auth",
18+
"commands": [
19+
"/usr/bin/true"
20+
],
21+
"options": {
22+
"env": {
23+
"set": {
24+
"TASK": "t_auth"
25+
}
26+
}
27+
}
28+
}
29+
],
30+
"options": {
31+
"env": {
32+
"set": {
33+
"ROLE": "r_auth"
34+
}
35+
}
36+
}
37+
}
38+
]
39+
}

tests/helpers/test_runner.rs

Lines changed: 39 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::process::{Command, ExitStatus, Stdio};
33
use std::io::Result as IoResult;
44

55
use bon::bon;
6+
use log::warn;
67

78
use crate::helpers::config_manager::ConfigManager;
89

@@ -58,25 +59,26 @@ impl TestRunner {
5859
for &user in user_list {
5960
let user_check = Command::new("id")
6061
.arg(user)
61-
.stdout(Stdio::null())
62-
.stderr(Stdio::null())
6362
.status();
6463
match user_check {
65-
Ok(_) => {}
66-
Err(e) => {
64+
Ok(e) => {
6765
//check if error is due to user not existing
68-
if e.kind() != std::io::ErrorKind::NotFound {
69-
eprintln!("Warning: Failed to check user '{}': {}", user, e);
70-
continue;
71-
}
72-
// User does not exist, attempt to create
73-
let create_status = Command::new("sudo")
74-
.args(&["useradd", "-m", user])
75-
.status();
76-
if let Err(e) = create_status {
77-
eprintln!("Warning: Failed to create user '{}': {}", user, e);
66+
if !e.success() {
67+
// User does not exist, attempt to create
68+
let create_status = Command::new("useradd")
69+
.args(&["-m", user])
70+
.status();
71+
if let Err(e) = create_status {
72+
println!("Warning: Failed to create user '{}': {}", user, e);
73+
}
74+
println!("Created user '{}' for testing purposes", user);
75+
added_users.push(user.to_string());
7876
}
79-
added_users.push(user.to_string());
77+
println!("User '{}' exists", user);
78+
79+
}
80+
Err(e) => {
81+
println!("Warning: Failed to check user '{}': {}", user, e);
8082
}
8183
}
8284
}
@@ -86,25 +88,23 @@ impl TestRunner {
8688
for &group in group_list {
8789
let group_check = Command::new("getent")
8890
.args(&["group", group])
89-
.stdout(Stdio::null())
90-
.stderr(Stdio::null())
9191
.status();
9292
match group_check {
93-
Ok(_) => {}
94-
Err(e) => {
95-
//check if error is due to group not existing
96-
if e.kind() != std::io::ErrorKind::NotFound {
97-
eprintln!("Warning: Failed to check group '{}': {}", group, e);
98-
continue;
99-
}
100-
// Group does not exist, attempt to create
101-
let create_status = Command::new("sudo")
102-
.args(&["groupadd", group])
103-
.status();
104-
if let Err(e) = create_status {
105-
eprintln!("Warning: Failed to create group '{}': {}", group, e);
93+
Ok(e) => {
94+
if !e.success() {
95+
// Group does not exist, attempt to create
96+
let create_status = Command::new("groupadd")
97+
.args(&[group])
98+
.status();
99+
if let Err(e) = create_status {
100+
println!("Warning: Failed to create group '{}': {}", group, e);
101+
}
102+
added_groups.push(group.to_string());
103+
println!("Created group '{}' for testing purposes", group);
106104
}
107-
added_groups.push(group.to_string());
105+
}
106+
Err(e) => {
107+
println!("Warning: Failed to check group '{}': {}", group, e);
108108
}
109109
}
110110
}
@@ -117,17 +117,19 @@ impl TestRunner {
117117
.stderr(Stdio::piped());
118118

119119
let output = command.output()?;
120+
println!("Output : {}", String::from_utf8(output.stdout.clone()).unwrap());
121+
println!("Error : {}", String::from_utf8(output.stderr.clone()).unwrap());
120122

121123
// Clean up any users or groups we added
122124
for user in added_users {
123-
let _ = Command::new("sudo")
124-
.args(&["userdel", "-r", &user])
125-
.status();
125+
let _ = Command::new("userdel")
126+
.args(&["-r", &user])
127+
.status()?;
126128
}
127129
for group in added_groups {
128-
let _ = Command::new("sudo")
129-
.args(&["groupdel", &group])
130-
.status();
130+
let _ = Command::new("groupdel")
131+
.args(&[&group])
132+
.status()?;
131133
}
132134

133135
Ok(CommandResult {

tests/integration_tests.rs

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,13 @@ mod tests {
187187
.groups(&["nobody"])
188188
.call()
189189
.expect("Failed to run dosr -u nobody id");
190-
190+
if !result.success {
191+
eprintln!("stderr: {}", result.stderr);
192+
println!("stdout: {}", result.stdout);
193+
}
191194
assert!(result.success, "Command failed: {}", result.stderr);
192-
assert!(result.stdout.contains("gid=65534(nobody)"));
195+
let re_gid = RegexBuilder::new().build(r"gid=\d+\(nobody\)").unwrap();
196+
assert!(re_gid.is_match(&result.stdout.as_bytes()).is_ok_and(|b| b));
193197
assert_eq!(result.exit_code, 0);
194198
}
195199

@@ -204,12 +208,47 @@ mod tests {
204208
.fixture_name("tests/fixtures/user_group.json")
205209
.env_vars(&[("LANG","en_US")])
206210
.call()
207-
.expect("Failed to run dosr -u nobody -g daemon,nobody id");
211+
.inspect_err(|e| eprintln!("Failed to run dosr -u nobody -g daemon,nobody id: {}",e)).unwrap();
212+
if !result.success {
213+
eprintln!("stderr: {}", result.stderr);
214+
println!("stdout: {}", result.stdout);
215+
}
208216
assert!(result.success, "Command failed: {}", result.stderr);
209217
let re_gid = RegexBuilder::new().build(r"gid=\d+\(daemon\)").unwrap();
210-
let re_groups = RegexBuilder::new().build(r"groups=\d+\(daemon\),65534\(nobody\)").unwrap();
218+
let re_groups = RegexBuilder::new().build(r"groups=\d+\(daemon\),\d+\(nobody\)").unwrap();
211219
assert!(re_gid.is_match(&result.stdout.as_bytes()).is_ok_and(|b| b), "stdout: {}", result.stdout);
212220
assert!(re_groups.is_match(&result.stdout.as_bytes()).is_ok_and(|b| b), "stdout: {}", result.stdout);
213221
assert_eq!(result.exit_code, 0);
214222
}
223+
224+
#[test]
225+
#[serial]
226+
fn test_dosr_auth() {
227+
// check that the /etc/pam.d/dosr_test file exists
228+
use std::fs;
229+
if !fs::metadata("/etc/pam.d/dosr_test").is_ok() {
230+
eprintln!("Skipping test_dosr_auth: /etc/pam.d/dosr_test not found");
231+
return;
232+
}
233+
let runner = get_test_runner().expect("Failed to setup test environment");
234+
let result = runner
235+
.run_dosr(&["/usr/bin/true"])
236+
.fixture_name("tests/fixtures/perform_auth.json")
237+
.call()
238+
.expect("Failed to run dosr with auth role");
239+
assert!(result.success, "Command unexpectedly failed: {}", result.stderr);
240+
assert_eq!(result.exit_code, 0);
241+
// assert that a timestamp cookie was created
242+
let path = std::path::Path::new("/var/run/sr/ts").join("0");
243+
assert!(path.exists(), "Timestamp cookie was not created");
244+
// run dosr -K to delete the timestamp cookie
245+
let result = runner
246+
.run_dosr(&["-K","/usr/bin/true"])
247+
.fixture_name("tests/fixtures/perform_auth.json")
248+
.call()
249+
.expect("Failed to run dosr with auth role");
250+
assert!(result.success, "Command unexpectedly failed: {}", result.stderr);
251+
assert_eq!(result.exit_code, 0);
252+
assert!(!path.exists(), "Timestamp cookie was not deleted");
253+
}
215254
}

0 commit comments

Comments
 (0)