1use std::io::Read;
9use std::path::{Path, PathBuf};
10
11use crate::json::{self, Json};
12use crate::store::StoreError;
13
14pub use aede_accounts::{
15 ACCOUNTS_FILE, Account, Accounts, ApiKey, FORMAT_VERSION, LOCAL_OWNER, MAX_ACCOUNTS,
16 MAX_API_KEY_LABEL_BYTES, MAX_API_KEYS, MAX_API_KEYS_PER_ACCOUNT, MAX_PASSWORD_BYTES, Role,
17 login_name, random_token,
18};
19
20const MAX_FILE_BYTES: u64 = 1024 * 1024;
21
22pub fn accounts_path(data_dir: &Path) -> PathBuf {
24 data_dir.join(ACCOUNTS_FILE)
25}
26
27pub fn check_private_file(path: &Path) -> Result<(), StoreError> {
30 inspect_private_file(path).map(|_| ())
31}
32
33fn inspect_private_file(path: &Path) -> Result<Option<std::fs::Metadata>, StoreError> {
34 match std::fs::symlink_metadata(path) {
35 Ok(metadata) => {
36 check_private_metadata(&metadata)?;
37 Ok(Some(metadata))
38 }
39 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
40 Err(error) => Err(error.into()),
41 }
42}
43
44fn check_private_metadata(metadata: &std::fs::Metadata) -> Result<(), StoreError> {
45 if !metadata.is_file() {
46 return Err(StoreError::AccountsInvalid(
47 "credentials require a regular file, never a symbolic link",
48 ));
49 }
50 #[cfg(unix)]
51 {
52 use std::os::unix::fs::PermissionsExt;
53 if metadata.permissions().mode() & 0o077 != 0 {
54 return Err(StoreError::AccountsInvalid(
55 "credential files must be private (chmod 600)",
56 ));
57 }
58 }
59 Ok(())
60}
61
62pub(crate) fn validate_private_read(
66 inspected: &std::fs::Metadata,
67 opened: &std::fs::Metadata,
68) -> Result<(), StoreError> {
69 check_private_metadata(inspected)?;
70 check_private_metadata(opened)?;
71 #[cfg(unix)]
72 {
73 use std::os::unix::fs::MetadataExt;
74 if inspected.dev() != opened.dev() || inspected.ino() != opened.ino() {
75 return Err(StoreError::AccountsInvalid(
76 "credential source changed while it was opened; retry",
77 ));
78 }
79 }
80 Ok(())
81}
82
83pub fn load(path: &Path) -> Result<Option<Accounts>, StoreError> {
88 load_with_open(path, |path| std::fs::File::open(path))
89}
90
91fn load_with_open(
92 path: &Path,
93 open: impl FnOnce(&Path) -> std::io::Result<std::fs::File>,
94) -> Result<Option<Accounts>, StoreError> {
95 let Some(inspected) = inspect_private_file(path)? else {
96 return Ok(None);
97 };
98 let file = open(path)?;
99 let opened = file.metadata()?;
100 validate_private_read(&inspected, &opened)?;
101 if opened.len() > MAX_FILE_BYTES {
102 return Err(StoreError::AccountsInvalid("account file exceeds 1 MiB"));
103 }
104 let mut text = String::new();
105 file.take(MAX_FILE_BYTES + 1).read_to_string(&mut text)?;
106 if text.len() as u64 > MAX_FILE_BYTES {
107 return Err(StoreError::AccountsInvalid("account file exceeds 1 MiB"));
108 }
109 from_json(&json::parse(&text).map_err(StoreError::AccountsParse)?).map(Some)
110}
111
112pub fn save(accounts: &Accounts, path: &Path) -> Result<(), StoreError> {
115 check_private_file(path)?;
116 let document = to_json(accounts);
117 from_json(&document)?;
118 crate::atomic_file::write(path, document.to_string_compact().as_bytes())?;
119 Ok(())
120}
121
122pub fn to_json(accounts: &Accounts) -> Json {
125 from_value(aede_accounts::to_json(accounts))
126}
127
128pub fn from_json(root: &Json) -> Result<Accounts, StoreError> {
131 aede_accounts::from_json(&to_value(root, 0)?)
132 .map_err(|error| StoreError::AccountsInvalid(error.reason()))
133}
134
135fn to_value(value: &Json, depth: usize) -> Result<serde_json::Value, StoreError> {
138 use serde_json::Value;
139 if depth >= 128 && matches!(value, Json::Arr(_) | Json::Obj(_)) {
140 return Err(StoreError::AccountsInvalid(
141 "malformed account store or unsupported version",
142 ));
143 }
144 Ok(match value {
145 Json::Null => Value::Null,
146 Json::Bool(value) => Value::Bool(*value),
147 Json::Num(value) => serde_json::Number::from_f64(*value).map_or(Value::Null, Value::Number),
149 Json::Str(value) => Value::String(value.clone()),
150 Json::Arr(values) => Value::Array(
151 values
152 .iter()
153 .map(|value| to_value(value, depth + 1))
154 .collect::<Result<_, _>>()?,
155 ),
156 Json::Obj(values) => Value::Object(
157 values
158 .iter()
159 .map(|(key, value)| Ok((key.clone(), to_value(value, depth + 1)?)))
160 .collect::<Result<_, StoreError>>()?,
161 ),
162 })
163}
164
165fn from_value(value: serde_json::Value) -> Json {
168 use serde_json::Value;
169 match value {
170 Value::Null => Json::Null,
171 Value::Bool(value) => Json::Bool(value),
172 Value::Number(value) => value.as_f64().map_or(Json::Null, Json::Num),
173 Value::String(value) => Json::Str(value),
174 Value::Array(values) => Json::Arr(values.into_iter().map(from_value).collect()),
175 Value::Object(values) => Json::Obj(
176 values
177 .into_iter()
178 .map(|(key, value)| (key, from_value(value)))
179 .collect(),
180 ),
181 }
182}
183
184#[cfg(test)]
185#[path = "accounts_tests.rs"]
186mod tests;
187
188#[cfg(test)]
189#[path = "accounts_test_support.rs"]
190mod test_support;