mkt_core/config/
secure_write.rs1use std::path::Path;
8
9use crate::error::Result;
10
11pub fn write_private(path: &Path, content: &str) -> Result<()> {
21 #[cfg(unix)]
22 {
23 use std::io::Write as _;
24 use std::os::unix::fs::OpenOptionsExt as _;
25 use std::os::unix::fs::PermissionsExt as _;
26
27 let mut file = std::fs::OpenOptions::new()
28 .write(true)
29 .create(true)
30 .truncate(true)
31 .mode(0o600)
32 .open(path)?;
33 file.write_all(content.as_bytes())?;
34 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
37 Ok(())
38 }
39 #[cfg(not(unix))]
40 {
41 std::fs::write(path, content)?;
42 Ok(())
43 }
44}
45
46#[must_use]
50pub fn permissions_warning(path: &Path) -> Option<String> {
51 #[cfg(unix)]
52 {
53 use std::os::unix::fs::PermissionsExt as _;
54 let mode = std::fs::metadata(path).ok()?.permissions().mode();
55 if mode & 0o077 != 0 {
56 return Some(format!(
57 "{} is accessible by other users (mode {:03o}); run: chmod 600 {}",
58 path.display(),
59 mode & 0o777,
60 path.display()
61 ));
62 }
63 None
64 }
65 #[cfg(not(unix))]
66 {
67 let _ = path;
68 None
69 }
70}
71
72#[cfg(all(test, unix))]
73mod tests {
74 #![allow(clippy::unwrap_used, clippy::expect_used)]
75
76 use std::os::unix::fs::PermissionsExt as _;
77
78 use super::*;
79
80 fn temp_path() -> std::path::PathBuf {
81 std::env::temp_dir().join(format!("mkt-secure-write-{}", uuid::Uuid::new_v4()))
82 }
83
84 #[test]
85 fn new_file_is_created_owner_only() {
86 let path = temp_path();
87 write_private(&path, "secret = true").unwrap();
88 let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
89 std::fs::remove_file(&path).unwrap();
90 assert_eq!(mode, 0o600, "new config files must be 0600, got {mode:03o}");
91 }
92
93 #[test]
94 fn existing_loose_file_is_tightened() {
95 let path = temp_path();
96 std::fs::write(&path, "old").unwrap();
97 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
98
99 write_private(&path, "new").unwrap();
100 let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
101 let content = std::fs::read_to_string(&path).unwrap();
102 std::fs::remove_file(&path).unwrap();
103 assert_eq!(mode, 0o600);
104 assert_eq!(content, "new");
105 }
106
107 #[test]
108 fn warning_fires_only_for_loose_permissions() {
109 let path = temp_path();
110 write_private(&path, "x").unwrap();
111 assert!(permissions_warning(&path).is_none());
112
113 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
114 let warning = permissions_warning(&path);
115 std::fs::remove_file(&path).unwrap();
116 let warning = warning.expect("0644 must warn");
117 assert!(warning.contains("chmod 600"), "{warning}");
118 assert!(warning.contains("644"), "{warning}");
119 }
120
121 #[test]
122 fn missing_file_does_not_warn() {
123 assert!(permissions_warning(Path::new("/nonexistent/mkt-test")).is_none());
124 }
125}