From 61811e4ef258e27d5dde2c4614a1b3fddd9ac4a4 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 24 Sep 2024 20:49:13 -0400 Subject: [PATCH] transactor handle connection; session creation --- .../user_sessions/create_user_session.rs | 1 + .../create_user_session_with_transactor.rs | 47 +++++++ .../src/queries/users/user_sessions/mod.rs | 1 + .../lib/mysql_queries/src/utils/transactor.rs | 15 +++ .../endpoints/users/google_sso_handler.rs | 120 +++++++++++++----- 5 files changed, 153 insertions(+), 31 deletions(-) create mode 100644 crates/lib/mysql_queries/src/queries/users/user_sessions/create_user_session_with_transactor.rs diff --git a/crates/lib/mysql_queries/src/queries/users/user_sessions/create_user_session.rs b/crates/lib/mysql_queries/src/queries/users/user_sessions/create_user_session.rs index ba58ed08f8..bd49371c62 100644 --- a/crates/lib/mysql_queries/src/queries/users/user_sessions/create_user_session.rs +++ b/crates/lib/mysql_queries/src/queries/users/user_sessions/create_user_session.rs @@ -5,6 +5,7 @@ use sqlx::MySqlPool; use errors::AnyhowResult; use tokens::tokens::user_sessions::UserSessionToken; +#[deprecated(note="use create_user_session_with_transactor")] pub async fn create_user_session(user_token: &str, ip_address: &str, mysql_pool: &MySqlPool) -> AnyhowResult { diff --git a/crates/lib/mysql_queries/src/queries/users/user_sessions/create_user_session_with_transactor.rs b/crates/lib/mysql_queries/src/queries/users/user_sessions/create_user_session_with_transactor.rs new file mode 100644 index 0000000000..a1da38930e --- /dev/null +++ b/crates/lib/mysql_queries/src/queries/users/user_sessions/create_user_session_with_transactor.rs @@ -0,0 +1,47 @@ +use anyhow::anyhow; +use log::info; +use sqlx::MySqlPool; + +use crate::utils::transactor::Transactor; +use errors::AnyhowResult; +use tokens::tokens::user_sessions::UserSessionToken; + +pub async fn create_user_session_with_transactor( + user_token: &str, + ip_address: &str, + mut transactor: Transactor<'_, '_> +) + -> AnyhowResult +{ + let session_token = UserSessionToken::generate().to_string(); + + let query = sqlx::query!( + r#" +INSERT INTO user_sessions ( + token, + user_token, + ip_address_creation, + expires_at +) +VALUES ( ?, ?, ?, NOW() + interval 1 year ) + "#, + session_token, + user_token.to_string(), + ip_address.to_string(), + ); + + let query_result = transactor.execute(query).await; + + let record_id = match query_result { + Ok(res) => { + res.last_insert_id() + }, + Err(err) => { + return Err(anyhow!("session creation DB error: {:?}", err)); + } + }; + + info!("Created session id: {}", record_id); + + Ok(session_token) +} diff --git a/crates/lib/mysql_queries/src/queries/users/user_sessions/mod.rs b/crates/lib/mysql_queries/src/queries/users/user_sessions/mod.rs index 6964dedf22..a591ae7554 100644 --- a/crates/lib/mysql_queries/src/queries/users/user_sessions/mod.rs +++ b/crates/lib/mysql_queries/src/queries/users/user_sessions/mod.rs @@ -1,4 +1,5 @@ pub mod create_user_session; +pub mod create_user_session_with_transactor; pub mod delete_user_session; pub mod get_user_session_by_token; pub mod get_user_session_by_token_light; diff --git a/crates/lib/mysql_queries/src/utils/transactor.rs b/crates/lib/mysql_queries/src/utils/transactor.rs index d49a4ad89b..819904f756 100644 --- a/crates/lib/mysql_queries/src/utils/transactor.rs +++ b/crates/lib/mysql_queries/src/utils/transactor.rs @@ -15,6 +15,11 @@ pub enum Transactor<'e, 't> { pool: &'e MySqlPool, }, + /// Transactions over an open connection. + Connection { + connection: &'e mut sqlx::MySqlConnection, + }, + /// Operations over an open transaction. Transaction { transaction: &'e mut Transaction<'t, MySql>, @@ -30,6 +35,13 @@ impl <'e, 't>Transactor<'e, 't> { } } + /// Constructor + pub fn for_connection(connection: &'e mut sqlx::MySqlConnection) -> Self { + Transactor::Connection { + connection, + } + } + /// Constructor pub fn for_transaction(transaction: &'e mut Transaction<'t, MySql>) -> Self { Transactor::Transaction { @@ -46,6 +58,9 @@ impl <'e, 't>Transactor<'e, 't> { Transactor::Pool { pool } => { query.execute(pool).await }, + Transactor::Connection { connection } => { + query.execute(connection).await + }, Transactor::Transaction { transaction } => { query.execute(&mut **transaction).await }, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/users/google_sso_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/users/google_sso_handler.rs index 6fe03147b3..ee8189efc6 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/users/google_sso_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/users/google_sso_handler.rs @@ -29,10 +29,11 @@ use google_sign_in::decode_and_verify_token_claims::decode_and_verify_token_clai use google_sign_in::VerificationOptions; use http_server_common::request::get_request_ip::get_request_ip; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; -use log::{info, warn}; +use log::{error, info, warn}; use mysql_queries::mediators::firehose_publisher::FirehosePublisher; use mysql_queries::queries::google_sign_in_accounts::get_google_sign_in_account_by_subject::{get_google_sign_in_account, GoogleSignInAccount}; use mysql_queries::queries::google_sign_in_accounts::insert_google_sign_in_account::{insert_google_sign_in_account, InsertGoogleSignInArgs}; +use mysql_queries::queries::users::user::account_creation::create_account_error::CreateAccountError; use mysql_queries::queries::users::user::account_creation::create_account_from_google_sso::{create_account_from_google_sso, CreateAccountFromGoogleSsoArgs}; use mysql_queries::queries::users::user_sessions::create_user_session::create_user_session; use mysql_queries::utils::transactor::Transactor; @@ -42,6 +43,7 @@ use sqlx::{Acquire, MySql, MySqlPool}; use tokens::tokens::user_sessions::UserSessionToken; use user_input_common::check_for_slurs::contains_slurs; use utoipa::ToSchema; +use mysql_queries::queries::users::user_sessions::create_user_session_with_transactor::create_user_session_with_transactor; #[derive(ToSchema, Deserialize)] pub struct GoogleCreateAccountRequest { @@ -179,6 +181,13 @@ pub async fn create_account_from_google_sign_in_handler( })?; match maybe_sso_account { + Some(sso_account) => { + login_existing_sso_account( + &http_request, + sso_account, + &mut mysql_connection, + ).await? + }, None => { create_new_sso_account( &http_request, @@ -187,7 +196,6 @@ pub async fn create_account_from_google_sign_in_handler( claims, ).await? }, - Some(sso_account) => login_existing_sso_account(sso_account).await, } /* @@ -267,6 +275,35 @@ fn build_options() -> VerificationOptions { } } +async fn login_existing_sso_account( + http_request: &HttpRequest, + sso_account: GoogleSignInAccount, + mysql_connection: &mut PoolConnection, +) + -> Result<(), GoogleCreateAccountErrorResponse> +{ + let user_token = match sso_account.maybe_user_token { + Some(token) => token.clone(), + None => { + // NB: If accounts get into this state (e.g. if we support de-linking), we'll need to + // consider how to migrate accounts and handle all the various account states. + // For now, we'll just deny this possibility. + warn!("no user token for existing google sign in account"); + return Err(GoogleCreateAccountErrorResponse::server_error()); + }, + }; + + let ip_address = get_request_ip(&http_request); + + let create_session_result = create_user_session_with_transactor( + user_token.as_str(), + &ip_address, + Transactor::for_connection(mysql_connection), + ).await; + + Ok(()) +} + async fn create_new_sso_account( http_request: &HttpRequest, subject: &str, @@ -283,11 +320,6 @@ async fn create_new_sso_account( let ip_address = get_request_ip(&http_request); - let display_name = generate_random_username(); - let username = display_name.trim().to_lowercase(); - - info!("generated username: {}", username); - let mut transaction = mysql_connection.begin() .await .map_err(|e| { @@ -295,9 +327,58 @@ async fn create_new_sso_account( GoogleCreateAccountErrorResponse::server_error() })?; + // NB: We use this routine to "normalize" email addresses in the user table. + // It won't necessarily match the email address in the Google claims. + // A better normalization function in the future may handle dots and plus + // signs in Gmail addresses, for instance. + let user_email_address = email_address.trim().to_lowercase(); + let user_email_gravatar_hash = email_to_gravatar(&user_email_address); + + let mut maybe_user_token = None; + + for _ in 0..3 { + // NB: We try a few times to make sure we don't hit a username collision. + let display_name = generate_random_username(); + let username = display_name.trim().to_lowercase(); + + info!("generated username: {}", username); + + let result = create_account_from_google_sso( + CreateAccountFromGoogleSsoArgs { + username: &username, + display_name: &display_name, + email_address: &user_email_address, + email_gravatar_hash: &user_email_gravatar_hash, + email_confirmed_by_google: claims.email_verified(), + ip_address: &ip_address, + maybe_source: None, // TOO: Add source + }, + Transactor::for_transaction(&mut transaction), + ).await; + + match result { + Ok(token) => { + maybe_user_token = Some(token); + break; + }, + Err(CreateAccountError::UsernameIsTaken) => { + continue; // NB: We'll try again with a new username. + }, + Err(err) => { + warn!("error creating account from google sso: {:?}", err); + return Err(GoogleCreateAccountErrorResponse::server_error()); + }, + } + } + + let user_token = maybe_user_token.ok_or_else(|| { + error!("no username without collision after several tries"); + GoogleCreateAccountErrorResponse::server_error() + })?; + let _token = insert_google_sign_in_account(InsertGoogleSignInArgs { subject, - maybe_user_token: None, + maybe_user_token: Some(&user_token), email_address, is_email_verified: claims.email_verified(), maybe_locale: None, @@ -311,28 +392,5 @@ async fn create_new_sso_account( GoogleCreateAccountErrorResponse::server_error() })?; - let email_address = email_address.trim().to_lowercase(); - let email_gravatar_hash = email_to_gravatar(&email_address); - - let user_token = create_account_from_google_sso( - CreateAccountFromGoogleSsoArgs { - username: &username, - display_name: &display_name, - email_address: &email_address, - email_gravatar_hash: &email_gravatar_hash, - email_confirmed_by_google: claims.email_verified(), - ip_address: &ip_address, - maybe_source: None, - }, - Transactor::for_transaction(&mut transaction), - ).await.map_err(|err| { - warn!("error creating account from google sso: {:?}", err); - GoogleCreateAccountErrorResponse::server_error() - })?; - Ok(()) } - -async fn login_existing_sso_account(sso_account: GoogleSignInAccount) { - -}