Skip to main content

faucet_common_elasticsearch/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3//! # faucet-common-elasticsearch
4//!
5//! Shared configuration types for the [`faucet-stream`](https://crates.io/crates/faucet-stream)
6//! Elasticsearch source and sink connectors.
7//!
8//! - [`ElasticsearchAuth`] — authentication modes (None, Basic, Bearer, ApiKey)
9//!
10//! The enum derives `Serialize`, `Deserialize`, and `JsonSchema` so it round-trips
11//! through YAML/JSON configs and CLI introspection. Its `Debug` impl masks
12//! credentials (`password`, `token`, `key`) as `"***"` so accidental logging of a
13//! config value never leaks secrets.
14
15use faucet_core::{Credential, FaucetError};
16use schemars::JsonSchema;
17use serde::{Deserialize, Serialize};
18
19/// Authentication method for Elasticsearch.
20///
21/// Serializes as `{ type: <method>, config: { … } }` (adjacent tagging,
22/// snake_case discriminators) — the consistent auth wire shape shared by
23/// every faucet connector.
24#[derive(Clone, Serialize, Deserialize, JsonSchema)]
25#[serde(tag = "type", content = "config", rename_all = "snake_case")]
26pub enum ElasticsearchAuth {
27    /// No authentication.
28    None,
29    /// HTTP Basic authentication.
30    Basic { username: String, password: String },
31    /// Bearer token authentication.
32    Bearer { token: String },
33    /// API key authentication (sent as `ApiKey` in the `Authorization` header).
34    ApiKey { key: String },
35}
36
37/// Map a [`Credential`] from a shared [`faucet_core::AuthProvider`] onto an
38/// [`ElasticsearchAuth`] variant.
39///
40/// Elasticsearch supports `Bearer` and `Basic` credentials. The `Header` and
41/// `Token` credential variants have no equivalent Elasticsearch auth mode, so
42/// they return [`FaucetError::Auth`].
43pub fn credential_to_auth(cred: Credential) -> Result<ElasticsearchAuth, FaucetError> {
44    match cred {
45        Credential::Bearer(token) => Ok(ElasticsearchAuth::Bearer { token }),
46        Credential::Basic { username, password } => {
47            Ok(ElasticsearchAuth::Basic { username, password })
48        }
49        other => Err(FaucetError::Auth(format!(
50            "Elasticsearch auth provider must yield a bearer or basic credential, got {other:?}"
51        ))),
52    }
53}
54
55impl std::fmt::Debug for ElasticsearchAuth {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        match self {
58            Self::None => write!(f, "None"),
59            Self::Basic { username, .. } => f
60                .debug_struct("Basic")
61                .field("username", username)
62                .field("password", &"***")
63                .finish(),
64            Self::Bearer { .. } => write!(f, "Bearer(***)"),
65            Self::ApiKey { .. } => write!(f, "ApiKey(***)"),
66        }
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn debug_masks_basic_password() {
76        let auth = ElasticsearchAuth::Basic {
77            username: "user".into(),
78            password: "secret".into(),
79        };
80        let debug = format!("{auth:?}");
81        assert!(debug.contains("user"));
82        assert!(debug.contains("***"));
83        assert!(!debug.contains("secret"));
84    }
85
86    #[test]
87    fn debug_masks_bearer_token() {
88        let auth = ElasticsearchAuth::Bearer {
89            token: "my-token".into(),
90        };
91        let debug = format!("{auth:?}");
92        assert!(debug.contains("***"));
93        assert!(!debug.contains("my-token"));
94    }
95
96    #[test]
97    fn debug_masks_api_key() {
98        let auth = ElasticsearchAuth::ApiKey {
99            key: "my-key".into(),
100        };
101        let debug = format!("{auth:?}");
102        assert!(debug.contains("***"));
103        assert!(!debug.contains("my-key"));
104    }
105
106    #[test]
107    fn debug_none_renders_unit() {
108        let auth = ElasticsearchAuth::None;
109        assert_eq!(format!("{auth:?}"), "None");
110    }
111
112    #[test]
113    fn serde_round_trip_basic() {
114        let auth = ElasticsearchAuth::Basic {
115            username: "u".into(),
116            password: "p".into(),
117        };
118        let json = serde_json::to_string(&auth).unwrap();
119        assert_eq!(
120            json,
121            r#"{"type":"basic","config":{"username":"u","password":"p"}}"#
122        );
123        let parsed: ElasticsearchAuth = serde_json::from_str(&json).unwrap();
124        assert!(matches!(parsed, ElasticsearchAuth::Basic { .. }));
125    }
126
127    #[test]
128    fn serde_round_trip_none() {
129        let json = serde_json::to_string(&ElasticsearchAuth::None).unwrap();
130        assert_eq!(json, r#"{"type":"none"}"#);
131        let parsed: ElasticsearchAuth = serde_json::from_str(&json).unwrap();
132        assert!(matches!(parsed, ElasticsearchAuth::None));
133    }
134
135    #[test]
136    fn serde_round_trip_bearer() {
137        let auth = ElasticsearchAuth::Bearer { token: "t".into() };
138        let json = serde_json::to_string(&auth).unwrap();
139        assert_eq!(json, r#"{"type":"bearer","config":{"token":"t"}}"#);
140        let parsed: ElasticsearchAuth = serde_json::from_str(&json).unwrap();
141        assert!(matches!(parsed, ElasticsearchAuth::Bearer { .. }));
142    }
143
144    #[test]
145    fn serde_round_trip_api_key() {
146        let auth = ElasticsearchAuth::ApiKey { key: "k".into() };
147        let json = serde_json::to_string(&auth).unwrap();
148        assert_eq!(json, r#"{"type":"api_key","config":{"key":"k"}}"#);
149        let parsed: ElasticsearchAuth = serde_json::from_str(&json).unwrap();
150        assert!(matches!(parsed, ElasticsearchAuth::ApiKey { .. }));
151    }
152}