From aeabc9cbf29e27a0f6fb9be08c26f3729335ce1b Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Fri, 31 Oct 2025 15:13:07 +0000 Subject: [PATCH] Only allow C-S device scopes when the C-S API scope has been requested It'd be weird for a client to request a device on the client-server API but yet not request any client-server API scopes to use it with. By adding this restriction, we can then create a partial index on the oauth2_sessions table to quickly identify sessions that have C-S API scopes and use this as a proxy metric for how many sessions may have device scopes. This in turn makes it feasible to efficiently limit the number of 'devices' a user has, or more precisely: the number of sessions with client-server API access. We can't do the same for device scopes themselves because, other than nastiness like parsing the JSON stringification of the scope list, it's not feasible to identify device scopes within a Postgres index predicate. Part of: #4339 --- .../authorization_grant.rego | 26 +++++++++++++++++++ .../authorization_grant_test.rego | 16 +++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/policies/authorization_grant/authorization_grant.rego b/policies/authorization_grant/authorization_grant.rego index b409cc889..79f737af1 100644 --- a/policies/authorization_grant/authorization_grant.rego +++ b/policies/authorization_grant/authorization_grant.rego @@ -98,6 +98,26 @@ uses_stable_scopes if { count({scope | some scope in scope_list; startswith(scope, "urn:matrix:client:")}) > 0 } +has_device_scope if { + scope_list := split(input.scope, " ") + count({scope | some scope in scope_list; startswith(scope, "urn:matrix:client:device:")}) > 0 +} + +has_device_scope if { + scope_list := split(input.scope, " ") + count({scope | some scope in scope_list; startswith(scope, "urn:matrix:org.matrix.msc2967.client:device:")}) > 0 +} + +has_cs_api_scope if { + scope_list := split(input.scope, " ") + count({scope | some scope in scope_list; startswith(scope, "urn:matrix:client:api:")}) > 0 +} + +has_cs_api_scope if { + scope_list := split(input.scope, " ") + count({scope | some scope in scope_list; startswith(scope, "urn:matrix:org.matrix.msc2967.client:api:")}) > 0 +} + # METADATA # entrypoint: true violation contains {"msg": msg} if { @@ -116,6 +136,12 @@ violation contains {"msg": "only one device scope is allowed at a time"} if { count({scope | some scope in scope_list; startswith(scope, "urn:matrix:client:device:")}) > 1 } +# Prevent the creation of C-S API devices for sessions that don't have C-S API access. +violation contains {"msg": "device scopes are only allowed when the client-server API scope is requested"} if { + has_device_scope + not has_cs_api_scope +} + violation contains {"msg": "request cannot mix unstable and stable scopes"} if { uses_stable_scopes uses_unstable_scopes diff --git a/policies/authorization_grant/authorization_grant_test.rego b/policies/authorization_grant/authorization_grant_test.rego index bbcac5748..6634eacb9 100644 --- a/policies/authorization_grant/authorization_grant_test.rego +++ b/policies/authorization_grant/authorization_grant_test.rego @@ -141,7 +141,21 @@ test_stable_device_scopes if { # Not authorization_grant.allowed for the client credentials grant not authorization_grant.allow with input.client as client with input.grant_type as "client_credentials" - with input.scope as "urn:matrix:client:device:AAbbCCdd01" + with input.scope as "urn:matrix:client:api:* urn:matrix:client:device:AAbbCCdd01" +} + +test_device_scope_only_with_cs_api_scope if { + not authorization_grant.allow with input.user as user + with input.client as client + with input.grant_type as "authorization_code" + # Requested a device scope but no C-S API scope: +with input.scope as "urn:matrix:client:device:AAbbCCdd01" + + not authorization_grant.allow with input.user as user + with input.client as client + with input.grant_type as "authorization_code" + # Requested a device scope but no C-S API scope: +with input.scope as "urn:matrix:org.matrix.msc2967.client:device:AAbbCCdd01" } test_mix_stable_and_unstable_scopes if {