Skip to main content

faucet_cli/
auth_catalog.rs

1//! The top-level `auth:` provider catalog.
2//!
3//! [`build_auth_catalog`] turns the config's `auth:` block (a map of named
4//! `{ type, config }` specs) into a map of shared [`SharedAuthProvider`]s, built
5//! **once** so that every connector referencing a provider via `auth: { ref }`
6//! gets the *same* `Arc` — one token cache, single-flight refresh, shared across
7//! all matrix rows.
8
9use std::collections::HashMap;
10
11use faucet_core::SharedAuthProvider;
12use serde_json::Value;
13
14use crate::error::{CliError, CliResult};
15
16/// Name → shared provider. An empty catalog is valid (configs with no `auth:`
17/// block); a connector that then references `{ ref }` errors with
18/// [`CliError::UnknownAuthProvider`].
19pub type AuthCatalog = HashMap<String, SharedAuthProvider>;
20
21/// Build the catalog from the config's optional `auth:` block.
22pub fn build_auth_catalog(specs: Option<&HashMap<String, Value>>) -> CliResult<AuthCatalog> {
23    let mut catalog = AuthCatalog::new();
24    let Some(specs) = specs else {
25        return Ok(catalog);
26    };
27    for (name, spec) in specs {
28        let provider =
29            faucet_auth::build_provider(spec).map_err(|e| CliError::AuthProviderBuild {
30                name: name.clone(),
31                message: e.to_string(),
32            })?;
33        catalog.insert(name.clone(), provider);
34    }
35    Ok(catalog)
36}
37
38/// Extract a connector config's `auth: { ref: <name> }` reference, if present.
39/// Returns `None` for inline auth or no auth.
40pub fn auth_ref(config: &Value) -> Option<String> {
41    config
42        .get("auth")
43        .and_then(|a| a.get("ref"))
44        .and_then(|r| r.as_str())
45        .map(String::from)
46}
47
48/// Resolve a provider name against the catalog, or error with the known names.
49pub fn resolve(catalog: &AuthCatalog, name: &str) -> CliResult<SharedAuthProvider> {
50    catalog
51        .get(name)
52        .cloned()
53        .ok_or_else(|| CliError::UnknownAuthProvider {
54            name: name.to_string(),
55            known: {
56                let mut k: Vec<String> = catalog.keys().cloned().collect();
57                k.sort();
58                k
59            },
60        })
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn empty_block_yields_empty_catalog() {
69        assert!(build_auth_catalog(None).unwrap().is_empty());
70    }
71
72    #[test]
73    fn builds_static_provider_and_resolves() {
74        let mut specs = HashMap::new();
75        specs.insert(
76            "tok".to_string(),
77            serde_json::json!({"type": "static", "config": {"token": "abc"}}),
78        );
79        let catalog = build_auth_catalog(Some(&specs)).unwrap();
80        assert!(resolve(&catalog, "tok").is_ok());
81        let err = resolve(&catalog, "missing").unwrap_err();
82        assert!(matches!(err, CliError::UnknownAuthProvider { .. }));
83    }
84
85    #[test]
86    fn bad_spec_errors_with_name() {
87        let mut specs = HashMap::new();
88        specs.insert("bad".to_string(), serde_json::json!({"type": "nope"}));
89        let err = build_auth_catalog(Some(&specs)).unwrap_err();
90        assert!(matches!(err, CliError::AuthProviderBuild { name, .. } if name == "bad"));
91    }
92
93    #[test]
94    fn auth_ref_extracts_reference() {
95        assert_eq!(
96            auth_ref(&serde_json::json!({"auth": {"ref": "sf"}})),
97            Some("sf".to_string())
98        );
99        assert_eq!(
100            auth_ref(&serde_json::json!({"auth": {"type": "bearer", "config": {"token": "x"}}})),
101            None
102        );
103        assert_eq!(auth_ref(&serde_json::json!({})), None);
104    }
105}