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 ;
79use 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
71124impl 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}
0 commit comments