1use std::collections::BTreeMap;
4
5use super::*;
6
7pub const MAX_API_KEYS: usize = 512;
9pub const MAX_API_KEYS_PER_ACCOUNT: usize = 8;
11pub const MAX_API_KEY_LABEL_BYTES: usize = 128;
13
14#[derive(Clone, PartialEq, Eq)]
17pub struct ApiKey {
18 pub id: String,
20 pub label: String,
22 pub created_at: u64,
24 owner: String,
25 revision: u64,
26 secret_hash: String,
27}
28
29impl std::fmt::Debug for ApiKey {
30 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 formatter
32 .debug_struct("ApiKey")
33 .field("id", &self.id)
34 .field("label", &self.label)
35 .field("created_at", &self.created_at)
36 .finish_non_exhaustive()
37 }
38}
39
40impl Accounts {
41 pub fn api_keys(&self, username: &str) -> Result<Vec<&ApiKey>, String> {
44 let account = self
45 .find(username)
46 .ok_or("no account has this login name")?;
47 Ok(self
48 .api_keys
49 .iter()
50 .filter(|key| key.owner == account.id)
51 .collect())
52 }
53
54 pub fn create_api_key(
60 &mut self,
61 username: &str,
62 label: &str,
63 now: u64,
64 ) -> Result<(ApiKey, String), String> {
65 let account = self
66 .find(username)
67 .ok_or("no account has this login name")?;
68 if !account.enabled {
69 return Err("disabled accounts cannot receive API keys".into());
70 }
71 if !valid_label(label) {
72 return Err(
73 "API key labels need non-whitespace text and at most 128 UTF-8 bytes".into(),
74 );
75 }
76 if self.api_keys.len() >= MAX_API_KEYS {
77 return Err("the installation already has 512 API keys".into());
78 }
79 if self
80 .api_keys
81 .iter()
82 .filter(|key| key.owner == account.id)
83 .count()
84 >= MAX_API_KEYS_PER_ACCOUNT
85 {
86 return Err("this account already has eight API keys".into());
87 }
88 if now > MAX_REVISION {
89 return Err("API key creation time is too large".into());
90 }
91 let id = random_token()?;
92 if self.api_keys.iter().any(|key| key.id == id) {
93 return Err("could not generate a unique API key identifier; retry".into());
94 }
95 let secret = random_token()?;
96 let key = ApiKey {
97 id,
98 label: label.into(),
99 created_at: now,
100 owner: account.id.clone(),
101 revision: account.revision,
102 secret_hash: hash_password(&secret)?,
103 };
104 let token = format!("{}.{}", key.id, secret);
105 self.api_keys.push(key.clone());
106 Ok((key, token))
107 }
108
109 pub fn revoke_api_key(&mut self, username: &str, key_id: &str) -> Result<(), String> {
111 let account = self
112 .find(username)
113 .ok_or("no account has this login name")?;
114 let index = self
115 .api_keys
116 .iter()
117 .position(|key| key.owner == account.id && key.id == key_id)
118 .ok_or("no API key with this identifier belongs to the account")?;
119 self.api_keys.remove(index);
120 Ok(())
121 }
122
123 pub fn authenticate_api_key(&self, token: &str) -> Option<(&Account, &ApiKey)> {
128 if token.len() != 129 {
129 return None;
130 }
131 let (id, secret) = token.split_once('.')?;
132 if !valid_token(id) || !valid_token(secret) {
133 return None;
134 }
135 let key = self.api_keys.iter().find(|key| key.id == id);
136 let hash = key.map_or(DUMMY_HASH, |key| key.secret_hash.as_str());
137 let parsed = PasswordHash::new(hash).ok()?;
138 let matches = hasher()
139 .ok()?
140 .verify_password(secret.as_bytes(), &parsed)
141 .is_ok();
142 let key = key.filter(|_| matches)?;
143 let account = self.api_key_account(&key.id, &key.owner, &self.epoch, key.revision)?;
144 Some((account, key))
145 }
146
147 pub fn api_key_account(
152 &self,
153 key_id: &str,
154 owner: &str,
155 epoch: &str,
156 revision: u64,
157 ) -> Option<&Account> {
158 let account = self.session_account(owner, epoch, revision)?;
159 self.api_keys
160 .iter()
161 .any(|key| key.id == key_id && key.owner == owner && key.revision == revision)
162 .then_some(account)
163 }
164
165 pub(super) fn purge_api_keys(&mut self, owner: &str) {
166 self.api_keys.retain(|key| key.owner != owner);
167 }
168}
169
170fn valid_label(label: &str) -> bool {
171 label.len() <= MAX_API_KEY_LABEL_BYTES && !label.trim().is_empty()
172}
173
174pub(super) fn to_json(keys: &[ApiKey]) -> Json {
175 Json::Array(
176 keys.iter()
177 .map(|key| {
178 serde_json::json!({
179 "id": key.id,
180 "label": key.label,
181 "created_at": key.created_at,
182 "owner": key.owner,
183 "revision": key.revision,
184 "secret_hash": key.secret_hash,
185 })
186 })
187 .collect(),
188 )
189}
190
191pub(super) fn from_json(
192 value: Option<&Json>,
193 accounts: &[Account],
194) -> Result<Vec<ApiKey>, InvalidStore> {
195 let Some(value) = value else {
196 return Ok(Vec::new());
197 };
198 let invalid = || InvalidStore::new("malformed or excessive API key records");
199 let rows = value
200 .as_array()
201 .filter(|rows| rows.len() <= MAX_API_KEYS)
202 .ok_or_else(invalid)?;
203 let mut keys = Vec::with_capacity(rows.len());
204 let mut ids = BTreeSet::new();
205 let mut counts = BTreeMap::<&str, usize>::new();
206 for row in rows {
207 let string = |field| row.get(field).and_then(Json::as_str).ok_or_else(invalid);
208 let number = |field| {
209 row.get(field)
210 .and_then(json_u64)
211 .filter(|value| *value <= MAX_REVISION)
212 .ok_or_else(invalid)
213 };
214 let id = string("id")?;
215 let label = string("label")?;
216 let owner = string("owner")?;
217 let secret_hash = string("secret_hash")?;
218 let revision = number("revision")?;
219 if !valid_token(id)
220 || !ids.insert(id)
221 || !valid_label(label)
222 || !valid_hash(secret_hash)
223 || !accounts.iter().any(|account| {
224 account.id == owner && account.enabled && account.revision == revision
225 })
226 {
227 return Err(invalid());
228 }
229 let count = counts.entry(owner).or_default();
230 *count += 1;
231 if *count > MAX_API_KEYS_PER_ACCOUNT {
232 return Err(invalid());
233 }
234 keys.push(ApiKey {
235 id: id.into(),
236 label: label.into(),
237 created_at: number("created_at")?,
238 owner: owner.into(),
239 revision,
240 secret_hash: secret_hash.into(),
241 });
242 }
243 Ok(keys)
244}
245
246#[cfg(test)]
247#[path = "keys_tests.rs"]
248mod tests;