Skip to content

Commit 4d0e2ba

Browse files
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 f4a933b commit 4d0e2ba

1 file changed

Lines changed: 200 additions & 43 deletions

File tree

crates/lib/src/bootc_composefs/selinux.rs

Lines changed: 200 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,150 @@ 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.atomic_write("not_policy.txt", "not a policy file").unwrap();
188+
tempdir.atomic_write("other.txt", "invalid format").unwrap();
189+
190+
let result = find_latest_policy_file(&tempdir);
191+
assert!(result.is_err());
192+
assert!(result.unwrap_err().to_string().contains("Failed to get latest SELinux policy"));
193+
}
194+
195+
#[test]
196+
fn test_find_latest_policy_file_invalid_version() {
197+
let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority()).unwrap();
198+
199+
tempdir.atomic_write("policy.abc", "invalid version").unwrap();
200+
201+
let result = find_latest_policy_file(&tempdir);
202+
assert!(result.is_err());
203+
assert!(result.unwrap_err().to_string().contains("Parsing abc as int"));
204+
}
205+
206+
#[test]
207+
fn test_find_latest_policy_file_negative_version() -> Result<()> {
208+
let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority())?;
209+
210+
tempdir.atomic_write("policy.5", "positive version")?;
211+
tempdir.atomic_write("policy.-1", "negative version")?;
212+
213+
let result = find_latest_policy_file(&tempdir)?;
214+
assert_eq!(result, "policy.5");
215+
216+
Ok(())
217+
}
218+
219+
#[test]
220+
fn test_find_latest_policy_file_skips_directories() -> Result<()> {
221+
let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority())?;
222+
223+
tempdir.create_dir("policy.99")?; // This should be skipped
224+
tempdir.atomic_write("policy.5", "actual policy file")?;
225+
226+
let result = find_latest_policy_file(&tempdir)?;
227+
assert_eq!(result, "policy.5");
228+
229+
Ok(())
230+
}
231+
232+
#[test]
233+
fn test_compute_policy_file_hash() -> Result<()> {
234+
let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority())?;
235+
236+
let test_content = "test policy content for hashing";
237+
tempdir.atomic_write("test_policy.30", test_content)?;
238+
239+
let hash = compute_policy_file_hash(&tempdir, "test_policy.30")?;
240+
241+
// Verify the hash is a valid SHA256 hash (64 hex characters)
242+
assert_eq!(hash.len(), 64);
243+
assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
244+
245+
// Verify consistent hashing
246+
let hash2 = compute_policy_file_hash(&tempdir, "test_policy.30")?;
247+
assert_eq!(hash, hash2);
248+
249+
Ok(())
250+
}
251+
252+
#[test]
253+
fn test_compute_policy_file_hash_different_content() -> Result<()> {
254+
let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority())?;
255+
256+
tempdir.atomic_write("policy1.30", "content 1")?;
257+
tempdir.atomic_write("policy2.30", "content 2")?;
258+
259+
let hash1 = compute_policy_file_hash(&tempdir, "policy1.30")?;
260+
let hash2 = compute_policy_file_hash(&tempdir, "policy2.30")?;
261+
262+
assert_ne!(hash1, hash2);
263+
264+
Ok(())
265+
}
266+
267+
#[test]
268+
fn test_compute_policy_file_hash_nonexistent_file() {
269+
let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority()).unwrap();
270+
271+
let result = compute_policy_file_hash(&tempdir, "nonexistent.30");
272+
assert!(result.is_err());
273+
assert!(result.unwrap_err().to_string().contains("Opening policy file"));
274+
}
275+
276+
#[test]
277+
fn test_compute_policy_file_hash_empty_file() -> Result<()> {
278+
let tempdir = cap_std_ext::cap_tempfile::tempdir(ambient_authority())?;
279+
280+
tempdir.atomic_write("empty_policy.30", "")?;
281+
282+
let hash = compute_policy_file_hash(&tempdir, "empty_policy.30")?;
283+
284+
// Should produce a valid hash even for empty file
285+
assert_eq!(hash.len(), 64);
286+
assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
287+
288+
// SHA256 of empty string
289+
assert_eq!(hash, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
290+
291+
Ok(())
292+
}
293+
}

0 commit comments

Comments
 (0)