Add max_session_threshold configuration (only apply session_limit to people under the threshold) (#5670)

Add `max_session_threshold` configuration to prevent new users from passing
session limits while not disrupting the people already past the limit.

> This is most applicable in scenarios where your homeserver has many legacy
> bots/scripts that login over and over (which ideally should be using
> [personal access tokens]
> (https://github.com/element-hq/matrix-authentication-service/issues/4492))
> and you want to avoid breaking their operation while maintaining some level
> of sanity with the number of devices that people can have. This will
> prevent anyone else from crossing the limit.

Spawning from https://github.com/element-hq/matrix-authentication-service/pull/5607#discussion_r3150765827

Part of https://github.com/element-hq/matrix-authentication-service/issues/4339 / https://github.com/element-hq/backend-internal/issues/199 tracking work to limit number of devices.
This commit is contained in:
Eric Eastwood authored and GitHub committed 2026-05-20 16:59:56 -05:00
commit a42737aab4
9 files changed
+510 -28

No files matched your search

+2
View File
@@ -156,6 +156,7 @@ pub async fn policy_factory_from_config(
.map(|c| SessionLimitConfig {
soft_limit: c.soft_limit,
hard_limit: c.hard_limit,
max_session_threshold: c.max_session_threshold,
dangerous_hard_limit_eviction: c.dangerous_hard_limit_eviction,
});
@@ -247,6 +248,7 @@ pub fn site_config_from_config(
.map(|c| SessionLimitConfig {
soft_limit: c.soft_limit,
hard_limit: c.hard_limit,
max_session_threshold: c.max_session_threshold,
dangerous_hard_limit_eviction: c.dangerous_hard_limit_eviction,
}),
device_code_grant_enabled: oauth_config.device_code_grant_enabled,
@@ -165,6 +165,17 @@ pub struct SessionLimitConfig {
///
/// [`dangerous_hard_limit_eviction`]: Self::dangerous_hard_limit_eviction
pub hard_limit: NonZeroU64,
/// When set, only accounts with <= `max_session_threshold` sessions have
/// the session limits applied.
///
/// This is most applicable in scenarios where your homeserver has many
/// legacy bots/scripts that login over and over (which ideally should
/// be using [personal access
/// tokens](https://github.com/element-hq/matrix-authentication-service/issues/4492))
/// and you want to avoid breaking their operation while maintaining some
/// level of sanity with the number of devices that people can have.
/// This will prevent anyone else from crossing the limit.
pub max_session_threshold: Option<NonZeroU64>,
/// Whether we should automatically choose the least recently used devices
/// to remove when the [`Self::hard_limit`] is reached; in order to
/// allow the new login to continue.
@@ -188,8 +199,14 @@ pub struct SessionLimitConfig {
/// and you want to avoid breaking their operation while maintaining some
/// level of sanity with the number of devices that people can have.
///
/// Removing devices is a non-trivial task for some homeservers to tackle
/// and can cause lots of device list changes, `/sync`, federation, and
/// replication traffic. Consider using [`max_session_threshold`] to
/// limit the size of accounts that are acted upon.
///
/// [`hard_limit`]: Self::hard_limit
/// [`dangerous_hard_limit_eviction`]: Self::dangerous_hard_limit_eviction
/// [`max_session_threshold`]: Self::max_session_threshold
#[serde(default = "default_false")]
pub dangerous_hard_limit_eviction: bool,
}
+1
View File
@@ -44,6 +44,7 @@ pub struct SessionExpirationConfig {
pub struct SessionLimitConfig {
pub soft_limit: NonZeroU64,
pub hard_limit: NonZeroU64,
pub max_session_threshold: Option<NonZeroU64>,
pub dangerous_hard_limit_eviction: bool,
}
+299 -27
View File
@@ -2022,6 +2022,7 @@ mod tests {
soft_limit: NonZeroU64::new(1).unwrap(),
// Some arbitrary high value (more than we login)
hard_limit: NonZeroU64::new(5).unwrap(),
max_session_threshold: None,
dangerous_hard_limit_eviction: false,
}),
..test_site_config()
@@ -2067,13 +2068,15 @@ mod tests {
);
}
/// Test that the `soft_limit` is not enforced for compat login.
/// Test that the `soft_limit` is not enforced for non-interactive login
/// (like the `m.login.password` compat Matrix login flow).
///
/// `soft_limit` is for when we allow the user to remove devices in
/// interactive contexts. With the compatibility login API, there is no
/// opportunity for us to present a web UI.
/// interactive contexts. If someone uses the `m.login.password`
/// compatibility login API, there is no opportunity for us to present a
/// web UI.
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_soft_limit_does_not_affect_compat_login(pool: PgPool) {
async fn test_session_soft_limit_does_not_affect_non_interactive_login(pool: PgPool) {
setup();
let state = TestState::from_pool_with_site_config(
pool,
@@ -2084,6 +2087,7 @@ mod tests {
soft_limit: NonZeroU64::new(1).unwrap(),
// Some arbitrary high value (more than we login)
hard_limit: NonZeroU64::new(5).unwrap(),
max_session_threshold: None,
dangerous_hard_limit_eviction: false,
}),
..test_site_config()
@@ -2107,6 +2111,8 @@ mod tests {
let _user = user_with_password(&state, "alice", "password", false).await;
// Keep logging in to add more sessions, more than the `soft_limit`
//
// We're using `m.login.password` login flow which is non-interactive
#[allow(clippy::range_plus_one)]
for _ in 0..(session_limit_config.soft_limit.get() + 1) {
let request = Request::post("/_matrix/client/v3/login").json(serde_json::json!({
@@ -2124,7 +2130,7 @@ mod tests {
/// Test that the `hard_limit` prevents more sessions
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_hard_limit_compat_login(pool: PgPool) {
async fn test_session_hard_limit_compat_login(pool: PgPool) {
setup();
let state = TestState::from_pool_with_site_config(
pool,
@@ -2134,6 +2140,7 @@ mod tests {
soft_limit: NonZeroU64::new(1).unwrap(),
// Lowest non-zero value so we don't have to login a bunch
hard_limit: NonZeroU64::new(1).unwrap(),
max_session_threshold: None,
dangerous_hard_limit_eviction: false,
}),
..test_site_config()
@@ -2149,46 +2156,309 @@ mod tests {
.expect("Expected `session_limit` configured for this test");
let _user = user_with_password(&state, "alice", "password", false).await;
// Keep logging in to add more sessions, up to the `hard_limit`
#[allow(clippy::range_plus_one)]
for _ in 0..session_limit_config.hard_limit.get() {
let request = Request::post("/_matrix/client/v3/login").json(serde_json::json!({
"type": "m.login.password",
"identifier": {
"type": "m.id.user",
"user": "alice",
},
"password": "password",
}));
let response = state.request(request.clone()).await;
response.assert_status(StatusCode::OK);
}
// One more login will tip us over the `hard_limit`
let request = Request::post("/_matrix/client/v3/login").json(serde_json::json!({
let password_login_json = serde_json::json!({
"type": "m.login.password",
"identifier": {
"type": "m.id.user",
"user": "alice",
},
"password": "password",
}));
});
// Keep logging in to add more sessions, up to the `hard_limit`
for _ in 0..session_limit_config.hard_limit.get() {
let request =
Request::post("/_matrix/client/v3/login").json(password_login_json.clone());
let response = state.request(request.clone()).await;
response.assert_status(StatusCode::OK);
}
// One more login will tip us over the `hard_limit`
let request = Request::post("/_matrix/client/v3/login").json(password_login_json);
let response = state.request(request.clone()).await;
response.assert_status(StatusCode::FORBIDDEN);
let body: serde_json::Value = response.json();
assert_eq!(
body.get("errcode")
.expect("Expected errror response to include an `errcode`"),
.expect("Expected error response to include an `errcode`"),
"M_FORBIDDEN",
"Expected `errcode` to be `M_FORBIDDEN`"
);
}
/// Test that session limits are enforced for anyone who is *under* the
/// `max_session_threshold` (when using interactive login methods)
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_session_limit_under_max_session_threshold_interactive(pool: PgPool) {
setup();
let state = TestState::from_pool_with_site_config(
pool,
SiteConfig {
session_limit: Some(SessionLimitConfig {
// Lowest non-zero value so we don't have to login a bunch
soft_limit: NonZeroU64::new(1).unwrap(),
hard_limit: NonZeroU64::new(1).unwrap(),
// The main thing we're trying to test
max_session_threshold: Some(NonZeroU64::new(1).unwrap()),
dangerous_hard_limit_eviction: false,
}),
..test_site_config()
},
)
.await
.unwrap();
let session_limit_config = state
.site_config
.session_limit
.as_ref()
.expect("Expected `session_limit` configured for this test");
// Make sure this is configured as its the main differentiator we're trying to
// test
session_limit_config.max_session_threshold.as_ref().expect("Expected `session_limit.max_session_threshold` to be configured at this point in the test");
let _user = user_with_password(&state, "alice", "password", false).await;
// Keep logging in to add more sessions, up to the `hard_limit`
for _ in 0..session_limit_config.hard_limit.get() {
let login_token = matrix_compat_sso_login(&state, "alice", "password")
.await
.expect("Should be able to login without issue when under the session limit");
// Exchange the `loginToken` for an access token. This is what actually
// creates the session.
let request = Request::post("/_matrix/client/v3/login").json(serde_json::json!({
"type": "m.login.token",
"token": login_token,
}));
let response = state.request(request).await;
response.assert_status(StatusCode::OK);
}
// One more login will tip us over the `hard_limit`
//
// Session limits are enforced because we're <= `max_session_threshold`
assert_matches!(
matrix_compat_sso_login(&state, "alice", "password").await,
Err(MatrixCompatSsoLoginError::ConsentForbidden {
page_type: ConsentForbiddenPageType::DeviceLimitReached,
..
})
);
}
/// Test that session limits are enforced for anyone who is *under* the
/// `max_session_threshold` (when using non-interactive login methods)
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_session_limit_under_max_session_threshold_non_interactive(pool: PgPool) {
setup();
let state = TestState::from_pool_with_site_config(
pool,
SiteConfig {
session_limit: Some(SessionLimitConfig {
// Lowest non-zero value so we don't have to login a bunch
soft_limit: NonZeroU64::new(1).unwrap(),
hard_limit: NonZeroU64::new(1).unwrap(),
// The main thing we're trying to test
max_session_threshold: Some(NonZeroU64::new(1).unwrap()),
dangerous_hard_limit_eviction: false,
}),
..test_site_config()
},
)
.await
.unwrap();
let session_limit_config = state
.site_config
.session_limit
.as_ref()
.expect("Expected `session_limit` configured for this test");
// Make sure this is configured as its the main differentiator we're trying to
// test
session_limit_config.max_session_threshold.as_ref().expect("Expected `session_limit.max_session_threshold` to be configured at this point in the test");
let _user = user_with_password(&state, "alice", "password", false).await;
let password_login_json = serde_json::json!({
"type": "m.login.password",
"identifier": {
"type": "m.id.user",
"user": "alice",
},
"password": "password",
});
// Keep logging in to add more sessions, up to the `hard_limit`
for _ in 0..session_limit_config.hard_limit.get() {
let request =
Request::post("/_matrix/client/v3/login").json(password_login_json.clone());
let response = state.request(request.clone()).await;
response.assert_status(StatusCode::OK);
}
// One more login will tip us over the `hard_limit`
//
// Session limits are enforced because we're <= `max_session_threshold`
let request = Request::post("/_matrix/client/v3/login").json(password_login_json);
let response = state.request(request.clone()).await;
response.assert_status(StatusCode::FORBIDDEN);
let body: serde_json::Value = response.json();
assert_eq!(
body.get("errcode")
.expect("Expected error response to include an `errcode`"),
"M_FORBIDDEN",
"Expected `errcode` to be `M_FORBIDDEN`"
);
}
/// Test that session limits are not enforced (logins are allowed) for
/// anyone who is already *past* the `max_session_threshold` (when using
/// interactive login methods)
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_session_limit_past_max_session_threshold_interactive(pool: PgPool) {
setup();
let mut state = TestState::from_pool_with_site_config(
pool.clone(),
SiteConfig {
// Setup an account with a few sessions before we add a `session_limit`
session_limit: None,
..test_site_config()
},
)
.await
.unwrap();
// Lowest non-zero value so we don't have to login a bunch
let upcoming_hard_limit = 1;
// Setup a user that has more sessions than the `max_session_threshold`.
//
// Keep logging in to add more sessions. We want to be past the
// `hard_limit`/`max_session_threshold`
let _user = user_with_password(&state, "alice", "password", false).await;
#[allow(clippy::range_plus_one)]
for _ in 0..(upcoming_hard_limit + 1) {
let login_token = matrix_compat_sso_login(&state, "alice", "password")
.await
.expect("Should be able to login without issue when under the session limit");
// Exchange the `loginToken` for an access token. This is what actually
// creates the session.
let request = Request::post("/_matrix/client/v3/login").json(serde_json::json!({
"type": "m.login.token",
"token": login_token,
}));
let response = state.request(request).await;
response.assert_status(StatusCode::OK);
}
// Update the app state to configure a `max_session_threshold`
state.site_config.session_limit = Some(SessionLimitConfig {
soft_limit: NonZeroU64::new(upcoming_hard_limit).unwrap(),
hard_limit: NonZeroU64::new(upcoming_hard_limit).unwrap(),
// The main thing we're trying to test
max_session_threshold: Some(NonZeroU64::new(upcoming_hard_limit).unwrap()),
dangerous_hard_limit_eviction: false,
});
let state = state.restart().await;
let session_limit_config = state
.site_config
.session_limit
.as_ref()
.expect("Expected `session_limit` to be configured at this point in the test");
// Make sure this is configured as its the main differentiator we're trying to
// test
session_limit_config.max_session_threshold.as_ref().expect("Expected `session_limit.max_session_threshold` to be configured at this point in the test");
// Since we're already above `max_session_threshold`, the `session_limit` won't
// stop us from adding another session.
let login_token = matrix_compat_sso_login(&state, "alice", "password")
.await
.expect("Should be able to login since we're past the `max_session_threshold`");
// Exchange the `loginToken` for an access token. This is what actually
// creates the session.
let request = Request::post("/_matrix/client/v3/login").json(serde_json::json!({
"type": "m.login.token",
"token": login_token,
}));
let response = state.request(request).await;
response.assert_status(StatusCode::OK);
}
/// Test that session limits are not enforced (logins are allowed) for
/// anyone who is already *past* the `max_session_threshold` (when using
/// non-interactive login methods)
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_session_limit_past_max_session_threshold_non_interactive(pool: PgPool) {
setup();
let mut state = TestState::from_pool_with_site_config(
pool.clone(),
SiteConfig {
// Setup an account with a few sessions before we add a `session_limit`
session_limit: None,
..test_site_config()
},
)
.await
.unwrap();
// Lowest non-zero value so we don't have to login a bunch
let upcoming_hard_limit = 1;
// Setup a user that has more sessions than the `max_session_threshold`.
//
// Keep logging in to add more sessions. We want to be past the
// `hard_limit`/`max_session_threshold`
let _user = user_with_password(&state, "alice", "password", false).await;
let password_login_json = serde_json::json!({
"type": "m.login.password",
"identifier": {
"type": "m.id.user",
"user": "alice",
},
"password": "password",
});
#[allow(clippy::range_plus_one)]
for _ in 0..(upcoming_hard_limit + 1) {
let request = Request::post("/_matrix/client/v3/login")
.json(serde_json::json!(password_login_json.clone()));
let response = state.request(request.clone()).await;
response.assert_status(StatusCode::OK);
}
// Update the app state to configure a `max_session_threshold`
state.site_config.session_limit = Some(SessionLimitConfig {
soft_limit: NonZeroU64::new(upcoming_hard_limit).unwrap(),
hard_limit: NonZeroU64::new(upcoming_hard_limit).unwrap(),
// The main thing we're trying to test
max_session_threshold: Some(NonZeroU64::new(upcoming_hard_limit).unwrap()),
dangerous_hard_limit_eviction: false,
});
let state = state.restart().await;
let session_limit_config = state
.site_config
.session_limit
.as_ref()
.expect("Expected `session_limit` to be configured at this point in the test");
// Make sure this is configured as its the main differentiator we're trying to
// test
session_limit_config.max_session_threshold.as_ref().expect("Expected `session_limit.max_session_threshold` to be configured at this point in the test");
// Since we're already above `max_session_threshold`, the `session_limit` won't
// stop us from adding another session.
let request = Request::post("/_matrix/client/v3/login")
.json(serde_json::json!(password_login_json.clone()));
let response = state.request(request.clone()).await;
response.assert_status(StatusCode::OK);
}
/// Test that the `dangerous_hard_limit_eviction` will automatically drop
/// old sessions when we go over the limit
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_dangerous_hard_limit_eviction_old_compat_login(pool: PgPool) {
async fn test_session_dangerous_hard_limit_eviction_old_compat_login(pool: PgPool) {
setup();
let state = TestState::from_pool_with_site_config(
pool,
@@ -2198,6 +2468,7 @@ mod tests {
soft_limit: NonZeroU64::new(1).unwrap(),
// Must be at least 2 when `dangerous_hard_limit_eviction`
hard_limit: NonZeroU64::new(2).unwrap(),
max_session_threshold: None,
// Option under test
dangerous_hard_limit_eviction: true,
}),
@@ -2355,7 +2626,7 @@ mod tests {
/// the oldest sessions when we go over the limit even if all of the
/// sessions are recent.
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_dangerous_hard_limit_eviction_recent_compat_login(pool: PgPool) {
async fn test_session_dangerous_hard_limit_eviction_recent_compat_login(pool: PgPool) {
setup();
let state = TestState::from_pool_with_site_config(
pool,
@@ -2365,6 +2636,7 @@ mod tests {
soft_limit: NonZeroU64::new(1).unwrap(),
// Must be at least 2 when `dangerous_hard_limit_eviction`
hard_limit: NonZeroU64::new(2).unwrap(),
max_session_threshold: None,
// Option under test
dangerous_hard_limit_eviction: true,
}),
+10 -1
View File
@@ -2927,8 +2927,17 @@
"format": "uint64",
"minimum": 1
},
"max_session_threshold": {
"description": "When set, only accounts with <= `max_session_threshold` sessions have\n the session limits applied.\n\n This is most applicable in scenarios where your homeserver has many\n legacy bots/scripts that login over and over (which ideally should\n be using [personal access\n tokens](https://github.com/element-hq/matrix-authentication-service/issues/4492))\n and you want to avoid breaking their operation while maintaining some\n level of sanity with the number of devices that people can have.\n This will prevent anyone else from crossing the limit.",
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 1
},
"dangerous_hard_limit_eviction": {
"description": "Whether we should automatically choose the least recently used devices\n to remove when the [`Self::hard_limit`] is reached; in order to\n allow the new login to continue.\n\n Disabled by default\n\n WARNING: Removing sessions is a potentially damaging operation. Any\n end-to-end encrypted history on the device will be lost and can only\n be recovered if you have another verified active device or have a\n recovery key setup.\n\n When using [`dangerous_hard_limit_eviction`], the [`hard_limit`] must be\n at least 2 to avoid catastrophically losing encrypted history and\n digital identity in pathological cases. Keep in mind this is a bare\n minimum restriction and you can still run into trouble.\n\n This is most applicable in scenarios where your homeserver has many\n legacy bots/scripts that login over and over (which ideally should\n be using [personal access\n tokens](https://github.com/element-hq/matrix-authentication-service/issues/4492))\n and you want to avoid breaking their operation while maintaining some\n level of sanity with the number of devices that people can have.\n\n [`hard_limit`]: Self::hard_limit\n [`dangerous_hard_limit_eviction`]: Self::dangerous_hard_limit_eviction",
"description": "Whether we should automatically choose the least recently used devices\n to remove when the [`Self::hard_limit`] is reached; in order to\n allow the new login to continue.\n\n Disabled by default\n\n WARNING: Removing sessions is a potentially damaging operation. Any\n end-to-end encrypted history on the device will be lost and can only\n be recovered if you have another verified active device or have a\n recovery key setup.\n\n When using [`dangerous_hard_limit_eviction`], the [`hard_limit`] must be\n at least 2 to avoid catastrophically losing encrypted history and\n digital identity in pathological cases. Keep in mind this is a bare\n minimum restriction and you can still run into trouble.\n\n This is most applicable in scenarios where your homeserver has many\n legacy bots/scripts that login over and over (which ideally should\n be using [personal access\n tokens](https://github.com/element-hq/matrix-authentication-service/issues/4492))\n and you want to avoid breaking their operation while maintaining some\n level of sanity with the number of devices that people can have.\n\n Removing devices is a non-trivial task for some homeservers to tackle\n and can cause lots of device list changes, `/sync`, federation, and\n replication traffic. Consider using [`max_session_threshold`] to\n limit the size of accounts that are acted upon.\n\n [`hard_limit`]: Self::hard_limit\n [`dangerous_hard_limit_eviction`]: Self::dangerous_hard_limit_eviction\n [`max_session_threshold`]: Self::max_session_threshold",
"type": "boolean",
"default": false
}
@@ -167,9 +167,32 @@ violation contains {
# Only apply if it's a user logging in (who therefore has countable sessions)
input.session_counts != null
# Only apply limits to accounts under the threshold (if configured)
passes_session_threshold
# For OAuth 2 login, a violation occurs when the soft limit has already been
# reached or exceeded.
# We use the soft limit because the user will be able to interactively remove
# sessions to return under the limit.
data.session_limit.soft_limit <= input.session_counts.total
}
# The session limits only apply to accounts within the `max_session_threshold`.
#
# True if the `max_session_threshold` isn't configured or <= `max_session_threshold`.
passes_session_threshold if {
# If no `session_limit` configured, automatically passes (undefined)
not data.session_limit
} else if {
# If no `session_limit` configured, automatically passes (null)
data.session_limit == null
} else if {
# If no `max_session_threshold` configured, automatically passes (undefined)
not data.session_limit.max_session_threshold
} else if {
# If no `max_session_threshold` configured, automatically passes (null)
data.session_limit.max_session_threshold == null
} else if {
# Otherwise, check whether the total number of sessions is under the threshold
input.session_counts.total <= data.session_limit.max_session_threshold
}
@@ -302,3 +302,35 @@ test_session_limiting_no_limit if {
result.allow
result.need_to_remove_sessions == 0
}
test_session_limiting_under_max_session_threshold if {
result := {
"allow": authorization_grant.allow,
"need_to_remove_sessions": need_to_remove_sessions(authorization_grant.violation),
} with input.user as user
with input.session_counts as {"total": 42} # Some value above `soft_limit` but below `max_session_threshold`
with data.session_limit as {
"soft_limit": 32,
"hard_limit": 64,
# The main thing we're trying to test
"max_session_threshold": 64,
}
not result.allow
result.need_to_remove_sessions == 11
}
test_no_session_limiting_past_max_session_threshold if {
result := {
"allow": authorization_grant.allow,
"need_to_remove_sessions": need_to_remove_sessions(authorization_grant.violation),
} with input.user as user
with input.session_counts as {"total": 128} # Some value above the limits and `max_session_threshold`
with data.session_limit as {
"soft_limit": 32,
"hard_limit": 64,
# The main thing we're trying to test
"max_session_threshold": 64,
}
result.allow
result.need_to_remove_sessions == 0
}
+26
View File
@@ -42,6 +42,9 @@ violation contains {
# (As then this login is not actually increasing the number of devices)
not input.session_replaced
# Only apply limits to accounts under the threshold (if configured)
passes_session_threshold
# For web-based 'compat SSO' login, a violation occurs when the soft limit has already been
# reached or exceeded.
# We use the soft limit because the user will be able to interactively remove
@@ -66,6 +69,9 @@ violation contains {
# (As then this login is not actually increasing the number of devices)
not input.session_replaced
# Only apply limits to accounts under the threshold (if configured)
passes_session_threshold
# For `m.login.password` login, a violation occurs when the hard limit has already been
# reached or exceeded.
# We don't use the soft limit because the user won't be able to interactively remove
@@ -78,3 +84,23 @@ is_interactive if {
# `m.login.password` and `m.login.token` (including the finalisation of an SSO login) are not
input.login.type == "m.login.sso"
}
# The session limits only apply to accounts within the `max_session_threshold`.
#
# True if the `max_session_threshold` isn't configured or <= `max_session_threshold`.
passes_session_threshold if {
# If no `session_limit` configured, automatically passes (undefined)
not data.session_limit
} else if {
# If no `session_limit` configured, automatically passes (null)
data.session_limit == null
} else if {
# If no `max_session_threshold` configured, automatically passes (undefined)
not data.session_limit.max_session_threshold
} else if {
# If no `max_session_threshold` configured, automatically passes (null)
data.session_limit.max_session_threshold == null
} else if {
# Otherwise, check whether the total number of sessions is under the threshold
input.session_counts.total <= data.session_limit.max_session_threshold
}
@@ -21,6 +21,32 @@ need_to_remove_sessions(violations) := need if {
# Tests session limiting when using (the interactive part of) `m.login.sso`
# (interactive, therefore `soft_limit` applies)
# =========================================================================
test_no_session_limiting_undefined if {
result := {
"allow": compat_login.allow,
"need_to_remove_sessions": need_to_remove_sessions(compat_login.violation),
} with input.user as user
with input.session_counts as {"total": 1}
with input.login as {"type": "m.login.sso"}
with input.session_replaced as false
with input._noop as null # The main thing we're trying to test (`data.session_limit` as undefined)
result.allow
result.need_to_remove_sessions == 0
}
test_no_session_limiting_null if {
result := {
"allow": compat_login.allow,
"need_to_remove_sessions": need_to_remove_sessions(compat_login.violation),
} with input.user as user
with input.session_counts as {"total": 1}
with input.login as {"type": "m.login.sso"}
with input.session_replaced as false
with data.session_limit as null # The main thing we're trying to test (null)
result.allow
result.need_to_remove_sessions == 0
}
test_session_limiting_sso_under_limit if {
result := {
"allow": compat_login.allow,
@@ -198,3 +224,77 @@ test_no_session_limiting_password_upon_replacement if {
result.allow
result.need_to_remove_sessions == 0
}
# Testing null vs undefined
test_session_limiting_null_max_session_threshold_under_limit if {
result := {
"allow": compat_login.allow,
"need_to_remove_sessions": need_to_remove_sessions(compat_login.violation),
} with input.user as user
with input.session_counts as {"total": 3} # Some value below `soft_limit`
with input.login as {"type": "m.login.sso"}
with input.session_replaced as false
with data.session_limit as {
"soft_limit": 32,
"hard_limit": 64,
# The main thing we're trying to test
"max_session_threshold": null,
}
result.allow
result.need_to_remove_sessions == 0
}
# Testing null vs undefined
test_session_limiting_null_max_session_threshold_over_limit if {
result := {
"allow": compat_login.allow,
"need_to_remove_sessions": need_to_remove_sessions(compat_login.violation),
} with input.user as user
with input.session_counts as {"total": 42} # Some value above `soft_limit`
with input.login as {"type": "m.login.sso"}
with input.session_replaced as false
with data.session_limit as {
"soft_limit": 32,
"hard_limit": 64,
# The main thing we're trying to test
"max_session_threshold": null,
}
not result.allow
result.need_to_remove_sessions == 11
}
test_session_limiting_under_max_session_threshold if {
result := {
"allow": compat_login.allow,
"need_to_remove_sessions": need_to_remove_sessions(compat_login.violation),
} with input.user as user
with input.session_counts as {"total": 42} # Some value above `soft_limit` but below `max_session_threshold`
with input.login as {"type": "m.login.sso"}
with input.session_replaced as false
with data.session_limit as {
"soft_limit": 32,
"hard_limit": 64,
# The main thing we're trying to test
"max_session_threshold": 64,
}
not result.allow
result.need_to_remove_sessions == 11
}
test_no_session_limiting_past_max_session_threshold if {
result := {
"allow": compat_login.allow,
"need_to_remove_sessions": need_to_remove_sessions(compat_login.violation),
} with input.user as user
with input.session_counts as {"total": 128} # Some value above the limits and `max_session_threshold`
with input.login as {"type": "m.login.sso"}
with input.session_replaced as false
with data.session_limit as {
"soft_limit": 32,
"hard_limit": 64,
# The main thing we're trying to test
"max_session_threshold": 64,
}
result.allow
result.need_to_remove_sessions == 0
}