1use serde::de::{SeqAccess, Visitor};
33use serde::{Deserialize, Deserializer, Serialize, Serializer};
34use std::fmt;
35use std::sync::Arc;
36
37use crate::Error;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
42#[serde(rename_all = "lowercase")]
43pub enum Action {
44 Read,
46 Write,
48 Admin,
50}
51
52#[derive(Debug, Clone, Default, PartialEq, Eq)]
54pub enum CollectionScope {
55 #[default]
57 All,
58 Patterns(Vec<String>),
62}
63
64impl CollectionScope {
65 fn from_patterns(patterns: Vec<String>) -> Self {
69 if patterns.is_empty() || patterns.iter().any(|p| p == "*") {
70 CollectionScope::All
71 } else {
72 CollectionScope::Patterns(patterns)
73 }
74 }
75
76 #[must_use]
78 pub fn matches(&self, collection: &str) -> bool {
79 match self {
80 CollectionScope::All => true,
81 CollectionScope::Patterns(patterns) => {
82 patterns.iter().any(|p| pattern_matches(p, collection))
83 }
84 }
85 }
86}
87
88fn pattern_matches(pattern: &str, name: &str) -> bool {
91 match pattern.strip_suffix('*') {
92 Some(prefix) => name.starts_with(prefix),
93 None => pattern == name,
94 }
95}
96
97#[derive(Debug, Clone, Serialize)]
100pub struct ApiKey {
101 pub secret: String,
103 pub role: Action,
105 pub collections: CollectionScope,
107 #[serde(skip_serializing_if = "Option::is_none")]
112 pub id: Option<String>,
113}
114
115impl ApiKey {
116 #[must_use]
118 pub fn admin(secret: impl Into<String>) -> Self {
119 Self {
120 secret: secret.into(),
121 role: Action::Admin,
122 collections: CollectionScope::All,
123 id: None,
124 }
125 }
126
127 pub(crate) fn actor_id(&self) -> String {
133 match &self.id {
134 Some(id) => id.clone(),
135 None => format!("key:{}", secret_fingerprint(&self.secret)),
136 }
137 }
138}
139
140fn secret_fingerprint(secret: &str) -> String {
143 use sha2::{Digest, Sha256};
144 let digest = Sha256::digest(secret.as_bytes());
145 digest[..8].iter().map(|b| format!("{b:02x}")).collect()
146}
147
148impl From<&str> for ApiKey {
149 fn from(secret: &str) -> Self {
150 ApiKey::admin(secret)
151 }
152}
153
154impl From<String> for ApiKey {
155 fn from(secret: String) -> Self {
156 ApiKey::admin(secret)
157 }
158}
159
160#[derive(Debug, Clone)]
162pub(crate) struct Principal {
163 role: Action,
164 collections: CollectionScope,
165 actor: Arc<str>,
168}
169
170impl Principal {
171 pub(crate) fn insecure() -> Self {
174 Self {
175 role: Action::Admin,
176 collections: CollectionScope::All,
177 actor: Arc::from("insecure"),
178 }
179 }
180
181 fn from_key(key: &ApiKey) -> Self {
182 Self {
183 role: key.role,
184 collections: key.collections.clone(),
185 actor: Arc::from(key.actor_id()),
186 }
187 }
188
189 pub(crate) fn actor(&self) -> &str {
191 &self.actor
192 }
193
194 pub(crate) fn require(&self, action: Action, collection: Option<&str>) -> Result<(), Error> {
199 let role_ok = self.role >= action;
200 let scope_ok = collection.is_none_or(|c| self.collections.matches(c));
201 if role_ok && scope_ok {
202 Ok(())
203 } else {
204 Err(Error::Forbidden(
205 "the API key's scope does not permit this operation".to_owned(),
206 ))
207 }
208 }
209
210 pub(crate) fn can_see(&self, collection: &str) -> bool {
213 self.collections.matches(collection)
214 }
215}
216
217pub(crate) fn authenticate(keys: &[ApiKey], presented: Option<&str>) -> Option<Principal> {
222 if keys.is_empty() {
223 return Some(Principal::insecure());
224 }
225 let token = presented?;
226 let mut matched: Option<&ApiKey> = None;
227 for key in keys {
229 if constant_time_eq(key.secret.as_bytes(), token.as_bytes()) {
230 matched = Some(key);
231 }
232 }
233 matched.map(Principal::from_key)
234}
235
236fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
238 if a.len() != b.len() {
239 return false;
240 }
241 let mut diff = 0u8;
242 for (x, y) in a.iter().zip(b) {
243 diff |= x ^ y;
244 }
245 diff == 0
246}
247
248impl<'de> Deserialize<'de> for CollectionScope {
250 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
251 where
252 D: Deserializer<'de>,
253 {
254 let patterns = Vec::<String>::deserialize(deserializer)?;
255 Ok(CollectionScope::from_patterns(patterns))
256 }
257}
258
259impl Serialize for CollectionScope {
260 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
261 where
262 S: Serializer,
263 {
264 match self {
265 CollectionScope::All => ["*"].serialize(serializer),
266 CollectionScope::Patterns(patterns) => patterns.serialize(serializer),
267 }
268 }
269}
270
271#[derive(Deserialize)]
274struct KeySpec {
275 secret: String,
276 role: Action,
277 #[serde(default)]
278 collections: CollectionScope,
279 #[serde(default)]
280 id: Option<String>,
281}
282
283pub(crate) fn de_api_keys<'de, D>(deserializer: D) -> Result<Vec<ApiKey>, D::Error>
288where
289 D: Deserializer<'de>,
290{
291 struct KeysVisitor;
292
293 impl<'de> Visitor<'de> for KeysVisitor {
294 type Value = Vec<ApiKey>;
295
296 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
297 f.write_str("a comma-separated string of secrets, or a list of secrets/key tables")
298 }
299
300 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
301 where
302 E: serde::de::Error,
303 {
304 Ok(value
305 .split(',')
306 .map(str::trim)
307 .filter(|s| !s.is_empty())
308 .map(ApiKey::admin)
309 .collect())
310 }
311
312 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
313 where
314 A: SeqAccess<'de>,
315 {
316 #[derive(Deserialize)]
317 #[serde(untagged)]
318 enum Entry {
319 Plain(String),
320 Structured(KeySpec),
321 }
322 let mut keys = Vec::new();
323 while let Some(entry) = seq.next_element::<Entry>()? {
324 keys.push(match entry {
325 Entry::Plain(secret) => ApiKey::admin(secret),
326 Entry::Structured(spec) => ApiKey {
327 secret: spec.secret,
328 role: spec.role,
329 collections: spec.collections,
330 id: spec.id,
331 },
332 });
333 }
334 Ok(keys)
335 }
336 }
337
338 deserializer.deserialize_any(KeysVisitor)
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344
345 #[test]
346 fn action_ordering_implies_lesser_privileges() {
347 assert!(Action::Admin > Action::Write);
348 assert!(Action::Write > Action::Read);
349 }
350
351 #[test]
352 fn collection_scope_matches_exact_prefix_and_all() {
353 let all = CollectionScope::from_patterns(vec!["*".to_owned()]);
354 assert!(matches!(all, CollectionScope::All));
355 assert!(all.matches("anything"));
356
357 let scoped = CollectionScope::from_patterns(vec!["acme.*".to_owned(), "shared".to_owned()]);
358 assert!(scoped.matches("acme.orders"));
359 assert!(scoped.matches("shared"));
360 assert!(!scoped.matches("beta.orders"));
361 assert!(!scoped.matches("acme")); assert!(!scoped.matches("shared2"));
363 }
364
365 #[test]
366 fn require_enforces_role_and_scope() {
367 let reader = Principal {
368 role: Action::Read,
369 collections: CollectionScope::Patterns(vec!["acme.*".to_owned()]),
370 actor: Arc::from("reader"),
371 };
372 assert!(reader.require(Action::Read, Some("acme.orders")).is_ok());
373 assert!(reader.require(Action::Write, Some("acme.orders")).is_err());
375 assert!(reader.require(Action::Read, Some("beta.orders")).is_err());
377 assert!(reader.require(Action::Read, None).is_ok());
379 assert!(reader.can_see("acme.orders"));
380 assert!(!reader.can_see("beta.orders"));
381 }
382
383 #[test]
384 fn insecure_principal_is_admin_over_all() {
385 let p = Principal::insecure();
386 assert!(p.require(Action::Admin, Some("anything")).is_ok());
387 assert!(p.can_see("anything"));
388 assert_eq!(p.actor(), "insecure");
389 }
390
391 #[test]
392 fn actor_id_uses_a_label_or_a_fingerprint_but_never_the_secret() {
393 let labeled = ApiKey {
395 id: Some("ci-admin".to_owned()),
396 ..ApiKey::admin("super-secret")
397 };
398 assert_eq!(labeled.actor_id(), "ci-admin");
399 assert_eq!(Principal::from_key(&labeled).actor(), "ci-admin");
400
401 let bare = ApiKey::admin("super-secret");
404 let id = bare.actor_id();
405 assert!(id.starts_with("key:"));
406 assert!(
407 !id.contains("super-secret"),
408 "the fingerprint must not contain the secret"
409 );
410 assert_eq!(id, ApiKey::admin("super-secret").actor_id());
411 assert_ne!(id, ApiKey::admin("other-secret").actor_id());
412 }
413
414 #[test]
415 fn authenticate_matches_secret_and_denies_others() {
416 let keys = vec![
417 ApiKey::admin("admin-secret"),
418 ApiKey {
419 secret: "reader-secret".to_owned(),
420 role: Action::Read,
421 collections: CollectionScope::Patterns(vec!["acme.*".to_owned()]),
422 id: None,
423 },
424 ];
425 assert!(authenticate(&[], None).is_some());
427 let reader = authenticate(&keys, Some("reader-secret")).expect("reader authenticates");
429 assert!(reader.require(Action::Write, Some("acme.x")).is_err());
430 assert!(authenticate(&keys, Some("nope")).is_none());
432 assert!(authenticate(&keys, None).is_none());
433 }
434
435 #[test]
436 fn de_api_keys_parses_csv_strings_and_structured_tables() {
437 #[derive(Deserialize)]
438 struct Wrap {
439 #[serde(deserialize_with = "de_api_keys")]
440 api_keys: Vec<ApiKey>,
441 }
442
443 let csv: Wrap = serde_json::from_str(r#"{"api_keys":"a, b ,c"}"#).unwrap();
445 assert_eq!(csv.api_keys.len(), 3);
446 assert!(csv.api_keys.iter().all(|k| k.role == Action::Admin));
447 assert_eq!(csv.api_keys[1].secret, "b");
448
449 let mixed: Wrap = serde_json::from_str(
451 r#"{"api_keys":["root",{"secret":"ro","role":"read","collections":["acme.*"]}]}"#,
452 )
453 .unwrap();
454 assert_eq!(mixed.api_keys[0].role, Action::Admin);
455 assert!(matches!(
456 mixed.api_keys[0].collections,
457 CollectionScope::All
458 ));
459 assert_eq!(mixed.api_keys[1].role, Action::Read);
460 assert!(mixed.api_keys[1].collections.matches("acme.x"));
461 assert!(!mixed.api_keys[1].collections.matches("beta.x"));
462
463 let defaulted: Wrap =
465 serde_json::from_str(r#"{"api_keys":[{"secret":"w","role":"write"}]}"#).unwrap();
466 assert!(matches!(
467 defaulted.api_keys[0].collections,
468 CollectionScope::All
469 ));
470 }
471}