diff --git a/entities/src/admin.rs b/entities/src/admin.rs new file mode 100644 index 00000000..dc8a737d --- /dev/null +++ b/entities/src/admin.rs @@ -0,0 +1,34 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.6 + +use crate::serialization; +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "admin")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: u64, + #[sea_orm(unique)] + pub user_id: u64, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::UserId", + to = "super::user::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + User, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/entities/src/admin_trail.rs b/entities/src/admin_trail.rs new file mode 100644 index 00000000..26ad1e3b --- /dev/null +++ b/entities/src/admin_trail.rs @@ -0,0 +1,37 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.6 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "admin_trail")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub caller: u64, + pub imitating_user: Option, + pub endpoint: String, + pub payload: String, + pub timestamp: DateTimeUtc, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::Caller", + to = "super::user::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + User2, + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::ImitatingUser", + to = "super::user::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + User1, +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/entities/src/login.rs b/entities/src/login.rs index e2600e84..f4af45a3 100644 --- a/entities/src/login.rs +++ b/entities/src/login.rs @@ -1,4 +1,4 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.5 +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.6 use crate::serialization; use sea_orm::entity::prelude::*; @@ -14,6 +14,7 @@ pub struct Model { pub bearer_token: Uuid, pub user_id: u64, pub expires_at: DateTimeUtc, + pub read_only: bool, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/entities/src/mod.rs b/entities/src/mod.rs index 5325e16d..2121477c 100644 --- a/entities/src/mod.rs +++ b/entities/src/mod.rs @@ -1,7 +1,9 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.5 +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.6 pub mod prelude; +pub mod admin; +pub mod admin_trail; pub mod login; pub mod pending_login; pub mod revert_log; diff --git a/entities/src/pending_login.rs b/entities/src/pending_login.rs index 2ec44223..196b851c 100644 --- a/entities/src/pending_login.rs +++ b/entities/src/pending_login.rs @@ -15,6 +15,7 @@ pub struct Model { #[sea_orm(column_type = "Text")] pub message: String, pub expires_at: DateTimeUtc, + pub imitating_user: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/entities/src/prelude.rs b/entities/src/prelude.rs index 218a3d47..bb19388d 100644 --- a/entities/src/prelude.rs +++ b/entities/src/prelude.rs @@ -1,5 +1,7 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.5 +pub use super::admin::Entity as Admin; +pub use super::admin_trail::Entity as AdminTrail; pub use super::login::Entity as Login; pub use super::pending_login::Entity as PendingLogin; pub use super::revert_log::Entity as RevertLog; diff --git a/migration/src/lib.rs b/migration/src/lib.rs index ca074f18..fe7e3ec6 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -12,7 +12,11 @@ mod m20221101_222349_archive_request; mod m20221108_200345_save_anon_stats; mod m20221211_124002_request_method_privacy; mod m20221213_134158_move_login_into_database; +mod m20230117_191358_admin_table; mod m20230119_204135_better_free_tier; +mod m20230130_124740_read_only_login_logic; +mod m20230130_165144_prepare_admin_imitation_pre_login; +mod m20230215_152254_admin_trail; pub struct Migrator; @@ -32,7 +36,11 @@ impl MigratorTrait for Migrator { Box::new(m20221108_200345_save_anon_stats::Migration), Box::new(m20221211_124002_request_method_privacy::Migration), Box::new(m20221213_134158_move_login_into_database::Migration), + Box::new(m20230117_191358_admin_table::Migration), Box::new(m20230119_204135_better_free_tier::Migration), + Box::new(m20230130_124740_read_only_login_logic::Migration), + Box::new(m20230130_165144_prepare_admin_imitation_pre_login::Migration), + Box::new(m20230215_152254_admin_trail::Migration), ] } } diff --git a/migration/src/m20230117_191358_admin_table.rs b/migration/src/m20230117_191358_admin_table.rs new file mode 100644 index 00000000..2505d314 --- /dev/null +++ b/migration/src/m20230117_191358_admin_table.rs @@ -0,0 +1,58 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // Replace the sample below with your own migration scripts + manager + .create_table( + Table::create() + .table(Admin::Table) + .col( + ColumnDef::new(Admin::Id) + .big_unsigned() + .not_null() + .auto_increment() + .primary_key(), + ) + .col( + ColumnDef::new(Admin::UserId) + .big_unsigned() + .unique_key() + .not_null() + ) + .foreign_key( + ForeignKey::create() + .name("fk-admin-user_id") + .from(Admin::Table, Admin::UserId) + .to(User::Table, User::Id), + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // Replace the sample below with your own migration scripts + manager + .drop_table(Table::drop().table(Admin::Table).to_owned()) + .await + } +} + +/// Learn more at https://docs.rs/sea-query#iden +#[derive(Iden)] +enum User { + Table, + Id +} + +#[derive(Iden)] +enum Admin { + Table, + Id, + UserId, +} diff --git a/migration/src/m20230130_124740_read_only_login_logic.rs b/migration/src/m20230130_124740_read_only_login_logic.rs new file mode 100644 index 00000000..064dc683 --- /dev/null +++ b/migration/src/m20230130_124740_read_only_login_logic.rs @@ -0,0 +1,42 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // Add a read-only column to the table + manager + .alter_table( + Table::alter() + .table(Login::Table) + .add_column( + ColumnDef::new(Login::ReadOnly) + .boolean() + .not_null() + ).to_owned() + ).await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // Drop the column from the table ... + manager + .alter_table( + Table::alter() + .table(Login::Table) + .drop_column(Login::ReadOnly) + .to_owned() + ).await + } +} + +/// Learn more at https://docs.rs/sea-query#iden +#[derive(Iden)] +enum Login { + Table, + Id, + BearerToken, + ReadOnly, + UserId, +} diff --git a/migration/src/m20230130_165144_prepare_admin_imitation_pre_login.rs b/migration/src/m20230130_165144_prepare_admin_imitation_pre_login.rs new file mode 100644 index 00000000..ff6ec868 --- /dev/null +++ b/migration/src/m20230130_165144_prepare_admin_imitation_pre_login.rs @@ -0,0 +1,56 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(PendingLogin::Table) + .add_column( + ColumnDef::new(PendingLogin::ImitatingUser) + .big_unsigned() + ) + .add_foreign_key(&TableForeignKey::new() + .name("fk-pending_login-imitating_user") + .from_tbl(PendingLogin::Table) + .to_tbl(User::Table) + .from_col(PendingLogin::ImitatingUser) + .to_col(User::Id) + ) + .to_owned() + ).await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(PendingLogin::Table) + .drop_foreign_key(Alias::new("fk-pending_login-imitating_user")) + .drop_column(PendingLogin::ImitatingUser) + .to_owned() + ).await + } +} + +/// Learn more at https://docs.rs/sea-query#iden +#[derive(Iden)] +enum PendingLogin { + Table, + Id, + Nonce, + Message, + ExpiresAt, + ImitatingUser, +} + +/// Learn more at https://docs.rs/sea-query#iden +#[derive(Iden)] +enum User { + Table, + Id +} diff --git a/migration/src/m20230215_152254_admin_trail.rs b/migration/src/m20230215_152254_admin_trail.rs new file mode 100644 index 00000000..994361ee --- /dev/null +++ b/migration/src/m20230215_152254_admin_trail.rs @@ -0,0 +1,93 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(AdminTrail::Table) + .if_not_exists() + .col( + ColumnDef::new(AdminTrail::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col( + ColumnDef::new(AdminTrail::Caller) + .big_unsigned() + .not_null() + // TODO: Add Foreign Key + ) + .foreign_key( + sea_query::ForeignKey::create() + .from(AdminTrail::Table, AdminTrail::Caller) + .to(User::Table, User::Id), + ) + .col( + ColumnDef::new(AdminTrail::ImitatingUser) + .big_unsigned() + // Can be null bcs maybe we're just logging in / using endpoints that don't imitate a user + // TODO: Add Foreign Key + ) + .foreign_key( + sea_query::ForeignKey::create() + .from(AdminTrail::Table, AdminTrail::ImitatingUser) + .to(User::Table, User::Id), + ) + .col( + ColumnDef::new(AdminTrail::Endpoint) + .string() + .not_null() + ) + .col( + ColumnDef::new(AdminTrail::Payload) + .string() + .not_null() + ) + .col( + ColumnDef::new(AdminTrail::Timestamp) + .timestamp() + .not_null() + .extra("DEFAULT CURRENT_TIMESTAMP".to_string()) + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // Replace the sample below with your own migration scripts + manager + .drop_table(Table::drop().table(AdminTrail::Table).to_owned()) + .await + } +} + +/// Learn more at https://docs.rs/sea-query#iden +#[derive(Iden)] +enum AdminTrail { + Table, + Id, + Caller, + ImitatingUser, + Endpoint, + Payload, + Timestamp +} + + +/// Learn more at https://docs.rs/sea-query#iden +#[derive(Iden)] +enum User { + Table, + Id, + Address, + Description, + Email, +} \ No newline at end of file diff --git a/scripts/apply-migrations.sh b/scripts/apply-migrations.sh new file mode 100644 index 00000000..3021239b --- /dev/null +++ b/scripts/apply-migrations.sh @@ -0,0 +1,3 @@ +# sea-orm-cli migrate up +# sea-orm-cli generate entity -u mysql://root:dev_web3_proxy@127.0.0.1:13306/dev_web3_proxy -o entities/src --with-serde both +# sea-orm-cli generate entity -t \ No newline at end of file diff --git a/scripts/install-test-suite.sh b/scripts/install-test-suite.sh new file mode 100644 index 00000000..75d2304f --- /dev/null +++ b/scripts/install-test-suite.sh @@ -0,0 +1,3 @@ +cargo install cargo-binstall +cargo binstall cargo-nextest +# cargo nextest run \ No newline at end of file diff --git a/scripts/manual-tests/16-change-user-tier.sh b/scripts/manual-tests/16-change-user-tier.sh new file mode 100644 index 00000000..2505935b --- /dev/null +++ b/scripts/manual-tests/16-change-user-tier.sh @@ -0,0 +1,44 @@ +# rm -rf data/ +# docker-compose up -d +# sea-orm-cli migrate up + +# Use CLI to create the admin that will call the endpoint +RUSTFLAGS="--cfg tokio_unstable" cargo run create_user --address 0xeB3E928A2E54BE013EF8241d4C9EaF4DfAE94D5a +RUSTFLAGS="--cfg tokio_unstable" cargo run change_admin_status 0xeB3E928A2E54BE013EF8241d4C9EaF4DfAE94D5a true + +# Use CLI to create the user whose role will be changed via the endpoint +RUSTFLAGS="--cfg tokio_unstable" cargo run create_user --address 0x077e43dcca20da9859daa3fd78b5998b81f794f7 + +# Run the proxyd instance +RUSTFLAGS="--cfg tokio_unstable" cargo run --release -- proxyd + +# Check if the instance is running +curl -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"web3_clientVersion","id":1}' 127.0.0.1:8544 + +# Open this website to get the nonce to log in +curl -X GET "http://127.0.0.1:8544/user/login/0xeB3E928A2E54BE013EF8241d4C9EaF4DfAE94D5a" + +# Use this site to sign a message +# https://www.myetherwallet.com/wallet/sign (whatever is output with the above code) +curl -X POST http://127.0.0.1:8544/user/login \ + -H 'Content-Type: application/json' \ + -d '{"address": "0xeb3e928a2e54be013ef8241d4c9eaf4dfae94d5a", "msg": "0x6c6c616d616e6f6465732e636f6d2077616e747320796f7520746f207369676e20696e207769746820796f757220457468657265756d206163636f756e743a0a3078654233453932384132453534424530313345463832343164344339456146344466414539344435610a0af09fa699f09fa699f09fa699f09fa699f09fa6990a0a5552493a2068747470733a2f2f6c6c616d616e6f6465732e636f6d2f0a56657273696f6e3a20310a436861696e2049443a20310a4e6f6e63653a2030314753414e37464d47574335314e50544737343338384a44350a4973737565642041743a20323032332d30322d31355431333a34363a33372e3037323739335a0a45787069726174696f6e2054696d653a20323032332d30322d31355431343a30363a33372e3037323739335a", "sig": "2d2eb576b2e6d05845710b7229f2a1ff9707e928fdcf571d1ce0ae094577e4310873fa1376c69440b60d6a1c76c62a4586b9d6426fb6559dee371e490d708f3e1b", "version": "3", "signer": "MEW"}' + +## Login in the user first (add a random bearer token into the database) +## (This segment was not yet tested, but should next time you run the query) +#INSERT INTO login (bearer_token, user_id, expires_at, read_only) VALUES ( +# "01GSAMZ6QY7KH9AQ", +# 1, +# "2024-01-01", +# FALSE +#); + +#curl -X POST -H "Content-Type: application/json" --data '{}' 127.0.0.1:8544/user/login +#curl -X GET "127.0.0.1:8544/user/login/0xeB3E928A2E54BE013EF8241d4C9EaF4DfAE94D5a/" +#curl -X GET "127.0.0.1:8544/admin/modify_role?user_address=0xeB3E928A2E54BE013EF8241d4C9EaF4DfAE94D5a&user_tier_title=Unlimited" + +# Now modify the user role and check this in the database +curl \ +-H "Authorization: Bearer 01GSANKVBB22D5P2351P4Y42NV" \ +-X GET "127.0.0.1:8544/admin/modify_role?user_address=0x077e43dcca20da9859daa3fd78b5998b81f794f7&user_tier_title=Unlimited&user_id=1" + diff --git a/scripts/manual-tests/19-admin-imitate-user.sh b/scripts/manual-tests/19-admin-imitate-user.sh new file mode 100644 index 00000000..de20f3c5 --- /dev/null +++ b/scripts/manual-tests/19-admin-imitate-user.sh @@ -0,0 +1,38 @@ +# rm -rf data/ +# docker-compose up -d +# sea-orm-cli migrate up + +# Use CLI to create the admin that will call the endpoint +RUSTFLAGS="--cfg tokio_unstable" cargo run create_user --address 0xeB3E928A2E54BE013EF8241d4C9EaF4DfAE94D5a +RUSTFLAGS="--cfg tokio_unstable" cargo run change_admin_status 0xeB3E928A2E54BE013EF8241d4C9EaF4DfAE94D5a true + +# Use CLI to create the user whose role will be changed via the endpoint +RUSTFLAGS="--cfg tokio_unstable" cargo run create_user --address 0x077e43dcca20da9859daa3fd78b5998b81f794f7 + +# Run the proxyd instance +RUSTFLAGS="--cfg tokio_unstable" cargo run --release -- proxyd + +# Check if the instance is running +curl -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"web3_clientVersion","id":1}' 127.0.0.1:8544 + +# Open this website to get the nonce to log in +curl \ +-H "Authorization: Bearer 01GSANKVBB22D5P2351P4Y42NV" \ +-X GET "http://127.0.0.1:8544/admin/imitate-login/0xeB3E928A2E54BE013EF8241d4C9EaF4DfAE94D5a/0x077e43dcca20da9859daa3fd78b5998b81f794f7" + +# Use this site to sign a message +# https://www.myetherwallet.com/wallet/sign (whatever is output with the above code) +curl -X POST http://127.0.0.1:8544/admin/imitate-login \ + -H 'Content-Type: application/json' \ + -H "Authorization: Bearer 01GSANKVBB22D5P2351P4Y42NV" \ + -d '{"address": "0xeb3e928a2e54be013ef8241d4c9eaf4dfae94d5a", "msg": "0x6c6c616d616e6f6465732e636f6d2077616e747320796f7520746f207369676e20696e207769746820796f757220457468657265756d206163636f756e743a0a3078654233453932384132453534424530313345463832343164344339456146344466414539344435610a0af09fa699f09fa699f09fa699f09fa699f09fa6990a0a5552493a2068747470733a2f2f6c6c616d616e6f6465732e636f6d2f0a56657273696f6e3a20310a436861696e2049443a20310a4e6f6e63653a20303147534150545132413932415332435752563158504d4347470a4973737565642041743a20323032332d30322d31355431343a31343a33352e3835303636385a0a45787069726174696f6e2054696d653a20323032332d30322d31355431343a33343a33352e3835303636385a", "sig": "d5fed789e98769b8b726a79f222f2e06476de15948d35c167c4f294bb98edf42244edc703b6d729e5d08bd73c318fc9729b985022229c7669a945d64da47ab641c", "version": "3", "signer": "MEW"}' + +# Now modify the user role and check this in the database +# 01GSAMMWQ41TVVH3DH8MSEP8X6 +# Now we can get a bearer-token to imitate the user +curl \ +-H "Authorization: Bearer 01GSAPZNVZ96ADJAEZ1VTRSA5T" \ +-X GET "127.0.0.1:8544/user/keys" + + +# docker-compose down diff --git a/web3_proxy/src/admin_queries.rs b/web3_proxy/src/admin_queries.rs new file mode 100644 index 00000000..0f8de6e1 --- /dev/null +++ b/web3_proxy/src/admin_queries.rs @@ -0,0 +1,122 @@ +use crate::app::Web3ProxyApp; +use crate::frontend::errors::FrontendErrorResponse; +use crate::user_queries::get_user_id_from_params; +use anyhow::Context; +use axum::{ + Json, + headers::{authorization::Bearer, Authorization}, + TypedHeader, +}; +use axum::response::{IntoResponse, Response}; +use entities::{admin, login, user, user_tier}; +use ethers::prelude::Address; +use ethers::types::Bytes; +use ethers::utils::keccak256; +use hashbrown::HashMap; +use http::StatusCode; +use migration::sea_orm::{self, ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter}; +use log::{info, debug}; +use redis_rate_limiter::redis::AsyncCommands; + +// TODO: Add some logic to check if the operating user is an admin +// If he is, return true +// If he is not, return false +// This function is used to give permission to certain users + + +pub async fn query_admin_modify_usertier<'a>( + app: &'a Web3ProxyApp, + bearer: Option>>, + params: &'a HashMap +) -> Result { + + // Quickly return if any of the input tokens are bad + let user_address: Vec = params + .get("user_address") + .ok_or_else(|| FrontendErrorResponse::BadRequest("Unable to find user_address key in request".to_string()))? + .parse::
() + .map_err(|_| FrontendErrorResponse::BadRequest("Unable to parse user_address as an Address".to_string()))? + .to_fixed_bytes().into(); + let user_tier_title = params + .get("user_tier_title") + .ok_or_else(||FrontendErrorResponse::BadRequest("Unable to get the user_tier_title key from the request".to_string()))?; + + // Prepare output body + let mut response_body = HashMap::new(); + + // Establish connections + let db_conn = app.db_conn().context("query_admin_modify_user needs a db")?; + let db_replica = app + .db_replica() + .context("query_user_stats needs a db replica")?; + let mut redis_conn = app + .redis_conn() + .await + .context("query_admin_modify_user had a redis connection error")? + .context("query_admin_modify_user needs a redis")?; + + // Will modify logic here + + + // Try to get the user who is calling from redis (if existent) / else from the database + // TODO: Make a single query, where you retrieve the user, and directly from it the secondary user (otherwise we do two jumpy, which is unnecessary) + // get the user id first. if it is 0, we should use a cache on the app + let caller_id = get_user_id_from_params(&mut redis_conn, &db_conn, &db_replica, bearer, ¶ms).await?; + + debug!("Caller id is: {:?}", caller_id); + + // Check if the caller is an admin (i.e. if he is in an admin table) + let admin: admin::Model = admin::Entity::find() + .filter(admin::Column::UserId.eq(caller_id)) + .one(&db_conn) + .await? + .ok_or(FrontendErrorResponse::AccessDenied)?; + + // If we are here, that means an admin was found, and we can safely proceed + + // Fetch the admin, and the user + let user: user::Model = user::Entity::find() + .filter(user::Column::Address.eq(user_address)) + .one(&db_conn) + .await? + .ok_or(FrontendErrorResponse::BadRequest("No user with this id found".to_string()))?; + // Return early if the target user_tier_id is the same as the original user_tier_id + response_body.insert( + "user_tier_title", + serde_json::Value::Number(user.user_tier_id.into()), + ); + + // Now we can modify the user's tier + let new_user_tier: user_tier::Model = user_tier::Entity::find() + .filter(user_tier::Column::Title.eq(user_tier_title.clone())) + .one(&db_conn) + .await? + .ok_or(FrontendErrorResponse::BadRequest("User Tier name was not found".to_string()))?; + + if user.user_tier_id == new_user_tier.id { + info!("user already has that tier"); + } else { + let mut user = user.clone().into_active_model(); + + user.user_tier_id = sea_orm::Set(new_user_tier.id); + + user.save(&db_conn).await?; + + info!("user's tier changed"); + } + + // Query the login table, and get all bearer tokens by this user + let bearer_tokens = login::Entity::find() + .filter(login::Column::UserId.eq(user.id)) + .all(&db_conn) + .await?; + + // Now delete these tokens ... + login::Entity::delete_many() + .filter(login::Column::UserId.eq(user.id)) + .exec(&db_conn) + .await?; + + Ok(Json(&response_body).into_response()) + +} diff --git a/web3_proxy/src/bin/web3_proxy_cli/change_user_admin_status.rs b/web3_proxy/src/bin/web3_proxy_cli/change_user_admin_status.rs new file mode 100644 index 00000000..081cd8a0 --- /dev/null +++ b/web3_proxy/src/bin/web3_proxy_cli/change_user_admin_status.rs @@ -0,0 +1,84 @@ +use anyhow::Context; +use argh::FromArgs; +use entities::{admin, login, user}; +use ethers::types::{Address, Bytes}; +use ethers::utils::keccak256; +use http::StatusCode; +use log::{debug, info}; +use migration::sea_orm::{ + self, ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, IntoActiveModel, + QueryFilter, +}; +use web3_proxy::frontend::errors::FrontendErrorResponse; + +/// change a user's admin status. eiter they are an admin, or they aren't +#[derive(FromArgs, PartialEq, Eq, Debug)] +#[argh(subcommand, name = "change_admin_status")] +pub struct ChangeUserAdminStatusSubCommand { + /// the address of the user whose admin status you want to modify + #[argh(positional)] + address: String, + + /// true if the user should be an admin, false otherwise + #[argh(positional)] + should_be_admin: bool, +} + +impl ChangeUserAdminStatusSubCommand { + pub async fn main(self, db_conn: &DatabaseConnection) -> anyhow::Result<()> { + let address: Address = self.address.parse()?; + let should_be_admin: bool = self.should_be_admin; + + let address: Vec = address.to_fixed_bytes().into(); + + // Find user in database + let user = user::Entity::find() + .filter(user::Column::Address.eq(address.clone())) + .one(db_conn) + .await? + .context(format!("No user with this id found {:?}", address))?; + + debug!("user: {:#?}", user); + + // Check if there is a record in the database + match admin::Entity::find() + .filter(admin::Column::UserId.eq(address)) + .one(db_conn) + .await? { + Some(old_admin) if !should_be_admin => { + // User is already an admin, but shouldn't be + old_admin.delete(db_conn).await?; + } + None if should_be_admin => { + // User is not an admin yet, but should be + let new_admin = admin::ActiveModel { + user_id: sea_orm::Set(user.id), + ..Default::default() + }; + new_admin.insert(db_conn).await?; + } + _ => { + // Do nothing in this case + debug!("no change needed for: {:#?}", user); + // Early return + return Ok(()); + } + } + + // Get the bearer tokens of this user and delete them ... + let bearer_tokens = login::Entity::find() + .filter(login::Column::UserId.eq(user.id)) + .all(db_conn) + .await?; + + // Remove any user logins from the database (incl. bearer tokens) + let delete_result = login::Entity::delete_many() + .filter(login::Column::UserId.eq(user.id)) + .exec(db_conn) + .await?; + + debug!("cleared modified logins: {:?}", delete_result); + + Ok(()) + } +} diff --git a/web3_proxy/src/bin/web3_proxy_cli/main.rs b/web3_proxy/src/bin/web3_proxy_cli/main.rs index 5dc564ac..966fbbc3 100644 --- a/web3_proxy/src/bin/web3_proxy_cli/main.rs +++ b/web3_proxy/src/bin/web3_proxy_cli/main.rs @@ -1,4 +1,5 @@ mod change_user_address; +mod change_user_admin_status; mod change_user_tier; mod change_user_tier_by_address; mod change_user_tier_by_key; @@ -70,6 +71,7 @@ pub struct Web3ProxyCli { #[argh(subcommand)] enum SubCommand { ChangeUserAddress(change_user_address::ChangeUserAddressSubCommand), + ChangeUserAdminStatus(change_user_admin_status::ChangeUserAdminStatusSubCommand), ChangeUserTier(change_user_tier::ChangeUserTierSubCommand), ChangeUserTierByAddress(change_user_tier_by_address::ChangeUserTierByAddressSubCommand), ChangeUserTierByKey(change_user_tier_by_key::ChangeUserTierByKeySubCommand), @@ -287,6 +289,15 @@ fn main() -> anyhow::Result<()> { x.main(&db_conn).await } + SubCommand::ChangeUserAdminStatus(x) => { + let db_url = cli_config + .db_url + .expect("'--config' (with a db) or '--db-url' is required to run change_user_admin_status"); + + let db_conn = get_db(db_url, 1, 1).await?; + + x.main(&db_conn).await + } SubCommand::ChangeUserTier(x) => { let db_url = cli_config .db_url @@ -299,7 +310,7 @@ fn main() -> anyhow::Result<()> { SubCommand::ChangeUserTierByAddress(x) => { let db_url = cli_config .db_url - .expect("'--config' (with a db) or '--db-url' is required to run proxyd"); + .expect("'--config' (with a db) or '--db-url' is required to run change_user_admin_status"); let db_conn = get_db(db_url, 1, 1).await?; diff --git a/web3_proxy/src/frontend/admin.rs b/web3_proxy/src/frontend/admin.rs new file mode 100644 index 00000000..286c1d44 --- /dev/null +++ b/web3_proxy/src/frontend/admin.rs @@ -0,0 +1,450 @@ +//! Handle admin helper logic + +use super::authorization::{login_is_authorized, RpcSecretKey}; +use super::errors::FrontendResult; +use crate::app::Web3ProxyApp; +use crate::user_queries::{get_page_from_params, get_user_id_from_params}; +use crate::user_queries::{ + get_chain_id_from_params, get_query_start_from_params, query_user_stats, StatResponse, +}; +use entities::prelude::{User, SecondaryUser}; +use crate::user_token::UserBearerToken; +use anyhow::Context; +use axum::headers::{Header, Origin, Referer, UserAgent}; +use axum::{ + extract::{Path, Query}, + headers::{authorization::Bearer, Authorization}, + response::IntoResponse, + Extension, Json, TypedHeader, +}; +use axum_client_ip::InsecureClientIp; +use axum_macros::debug_handler; +use chrono::{TimeZone, Utc}; +use entities::sea_orm_active_enums::{LogLevel, Role}; +use entities::{admin, admin_trail, login, pending_login, revert_log, rpc_key, secondary_user, user, user_tier}; +use ethers::{abi::AbiEncode, prelude::Address, types::Bytes}; +use hashbrown::HashMap; +use http::{HeaderValue, StatusCode}; +use ipnet::IpNet; +use itertools::Itertools; +use log::{debug, info, warn}; +use migration::sea_orm::prelude::Uuid; +use migration::sea_orm::{ + self, ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, PaginatorTrait, QueryFilter, + QueryOrder, TransactionTrait, TryIntoModel, +}; +use serde::Deserialize; +use serde_json::json; +use siwe::{Message, VerificationOpts}; +use std::ops::Add; +use std::str::FromStr; +use std::sync::Arc; +use time::{Duration, OffsetDateTime}; +use ulid::Ulid; +use crate::admin_queries::query_admin_modify_usertier; +use crate::frontend::errors::FrontendErrorResponse; +use crate::{PostLogin, PostLoginQuery}; + +/// `GET /admin/modify_role` -- As an admin, modify a user's user-tier +/// +/// - user_address that is to be modified +/// - user_role_tier that is supposed to be adapted +#[debug_handler] +pub async fn admin_change_user_roles( + Extension(app): Extension>, + bearer: Option>>, + Query(params): Query>, +) -> FrontendResult { + let response = query_admin_modify_usertier(&app, bearer, ¶ms).await?; + + Ok(response) +} + +/// `GET /admin/imitate-login/:admin_address/:user_address` -- Being an admin, login as a user in read-only mode +/// +/// - user_address that is to be logged in by +/// We assume that the admin has already logged in, and has a bearer token ... +#[debug_handler] +pub async fn admin_login_get( + Extension(app): Extension>, + InsecureClientIp(ip): InsecureClientIp, + Path(mut params): Path>, +) -> FrontendResult { + // First check if the login is authorized + login_is_authorized(&app, ip).await?; + + // create a message and save it in redis + // TODO: how many seconds? get from config? + + // Same parameters as when someone logs in as a user + let expire_seconds: usize = 20 * 60; + let nonce = Ulid::new(); + let issued_at = OffsetDateTime::now_utc(); + let expiration_time = issued_at.add(Duration::new(expire_seconds as i64, 0)); + + // The admin user is the one that basically logs in, on behalf of the user + // This will generate a login id for the admin, which we will be caching ... + // I suppose with this, the admin can be logged in to one session at a time + // let (caller, _semaphore) = app.bearer_is_authorized(bearer_token).await?; + + // Finally, check if the user is an admin. If he is, return "true" as the third triplet. + // TODO: consider wrapping the output in a struct, instead of a triplet + // TODO: Could try to merge this into the above query ... + // This query will fail if it's not the admin... + + // get the admin field ... + let admin_address: Address = params + .get("admin_address") + .ok_or_else(|| FrontendErrorResponse::BadRequest("Unable to find admin_address key in request".to_string()))? + .parse::
() + .map_err(|err| { FrontendErrorResponse::BadRequest("Unable to parse user_address as an Address".to_string()) })?; + + + // Fetch the user_address parameter from the login string ... (as who we want to be logging in ...) + let user_address: Vec = params + .get("user_address") + .ok_or_else(|| FrontendErrorResponse::BadRequest("Unable to find user_address key in request".to_string()))? + .parse::
() + .map_err(|err| { FrontendErrorResponse::BadRequest("Unable to parse user_address as an Address".to_string(), ) })? + .to_fixed_bytes().into(); + + // We want to login to llamanodes.com + let login_domain = app + .config + .login_domain + .clone() + .unwrap_or_else(|| "llamanodes.com".to_string()); + + // Also there must basically be a token, that says that one admin logins _as a user_. + // I'm not yet fully sure how to handle with that logic specifically ... + // TODO: get most of these from the app config + // TODO: Let's check again who the message needs to be signed by; + // if the message does not have to be signed by the user, include the user ... + let message = Message { + // TODO: don't unwrap + // TODO: accept a login_domain from the request? + domain: login_domain.parse().unwrap(), + // In the case of the admin, the admin needs to sign the message, so we include this logic ... + address: admin_address.to_fixed_bytes(), // user_address.to_fixed_bytes(), + // TODO: config for statement + statement: Some("🦙🦙🦙🦙🦙".to_string()), + // TODO: don't unwrap + uri: format!("https://{}/", login_domain).parse().unwrap(), + version: siwe::Version::V1, + chain_id: 1, + expiration_time: Some(expiration_time.into()), + issued_at: issued_at.into(), + nonce: nonce.to_string(), + not_before: None, + request_id: None, + resources: vec![], + }; + + let db_conn = app.db_conn().context("login requires a database")?; + let db_replica = app.db_replica().context("login requires a replica database")?; + + // 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 ... + let user = user::Entity::find() + .filter(user::Column::Address.eq(user_address)) + .one(db_replica.conn()) + .await? + .ok_or(FrontendErrorResponse::BadRequest("Could not find user in db".to_string()))?; + + let admin = user::Entity::find() + .filter(user::Column::Address.eq(admin_address.encode())) + .one(db_replica.conn()) + .await? + .ok_or(FrontendErrorResponse::BadRequest("Could not find admin in db".to_string()))?; + + // Note that the admin is trying to log in as this user + let trail = admin_trail::ActiveModel { + caller: sea_orm::Set(admin.id), + imitating_user: sea_orm::Set(Some(user.id)), + endpoint: sea_orm::Set("admin_login_get".to_string()), + payload: sea_orm::Set(format!("{:?}", params)), + ..Default::default() + }; + trail + .save(&db_conn) + .await + .context("saving user's pending_login")?; + + // Can there be two login-sessions at the same time? + // I supposed if the user logs in, the admin would be logged out and vice versa + + // massage types to fit in the database. sea-orm does not make this very elegant + let uuid = Uuid::from_u128(nonce.into()); + // we add 1 to expire_seconds just to be sure the database has the key for the full expiration_time + let expires_at = Utc + .timestamp_opt(expiration_time.unix_timestamp() + 1, 0) + .unwrap(); + + // we do not store a maximum number of attempted logins. anyone can request so we don't want to allow DOS attacks + // add a row to the database for this user + let user_pending_login = pending_login::ActiveModel { + id: sea_orm::NotSet, + nonce: sea_orm::Set(uuid), + message: sea_orm::Set(message.to_string()), + expires_at: sea_orm::Set(expires_at), + imitating_user: sea_orm::Set(Some(user.id)) + }; + + user_pending_login + .save(&db_conn) + .await + .context("saving an admin trail pre login")?; + + // there are multiple ways to sign messages and not all wallets support them + // TODO: default message eip from config? + let message_eip = params + .remove("message_eip") + .unwrap_or_else(|| "eip4361".to_string()); + + let message: String = match message_eip.as_str() { + "eip191_bytes" => Bytes::from(message.eip191_bytes().unwrap()).to_string(), + "eip191_hash" => Bytes::from(&message.eip191_hash().unwrap()).to_string(), + "eip4361" => message.to_string(), + _ => { + // TODO: custom error that is handled a 401 + return Err(anyhow::anyhow!("invalid message eip given").into()); + } + }; + + Ok(message.into_response()) +} + +/// `POST /admin/login` - Register or login by posting a signed "siwe" message +/// It is recommended to save the returned bearer token in a cookie. +/// The bearer token can be used to authenticate other requests, such as getting user user's tats or modifying the user's profile +#[debug_handler] +pub async fn admin_login_post( + Extension(app): Extension>, + InsecureClientIp(ip): InsecureClientIp, + Query(query): Query, + Json(payload): Json, +) -> FrontendResult { + login_is_authorized(&app, ip).await?; + + // Check for the signed bytes .. + // TODO: this seems too verbose. how can we simply convert a String into a [u8; 65] + let their_sig_bytes = Bytes::from_str(&payload.sig).context("parsing sig")?; + if their_sig_bytes.len() != 65 { + return Err(anyhow::anyhow!("checking signature length").into()); + } + let mut their_sig: [u8; 65] = [0; 65]; + for x in 0..65 { + their_sig[x] = their_sig_bytes[x] + } + + // we can't trust that they didn't tamper with the message in some way. like some clients return it hex encoded + // TODO: checking 0x seems fragile, but I think it will be fine. siwe message text shouldn't ever start with 0x + let their_msg: Message = if payload.msg.starts_with("0x") { + let their_msg_bytes = Bytes::from_str(&payload.msg).context("parsing payload message")?; + + // TODO: lossy or no? + String::from_utf8_lossy(their_msg_bytes.as_ref()) + .parse::() + .context("parsing hex string message")? + } else { + payload + .msg + .parse::() + .context("parsing string message")? + }; + + // the only part of the message we will trust is their nonce + // TODO: this is fragile. have a helper function/struct for redis keys + let login_nonce = UserBearerToken::from_str(&their_msg.nonce)?; + + // fetch the message we gave them from our database + let db_replica = app.db_replica().context("Getting database connection")?; + + // massage type for the db + let login_nonce_uuid: Uuid = login_nonce.clone().into(); + + // TODO: Here we will need to re-find the parameter where the admin wants to log-in as the user ... + let user_pending_login = pending_login::Entity::find() + .filter(pending_login::Column::Nonce.eq(login_nonce_uuid)) + .one(db_replica.conn()) + .await + .context("database error while finding pending_login")? + .context("login nonce not found")?; + + let our_msg: siwe::Message = user_pending_login + .message + .parse() + .context("parsing siwe message")?; + + // default options are fine. the message includes timestamp and domain and nonce + let verify_config = VerificationOpts::default(); + + let db_conn = app + .db_conn() + .context("deleting expired pending logins requires a db")?; + + if let Err(err_1) = our_msg + .verify(&their_sig, &verify_config) + .await + .context("verifying signature against our local message") + { + // verification method 1 failed. try eip191 + if let Err(err_191) = our_msg + .verify_eip191(&their_sig) + .context("verifying eip191 signature against our local message") + { + + // delete ALL expired rows. + let now = Utc::now(); + let delete_result = pending_login::Entity::delete_many() + .filter(pending_login::Column::ExpiresAt.lte(now)) + .exec(&db_conn) + .await?; + + // TODO: emit a stat? if this is high something weird might be happening + debug!("cleared expired pending_logins: {:?}", delete_result); + + return Err(anyhow::anyhow!( + "both the primary and eip191 verification failed: {:#?}; {:#?}", + err_1, + err_191 + ) + .into()); + } + } + + // TODO: Maybe add a context? + let imitating_user_id = user_pending_login.imitating_user + .context("getting address of the imitating user")?; + + // TODO: limit columns or load whole user? + // TODO: Right now this loads the whole admin. I assume we might want to load the user though (?) figure this out as we go along... + let admin = user::Entity::find() + .filter(user::Column::Address.eq(our_msg.address.as_ref())) + .one(db_replica.conn()) + .await? + .context("getting admin address")?; + + let imitating_user = user::Entity::find() + .filter(user::Column::Id.eq(imitating_user_id)) + .one(db_replica.conn()) + .await? + .context("admin address was not found!")?; + + // Add a message that the admin has logged in + // Note that the admin is trying to log in as this user + let trail = admin_trail::ActiveModel { + caller: sea_orm::Set(admin.id), + imitating_user: sea_orm::Set(Some(imitating_user.id)), + endpoint: sea_orm::Set("admin_login_post".to_string()), + payload: sea_orm::Set(format!("{:?}", payload)), + ..Default::default() + }; + trail + .save(&db_conn) + .await + .context("saving an admin trail post login")?; + + // I supposed we also get the rpc_key, whatever this is used for (?). + // I think the RPC key should still belong to the admin though in this case ... + + // the user is already registered + let admin_rpc_key = rpc_key::Entity::find() + .filter(rpc_key::Column::UserId.eq(admin.id)) + .all(db_replica.conn()) + .await + .context("failed loading user's key")?; + + // create a bearer token for the user. + let user_bearer_token = UserBearerToken::default(); + + // json response with everything in it + // we could return just the bearer token, but I think they will always request api keys and the user profile + let response_json = json!({ + "rpc_keys": admin_rpc_key + .into_iter() + .map(|uk| (uk.id, uk)) + .collect::>(), + "bearer_token": user_bearer_token, + "imitating_user": imitating_user, + "admin_user": admin, + }); + + let response = (StatusCode::OK, Json(response_json)).into_response(); + + // add bearer to the database + + // expire in 2 days, because this is more critical (and shouldn't need to be done so long!) + let expires_at = Utc::now() + .checked_add_signed(chrono::Duration::days(2)) + .unwrap(); + + // TODO: Here, the bearer token should include a message + // TODO: Above, make sure that the calling address is an admin! + // TODO: Above, make sure that the signed is the admin (address field), + // but then in this request, the admin can pick which user to sign up as + let user_login = login::ActiveModel { + id: sea_orm::NotSet, + bearer_token: sea_orm::Set(user_bearer_token.uuid()), + user_id: sea_orm::Set(imitating_user.id), // Yes, this should be the user ... because the rest of the applications takes this item, from the initial user + expires_at: sea_orm::Set(expires_at), + read_only: sea_orm::Set(true) + }; + + user_login + .save(&db_conn) + .await + .context("saving user login")?; + + if let Err(err) = user_pending_login + .into_active_model() + .delete(&db_conn) + .await + { + warn!("Failed to delete nonce:{}: {}", login_nonce.0, err); + } + + Ok(response) + +} + +// TODO: This is basically an exact copy of the user endpoint, I should probabl refactor this code ... +/// `POST /admin/imitate-logout` - Forget the bearer token in the `Authentication` header. +#[debug_handler] +pub async fn admin_logout_post( + Extension(app): Extension>, + TypedHeader(Authorization(bearer)): TypedHeader>, +) -> FrontendResult { + let user_bearer = UserBearerToken::try_from(bearer)?; + + let db_conn = app.db_conn().context("database needed for user logout")?; + + if let Err(err) = login::Entity::delete_many() + .filter(login::Column::BearerToken.eq(user_bearer.uuid())) + .exec(&db_conn) + .await + { + debug!("Failed to delete {}: {}", user_bearer.redis_key(), err); + } + + let now = Utc::now(); + + // also delete any expired logins + let delete_result = login::Entity::delete_many() + .filter(login::Column::ExpiresAt.lte(now)) + .exec(&db_conn) + .await; + + debug!("Deleted expired logins: {:?}", delete_result); + + // also delete any expired pending logins + let delete_result = login::Entity::delete_many() + .filter(login::Column::ExpiresAt.lte(now)) + .exec(&db_conn) + .await; + + debug!("Deleted expired pending logins: {:?}", delete_result); + + // TODO: what should the response be? probably json something + Ok("goodbye".into_response()) +} diff --git a/web3_proxy/src/frontend/authorization.rs b/web3_proxy/src/frontend/authorization.rs index 6d014ebd..342addfc 100644 --- a/web3_proxy/src/frontend/authorization.rs +++ b/web3_proxy/src/frontend/authorization.rs @@ -9,7 +9,7 @@ use axum::headers::authorization::Bearer; use axum::headers::{Header, Origin, Referer, UserAgent}; use chrono::Utc; use deferred_rate_limiter::DeferredRateLimitResult; -use entities::{login, rpc_key, user, user_tier}; +use entities::{admin, login, rpc_key, user, user_tier}; use ethers::types::Bytes; use ethers::utils::keccak256; use futures::TryFutureExt; diff --git a/web3_proxy/src/frontend/mod.rs b/web3_proxy/src/frontend/mod.rs index 99c7882f..1d9f5981 100644 --- a/web3_proxy/src/frontend/mod.rs +++ b/web3_proxy/src/frontend/mod.rs @@ -2,6 +2,7 @@ //! //! Important reading about axum extractors: https://docs.rs/axum/latest/axum/extract/index.html#the-order-of-extractors +pub mod admin; pub mod authorization; pub mod errors; // TODO: these are only public so docs are generated. What's a better way to do this? @@ -167,6 +168,14 @@ pub async fn serve(port: u16, proxy_app: Arc) -> anyhow::Result<() ) .route("/user/stats/detailed", get(users::user_stats_detailed_get)) .route("/user/logout", post(users::user_logout_post)) + .route("/admin/modify_role", get(admin::admin_change_user_roles)) + .route("/admin/imitate-login/:admin_address/:user_address", get(admin::admin_login_get)) + .route( + "/admin/imitate-login/:admin_address/:user_address/:message_eip", + get(admin::admin_login_get), + ) + .route("/admin/imitate-login", post(admin::admin_login_post)) + .route("/admin/imitate-logout", post(admin::admin_logout_post)) // // Axum layers // layers are ordered bottom up diff --git a/web3_proxy/src/frontend/users.rs b/web3_proxy/src/frontend/users.rs index 9acbb3c0..9fada899 100644 --- a/web3_proxy/src/frontend/users.rs +++ b/web3_proxy/src/frontend/users.rs @@ -3,10 +3,11 @@ use super::authorization::{login_is_authorized, RpcSecretKey}; use super::errors::FrontendResult; use crate::app::Web3ProxyApp; -use crate::user_queries::get_page_from_params; +use crate::user_queries::{get_page_from_params, get_user_id_from_params}; use crate::user_queries::{ get_chain_id_from_params, get_query_start_from_params, query_user_stats, StatResponse, }; +use entities::prelude::{User, SecondaryUser}; use crate::user_token::UserBearerToken; use anyhow::Context; use axum::headers::{Header, Origin, Referer, UserAgent}; @@ -19,14 +20,14 @@ use axum::{ use axum_client_ip::InsecureClientIp; use axum_macros::debug_handler; use chrono::{TimeZone, Utc}; -use entities::sea_orm_active_enums::LogLevel; -use entities::{login, pending_login, revert_log, rpc_key, user}; +use entities::sea_orm_active_enums::{LogLevel, Role}; +use entities::{login, pending_login, revert_log, rpc_key, secondary_user, user, user_tier}; use ethers::{prelude::Address, types::Bytes}; use hashbrown::HashMap; use http::{HeaderValue, StatusCode}; use ipnet::IpNet; use itertools::Itertools; -use log::{debug, warn}; +use log::{debug, info, warn}; use migration::sea_orm::prelude::Uuid; use migration::sea_orm::{ self, ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, PaginatorTrait, QueryFilter, @@ -40,6 +41,12 @@ use std::str::FromStr; use std::sync::Arc; use time::{Duration, OffsetDateTime}; use ulid::Ulid; +use entities::user::Relation::UserTier; +use migration::extension::postgres::Type; +use thread_fast_rng::rand; +use crate::admin_queries::query_admin_modify_usertier; +use crate::frontend::errors::FrontendErrorResponse; +use crate::{PostLogin, PostLoginQuery}; /// `GET /user/login/:user_address` or `GET /user/login/:user_address/:message_eip` -- Start the "Sign In with Ethereum" (siwe) login flow. /// @@ -128,6 +135,7 @@ pub async fn user_login_get( nonce: sea_orm::Set(uuid), message: sea_orm::Set(message.to_string()), expires_at: sea_orm::Set(expires_at), + imitating_user: sea_orm::Set(None) }; user_pending_login @@ -154,24 +162,6 @@ pub async fn user_login_get( Ok(message.into_response()) } -/// Query params for our `post_login` handler. -#[derive(Debug, Deserialize)] -pub struct PostLoginQuery { - /// While we are in alpha/beta, we require users to supply an invite code. - /// The invite code (if any) is set in the application's config. - /// This may eventually provide some sort of referral bonus. - pub invite_code: Option, -} - -/// JSON body to our `post_login` handler. -/// Currently only siwe logins that send an address, msg, and sig are allowed. -/// Email/password and other login methods are planned. -#[derive(Debug, Deserialize)] -pub struct PostLogin { - sig: String, - msg: String, -} - /// `POST /user/login` - Register or login by posting a signed "siwe" message. /// It is recommended to save the returned bearer token in a cookie. /// The bearer token can be used to authenticate other requests, such as getting the user's stats or modifying the user's profile. @@ -365,6 +355,7 @@ pub async fn user_login_post( bearer_token: sea_orm::Set(user_bearer_token.uuid()), user_id: sea_orm::Set(u.id), expires_at: sea_orm::Set(expires_at), + read_only: sea_orm::Set(false) }; user_login diff --git a/web3_proxy/src/lib.rs b/web3_proxy/src/lib.rs index cfcd4ba9..e31d0972 100644 --- a/web3_proxy/src/lib.rs +++ b/web3_proxy/src/lib.rs @@ -1,5 +1,6 @@ pub mod app; pub mod app_stats; +pub mod admin_queries; pub mod atomics; pub mod block_number; pub mod config; @@ -10,3 +11,24 @@ pub mod pagerduty; pub mod rpcs; pub mod user_queries; pub mod user_token; + +use serde::Deserialize; + +// Push some commonly used types here. Can establish a folder later on +/// Query params for our `post_login` handler. +#[derive(Debug, Deserialize)] +pub struct PostLoginQuery { + /// While we are in alpha/beta, we require users to supply an invite code. + /// The invite code (if any) is set in the application's config. + /// This may eventually provide some sort of referral bonus. + invite_code: Option, +} + +/// JSON body to our `post_login` handler. +/// Currently only siwe logins that send an address, msg, and sig are allowed. +/// Email/password and other login methods are planned. +#[derive(Debug, Deserialize)] +pub struct PostLogin { + sig: String, + msg: String, +} diff --git a/web3_proxy/src/user_queries.rs b/web3_proxy/src/user_queries.rs index 6d2f5a94..ad448b6c 100644 --- a/web3_proxy/src/user_queries.rs +++ b/web3_proxy/src/user_queries.rs @@ -9,6 +9,7 @@ use axum::{ TypedHeader, }; use chrono::{NaiveDateTime, Utc}; +use ethers::prelude::Address; use entities::{login, rpc_accounting, rpc_key}; use hashbrown::HashMap; use http::StatusCode; @@ -34,6 +35,7 @@ pub async fn get_user_id_from_params( bearer: Option>>, params: &HashMap, ) -> Result { + debug!("bearer and params are: {:?} {:?}", bearer, params); match (bearer, params.get("user_id")) { (Some(TypedHeader(Authorization(bearer))), Some(user_id)) => { // check for the bearer cache key