vector/common/http/
server_auth.rs

1//! Shared authentication config between components that use HTTP.
2use 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    lookup::OwnedTargetPath,
16    sensitive_string::SensitiveString,
17};
18use vector_vrl_metrics::MetricsStorage;
19use vrl::{
20    compiler::{CompilationResult, CompileConfig, Program, runtime::Runtime},
21    core::Value,
22    prelude::TypeState,
23    value::{KeyString, ObjectMap},
24};
25
26use crate::format_vrl_diagnostics;
27
28use super::ErrorMessage;
29
30/// Configuration of the authentication strategy for server mode sinks and sources.
31///
32/// Use the HTTP authentication with HTTPS only. The authentication credentials are passed as an
33/// HTTP header without any additional encryption beyond what is provided by the transport itself.
34#[configurable_component(no_deser)]
35#[derive(Clone, Debug, Eq, PartialEq)]
36#[configurable(metadata(docs::enum_tag_description = "The authentication strategy to use."))]
37#[serde(tag = "strategy", rename_all = "snake_case")]
38pub enum HttpServerAuthConfig {
39    /// Basic authentication.
40    ///
41    /// The username and password are concatenated and encoded using [base64][base64].
42    ///
43    /// [base64]: https://en.wikipedia.org/wiki/Base64
44    Basic {
45        /// The basic authentication username.
46        #[configurable(metadata(docs::examples = "${USERNAME}"))]
47        #[configurable(metadata(docs::examples = "username"))]
48        username: String,
49
50        /// The basic authentication password.
51        #[configurable(metadata(docs::examples = "${PASSWORD}"))]
52        #[configurable(metadata(docs::examples = "password"))]
53        password: SensitiveString,
54    },
55
56    /// Custom authentication using VRL code.
57    ///
58    /// Takes in request and validates it using VRL code. The VRL program must return a boolean.
59    Custom {
60        /// The VRL boolean expression.
61        source: String,
62    },
63}
64
65// Custom deserializer implementation to default `strategy` to `basic`
66impl<'de> Deserialize<'de> for HttpServerAuthConfig {
67    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
68    where
69        D: serde::Deserializer<'de>,
70    {
71        struct HttpServerAuthConfigVisitor;
72
73        const FIELD_KEYS: [&str; 4] = ["strategy", "username", "password", "source"];
74
75        impl<'de> Visitor<'de> for HttpServerAuthConfigVisitor {
76            type Value = HttpServerAuthConfig;
77
78            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
79                formatter.write_str("a valid authentication strategy (basic or custom)")
80            }
81
82            fn visit_map<A>(self, mut map: A) -> Result<HttpServerAuthConfig, A::Error>
83            where
84                A: MapAccess<'de>,
85            {
86                let mut fields: HashMap<&str, String> = HashMap::default();
87
88                while let Some(key) = map.next_key::<String>()? {
89                    if let Some(field_index) = FIELD_KEYS.iter().position(|k| *k == key.as_str()) {
90                        if fields.contains_key(FIELD_KEYS[field_index]) {
91                            return Err(Error::duplicate_field(FIELD_KEYS[field_index]));
92                        }
93                        fields.insert(FIELD_KEYS[field_index], map.next_value()?);
94                    } else {
95                        return Err(Error::unknown_field(&key, &FIELD_KEYS));
96                    }
97                }
98
99                // Default to "basic" if strategy is missing
100                let strategy = fields
101                    .get("strategy")
102                    .map(String::as_str)
103                    .unwrap_or_else(|| "basic");
104
105                match strategy {
106                    "basic" => {
107                        let username = fields
108                            .remove("username")
109                            .ok_or_else(|| Error::missing_field("username"))?;
110                        let password = fields
111                            .remove("password")
112                            .ok_or_else(|| Error::missing_field("password"))?;
113                        Ok(HttpServerAuthConfig::Basic {
114                            username,
115                            password: SensitiveString::from(password),
116                        })
117                    }
118                    "custom" => {
119                        let source = fields
120                            .remove("source")
121                            .ok_or_else(|| Error::missing_field("source"))?;
122                        Ok(HttpServerAuthConfig::Custom { source })
123                    }
124                    _ => Err(Error::unknown_variant(strategy, &["basic", "custom"])),
125                }
126            }
127        }
128
129        deserializer.deserialize_map(HttpServerAuthConfigVisitor)
130    }
131}
132
133impl HttpServerAuthConfig {
134    /// Builds an auth matcher based on provided configuration.
135    /// Used to validate configuration if needed, before passing it to the
136    /// actual component for usage.
137    pub fn build(
138        &self,
139        enrichment_tables: &vector_lib::enrichment::TableRegistry,
140        metrics_storage: &MetricsStorage,
141    ) -> crate::Result<HttpServerAuthMatcher> {
142        match self {
143            HttpServerAuthConfig::Basic { username, password } => {
144                Ok(HttpServerAuthMatcher::AuthHeader(
145                    Authorization::basic(username, password.inner()).0.encode(),
146                    "Invalid username/password",
147                ))
148            }
149            HttpServerAuthConfig::Custom { source } => {
150                let state = TypeState::default();
151
152                let mut config = CompileConfig::default();
153                config.set_custom(enrichment_tables.clone());
154                config.set_custom(metrics_storage.clone());
155                // Lock the event body (.field) as read-only, but leave metadata (%field) writable
156                // so the VRL program can enrich authenticated events via %field = value.
157                config.set_read_only_path(OwnedTargetPath::event_root(), true);
158
159                let CompilationResult {
160                    program,
161                    warnings,
162                    config: _,
163                } = compile_vrl(source, &vector_vrl_functions::all(), &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/// Built auth matcher with validated configuration
182/// Can be used directly in a component to validate authentication in HTTP requests
183#[allow(clippy::large_enum_variant)]
184#[derive(Clone, Debug)]
185pub enum HttpServerAuthMatcher {
186    /// Matcher for comparing exact value of Authorization header
187    AuthHeader(HeaderValue, &'static str),
188    /// Matcher for running VRL script for requests, to allow for custom validation.
189    /// Metadata (`%field`) writes in the program are extracted and returned to the caller
190    /// for injection into authenticated events.
191    Vrl {
192        /// Compiled VRL script
193        program: Program,
194    },
195}
196
197impl HttpServerAuthMatcher {
198    /// Validates the request. Returns `Ok(Some(enrichment))` when auth passes and the VRL program
199    /// wrote `%field` values; returns `Ok(None)` when auth passes with no metadata enrichment.
200    pub fn handle_auth(
201        &self,
202        address: Option<&SocketAddr>,
203        headers: &HeaderMap<HeaderValue>,
204        path: &str,
205    ) -> Result<Option<ObjectMap>, ErrorMessage> {
206        match self {
207            HttpServerAuthMatcher::AuthHeader(expected, err_message) => {
208                if let Some(header) = headers.get(AUTHORIZATION) {
209                    if expected == header {
210                        Ok(None)
211                    } else {
212                        Err(ErrorMessage::new(
213                            StatusCode::UNAUTHORIZED,
214                            err_message.to_string(),
215                        ))
216                    }
217                } else {
218                    Err(ErrorMessage::new(
219                        StatusCode::UNAUTHORIZED,
220                        "No authorization header".to_owned(),
221                    ))
222                }
223            }
224            HttpServerAuthMatcher::Vrl { program } => {
225                self.handle_vrl_auth(address, headers, path, program)
226            }
227        }
228    }
229
230    fn handle_vrl_auth(
231        &self,
232        address: Option<&SocketAddr>,
233        headers: &HeaderMap<HeaderValue>,
234        path: &str,
235        program: &Program,
236    ) -> Result<Option<ObjectMap>, ErrorMessage> {
237        let mut target = VrlTarget::new(
238            Event::Log(LogEvent::from_map(
239                ObjectMap::from([
240                    (
241                        "headers".into(),
242                        Value::Object(
243                            headers
244                                .iter()
245                                .map(|(k, v)| {
246                                    (
247                                        KeyString::from(k.to_string()),
248                                        Value::Bytes(Bytes::copy_from_slice(v.as_bytes())),
249                                    )
250                                })
251                                .collect::<ObjectMap>(),
252                        ),
253                    ),
254                    (
255                        "address".into(),
256                        address.map_or(Value::Null, |a| Value::from(a.ip().to_string())),
257                    ),
258                    ("path".into(), Value::from(path.to_owned())),
259                ]),
260                Default::default(),
261            )),
262            program.info(),
263            false,
264        );
265        let timezone = TimeZone::default();
266
267        let result = Runtime::default().resolve(&mut target, program, &timezone);
268        match result.map_err(|e| {
269            warn!("Handling auth failed: {}", e);
270            ErrorMessage::new(StatusCode::UNAUTHORIZED, "Auth failed".to_owned())
271        })? {
272            vrl::core::Value::Boolean(true) => {
273                let enrichment = if let VrlTarget::LogEvent(_, metadata) = &target {
274                    metadata
275                        .value()
276                        .as_object()
277                        .filter(|m| !m.is_empty())
278                        .cloned()
279                } else {
280                    None
281                };
282                Ok(enrichment)
283            }
284            vrl::core::Value::Boolean(false) => Err(ErrorMessage::new(
285                StatusCode::UNAUTHORIZED,
286                "Auth failed".to_owned(),
287            )),
288            _ => Err(ErrorMessage::new(
289                StatusCode::UNAUTHORIZED,
290                "Invalid return value".to_owned(),
291            )),
292        }
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use indoc::indoc;
299
300    use super::*;
301    use crate::test_util::{addr::next_addr, random_string};
302
303    impl HttpServerAuthMatcher {
304        fn auth_header(self) -> (HeaderValue, &'static str) {
305            match self {
306                HttpServerAuthMatcher::AuthHeader(header_value, error_message) => {
307                    (header_value, error_message)
308                }
309                HttpServerAuthMatcher::Vrl { .. } => {
310                    panic!("Expected HttpServerAuthMatcher::AuthHeader")
311                }
312            }
313        }
314    }
315
316    #[test]
317    fn config_should_default_to_basic() {
318        let config: HttpServerAuthConfig = serde_yaml::from_str(indoc! { r#"
319            username: foo
320            password: bar
321            "#
322        })
323        .unwrap();
324
325        if let HttpServerAuthConfig::Basic { username, password } = config {
326            assert_eq!(username, "foo");
327            assert_eq!(password.inner(), "bar");
328        } else {
329            panic!("Expected HttpServerAuthConfig::Basic");
330        }
331    }
332
333    #[test]
334    fn config_should_support_explicit_basic_strategy() {
335        let config: HttpServerAuthConfig = serde_yaml::from_str(indoc! { r#"
336            strategy: basic
337            username: foo
338            password: bar
339            "#
340        })
341        .unwrap();
342
343        if let HttpServerAuthConfig::Basic { username, password } = config {
344            assert_eq!(username, "foo");
345            assert_eq!(password.inner(), "bar");
346        } else {
347            panic!("Expected HttpServerAuthConfig::Basic");
348        }
349    }
350
351    #[test]
352    fn config_should_support_custom_strategy() {
353        let config: HttpServerAuthConfig = serde_yaml::from_str(indoc! { r#"
354            strategy: custom
355            source: "true"
356            "#
357        })
358        .unwrap();
359
360        assert!(matches!(config, HttpServerAuthConfig::Custom { .. }));
361        if let HttpServerAuthConfig::Custom { source } = config {
362            assert_eq!(source, "true");
363        } else {
364            panic!("Expected HttpServerAuthConfig::Custom");
365        }
366    }
367
368    #[test]
369    fn build_basic_auth_should_always_work() {
370        let basic_auth = HttpServerAuthConfig::Basic {
371            username: random_string(16),
372            password: random_string(16).into(),
373        };
374
375        let matcher = basic_auth.build(&Default::default(), &Default::default());
376
377        assert!(matcher.is_ok());
378        assert!(matches!(
379            matcher.unwrap(),
380            HttpServerAuthMatcher::AuthHeader { .. }
381        ));
382    }
383
384    #[test]
385    fn build_basic_auth_should_use_username_password_related_message() {
386        let basic_auth = HttpServerAuthConfig::Basic {
387            username: random_string(16),
388            password: random_string(16).into(),
389        };
390
391        let (_, error_message) = basic_auth
392            .build(&Default::default(), &Default::default())
393            .unwrap()
394            .auth_header();
395        assert_eq!("Invalid username/password", error_message);
396    }
397
398    #[test]
399    fn build_basic_auth_should_use_encode_basic_header() {
400        let username = random_string(16);
401        let password = random_string(16);
402        let basic_auth = HttpServerAuthConfig::Basic {
403            username: username.clone(),
404            password: password.clone().into(),
405        };
406
407        let (header, _) = basic_auth
408            .build(&Default::default(), &Default::default())
409            .unwrap()
410            .auth_header();
411        assert_eq!(
412            Authorization::basic(&username, &password).0.encode(),
413            header
414        );
415    }
416
417    #[test]
418    fn build_custom_should_fail_on_invalid_source() {
419        let custom_auth = HttpServerAuthConfig::Custom {
420            source: "invalid VRL source".to_string(),
421        };
422
423        assert!(
424            custom_auth
425                .build(&Default::default(), &Default::default())
426                .is_err()
427        );
428    }
429
430    #[test]
431    fn build_custom_should_fail_on_non_boolean_return_type() {
432        let custom_auth = HttpServerAuthConfig::Custom {
433            source: indoc! {r#"
434                .success = true
435                .
436                "#}
437            .to_string(),
438        };
439
440        assert!(
441            custom_auth
442                .build(&Default::default(), &Default::default())
443                .is_err()
444        );
445    }
446
447    #[test]
448    fn build_custom_should_success_on_proper_source_with_boolean_return_type() {
449        let custom_auth = HttpServerAuthConfig::Custom {
450            source: indoc! {r#"
451                .headers.authorization == "Basic test"
452                "#}
453            .to_string(),
454        };
455
456        assert!(
457            custom_auth
458                .build(&Default::default(), &Default::default())
459                .is_ok()
460        );
461    }
462
463    #[test]
464    fn basic_auth_matcher_should_return_401_when_missing_auth_header() {
465        let basic_auth = HttpServerAuthConfig::Basic {
466            username: random_string(16),
467            password: random_string(16).into(),
468        };
469
470        let matcher = basic_auth
471            .build(&Default::default(), &Default::default())
472            .unwrap();
473
474        let (_guard, addr) = next_addr();
475        let result = matcher.handle_auth(Some(&addr), &HeaderMap::new(), "/");
476
477        assert!(result.is_err());
478        let error = result.unwrap_err();
479        assert_eq!(401, error.code());
480        assert_eq!("No authorization header", error.message());
481    }
482
483    #[test]
484    fn basic_auth_matcher_should_return_401_and_with_wrong_credentials() {
485        let basic_auth = HttpServerAuthConfig::Basic {
486            username: random_string(16),
487            password: random_string(16).into(),
488        };
489
490        let matcher = basic_auth
491            .build(&Default::default(), &Default::default())
492            .unwrap();
493
494        let mut headers = HeaderMap::new();
495        headers.insert(AUTHORIZATION, HeaderValue::from_static("Basic wrong"));
496        let (_guard, addr) = next_addr();
497        let result = matcher.handle_auth(Some(&addr), &headers, "/");
498
499        assert!(result.is_err());
500        let error = result.unwrap_err();
501        assert_eq!(401, error.code());
502        assert_eq!("Invalid username/password", error.message());
503    }
504
505    #[test]
506    fn basic_auth_matcher_should_return_ok_for_correct_credentials() {
507        let username = random_string(16);
508        let password = random_string(16);
509        let basic_auth = HttpServerAuthConfig::Basic {
510            username: username.clone(),
511            password: password.clone().into(),
512        };
513
514        let matcher = basic_auth
515            .build(&Default::default(), &Default::default())
516            .unwrap();
517
518        let mut headers = HeaderMap::new();
519        headers.insert(
520            AUTHORIZATION,
521            Authorization::basic(&username, &password).0.encode(),
522        );
523        let (_guard, addr) = next_addr();
524        let result = matcher.handle_auth(Some(&addr), &headers, "/");
525
526        assert!(result.is_ok());
527    }
528
529    #[test]
530    fn custom_auth_matcher_should_return_ok_for_true_vrl_script_result() {
531        let custom_auth = HttpServerAuthConfig::Custom {
532            source: r#".headers.authorization == "test""#.to_string(),
533        };
534
535        let matcher = custom_auth
536            .build(&Default::default(), &Default::default())
537            .unwrap();
538
539        let mut headers = HeaderMap::new();
540        headers.insert(AUTHORIZATION, HeaderValue::from_static("test"));
541        let (_guard, addr) = next_addr();
542        let result = matcher.handle_auth(Some(&addr), &headers, "/");
543
544        assert!(result.is_ok());
545    }
546
547    #[test]
548    fn custom_auth_matcher_should_be_able_to_check_address() {
549        let (_guard, addr) = next_addr();
550        let addr_string = addr.ip().to_string();
551        let custom_auth = HttpServerAuthConfig::Custom {
552            source: format!(".address == \"{addr_string}\""),
553        };
554
555        let matcher = custom_auth
556            .build(&Default::default(), &Default::default())
557            .unwrap();
558
559        let headers = HeaderMap::new();
560        let result = matcher.handle_auth(Some(&addr), &headers, "/");
561
562        assert!(result.is_ok());
563    }
564
565    #[test]
566    fn custom_auth_matcher_should_work_with_missing_address_too() {
567        let (_guard, addr) = next_addr();
568        let addr_string = addr.ip().to_string();
569        let custom_auth = HttpServerAuthConfig::Custom {
570            source: format!(".address == \"{addr_string}\""),
571        };
572
573        let matcher = custom_auth
574            .build(&Default::default(), &Default::default())
575            .unwrap();
576
577        let headers = HeaderMap::new();
578        let result = matcher.handle_auth(None, &headers, "/");
579
580        assert!(result.is_err());
581    }
582
583    #[test]
584    fn custom_auth_matcher_should_be_able_to_check_path() {
585        let custom_auth = HttpServerAuthConfig::Custom {
586            source: r#".path == "/ok""#.to_string(),
587        };
588
589        let matcher = custom_auth
590            .build(&Default::default(), &Default::default())
591            .unwrap();
592
593        let headers = HeaderMap::new();
594        let (_guard, addr) = next_addr();
595        let result = matcher.handle_auth(Some(&addr), &headers, "/ok");
596
597        assert!(result.is_ok());
598    }
599
600    #[test]
601    fn custom_auth_matcher_should_return_401_with_wrong_path() {
602        let custom_auth = HttpServerAuthConfig::Custom {
603            source: r#".path == "/ok""#.to_string(),
604        };
605
606        let matcher = custom_auth
607            .build(&Default::default(), &Default::default())
608            .unwrap();
609
610        let headers = HeaderMap::new();
611        let (_guard, addr) = next_addr();
612        let result = matcher.handle_auth(Some(&addr), &headers, "/bad");
613
614        assert!(result.is_err());
615    }
616
617    #[test]
618    fn custom_auth_matcher_should_return_401_for_false_vrl_script_result() {
619        let custom_auth = HttpServerAuthConfig::Custom {
620            source: r#".headers.authorization == "test""#.to_string(),
621        };
622
623        let matcher = custom_auth
624            .build(&Default::default(), &Default::default())
625            .unwrap();
626
627        let mut headers = HeaderMap::new();
628        headers.insert(AUTHORIZATION, HeaderValue::from_static("wrong value"));
629        let (_guard, addr) = next_addr();
630        let result = matcher.handle_auth(Some(&addr), &headers, "/");
631
632        assert!(result.is_err());
633        let error = result.unwrap_err();
634        assert_eq!(401, error.code());
635        assert_eq!("Auth failed", error.message());
636    }
637
638    #[test]
639    fn custom_auth_matcher_should_return_401_for_failed_script_execution() {
640        let custom_auth = HttpServerAuthConfig::Custom {
641            source: "abort".to_string(),
642        };
643
644        let matcher = custom_auth
645            .build(&Default::default(), &Default::default())
646            .unwrap();
647
648        let mut headers = HeaderMap::new();
649        headers.insert(AUTHORIZATION, HeaderValue::from_static("test"));
650        let (_guard, addr) = next_addr();
651        let result = matcher.handle_auth(Some(&addr), &headers, "/");
652
653        assert!(result.is_err());
654        let error = result.unwrap_err();
655        assert_eq!(401, error.code());
656        assert_eq!("Auth failed", error.message());
657    }
658
659    // Backward-compat: existing `custom` scripts that don't write metadata still work and return
660    // Ok(None) — no enrichment, no change in behavior.
661    #[test]
662    fn custom_auth_matcher_returns_none_enrichment_when_no_metadata_written() {
663        let custom_auth = HttpServerAuthConfig::Custom {
664            source: r#".headers.authorization == "Bearer token""#.to_string(),
665        };
666
667        let matcher = custom_auth
668            .build(&Default::default(), &Default::default())
669            .unwrap();
670
671        let mut headers = HeaderMap::new();
672        headers.insert(AUTHORIZATION, HeaderValue::from_static("Bearer token"));
673        let (_guard, addr) = next_addr();
674        let result = matcher.handle_auth(Some(&addr), &headers, "/");
675
676        assert!(result.is_ok());
677        assert_eq!(
678            None,
679            result.unwrap(),
680            "no metadata written => no enrichment"
681        );
682    }
683
684    // Existing `custom` scripts that write metadata via `%field = value` now enrich events.
685    #[test]
686    fn custom_auth_matcher_returns_enrichment_when_metadata_written() {
687        let custom_auth = HttpServerAuthConfig::Custom {
688            source: indoc! {r#"
689                %tenant_id = "acme"
690                true
691                "#}
692            .to_string(),
693        };
694
695        let matcher = custom_auth
696            .build(&Default::default(), &Default::default())
697            .unwrap();
698
699        let headers = HeaderMap::new();
700        let (_guard, addr) = next_addr();
701        let result = matcher.handle_auth(Some(&addr), &headers, "/");
702
703        assert!(result.is_ok());
704        let enrichment = result.unwrap().expect("expected enrichment map");
705        assert_eq!(
706            enrichment.get("tenant_id").cloned(),
707            Some(vrl::core::Value::from("acme")),
708        );
709    }
710
711    // Existing `custom` scripts still cannot mutate event body fields.
712    #[test]
713    fn custom_auth_build_fails_when_event_body_write_attempted() {
714        let custom_auth = HttpServerAuthConfig::Custom {
715            source: indoc! {r#"
716                .new_field = "value"
717                true
718                "#}
719            .to_string(),
720        };
721
722        assert!(
723            custom_auth
724                .build(&Default::default(), &Default::default())
725                .is_err(),
726            "writing to event body (.field) must be rejected at compile time"
727        );
728    }
729}