Skip to content

Commit bf3987a

Browse files
Johan-Liebert1cgwalters
authored andcommitted
composefs/selinux: Add tests
Tests added by Claude Code Assisted-by: Claude Code (Sonnet 4) Signed-off-by: Pragyan Poudyal <pragyanpoudyal41999@gmail.com>
1 parent e59e967 commit bf3987a

1 file changed

Lines changed: 222 additions & 43 deletions

File tree

crates/lib/src/bootc_composefs/selinux.rs

Lines changed: 222 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,57 @@ const SELINUX_CONFIG_PATH: &str = "etc/selinux/config";
1313
const SELINUX_TYPE: &str = "SELINUXTYPE=";
1414
const POLICY_FILE_PREFIX: &str = "policy.";
1515

16+
/// Find the highest versioned policy file in the given directory
17+
fn find_latest_policy_file(policy_dir: &Dir) -> Result<String> {
18+
let mut highest_policy_version = -1;
19+
let mut latest_policy_name = None;
20+
21+
for entry in policy_dir
22+
.entries_utf8()
23+
.context("Getting policy dir entries")?
24+
{
25+
let entry = entry?;
26+
27+
if !entry.file_type()?.is_file() {
28+
// We don't want symlinks, another directory etc
29+
continue;
30+
}
31+
32+
let filename = entry.file_name()?;
33+
34+
match filename.strip_prefix(POLICY_FILE_PREFIX) {
35+
Some(version) => {
36+
let v_int = version
37+
.parse::<i32>()
38+
.with_context(|| anyhow::anyhow!("Parsing {version} as int"))?;
39+
40+
if v_int < highest_policy_version {
41+
continue;
42+
}
43+
44+
highest_policy_version = v_int;
45+
latest_policy_name = Some(filename.to_string());
46+
}
47+
48+
None => continue,
49+
};
50+
}
51+
52+
latest_policy_name.ok_or_else(|| anyhow::anyhow!("Failed to get latest SELinux policy"))
53+
}
54+
55+
/// Compute SHA256 hash of a policy file
56+
fn compute_policy_file_hash(deployment_root: &Dir, full_path: &str) -> Result<String> {
57+
let mut file = deployment_root
58+
.open(full_path)
59+
.context("Opening policy file")?;
60+
let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha256())?;
61+
std::io::copy(&mut file, &mut hasher)?;
62+
63+
let hash = hex::encode(hasher.finish().context("Computing hash")?);
64+
Ok(hash)
65+
}
66+
1667
#[context("Getting SELinux policy for deployment {depl_id}")]
1768
fn get_selinux_policy_for_deployment(
1869
storage: &Storage,
@@ -50,56 +101,15 @@ fn get_selinux_policy_for_deployment(
50101

51102
let policy_dir_path = format!("etc/selinux/{type_}/policy");
52103

53-
let mut highest_policy_version = -1;
54-
let mut latest_policy_name = None;
55-
56104
let policy_dir = deployment_root
57105
.open_dir(&policy_dir_path)
58106
.context("Opening selinux policy dir")?;
59107

60-
for entry in policy_dir
61-
.entries_utf8()
62-
.context("Getting policy dir entries")?
63-
{
64-
let entry = entry?;
65-
66-
if !entry.file_type()?.is_file() {
67-
// We don't want symlinks, another directory etc
68-
continue;
69-
}
70-
71-
let filename = entry.file_name()?;
72-
73-
match filename.strip_prefix(POLICY_FILE_PREFIX) {
74-
Some(version) => {
75-
let v_int = version
76-
.parse::<i32>()
77-
.with_context(|| anyhow::anyhow!("Parsing {version} as int"))?;
78-
79-
if v_int < highest_policy_version {
80-
continue;
81-
}
82-
83-
highest_policy_version = v_int;
84-
latest_policy_name = Some(filename.to_string());
85-
}
86-
87-
None => continue,
88-
};
89-
}
90-
91-
let policy_name =
92-
latest_policy_name.ok_or_else(|| anyhow::anyhow!("Failed to get latest SELinux policy"))?;
108+
let policy_name = find_latest_policy_file(&policy_dir)?;
93109

94110
let full_path = format!("{policy_dir_path}/{policy_name}");
95111

96-
let mut file = deployment_root
97-
.open(full_path)
98-
.context("Opening policy file")?;
99-
let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha256())?;
100-
std::io::copy(&mut file, &mut hasher)?;
101-
102-
let hash = hex::encode(hasher.finish().context("Computing hash")?);
112+
let hash = compute_policy_file_hash(&deployment_root, &full_path)?;
103113

104114
Ok(Some(hash))
105115
}
@@ -134,3 +144,172 @@ pub(crate) fn are_selinux_policies_compatible(
134144

135145
Ok(sl_policy_match)
136146
}
147+
148+
#[cfg(test)]
149+
mod tests {
150+
use super::*;
151+
use cap_std_ext::cap_std::ambient_authority;
152+
use cap_std_ext::dirext::CapStdExtDirExt;
153+
154+
#[test]
155+
fn test_find_latest_policy_file() -> Result<()> {
156+
let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority())?;
157+
158+
// Create policy files with different versions
159+
tempdir.atomic_write("policy.30", "policy content 30")?;
160+
tempdir.atomic_write("policy.31", "policy content 31")?;
161+
tempdir.atomic_write("policy.29", "policy content 29")?;
162+
tempdir.atomic_write("not_policy.32", "not a policy file")?;
163+
tempdir.atomic_write("other_policy.txt", "invalid policy file")?;
164+
165+
let result = find_latest_policy_file(&tempdir)?;
166+
assert_eq!(result, "policy.31");
167+
168+
Ok(())
169+
}
170+
171+
#[test]
172+
fn test_find_latest_policy_file_with_single_file() -> Result<()> {
173+
let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority())?;
174+
175+
tempdir.atomic_write("policy.25", "single policy file")?;
176+
177+
let result = find_latest_policy_file(&tempdir)?;
178+
assert_eq!(result, "policy.25");
179+
180+
Ok(())
181+
}
182+
183+
#[test]
184+
fn test_find_latest_policy_file_no_policy_files() {
185+
let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority()).unwrap();
186+
187+
tempdir
188+
.atomic_write("not_policy.txt", "not a policy file")
189+
.unwrap();
190+
tempdir.atomic_write("other.txt", "invalid format").unwrap();
191+
192+
let result = find_latest_policy_file(&tempdir);
193+
assert!(result.is_err());
194+
assert!(
195+
result
196+
.unwrap_err()
197+
.to_string()
198+
.contains("Failed to get latest SELinux policy")
199+
);
200+
}
201+
202+
#[test]
203+
fn test_find_latest_policy_file_invalid_version() {
204+
let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority()).unwrap();
205+
206+
tempdir
207+
.atomic_write("policy.abc", "invalid version")
208+
.unwrap();
209+
210+
let result = find_latest_policy_file(&tempdir);
211+
assert!(result.is_err());
212+
assert!(
213+
result
214+
.unwrap_err()
215+
.to_string()
216+
.contains("Parsing abc as int")
217+
);
218+
}
219+
220+
#[test]
221+
fn test_find_latest_policy_file_negative_version() -> Result<()> {
222+
let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority())?;
223+
224+
tempdir.atomic_write("policy.5", "positive version")?;
225+
tempdir.atomic_write("policy.-1", "negative version")?;
226+
227+
let result = find_latest_policy_file(&tempdir)?;
228+
assert_eq!(result, "policy.5");
229+
230+
Ok(())
231+
}
232+
233+
#[test]
234+
fn test_find_latest_policy_file_skips_directories() -> Result<()> {
235+
let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority())?;
236+
237+
tempdir.create_dir("policy.99")?; // This should be skipped
238+
tempdir.atomic_write("policy.5", "actual policy file")?;
239+
240+
let result = find_latest_policy_file(&tempdir)?;
241+
assert_eq!(result, "policy.5");
242+
243+
Ok(())
244+
}
245+
246+
#[test]
247+
fn test_compute_policy_file_hash() -> Result<()> {
248+
let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority())?;
249+
250+
let test_content = "test policy content for hashing";
251+
tempdir.atomic_write("test_policy.30", test_content)?;
252+
253+
let hash = compute_policy_file_hash(&tempdir, "test_policy.30")?;
254+
255+
// Verify the hash is a valid SHA256 hash (64 hex characters)
256+
assert_eq!(hash.len(), 64);
257+
assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
258+
259+
// Verify consistent hashing
260+
let hash2 = compute_policy_file_hash(&tempdir, "test_policy.30")?;
261+
assert_eq!(hash, hash2);
262+
263+
Ok(())
264+
}
265+
266+
#[test]
267+
fn test_compute_policy_file_hash_different_content() -> Result<()> {
268+
let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority())?;
269+
270+
tempdir.atomic_write("policy1.30", "content 1")?;
271+
tempdir.atomic_write("policy2.30", "content 2")?;
272+
273+
let hash1 = compute_policy_file_hash(&tempdir, "policy1.30")?;
274+
let hash2 = compute_policy_file_hash(&tempdir, "policy2.30")?;
275+
276+
assert_ne!(hash1, hash2);
277+
278+
Ok(())
279+
}
280+
281+
#[test]
282+
fn test_compute_policy_file_hash_nonexistent_file() {
283+
let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority()).unwrap();
284+
285+
let result = compute_policy_file_hash(&tempdir, "nonexistent.30");
286+
assert!(result.is_err());
287+
assert!(
288+
result
289+
.unwrap_err()
290+
.to_string()
291+
.contains("Opening policy file")
292+
);
293+
}
294+
295+
#[test]
296+
fn test_compute_policy_file_hash_empty_file() -> Result<()> {
297+
let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority())?;
298+
299+
tempdir.atomic_write("empty_policy.30", "")?;
300+
301+
let hash = compute_policy_file_hash(&tempdir, "empty_policy.30")?;
302+
303+
// Should produce a valid hash even for empty file
304+
assert_eq!(hash.len(), 64);
305+
assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
306+
307+
// SHA256 of empty string
308+
assert_eq!(
309+
hash,
310+
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
311+
);
312+
313+
Ok(())
314+
}
315+
}

0 commit comments

Comments
 (0)