From 74276140c6a0bad1e3c858070af2e873de67511c Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Fri, 4 Apr 2025 17:52:08 +0100 Subject: [PATCH] UNFINISHED: finish active sessions when replacing a device --- crates/handlers/src/compat/login.rs | 8 +++ ...d055954f697f257de026b74ec408938a52a1a.json | 16 ++++++ ...6691f260a7eca4bf494d6fb11c7cf699adaad.json | 16 ++++++ crates/storage-pg/src/app_session.rs | 55 ++++++++++++++++++- crates/storage/src/app_session.rs | 23 +++++++- 5 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 crates/storage-pg/.sqlx/query-373f7eb215b0e515b000a37e55bd055954f697f257de026b74ec408938a52a1a.json create mode 100644 crates/storage-pg/.sqlx/query-b74e4d620bed4832a4e8e713a346691f260a7eca4bf494d6fb11c7cf699adaad.json diff --git a/crates/handlers/src/compat/login.rs b/crates/handlers/src/compat/login.rs index 70b835f32..d36956a08 100644 --- a/crates/handlers/src/compat/login.rs +++ b/crates/handlers/src/compat/login.rs @@ -468,6 +468,10 @@ async fn token_login( .await .map_err(RouteError::ProvisionDeviceFailed)?; + repo.app_session() + .finish_sessions_to_replace_device(clock, &browser_session.user, &device) + .await?; + let compat_session = repo .compat_session() .add( @@ -563,6 +567,10 @@ async fn user_password_login( .await .map_err(RouteError::ProvisionDeviceFailed)?; + repo.app_session() + .finish_sessions_to_replace_device(clock, &user, &device) + .await?; + let session = repo .compat_session() .add(&mut rng, clock, &user, device, None, false) diff --git a/crates/storage-pg/.sqlx/query-373f7eb215b0e515b000a37e55bd055954f697f257de026b74ec408938a52a1a.json b/crates/storage-pg/.sqlx/query-373f7eb215b0e515b000a37e55bd055954f697f257de026b74ec408938a52a1a.json new file mode 100644 index 000000000..9ebd78f6f --- /dev/null +++ b/crates/storage-pg/.sqlx/query-373f7eb215b0e515b000a37e55bd055954f697f257de026b74ec408938a52a1a.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE oauth2_sessions SET finished_at = $3 WHERE user_id = $1 AND $2 = ANY(scope_list) AND finished_at IS NULL\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "373f7eb215b0e515b000a37e55bd055954f697f257de026b74ec408938a52a1a" +} diff --git a/crates/storage-pg/.sqlx/query-b74e4d620bed4832a4e8e713a346691f260a7eca4bf494d6fb11c7cf699adaad.json b/crates/storage-pg/.sqlx/query-b74e4d620bed4832a4e8e713a346691f260a7eca4bf494d6fb11c7cf699adaad.json new file mode 100644 index 000000000..68f1b1764 --- /dev/null +++ b/crates/storage-pg/.sqlx/query-b74e4d620bed4832a4e8e713a346691f260a7eca4bf494d6fb11c7cf699adaad.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE compat_sessions SET finished_at = $3 WHERE user_id = $1 AND device_id = $2 AND finished_at IS NULL\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "b74e4d620bed4832a4e8e713a346691f260a7eca4bf494d6fb11c7cf699adaad" +} diff --git a/crates/storage-pg/src/app_session.rs b/crates/storage-pg/src/app_session.rs index ea97ec2d4..2ea21d940 100644 --- a/crates/storage-pg/src/app_session.rs +++ b/crates/storage-pg/src/app_session.rs @@ -7,9 +7,11 @@ //! A module containing PostgreSQL implementation of repositories for sessions use async_trait::async_trait; -use mas_data_model::{CompatSession, CompatSessionState, Device, Session, SessionState, UserAgent}; +use mas_data_model::{ + CompatSession, CompatSessionState, Device, Session, SessionState, User, UserAgent, +}; use mas_storage::{ - Page, Pagination, + Clock, Page, Pagination, app_session::{AppSession, AppSessionFilter, AppSessionRepository, AppSessionState}, compat::CompatSessionFilter, oauth2::OAuth2SessionFilter, @@ -21,6 +23,7 @@ use sea_query::{ use sea_query_binder::SqlxBinder; use sqlx::PgConnection; use ulid::Ulid; +use uuid::Uuid; use crate::{ DatabaseError, ExecuteExt, @@ -457,6 +460,54 @@ impl AppSessionRepository for PgAppSessionRepository<'_> { .try_into() .map_err(DatabaseError::to_invalid_operation) } + + #[tracing::instrument( + name = "db.app_session.finish_sessions_to_replace_device", + fields( + db.query.text, + %user.id, + %device_id = device.as_str() + ), + skip_all, + err, + )] + async fn finish_sessions_to_replace_device( + &mut self, + clock: &dyn Clock, + user: &User, + device: &Device, + ) -> Result<(), Self::Error> { + // TODO need to invoke this from all the oauth2 login sites + // TODO CREATE A SECOND SPAN FOR THE SECOND QUERY + let finished_at = clock.now(); + sqlx::query!( + " + UPDATE compat_sessions SET finished_at = $3 WHERE user_id = $1 AND device_id = $2 AND finished_at IS NULL + ", + Uuid::from(user.id), + device.as_str(), + finished_at + ) + .traced() + .execute(&mut *self.conn) + .await?; + + if let Ok(device_as_scope_token) = device.to_scope_token() { + sqlx::query!( + " + UPDATE oauth2_sessions SET finished_at = $3 WHERE user_id = $1 AND $2 = ANY(scope_list) AND finished_at IS NULL + ", + Uuid::from(user.id), + device_as_scope_token.as_str(), + finished_at + ) + .traced() + .execute(&mut *self.conn) + .await?; + } + + Ok(()) + } } #[cfg(test)] diff --git a/crates/storage/src/app_session.rs b/crates/storage/src/app_session.rs index 52fc4483d..fd1850d3d 100644 --- a/crates/storage/src/app_session.rs +++ b/crates/storage/src/app_session.rs @@ -10,7 +10,7 @@ use async_trait::async_trait; use chrono::{DateTime, Utc}; use mas_data_model::{BrowserSession, CompatSession, Device, Session, User}; -use crate::{Page, Pagination, repository_impl}; +use crate::{Clock, Page, Pagination, repository_impl}; /// The state of a session #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -188,6 +188,20 @@ pub trait AppSessionRepository: Send + Sync { /// /// Returns [`Self::Error`] if the underlying repository fails async fn count(&mut self, filter: AppSessionFilter<'_>) -> Result; + + /// Finishes any application sessions that are using the specified device's + /// ID. + /// + /// This is intended for logging in using an existing device ID (i.e. + /// replacing a device). + /// + /// Should be called *before* creating a new session for the device. + async fn finish_sessions_to_replace_device( + &mut self, + clock: &dyn Clock, + user: &User, + device: &Device, + ) -> Result<(), Self::Error>; } repository_impl!(AppSessionRepository: @@ -198,4 +212,11 @@ repository_impl!(AppSessionRepository: ) -> Result, Self::Error>; async fn count(&mut self, filter: AppSessionFilter<'_>) -> Result; + + async fn finish_sessions_to_replace_device( + &mut self, + clock: &dyn Clock, + user: &User, + device: &Device, + ) -> Result<(), Self::Error>; );