Skip to content
This repository was archived by the owner on Sep 10, 2024. It is now read-only.

Commit eff6672

Browse files
committed
New config options to set the database certificates
1 parent bd3b19e commit eff6672

File tree

4 files changed

+243
-8
lines changed

4 files changed

+243
-8
lines changed

crates/cli/src/util.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,54 @@ fn database_connect_options_from_config(
228228
opts
229229
};
230230

231+
let options = match (config.ssl_ca.as_deref(), config.ssl_ca_file.as_deref()) {
232+
(None, None) => options,
233+
(Some(pem), None) => options.ssl_root_cert_from_pem(pem.as_bytes().to_owned()),
234+
(None, Some(path)) => options.ssl_root_cert(path),
235+
(Some(_), Some(_)) => {
236+
anyhow::bail!("invalid database configuration: both `ssl_ca` and `ssl_ca_file` are set")
237+
}
238+
};
239+
240+
let options = match (
241+
config.ssl_certificate.as_deref(),
242+
config.ssl_certificate_file.as_deref(),
243+
) {
244+
(None, None) => options,
245+
(Some(pem), None) => options.ssl_client_cert_from_pem(pem.as_bytes()),
246+
(None, Some(path)) => options.ssl_client_cert(path),
247+
(Some(_), Some(_)) => {
248+
anyhow::bail!("invalid database configuration: both `ssl_certificate` and `ssl_certificate_file` are set")
249+
}
250+
};
251+
252+
let options = match (config.ssl_key.as_deref(), config.ssl_key_file.as_deref()) {
253+
(None, None) => options,
254+
(Some(pem), None) => options.ssl_client_key_from_pem(pem.as_bytes()),
255+
(None, Some(path)) => options.ssl_client_key(path),
256+
(Some(_), Some(_)) => {
257+
anyhow::bail!(
258+
"invalid database configuration: both `ssl_key` and `ssl_key_file` are set"
259+
)
260+
}
261+
};
262+
263+
let options = match &config.ssl_mode {
264+
Some(ssl_mode) => {
265+
let ssl_mode = match ssl_mode {
266+
mas_config::PgSslMode::Disable => sqlx::postgres::PgSslMode::Disable,
267+
mas_config::PgSslMode::Allow => sqlx::postgres::PgSslMode::Allow,
268+
mas_config::PgSslMode::Prefer => sqlx::postgres::PgSslMode::Prefer,
269+
mas_config::PgSslMode::Require => sqlx::postgres::PgSslMode::Require,
270+
mas_config::PgSslMode::VerifyCa => sqlx::postgres::PgSslMode::VerifyCa,
271+
mas_config::PgSslMode::VerifyFull => sqlx::postgres::PgSslMode::VerifyFull,
272+
};
273+
274+
options.ssl_mode(ssl_mode)
275+
}
276+
None => options,
277+
};
278+
231279
let options = options
232280
.log_statements(LevelFilter::Debug)
233281
.log_slow_statements(LevelFilter::Warn, Duration::from_millis(100));

crates/config/src/sections/database.rs

Lines changed: 115 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ impl Default for DatabaseConfig {
5555
username: None,
5656
password: None,
5757
database: None,
58+
ssl_mode: None,
59+
ssl_ca: None,
60+
ssl_ca_file: None,
61+
ssl_certificate: None,
62+
ssl_certificate_file: None,
63+
ssl_key: None,
64+
ssl_key_file: None,
5865
max_connections: default_max_connections(),
5966
min_connections: Default::default(),
6067
connect_timeout: default_connect_timeout(),
@@ -64,6 +71,34 @@ impl Default for DatabaseConfig {
6471
}
6572
}
6673

74+
/// Options for controlling the level of protection provided for PostgreSQL SSL
75+
/// connections.
76+
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
77+
#[serde(rename_all = "kebab-case")]
78+
pub enum PgSslMode {
79+
/// Only try a non-SSL connection.
80+
Disable,
81+
82+
/// First try a non-SSL connection; if that fails, try an SSL connection.
83+
Allow,
84+
85+
/// First try an SSL connection; if that fails, try a non-SSL connection.
86+
Prefer,
87+
88+
/// Only try an SSL connection. If a root CA file is present, verify the
89+
/// connection in the same way as if `VerifyCa` was specified.
90+
Require,
91+
92+
/// Only try an SSL connection, and verify that the server certificate is
93+
/// issued by a trusted certificate authority (CA).
94+
VerifyCa,
95+
96+
/// Only try an SSL connection; verify that the server certificate is issued
97+
/// by a trusted CA and that the requested server host name matches that
98+
/// in the certificate.
99+
VerifyFull,
100+
}
101+
67102
/// Database connection configuration
68103
#[serde_as]
69104
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
@@ -115,6 +150,50 @@ pub struct DatabaseConfig {
115150
#[serde(skip_serializing_if = "Option::is_none")]
116151
pub database: Option<String>,
117152

153+
/// How to handle SSL connections
154+
#[serde(skip_serializing_if = "Option::is_none")]
155+
pub ssl_mode: Option<PgSslMode>,
156+
157+
/// The PEM-encoded root certificate for SSL connections
158+
///
159+
/// This must not be specified if the `ssl_ca_file` option is specified.
160+
#[serde(skip_serializing_if = "Option::is_none")]
161+
pub ssl_ca: Option<String>,
162+
163+
/// Path to the root certificate for SSL connections
164+
///
165+
/// This must not be specified if the `ssl_ca` option is specified.
166+
#[serde(skip_serializing_if = "Option::is_none")]
167+
#[schemars(with = "Option<String>")]
168+
pub ssl_ca_file: Option<Utf8PathBuf>,
169+
170+
/// The PEM-encoded client certificate for SSL connections
171+
///
172+
/// This must not be specified if the `ssl_certificate_file` option is
173+
/// specified.
174+
#[serde(skip_serializing_if = "Option::is_none")]
175+
pub ssl_certificate: Option<String>,
176+
177+
/// Path to the client certificate for SSL connections
178+
///
179+
/// This must not be specified if the `ssl_certificate` option is specified.
180+
#[serde(skip_serializing_if = "Option::is_none")]
181+
#[schemars(with = "Option<String>")]
182+
pub ssl_certificate_file: Option<Utf8PathBuf>,
183+
184+
/// The PEM-encoded client key for SSL connections
185+
///
186+
/// This must not be specified if the `ssl_key_file` option is specified.
187+
#[serde(skip_serializing_if = "Option::is_none")]
188+
pub ssl_key: Option<String>,
189+
190+
/// Path to the client key for SSL connections
191+
///
192+
/// This must not be specified if the `ssl_key` option is specified.
193+
#[serde(skip_serializing_if = "Option::is_none")]
194+
#[schemars(with = "Option<String>")]
195+
pub ssl_key_file: Option<Utf8PathBuf>,
196+
118197
/// Set the maximum number of connections the pool should maintain
119198
#[serde(default = "default_max_connections")]
120199
pub max_connections: NonZeroU32,
@@ -153,6 +232,12 @@ impl ConfigurationSection for DatabaseConfig {
153232

154233
fn validate(&self, figment: &figment::Figment) -> Result<(), figment::error::Error> {
155234
let metadata = figment.find_metadata(Self::PATH.unwrap());
235+
let annotate = |mut error: figment::Error| {
236+
error.metadata = metadata.cloned();
237+
error.profile = Some(figment::Profile::Default);
238+
error.path = vec![Self::PATH.unwrap().to_owned()];
239+
Err(error)
240+
};
156241

157242
// Check that the user did not specify both `uri` and the split options at the
158243
// same time
@@ -164,19 +249,42 @@ impl ConfigurationSection for DatabaseConfig {
164249
|| self.database.is_some();
165250

166251
if self.uri.is_some() && has_split_options {
167-
let mut error = figment::error::Error::from(
252+
return annotate(figment::error::Error::from(
168253
"uri must not be specified if host, port, socket, username, password, or database are specified".to_owned(),
169-
);
170-
error.metadata = metadata.cloned();
171-
error.profile = Some(figment::Profile::Default);
172-
error.path = vec![Self::PATH.unwrap().to_owned(), "uri".to_owned()];
173-
return Err(error);
254+
));
255+
}
256+
257+
if self.ssl_ca.is_some() && self.ssl_ca_file.is_some() {
258+
return annotate(figment::error::Error::from(
259+
"ssl_ca must not be specified if ssl_ca_file is specified".to_owned(),
260+
));
261+
}
262+
263+
if self.ssl_certificate.is_some() && self.ssl_certificate_file.is_some() {
264+
return annotate(figment::error::Error::from(
265+
"ssl_certificate must not be specified if ssl_certificate_file is specified"
266+
.to_owned(),
267+
));
268+
}
269+
270+
if self.ssl_key.is_some() && self.ssl_key_file.is_some() {
271+
return annotate(figment::error::Error::from(
272+
"ssl_key must not be specified if ssl_key_file is specified".to_owned(),
273+
));
274+
}
275+
276+
if (self.ssl_key.is_some() || self.ssl_key_file.is_some())
277+
^ (self.ssl_certificate.is_some() || self.ssl_certificate_file.is_some())
278+
{
279+
return annotate(figment::error::Error::from(
280+
"both a ssl_certificate and a ssl_key must be set at the same time or none of them"
281+
.to_owned(),
282+
));
174283
}
175284

176285
Ok(())
177286
}
178287
}
179-
180288
#[cfg(test)]
181289
mod tests {
182290
use figment::{

crates/config/src/sections/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ pub use self::{
3535
branding::BrandingConfig,
3636
captcha::{CaptchaConfig, CaptchaServiceKind},
3737
clients::{ClientAuthMethodConfig, ClientConfig, ClientsConfig},
38-
database::DatabaseConfig,
38+
database::{DatabaseConfig, PgSslMode},
3939
email::{EmailConfig, EmailSmtpMode, EmailTransportKind},
4040
experimental::ExperimentalConfig,
4141
http::{

docs/config.schema.json

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,6 +1003,38 @@
10031003
"description": "The database name\n\nThis must not be specified if `uri` is specified.",
10041004
"type": "string"
10051005
},
1006+
"ssl_mode": {
1007+
"description": "How to handle SSL connections",
1008+
"allOf": [
1009+
{
1010+
"$ref": "#/definitions/PgSslMode"
1011+
}
1012+
]
1013+
},
1014+
"ssl_ca": {
1015+
"description": "The PEM-encoded root certificate for SSL connections\n\nThis must not be specified if the `ssl_ca_file` option is specified.",
1016+
"type": "string"
1017+
},
1018+
"ssl_ca_file": {
1019+
"description": "Path to the root certificate for SSL connections\n\nThis must not be specified if the `ssl_ca` option is specified.",
1020+
"type": "string"
1021+
},
1022+
"ssl_certificate": {
1023+
"description": "The PEM-encoded client certificate for SSL connections\n\nThis must not be specified if the `ssl_certificate_file` option is specified.",
1024+
"type": "string"
1025+
},
1026+
"ssl_certificate_file": {
1027+
"description": "Path to the client certificate for SSL connections\n\nThis must not be specified if the `ssl_certificate` option is specified.",
1028+
"type": "string"
1029+
},
1030+
"ssl_key": {
1031+
"description": "The PEM-encoded client key for SSL connections\n\nThis must not be specified if the `ssl_key_file` option is specified.",
1032+
"type": "string"
1033+
},
1034+
"ssl_key_file": {
1035+
"description": "Path to the client key for SSL connections\n\nThis must not be specified if the `ssl_key` option is specified.",
1036+
"type": "string"
1037+
},
10061038
"max_connections": {
10071039
"description": "Set the maximum number of connections the pool should maintain",
10081040
"default": 10,
@@ -1044,6 +1076,53 @@
10441076
"type": "string",
10451077
"format": "hostname"
10461078
},
1079+
"PgSslMode": {
1080+
"description": "Options for controlling the level of protection provided for PostgreSQL SSL connections.",
1081+
"oneOf": [
1082+
{
1083+
"description": "Only try a non-SSL connection.",
1084+
"type": "string",
1085+
"enum": [
1086+
"disable"
1087+
]
1088+
},
1089+
{
1090+
"description": "First try a non-SSL connection; if that fails, try an SSL connection.",
1091+
"type": "string",
1092+
"enum": [
1093+
"allow"
1094+
]
1095+
},
1096+
{
1097+
"description": "First try an SSL connection; if that fails, try a non-SSL connection.",
1098+
"type": "string",
1099+
"enum": [
1100+
"prefer"
1101+
]
1102+
},
1103+
{
1104+
"description": "Only try an SSL connection. If a root CA file is present, verify the connection in the same way as if `VerifyCa` was specified.",
1105+
"type": "string",
1106+
"enum": [
1107+
"require"
1108+
]
1109+
},
1110+
{
1111+
"description": "Only try an SSL connection, and verify that the server certificate is issued by a trusted certificate authority (CA).",
1112+
"type": "string",
1113+
"enum": [
1114+
"verify-ca"
1115+
]
1116+
},
1117+
{
1118+
"description": "Only try an SSL connection; verify that the server certificate is issued by a trusted CA and that the requested server host name matches that in the certificate.",
1119+
"type": "string",
1120+
"enum": [
1121+
"verify-full"
1122+
]
1123+
}
1124+
]
1125+
},
10471126
"TelemetryConfig": {
10481127
"description": "Configuration related to sending monitoring data",
10491128
"type": "object",

0 commit comments

Comments
 (0)