From 935681fde7919986b7978a848446d84aad15d1dc Mon Sep 17 00:00:00 2001 From: yenicelik Date: Sun, 25 Jun 2023 14:29:54 -0400 Subject: [PATCH 1/2] stats subuser + premium access logic --- web3_proxy/src/errors.rs | 24 ++++++++ web3_proxy/src/stats/influxdb_queries.rs | 70 ++++++++++++++++++++++-- 2 files changed, 90 insertions(+), 4 deletions(-) diff --git a/web3_proxy/src/errors.rs b/web3_proxy/src/errors.rs index 76f11cd9..0db91821 100644 --- a/web3_proxy/src/errors.rs +++ b/web3_proxy/src/errors.rs @@ -44,6 +44,8 @@ impl From for Web3ProxyResult<()> { pub enum Web3ProxyError { Abi(ethers::abi::Error), AccessDenied, + AccessDeniedLowBalance, + AccessDeniedNoSubuser, #[error(ignore)] Anyhow(anyhow::Error), Arc(Arc), @@ -188,6 +190,28 @@ impl Web3ProxyError { }, ) } + Self::AccessDeniedLowBalance => { + trace!("access denied due to low balance"); + ( + StatusCode::FORBIDDEN, + JsonRpcErrorData { + message: "FORBIDDEN: LOW BALANCE".into(), + code: StatusCode::FORBIDDEN.as_u16().into(), + data: None, + }, + ) + } + Self::AccessDeniedNoSubuser => { + trace!("access denied not a subuser"); + ( + StatusCode::FORBIDDEN, + JsonRpcErrorData { + message: "FORBIDDEN: NOT A SUBUSER".into(), + code: StatusCode::FORBIDDEN.as_u16().into(), + data: None, + }, + ) + } Self::Anyhow(err) => { warn!("anyhow. err={:?}", err); ( diff --git a/web3_proxy/src/stats/influxdb_queries.rs b/web3_proxy/src/stats/influxdb_queries.rs index 7f452eaa..d2380f23 100644 --- a/web3_proxy/src/stats/influxdb_queries.rs +++ b/web3_proxy/src/stats/influxdb_queries.rs @@ -15,12 +15,13 @@ use axum::{ Json, TypedHeader, }; use entities::sea_orm_active_enums::Role; -use entities::{rpc_key, secondary_user}; +use entities::{balance, rpc_key, secondary_user}; use fstrings::{f, format_args_f}; use hashbrown::HashMap; use influxdb2::api::query::FluxRecord; use influxdb2::models::Query; use migration::sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; +use rust_decimal::Decimal; use serde_json::json; use tracing::{debug, error, trace, warn}; use ulid::Ulid; @@ -28,10 +29,10 @@ use ulid::Ulid; pub async fn query_user_stats<'a>( app: &'a Web3ProxyApp, bearer: Option>>, - params: &'a HashMap, + mut params: &'a HashMap, stat_response_type: StatType, ) -> Web3ProxyResponse { - let (user_id, _semaphore) = match bearer { + let (caller_user_id, _semaphore) = match bearer { Some(TypedHeader(Authorization(bearer))) => { let (user, semaphore) = app.bearer_is_authorized(bearer).await?; (user.id, Some(semaphore)) @@ -40,14 +41,75 @@ pub async fn query_user_stats<'a>( }; // Return an error if the bearer is **not** set, but the StatType is Detailed - if stat_response_type == StatType::Detailed && user_id == 0 { + if stat_response_type == StatType::Detailed && caller_user_id == 0 { return Err(Web3ProxyError::BadRequest( "Detailed Stats Response requires you to authorize with a bearer token".into(), )); } + // Read the (optional) user-id from the request, this is the logic for subusers + let user_id: u64 = params + .get("user_id") + .and_then(|x| x.parse::().ok()) + .unwrap_or_else(|| caller_user_id); + let db_replica = app.db_replica()?; + // In any case, we don't allow stats if the target user does not have a balance + // No subuser, we can check the balance directly + match balance::Entity::find() + .filter(balance::Column::UserId.eq(user_id)) + .one(db_replica.as_ref()) + .await? + { + // TODO: We should add the threshold that determines if a user is premium into app.config or so + Some(user_balance) => { + if user_balance.total_spent_outside_free_tier - user_balance.total_deposits + < Decimal::from(0) + { + debug!("User has 0 balance"); + return Err(Web3ProxyError::AccessDeniedLowBalance); + } + // Otherwise make the user pass + } + None => { + debug!("User does not have a balance record, implying that he has no balance. Users must have a balance to access their stats dashboards"); + return Err(Web3ProxyError::AccessDeniedLowBalance); + } + } + + // (Possible) subuser relation + // Check if the caller is a proper subuser (there is a subuser record) + // Check if the subuser has more than collaborator status + if user_id != caller_user_id { + // Find all rpc-keys related to the caller user + let user_rpc_keys: Vec = rpc_key::Entity::find() + .filter(rpc_key::Column::UserId.eq(user_id)) + .all(db_replica.as_ref()) + .await? + .into_iter() + .map(|x| x.id) + .collect::>(); + + match secondary_user::Entity::find() + .filter(secondary_user::Column::UserId.eq(caller_user_id)) + .filter(secondary_user::Column::RpcSecretKeyId.is_in(user_rpc_keys)) + .one(db_replica.as_ref()) + .await? + { + Some(secondary_user_record) => { + if secondary_user_record.role == Role::Collaborator { + debug!("Subuser is only a collaborator, collaborators cannot see stats"); + return Err(Web3ProxyError::AccessDenied); + } + } + None => { + // Then we must do an access denied + return Err(Web3ProxyError::AccessDeniedNoSubuser); + } + } + } + // TODO: have a getter for this. do we need a connection pool on it? let influxdb_client = app .influxdb_client From 4c157cfcf73639075056c93680ea0faab2477afb Mon Sep 17 00:00:00 2001 From: yenicelik Date: Sun, 25 Jun 2023 15:05:27 -0400 Subject: [PATCH 2/2] made paymentrequired error code instead of introducing a new one --- web3_proxy/src/errors.rs | 12 ------------ web3_proxy/src/stats/influxdb_queries.rs | 10 +++++----- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/web3_proxy/src/errors.rs b/web3_proxy/src/errors.rs index 0db91821..e0241502 100644 --- a/web3_proxy/src/errors.rs +++ b/web3_proxy/src/errors.rs @@ -44,7 +44,6 @@ impl From for Web3ProxyResult<()> { pub enum Web3ProxyError { Abi(ethers::abi::Error), AccessDenied, - AccessDeniedLowBalance, AccessDeniedNoSubuser, #[error(ignore)] Anyhow(anyhow::Error), @@ -190,17 +189,6 @@ impl Web3ProxyError { }, ) } - Self::AccessDeniedLowBalance => { - trace!("access denied due to low balance"); - ( - StatusCode::FORBIDDEN, - JsonRpcErrorData { - message: "FORBIDDEN: LOW BALANCE".into(), - code: StatusCode::FORBIDDEN.as_u16().into(), - data: None, - }, - ) - } Self::AccessDeniedNoSubuser => { trace!("access denied not a subuser"); ( diff --git a/web3_proxy/src/stats/influxdb_queries.rs b/web3_proxy/src/stats/influxdb_queries.rs index d2380f23..e1f432ee 100644 --- a/web3_proxy/src/stats/influxdb_queries.rs +++ b/web3_proxy/src/stats/influxdb_queries.rs @@ -67,14 +67,14 @@ pub async fn query_user_stats<'a>( if user_balance.total_spent_outside_free_tier - user_balance.total_deposits < Decimal::from(0) { - debug!("User has 0 balance"); - return Err(Web3ProxyError::AccessDeniedLowBalance); + trace!("User has 0 balance"); + return Err(Web3ProxyError::PaymentRequired); } // Otherwise make the user pass } None => { - debug!("User does not have a balance record, implying that he has no balance. Users must have a balance to access their stats dashboards"); - return Err(Web3ProxyError::AccessDeniedLowBalance); + trace!("User does not have a balance record, implying that he has no balance. Users must have a balance to access their stats dashboards"); + return Err(Web3ProxyError::PaymentRequired); } } @@ -99,7 +99,7 @@ pub async fn query_user_stats<'a>( { Some(secondary_user_record) => { if secondary_user_record.role == Role::Collaborator { - debug!("Subuser is only a collaborator, collaborators cannot see stats"); + trace!("Subuser is only a collaborator, collaborators cannot see stats"); return Err(Web3ProxyError::AccessDenied); } }