Skip to content

Commit 9c2bde0

Browse files
committed
feat(http_server source): make custom auth type writable in metadata
1 parent 8d83e17 commit 9c2bde0

4 files changed

Lines changed: 143 additions & 21 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: 101 additions & 16 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,9 +55,11 @@ 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.
59+
/// Metadata fields written via `%field = value` in the VRL program are extracted and injected
60+
/// into every authenticated event.
5861
Custom {
59-
/// The VRL boolean expression.
62+
/// The VRL boolean expression. May write `%field = value` to enrich events.
6063
source: String,
6164
},
6265
}
@@ -151,7 +154,9 @@ impl HttpServerAuthConfig {
151154
let mut config = CompileConfig::default();
152155
config.set_custom(enrichment_tables.clone());
153156
config.set_custom(metrics_storage.clone());
154-
config.set_read_only();
157+
// Lock the event body (.field) as read-only, but leave metadata (%field) writable
158+
// so the VRL program can enrich authenticated events via %field = value.
159+
config.set_read_only_path(OwnedTargetPath::event_root(), true);
155160

156161
let CompilationResult {
157162
program,
@@ -182,26 +187,29 @@ impl HttpServerAuthConfig {
182187
pub enum HttpServerAuthMatcher {
183188
/// Matcher for comparing exact value of Authorization header
184189
AuthHeader(HeaderValue, &'static str),
185-
/// Matcher for running VRL script for requests, to allow for custom validation
190+
/// Matcher for running VRL script for requests, to allow for custom validation.
191+
/// Metadata (`%field`) writes in the program are extracted and returned to the caller
192+
/// for injection into authenticated events.
186193
Vrl {
187194
/// Compiled VRL script
188195
program: Program,
189196
},
190197
}
191198

192199
impl HttpServerAuthMatcher {
193-
/// Compares passed headers to the matcher
200+
/// Validates the request. Returns `Ok(Some(enrichment))` when auth passes and the VRL program
201+
/// wrote `%field` values; returns `Ok(None)` when auth passes with no metadata enrichment.
194202
pub fn handle_auth(
195203
&self,
196204
address: Option<&SocketAddr>,
197205
headers: &HeaderMap<HeaderValue>,
198206
path: &str,
199-
) -> Result<(), ErrorMessage> {
207+
) -> Result<Option<ObjectMap>, ErrorMessage> {
200208
match self {
201209
HttpServerAuthMatcher::AuthHeader(expected, err_message) => {
202210
if let Some(header) = headers.get(AUTHORIZATION) {
203211
if expected == header {
204-
Ok(())
212+
Ok(None)
205213
} else {
206214
Err(ErrorMessage::new(
207215
StatusCode::UNAUTHORIZED,
@@ -227,7 +235,7 @@ impl HttpServerAuthMatcher {
227235
headers: &HeaderMap<HeaderValue>,
228236
path: &str,
229237
program: &Program,
230-
) -> Result<(), ErrorMessage> {
238+
) -> Result<Option<ObjectMap>, ErrorMessage> {
231239
let mut target = VrlTarget::new(
232240
Event::Log(LogEvent::from_map(
233241
ObjectMap::from([
@@ -263,16 +271,22 @@ impl HttpServerAuthMatcher {
263271
warn!("Handling auth failed: {}", e);
264272
ErrorMessage::new(StatusCode::UNAUTHORIZED, "Auth failed".to_owned())
265273
})? {
266-
vrl::core::Value::Boolean(result) => {
267-
if result {
268-
Ok(())
274+
vrl::core::Value::Boolean(true) => {
275+
let enrichment = if let VrlTarget::LogEvent(_, metadata) = &target {
276+
metadata
277+
.value()
278+
.as_object()
279+
.filter(|m| !m.is_empty())
280+
.cloned()
269281
} else {
270-
Err(ErrorMessage::new(
271-
StatusCode::UNAUTHORIZED,
272-
"Auth failed".to_owned(),
273-
))
274-
}
282+
None
283+
};
284+
Ok(enrichment)
275285
}
286+
vrl::core::Value::Boolean(false) => Err(ErrorMessage::new(
287+
StatusCode::UNAUTHORIZED,
288+
"Auth failed".to_owned(),
289+
)),
276290
_ => Err(ErrorMessage::new(
277291
StatusCode::UNAUTHORIZED,
278292
"Invalid return value".to_owned(),
@@ -643,4 +657,75 @@ mod tests {
643657
assert_eq!(401, error.code());
644658
assert_eq!("Auth failed", error.message());
645659
}
660+
661+
// Backward-compat: existing `custom` scripts that don't write metadata still work and return
662+
// Ok(None) — no enrichment, no change in behavior.
663+
#[test]
664+
fn custom_auth_matcher_returns_none_enrichment_when_no_metadata_written() {
665+
let custom_auth = HttpServerAuthConfig::Custom {
666+
source: r#".headers.authorization == "Bearer token""#.to_string(),
667+
};
668+
669+
let matcher = custom_auth
670+
.build(&Default::default(), &Default::default())
671+
.unwrap();
672+
673+
let mut headers = HeaderMap::new();
674+
headers.insert(AUTHORIZATION, HeaderValue::from_static("Bearer token"));
675+
let (_guard, addr) = next_addr();
676+
let result = matcher.handle_auth(Some(&addr), &headers, "/");
677+
678+
assert!(result.is_ok());
679+
assert_eq!(
680+
None,
681+
result.unwrap(),
682+
"no metadata written => no enrichment"
683+
);
684+
}
685+
686+
// Existing `custom` scripts that write metadata via `%field = value` now enrich events.
687+
#[test]
688+
fn custom_auth_matcher_returns_enrichment_when_metadata_written() {
689+
let custom_auth = HttpServerAuthConfig::Custom {
690+
source: indoc! {r#"
691+
%tenant_id = "acme"
692+
true
693+
"#}
694+
.to_string(),
695+
};
696+
697+
let matcher = custom_auth
698+
.build(&Default::default(), &Default::default())
699+
.unwrap();
700+
701+
let headers = HeaderMap::new();
702+
let (_guard, addr) = next_addr();
703+
let result = matcher.handle_auth(Some(&addr), &headers, "/");
704+
705+
assert!(result.is_ok());
706+
let enrichment = result.unwrap().expect("expected enrichment map");
707+
assert_eq!(
708+
enrichment.get("tenant_id").cloned(),
709+
Some(vrl::core::Value::from("acme")),
710+
);
711+
}
712+
713+
// Existing `custom` scripts still cannot mutate event body fields.
714+
#[test]
715+
fn custom_auth_build_fails_when_event_body_write_attempted() {
716+
let custom_auth = HttpServerAuthConfig::Custom {
717+
source: indoc! {r#"
718+
.new_field = "value"
719+
true
720+
"#}
721+
.to_string(),
722+
};
723+
724+
assert!(
725+
custom_auth
726+
.build(&Default::default(), &Default::default())
727+
.is_err(),
728+
"writing to event body (.field) must be rejected at compile time"
729+
);
730+
}
646731
}

src/sources/http_server.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use vector_lib::{
1616
lookup::{lookup_v2::OptionalValuePath, owned_value_path, path},
1717
schema::Definition,
1818
};
19-
use vrl::value::{Kind, kind::Collection};
19+
use vrl::value::{Kind, ObjectMap, kind::Collection};
2020
use warp::http::HeaderMap;
2121

2222
use crate::{
@@ -521,6 +521,25 @@ impl HttpSource for SimpleHttpSource {
521521
fn enable_source_ip(&self) -> bool {
522522
self.host_key.path.is_some()
523523
}
524+
525+
/// Injects `%field` enrichment from a `custom` auth VRL program into events.
526+
/// Legacy namespace: inserted into the event body (no-op if key already exists).
527+
/// Vector namespace: inserted into event metadata under `http_server.<field>`.
528+
fn inject_auth_enrichment(&self, events: &mut [Event], enrichment: ObjectMap) {
529+
for event in events.iter_mut() {
530+
if let Event::Log(log) = event {
531+
for (key, value) in &enrichment {
532+
self.log_namespace.insert_source_metadata(
533+
SimpleHttpConfig::NAME,
534+
log,
535+
Some(LegacyKey::InsertIfEmpty(path!(key.as_str()))),
536+
path!(key.as_str()),
537+
value.clone(),
538+
);
539+
}
540+
}
541+
}
542+
}
524543
}
525544

526545
#[cfg(test)]

src/sources/util/http/prelude.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use vector_lib::{
1111
config::SourceAcknowledgementsConfig,
1212
event::{BatchNotifier, BatchStatus, BatchStatusReceiver, Event},
1313
};
14+
use vrl::value::ObjectMap;
1415
use warp::{
1516
Filter,
1617
filters::{
@@ -56,6 +57,9 @@ pub trait HttpSource: Clone + Send + Sync + 'static {
5657
path: &str,
5758
) -> Result<Vec<Event>, ErrorMessage>;
5859

60+
/// Called after `enrich_events` when `custom` auth returned metadata enrichment fields.
61+
fn inject_auth_enrichment(&self, _events: &mut [Event], _enrichment: ObjectMap) {}
62+
5963
fn decode(&self, encoding_header: Option<&str>, body: Bytes) -> Result<Bytes, ErrorMessage> {
6064
decompress_body(encoding_header, body)
6165
}
@@ -132,23 +136,27 @@ pub trait HttpSource: Clone + Send + Sync + 'static {
132136
let http_path = path.as_str();
133137
let events = auth_matcher
134138
.as_ref()
135-
.map_or(Ok(()), |a| {
139+
.map_or(Ok(None), |a| {
136140
a.handle_auth(
137141
addr.as_ref().map(|a| a.0).as_ref(),
138142
&headers,
139143
path.as_str(),
140144
)
141145
})
142-
.and_then(|()| self.decode(encoding_header.as_deref(), body))
143-
.and_then(|body| {
146+
.and_then(|auth_enrichment| {
147+
self.decode(encoding_header.as_deref(), body)
148+
.map(|body| (body, auth_enrichment))
149+
})
150+
.and_then(|(body, auth_enrichment)| {
144151
emit!(HttpBytesReceived {
145152
byte_size: body.len(),
146153
http_path,
147154
protocol,
148155
});
149156
self.build_events(body, &headers, &query_parameters, path.as_str())
157+
.map(|events| (events, auth_enrichment))
150158
})
151-
.map(|mut events| {
159+
.map(|(mut events, auth_enrichment)| {
152160
emit!(HttpEventsReceived {
153161
count: events.len(),
154162
byte_size: events.estimated_json_encoded_size_of(),
@@ -166,6 +174,10 @@ pub trait HttpSource: Clone + Send + Sync + 'static {
166174
.as_ref(),
167175
);
168176

177+
if let Some(enrichment) = auth_enrichment {
178+
self.inject_auth_enrichment(&mut events, enrichment);
179+
}
180+
169181
events
170182
});
171183

0 commit comments

Comments
 (0)