mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-11-15 13:16:56 +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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user