From 588a04b0bad3ced4cbbd5aaadbc86890138f8f14 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Mon, 3 Mar 2025 17:24:15 +0100 Subject: [PATCH] Allow configuring the connection to the homeserver to be read-only. --- crates/cli/src/util.rs | 30 +++++++---- crates/config/src/sections/matrix.rs | 21 ++++++++ crates/config/src/sections/mod.rs | 2 +- crates/matrix/src/lib.rs | 5 +- crates/matrix/src/readonly.rs | 78 ++++++++++++++++++++++++++++ docs/config.schema.json | 28 ++++++++++ 6 files changed, 153 insertions(+), 11 deletions(-) create mode 100644 crates/matrix/src/readonly.rs diff --git a/crates/cli/src/util.rs b/crates/cli/src/util.rs index 3fca99535..27c23eeb5 100644 --- a/crates/cli/src/util.rs +++ b/crates/cli/src/util.rs @@ -9,13 +9,13 @@ use std::{sync::Arc, time::Duration}; use anyhow::Context; use mas_config::{ AccountConfig, BrandingConfig, CaptchaConfig, DatabaseConfig, EmailConfig, EmailSmtpMode, - EmailTransportKind, ExperimentalConfig, MatrixConfig, PasswordsConfig, PolicyConfig, - TemplatesConfig, + EmailTransportKind, ExperimentalConfig, HomeserverKind, MatrixConfig, PasswordsConfig, + PolicyConfig, TemplatesConfig, }; use mas_data_model::{SessionExpirationConfig, SiteConfig}; use mas_email::{MailTransport, Mailer}; use mas_handlers::passwords::PasswordManager; -use mas_matrix::HomeserverConnection; +use mas_matrix::{HomeserverConnection, ReadOnlyHomeserverConnection}; use mas_matrix_synapse::SynapseConnection; use mas_policy::PolicyFactory; use mas_router::UrlBuilder; @@ -354,12 +354,24 @@ pub fn homeserver_connection_from_config( config: &MatrixConfig, http_client: reqwest::Client, ) -> Arc { - Arc::new(SynapseConnection::new( - config.homeserver.clone(), - config.endpoint.clone(), - config.secret.clone(), - http_client, - )) + match config.kind { + HomeserverKind::Synapse => Arc::new(SynapseConnection::new( + config.homeserver.clone(), + config.endpoint.clone(), + config.secret.clone(), + http_client, + )), + HomeserverKind::SynapseReadOnly => { + let connection = SynapseConnection::new( + config.homeserver.clone(), + config.endpoint.clone(), + config.secret.clone(), + http_client, + ); + let readonly = ReadOnlyHomeserverConnection::new(connection); + Arc::new(readonly) + } + } } #[cfg(test)] diff --git a/crates/config/src/sections/matrix.rs b/crates/config/src/sections/matrix.rs index eb145fa8c..d5e35907e 100644 --- a/crates/config/src/sections/matrix.rs +++ b/crates/config/src/sections/matrix.rs @@ -23,10 +23,29 @@ fn default_endpoint() -> Url { Url::parse("http://localhost:8008/").unwrap() } +/// The kind of homeserver it is. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)] +#[serde(rename_all = "snake_case")] +pub enum HomeserverKind { + /// Homeserver is Synapse + #[default] + Synapse, + + /// Homeserver is Synapse, in read-only mode + /// + /// This is meant for testing rolling out Matrix Authentication Service with + /// no risk of writing data to the homeserver. + SynapseReadOnly, +} + /// Configuration related to the Matrix homeserver #[serde_as] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct MatrixConfig { + /// The kind of homeserver it is. + #[serde(default)] + pub kind: HomeserverKind, + /// The server name of the homeserver. #[serde(default = "default_homeserver")] pub homeserver: String, @@ -49,6 +68,7 @@ impl MatrixConfig { R: Rng + Send, { Self { + kind: HomeserverKind::default(), homeserver: default_homeserver(), secret: Alphanumeric.sample_string(&mut rng, 32), endpoint: default_endpoint(), @@ -57,6 +77,7 @@ impl MatrixConfig { pub(crate) fn test() -> Self { Self { + kind: HomeserverKind::default(), homeserver: default_homeserver(), secret: "test".to_owned(), endpoint: default_endpoint(), diff --git a/crates/config/src/sections/mod.rs b/crates/config/src/sections/mod.rs index aa773e70b..d415f646a 100644 --- a/crates/config/src/sections/mod.rs +++ b/crates/config/src/sections/mod.rs @@ -37,7 +37,7 @@ pub use self::{ BindConfig as HttpBindConfig, HttpConfig, ListenerConfig as HttpListenerConfig, Resource as HttpResource, TlsConfig as HttpTlsConfig, UnixOrTcp, }, - matrix::MatrixConfig, + matrix::{HomeserverKind, MatrixConfig}, passwords::{Algorithm as PasswordAlgorithm, PasswordsConfig}, policy::PolicyConfig, rate_limiting::RateLimitingConfig, diff --git a/crates/matrix/src/lib.rs b/crates/matrix/src/lib.rs index 1a3c036a8..59cdb4880 100644 --- a/crates/matrix/src/lib.rs +++ b/crates/matrix/src/lib.rs @@ -5,12 +5,15 @@ // Please see LICENSE in the repository root for full details. mod mock; +mod readonly; use std::{collections::HashSet, sync::Arc}; use ruma_common::UserId; -pub use self::mock::HomeserverConnection as MockHomeserverConnection; +pub use self::{ + mock::HomeserverConnection as MockHomeserverConnection, readonly::ReadOnlyHomeserverConnection, +}; #[derive(Debug)] pub struct MatrixUser { diff --git a/crates/matrix/src/readonly.rs b/crates/matrix/src/readonly.rs new file mode 100644 index 000000000..b51040080 --- /dev/null +++ b/crates/matrix/src/readonly.rs @@ -0,0 +1,78 @@ +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. + +use std::collections::HashSet; + +use crate::{HomeserverConnection, MatrixUser, ProvisionRequest}; + +/// A wrapper around a [`HomeserverConnection`] that only allows read +/// operations. +pub struct ReadOnlyHomeserverConnection { + inner: C, +} + +impl ReadOnlyHomeserverConnection { + pub fn new(inner: C) -> Self + where + C: HomeserverConnection, + { + Self { inner } + } +} + +#[async_trait::async_trait] +impl HomeserverConnection for ReadOnlyHomeserverConnection { + fn homeserver(&self) -> &str { + self.inner.homeserver() + } + + async fn query_user(&self, mxid: &str) -> Result { + self.inner.query_user(mxid).await + } + + async fn provision_user(&self, _request: &ProvisionRequest) -> Result { + anyhow::bail!("Provisioning is not supported in read-only mode"); + } + + async fn is_localpart_available(&self, localpart: &str) -> Result { + self.inner.is_localpart_available(localpart).await + } + + async fn create_device(&self, _mxid: &str, _device_id: &str) -> Result<(), anyhow::Error> { + anyhow::bail!("Device creation is not supported in read-only mode"); + } + + async fn delete_device(&self, _mxid: &str, _device_id: &str) -> Result<(), anyhow::Error> { + anyhow::bail!("Device deletion is not supported in read-only mode"); + } + + async fn sync_devices( + &self, + _mxid: &str, + _devices: HashSet, + ) -> Result<(), anyhow::Error> { + anyhow::bail!("Device synchronization is not supported in read-only mode"); + } + + async fn delete_user(&self, _mxid: &str, _erase: bool) -> Result<(), anyhow::Error> { + anyhow::bail!("User deletion is not supported in read-only mode"); + } + + async fn reactivate_user(&self, _mxid: &str) -> Result<(), anyhow::Error> { + anyhow::bail!("User reactivation is not supported in read-only mode"); + } + + async fn set_displayname(&self, _mxid: &str, _displayname: &str) -> Result<(), anyhow::Error> { + anyhow::bail!("User displayname update is not supported in read-only mode"); + } + + async fn unset_displayname(&self, _mxid: &str) -> Result<(), anyhow::Error> { + anyhow::bail!("User displayname update is not supported in read-only mode"); + } + + async fn allow_cross_signing_reset(&self, _mxid: &str) -> Result<(), anyhow::Error> { + anyhow::bail!("Allowing cross-signing reset is not supported in read-only mode"); + } +} diff --git a/docs/config.schema.json b/docs/config.schema.json index ce5c12aa3..a998f08fd 100644 --- a/docs/config.schema.json +++ b/docs/config.schema.json @@ -1612,6 +1612,15 @@ "secret" ], "properties": { + "kind": { + "description": "The kind of homeserver it is.", + "default": "synapse", + "allOf": [ + { + "$ref": "#/definitions/HomeserverKind" + } + ] + }, "homeserver": { "description": "The server name of the homeserver.", "default": "localhost:8008", @@ -1629,6 +1638,25 @@ } } }, + "HomeserverKind": { + "description": "The kind of homeserver it is.", + "oneOf": [ + { + "description": "Homeserver is Synapse", + "type": "string", + "enum": [ + "synapse" + ] + }, + { + "description": "Homeserver is Synapse, in read-only mode\n\nThis is meant for testing rolling out Matrix Authentication Service with no risk of writing data to the homeserver.", + "type": "string", + "enum": [ + "synapse_read_only" + ] + } + ] + }, "PolicyConfig": { "description": "Application secrets", "type": "object",