Merge pull request #36 from yenicelik/19-admin-imitate
Admin endpoint (Godmode / Imitate User)
This commit is contained in:
commit
1ffccbe229
34
entities/src/admin.rs
Normal file
34
entities/src/admin.rs
Normal file
@ -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<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
37
entities/src/admin_trail.rs
Normal file
37
entities/src/admin_trail.rs
Normal file
@ -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<u64>,
|
||||
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 {}
|
@ -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)]
|
||||
|
@ -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;
|
||||
|
@ -15,6 +15,7 @@ pub struct Model {
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub message: String,
|
||||
pub expires_at: DateTimeUtc,
|
||||
pub imitating_user: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
@ -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;
|
||||
|
@ -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),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
58
migration/src/m20230117_191358_admin_table.rs
Normal file
58
migration/src/m20230117_191358_admin_table.rs
Normal file
@ -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,
|
||||
}
|
42
migration/src/m20230130_124740_read_only_login_logic.rs
Normal file
42
migration/src/m20230130_124740_read_only_login_logic.rs
Normal file
@ -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,
|
||||
}
|
@ -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
|
||||
}
|
93
migration/src/m20230215_152254_admin_trail.rs
Normal file
93
migration/src/m20230215_152254_admin_trail.rs
Normal file
@ -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,
|
||||
}
|
3
scripts/apply-migrations.sh
Normal file
3
scripts/apply-migrations.sh
Normal file
@ -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 <table_name>
|
3
scripts/install-test-suite.sh
Normal file
3
scripts/install-test-suite.sh
Normal file
@ -0,0 +1,3 @@
|
||||
cargo install cargo-binstall
|
||||
cargo binstall cargo-nextest
|
||||
# cargo nextest run
|
44
scripts/manual-tests/16-change-user-tier.sh
Normal file
44
scripts/manual-tests/16-change-user-tier.sh
Normal file
@ -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"
|
||||
|
38
scripts/manual-tests/19-admin-imitate-user.sh
Normal file
38
scripts/manual-tests/19-admin-imitate-user.sh
Normal file
@ -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
|
122
web3_proxy/src/admin_queries.rs
Normal file
122
web3_proxy/src/admin_queries.rs
Normal file
@ -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<TypedHeader<Authorization<Bearer>>>,
|
||||
params: &'a HashMap<String, String>
|
||||
) -> Result<Response, FrontendErrorResponse> {
|
||||
|
||||
// Quickly return if any of the input tokens are bad
|
||||
let user_address: Vec<u8> = params
|
||||
.get("user_address")
|
||||
.ok_or_else(|| FrontendErrorResponse::BadRequest("Unable to find user_address key in request".to_string()))?
|
||||
.parse::<Address>()
|
||||
.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())
|
||||
|
||||
}
|
@ -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<u8> = 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(())
|
||||
}
|
||||
}
|
@ -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?;
|
||||
|
||||
|
450
web3_proxy/src/frontend/admin.rs
Normal file
450
web3_proxy/src/frontend/admin.rs
Normal file
@ -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<Arc<Web3ProxyApp>>,
|
||||
bearer: Option<TypedHeader<Authorization<Bearer>>>,
|
||||
Query(params): Query<HashMap<String, String>>,
|
||||
) -> 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<Arc<Web3ProxyApp>>,
|
||||
InsecureClientIp(ip): InsecureClientIp,
|
||||
Path(mut params): Path<HashMap<String, String>>,
|
||||
) -> 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::<Address>()
|
||||
.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<u8> = params
|
||||
.get("user_address")
|
||||
.ok_or_else(|| FrontendErrorResponse::BadRequest("Unable to find user_address key in request".to_string()))?
|
||||
.parse::<Address>()
|
||||
.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<Arc<Web3ProxyApp>>,
|
||||
InsecureClientIp(ip): InsecureClientIp,
|
||||
Query(query): Query<PostLoginQuery>,
|
||||
Json(payload): Json<PostLogin>,
|
||||
) -> 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::<siwe::Message>()
|
||||
.context("parsing hex string message")?
|
||||
} else {
|
||||
payload
|
||||
.msg
|
||||
.parse::<siwe::Message>()
|
||||
.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::<HashMap<_, _>>(),
|
||||
"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<Arc<Web3ProxyApp>>,
|
||||
TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
|
||||
) -> 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())
|
||||
}
|
@ -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;
|
||||
|
@ -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<Web3ProxyApp>) -> 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
|
||||
|
@ -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<String>,
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
@ -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<String>,
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
@ -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<TypedHeader<Authorization<Bearer>>>,
|
||||
params: &HashMap<String, String>,
|
||||
) -> Result<u64, FrontendErrorResponse> {
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user