From 52f9a11d7819d702696eb0bde998df6a4e1e6c5b Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 22 Sep 2026 15:36:08 +0200 Subject: [PATCH] Show the reason on the policy violation page for admin scopes and disallowed clients Violations from the `authorization_grant` policy for `urn:synapse:admin:*` and `urn:mas:admin` now carry the `admin-scope-not-allowed` code, and `client-not-allowed` is reserved for policies which reject the client itself (used by the Pro policies). The policy violation pages render a specific reason for each code under a shared headline, instead of the generic explanation. Fixes #5995 --- crates/policy/src/model.rs | 9 ++++ crates/templates/src/context.rs | 52 ++++++++++++++++++- .../authorization_grant.rego | 13 +++++ .../authorization_grant_test.rego | 27 ++++++++++ .../pages/compat_login_policy_violation.html | 20 ++++--- templates/pages/policy_violation.html | 27 ++++++---- translations/en.json | 44 ++++++++++++---- 7 files changed, 163 insertions(+), 29 deletions(-) diff --git a/crates/policy/src/model.rs b/crates/policy/src/model.rs index 83ceb08b9..e408c63c3 100644 --- a/crates/policy/src/model.rs +++ b/crates/policy/src/model.rs @@ -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", } } diff --git a/crates/templates/src/context.rs b/crates/templates/src/context.rs index 6148cc165..937a2b66e 100644 --- a/crates/templates/src/context.rs +++ b/crates/templates/src/context.rs @@ -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 }), + }, + ], + }, ]) } } diff --git a/policies/authorization_grant/authorization_grant.rego b/policies/authorization_grant/authorization_grant.rego index f02fdaac4..a4d1e0708 100644 --- a/policies/authorization_grant/authorization_grant.rego +++ b/policies/authorization_grant/authorization_grant.rego @@ -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 diff --git a/policies/authorization_grant/authorization_grant_test.rego b/policies/authorization_grant/authorization_grant_test.rego index 5186e6c3e..093cc9510 100644 --- a/policies/authorization_grant/authorization_grant_test.rego +++ b/policies/authorization_grant/authorization_grant_test.rego @@ -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 { diff --git a/templates/pages/compat_login_policy_violation.html b/templates/pages/compat_login_policy_violation.html index d6b7372dc..2796475f5 100644 --- a/templates/pages/compat_login_policy_violation.html +++ b/templates/pages/compat_login_policy_violation.html @@ -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 %}
@@ -16,15 +17,20 @@ Please see LICENSE files in the repository root for full details. {{ icon.error_solid() }} - {% if is_too_many_sessions %} + {% if code == "too-many-sessions" %}

{{ _("mas.policy_violation.too_many_sessions.heading") }}

-

{{ _("mas.policy_violation.too_many_sessions.description", num_devices_to_remove=num_sessions_need_to_remove) }}

+

{{ _("mas.policy_violation.too_many_sessions.description", num_devices_to_remove=violation.need_to_remove) }}

{% else %}
-

{{ _("mas.policy_violation.heading") }}

-

{{ _("mas.policy_violation.description") }}

+ {% if code == "client-not-allowed" %} +

{{ _("mas.policy_violation.client_not_allowed.heading") }}

+

{{ _("mas.policy_violation.client_not_allowed.description") }}

+ {% else %} +

{{ _("mas.policy_violation.heading") }}

+

{{ _("mas.policy_violation.description") }}

+ {% endif %}
{% endif %}
@@ -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) }} - {% 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 %} diff --git a/templates/pages/policy_violation.html b/templates/pages/policy_violation.html index d44593c85..b2eff9fd6 100644 --- a/templates/pages/policy_violation.html +++ b/templates/pages/policy_violation.html @@ -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() }} - {% if is_too_many_sessions %} -
+
+ {% if code == "too-many-sessions" %}

{{ _("mas.policy_violation.too_many_sessions.heading") }}

-

{{ _("mas.policy_violation.too_many_sessions.description", num_devices_to_remove=num_sessions_need_to_remove) }}

-
- {% else %} -
+

{{ _("mas.policy_violation.too_many_sessions.description", num_devices_to_remove=violation.need_to_remove) }}

+ {% elif code == "admin-scope-not-allowed" %} +

{{ _("mas.policy_violation.admin_scope_not_allowed.heading") }}

+

{{ _("mas.policy_violation.admin_scope_not_allowed.description") }}

+ {% elif code == "client-not-allowed" %} +

{{ _("mas.policy_violation.client_not_allowed.heading") }}

+

{{ _("mas.policy_violation.client_not_allowed.description") }}

+ {% else %}

{{ _("mas.policy_violation.heading") }}

{{ _("mas.policy_violation.description") }}

-
- {% endif %} + {% endif %} +
@@ -60,7 +65,7 @@ Please see LICENSE files in the repository root for full details.
- {% 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 %} diff --git a/translations/en.json b/translations/en.json index b68e4e84d..4edbea8b0 100644 --- a/translations/en.json +++ b/translations/en.json @@ -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 %(username)s", "@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, remove %(num_devices_to_remove)s of your existing devices 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" } } },