Introduce compat login policy

This commit is contained in:
Olivier 'reivilibre
2025-11-25 18:20:14 +00:00
parent 1d2f7fecf8
commit 069b57758b
6 changed files with 251 additions and 1 deletions

View File

@@ -12,7 +12,7 @@
use std::path::{Path, PathBuf};
use mas_policy::model::{
AuthorizationGrantInput, ClientRegistrationInput, EmailInput, RegisterInput,
AuthorizationGrantInput, ClientRegistrationInput, CompatLoginInput, EmailInput, RegisterInput,
};
use schemars::{JsonSchema, generate::SchemaSettings};
@@ -42,5 +42,6 @@ fn main() {
write_schema::<RegisterInput>(output_root, "register_input.json");
write_schema::<ClientRegistrationInput>(output_root, "client_registration_input.json");
write_schema::<AuthorizationGrantInput>(output_root, "authorization_grant_input.json");
write_schema::<CompatLoginInput>(output_root, "compat_login_input.json");
write_schema::<EmailInput>(output_root, "email_input.json");
}

View File

@@ -187,6 +187,32 @@ pub struct AuthorizationGrantInput<'a> {
pub requester: Requester,
}
/// Input for the compatibility login policy.
#[derive(Serialize, Debug, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct CompatLoginInput<'a> {
#[schemars(with = "std::collections::HashMap<String, serde_json::Value>")]
pub user: &'a User,
/// How many sessions the user has.
pub session_counts: SessionCounts,
// TODO is this actually what we care about? Don't we care a bit more about whether we're in an
// interactive context or a non-interactive context? (SSO type has both phases :()
pub login_type: CompatLoginType,
pub requester: Requester,
}
#[derive(Serialize, Debug, JsonSchema)]
pub enum CompatLoginType {
#[serde(rename = "m.login.sso")]
WebSso,
#[serde(rename = "m.login.password")]
Password,
}
/// Information about how many sessions the user has
#[derive(Serialize, Debug, JsonSchema)]
pub struct SessionCounts {

View File

@@ -16,6 +16,7 @@ INPUTS := \
client_registration/client_registration.rego \
register/register.rego \
authorization_grant/authorization_grant.rego \
compat_login/compat_login.rego \
email/email.rego
ifeq ($(DOCKER), 1)
@@ -38,6 +39,7 @@ policy.wasm: $(INPUTS)
-e "client_registration/violation" \
-e "register/violation" \
-e "authorization_grant/violation" \
-e "compat_login/violation" \
-e "email/violation" \
$^
tar xzf bundle.tar.gz /policy.wasm

View File

@@ -0,0 +1,61 @@
# Copyright 2025 Element Creations Ltd.
#
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
# Please see LICENSE files in the repository root for full details.
# METADATA
# schemas:
# - input: schema["compat_login_input"]
package compat_login
import rego.v1
import data.common
default allow := false
allow if {
count(violation) == 0
}
violation contains {"msg": sprintf(
"Requester [%s] isn't allowed to do this action",
[common.format_requester(input.requester)],
)} if {
common.requester_banned(input.requester, data.requester)
}
violation contains {
"code": "too-many-sessions",
"msg": "user has too many active sessions (soft limit)",
} if {
# Only apply if session limits are enabled in the config
data.session_limit != null
# This is a web-based interactive login
# TODO not strictly correct...
input.login_type == "m.login.sso"
# 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
# sessions to return under the limit.
data.session_limit.soft_limit <= input.session_counts.total
}
violation contains {
"code": "too-many-sessions",
"msg": "user has too many active sessions (hard limit)",
} if {
# Only apply if session limits are enabled in the config
data.session_limit != null
# This is not a web-based interactive login
input.login_type == "m.login.password"
# 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
# sessions to return under the limit.
data.session_limit.hard_limit <= input.session_counts.total
}

View File

@@ -0,0 +1,72 @@
# Copyright 2025 Element Creations Ltd.
#
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
# Please see LICENSE files in the repository root for full details.
package compat_login_test
import data.compat_login
import rego.v1
user := {"username": "john"}
test_session_limiting_sso if {
compat_login.allow with input.user as user
with input.session_counts as {"total": 1}
with input.login_type as "m.login.sso"
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
compat_login.allow with input.user as user
with input.session_counts as {"total": 31}
with input.login_type as "m.login.sso"
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
not compat_login.allow with input.user as user
with input.session_counts as {"total": 32}
with input.login_type as "m.login.sso"
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
not compat_login.allow with input.user as user
with input.session_counts as {"total": 42}
with input.login_type as "m.login.sso"
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
not compat_login.allow with input.user as user
with input.session_counts as {"total": 65}
with input.login_type as "m.login.sso"
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
# No limit configured
compat_login.allow with input.user as user
with input.session_counts as {"total": 1}
with input.login_type as "m.login.sso"
with data.session_limit as null
}
test_session_limiting_password if {
compat_login.allow with input.user as user
with input.session_counts as {"total": 1}
with input.login_type as "m.login.password"
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
compat_login.allow with input.user as user
with input.session_counts as {"total": 63}
with input.login_type as "m.login.password"
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
not compat_login.allow with input.user as user
with input.session_counts as {"total": 64}
with input.login_type as "m.login.password"
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
not compat_login.allow with input.user as user
with input.session_counts as {"total": 65}
with input.login_type as "m.login.password"
with data.session_limit as {"soft_limit": 32, "hard_limit": 64}
# No limit configured
compat_login.allow with input.user as user
with input.session_counts as {"total": 1}
with input.login_type as "m.login.password"
with data.session_limit as null
}

View File

@@ -0,0 +1,88 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "CompatLoginInput",
"description": "Input for the compatibility login policy.",
"type": "object",
"required": [
"login_type",
"requester",
"session_counts",
"user"
],
"properties": {
"user": {
"type": "object",
"additionalProperties": true
},
"session_counts": {
"description": "How many sessions the user has.",
"allOf": [
{
"$ref": "#/definitions/SessionCounts"
}
]
},
"login_type": {
"$ref": "#/definitions/CompatLoginType"
},
"requester": {
"$ref": "#/definitions/Requester"
}
},
"definitions": {
"SessionCounts": {
"description": "Information about how many sessions the user has",
"type": "object",
"required": [
"compat",
"oauth2",
"personal",
"total"
],
"properties": {
"total": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"oauth2": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"compat": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"personal": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
}
},
"CompatLoginType": {
"type": "string",
"enum": [
"m.login.sso",
"m.login.password"
]
},
"Requester": {
"description": "Identity of the requester",
"type": "object",
"properties": {
"ip_address": {
"description": "IP address of the entity making the request",
"type": "string",
"format": "ip"
},
"user_agent": {
"description": "User agent of the entity making the request",
"type": "string"
}
}
}
}
}