mirror of
https://github.com/element-hq/matrix-authentication-service.git
synced 2026-05-24 13:05:36 +00:00
Merge branch 'madlittlemods/hard_limit_eviction' into madlittlemods/soft-limit-account-session-management
Conflicts: crates/config/src/sections/experimental.rs
This commit is contained in:
Generated
+30
-30
@@ -3098,7 +3098,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-axum-utils"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0-rc.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -3132,7 +3132,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-cli"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0-rc.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -3207,7 +3207,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-config"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0-rc.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"camino",
|
||||
@@ -3238,7 +3238,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-context"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0-rc.0"
|
||||
dependencies = [
|
||||
"console",
|
||||
"opentelemetry",
|
||||
@@ -3254,7 +3254,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-data-model"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0-rc.0"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"chrono",
|
||||
@@ -3276,7 +3276,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-email"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0-rc.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"lettre",
|
||||
@@ -3287,7 +3287,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-handlers"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0-rc.0"
|
||||
dependencies = [
|
||||
"aide",
|
||||
"anyhow",
|
||||
@@ -3368,7 +3368,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-http"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0-rc.0"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"headers",
|
||||
@@ -3389,7 +3389,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-i18n"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0-rc.0"
|
||||
dependencies = [
|
||||
"camino",
|
||||
"icu_calendar",
|
||||
@@ -3411,7 +3411,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-i18n-scan"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0-rc.0"
|
||||
dependencies = [
|
||||
"camino",
|
||||
"clap",
|
||||
@@ -3425,7 +3425,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-iana"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0-rc.0"
|
||||
dependencies = [
|
||||
"schemars 0.9.0",
|
||||
"serde",
|
||||
@@ -3433,7 +3433,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-iana-codegen"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0-rc.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3450,7 +3450,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-jose"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0-rc.0"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"chrono",
|
||||
@@ -3480,7 +3480,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-keystore"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0-rc.0"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"base64ct",
|
||||
@@ -3508,7 +3508,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-listener"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0-rc.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -3532,7 +3532,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-matrix"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0-rc.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3542,7 +3542,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-matrix-synapse"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0-rc.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3559,7 +3559,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-oidc-client"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0-rc.0"
|
||||
dependencies = [
|
||||
"assert_matches",
|
||||
"async-trait",
|
||||
@@ -3595,7 +3595,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-policy"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0-rc.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -3612,7 +3612,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-router"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0-rc.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"serde",
|
||||
@@ -3623,7 +3623,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-spa"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0-rc.0"
|
||||
dependencies = [
|
||||
"camino",
|
||||
"serde",
|
||||
@@ -3632,7 +3632,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-storage"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0-rc.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -3654,7 +3654,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-storage-pg"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0-rc.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -3684,7 +3684,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-tasks"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0-rc.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3716,7 +3716,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-templates"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0-rc.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -3748,7 +3748,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mas-tower"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0-rc.0"
|
||||
dependencies = [
|
||||
"http",
|
||||
"opentelemetry",
|
||||
@@ -4001,7 +4001,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oauth2-types"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0-rc.0"
|
||||
dependencies = [
|
||||
"assert_matches",
|
||||
"base64ct",
|
||||
@@ -5199,9 +5199,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.10"
|
||||
version = "0.103.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
|
||||
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"ring",
|
||||
@@ -6106,7 +6106,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn2mas"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0-rc.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
|
||||
+30
-30
@@ -9,7 +9,7 @@ members = ["crates/*"]
|
||||
resolver = "2"
|
||||
|
||||
# Updated in the CI with a `sed` command
|
||||
package.version = "1.15.0"
|
||||
package.version = "1.16.0-rc.0"
|
||||
package.license = "AGPL-3.0-only OR LicenseRef-Element-Commercial"
|
||||
package.authors = ["Element Backend Team"]
|
||||
package.edition = "2024"
|
||||
@@ -39,35 +39,35 @@ broken_intra_doc_links = "deny"
|
||||
[workspace.dependencies]
|
||||
|
||||
# Workspace crates
|
||||
mas-axum-utils = { path = "./crates/axum-utils/", version = "=1.15.0" }
|
||||
mas-cli = { path = "./crates/cli/", version = "=1.15.0" }
|
||||
mas-config = { path = "./crates/config/", version = "=1.15.0" }
|
||||
mas-context = { path = "./crates/context/", version = "=1.15.0" }
|
||||
mas-data-model = { path = "./crates/data-model/", version = "=1.15.0" }
|
||||
mas-email = { path = "./crates/email/", version = "=1.15.0" }
|
||||
mas-graphql = { path = "./crates/graphql/", version = "=1.15.0" }
|
||||
mas-handlers = { path = "./crates/handlers/", version = "=1.15.0" }
|
||||
mas-http = { path = "./crates/http/", version = "=1.15.0" }
|
||||
mas-i18n = { path = "./crates/i18n/", version = "=1.15.0" }
|
||||
mas-i18n-scan = { path = "./crates/i18n-scan/", version = "=1.15.0" }
|
||||
mas-iana = { path = "./crates/iana/", version = "=1.15.0" }
|
||||
mas-iana-codegen = { path = "./crates/iana-codegen/", version = "=1.15.0" }
|
||||
mas-jose = { path = "./crates/jose/", version = "=1.15.0" }
|
||||
mas-keystore = { path = "./crates/keystore/", version = "=1.15.0" }
|
||||
mas-listener = { path = "./crates/listener/", version = "=1.15.0" }
|
||||
mas-matrix = { path = "./crates/matrix/", version = "=1.15.0" }
|
||||
mas-matrix-synapse = { path = "./crates/matrix-synapse/", version = "=1.15.0" }
|
||||
mas-oidc-client = { path = "./crates/oidc-client/", version = "=1.15.0" }
|
||||
mas-policy = { path = "./crates/policy/", version = "=1.15.0" }
|
||||
mas-router = { path = "./crates/router/", version = "=1.15.0" }
|
||||
mas-spa = { path = "./crates/spa/", version = "=1.15.0" }
|
||||
mas-storage = { path = "./crates/storage/", version = "=1.15.0" }
|
||||
mas-storage-pg = { path = "./crates/storage-pg/", version = "=1.15.0" }
|
||||
mas-tasks = { path = "./crates/tasks/", version = "=1.15.0" }
|
||||
mas-templates = { path = "./crates/templates/", version = "=1.15.0" }
|
||||
mas-tower = { path = "./crates/tower/", version = "=1.15.0" }
|
||||
oauth2-types = { path = "./crates/oauth2-types/", version = "=1.15.0" }
|
||||
syn2mas = { path = "./crates/syn2mas", version = "=1.15.0" }
|
||||
mas-axum-utils = { path = "./crates/axum-utils/", version = "=1.16.0-rc.0" }
|
||||
mas-cli = { path = "./crates/cli/", version = "=1.16.0-rc.0" }
|
||||
mas-config = { path = "./crates/config/", version = "=1.16.0-rc.0" }
|
||||
mas-context = { path = "./crates/context/", version = "=1.16.0-rc.0" }
|
||||
mas-data-model = { path = "./crates/data-model/", version = "=1.16.0-rc.0" }
|
||||
mas-email = { path = "./crates/email/", version = "=1.16.0-rc.0" }
|
||||
mas-graphql = { path = "./crates/graphql/", version = "=1.16.0-rc.0" }
|
||||
mas-handlers = { path = "./crates/handlers/", version = "=1.16.0-rc.0" }
|
||||
mas-http = { path = "./crates/http/", version = "=1.16.0-rc.0" }
|
||||
mas-i18n = { path = "./crates/i18n/", version = "=1.16.0-rc.0" }
|
||||
mas-i18n-scan = { path = "./crates/i18n-scan/", version = "=1.16.0-rc.0" }
|
||||
mas-iana = { path = "./crates/iana/", version = "=1.16.0-rc.0" }
|
||||
mas-iana-codegen = { path = "./crates/iana-codegen/", version = "=1.16.0-rc.0" }
|
||||
mas-jose = { path = "./crates/jose/", version = "=1.16.0-rc.0" }
|
||||
mas-keystore = { path = "./crates/keystore/", version = "=1.16.0-rc.0" }
|
||||
mas-listener = { path = "./crates/listener/", version = "=1.16.0-rc.0" }
|
||||
mas-matrix = { path = "./crates/matrix/", version = "=1.16.0-rc.0" }
|
||||
mas-matrix-synapse = { path = "./crates/matrix-synapse/", version = "=1.16.0-rc.0" }
|
||||
mas-oidc-client = { path = "./crates/oidc-client/", version = "=1.16.0-rc.0" }
|
||||
mas-policy = { path = "./crates/policy/", version = "=1.16.0-rc.0" }
|
||||
mas-router = { path = "./crates/router/", version = "=1.16.0-rc.0" }
|
||||
mas-spa = { path = "./crates/spa/", version = "=1.16.0-rc.0" }
|
||||
mas-storage = { path = "./crates/storage/", version = "=1.16.0-rc.0" }
|
||||
mas-storage-pg = { path = "./crates/storage-pg/", version = "=1.16.0-rc.0" }
|
||||
mas-tasks = { path = "./crates/tasks/", version = "=1.16.0-rc.0" }
|
||||
mas-templates = { path = "./crates/templates/", version = "=1.16.0-rc.0" }
|
||||
mas-tower = { path = "./crates/tower/", version = "=1.16.0-rc.0" }
|
||||
oauth2-types = { path = "./crates/oauth2-types/", version = "=1.16.0-rc.0" }
|
||||
syn2mas = { path = "./crates/syn2mas", version = "=1.16.0-rc.0" }
|
||||
|
||||
# OpenAPI schema generation and validation
|
||||
[workspace.dependencies.aide]
|
||||
|
||||
@@ -156,7 +156,7 @@ pub async fn policy_factory_from_config(
|
||||
.map(|c| SessionLimitConfig {
|
||||
soft_limit: c.soft_limit,
|
||||
hard_limit: c.hard_limit,
|
||||
hard_limit_eviction: c.hard_limit_eviction,
|
||||
dangerous_hard_limit_eviction: c.dangerous_hard_limit_eviction,
|
||||
});
|
||||
|
||||
let data = mas_policy::Data::new(mas_policy::BaseData {
|
||||
@@ -246,7 +246,7 @@ pub fn site_config_from_config(
|
||||
.map(|c| SessionLimitConfig {
|
||||
soft_limit: c.soft_limit,
|
||||
hard_limit: c.hard_limit,
|
||||
hard_limit_eviction: c.hard_limit_eviction,
|
||||
dangerous_hard_limit_eviction: c.dangerous_hard_limit_eviction,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -155,13 +155,14 @@ pub struct SessionLimitConfig {
|
||||
///
|
||||
/// [`hard_limit`]: Self::hard_limit
|
||||
pub soft_limit: NonZeroU64,
|
||||
/// Upon login, when `hard_limit_eviction: false`, will refuse the new login
|
||||
/// (policy violation error), otherwise, see [`hard_limit_eviction`].
|
||||
/// Upon login, when `dangerous_hard_limit_eviction: false`, will refuse the
|
||||
/// new login (policy violation error), otherwise, see
|
||||
/// [`dangerous_hard_limit_eviction`].
|
||||
///
|
||||
/// The hard limit is enforced in all contexts
|
||||
/// (interactive/non-interactive).
|
||||
///
|
||||
/// [`hard_limit_eviction`]: Self::hard_limit_eviction
|
||||
/// [`dangerous_hard_limit_eviction`]: Self::dangerous_hard_limit_eviction
|
||||
pub hard_limit: NonZeroU64,
|
||||
/// Whether we should automatically choose the least recently used devices
|
||||
/// to remove when the [`Self::hard_limit`] is reached; in order to
|
||||
@@ -174,10 +175,10 @@ pub struct SessionLimitConfig {
|
||||
/// be recovered if you have another verified active device or have a
|
||||
/// recovery key setup.
|
||||
///
|
||||
/// When using [`hard_limit_eviction`], the [`hard_limit`] must be
|
||||
/// at-least 2 to avoid catastropically losing encrypted history and digital
|
||||
/// identity in pathological cases. Keep in mind this is a bare minimum
|
||||
/// restriction and you can still run into trouble.
|
||||
/// When using [`dangerous_hard_limit_eviction`], the [`hard_limit`] must be
|
||||
/// at least 2 to avoid catastrophically losing encrypted history and
|
||||
/// digital identity in pathological cases. Keep in mind this is a bare
|
||||
/// minimum restriction and you can still run into trouble.
|
||||
///
|
||||
/// This is most applicable in scenarios where your homeserver has many
|
||||
/// legacy bots/scripts that login over and over (which ideally should
|
||||
@@ -187,9 +188,9 @@ pub struct SessionLimitConfig {
|
||||
/// level of sanity with the number of devices that people can have.
|
||||
///
|
||||
/// [`hard_limit`]: Self::hard_limit
|
||||
/// [`hard_limit_eviction`]: Self::hard_limit_eviction
|
||||
/// [`dangerous_hard_limit_eviction`]: Self::dangerous_hard_limit_eviction
|
||||
#[serde(default = "default_false")]
|
||||
pub hard_limit_eviction: bool,
|
||||
pub dangerous_hard_limit_eviction: bool,
|
||||
}
|
||||
|
||||
impl SessionLimitConfig {
|
||||
@@ -208,10 +209,10 @@ impl SessionLimitConfig {
|
||||
.into());
|
||||
}
|
||||
|
||||
// See [`SessionLimitConfig::hard_limit_eviction`] docstring
|
||||
if self.hard_limit_eviction && self.hard_limit.get() < 2 {
|
||||
// See [`SessionLimitConfig::dangerous_hard_limit_eviction`] docstring
|
||||
if self.dangerous_hard_limit_eviction && self.hard_limit.get() < 2 {
|
||||
return Err(figment::error::Error::from(
|
||||
"Session `hard_limit` must be at-least 2 when automatic `hard_limit_eviction` is set. \
|
||||
"Session `hard_limit` must be at least 2 when automatic `dangerous_hard_limit_eviction` is set. \
|
||||
See configuration docs for more info.",
|
||||
).with_path("hard_limit").into());
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ pub struct SessionExpirationConfig {
|
||||
pub struct SessionLimitConfig {
|
||||
pub soft_limit: NonZeroU64,
|
||||
pub hard_limit: NonZeroU64,
|
||||
pub hard_limit_eviction: bool,
|
||||
pub dangerous_hard_limit_eviction: bool,
|
||||
}
|
||||
|
||||
/// Random site configuration we want accessible in various places.
|
||||
|
||||
+245
-155
@@ -54,6 +54,10 @@ static LOGIN_COUNTER: LazyLock<Counter<u64>> = LazyLock::new(|| {
|
||||
const TYPE: Key = Key::from_static_str("type");
|
||||
const RESULT: Key = Key::from_static_str("result");
|
||||
|
||||
/// This matches the `getNinetyDaysAgo()` used in the web UI for "inactive"
|
||||
/// sessions.
|
||||
const INACTIVE_SESSION_THRESHOLD: chrono::TimeDelta = Duration::days(90);
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(tag = "type")]
|
||||
enum LoginType {
|
||||
@@ -517,129 +521,95 @@ async fn process_violations_for_compat_login(
|
||||
..
|
||||
},
|
||||
] => {
|
||||
let session_limit_config = session_limit_config
|
||||
.expect("We should have a `session_limit` config if we are seeing a `TooManySessions` violation. \
|
||||
This is most likely a programming error.");
|
||||
|
||||
let need_to_remove = usize::try_from(*need_to_remove).map_err(|err| {
|
||||
RouteError::Internal(
|
||||
anyhow::anyhow!("Unable to convert `need_to_remove` to usize: {err}").into(),
|
||||
)
|
||||
})?;
|
||||
|
||||
// When logging in with the compatibility API, there is no way for us to
|
||||
// display any web UI for people to remove devices, so we instead
|
||||
// automatically remove their oldest devices (when `hard_limit_eviction`
|
||||
// is configured).
|
||||
if session_limit_config.hard_limit_eviction {
|
||||
// Find the least recently used (LRU) compat sessions
|
||||
//
|
||||
// In the future, it may be nice to avoid sessions with
|
||||
// cryptographic state (what does that mean exactly? keys uploaded
|
||||
// for device?).
|
||||
//
|
||||
// FIXME: We could potentially use
|
||||
// `repo.compat_session().finish_bulk(...)` if it had the ability to
|
||||
// limit and order.
|
||||
let lru_compat_sessions = {
|
||||
// TODO: In the future, instead of all of this faff, we can simply order
|
||||
// by `last_active_at`
|
||||
//
|
||||
// XXX: Since we can't order by `last_active_at` yet, we instead filter
|
||||
// the list down to "inactive" sessions (`last_active_at` > 90 days
|
||||
// ago). And by the nature of
|
||||
// [`mas_data_model::compat::CompatSession::id`] being a `Ulid`/`Uuid`
|
||||
// (the query is ordered by `compat_session_id`), the first bytes are a
|
||||
// timestamp so we'll be getting the 'oldest created' sessions which is
|
||||
// another good proxy.
|
||||
|
||||
let mut edges_to_consider = Vec::new();
|
||||
|
||||
// First, find the "inactive" sessions
|
||||
let inactive_threshold_date = clock.now() - Duration::days(90);
|
||||
let inactive_compat_session_page = repo
|
||||
.compat_session()
|
||||
.list(
|
||||
CompatSessionFilter::new()
|
||||
.for_user(user)
|
||||
.active_only()
|
||||
.with_last_active_before(inactive_threshold_date),
|
||||
// We fetch a minimum of 100 sessions (more than we need in
|
||||
// normal cases) so we can sort by `last_active_at` after it
|
||||
// gets back from the database and can get even closer to
|
||||
// removing the oldest sessions.
|
||||
Pagination::first(std::cmp::max(need_to_remove, 100)),
|
||||
// Normally, if we are seeing a `TooManySessions` violation, we would
|
||||
// expect `session_limit_config` to be filled in but if someone created
|
||||
// their own policies which emit a `TooManySessions` violation that isn't
|
||||
// based on the configured `session_limit`, we could also end up here.
|
||||
//
|
||||
// If you're using the default policies in MAS, `session_limit_config` being
|
||||
// `None` would be a programming error.
|
||||
match session_limit_config {
|
||||
Some(session_limit_config) => {
|
||||
let need_to_remove = usize::try_from(*need_to_remove).map_err(|err| {
|
||||
RouteError::Internal(
|
||||
anyhow::anyhow!("Unable to convert `need_to_remove` to usize: {err}")
|
||||
.into(),
|
||||
)
|
||||
.await?;
|
||||
edges_to_consider.extend(inactive_compat_session_page.edges);
|
||||
})?;
|
||||
|
||||
// If there aren't enough "inactive" sessions, supplement with active ones
|
||||
if edges_to_consider.len() < need_to_remove {
|
||||
let active_compat_session_page = repo
|
||||
.compat_session()
|
||||
.list(
|
||||
// If we try to use
|
||||
// `.with_last_active_after(inactive_threshold_date)`
|
||||
// here, it will exclude all of the rows where
|
||||
// `last_active_at` is null which we want to include.
|
||||
CompatSessionFilter::new().for_user(user).active_only(),
|
||||
// We fetch a minimum of 100 sessions (more than we need in
|
||||
// normal cases) so we can sort by `last_active_at` after it
|
||||
// gets back from the database and can get even closer to
|
||||
// removing the oldest sessions.
|
||||
Pagination::first(std::cmp::max(need_to_remove, 100)),
|
||||
)
|
||||
.await?;
|
||||
edges_to_consider.extend(active_compat_session_page.edges);
|
||||
}
|
||||
// When logging in with the compatibility API, there is no way for us to
|
||||
// display any web UI for people to remove devices, so we instead
|
||||
// automatically remove their oldest devices (when
|
||||
// `dangerous_hard_limit_eviction` is configured).
|
||||
if session_limit_config.dangerous_hard_limit_eviction {
|
||||
// Find the least recently used (LRU) compat sessions
|
||||
//
|
||||
// FIXME: In the future, it would be nice to avoid sessions with
|
||||
// cryptographic state. What does that mean exactly? Keys
|
||||
// uploaded for device? The spec says this:
|
||||
// > For all intents and purposes, non-cryptographic devices are
|
||||
// > a completely separate concept and do not exist from the
|
||||
// > perspective of the cryptography layer since they do not
|
||||
// > have [device] identity keys, so it is impossible to send
|
||||
// > them decryption keys.
|
||||
// >
|
||||
// > -- https://spec.matrix.org/v1.18/client-server-api/#recommended-client-behaviour
|
||||
//
|
||||
// FIXME: Instead of finding, then finishing in separate steps,
|
||||
// we could potentially use
|
||||
// `repo.compat_session().finish_bulk(...)` if it had the
|
||||
// ability to limit and order.
|
||||
let lru_compat_sessions =
|
||||
find_lru_compat_sessions_flawed(clock, repo, user, need_to_remove)
|
||||
.await?;
|
||||
|
||||
// De-duplicate the sessions across both pages
|
||||
let compat_session_map = {
|
||||
let mut compat_session_map = HashMap::new();
|
||||
for edge in edges_to_consider {
|
||||
let (compat_session, _) = edge.node;
|
||||
compat_session_map.insert(compat_session.id, compat_session);
|
||||
// For now, we only automatically clean up compatibility sessions.
|
||||
// If there aren't enough sessions that we could clean up, we just
|
||||
// throw an error with an explanation.
|
||||
if lru_compat_sessions.len() < need_to_remove {
|
||||
return Err(RouteError::PolicyHardSessionLimitReached);
|
||||
}
|
||||
compat_session_map
|
||||
};
|
||||
|
||||
// List of compat sessions sorted by `last_active_at` ascending
|
||||
let sorted_compat_sessions = {
|
||||
let mut compat_sessions: Vec<mas_data_model::CompatSession> =
|
||||
compat_session_map.into_values().collect();
|
||||
// Sort by `last_active_at` (ascending)
|
||||
compat_sessions.sort_by_key(|compat_session| {
|
||||
(
|
||||
// We mainly care about sorting by `last_active_at`
|
||||
compat_session.last_active_at,
|
||||
// Tie-break based on `created_at`
|
||||
compat_session.created_at,
|
||||
// Tie-break based on `id` for determinism
|
||||
compat_session.id,
|
||||
)
|
||||
});
|
||||
compat_sessions
|
||||
};
|
||||
// Remove the sessions (only as much as necessary, `need_to_remove`)
|
||||
for compat_session in &lru_compat_sessions[0..need_to_remove] {
|
||||
// Log what's happening so we have some explanation if someone asks
|
||||
//
|
||||
// FIXME: In the future, it would probably good to mark the reason
|
||||
// down in the database for a better paper trail.
|
||||
tracing::info!(
|
||||
// So we can easily find logs for a given user
|
||||
user_id = user.id.to_string(),
|
||||
username = user.username,
|
||||
// So we can easily look it up in the MAS database
|
||||
compat_session_id = compat_session.id.to_string(),
|
||||
// Make it easier to line up with what the user may be talking
|
||||
// about
|
||||
device_id = compat_session
|
||||
.device
|
||||
.as_ref()
|
||||
.map(mas_data_model::Device::as_str),
|
||||
"Automatically removing compat session for user (`dangerous_hard_limit_eviction`)"
|
||||
);
|
||||
|
||||
sorted_compat_sessions
|
||||
};
|
||||
|
||||
// For now, we only automatically clean up compatibility sessions.
|
||||
// If there aren't enough sessions that we could clean up, we just
|
||||
// throw an error with an explanation.
|
||||
if lru_compat_sessions.len() < need_to_remove {
|
||||
return Err(RouteError::PolicyHardSessionLimitReached);
|
||||
// Remove the session
|
||||
repo.compat_session()
|
||||
.finish(clock, compat_session.to_owned())
|
||||
.await?;
|
||||
}
|
||||
} else {
|
||||
// Tell the user about the limit
|
||||
return Err(RouteError::PolicyHardSessionLimitReached);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the sessions (only as much as necessary, `need_to_remove`)
|
||||
for compat_session in &lru_compat_sessions[0..need_to_remove] {
|
||||
repo.compat_session()
|
||||
.finish(clock, compat_session.to_owned())
|
||||
.await?;
|
||||
// If we got here, it means they are using their own custom policies
|
||||
// which don't take into account the configured `session_limit`.
|
||||
//
|
||||
// We don't know the actual reason behind the policy emitting the
|
||||
// violation so we just have to show a generic policy rejected page.
|
||||
None => {
|
||||
// FIXME: We should be exposing the violations to the user
|
||||
return Err(RouteError::PolicyRejected);
|
||||
}
|
||||
} else {
|
||||
// Tell the user about the limit
|
||||
return Err(RouteError::PolicyHardSessionLimitReached);
|
||||
}
|
||||
}
|
||||
// Nothing is wrong
|
||||
@@ -654,6 +624,113 @@ async fn process_violations_for_compat_login(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// We fetch a minimum number of sessions (2160, more than we need in normal
|
||||
/// cases) so we can sort by `last_active_at` after it gets back from the
|
||||
/// database and can get even closer to removing the true oldest sessions.
|
||||
///
|
||||
/// The 2160 number was chosen based on someone having a script that runs every
|
||||
/// hour for the the 90-day `INACTIVE_SESSION_THRESHOLD`. Additionally, it also
|
||||
/// aligns nicely with < 0.001% of people on matrix.org having less than 2160
|
||||
/// sessions and reasoning how much memory is reasonable to spend on this
|
||||
/// operation to get things right. Assuming each row is ~1 KiB (pessimistic high
|
||||
/// bound, see next paragraph below) we end up at ~2 MiB of memory.
|
||||
///
|
||||
/// Each item in the page is `(CompatSession, Option<CompatSsoLogin>)` where
|
||||
/// `CompatSession` is 192 bytes plus a couple of strings (device name and user
|
||||
/// agent) (assume pessimistic 512 total bytes). And `CompatSsoLogin` which is
|
||||
/// also 192 bytes with a `login_token` string which should be no more than 32
|
||||
/// bytes.
|
||||
const MINIMUM_SESSIONS_TO_FETCH: usize = 2160;
|
||||
|
||||
/// Find the least recently used (LRU) compat sessions
|
||||
///
|
||||
/// The results of this function are flawed (for accounts with more sessions
|
||||
/// than `MINIMUM_SESSIONS_TO_FETCH`) because we can't order by `last_active_at`
|
||||
/// and get an absolute sort of actually least recently used sessions. But we do
|
||||
/// a pretty good job at working around the problem (see internal comments for
|
||||
/// details).
|
||||
async fn find_lru_compat_sessions_flawed(
|
||||
clock: &dyn Clock,
|
||||
repo: &mut BoxRepository,
|
||||
user: &User,
|
||||
// Like a limit we this function may return more more results
|
||||
num_requested: usize,
|
||||
) -> Result<Vec<CompatSession>, RouteError> {
|
||||
// TODO: In the future, instead of all of this faff, we can simply order
|
||||
// by `last_active_at`
|
||||
|
||||
let mut edges_to_consider = Vec::new();
|
||||
|
||||
// First, find the "inactive" sessions
|
||||
//
|
||||
// XXX: Since we can't order by `last_active_at` yet, we instead
|
||||
// filter the list down to "inactive" sessions (`last_active_at` >
|
||||
// 90 days ago) (this matches the `getNinetyDaysAgo()` used in the
|
||||
// web UI for "inactive" sessions). And by the nature of
|
||||
// [`mas_data_model::compat::CompatSession::id`] being a
|
||||
// `Ulid` (the query is ordered by `compat_session_id`), the
|
||||
// first bytes are a timestamp so we'll be getting the 'oldest
|
||||
// created' sessions which is another good proxy.
|
||||
let inactive_threshold_date = clock.now() - INACTIVE_SESSION_THRESHOLD;
|
||||
let inactive_compat_session_page = repo
|
||||
.compat_session()
|
||||
.list(
|
||||
CompatSessionFilter::new()
|
||||
.for_user(user)
|
||||
.active_only()
|
||||
.with_last_active_before(inactive_threshold_date),
|
||||
Pagination::first(std::cmp::max(num_requested, MINIMUM_SESSIONS_TO_FETCH)),
|
||||
)
|
||||
.await?;
|
||||
edges_to_consider.extend(inactive_compat_session_page.edges);
|
||||
|
||||
// If there aren't enough "inactive" sessions, supplement with active ones
|
||||
if edges_to_consider.len() < num_requested {
|
||||
let active_compat_session_page = repo
|
||||
.compat_session()
|
||||
.list(
|
||||
// If we try to use
|
||||
// `.with_last_active_after(inactive_threshold_date)`
|
||||
// here, it will exclude all of the rows where
|
||||
// `last_active_at` is null which we want to include.
|
||||
CompatSessionFilter::new().for_user(user).active_only(),
|
||||
Pagination::first(std::cmp::max(num_requested, MINIMUM_SESSIONS_TO_FETCH)),
|
||||
)
|
||||
.await?;
|
||||
edges_to_consider.extend(active_compat_session_page.edges);
|
||||
}
|
||||
|
||||
// De-duplicate the sessions across both pages
|
||||
let compat_session_map = {
|
||||
let mut compat_session_map = HashMap::new();
|
||||
for edge in edges_to_consider {
|
||||
let (compat_session, _) = edge.node;
|
||||
compat_session_map.insert(compat_session.id, compat_session);
|
||||
}
|
||||
compat_session_map
|
||||
};
|
||||
|
||||
// List of compat sessions sorted by `last_active_at` ascending
|
||||
let sorted_compat_sessions = {
|
||||
let mut compat_sessions: Vec<mas_data_model::CompatSession> =
|
||||
compat_session_map.into_values().collect();
|
||||
// Sort by `last_active_at` (ascending)
|
||||
compat_sessions.sort_by_key(|compat_session| {
|
||||
(
|
||||
// We mainly care about sorting by `last_active_at`
|
||||
compat_session.last_active_at,
|
||||
// Tie-break based on `created_at`
|
||||
compat_session.created_at,
|
||||
// Tie-break based on `id` for determinism
|
||||
compat_session.id,
|
||||
)
|
||||
});
|
||||
compat_sessions
|
||||
};
|
||||
|
||||
Ok(sorted_compat_sessions)
|
||||
}
|
||||
|
||||
async fn token_login(
|
||||
rng: &mut (dyn RngCore + Send),
|
||||
clock: &dyn Clock,
|
||||
@@ -910,7 +987,11 @@ async fn user_password_login(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{collections::HashSet, num::NonZeroU64, ops::Sub};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
num::NonZeroU64,
|
||||
ops::{Mul, Sub},
|
||||
};
|
||||
|
||||
use hyper::Request;
|
||||
use mas_matrix::{HomeserverConnection, ProvisionRequest};
|
||||
@@ -1678,7 +1759,7 @@ mod tests {
|
||||
soft_limit: NonZeroU64::new(1).unwrap(),
|
||||
// Some arbitrary high value (more than we login)
|
||||
hard_limit: NonZeroU64::new(5).unwrap(),
|
||||
hard_limit_eviction: false,
|
||||
dangerous_hard_limit_eviction: false,
|
||||
}),
|
||||
..test_site_config()
|
||||
},
|
||||
@@ -1728,7 +1809,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(),
|
||||
hard_limit_eviction: false,
|
||||
dangerous_hard_limit_eviction: false,
|
||||
}),
|
||||
..test_site_config()
|
||||
},
|
||||
@@ -1779,10 +1860,10 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that the `hard_limit_eviction` will automatically drop old sessions
|
||||
/// when we go over the limit
|
||||
/// 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_hard_limit_eviction_old_compat_login(pool: PgPool) {
|
||||
async fn test_dangerous_hard_limit_eviction_old_compat_login(pool: PgPool) {
|
||||
setup();
|
||||
let state = TestState::from_pool_with_site_config(
|
||||
pool,
|
||||
@@ -1790,10 +1871,10 @@ mod tests {
|
||||
session_limit: Some(SessionLimitConfig {
|
||||
// (doesn't matter)
|
||||
soft_limit: NonZeroU64::new(1).unwrap(),
|
||||
// Must be at-least 2 when `hard_limit_eviction`
|
||||
// Must be at least 2 when `dangerous_hard_limit_eviction`
|
||||
hard_limit: NonZeroU64::new(2).unwrap(),
|
||||
// Option under test
|
||||
hard_limit_eviction: true,
|
||||
dangerous_hard_limit_eviction: true,
|
||||
}),
|
||||
..test_site_config()
|
||||
},
|
||||
@@ -1811,23 +1892,7 @@ mod tests {
|
||||
|
||||
let mut login_device_ids: Vec<String> = Vec::new();
|
||||
|
||||
// Keep logging in to add more sessions, up to the `hard_limit`. Then `+ 1` for
|
||||
// one more login will drop one of our old sessions to make room for the new
|
||||
// login
|
||||
#[allow(clippy::range_plus_one)]
|
||||
for login_index in 0..(session_limit_config.hard_limit.get() + 1) {
|
||||
let original_time = state.clock.now();
|
||||
// All of the logins except the last one should be in the past
|
||||
if login_index <= session_limit_config.hard_limit.get() {
|
||||
// Rewind time so the logins appear older than our "inactive" threshold (90
|
||||
// days)
|
||||
let login_index_i64: i64 = login_index.try_into().unwrap();
|
||||
state
|
||||
.clock
|
||||
// Each login is a day earlier
|
||||
.advance(Duration::days(-200 + login_index_i64));
|
||||
}
|
||||
|
||||
let do_login = async || {
|
||||
let request = Request::post("/_matrix/client/v3/login").json(serde_json::json!({
|
||||
"type": "m.login.password",
|
||||
"identifier": {
|
||||
@@ -1848,16 +1913,34 @@ mod tests {
|
||||
panic!("Expected `device_id` to be a string")
|
||||
}
|
||||
};
|
||||
login_device_ids.push(device_id);
|
||||
|
||||
// Wait for `last_active_at` to be set
|
||||
state.activity_tracker.flush().await;
|
||||
|
||||
// Restore time
|
||||
state.clock.advance(original_time - state.clock.now());
|
||||
}
|
||||
// Return the new device
|
||||
device_id
|
||||
};
|
||||
|
||||
// Sanity check that the compat sessions have `last_active_at` set. This is
|
||||
// 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 device_id = do_login().await;
|
||||
login_device_ids.push(device_id);
|
||||
|
||||
// Advance time so it appears like each login happens a day after each other
|
||||
state.clock.advance(Duration::days(1));
|
||||
}
|
||||
let time_after_past_logins = state.clock.now();
|
||||
|
||||
// Jump to "current time" (anything > INACTIVE_SESSION_THRESHOLD) which will
|
||||
// make all of those past logins to be considered "inactive" at this point.
|
||||
state.clock.advance(INACTIVE_SESSION_THRESHOLD.mul(2));
|
||||
assert!(
|
||||
state.clock.now() - time_after_past_logins > INACTIVE_SESSION_THRESHOLD,
|
||||
"Expected 'current time' login to happen > INACTIVE_SESSION_THRESHOLD from when the past logins happened"
|
||||
);
|
||||
|
||||
// Sanity check that the past compat sessions have `last_active_at` set. This is
|
||||
// important as `last_active_at` starts out null.
|
||||
let mut repo = state.repository().await.unwrap();
|
||||
let compat_session_page = repo
|
||||
@@ -1874,11 +1957,18 @@ mod tests {
|
||||
.last_active_at
|
||||
.expect("We expect compat sessions to have `last_active_at` set for this test");
|
||||
assert!(
|
||||
last_active_at < (state.clock.now().sub(Duration::days(90))),
|
||||
"Expected compat sessions to have a `last_active_at` older than the 90 day 'inactive' threshold"
|
||||
last_active_at < (state.clock.now().sub(INACTIVE_SESSION_THRESHOLD)),
|
||||
"Expected past compat sessions to have a `last_active_at` older than the `INACTIVE_SESSION_THRESHOLD`"
|
||||
);
|
||||
}
|
||||
|
||||
// Now the user wants to login in the "current time".
|
||||
//
|
||||
// One more login will drop one of our old sessions to make room for the new
|
||||
// login
|
||||
let device_id = do_login().await;
|
||||
login_device_ids.push(device_id);
|
||||
|
||||
// Ensure we still only have two sessions (`session_limit_config.hard_limit`).
|
||||
// We're sanity checking across all session types.
|
||||
let session_counts = count_user_sessions_for_limiting(&mut repo, &user)
|
||||
@@ -1908,7 +1998,7 @@ mod tests {
|
||||
.0
|
||||
.device
|
||||
.clone()
|
||||
.expect("Expected each login should havea a device")
|
||||
.expect("Expected each login should have a device")
|
||||
.as_str()
|
||||
.to_owned()
|
||||
})
|
||||
@@ -1936,11 +2026,11 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that the `hard_limit_eviction` will automatically drop the oldest
|
||||
/// sessions when we go over the limit even if all of the sessions are
|
||||
/// recent.
|
||||
/// Test that the `dangerous_hard_limit_eviction` will automatically drop
|
||||
/// 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_hard_limit_eviction_recent_compat_login(pool: PgPool) {
|
||||
async fn test_dangerous_hard_limit_eviction_recent_compat_login(pool: PgPool) {
|
||||
setup();
|
||||
let state = TestState::from_pool_with_site_config(
|
||||
pool,
|
||||
@@ -1948,10 +2038,10 @@ mod tests {
|
||||
session_limit: Some(SessionLimitConfig {
|
||||
// (doesn't matter)
|
||||
soft_limit: NonZeroU64::new(1).unwrap(),
|
||||
// Must be at-least 2 when `hard_limit_eviction`
|
||||
// Must be at least 2 when `dangerous_hard_limit_eviction`
|
||||
hard_limit: NonZeroU64::new(2).unwrap(),
|
||||
// Option under test
|
||||
hard_limit_eviction: true,
|
||||
dangerous_hard_limit_eviction: true,
|
||||
}),
|
||||
..test_site_config()
|
||||
},
|
||||
@@ -2039,7 +2129,7 @@ mod tests {
|
||||
.0
|
||||
.device
|
||||
.clone()
|
||||
.expect("Expected each login should havea a device")
|
||||
.expect("Expected each login should have a device")
|
||||
.as_str()
|
||||
.to_owned()
|
||||
})
|
||||
|
||||
@@ -133,6 +133,26 @@ impl CallbackDestination {
|
||||
let new_qs = serde_urlencoded::to_string(merged)?;
|
||||
|
||||
redirect_uri.set_query(Some(&new_qs));
|
||||
if redirect_uri.fragment().is_none() {
|
||||
// Ensure that the Location header (redirect target)
|
||||
// includes a URL fragment (#) of some sort.
|
||||
//
|
||||
// Any fragment present in the Location header URL that the server redirects to
|
||||
// (e.g., via a 303 response) will overwrite the client’s existing fragment,
|
||||
// otherwise the fragment will be preserved across the
|
||||
// redirect (and may contain sensitive information,
|
||||
// or confuse the downstream client).
|
||||
//
|
||||
// If the redirect_uri already contains a fragment, that fragment will do the
|
||||
// same job, so we leave it alone — we don't want to mangle the client's
|
||||
// configured redirect URL by replacing it with a blank fragment.
|
||||
// Otherwise, set a fragment of empty string (effectively appending `#` to the
|
||||
// URL).
|
||||
//
|
||||
// Browser behaviour is documented as part of the 'location URL' algorithm at
|
||||
// https://fetch.spec.whatwg.org/commit-snapshots/809904366f33a673a9489b81155ee9e3edd29c12#concept-response-location-url
|
||||
redirect_uri.set_fragment(Some(""));
|
||||
}
|
||||
|
||||
Ok(Redirect::to(redirect_uri.as_str()).into_response())
|
||||
}
|
||||
@@ -164,3 +184,68 @@ impl CallbackDestination {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use hyper::{Request, StatusCode};
|
||||
use mas_router::SimpleRoute;
|
||||
use oauth2_types::registration::ClientRegistrationResponse;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup};
|
||||
|
||||
/// Test that checks the content of the `Location` header
|
||||
/// in response to an authorization request.
|
||||
///
|
||||
/// Specifically, we expect to see an empty fragment (`#`)
|
||||
/// at the end of the URL in order to overwrite any fragment
|
||||
/// that the browser might otherwise preserve across the redirect.
|
||||
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
|
||||
async fn test_query_mode_location_header(pool: PgPool) {
|
||||
setup();
|
||||
let state = TestState::from_pool(pool).await.unwrap();
|
||||
|
||||
// Register an OAuth2 client
|
||||
let request =
|
||||
Request::post(mas_router::OAuth2RegistrationEndpoint::PATH).json(serde_json::json!({
|
||||
"client_uri": "https://example.com/",
|
||||
"redirect_uris": ["https://example.com/callback"],
|
||||
"token_endpoint_auth_method": "none",
|
||||
"response_types": ["code"],
|
||||
"grant_types": ["authorization_code"],
|
||||
}));
|
||||
|
||||
let response = state.request(request).await;
|
||||
response.assert_status(StatusCode::CREATED);
|
||||
|
||||
let registration: ClientRegistrationResponse = response.json();
|
||||
let client_id = registration.client_id;
|
||||
|
||||
// Send an authorization request with response_mode=query and prompt=none.
|
||||
// prompt=none always fails with login_required since there is no session,
|
||||
// which exercises the CallbackDestinationMode::Query path.
|
||||
|
||||
// Build /authorize query parameters
|
||||
let query = url::form_urlencoded::Serializer::new(String::new())
|
||||
.append_pair("response_type", "code")
|
||||
.append_pair("client_id", &client_id)
|
||||
.append_pair("redirect_uri", "https://example.com/callback")
|
||||
.append_pair("scope", "openid")
|
||||
.append_pair("state", "test-state-value")
|
||||
.append_pair("response_mode", "query")
|
||||
.append_pair("prompt", "none")
|
||||
.finish();
|
||||
|
||||
let response = state
|
||||
.request(Request::get(format!("https://example.com/authorize?{query}")).empty())
|
||||
.await;
|
||||
|
||||
response.assert_status(StatusCode::SEE_OTHER);
|
||||
|
||||
// Check the form of the Location redirect
|
||||
response.assert_header_value(
|
||||
hyper::header::LOCATION,
|
||||
"https://example.com/callback?state=test-state-value&error=login_required&error_description=The+Authorization+Server+requires+End-User+authentication.#",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ pub enum ViolationVariant {
|
||||
|
||||
/// The user has reached their session limit.
|
||||
TooManySessions {
|
||||
/// How many devices to remove
|
||||
/// How many devices need to be removed to make room for the new session
|
||||
need_to_remove: u32,
|
||||
},
|
||||
}
|
||||
@@ -73,7 +73,7 @@ impl ViolationVariant {
|
||||
Self::EmailDomainBanned => "email-domain-banned",
|
||||
Self::EmailNotAllowed => "email-not-allowed",
|
||||
Self::EmailBanned => "email-banned",
|
||||
Self::TooManySessions { need_to_remove: _ } => "too-many-sessions",
|
||||
Self::TooManySessions { .. } => "too-many-sessions",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2904,13 +2904,13 @@
|
||||
"minimum": 1
|
||||
},
|
||||
"hard_limit": {
|
||||
"description": "Upon login, when `hard_limit_eviction: false`, will refuse the new login\n (policy violation error), otherwise, see [`hard_limit_eviction`].\n\n The hard limit is enforced in all contexts\n (interactive/non-interactive).\n\n [`hard_limit_eviction`]: Self::hard_limit_eviction",
|
||||
"description": "Upon login, when `dangerous_hard_limit_eviction: false`, will refuse the\n new login (policy violation error), otherwise, see\n [`dangerous_hard_limit_eviction`].\n\n The hard limit is enforced in all contexts\n (interactive/non-interactive).\n\n [`dangerous_hard_limit_eviction`]: Self::dangerous_hard_limit_eviction",
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 1
|
||||
},
|
||||
"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 [`hard_limit_eviction`], the [`hard_limit`] must be\n at-least 2 to avoid catastropically losing encrypted history and digital\n identity in pathological cases. Keep in mind this is a bare minimum\n 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 [`hard_limit_eviction`]: Self::hard_limit_eviction",
|
||||
"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",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export type LocalazyMetadata = {
|
||||
};
|
||||
|
||||
const localazyMetadata: LocalazyMetadata = {
|
||||
projectUrl: "https://localazy.com/p/matrix-authentication-service!v1.15",
|
||||
projectUrl: "https://localazy.com/p/matrix-authentication-service",
|
||||
baseLocale: "en",
|
||||
languages: [
|
||||
{
|
||||
@@ -208,25 +208,25 @@ const localazyMetadata: LocalazyMetadata = {
|
||||
file: "frontend.json",
|
||||
path: "",
|
||||
cdnFiles: {
|
||||
"cs": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/cs/frontend.json",
|
||||
"da": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/da/frontend.json",
|
||||
"de": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/de/frontend.json",
|
||||
"en": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/en/frontend.json",
|
||||
"et": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/et/frontend.json",
|
||||
"fi": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/fi/frontend.json",
|
||||
"fr": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/fr/frontend.json",
|
||||
"hu": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/hu/frontend.json",
|
||||
"nb_NO": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/nb-NO/frontend.json",
|
||||
"nl": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/nl/frontend.json",
|
||||
"pl": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/pl/frontend.json",
|
||||
"pt": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/pt/frontend.json",
|
||||
"pt_BR": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/pt-BR/frontend.json",
|
||||
"ru": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/ru/frontend.json",
|
||||
"sk": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/sk/frontend.json",
|
||||
"sv": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/sv/frontend.json",
|
||||
"uk": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/uk/frontend.json",
|
||||
"uz": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/uz/frontend.json",
|
||||
"zh#Hans": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/zh-Hans/frontend.json"
|
||||
"cs": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/cs/frontend.json",
|
||||
"da": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/da/frontend.json",
|
||||
"de": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/de/frontend.json",
|
||||
"en": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/en/frontend.json",
|
||||
"et": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/et/frontend.json",
|
||||
"fi": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/fi/frontend.json",
|
||||
"fr": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/fr/frontend.json",
|
||||
"hu": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/hu/frontend.json",
|
||||
"nb_NO": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/nb-NO/frontend.json",
|
||||
"nl": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/nl/frontend.json",
|
||||
"pl": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/pl/frontend.json",
|
||||
"pt": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/pt/frontend.json",
|
||||
"pt_BR": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/pt-BR/frontend.json",
|
||||
"ru": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/ru/frontend.json",
|
||||
"sk": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/sk/frontend.json",
|
||||
"sv": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/sv/frontend.json",
|
||||
"uk": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/uk/frontend.json",
|
||||
"uz": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/uz/frontend.json",
|
||||
"zh#Hans": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/7c203a8ac8bd48c3c4609a8effcd0fbac430f9b2/zh-Hans/frontend.json"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -234,25 +234,25 @@ const localazyMetadata: LocalazyMetadata = {
|
||||
file: "file.json",
|
||||
path: "",
|
||||
cdnFiles: {
|
||||
"cs": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/cs/file.json",
|
||||
"da": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/da/file.json",
|
||||
"de": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/de/file.json",
|
||||
"en": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/en/file.json",
|
||||
"et": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/et/file.json",
|
||||
"fi": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/fi/file.json",
|
||||
"fr": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/fr/file.json",
|
||||
"hu": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/hu/file.json",
|
||||
"nb_NO": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/nb-NO/file.json",
|
||||
"nl": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/nl/file.json",
|
||||
"pl": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/pl/file.json",
|
||||
"pt": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/pt/file.json",
|
||||
"pt_BR": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/pt-BR/file.json",
|
||||
"ru": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/ru/file.json",
|
||||
"sk": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/sk/file.json",
|
||||
"sv": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/sv/file.json",
|
||||
"uk": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/uk/file.json",
|
||||
"uz": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/uz/file.json",
|
||||
"zh#Hans": "https://delivery.localazy.com/_a6577655471040064500233bbe56/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/zh-Hans/file.json"
|
||||
"cs": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/cs/file.json",
|
||||
"da": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/da/file.json",
|
||||
"de": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/de/file.json",
|
||||
"en": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/en/file.json",
|
||||
"et": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/et/file.json",
|
||||
"fi": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/fi/file.json",
|
||||
"fr": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/fr/file.json",
|
||||
"hu": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/hu/file.json",
|
||||
"nb_NO": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/nb-NO/file.json",
|
||||
"nl": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/nl/file.json",
|
||||
"pl": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/pl/file.json",
|
||||
"pt": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/pt/file.json",
|
||||
"pt_BR": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/pt-BR/file.json",
|
||||
"ru": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/ru/file.json",
|
||||
"sk": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/sk/file.json",
|
||||
"sv": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/sv/file.json",
|
||||
"uk": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/uk/file.json",
|
||||
"uz": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/uz/file.json",
|
||||
"zh#Hans": "https://delivery.localazy.com/_a7686032324574572744739e0707/_e0/5b69b0350dccfd47c245a5d41c1b9fdf6912cc6e/zh-Hans/file.json"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+10
-10
@@ -258,10 +258,10 @@
|
||||
"button": "Nollaa identiteetti",
|
||||
"cancelled": {
|
||||
"description_1": "Voit sulkea tämän ikkunan ja palata sovellukseen jatkaaksesi.",
|
||||
"description_2": "Jos olet kirjautunut ulos kaikkialta etkä muista palautuskoodiasi, sinun on silti nollattava identiteettisi.",
|
||||
"heading": "Identiteetin nollaus peruutettu."
|
||||
"description_2": "Jos sinulla ei ole muita vahvistettuja laitteita eikä palautuskoodiasi käytettävissä, sinun on nollattava digitaalinen identiteettisi, jotta voit jatkaa sovelluksen käyttöä.",
|
||||
"heading": "Digitaalisen identiteetin nollaus peruutettu."
|
||||
},
|
||||
"description": "Jos et ole kirjautunut muihin laitteisiin ja olet kadottanut palautusavaimesi, sinun on nollattava identiteettisi, jotta voit jatkaa sovelluksen käyttöä.",
|
||||
"description": "Jos sinulla ei ole muita vahvistettuja laitteita eikä palautuskoodiasi käytettävissä, sinun on nollattava digitaalinen identiteettisi, jotta voit jatkaa sovelluksen käyttöä.",
|
||||
"effect_list": {
|
||||
"negative_1": "Menetät nykyisen viestihistoriasi",
|
||||
"negative_2": "Sinun on vahvistettava kaikki olemassa olevat laitteesi ja yhteystietosi uudelleen",
|
||||
@@ -271,18 +271,18 @@
|
||||
},
|
||||
"failure": {
|
||||
"description": "Tämä saattaa olla väliaikainen ongelma, joten yritä myöhemmin uudelleen. Jos ongelma jatkuu, ota yhteyttä palvelimen ylläpitäjään.",
|
||||
"heading": "Kryptografisen identiteetin nollauksen salliminen epäonnistui",
|
||||
"title": "Kryptografisen identiteetin nollauksen salliminen epäonnistui"
|
||||
"heading": "Digitaalisen identiteetin nollauksen salliminen epäonnistui",
|
||||
"title": "Digitaalisen identiteetin nollauksen salliminen epäonnistui"
|
||||
},
|
||||
"finish_reset": "Viimeistele nollaus",
|
||||
"heading": "Nollaa identiteettisi, jos et voi vahvistaa muulla tavalla",
|
||||
"heading": "Nollaa digitaalinen identiteettisi, jos et voi vahvistaa muulla tavalla",
|
||||
"start_reset": "Aloita nollaus",
|
||||
"success": {
|
||||
"description": "Identiteetin nollaus on hyväksytty seuraavaksi {{minutes}} minuutiksi. Voit sulkea tämän ikkunan ja palata sovellukseen jatkaaksesi.",
|
||||
"heading": "Identiteetin nollaus onnistui. Palaa takaisin sovellukseen viimeistelläksesi prosessin.",
|
||||
"title": "Kryptografisen identiteetin nollaus tilapäisesti sallittu"
|
||||
"description": "Digitaalisen identiteetin nollaus on hyväksytty seuraavaksi {{minutes}} minuutiksi. Voit sulkea tämän ikkunan ja palata sovellukseen jatkaaksesi.",
|
||||
"heading": "Digitaalisen identiteetin nollaus onnistui. Palaa takaisin sovellukseen viimeistelläksesi prosessin.",
|
||||
"title": "Digitaalisen identiteetin nollaus tilapäisesti sallittu"
|
||||
},
|
||||
"warning": "Nollaa identiteettisi vain, jos et voi käyttää toista laitetta, johon olet kirjautunut, ja olet kadottanut palautusavaimesi."
|
||||
"warning": "Nollaa digitaalinen identiteettisi vain, jos sinulla ei ole toista vahvistettua laitetta tai palautusavaintasi käytettävissä."
|
||||
},
|
||||
"selectable_session": {
|
||||
"label": "Valitse istunto"
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
"heading": "L’adresse e-mail {{email}} est déjà utilisée."
|
||||
},
|
||||
"end_session_button": {
|
||||
"confirmation_modal_body_text": "Assurez-vous de toujours avoir accès à un autre appareil vérifié ou à votre clé de récupération afin d'éviter de perdre l'historique de vos discussions chiffrées.",
|
||||
"confirmation_modal_body_text": "Make sure you always have access to another verified device or your recovery key to avoid losing your encrypted chat history.",
|
||||
"confirmation_modal_title": "Êtes-vous sûr de vouloir terminer cette session ?",
|
||||
"text": "Supprimer l’appareil"
|
||||
},
|
||||
|
||||
@@ -125,8 +125,8 @@
|
||||
"heading": "A(z) {{email}} e-mail-cím már használatban van."
|
||||
},
|
||||
"end_session_button": {
|
||||
"confirmation_modal_body_text": "Make sure you always have access to another verified device or your recovery key to avoid losing your encrypted chat history.",
|
||||
"confirmation_modal_title": "Biztos, hogy befejezi a munkamenetet?",
|
||||
"confirmation_modal_body_text": "Győződjön meg róla, hogy mindig hozzáfér egy másik ellenőrzött eszközhöz vagy a helyreállítási kulcsához, hogy elkerülje a titkosított csevegési előzmények elvesztését.",
|
||||
"confirmation_modal_title": "Biztos, hogy eltávolítja ezt az eszközt?",
|
||||
"text": "Eszköz eltávolítása"
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -159,7 +159,7 @@ violation contains {
|
||||
"msg": "user has too many active sessions",
|
||||
# `+ 1` because when you're at 2 sessions, and the limit is 2, you have to make room
|
||||
# for the new session
|
||||
"need_to_remove": (input.session_counts.total - data.session_limit.hard_limit) + 1,
|
||||
"need_to_remove": (input.session_counts.total - data.session_limit.soft_limit) + 1,
|
||||
} if {
|
||||
# Only apply if session limits are enabled in the config
|
||||
data.session_limit != null
|
||||
|
||||
@@ -223,34 +223,82 @@ test_mas_scopes if {
|
||||
with input.scope as "urn:mas:admin"
|
||||
}
|
||||
|
||||
test_session_limiting if {
|
||||
authorization_grant.allow with input.user as user
|
||||
# Helper utility to extract the number of sessions that they `need_to_remove`, returns 0
|
||||
# if the `too-many-sessions` violation is not found
|
||||
need_to_remove_sessions(violations) := need if {
|
||||
some v in violations
|
||||
v.code == "too-many-sessions"
|
||||
need := v.need_to_remove
|
||||
} else := 0
|
||||
|
||||
# Tests session limiting when using OAuth 2.0 authorization grants
|
||||
# (interactive, therefore `soft_limit` applies)
|
||||
# =========================================================================
|
||||
test_session_limiting_under_limit 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": 1}
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
result.allow
|
||||
result.need_to_remove_sessions == 0
|
||||
}
|
||||
|
||||
authorization_grant.allow with input.user as user
|
||||
test_session_limiting_under_soft_limit 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": 31}
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
result.allow
|
||||
result.need_to_remove_sessions == 0
|
||||
}
|
||||
|
||||
not authorization_grant.allow with input.user as user
|
||||
test_session_limiting_hit_soft_limit 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": 32}
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
not result.allow
|
||||
result.need_to_remove_sessions == 1
|
||||
}
|
||||
|
||||
not authorization_grant.allow with input.user as user
|
||||
test_session_limiting_over_soft_limit 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}
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
not result.allow
|
||||
result.need_to_remove_sessions == 11
|
||||
}
|
||||
|
||||
not authorization_grant.allow with input.user as user
|
||||
test_session_limiting_over_hard_limit 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": 65}
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
not result.allow
|
||||
|
||||
# Only the `soft_limit` applies to the interactive login
|
||||
result.need_to_remove_sessions == 34
|
||||
}
|
||||
|
||||
test_session_limiting_no_limit if {
|
||||
# No limit configured
|
||||
authorization_grant.allow with input.user as user
|
||||
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": 1}
|
||||
with data.session_limit as null
|
||||
|
||||
# Client credentials grant
|
||||
authorization_grant.allow with input.user as user
|
||||
with input.session_counts as null
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
result.allow
|
||||
result.need_to_remove_sessions == 0
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ violation contains {
|
||||
"msg": "user has too many active sessions (soft limit)",
|
||||
# `+ 1` because when you're at 2 sessions, and the limit is 2, you have to make room
|
||||
# for the new session
|
||||
"need_to_remove": (input.session_counts.total - data.session_limit.hard_limit) + 1,
|
||||
"need_to_remove": (input.session_counts.total - data.session_limit.soft_limit) + 1,
|
||||
} if {
|
||||
# Only apply if session limits are enabled in the config
|
||||
data.session_limit != null
|
||||
|
||||
@@ -10,90 +10,191 @@ import rego.v1
|
||||
|
||||
user := {"username": "john"}
|
||||
|
||||
# Helper utility to extract the number of sessions that they `need_to_remove`, returns 0
|
||||
# if the `too-many-sessions` violation is not found
|
||||
need_to_remove_sessions(violations) := need if {
|
||||
some v in violations
|
||||
v.code == "too-many-sessions"
|
||||
need := v.need_to_remove
|
||||
} else := 0
|
||||
|
||||
# Tests session limiting when using (the interactive part of) `m.login.sso`
|
||||
test_session_limiting_sso if {
|
||||
compat_login.allow with input.user as user
|
||||
# (interactive, therefore `soft_limit` applies)
|
||||
# =========================================================================
|
||||
test_session_limiting_sso_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": 1}
|
||||
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}
|
||||
result.allow
|
||||
result.need_to_remove_sessions == 0
|
||||
}
|
||||
|
||||
compat_login.allow with input.user as user
|
||||
test_session_limiting_sso_barely_under_soft_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": 31}
|
||||
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}
|
||||
result.allow
|
||||
result.need_to_remove_sessions == 0
|
||||
}
|
||||
|
||||
not compat_login.allow with input.user as user
|
||||
test_session_limiting_sso_hit_soft_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": 32}
|
||||
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}
|
||||
not result.allow
|
||||
result.need_to_remove_sessions == 1
|
||||
}
|
||||
|
||||
not compat_login.allow with input.user as user
|
||||
test_session_limiting_sso_over_soft_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}
|
||||
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}
|
||||
not result.allow
|
||||
result.need_to_remove_sessions == 11
|
||||
}
|
||||
|
||||
not compat_login.allow with input.user as user
|
||||
test_session_limiting_sso_over_hard_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": 65}
|
||||
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}
|
||||
not result.allow
|
||||
|
||||
# Only the `soft_limit` applies to the interactive `m.login.sso` login
|
||||
result.need_to_remove_sessions == 34
|
||||
}
|
||||
|
||||
test_session_limiting_sso_no_limit if {
|
||||
# No limit configured
|
||||
compat_login.allow with input.user as user
|
||||
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
|
||||
result.allow
|
||||
result.need_to_remove_sessions == 0
|
||||
}
|
||||
|
||||
# Test session limiting when using `m.login.password`
|
||||
test_session_limiting_password if {
|
||||
compat_login.allow with input.user as user
|
||||
# Test session limiting when using `m.login.password` (not interactive, therefore
|
||||
# `hard_limit` applies)
|
||||
# =========================================================================
|
||||
test_session_limiting_password_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": 1}
|
||||
with input.login as {"type": "m.login.password"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
result.allow
|
||||
result.need_to_remove_sessions == 0
|
||||
}
|
||||
|
||||
compat_login.allow with input.user as user
|
||||
test_session_limiting_password_under_hard_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": 63}
|
||||
with input.login as {"type": "m.login.password"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
result.allow
|
||||
result.need_to_remove_sessions == 0
|
||||
}
|
||||
|
||||
not compat_login.allow with input.user as user
|
||||
test_session_limiting_password_hit_hard_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": 64}
|
||||
with input.login as {"type": "m.login.password"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
not result.allow
|
||||
result.need_to_remove_sessions == 1
|
||||
}
|
||||
|
||||
not compat_login.allow with input.user as user
|
||||
test_session_limiting_password_over_hard_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": 65}
|
||||
with input.login as {"type": "m.login.password"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
not result.allow
|
||||
result.need_to_remove_sessions == 2
|
||||
}
|
||||
|
||||
test_session_limiting_password_no_limit if {
|
||||
# No limit configured
|
||||
compat_login.allow with input.user as user
|
||||
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.password"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as null
|
||||
result.allow
|
||||
result.need_to_remove_sessions == 0
|
||||
}
|
||||
|
||||
test_no_session_limiting_upon_replacement if {
|
||||
not compat_login.allow with input.user as user
|
||||
with input.session_counts as {"total": 65}
|
||||
with input.login as {"type": "m.login.password"}
|
||||
with input.session_replaced as false
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
|
||||
not compat_login.allow with input.user as user
|
||||
# If the session is replacing an existing session, no need to throw any violations about
|
||||
# too many sessions
|
||||
test_no_session_limiting_sso_upon_replacement 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": 65}
|
||||
with input.login as {"type": "m.login.sso"}
|
||||
with input.session_replaced as false
|
||||
with input.session_replaced as true
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
result.allow
|
||||
result.need_to_remove_sessions == 0
|
||||
}
|
||||
|
||||
test_no_session_limiting_password_upon_replacement 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": 65}
|
||||
with input.login as {"type": "m.login.password"}
|
||||
with input.session_replaced as true
|
||||
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
|
||||
result.allow
|
||||
result.need_to_remove_sessions == 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user