mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-11-14 20:56:49 +00:00
feat: Add support for MSC4484
This commit is contained in:
1 parent
3a20ad7ff2
commit
823ee9f6e1
11 files changed
+190
-76
No files matched your search
+57
-14
@@ -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<R: IncomingRequest + Any>(
|
||||
fn check<R: IncomingRequest<Authentication = Self> + 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<R: IncomingRequest + Any>(
|
||||
async fn check<R: IncomingRequest<Authentication = Self> + 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<R: IncomingRequest + Any>(
|
||||
async fn check<R: IncomingRequest<Authentication = Self> + Any>(
|
||||
services: &Services,
|
||||
_incoming_request: &hyper::Request<&[u8]>,
|
||||
authentication: Self::Output,
|
||||
query: AuthQueryParams,
|
||||
) -> Result<Self::Identity> {
|
||||
check_access_token(services, &authentication, query, TypeId::of::<R>()).await
|
||||
check_access_token(
|
||||
services,
|
||||
&authentication,
|
||||
query,
|
||||
TypeId::of::<R>(),
|
||||
R::required_client_scopes(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl CheckAuth for AccessTokenOptional {
|
||||
type Identity = Option<ClientIdentity>;
|
||||
|
||||
async fn check<R: IncomingRequest + Any>(
|
||||
async fn check<R: IncomingRequest<Authentication = Self> + Any>(
|
||||
services: &Services,
|
||||
_incoming_request: &hyper::Request<&[u8]>,
|
||||
authentication: Self::Output,
|
||||
query: AuthQueryParams,
|
||||
) -> Result<Self::Identity> {
|
||||
if let Some(authentication) = authentication {
|
||||
check_access_token(services, &authentication, query, TypeId::of::<R>())
|
||||
.await
|
||||
.map(Some)
|
||||
check_access_token(
|
||||
services,
|
||||
&authentication,
|
||||
query,
|
||||
TypeId::of::<R>(),
|
||||
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<RegistrationInfo>;
|
||||
|
||||
async fn check<R: IncomingRequest + Any>(
|
||||
async fn check<R: IncomingRequest<Authentication = Self> + 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<R: IncomingRequest + Any>(
|
||||
fn check<R: IncomingRequest<Authentication = Self> + 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<ClientIdentity>;
|
||||
|
||||
async fn check<R: IncomingRequest + Any>(
|
||||
async fn check<R: IncomingRequest<Authentication = Self> + 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::<R>())
|
||||
check_access_token(services, &authentication, query, TypeId::of::<R>(), &[])
|
||||
.await
|
||||
.map(Some)
|
||||
} else {
|
||||
@@ -259,6 +275,7 @@ async fn check_access_token(
|
||||
token: &str,
|
||||
query: AuthQueryParams,
|
||||
route: TypeId,
|
||||
required_scopes: &[OAuthClientScope],
|
||||
) -> Result<ClientIdentity> {
|
||||
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(
|
||||
|
||||
@@ -48,5 +48,7 @@ pub fn unstable_features() -> BTreeMap<String, bool> {
|
||||
("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),
|
||||
])
|
||||
}
|
||||
@@ -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<BTreeSet<V>, D::Error>
|
||||
pub(super) fn btreeset_skip_err<'de, D, V>(de: D) -> Result<BTreeSet<V>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
V: Deserialize<'de> + Hash + Eq + Ord,
|
||||
|
||||
+40
-24
@@ -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<OAuthClientScope> {
|
||||
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<H: std::hash::Hasher>(&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<BTreeSet<Scope>, String> {
|
||||
#[allow(clippy::trivial_regex)]
|
||||
pub fn to_scopes(&self) -> Result<HashSet<RequestedScope>, 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
|
||||
|
||||
+39
-12
@@ -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<admin::Service>,
|
||||
users: Dep<users::Service>,
|
||||
config: Dep<config::Service>,
|
||||
}
|
||||
@@ -55,7 +56,9 @@ struct Services {
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct SessionInfo {
|
||||
pub client_id: String,
|
||||
pub scopes: BTreeSet<Scope>,
|
||||
// Ignore unknown scopes, old databases might have a device scope in here
|
||||
#[serde(deserialize_with = "client_metadata::btreeset_skip_err")]
|
||||
pub scopes: BTreeSet<OAuthClientScope>,
|
||||
current_refresh_token: String,
|
||||
}
|
||||
|
||||
@@ -68,7 +71,7 @@ struct RefreshTokenInfo {
|
||||
|
||||
struct PendingAuthCodeGrant {
|
||||
authorizing_user: OwnedUserId,
|
||||
requested_scopes: BTreeSet<Scope>,
|
||||
requested_scopes: HashSet<RequestedScope>,
|
||||
client_name: Option<String>,
|
||||
expected_client_id: String,
|
||||
expected_redirect_uri: Url,
|
||||
@@ -92,7 +95,7 @@ impl PendingAuthCodeGrant {
|
||||
|
||||
struct PendingDeviceCodeGrant {
|
||||
state: DeviceCodeGrantState,
|
||||
requested_scopes: BTreeSet<Scope>,
|
||||
requested_scopes: HashSet<RequestedScope>,
|
||||
client_name: Option<String>,
|
||||
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<Scope>,
|
||||
pub requested_scopes: HashSet<RequestedScope>,
|
||||
}
|
||||
|
||||
/// 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<Arc<Self>> {
|
||||
Ok(Arc::new(Self {
|
||||
services: Services {
|
||||
admin: args.depend::<admin::Service>("admin"),
|
||||
users: args.depend::<users::Service>("users"),
|
||||
config: args.depend::<config::Service>("config"),
|
||||
},
|
||||
@@ -540,17 +544,20 @@ impl Service {
|
||||
async fn create_session(
|
||||
&self,
|
||||
authorizing_user: OwnedUserId,
|
||||
requested_scopes: BTreeSet<Scope>,
|
||||
requested_scopes: HashSet<RequestedScope>,
|
||||
client_name: Option<String>,
|
||||
client_id: String,
|
||||
) -> Result<TokenResponse, OAuthError> {
|
||||
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<RequestedScope>,
|
||||
) -> 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
|
||||
|
||||
@@ -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<Scope>,
|
||||
pub scopes: BTreeSet<OAuthClientScope>,
|
||||
}
|
||||
|
||||
impl HtmlSafe for ClientScopes {}
|
||||
|
||||
@@ -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),
|
||||
))
|
||||
},
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<ul>
|
||||
{% for scope in scopes %}
|
||||
{% match scope %}
|
||||
{% when Scope::ClientApi %}
|
||||
<li>Send messages and interact with chatrooms on your behalf</li>
|
||||
{% when Scope::Device(_) %}
|
||||
<li>Access your Matrix account</li>
|
||||
{% when OAuthClientScope::ApiFullAccess %}
|
||||
<li>Access and control all features of your Matrix account</li>
|
||||
{% when OAuthClientScope::ServerAdministration %}
|
||||
<li><em class="danger">Access your homeserver's administrator tools</em></li>
|
||||
{% when _ %}
|
||||
<li>missingno</li>
|
||||
{% endmatch %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@@ -21,14 +21,26 @@ Authorize client
|
||||
</div>
|
||||
{{ Avatar::for_client(client_metadata) }}
|
||||
</div>
|
||||
|
||||
<div class="identity">
|
||||
Signed in as <code>{{ user_id }}</code>. <a href="{{ crate::ROUTE_PREFIX }}/account/logout?{{ logout_query }}">Switch accounts</a>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<b>{{ client_name }}</b> (<a href="{{ client_metadata.client_uri }}">{{ client_metadata.client_uri.domain().unwrap() }}</a>) would like
|
||||
your permission to:
|
||||
|
||||
{{ scopes }}
|
||||
</p>
|
||||
|
||||
{% if scopes.scopes.contains(&OAuthClientScope::ServerAdministration) %}
|
||||
<p class="card danger">
|
||||
⚠️ {{ 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.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% match (&client_metadata.policy_uri, &client_metadata.tos_uri) %}
|
||||
{% when (Some(policy_uri), Some(tos_uri)) %}
|
||||
<p>
|
||||
|
||||
Reference in new issue
Block a user