use auth middleware for prometheus metrics
This commit is contained in:
Generated
-49
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user