Skip to content

Commit 313c26c

Browse files
authored
Merge pull request #83 from LeChatP/dev
v3.2.2
2 parents 2b2b276 + 236a0a3 commit 313c26c

File tree

12 files changed

+141
-106
lines changed

12 files changed

+141
-106
lines changed

.github/workflows/pkg.yml

Lines changed: 0 additions & 41 deletions
This file was deleted.

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ members = ["xtask", "rar-common"]
44
[package]
55
name = "rootasrole"
66
# The project version is managed on json file in resources/rootasrole.json
7-
version = "3.2.1"
7+
version = "3.2.2"
88
rust-version = "1.83.0"
99
authors = ["Eddie Billoir <[email protected]>"]
1010
edition = "2021"
@@ -71,7 +71,7 @@ toml = "0.8"
7171
chrono = { version = "0.4", features = ["unstable-locales"] }
7272

7373
[dependencies]
74-
rar-common = { path = "rar-common", version = "3.2.1", package = "rootasrole-core" }
74+
rar-common = { path = "rar-common", version = "3.2.2", package = "rootasrole-core" }
7575
log = "0.4"
7676
libc = "0.2"
7777
strum = { version = "0.26", features = ["derive"] }

README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,18 @@
55
</p>
66
<p align="center">
77

8-
<img alt="Build Status" src="https://img.shields.io/github/actions/workflow/status/LeChatP/RootAsRole/build.yml?label=Build"/>
9-
<img alt="Test Status" src="https://img.shields.io/github/actions/workflow/status/LeChatP/RootAsRole/tests.yml?label=Unit%20Tests">
10-
<a href="https://codecov.io/gh/LeChatP/RootAsRole" ><img src="https://codecov.io/gh/LeChatP/RootAsRole/branch/main/graph/badge.svg?token=6J7CRGEIG8"/></a>
11-
<img alt="GitHub" src="https://img.shields.io/github/license/LeChatP/RootAsRole">
8+
<img alt="crates.io" src="https://img.shields.io/crates/v/rootasrole.svg?style=for-the-badge&label=Version&color=e37602&logo=rust" height="25"/>
9+
<img alt="Build Status" src="https://img.shields.io/github/actions/workflow/status/LeChatP/RootAsRole/build.yml?style=for-the-badge&logo=githubactions&label=Build&logoColor=white" height="25"/>
10+
<img alt="Tests Status" src="https://img.shields.io/github/actions/workflow/status/LeChatP/RootAsRole/tests.yml?style=for-the-badge&logo=githubactions&logoColor=white&label=Tests" height="25"/>
11+
<img alt="Codecov" src="https://img.shields.io/codecov/c/github/lechatp/rootasrole?style=for-the-badge&logo=codecov&color=green&link=https%3A%2F%2Fapp.codecov.io%2Fgh%2FLeChatP%2FRootAsRole" height="25">
12+
<img alt="GitHub" src="https://img.shields.io/github/license/LeChatP/RootAsRole?style=for-the-badge&logo=github&logoColor=white" height="25"/>
13+
1214

1315
</p>
1416
<!-- The project version is managed on json file in resources/rootasrole.json -->
1517
<!-- markdownlint-restore -->
1618

17-
# RootAsRole (V3.2.1) — A better alternative to `sudo(-rs)`/`su` • ⚡ Blazing fast • 🛡️ Memory-safe • 🔐 Security-oriented
19+
# RootAsRole — A better alternative to `sudo(-rs)`/`su` • ⚡ Blazing fast • 🛡️ Memory-safe • 🔐 Security-oriented
1820

1921
RootAsRole is a Linux/Unix privilege delegation tool based on **Role-Based Access Control (RBAC)**. It empowers administrators to assign precise privileges — not full root — to users and commands.
2022

build.rs

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -35,28 +35,6 @@ fn set_cargo_version(package_version: &str, file: &str) -> Result<(), Box<dyn Er
3535
Ok(())
3636
}
3737

38-
fn set_readme_version(package_version: &str, file: &str) -> Result<(), Box<dyn Error>> {
39-
let readme = File::open(std::path::Path::new(file)).expect("README.md not found");
40-
let reader = BufReader::new(readme);
41-
let lines = reader.lines().map(|l| l.unwrap()).collect::<Vec<String>>();
42-
let mut readme = File::create(std::path::Path::new(file)).expect("README.md not found");
43-
for line in lines {
44-
if line.starts_with("# RootAsRole (V") {
45-
let mut s = line.split("(V").next().unwrap().to_string();
46-
let end = line
47-
.split(')')
48-
.skip(1)
49-
.fold(String::new(), |acc, x| acc + ")" + x);
50-
s.push_str(&format!("(V{}{}", package_version, end));
51-
writeln!(readme, "{}", s)?;
52-
} else {
53-
writeln!(readme, "{}", line)?;
54-
}
55-
}
56-
readme.sync_all()?;
57-
Ok(())
58-
}
59-
6038
fn some_kind_of_uppercase_first_letter(s: &str) -> String {
6139
let mut c = s.chars();
6240
match c.next() {
@@ -135,10 +113,6 @@ fn main() {
135113
eprintln!("cargo:warning={}", err);
136114
}
137115

138-
if let Err(err) = set_readme_version(&package_version, "README.md") {
139-
eprintln!("cargo:warning={}", err);
140-
}
141-
142116
if let Err(err) = set_man_version(&package_version, "resources/man/en_US.md", Locale::en_US) {
143117
eprintln!("cargo:warning={}", err);
144118
}

rar-common/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rootasrole-core"
3-
version = "3.2.1"
3+
version = "3.2.2"
44
edition = "2021"
55
description = "This core crate for the RootAsRole project."
66
license = "LGPL-3.0-or-later"

rar-common/src/lib.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,7 @@ impl LockedSettingsFile {
397397
with_mutable_config(self.fd.deref_mut(), |file| {
398398
debug!("Toggled immutable off for config file");
399399
file.rewind()?;
400+
file.set_len(0)?;
400401
write_json_config(&versionned, file)
401402
})?;
402403
} else {
@@ -613,6 +614,7 @@ mod tests {
613614

614615
use crate::database::actor::SActor;
615616
use crate::database::structs::{SCommand, SCommands, SCredentials, SRole, STask, SetBehavior};
617+
use crate::util::unlock_immutable;
616618

617619
use super::*;
618620

@@ -1353,4 +1355,86 @@ mod tests {
13531355
// File should exist now
13541356
assert!(PathBuf::from(test_file).exists());
13551357
}
1358+
1359+
#[test]
1360+
fn test_locked_settings_truncates_file_on_save() {
1361+
if has_privileges(&[Cap::LINUX_IMMUTABLE]).is_ok_and(|b| !b) {
1362+
println!("Test skipped due to insufficient privileges in test environment");
1363+
return;
1364+
}
1365+
let test_file = "/tmp/test_locked_settings_truncates_file_on_save.json";
1366+
let _cleanup = defer(|| {
1367+
let filename = PathBuf::from(test_file)
1368+
.canonicalize()
1369+
.unwrap_or(test_file.into());
1370+
if std::fs::remove_file(&filename).is_err() {
1371+
debug!("Failed to delete the file: {}", filename.display());
1372+
}
1373+
});
1374+
1375+
// Create a test file with some initial content
1376+
let initial_content = r#"{
1377+
"version": "0.1.0",
1378+
"storage": {
1379+
"method": "JSON"
1380+
},
1381+
"config": {
1382+
"roles": [
1383+
{
1384+
"name": "old_role",
1385+
"actors": [],
1386+
"tasks": []
1387+
},
1388+
{
1389+
"name": "another_old_role",
1390+
"actors": [],
1391+
"tasks": []
1392+
},
1393+
{
1394+
"name": "yet_another_old_role",
1395+
"actors": [],
1396+
"tasks": []
1397+
},
1398+
{
1399+
"name": "oldest_role",
1400+
"actors": [],
1401+
"tasks": []
1402+
}
1403+
]
1404+
}
1405+
}"#;
1406+
let mut file = File::create(test_file).unwrap();
1407+
with_mutable_config(&mut file, |file| {
1408+
file.write_all(initial_content.as_bytes()).unwrap();
1409+
Ok(())
1410+
})
1411+
.unwrap();
1412+
1413+
// Open the file using LockedSettingsFile
1414+
let mut locked = LockedSettingsFile::open(
1415+
test_file,
1416+
std::fs::OpenOptions::new()
1417+
.read(true)
1418+
.write(true)
1419+
.to_owned(),
1420+
true, // write mode
1421+
)
1422+
.unwrap();
1423+
1424+
// Modify the settings to have fewer roles
1425+
let new_config = SConfig::builder().build();
1426+
locked.data.borrow_mut().config = Some(new_config);
1427+
locked.save().unwrap();
1428+
// Read back the file content
1429+
let mut file = File::open(test_file).unwrap();
1430+
let mut content = String::new();
1431+
file.read_to_string(&mut content).unwrap();
1432+
// The content should match the new settings and not contain old roles
1433+
assert!(!content.contains("old_role"));
1434+
assert!(!content.contains("another_old_role"));
1435+
assert!(!content.contains("yet_another_old_role"));
1436+
assert!(!content.contains("oldest_role"));
1437+
unlock_immutable(&mut file).unwrap();
1438+
fs::remove_file(test_file).unwrap();
1439+
}
13561440
}

rar-common/src/util.rs

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,24 @@ pub fn with_mutable_config<F, R>(file: &mut File, f: F) -> std::io::Result<R>
9494
where
9595
F: FnOnce(&mut File) -> io::Result<R>,
9696
{
97+
let mut val = unlock_immutable(file)?;
98+
let res = f(file);
99+
val |= FS_IMMUTABLE_FL;
100+
lock_immutable(file, val)?;
101+
res
102+
}
103+
104+
pub fn lock_immutable(file: &mut File, mut val: u32) -> Result<(), io::Error> {
105+
immutable_required_privileges(file, || {
106+
if unsafe { nix::libc::ioctl(file.as_raw_fd(), FS_IOC_SETFLAGS, &mut val) } < 0 {
107+
return Err(std::io::Error::last_os_error());
108+
}
109+
Ok(())
110+
})?;
111+
Ok(())
112+
}
113+
114+
pub fn unlock_immutable(file: &mut File) -> Result<u32, io::Error> {
97115
let mut val = 0;
98116
if unsafe { nix::libc::ioctl(file.as_raw_fd(), FS_IOC_GETFLAGS, &mut val) } < 0 {
99117
return Err(std::io::Error::last_os_error());
@@ -109,15 +127,7 @@ where
109127
} else {
110128
warn!("Config file was not immutable.");
111129
}
112-
let res = f(file);
113-
val |= FS_IMMUTABLE_FL;
114-
immutable_required_privileges(file, || {
115-
if unsafe { nix::libc::ioctl(file.as_raw_fd(), FS_IOC_SETFLAGS, &mut val) } < 0 {
116-
return Err(std::io::Error::last_os_error());
117-
}
118-
Ok(())
119-
})?;
120-
res
130+
Ok(val)
121131
}
122132

123133
pub fn warn_if_mutable(file: &File, return_err: bool) -> std::io::Result<()> {
@@ -336,7 +346,10 @@ pub fn open_lock_with_privileges<P: AsRef<Path>>(
336346
if e.kind() != std::io::ErrorKind::PermissionDenied {
337347
return Err(e);
338348
}
339-
debug!("Permission denied while opening file, retrying with privileges",);
349+
debug!(
350+
"Permission denied while opening {} file, retrying with privileges",
351+
p.as_ref().display()
352+
);
340353
with_privileges(&[Cap::DAC_READ_SEARCH], || options.open(&p)).or_else(|e| {
341354
if e.kind() != std::io::ErrorKind::PermissionDenied {
342355
return Err(e);
@@ -353,7 +366,10 @@ pub fn read_with_privileges<P: AsRef<Path>>(p: P) -> std::io::Result<File> {
353366
if e.kind() != std::io::ErrorKind::PermissionDenied {
354367
return Err(e);
355368
}
356-
debug!("Permission denied while opening file, retrying with privileges",);
369+
debug!(
370+
"Permission denied while opening {} file, retrying with privileges",
371+
p.as_ref().display()
372+
);
357373
with_privileges(&[Cap::DAC_READ_SEARCH], || std::fs::File::open(&p)).or_else(|e| {
358374
if e.kind() != std::io::ErrorKind::PermissionDenied {
359375
return Err(e);
@@ -368,7 +384,10 @@ pub fn remove_with_privileges<P: AsRef<Path>>(p: P) -> std::io::Result<()> {
368384
if e.kind() != std::io::ErrorKind::PermissionDenied {
369385
return Err(e);
370386
}
371-
debug!("Permission denied while removing file, retrying with privileges",);
387+
debug!(
388+
"Permission denied while removing {} file, retrying with privileges",
389+
p.as_ref().display()
390+
);
372391
with_privileges(&[Cap::DAC_OVERRIDE], || std::fs::remove_file(&p))
373392
})
374393
}
@@ -378,7 +397,10 @@ pub fn create_dir_all_with_privileges<P: AsRef<Path>>(p: P) -> std::io::Result<(
378397
if e.kind() != std::io::ErrorKind::PermissionDenied {
379398
return Err(e);
380399
}
381-
debug!("Permission denied while creating directory, retrying with privileges",);
400+
debug!(
401+
"Permission denied while creating {} directory, retrying with privileges",
402+
p.as_ref().display()
403+
);
382404
with_privileges(&[Cap::DAC_OVERRIDE], || std::fs::create_dir_all(p))
383405
})
384406
}

resources/man/en_US.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
% RootAsRole(8) RootAsRole 3.2.1 | System Manager's Manual
1+
% RootAsRole(8) RootAsRole 3.2.2 | System Manager's Manual
22
% Eddie Billoir <[email protected]>
33
% August 2025
44

resources/man/fr_FR.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
% RootAsRole(8) RootAsRole 3.2.1 | Manuel de l'administrateur système
1+
% RootAsRole(8) RootAsRole 3.2.2 | Manuel de l'administrateur système
22
% Eddie Billoir <[email protected]>
33
% Août 2025
44

src/sr/pam/mod.rs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -267,23 +267,21 @@ mod tests {
267267

268268
#[test]
269269
fn test_check_auth_required_but_valid_timeout() {
270+
if env!("RAR_PAM_SERVICE") == "dosr" {
271+
println!("Skipping test_check_auth_required_but_valid_timeout because RAR_PAM_SERVICE is set to original dosr");
272+
return;
273+
}
270274
let authentication = SAuthentication::Perform;
271275
let timeout = create_test_timeout();
272276
let user = create_test_user();
273277

274-
// This test depends on the timeout::is_valid implementation
275-
// In a real environment, you might want to mock this
276-
let result = check_auth(&authentication, &timeout, &user, "Password: ");
277-
// Result will depend on whether there's a valid timeout cookie
278-
// We're just testing that it doesn't panic
279-
assert!(result.is_ok() || result.is_err());
278+
let _ = check_auth(&authentication, &timeout, &user, "Password: ");
280279
}
281280

282281
#[test]
283282
fn test_conversation_handler_no_interact_flag() {
284283
let handler = SrConversationHandler::builder().no_interact().build();
285284

286-
// When no_interact is true, both prompt methods should return ConversationError
287285
let prompt_result = handler.prompt(OsStr::new("Test prompt"));
288286
assert!(matches!(prompt_result, Err(ErrorCode::ConversationError)));
289287

@@ -299,10 +297,8 @@ mod tests {
299297
let custom_prompt = "Enter your secret: ";
300298
let handler = SrConversationHandler::new(custom_prompt);
301299

302-
// Test that the handler stores the custom prompt
303300
assert_eq!(handler.prompt, custom_prompt);
304301

305-
// Test that it recognizes standard PAM prompts
306302
assert!(handler.is_pam_password_prompt(&"Password:"));
307303
assert!(handler.is_pam_password_prompt(&"Password: "));
308304
}

0 commit comments

Comments
 (0)