-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(sources): add custom auth strategy for components with HTTP server #22236
base: master
Are you sure you want to change the base?
Conversation
This adds `custom` auth strategy for components with HTTP server (`http_server`, `datadog_agent`, `opentelemetry`, `prometheus`) besides the default basic auth. This is a breaking change because `strategy` is now required for auth - for existing configurations `strategy: "basic"` needs to be added. Related: vectordotdev#22213
I have made this a breaking change, requiring explicit |
Co-authored-by: Esther Kim <[email protected]>
src/common/http/server_auth.rs
Outdated
/// HTTP header without any additional encryption beyond what is provided by the transport itself. | ||
#[configurable_component] | ||
#[derive(Clone, Debug, Eq, PartialEq)] | ||
#[serde(deny_unknown_fields, rename_all = "snake_case", tag = "strategy")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like we have another use case for serde-rs/serde#2231.
Related: #22212 (comment)
💭 Thinking how to avoid breaking behavior for users. I will play locally with a custom deserializer and come back to you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Change to HttpServerAuthConfig
to:
#[configurable_component(no_deser)]
Custom deserializer (mostly AI generated):
// Custom deserializer to default `strategy` to `basic`
impl<'de> Deserialize<'de> for HttpServerAuthConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct HttpServerAuthConfigVisitor;
impl<'de> Visitor<'de> for HttpServerAuthConfigVisitor {
type Value = HttpServerAuthConfig;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a valid authentication strategy (basic or custom)")
}
fn visit_map<A>(self, mut map: A) -> Result<HttpServerAuthConfig, A::Error>
where
A: MapAccess<'de>,
{
let mut strategy: Option<String> = None;
let mut username: Option<String> = None;
let mut password: Option<String> = None;
let mut source: Option<String> = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"strategy" => {
if strategy.is_some() {
return Err(de::Error::duplicate_field("strategy"));
}
strategy = Some(map.next_value()?);
}
"username" => {
if username.is_some() {
return Err(de::Error::duplicate_field("username"));
}
username = Some(map.next_value()?);
}
"password" => {
if password.is_some() {
return Err(de::Error::duplicate_field("password"));
}
password = Some(map.next_value()?);
}
"source" => {
if source.is_some() {
return Err(de::Error::duplicate_field("source"));
}
source = Some(map.next_value()?);
}
_ => {
return Err(de::Error::unknown_field(
&key,
&["strategy", "username", "password", "source"],
));
}
}
}
// Default to "basic" if strategy is missing
let strategy = strategy.unwrap_or_else(|| "basic".to_string());
match strategy.as_str() {
"basic" => {
let username = username.ok_or_else(|| de::Error::missing_field("username"))?;
let password = password.ok_or_else(|| de::Error::missing_field("password"))?;
Ok(HttpServerAuthConfig::Basic {
username,
password: SensitiveString::from(password),
})
}
"custom" => {
let source = source.ok_or_else(|| de::Error::missing_field("source"))?;
Ok(HttpServerAuthConfig::Custom { source })
}
_ => Err(de::Error::unknown_variant(&strategy, &["basic", "custom"])),
}
}
}
deserializer.deserialize_map(HttpServerAuthConfigVisitor)
}
}
Tried with, config 1:
sources:
s0:
type: http_server
address: 0.0.0.0:80
auth:
# not specifying strategy
username: foo
password: bar
...
..and config 2:
sources:
s0:
type: http_server
address: 0.0.0.0:80
auth:
strategy: custom
source: "true"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW the approach I went with in #22212 cheats somewhat, but avoids needing to define a custom deserializer - https://github.com/vectordotdev/vector/pull/22212/files#diff-59ec8f4321099ed75b904b203298b94219eba315f8d7fc0cf54d13b148a0edc7R72
You effectively just have an untagged enum to union the tagged and untagged versions of the config when deserializing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That looks good to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @esensar! Happy that we can avoid breaking existing behavior. This PR is almost there.
@@ -124,15 +115,13 @@ pub trait HttpSource: Clone + Send + Sync + 'static { | |||
}) | |||
.untuple_one() | |||
.and(warp::path::full()) | |||
.and(warp::header::optional::<String>("authorization")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this no longer needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Authorization is now handled by handle_auth
which takes in all headers, and they are already picked up below (warp::header::headers_cloned()
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good. I will do one last pass tomorrow before merging.
Please merge the latest master branch to pick up CI fixes. |
src/common/http/server_auth.rs
Outdated
|
||
impl HttpServerAuthMatcher { | ||
#[cfg(test)] | ||
fn auth_header(self) -> (HeaderValue, &'static str) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Move this test-only function in a new impl
block inside mod tests
.
Co-authored-by: Pavlos Rontidis <[email protected]>
This https://github.com/vectordotdev/vector/actions/runs/13245571933/job/36971462667?pr=22236 fails because workflows on contributor PRs don't have access to the secrets. I will update here when this is fixed. |
There's also another kind of failure here: Run make check-component-docs |
I forgot to run it after the deserialization changes. I need to make some minor changes and then it should be fine. |
Head branch was pushed to by a user without write access
Summary
This adds
custom
auth strategy for components with HTTP server (http_server
,datadog_agent
,opentelemetry
,prometheus
) besides the default basic auth. This is a breaking change becausestrategy
is now required for auth - for existing configurationsstrategy: "basic"
needs to be added.Change Type
Is this a breaking change?
How did you test this PR?
Besides the tests added to the codebase, I ran basic tests with
http_server
source component:Tested by making calls via curl:
Does this PR include user facing changes?
Checklist
make check-all
is a good command to run locally. This check isdefined here. Some of these
checks might not be relevant to your PR. For Rust changes, at the very least you should run:
cargo fmt --all
cargo clippy --workspace --all-targets -- -D warnings
cargo nextest run --workspace
(alternatively, you can runcargo test --all
)Cargo.lock
), pleaserun
dd-rust-license-tool write
to regenerate the license inventory and commit the changes (if any). More details here.References
Related: #22213