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 vrl::{
18 compiler::{CompilationResult, CompileConfig, Program, runtime::Runtime},
19 core::Value,
20 prelude::TypeState,
21 value::{KeyString, ObjectMap},
22};
23
24use crate::format_vrl_diagnostics;
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)
164 .map_err(|diagnostics| format_vrl_diagnostics(source, diagnostics))?;
165
166 if !program.final_type_info().result.is_boolean() {
167 return Err("VRL conditions must return a boolean.".into());
168 }
169
170 if !warnings.is_empty() {
171 let warnings = format_vrl_diagnostics(source, warnings);
172 warn!(message = "VRL compilation warning.", %warnings);
173 }
174
175 Ok(HttpServerAuthMatcher::Vrl { program })
176 }
177 }
178 }
179}
180
181#[allow(clippy::large_enum_variant)]
184#[derive(Clone, Debug)]
185pub enum HttpServerAuthMatcher {
186 AuthHeader(HeaderValue, &'static str),
188 Vrl {
190 program: Program,
192 },
193}
194
195impl HttpServerAuthMatcher {
196 pub fn handle_auth(
198 &self,
199 address: Option<&SocketAddr>,
200 headers: &HeaderMap<HeaderValue>,
201 path: &str,
202 ) -> Result<(), ErrorMessage> {
203 match self {
204 HttpServerAuthMatcher::AuthHeader(expected, err_message) => {
205 if let Some(header) = headers.get(AUTHORIZATION) {
206 if expected == header {
207 Ok(())
208 } else {
209 Err(ErrorMessage::new(
210 StatusCode::UNAUTHORIZED,
211 err_message.to_string(),
212 ))
213 }
214 } else {
215 Err(ErrorMessage::new(
216 StatusCode::UNAUTHORIZED,
217 "No authorization header".to_owned(),
218 ))
219 }
220 }
221 HttpServerAuthMatcher::Vrl { program } => {
222 self.handle_vrl_auth(address, headers, path, program)
223 }
224 }
225 }
226
227 fn handle_vrl_auth(
228 &self,
229 address: Option<&SocketAddr>,
230 headers: &HeaderMap<HeaderValue>,
231 path: &str,
232 program: &Program,
233 ) -> Result<(), ErrorMessage> {
234 let mut target = VrlTarget::new(
235 Event::Log(LogEvent::from_map(
236 ObjectMap::from([
237 (
238 "headers".into(),
239 Value::Object(
240 headers
241 .iter()
242 .map(|(k, v)| {
243 (
244 KeyString::from(k.to_string()),
245 Value::Bytes(Bytes::copy_from_slice(v.as_bytes())),
246 )
247 })
248 .collect::<ObjectMap>(),
249 ),
250 ),
251 (
252 "address".into(),
253 address.map_or(Value::Null, |a| Value::from(a.ip().to_string())),
254 ),
255 ("path".into(), Value::from(path.to_owned())),
256 ]),
257 Default::default(),
258 )),
259 program.info(),
260 false,
261 );
262 let timezone = TimeZone::default();
263
264 let result = Runtime::default().resolve(&mut target, program, &timezone);
265 match result.map_err(|e| {
266 warn!("Handling auth failed: {}", e);
267 ErrorMessage::new(StatusCode::UNAUTHORIZED, "Auth failed".to_owned())
268 })? {
269 vrl::core::Value::Boolean(result) => {
270 if result {
271 Ok(())
272 } else {
273 Err(ErrorMessage::new(
274 StatusCode::UNAUTHORIZED,
275 "Auth failed".to_owned(),
276 ))
277 }
278 }
279 _ => Err(ErrorMessage::new(
280 StatusCode::UNAUTHORIZED,
281 "Invalid return value".to_owned(),
282 )),
283 }
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use indoc::indoc;
290
291 use super::*;
292 use crate::test_util::{next_addr, random_string};
293
294 impl HttpServerAuthMatcher {
295 fn auth_header(self) -> (HeaderValue, &'static str) {
296 match self {
297 HttpServerAuthMatcher::AuthHeader(header_value, error_message) => {
298 (header_value, error_message)
299 }
300 HttpServerAuthMatcher::Vrl { .. } => {
301 panic!("Expected HttpServerAuthMatcher::AuthHeader")
302 }
303 }
304 }
305 }
306
307 #[test]
308 fn config_should_default_to_basic() {
309 let config: HttpServerAuthConfig = serde_yaml::from_str(indoc! { r#"
310 username: foo
311 password: bar
312 "#
313 })
314 .unwrap();
315
316 if let HttpServerAuthConfig::Basic { username, password } = config {
317 assert_eq!(username, "foo");
318 assert_eq!(password.inner(), "bar");
319 } else {
320 panic!("Expected HttpServerAuthConfig::Basic");
321 }
322 }
323
324 #[test]
325 fn config_should_support_explicit_basic_strategy() {
326 let config: HttpServerAuthConfig = serde_yaml::from_str(indoc! { r#"
327 strategy: basic
328 username: foo
329 password: bar
330 "#
331 })
332 .unwrap();
333
334 if let HttpServerAuthConfig::Basic { username, password } = config {
335 assert_eq!(username, "foo");
336 assert_eq!(password.inner(), "bar");
337 } else {
338 panic!("Expected HttpServerAuthConfig::Basic");
339 }
340 }
341
342 #[test]
343 fn config_should_support_custom_strategy() {
344 let config: HttpServerAuthConfig = serde_yaml::from_str(indoc! { r#"
345 strategy: custom
346 source: "true"
347 "#
348 })
349 .unwrap();
350
351 assert!(matches!(config, HttpServerAuthConfig::Custom { .. }));
352 if let HttpServerAuthConfig::Custom { source } = config {
353 assert_eq!(source, "true");
354 } else {
355 panic!("Expected HttpServerAuthConfig::Custom");
356 }
357 }
358
359 #[test]
360 fn build_basic_auth_should_always_work() {
361 let basic_auth = HttpServerAuthConfig::Basic {
362 username: random_string(16),
363 password: random_string(16).into(),
364 };
365
366 let matcher = basic_auth.build(&Default::default());
367
368 assert!(matcher.is_ok());
369 assert!(matches!(
370 matcher.unwrap(),
371 HttpServerAuthMatcher::AuthHeader { .. }
372 ));
373 }
374
375 #[test]
376 fn build_basic_auth_should_use_username_password_related_message() {
377 let basic_auth = HttpServerAuthConfig::Basic {
378 username: random_string(16),
379 password: random_string(16).into(),
380 };
381
382 let (_, error_message) = basic_auth.build(&Default::default()).unwrap().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.build(&Default::default()).unwrap().auth_header();
396 assert_eq!(
397 Authorization::basic(&username, &password).0.encode(),
398 header
399 );
400 }
401
402 #[test]
403 fn build_custom_should_fail_on_invalid_source() {
404 let custom_auth = HttpServerAuthConfig::Custom {
405 source: "invalid VRL source".to_string(),
406 };
407
408 assert!(custom_auth.build(&Default::default()).is_err());
409 }
410
411 #[test]
412 fn build_custom_should_fail_on_non_boolean_return_type() {
413 let custom_auth = HttpServerAuthConfig::Custom {
414 source: indoc! {r#"
415 .success = true
416 .
417 "#}
418 .to_string(),
419 };
420
421 assert!(custom_auth.build(&Default::default()).is_err());
422 }
423
424 #[test]
425 fn build_custom_should_success_on_proper_source_with_boolean_return_type() {
426 let custom_auth = HttpServerAuthConfig::Custom {
427 source: indoc! {r#"
428 .headers.authorization == "Basic test"
429 "#}
430 .to_string(),
431 };
432
433 assert!(custom_auth.build(&Default::default()).is_ok());
434 }
435
436 #[test]
437 fn basic_auth_matcher_should_return_401_when_missing_auth_header() {
438 let basic_auth = HttpServerAuthConfig::Basic {
439 username: random_string(16),
440 password: random_string(16).into(),
441 };
442
443 let matcher = basic_auth.build(&Default::default()).unwrap();
444
445 let result = matcher.handle_auth(Some(&next_addr()), &HeaderMap::new(), "/");
446
447 assert!(result.is_err());
448 let error = result.unwrap_err();
449 assert_eq!(401, error.code());
450 assert_eq!("No authorization header", error.message());
451 }
452
453 #[test]
454 fn basic_auth_matcher_should_return_401_and_with_wrong_credentials() {
455 let basic_auth = HttpServerAuthConfig::Basic {
456 username: random_string(16),
457 password: random_string(16).into(),
458 };
459
460 let matcher = basic_auth.build(&Default::default()).unwrap();
461
462 let mut headers = HeaderMap::new();
463 headers.insert(AUTHORIZATION, HeaderValue::from_static("Basic wrong"));
464 let result = matcher.handle_auth(Some(&next_addr()), &headers, "/");
465
466 assert!(result.is_err());
467 let error = result.unwrap_err();
468 assert_eq!(401, error.code());
469 assert_eq!("Invalid username/password", error.message());
470 }
471
472 #[test]
473 fn basic_auth_matcher_should_return_ok_for_correct_credentials() {
474 let username = random_string(16);
475 let password = random_string(16);
476 let basic_auth = HttpServerAuthConfig::Basic {
477 username: username.clone(),
478 password: password.clone().into(),
479 };
480
481 let matcher = basic_auth.build(&Default::default()).unwrap();
482
483 let mut headers = HeaderMap::new();
484 headers.insert(
485 AUTHORIZATION,
486 Authorization::basic(&username, &password).0.encode(),
487 );
488 let result = matcher.handle_auth(Some(&next_addr()), &headers, "/");
489
490 assert!(result.is_ok());
491 }
492
493 #[test]
494 fn custom_auth_matcher_should_return_ok_for_true_vrl_script_result() {
495 let custom_auth = HttpServerAuthConfig::Custom {
496 source: r#".headers.authorization == "test""#.to_string(),
497 };
498
499 let matcher = custom_auth.build(&Default::default()).unwrap();
500
501 let mut headers = HeaderMap::new();
502 headers.insert(AUTHORIZATION, HeaderValue::from_static("test"));
503 let result = matcher.handle_auth(Some(&next_addr()), &headers, "/");
504
505 assert!(result.is_ok());
506 }
507
508 #[test]
509 fn custom_auth_matcher_should_be_able_to_check_address() {
510 let addr = next_addr();
511 let addr_string = addr.ip().to_string();
512 let custom_auth = HttpServerAuthConfig::Custom {
513 source: format!(".address == \"{addr_string}\""),
514 };
515
516 let matcher = custom_auth.build(&Default::default()).unwrap();
517
518 let headers = HeaderMap::new();
519 let result = matcher.handle_auth(Some(&next_addr()), &headers, "/");
520
521 assert!(result.is_ok());
522 }
523
524 #[test]
525 fn custom_auth_matcher_should_work_with_missing_address_too() {
526 let addr = next_addr();
527 let addr_string = addr.ip().to_string();
528 let custom_auth = HttpServerAuthConfig::Custom {
529 source: format!(".address == \"{addr_string}\""),
530 };
531
532 let matcher = custom_auth.build(&Default::default()).unwrap();
533
534 let headers = HeaderMap::new();
535 let result = matcher.handle_auth(None, &headers, "/");
536
537 assert!(result.is_err());
538 }
539
540 #[test]
541 fn custom_auth_matcher_should_be_able_to_check_path() {
542 let custom_auth = HttpServerAuthConfig::Custom {
543 source: r#".path == "/ok""#.to_string(),
544 };
545
546 let matcher = custom_auth.build(&Default::default()).unwrap();
547
548 let headers = HeaderMap::new();
549 let result = matcher.handle_auth(Some(&next_addr()), &headers, "/ok");
550
551 assert!(result.is_ok());
552 }
553
554 #[test]
555 fn custom_auth_matcher_should_return_401_with_wrong_path() {
556 let custom_auth = HttpServerAuthConfig::Custom {
557 source: r#".path == "/ok""#.to_string(),
558 };
559
560 let matcher = custom_auth.build(&Default::default()).unwrap();
561
562 let headers = HeaderMap::new();
563 let result = matcher.handle_auth(Some(&next_addr()), &headers, "/bad");
564
565 assert!(result.is_err());
566 }
567
568 #[test]
569 fn custom_auth_matcher_should_return_401_for_false_vrl_script_result() {
570 let custom_auth = HttpServerAuthConfig::Custom {
571 source: r#".headers.authorization == "test""#.to_string(),
572 };
573
574 let matcher = custom_auth.build(&Default::default()).unwrap();
575
576 let mut headers = HeaderMap::new();
577 headers.insert(AUTHORIZATION, HeaderValue::from_static("wrong value"));
578 let result = matcher.handle_auth(Some(&next_addr()), &headers, "/");
579
580 assert!(result.is_err());
581 let error = result.unwrap_err();
582 assert_eq!(401, error.code());
583 assert_eq!("Auth failed", error.message());
584 }
585
586 #[test]
587 fn custom_auth_matcher_should_return_401_for_failed_script_execution() {
588 let custom_auth = HttpServerAuthConfig::Custom {
589 source: "abort".to_string(),
590 };
591
592 let matcher = custom_auth.build(&Default::default()).unwrap();
593
594 let mut headers = HeaderMap::new();
595 headers.insert(AUTHORIZATION, HeaderValue::from_static("test"));
596 let result = matcher.handle_auth(Some(&next_addr()), &headers, "/");
597
598 assert!(result.is_err());
599 let error = result.unwrap_err();
600 assert_eq!(401, error.code());
601 assert_eq!("Auth failed", error.message());
602 }
603}