Show the reason on the policy violation page for admin scopes and disallowed clients (#5996)

This commit is contained in:
Quentin Gliech authored and GitHub committed 2026-09-22 17:34:22 +02:00
commit 56a0c2bb63
7 files changed
+163 -29

No files matched your search

+9
View File
@@ -1,3 +1,4 @@
// Copyright 2025, 2026 Element Creations Ltd.
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
//
@@ -51,6 +52,12 @@ pub enum ViolationVariant {
/// The email address is banned.
EmailBanned,
/// An admin scope was requested but is not allowed.
AdminScopeNotAllowed,
/// The client is not allowed by the policy.
ClientNotAllowed,
/// The user has reached their session limit.
TooManySessions {
/// How many devices need to be removed to make room for the new session
@@ -73,6 +80,8 @@ impl ViolationVariant {
Self::EmailDomainBanned => "email-domain-banned",
Self::EmailNotAllowed => "email-not-allowed",
Self::EmailBanned => "email-banned",
Self::AdminScopeNotAllowed => "admin-scope-not-allowed",
Self::ClientNotAllowed => "client-not-allowed",
Self::TooManySessions { .. } => "too-many-sessions",
}
}
+51 -1
View File
@@ -842,7 +842,7 @@ impl TemplateContext for PolicyViolationContext {
let authorization_grant_invalid_scope =
PolicyViolationContext::for_authorization_grant(
grant,
grant.clone(),
client.clone(),
vec![Violation {
msg: "scope 'foo' not allowed".to_owned(),
@@ -851,6 +851,30 @@ impl TemplateContext for PolicyViolationContext {
variant: None,
}],
);
let authorization_grant_admin_scope =
PolicyViolationContext::for_authorization_grant(
grant.clone(),
client.clone(),
vec![Violation {
msg: "scope 'urn:mas:admin' requires admin privileges".to_owned(),
redirect_uri: None,
field: None,
variant: Some(ViolationVariant::AdminScopeNotAllowed),
}],
);
let authorization_grant_client_not_allowed =
PolicyViolationContext::for_authorization_grant(
grant,
client.clone(),
vec![Violation {
msg: "This client is not allowed to login".to_owned(),
redirect_uri: None,
field: None,
variant: Some(ViolationVariant::ClientNotAllowed),
}],
);
let device_code_grant = PolicyViolationContext::for_device_code_grant(
DeviceCodeGrant {
id: Ulid::from_datetime_with_rng(now, rng),
@@ -898,6 +922,8 @@ impl TemplateContext for PolicyViolationContext {
[
authorization_grant,
authorization_grant_invalid_scope,
authorization_grant_admin_scope,
authorization_grant_client_not_allowed,
device_code_grant,
device_code_grant_invalid_scope,
]
@@ -976,6 +1002,30 @@ impl TemplateContext for CompatLoginPolicyViolationContext {
variant: Some(ViolationVariant::TooManySessions { need_to_remove: 1 }),
}],
},
CompatLoginPolicyViolationContext {
violations: vec![Violation {
msg: "This client is not allowed to login".to_owned(),
redirect_uri: None,
field: None,
variant: Some(ViolationVariant::ClientNotAllowed),
}],
},
CompatLoginPolicyViolationContext {
violations: vec![
Violation {
msg: "scope 'foo' not allowed".to_owned(),
redirect_uri: None,
field: None,
variant: None,
},
Violation {
msg: "user has too many active sessions".to_owned(),
redirect_uri: None,
field: None,
variant: Some(ViolationVariant::TooManySessions { need_to_remove: 1 }),
},
],
},
])
}
}
@@ -1,3 +1,4 @@
# Copyright 2025, 2026 Element Creations Ltd.
# Copyright 2025 New Vector Ltd.
#
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
@@ -88,6 +89,10 @@ allowed_scope("urn:matrix:org.matrix.msc2967.client:api:*") if {
interactive_grant_type(input.grant_type)
}
admin_scope("urn:synapse:admin:*") := true
admin_scope("urn:mas:admin") := true
uses_unstable_scopes if {
scope_list := split(input.scope, " ")
count({scope | some scope in scope_list; startswith(scope, "urn:matrix:org.matrix.msc2967.client:")}) > 0
@@ -122,10 +127,18 @@ has_cs_api_scope if {
# entrypoint: true
violation contains {"msg": msg} if {
some scope in split(input.scope, " ")
not admin_scope(scope)
not allowed_scope(scope)
msg := sprintf("scope '%s' not allowed", [scope])
}
violation contains {"code": "admin-scope-not-allowed", "msg": msg} if {
some scope in split(input.scope, " ")
admin_scope(scope)
not allowed_scope(scope)
msg := sprintf("scope '%s' requires admin privileges", [scope])
}
violation contains {"msg": "only one device scope is allowed at a time"} if {
scope_list := split(input.scope, " ")
count({scope | some scope in scope_list; startswith(scope, "urn:matrix:org.matrix.msc2967.client:device:")}) > 1
@@ -1,3 +1,4 @@
# Copyright 2025, 2026 Element Creations Ltd.
# Copyright 2025 New Vector Ltd.
#
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
@@ -223,6 +224,32 @@ test_mas_scopes if {
with input.scope as "urn:mas:admin"
}
test_synapse_admin_scope_violation if {
violations := authorization_grant.violation with input.user as user
with input.client as client
with data.admin_users as []
with input.grant_type as "authorization_code"
with input.scope as "urn:synapse:admin:*"
violations == {{
"code": "admin-scope-not-allowed",
"msg": "scope 'urn:synapse:admin:*' requires admin privileges",
}}
}
test_mas_admin_scope_violation if {
violations := authorization_grant.violation with input.user as user
with input.client as client
with data.admin_users as []
with input.grant_type as "authorization_code"
with input.scope as "urn:mas:admin"
violations == {{
"code": "admin-scope-not-allowed",
"msg": "scope 'urn:mas:admin' requires admin privileges",
}}
}
# 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 {
@@ -1,4 +1,5 @@
{#
Copyright 2025, 2026 Element Creations Ltd.
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
@@ -7,8 +8,8 @@ Please see LICENSE files in the repository root for full details.
{% extends "base.html" %}
{% set is_too_many_sessions = violations | length == 1 and violations[0].code | default("") == "too-many-sessions" %}
{% set num_sessions_need_to_remove = violations[0].need_to_remove if is_too_many_sessions else none %}
{% set violation = violations | selectattr("code", "defined") | first | default({}) %}
{% set code = violation.code | default("") %}
{% block content %}
<header class="page-heading">
@@ -16,15 +17,20 @@ Please see LICENSE files in the repository root for full details.
{{ icon.error_solid() }}
</div>
{% if is_too_many_sessions %}
{% if code == "too-many-sessions" %}
<div class="header" data-testid="device-limit-reached">
<h1 class="title">{{ _("mas.policy_violation.too_many_sessions.heading") }}</h1>
<p class="text">{{ _("mas.policy_violation.too_many_sessions.description", num_devices_to_remove=num_sessions_need_to_remove) }}</p>
<p class="text">{{ _("mas.policy_violation.too_many_sessions.description", num_devices_to_remove=violation.need_to_remove) }}</p>
</div>
{% else %}
<div class="header" data-testid="generic-policy-violation">
<h1 class="title">{{ _("mas.policy_violation.heading") }}</h1>
<p class="text">{{ _("mas.policy_violation.description") }}</p>
{% if code == "client-not-allowed" %}
<h1 class="title">{{ _("mas.policy_violation.client_not_allowed.heading") }}</h1>
<p class="text">{{ _("mas.policy_violation.client_not_allowed.description") }}</p>
{% else %}
<h1 class="title">{{ _("mas.policy_violation.heading") }}</h1>
<p class="text">{{ _("mas.policy_violation.description") }}</p>
{% endif %}
</div>
{% endif %}
</header>
@@ -38,7 +44,7 @@ Please see LICENSE files in the repository root for full details.
{{ logout.button(text=_("action.sign_out"), csrf_token=csrf_token, post_logout_action=action, as_link=True) }}
</div>
{% if is_too_many_sessions %}
{% if code == "too-many-sessions" %}
{{ button.link(text=_("mas.policy_violation.too_many_sessions.manage_devices"), href="/account/sessions") }}
{% endif %}
</main>
+16 -11
View File
@@ -1,4 +1,5 @@
{#
Copyright 2025, 2026 Element Creations Ltd.
Copyright 2024, 2025 New Vector Ltd.
Copyright 2022-2024 The Matrix.org Foundation C.I.C.
@@ -8,8 +9,8 @@ Please see LICENSE files in the repository root for full details.
{% extends "base.html" %}
{% set is_too_many_sessions = violations | length == 1 and violations[0].code | default("") == "too-many-sessions" %}
{% set num_sessions_need_to_remove = violations[0].need_to_remove if is_too_many_sessions else none %}
{% set violation = violations | selectattr("code", "defined") | first | default({}) %}
{% set code = violation.code | default("") %}
{% block content %}
{% set client_display_name = client.client_name or client.client_id %}
@@ -19,17 +20,21 @@ Please see LICENSE files in the repository root for full details.
{{ icon.error_solid() }}
</div>
{% if is_too_many_sessions %}
<div class="header">
<div class="header">
{% if code == "too-many-sessions" %}
<h1 class="title">{{ _("mas.policy_violation.too_many_sessions.heading") }}</h1>
<p class="text">{{ _("mas.policy_violation.too_many_sessions.description", num_devices_to_remove=num_sessions_need_to_remove) }}</p>
</div>
{% else %}
<div class="header">
<p class="text">{{ _("mas.policy_violation.too_many_sessions.description", num_devices_to_remove=violation.need_to_remove) }}</p>
{% elif code == "admin-scope-not-allowed" %}
<h1 class="title">{{ _("mas.policy_violation.admin_scope_not_allowed.heading") }}</h1>
<p class="text">{{ _("mas.policy_violation.admin_scope_not_allowed.description") }}</p>
{% elif code == "client-not-allowed" %}
<h1 class="title">{{ _("mas.policy_violation.client_not_allowed.heading") }}</h1>
<p class="text">{{ _("mas.policy_violation.client_not_allowed.description") }}</p>
{% else %}
<h1 class="title">{{ _("mas.policy_violation.heading") }}</h1>
<p class="text">{{ _("mas.policy_violation.description") }}</p>
</div>
{% endif %}
{% endif %}
</div>
</header>
<main class="flex flex-col gap-10">
@@ -60,7 +65,7 @@ Please see LICENSE files in the repository root for full details.
</div>
<div class="flex flex-col gap-6">
{% if is_too_many_sessions %}
{% if code == "too-many-sessions" %}
{{ button.link(text=_("mas.policy_violation.too_many_sessions.manage_devices"), href="/account/sessions") }}
{% endif %}
+34 -10
View File
@@ -6,7 +6,7 @@
},
"cancel": "Cancel",
"@cancel": {
"context": "pages/consent.html:81:11-29, pages/device_consent.html:179:13-31, pages/device_link.html:57:33-51, pages/policy_violation.html:70:15-33, pages/reauth.html:37:13-31"
"context": "pages/consent.html:81:11-29, pages/device_consent.html:179:13-31, pages/device_link.html:57:33-51, pages/policy_violation.html:75:15-33, pages/reauth.html:37:13-31"
},
"continue": "Continue",
"@continue": {
@@ -22,7 +22,7 @@
},
"sign_out": "Sign out",
"@sign_out": {
"context": "pages/account/logged_out.html:23:28-48, pages/compat_login_policy_violation.html:38:28-48, pages/index.html:28:28-48, pages/policy_violation.html:59:28-48, pages/upstream_oauth2/link_mismatch.html:24:24-44, pages/upstream_oauth2/suggest_link.html:32:26-46"
"context": "pages/account/logged_out.html:23:28-48, pages/compat_login_policy_violation.html:44:28-48, pages/index.html:28:28-48, pages/policy_violation.html:64:28-48, pages/upstream_oauth2/link_mismatch.html:24:24-44, pages/upstream_oauth2/suggest_link.html:32:26-46"
},
"skip": "Skip",
"@skip": {
@@ -531,32 +531,56 @@
"description": "Separator between the login methods"
},
"policy_violation": {
"description": "This might be because of the client which authored the request, the currently logged in user, or the request itself.",
"admin_scope_not_allowed": {
"description": "This application requires administrator privileges. Sign in with an administrator account to continue.",
"@description": {
"context": "pages/policy_violation.html:29:27-88",
"description": "Displayed when the client requested an admin scope and the user is not an administrator"
},
"heading": "Administrator access required",
"@heading": {
"context": "pages/policy_violation.html:28:29-86",
"description": "Displayed when the client requested an admin scope and the user is not an administrator"
}
},
"client_not_allowed": {
"description": "This application is not allowed to sign in to this service.",
"@description": {
"context": "pages/compat_login_policy_violation.html:29:29-85, pages/policy_violation.html:32:27-83",
"description": "Displayed when the policy does not allow the client"
},
"heading": "Application not allowed",
"@heading": {
"context": "pages/compat_login_policy_violation.html:28:31-83, pages/policy_violation.html:31:29-81",
"description": "Displayed when the policy does not allow the client"
}
},
"description": "Your request has been denied by the policy of this service. This might be because of the application, your account, or the request itself.",
"@description": {
"context": "pages/compat_login_policy_violation.html:27:27-64, pages/policy_violation.html:30:27-64",
"context": "pages/compat_login_policy_violation.html:32:29-66, pages/policy_violation.html:35:27-64",
"description": "Displayed when an authorization request is denied by the policy"
},
"heading": "The authorization request was denied by the policy enforced by this service",
"heading": "Request denied",
"@heading": {
"context": "pages/compat_login_policy_violation.html:26:29-62, pages/policy_violation.html:29:29-62",
"context": "pages/compat_login_policy_violation.html:31:31-64, pages/policy_violation.html:34:29-62",
"description": "Displayed when an authorization request is denied by the policy"
},
"logged_as": "Logged as <span class=\"font-semibold\">%(username)s</span>",
"@logged_as": {
"context": "pages/compat_login_policy_violation.html:35:11-86, pages/policy_violation.html:56:11-86"
"context": "pages/compat_login_policy_violation.html:41:11-86, pages/policy_violation.html:61:11-86"
},
"too_many_sessions": {
"description": "Your account is already signed in on the maximum number of devices. To continue, <span class=\"font-semibold\">remove %(num_devices_to_remove)s of your existing devices</span> and sign in again.",
"@description": {
"context": "pages/compat_login_policy_violation.html:22:27-133, pages/policy_violation.html:25:27-133"
"context": "pages/compat_login_policy_violation.html:23:27-130, pages/policy_violation.html:26:27-130"
},
"heading": "Device limit reached",
"@heading": {
"context": "pages/compat_login_policy_violation.html:21:29-80, pages/policy_violation.html:24:29-80"
"context": "pages/compat_login_policy_violation.html:22:29-80, pages/policy_violation.html:25:29-80"
},
"manage_devices": "Manage devices",
"@manage_devices": {
"context": "pages/compat_login_policy_violation.html:42:26-84, pages/policy_violation.html:64:28-86"
"context": "pages/compat_login_policy_violation.html:48:26-84, pages/policy_violation.html:69:28-86"
}
}
},