Fix Issue 65 Pt. 2 (#153)
* change subuser balance logic and add message to access denied errors * clearer import * comments * comments
This commit is contained in:
parent
ebceb0d7d6
commit
280e2075d3
@ -57,11 +57,11 @@ pub async fn query_admin_modify_usertier<'a>(
|
||||
trace!(%caller_id, "query_admin_modify_usertier");
|
||||
|
||||
// Check if the caller is an admin (i.e. if he is in an admin table)
|
||||
let _admin: admin::Model = admin::Entity::find()
|
||||
let _admin = admin::Entity::find()
|
||||
.filter(admin::Column::UserId.eq(caller_id))
|
||||
.one(db_conn)
|
||||
.await?
|
||||
.ok_or(Web3ProxyError::AccessDenied)?;
|
||||
.ok_or(Web3ProxyError::AccessDenied("not an admin".into()))?;
|
||||
|
||||
// If we are here, that means an admin was found, and we can safely proceed
|
||||
|
||||
|
@ -110,8 +110,6 @@ pub struct Web3ProxyApp {
|
||||
pub frontend_ip_rate_limiter: Option<DeferredRateLimiter<IpAddr>>,
|
||||
/// rate limit authenticated users
|
||||
pub frontend_registered_user_rate_limiter: Option<DeferredRateLimiter<u64>>,
|
||||
/// Optional time series database for making pretty graphs that load quickly
|
||||
pub influxdb_client: Option<influxdb2::Client>,
|
||||
/// concurrent/parallel request limits for anonymous users
|
||||
pub ip_semaphores: Cache<IpAddr, Arc<Semaphore>>,
|
||||
pub kafka_producer: Option<rdkafka::producer::FutureProducer>,
|
||||
@ -138,6 +136,10 @@ pub struct Web3ProxyApp {
|
||||
/// channel for sending stats in a background task
|
||||
pub stat_sender: Option<flume::Sender<AppStat>>,
|
||||
|
||||
/// Optional time series database for making pretty graphs that load quickly
|
||||
influxdb_client: Option<influxdb2::Client>,
|
||||
/// Simple way to connect ethers Contracsts to the proxy
|
||||
/// TODO: make this more efficient
|
||||
internal_provider: OnceCell<Arc<EthersHttpProvider>>,
|
||||
}
|
||||
|
||||
@ -709,6 +711,10 @@ impl Web3ProxyApp {
|
||||
self.watch_consensus_head_receiver.clone()
|
||||
}
|
||||
|
||||
pub fn influxdb_client(&self) -> Web3ProxyResult<&influxdb2::Client> {
|
||||
self.influxdb_client.as_ref().ok_or(Web3ProxyError::NoDatabase)
|
||||
}
|
||||
|
||||
/// an ethers provider that you can use with ether's abigen.
|
||||
/// this works for now, but I don't like it
|
||||
/// TODO: I would much prefer we figure out the traits and `impl JsonRpcClient for Web3ProxyApp`
|
||||
@ -1609,7 +1615,7 @@ impl Web3ProxyApp {
|
||||
method => {
|
||||
if method.starts_with("admin_") {
|
||||
// TODO: emit a stat? will probably just be noise
|
||||
return Err(Web3ProxyError::AccessDenied);
|
||||
return Err(Web3ProxyError::AccessDenied("admin methods are not allowed".into()));
|
||||
}
|
||||
|
||||
// TODO: if no servers synced, wait for them to be synced? probably better to error and let haproxy retry another server
|
||||
|
@ -44,8 +44,9 @@ impl From<Web3ProxyError> for Web3ProxyResult<()> {
|
||||
#[derive(Debug, Display, Error, From)]
|
||||
pub enum Web3ProxyError {
|
||||
Abi(ethers::abi::Error),
|
||||
AccessDenied,
|
||||
AccessDeniedNoSubuser,
|
||||
#[error(ignore)]
|
||||
#[from(ignore)]
|
||||
AccessDenied(Cow<'static, str>),
|
||||
#[error(ignore)]
|
||||
Anyhow(anyhow::Error),
|
||||
Arc(Arc<Self>),
|
||||
@ -117,6 +118,7 @@ pub enum Web3ProxyError {
|
||||
},
|
||||
NotFound,
|
||||
#[error(ignore)]
|
||||
#[from(ignore)]
|
||||
NotImplemented(Cow<'static, str>),
|
||||
NoVolatileRedisDatabase,
|
||||
OriginRequired,
|
||||
@ -186,24 +188,13 @@ impl Web3ProxyError {
|
||||
},
|
||||
)
|
||||
}
|
||||
Self::AccessDenied => {
|
||||
Self::AccessDenied(msg) => {
|
||||
// TODO: attach something to this trace. probably don't include much in the message though. don't want to leak creds by accident
|
||||
trace!("access denied");
|
||||
trace!(%msg, "access denied");
|
||||
(
|
||||
StatusCode::FORBIDDEN,
|
||||
JsonRpcErrorData {
|
||||
message: "FORBIDDEN".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(),
|
||||
message: format!("FORBIDDEN: {}", msg).into(),
|
||||
code: StatusCode::FORBIDDEN.as_u16().into(),
|
||||
data: None,
|
||||
},
|
||||
|
@ -67,10 +67,7 @@ pub async fn admin_increase_balance(
|
||||
.filter(admin::Column::UserId.eq(caller_id))
|
||||
.one(&txn)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
warn!(%caller_id, "not an admin");
|
||||
Web3ProxyError::AccessDenied
|
||||
})?;
|
||||
.ok_or_else(|| Web3ProxyError::AccessDenied("not an admin".into()))?;
|
||||
|
||||
let user_entry: user::Model = user::Entity::find()
|
||||
.filter(user::Column::Address.eq(payload.user_address.as_bytes()))
|
||||
@ -215,7 +212,7 @@ pub async fn admin_imitate_login_get(
|
||||
.filter(user::Column::Address.eq(admin_address.as_bytes()))
|
||||
.one(db_replica.as_ref())
|
||||
.await?
|
||||
.ok_or(Web3ProxyError::AccessDenied)?;
|
||||
.ok_or(Web3ProxyError::AccessDenied("not an admin".into()))?;
|
||||
|
||||
// Get the user that we want to imitate from the read-only database (their id ...)
|
||||
// TODO: Only get the id, not the whole user object ...
|
||||
|
@ -122,7 +122,7 @@ pub async fn user_balance_post(
|
||||
|
||||
(authorization, None)
|
||||
} else {
|
||||
return Err(Web3ProxyError::AccessDenied);
|
||||
return Err(Web3ProxyError::AccessDenied("no bearer token or ip".into()));
|
||||
};
|
||||
|
||||
// Get the transaction hash
|
||||
|
@ -190,7 +190,9 @@ pub async fn rpc_keys_management(
|
||||
{
|
||||
Ok(rpc_key.into_active_model())
|
||||
} else {
|
||||
Err(Web3ProxyError::AccessDenied)
|
||||
Err(Web3ProxyError::AccessDenied(
|
||||
"secondary user is not an admin or owner".into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
Some((_, None)) => Err(Web3ProxyError::BadResponse(
|
||||
|
@ -44,7 +44,7 @@ pub async fn get_user_id_from_params(
|
||||
.filter(login::Column::BearerToken.eq(user_bearer_token.uuid()))
|
||||
.one(db_replica.as_ref())
|
||||
.await?
|
||||
.ok_or(Web3ProxyError::AccessDenied)?;
|
||||
.ok_or(Web3ProxyError::AccessDenied("unknown bearer token".into()))?;
|
||||
|
||||
// if expired, delete ALL expired logins
|
||||
let now = Utc::now();
|
||||
@ -59,7 +59,7 @@ pub async fn get_user_id_from_params(
|
||||
// TODO: emit a stat? if this is high something weird might be happening
|
||||
trace!("cleared expired logins: {:?}", delete_result);
|
||||
|
||||
return Err(Web3ProxyError::AccessDenied);
|
||||
return Err(Web3ProxyError::AccessDenied("login expired".into()));
|
||||
}
|
||||
|
||||
save_to_redis = true;
|
||||
@ -75,7 +75,9 @@ pub async fn get_user_id_from_params(
|
||||
let user_id: u64 = user_id.parse().context("Parsing user_id param")?;
|
||||
|
||||
if bearer_user_id != user_id {
|
||||
return Err(Web3ProxyError::AccessDenied);
|
||||
return Err(Web3ProxyError::AccessDenied(
|
||||
"bearer_user_id and user_id mismatch".into(),
|
||||
));
|
||||
}
|
||||
|
||||
if save_to_redis {
|
||||
@ -102,7 +104,9 @@ pub async fn get_user_id_from_params(
|
||||
// TODO: proper error code from a useful error code
|
||||
// TODO: maybe instead of this sharp edged warn, we have a config value?
|
||||
// TODO: check config for if we should deny or allow this
|
||||
Err(Web3ProxyError::AccessDenied)
|
||||
Err(Web3ProxyError::AccessDenied(
|
||||
"bearer token required when requesting a specific id".into(),
|
||||
))
|
||||
// // TODO: make this a flag
|
||||
// warn!("allowing without auth during development!");
|
||||
// Ok(x.parse()?)
|
||||
|
@ -314,7 +314,7 @@ impl Web3Rpc {
|
||||
let head_block_num = self
|
||||
.internal_request::<_, U256>(
|
||||
"eth_blockNumber",
|
||||
&None::<()>,
|
||||
&[(); 0],
|
||||
// error here are expected, so keep the level low
|
||||
Some(Level::DEBUG.into()),
|
||||
Some(2),
|
||||
@ -431,7 +431,7 @@ impl Web3Rpc {
|
||||
let found_chain_id: U64 = self
|
||||
.internal_request(
|
||||
"eth_chainId",
|
||||
&None::<()>,
|
||||
&[(); 0],
|
||||
Some(Level::TRACE.into()),
|
||||
Some(2),
|
||||
Some(Duration::from_secs(5)),
|
||||
|
@ -15,7 +15,7 @@ use axum::{
|
||||
Json, TypedHeader,
|
||||
};
|
||||
use entities::sea_orm_active_enums::Role;
|
||||
use entities::{balance, rpc_key, secondary_user};
|
||||
use entities::{balance, rpc_key, secondary_user, user, user_tier};
|
||||
use fstrings::{f, format_args_f};
|
||||
use hashbrown::HashMap;
|
||||
use influxdb2::api::query::FluxRecord;
|
||||
@ -32,59 +32,76 @@ pub async fn query_user_stats<'a>(
|
||||
params: &'a HashMap<String, String>,
|
||||
stat_response_type: StatType,
|
||||
) -> Web3ProxyResponse {
|
||||
let (caller_user_id, _semaphore) = match bearer {
|
||||
let (caller_user, _semaphore) = match bearer {
|
||||
Some(TypedHeader(Authorization(bearer))) => {
|
||||
let (user, semaphore) = app.bearer_is_authorized(bearer).await?;
|
||||
(user.id, Some(semaphore))
|
||||
|
||||
(Some(user), Some(semaphore))
|
||||
}
|
||||
None => (0, None),
|
||||
None => (None, None),
|
||||
};
|
||||
|
||||
// Return an error if the bearer is **not** set, but the StatType is Detailed
|
||||
if stat_response_type == StatType::Detailed && caller_user_id == 0 {
|
||||
return Err(Web3ProxyError::BadRequest(
|
||||
if stat_response_type == StatType::Detailed && caller_user.is_none() {
|
||||
return Err(Web3ProxyError::AccessDenied(
|
||||
"Detailed Stats Response requires you to authorize with a bearer token".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let db_replica = app.db_replica()?;
|
||||
|
||||
// Read the (optional) user-id from the request, this is the logic for subusers
|
||||
// If there is no bearer token, this is not allowed
|
||||
let user_id: u64 = params
|
||||
.get("user_id")
|
||||
.and_then(|x| x.parse::<u64>().ok())
|
||||
.unwrap_or(caller_user_id);
|
||||
.unwrap_or_else(|| caller_user.as_ref().map(|x| x.id).unwrap_or_default());
|
||||
|
||||
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
|
||||
if user_id != 0 {
|
||||
match balance::Entity::find()
|
||||
// Only allow stats if the user has an active premium role
|
||||
// TODO: move this to a helper. it should be simple to check that a user has an active premium account
|
||||
if let Some(caller_user) = &caller_user {
|
||||
// get the balance of the user whose stats we are going to fetch (might be self, but might be another user)
|
||||
let (total_deposits, total_spent) = 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_deposits - user_balance.total_spent_outside_free_tier
|
||||
<= Decimal::from(0)
|
||||
{
|
||||
trace!("User has 0 balance");
|
||||
return Err(Web3ProxyError::PaymentRequired);
|
||||
}
|
||||
// Otherwise make the user pass
|
||||
}
|
||||
None => {
|
||||
trace!("User does not have a balance record, implying that he has no balance. Users must have a balance to access their stats dashboards");
|
||||
Some(user_balance) => (
|
||||
user_balance.total_deposits,
|
||||
user_balance.total_spent_outside_free_tier,
|
||||
),
|
||||
None => (0.into(), 0.into()),
|
||||
};
|
||||
|
||||
let balance_remaining = total_deposits - total_spent;
|
||||
|
||||
// TODO: We should add the threshold that determines if a user is premium into app.config. hard coding to $10 works for now
|
||||
if total_deposits < Decimal::from(10) || balance_remaining <= Decimal::from(0) {
|
||||
// get the user tier so we can see if it is a tier that has downgrades
|
||||
let relevant_balance_user_tier_id = if user_id == caller_user.id {
|
||||
caller_user.user_tier_id
|
||||
} else {
|
||||
let user = user::Entity::find_by_id(user_id)
|
||||
.one(db_replica.as_ref())
|
||||
.await?
|
||||
.web3_context("user_id not found")?;
|
||||
|
||||
user.user_tier_id
|
||||
};
|
||||
let user_tier = user_tier::Entity::find_by_id(relevant_balance_user_tier_id)
|
||||
.one(db_replica.as_ref())
|
||||
.await?
|
||||
.web3_context("user_tier not found")?;
|
||||
|
||||
if user_tier.downgrade_tier_id.is_some() {
|
||||
trace!(%user_id, "User does not have enough balance to qualify for premium");
|
||||
return Err(Web3ProxyError::PaymentRequired);
|
||||
}
|
||||
}
|
||||
|
||||
// (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<u64> = rpc_key::Entity::find()
|
||||
if user_id != caller_user.id {
|
||||
// check that there is at least on rpc-keys owned by the requested user and related to the caller user
|
||||
let user_rpc_key_ids: Vec<u64> = rpc_key::Entity::find()
|
||||
.filter(rpc_key::Column::UserId.eq(user_id))
|
||||
.all(db_replica.as_ref())
|
||||
.await?
|
||||
@ -92,31 +109,26 @@ pub async fn query_user_stats<'a>(
|
||||
.map(|x| x.id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
match secondary_user::Entity::find()
|
||||
.filter(secondary_user::Column::UserId.eq(caller_user_id))
|
||||
.filter(secondary_user::Column::RpcSecretKeyId.is_in(user_rpc_keys))
|
||||
if secondary_user::Entity::find()
|
||||
.filter(secondary_user::Column::UserId.eq(caller_user.id))
|
||||
.filter(secondary_user::Column::RpcSecretKeyId.is_in(user_rpc_key_ids))
|
||||
.filter(secondary_user::Column::Role.ne(Role::Collaborator))
|
||||
.one(db_replica.as_ref())
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
Some(secondary_user_record) => {
|
||||
if secondary_user_record.role == Role::Collaborator {
|
||||
trace!("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);
|
||||
}
|
||||
return Err(Web3ProxyError::AccessDenied(
|
||||
"Not a subuser of the given user_id".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
} else if user_id != 0 {
|
||||
return Err(Web3ProxyError::AccessDenied(
|
||||
"User Stats Response requires you to authorize with a bearer token".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// TODO: have a getter for this. do we need a connection pool on it?
|
||||
let influxdb_client = app
|
||||
.influxdb_client
|
||||
.as_ref()
|
||||
.context("query_user_stats needs an influxdb client")?;
|
||||
let influxdb_client = app.influxdb_client()?;
|
||||
|
||||
let query_window_seconds = get_query_window_seconds_from_params(params)?;
|
||||
let query_start = get_query_start_from_params(params)?.timestamp();
|
||||
|
Loading…
Reference in New Issue
Block a user