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    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/// Configuration of the authentication strategy for server mode sinks and sources.
30///
31/// Use the HTTP authentication with HTTPS only. The authentication credentials are passed as an
32/// HTTP header without any additional encryption beyond what is provided by the transport itself.
33#[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 authentication.
39    ///
40    /// The username and password are concatenated and encoded using [base64][base64].
41    ///
42    /// [base64]: https://en.wikipedia.org/wiki/Base64
43    Basic {
44        /// The basic authentication username.
45        #[configurable(metadata(docs::examples = "${USERNAME}"))]
46        #[configurable(metadata(docs::examples = "username"))]
47        username: String,
48
49        /// The basic authentication password.
50        #[configurable(metadata(docs::examples = "${PASSWORD}"))]
51        #[configurable(metadata(docs::examples = "password"))]
52        password: SensitiveString,
53    },
54
55    /// Custom authentication using VRL code.
56    ///
57    /// Takes in request and validates it using VRL code.
58    Custom {
59        /// The VRL boolean expression.
60        source: String,
61    },
62}
63
64// Custom deserializer implementation to default `strategy` to `basic`
65impl<'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                // Default to "basic" if strategy is missing
99                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    /// Builds an auth matcher based on provided configuration.
134    /// Used to validate configuration if needed, before passing it to the
135    /// actual component for usage.
136    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/// Built auth matcher with validated configuration
179/// Can be used directly in a component to validate authentication in HTTP requests
180#[allow(clippy::large_enum_variant)]
181#[derive(Clone, Debug)]
182pub enum HttpServerAuthMatcher {
183    /// Matcher for comparing exact value of Authorization header
184    AuthHeader(HeaderValue, &'static str),
185    /// Matcher for running VRL script for requests, to allow for custom validation
186    Vrl {
187        /// Compiled VRL script
188        program: Program,
189    },
190}
191
192impl HttpServerAuthMatcher {
193    /// Compares passed headers to the matcher
194    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}