Skip to content

Commit a467032

Browse files
committed
Add matrix.secret_file config option
1 parent 209f180 commit a467032

File tree

7 files changed

+144
-36
lines changed

7 files changed

+144
-36
lines changed

crates/cli/src/commands/doctor.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ impl Options {
4444
r"The homeserver host in the config (`matrix.homeserver`) is not a valid domain.
4545
See {DOCS_BASE}/setup/homeserver.html",
4646
)?;
47+
let admin_token = config.matrix.secret().await?;
4748
let hs_api = config.matrix.endpoint;
48-
let admin_token = config.matrix.secret;
4949

5050
if !issuer.starts_with("https://") {
5151
warn!(

crates/cli/src/commands/manage.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -591,7 +591,7 @@ impl Options {
591591
MatrixConfig::extract(figment).map_err(anyhow::Error::from_boxed)?;
592592

593593
let password_manager = password_manager_from_config(&password_config).await?;
594-
let homeserver = homeserver_connection_from_config(&matrix_config, http_client);
594+
let homeserver = homeserver_connection_from_config(&matrix_config, http_client).await?;
595595
let mut conn = database_connection_from_config(&database_config).await?;
596596
let txn = conn.begin().await?;
597597
let mut repo = PgRepository::from_conn(txn);

crates/cli/src/commands/server.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ impl Options {
168168
let http_client = mas_http::reqwest_client();
169169

170170
let homeserver_connection =
171-
homeserver_connection_from_config(&config.matrix, http_client.clone());
171+
homeserver_connection_from_config(&config.matrix, http_client.clone()).await?;
172172

173173
if !self.no_worker {
174174
let mailer = mailer_from_config(&config.email, &templates)?;

crates/cli/src/commands/worker.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ impl Options {
5959
test_mailer_in_background(&mailer, Duration::from_secs(30));
6060

6161
let http_client = mas_http::reqwest_client();
62-
let conn = homeserver_connection_from_config(&config.matrix, http_client);
62+
let conn = homeserver_connection_from_config(&config.matrix, http_client).await?;
6363

6464
drop(config);
6565

crates/cli/src/util.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -464,36 +464,36 @@ pub async fn load_policy_factory_dynamic_data(
464464

465465
/// Create a clonable, type-erased [`HomeserverConnection`] from the
466466
/// configuration
467-
pub fn homeserver_connection_from_config(
467+
pub async fn homeserver_connection_from_config(
468468
config: &MatrixConfig,
469469
http_client: reqwest::Client,
470-
) -> Arc<dyn HomeserverConnection> {
471-
match config.kind {
470+
) -> anyhow::Result<Arc<dyn HomeserverConnection>> {
471+
Ok(match config.kind {
472472
HomeserverKind::Synapse | HomeserverKind::SynapseLegacy => {
473473
Arc::new(LegacySynapseConnection::new(
474474
config.homeserver.clone(),
475475
config.endpoint.clone(),
476-
config.secret.clone(),
476+
config.secret().await?,
477477
http_client,
478478
))
479479
}
480480
HomeserverKind::SynapseModern => Arc::new(SynapseConnection::new(
481481
config.homeserver.clone(),
482482
config.endpoint.clone(),
483-
config.secret.clone(),
483+
config.secret().await?,
484484
http_client,
485485
)),
486486
HomeserverKind::SynapseReadOnly => {
487487
let connection = LegacySynapseConnection::new(
488488
config.homeserver.clone(),
489489
config.endpoint.clone(),
490-
config.secret.clone(),
490+
config.secret().await?,
491491
http_client,
492492
);
493493
let readonly = ReadOnlyHomeserverConnection::new(connection);
494494
Arc::new(readonly)
495495
}
496-
}
496+
})
497497
}
498498

499499
#[cfg(test)]

crates/config/src/sections/matrix.rs

Lines changed: 130 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
55
// Please see LICENSE files in the repository root for full details.
66

7+
use anyhow::bail;
8+
use camino::Utf8PathBuf;
79
use rand::{
810
Rng,
911
distributions::{Alphanumeric, DistString},
@@ -48,6 +50,54 @@ pub enum HomeserverKind {
4850
SynapseModern,
4951
}
5052

53+
/// Shared secret between MAS and the homeserver.
54+
///
55+
/// It either holds the secret value directly or references a file where the
56+
/// secret is stored.
57+
#[derive(Clone, Debug)]
58+
pub enum Secret {
59+
File(Utf8PathBuf),
60+
Value(String),
61+
}
62+
63+
/// Secret fields as serialized in JSON.
64+
#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
65+
struct SecretRaw {
66+
#[schemars(with = "Option<String>")]
67+
#[serde(skip_serializing_if = "Option::is_none")]
68+
secret_file: Option<Utf8PathBuf>,
69+
#[serde(skip_serializing_if = "Option::is_none")]
70+
secret: Option<String>,
71+
}
72+
73+
impl TryFrom<SecretRaw> for Secret {
74+
type Error = anyhow::Error;
75+
76+
fn try_from(value: SecretRaw) -> Result<Self, Self::Error> {
77+
match (value.secret, value.secret_file) {
78+
(None, None) => bail!("Missing `secret` or `secret_file`"),
79+
(None, Some(path)) => Ok(Secret::File(path)),
80+
(Some(secret), None) => Ok(Secret::Value(secret)),
81+
(Some(_), Some(_)) => bail!("Cannot specify both `secret` and `secret_file`"),
82+
}
83+
}
84+
}
85+
86+
impl From<Secret> for SecretRaw {
87+
fn from(value: Secret) -> Self {
88+
match value {
89+
Secret::File(path) => SecretRaw {
90+
secret_file: Some(path),
91+
secret: None,
92+
},
93+
Secret::Value(secret) => SecretRaw {
94+
secret_file: None,
95+
secret: Some(secret),
96+
},
97+
}
98+
}
99+
}
100+
51101
/// Configuration related to the Matrix homeserver
52102
#[serde_as]
53103
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
@@ -61,7 +111,10 @@ pub struct MatrixConfig {
61111
pub homeserver: String,
62112

63113
/// Shared secret to use for calls to the admin API
64-
pub secret: String,
114+
#[schemars(with = "SecretRaw")]
115+
#[serde_as(as = "serde_with::TryFromInto<SecretRaw>")]
116+
#[serde(flatten)]
117+
pub secret: Secret,
65118

66119
/// The base URL of the homeserver's client API
67120
#[serde(default = "default_endpoint")]
@@ -73,14 +126,28 @@ impl ConfigurationSection for MatrixConfig {
73126
}
74127

75128
impl MatrixConfig {
129+
/// Returns the shared secret.
130+
///
131+
/// If `secret_file` was given, the secret is read from that file.
132+
///
133+
/// # Errors
134+
///
135+
/// Returns an error when the shared secret could not be read from file.
136+
pub async fn secret(&self) -> anyhow::Result<String> {
137+
Ok(match &self.secret {
138+
Secret::File(path) => tokio::fs::read_to_string(path).await?,
139+
Secret::Value(secret) => secret.clone(),
140+
})
141+
}
142+
76143
pub(crate) fn generate<R>(mut rng: R) -> Self
77144
where
78145
R: Rng + Send,
79146
{
80147
Self {
81148
kind: HomeserverKind::default(),
82149
homeserver: default_homeserver(),
83-
secret: Alphanumeric.sample_string(&mut rng, 32),
150+
secret: Secret::Value(Alphanumeric.sample_string(&mut rng, 32)),
84151
endpoint: default_endpoint(),
85152
}
86153
}
@@ -89,7 +156,7 @@ impl MatrixConfig {
89156
Self {
90157
kind: HomeserverKind::default(),
91158
homeserver: default_homeserver(),
92-
secret: "test".to_owned(),
159+
secret: Secret::Value("test".to_owned()),
93160
endpoint: default_endpoint(),
94161
}
95162
}
@@ -101,29 +168,68 @@ mod tests {
101168
Figment, Jail,
102169
providers::{Format, Yaml},
103170
};
171+
use tokio::{runtime::Handle, task};
104172

105173
use super::*;
106174

107-
#[test]
108-
fn load_config() {
109-
Jail::expect_with(|jail| {
110-
jail.create_file(
111-
"config.yaml",
112-
r"
113-
matrix:
114-
homeserver: matrix.org
115-
secret: test
116-
",
117-
)?;
118-
119-
let config = Figment::new()
120-
.merge(Yaml::file("config.yaml"))
121-
.extract_inner::<MatrixConfig>("matrix")?;
122-
123-
assert_eq!(&config.homeserver, "matrix.org");
124-
assert_eq!(&config.secret, "test");
125-
126-
Ok(())
127-
});
175+
#[tokio::test]
176+
async fn load_config() {
177+
task::spawn_blocking(|| {
178+
Jail::expect_with(|jail| {
179+
jail.create_file(
180+
"config.yaml",
181+
r"
182+
matrix:
183+
homeserver: matrix.org
184+
secret_file: secret
185+
",
186+
)?;
187+
jail.create_file("secret", r"m472!x53c237")?;
188+
189+
let config = Figment::new()
190+
.merge(Yaml::file("config.yaml"))
191+
.extract_inner::<MatrixConfig>("matrix")?;
192+
193+
Handle::current().block_on(async move {
194+
assert_eq!(&config.homeserver, "matrix.org");
195+
assert!(matches!(config.secret, Secret::File(ref p) if p == "secret"));
196+
assert_eq!(config.secret().await.unwrap(), "m472!x53c237");
197+
});
198+
199+
Ok(())
200+
});
201+
})
202+
.await
203+
.unwrap();
204+
}
205+
206+
#[tokio::test]
207+
async fn load_config_inline_secrets() {
208+
task::spawn_blocking(|| {
209+
Jail::expect_with(|jail| {
210+
jail.create_file(
211+
"config.yaml",
212+
r"
213+
matrix:
214+
homeserver: matrix.org
215+
secret: m472!x53c237
216+
",
217+
)?;
218+
219+
let config = Figment::new()
220+
.merge(Yaml::file("config.yaml"))
221+
.extract_inner::<MatrixConfig>("matrix")?;
222+
223+
Handle::current().block_on(async move {
224+
assert_eq!(&config.homeserver, "matrix.org");
225+
assert!(matches!(config.secret, Secret::Value(ref v) if v == "m472!x53c237"));
226+
assert_eq!(config.secret().await.unwrap(), "m472!x53c237");
227+
});
228+
229+
Ok(())
230+
});
231+
})
232+
.await
233+
.unwrap();
128234
}
129235
}

docs/reference/configuration.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,9 @@ matrix:
135135

136136
# Shared secret used to authenticate the service to the homeserver
137137
# This must be of high entropy, because leaking this secret would allow anyone to perform admin actions on the homeserver
138-
secret: "SomeRandomSecret"
138+
secret_file: /path/to/secret/file
139+
# Alternatively, the shared secret can be passed inline.
140+
# secret: "SomeRandomSecret"
139141

140142
# URL to which the homeserver is accessible from the service
141143
endpoint: "http://localhost:8008"

0 commit comments

Comments
 (0)