1use std::{collections::HashMap, fmt, net::SocketAddr};
3
4use bytes::Bytes;
5use headers::{authorization::Credentials, Authorization};
6use http::{header::AUTHORIZATION, HeaderMap, HeaderValue, StatusCode};
7use serde::{
8 de::{Error, MapAccess, Visitor},
9 Deserialize,
10};
11use vector_config::configurable_component;
12use vector_lib::{
13 compile_vrl,
14 event::{Event, LogEvent, VrlTarget},
15 sensitive_string::SensitiveString,
16 TimeZone,
17};
18use vrl::{
19 compiler::{runtime::Runtime, CompilationResult, CompileConfig, Program},
20 core::Value,
21 diagnostic::Formatter,
22 prelude::TypeState,
23 value::{KeyString, ObjectMap},
24};
25
26use super::ErrorMessage;
27
28#[configurable_component(no_deser)]
33#[derive(Clone, Debug, Eq, PartialEq)]
34#[configurable(metadata(docs::enum_tag_description = "The authentication strategy to use."))]
35#[serde(tag = "strategy", rename_all = "snake_case")]
36pub enum HttpServerAuthConfig {
37 Basic {
43 #[configurable(metadata(docs::examples = "${USERNAME}"))]
45 #[configurable(metadata(docs::examples = "username"))]
46 username: String,
47
48 #[configurable(metadata(docs::examples = "${PASSWORD}"))]
50 #[configurable(metadata(docs::examples = "password"))]
51 password: SensitiveString,
52 },
53
54 Custom {
58 source: String,
60 },
61}
62
63impl<'de> Deserialize<'de> for HttpServerAuthConfig {
65 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
66 where
67 D: serde::Deserializer<'de>,
68 {
69 struct HttpServerAuthConfigVisitor;
70
71 const FIELD_KEYS: [&str; 4] = ["strategy", "username", "password", "source"];
72
73 impl<'de> Visitor<'de> for HttpServerAuthConfigVisitor {
74 type Value = HttpServerAuthConfig;
75
76 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
77 formatter.write_str("a valid authentication strategy (basic or custom)")
78 }
79
80 fn visit_map<A>(self, mut map: A) -> Result<HttpServerAuthConfig, A::Error>
81 where
82 A: MapAccess<'de>,
83 {
84 let mut fields: HashMap<&str, String> = HashMap::default();
85
86 while let Some(key) = map.next_key::<String>()? {
87 if let Some(field_index) = FIELD_KEYS.iter().position(|k| *k == key.as_str()) {
88 if fields.contains_key(FIELD_KEYS[field_index]) {
89 return Err(Error::duplicate_field(FIELD_KEYS[field_index]));
90 }
91 fields.insert(FIELD_KEYS[field_index], map.next_value()?);
92 } else {
93 return Err(Error::unknown_field(&key, &FIELD_KEYS));
94 }
95 }
96
97 let strategy = fields
99 .get("strategy")
100 .map(String::as_str)
101 .unwrap_or_else(|| "basic");
102
103 match strategy {
104 "basic" => {
105 let username = fields
106 .remove("username")
107 .ok_or_else(|| Error::missing_field("username"))?;
108 let password = fields
109 .remove("password")
110 .ok_or_else(|| Error::missing_field("password"))?;
111 Ok(HttpServerAuthConfig::Basic {
112 username,
113 password: SensitiveString::from(password),
114 })
115 }
116 "custom" => {
117 let source = fields
118 .remove("source")
119 .ok_or_else(|| Error::missing_field("source"))?;
120 Ok(HttpServerAuthConfig::Custom { source })
121 }
122 _ => Err(Error::unknown_variant(strategy, &["basic", "custom"])),
123 }
124 }
125 }
126
127 deserializer.deserialize_map(HttpServerAuthConfigVisitor)
128 }
129}
130
131impl HttpServerAuthConfig {
132 pub fn build(
136 &self,
137 enrichment_tables: &vector_lib::enrichment::TableRegistry,
138 ) -> crate::Result<HttpServerAuthMatcher> {
139 match self {
140 HttpServerAuthConfig::Basic { username, password } => {
141 Ok(HttpServerAuthMatcher::AuthHeader(
142 Authorization::basic(username, password.inner()).0.encode(),
143 "Invalid username/password",
144 ))
145 }
146 HttpServerAuthConfig::Custom { source } => {
147 let functions = vrl::stdlib::all()
148 .into_iter()
149 .chain(vector_lib::enrichment::vrl_functions())
150 .chain(vector_vrl_functions::all())
151 .collect::<Vec<_>>();
152
153 let state = TypeState::default();
154
155 let mut config = CompileConfig::default();
156 config.set_custom(enrichment_tables.clone());
157 config.set_read_only();
158
159 let CompilationResult {
160 program,
161 warnings,
162 config: _,
163 } = compile_vrl(source, &functions, &state, config).map_err(|diagnostics| {
164 Formatter::new(source, diagnostics).colored().to_string()
165 })?;
166
167 if !program.final_type_info().result.is_boolean() {
168 return Err("VRL conditions must return a boolean.".into());
169 }
170
171 if !warnings.is_empty() {
172 let warnings = Formatter::new(source, warnings).colored().to_string();
173 warn!(message = "VRL compilation warning.", %warnings);
174 }
175
176 Ok(HttpServerAuthMatcher::Vrl { program })
177 }
178 }
179 }
180}
181
182#[allow(clippy::large_enum_variant)]
185#[derive(Clone, Debug)]
186pub enum HttpServerAuthMatcher {
187 AuthHeader(HeaderValue, &'static str),
189 Vrl {
191 program: Program,
193 },
194}
195
196impl HttpServerAuthMatcher {
197 pub fn handle_auth(
199 &self,
200 address: Option<&SocketAddr>,
201 headers: &HeaderMap<HeaderValue>,
202 path: &str,
203 ) -> Result<(), ErrorMessage> {
204 match self {
205 HttpServerAuthMatcher::AuthHeader(expected, err_message) => {
206 if let Some(header) = headers.get(AUTHORIZATION) {
207 if expected == header {
208 Ok(())
209 } else {
210 Err(ErrorMessage::new(
211 StatusCode::UNAUTHORIZED,
212 err_message.to_string(),
213 ))
214 }
215 } else {
216 Err(ErrorMessage::new(
217 StatusCode::UNAUTHORIZED,
218 "No authorization header".to_owned(),
219 ))
220 }
221 }
222 HttpServerAuthMatcher::Vrl { program } => {
223 self.handle_vrl_auth(address, headers, path, program)
224 }
225 }
226 }
227
228 fn handle_vrl_auth(
229 &self,
230 address: Option<&SocketAddr>,
231 headers: &HeaderMap<HeaderValue>,
232 path: &str,
233 program: &Program,
234 ) -> Result<(), ErrorMessage> {
235 let mut target = VrlTarget::new(
236 Event::Log(LogEvent::from_map(
237 ObjectMap::from([
238 (
239 "headers".into(),
240 Value::Object(
241 headers
242 .iter()
243 .map(|(k, v)| {
244 (
245 KeyString::from(k.to_string()),
246 Value::Bytes(Bytes::copy_from_slice(v.as_bytes())),
247 )
248 })
249 .collect::<ObjectMap>(),
250 ),
251 ),
252 (
253 "address".into(),
254 address.map_or(Value::Null, |a| Value::from(a.ip().to_string())),
255 ),
256 ("path".into(), Value::from(path.to_owned())),
257 ]),
258 Default::default(),
259 )),
260 program.info(),
261 false,
262 );
263 let timezone = TimeZone::default();
264
265 let result = Runtime::default().resolve(&mut target, program, &timezone);
266 match result.map_err(|e| {
267 warn!("Handling auth failed: {}", e);
268 ErrorMessage::new(StatusCode::UNAUTHORIZED, "Auth failed".to_owned())
269 })? {
270 vrl::core::Value::Boolean(result) => {
271 if result {
272 Ok(())
273 } else {
274 Err(ErrorMessage::new(
275 StatusCode::UNAUTHORIZED,
276 "Auth failed".to_owned(),
277 ))
278 }
279 }
280 _ => Err(ErrorMessage::new(
281 StatusCode::UNAUTHORIZED,
282 "Invalid return value".to_owned(),
283 )),
284 }
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use crate::test_util::{next_addr, random_string};
291 use indoc::indoc;
292
293 use super::*;
294
295 impl HttpServerAuthMatcher {
296 fn auth_header(self) -> (HeaderValue, &'static str) {
297 match self {
298 HttpServerAuthMatcher::AuthHeader(header_value, error_message) => {
299 (header_value, error_message)
300 }
301 HttpServerAuthMatcher::Vrl { .. } => {
302 panic!("Expected HttpServerAuthMatcher::AuthHeader")
303 }
304 }
305 }
306 }
307
308 #[test]
309 fn config_should_default_to_basic() {
310 let config: HttpServerAuthConfig = serde_yaml::from_str(indoc! { r#"
311 username: foo
312 password: bar
313 "#
314 })
315 .unwrap();
316
317 if let HttpServerAuthConfig::Basic { username, password } = config {
318 assert_eq!(username, "foo");
319 assert_eq!(password.inner(), "bar");
320 } else {
321 panic!("Expected HttpServerAuthConfig::Basic");
322 }
323 }
324
325 #[test]
326 fn config_should_support_explicit_basic_strategy() {
327 let config: HttpServerAuthConfig = serde_yaml::from_str(indoc! { r#"
328 strategy: basic
329 username: foo
330 password: bar
331 "#
332 })
333 .unwrap();
334
335 if let HttpServerAuthConfig::Basic { username, password } = config {
336 assert_eq!(username, "foo");
337 assert_eq!(password.inner(), "bar");
338 } else {
339 panic!("Expected HttpServerAuthConfig::Basic");
340 }
341 }
342
343 #[test]
344 fn config_should_support_custom_strategy() {
345 let config: HttpServerAuthConfig = serde_yaml::from_str(indoc! { r#"
346 strategy: custom
347 source: "true"
348 "#
349 })
350 .unwrap();
351
352 assert!(matches!(config, HttpServerAuthConfig::Custom { .. }));
353 if let HttpServerAuthConfig::Custom { source } = config {
354 assert_eq!(source, "true");
355 } else {
356 panic!("Expected HttpServerAuthConfig::Custom");
357 }
358 }
359
360 #[test]
361 fn build_basic_auth_should_always_work() {
362 let basic_auth = HttpServerAuthConfig::Basic {
363 username: random_string(16),
364 password: random_string(16).into(),
365 };
366
367 let matcher = basic_auth.build(&Default::default());
368
369 assert!(matcher.is_ok());
370 assert!(matches!(
371 matcher.unwrap(),
372 HttpServerAuthMatcher::AuthHeader { .. }
373 ));
374 }
375
376 #[test]
377 fn build_basic_auth_should_use_username_password_related_message() {
378 let basic_auth = HttpServerAuthConfig::Basic {
379 username: random_string(16),
380 password: random_string(16).into(),
381 };
382
383 let (_, error_message) = basic_auth.build(&Default::default()).unwrap().auth_header();
384 assert_eq!("Invalid username/password", error_message);
385 }
386
387 #[test]
388 fn build_basic_auth_should_use_encode_basic_header() {
389 let username = random_string(16);
390 let password = random_string(16);
391 let basic_auth = HttpServerAuthConfig::Basic {
392 username: username.clone(),
393 password: password.clone().into(),
394 };
395
396 let (header, _) = basic_auth.build(&Default::default()).unwrap().auth_header();
397 assert_eq!(
398 Authorization::basic(&username, &password).0.encode(),
399 header
400 );
401 }
402
403 #[test]
404 fn build_custom_should_fail_on_invalid_source() {
405 let custom_auth = HttpServerAuthConfig::Custom {
406 source: "invalid VRL source".to_string(),
407 };
408
409 assert!(custom_auth.build(&Default::default()).is_err());
410 }
411
412 #[test]
413 fn build_custom_should_fail_on_non_boolean_return_type() {
414 let custom_auth = HttpServerAuthConfig::Custom {
415 source: indoc! {r#"
416 .success = true
417 .
418 "#}
419 .to_string(),
420 };
421
422 assert!(custom_auth.build(&Default::default()).is_err());
423 }
424
425 #[test]
426 fn build_custom_should_success_on_proper_source_with_boolean_return_type() {
427 let custom_auth = HttpServerAuthConfig::Custom {
428 source: indoc! {r#"
429 .headers.authorization == "Basic test"
430 "#}
431 .to_string(),
432 };
433
434 assert!(custom_auth.build(&Default::default()).is_ok());
435 }
436
437 #[test]
438 fn basic_auth_matcher_should_return_401_when_missing_auth_header() {
439 let basic_auth = HttpServerAuthConfig::Basic {
440 username: random_string(16),
441 password: random_string(16).into(),
442 };
443
444 let matcher = basic_auth.build(&Default::default()).unwrap();
445
446 let result = matcher.handle_auth(Some(&next_addr()), &HeaderMap::new(), "/");
447
448 assert!(result.is_err());
449 let error = result.unwrap_err();
450 assert_eq!(401, error.code());
451 assert_eq!("No authorization header", error.message());
452 }
453
454 #[test]
455 fn basic_auth_matcher_should_return_401_and_with_wrong_credentials() {
456 let basic_auth = HttpServerAuthConfig::Basic {
457 username: random_string(16),
458 password: random_string(16).into(),
459 };
460
461 let matcher = basic_auth.build(&Default::default()).unwrap();
462
463 let mut headers = HeaderMap::new();
464 headers.insert(AUTHORIZATION, HeaderValue::from_static("Basic wrong"));
465 let result = matcher.handle_auth(Some(&next_addr()), &headers, "/");
466
467 assert!(result.is_err());
468 let error = result.unwrap_err();
469 assert_eq!(401, error.code());
470 assert_eq!("Invalid username/password", error.message());
471 }
472
473 #[test]
474 fn basic_auth_matcher_should_return_ok_for_correct_credentials() {
475 let username = random_string(16);
476 let password = random_string(16);
477 let basic_auth = HttpServerAuthConfig::Basic {
478 username: username.clone(),
479 password: password.clone().into(),
480 };
481
482 let matcher = basic_auth.build(&Default::default()).unwrap();
483
484 let mut headers = HeaderMap::new();
485 headers.insert(
486 AUTHORIZATION,
487 Authorization::basic(&username, &password).0.encode(),
488 );
489 let result = matcher.handle_auth(Some(&next_addr()), &headers, "/");
490
491 assert!(result.is_ok());
492 }
493
494 #[test]
495 fn custom_auth_matcher_should_return_ok_for_true_vrl_script_result() {
496 let custom_auth = HttpServerAuthConfig::Custom {
497 source: r#".headers.authorization == "test""#.to_string(),
498 };
499
500 let matcher = custom_auth.build(&Default::default()).unwrap();
501
502 let mut headers = HeaderMap::new();
503 headers.insert(AUTHORIZATION, HeaderValue::from_static("test"));
504 let result = matcher.handle_auth(Some(&next_addr()), &headers, "/");
505
506 assert!(result.is_ok());
507 }
508
509 #[test]
510 fn custom_auth_matcher_should_be_able_to_check_address() {
511 let addr = next_addr();
512 let addr_string = addr.ip().to_string();
513 let custom_auth = HttpServerAuthConfig::Custom {
514 source: format!(".address == \"{addr_string}\""),
515 };
516
517 let matcher = custom_auth.build(&Default::default()).unwrap();
518
519 let headers = HeaderMap::new();
520 let result = matcher.handle_auth(Some(&next_addr()), &headers, "/");
521
522 assert!(result.is_ok());
523 }
524
525 #[test]
526 fn custom_auth_matcher_should_work_with_missing_address_too() {
527 let addr = next_addr();
528 let addr_string = addr.ip().to_string();
529 let custom_auth = HttpServerAuthConfig::Custom {
530 source: format!(".address == \"{addr_string}\""),
531 };
532
533 let matcher = custom_auth.build(&Default::default()).unwrap();
534
535 let headers = HeaderMap::new();
536 let result = matcher.handle_auth(None, &headers, "/");
537
538 assert!(result.is_err());
539 }
540
541 #[test]
542 fn custom_auth_matcher_should_be_able_to_check_path() {
543 let custom_auth = HttpServerAuthConfig::Custom {
544 source: r#".path == "/ok""#.to_string(),
545 };
546
547 let matcher = custom_auth.build(&Default::default()).unwrap();
548
549 let headers = HeaderMap::new();
550 let result = matcher.handle_auth(Some(&next_addr()), &headers, "/ok");
551
552 assert!(result.is_ok());
553 }
554
555 #[test]
556 fn custom_auth_matcher_should_return_401_with_wrong_path() {
557 let custom_auth = HttpServerAuthConfig::Custom {
558 source: r#".path == "/ok""#.to_string(),
559 };
560
561 let matcher = custom_auth.build(&Default::default()).unwrap();
562
563 let headers = HeaderMap::new();
564 let result = matcher.handle_auth(Some(&next_addr()), &headers, "/bad");
565
566 assert!(result.is_err());
567 }
568
569 #[test]
570 fn custom_auth_matcher_should_return_401_for_false_vrl_script_result() {
571 let custom_auth = HttpServerAuthConfig::Custom {
572 source: r#".headers.authorization == "test""#.to_string(),
573 };
574
575 let matcher = custom_auth.build(&Default::default()).unwrap();
576
577 let mut headers = HeaderMap::new();
578 headers.insert(AUTHORIZATION, HeaderValue::from_static("wrong value"));
579 let result = matcher.handle_auth(Some(&next_addr()), &headers, "/");
580
581 assert!(result.is_err());
582 let error = result.unwrap_err();
583 assert_eq!(401, error.code());
584 assert_eq!("Auth failed", error.message());
585 }
586
587 #[test]
588 fn custom_auth_matcher_should_return_401_for_failed_script_execution() {
589 let custom_auth = HttpServerAuthConfig::Custom {
590 source: "abort".to_string(),
591 };
592
593 let matcher = custom_auth.build(&Default::default()).unwrap();
594
595 let mut headers = HeaderMap::new();
596 headers.insert(AUTHORIZATION, HeaderValue::from_static("test"));
597 let result = matcher.handle_auth(Some(&next_addr()), &headers, "/");
598
599 assert!(result.is_err());
600 let error = result.unwrap_err();
601 assert_eq!(401, error.code());
602 assert_eq!("Auth failed", error.message());
603 }
604}