@@ -12,6 +12,7 @@ use vector_config::configurable_component;
1212use vector_lib:: {
1313 TimeZone , compile_vrl,
1414 event:: { Event , LogEvent , VrlTarget } ,
15+ lookup:: OwnedTargetPath ,
1516 sensitive_string:: SensitiveString ,
1617} ;
1718use vector_vrl_metrics:: MetricsStorage ;
@@ -54,7 +55,7 @@ pub enum HttpServerAuthConfig {
5455
5556 /// Custom authentication using VRL code.
5657 ///
57- /// Takes in request and validates it using VRL code.
58+ /// Takes in request and validates it using VRL code. The VRL program must return a boolean.
5859 Custom {
5960 /// The VRL boolean expression.
6061 source : String ,
@@ -151,7 +152,9 @@ impl HttpServerAuthConfig {
151152 let mut config = CompileConfig :: default ( ) ;
152153 config. set_custom ( enrichment_tables. clone ( ) ) ;
153154 config. set_custom ( metrics_storage. clone ( ) ) ;
154- config. set_read_only ( ) ;
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 ) ;
155158
156159 let CompilationResult {
157160 program,
@@ -182,26 +185,29 @@ impl HttpServerAuthConfig {
182185pub enum HttpServerAuthMatcher {
183186 /// Matcher for comparing exact value of Authorization header
184187 AuthHeader ( HeaderValue , & ' static str ) ,
185- /// Matcher for running VRL script for requests, to allow for custom validation
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.
186191 Vrl {
187192 /// Compiled VRL script
188193 program : Program ,
189194 } ,
190195}
191196
192197impl HttpServerAuthMatcher {
193- /// Compares passed headers to the matcher
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.
194200 pub fn handle_auth (
195201 & self ,
196202 address : Option < & SocketAddr > ,
197203 headers : & HeaderMap < HeaderValue > ,
198204 path : & str ,
199- ) -> Result < ( ) , ErrorMessage > {
205+ ) -> Result < Option < ObjectMap > , ErrorMessage > {
200206 match self {
201207 HttpServerAuthMatcher :: AuthHeader ( expected, err_message) => {
202208 if let Some ( header) = headers. get ( AUTHORIZATION ) {
203209 if expected == header {
204- Ok ( ( ) )
210+ Ok ( None )
205211 } else {
206212 Err ( ErrorMessage :: new (
207213 StatusCode :: UNAUTHORIZED ,
@@ -227,7 +233,7 @@ impl HttpServerAuthMatcher {
227233 headers : & HeaderMap < HeaderValue > ,
228234 path : & str ,
229235 program : & Program ,
230- ) -> Result < ( ) , ErrorMessage > {
236+ ) -> Result < Option < ObjectMap > , ErrorMessage > {
231237 let mut target = VrlTarget :: new (
232238 Event :: Log ( LogEvent :: from_map (
233239 ObjectMap :: from ( [
@@ -263,16 +269,22 @@ impl HttpServerAuthMatcher {
263269 warn ! ( "Handling auth failed: {}" , e) ;
264270 ErrorMessage :: new ( StatusCode :: UNAUTHORIZED , "Auth failed" . to_owned ( ) )
265271 } ) ? {
266- vrl:: core:: Value :: Boolean ( result) => {
267- if result {
268- Ok ( ( ) )
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 ( )
269279 } else {
270- Err ( ErrorMessage :: new (
271- StatusCode :: UNAUTHORIZED ,
272- "Auth failed" . to_owned ( ) ,
273- ) )
274- }
280+ None
281+ } ;
282+ Ok ( enrichment)
275283 }
284+ vrl:: core:: Value :: Boolean ( false ) => Err ( ErrorMessage :: new (
285+ StatusCode :: UNAUTHORIZED ,
286+ "Auth failed" . to_owned ( ) ,
287+ ) ) ,
276288 _ => Err ( ErrorMessage :: new (
277289 StatusCode :: UNAUTHORIZED ,
278290 "Invalid return value" . to_owned ( ) ,
@@ -643,4 +655,75 @@ mod tests {
643655 assert_eq ! ( 401 , error. code( ) ) ;
644656 assert_eq ! ( "Auth failed" , error. message( ) ) ;
645657 }
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+ }
646729}
0 commit comments