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 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/// Configuration of the authentication strategy for server mode sinks and sources.
29///
30/// Use the HTTP authentication with HTTPS only. The authentication credentials are passed as an
31/// HTTP header without any additional encryption beyond what is provided by the transport itself.
32#[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 authentication.
38    ///
39    /// The username and password are concatenated and encoded using [base64][base64].
40    ///
41    /// [base64]: https://en.wikipedia.org/wiki/Base64
42    Basic {
43        /// The basic authentication username.
44        #[configurable(metadata(docs::examples = "${USERNAME}"))]
45        #[configurable(metadata(docs::examples = "username"))]
46        username: String,
47
48        /// The basic authentication password.
49        #[configurable(metadata(docs::examples = "${PASSWORD}"))]
50        #[configurable(metadata(docs::examples = "password"))]
51        password: SensitiveString,
52    },
53
54    /// Custom authentication using VRL code.
55    ///
56    /// Takes in request and validates it using VRL code.
57    Custom {
58        /// The VRL boolean expression.
59        source: String,
60    },
61}
62
63// Custom deserializer implementation to default `strategy` to `basic`
64impl<'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                // Default to "basic" if strategy is missing
98                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    /// Builds an auth matcher based on provided configuration.
133    /// Used to validate configuration if needed, before passing it to the
134    /// actual component for usage.
135    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/// 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    Vrl {
190        /// Compiled VRL script
191        program: Program,
192    },
193}
194
195impl HttpServerAuthMatcher {
196    /// Compares passed headers to the matcher
197    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}