web3-proxy/web3_proxy/src/frontend/users/subuser.rs
Bryan Stitt 8a097dabbe
Bryan devel 2023-05-12 (#67)
* add minor todo

* BadRequest instead of web3_context

* more bad request error codes

* use tokio-uring for the tcp listener

* clear block instead of panic

* clone earlier

* more watch channels instead of rwlocks

* drop uring for now (its single threaded) and combine get/post/put routes

* clean up iter vs into_iter and unnecessary collect

* arcswap instead of rwlock for Web3Rpcs.by_name

* cargo upgrade

* uuid fast-rng and alphabetize

* if protected rpcs, only use protected rpcs

* listenfd

* make connectinfo optional

* try_get_with_by_ref instead of try_get_with

* anyhow ensure. and try_get_with_as_ref isn't actually needed

* fix feature flags

* more refs and less clone

* automatic retry for eth_getTransactionReceipt and eth_getTransactionByHash

thanks for the report Lefteris @ Rotki

* ArcSwap for provider

* set archive_request to true on transaction retrying

* merge durable stats

* Revert "ArcSwap for provider"

This reverts commit 166d77f204cde9fa7722c0cefecbb27008749d47.

* comments

* less clones

* more refs

* fix test

* add optional mimalloc feature

* remove stale dependency

* sort

* cargo upgrade

* lint constants

* add todo

* another todo

* lint

* anyhow::ensure instead of panic

* allow rpc_accounting_v2 entries for requests without an rpc key
2023-05-12 15:15:32 -07:00

429 lines
16 KiB
Rust

//! Handle subusers, viewing subusers, and viewing accessible rpc-keys
use crate::app::Web3ProxyApp;
use crate::frontend::authorization::RpcSecretKey;
use crate::frontend::errors::{Web3ProxyError, Web3ProxyErrorContext, Web3ProxyResponse};
use anyhow::Context;
use axum::{
extract::Query,
headers::{authorization::Bearer, Authorization},
response::IntoResponse,
Extension, Json, TypedHeader,
};
use axum_macros::debug_handler;
use entities::sea_orm_active_enums::Role;
use entities::{balance, rpc_key, secondary_user, user, user_tier};
use ethers::types::Address;
use hashbrown::HashMap;
use http::StatusCode;
use log::{debug, warn};
use migration::sea_orm;
use migration::sea_orm::prelude::Decimal;
use migration::sea_orm::ActiveModelTrait;
use migration::sea_orm::ColumnTrait;
use migration::sea_orm::EntityTrait;
use migration::sea_orm::IntoActiveModel;
use migration::sea_orm::QueryFilter;
use migration::sea_orm::TransactionTrait;
use serde_json::json;
use std::sync::Arc;
use ulid::{self, Ulid};
use uuid::Uuid;
pub async fn get_keys_as_subuser(
Extension(app): Extension<Arc<Web3ProxyApp>>,
TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
Query(_params): Query<HashMap<String, String>>,
) -> Web3ProxyResponse {
// First, authenticate
let (subuser, _semaphore) = app.bearer_is_authorized(bearer).await?;
let db_replica = app
.db_replica()
.context("getting replica db for user's revert logs")?;
// TODO: JOIN over RPC_KEY, SUBUSER, PRIMARY_USER and return these items
// Get all secondary users that have access to this rpc key
let secondary_user_entities = secondary_user::Entity::find()
.filter(secondary_user::Column::UserId.eq(subuser.id))
.all(db_replica.conn())
.await?
.into_iter()
.map(|x| (x.rpc_secret_key_id, x))
.collect::<HashMap<u64, secondary_user::Model>>();
// Now return a list of all subusers (their wallets)
let rpc_key_entities: Vec<(rpc_key::Model, Option<user::Model>)> = rpc_key::Entity::find()
.filter(
rpc_key::Column::Id.is_in(
secondary_user_entities
.iter()
.map(|(x, _)| *x)
.collect::<Vec<_>>(),
),
)
.find_also_related(user::Entity)
.all(db_replica.conn())
.await?;
// TODO: Merge rpc-key with respective user (join is probably easiest ...)
// Now return the list
let response_json = json!({
"subuser": format!("{:?}", Address::from_slice(&subuser.address)),
"rpc_keys": rpc_key_entities
.into_iter()
.flat_map(|(rpc_key, rpc_owner)| {
match rpc_owner {
Some(inner_rpc_owner) => {
let mut tmp = HashMap::new();
tmp.insert("rpc-key", serde_json::Value::String(Ulid::from(rpc_key.secret_key).to_string()));
tmp.insert("rpc-owner", serde_json::Value::String(format!("{:?}", Address::from_slice(&inner_rpc_owner.address))));
tmp.insert("role", serde_json::Value::String(format!("{:?}", secondary_user_entities.get(&rpc_key.id).unwrap().role))); // .to_string() returns ugly "'...'"
Some(tmp)
},
None => {
// error!("Found RPC secret key with no user!".to_owned());
None
}
}
})
.collect::<Vec::<_>>(),
});
Ok(Json(response_json).into_response())
}
pub async fn get_subusers(
Extension(app): Extension<Arc<Web3ProxyApp>>,
TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
Query(mut params): Query<HashMap<String, String>>,
) -> Web3ProxyResponse {
// First, authenticate
let (user, _semaphore) = app.bearer_is_authorized(bearer).await?;
let db_replica = app
.db_replica()
.context("getting replica db for user's revert logs")?;
// Second, check if the user is a premium user
let user_tier = user_tier::Entity::find()
.filter(user_tier::Column::Id.eq(user.user_tier_id))
.one(db_replica.conn())
.await?
.ok_or(Web3ProxyError::BadRequest(
"Could not find user in db although bearer token is there!".to_string(),
))?;
debug!("User tier is: {:?}", user_tier);
// TODO: This shouldn't be hardcoded. Also, it should be an enum, not sth like this ...
if user_tier.id != 6 {
return Err(
anyhow::anyhow!("User is not premium. Must be premium to create referrals.").into(),
);
}
let rpc_key: Ulid = params
.remove("rpc_key")
// TODO: map_err so this becomes a 500. routing must be bad
.ok_or(Web3ProxyError::BadRequest(
"You have not provided the 'rpc_key' whose access to modify".to_string(),
))?
.parse()
.context(format!("unable to parse rpc_key {:?}", params))?;
// Get the rpc key id
let rpc_key = rpc_key::Entity::find()
.filter(rpc_key::Column::SecretKey.eq(Uuid::from(rpc_key)))
.one(db_replica.conn())
.await?
.ok_or(Web3ProxyError::BadRequest(
"The provided RPC key cannot be found".to_string(),
))?;
// Get all secondary users that have access to this rpc key
let secondary_user_entities = secondary_user::Entity::find()
.filter(secondary_user::Column::RpcSecretKeyId.eq(rpc_key.id))
.all(db_replica.conn())
.await?
.into_iter()
.map(|x| (x.user_id, x))
.collect::<HashMap<u64, secondary_user::Model>>();
// Now return a list of all subusers (their wallets)
let subusers = user::Entity::find()
.filter(
user::Column::Id.is_in(
secondary_user_entities
.iter()
.map(|(x, _)| *x)
.collect::<Vec<_>>(),
),
)
.all(db_replica.conn())
.await?;
warn!("Subusers are: {:?}", subusers);
// Now return the list
let response_json = json!({
"caller": format!("{:?}", Address::from_slice(&user.address)),
"rpc_key": rpc_key,
"subusers": subusers
.into_iter()
.map(|subuser| {
let mut tmp = HashMap::new();
// .encode_hex()
tmp.insert("address", serde_json::Value::String(format!("{:?}", Address::from_slice(&subuser.address))));
tmp.insert("role", serde_json::Value::String(format!("{:?}", secondary_user_entities.get(&subuser.id).unwrap().role)));
json!(tmp)
})
.collect::<Vec::<_>>(),
});
Ok(Json(response_json).into_response())
}
#[debug_handler]
pub async fn modify_subuser(
Extension(app): Extension<Arc<Web3ProxyApp>>,
TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
Query(mut params): Query<HashMap<String, String>>,
) -> Web3ProxyResponse {
// First, authenticate
let (user, _semaphore) = app.bearer_is_authorized(bearer).await?;
let db_replica = app
.db_replica()
.context("getting replica db for user's revert logs")?;
// Second, check if the user is a premium user
let user_tier = user_tier::Entity::find()
.filter(user_tier::Column::Id.eq(user.user_tier_id))
.one(db_replica.conn())
.await?
.ok_or(Web3ProxyError::BadRequest(
"Could not find user in db although bearer token is there!".to_string(),
))?;
debug!("User tier is: {:?}", user_tier);
// TODO: This shouldn't be hardcoded. Also, it should be an enum, not sth like this ...
if user_tier.id != 6 {
return Err(
anyhow::anyhow!("User is not premium. Must be premium to create referrals.").into(),
);
}
warn!("Parameters are: {:?}", params);
// Then, distinguish the endpoint to modify
let rpc_key_to_modify: Ulid = params
.remove("rpc_key")
// TODO: map_err so this becomes a 500. routing must be bad
.ok_or(Web3ProxyError::BadRequest(
"You have not provided the 'rpc_key' whose access to modify".to_string(),
))?
.parse::<Ulid>()
.context(format!("unable to parse rpc_key {:?}", params))?;
// let rpc_key_to_modify: Uuid = ulid::serde::ulid_as_uuid::deserialize(rpc_key_to_modify)?;
let subuser_address: Address = params
.remove("subuser_address")
// TODO: map_err so this becomes a 500. routing must be bad
.ok_or(Web3ProxyError::BadRequest(
"You have not provided the 'user_address' whose access to modify".to_string(),
))?
.parse()
.context(format!("unable to parse subuser_address {:?}", params))?;
// TODO: Check subuser address for eip55 checksum
let keep_subuser: bool = match params
.remove("new_status")
// TODO: map_err so this becomes a 500. routing must be bad
.ok_or(Web3ProxyError::BadRequest(
"You have not provided the new_stats key in the request".to_string(),
))?
.as_str()
{
"upsert" => Ok(true),
"remove" => Ok(false),
_ => Err(Web3ProxyError::BadRequest(
"'new_status' must be one of 'upsert' or 'remove'".to_string(),
)),
}?;
let new_role: Role = match params
.remove("new_role")
// TODO: map_err so this becomes a 500. routing must be bad
.ok_or(Web3ProxyError::BadRequest(
"You have not provided the new_stats key in the request".to_string(),
))?
.as_str()
{
// TODO: Technically, if this is the new owner, we should transpose the full table.
// For now, let's just not allow the primary owner to just delete his account
// (if there is even such a functionality)
"owner" => Ok(Role::Owner),
"admin" => Ok(Role::Admin),
"collaborator" => Ok(Role::Collaborator),
_ => Err(Web3ProxyError::BadRequest(
"'new_role' must be one of 'owner', 'admin', 'collaborator'".to_string(),
)),
}?;
// ---------------------------
// First, check if the user exists as a user. If not, add them
// (and also create a balance, and rpc_key, same procedure as logging in for first time)
// ---------------------------
let subuser = user::Entity::find()
.filter(user::Column::Address.eq(subuser_address.as_ref()))
.one(db_replica.conn())
.await?;
let rpc_key_entity = rpc_key::Entity::find()
.filter(rpc_key::Column::SecretKey.eq(Uuid::from(rpc_key_to_modify)))
.one(db_replica.conn())
.await?
.ok_or(Web3ProxyError::BadRequest(
"Provided RPC key does not exist!".to_owned(),
))?;
// Make sure that the user owns the rpc_key_entity
if rpc_key_entity.user_id != user.id {
return Err(Web3ProxyError::BadRequest(
"you must own the RPC for which you are giving permissions out".to_string(),
));
}
// TODO: There is a good chunk of duplicate logic as login-post. Consider refactoring ...
let db_conn = app.db_conn().web3_context("login requires a db")?;
let (subuser, _subuser_rpc_keys, _status_code) = match subuser {
None => {
let txn = db_conn.begin().await?;
// First add a user; the only thing we need from them is an address
// everything else is optional
let subuser = user::ActiveModel {
address: sea_orm::Set(subuser_address.to_fixed_bytes().into()), // Address::from_slice(
..Default::default()
};
let subuser = subuser.insert(&txn).await?;
// create the user's first api key
let rpc_secret_key = RpcSecretKey::new();
let subuser_rpc_key = rpc_key::ActiveModel {
user_id: sea_orm::Set(subuser.id),
secret_key: sea_orm::Set(rpc_secret_key.into()),
description: sea_orm::Set(None),
..Default::default()
};
let subuser_rpc_keys = vec![subuser_rpc_key
.insert(&txn)
.await
.web3_context("Failed saving new user key")?];
// We should also create the balance entry ...
let subuser_balance = balance::ActiveModel {
user_id: sea_orm::Set(subuser.id),
available_balance: sea_orm::Set(Decimal::new(0, 0)),
used_balance: sea_orm::Set(Decimal::new(0, 0)),
..Default::default()
};
subuser_balance.insert(&txn).await?;
// save the user and key to the database
txn.commit().await?;
(subuser, subuser_rpc_keys, StatusCode::CREATED)
}
Some(subuser) => {
if subuser.id == user.id {
return Err(Web3ProxyError::BadRequest(
"you cannot make a subuser out of yourself".to_string(),
));
}
// Let's say that a user that exists can actually also redeem a key in retrospect...
// the user is already registered
let subuser_rpc_keys = rpc_key::Entity::find()
.filter(rpc_key::Column::UserId.eq(subuser.id))
.all(db_replica.conn())
.await
.web3_context("failed loading user's key")?;
(subuser, subuser_rpc_keys, StatusCode::OK)
}
};
// --------------------------------
// Now apply the operation
// Either add the subuser
// Or revoke his subuser status
// --------------------------------
// Search for subuser first of all
// There should be a unique-constraint on user-id + rpc_key
let subuser_entry_secondary_user = secondary_user::Entity::find()
.filter(secondary_user::Column::UserId.eq(subuser.id))
.filter(secondary_user::Column::RpcSecretKeyId.eq(rpc_key_entity.id))
.one(db_replica.conn())
.await
.web3_context("failed using the db to check for a subuser")?;
let txn = db_conn.begin().await?;
let mut action = "no action";
match subuser_entry_secondary_user {
Some(secondary_user) => {
// In this case, remove the subuser
let mut active_subuser_entry_secondary_user = secondary_user.into_active_model();
if !keep_subuser {
// Remove the user
active_subuser_entry_secondary_user.delete(&db_conn).await?;
action = "removed";
} else {
// Just change the role
active_subuser_entry_secondary_user.role = sea_orm::Set(new_role.clone());
active_subuser_entry_secondary_user.save(&db_conn).await?;
action = "role modified";
}
}
None if keep_subuser => {
let active_subuser_entry_secondary_user = secondary_user::ActiveModel {
user_id: sea_orm::Set(subuser.id),
rpc_secret_key_id: sea_orm::Set(rpc_key_entity.id),
role: sea_orm::Set(new_role.clone()),
..Default::default()
};
active_subuser_entry_secondary_user.insert(&txn).await?;
action = "added";
}
_ => {
// Return if the user should be removed and if there is no entry;
// in this case, the user is not entered
// Return if the user should be added and there is already an entry;
// in this case, they were already added, so we can skip this
// Do nothing in this case
}
};
txn.commit().await?;
let response = (
StatusCode::OK,
Json(json!({
"rpc_key": rpc_key_to_modify,
"subuser_address": subuser_address,
"keep_user": keep_subuser,
"new_role": new_role,
"action": action
})),
)
.into_response();
// Return early if the log was added, assume there is at most one valid log per transaction
Ok(response)
}