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