Skip to content

Commit cef1369

Browse files
committed
Support compatibility sessions that do not have devices
This is needed for full support of existing Synapse access tokens
1 parent 2605d3a commit cef1369

File tree

12 files changed

+62
-41
lines changed

12 files changed

+62
-41
lines changed

crates/cli/src/commands/manage.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ impl Options {
341341
info!(
342342
%compat_access_token.id,
343343
%compat_session.id,
344-
%compat_session.device,
344+
compat_session.device = compat_session.device.map(tracing::field::display),
345345
%user.id,
346346
%user.username,
347347
"Compatibility token issued: {}", compat_access_token.token

crates/data-model/src/compat/session.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ pub struct CompatSession {
7171
pub id: Ulid,
7272
pub state: CompatSessionState,
7373
pub user_id: Ulid,
74-
pub device: Device,
74+
pub device: Option<Device>,
7575
pub user_session_id: Option<Ulid>,
7676
pub created_at: DateTime<Utc>,
7777
pub is_synapse_admin: bool,

crates/handlers/src/compat/login.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ pub enum Identifier {
130130
#[derive(Debug, Serialize, Deserialize)]
131131
pub struct ResponseBody {
132132
access_token: String,
133-
device_id: Device,
133+
device_id: Option<Device>,
134134
user_id: String,
135135
refresh_token: Option<String>,
136136
#[serde_as(as = "Option<DurationMilliSeconds<i64>>")]
@@ -601,7 +601,7 @@ mod tests {
601601

602602
let body: ResponseBody = response.json();
603603
assert!(!body.access_token.is_empty());
604-
assert_eq!(body.device_id.as_str().len(), 10);
604+
assert_eq!(body.device_id.as_ref().unwrap().as_str().len(), 10);
605605
assert_eq!(body.user_id, "@alice:example.com");
606606
assert_eq!(body.refresh_token, None);
607607
assert_eq!(body.expires_in_ms, None);
@@ -622,7 +622,7 @@ mod tests {
622622

623623
let body: ResponseBody = response.json();
624624
assert!(!body.access_token.is_empty());
625-
assert_eq!(body.device_id.as_str().len(), 10);
625+
assert_eq!(body.device_id.as_ref().unwrap().as_str().len(), 10);
626626
assert_eq!(body.user_id, "@alice:example.com");
627627
assert!(body.refresh_token.is_some());
628628
assert!(body.expires_in_ms.is_some());
@@ -776,7 +776,7 @@ mod tests {
776776

777777
let body: ResponseBody = response.json();
778778
assert!(!body.access_token.is_empty());
779-
assert_eq!(body.device_id, device);
779+
assert_eq!(body.device_id, Some(device));
780780
assert_eq!(body.user_id, "@alice:example.com");
781781
assert_eq!(body.refresh_token, None);
782782
assert_eq!(body.expires_in_ms, None);

crates/handlers/src/graphql/model/compat_sessions.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use anyhow::Context as _;
88
use async_graphql::{Context, Description, Enum, Object, ID};
99
use chrono::{DateTime, Utc};
10+
use mas_data_model::Device;
1011
use mas_storage::{compat::CompatSessionRepository, user::UserRepository};
1112
use url::Url;
1213

@@ -81,8 +82,8 @@ impl CompatSession {
8182
}
8283

8384
/// The Matrix Device ID of this session.
84-
async fn device_id(&self) -> &str {
85-
self.session.device.as_str()
85+
async fn device_id(&self) -> Option<&str> {
86+
self.session.device.as_ref().map(Device::as_str)
8687
}
8788

8889
/// When the object was created.

crates/handlers/src/oauth2/introspection.rs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use mas_axum_utils::{
1010
client_authorization::{ClientAuthorization, CredentialsVerificationError},
1111
sentry::SentryEventID,
1212
};
13-
use mas_data_model::{TokenFormatError, TokenType};
13+
use mas_data_model::{Device, TokenFormatError, TokenType};
1414
use mas_iana::oauth::{OAuthClientAuthenticationMethod, OAuthTokenTypeHint};
1515
use mas_keystore::Encrypter;
1616
use mas_storage::{
@@ -364,11 +364,12 @@ pub(crate) async fn post(
364364
}
365365

366366
// Grant the synapse admin scope if the session has the admin flag set.
367-
let synapse_admin = session.is_synapse_admin.then_some(SYNAPSE_ADMIN_SCOPE);
368-
let device_scope = session.device.to_scope_token();
369-
let scope = [API_SCOPE, device_scope]
367+
let synapse_admin_scope_opt = session.is_synapse_admin.then_some(SYNAPSE_ADMIN_SCOPE);
368+
let device_scope_opt = session.device.as_ref().map(Device::to_scope_token);
369+
let scope = [API_SCOPE]
370370
.into_iter()
371-
.chain(synapse_admin)
371+
.chain(device_scope_opt)
372+
.chain(synapse_admin_scope_opt)
372373
.collect();
373374

374375
activity_tracker
@@ -423,11 +424,12 @@ pub(crate) async fn post(
423424
}
424425

425426
// Grant the synapse admin scope if the session has the admin flag set.
426-
let synapse_admin = session.is_synapse_admin.then_some(SYNAPSE_ADMIN_SCOPE);
427-
let device_scope = session.device.to_scope_token();
428-
let scope = [API_SCOPE, device_scope]
427+
let synapse_admin_scope_opt = session.is_synapse_admin.then_some(SYNAPSE_ADMIN_SCOPE);
428+
let device_scope_opt = session.device.as_ref().map(Device::to_scope_token);
429+
let scope = [API_SCOPE]
429430
.into_iter()
430-
.chain(synapse_admin)
431+
.chain(device_scope_opt)
432+
.chain(synapse_admin_scope_opt)
431433
.collect();
432434

433435
activity_tracker

crates/storage-pg/.sqlx/query-bb6f55a4cc10bec8ec0fc138485f6b4d308302bb1fa3accb12932d1e5ce457e9.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- Copyright 2025 New Vector Ltd.
2+
--
3+
-- SPDX-License-Identifier: AGPL-3.0-only
4+
-- Please see LICENSE in the repository root for full details.
5+
6+
-- Drop the `NOT NULL` requirement on compat sessions, so we can import device-less access tokens from Synapse.
7+
ALTER TABLE compat_sessions ALTER COLUMN device_id DROP NOT NULL;

crates/storage-pg/src/app_session.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -117,16 +117,19 @@ impl TryFrom<AppSessionLookup> for AppSession {
117117
None,
118118
Some(user_id),
119119
None,
120-
Some(device_id),
120+
device_id_opt,
121121
Some(is_synapse_admin),
122122
) => {
123123
let id = compat_session_id.into();
124-
let device = Device::try_from(device_id).map_err(|e| {
125-
DatabaseInconsistencyError::on("compat_sessions")
126-
.column("device_id")
127-
.row(id)
128-
.source(e)
129-
})?;
124+
let device = device_id_opt
125+
.map(Device::try_from)
126+
.transpose()
127+
.map_err(|e| {
128+
DatabaseInconsistencyError::on("compat_sessions")
129+
.column("device_id")
130+
.row(id)
131+
.source(e)
132+
})?;
130133

131134
let state = match finished_at {
132135
None => CompatSessionState::Valid,

crates/storage-pg/src/compat/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ mod tests {
8383
.await
8484
.unwrap();
8585
assert_eq!(session.user_id, user.id);
86-
assert_eq!(session.device.as_str(), device_str);
86+
assert_eq!(session.device.as_ref().unwrap().as_str(), device_str);
8787
assert!(session.is_valid());
8888
assert!(!session.is_finished());
8989

@@ -117,7 +117,7 @@ mod tests {
117117
.expect("compat session not found");
118118
assert_eq!(session_lookup.id, session.id);
119119
assert_eq!(session_lookup.user_id, user.id);
120-
assert_eq!(session_lookup.device.as_str(), device_str);
120+
assert_eq!(session.device.as_ref().unwrap().as_str(), device_str);
121121
assert!(session_lookup.is_valid());
122122
assert!(!session_lookup.is_finished());
123123

@@ -154,7 +154,7 @@ mod tests {
154154
let session_lookup = &list.edges[0].0;
155155
assert_eq!(session_lookup.id, session.id);
156156
assert_eq!(session_lookup.user_id, user.id);
157-
assert_eq!(session_lookup.device.as_str(), device_str);
157+
assert_eq!(session.device.as_ref().unwrap().as_str(), device_str);
158158
assert!(session_lookup.is_valid());
159159
assert!(!session_lookup.is_finished());
160160

crates/storage-pg/src/compat/session.rs

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ impl<'c> PgCompatSessionRepository<'c> {
4747

4848
struct CompatSessionLookup {
4949
compat_session_id: Uuid,
50-
device_id: String,
50+
device_id: Option<String>,
5151
user_id: Uuid,
5252
user_session_id: Option<Uuid>,
5353
created_at: DateTime<Utc>,
@@ -63,12 +63,16 @@ impl TryFrom<CompatSessionLookup> for CompatSession {
6363

6464
fn try_from(value: CompatSessionLookup) -> Result<Self, Self::Error> {
6565
let id = value.compat_session_id.into();
66-
let device = Device::try_from(value.device_id).map_err(|e| {
67-
DatabaseInconsistencyError::on("compat_sessions")
68-
.column("device_id")
69-
.row(id)
70-
.source(e)
71-
})?;
66+
let device = value
67+
.device_id
68+
.map(Device::try_from)
69+
.transpose()
70+
.map_err(|e| {
71+
DatabaseInconsistencyError::on("compat_sessions")
72+
.column("device_id")
73+
.row(id)
74+
.source(e)
75+
})?;
7276

7377
let state = match value.finished_at {
7478
None => CompatSessionState::Valid,
@@ -118,12 +122,12 @@ impl TryFrom<CompatSessionAndSsoLoginLookup> for (CompatSession, Option<CompatSs
118122

119123
fn try_from(value: CompatSessionAndSsoLoginLookup) -> Result<Self, Self::Error> {
120124
let id = value.compat_session_id.into();
121-
let device = Device::try_from(value.device_id).map_err(|e| {
125+
let device = Some(Device::try_from(value.device_id).map_err(|e| {
122126
DatabaseInconsistencyError::on("compat_sessions")
123127
.column("device_id")
124128
.row(id)
125129
.source(e)
126-
})?;
130+
})?);
127131

128132
let state = match value.finished_at {
129133
None => CompatSessionState::Valid,
@@ -347,7 +351,7 @@ impl CompatSessionRepository for PgCompatSessionRepository<'_> {
347351
id,
348352
state: CompatSessionState::default(),
349353
user_id: user.id,
350-
device,
354+
device: Some(device),
351355
user_session_id: browser_session.map(|s| s.id),
352356
created_at,
353357
is_synapse_admin,
@@ -364,7 +368,7 @@ impl CompatSessionRepository for PgCompatSessionRepository<'_> {
364368
db.query.text,
365369
%compat_session.id,
366370
user.id = %compat_session.user_id,
367-
compat_session.device.id = compat_session.device.as_str(),
371+
compat_session.device.id = compat_session.device.as_ref().map(mas_data_model::Device::as_str),
368372
),
369373
err,
370374
)]

0 commit comments

Comments
 (0)