Allow configuring the connection to the homeserver to be read-only.

This commit is contained in:
Quentin Gliech
2025-03-03 17:24:15 +01:00
parent df5de81c92
commit 588a04b0ba
6 changed files with 153 additions and 11 deletions

View File

@@ -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<dyn HomeserverConnection> {
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)]

View File

@@ -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(),

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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<C> {
inner: C,
}
impl<C> ReadOnlyHomeserverConnection<C> {
pub fn new(inner: C) -> Self
where
C: HomeserverConnection,
{
Self { inner }
}
}
#[async_trait::async_trait]
impl<C: HomeserverConnection> HomeserverConnection for ReadOnlyHomeserverConnection<C> {
fn homeserver(&self) -> &str {
self.inner.homeserver()
}
async fn query_user(&self, mxid: &str) -> Result<MatrixUser, anyhow::Error> {
self.inner.query_user(mxid).await
}
async fn provision_user(&self, _request: &ProvisionRequest) -> Result<bool, anyhow::Error> {
anyhow::bail!("Provisioning is not supported in read-only mode");
}
async fn is_localpart_available(&self, localpart: &str) -> Result<bool, anyhow::Error> {
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<String>,
) -> 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");
}
}

View File

@@ -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",