atomcode_core/auth/
mod.rs1#[cfg(unix)]
6use std::io::Write;
7use std::path::Path;
8#[cfg(unix)]
9use std::path::PathBuf;
10
11use anyhow::{Context, Result};
12
13pub mod oauth;
14
15pub use oauth::*;
16
17pub fn write_auth_file_secure(path: &Path, content: &str) -> Result<()> {
18 if let Some(parent) = path.parent() {
19 ensure_private_dir(parent)?;
20 }
21
22 #[cfg(unix)]
23 {
24 use std::fs::OpenOptions;
25 use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
26
27 let tmp_path = temp_auth_path(path);
28 let mut file = OpenOptions::new()
29 .create_new(true)
30 .write(true)
31 .truncate(true)
32 .mode(0o600)
33 .open(&tmp_path)
34 .with_context(|| {
35 format!("Failed to create temp auth file at {}", tmp_path.display())
36 })?;
37
38 file.write_all(content.as_bytes())
39 .context("Failed to write auth content")?;
40 file.sync_all().context("Failed to sync auth file")?;
41 drop(file);
42
43 std::fs::rename(&tmp_path, path).with_context(|| {
44 format!(
45 "Failed to atomically replace auth file from {} to {}",
46 tmp_path.display(),
47 path.display()
48 )
49 })?;
50 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
51 .with_context(|| format!("Failed to chmod 600 {}", path.display()))?;
52 }
53
54 #[cfg(not(unix))]
55 {
56 std::fs::write(path, content)
57 .with_context(|| format!("Failed to write auth file at {}", path.display()))?;
58 }
59
60 Ok(())
61}
62
63#[cfg(unix)]
64fn ensure_private_dir(path: &Path) -> Result<()> {
65 use std::fs::DirBuilder;
66 use std::os::unix::fs::DirBuilderExt;
67 use std::os::unix::fs::PermissionsExt;
68
69 if path.is_dir() {
70 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))
71 .with_context(|| format!("Failed to chmod 700 {}", path.display()))?;
72 return Ok(());
73 }
74
75 if let Some(parent) = path.parent() {
76 if !parent.as_os_str().is_empty() && !parent.exists() {
77 std::fs::create_dir_all(parent).with_context(|| {
78 format!("Failed to create parent directory for {}", path.display())
79 })?;
80 }
81 }
82
83 let mut builder = DirBuilder::new();
84 builder.mode(0o700);
85 builder
86 .create(path)
87 .with_context(|| format!("Failed to create auth directory {}", path.display()))?;
88 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))
89 .with_context(|| format!("Failed to chmod 700 {}", path.display()))?;
90 Ok(())
91}
92
93#[cfg(not(unix))]
94fn ensure_private_dir(path: &Path) -> Result<()> {
95 std::fs::create_dir_all(path)
96 .with_context(|| format!("Failed to create auth directory {}", path.display()))?;
97 Ok(())
98}
99
100#[cfg(unix)]
101fn temp_auth_path(path: &Path) -> PathBuf {
102 let pid = std::process::id();
103 let nanos = std::time::SystemTime::now()
104 .duration_since(std::time::UNIX_EPOCH)
105 .unwrap_or_default()
106 .as_nanos();
107 let file_name = path
108 .file_name()
109 .and_then(|name| name.to_str())
110 .unwrap_or("auth.toml");
111 path.with_file_name(format!(".{}.{}.{}.tmp", file_name, pid, nanos))
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[cfg(unix)]
119 #[test]
120 fn write_auth_file_secure_sets_private_permissions() {
121 use std::os::unix::fs::PermissionsExt;
122
123 let tmp = tempfile::tempdir().unwrap();
124 let auth_path = tmp.path().join("nested").join("auth.toml");
125
126 write_auth_file_secure(&auth_path, "access_token = \"secret\"\n").unwrap();
127
128 let dir_mode = std::fs::metadata(auth_path.parent().unwrap())
129 .unwrap()
130 .permissions()
131 .mode()
132 & 0o777;
133 let file_mode = std::fs::metadata(&auth_path).unwrap().permissions().mode() & 0o777;
134
135 assert_eq!(dir_mode, 0o700);
136 assert_eq!(file_mode, 0o600);
137 }
138
139 #[cfg(unix)]
140 #[test]
141 fn write_auth_file_secure_tightens_existing_file_permissions() {
142 use std::fs::OpenOptions;
143 use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
144
145 let tmp = tempfile::tempdir().unwrap();
146 let auth_dir = tmp.path().join("auth-home");
147 ensure_private_dir(&auth_dir).unwrap();
148 let auth_path = auth_dir.join("auth.toml");
149
150 let mut file = OpenOptions::new()
151 .create_new(true)
152 .write(true)
153 .mode(0o644)
154 .open(&auth_path)
155 .unwrap();
156 file.write_all(b"old").unwrap();
157 drop(file);
158
159 write_auth_file_secure(&auth_path, "access_token = \"new\"\n").unwrap();
160
161 let file_mode = std::fs::metadata(&auth_path).unwrap().permissions().mode() & 0o777;
162 assert_eq!(file_mode, 0o600);
163 }
164}