Skip to content

Commit e5e2e15

Browse files
authored
feat(http_server source): allow custom auth to enrich events via metadata writes (#25391)
* feat(http_server source): make custom auth type writable in metadata * fix(http_server source): warn when auth enrichment is dropped by unsupported sources Sources that implement HttpSource but don't override inject_auth_enrichment (heroku_logs, prometheus pushgateway/remote_write) would silently drop any %field metadata written in a custom auth VRL program. Emit a warning instead so users get actionable feedback rather than silent data loss. * update docs * protect reserved fields * allow metadata enrichment for metrics and traces * dont use hard-coded list of reserved fields * fix docs
1 parent ee870f9 commit e5e2e15

10 files changed

Lines changed: 379 additions & 35 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
The `custom` auth strategy for the `http_server` source now supports event enrichment via metadata
2+
writes. VRL programs can write `%field = value` during authentication; those values are injected
3+
into every successfully authenticated event. The event body (`.field`) remains read-only. Existing
4+
`custom` programs that do not write metadata are unaffected.
5+
6+
authors: 20agbekodo

src/common/http/server_auth.rs

Lines changed: 98 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use vector_config::configurable_component;
1212
use vector_lib::{
1313
TimeZone, compile_vrl,
1414
event::{Event, LogEvent, VrlTarget},
15+
lookup::OwnedTargetPath,
1516
sensitive_string::SensitiveString,
1617
};
1718
use 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 {
182185
pub 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

192197
impl 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

Comments
 (0)