From 8831ad965a74f0af756832568bccf3fdbef18bdf Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Thu, 13 Feb 2025 00:30:01 +1000 Subject: [PATCH 1/3] feat: global auth token --- Cargo.lock | 1 + crates/server/Cargo.toml | 1 + crates/server/README.md | 11 ++++ crates/server/src/api/auth_layer.rs | 84 ++++++++++++++++++++++++++++ crates/server/src/api/mod.rs | 11 +++- crates/server/src/bin/warg-server.rs | 9 +++ crates/server/src/lib.rs | 9 +++ 7 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 crates/server/src/api/auth_layer.rs diff --git a/Cargo.lock b/Cargo.lock index c5080af5..21fe403d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4404,6 +4404,7 @@ dependencies = [ "diesel_json", "diesel_migrations", "futures", + "futures-util", "indexmap 2.2.6", "secrecy", "serde", diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index f12fb42d..4a290651 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -40,6 +40,7 @@ diesel_migrations = { workspace = true, optional = true } diesel-derive-enum = { workspace = true, optional = true, features = ["postgres"] } serde_json = { workspace = true, optional = true } chrono = { workspace = true, optional = true } +futures-util = { workspace = true } [features] default = [] diff --git a/crates/server/README.md b/crates/server/README.md index 02709fd0..44198ea8 100644 --- a/crates/server/README.md +++ b/crates/server/README.md @@ -63,3 +63,14 @@ WARG_NAMESPACE=example WARG_DATABASE_URL=postgres://postgres:password@localhost/ The `--data-store postgres` flag starts the server with PostgreSQL data storage. The server may now be restarted and will continue to use the same database. + +### Authorization + +By default, the server is publicly accessible with authorization +managed by the operator logs and key IDs. + +It is possible to secure all endpoints in the registry with a global bearer +token by env, with `WARG_GLOBAL_AUTH_TOKEN=`, or by CLI arg with +`--global-auth-token `. +If this is set, then requests to the server must contain the `Authorization` +header with the `Bearer ` token. diff --git a/crates/server/src/api/auth_layer.rs b/crates/server/src/api/auth_layer.rs new file mode 100644 index 00000000..5d26df42 --- /dev/null +++ b/crates/server/src/api/auth_layer.rs @@ -0,0 +1,84 @@ +//! An authentication layer to secure endpoints with a bearer token. +use axum::{ + http::{Request, StatusCode}, + response::Response, +}; +use futures::future::BoxFuture; +use std::{ + sync::Arc, + task::{Context, Poll}, +}; +use tower::Layer; + +#[derive(Clone)] +pub struct AuthLayer { + secret: Arc, +} + +impl AuthLayer { + pub fn new>(secret: S) -> Self { + AuthLayer { + secret: Arc::new(secret.into()), + } + } +} + +impl Layer for AuthLayer { + type Service = AuthService; + + fn layer(&self, inner: S) -> Self::Service { + AuthService { + inner, + + secret: self.secret.clone(), + } + } +} + +#[derive(Clone)] +pub struct AuthService { + inner: S, + + secret: Arc, +} + +impl tower::Service> for AuthService +where + S: tower::Service, Response = Response> + Send + 'static, + + S::Future: Send + 'static, + + B: Send + 'static, +{ + type Response = Response; + + type Error = S::Error; + + type Future = BoxFuture<'static, Result>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: Request) -> Self::Future { + let secret = self.secret.clone(); + let expected = format!("Bearer {}", secret); + let header_value_opt = req + .headers() + .get("authorization") + .and_then(|h| h.to_str().ok()); + if let Some(header_value) = header_value_opt { + if header_value == expected { + // The header is correct, forward the request. + return Box::pin(self.inner.call(req)); + } + } + // The header is incorrect, return an Unauthorized response. + Box::pin(async move { + Ok(Response::builder() + .status(StatusCode::UNAUTHORIZED) + .body("Unauthorized".into()) + .unwrap()) + }) + } +} diff --git a/crates/server/src/api/mod.rs b/crates/server/src/api/mod.rs index 72e779fb..67c2f1af 100644 --- a/crates/server/src/api/mod.rs +++ b/crates/server/src/api/mod.rs @@ -1,3 +1,4 @@ +use crate::api::auth_layer::AuthLayer; use crate::{ policy::{content::ContentPolicy, record::RecordPolicy}, services::CoreService, @@ -14,6 +15,7 @@ use tower_http::{ use tracing::{Level, Span}; use url::Url; +mod auth_layer; pub mod v1; #[cfg(feature = "debug")] @@ -27,11 +29,12 @@ pub fn create_router( files_dir: PathBuf, content_policy: Option>, record_policy: Option>, + global_auth_token: Option, ) -> Router { let router = Router::new(); #[cfg(feature = "debug")] let router = router.nest("/debug", debug::Config::new(core.clone()).into_router()); - router + let mut router = router .nest( "/v1", v1::create_router( @@ -67,5 +70,9 @@ pub fn create_router( axum::http::header::ACCEPT, ]), ), - ) + ); + if let Some(token) = global_auth_token { + router = router.layer(AuthLayer::new(token)); + } + router } diff --git a/crates/server/src/bin/warg-server.rs b/crates/server/src/bin/warg-server.rs index 3a092986..50d4e698 100644 --- a/crates/server/src/bin/warg-server.rs +++ b/crates/server/src/bin/warg-server.rs @@ -35,6 +35,11 @@ struct Args { #[arg(long, env = "WARG_CONTENT_BASE_URL")] content_base_url: Option, + /// An optional bearer auth token that, if set, will be required on + /// all requests to the registry under the authorization header. + #[arg(long, env = "WARG_GLOBAL_AUTH_TOKEN")] + global_auth_token: Option, + /// The data store to use for the server. #[arg(long, env = "WARG_DATA_STORE", default_value = "memory")] data_store: DataStoreKind, @@ -112,6 +117,10 @@ async fn main() -> Result<()> { config = config.with_content_base_url(url); } + if let Some(global_auth_token) = args.global_auth_token { + config = config.with_global_auth_token(Some(global_auth_token)); + } + if let Some(path) = args.authorized_keys_file { let authorized_keys_data = std::fs::read_to_string(&path) .with_context(|| format!("failed to read authorized keys from {path:?}"))?; diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index b574ea38..2104a4d8 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -29,6 +29,7 @@ pub struct Config { addr: Option, data_store: Option>, content_dir: PathBuf, + global_auth_token: Option, content_base_url: Option, shutdown: Option, checkpoint_interval: Option, @@ -74,6 +75,7 @@ impl Config { addr: None, data_store: None, content_dir, + global_auth_token: None, content_base_url: None, shutdown: None, checkpoint_interval: None, @@ -140,6 +142,12 @@ impl Config { self.record_policy = Some(Arc::new(policy)); self } + + /// Sets the optional global auth token. + pub fn with_global_auth_token(mut self, global_auth_token: Option) -> Self { + self.global_auth_token = global_auth_token; + self + } } /// Represents the warg registry server. @@ -225,6 +233,7 @@ impl Server { files_dir, self.config.content_policy, self.config.record_policy, + self.config.global_auth_token, ); Ok(InitializedServer { From 1774253ee43344d8a81552e3d9a34e7efef1d1b6 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Thu, 13 Feb 2025 01:17:28 +1000 Subject: [PATCH 2/3] chore: send bearer token in client --- crates/client/src/api.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/client/src/api.rs b/crates/client/src/api.rs index 8b600a96..205bf67a 100644 --- a/crates/client/src/api.rs +++ b/crates/client/src/api.rs @@ -11,6 +11,7 @@ use reqwest::{ use secrecy::{ExposeSecret, Secret}; use serde::de::DeserializeOwned; use std::borrow::Cow; +use reqwest::header::AUTHORIZATION; use thiserror::Error; use warg_api::{ v1::{ @@ -206,9 +207,14 @@ impl Client { /// Creates a new API client with the given URL. pub fn new(url: impl IntoUrl, auth_token: Option>) -> Result { let url = RegistryUrl::new(url)?; + let mut headers = HeaderMap::new(); + if let Some(token) = &auth_token { + headers.append(AUTHORIZATION, format!("Bearer {}", token.expose_secret()).parse()?); + } + let client = reqwest::Client::builder().default_headers(headers).build()?; Ok(Self { url, - client: reqwest::Client::new(), + client, warg_registry_header: None, auth_token, }) From 3a798f324bca2916045252b5ce6dd729eed56c6d Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Thu, 13 Feb 2025 01:21:09 +1000 Subject: [PATCH 3/3] chore: fmt --- crates/client/src/api.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/client/src/api.rs b/crates/client/src/api.rs index 205bf67a..2947dbc8 100644 --- a/crates/client/src/api.rs +++ b/crates/client/src/api.rs @@ -4,6 +4,7 @@ use anyhow::{anyhow, Result}; use bytes::Bytes; use futures_util::{future::ready, stream::once, Stream, StreamExt, TryStreamExt}; use indexmap::IndexMap; +use reqwest::header::AUTHORIZATION; use reqwest::{ header::{HeaderMap, HeaderValue}, Body, IntoUrl, Method, RequestBuilder, Response, StatusCode, @@ -11,7 +12,6 @@ use reqwest::{ use secrecy::{ExposeSecret, Secret}; use serde::de::DeserializeOwned; use std::borrow::Cow; -use reqwest::header::AUTHORIZATION; use thiserror::Error; use warg_api::{ v1::{ @@ -209,9 +209,14 @@ impl Client { let url = RegistryUrl::new(url)?; let mut headers = HeaderMap::new(); if let Some(token) = &auth_token { - headers.append(AUTHORIZATION, format!("Bearer {}", token.expose_secret()).parse()?); + headers.append( + AUTHORIZATION, + format!("Bearer {}", token.expose_secret()).parse()?, + ); } - let client = reqwest::Client::builder().default_headers(headers).build()?; + let client = reqwest::Client::builder() + .default_headers(headers) + .build()?; Ok(Self { url, client,