Skip to main content

atomcode_core/auth/
mod.rs

1// `Write::write_all` and `PathBuf` only appear inside `#[cfg(unix)]`
2// blocks below (the atomic-rename + chmod-600 path uses them; the
3// Windows fallback at line 49 just calls `std::fs::write`). Gate the
4// imports so a Windows build doesn't fire unused_imports.
5#[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}