Skip to content

Commit b9e342c

Browse files
authored
Add matrix.secret_file config option (element-hq#4840)
2 parents 5a0bf9c + 9969898 commit b9e342c

File tree

8 files changed

+153
-44
lines changed

8 files changed

+153
-44
lines changed

crates/cli/src/commands/doctor.rs

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

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

crates/cli/src/commands/manage.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,8 @@ impl Options {
325325
let matrix_config =
326326
MatrixConfig::extract(figment).map_err(anyhow::Error::from_boxed)?;
327327
let http_client = mas_http::reqwest_client();
328-
let homeserver = homeserver_connection_from_config(&matrix_config, http_client);
328+
let homeserver =
329+
homeserver_connection_from_config(&matrix_config, http_client).await?;
329330
let mut conn = database_connection_from_config(&database_config).await?;
330331
let txn = conn.begin().await?;
331332
let mut repo = PgRepository::from_conn(txn);
@@ -612,7 +613,8 @@ impl Options {
612613
MatrixConfig::extract(figment).map_err(anyhow::Error::from_boxed)?;
613614

614615
let password_manager = password_manager_from_config(&password_config).await?;
615-
let homeserver = homeserver_connection_from_config(&matrix_config, http_client);
616+
let homeserver =
617+
homeserver_connection_from_config(&matrix_config, http_client).await?;
616618
let mut conn = database_connection_from_config(&database_config).await?;
617619
let txn = conn.begin().await?;
618620
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
@@ -167,7 +167,7 @@ impl Options {
167167
let http_client = mas_http::reqwest_client();
168168

169169
let homeserver_connection =
170-
homeserver_connection_from_config(&config.matrix, http_client.clone());
170+
homeserver_connection_from_config(&config.matrix, http_client.clone()).await?;
171171

172172
if !self.no_worker {
173173
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::SynapseModern => {
473473
Arc::new(SynapseConnection::new(
474474
config.homeserver.clone(),
475475
config.endpoint.clone(),
476-
config.secret.clone(),
476+
config.secret().await?,
477477
http_client,
478478
))
479479
}
480480
HomeserverKind::SynapseLegacy => Arc::new(LegacySynapseConnection::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 = SynapseConnection::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},
@@ -44,6 +46,54 @@ pub enum HomeserverKind {
4446
SynapseModern,
4547
}
4648

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

59109
/// Shared secret to use for calls to the admin API
60-
pub secret: String,
110+
#[schemars(with = "SecretRaw")]
111+
#[serde_as(as = "serde_with::TryFromInto<SecretRaw>")]
112+
#[serde(flatten)]
113+
pub secret: Secret,
61114

62115
/// The base URL of the homeserver's client API
63116
#[serde(default = "default_endpoint")]
@@ -69,14 +122,28 @@ impl ConfigurationSection for MatrixConfig {
69122
}
70123

71124
impl MatrixConfig {
125+
/// Returns the shared secret.
126+
///
127+
/// If `secret_file` was given, the secret is read from that file.
128+
///
129+
/// # Errors
130+
///
131+
/// Returns an error when the shared secret could not be read from file.
132+
pub async fn secret(&self) -> anyhow::Result<String> {
133+
Ok(match &self.secret {
134+
Secret::File(path) => tokio::fs::read_to_string(path).await?,
135+
Secret::Value(secret) => secret.clone(),
136+
})
137+
}
138+
72139
pub(crate) fn generate<R>(mut rng: R) -> Self
73140
where
74141
R: Rng + Send,
75142
{
76143
Self {
77144
kind: HomeserverKind::default(),
78145
homeserver: default_homeserver(),
79-
secret: Alphanumeric.sample_string(&mut rng, 32),
146+
secret: Secret::Value(Alphanumeric.sample_string(&mut rng, 32)),
80147
endpoint: default_endpoint(),
81148
}
82149
}
@@ -85,7 +152,7 @@ impl MatrixConfig {
85152
Self {
86153
kind: HomeserverKind::default(),
87154
homeserver: default_homeserver(),
88-
secret: "test".to_owned(),
155+
secret: Secret::Value("test".to_owned()),
89156
endpoint: default_endpoint(),
90157
}
91158
}
@@ -97,29 +164,68 @@ mod tests {
97164
Figment, Jail,
98165
providers::{Format, Yaml},
99166
};
167+
use tokio::{runtime::Handle, task};
100168

101169
use super::*;
102170

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

docs/config.schema.json

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1677,9 +1677,6 @@
16771677
"MatrixConfig": {
16781678
"description": "Configuration related to the Matrix homeserver",
16791679
"type": "object",
1680-
"required": [
1681-
"secret"
1682-
],
16831680
"properties": {
16841681
"kind": {
16851682
"description": "The kind of homeserver it is.",
@@ -1695,15 +1692,17 @@
16951692
"default": "localhost:8008",
16961693
"type": "string"
16971694
},
1698-
"secret": {
1699-
"description": "Shared secret to use for calls to the admin API",
1700-
"type": "string"
1701-
},
17021695
"endpoint": {
17031696
"description": "The base URL of the homeserver's client API",
17041697
"default": "http://localhost:8008/",
17051698
"type": "string",
17061699
"format": "uri"
1700+
},
1701+
"secret_file": {
1702+
"type": "string"
1703+
},
1704+
"secret": {
1705+
"type": "string"
17071706
}
17081707
}
17091708
},

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)