1use std::collections::BTreeMap;
4use std::fs;
5use std::io::Write;
6use std::path::{Path, PathBuf};
7
8use super::{AuthError, AuthStore};
9
10impl AuthStore {
11 pub fn load(path: &Path) -> Result<Self, AuthError> {
21 let content = match fs::read_to_string(path) {
22 Ok(content) => content,
23 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(Self::new()),
24 Err(err) => return Err(AuthError::Read(path.to_path_buf(), err)),
25 };
26 toml::from_str(&content).map_err(|err: toml::de::Error| {
27 AuthError::Parse(path.to_path_buf(), err.message().to_owned())
28 })
29 }
30
31 pub fn save(&self, path: &Path) -> Result<(), AuthError> {
36 if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
37 ensure_dir_private(parent)?;
38 }
39
40 let lock_path = lock_path(path);
41 let _lock = crate::file_lock::exclusive(&lock_path)
42 .map_err(|err| AuthError::Lock(lock_path.clone(), err))?;
43
44 let mut pending = self
45 .pending
46 .lock()
47 .unwrap_or_else(std::sync::PoisonError::into_inner);
48 let mut keys = Self::load(path)?.keys;
49 for endpoint in pending.iter() {
50 if let Some(key) = self.keys.get(endpoint) {
51 keys.insert(endpoint.clone(), key.clone());
52 } else {
53 keys.remove(endpoint);
54 }
55 }
56 let body = serialize_keys(keys)?;
57 write_private(path, &body)?;
58 pending.clear();
59 Ok(())
60 }
61}
62
63fn serialize_keys(keys: BTreeMap<String, String>) -> Result<String, AuthError> {
64 let store = AuthStore {
65 keys,
66 ..AuthStore::default()
67 };
68 toml::to_string_pretty(&store).map_err(|err| AuthError::Serialize(err.to_string()))
69}
70
71fn lock_path(path: &Path) -> PathBuf {
72 let mut name = path
73 .file_name()
74 .unwrap_or_else(|| std::ffi::OsStr::new("auth.toml"))
75 .to_os_string();
76 name.push(".lock");
77 path.with_file_name(name)
78}
79
80pub(crate) fn ensure_dir_private(dir: &Path) -> Result<(), AuthError> {
86 ensure_dir_private_with(dir, create_private_dir, sync_directory)
87}
88
89fn ensure_dir_private_with(
90 dir: &Path,
91 create_component: impl FnMut(&Path) -> std::io::Result<()>,
92 sync_directory: impl FnMut(&Path) -> std::io::Result<()>,
93) -> Result<(), AuthError> {
94 ensure_dir_private_with_metadata(
95 dir,
96 |path| fs::symlink_metadata(path),
97 create_component,
98 sync_directory,
99 )
100}
101
102fn ensure_dir_private_with_metadata(
103 dir: &Path,
104 mut metadata: impl FnMut(&Path) -> std::io::Result<fs::Metadata>,
105 mut create_component: impl FnMut(&Path) -> std::io::Result<()>,
106 mut sync_directory: impl FnMut(&Path) -> std::io::Result<()>,
107) -> Result<(), AuthError> {
108 let mut missing = Vec::new();
109 let mut cursor = dir;
110 loop {
111 match metadata(cursor) {
112 Ok(_) => break,
113 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
114 missing.push(cursor.to_path_buf());
115 }
116 Err(err) => return Err(AuthError::Write(dir.to_path_buf(), err)),
117 }
118 let Some(parent) = cursor
119 .parent()
120 .filter(|parent| !parent.as_os_str().is_empty())
121 else {
122 break;
123 };
124 cursor = parent;
125 }
126
127 for created in missing.iter().rev() {
128 match create_component(created) {
129 Ok(()) => {
130 restrict(created, 0o700)?;
131 sync_directory(created)
132 .map_err(|err| AuthError::Write(created.to_path_buf(), err))?;
133 sync_directory(temporary_parent(created))
134 .map_err(|err| AuthError::Write(created.to_path_buf(), err))?;
135 }
136 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
137 require_directory(created)?;
138 }
139 Err(err) => return Err(AuthError::Write(created.to_path_buf(), err)),
140 }
141 }
142 require_directory(dir)
143}
144
145fn create_private_dir(path: &Path) -> std::io::Result<()> {
146 #[cfg(unix)]
147 {
148 use std::os::unix::fs::DirBuilderExt;
149
150 let mut builder = fs::DirBuilder::new();
151 builder.mode(0o700).create(path)
152 }
153
154 #[cfg(not(unix))]
155 {
156 fs::create_dir(path)
157 }
158}
159
160fn require_directory(path: &Path) -> Result<(), AuthError> {
161 match fs::metadata(path) {
162 Ok(metadata) if metadata.is_dir() => Ok(()),
163 Ok(_) => Err(AuthError::Write(
164 path.to_path_buf(),
165 std::io::Error::from(std::io::ErrorKind::NotADirectory),
166 )),
167 Err(err) => Err(AuthError::Write(path.to_path_buf(), err)),
168 }
169}
170
171fn write_private(path: &Path, body: &str) -> Result<(), AuthError> {
177 write_private_with(path, body, sync_directory)
178}
179
180fn write_private_with(
181 path: &Path,
182 body: &str,
183 sync_parent: impl FnOnce(&Path) -> std::io::Result<()>,
184) -> Result<(), AuthError> {
185 let parent = temporary_parent(path);
186 let mut temporary = tempfile::NamedTempFile::new_in(parent)
187 .map_err(|err| AuthError::Write(path.to_path_buf(), err))?;
188 temporary
189 .write_all(body.as_bytes())
190 .map_err(|err| AuthError::Write(path.to_path_buf(), err))?;
191 temporary
192 .as_file()
193 .sync_all()
194 .map_err(|err| AuthError::Write(path.to_path_buf(), err))?;
195 restrict(temporary.path(), 0o600)?;
196 temporary
197 .persist(path)
198 .map_err(|err| AuthError::Write(path.to_path_buf(), err.error))?;
199 sync_parent(parent).map_err(|err| AuthError::Write(path.to_path_buf(), err))
200}
201
202#[cfg(unix)]
203fn sync_directory(path: &Path) -> std::io::Result<()> {
204 fs::File::open(path)?.sync_all()
205}
206
207#[cfg(not(unix))]
208fn sync_directory(_path: &Path) -> std::io::Result<()> {
209 Ok(())
210}
211
212pub(crate) fn temporary_parent(path: &Path) -> &Path {
213 path.parent()
214 .filter(|parent| !parent.as_os_str().is_empty())
215 .unwrap_or_else(|| Path::new("."))
216}
217
218#[cfg(unix)]
219pub(crate) fn restrict(path: &Path, mode: u32) -> Result<(), AuthError> {
220 use std::os::unix::fs::PermissionsExt;
221 fs::set_permissions(path, fs::Permissions::from_mode(mode))
222 .map_err(|err| AuthError::Write(path.to_path_buf(), err))
223}
224
225#[cfg(not(unix))]
226pub(crate) fn restrict(_path: &Path, _mode: u32) -> Result<(), AuthError> {
230 Ok(())
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 #[cfg(unix)]
237 use crate::test_support::assert_mode;
238
239 fn assert_write_error(error: AuthError, path: &Path, kind: std::io::ErrorKind) {
240 match error {
241 AuthError::Write(actual_path, source) => {
242 assert_eq!(actual_path, path);
243 assert_eq!(source.kind(), kind);
244 }
245 other => panic!("expected write error, got {other:?}"),
246 }
247 }
248
249 #[test]
250 fn metadata_errors_are_not_treated_as_missing_directories() {
251 let dir = Path::new("denied");
252 let error = ensure_dir_private_with_metadata(
253 dir,
254 |_| Err(std::io::Error::from(std::io::ErrorKind::PermissionDenied)),
255 |_| panic!("metadata failure must not attempt directory creation"),
256 |_| panic!("metadata failure must not attempt directory sync"),
257 )
258 .expect_err("metadata failure must remain fatal");
259
260 assert_write_error(error, dir, std::io::ErrorKind::PermissionDenied);
261 }
262
263 #[test]
264 fn creation_errors_are_not_treated_as_races() {
265 let base = tempfile::tempdir().expect("tempdir");
266 let dir = base.path().join("denied");
267 let error = ensure_dir_private_with(
268 &dir,
269 |_| Err(std::io::Error::from(std::io::ErrorKind::PermissionDenied)),
270 |_| panic!("failed directory creation must not be synced"),
271 )
272 .expect_err("creation failure must remain fatal");
273
274 assert_write_error(error, &dir, std::io::ErrorKind::PermissionDenied);
275 }
276
277 #[test]
278 fn a_store_parent_must_be_a_directory() {
279 let base = tempfile::tempdir().expect("tempdir");
280 let file = base.path().join("not-a-directory");
281 std::fs::write(&file, "data").expect("fixture file");
282
283 let error = require_directory(&file).expect_err("file must not pass as directory");
284
285 assert_write_error(error, &file, std::io::ErrorKind::NotADirectory);
286 }
287
288 #[test]
289 fn private_directory_creation_creates_the_requested_path() {
290 let base = tempfile::tempdir().expect("tempdir");
291 let dir = base.path().join("private");
292
293 create_private_dir(&dir).expect("create private directory");
294
295 assert!(dir.is_dir());
296 #[cfg(unix)]
297 assert_mode(&dir, 0o700);
298 }
299
300 #[cfg(unix)]
301 #[test]
302 fn syncing_a_missing_directory_reports_not_found() {
303 let base = tempfile::tempdir().expect("tempdir");
304 let missing = base.path().join("missing");
305
306 let error = sync_directory(&missing).expect_err("missing directory cannot be synced");
307
308 assert_eq!(error.kind(), std::io::ErrorKind::NotFound);
309 }
310
311 #[test]
312 fn publication_syncs_the_parent_after_the_store_is_visible() {
313 let dir = tempfile::tempdir().expect("tempdir");
314 let path = dir.path().join("auth.toml");
315 let mut called = false;
316
317 write_private_with(&path, "[keys]\n", |parent| {
318 assert_eq!(parent, dir.path());
319 assert_eq!(
320 std::fs::read_to_string(&path).expect("published store"),
321 "[keys]\n"
322 );
323 called = true;
324 Ok(())
325 })
326 .expect("publish and sync");
327
328 assert!(called, "the containing directory was not synced");
329 }
330
331 #[test]
332 fn creating_nested_store_directories_syncs_each_new_link() {
333 let base = tempfile::tempdir().expect("tempdir");
334 let dir = base.path().join("first").join("second");
335 let mut seen = Vec::new();
336
337 ensure_dir_private_with(
338 &dir,
339 |component| {
340 std::fs::create_dir(component)?;
341 #[cfg(unix)]
342 {
343 use std::os::unix::fs::PermissionsExt;
344 std::fs::set_permissions(component, std::fs::Permissions::from_mode(0o755))?;
345 }
346 Ok(())
347 },
348 |directory| {
349 assert!(directory.exists(), "directory was not visible before sync");
350 seen.push(directory.to_path_buf());
351 Ok(())
352 },
353 )
354 .expect("create and sync directory tree");
355
356 assert_eq!(
357 seen,
358 vec![
359 base.path().join("first"),
360 base.path().to_path_buf(),
361 dir.clone(),
362 base.path().join("first"),
363 ]
364 );
365 #[cfg(unix)]
366 {
367 for created in [base.path().join("first"), dir] {
368 assert_mode(&created, 0o700);
369 }
370 }
371 }
372
373 #[cfg(unix)]
374 #[test]
375 fn a_directory_created_by_a_racing_process_is_not_claimed() {
376 use std::os::unix::fs::PermissionsExt;
377
378 let base = tempfile::tempdir().expect("tempdir");
379 let dir = base.path().join("raced");
380
381 ensure_dir_private_with(
382 &dir,
383 |component| {
384 std::fs::create_dir(component)?;
385 std::fs::set_permissions(component, std::fs::Permissions::from_mode(0o755))?;
386 Err(std::io::Error::from(std::io::ErrorKind::AlreadyExists))
387 },
388 |_| panic!("a directory drep did not create must not be synced as its own"),
389 )
390 .expect("accept raced directory");
391
392 assert_mode(&dir, 0o755);
393 }
394}