|
8 | 8 |
|
9 | 9 | use axum::response::{Html, IntoResponse as _, Response}; |
10 | 10 | use mas_axum_utils::{SessionInfoExt, cookies::CookieJar, csrf::CsrfExt}; |
11 | | -use mas_data_model::{BrowserSession, Clock}; |
| 11 | +use mas_data_model::{BrowserSession, Clock, User}; |
12 | 12 | 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 | +}; |
14 | 18 | use mas_templates::{AccountInactiveContext, TemplateContext, Templates}; |
15 | 19 | use rand::RngCore; |
16 | 20 | use thiserror::Error; |
@@ -102,3 +106,62 @@ pub async fn load_session_or_fallback( |
102 | 106 | maybe_session: Some(session), |
103 | 107 | }) |
104 | 108 | } |
| 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