concurrency limits on bearer token actions
This commit is contained in:
parent
21956afe73
commit
a67b85a327
@ -112,6 +112,8 @@ pub struct Web3ProxyApp {
|
|||||||
pub user_key_cache: Cache<Ulid, UserKeyData, hashbrown::hash_map::DefaultHashBuilder>,
|
pub user_key_cache: Cache<Ulid, UserKeyData, hashbrown::hash_map::DefaultHashBuilder>,
|
||||||
pub user_key_semaphores: Cache<u64, Arc<Semaphore>, hashbrown::hash_map::DefaultHashBuilder>,
|
pub user_key_semaphores: Cache<u64, Arc<Semaphore>, hashbrown::hash_map::DefaultHashBuilder>,
|
||||||
pub ip_semaphores: Cache<IpAddr, Arc<Semaphore>, hashbrown::hash_map::DefaultHashBuilder>,
|
pub ip_semaphores: Cache<IpAddr, Arc<Semaphore>, hashbrown::hash_map::DefaultHashBuilder>,
|
||||||
|
pub bearer_token_semaphores:
|
||||||
|
Cache<String, Arc<Semaphore>, hashbrown::hash_map::DefaultHashBuilder>,
|
||||||
pub stat_sender: Option<flume::Sender<Web3ProxyStat>>,
|
pub stat_sender: Option<flume::Sender<Web3ProxyStat>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -396,6 +398,7 @@ impl Web3ProxyApp {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// these two rate limiters can share the base limiter
|
// these two rate limiters can share the base limiter
|
||||||
|
// these are deferred rate limiters because we don't want redis network requests on the hot path
|
||||||
// TODO: take cache_size from config
|
// TODO: take cache_size from config
|
||||||
frontend_ip_rate_limiter = Some(DeferredRateLimiter::<IpAddr>::new(
|
frontend_ip_rate_limiter = Some(DeferredRateLimiter::<IpAddr>::new(
|
||||||
10_000,
|
10_000,
|
||||||
@ -407,7 +410,6 @@ impl Web3ProxyApp {
|
|||||||
10_000, "key", rpc_rrl, None,
|
10_000, "key", rpc_rrl, None,
|
||||||
));
|
));
|
||||||
|
|
||||||
// don't defer this one because it will have a low request per peiod
|
|
||||||
login_rate_limiter = Some(RedisRateLimiter::new(
|
login_rate_limiter = Some(RedisRateLimiter::new(
|
||||||
"web3_proxy",
|
"web3_proxy",
|
||||||
"login",
|
"login",
|
||||||
@ -454,12 +456,15 @@ impl Web3ProxyApp {
|
|||||||
|
|
||||||
// create semaphores for concurrent connection limits
|
// create semaphores for concurrent connection limits
|
||||||
// TODO: what should tti be for semaphores?
|
// TODO: what should tti be for semaphores?
|
||||||
let user_key_semaphores = Cache::builder()
|
let bearer_token_semaphores = Cache::builder()
|
||||||
.time_to_idle(Duration::from_secs(120))
|
.time_to_idle(Duration::from_secs(120))
|
||||||
.build_with_hasher(hashbrown::hash_map::DefaultHashBuilder::new());
|
.build_with_hasher(hashbrown::hash_map::DefaultHashBuilder::new());
|
||||||
let ip_semaphores = Cache::builder()
|
let ip_semaphores = Cache::builder()
|
||||||
.time_to_idle(Duration::from_secs(120))
|
.time_to_idle(Duration::from_secs(120))
|
||||||
.build_with_hasher(hashbrown::hash_map::DefaultHashBuilder::new());
|
.build_with_hasher(hashbrown::hash_map::DefaultHashBuilder::new());
|
||||||
|
let user_key_semaphores = Cache::builder()
|
||||||
|
.time_to_idle(Duration::from_secs(120))
|
||||||
|
.build_with_hasher(hashbrown::hash_map::DefaultHashBuilder::new());
|
||||||
|
|
||||||
let app = Self {
|
let app = Self {
|
||||||
config: top_config.app,
|
config: top_config.app,
|
||||||
@ -477,8 +482,9 @@ impl Web3ProxyApp {
|
|||||||
app_metrics,
|
app_metrics,
|
||||||
open_request_handle_metrics,
|
open_request_handle_metrics,
|
||||||
user_key_cache,
|
user_key_cache,
|
||||||
user_key_semaphores,
|
bearer_token_semaphores,
|
||||||
ip_semaphores,
|
ip_semaphores,
|
||||||
|
user_key_semaphores,
|
||||||
stat_sender,
|
stat_sender,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -79,6 +79,11 @@ pub struct AppConfig {
|
|||||||
/// None = no code needed
|
/// None = no code needed
|
||||||
pub invite_code: Option<String>,
|
pub invite_code: Option<String>,
|
||||||
|
|
||||||
|
/// Rate limit for bearer token authenticated entrypoints.
|
||||||
|
/// This is separate from the rpc limits.
|
||||||
|
#[serde(default = "default_bearer_token_max_concurrent_requests")]
|
||||||
|
pub bearer_token_max_concurrent_requests: u64,
|
||||||
|
|
||||||
/// Rate limit for the login entrypoint.
|
/// Rate limit for the login entrypoint.
|
||||||
/// This is separate from the rpc limits.
|
/// This is separate from the rpc limits.
|
||||||
#[serde(default = "default_login_rate_limit_per_minute")]
|
#[serde(default = "default_login_rate_limit_per_minute")]
|
||||||
@ -148,6 +153,11 @@ fn default_public_requests_per_minute() -> Option<u64> {
|
|||||||
Some(0)
|
Some(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Having a low amount of concurrent requests for bearer tokens keeps us from hammering the database.
|
||||||
|
fn default_bearer_token_max_concurrent_requests() -> u64 {
|
||||||
|
2
|
||||||
|
}
|
||||||
|
|
||||||
/// Having a low amount of requests per minute for login is safest.
|
/// Having a low amount of requests per minute for login is safest.
|
||||||
fn default_login_rate_limit_per_minute() -> u64 {
|
fn default_login_rate_limit_per_minute() -> u64 {
|
||||||
10
|
10
|
||||||
|
@ -354,14 +354,12 @@ impl Web3ProxyApp {
|
|||||||
if let Some(max_concurrent_requests) = user_data.max_concurrent_requests {
|
if let Some(max_concurrent_requests) = user_data.max_concurrent_requests {
|
||||||
let semaphore = self
|
let semaphore = self
|
||||||
.user_key_semaphores
|
.user_key_semaphores
|
||||||
.try_get_with(user_data.user_key_id, async move {
|
.get_with(user_data.user_key_id, async move {
|
||||||
let s = Semaphore::new(max_concurrent_requests as usize);
|
let s = Semaphore::new(max_concurrent_requests as usize);
|
||||||
trace!("new semaphore for user_key_id {}", user_data.user_key_id);
|
trace!("new semaphore for user_key_id {}", user_data.user_key_id);
|
||||||
Ok::<_, anyhow::Error>(Arc::new(s))
|
Arc::new(s)
|
||||||
})
|
})
|
||||||
.await
|
.await;
|
||||||
// TODO: is this the best way to handle an arc
|
|
||||||
.map_err(|err| anyhow::anyhow!(err))?;
|
|
||||||
|
|
||||||
// if semaphore.available_permits() == 0 {
|
// if semaphore.available_permits() == 0 {
|
||||||
// // TODO: concurrent limit hit! emit a stat
|
// // TODO: concurrent limit hit! emit a stat
|
||||||
|
@ -25,6 +25,7 @@ use std::ops::Add;
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use time::{Duration, OffsetDateTime};
|
use time::{Duration, OffsetDateTime};
|
||||||
|
use tokio::sync::Semaphore;
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
|
|
||||||
@ -351,11 +352,11 @@ pub async fn user_logout_post(
|
|||||||
Ok("goodbye".into_response())
|
Ok("goodbye".into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// the JSON input to the `post_user` handler
|
/// the JSON input to the `post_user` handler.
|
||||||
/// This handles updating
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct UserProfilePost {
|
pub struct UserProfilePost {
|
||||||
primary_address: Address,
|
primary_address: Address,
|
||||||
|
new_primary_address: Option<Address>,
|
||||||
// TODO: make sure the email address is valid. probably have a "verified" column in the database
|
// TODO: make sure the email address is valid. probably have a "verified" column in the database
|
||||||
email: Option<String>,
|
email: Option<String>,
|
||||||
}
|
}
|
||||||
@ -363,28 +364,35 @@ pub struct UserProfilePost {
|
|||||||
/// `POST /user/profile` -- modify the account connected to the bearer token in the `Authentication` header.
|
/// `POST /user/profile` -- modify the account connected to the bearer token in the `Authentication` header.
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub async fn user_profile_post(
|
pub async fn user_profile_post(
|
||||||
TypedHeader(Authorization(bearer_token)): TypedHeader<Authorization<Bearer>>,
|
|
||||||
ClientIp(ip): ClientIp,
|
|
||||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||||
|
TypedHeader(Authorization(bearer_token)): TypedHeader<Authorization<Bearer>>,
|
||||||
Json(payload): Json<UserProfilePost>,
|
Json(payload): Json<UserProfilePost>,
|
||||||
) -> FrontendResult {
|
) -> FrontendResult {
|
||||||
login_is_authorized(&app, ip).await?;
|
let user = ProtectedAction::UserProfilePost(payload.primary_address)
|
||||||
|
.authorize(app.as_ref(), bearer_token)
|
||||||
let user = ProtectedAction::PostUser(payload.primary_address)
|
|
||||||
.verify(app.as_ref(), bearer_token)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut user: user::ActiveModel = user.into();
|
let mut user: user::ActiveModel = user.into();
|
||||||
|
|
||||||
// TODO: rate limit by user, too?
|
// TODO: require a message from the new address to finish the change
|
||||||
|
if let Some(new_primary_address) = payload.new_primary_address {
|
||||||
|
if new_primary_address.is_zero() {
|
||||||
|
// TODO: allow this if some other authentication method is set
|
||||||
|
return Err(anyhow::anyhow!("cannot clear primary address").into());
|
||||||
|
} else {
|
||||||
|
let new_primary_address = Vec::from(new_primary_address.as_ref());
|
||||||
|
|
||||||
// TODO: allow changing the primary address, too. require a message from the new address to finish the change
|
user.address = sea_orm::Set(new_primary_address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(x) = payload.email {
|
if let Some(x) = payload.email {
|
||||||
// TODO: only Set if no change
|
// TODO: only Set if no change
|
||||||
if x.is_empty() {
|
if x.is_empty() {
|
||||||
user.email = sea_orm::Set(None);
|
user.email = sea_orm::Set(None);
|
||||||
} else {
|
} else {
|
||||||
|
// TODO: do some basic validation
|
||||||
|
// TODO: don't set immediatly, send a confirmation email first
|
||||||
user.email = sea_orm::Set(Some(x));
|
user.email = sea_orm::Set(Some(x));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -393,7 +401,8 @@ pub async fn user_profile_post(
|
|||||||
|
|
||||||
user.save(&db_conn).await?;
|
user.save(&db_conn).await?;
|
||||||
|
|
||||||
todo!("finish post_user");
|
// TODO: what should this return? the user?
|
||||||
|
Ok("success".into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `GET /user/balance` -- Use a bearer token to get the user's balance and spend.
|
/// `GET /user/balance` -- Use a bearer token to get the user's balance and spend.
|
||||||
@ -411,7 +420,7 @@ pub async fn user_balance_get(
|
|||||||
todo!("user_balance_get");
|
todo!("user_balance_get");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `POST /user/balance` -- Manually process a confirmed txid to update a user's balance.
|
/// `POST /user/balance/:txhash` -- Manually process a confirmed txid to update a user's balance.
|
||||||
///
|
///
|
||||||
/// We will subscribe to events to watch for any user deposits, but sometimes events can be missed.
|
/// We will subscribe to events to watch for any user deposits, but sometimes events can be missed.
|
||||||
///
|
///
|
||||||
@ -434,6 +443,10 @@ pub async fn user_keys_get(
|
|||||||
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
Extension(app): Extension<Arc<Web3ProxyApp>>,
|
||||||
TypedHeader(Authorization(bearer_token)): TypedHeader<Authorization<Bearer>>,
|
TypedHeader(Authorization(bearer_token)): TypedHeader<Authorization<Bearer>>,
|
||||||
) -> FrontendResult {
|
) -> FrontendResult {
|
||||||
|
let user = ProtectedAction::UserKeysGet
|
||||||
|
.authorize(app.as_ref(), bearer_token)
|
||||||
|
.await?;
|
||||||
|
|
||||||
todo!("user_keys_get");
|
todo!("user_keys_get");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -507,16 +520,29 @@ pub async fn user_stats_aggregate_get(
|
|||||||
/// Handle authorization for a given address and bearer token.
|
/// Handle authorization for a given address and bearer token.
|
||||||
// TODO: what roles should exist?
|
// TODO: what roles should exist?
|
||||||
enum ProtectedAction {
|
enum ProtectedAction {
|
||||||
PostUser(Address),
|
UserKeysGet,
|
||||||
|
UserProfilePost(Address),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProtectedAction {
|
impl ProtectedAction {
|
||||||
/// Verify that the given bearer token and address are allowed to take the specified action.
|
/// Verify that the given bearer token and address are allowed to take the specified action.
|
||||||
async fn verify(self, app: &Web3ProxyApp, bearer: Bearer) -> anyhow::Result<user::Model> {
|
/// This includes concurrent request limiting.
|
||||||
|
async fn authorize(self, app: &Web3ProxyApp, bearer: Bearer) -> anyhow::Result<user::Model> {
|
||||||
// get the attached address from redis for the given auth_token.
|
// get the attached address from redis for the given auth_token.
|
||||||
let mut redis_conn = app.redis_conn().await?;
|
let mut redis_conn = app.redis_conn().await?;
|
||||||
|
|
||||||
// TODO: move this to a helper function
|
// limit concurrent requests
|
||||||
|
let semaphore = app
|
||||||
|
.bearer_token_semaphores
|
||||||
|
.get_with(bearer.token().to_string(), async move {
|
||||||
|
let s = Semaphore::new(app.config.bearer_token_max_concurrent_requests as usize);
|
||||||
|
Arc::new(s)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let _semaphore_permit = semaphore.acquire().await?;
|
||||||
|
|
||||||
|
// get the user id for this bearer token
|
||||||
|
// TODO: move redis key building to a helper function
|
||||||
let bearer_cache_key = format!("bearer:{}", bearer.token());
|
let bearer_cache_key = format!("bearer:{}", bearer.token());
|
||||||
|
|
||||||
// TODO: move this to a helper function
|
// TODO: move this to a helper function
|
||||||
@ -526,18 +552,20 @@ impl ProtectedAction {
|
|||||||
.context("fetching bearer cache key from redis")?
|
.context("fetching bearer cache key from redis")?
|
||||||
.context("unknown bearer token")?;
|
.context("unknown bearer token")?;
|
||||||
|
|
||||||
|
// turn user id into a user
|
||||||
let db_conn = app.db_conn().context("Getting database connection")?;
|
let db_conn = app.db_conn().context("Getting database connection")?;
|
||||||
|
let user = user::Entity::find_by_id(user_id)
|
||||||
// turn user key id into a user key
|
|
||||||
let user_data = user::Entity::find_by_id(user_id)
|
|
||||||
.one(&db_conn)
|
.one(&db_conn)
|
||||||
.await
|
.await
|
||||||
.context("fetching user from db by id")?
|
.context("fetching user from db by id")?
|
||||||
.context("unknown user id")?;
|
.context("unknown user id")?;
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
Self::PostUser(primary_address) => {
|
Self::UserKeysGet => {
|
||||||
let user_address = Address::from_slice(&user_data.address);
|
// no extra checks needed. bearer token gave us a user
|
||||||
|
}
|
||||||
|
Self::UserProfilePost(primary_address) => {
|
||||||
|
let user_address = Address::from_slice(&user.address);
|
||||||
|
|
||||||
if user_address != primary_address {
|
if user_address != primary_address {
|
||||||
// TODO: check secondary users
|
// TODO: check secondary users
|
||||||
@ -546,6 +574,6 @@ impl ProtectedAction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(user_data)
|
Ok(user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user