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