diff --git a/src/api/Cargo.toml b/src/api/Cargo.toml index 34fa951da..4d885fe41 100644 --- a/src/api/Cargo.toml +++ b/src/api/Cargo.toml @@ -62,6 +62,8 @@ zstd_compression = [ "reqwest/zstd", ] +admin_api = [] + [dependencies] async-trait.workspace = true axum-client-ip.workspace = true diff --git a/src/api/admin/mod.rs b/src/api/admin/mod.rs deleted file mode 100644 index 5d505fa82..000000000 --- a/src/api/admin/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod rooms; diff --git a/src/api/admin/rooms/mod.rs b/src/api/admin/rooms/mod.rs deleted file mode 100644 index accb6f6a6..000000000 --- a/src/api/admin/rooms/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod ban; -pub mod list; diff --git a/src/api/client/admin/mod.rs b/src/api/client/admin/mod.rs index 88f434385..e4bce2aba 100644 --- a/src/api/client/admin/mod.rs +++ b/src/api/client/admin/mod.rs @@ -1,4 +1,5 @@ mod lock; +pub(crate) mod site; mod suspend; pub(crate) use self::{lock::*, suspend::*}; diff --git a/src/api/client/admin/site/mod.rs b/src/api/client/admin/site/mod.rs new file mode 100644 index 000000000..3207db92a --- /dev/null +++ b/src/api/client/admin/site/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod rooms; +pub(crate) mod users; diff --git a/src/api/admin/rooms/ban.rs b/src/api/client/admin/site/rooms/ban.rs similarity index 93% rename from src/api/admin/rooms/ban.rs rename to src/api/client/admin/site/rooms/ban.rs index c24ff7691..81ec65d6a 100644 --- a/src/api/admin/rooms/ban.rs +++ b/src/api/client/admin/site/rooms/ban.rs @@ -1,12 +1,12 @@ use axum::extract::State; -use conduwuit::{Err, Result, info, utils::ReadyExt, warn}; +use conduwuit::{info, utils::ReadyExt, warn, Err, Result}; use futures::{FutureExt, StreamExt}; -use ruma::{OwnedRoomAliasId, events::room::message::RoomMessageEventContent}; +use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomAliasId}; use ruminuwuity::admin::continuwuity::rooms; -use crate::{Ruma, client::leave_room}; +use crate::{client::leave_room, Ruma}; -/// # `PUT /_continuwuity/admin/rooms/{roomID}/ban` +/// # `PUT /_continuwuity/admin/v1/rooms/{roomID}/ban` /// /// Bans or unbans a room. pub(crate) async fn ban_room( diff --git a/src/api/admin/rooms/list.rs b/src/api/client/admin/site/rooms/list.rs similarity index 94% rename from src/api/admin/rooms/list.rs rename to src/api/client/admin/site/rooms/list.rs index fe7fc0857..48594a40d 100644 --- a/src/api/admin/rooms/list.rs +++ b/src/api/client/admin/site/rooms/list.rs @@ -6,7 +6,7 @@ use ruminuwuity::admin::continuwuity::rooms; use crate::Ruma; -/// # `GET /_continuwuity/admin/rooms/list` +/// # `GET /_continuwuity/admin/v1/rooms/list` /// /// Lists all rooms known to this server, excluding banned ones. pub(crate) async fn list_rooms( diff --git a/src/api/client/admin/site/rooms/mod.rs b/src/api/client/admin/site/rooms/mod.rs new file mode 100644 index 000000000..e41d4e714 --- /dev/null +++ b/src/api/client/admin/site/rooms/mod.rs @@ -0,0 +1,5 @@ +mod ban; +mod list; + +pub(crate) use ban::ban_room; +pub(crate) use list::list_rooms; diff --git a/src/api/client/admin/site/users/list.rs b/src/api/client/admin/site/users/list.rs new file mode 100644 index 000000000..7541f97bd --- /dev/null +++ b/src/api/client/admin/site/users/list.rs @@ -0,0 +1,42 @@ +use axum::extract::State; +use conduwuit::Err; +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, + body: Ruma, +) -> conduwuit::Result { + let sender_user = body.sender_user(); + + if !services.users.is_admin(sender_user).await { + return Err!(Request(Forbidden("Only server administrators can use this endpoint"))); + } + + let mut users = Vec::new(); + while let Some(user_id) = services.users.list_local_users().next().await { + 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), + ); + users.push(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, + }); + } + + Ok(users::list::v1::Response::new(users)) +} diff --git a/src/api/client/admin/site/users/mod.rs b/src/api/client/admin/site/users/mod.rs new file mode 100644 index 000000000..7e67b3889 --- /dev/null +++ b/src/api/client/admin/site/users/mod.rs @@ -0,0 +1,3 @@ +mod list; + +pub(crate) use list::list_users_route; diff --git a/src/api/mod.rs b/src/api/mod.rs index 7cbd3d209..b8deb28b8 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -11,8 +11,6 @@ pub mod client; pub mod router; pub mod server; -pub mod admin; - pub(crate) use self::router::{Ruma, RumaResponse, State}; conduwuit::mod_ctor! {} diff --git a/src/api/router.rs b/src/api/router.rs index 5634fce5c..0a0c51af5 100644 --- a/src/api/router.rs +++ b/src/api/router.rs @@ -6,17 +6,19 @@ mod response; use std::str::FromStr; use axum::{ - Router, response::{IntoResponse, Redirect}, routing::{any, get, post}, + Router, }; use conduwuit::err; pub(super) use conduwuit_service::state::State; -use http::{Uri, uri}; +use http::{uri, Uri}; 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) -> Router { let config = &state.server.config; @@ -191,9 +193,7 @@ pub fn build(router: Router, state: State) -> Router { .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 @@ -279,6 +279,14 @@ pub fn build(router: Router, state: State) -> Router { .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::rooms::ban_room) + .ruma_route(&admin_api::rooms::list_rooms) + }; + router } diff --git a/src/main/Cargo.toml b/src/main/Cargo.toml index fafaef6d8..50354d91b 100644 --- a/src/main/Cargo.toml +++ b/src/main/Cargo.toml @@ -68,6 +68,7 @@ full = [ "jemalloc_prof", "perf_measurements", "tokio_console", + "conduwuit-api/admin_api", ] brotli_compression = [ diff --git a/src/ruminuwuity/admin/continuwuity/mod.rs b/src/ruminuwuity/admin/continuwuity/mod.rs index 5d505fa82..ed9adcb7b 100644 --- a/src/ruminuwuity/admin/continuwuity/mod.rs +++ b/src/ruminuwuity/admin/continuwuity/mod.rs @@ -1 +1,2 @@ pub mod rooms; +pub mod users; diff --git a/src/ruminuwuity/admin/continuwuity/rooms/list.rs b/src/ruminuwuity/admin/continuwuity/rooms/list.rs index 62a566052..75aafc996 100644 --- a/src/ruminuwuity/admin/continuwuity/rooms/list.rs +++ b/src/ruminuwuity/admin/continuwuity/rooms/list.rs @@ -11,7 +11,7 @@ pub mod v1 { authentication: AccessToken, history: { unstable("org.continuwuity.admin") => "/_continuwuity/admin/rooms/list", - 1.0 => "/_continuwuity/admin/v1/rooms/list", + 1.0 => "/_continuwuity/admin/v1/rooms", } } diff --git a/src/ruminuwuity/admin/continuwuity/users/create.rs b/src/ruminuwuity/admin/continuwuity/users/create.rs new file mode 100644 index 000000000..d02557cc6 --- /dev/null +++ b/src/ruminuwuity/admin/continuwuity/users/create.rs @@ -0,0 +1,99 @@ +pub mod v1 { + use ruma::{ + OwnedMxcUri, OwnedRoomOrAliasId, OwnedUserId, + api::{auth_scheme::AccessToken, request, response}, + metadata, + }; + + metadata! { + method: POST, + rate_limited: false, + authentication: AccessToken, + 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 display name to set upon creation. + #[serde(default, skip_serializing_if = "ruma::serde::is_default")] + pub display_name: Option, + + /// The avatar URI to set upon creation. + #[serde(default, skip_serializing_if = "ruma::serde::is_default")] + pub avatar_url: Option, + + /// 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, + } + + #[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, + 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 } } + } +} diff --git a/src/ruminuwuity/admin/continuwuity/users/list.rs b/src/ruminuwuity/admin/continuwuity/users/list.rs new file mode 100644 index 000000000..8348c603c --- /dev/null +++ b/src/ruminuwuity/admin/continuwuity/users/list.rs @@ -0,0 +1,126 @@ +pub mod v1 { + use ruma::{ + OwnedUserId, + api::{auth_scheme::AccessToken, request, response}, + metadata, + }; + use serde::Deserialize; + + metadata! { + method: PUT, + rate_limited: false, + authentication: AccessToken, + 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, + } + + #[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, + } + + impl Request { + #[must_use] + pub fn new() -> Self { Self::default() } + } + + impl Response { + #[must_use] + pub fn new(users: Vec) -> 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()); + } + } +} diff --git a/src/ruminuwuity/admin/continuwuity/users/mod.rs b/src/ruminuwuity/admin/continuwuity/users/mod.rs new file mode 100644 index 000000000..fa6613e51 --- /dev/null +++ b/src/ruminuwuity/admin/continuwuity/users/mod.rs @@ -0,0 +1,2 @@ +pub mod create; +pub mod list;