use auth middleware for prometheus metrics

This commit is contained in:
Jędrzej Stuczyński
2024-05-26 13:07:16 +01:00
parent 61e08d6e43
commit 77b4fd2aa5
11 changed files with 58 additions and 91 deletions
Generated
-49
View File
@@ -447,29 +447,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "axum-extra"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0be6ea09c9b96cb5076af0de2e383bd2bc0c18f827cf1967bdd353e0b910d733"
dependencies = [
"axum 0.7.5",
"axum-core 0.4.3",
"bytes",
"futures-util",
"headers",
"http 1.1.0",
"http-body 1.0.0",
"http-body-util",
"mime",
"pin-project-lite",
"serde",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "backtrace"
version = "0.3.71"
@@ -2738,30 +2715,6 @@ dependencies = [
"num-traits",
]
[[package]]
name = "headers"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9"
dependencies = [
"base64 0.21.7",
"bytes",
"headers-core",
"http 1.1.0",
"httpdate",
"mime",
"sha1",
]
[[package]]
name = "headers-core"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
dependencies = [
"http 1.1.0",
]
[[package]]
name = "heck"
version = "0.3.3"
@@ -5126,11 +5079,9 @@ name = "nym-node-http-api"
version = "0.1.0"
dependencies = [
"axum 0.7.5",
"axum-extra",
"colored",
"dashmap",
"fastrand 2.1.0",
"headers",
"hmac 0.12.1",
"hyper 1.3.1",
"ipnetwork 0.16.0",
@@ -14,11 +14,25 @@ use zeroize::Zeroizing;
#[derive(Debug, Clone)]
pub struct BearerAuthLayer {
bearer_token: Arc<Zeroizing<String>>,
allow_empty: bool,
}
impl BearerAuthLayer {
pub fn new(bearer_token: Arc<Zeroizing<String>>) -> Self {
BearerAuthLayer { bearer_token }
BearerAuthLayer {
bearer_token,
allow_empty: false,
}
}
pub fn new_raw(bearer_token: impl Into<String>) -> Self {
BearerAuthLayer::new(Arc::new(Zeroizing::new(bearer_token.into())))
}
#[must_use]
pub fn with_allow_empty(mut self, allow_empty: bool) -> Self {
self.allow_empty = allow_empty;
self
}
}
@@ -26,7 +40,7 @@ impl<S> Layer<S> for BearerAuthLayer {
type Service = RequireBearerAuth<S>;
fn layer(&self, inner: S) -> Self::Service {
RequireBearerAuth::new(inner, self.bearer_token.clone())
RequireBearerAuth::new(inner, self.bearer_token.clone()).with_allow_empty(self.allow_empty)
}
}
@@ -34,6 +48,7 @@ impl<S> Layer<S> for BearerAuthLayer {
pub struct RequireBearerAuth<S> {
inner: S,
bearer_token: Arc<Zeroizing<String>>,
allow_empty: bool,
}
impl<S> RequireBearerAuth<S> {
@@ -41,9 +56,16 @@ impl<S> RequireBearerAuth<S> {
RequireBearerAuth {
inner,
bearer_token,
allow_empty: false,
}
}
#[must_use]
pub fn with_allow_empty(mut self, allow_empty: bool) -> Self {
self.allow_empty = allow_empty;
self
}
fn check_auth_header(&self, header: Option<&HeaderValue>) -> Result<(), &'static str> {
let Some(token) = header else {
trace!("missing header");
@@ -103,6 +125,16 @@ where
fn call(&mut self, req: Request) -> Self::Future {
debug!("checking the auth");
if self.bearer_token.is_empty() && !self.allow_empty {
return Box::pin(async move {
Ok((
StatusCode::INTERNAL_SERVER_ERROR,
"no valid access token has been specified on the server",
)
.into_response())
});
}
let auth_header = req.headers().get(header::AUTHORIZATION);
match self.check_auth_header(auth_header) {
+2
View File
@@ -61,6 +61,8 @@ rocket_cors = { workspace = true }
rocket_okapi = { workspace = true, features = ["swagger"] }
okapi = { workspace = true, features = ["impl_json_schema"] }
## ephemera-specific
#actix-web = "4"
#array-bytes = "6.0.0"
-2
View File
@@ -8,8 +8,6 @@ license.workspace = true
[dependencies]
axum.workspace = true
axum-extra = { workspace = true, features = ["typed-header"] }
headers.workspace = true
# useful for `#[axum_macros::debug_handler]`
#axum-macros = "0.3.8"
@@ -8,24 +8,30 @@ use crate::state::metrics::MetricsAppState;
use axum::extract::FromRef;
use axum::routing::get;
use axum::Router;
use nym_http_api_common::middleware::BearerAuthLayer;
use nym_node_requests::routes::api::v1::metrics;
pub mod mixing;
pub mod prometheus;
pub mod verloc;
#[derive(Debug, Clone, Default)]
#[derive(Debug, Default, Clone)]
pub struct Config {
//
pub prometheus_token: String,
}
pub(super) fn routes<S>(_config: Config) -> Router<S>
pub(super) fn routes<S>(config: Config) -> Router<S>
where
S: Send + Sync + 'static + Clone,
MetricsAppState: FromRef<S>,
{
let auth_middleware = BearerAuthLayer::new_raw(config.prometheus_token);
Router::new()
.route(metrics::MIXING, get(mixing_stats))
.route(metrics::VERLOC, get(verloc_stats))
.route(metrics::PROMETHEUS, get(prometheus_metrics))
.route(
metrics::PROMETHEUS,
get(prometheus_metrics).layer(auth_middleware),
)
}
@@ -1,12 +1,6 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::state::metrics::MetricsAppState;
use axum::extract::State;
use axum::http::StatusCode;
use axum_extra::TypedHeader;
use headers::authorization::Bearer;
use headers::Authorization;
use nym_metrics::metrics;
/// Returns `prometheus` compatible metrics
@@ -25,21 +19,6 @@ use nym_metrics::metrics;
("prometheus_token" = [])
)
)]
pub(crate) async fn prometheus_metrics<'a>(
TypedHeader(authorization): TypedHeader<Authorization<Bearer>>,
State(state): State<MetricsAppState>,
) -> Result<String, StatusCode> {
if authorization.token().is_empty() {
return Err(StatusCode::UNAUTHORIZED);
}
// TODO: is 500 the correct error code here?
let Some(metrics_key) = state.prometheus_access_token else {
return Err(StatusCode::INTERNAL_SERVER_ERROR);
};
if metrics_key != authorization.token() {
return Err(StatusCode::UNAUTHORIZED);
}
Ok(metrics!())
pub(crate) async fn prometheus_metrics() -> String {
metrics!()
}
@@ -148,6 +148,12 @@ impl Config {
self.api.v1_config.ip_packet_router.details = Some(ip_packet_router);
self
}
#[must_use]
pub fn with_metrics_token(mut self, bearer_token: impl Into<Option<String>>) -> Self {
self.api.v1_config.metrics.prometheus_token = bearer_token.into().unwrap_or_default();
self
}
}
pub struct NymNodeRouter {
@@ -134,8 +134,6 @@ impl VerlocStatsState {
#[derive(Debug, Clone, Default)]
pub struct MetricsAppState {
pub(crate) prometheus_access_token: Option<String>,
pub(crate) mixing_stats: SharedMixingStats,
pub(crate) verloc: SharedVerlocStats,
@@ -37,10 +37,4 @@ impl AppState {
self.metrics.verloc = verloc_stats;
self
}
#[must_use]
pub fn with_metrics_key(mut self, bearer_token: impl Into<Option<String>>) -> Self {
self.metrics.prometheus_access_token = bearer_token.into();
self
}
}
+1
View File
@@ -369,6 +369,7 @@ pub struct Http {
/// An optional bearer token for accessing certain http endpoints.
/// Currently only used for obtaining mixnode's stats.
#[serde(default)]
#[serde(deserialize_with = "de_maybe_stringified")]
pub access_token: Option<String>,
/// Specify whether basic system information should be exposed.
+3 -3
View File
@@ -540,7 +540,8 @@ impl NymNode {
.with_ip_packet_router_details(ipr_details)
.with_used_exit_policy(exit_policy_details)
.with_description(self.description.clone())
.with_auxiliary_details(auxiliary_details);
.with_auxiliary_details(auxiliary_details)
.with_metrics_token(self.config.http.access_token.clone());
if self.config.http.expose_system_info {
config = config.with_system_info(get_system_info(
@@ -560,8 +561,7 @@ impl NymNode {
let app_state = AppState::new()
.with_mixing_stats(self.mixnode.mixing_stats.clone())
.with_verloc_stats(self.verloc_stats.clone())
.with_metrics_key(self.config.http.access_token.clone());
.with_verloc_stats(self.verloc_stats.clone());
Ok(NymNodeRouter::new(config, Some(app_state), Some(wg_state))
.build_server(&self.config.http.bind_address)