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 0bb275f
Show file tree
Hide file tree
Showing 8 changed files with 437 additions and 568 deletions.
803 changes: 303 additions & 500 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
8 changes: 8 additions & 0 deletions auth/auth-server/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ 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),
Expand All @@ -48,6 +51,11 @@ impl AuthServerError {
Self::Serde(msg.to_string())
}

/// Create a new setup error
pub fn setup<T: ToString>(msg: T) -> Self {

Check failure on line 55 in auth/auth-server/src/error.rs

View workflow job for this annotation

GitHub Actions / clippy

this argument is passed by value, but not consumed in the function body

error: this argument is passed by value, but not consumed in the function body --> auth/auth-server/src/error.rs:55:36 | 55 | pub fn setup<T: ToString>(msg: T) -> Self { | ^ help: consider taking a reference instead: `&T` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_pass_by_value
Self::Setup(msg.to_string())
}

/// Create a new unauthorized error
pub fn unauthorized<T: ToString>(msg: T) -> Self {

Check failure on line 60 in auth/auth-server/src/error.rs

View workflow job for this annotation

GitHub Actions / clippy

this argument is passed by value, but not consumed in the function body

error: this argument is passed by value, but not consumed in the function body --> auth/auth-server/src/error.rs:60:43 | 60 | pub fn unauthorized<T: ToString>(msg: T) -> Self { | ^ help: consider taking a reference instead: `&T` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_pass_by_value
Self::Unauthorized(msg.to_string())
Expand Down
30 changes: 21 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,13 @@ pub enum ApiError {
Unauthorized,
}

impl ApiError {
/// Create a new internal server error
pub fn internal<T: ToString>(msg: T) -> Self {

Check failure on line 85 in auth/auth-server/src/main.rs

View workflow job for this annotation

GitHub Actions / clippy

this argument is passed by value, but not consumed in the function body

error: this argument is passed by value, but not consumed in the function body --> auth/auth-server/src/main.rs:85:39 | 85 | pub fn internal<T: ToString>(msg: T) -> Self { | ^ help: consider taking a reference instead: `&T` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_pass_by_value
Self::InternalError(msg.to_string())
}
}

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

Expand Down Expand Up @@ -108,7 +115,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 +132,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)
}
}
71 changes: 65 additions & 6 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 @@ -46,14 +54,17 @@ impl Server {
let db_pool = create_db_pool(&args.database_url).await?;

// Parse the decryption key as a base64 encoded string
let encryption_key = general_purpose::STANDARD
let encryption_key = general_purpose::STANDARD_NO_PAD
.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?;

// 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 async 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(())
}

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

View workflow job for this annotation

GitHub Actions / clippy

unused `async` for function with no await statements

error: unused `async` for function with no await statements --> auth/auth-server/src/server/mod.rs:114:5 | 114 | / pub async fn admin_authenticate( 115 | | &self, 116 | | path: &str, 117 | | headers: &mut HeaderMap, ... | 123 | | Ok(()) 124 | | } | |_____^ | = help: consider removing the `async` from this function = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#unused_async = note: requested on the command line with `-D clippy::unused-async`
}

/// 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 0bb275f

Please sign in to comment.