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