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}