mirror of
https://github.com/trailbaseio/trailbase.git
synced 2025-12-30 14:19:43 -06:00
Extend CLI's user/admin capabilities and disallow change/delete operations on admins from UI. #83
This commit is contained in:
@@ -1,3 +1,10 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Request changes to user with given `id`.
|
||||
*
|
||||
* NOTE: We don't allow admin promotions and especially demotions, since they could easily be
|
||||
* abused. Instead we relegate such critical actions to the CLI, which limits them to sys
|
||||
* admins over mere TrailBase admins.
|
||||
*/
|
||||
export type UpdateUserRequest = { id: string, email: string | null, password: string | null, verified: boolean | null, };
|
||||
|
||||
@@ -60,12 +60,12 @@ pub enum SubCommands {
|
||||
/// Optional suffix used for the generated migration file: U<timetamp>__<suffix>.sql.
|
||||
suffix: Option<String>,
|
||||
},
|
||||
/// Simple admin management (use dashboard for everything else).
|
||||
/// Manage admin users (list, demote, promote).
|
||||
Admin {
|
||||
#[command(subcommand)]
|
||||
cmd: Option<AdminSubCommands>,
|
||||
},
|
||||
/// Simple user management (use dashboard for everything else).
|
||||
/// Manage users. Unlike the admin UI this will also let you change admin users.
|
||||
User {
|
||||
#[command(subcommand)]
|
||||
cmd: Option<UserSubCommands>,
|
||||
@@ -160,26 +160,54 @@ pub enum AdminSubCommands {
|
||||
List,
|
||||
/// Demotes admin user to normal user.
|
||||
Demote {
|
||||
/// E-mail of the admin who's demoted.
|
||||
email: String,
|
||||
/// Admin in question, either email or UUID.
|
||||
user: String,
|
||||
},
|
||||
/// Promotes user to admin.
|
||||
Promote {
|
||||
/// E-mail of the user who's promoted to admin.
|
||||
email: String,
|
||||
/// User in question, either email or UUID.
|
||||
user: String,
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: Add "create user" (low priority since users can be created via the UI).
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum UserSubCommands {
|
||||
// TODO: create new user. Low prio, use dashboard.
|
||||
/// Resets a users password.
|
||||
ResetPassword {
|
||||
/// E-mail of the user who's password is being reset.
|
||||
email: String,
|
||||
/// Password to set.
|
||||
/// Change a user's password.
|
||||
ChangePassword {
|
||||
/// User in question, either email or UUID.
|
||||
user: String,
|
||||
/// New password to set for user.
|
||||
password: String,
|
||||
},
|
||||
/// Mint auth tokens for the given user.
|
||||
MintToken { email: String },
|
||||
/// Change a user's email.
|
||||
ChangeEmail {
|
||||
/// User in question, either email or UUID.
|
||||
user: String,
|
||||
/// New email address to set for user.
|
||||
new_email: String,
|
||||
},
|
||||
/// Delete a user.
|
||||
Delete {
|
||||
/// User in question, either email or UUID.
|
||||
user: String,
|
||||
},
|
||||
/// Change a user's verification state.
|
||||
Verify {
|
||||
/// User in question, either email or UUID.
|
||||
user: String,
|
||||
/// User's verification state to set.
|
||||
#[arg(default_value = "true")]
|
||||
verified: bool,
|
||||
},
|
||||
/// Invalidate user session, thus requiring them to re-auth when their auth token expires.
|
||||
InvalidateSession {
|
||||
/// User in question, either email or UUID.
|
||||
user: String,
|
||||
},
|
||||
/// Mint auth token for the given user.
|
||||
MintToken {
|
||||
/// User in question, either email or UUID.
|
||||
user: String,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -5,13 +5,12 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
|
||||
use chrono::TimeZone;
|
||||
use clap::{CommandFactory, Parser};
|
||||
use log::*;
|
||||
use serde::Deserialize;
|
||||
use std::rc::Rc;
|
||||
use tokio::{fs, io::AsyncWriteExt};
|
||||
use trailbase::{
|
||||
DataDir, Server, ServerOptions,
|
||||
api::{self, Email, InitArgs, JsonSchemaMode, TokenClaims, init_app_state},
|
||||
api::{self, Email, InitArgs, JsonSchemaMode, init_app_state},
|
||||
constants::USER_TABLE,
|
||||
};
|
||||
use utoipa::OpenApi;
|
||||
@@ -41,7 +40,6 @@ fn init_logger(dev: bool) {
|
||||
struct DbUser {
|
||||
id: [u8; 16],
|
||||
email: String,
|
||||
verified: bool,
|
||||
created: i64,
|
||||
updated: i64,
|
||||
}
|
||||
@@ -52,19 +50,6 @@ impl DbUser {
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_user_by_email(conn: &api::Connection, email: &str) -> Result<DbUser, BoxError> {
|
||||
if let Some(user) = conn
|
||||
.read_query_value::<DbUser>(
|
||||
format!("SELECT * FROM {USER_TABLE} WHERE email = $1"),
|
||||
(email.to_string(),),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(user);
|
||||
}
|
||||
return Err("not found".into());
|
||||
}
|
||||
|
||||
async fn async_main() -> Result<(), BoxError> {
|
||||
let args = DefaultCommandLineArgs::parse();
|
||||
let data_dir = DataDir(args.data_dir.clone());
|
||||
@@ -171,25 +156,13 @@ async fn async_main() -> Result<(), BoxError> {
|
||||
);
|
||||
}
|
||||
}
|
||||
Some(AdminSubCommands::Demote { email }) => {
|
||||
conn
|
||||
.execute(
|
||||
format!("UPDATE {USER_TABLE} SET admin = FALSE WHERE email = $1"),
|
||||
(email.clone(),),
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("'{email}' has been demoted");
|
||||
Some(AdminSubCommands::Demote { user }) => {
|
||||
let id = api::cli::demote_admin_to_user(&conn, to_user_reference(user)).await?;
|
||||
println!("Demoted admin to user for '{id}'");
|
||||
}
|
||||
Some(AdminSubCommands::Promote { email }) => {
|
||||
conn
|
||||
.execute(
|
||||
format!("UPDATE {USER_TABLE} SET admin = TRUE WHERE email = $1"),
|
||||
(email.clone(),),
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("'{email}' is now an admin");
|
||||
Some(AdminSubCommands::Promote { user }) => {
|
||||
let id = api::cli::promote_user_to_admin(&conn, to_user_reference(user)).await?;
|
||||
println!("Promoted user to admin for '{id}'");
|
||||
}
|
||||
None => {
|
||||
DefaultCommandLineArgs::command()
|
||||
@@ -205,31 +178,30 @@ async fn async_main() -> Result<(), BoxError> {
|
||||
let (conn, _) = api::init_main_db(Some(&data_dir), None, None)?;
|
||||
|
||||
match cmd {
|
||||
Some(UserSubCommands::ResetPassword { email, password }) => {
|
||||
if get_user_by_email(&conn, &email).await.is_err() {
|
||||
return Err(format!("User with email='{email}' not found.").into());
|
||||
}
|
||||
api::force_password_reset(&conn, email.clone(), password).await?;
|
||||
|
||||
println!("Password updated for '{email}'");
|
||||
Some(UserSubCommands::ChangePassword { user, password }) => {
|
||||
let id = api::cli::change_password(&conn, to_user_reference(user), &password).await?;
|
||||
println!("Updated password for '{id}'");
|
||||
}
|
||||
Some(UserSubCommands::MintToken { email }) => {
|
||||
let user = get_user_by_email(&conn, &email).await?;
|
||||
let jwt = api::JwtHelper::init_from_path(&data_dir).await?;
|
||||
|
||||
if !user.verified {
|
||||
warn!("User '{email}' not verified");
|
||||
}
|
||||
|
||||
let claims = TokenClaims::new(
|
||||
user.verified,
|
||||
user.uuid(),
|
||||
user.email,
|
||||
chrono::Duration::hours(12),
|
||||
);
|
||||
let token = jwt.encode(&claims)?;
|
||||
|
||||
println!("Bearer {token}");
|
||||
Some(UserSubCommands::ChangeEmail { user, new_email }) => {
|
||||
let id = api::cli::change_email(&conn, to_user_reference(user), &new_email).await?;
|
||||
println!("Updated email for '{id}'");
|
||||
}
|
||||
Some(UserSubCommands::Delete { user }) => {
|
||||
api::cli::delete_user(&conn, to_user_reference(user.clone())).await?;
|
||||
println!("Deleted user '{user}'");
|
||||
}
|
||||
Some(UserSubCommands::Verify { user, verified }) => {
|
||||
let id = api::cli::set_verified(&conn, to_user_reference(user), verified).await?;
|
||||
println!("Set verified={verified} for '{id}'");
|
||||
}
|
||||
Some(UserSubCommands::InvalidateSession { user }) => {
|
||||
api::cli::invalidate_sessions(&conn, to_user_reference(user.clone())).await?;
|
||||
println!("Sessions invalidated for '{user}'");
|
||||
}
|
||||
Some(UserSubCommands::MintToken { user }) => {
|
||||
let auth_token =
|
||||
api::cli::mint_auth_token(&data_dir, &conn, to_user_reference(user.clone())).await?;
|
||||
println!("Bearer {auth_token}");
|
||||
}
|
||||
None => {
|
||||
DefaultCommandLineArgs::command()
|
||||
@@ -269,6 +241,13 @@ async fn async_main() -> Result<(), BoxError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn to_user_reference(user: String) -> api::cli::UserReference {
|
||||
if user.contains("@") {
|
||||
return api::cli::UserReference::Email(user);
|
||||
}
|
||||
return api::cli::UserReference::Id(user);
|
||||
}
|
||||
|
||||
fn main() -> Result<(), BoxError> {
|
||||
let runtime = Rc::new(
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
|
||||
@@ -10,8 +10,8 @@ use ts_rs::TS;
|
||||
|
||||
use crate::admin::AdminError as Error;
|
||||
use crate::admin::rows::delete_row;
|
||||
use crate::admin::user::is_demo_admin;
|
||||
use crate::app_state::AppState;
|
||||
use crate::auth::util::is_admin;
|
||||
use crate::util::uuid_to_b64;
|
||||
|
||||
#[derive(Debug, Deserialize, Default, TS)]
|
||||
@@ -24,8 +24,10 @@ pub async fn delete_user_handler(
|
||||
State(state): State<AppState>,
|
||||
Json(request): Json<DeleteUserRequest>,
|
||||
) -> Result<Response, Error> {
|
||||
if state.demo_mode() && is_demo_admin(&state, &request.id).await {
|
||||
return Err(Error::Precondition("Deleting demo admin forbidden".into()));
|
||||
if is_admin(&state, &request.id).await {
|
||||
return Err(Error::Precondition(
|
||||
"Admins can only be deleted using the CLI to prevent abuse".into(),
|
||||
));
|
||||
}
|
||||
|
||||
delete_row(
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
use trailbase_sqlite::params;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::AppState;
|
||||
use crate::constants::USER_TABLE;
|
||||
|
||||
mod create_user;
|
||||
mod delete_user;
|
||||
mod list_users;
|
||||
@@ -14,26 +8,6 @@ pub(super) use delete_user::delete_user_handler;
|
||||
pub(super) use list_users::list_users_handler;
|
||||
pub(super) use update_user::update_user_handler;
|
||||
|
||||
pub async fn is_demo_admin(state: &AppState, id: &Uuid) -> bool {
|
||||
assert!(state.demo_mode());
|
||||
|
||||
let userid: [u8; 16] = id.into_bytes();
|
||||
return match state
|
||||
.user_conn()
|
||||
.read_query_value(
|
||||
format!("SELECT EXISTS(SELECT * FROM {USER_TABLE} WHERE id=$1 AND email='admin@localhost')"),
|
||||
params!(userid),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value.unwrap_or(true),
|
||||
Err(err) => {
|
||||
log::error!("{err}");
|
||||
true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use create_user::create_user_for_test;
|
||||
|
||||
|
||||
@@ -10,11 +10,16 @@ use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::admin::AdminError as Error;
|
||||
use crate::admin::user::is_demo_admin;
|
||||
use crate::app_state::AppState;
|
||||
use crate::auth::password::hash_password;
|
||||
use crate::auth::util::is_admin;
|
||||
use crate::constants::USER_TABLE;
|
||||
|
||||
/// Request changes to user with given `id`.
|
||||
///
|
||||
/// NOTE: We don't allow admin promotions and especially demotions, since they could easily be
|
||||
/// abused. Instead we relegate such critical actions to the CLI, which limits them to sys
|
||||
/// admins over mere TrailBase admins.
|
||||
#[derive(Debug, Serialize, Deserialize, Default, TS)]
|
||||
#[ts(export)]
|
||||
pub struct UpdateUserRequest {
|
||||
@@ -29,8 +34,10 @@ pub async fn update_user_handler(
|
||||
State(state): State<AppState>,
|
||||
Json(request): Json<UpdateUserRequest>,
|
||||
) -> Result<Response, Error> {
|
||||
if state.demo_mode() && is_demo_admin(&state, &request.id).await {
|
||||
return Err(Error::Precondition("Updating demo admin forbidden".into()));
|
||||
if is_admin(&state, &request.id).await {
|
||||
return Err(Error::Precondition(
|
||||
"Admins can only be updated using the CLI to prevent abuse".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let hashed_password = match &request.password {
|
||||
|
||||
@@ -24,7 +24,7 @@ pub(crate) async fn delete_handler(
|
||||
user: User,
|
||||
cookies: Cookies,
|
||||
) -> Result<Response, AuthError> {
|
||||
let _ = delete_all_sessions_for_user(&state, user.uuid).await;
|
||||
let _ = delete_all_sessions_for_user(state.user_conn(), user.uuid).await;
|
||||
|
||||
lazy_static! {
|
||||
static ref QUERY: String = format!(r#"DELETE FROM "{USER_TABLE}" WHERE id = $1"#);
|
||||
|
||||
@@ -285,7 +285,10 @@ pub async fn login_with_password(
|
||||
normalized_email: &str,
|
||||
password: &str,
|
||||
) -> Result<NewTokens, AuthError> {
|
||||
let db_user: DbUser = user_by_email(state, normalized_email).await?;
|
||||
let db_user: DbUser = user_by_email(state, normalized_email).await.map_err(|_| {
|
||||
// Don't leak if user wasn't found or password was wrong.
|
||||
return AuthError::Unauthorized;
|
||||
})?;
|
||||
|
||||
// Validate password.
|
||||
check_user_password(&db_user, password, state.demo_mode())?;
|
||||
@@ -293,14 +296,7 @@ pub async fn login_with_password(
|
||||
let (auth_token_ttl, _refresh_token_ttl) = state.access_config(|c| c.auth.token_ttls());
|
||||
let user_id = db_user.uuid();
|
||||
|
||||
let tokens = mint_new_tokens(
|
||||
state,
|
||||
db_user.verified,
|
||||
user_id,
|
||||
db_user.email,
|
||||
auth_token_ttl,
|
||||
)
|
||||
.await?;
|
||||
let tokens = mint_new_tokens(state.user_conn(), &db_user, auth_token_ttl).await?;
|
||||
|
||||
return Ok(NewTokens {
|
||||
id: user_id,
|
||||
|
||||
@@ -44,7 +44,7 @@ pub async fn logout_handler(
|
||||
remove_all_cookies(&cookies);
|
||||
|
||||
if let Some(user) = user {
|
||||
delete_all_sessions_for_user(&state, user.uuid).await?;
|
||||
delete_all_sessions_for_user(state.user_conn(), user.uuid).await?;
|
||||
}
|
||||
|
||||
return Ok(Redirect::to(redirect.as_deref().unwrap_or_else(|| {
|
||||
|
||||
@@ -8,7 +8,6 @@ use serde::Deserialize;
|
||||
use trailbase_sqlite::params;
|
||||
use ts_rs::TS;
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::app_state::AppState;
|
||||
use crate::constants::USER_TABLE;
|
||||
@@ -169,21 +168,3 @@ pub async fn reset_password_update_handler(
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub async fn force_password_reset(
|
||||
user_conn: &trailbase_sqlite::Connection,
|
||||
email: String,
|
||||
password: String,
|
||||
) -> Result<Uuid, AuthError> {
|
||||
let hashed_password = hash_password(&password)?;
|
||||
|
||||
lazy_static! {
|
||||
static ref UPDATE_PASSWORD_QUERY: String =
|
||||
format!("UPDATE '{USER_TABLE}' SET password_hash = $1 WHERE email = $2 RETURNING id");
|
||||
}
|
||||
|
||||
return user_conn
|
||||
.write_query_value(&*UPDATE_PASSWORD_QUERY, params!(hashed_password, email))
|
||||
.await?
|
||||
.ok_or(AuthError::NotFound);
|
||||
}
|
||||
|
||||
@@ -86,16 +86,8 @@ pub(crate) async fn auth_code_to_token_handler(
|
||||
};
|
||||
|
||||
let (auth_token_ttl, _refresh_token_ttl) = state.access_config(|c| c.auth.token_ttls());
|
||||
let user_id = db_user.uuid();
|
||||
|
||||
let tokens = mint_new_tokens(
|
||||
&state,
|
||||
db_user.verified,
|
||||
user_id,
|
||||
db_user.email,
|
||||
auth_token_ttl,
|
||||
)
|
||||
.await?;
|
||||
let tokens = mint_new_tokens(state.user_conn(), &db_user, auth_token_ttl).await?;
|
||||
let auth_token = state
|
||||
.jwt()
|
||||
.encode(&tokens.auth_token_claims)
|
||||
|
||||
180
trailbase-core/src/auth/cli.rs
Normal file
180
trailbase-core/src/auth/cli.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
use base64::prelude::*;
|
||||
use lazy_static::lazy_static;
|
||||
use trailbase_sqlite::{Connection, params};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::DataDir;
|
||||
use crate::auth::AuthError;
|
||||
use crate::auth::password::hash_password;
|
||||
use crate::auth::tokens::mint_new_tokens;
|
||||
use crate::auth::user::DbUser;
|
||||
use crate::auth::util::{get_user_by_email, get_user_by_id};
|
||||
use crate::constants::USER_TABLE;
|
||||
|
||||
pub enum UserReference {
|
||||
Email(String),
|
||||
Id(String),
|
||||
}
|
||||
|
||||
impl UserReference {
|
||||
async fn lookup_user(&self, user_conn: &Connection) -> Result<DbUser, AuthError> {
|
||||
return match self {
|
||||
Self::Email(email) => get_user_by_email(user_conn, email).await,
|
||||
Self::Id(id) => {
|
||||
let decoded_id = Uuid::parse_str(id).or_else(|_| {
|
||||
let bytes = BASE64_URL_SAFE.decode(id).map_err(|err| {
|
||||
AuthError::FailedDependency(format!("Failed to parse Base64: {err}").into())
|
||||
})?;
|
||||
return Uuid::from_slice(&bytes).map_err(|err| {
|
||||
AuthError::FailedDependency(format!("Failed to parse UUID from slice: {err}").into())
|
||||
});
|
||||
})?;
|
||||
get_user_by_id(user_conn, &decoded_id).await
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn change_password(
|
||||
user_conn: &trailbase_sqlite::Connection,
|
||||
user: UserReference,
|
||||
password: &str,
|
||||
) -> Result<Uuid, AuthError> {
|
||||
let db_user = user.lookup_user(user_conn).await?;
|
||||
|
||||
let hashed_password = hash_password(password)?;
|
||||
|
||||
lazy_static! {
|
||||
static ref UPDATE_PASSWORD_QUERY: String =
|
||||
format!("UPDATE '{USER_TABLE}' SET password_hash = $1 WHERE id = $2 RETURNING id");
|
||||
}
|
||||
|
||||
return user_conn
|
||||
.write_query_value(
|
||||
&*UPDATE_PASSWORD_QUERY,
|
||||
params!(hashed_password, db_user.id),
|
||||
)
|
||||
.await?
|
||||
.ok_or(AuthError::NotFound);
|
||||
}
|
||||
|
||||
pub async fn change_email(
|
||||
user_conn: &trailbase_sqlite::Connection,
|
||||
user: UserReference,
|
||||
new_email: &str,
|
||||
) -> Result<Uuid, AuthError> {
|
||||
let db_user = user.lookup_user(user_conn).await?;
|
||||
|
||||
lazy_static! {
|
||||
static ref UPDATE_EMAIL_QUERY: String =
|
||||
format!("UPDATE '{USER_TABLE}' SET email = $1 WHERE id = $2 RETURNING id");
|
||||
}
|
||||
|
||||
return user_conn
|
||||
.write_query_value(
|
||||
&*UPDATE_EMAIL_QUERY,
|
||||
params!(new_email.to_string(), db_user.id),
|
||||
)
|
||||
.await?
|
||||
.ok_or(AuthError::NotFound);
|
||||
}
|
||||
|
||||
pub async fn delete_user(
|
||||
user_conn: &trailbase_sqlite::Connection,
|
||||
user: UserReference,
|
||||
) -> Result<(), AuthError> {
|
||||
let db_user = user.lookup_user(user_conn).await?;
|
||||
|
||||
lazy_static! {
|
||||
static ref DELETE_QUERY: String = format!(r#"DELETE FROM "{USER_TABLE}" WHERE id = $1"#);
|
||||
}
|
||||
|
||||
let rows_affected = user_conn
|
||||
.execute(&*DELETE_QUERY, params!(db_user.id))
|
||||
.await?;
|
||||
if rows_affected > 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
return Err(AuthError::NotFound);
|
||||
}
|
||||
|
||||
pub async fn set_verified(
|
||||
user_conn: &trailbase_sqlite::Connection,
|
||||
user: UserReference,
|
||||
verified: bool,
|
||||
) -> Result<Uuid, AuthError> {
|
||||
let db_user = user.lookup_user(user_conn).await?;
|
||||
|
||||
lazy_static! {
|
||||
static ref SET_VERIFIED_QUERY: String =
|
||||
format!("UPDATE '{USER_TABLE}' SET verified = $1 WHERE id = $2 RETURNING id");
|
||||
}
|
||||
|
||||
return user_conn
|
||||
.write_query_value(&*SET_VERIFIED_QUERY, params!(verified, db_user.id))
|
||||
.await?
|
||||
.ok_or(AuthError::NotFound);
|
||||
}
|
||||
|
||||
pub async fn invalidate_sessions(
|
||||
user_conn: &trailbase_sqlite::Connection,
|
||||
user: UserReference,
|
||||
) -> Result<(), AuthError> {
|
||||
let db_user = user.lookup_user(user_conn).await?;
|
||||
|
||||
crate::auth::util::delete_all_sessions_for_user(user_conn, Uuid::from_bytes(db_user.id)).await?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
pub async fn mint_auth_token(
|
||||
data_dir: &DataDir,
|
||||
user_conn: &trailbase_sqlite::Connection,
|
||||
user: UserReference,
|
||||
) -> Result<String, AuthError> {
|
||||
let jwt = crate::api::JwtHelper::init_from_path(data_dir)
|
||||
.await
|
||||
.map_err(|err| AuthError::FailedDependency(err.into()))?;
|
||||
let db_user = user.lookup_user(user_conn).await?;
|
||||
|
||||
let tokens = mint_new_tokens(user_conn, &db_user, chrono::Duration::hours(12)).await?;
|
||||
|
||||
let auth_token = jwt
|
||||
.encode(&tokens.auth_token_claims)
|
||||
.map_err(|err| AuthError::Internal(err.into()))?;
|
||||
|
||||
return Ok(auth_token);
|
||||
}
|
||||
|
||||
pub async fn promote_user_to_admin(
|
||||
user_conn: &trailbase_sqlite::Connection,
|
||||
user: UserReference,
|
||||
) -> Result<Uuid, AuthError> {
|
||||
let db_user = user.lookup_user(user_conn).await?;
|
||||
|
||||
lazy_static! {
|
||||
static ref PROMOTE_ADMIN_QUERY: String =
|
||||
format!("UPDATE {USER_TABLE} SET admin = TRUE WHERE id = $1 RETURNING id");
|
||||
}
|
||||
return user_conn
|
||||
.write_query_value(&*PROMOTE_ADMIN_QUERY, params!(db_user.id))
|
||||
.await?
|
||||
.ok_or(AuthError::NotFound);
|
||||
}
|
||||
|
||||
pub async fn demote_admin_to_user(
|
||||
user_conn: &trailbase_sqlite::Connection,
|
||||
user: UserReference,
|
||||
) -> Result<Uuid, AuthError> {
|
||||
let db_user = user.lookup_user(user_conn).await?;
|
||||
|
||||
lazy_static! {
|
||||
static ref DEMOTE_ADMIN_QUERY: String =
|
||||
format!("UPDATE {USER_TABLE} SET admin = FALSE WHERE id = $1 RETURNING id");
|
||||
}
|
||||
return user_conn
|
||||
.write_query_value(&*DEMOTE_ADMIN_QUERY, params!(db_user.id))
|
||||
.await?
|
||||
.ok_or(AuthError::NotFound);
|
||||
}
|
||||
@@ -8,8 +8,6 @@ use thiserror::Error;
|
||||
pub enum AuthError {
|
||||
#[error("Unauthorized")]
|
||||
Unauthorized,
|
||||
#[error("Unauthorized")]
|
||||
UnauthorizedExt(Box<dyn std::error::Error + Send + Sync>),
|
||||
#[error("Forbidden")]
|
||||
Forbidden,
|
||||
#[error("Conflict")]
|
||||
@@ -66,10 +64,6 @@ impl IntoResponse for AuthError {
|
||||
fn into_response(self) -> Response {
|
||||
let (status, body) = match self {
|
||||
Self::Unauthorized => (StatusCode::UNAUTHORIZED, None),
|
||||
Self::UnauthorizedExt(msg) if cfg!(debug_assertions) => {
|
||||
(StatusCode::UNAUTHORIZED, Some(msg.to_string()))
|
||||
}
|
||||
Self::UnauthorizedExt(_msg) => (StatusCode::UNAUTHORIZED, None),
|
||||
Self::Forbidden => (StatusCode::FORBIDDEN, None),
|
||||
Self::Conflict => (StatusCode::CONFLICT, None),
|
||||
Self::NotFound => (StatusCode::NOT_FOUND, None),
|
||||
|
||||
@@ -4,6 +4,7 @@ use axum::{
|
||||
};
|
||||
use utoipa::OpenApi;
|
||||
|
||||
pub mod cli;
|
||||
pub mod jwt;
|
||||
pub mod user;
|
||||
|
||||
@@ -17,7 +18,6 @@ pub(crate) mod util;
|
||||
mod error;
|
||||
mod ui;
|
||||
|
||||
pub use api::reset_password::force_password_reset;
|
||||
pub use error::AuthError;
|
||||
pub use jwt::{JwtHelper, TokenClaims};
|
||||
pub(crate) use ui::auth_ui_router;
|
||||
|
||||
@@ -131,14 +131,7 @@ pub(crate) async fn callback_from_external_auth_provider(
|
||||
auth_token_claims,
|
||||
refresh_token,
|
||||
..
|
||||
} = mint_new_tokens(
|
||||
&state,
|
||||
db_user.verified,
|
||||
db_user.uuid(),
|
||||
db_user.email,
|
||||
expires_in,
|
||||
)
|
||||
.await?;
|
||||
} = mint_new_tokens(state.user_conn(), &db_user, expires_in).await?;
|
||||
|
||||
let auth_token = state
|
||||
.jwt()
|
||||
|
||||
@@ -5,7 +5,7 @@ use axum::{
|
||||
use chrono::Duration;
|
||||
use lazy_static::lazy_static;
|
||||
use tower_cookies::Cookies;
|
||||
use trailbase_sqlite::params;
|
||||
use trailbase_sqlite::{Connection, params};
|
||||
|
||||
use crate::app_state::AppState;
|
||||
use crate::auth::AuthError;
|
||||
@@ -159,20 +159,19 @@ pub struct FreshTokens {
|
||||
}
|
||||
|
||||
pub(crate) async fn mint_new_tokens(
|
||||
state: &AppState,
|
||||
verified: bool,
|
||||
user_id: uuid::Uuid,
|
||||
user_email: String,
|
||||
user_conn: &Connection,
|
||||
db_user: &DbUser,
|
||||
expires_in: Duration,
|
||||
) -> Result<FreshTokens, AuthError> {
|
||||
assert!(verified);
|
||||
let verified = db_user.verified;
|
||||
if !verified {
|
||||
return Err(AuthError::Internal(
|
||||
"Cannot mint tokens for unverified user".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let claims = TokenClaims::new(verified, user_id, user_email, expires_in);
|
||||
let user_id = db_user.uuid();
|
||||
let claims = TokenClaims::new(verified, user_id, db_user.email.clone(), expires_in);
|
||||
|
||||
// Unlike JWT auth tokens, refresh tokens are opaque.
|
||||
let refresh_token = generate_random_string(REFRESH_TOKEN_LENGTH);
|
||||
@@ -181,8 +180,7 @@ pub(crate) async fn mint_new_tokens(
|
||||
format!("INSERT INTO '{SESSION_TABLE}' (user, refresh_token) VALUES ($1, $2)");
|
||||
}
|
||||
|
||||
state
|
||||
.user_conn()
|
||||
user_conn
|
||||
.execute(
|
||||
&*QUERY,
|
||||
params!(user_id.into_bytes().to_vec(), refresh_token.clone(),),
|
||||
|
||||
@@ -105,8 +105,7 @@ impl User {
|
||||
/// Construct new verified [User] from [TokenClaims]. This is used when picking
|
||||
/// credentials/tokens from headers/cookies.
|
||||
pub(crate) fn from_token_claims(claims: TokenClaims) -> Result<Self, AuthError> {
|
||||
let uuid = b64_to_uuid(&claims.sub)
|
||||
.map_err(|_err| AuthError::UnauthorizedExt("invalid user id".into()))?;
|
||||
let uuid = b64_to_uuid(&claims.sub).map_err(|_err| AuthError::BadRequest("invalid user id"))?;
|
||||
|
||||
return Ok(Self {
|
||||
id: claims.sub,
|
||||
|
||||
@@ -6,12 +6,12 @@ use tower_cookies::{
|
||||
Cookie, Cookies,
|
||||
cookie::{self, SameSite},
|
||||
};
|
||||
use trailbase_sqlite::params;
|
||||
use trailbase_sqlite::{Connection, params};
|
||||
use validator::ValidateEmail;
|
||||
|
||||
use crate::AppState;
|
||||
use crate::auth::AuthError;
|
||||
use crate::auth::user::{DbUser, User};
|
||||
use crate::auth::user::DbUser;
|
||||
use crate::constants::{
|
||||
COOKIE_AUTH_TOKEN, COOKIE_OAUTH_STATE, COOKIE_REFRESH_TOKEN, SESSION_TABLE, USER_TABLE,
|
||||
};
|
||||
@@ -157,16 +157,16 @@ pub async fn get_user_by_email(
|
||||
let db_user = user_conn
|
||||
.read_query_value::<DbUser>(&*QUERY, params!(email.to_string()))
|
||||
.await
|
||||
.map_err(|_err| AuthError::UnauthorizedExt("user not found by email".into()))?;
|
||||
.map_err(|_err| AuthError::NotFound)?;
|
||||
|
||||
return db_user.ok_or_else(|| AuthError::UnauthorizedExt("invalid user".into()));
|
||||
return db_user.ok_or_else(|| AuthError::NotFound);
|
||||
}
|
||||
|
||||
pub async fn user_by_id(state: &AppState, id: &uuid::Uuid) -> Result<DbUser, AuthError> {
|
||||
return get_user_by_id(state.user_conn(), id).await;
|
||||
}
|
||||
|
||||
async fn get_user_by_id(
|
||||
pub async fn get_user_by_id(
|
||||
user_conn: &trailbase_sqlite::Connection,
|
||||
id: &uuid::Uuid,
|
||||
) -> Result<DbUser, AuthError> {
|
||||
@@ -176,9 +176,9 @@ async fn get_user_by_id(
|
||||
let db_user = user_conn
|
||||
.read_query_value::<DbUser>(&*QUERY, params!(id.into_bytes()))
|
||||
.await
|
||||
.map_err(|_err| AuthError::UnauthorizedExt("User not found by id".into()))?;
|
||||
.map_err(|_err| AuthError::NotFound)?;
|
||||
|
||||
return db_user.ok_or_else(|| AuthError::UnauthorizedExt("invalid user".into()));
|
||||
return db_user.ok_or_else(|| AuthError::NotFound);
|
||||
}
|
||||
|
||||
pub async fn user_exists(state: &AppState, email: &str) -> Result<bool, AuthError> {
|
||||
@@ -193,14 +193,14 @@ pub async fn user_exists(state: &AppState, email: &str) -> Result<bool, AuthErro
|
||||
.ok_or_else(|| AuthError::Internal("query should return".into()));
|
||||
}
|
||||
|
||||
pub(crate) async fn is_admin(state: &AppState, user: &User) -> bool {
|
||||
pub(crate) async fn is_admin(state: &AppState, user_id: &uuid::Uuid) -> bool {
|
||||
lazy_static! {
|
||||
static ref QUERY: String = format!(r#"SELECT admin FROM "{USER_TABLE}" WHERE id = $1"#);
|
||||
};
|
||||
|
||||
let Ok(Some(row)) = state
|
||||
.user_conn()
|
||||
.read_query_row_f(&*QUERY, params!(user.uuid.as_bytes().to_vec()), |row| {
|
||||
.read_query_row_f(&*QUERY, params!(user_id.as_bytes().to_vec()), |row| {
|
||||
row.get(0)
|
||||
})
|
||||
.await
|
||||
@@ -212,7 +212,7 @@ pub(crate) async fn is_admin(state: &AppState, user: &User) -> bool {
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_all_sessions_for_user(
|
||||
state: &AppState,
|
||||
user_conn: &Connection,
|
||||
user_id: uuid::Uuid,
|
||||
) -> Result<usize, AuthError> {
|
||||
lazy_static! {
|
||||
@@ -220,8 +220,7 @@ pub(crate) async fn delete_all_sessions_for_user(
|
||||
};
|
||||
|
||||
return Ok(
|
||||
state
|
||||
.user_conn()
|
||||
user_conn
|
||||
.execute(
|
||||
&*QUERY,
|
||||
[trailbase_sqlite::Value::Blob(user_id.into_bytes().to_vec())],
|
||||
|
||||
@@ -63,7 +63,7 @@ pub mod openapi {
|
||||
pub mod api {
|
||||
pub use crate::admin::user::{CreateUserRequest, create_user_handler};
|
||||
pub use crate::auth::api::login::login_with_password;
|
||||
pub use crate::auth::{JwtHelper, TokenClaims, force_password_reset};
|
||||
pub use crate::auth::{JwtHelper, TokenClaims, cli};
|
||||
pub use crate::connection::{Connection, init_main_db};
|
||||
pub use crate::email::{Email, EmailError};
|
||||
pub use crate::migrations::new_unique_migration_filename;
|
||||
|
||||
@@ -383,7 +383,7 @@ async fn assert_admin_api_access(
|
||||
) -> Result<Response, AuthError> {
|
||||
let user = req.extract_parts_with_state::<User, _>(&state).await?;
|
||||
|
||||
if !is_admin(&state, &user).await {
|
||||
if !is_admin(&state, &user.uuid).await {
|
||||
return Err(AuthError::Forbidden);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user