mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-06-10 20:01:50 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 61a2e236b6 | |||
| d4fdf87daa | |||
| 513259a837 | |||
| 0f14a91bf3 | |||
| d557ed9a2c | |||
| cad2bb659b | |||
| 4ee69f9061 | |||
| 9812067c39 | |||
| 10136d4f78 | |||
| d6d0694387 | |||
| 0db74089c1 | |||
| efe37dab12 | |||
| 1f16468dac | |||
| 00bdffb783 | |||
| ed83d8fbb4 | |||
| 50f22cbf10 |
Generated
+21
-21
@@ -4263,8 +4263,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma"
|
||||
version = "0.15.1"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
|
||||
version = "0.16.0"
|
||||
source = "git+https://github.com/gingershaped/ruwuma.git?rev=a0178c4e5e1729d27cf2f1c4dacf77b763987749#a0178c4e5e1729d27cf2f1c4dacf77b763987749"
|
||||
dependencies = [
|
||||
"assign",
|
||||
"js_int",
|
||||
@@ -4282,8 +4282,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma-appservice-api"
|
||||
version = "0.15.0"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
|
||||
version = "0.16.0"
|
||||
source = "git+https://github.com/gingershaped/ruwuma.git?rev=a0178c4e5e1729d27cf2f1c4dacf77b763987749#a0178c4e5e1729d27cf2f1c4dacf77b763987749"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"ruma-common",
|
||||
@@ -4294,8 +4294,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma-client-api"
|
||||
version = "0.23.1"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
|
||||
version = "0.24.0"
|
||||
source = "git+https://github.com/gingershaped/ruwuma.git?rev=a0178c4e5e1729d27cf2f1c4dacf77b763987749#a0178c4e5e1729d27cf2f1c4dacf77b763987749"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"assign",
|
||||
@@ -4316,8 +4316,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma-common"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
|
||||
version = "0.19.0"
|
||||
source = "git+https://github.com/gingershaped/ruwuma.git?rev=a0178c4e5e1729d27cf2f1c4dacf77b763987749#a0178c4e5e1729d27cf2f1c4dacf77b763987749"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"base64 0.22.1",
|
||||
@@ -4349,8 +4349,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma-events"
|
||||
version = "0.33.0"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
|
||||
version = "0.34.0"
|
||||
source = "git+https://github.com/gingershaped/ruwuma.git?rev=a0178c4e5e1729d27cf2f1c4dacf77b763987749#a0178c4e5e1729d27cf2f1c4dacf77b763987749"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"indexmap",
|
||||
@@ -4370,8 +4370,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma-federation-api"
|
||||
version = "0.14.0"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
|
||||
version = "0.15.0"
|
||||
source = "git+https://github.com/gingershaped/ruwuma.git?rev=a0178c4e5e1729d27cf2f1c4dacf77b763987749#a0178c4e5e1729d27cf2f1c4dacf77b763987749"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"headers",
|
||||
@@ -4394,7 +4394,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-identifiers-validation"
|
||||
version = "0.12.1"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
|
||||
source = "git+https://github.com/gingershaped/ruwuma.git?rev=a0178c4e5e1729d27cf2f1c4dacf77b763987749#a0178c4e5e1729d27cf2f1c4dacf77b763987749"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"thiserror",
|
||||
@@ -4402,8 +4402,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma-macros"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
|
||||
version = "0.19.0"
|
||||
source = "git+https://github.com/gingershaped/ruwuma.git?rev=a0178c4e5e1729d27cf2f1c4dacf77b763987749#a0178c4e5e1729d27cf2f1c4dacf77b763987749"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"cfg-if",
|
||||
@@ -4418,8 +4418,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma-push-gateway-api"
|
||||
version = "0.14.0"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
|
||||
version = "0.15.0"
|
||||
source = "git+https://github.com/gingershaped/ruwuma.git?rev=a0178c4e5e1729d27cf2f1c4dacf77b763987749#a0178c4e5e1729d27cf2f1c4dacf77b763987749"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"ruma-common",
|
||||
@@ -4430,8 +4430,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma-signatures"
|
||||
version = "0.20.0"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
|
||||
version = "0.21.0"
|
||||
source = "git+https://github.com/gingershaped/ruwuma.git?rev=a0178c4e5e1729d27cf2f1c4dacf77b763987749#a0178c4e5e1729d27cf2f1c4dacf77b763987749"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"ed25519-dalek",
|
||||
@@ -4446,8 +4446,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma-state-res"
|
||||
version = "0.16.0"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
|
||||
version = "0.17.0"
|
||||
source = "git+https://github.com/gingershaped/ruwuma.git?rev=a0178c4e5e1729d27cf2f1c4dacf77b763987749#a0178c4e5e1729d27cf2f1c4dacf77b763987749"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"ruma-common",
|
||||
|
||||
+3
-2
@@ -343,8 +343,8 @@ version = "1.1.1"
|
||||
# Used for matrix spec type definitions and helpers
|
||||
[workspace.dependencies.ruma]
|
||||
# version = "0.14.1"
|
||||
git = "https://github.com/ruma/ruma.git"
|
||||
rev = "3ecd80b92794d2d93f657a7b3db62d4be237526b"
|
||||
git = "https://github.com/gingershaped/ruwuma.git"
|
||||
rev = "a0178c4e5e1729d27cf2f1c4dacf77b763987749"
|
||||
features = [
|
||||
"appservice-api-c",
|
||||
"client-api",
|
||||
@@ -379,6 +379,7 @@ features = [
|
||||
"unstable-msc4406",
|
||||
"unstable-msc4439",
|
||||
"unstable-msc4466",
|
||||
"unstable-msc4484",
|
||||
"unstable-extensible-events",
|
||||
]
|
||||
|
||||
|
||||
@@ -2001,7 +2001,7 @@
|
||||
# privacy_policy = { name = "Privacy Policy", url = "https://homeserver.example/en/privacy_policy.html" }
|
||||
# ```
|
||||
#
|
||||
#documents =
|
||||
#documents = {}
|
||||
|
||||
#[global.oauth]
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ pub(super) async fn suspend(&self, user_id: String) -> Result {
|
||||
// TODO: Record the actual user that sent the suspension where possible
|
||||
self.services
|
||||
.users
|
||||
.suspend_account(&user_id, self.sender_or_service_user())
|
||||
.suspend_account(&user_id, self.sender)
|
||||
.await;
|
||||
|
||||
self.write_str(&format!("User {user_id} has been suspended."))
|
||||
@@ -939,7 +939,7 @@ pub(super) async fn lock(&self, user_id: String) -> Result {
|
||||
}
|
||||
self.services
|
||||
.users
|
||||
.lock_account(&user_id, self.sender_or_service_user())
|
||||
.lock_account(&user_id, self.sender)
|
||||
.await;
|
||||
|
||||
self.write_str(&format!("User {user_id} has been locked."))
|
||||
|
||||
@@ -62,6 +62,8 @@ zstd_compression = [
|
||||
"reqwest/zstd",
|
||||
]
|
||||
|
||||
admin_api = []
|
||||
|
||||
[dependencies]
|
||||
async-trait.workspace = true
|
||||
axum-client-ip.workspace = true
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub mod rooms;
|
||||
@@ -1,36 +0,0 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result};
|
||||
use futures::StreamExt;
|
||||
use ruma::OwnedRoomId;
|
||||
use ruminuwuity::admin::continuwuity::rooms;
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
/// # `GET /_continuwuity/admin/rooms/list`
|
||||
///
|
||||
/// Lists all rooms known to this server, excluding banned ones.
|
||||
pub(crate) async fn list_rooms(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<rooms::list::v1::Request>,
|
||||
) -> Result<rooms::list::v1::Response> {
|
||||
let sender_user = body.identity.expect_sender_user()?;
|
||||
if !services.users.is_admin(sender_user).await {
|
||||
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
|
||||
}
|
||||
|
||||
let mut rooms: Vec<OwnedRoomId> = services
|
||||
.rooms
|
||||
.metadata
|
||||
.iter_ids()
|
||||
.filter_map(|room_id| async move {
|
||||
if !services.rooms.metadata.is_banned(&room_id).await {
|
||||
Some(room_id.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
.await;
|
||||
rooms.sort();
|
||||
Ok(rooms::list::v1::Response::new(rooms))
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod ban;
|
||||
pub mod list;
|
||||
@@ -99,7 +99,7 @@ pub(crate) async fn register_route(
|
||||
.users
|
||||
.create_local_account(&user_id, password, identity.email)
|
||||
.await;
|
||||
|
||||
services.users.join_auto_join_rooms(&user_id).await;
|
||||
user_id
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::Err;
|
||||
use ruma::api::client::admin::{is_user_locked, lock_user};
|
||||
|
||||
use crate::router::Ruma;
|
||||
|
||||
/// # `GET /_matrix/client/v1/admin/lock/{userId}`
|
||||
///
|
||||
/// Check the account lock status of a target user
|
||||
pub(crate) async fn get_locked_status(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<is_user_locked::v1::Request>,
|
||||
) -> conduwuit::Result<is_user_locked::v1::Response> {
|
||||
if !services.users.is_active_local(&body.user_id).await {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Can only check the lock status of active local users"
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(is_user_locked::v1::Response::new(
|
||||
services.users.is_locked(&body.user_id).await?,
|
||||
))
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/v1/admin/lock/{userId}`
|
||||
///
|
||||
/// Set the account lock status of a target user
|
||||
pub(crate) async fn put_locked_status(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<lock_user::v1::Request>,
|
||||
) -> conduwuit::Result<lock_user::v1::Response> {
|
||||
if !services.users.is_active_local(&body.user_id).await {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Can only set the locked status of active local users"
|
||||
)));
|
||||
}
|
||||
|
||||
if body.identity.sender_user() == Some(&body.user_id) {
|
||||
return Err!(Request(Forbidden("You cannot lock yourself")));
|
||||
}
|
||||
|
||||
if services.users.is_admin(&body.user_id).await {
|
||||
return Err!(Request(Forbidden("You cannot lock another server administrator")));
|
||||
}
|
||||
|
||||
if services.users.is_locked(&body.user_id).await? == body.locked {
|
||||
// No change
|
||||
return Ok(lock_user::v1::Response::new(body.locked));
|
||||
}
|
||||
|
||||
let action = if body.locked {
|
||||
services
|
||||
.users
|
||||
.lock_account(&body.user_id, body.identity.sender_user())
|
||||
.await;
|
||||
"suspended"
|
||||
} else {
|
||||
services.users.unlock_account(&body.user_id).await;
|
||||
"unsuspended"
|
||||
};
|
||||
|
||||
if services.config.admin_room_notices {
|
||||
// Notify the admin room that an account has been un/suspended
|
||||
services
|
||||
.admin
|
||||
.send_text(&format!("{} has been {} by {}.", body.user_id, action, body.identity))
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(lock_user::v1::Response::new(body.locked))
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
mod lock;
|
||||
pub(crate) mod site;
|
||||
mod suspend;
|
||||
|
||||
pub(crate) use self::suspend::*;
|
||||
pub(crate) use self::{lock::*, suspend::*};
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
pub(crate) mod rooms;
|
||||
pub(crate) mod users;
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
use crate::{Ruma, client::leave_room};
|
||||
|
||||
/// # `PUT /_continuwuity/admin/rooms/{roomID}/ban`
|
||||
/// # `PUT /_continuwuity/admin/v1/rooms/{roomID}/ban`
|
||||
///
|
||||
/// Bans or unbans a room.
|
||||
pub(crate) async fn ban_room(
|
||||
@@ -0,0 +1,178 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{
|
||||
Event, Result,
|
||||
utils::stream::{BroadbandExt, WidebandExt},
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use ruma::{
|
||||
OwnedRoomId,
|
||||
events::{
|
||||
StateEventType,
|
||||
room::{
|
||||
create::RoomCreateEventContent,
|
||||
encryption::PossiblyRedactedRoomEncryptionEventContent,
|
||||
tombstone::PossiblyRedactedRoomTombstoneEventContent,
|
||||
},
|
||||
},
|
||||
};
|
||||
use ruminuwuity::admin::continuwuity::rooms;
|
||||
use tokio::join;
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
/// # `GET /_continuwuity/admin/rooms`
|
||||
///
|
||||
/// Lists all room IDs known to this server, excluding banned ones.
|
||||
///
|
||||
/// This is the legacy version of the endpoint, which does not support
|
||||
/// pagination or including banned rooms. It is recommended to use the
|
||||
/// `/v1/rooms` endpoint instead. This endpoint may be removed in a future
|
||||
/// release.
|
||||
pub(crate) async fn legacy_list_rooms_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<rooms::list::unstable::Request>,
|
||||
) -> Result<rooms::list::unstable::Response> {
|
||||
let mut rooms: Vec<OwnedRoomId> = services
|
||||
.rooms
|
||||
.metadata
|
||||
.iter_ids()
|
||||
.filter_map(|room_id| async move {
|
||||
if !services.rooms.metadata.is_banned(&room_id).await {
|
||||
Some(room_id.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
.await;
|
||||
rooms.sort();
|
||||
Ok(rooms::list::unstable::Response::new(rooms))
|
||||
}
|
||||
|
||||
/// # `GET /_continuwuity/admin/v1/rooms`
|
||||
///
|
||||
/// Lists rooms known to this server.
|
||||
pub(crate) async fn list_rooms_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<rooms::list::v1::Request>,
|
||||
) -> Result<rooms::list::v1::Response> {
|
||||
let include_banned_rooms = body.include_banned_rooms;
|
||||
let rooms = services
|
||||
.rooms
|
||||
.metadata
|
||||
.iter_ids()
|
||||
.wide_filter_map(|room_id| async move {
|
||||
if include_banned_rooms || !services.rooms.metadata.is_banned(&room_id).await {
|
||||
Some(room_id.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.skip(body.offset.unwrap_or_default())
|
||||
.take(body.limit.unwrap_or(100).min(100))
|
||||
.broad_filter_map(|room_id| async move {
|
||||
let (
|
||||
banned,
|
||||
disabled,
|
||||
member_count,
|
||||
local_member_count,
|
||||
resident_server_count,
|
||||
published,
|
||||
create_event,
|
||||
encryption_event,
|
||||
name_event,
|
||||
topic_event,
|
||||
canonical_alias_event,
|
||||
join_rules_event,
|
||||
history_visibility_event,
|
||||
tombstone_event,
|
||||
) = join!(
|
||||
services.rooms.metadata.is_banned(&room_id),
|
||||
services.rooms.metadata.is_disabled(&room_id),
|
||||
services.rooms.state_cache.room_joined_count(&room_id),
|
||||
services
|
||||
.rooms
|
||||
.state_cache
|
||||
.active_local_users_in_room(&room_id)
|
||||
.count(),
|
||||
services.rooms.state_cache.room_servers(&room_id).count(),
|
||||
services.rooms.directory.is_public_room(&room_id),
|
||||
services.rooms.state_accessor.room_state_get(
|
||||
&room_id,
|
||||
&StateEventType::RoomCreate,
|
||||
""
|
||||
),
|
||||
services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content::<PossiblyRedactedRoomEncryptionEventContent>(
|
||||
&room_id,
|
||||
&StateEventType::RoomEncryption,
|
||||
""
|
||||
),
|
||||
services.rooms.state_accessor.room_state_get_content(
|
||||
&room_id,
|
||||
&StateEventType::RoomName,
|
||||
""
|
||||
),
|
||||
services.rooms.state_accessor.room_state_get_content(
|
||||
&room_id,
|
||||
&StateEventType::RoomTopic,
|
||||
""
|
||||
),
|
||||
services.rooms.state_accessor.room_state_get_content(
|
||||
&room_id,
|
||||
&StateEventType::RoomCanonicalAlias,
|
||||
""
|
||||
),
|
||||
services.rooms.state_accessor.room_state_get_content(
|
||||
&room_id,
|
||||
&StateEventType::RoomJoinRules,
|
||||
""
|
||||
),
|
||||
services.rooms.state_accessor.room_state_get_content(
|
||||
&room_id,
|
||||
&StateEventType::RoomHistoryVisibility,
|
||||
""
|
||||
),
|
||||
services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content::<PossiblyRedactedRoomTombstoneEventContent>(
|
||||
&room_id,
|
||||
&StateEventType::RoomTombstone,
|
||||
""
|
||||
),
|
||||
);
|
||||
let Ok(create_event) = create_event else {
|
||||
return None;
|
||||
};
|
||||
let create_content = create_event
|
||||
.get_content::<RoomCreateEventContent>()
|
||||
.expect("m.room.create content must be valid");
|
||||
Some(rooms::list::v1::MinimalRoomInfo {
|
||||
room_id,
|
||||
banned,
|
||||
disabled,
|
||||
member_count: usize::try_from(member_count.unwrap_or_default())
|
||||
.expect("u64 should fit in usize"),
|
||||
local_member_count,
|
||||
resident_server_count,
|
||||
creators: vec![create_event.sender],
|
||||
encrypted: encryption_event.is_ok_and(|c| c.algorithm.is_some()),
|
||||
federated: create_content.federate,
|
||||
published,
|
||||
version: create_content.room_version,
|
||||
name: name_event.unwrap_or(None),
|
||||
topic: topic_event.unwrap_or(None),
|
||||
canonical_alias: canonical_alias_event.unwrap_or(None),
|
||||
join_rules: join_rules_event.unwrap_or(None),
|
||||
history_visibility: history_visibility_event.unwrap_or(None),
|
||||
predecessor: create_content.predecessor.map(|c| c.room_id),
|
||||
successor: tombstone_event.map_or(None, |c| c.replacement_room),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
.await;
|
||||
Ok(rooms::list::v1::Response::new(rooms))
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mod ban;
|
||||
mod list;
|
||||
|
||||
pub(crate) use ban::ban_room;
|
||||
pub(crate) use list::*;
|
||||
@@ -0,0 +1,119 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{
|
||||
Err, err, error, info,
|
||||
utils::{IterStream, stream::BroadbandExt},
|
||||
warn,
|
||||
};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use ruma::UserId;
|
||||
use ruminuwuity::admin::continuwuity::users;
|
||||
use service::users::HashedPassword;
|
||||
|
||||
use crate::router::Ruma;
|
||||
|
||||
/// # `POST /_continuwuity/admin/v1/users/create`
|
||||
///
|
||||
/// Creates a new user.
|
||||
pub(crate) async fn create_user_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<users::create::v1::Request>,
|
||||
) -> conduwuit::Result<users::create::v1::Response> {
|
||||
let email = body
|
||||
.email
|
||||
.clone()
|
||||
.map(lettre::Address::try_from)
|
||||
.transpose()
|
||||
.map_err(|e| err!(Request(BadJson("Invalid email address: {e}"))))?;
|
||||
|
||||
let ref user_id = services
|
||||
.users
|
||||
.determine_registration_user_id(Some(body.localpart.clone()), email.as_ref(), None)
|
||||
.await?;
|
||||
|
||||
services
|
||||
.users
|
||||
.create_local_account(user_id, HashedPassword::new(&body.password)?, email)
|
||||
.await;
|
||||
|
||||
if body.suspended {
|
||||
services
|
||||
.users
|
||||
.suspend_account(&user_id, body.identity.sender_user())
|
||||
.await;
|
||||
}
|
||||
if body.locked {
|
||||
services
|
||||
.users
|
||||
.lock_account(user_id, body.identity.sender_user())
|
||||
.await;
|
||||
}
|
||||
if body.login_disabled {
|
||||
services.users.disable_login(user_id);
|
||||
}
|
||||
if let Some(ref value) = body.display_name {
|
||||
services.users.set_profile_key(
|
||||
user_id,
|
||||
"displayname",
|
||||
Some(serde_json::to_value(value)?),
|
||||
);
|
||||
}
|
||||
if let Some(ref value) = body.avatar_url {
|
||||
services
|
||||
.users
|
||||
.set_profile_key(user_id, "avatar_url", Some(serde_json::to_value(value)?));
|
||||
}
|
||||
if body.admin {
|
||||
services
|
||||
.admin
|
||||
.make_user_admin(user_id)
|
||||
.await
|
||||
.inspect_err(|e| error!("failed to make new user {user_id} an admin: {e}"))
|
||||
.ok();
|
||||
}
|
||||
if !body.skip_auto_join {
|
||||
services.users.join_auto_join_rooms(user_id).await;
|
||||
}
|
||||
|
||||
body.auto_join_rooms
|
||||
.clone()
|
||||
.into_iter()
|
||||
.stream()
|
||||
.broad_filter_map(|room| async move {
|
||||
services
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_with_servers(&room, None)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
warn!(
|
||||
"Failed to resolve room alias to room ID when attempting to auto join \
|
||||
{room}: {e}"
|
||||
);
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.for_each_concurrent(None, |(room_id, servers)| async move {
|
||||
match services
|
||||
.rooms
|
||||
.membership
|
||||
.join_room(
|
||||
user_id,
|
||||
&room_id,
|
||||
Some("Automatically joining this room upon registration".to_owned()),
|
||||
servers.as_ref(),
|
||||
)
|
||||
.boxed()
|
||||
.await
|
||||
{
|
||||
| Err(e) => {
|
||||
warn!("Failed to automatically join {user_id} to {room_id}: {e}");
|
||||
},
|
||||
| _ => {
|
||||
info!("Automatically joined room {user_id} to {room_id}");
|
||||
},
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(users::create::v1::Response::new(user_id.to_owned()))
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::utils::stream::WidebandExt;
|
||||
use futures::StreamExt;
|
||||
use ruminuwuity::admin::continuwuity::users;
|
||||
use tokio::join;
|
||||
|
||||
use crate::router::Ruma;
|
||||
|
||||
/// # `GET /_continuwuity/admin/v1/users`
|
||||
///
|
||||
/// Lists all users on this homeserver.
|
||||
pub(crate) async fn list_users_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<users::list::v1::Request>,
|
||||
) -> conduwuit::Result<users::list::v1::Response> {
|
||||
let users = services
|
||||
.users
|
||||
.list_local_users()
|
||||
.skip(body.offset.unwrap_or_default())
|
||||
.take(body.limit.unwrap_or(100).min(100))
|
||||
.wide_filter_map(|user_id| async move {
|
||||
let (deactivated, suspended, locked, admin, login_disabled) = join!(
|
||||
services.users.is_deactivated(&user_id),
|
||||
services.users.is_suspended(&user_id),
|
||||
services.users.is_locked(&user_id),
|
||||
services.users.is_admin(&user_id),
|
||||
services.users.is_login_disabled(&user_id),
|
||||
);
|
||||
Some(users::list::v1::User {
|
||||
user_id: user_id.clone(),
|
||||
deactivated: deactivated.unwrap_or_default(),
|
||||
suspended: suspended.unwrap_or_default(),
|
||||
locked: locked.unwrap_or_default(),
|
||||
admin,
|
||||
login_disabled,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
Ok(users::list::v1::Response::new(users))
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mod create;
|
||||
mod list;
|
||||
|
||||
pub(crate) use create::*;
|
||||
pub(crate) use list::*;
|
||||
@@ -1,7 +1,7 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result};
|
||||
use futures::future::{join, join3};
|
||||
use ruminuwuity::admin::{get_suspended, set_suspended};
|
||||
use ruma::api::client::admin::{is_user_suspended, suspend_user};
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
/// Check the suspension status of a target user
|
||||
pub(crate) async fn get_suspended_status(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<get_suspended::v1::Request>,
|
||||
) -> Result<get_suspended::v1::Response> {
|
||||
body: Ruma<is_user_suspended::v1::Request>,
|
||||
) -> Result<is_user_suspended::v1::Response> {
|
||||
let (admin, active) = join(
|
||||
services.users.is_admin(body.identity.expect_sender_user()?),
|
||||
services.users.is_active(&body.user_id),
|
||||
@@ -26,7 +26,7 @@ pub(crate) async fn get_suspended_status(
|
||||
if !active {
|
||||
return Err!(Request(NotFound("Unknown user")));
|
||||
}
|
||||
Ok(get_suspended::v1::Response::new(
|
||||
Ok(is_user_suspended::v1::Response::new(
|
||||
services.users.is_suspended(&body.user_id).await?,
|
||||
))
|
||||
}
|
||||
@@ -36,8 +36,8 @@ pub(crate) async fn get_suspended_status(
|
||||
/// Set the suspension status of a target user
|
||||
pub(crate) async fn put_suspended_status(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<set_suspended::v1::Request>,
|
||||
) -> Result<set_suspended::v1::Response> {
|
||||
body: Ruma<suspend_user::v1::Request>,
|
||||
) -> Result<suspend_user::v1::Response> {
|
||||
let sender_user = body.identity.expect_sender_user()?;
|
||||
|
||||
let (sender_admin, active, target_admin) = join3(
|
||||
@@ -64,13 +64,13 @@ pub(crate) async fn put_suspended_status(
|
||||
}
|
||||
if services.users.is_suspended(&body.user_id).await? == body.suspended {
|
||||
// No change
|
||||
return Ok(set_suspended::v1::Response::new(body.suspended));
|
||||
return Ok(suspend_user::v1::Response::new(body.suspended));
|
||||
}
|
||||
|
||||
let action = if body.suspended {
|
||||
services
|
||||
.users
|
||||
.suspend_account(&body.user_id, sender_user)
|
||||
.suspend_account(&body.user_id, body.identity.sender_user())
|
||||
.await;
|
||||
"suspended"
|
||||
} else {
|
||||
@@ -86,5 +86,5 @@ pub(crate) async fn put_suspended_status(
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(set_suspended::v1::Response::new(body.suspended))
|
||||
Ok(suspend_user::v1::Response::new(body.suspended))
|
||||
}
|
||||
|
||||
@@ -31,6 +31,12 @@ pub(crate) async fn get_profile_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<get_profile::v3::Request>,
|
||||
) -> Result<get_profile::v3::Response> {
|
||||
if services.config.require_auth_for_profile_requests && body.identity.is_none() {
|
||||
return Err!(Request(Unauthorized(
|
||||
"This server requires authentication to view user profiles."
|
||||
)));
|
||||
}
|
||||
|
||||
let Some(profile) = fetch_full_profile(&services, &body.user_id).await else {
|
||||
return Err!(Request(NotFound("This user's profile could not be fetched.")));
|
||||
};
|
||||
@@ -42,6 +48,12 @@ pub(crate) async fn get_profile_field_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<get_profile_field::v3::Request>,
|
||||
) -> Result<get_profile_field::v3::Response> {
|
||||
if services.config.require_auth_for_profile_requests && body.identity.is_none() {
|
||||
return Err!(Request(Unauthorized(
|
||||
"This server requires authentication to view user profiles."
|
||||
)));
|
||||
}
|
||||
|
||||
let value = fetch_profile_field(&services, &body.user_id, body.field.clone()).await?;
|
||||
|
||||
Ok(assign!(get_profile_field::v3::Response::default(), { value }))
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
pub mod router;
|
||||
pub mod server;
|
||||
|
||||
pub mod admin;
|
||||
|
||||
pub(crate) use self::router::{Ruma, RumaResponse, State};
|
||||
|
||||
conduwuit::mod_ctor! {}
|
||||
|
||||
+16
-4
@@ -16,7 +16,9 @@
|
||||
|
||||
use self::handler::RouterExt;
|
||||
pub(super) use self::{args::Args as Ruma, auth::ClientIdentity, response::RumaResponse};
|
||||
use crate::{admin, client, server};
|
||||
#[cfg(feature = "admin_api")]
|
||||
use crate::client::admin::site as admin_api;
|
||||
use crate::{client, server};
|
||||
|
||||
pub fn build(router: Router<State>, state: State) -> Router<State> {
|
||||
let config = &state.server.config;
|
||||
@@ -181,6 +183,8 @@ pub fn build(router: Router<State>, state: State) -> Router<State> {
|
||||
.ruma_route(&client::get_room_summary)
|
||||
.ruma_route(&client::get_suspended_status)
|
||||
.ruma_route(&client::put_suspended_status)
|
||||
.ruma_route(&client::get_locked_status)
|
||||
.ruma_route(&client::put_locked_status)
|
||||
.ruma_route(&client::well_known_support)
|
||||
.ruma_route(&client::well_known_client)
|
||||
.ruma_route(&client::well_known_policy_server)
|
||||
@@ -189,9 +193,7 @@ pub fn build(router: Router<State>, state: State) -> Router<State> {
|
||||
.ruma_route(&client::get_authorization_server_metadata_route)
|
||||
.merge(client::oauth::router(state))
|
||||
.route("/_conduwuit/server_version", get(client::conduwuit_server_version))
|
||||
.route("/_continuwuity/server_version", get(client::conduwuit_server_version))
|
||||
.ruma_route(&admin::rooms::ban::ban_room)
|
||||
.ruma_route(&admin::rooms::list::list_rooms);
|
||||
.route("/_continuwuity/server_version", get(client::conduwuit_server_version));
|
||||
|
||||
if config.allow_federation {
|
||||
router = router
|
||||
@@ -276,6 +278,16 @@ pub fn build(router: Router<State>, state: State) -> Router<State> {
|
||||
.route("/_matrix/media/r0/preview_url", any(redirect_legacy_preview));
|
||||
}
|
||||
|
||||
#[cfg(feature = "admin_api")]
|
||||
{
|
||||
router = router
|
||||
.ruma_route(&admin_api::users::list_users_route)
|
||||
.ruma_route(&admin_api::users::create_user_route)
|
||||
.ruma_route(&admin_api::rooms::ban_room)
|
||||
.ruma_route(&admin_api::rooms::legacy_list_rooms_route)
|
||||
.ruma_route(&admin_api::rooms::list_rooms_route);
|
||||
};
|
||||
|
||||
router
|
||||
}
|
||||
|
||||
|
||||
+184
-145
@@ -1,11 +1,14 @@
|
||||
use std::any::{Any, TypeId};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
fmt::Display,
|
||||
};
|
||||
|
||||
use conduwuit::{Err, Error, Result, err};
|
||||
use http::StatusCode;
|
||||
use ruma::{
|
||||
DeviceId, OwnedDeviceId, OwnedServerName, OwnedUserId, UserId,
|
||||
api::{
|
||||
IncomingRequest,
|
||||
IncomingRequest, OAuthScope,
|
||||
auth_scheme::{
|
||||
AccessToken, AccessTokenOptional, AppserviceToken, AppserviceTokenOptional,
|
||||
AuthScheme, NoAccessToken, NoAuthentication,
|
||||
@@ -76,68 +79,66 @@ pub(crate) fn appservice_info(&self) -> Option<&RegistrationInfo> {
|
||||
pub(crate) fn is_appservice(&self) -> bool { matches!(self, Self::Appservice { .. }) }
|
||||
}
|
||||
|
||||
impl Display for ClientIdentity {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
| Self::User { sender_user, sender_device } =>
|
||||
write!(f, "{sender_user} ({sender_device})"),
|
||||
| Self::Appservice { sender_user, appservice_info, .. } =>
|
||||
write!(f, "appservice `{}` using {sender_user}", appservice_info.registration.id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait CheckAuth: AuthScheme {
|
||||
type Identity: Send;
|
||||
|
||||
fn authenticate<R: IncomingRequest + Any, B: AsRef<[u8]> + Sync>(
|
||||
fn authenticate<R: IncomingRequest<Authentication = Self> + Any, B: AsRef<[u8]> + Sync>(
|
||||
services: &Services,
|
||||
incoming_request: &hyper::Request<B>,
|
||||
query: AuthQueryParams,
|
||||
) -> impl Future<Output = Result<Self::Identity>> + Send {
|
||||
async move {
|
||||
let route = TypeId::of::<R>();
|
||||
|
||||
let output = Self::extract_authentication(incoming_request).map_err(|err| {
|
||||
err!(Request(Unauthorized(warn!(
|
||||
"Failed to extract authorization: {}",
|
||||
"Failed to extract request authentication: {}",
|
||||
err.into()
|
||||
))))
|
||||
})?;
|
||||
|
||||
Self::verify(services, output, incoming_request, query, route).await
|
||||
Self::verify::<R, B>(services, output, incoming_request, query).await
|
||||
}
|
||||
}
|
||||
|
||||
fn verify<B: AsRef<[u8]> + Sync>(
|
||||
fn verify<R: IncomingRequest<Authentication = Self> + Any, B: AsRef<[u8]> + Sync>(
|
||||
services: &Services,
|
||||
output: Self::Output,
|
||||
request: &hyper::Request<B>,
|
||||
query: AuthQueryParams,
|
||||
route: TypeId,
|
||||
) -> impl Future<Output = Result<Self::Identity>> + Send;
|
||||
}
|
||||
|
||||
impl CheckAuth for ServerSignatures {
|
||||
type Identity = OwnedServerName;
|
||||
|
||||
async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
async fn verify<R: IncomingRequest<Authentication = Self> + Any, B: AsRef<[u8]> + Sync>(
|
||||
services: &Services,
|
||||
output: Self::Output,
|
||||
request: &hyper::Request<B>,
|
||||
_query: AuthQueryParams,
|
||||
_route: TypeId,
|
||||
) -> Result<Self::Identity> {
|
||||
let destination = services.globals.server_name();
|
||||
if output
|
||||
.destination
|
||||
.as_ref()
|
||||
.is_some_and(|supplied_destination| supplied_destination != destination)
|
||||
{
|
||||
return Err!(Request(Unauthorized("Destination mismatch.")));
|
||||
}
|
||||
|
||||
let key = services
|
||||
.server_keys
|
||||
.get_verify_key(&output.origin, &output.key)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
err!(Request(Unauthorized(warn!("Failed to fetch signing keys: {e}"))))
|
||||
.map_err(|err| {
|
||||
err!(Request(Unauthorized(warn!("Failed to fetch signing keys: {err}"))))
|
||||
})?;
|
||||
|
||||
let keys: PubKeys = [(output.key.to_string(), key.key)].into();
|
||||
let keys: PubKeyMap = [(output.origin.as_str().into(), keys)].into();
|
||||
|
||||
match output.verify_request(request, destination, &keys) {
|
||||
match output.verify_request(request, services.globals.server_name(), &keys) {
|
||||
| Ok(()) => {
|
||||
if services
|
||||
.moderation
|
||||
@@ -159,112 +160,36 @@ async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
impl CheckAuth for AccessToken {
|
||||
type Identity = ClientIdentity;
|
||||
|
||||
async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
async fn verify<R: IncomingRequest<Authentication = Self> + Any, B: AsRef<[u8]> + Sync>(
|
||||
services: &Services,
|
||||
output: Self::Output,
|
||||
_request: &hyper::Request<B>,
|
||||
query: AuthQueryParams,
|
||||
route: TypeId,
|
||||
) -> Result<Self::Identity> {
|
||||
if let Some((sender_user, sender_device, status)) =
|
||||
services.users.find_from_token(&output).await
|
||||
{
|
||||
// If the token is expired we return a soft logout
|
||||
if matches!(status, AccessTokenStatus::Expired) {
|
||||
return Err(Error::Request(
|
||||
ErrorKind::UnknownToken(
|
||||
assign!(UnknownTokenErrorData::new(), { soft_logout: true }),
|
||||
),
|
||||
"This token has expired".into(),
|
||||
StatusCode::UNAUTHORIZED,
|
||||
));
|
||||
}
|
||||
|
||||
// Locked users can only use /logout and /logout/all
|
||||
if services
|
||||
.users
|
||||
.is_locked(&sender_user)
|
||||
.await
|
||||
.is_ok_and(std::convert::identity)
|
||||
{
|
||||
if !(route == TypeId::of::<client::session::logout::v3::Request>()
|
||||
|| route == TypeId::of::<client::session::logout_all::v3::Request>())
|
||||
{
|
||||
return Err!(Request(UserLocked("Your account is locked.")));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ClientIdentity::User { sender_user, sender_device })
|
||||
} else if let Ok(appservice_info) = services.appservice.find_from_token(&output).await {
|
||||
let Ok(sender_user) = query.user_id.clone().map_or_else(
|
||||
|| {
|
||||
UserId::parse_with_server_name(
|
||||
appservice_info.registration.sender_localpart.as_str(),
|
||||
services.globals.server_name(),
|
||||
)
|
||||
},
|
||||
UserId::parse,
|
||||
) else {
|
||||
return Err!(Request(InvalidUsername("Username is invalid.")));
|
||||
};
|
||||
|
||||
if !appservice_info.is_user_match(&sender_user) {
|
||||
return Err!(Request(Exclusive("User is not in namespace.")));
|
||||
}
|
||||
|
||||
// MSC3202/MSC4190: Handle device_id masquerading for appservices.
|
||||
// The device_id can be provided via `device_id` or
|
||||
// `org.matrix.msc3202.device_id` query parameter.
|
||||
let sender_device =
|
||||
if let Some(device_id) = query.device_id.as_deref().map(Into::into) {
|
||||
// Verify the device exists for this user
|
||||
if services
|
||||
.users
|
||||
.get_device_metadata(&sender_user, device_id)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return Err!(Request(Forbidden(
|
||||
"Device does not exist for user or appservice cannot masquerade as \
|
||||
this device."
|
||||
)));
|
||||
}
|
||||
|
||||
Some(device_id.to_owned())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(ClientIdentity::Appservice {
|
||||
sender_user,
|
||||
sender_device,
|
||||
appservice_info: Box::new(appservice_info),
|
||||
})
|
||||
} else {
|
||||
Err(Error::Request(
|
||||
ErrorKind::UnknownToken(UnknownTokenErrorData::new()),
|
||||
"Invalid token".into(),
|
||||
StatusCode::UNAUTHORIZED,
|
||||
))
|
||||
}
|
||||
verify_access_token(services, output, query, TypeId::of::<R>(), R::required_scopes())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl CheckAuth for AccessTokenOptional {
|
||||
type Identity = Option<ClientIdentity>;
|
||||
|
||||
async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
async fn verify<R: IncomingRequest<Authentication = Self> + Any, B: AsRef<[u8]> + Sync>(
|
||||
services: &Services,
|
||||
output: Self::Output,
|
||||
request: &hyper::Request<B>,
|
||||
_request: &hyper::Request<B>,
|
||||
query: AuthQueryParams,
|
||||
route: TypeId,
|
||||
) -> Result<Self::Identity> {
|
||||
match output {
|
||||
| Some(token) =>
|
||||
<AccessToken as CheckAuth>::verify(services, token, request, query, route)
|
||||
.await
|
||||
.map(Some),
|
||||
| Some(token) => verify_access_token(
|
||||
services,
|
||||
token,
|
||||
query,
|
||||
TypeId::of::<R>(),
|
||||
R::required_scopes(),
|
||||
)
|
||||
.await
|
||||
.map(Some),
|
||||
| None => Ok(None),
|
||||
}
|
||||
}
|
||||
@@ -273,36 +198,29 @@ async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
impl CheckAuth for AppserviceToken {
|
||||
type Identity = RegistrationInfo;
|
||||
|
||||
async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
async fn verify<R: IncomingRequest<Authentication = Self> + Any, B: AsRef<[u8]> + Sync>(
|
||||
services: &Services,
|
||||
output: Self::Output,
|
||||
_request: &hyper::Request<B>,
|
||||
_query: AuthQueryParams,
|
||||
_route: TypeId,
|
||||
) -> Result<Self::Identity> {
|
||||
let Ok(appservice_info) = services.appservice.find_from_token(&output).await else {
|
||||
return Err!(Request(Unauthorized("Invalid appservice token.")));
|
||||
};
|
||||
|
||||
Ok(appservice_info)
|
||||
verify_appservice_access_token(services, output).await
|
||||
}
|
||||
}
|
||||
|
||||
impl CheckAuth for AppserviceTokenOptional {
|
||||
type Identity = Option<RegistrationInfo>;
|
||||
|
||||
async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
async fn verify<R: IncomingRequest<Authentication = Self> + Any, B: AsRef<[u8]> + Sync>(
|
||||
services: &Services,
|
||||
output: Self::Output,
|
||||
request: &hyper::Request<B>,
|
||||
query: AuthQueryParams,
|
||||
route: TypeId,
|
||||
_request: &hyper::Request<B>,
|
||||
_query: AuthQueryParams,
|
||||
) -> Result<Self::Identity> {
|
||||
match output {
|
||||
| Some(token) =>
|
||||
<AppserviceToken as CheckAuth>::verify(services, token, request, query, route)
|
||||
.await
|
||||
.map(Some),
|
||||
| Some(token) => verify_appservice_access_token(services, token)
|
||||
.await
|
||||
.map(Some),
|
||||
| None => Ok(None),
|
||||
}
|
||||
}
|
||||
@@ -311,12 +229,11 @@ async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
impl CheckAuth for NoAuthentication {
|
||||
type Identity = ();
|
||||
|
||||
async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
async fn verify<R: IncomingRequest<Authentication = Self> + Any, B: AsRef<[u8]> + Sync>(
|
||||
_services: &Services,
|
||||
_output: Self::Output,
|
||||
_request: &hyper::Request<B>,
|
||||
_query: AuthQueryParams,
|
||||
_route: TypeId,
|
||||
) -> Result<Self::Identity> {
|
||||
Ok(())
|
||||
}
|
||||
@@ -325,31 +242,153 @@ async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
impl CheckAuth for NoAccessToken {
|
||||
type Identity = Option<ClientIdentity>;
|
||||
|
||||
async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
async fn verify<R: IncomingRequest<Authentication = Self> + Any, B: AsRef<[u8]> + Sync>(
|
||||
services: &Services,
|
||||
_output: Self::Output,
|
||||
request: &hyper::Request<B>,
|
||||
query: AuthQueryParams,
|
||||
route: TypeId,
|
||||
) -> Result<Self::Identity> {
|
||||
// We handle these the same as AccessTokenOptional
|
||||
let token = AccessTokenOptional::extract_authentication(request).map_err(|err| {
|
||||
err!(Request(Unauthorized(warn!("Failed to extract authorization: {}", err))))
|
||||
})?;
|
||||
|
||||
// Check special access restrictions
|
||||
if (route == TypeId::of::<client::profile::get_avatar_url::v3::Request>()
|
||||
|| route == TypeId::of::<client::profile::get_display_name::v3::Request>()
|
||||
|| route == TypeId::of::<client::profile::get_profile_field::v3::Request>()
|
||||
|| route == TypeId::of::<client::profile::get_profile::v3::Request>())
|
||||
&& services.config.require_auth_for_profile_requests
|
||||
&& token.is_none()
|
||||
{
|
||||
return Err!(Request(Unauthorized(
|
||||
"This server requires authentication to access user profiles."
|
||||
)));
|
||||
match token {
|
||||
| Some(token) => verify_access_token(
|
||||
services,
|
||||
token,
|
||||
query,
|
||||
TypeId::of::<R>(),
|
||||
// Assume that no scopes are required for these endpoints since
|
||||
// ostensibly they don't require authentication
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.map(Some),
|
||||
| None => Ok(None),
|
||||
}
|
||||
|
||||
<AccessTokenOptional as CheckAuth>::verify(services, token, request, query, route).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn verify_access_token(
|
||||
services: &Services,
|
||||
output: String,
|
||||
query: AuthQueryParams,
|
||||
route: TypeId,
|
||||
required_scopes: &[OAuthScope],
|
||||
) -> Result<ClientIdentity> {
|
||||
if let Some((sender_user, sender_device, status)) =
|
||||
services.users.find_from_token(&output).await
|
||||
{
|
||||
// If the token is expired we return a soft logout
|
||||
if matches!(status, AccessTokenStatus::Expired) {
|
||||
return Err(Error::Request(
|
||||
ErrorKind::UnknownToken(
|
||||
assign!(UnknownTokenErrorData::new(), { soft_logout: true }),
|
||||
),
|
||||
"This access token has expired.".into(),
|
||||
StatusCode::UNAUTHORIZED,
|
||||
));
|
||||
}
|
||||
|
||||
// Locked users can only use /logout and /logout/all
|
||||
if services
|
||||
.users
|
||||
.is_locked(&sender_user)
|
||||
.await
|
||||
.is_ok_and(std::convert::identity)
|
||||
{
|
||||
if !(route == TypeId::of::<client::session::logout::v3::Request>()
|
||||
|| route == TypeId::of::<client::session::logout_all::v3::Request>())
|
||||
{
|
||||
return Err!(Request(UserLocked("Your account is locked.")));
|
||||
}
|
||||
}
|
||||
|
||||
// If this device is bound to an OAuth session, check its scopes. This will also
|
||||
// handle admin-only endpoints for OAuth clients.
|
||||
if let Some(session) = services
|
||||
.oauth
|
||||
.get_session_info_for_device(&sender_user, &sender_device)
|
||||
.await
|
||||
{
|
||||
if required_scopes
|
||||
.iter()
|
||||
.all(|scope| !session.scopes.contains(scope))
|
||||
{
|
||||
return Err!(Request(Forbidden(
|
||||
"You don't have the necessary scopes to use this endpoint."
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
// Otherwise, explicitly check if the endpoint is restricted to admins only.
|
||||
if required_scopes.contains(&OAuthScope::ServerAdministration)
|
||||
&& !services.users.is_admin(&sender_user).await
|
||||
{
|
||||
return Err!(Request(Forbidden(
|
||||
"Only server administrators can use this endpoint."
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ClientIdentity::User { sender_user, sender_device })
|
||||
} else if let Ok(appservice_info) = services.appservice.find_from_token(&output).await {
|
||||
let Ok(sender_user) = query.user_id.clone().map_or_else(
|
||||
|| {
|
||||
UserId::parse_with_server_name(
|
||||
appservice_info.registration.sender_localpart.as_str(),
|
||||
services.globals.server_name(),
|
||||
)
|
||||
},
|
||||
UserId::parse,
|
||||
) else {
|
||||
return Err!(Request(InvalidUsername("Username is invalid.")));
|
||||
};
|
||||
|
||||
if !appservice_info.is_user_match(&sender_user) {
|
||||
return Err!(Request(Exclusive("User is not in this appservice's namespace.")));
|
||||
}
|
||||
|
||||
// MSC3202/MSC4190: Handle device_id masquerading for appservices.
|
||||
// The device_id can be provided via `device_id` or
|
||||
// `org.matrix.msc3202.device_id` query parameter.
|
||||
let sender_device = if let Some(device_id) = query.device_id.as_deref().map(Into::into) {
|
||||
// Verify the device exists for this user
|
||||
if services
|
||||
.users
|
||||
.get_device_metadata(&sender_user, device_id)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return Err!(Request(Forbidden("Appservice cannot masquerade as this device.")));
|
||||
}
|
||||
|
||||
Some(device_id.to_owned())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(ClientIdentity::Appservice {
|
||||
sender_user,
|
||||
sender_device,
|
||||
appservice_info: Box::new(appservice_info),
|
||||
})
|
||||
} else {
|
||||
Err(Error::Request(
|
||||
ErrorKind::UnknownToken(UnknownTokenErrorData::new()),
|
||||
"Invalid access token.".into(),
|
||||
StatusCode::UNAUTHORIZED,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn verify_appservice_access_token(
|
||||
services: &Services,
|
||||
output: String,
|
||||
) -> Result<RegistrationInfo> {
|
||||
let Ok(appservice_info) = services.appservice.find_from_token(&output).await else {
|
||||
return Err!(Request(Unauthorized("Invalid appservice token.")));
|
||||
};
|
||||
|
||||
Ok(appservice_info)
|
||||
}
|
||||
|
||||
@@ -2367,7 +2367,6 @@ pub struct RegistrationTerms {
|
||||
/// The language code to provide to clients along with the policy documents.
|
||||
///
|
||||
/// default: "en"
|
||||
#[serde(default = "default_terms_language")]
|
||||
pub language: String,
|
||||
/// Policy documents, such as terms and conditions or a privacy policy,
|
||||
/// which users must agree to when registering an account.
|
||||
@@ -2377,6 +2376,8 @@ pub struct RegistrationTerms {
|
||||
/// [global.registration_terms.documents]
|
||||
/// privacy_policy = { name = "Privacy Policy", url = "https://homeserver.example/en/privacy_policy.html" }
|
||||
/// ```
|
||||
///
|
||||
/// default: {}
|
||||
pub documents: BTreeMap<String, TermsDocument>,
|
||||
}
|
||||
|
||||
@@ -2804,5 +2805,3 @@ fn default_client_response_timeout() -> u64 { 120 }
|
||||
fn default_client_shutdown_timeout() -> u64 { 15 }
|
||||
|
||||
fn default_sender_shutdown_timeout() -> u64 { 5 }
|
||||
|
||||
fn default_terms_language() -> String { "en".to_owned() }
|
||||
|
||||
@@ -118,7 +118,7 @@ pub enum Error {
|
||||
#[error(transparent)]
|
||||
Mxid(#[from] ruma::IdParseError),
|
||||
#[error("from {0}: {1}")]
|
||||
Redaction(ruma::OwnedServerName, ruma::canonical_json::RedactionError),
|
||||
Redaction(ruma::OwnedServerName, ruma::canonical_json::CanonicalJsonFieldError),
|
||||
#[error("{0:?}: {1}")]
|
||||
Request(ErrorKind, Cow<'static, str>, http::StatusCode),
|
||||
#[error(transparent)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{borrow::Borrow, collections::BTreeSet};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use futures::{
|
||||
Future,
|
||||
@@ -824,7 +824,7 @@ struct GetThirdPartyInvite {
|
||||
|
||||
let prev_event_is_create_event = prev_events
|
||||
.next()
|
||||
.is_some_and(|event_id| event_id.borrow() == create_room.event_id().borrow());
|
||||
.is_some_and(|event_id| event_id == create_room.event_id());
|
||||
let no_more_prev_events = prev_events.next().is_none();
|
||||
|
||||
if prev_event_is_create_event && no_more_prev_events {
|
||||
|
||||
@@ -44,5 +44,6 @@ pub fn unstable_features() -> BTreeMap<String, bool> {
|
||||
("uk.timedout.msc4323".to_owned(), true), /* agnostic suspend (https://github.com/matrix-org/matrix-spec-proposals/pull/4323) */
|
||||
("org.matrix.msc4155".to_owned(), true), /* invite filtering (https://github.com/matrix-org/matrix-spec-proposals/pull/4155) */
|
||||
("computer.gingershaped.msc4466".to_owned(), true), /* profile change propagation (https://github.com/matrix-org/matrix-spec-proposals/pull/4466) */
|
||||
("org.continuwuity.msc4484.unstable".to_owned(), true), /* server admin oauth scope (https://github.com/matrix-org/matrix-spec-proposals/pull/4484) */
|
||||
])
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ full = [
|
||||
"jemalloc_prof",
|
||||
"perf_measurements",
|
||||
"tokio_console",
|
||||
"conduwuit-api/admin_api",
|
||||
]
|
||||
|
||||
brotli_compression = [
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod rooms;
|
||||
pub mod users;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pub mod v1 {
|
||||
use ruma::{
|
||||
OwnedRoomAliasId, OwnedRoomId, OwnedUserId,
|
||||
api::{auth_scheme::AccessToken, request, response},
|
||||
api::{OAuthScope, auth_scheme::AccessToken, request, response},
|
||||
metadata,
|
||||
};
|
||||
|
||||
@@ -9,8 +9,10 @@ pub mod v1 {
|
||||
method: PUT,
|
||||
rate_limited: false,
|
||||
authentication: AccessToken,
|
||||
required_scopes: [OAuthScope::ServerAdministration],
|
||||
history: {
|
||||
1.0 => "/_continuwuity/admin/rooms/{room_id}/ban",
|
||||
unstable("org.continuwuity.admin") => "/_continuwuity/admin/rooms/{room_id}/ban",
|
||||
1.0 => "/_continuwuity/admin/v1/rooms/{room_id}/ban",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,8 +31,11 @@ pub struct Request {
|
||||
|
||||
#[response]
|
||||
pub struct Response {
|
||||
/// Users who were successfully kicked from this room.
|
||||
pub kicked_users: Vec<OwnedUserId>,
|
||||
/// Users who could not be kicked from the room.
|
||||
pub failed_kicked_users: Vec<OwnedUserId>,
|
||||
/// Any local aliases that were removed from the room.
|
||||
pub local_aliases: Vec<OwnedRoomAliasId>,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pub mod v1 {
|
||||
pub mod unstable {
|
||||
use ruma::{
|
||||
OwnedRoomId,
|
||||
api::{auth_scheme::AccessToken, request, response},
|
||||
api::{OAuthScope, auth_scheme::AccessToken, request, response},
|
||||
metadata,
|
||||
};
|
||||
|
||||
@@ -9,8 +9,9 @@ pub mod v1 {
|
||||
method: GET,
|
||||
rate_limited: false,
|
||||
authentication: AccessToken,
|
||||
required_scopes: [OAuthScope::ServerAdministration],
|
||||
history: {
|
||||
1.0 => "/_continuwuity/admin/rooms/list",
|
||||
unstable => "/_continuwuity/admin/rooms/list",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +21,7 @@ pub mod v1 {
|
||||
|
||||
#[response]
|
||||
pub struct Response {
|
||||
/// A list of room IDs known to this server.
|
||||
pub rooms: Vec<OwnedRoomId>,
|
||||
}
|
||||
|
||||
@@ -33,3 +35,133 @@ impl Response {
|
||||
pub fn new(rooms: Vec<OwnedRoomId>) -> Self { Self { rooms } }
|
||||
}
|
||||
}
|
||||
|
||||
pub mod v1 {
|
||||
use ruma::{
|
||||
OwnedRoomId, OwnedUserId, RoomVersionId,
|
||||
api::{auth_scheme::AccessToken, request, response},
|
||||
events::room::{
|
||||
canonical_alias::PossiblyRedactedRoomCanonicalAliasEventContent,
|
||||
history_visibility::PossiblyRedactedRoomHistoryVisibilityEventContent,
|
||||
join_rules::PossiblyRedactedRoomJoinRulesEventContent,
|
||||
name::PossiblyRedactedRoomNameEventContent,
|
||||
topic::PossiblyRedactedRoomTopicEventContent,
|
||||
},
|
||||
metadata,
|
||||
serde::{default_true, is_default},
|
||||
};
|
||||
|
||||
metadata! {
|
||||
method: GET,
|
||||
rate_limited: false,
|
||||
authentication: AccessToken,
|
||||
history: {
|
||||
1.0 => "/_continuwuity/admin/v1/rooms",
|
||||
}
|
||||
}
|
||||
|
||||
#[request]
|
||||
#[derive(Default)]
|
||||
pub struct Request {
|
||||
/// The maximum number of results to return in this page. Maximum (and
|
||||
/// default) is 100.
|
||||
#[ruma_api(query)]
|
||||
#[serde(default, skip_serializing_if = "is_default")]
|
||||
pub limit: Option<usize>,
|
||||
|
||||
/// The number of results to skip over before returning results. Default
|
||||
/// is 0.
|
||||
#[ruma_api(query)]
|
||||
#[serde(default, skip_serializing_if = "is_default")]
|
||||
pub offset: Option<usize>,
|
||||
|
||||
/// If true, includes banned rooms in the response.
|
||||
#[ruma_api(query)]
|
||||
#[serde(default, skip_serializing_if = "is_default")]
|
||||
pub include_banned_rooms: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct MinimalRoomInfo {
|
||||
/// The room's unique ID.
|
||||
pub room_id: OwnedRoomId,
|
||||
/// If true, this room is banned, and cannot be joined by non-admins.
|
||||
#[serde(default, skip_serializing_if = "is_default")]
|
||||
pub banned: bool,
|
||||
/// If true, this room has federation disabled, but can still be locally
|
||||
/// used.
|
||||
#[serde(default, skip_serializing_if = "is_default")]
|
||||
pub disabled: bool,
|
||||
/// The total number of joined members in this room.
|
||||
#[serde(default, skip_serializing_if = "is_default")]
|
||||
pub member_count: usize,
|
||||
/// The total number of joined members in this room that are local to
|
||||
/// this server.
|
||||
#[serde(default, skip_serializing_if = "is_default")]
|
||||
pub local_member_count: usize,
|
||||
/// The number of unique homeservers currently joined to this room.
|
||||
#[serde(default, skip_serializing_if = "is_default")]
|
||||
pub resident_server_count: usize,
|
||||
/// The users who created this room.
|
||||
///
|
||||
/// The first entry is always the sender of the `m.room.create` event.
|
||||
/// Any entries thereafter are additional creators in v12+ rooms. An
|
||||
/// empty vec indicates the room is not known.
|
||||
#[serde(default, skip_serializing_if = "is_default")]
|
||||
pub creators: Vec<OwnedUserId>,
|
||||
/// If true, this room has encryption enabled.
|
||||
#[serde(default, skip_serializing_if = "is_default")]
|
||||
pub encrypted: bool,
|
||||
/// If true, this room is allowed to be federated (`m.federate` is not
|
||||
/// `false` in `m.room.create`).
|
||||
#[serde(default = "default_true", skip_serializing_if = "is_default")]
|
||||
pub federated: bool,
|
||||
/// If true, this room is published to this server's room directory.
|
||||
#[serde(default, skip_serializing_if = "is_default")]
|
||||
pub published: bool,
|
||||
/// The version of the room.
|
||||
pub version: RoomVersionId,
|
||||
/// The event content for the `m.room.name` event, if any is present.
|
||||
/// May be redacted.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<PossiblyRedactedRoomNameEventContent>,
|
||||
/// The event content for the `m.room.topic` event, if any is present.
|
||||
/// May be redacted.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub topic: Option<PossiblyRedactedRoomTopicEventContent>,
|
||||
/// The event content for the `m.room.canonical_alias` event, if any is
|
||||
/// present. May be redacted.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub canonical_alias: Option<PossiblyRedactedRoomCanonicalAliasEventContent>,
|
||||
/// The event content for the `m.room.join_rules` event, if any is
|
||||
/// present. May be redacted.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub join_rules: Option<PossiblyRedactedRoomJoinRulesEventContent>,
|
||||
/// The event content for the `m.room.history_visibility` event, if any
|
||||
/// is present. May be redacted.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub history_visibility: Option<PossiblyRedactedRoomHistoryVisibilityEventContent>,
|
||||
/// The ID of the room which replaces this one, if any.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub successor: Option<OwnedRoomId>,
|
||||
/// The ID of the room which preceded this one, if any.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub predecessor: Option<OwnedRoomId>,
|
||||
}
|
||||
|
||||
#[response]
|
||||
pub struct Response {
|
||||
/// A list of rooms known to this server.
|
||||
pub rooms: Vec<MinimalRoomInfo>,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
#[must_use]
|
||||
pub fn new() -> Self { Self::default() }
|
||||
}
|
||||
|
||||
impl Response {
|
||||
#[must_use]
|
||||
pub fn new(rooms: Vec<MinimalRoomInfo>) -> Self { Self { rooms } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
pub mod v1 {
|
||||
use ruma::{
|
||||
OwnedMxcUri, OwnedRoomOrAliasId, OwnedUserId,
|
||||
api::{OAuthScope, auth_scheme::AccessToken, request, response},
|
||||
metadata,
|
||||
};
|
||||
|
||||
metadata! {
|
||||
method: POST,
|
||||
rate_limited: false,
|
||||
authentication: AccessToken,
|
||||
required_scopes: [OAuthScope::ServerAdministration],
|
||||
history: {
|
||||
1.0 => "/_continuwuity/admin/v1/users/create",
|
||||
},
|
||||
}
|
||||
|
||||
#[request]
|
||||
pub struct Request {
|
||||
/// The user's localpart (the identifier between `@` and `:`). Cannot be
|
||||
/// blank.
|
||||
pub localpart: String,
|
||||
|
||||
/// The user's desired password. Cannot be blank.
|
||||
pub password: String,
|
||||
|
||||
/// The user's email address, if any.
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub email: Option<String>,
|
||||
|
||||
/// The display name to set upon creation.
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub display_name: Option<String>,
|
||||
|
||||
/// The avatar URI to set upon creation.
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub avatar_url: Option<OwnedMxcUri>,
|
||||
|
||||
/// Suspends the user immediately upon creation. They can still log in.
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub suspended: bool,
|
||||
|
||||
/// Locks the user immediately upon creation. They will receive
|
||||
/// M_USER_LOCKED upon login.
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub locked: bool,
|
||||
|
||||
/// Disables the user's login immediately upon creation.
|
||||
///
|
||||
/// The user can still be used if an admin generates an access token for
|
||||
/// the account, but the user will not be able to use `POST
|
||||
/// /_matrix/client/v3/login`.
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub login_disabled: bool,
|
||||
|
||||
/// Promotes the user to a server administrator immediately upon
|
||||
/// creation.
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub admin: bool,
|
||||
|
||||
/// Skips joining rooms in the server's configured auto_join_rooms.
|
||||
///
|
||||
/// If this is false, all rooms in the config.toml's `auto_join_rooms`
|
||||
/// will be automatically joined upon creation. If `auto_join_rooms`
|
||||
/// is supplied in this request too, those rooms will be joined
|
||||
/// afterwards.
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub skip_auto_join: bool,
|
||||
|
||||
/// Additional rooms to auto-join the new user to. If `skip_auto_join`
|
||||
/// is `true`, these rooms will still be joined.
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub auto_join_rooms: Vec<OwnedRoomOrAliasId>,
|
||||
}
|
||||
|
||||
#[response]
|
||||
pub struct Response {
|
||||
/// The fully qualified user ID of the newly created user.
|
||||
pub user_id: OwnedUserId,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
#[must_use]
|
||||
pub fn new(localpart: String, password: String) -> Self {
|
||||
Self {
|
||||
localpart,
|
||||
password,
|
||||
email: None,
|
||||
display_name: None,
|
||||
avatar_url: None,
|
||||
suspended: false,
|
||||
locked: false,
|
||||
login_disabled: false,
|
||||
admin: false,
|
||||
skip_auto_join: false,
|
||||
auto_join_rooms: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Response {
|
||||
#[must_use]
|
||||
pub fn new(user_id: OwnedUserId) -> Self { Self { user_id } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
pub mod v1 {
|
||||
use ruma::{
|
||||
OwnedUserId,
|
||||
api::{OAuthScope, auth_scheme::AccessToken, request, response},
|
||||
metadata,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
metadata! {
|
||||
method: GET,
|
||||
rate_limited: false,
|
||||
authentication: AccessToken,
|
||||
required_scopes: [OAuthScope::ServerAdministration],
|
||||
history: {
|
||||
1.0 => "/_continuwuity/admin/v1/users",
|
||||
}
|
||||
}
|
||||
|
||||
#[request]
|
||||
#[derive(Default)]
|
||||
pub struct Request {
|
||||
/// If true, includes deactivated users in the response.
|
||||
#[ruma_api(query)]
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub include_deactivated: bool,
|
||||
/// If true, includes locked users in the response.
|
||||
#[ruma_api(query)]
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub include_locked: bool,
|
||||
/// If true, includes suspended users in the response.
|
||||
#[ruma_api(query)]
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub include_suspended: bool,
|
||||
|
||||
/// The maximum number of results to return in this page. Maximum (and
|
||||
/// default) is 100.
|
||||
#[ruma_api(query)]
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub limit: Option<usize>,
|
||||
|
||||
/// The number of results to skip over before returning results. Default
|
||||
/// is 0.
|
||||
#[ruma_api(query)]
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub offset: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, serde::Serialize)]
|
||||
pub struct User {
|
||||
/// The full user ID of the user.
|
||||
pub user_id: OwnedUserId,
|
||||
|
||||
/// Whether this user is deactivated.
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub deactivated: bool,
|
||||
|
||||
/// Whether this user is suspended.
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub suspended: bool,
|
||||
|
||||
/// Whether this user is locked.
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub locked: bool,
|
||||
|
||||
/// Whether this user is an admin.
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub admin: bool,
|
||||
|
||||
/// Whether this user has their login disabled.
|
||||
#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
|
||||
pub login_disabled: bool,
|
||||
}
|
||||
|
||||
impl User {
|
||||
#[must_use]
|
||||
pub fn new(user_id: OwnedUserId) -> Self {
|
||||
Self {
|
||||
user_id,
|
||||
deactivated: false,
|
||||
suspended: false,
|
||||
locked: false,
|
||||
admin: false,
|
||||
login_disabled: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[response]
|
||||
#[derive(Default)]
|
||||
pub struct Response {
|
||||
pub users: Vec<User>,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
#[must_use]
|
||||
pub fn new() -> Self { Self::default() }
|
||||
}
|
||||
|
||||
impl Response {
|
||||
#[must_use]
|
||||
pub fn new(users: Vec<User>) -> Self { Self { users } }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use assign::assign;
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn request_defaults() {
|
||||
let req = Request::new();
|
||||
assert!(!req.include_deactivated && !req.include_locked && !req.include_suspended);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_serialize_omits_default_values() {
|
||||
let user_id = OwnedUserId::try_from("@alice:example.org".to_owned()).unwrap();
|
||||
let user = User::new(user_id.clone());
|
||||
|
||||
let expected = json!({ "user_id": user_id.to_string() });
|
||||
assert_eq!(serde_json::to_value(&user).expect("failed to serialize user"), expected);
|
||||
|
||||
let suspended_user = assign!(user, {suspended: true});
|
||||
let expected2 = json!({ "user_id": "@alice:example.org", "suspended": true});
|
||||
assert_eq!(
|
||||
serde_json::to_value(&suspended_user).expect("failed to serialize user"),
|
||||
expected2
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_defaults() {
|
||||
let response = Response::default();
|
||||
assert!(response.users.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod create;
|
||||
pub mod list;
|
||||
@@ -1,53 +0,0 @@
|
||||
//! `GET /_matrix/client/v1/admin/suspend/{userId}`
|
||||
//!
|
||||
//! Check the suspension status of a target user
|
||||
|
||||
pub mod v1 {
|
||||
//! `/_matrix/client/unstable/uk.timedout.msc4323/admin/suspend/{userID}`
|
||||
//! ([msc])
|
||||
//!
|
||||
//! [msc]: https://github.com/matrix-org/matrix-spec-proposals/pull/4323
|
||||
|
||||
use ruma::{
|
||||
OwnedUserId,
|
||||
api::{auth_scheme::AccessToken, request, response},
|
||||
metadata,
|
||||
};
|
||||
|
||||
metadata! {
|
||||
method: GET,
|
||||
rate_limited: false,
|
||||
authentication: AccessToken,
|
||||
history: {
|
||||
unstable => "/_matrix/client/unstable/uk.timedout.msc4323/admin/suspend/{user_id}",
|
||||
1.18 => "/_matrix/client/v1/admin/suspend/{user_id}",
|
||||
}
|
||||
}
|
||||
|
||||
/// Request type for the get & set user suspension status endpoint.
|
||||
#[request(error = ruma::api::error::Error)]
|
||||
pub struct Request {
|
||||
/// The user to look up.
|
||||
#[ruma_api(path)]
|
||||
pub user_id: OwnedUserId,
|
||||
}
|
||||
|
||||
/// Response type for the suspension endpoints
|
||||
#[response(error = ruma::api::error::Error)]
|
||||
pub struct Response {
|
||||
/// Whether the user is currently suspended.
|
||||
pub suspended: bool,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
/// Creates a new `Request` with the given user id.
|
||||
#[must_use]
|
||||
pub fn new(user_id: OwnedUserId) -> Self { Self { user_id } }
|
||||
}
|
||||
|
||||
impl Response {
|
||||
/// Creates a new `Response` with the given suspension status.
|
||||
#[must_use]
|
||||
pub fn new(suspended: bool) -> Self { Self { suspended } }
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1 @@
|
||||
pub mod continuwuity;
|
||||
pub mod get_suspended;
|
||||
pub mod set_suspended;
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
//! `PUT /_matrix/client/v1/admin/suspend/{userId}`
|
||||
//!
|
||||
//! Set the suspension status of a target user
|
||||
|
||||
pub mod v1 {
|
||||
//! `/_matrix/client/unstable/uk.timedout.msc4323/admin/suspend/{userID}`
|
||||
//! ([msc])
|
||||
//!
|
||||
//! [msc]: https://github.com/matrix-org/matrix-spec-proposals/pull/4323
|
||||
|
||||
use ruma::{
|
||||
OwnedUserId,
|
||||
api::{auth_scheme::AccessToken, request, response},
|
||||
metadata,
|
||||
};
|
||||
|
||||
metadata! {
|
||||
method: PUT,
|
||||
rate_limited: false,
|
||||
authentication: AccessToken,
|
||||
history: {
|
||||
unstable => "/_matrix/client/unstable/uk.timedout.msc4323/admin/suspend/{user_id}",
|
||||
1.18 => "/_matrix/client/v1/admin/suspend/{user_id}",
|
||||
}
|
||||
}
|
||||
|
||||
/// Request type for the set user suspension status endpoint.
|
||||
#[request(error = ruma::api::error::Error)]
|
||||
pub struct Request {
|
||||
/// The user to look up.
|
||||
#[ruma_api(path)]
|
||||
pub user_id: OwnedUserId,
|
||||
|
||||
pub suspended: bool,
|
||||
}
|
||||
|
||||
/// Response type for the suspension endpoints
|
||||
#[response(error = ruma::api::error::Error)]
|
||||
pub struct Response {
|
||||
/// Whether the user is currently suspended.
|
||||
pub suspended: bool,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
/// Creates a new `Request` with the given user id.
|
||||
#[must_use]
|
||||
pub fn new(user_id: OwnedUserId, suspended: bool) -> Self { Self { user_id, suspended } }
|
||||
}
|
||||
|
||||
impl Response {
|
||||
/// Creates a new `Response` with the given suspension status.
|
||||
#[must_use]
|
||||
pub fn new(suspended: bool) -> Self { Self { suspended } }
|
||||
}
|
||||
}
|
||||
+38
-18
@@ -8,7 +8,7 @@
|
||||
};
|
||||
|
||||
use regex::Regex;
|
||||
use ruma::OwnedDeviceId;
|
||||
use ruma::{OwnedDeviceId, api::OAuthScope};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
@@ -55,26 +55,39 @@ pub enum Prompt {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialOrd, Ord)]
|
||||
pub enum Scope {
|
||||
pub enum RequestedScope {
|
||||
Device(OwnedDeviceId),
|
||||
ClientApi,
|
||||
FullAccess,
|
||||
ServerAdministration,
|
||||
}
|
||||
|
||||
impl PartialEq for Scope {
|
||||
impl RequestedScope {
|
||||
pub fn as_granted_scope(&self) -> Option<OAuthScope> {
|
||||
match self {
|
||||
| Self::FullAccess => Some(OAuthScope::FullAccess),
|
||||
| Self::ServerAdministration => Some(OAuthScope::ServerAdministration),
|
||||
| Self::Device(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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::FullAccess => "urn:matrix:client:api:*".to_owned(),
|
||||
| Self::Device(device_id) => format!("urn:matrix:client:device:{device_id}"),
|
||||
| Self::ServerAdministration =>
|
||||
"urn:matrix:client:cc.c10y.msc4484.server_administration".to_owned(),
|
||||
};
|
||||
|
||||
f.write_str(&urn)
|
||||
@@ -85,22 +98,27 @@ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
pub struct RawScopes(String);
|
||||
|
||||
impl RawScopes {
|
||||
pub fn to_scopes(&self) -> Result<BTreeSet<Scope>, String> {
|
||||
let client_api_token_regex =
|
||||
pub fn to_scopes(&self) -> Result<BTreeSet<RequestedScope>, String> {
|
||||
let full_access_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();
|
||||
let server_administration_regex =
|
||||
Regex::new(r"urn:matrix:client:cc.c10y.msc4484.server_administration").unwrap();
|
||||
|
||||
let mut scopes = BTreeSet::new();
|
||||
|
||||
for token in self.0.split(' ') {
|
||||
let scope_was_new = {
|
||||
if client_api_token_regex.is_match(token) {
|
||||
scopes.insert(Scope::ClientApi)
|
||||
if full_access_regex.is_match(token) {
|
||||
scopes.insert(RequestedScope::FullAccess)
|
||||
} 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 server_administration_regex.is_match(token) {
|
||||
scopes.insert(RequestedScope::ServerAdministration)
|
||||
} else if token == "openid" {
|
||||
// TODO(unspecced): Element sets this scope but doesn't use it for anything
|
||||
true
|
||||
@@ -125,7 +143,6 @@ pub struct OAuthError {
|
||||
}
|
||||
|
||||
impl OAuthError {
|
||||
#[must_use]
|
||||
pub const fn invalid_request(error_description: &'static str) -> Self {
|
||||
Self {
|
||||
error: ErrorCode::InvalidRequest,
|
||||
@@ -133,7 +150,6 @@ pub const fn invalid_request(error_description: &'static str) -> Self {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn invalid_grant(error_description: &'static str) -> Self {
|
||||
Self {
|
||||
error: ErrorCode::InvalidGrant,
|
||||
@@ -161,9 +177,13 @@ pub enum ErrorCode {
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct AuthorizationCodeResponse {
|
||||
pub state: String,
|
||||
pub code: String,
|
||||
#[serde(untagged)]
|
||||
pub enum AuthorizationCodeResponse {
|
||||
Success {
|
||||
state: String,
|
||||
code: String,
|
||||
},
|
||||
Error(OAuthError),
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
||||
+52
-46
@@ -11,8 +11,7 @@
|
||||
};
|
||||
use database::{Deserialized, Json, Map};
|
||||
use itertools::Itertools;
|
||||
use lru_cache::LruCache;
|
||||
use ruma::{DeviceId, OwnedDeviceId, OwnedUserId, UserId};
|
||||
use ruma::{DeviceId, OwnedDeviceId, OwnedUserId, UserId, api::OAuthScope};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
@@ -22,7 +21,7 @@
|
||||
client_metadata::{ApplicationType, ClientMetadata, ResponseType},
|
||||
grant::{
|
||||
AuthorizationCodeQuery, AuthorizationCodeResponse, CodeChallengeMethod, ErrorCode,
|
||||
OAuthError, ResponseMode, Scope, TokenRequest, TokenResponse, TokenType,
|
||||
OAuthError, RequestedScope, ResponseMode, TokenRequest, TokenResponse, TokenType,
|
||||
},
|
||||
},
|
||||
users,
|
||||
@@ -35,7 +34,7 @@ pub struct Service {
|
||||
services: Services,
|
||||
db: Data,
|
||||
tickets: Mutex<HashMap<String, HashMap<OAuthTicket, SystemTime>>>,
|
||||
pending_code_grants: tokio::sync::Mutex<LruCache<String, PendingCodeGrant>>,
|
||||
pending_code_grants: tokio::sync::Mutex<HashMap<String, PendingCodeGrant>>,
|
||||
}
|
||||
|
||||
struct Data {
|
||||
@@ -51,7 +50,7 @@ struct Services {
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct SessionInfo {
|
||||
pub client_id: String,
|
||||
pub scopes: BTreeSet<Scope>,
|
||||
pub scopes: BTreeSet<OAuthScope>,
|
||||
current_refresh_token: String,
|
||||
}
|
||||
|
||||
@@ -64,7 +63,7 @@ struct RefreshTokenInfo {
|
||||
|
||||
struct PendingCodeGrant {
|
||||
authorizing_user: OwnedUserId,
|
||||
requested_scopes: BTreeSet<Scope>,
|
||||
requested_scopes: BTreeSet<RequestedScope>,
|
||||
client_name: Option<String>,
|
||||
expected_client_id: String,
|
||||
expected_redirect_uri: Url,
|
||||
@@ -119,9 +118,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
refreshtoken_refreshtokeninfo: args.db["refreshtoken_refreshtokeninfo"].clone(),
|
||||
},
|
||||
tickets: Mutex::default(),
|
||||
pending_code_grants: tokio::sync::Mutex::new(LruCache::new(
|
||||
Self::MAX_PENDING_CODE_GRANTS,
|
||||
)),
|
||||
pending_code_grants: tokio::sync::Mutex::default(),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -130,10 +127,6 @@ fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
|
||||
|
||||
impl Service {
|
||||
const ACCESS_TOKEN_MAX_AGE: Duration = Duration::from_hours(1);
|
||||
// Maximum number of pending code grants which will be held in memory at once,
|
||||
// to prevent unbounded memory use if someone decides to repeatedly reload the
|
||||
// grant page.
|
||||
const MAX_PENDING_CODE_GRANTS: usize = 100;
|
||||
const RANDOM_TOKEN_LENGTH: usize = 32;
|
||||
|
||||
fn generate_token() -> String { utils::random_string(Self::RANDOM_TOKEN_LENGTH) }
|
||||
@@ -224,49 +217,59 @@ pub async fn request_authorization_code(
|
||||
}
|
||||
}
|
||||
|
||||
let requested_scopes = query.scope.to_scopes()?;
|
||||
|
||||
let redirect_uri_query_separator = match query.response_mode {
|
||||
| ResponseMode::Fragment => '#',
|
||||
| ResponseMode::Query => '?',
|
||||
};
|
||||
|
||||
let code = PendingCodeGrant::generate_code();
|
||||
let response = 'response: {
|
||||
let requested_scopes = query.scope.to_scopes()?;
|
||||
|
||||
info!(
|
||||
client_id = &query.client_id,
|
||||
client_name = &client_metadata.client_name,
|
||||
?requested_scopes,
|
||||
?authorizing_user,
|
||||
"Issuing oauth authorization code"
|
||||
);
|
||||
if requested_scopes.contains(&RequestedScope::ServerAdministration) {
|
||||
// Only server admins can request this scope
|
||||
if !self.services.users.is_admin(&authorizing_user).await {
|
||||
break 'response AuthorizationCodeResponse::Error(OAuthError {
|
||||
error: ErrorCode::AccessDenied,
|
||||
error_description: "You are not a server administrator.".into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let code = PendingCodeGrant::generate_code();
|
||||
|
||||
info!(
|
||||
client_id = &query.client_id,
|
||||
client_name = &client_metadata.client_name,
|
||||
?requested_scopes,
|
||||
?authorizing_user,
|
||||
"Issuing oauth authorization code"
|
||||
);
|
||||
|
||||
let pending_grant = PendingCodeGrant {
|
||||
authorizing_user,
|
||||
requested_scopes,
|
||||
client_name: client_metadata.client_name,
|
||||
expected_client_id: query.client_id,
|
||||
expected_redirect_uri: query.redirect_uri.clone(),
|
||||
code_challenge: query.code_challenge,
|
||||
requested_at: SystemTime::now(),
|
||||
};
|
||||
|
||||
self.pending_code_grants
|
||||
.lock()
|
||||
.await
|
||||
.insert(code.clone(), pending_grant);
|
||||
|
||||
AuthorizationCodeResponse::Success { state: query.state, code }
|
||||
};
|
||||
|
||||
let redirect_uri = format!(
|
||||
"{}{}{}",
|
||||
query.redirect_uri,
|
||||
redirect_uri_query_separator,
|
||||
serde_urlencoded::to_string(AuthorizationCodeResponse {
|
||||
state: query.state,
|
||||
code: code.clone(),
|
||||
})
|
||||
.unwrap(),
|
||||
serde_urlencoded::to_string(response).unwrap(),
|
||||
);
|
||||
|
||||
let pending_grant = PendingCodeGrant {
|
||||
authorizing_user,
|
||||
requested_scopes,
|
||||
client_name: client_metadata.client_name,
|
||||
expected_client_id: query.client_id,
|
||||
expected_redirect_uri: query.redirect_uri,
|
||||
code_challenge: query.code_challenge,
|
||||
requested_at: SystemTime::now(),
|
||||
};
|
||||
|
||||
self.pending_code_grants
|
||||
.lock()
|
||||
.await
|
||||
.insert(code, pending_grant);
|
||||
|
||||
Ok(redirect_uri)
|
||||
}
|
||||
|
||||
@@ -339,7 +342,7 @@ pub async fn revoke_token(&self, token: String) -> Result<(), OAuthError> {
|
||||
async fn create_session(
|
||||
&self,
|
||||
authorizing_user: OwnedUserId,
|
||||
requested_scopes: BTreeSet<Scope>,
|
||||
requested_scopes: BTreeSet<RequestedScope>,
|
||||
client_name: Option<String>,
|
||||
client_id: String,
|
||||
) -> Result<TokenResponse, OAuthError> {
|
||||
@@ -349,7 +352,7 @@ async fn create_session(
|
||||
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)
|
||||
} else {
|
||||
None
|
||||
@@ -391,7 +394,10 @@ async fn create_session(
|
||||
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_granted_scope)
|
||||
.collect(),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
+14
-13
@@ -52,7 +52,7 @@ pub struct UserSuspension {
|
||||
/// When the user was suspended (Unix timestamp in milliseconds)
|
||||
pub suspended_at: u64,
|
||||
/// User ID of who suspended this user
|
||||
pub suspended_by: String,
|
||||
pub suspended_by: Option<String>,
|
||||
}
|
||||
|
||||
/// A password hash. This is only for use when setting a user's password,
|
||||
@@ -229,6 +229,9 @@ pub async fn create(&self, user_id: &UserId, password: Option<HashedPassword>) -
|
||||
}
|
||||
|
||||
/// Create a new account for a local human or bot user.
|
||||
///
|
||||
/// Does not automatically join the user to auto join rooms. Use
|
||||
/// `join_auto_join_rooms` for that.
|
||||
pub async fn create_local_account(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
@@ -287,8 +290,7 @@ pub async fn create_local_account(
|
||||
// register, suspend them.
|
||||
if !was_first_user && self.services.config.suspend_on_register {
|
||||
// Note that we can still do auto joins for suspended users
|
||||
self.suspend_account(user_id, &self.services.globals.server_user)
|
||||
.await;
|
||||
self.suspend_account(user_id, None).await;
|
||||
|
||||
// And send an @room notice to the admin room, to prompt admins to review the
|
||||
// new user and ideally unsuspend them if deemed appropriate.
|
||||
@@ -303,8 +305,11 @@ pub async fn create_local_account(
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
info!("Created new user account for {user_id}");
|
||||
}
|
||||
|
||||
// Autojoin the user to the configured autojoin rooms
|
||||
/// Autojoin the user to the configured autojoin rooms
|
||||
pub async fn join_auto_join_rooms(&self, user_id: &UserId) {
|
||||
for room in &self.services.config.auto_join_rooms {
|
||||
let Ok(room_id) = self.services.alias.resolve(room).await else {
|
||||
error!(
|
||||
@@ -320,9 +325,7 @@ pub async fn create_local_account(
|
||||
.server_in_room(self.services.globals.server_name(), &room_id)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"Skipping room {room} to automatically join as we have never joined before."
|
||||
);
|
||||
warn!("Skipping auto-room {room} as we have never joined before.");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -354,8 +357,6 @@ pub async fn create_local_account(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("Created new user account for {user_id}");
|
||||
}
|
||||
|
||||
pub async fn determine_registration_user_id(
|
||||
@@ -480,13 +481,13 @@ pub async fn deactivate_account(&self, user_id: &UserId) -> Result<()> {
|
||||
}
|
||||
|
||||
/// Suspend account, placing it in a read-only state
|
||||
pub async fn suspend_account(&self, user_id: &UserId, suspending_user: &UserId) {
|
||||
pub async fn suspend_account(&self, user_id: &UserId, suspending_user: Option<&UserId>) {
|
||||
self.db.userid_suspension.raw_put(
|
||||
user_id,
|
||||
Json(UserSuspension {
|
||||
suspended: true,
|
||||
suspended_at: MilliSecondsSinceUnixEpoch::now().get().into(),
|
||||
suspended_by: suspending_user.to_string(),
|
||||
suspended_by: suspending_user.map(ToString::to_string),
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -496,7 +497,7 @@ pub async fn unsuspend_account(&self, user_id: &UserId) {
|
||||
self.db.userid_suspension.remove(user_id);
|
||||
}
|
||||
|
||||
pub async fn lock_account(&self, user_id: &UserId, locking_user: &UserId) {
|
||||
pub async fn lock_account(&self, user_id: &UserId, locking_user: Option<&UserId>) {
|
||||
// NOTE: Locking is basically just suspension with a more severe effect,
|
||||
// so we'll just re-use the suspension data structure to store the lock state.
|
||||
let suspension = self
|
||||
@@ -508,7 +509,7 @@ pub async fn lock_account(&self, user_id: &UserId, locking_user: &UserId) {
|
||||
.unwrap_or_else(|_| UserSuspension {
|
||||
suspended: true,
|
||||
suspended_at: MilliSecondsSinceUnixEpoch::now().get().into(),
|
||||
suspended_by: locking_user.to_string(),
|
||||
suspended_by: locking_user.map(ToString::to_string),
|
||||
});
|
||||
|
||||
self.db.userid_lock.raw_put(user_id, Json(suspension));
|
||||
|
||||
@@ -528,6 +528,7 @@ async fn complete_registration(
|
||||
.registration_tokens
|
||||
.mark_token_as_used(registration_token);
|
||||
}
|
||||
services.users.join_auto_join_rooms(&user_id).await;
|
||||
|
||||
let user_session = UserSession { user_id, last_login: SystemTime::now() };
|
||||
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
use askama::{Template, filters::HtmlSafe};
|
||||
use base64::Engine;
|
||||
use conduwuit_core::{result::FlatOk, utils};
|
||||
use conduwuit_service::{
|
||||
Services,
|
||||
media::mxc::Mxc,
|
||||
oauth::{client_metadata::ClientMetadata, grant::Scope},
|
||||
use conduwuit_service::{Services, media::mxc::Mxc, oauth::client_metadata::ClientMetadata};
|
||||
use ruma::{
|
||||
OwnedDeviceId, OwnedUserId, UserId,
|
||||
api::{OAuthScope, client::device::Device},
|
||||
};
|
||||
use ruma::{OwnedDeviceId, OwnedUserId, UserId, api::client::device::Device};
|
||||
|
||||
pub(super) mod form;
|
||||
|
||||
@@ -183,7 +182,7 @@ pub(super) async fn for_device(
|
||||
#[derive(Debug, Template)]
|
||||
#[template(path = "_components/client_scopes.html.j2")]
|
||||
pub(super) struct ClientScopes {
|
||||
pub scopes: BTreeSet<Scope>,
|
||||
pub scopes: BTreeSet<OAuthScope>,
|
||||
}
|
||||
|
||||
impl HtmlSafe for ClientScopes {}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
response::Redirect,
|
||||
routing::on,
|
||||
};
|
||||
use conduwuit_service::oauth::grant::{AuthorizationCodeQuery, Prompt};
|
||||
use conduwuit_service::oauth::grant::{AuthorizationCodeQuery, Prompt, RequestedScope};
|
||||
use ruma::OwnedUserId;
|
||||
use url::Url;
|
||||
|
||||
@@ -100,7 +100,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_granted_scope)
|
||||
.collect();
|
||||
|
||||
let client_name = if let Some(name) = &client.client_name {
|
||||
name
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
--background-color: #fff;
|
||||
--text-color: #000;
|
||||
--secondary: #555;
|
||||
--secondary: #666;
|
||||
--bg: oklch(0.76 0.0854 317.27);
|
||||
--panel-bg: oklch(0.91 0.042 317.27);
|
||||
--c1: oklch(0.44 0.177 353.06);
|
||||
@@ -84,10 +84,6 @@ footer {
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
--name-lightness: 0.35;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
@@ -309,26 +305,6 @@ ul.bullet-separated {
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
|
||||
.kitty {
|
||||
margin-bottom: 0;
|
||||
overflow: clip;
|
||||
width: fit-content;
|
||||
position: relative;
|
||||
float: right;
|
||||
cursor: default;
|
||||
font-size: small;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
transform: translateX(calc(100% - 1.5em));
|
||||
transition: transform 0.3s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 425px) {
|
||||
main {
|
||||
padding-block-start: 2rem;
|
||||
@@ -340,20 +316,6 @@ ul.bullet-separated {
|
||||
width: 100%;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
ul.bullet-separated {
|
||||
display: block;
|
||||
padding-left: inherit;
|
||||
|
||||
li {
|
||||
display: list-item;
|
||||
list-style-type: disc;
|
||||
|
||||
&::before {
|
||||
content: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 799px) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% match avatar_type %}
|
||||
{% when AvatarType::Initial with (initial) %}
|
||||
<span class="avatar" role="img" aria-hidden="true">{{ initial }}</span>
|
||||
<span class="avatar" role="img">{{ initial }}</span>
|
||||
{% when AvatarType::Image with (src) %}
|
||||
<img class="avatar" src="{{ src }}" aria-hidden="true">
|
||||
<img class="avatar" src="{{ src }}">
|
||||
{% endmatch %}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<ul>
|
||||
{% for scope in scopes %}
|
||||
{% match scope %}
|
||||
{% when Scope::ClientApi %}
|
||||
{% when OAuthScope::FullAccess %}
|
||||
<li>Send messages and interact with chatrooms on your behalf</li>
|
||||
{% when Scope::Device(_) %}
|
||||
<li>Access your Matrix account</li>
|
||||
{% when OAuthScope::ServerAdministration %}
|
||||
<li>⚠️ Administrate this homeserver
|
||||
<br><em class="negative">This is a dangerous permission. Make sure you trust this app.</em></li>
|
||||
{% when _ %}
|
||||
<li>missingno</li>
|
||||
{% endmatch %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
{%~ block content %}{% endblock ~%}
|
||||
{%~ block footer ~%}
|
||||
<footer>
|
||||
<a href="{{ crate::ROUTE_PREFIX }}/"><img class="logo" alt="Continuwuity logo" src="{{ crate::ROUTE_PREFIX }}/resources/logo.svg"></a>
|
||||
<a href="{{ crate::ROUTE_PREFIX }}/"><img class="logo" src="{{ crate::ROUTE_PREFIX }}/resources/logo.svg"></a>
|
||||
<p>Powered by <a href="https://continuwuity.org">Continuwuity</a> {{ env!("CARGO_PKG_VERSION") }} • <a href="{{ crate::ROUTE_PREFIX }}/about">About</a></p>
|
||||
</footer>
|
||||
{%~ endblock ~%}
|
||||
|
||||
@@ -5,11 +5,11 @@ About server
|
||||
{%- endblock -%}
|
||||
|
||||
{%- block content -%}
|
||||
<div class="panel middle">
|
||||
<div class="panel">
|
||||
<h1>About {{ server_name }}</h1>
|
||||
{% if let Some(support_page) = support_page %}
|
||||
<p>
|
||||
Visit this server's website: <a href="{{ support_page }} target="_blank">{{ support_page }}</a>
|
||||
Support: <a href="{{ support_page }} target="_blank">{{ support_page }}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if !contacts.is_empty() %}
|
||||
@@ -32,7 +32,7 @@ About server
|
||||
: <ul class="bullet-separated">
|
||||
{%- if let Some(matrix_id) = contact.matrix_id -%}
|
||||
<li><a href="matrix:u/{{ matrix_id.localpart() }}:{{ matrix_id.server_name() }}?action=chat" target="_blank">{{ matrix_id }}</a></li>
|
||||
{%~ endif -%}
|
||||
{%- endif -%}
|
||||
{%- if let Some(email_address) = contact.email_address -%}
|
||||
<li><a href="mailto:{{ email_address }}" target="_blank">{{ email_address }}</a>
|
||||
{%- if let Some(pgp_key) = contact.pgp_key -%}
|
||||
@@ -58,7 +58,7 @@ About server
|
||||
</ul>
|
||||
{% endif %}
|
||||
<p>
|
||||
Server version <b>{{ env!("CARGO_PKG_VERSION") }}</b>
|
||||
Server version {{ env!("CARGO_PKG_VERSION") }}
|
||||
{%~ if let Some(version_info) = conduwuit_build_metadata::version_tag() ~%}
|
||||
{%~ if let Some(url) = conduwuit_build_metadata::GIT_REMOTE_COMMIT_URL.or(conduwuit_build_metadata::GIT_REMOTE_WEB_URL) ~%}
|
||||
(<a href="{{ url }}">{{ version_info }}</a>)
|
||||
@@ -73,13 +73,12 @@ About server
|
||||
a high-performance and community-driven <a href="https://matrix.org">Matrix</a> homeserver
|
||||
maintained as an open source project by volunteers from around the world.
|
||||
</p>
|
||||
<ul class="bullet-separated">
|
||||
<li><a href="https://forgejo.ellis.link/continuwuation/continuwuity">Source code</a></li>
|
||||
<li><a href="https://matrix.to/#/#continuwuity:continuwuity.org">Official Matrix chatroom</a></li>
|
||||
<li><span class="project-name">❤</span> <a href="https://opencollective.com/continuwuity">Support the project</a></li>
|
||||
</ul>
|
||||
<p class="kitty">
|
||||
<span>🐈 meow :3</span>
|
||||
<p>
|
||||
<ul class="bullet-separated">
|
||||
<li><a href="https://forgejo.ellis.link/continuwuation/continuwuity">Source code</a></li>
|
||||
<li><a href="https://matrix.to/#/#continuwuity:continuwuity.org">Official Matrix chatroom</a></li>
|
||||
<li><span class="project-name">❤</span> <a href="https://opencollective.com/continuwuity">Support the project</a></li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
{%- block content -%}
|
||||
<div class="error-body">
|
||||
<pre class="k10y" aria-hidden="true">
|
||||
<pre class="k10y" aria-hidden>
|
||||
/> フ
|
||||
| _ _|
|
||||
/` ミ_xノ
|
||||
|
||||
@@ -15,7 +15,7 @@ Authorize client
|
||||
<div class="red-avatar">
|
||||
{{ user_avatar }}
|
||||
</div>
|
||||
<div class="separator" aria-hidden="true">
|
||||
<div class="separator" aria-hidden>
|
||||
⇄
|
||||
</div>
|
||||
{{ client_avatar }}
|
||||
|
||||
@@ -29,11 +29,11 @@ Log in
|
||||
<form method="post">
|
||||
<p>
|
||||
<label for="identifier">Username or email address</label>
|
||||
<input type="text" id="identifier" name="identifier" autocomplete="username">
|
||||
<input type="text" name="identifier" autocomplete="username">
|
||||
</p>
|
||||
<p>
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" autocomplete="current-password">
|
||||
<input type="password" name="password" autocomplete="current-password">
|
||||
</p>
|
||||
<button type="submit">Log in</button>
|
||||
</form>
|
||||
@@ -51,7 +51,7 @@ Log in
|
||||
<form method="post">
|
||||
<p>
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" autocomplete="current-password">
|
||||
<input type="password" name="password" autocomplete="current-password">
|
||||
</p>
|
||||
<button type="submit">Continue</button>
|
||||
</form>
|
||||
|
||||
@@ -55,7 +55,7 @@ Sign up
|
||||
<label for="username">Username</label>
|
||||
<span class="username-input">
|
||||
<span>@</span>
|
||||
<input type="text" id="username" name="username" autocomplete="username" required>
|
||||
<input type="text" name="username" autocomplete="username" required>
|
||||
<span>:{{ server_name }}</span>
|
||||
</span>
|
||||
{% if let Some(username_error) = username_error %}
|
||||
@@ -98,9 +98,9 @@ Sign up
|
||||
<span class="username-input">
|
||||
<span>@</span>
|
||||
{% if let Some(username) = username %}
|
||||
<input type="text" id="username" name="username" value="{{ username }}" autocomplete="username" required>
|
||||
<input type="text" name="username" value="{{ username }}" autocomplete="username" required>
|
||||
{% else %}
|
||||
<input type="text" id="username" name="username" autocomplete="username" required>
|
||||
<input type="text" name="username" autocomplete="username" required>
|
||||
{% endif %}
|
||||
<span>:{{ server_name }}</span>
|
||||
</span>
|
||||
@@ -114,18 +114,18 @@ Sign up
|
||||
{% endif %}
|
||||
<p>
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" autocomplete="new-password" required>
|
||||
<input type="password" name="password" autocomplete="new-password" required>
|
||||
{{ form::errors(field_errors, std::borrow::Cow::Borrowed("password")) }}
|
||||
</p>
|
||||
<p>
|
||||
<label for="confirm_password">Confirm password</label>
|
||||
<input type="password" id="confirm_password" name="confirm_password" autocomplete="new-password" required>
|
||||
<input type="password" name="confirm_password" autocomplete="new-password" required>
|
||||
{{ form::errors(field_errors, std::borrow::Cow::Borrowed("confirm_password")) }}
|
||||
</p>
|
||||
{% if require_email %}
|
||||
<p>
|
||||
<label for="email">Email address</label>
|
||||
<input type="email" id="email" name="email" required>
|
||||
<input type="email" name="email" required>
|
||||
{{ form::errors(field_errors, std::borrow::Cow::Borrowed("email")) }}
|
||||
</p>
|
||||
{% endif %}
|
||||
@@ -153,8 +153,8 @@ Sign up
|
||||
</div>
|
||||
{% else %}
|
||||
<p>
|
||||
<label for="registration_token">Registration token</label>
|
||||
<input type="text" id="registration_token" name="registration_token" autocomplete="none" required>
|
||||
<label for="username">Registration token</label>
|
||||
<input type="text" name="registration_token" autocomplete="none" required>
|
||||
{% if is_first_run %}
|
||||
<small>Check the server console to find the registration token.</small>
|
||||
{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user