Skip to content

Commit

Permalink
auth-server: Use admin auth to proxy relayer requests
Browse files Browse the repository at this point in the history
  • Loading branch information
joeykraut committed Oct 23, 2024
1 parent ea86511 commit e1ace78
Show file tree
Hide file tree
Showing 9 changed files with 454 additions and 571 deletions.
810 changes: 306 additions & 504 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion auth/auth-server-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub const API_KEYS_PATH: &str = "api-keys";
/// The path to mark an API key as inactive
///
/// POST /api-keys/{id}/deactivate
pub const DEACTIVATE_API_KEY_PATH: &str = "deactivate";
pub const DEACTIVATE_API_KEY_PATH: &str = "/api-keys/{id}/deactivate";

/// A request to create a new API key
#[derive(Debug, Deserialize)]
Expand Down
14 changes: 14 additions & 0 deletions auth/auth-server/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,33 +22,47 @@ pub enum AuthServerError {
/// Error serializing or deserializing a stored value
#[error("Error serializing/deserializing a stored value: {0}")]
Serde(String),
/// Error setting up the auth server
#[error("Error setting up the auth server: {0}")]
Setup(String),
/// Unauthorized
#[error("Unauthorized: {0}")]
Unauthorized(String),
}

impl AuthServerError {
/// Create a new database connection error
#[allow(clippy::needless_pass_by_value)]
pub fn db<T: ToString>(msg: T) -> Self {
Self::DatabaseConnection(msg.to_string())
}

/// Create a new encryption error
#[allow(clippy::needless_pass_by_value)]
pub fn encryption<T: ToString>(msg: T) -> Self {
Self::Encryption(msg.to_string())
}

/// Create a new decryption error
#[allow(clippy::needless_pass_by_value)]
pub fn decryption<T: ToString>(msg: T) -> Self {
Self::Decryption(msg.to_string())
}

/// Create a new serde error
#[allow(clippy::needless_pass_by_value)]
pub fn serde<T: ToString>(msg: T) -> Self {
Self::Serde(msg.to_string())
}

/// Create a new setup error
#[allow(clippy::needless_pass_by_value)]
pub fn setup<T: ToString>(msg: T) -> Self {
Self::Setup(msg.to_string())
}

/// Create a new unauthorized error
#[allow(clippy::needless_pass_by_value)]
pub fn unauthorized<T: ToString>(msg: T) -> Self {
Self::Unauthorized(msg.to_string())
}
Expand Down
31 changes: 22 additions & 9 deletions auth/auth-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub(crate) mod models;
pub(crate) mod schema;
mod server;

use auth_server_api::{API_KEYS_PATH, DEACTIVATE_API_KEY_PATH};
use auth_server_api::API_KEYS_PATH;
use clap::Parser;
use renegade_util::telemetry::configure_telemetry;
use reqwest::StatusCode;
Expand Down Expand Up @@ -80,6 +80,14 @@ pub enum ApiError {
Unauthorized,
}

impl ApiError {
/// Create a new internal server error
#[allow(clippy::needless_pass_by_value)]
pub fn internal<T: ToString>(msg: T) -> Self {
Self::InternalError(msg.to_string())
}
}

// Implement warp::reject::Reject for ApiError
impl warp::reject::Reject for ApiError {}

Expand Down Expand Up @@ -108,7 +116,7 @@ async fn main() {
let server = Server::new(args).await.expect("Failed to create server");
let server = Arc::new(server);

// --- Routes --- //
// --- Management Routes --- //

// Ping route
let ping = warp::path("ping")
Expand All @@ -125,24 +133,29 @@ async fn main() {
// Expire an API key
let expire_api_key = warp::path(API_KEYS_PATH)
.and(warp::path::param::<Uuid>())
.and(warp::path(DEACTIVATE_API_KEY_PATH))
.and(warp::path("deactivate"))
.and(warp::post())
.and(with_server(server.clone()))
.and_then(|id: Uuid, server: Arc<Server>| async move { server.expire_key(id).await });

// Proxy route
let proxy = warp::path::full()
.and(warp::method())
// --- Proxied Routes --- //

let atomic_match_path = warp::path("v0")
.and(warp::path("matching-engine"))
.and(warp::path("request-external-match"))
.and(warp::post())
.and(warp::path::full())
.and(warp::header::headers_cloned())
.and(warp::body::bytes())
.and(with_server(server.clone()))
.and_then(|path, method, headers, body, server: Arc<Server>| async move {
server.handle_proxy_request(path, method, headers, body).await
.and_then(|path, headers, body, server: Arc<Server>| async move {
server.handle_external_match_request(path, headers, body).await
});

// Bind the server and listen
info!("Starting auth server on port {}", listen_addr.port());
let routes = ping.or(add_api_key).or(expire_api_key).or(proxy).recover(handle_rejection);
let routes =
ping.or(add_api_key).or(expire_api_key).or(atomic_match_path).recover(handle_rejection);
warp::serve(routes).bind(listen_addr).await;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,64 +1,18 @@
//! Handler code for proxied relayer requests
//!
//! At a high level the server must first authenticate the request, then forward
//! it to the relayer with admin authentication
//! Handles API authentication

use auth_server_api::RENEGADE_API_KEY_HEADER;
use bytes::Bytes;
use http::{HeaderMap, Method};
use http::HeaderMap;
use renegade_api::auth::validate_expiring_auth;
use renegade_common::types::wallet::keychain::HmacKey;
use tracing::error;
use uuid::Uuid;
use warp::{reject::Rejection, reply::Reply};

use crate::{error::AuthServerError, ApiError};

use super::{helpers::aes_decrypt, Server};

/// Handle a proxied request
impl Server {
/// Handle a request meant to be authenticated and proxied to the relayer
pub async fn handle_proxy_request(
&self,
path: warp::path::FullPath,
method: Method,
mut headers: warp::hyper::HeaderMap,
body: Bytes,
) -> Result<impl Reply, Rejection> {
// Authorize the request
self.authorize_request(path.as_str(), &mut headers, &body).await?;

// Forward the request to the relayer
let url = format!("{}{}", self.relayer_url, path.as_str());
let req = self.client.request(method, &url).headers(headers).body(body);

// TODO: Add admin auth here
match req.send().await {
Ok(resp) => {
let status = resp.status();
let headers = resp.headers().clone();
let body = resp.bytes().await.map_err(|e| {
warp::reject::custom(ApiError::InternalError(format!(
"Failed to read response body: {e}"
)))
})?;

let mut response = warp::http::Response::new(body);
*response.status_mut() = status;
*response.headers_mut() = headers;

Ok(response)
},
Err(e) => {
error!("Error proxying request: {}", e);
Err(warp::reject::custom(ApiError::InternalError(e.to_string())))
},
}
}

/// Authorize a request
async fn authorize_request(
pub(crate) async fn authorize_request(
&self,
path: &str,
headers: &mut HeaderMap,
Expand Down
28 changes: 28 additions & 0 deletions auth/auth-server/src/server/handle_external_match.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//! Handler code for proxied relayer requests
//!
//! At a high level the server must first authenticate the request, then forward
//! it to the relayer with admin authentication

use bytes::Bytes;
use http::Method;
use warp::{reject::Rejection, reply::Reply};

use super::Server;

/// Handle a proxied request
impl Server {
/// Handle an external match request
pub async fn handle_external_match_request(
&self,
path: warp::path::FullPath,
mut headers: warp::hyper::HeaderMap,
body: Bytes,
) -> Result<impl Reply, Rejection> {
// Authorize the request
self.authorize_request(path.as_str(), &mut headers, &body).await?;

// Send the request to the relayer
let resp = self.send_admin_request(Method::POST, path.as_str(), headers, body).await?;
Ok(resp)
}
}
8 changes: 8 additions & 0 deletions auth/auth-server/src/server/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,12 @@ mod tests {
let decrypted = aes_decrypt(&encrypted, &key).unwrap();
assert_eq!(value, decrypted);
}

/// Generate an encryption key, base64 encode it, and print it
#[test]
pub fn generate_encryption_key() {
let key = Aes128Gcm::generate_key(&mut thread_rng());
let encoded = general_purpose::STANDARD.encode(&key);
println!("{}", encoded);
}
}
69 changes: 64 additions & 5 deletions auth/auth-server/src/server/mod.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,33 @@
//! Defines the server struct and associated functions
//!
//! The server is a dependency injection container for the authentication server
mod api_auth;
mod handle_external_match;
mod handle_key_management;
mod handle_proxy;
mod helpers;
mod queries;

use crate::{error::AuthServerError, Cli};
use crate::{error::AuthServerError, ApiError, Cli};
use base64::{engine::general_purpose, Engine};
use bb8::{Pool, PooledConnection};
use bytes::Bytes;
use diesel::ConnectionError;
use diesel_async::{
pooled_connection::{AsyncDieselConnectionManager, ManagerConfig},
AsyncPgConnection,
};
use http::{HeaderMap, Method, Response};
use native_tls::TlsConnector;
use postgres_native_tls::MakeTlsConnector;
use renegade_api::auth::add_expiring_auth_to_headers;
use renegade_common::types::wallet::keychain::HmacKey;
use reqwest::Client;
use std::sync::Arc;
use std::{sync::Arc, time::Duration};
use tracing::error;

/// The duration for which the admin authentication is valid
const ADMIN_AUTH_DURATION_MS: u64 = 5_000; // 5 seconds

/// The DB connection type
pub type DbConn<'a> = PooledConnection<'a, AsyncDieselConnectionManager<AsyncPgConnection>>;
/// The DB pool type
Expand All @@ -32,7 +40,7 @@ pub struct Server {
/// The URL of the relayer
pub relayer_url: String,
/// The admin key for the relayer
pub relayer_admin_key: String,
pub relayer_admin_key: HmacKey,
/// The encryption key for storing API secrets
pub encryption_key: Vec<u8>,
/// The HTTP client
Expand All @@ -50,10 +58,13 @@ impl Server {
.decode(&args.encryption_key)
.map_err(AuthServerError::encryption)?;

let relayer_admin_key =
HmacKey::from_base64_string(&args.relayer_admin_key).map_err(AuthServerError::setup)?;

Ok(Self {
db_pool: Arc::new(db_pool),
relayer_url: args.relayer_url,
relayer_admin_key: args.relayer_admin_key,
relayer_admin_key,
encryption_key,
client: Client::new(),
})
Expand All @@ -63,6 +74,54 @@ impl Server {
pub async fn get_db_conn(&self) -> Result<DbConn, AuthServerError> {
self.db_pool.get().await.map_err(AuthServerError::db)
}

/// Send a proxied request to the relayer with admin authentication
pub(crate) async fn send_admin_request(
&self,
method: Method,
path: &str,
mut headers: HeaderMap,
body: Bytes,
) -> Result<Response<Bytes>, ApiError> {
// Admin authenticate the request
self.admin_authenticate(path, &mut headers, &body).await?;

Check failure on line 87 in auth/auth-server/src/server/mod.rs

View workflow job for this annotation

GitHub Actions / clippy

`std::result::Result<(), ApiError>` is not a future

error[E0277]: `std::result::Result<(), ApiError>` is not a future --> auth/auth-server/src/server/mod.rs:87:60 | 87 | self.admin_authenticate(path, &mut headers, &body).await?; | -^^^^^ | || | |`std::result::Result<(), ApiError>` is not a future | help: remove the `.await` | = help: the trait `warp::Future` is not implemented for `std::result::Result<(), ApiError>`, which is required by `std::result::Result<(), ApiError>: std::future::IntoFuture` = note: std::result::Result<(), ApiError> must be a future or must implement `IntoFuture` to be awaited = note: required for `std::result::Result<(), ApiError>` to implement `std::future::IntoFuture`

// Forward the request to the relayer
let url = format!("{}{}", self.relayer_url, path);
let req = self.client.request(method, &url).headers(headers).body(body);
match req.send().await {
Ok(resp) => {
let status = resp.status();
let headers = resp.headers().clone();
let body = resp.bytes().await.map_err(|e| {
ApiError::internal(format!("Failed to read response body: {e}"))
})?;

let mut response = warp::http::Response::new(body);
*response.status_mut() = status;
*response.headers_mut() = headers;

Ok(response)
},
Err(e) => {
error!("Error proxying request: {}", e);
Err(ApiError::internal(e))
},
}
}

/// Admin authenticate a request
pub fn admin_authenticate(
&self,
path: &str,
headers: &mut HeaderMap,
body: &[u8],
) -> Result<(), ApiError> {
let key = self.relayer_admin_key;
let expiration = Duration::from_millis(ADMIN_AUTH_DURATION_MS);
add_expiring_auth_to_headers(path, headers, body, &key, expiration);
Ok(())
}
}

/// Create a database pool
Expand Down
11 changes: 8 additions & 3 deletions auth/auth-server/src/server/queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,18 @@ impl Server {
/// Get the API key entry for a given key
pub async fn get_api_key_entry(&self, api_key: Uuid) -> Result<ApiKey, AuthServerError> {
let mut conn = self.get_db_conn().await?;
api_keys::table
let result = api_keys::table
.filter(api_keys::id.eq(api_key))
.limit(1)
.load::<ApiKey>(&mut conn)
.await
.map_err(AuthServerError::db)
.map(|res| res[0].clone())
.map_err(AuthServerError::db)?;

if result.is_empty() {
Err(AuthServerError::unauthorized("API key not found"))
} else {
Ok(result[0].clone())
}
}

// --- Setters --- //
Expand Down

0 comments on commit e1ace78

Please sign in to comment.