diff --git a/crates/data-model/src/compat/device.rs b/crates/data-model/src/compat/device.rs index ca34ff2ac..fbe0d147a 100644 --- a/crates/data-model/src/compat/device.rs +++ b/crates/data-model/src/compat/device.rs @@ -13,7 +13,8 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; static GENERATED_DEVICE_ID_LENGTH: usize = 10; -static DEVICE_SCOPE_PREFIX: &str = "urn:matrix:org.matrix.msc2967.client:device:"; +static UNSTABLE_DEVICE_SCOPE_PREFIX: &str = "urn:matrix:org.matrix.msc2967.client:device:"; +static STABLE_DEVICE_SCOPE_PREFIX: &str = "urn:matrix:client:device:"; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(transparent)] @@ -28,16 +29,21 @@ pub enum ToScopeTokenError { } impl Device { - /// Get the corresponding [`ScopeToken`] for that device + /// Get the corresponding stable and unstable [`ScopeToken`] for that device /// /// # Errors /// /// Returns an error if the device ID contains characters that can't be /// encoded in a scope - pub fn to_scope_token(&self) -> Result { - format!("{DEVICE_SCOPE_PREFIX}{}", self.id) - .parse() - .map_err(|_| ToScopeTokenError::InvalidCharacters) + pub fn to_scope_token(&self) -> Result<[ScopeToken; 2], ToScopeTokenError> { + Ok([ + format!("{STABLE_DEVICE_SCOPE_PREFIX}{}", self.id) + .parse() + .map_err(|_| ToScopeTokenError::InvalidCharacters)?, + format!("{UNSTABLE_DEVICE_SCOPE_PREFIX}{}", self.id) + .parse() + .map_err(|_| ToScopeTokenError::InvalidCharacters)?, + ]) } /// Get the corresponding [`Device`] from a [`ScopeToken`] @@ -45,7 +51,9 @@ impl Device { /// Returns `None` if the [`ScopeToken`] is not a device scope #[must_use] pub fn from_scope_token(token: &ScopeToken) -> Option { - let id = token.as_str().strip_prefix(DEVICE_SCOPE_PREFIX)?; + let stable = token.as_str().strip_prefix(STABLE_DEVICE_SCOPE_PREFIX); + let unstable = token.as_str().strip_prefix(UNSTABLE_DEVICE_SCOPE_PREFIX); + let id = stable.or(unstable)?; Some(Device::from(id.to_owned())) } @@ -89,12 +97,23 @@ mod test { #[test] fn test_device_id_to_from_scope_token() { let device = Device::from("AABBCCDDEE".to_owned()); - let scope_token = device.to_scope_token().unwrap(); + let [stable_scope_token, unstable_scope_token] = device.to_scope_token().unwrap(); assert_eq!( - scope_token.as_str(), + stable_scope_token.as_str(), + "urn:matrix:client:device:AABBCCDDEE" + ); + assert_eq!( + unstable_scope_token.as_str(), "urn:matrix:org.matrix.msc2967.client:device:AABBCCDDEE" ); - assert_eq!(Device::from_scope_token(&scope_token), Some(device)); + assert_eq!( + Device::from_scope_token(&unstable_scope_token).as_ref(), + Some(&device) + ); + assert_eq!( + Device::from_scope_token(&stable_scope_token).as_ref(), + Some(&device) + ); assert_eq!(Device::from_scope_token(&OPENID), None); } } diff --git a/crates/handlers/src/admin/model.rs b/crates/handlers/src/admin/model.rs index 03fd72ad0..ceb19692a 100644 --- a/crates/handlers/src/admin/model.rs +++ b/crates/handlers/src/admin/model.rs @@ -375,7 +375,7 @@ impl OAuth2Session { user_id: Some(Ulid::from_bytes([0x04; 16])), user_session_id: Some(Ulid::from_bytes([0x05; 16])), client_id: Ulid::from_bytes([0x06; 16]), - scope: "urn:matrix:org.matrix.msc2967.client:api:*".to_owned(), + scope: "urn:matrix:client:api:*".to_owned(), user_agent: Some("Mozilla/5.0".to_owned()), last_active_at: Some(DateTime::default()), last_active_ip: Some("127.0.0.1".parse().unwrap()), diff --git a/crates/handlers/src/graphql/query/session.rs b/crates/handlers/src/graphql/query/session.rs index 1115bed00..24368e5f7 100644 --- a/crates/handlers/src/graphql/query/session.rs +++ b/crates/handlers/src/graphql/query/session.rs @@ -11,7 +11,6 @@ use mas_storage::{ compat::{CompatSessionFilter, CompatSessionRepository}, oauth2::OAuth2SessionFilter, }; -use oauth2_types::scope::Scope; use crate::graphql::{ UserId, @@ -77,20 +76,11 @@ impl SessionQuery { )))); } - // Then, try to find an OAuth 2.0 session. Because we don't have any dedicated - // device column, we're looking up using the device scope. - // All device IDs can't necessarily be encoded as a scope. If it's not the case, - // we'll skip looking for OAuth 2.0 sessions. - let Ok(scope_token) = device.to_scope_token() else { - repo.cancel().await?; - - return Ok(None); - }; - let scope = Scope::from_iter([scope_token]); + // Then, try to find an OAuth 2.0 session. let filter = OAuth2SessionFilter::new() .for_user(&user) .active_only() - .with_scope(&scope); + .for_device(&device); let sessions = repo.oauth2_session().list(filter, pagination).await?; // It's possible to have multiple active OAuth 2.0 sessions. For now, we just diff --git a/crates/handlers/src/oauth2/introspection.rs b/crates/handlers/src/oauth2/introspection.rs index 3ae4a3a1c..5c1e00a36 100644 --- a/crates/handlers/src/oauth2/introspection.rs +++ b/crates/handlers/src/oauth2/introspection.rs @@ -4,7 +4,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. -use std::sync::LazyLock; +use std::{collections::BTreeSet, sync::LazyLock}; use axum::{Json, extract::State, http::HeaderValue, response::IntoResponse}; use hyper::{HeaderMap, StatusCode}; @@ -24,7 +24,7 @@ use mas_storage::{ use oauth2_types::{ errors::{ClientError, ClientErrorCode}, requests::{IntrospectionRequest, IntrospectionResponse}, - scope::ScopeToken, + scope::{Scope, ScopeToken}, }; use opentelemetry::{Key, KeyValue, metrics::Counter}; use thiserror::Error; @@ -190,9 +190,33 @@ const INACTIVE: IntrospectionResponse = IntrospectionResponse { device_id: None, }; -const API_SCOPE: ScopeToken = ScopeToken::from_static("urn:matrix:org.matrix.msc2967.client:api:*"); +const UNSTABLE_API_SCOPE: ScopeToken = + ScopeToken::from_static("urn:matrix:org.matrix.msc2967.client:api:*"); +const STABLE_API_SCOPE: ScopeToken = ScopeToken::from_static("urn:matrix:client:api:*"); const SYNAPSE_ADMIN_SCOPE: ScopeToken = ScopeToken::from_static("urn:synapse:admin:*"); +/// Normalize a scope by adding the stable and unstable API scopes equivalents +/// if missing +fn normalize_scope(mut scope: Scope) -> Scope { + // Here we abuse the fact that the scope is a BTreeSet to not care about + // duplicates + let mut to_add = BTreeSet::new(); + for token in &*scope { + if token == &STABLE_API_SCOPE { + to_add.insert(UNSTABLE_API_SCOPE); + } else if token == &UNSTABLE_API_SCOPE { + to_add.insert(STABLE_API_SCOPE); + } else if let Some(device) = Device::from_scope_token(token) { + let tokens = device + .to_scope_token() + .expect("from/to scope token rountrip should never fail"); + to_add.extend(tokens); + } + } + scope.append(&mut to_add); + scope +} + #[tracing::instrument( name = "handlers.oauth2.introspection.post", fields(client.id = client_authorization.client_id()), @@ -311,9 +335,11 @@ pub(crate) async fn post( ], ); + let scope = normalize_scope(session.scope); + IntrospectionResponse { active: true, - scope: Some(session.scope), + scope: Some(scope), client_id: Some(session.client_id.to_string()), username, token_type: Some(OAuthTokenTypeHint::AccessToken), @@ -382,9 +408,11 @@ pub(crate) async fn post( ], ); + let scope = normalize_scope(session.scope); + IntrospectionResponse { active: true, - scope: Some(session.scope), + scope: Some(scope), client_id: Some(session.client_id.to_string()), username, token_type: Some(OAuthTokenTypeHint::RefreshToken), @@ -446,9 +474,9 @@ pub(crate) async fn post( .transpose()? }; - let scope = [API_SCOPE] + let scope = [STABLE_API_SCOPE, UNSTABLE_API_SCOPE] .into_iter() - .chain(device_scope_opt) + .chain(device_scope_opt.into_iter().flatten()) .chain(synapse_admin_scope_opt) .collect(); @@ -530,9 +558,9 @@ pub(crate) async fn post( .transpose()? }; - let scope = [API_SCOPE] + let scope = [STABLE_API_SCOPE, UNSTABLE_API_SCOPE] .into_iter() - .chain(device_scope_opt) + .chain(device_scope_opt.into_iter().flatten()) .chain(synapse_admin_scope_opt) .collect(); @@ -879,7 +907,7 @@ mod tests { let refresh_token = response["refresh_token"].as_str().unwrap(); let device_id = response["device_id"].as_str().unwrap(); let expected_scope: Scope = - format!("urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:{device_id}") + format!("urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:{device_id} urn:matrix:client:api:* urn:matrix:client:device:{device_id}") .parse() .unwrap(); @@ -912,7 +940,7 @@ mod tests { assert_eq!(response.token_type, Some(OAuthTokenTypeHint::AccessToken)); assert_eq!( response.scope.map(|s| s.to_string()), - Some("urn:matrix:org.matrix.msc2967.client:api:*".to_owned()) + Some("urn:matrix:client:api:* urn:matrix:org.matrix.msc2967.client:api:*".to_owned()) ); assert_eq!(response.device_id.as_deref(), Some(device_id)); diff --git a/crates/oauth2-types/src/scope.rs b/crates/oauth2-types/src/scope.rs index a13719e54..c758a8032 100644 --- a/crates/oauth2-types/src/scope.rs +++ b/crates/oauth2-types/src/scope.rs @@ -10,7 +10,13 @@ #![allow(clippy::module_name_repetitions)] -use std::{borrow::Cow, collections::BTreeSet, iter::FromIterator, ops::Deref, str::FromStr}; +use std::{ + borrow::Cow, + collections::BTreeSet, + iter::FromIterator, + ops::{Deref, DerefMut}, + str::FromStr, +}; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -121,6 +127,12 @@ impl Deref for Scope { } } +impl DerefMut for Scope { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + impl FromStr for Scope { type Err = InvalidScope; @@ -248,6 +260,7 @@ mod tests { ); assert!(Scope::from_str("http://example.com").is_ok()); - assert!(Scope::from_str("urn:matrix:org.matrix.msc2967.client:*").is_ok()); + assert!(Scope::from_str("urn:matrix:client:api:*").is_ok()); + assert!(Scope::from_str("urn:matrix:org.matrix.msc2967.client:api:*").is_ok()); } } diff --git a/crates/storage-pg/.sqlx/query-373f7eb215b0e515b000a37e55bd055954f697f257de026b74ec408938a52a1a.json b/crates/storage-pg/.sqlx/query-373f7eb215b0e515b000a37e55bd055954f697f257de026b74ec408938a52a1a.json deleted file mode 100644 index 9ebd78f6f..000000000 --- a/crates/storage-pg/.sqlx/query-373f7eb215b0e515b000a37e55bd055954f697f257de026b74ec408938a52a1a.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE oauth2_sessions SET finished_at = $3 WHERE user_id = $1 AND $2 = ANY(scope_list) AND finished_at IS NULL\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid", - "Text", - "Timestamptz" - ] - }, - "nullable": [] - }, - "hash": "373f7eb215b0e515b000a37e55bd055954f697f257de026b74ec408938a52a1a" -} diff --git a/crates/storage-pg/.sqlx/query-5da7a197e0008f100ad4daa78f4aa6515f0fc9eb54075e8d6d15520d25b75172.json b/crates/storage-pg/.sqlx/query-5da7a197e0008f100ad4daa78f4aa6515f0fc9eb54075e8d6d15520d25b75172.json new file mode 100644 index 000000000..56298e4da --- /dev/null +++ b/crates/storage-pg/.sqlx/query-5da7a197e0008f100ad4daa78f4aa6515f0fc9eb54075e8d6d15520d25b75172.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE oauth2_sessions\n SET finished_at = $4\n WHERE user_id = $1\n AND ($2 = ANY(scope_list) OR $3 = ANY(scope_list))\n AND finished_at IS NULL\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "5da7a197e0008f100ad4daa78f4aa6515f0fc9eb54075e8d6d15520d25b75172" +} diff --git a/crates/storage-pg/src/app_session.rs b/crates/storage-pg/src/app_session.rs index cd5e40b53..4424406f2 100644 --- a/crates/storage-pg/src/app_session.rs +++ b/crates/storage-pg/src/app_session.rs @@ -499,17 +499,24 @@ impl AppSessionRepository for PgAppSessionRepository<'_> { .instrument(span) .await?; - if let Ok(device_as_scope_token) = device.to_scope_token() { + if let Ok([stable_device_as_scope_token, unstable_device_as_scope_token]) = + device.to_scope_token() + { let span = tracing::info_span!( "db.app_session.finish_sessions_to_replace_device.oauth2_sessions", { DB_QUERY_TEXT } = tracing::field::Empty, ); sqlx::query!( " - UPDATE oauth2_sessions SET finished_at = $3 WHERE user_id = $1 AND $2 = ANY(scope_list) AND finished_at IS NULL + UPDATE oauth2_sessions + SET finished_at = $4 + WHERE user_id = $1 + AND ($2 = ANY(scope_list) OR $3 = ANY(scope_list)) + AND finished_at IS NULL ", Uuid::from(user.id), - device_as_scope_token.as_str(), + stable_device_as_scope_token.as_str(), + unstable_device_as_scope_token.as_str(), finished_at ) .record(&span) @@ -652,7 +659,10 @@ mod tests { .unwrap(); let device2 = Device::generate(&mut rng); - let scope = Scope::from_iter([OPENID, device2.to_scope_token().unwrap()]); + let scope: Scope = [OPENID] + .into_iter() + .chain(device2.to_scope_token().unwrap().into_iter()) + .collect(); // We're moving the clock forward by 1 minute between each session to ensure // we're getting consistent ordering in lists. diff --git a/crates/storage-pg/src/oauth2/session.rs b/crates/storage-pg/src/oauth2/session.rs index d2fbd8130..50cfa03ce 100644 --- a/crates/storage-pg/src/oauth2/session.rs +++ b/crates/storage-pg/src/oauth2/session.rs @@ -15,7 +15,10 @@ use mas_storage::{ }; use oauth2_types::scope::{Scope, ScopeToken}; use rand::RngCore; -use sea_query::{Expr, PgFunc, PostgresQueryBuilder, Query, enum_def, extension::postgres::PgExpr}; +use sea_query::{ + Condition, Expr, PgFunc, PostgresQueryBuilder, Query, SimpleExpr, enum_def, + extension::postgres::PgExpr, +}; use sea_query_binder::SqlxBinder; use sqlx::PgConnection; use ulid::Ulid; @@ -126,12 +129,19 @@ impl Filter for OAuth2SessionFilter<'_> { .ne(Expr::all(static_clients)) } })) - .add_option(self.device().map(|device| { - if let Ok(scope_token) = device.to_scope_token() { - Expr::val(scope_token.to_string()).eq(PgFunc::any(Expr::col(( - OAuth2Sessions::Table, - OAuth2Sessions::ScopeList, - )))) + .add_option(self.device().map(|device| -> SimpleExpr { + if let Ok([stable_scope_token, unstable_scope_token]) = device.to_scope_token() { + Condition::any() + .add( + Expr::val(stable_scope_token.to_string()).eq(PgFunc::any(Expr::col(( + OAuth2Sessions::Table, + OAuth2Sessions::ScopeList, + )))), + ) + .add(Expr::val(unstable_scope_token.to_string()).eq(PgFunc::any( + Expr::col((OAuth2Sessions::Table, OAuth2Sessions::ScopeList)), + ))) + .into() } else { // If the device ID can't be encoded as a scope token, match no rows Expr::val(false).into() diff --git a/docs/api/spec.json b/docs/api/spec.json index 0082ea37c..49dd7b0dd 100644 --- a/docs/api/spec.json +++ b/docs/api/spec.json @@ -469,7 +469,7 @@ "user_id": "040G2081040G2081040G208104", "user_session_id": "050M2GA1850M2GA1850M2GA185", "client_id": "060R30C1G60R30C1G60R30C1G6", - "scope": "urn:matrix:org.matrix.msc2967.client:api:*", + "scope": "urn:matrix:client:api:*", "user_agent": "Mozilla/5.0", "last_active_at": "1970-01-01T00:00:00Z", "last_active_ip": "127.0.0.1", diff --git a/docs/reference/scopes.md b/docs/reference/scopes.md index 78a261c91..fcb76d352 100644 --- a/docs/reference/scopes.md +++ b/docs/reference/scopes.md @@ -4,9 +4,8 @@ The [default policy](../topics/policy.md#authorization-requests) shipped with MA - [`openid`](#openid) - [`email`](#email) - - [`urn:matrix:org.matrix.msc2967.client:api:*`](#urnmatrixorgmatrixmsc2967clientapi) - - [`urn:matrix:org.matrix.msc2967.client:device:[device id]`](#urnmatrixorgmatrixmsc2967clientdevicedevice-id) - - [`urn:matrix:org.matrix.msc2967.client:guest`](#urnmatrixorgmatrixmsc2967clientguest) + - [`urn:matrix:client:api:*`](#urnmatrixclientapi) + - [`urn:matrix:client:device:[device id]`](#urnmatrixclientdevicedevice-id) - [`urn:synapse:admin:*`](#urnsynapseadmin) - [`urn:mas:admin`](#urnmasadmin) - [`urn:mas:graphql:*`](#urnmasgraphql) @@ -33,13 +32,13 @@ The default policy allows any client and any user to request this scope. Those scopes are specific to the Matrix protocol and are part of [MSC2967]. -### `urn:matrix:org.matrix.msc2967.client:api:*` +### `urn:matrix:client:api:*` This scope grants access to the full Matrix client-server API. The default policy allows any client and any user to request this scope. -### `urn:matrix:org.matrix.msc2967.client:device:[device id]` +### `urn:matrix:client:device:[device id]` This scope sets the device ID of the session, where `[device id]` is the device ID of the session. Currently, MAS only allows the following characters in the device ID: `a-z`, `A-Z`, `0-9` and `-`. @@ -49,15 +48,6 @@ There can only be one device ID in the scope list of a session. The default policy allows any client and any user to request this scope. -### `urn:matrix:org.matrix.msc2967.client:guest` - -This scope grants access to a restricted set of endpoints that are available to guest users. -It is mutually exclusive with the `urn:matrix:org.matrix.msc2967.client:api:*` scope. - -Note that MAS doesn't yet implement any special semantic around guest users, but this scope is reserved for future use. - -The default policy allows any client and any user to request this scope. - ## Synapse-specific scopes MAS also supports one Synapse-specific scope, which aren't formally defined in any specification. @@ -67,7 +57,7 @@ MAS also supports one Synapse-specific scope, which aren't formally defined in a This scope grants access to the [Synapse admin API]. Because of how Synapse works for now, this scope by itself isn't sufficient to access the admin API. -A session wanting to access the admin API also needs to have the `urn:matrix:org.matrix.msc2967.client:api:*` scope. +A session wanting to access the admin API also needs to have the `urn:matrix:client:api:*` scope. The default policy doesn't allow everyone to request this scope. It allows: diff --git a/frontend/src/utils/deviceIdFromScope.test.ts b/frontend/src/utils/deviceIdFromScope.test.ts index 5992cc91a..5c44dd715 100644 --- a/frontend/src/utils/deviceIdFromScope.test.ts +++ b/frontend/src/utils/deviceIdFromScope.test.ts @@ -10,12 +10,18 @@ import { describe, expect, it } from "vitest"; import { getDeviceIdFromScope } from "./deviceIdFromScope"; describe("getDeviceIdFromScope()", () => { - it("returns deviceid when device is part of scope", () => { + it("returns deviceid when device is part of scope (unstable)", () => { const scope = "openid urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:abcd1234"; expect(getDeviceIdFromScope(scope)).toEqual("abcd1234"); }); + it("returns deviceid when device is part of scope (stable)", () => { + const scope = + "openid urn:matrix:client:api:* urn:matrix:client:device:abcd1234"; + expect(getDeviceIdFromScope(scope)).toEqual("abcd1234"); + }); + it("returns undefined when device not part of scope", () => { const scope = "openid some:other:scope "; expect(getDeviceIdFromScope(scope)).toBeUndefined(); diff --git a/frontend/src/utils/deviceIdFromScope.ts b/frontend/src/utils/deviceIdFromScope.ts index ac79171d6..a6ebcf5b9 100644 --- a/frontend/src/utils/deviceIdFromScope.ts +++ b/frontend/src/utils/deviceIdFromScope.ts @@ -5,7 +5,8 @@ * Please see LICENSE in the repository root for full details. */ -const DEVICE_PREFIX = "urn:matrix:org.matrix.msc2967.client:device:"; +const UNSTABLE_DEVICE_PREFIX = "urn:matrix:org.matrix.msc2967.client:device:"; +const STABLE_DEVICE_PREFIX = "urn:matrix:client:device:"; /** * Device scopes are suffixed with the deviceId @@ -14,6 +15,7 @@ const DEVICE_PREFIX = "urn:matrix:org.matrix.msc2967.client:device:"; * @returns deviceId, or undefined when not a device scope */ export const getDeviceIdFromScope = (scope: string): string | undefined => { - const [, deviceId] = scope.split(DEVICE_PREFIX); - return deviceId; + const [, stableDeviceId] = scope.split(STABLE_DEVICE_PREFIX); + const [, unstableDeviceId] = scope.split(UNSTABLE_DEVICE_PREFIX); + return stableDeviceId || unstableDeviceId; }; diff --git a/policies/authorization_grant/authorization_grant.rego b/policies/authorization_grant/authorization_grant.rego index 72fa7ee8b..e999ebb70 100644 --- a/policies/authorization_grant/authorization_grant.rego +++ b/policies/authorization_grant/authorization_grant.rego @@ -67,11 +67,32 @@ allowed_scope(scope) if { regex.match(`^urn:matrix:org.matrix.msc2967.client:device:[A-Za-z0-9._~!$&'()*+,;=:@/-]{10,}$`, scope) } +allowed_scope(scope) if { + # Grant access to the C-S API only if there is a user + interactive_grant_type(input.grant_type) + regex.match(`^urn:matrix:client:device:[A-Za-z0-9._~!$&'()*+,;=:@/-]{10,}$`, scope) +} + +allowed_scope("urn:matrix:client:api:*") if { + # Grant access to the C-S API only if there is a user + interactive_grant_type(input.grant_type) +} + allowed_scope("urn:matrix:org.matrix.msc2967.client:api:*") if { # Grant access to the C-S API only if there is a user interactive_grant_type(input.grant_type) } +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 +} + +uses_stable_scopes if { + scope_list := split(input.scope, " ") + count({scope | some scope in scope_list; startswith(scope, "urn:matrix:client:")}) > 0 +} + # METADATA # entrypoint: true violation contains {"msg": msg} if { @@ -85,6 +106,16 @@ violation contains {"msg": "only one device scope is allowed at a time"} if { count({scope | some scope in scope_list; startswith(scope, "urn:matrix:org.matrix.msc2967.client:device:")}) > 1 } +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:client:device:")}) > 1 +} + +violation contains {"msg": "request cannot mix unstable and stable scopes"} if { + uses_stable_scopes + uses_unstable_scopes +} + violation contains {"msg": sprintf( "Requester [%s] isn't allowed to do this action", [common.format_requester(input.requester)], diff --git a/policies/authorization_grant/authorization_grant_test.rego b/policies/authorization_grant/authorization_grant_test.rego index 3594bfac2..fdf097da2 100644 --- a/policies/authorization_grant/authorization_grant_test.rego +++ b/policies/authorization_grant/authorization_grant_test.rego @@ -35,7 +35,7 @@ test_standard_scopes if { with input.scope as "profile" } -test_matrix_scopes if { +test_matrix_unstable_scopes if { authorization_grant.allow with input.user as user with input.client as client with input.grant_type as "authorization_code" @@ -52,7 +52,24 @@ test_matrix_scopes if { with input.scope as "urn:matrix:org.matrix.msc2967.client:api:*" } -test_device_scopes if { +test_matrix_stable_scopes if { + authorization_grant.allow with input.user as user + with input.client as client + with input.grant_type as "authorization_code" + with input.scope as "urn:matrix:client:api:*" + + authorization_grant.allow with input.user as user + with input.client as client + with input.grant_type as "urn:ietf:params:oauth:grant-type:device_code" + with input.scope as "urn:matrix:client:api:*" + + not authorization_grant.allow with input.user as user + with input.client as client + with input.grant_type as "client_credentials" + with input.scope as "urn:matrix:client:api:*" +} + +test_unstable_device_scopes if { authorization_grant.allow with input.user as user with input.client as client with input.grant_type as "authorization_code" @@ -87,6 +104,58 @@ test_device_scopes if { with input.scope as "urn:matrix:org.matrix.msc2967.client:device:AAbbCCdd01" } +test_stable_device_scopes if { + authorization_grant.allow with input.user as user + with input.client as client + with input.grant_type as "authorization_code" + with input.scope as "urn:matrix:client:device:AAbbCCdd01" + + authorization_grant.allow with input.user as user + with input.client as client + with input.grant_type as "authorization_code" + with input.scope as "urn:matrix:client:device:AAbbCCdd01-asdasdsa1-2313" + + # Too short + not authorization_grant.allow with input.user as user + with input.client as client + with input.grant_type as "authorization_code" + with input.scope as "urn:matrix:client:device:abcd" + + # Multiple device scope + not authorization_grant.allow with input.user as user + with input.client as client + with input.grant_type as "authorization_code" + with input.scope as "urn:matrix:client:device:AAbbCCdd01 urn:matrix:client:device:AAbbCCdd02" + + # Allowed with the device code grant + authorization_grant.allow with input.user as user + with input.client as client + with input.grant_type as "urn:ietf:params:oauth:grant-type:device_code" + with input.scope as "urn:matrix:client:device:AAbbCCdd01" + + # 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" +} + +test_mix_stable_and_unstable_scopes if { + not authorization_grant.allow with input.user as user + with input.client as client + with input.grant_type as "authorization_code" + with input.scope as "urn:matrix:org.matrix.msc2967.client:api:* 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" + with input.scope as "urn:matrix:client:api:* urn:matrix:org.matrix.msc2967.client:device:AAbbCCdd01" + + not authorization_grant.allow with input.user as user + with input.client as client + with input.grant_type as "authorization_code" + with input.scope as "urn:matrix:client:api:* urn:matrix:org.matrix.msc2967.client:api:*" +} + test_synapse_admin_scopes if { some grant_type in ["authorization_code", "urn:ietf:params:oauth:grant-type:device_code"] diff --git a/templates/components/scope.html b/templates/components/scope.html index 2f0ae107c..8ea4faf9c 100644 --- a/templates/components/scope.html +++ b/templates/components/scope.html @@ -14,14 +14,14 @@ {% elif scope == "urn:mas:graphql:*" %}
  • {{ icon.info() }}

    {{ _("mas.scope.edit_profile") }}

  • {{ icon.computer() }}

    {{ _("mas.scope.manage_sessions") }}

  • - {% elif scope == "urn:matrix:org.matrix.msc2967.client:api:*" %} + {% elif scope == "urn:matrix:client:api:*" or scope == "urn:matrix:org.matrix.msc2967.client:api:*" %}
  • {{ icon.chat() }}

    {{ _("mas.scope.view_messages") }}

  • {{ icon.send() }}

    {{ _("mas.scope.send_messages") }}

  • {% elif scope == "urn:synapse:admin:*" %}
  • {{ icon.error_solid() }}

    {{ _("mas.scope.synapse_admin") }}

  • {% elif scope == "urn:mas:admin" %}
  • {{ icon.error_solid() }}

    {{ _("mas.scope.mas_admin") }}

  • - {% elif scope is startingwith("urn:matrix:org.matrix.msc2967.client:device:") %} + {% elif scope is startingwith("urn:matrix:client:device:") or scope is startingwith("urn:matrix:org.matrix.msc2967.client:device:") %} {# We hide this scope #} {% else %}
  • {{ icon.info() }}

    {{ scope }}

  • diff --git a/templates/pages/sso.html b/templates/pages/sso.html index 5104dc594..37c6d2b90 100644 --- a/templates/pages/sso.html +++ b/templates/pages/sso.html @@ -23,7 +23,7 @@

    Allow access to your account?