mirror of
https://github.com/element-hq/matrix-authentication-service.git
synced 2026-10-23 10:53:37 +00:00
Show the reason on the policy violation page for admin scopes and disallowed clients (#5996)
This commit is contained in:
7 files changed
+163
-29
No files matched your search
@@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user