diff --git a/Cargo.lock b/Cargo.lock index 83d031dfa..83f0e9041 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4947,7 +4947,7 @@ dependencies = [ [[package]] name = "ruma" version = "0.16.0" -source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229" +source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85" dependencies = [ "assign", "js_int", @@ -4966,7 +4966,7 @@ dependencies = [ [[package]] name = "ruma-appservice-api" version = "0.16.0" -source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229" +source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85" dependencies = [ "http", "js_int", @@ -4979,7 +4979,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.24.0" -source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229" +source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85" dependencies = [ "as_variant", "assign", @@ -5001,7 +5001,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.19.0" -source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229" +source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85" dependencies = [ "as_variant", "base64 0.22.1", @@ -5034,7 +5034,7 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.34.0" -source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229" +source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85" dependencies = [ "as_variant", "indexmap 2.14.2", @@ -5055,7 +5055,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.15.0" -source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229" +source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85" dependencies = [ "bytes", "headers", @@ -5078,7 +5078,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.12.1" -source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229" +source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85" dependencies = [ "js_int", "thiserror 2.0.21", @@ -5087,7 +5087,7 @@ dependencies = [ [[package]] name = "ruma-macros" version = "0.19.0" -source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229" +source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85" dependencies = [ "as_variant", "cfg-if", @@ -5103,7 +5103,7 @@ dependencies = [ [[package]] name = "ruma-push-gateway-api" version = "0.15.0" -source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229" +source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85" dependencies = [ "js_int", "ruma-common", @@ -5115,7 +5115,7 @@ dependencies = [ [[package]] name = "ruma-signatures" version = "0.21.0" -source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229" +source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85" dependencies = [ "base64 0.22.1", "ed25519-dalek 3.0.0", @@ -5132,7 +5132,7 @@ dependencies = [ [[package]] name = "ruma-state-res" version = "0.17.0" -source = "git+https://github.com/ruma/ruma.git?rev=e927699d374114843f9f41ee133a5ced08449229#e927699d374114843f9f41ee133a5ced08449229" +source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85" dependencies = [ "js_int", "ruma-common", diff --git a/Cargo.toml b/Cargo.toml index 67ac2ad10..dce3355d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -348,7 +348,7 @@ version = "1.1.1" # Used for matrix spec type definitions and helpers [workspace.dependencies.ruma] git = "https://github.com/ruma/ruma.git" -rev = "e927699d374114843f9f41ee133a5ced08449229" +rev = "fe8570dbca23f4263165748ed898aad126b23d85" features = [ "appservice-api-c", "client-api", diff --git a/src/api/router/auth.rs b/src/api/router/auth.rs index b943ce2b6..6ba31ac94 100644 --- a/src/api/router/auth.rs +++ b/src/api/router/auth.rs @@ -1,4 +1,7 @@ -use std::any::{Any, TypeId}; +use std::{ + any::{Any, TypeId}, + collections::BTreeSet, +}; use conduwuit::{ Err, Error, Result, err, @@ -9,7 +12,7 @@ use ruma::{ DeviceId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedServerName, OwnedUserId, UInt, UserId, api::{ - IncomingRequest, + IncomingRequest, OAuthClientScope, auth_scheme::{ AccessToken, AccessTokenOptional, AppserviceToken, AppserviceTokenOptional, AuthScheme, NoAccessToken, NoAuthentication, @@ -84,7 +87,7 @@ impl ClientIdentity { pub(crate) trait CheckAuth: AuthScheme { type Identity: Send; - fn check( + fn check + Any>( services: &Services, incoming_request: &hyper::Request<&[u8]>, authentication: Self::Output, @@ -95,7 +98,7 @@ pub(crate) trait CheckAuth: AuthScheme { impl CheckAuth for ServerSignatures { type Identity = OwnedServerName; - async fn check( + async fn check + Any>( services: &Services, incoming_request: &hyper::Request<&[u8]>, authentication: Self::Output, @@ -155,29 +158,42 @@ impl CheckAuth for ServerSignatures { impl CheckAuth for AccessToken { type Identity = ClientIdentity; - async fn check( + async fn check + Any>( services: &Services, _incoming_request: &hyper::Request<&[u8]>, authentication: Self::Output, query: AuthQueryParams, ) -> Result { - check_access_token(services, &authentication, query, TypeId::of::()).await + check_access_token( + services, + &authentication, + query, + TypeId::of::(), + R::required_client_scopes(), + ) + .await } } impl CheckAuth for AccessTokenOptional { type Identity = Option; - async fn check( + async fn check + Any>( services: &Services, _incoming_request: &hyper::Request<&[u8]>, authentication: Self::Output, query: AuthQueryParams, ) -> Result { if let Some(authentication) = authentication { - check_access_token(services, &authentication, query, TypeId::of::()) - .await - .map(Some) + check_access_token( + services, + &authentication, + query, + TypeId::of::(), + R::required_client_scopes(), + ) + .await + .map(Some) } else { Ok(None) } @@ -200,7 +216,7 @@ impl CheckAuth for AppserviceToken { impl CheckAuth for AppserviceTokenOptional { type Identity = Option; - async fn check( + async fn check + Any>( services: &Services, _incoming_request: &hyper::Request<&[u8]>, authentication: Self::Output, @@ -219,7 +235,7 @@ impl CheckAuth for AppserviceTokenOptional { impl CheckAuth for NoAuthentication { type Identity = (); - fn check( + fn check + Any>( _services: &Services, _incoming_request: &hyper::Request<&[u8]>, _authentication: Self::Output, @@ -232,7 +248,7 @@ impl CheckAuth for NoAuthentication { impl CheckAuth for NoAccessToken { type Identity = Option; - async fn check( + async fn check + Any>( services: &Services, incoming_request: &hyper::Request<&[u8]>, _authentication: Self::Output, @@ -245,7 +261,7 @@ impl CheckAuth for NoAccessToken { })?; if let Some(authentication) = authentication { - check_access_token(services, &authentication, query, TypeId::of::()) + check_access_token(services, &authentication, query, TypeId::of::(), &[]) .await .map(Some) } else { @@ -259,6 +275,7 @@ async fn check_access_token( token: &str, query: AuthQueryParams, route: TypeId, + required_scopes: &[OAuthClientScope], ) -> Result { if token.is_empty() { return Err!(Request(Unauthorized("Empty access token."))); @@ -292,6 +309,32 @@ async fn check_access_token( } } + // Make sure the user has the right scopes to use the route + let user_scopes = if let Some(session_info) = services + .oauth + .get_session_info_for_device(&sender_user, &sender_device) + .await + { + session_info.scopes + } else { + let mut scopes = BTreeSet::from_iter([OAuthClientScope::ApiFullAccess]); + + if services.admin.user_is_admin(&sender_user).await { + scopes.insert(OAuthClientScope::ServerAdministration); + } + + scopes + }; + + if !required_scopes + .iter() + .any(|scope| user_scopes.contains(scope)) + { + return Err!(Request(Forbidden( + "You do not have permission to access this endpoint." + ))); + } + Ok(ClientIdentity::User { sender_user, sender_device }) } else if let Ok(appservice_info) = services.appservice.find_from_token(token).await { let Ok(sender_user) = query.user_id.clone().map_or_else( diff --git a/src/core/matrix/versions.rs b/src/core/matrix/versions.rs index 1e8050cf2..ae681e3ca 100644 --- a/src/core/matrix/versions.rs +++ b/src/core/matrix/versions.rs @@ -48,5 +48,7 @@ pub fn unstable_features() -> BTreeMap { ("org.matrix.msc4174".to_owned(), true), // Membership-based invite blocking (https://github.com/matrix-org/matrix-spec-proposals/pull/4494) ("uk.timedout.msc4494".to_owned(), true), + // server administration OAuth scope (https://github.com/matrix-org/matrix-spec-proposals/pull/4484) + ("org.continuwuity.msc4484.unstable".to_owned(), true), ]) } diff --git a/src/service/oauth/client_metadata.rs b/src/service/oauth/client_metadata.rs index 3aa4d5943..078452088 100644 --- a/src/service/oauth/client_metadata.rs +++ b/src/service/oauth/client_metadata.rs @@ -147,7 +147,7 @@ pub enum ResponseType { /// Deserialize a BTreeSet from a sequence, skipping items which fail to /// deserialize. This is used as a deserialize helper for ClientMetadata to /// ignore unknown enum variants in a few fields. -fn btreeset_skip_err<'de, D, V>(de: D) -> Result, D::Error> +pub(super) fn btreeset_skip_err<'de, D, V>(de: D) -> Result, D::Error> where D: Deserializer<'de>, V: Deserialize<'de> + Hash + Eq + Ord, diff --git a/src/service/oauth/grant.rs b/src/service/oauth/grant.rs index 39955b825..e0ee34eb6 100644 --- a/src/service/oauth/grant.rs +++ b/src/service/oauth/grant.rs @@ -1,14 +1,14 @@ use std::{ borrow::Cow, - collections::BTreeSet, + collections::HashSet, error::Error, fmt::{Debug, Display}, hash::Hash, mem::discriminant, }; -use regex::Regex; -use ruma::OwnedDeviceId; +use regex::regex; +use ruma::{OwnedDeviceId, api::OAuthClientScope}; use serde::{Deserialize, Serialize}; use url::Url; @@ -82,30 +82,42 @@ pub enum Prompt { Unknown, } -#[derive(Debug, Clone, Deserialize, Serialize, PartialOrd, Ord)] -pub enum Scope { +#[derive(Debug, Clone, Deserialize, Serialize)] +pub enum RequestedScope { Device(OwnedDeviceId), ClientApi, + ServerAdministration, } -impl PartialEq for Scope { +impl RequestedScope { + #[must_use] + pub fn as_client_scope(&self) -> Option { + match self { + | Self::ClientApi => Some(OAuthClientScope::ApiFullAccess), + | Self::Device(_) => None, + | Self::ServerAdministration => Some(OAuthClientScope::ServerAdministration), + } + } +} + +impl PartialEq for RequestedScope { fn eq(&self, other: &Self) -> bool { discriminant(self) == discriminant(other) } } -impl Eq for Scope {} +impl Eq for RequestedScope {} -impl Hash for Scope { +impl Hash for RequestedScope { fn hash(&self, state: &mut H) { discriminant(self).hash(state); } } -impl Display for Scope { +impl Display for RequestedScope { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let urn = match self { - | Self::ClientApi => "urn:matrix:client:api:*".to_owned(), - | Self::Device(device_id) => format!("urn:matrix:client:device:{device_id}"), - }; - - f.write_str(&urn) + match self { + | Self::ClientApi => write!(f, "urn:matrix:client:api:*"), + | Self::Device(device_id) => write!(f, "urn:matrix:client:device:{device_id}"), + | Self::ServerAdministration => + write!(f, "urn:matrix:client:cc.c10y.msc4484.server_administration"), + } } } @@ -113,22 +125,26 @@ impl Display for Scope { pub struct RawScopes(String); impl RawScopes { - pub fn to_scopes(&self) -> Result, String> { + #[allow(clippy::trivial_regex)] + pub fn to_scopes(&self) -> Result, String> { let client_api_token_regex = - Regex::new(r"urn:matrix:(client|org.matrix.msc2967.client):api:\*").unwrap(); - let device_token_regex = Regex::new( - r"urn:matrix:(client|org.matrix.msc2967.client):device:([a-zA-Z0-9-._~]{5,})", - ) - .unwrap(); + regex!(r"urn:matrix:(client|org.matrix.msc2967.client):api:\*"); + let device_token_regex = + regex!(r"urn:matrix:(client|org.matrix.msc2967.client):device:([a-zA-Z0-9-._~]{5,})"); + let server_administration_regex = + regex!(r"urn:matrix:client:cc.c10y.msc4484.server_administration"); - let mut scopes = BTreeSet::new(); + let mut scopes = HashSet::new(); for token in self.0.split(' ') { let scope_was_new = { if client_api_token_regex.is_match(token) { - scopes.insert(Scope::ClientApi) + scopes.insert(RequestedScope::ClientApi) + } else if server_administration_regex.is_match(token) { + scopes.insert(RequestedScope::ServerAdministration) } else if let Some(captures) = device_token_regex.captures(token) { - scopes.insert(Scope::Device(captures.get(2).unwrap().as_str().into())) + scopes + .insert(RequestedScope::Device(captures.get(2).unwrap().as_str().into())) } else if token == "openid" { // TODO(unspecced): Element sets this scope but doesn't use // it for anything diff --git a/src/service/oauth/mod.rs b/src/service/oauth/mod.rs index b5e576f41..6811aa2d5 100644 --- a/src/service/oauth/mod.rs +++ b/src/service/oauth/mod.rs @@ -1,5 +1,5 @@ use std::{ - collections::{BTreeSet, HashMap}, + collections::{BTreeSet, HashMap, HashSet}, sync::{Arc, Mutex}, time::{Duration, SystemTime}, }; @@ -13,18 +13,18 @@ use database::{Deserialized, Json, Map}; use itertools::Itertools; use lru_cache::LruCache; use rand::distr::{Distribution, slice::Choose}; -use ruma::{DeviceId, OwnedDeviceId, OwnedUserId, UserId}; +use ruma::{DeviceId, OwnedDeviceId, OwnedUserId, UserId, api::OAuthClientScope}; use serde::{Deserialize, Serialize}; use url::Url; use crate::{ - Dep, config, + Dep, admin, config, oauth::{ client_metadata::{ApplicationType, ClientMetadata, GrantType, ResponseType}, grant::{ AuthorizationCodeQuery, AuthorizationCodeResponse, CodeChallengeMethod, - DeviceCodeRequest, DeviceCodeResponse, ErrorCode, OAuthError, ResponseMode, Scope, - TokenRequest, TokenRequestType, TokenResponse, TokenType, + DeviceCodeRequest, DeviceCodeResponse, ErrorCode, OAuthError, RequestedScope, + ResponseMode, TokenRequest, TokenRequestType, TokenResponse, TokenType, }, }, users::{self, DeviceToken}, @@ -48,6 +48,7 @@ struct Data { } struct Services { + admin: Dep, users: Dep, config: Dep, } @@ -55,7 +56,9 @@ struct Services { #[derive(Debug, Deserialize, Serialize)] pub struct SessionInfo { pub client_id: String, - pub scopes: BTreeSet, + // Ignore unknown scopes, old databases might have a device scope in here + #[serde(deserialize_with = "client_metadata::btreeset_skip_err")] + pub scopes: BTreeSet, current_refresh_token: String, } @@ -68,7 +71,7 @@ struct RefreshTokenInfo { struct PendingAuthCodeGrant { authorizing_user: OwnedUserId, - requested_scopes: BTreeSet, + requested_scopes: HashSet, client_name: Option, expected_client_id: String, expected_redirect_uri: Url, @@ -92,7 +95,7 @@ impl PendingAuthCodeGrant { struct PendingDeviceCodeGrant { state: DeviceCodeGrantState, - requested_scopes: BTreeSet, + requested_scopes: HashSet, client_name: Option, client_id: String, requested_at: SystemTime, @@ -124,7 +127,7 @@ impl PendingDeviceCodeGrant { pub struct DeviceCodeGrantInfo { pub device_code: String, pub client_metadata: ClientMetadata, - pub requested_scopes: BTreeSet, + pub requested_scopes: HashSet, } /// A time-limited grant for a client to perform some sensitive action. @@ -148,6 +151,7 @@ impl crate::Service for Service { fn build(args: crate::Args<'_>) -> Result> { Ok(Arc::new(Self { services: Services { + admin: args.depend::("admin"), users: args.depend::("users"), config: args.depend::("config"), }, @@ -540,17 +544,20 @@ impl Service { async fn create_session( &self, authorizing_user: OwnedUserId, - requested_scopes: BTreeSet, + requested_scopes: HashSet, client_name: Option, client_id: String, ) -> Result { + self.check_requested_scopes(&authorizing_user, &requested_scopes) + .await?; + let access_token = DeviceToken::new_random().with_max_age(Self::ACCESS_TOKEN_MAX_AGE); let refresh_token = Self::generate_token(); let device_id = requested_scopes .iter() .find_map(|scope| { - if let Scope::Device(device_id) = scope { + if let RequestedScope::Device(device_id) = scope { Some(device_id.to_owned()) } else { None @@ -599,7 +606,10 @@ impl Service { Json(SessionInfo { client_id: client_id.clone(), current_refresh_token: refresh_token.clone(), - scopes: requested_scopes.clone(), + scopes: requested_scopes + .iter() + .filter_map(RequestedScope::as_client_scope) + .collect(), }), ); @@ -700,6 +710,23 @@ impl Service { } } + async fn check_requested_scopes( + &self, + authorizing_user: &UserId, + requested_scopes: &HashSet, + ) -> Result<(), OAuthError> { + if requested_scopes.contains(&RequestedScope::ServerAdministration) + && !self.services.admin.user_is_admin(authorizing_user).await + { + return Err(OAuthError::new( + ErrorCode::AccessDenied, + "You are not a server administrator.".to_owned(), + )); + } + + Ok(()) + } + /// Issue a ticket for `localpart` to perform some action. pub fn issue_ticket(&self, localpart: String, ticket: OAuthTicket) { self.tickets diff --git a/src/web/pages/components/mod.rs b/src/web/pages/components/mod.rs index 561f2b630..d66ad2dcb 100644 --- a/src/web/pages/components/mod.rs +++ b/src/web/pages/components/mod.rs @@ -2,11 +2,11 @@ use std::{collections::BTreeSet, time::SystemTime}; use askama::{Template, filters::HtmlSafe}; use conduwuit_core::utils; -use conduwuit_service::{ - Services, - oauth::{client_metadata::ClientMetadata, grant::Scope}, +use conduwuit_service::{Services, oauth::client_metadata::ClientMetadata}; +use ruma::{ + OwnedDeviceId, OwnedUserId, UserId, + api::{OAuthClientScope, client::device::Device}, }; -use ruma::{OwnedDeviceId, OwnedUserId, UserId, api::client::device::Device}; pub(super) mod form; @@ -202,7 +202,7 @@ impl DeviceCard { #[derive(Debug, Template)] #[template(path = "_components/client_scopes.html.j2")] pub(super) struct ClientScopes { - pub scopes: BTreeSet, + pub scopes: BTreeSet, } impl HtmlSafe for ClientScopes {} diff --git a/src/web/pages/oauth/grant.rs b/src/web/pages/oauth/grant.rs index 5fe7656f1..6f1c46c73 100644 --- a/src/web/pages/oauth/grant.rs +++ b/src/web/pages/oauth/grant.rs @@ -6,9 +6,9 @@ use axum::{ }; use conduwuit_service::oauth::{ client_metadata::ClientMetadata, - grant::{AuthorizationCodeQuery, DeviceCodeVerifyQuery, Prompt}, + grant::{AuthorizationCodeQuery, DeviceCodeVerifyQuery, Prompt, RequestedScope}, }; -use ruma::OwnedUserId; +use ruma::{OwnedUserId, api::OAuthClientScope}; use serde::{Deserialize, de::IgnoredAny}; use crate::{ @@ -102,7 +102,13 @@ async fn route_authorization_code( return Err(WebError::BadRequest("Invalid client ID".to_owned())); }; - let scopes = query.scope.to_scopes().map_err(WebError::BadRequest)?; + let scopes = query + .scope + .to_scopes() + .map_err(WebError::BadRequest)? + .iter() + .filter_map(RequestedScope::as_client_scope) + .collect(); let user_avatar = Avatar::for_local_user(&services, &user_id).await; @@ -183,6 +189,12 @@ async fn route_device_code( )); }; + let scopes = grant_info + .requested_scopes + .iter() + .filter_map(RequestedScope::as_client_scope) + .collect(); + let user_avatar = Avatar::for_local_user(&services, &user_id).await; response!(Grant::new( @@ -196,7 +208,7 @@ async fn route_device_code( user_id, user_avatar, grant_info.client_metadata, - ClientScopes { scopes: grant_info.requested_scopes }, + ClientScopes { scopes }, Some(grant_info.device_code), )) }, diff --git a/src/web/pages/templates/_components/client_scopes.html.j2 b/src/web/pages/templates/_components/client_scopes.html.j2 index bf7df3aec..ef814307e 100644 --- a/src/web/pages/templates/_components/client_scopes.html.j2 +++ b/src/web/pages/templates/_components/client_scopes.html.j2 @@ -1,10 +1,12 @@
    {% for scope in scopes %} {% match scope %} - {% when Scope::ClientApi %} -
  • Send messages and interact with chatrooms on your behalf
  • - {% when Scope::Device(_) %} -
  • Access your Matrix account
  • + {% when OAuthClientScope::ApiFullAccess %} +
  • Access and control all features of your Matrix account
  • + {% when OAuthClientScope::ServerAdministration %} +
  • Access your homeserver's administrator tools
  • + {% when _ %} +
  • missingno
  • {% endmatch %} {% endfor %}
diff --git a/src/web/pages/templates/grant.html.j2 b/src/web/pages/templates/grant.html.j2 index 711d384e2..bc5a09624 100644 --- a/src/web/pages/templates/grant.html.j2 +++ b/src/web/pages/templates/grant.html.j2 @@ -21,14 +21,26 @@ Authorize client {{ Avatar::for_client(client_metadata) }} +
Signed in as {{ user_id }}. Switch accounts
+

{{ client_name }} ({{ client_metadata.client_uri.domain().unwrap() }}) would like your permission to: + {{ scopes }}

+ + {% if scopes.scopes.contains(&OAuthClientScope::ServerAdministration) %} +

+ ⚠️ {{ client_name }} wants to be able to administrate your homeserver. If it is untrustworthy, + it could use its administrator powers to cause severe damage to your homeserver's users and rooms. + Make sure you trust it fully before you continue. +

+ {% endif %} + {% match (&client_metadata.policy_uri, &client_metadata.tos_uri) %} {% when (Some(policy_uri), Some(tos_uri)) %}