Skip to main content

elasticctl_core/
auth.rs

1use crate::config::Profile;
2use crate::error::{Error, ErrorKind, Result};
3use base64::Engine as _;
4use base64::engine::general_purpose::STANDARD;
5
6#[derive(Clone, PartialEq, Eq)]
7pub enum Credential {
8    ApiKey(String),
9    Basic { username: String, password: String },
10}
11
12impl std::fmt::Debug for Credential {
13    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14        match self {
15            Credential::ApiKey(_) => f.write_str("Credential::ApiKey(***)"),
16            Credential::Basic { username, .. } => {
17                write!(
18                    f,
19                    "Credential::Basic {{ username: {username:?}, password: *** }}"
20                )
21            }
22        }
23    }
24}
25
26impl Credential {
27    pub fn from_profile(p: &Profile) -> Result<Credential> {
28        if let Some(key) = &p.api_key
29            && !key.trim().is_empty()
30        {
31            return Ok(Credential::ApiKey(key.clone()));
32        }
33        match (&p.username, &p.password) {
34            (Some(u), Some(pw)) => Ok(Credential::Basic {
35                username: u.clone(),
36                password: pw.clone(),
37            }),
38            _ => Err(Error::new(
39                ErrorKind::Auth,
40                "No credential configured. Set api_key, or both username and password.",
41            )),
42        }
43    }
44
45    pub fn is_configured(profile: &Profile) -> bool {
46        Self::from_profile(profile).is_ok()
47    }
48
49    pub fn header_value(&self) -> String {
50        match self {
51            Credential::ApiKey(k) => format!("ApiKey {k}"),
52            Credential::Basic { username, password } => {
53                format!(
54                    "Basic {}",
55                    STANDARD.encode(format!("{username}:{password}"))
56                )
57            }
58        }
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    fn profile() -> Profile {
67        Profile {
68            kibana_url: "https://kb.example.com".into(),
69            es_url: None,
70            api_key: None,
71            username: None,
72            password: None,
73            space: "default".into(),
74            verify: true,
75            timeout_secs: 30,
76        }
77    }
78
79    #[test]
80    fn api_key_is_sent_verbatim_because_elastic_keys_are_already_encoded() {
81        let c = Credential::ApiKey("essu_abc123".into());
82        assert_eq!(c.header_value(), "ApiKey essu_abc123");
83    }
84
85    #[test]
86    fn basic_auth_is_base64_encoded() {
87        let c = Credential::Basic {
88            username: "elastic".into(),
89            password: "changeme".into(),
90        };
91        assert_eq!(c.header_value(), "Basic ZWxhc3RpYzpjaGFuZ2VtZQ==");
92    }
93
94    #[test]
95    fn api_key_wins_when_both_credentials_are_configured() {
96        let mut p = profile();
97        p.api_key = Some("essu_abc".into());
98        p.username = Some("elastic".into());
99        p.password = Some("changeme".into());
100        assert!(matches!(
101            Credential::from_profile(&p).unwrap(),
102            Credential::ApiKey(_)
103        ));
104    }
105
106    #[test]
107    fn a_profile_with_no_credential_is_an_auth_error() {
108        let err = Credential::from_profile(&profile()).unwrap_err();
109        assert_eq!(err.kind, ErrorKind::Auth);
110    }
111
112    #[test]
113    fn a_username_without_a_password_is_an_auth_error() {
114        let mut p = profile();
115        p.username = Some("elastic".into());
116        assert_eq!(
117            Credential::from_profile(&p).unwrap_err().kind,
118            ErrorKind::Auth
119        );
120    }
121
122    #[test]
123    fn is_configured_true_for_a_valid_api_key() {
124        let mut p = profile();
125        p.api_key = Some("essu_abc".into());
126        assert!(Credential::is_configured(&p));
127    }
128
129    #[test]
130    fn is_configured_false_for_an_empty_api_key_with_no_basic_auth() {
131        let mut p = profile();
132        p.api_key = Some("".into());
133        assert!(!Credential::is_configured(&p));
134    }
135
136    #[test]
137    fn is_configured_false_for_a_username_without_a_password() {
138        let mut p = profile();
139        p.username = Some("elastic".into());
140        assert!(!Credential::is_configured(&p));
141    }
142
143    #[test]
144    fn debug_redacts_api_key_material() {
145        let c = Credential::ApiKey("essu_secret".into());
146        let debug_str = format!("{:?}", c);
147        assert!(!debug_str.contains("essu_secret"));
148        assert!(debug_str.contains("***"));
149    }
150
151    #[test]
152    fn debug_redacts_password_but_shows_username() {
153        let c = Credential::Basic {
154            username: "elastic".into(),
155            password: "changeme".into(),
156        };
157        let debug_str = format!("{:?}", c);
158        assert!(!debug_str.contains("changeme"));
159        assert!(debug_str.contains("elastic"));
160        assert!(debug_str.contains("***"));
161    }
162}