Skip to content

Commit 86612b1

Browse files
committed
Add helper function to count user sessions for limiting
1 parent 2bb11b2 commit 86612b1

File tree

1 file changed

+65
-2
lines changed

1 file changed

+65
-2
lines changed

crates/handlers/src/session.rs

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@
88
99
use axum::response::{Html, IntoResponse as _, Response};
1010
use mas_axum_utils::{SessionInfoExt, cookies::CookieJar, csrf::CsrfExt};
11-
use mas_data_model::{BrowserSession, Clock};
11+
use mas_data_model::{BrowserSession, Clock, User};
1212
use mas_i18n::DataLocale;
13-
use mas_storage::{BoxRepository, RepositoryError};
13+
use mas_policy::model::SessionCounts;
14+
use mas_storage::{
15+
BoxRepository, RepositoryError, compat::CompatSessionFilter, oauth2::OAuth2SessionFilter,
16+
personal::PersonalSessionFilter,
17+
};
1418
use mas_templates::{AccountInactiveContext, TemplateContext, Templates};
1519
use rand::RngCore;
1620
use thiserror::Error;
@@ -102,3 +106,62 @@ pub async fn load_session_or_fallback(
102106
maybe_session: Some(session),
103107
})
104108
}
109+
110+
/// Get a count of sessions for the given user, for the purposes of session
111+
/// limiting.
112+
///
113+
/// Includes:
114+
/// - OAuth 2 sessions
115+
/// - Compatibility sessions
116+
/// - Personal sessions (unless owned by a different user)
117+
///
118+
/// # Backstory
119+
///
120+
/// Originally, we were only intending to count sessions with devices in this
121+
/// result, because those are the entries that are expensive for Synapse and
122+
/// also would not hinder use of deviceless clients (like Element Admin, an
123+
/// admin dashboard).
124+
///
125+
/// However, to do so, we would need to count only sessions including device
126+
/// scopes. To do this efficiently, we'd need a partial index on sessions
127+
/// including device scopes.
128+
///
129+
/// It turns out that this can't be done cleanly (as we need to, in Postgres,
130+
/// match scope lists where one of the scopes matches one of 2 known prefixes),
131+
/// at least not without somewhat uncomfortable stored functions.
132+
///
133+
/// So for simplicity's sake, we now count all sessions.
134+
/// For practical use cases, it's not likely to make a noticeable difference
135+
/// (and maybe it's good that there's an overall limit).
136+
pub(crate) async fn count_user_sessions_for_limiting(
137+
repo: &mut BoxRepository,
138+
user: &User,
139+
) -> anyhow::Result<SessionCounts> {
140+
let oauth2 = repo
141+
.oauth2_session()
142+
.count(OAuth2SessionFilter::new().active_only().for_user(user))
143+
.await? as u64;
144+
145+
let compat = repo
146+
.compat_session()
147+
.count(CompatSessionFilter::new().active_only().for_user(user))
148+
.await? as u64;
149+
150+
// Only include self-owned personal sessions, not administratively-owned ones
151+
let personal = repo
152+
.personal_session()
153+
.count(
154+
PersonalSessionFilter::new()
155+
.active_only()
156+
.for_actor_user(user)
157+
.for_owner_user(user),
158+
)
159+
.await? as u64;
160+
161+
Ok(SessionCounts {
162+
total: oauth2 + compat + personal,
163+
oauth2,
164+
compat,
165+
personal,
166+
})
167+
}

0 commit comments

Comments
 (0)