Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

auth-server: Use admin auth to proxy relayer requests #52

Merged
merged 1 commit into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
/// 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 @@
.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 @@
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
Loading