1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8#[derive(Clone, Debug, Serialize, Deserialize)]
10pub struct AppConfig {
11 #[serde(default = "default_db_uri")]
16 pub db_uri: String,
17
18 #[serde(default = "default_db_schemas")]
20 pub db_schemas: Vec<String>,
21
22 pub db_anon_role: Option<String>,
24
25 #[serde(default = "default_pool_size")]
27 pub db_pool_size: u32,
28
29 #[serde(default = "default_pool_timeout")]
31 pub db_pool_timeout: u64,
32
33 #[serde(default = "default_tx_isolation")]
39 pub db_tx_isolation: IsolationLevel,
40
41 #[serde(default = "default_true")]
43 pub db_prepared_statements: bool,
44
45 #[serde(default = "default_extra_search_path")]
53 pub db_extra_search_path: Vec<String>,
54
55 #[serde(default = "default_db_channel")]
57 pub db_channel: String,
58
59 #[serde(default)]
61 pub db_channel_enabled: bool,
62
63 pub db_pre_request: Option<String>,
66
67 pub db_max_rows: Option<i64>,
69
70 #[serde(default)]
75 pub db_aggregates_enabled: bool,
76
77 #[serde(default = "default_host")]
82 pub server_host: String,
83
84 #[serde(default = "default_port")]
86 pub server_port: u16,
87
88 #[serde(default)]
96 pub server_cors_origins: Vec<String>,
97
98 #[serde(default = "default_max_body_size")]
105 pub max_body_size: usize,
106
107 pub server_unix_socket: Option<String>,
109
110 pub admin_server_port: Option<u16>,
112
113 pub jwt_secret: Option<String>,
118
119 #[serde(default)]
121 pub jwt_secret_is_base64: bool,
122
123 pub jwt_aud: Option<String>,
125
126 #[serde(default = "default_jwt_role_claim")]
128 pub jwt_role_claim_key: String,
129
130 #[serde(default = "default_true")]
132 pub jwt_cache_enabled: bool,
133
134 #[serde(default = "default_jwt_cache_max")]
139 pub jwt_cache_max_lifetime: u64,
140
141 pub hasura_admin_secret: Option<String>,
154
155 pub hasura_unauthorized_role: Option<String>,
159
160 pub openapi_server_proxy_uri: Option<String>,
167
168 #[serde(default = "default_openapi_mode")]
170 pub openapi_mode: OpenApiMode,
171
172 #[serde(default = "default_log_level")]
177 pub log_level: LogLevel,
178
179 #[serde(default)]
184 pub role_settings: HashMap<String, RoleSettings>,
185
186 #[serde(default)]
188 pub app_settings: HashMap<String, String>,
189
190 #[serde(default)]
202 pub compat_mode: bool,
203}
204
205impl Default for AppConfig {
206 fn default() -> Self {
207 Self {
208 db_uri: default_db_uri(),
209 db_schemas: default_db_schemas(),
210 db_anon_role: None,
211 db_pool_size: default_pool_size(),
212 db_pool_timeout: default_pool_timeout(),
213 db_tx_isolation: default_tx_isolation(),
214 db_prepared_statements: true,
215 db_extra_search_path: default_extra_search_path(),
216 db_channel: default_db_channel(),
217 db_channel_enabled: false,
218 db_pre_request: None,
219 db_max_rows: None,
220 db_aggregates_enabled: false,
221 server_host: default_host(),
222 server_port: default_port(),
223 server_cors_origins: Vec::new(),
224 max_body_size: default_max_body_size(),
225 server_unix_socket: None,
226 admin_server_port: None,
227 jwt_secret: None,
228 jwt_secret_is_base64: false,
229 jwt_aud: None,
230 jwt_role_claim_key: default_jwt_role_claim(),
231 jwt_cache_enabled: true,
232 jwt_cache_max_lifetime: default_jwt_cache_max(),
233 hasura_admin_secret: None,
234 hasura_unauthorized_role: None,
235 openapi_server_proxy_uri: None,
236 openapi_mode: OpenApiMode::FollowPrivileges,
237 log_level: default_log_level(),
238 role_settings: HashMap::new(),
239 app_settings: HashMap::new(),
240 compat_mode: false,
241 }
242 }
243}
244
245fn warn_ignored(var: &str, value: &str, expected: &str) {
249 tracing::warn!("Ignoring {}={:?}: expected {}", var, value, expected);
250}
251
252fn env_bool(value: &str) -> bool {
254 matches!(
255 value.trim().to_ascii_lowercase().as_str(),
256 "true" | "1" | "yes" | "on"
257 )
258}
259
260impl AppConfig {
261 pub fn from_env() -> Self {
263 let mut config = Self::default();
264
265 if let Ok(uri) = std::env::var("PGRST_DB_URI") {
266 config.db_uri = uri;
267 }
268 if let Ok(uri) = std::env::var("DATABASE_URL") {
269 config.db_uri = uri;
270 }
271 if let Ok(schemas) = std::env::var("PGRST_DB_SCHEMAS") {
272 config.db_schemas = schemas.split(',').map(|s| s.trim().to_string()).collect();
273 }
274 if let Ok(role) = std::env::var("PGRST_DB_ANON_ROLE") {
275 config.db_anon_role = Some(role);
276 }
277 for var in ["PGRST_DB_POOL", "PGRST_DB_POOL_SIZE"] {
283 if let Ok(size) = std::env::var(var) {
284 match size.parse::<u32>() {
285 Ok(n) if n > 0 => config.db_pool_size = n,
286 _ => warn_ignored(var, &size, "a positive integer"),
287 }
288 }
289 }
290 if let Ok(secs) = std::env::var("PGRST_DB_POOL_TIMEOUT") {
291 match secs.parse::<u64>() {
292 Ok(n) if n > 0 => config.db_pool_timeout = n,
293 _ => warn_ignored("PGRST_DB_POOL_TIMEOUT", &secs, "a positive integer"),
294 }
295 }
296 if let Ok(level) = std::env::var("PGRST_DB_TX_ISOLATION") {
297 match IsolationLevel::parse_config(&level) {
298 Some(l) => config.db_tx_isolation = l,
299 None => warn_ignored(
300 "PGRST_DB_TX_ISOLATION",
301 &level,
302 "one of: read committed, repeatable read, serializable",
303 ),
304 }
305 }
306 if let Ok(v) = std::env::var("PGRST_DB_EXTRA_SEARCH_PATH") {
307 config.db_extra_search_path = v
308 .split(',')
309 .map(|s| s.trim().to_string())
310 .filter(|s| !s.is_empty())
311 .collect();
312 }
313
314 if let Ok(v) = std::env::var("PGRST_DB_AGGREGATES_ENABLED") {
315 config.db_aggregates_enabled = env_bool(&v);
316 }
317 if let Ok(v) = std::env::var("PGRST_DB_PREPARED_STATEMENTS") {
318 config.db_prepared_statements = env_bool(&v);
319 }
320 if let Ok(v) = std::env::var("PGRST_DB_PRE_REQUEST") {
321 if !v.trim().is_empty() {
322 config.db_pre_request = Some(v.trim().to_string());
323 }
324 }
325 if let Ok(v) = std::env::var("PGRST_DB_CHANNEL") {
326 if !v.trim().is_empty() {
327 config.db_channel = v.trim().to_string();
328 }
329 }
330 if let Ok(v) = std::env::var("PGRST_DB_CHANNEL_ENABLED") {
331 config.db_channel_enabled = env_bool(&v);
332 }
333
334 for var in ["PGRST_DB_MAX_ROWS", "PGRST_MAX_ROWS"] {
336 if let Ok(max_rows) = std::env::var(var) {
337 match max_rows.parse::<i64>() {
338 Ok(n) if n >= 0 => config.db_max_rows = Some(n),
339 _ => warn_ignored(var, &max_rows, "a non-negative integer"),
340 }
341 }
342 }
343 if let Ok(secret) = std::env::var("PGRST_JWT_SECRET") {
344 config.jwt_secret = Some(secret);
345 }
346 if let Ok(aud) = std::env::var("PGRST_JWT_AUD") {
347 config.jwt_aud = Some(aud);
348 }
349 if let Ok(v) = std::env::var("PGRST_JWT_SECRET_IS_BASE64") {
354 config.jwt_secret_is_base64 = env_bool(&v);
355 }
356 if let Ok(key) = std::env::var("PGRST_JWT_ROLE_CLAIM_KEY") {
357 if !key.trim().is_empty() {
358 config.jwt_role_claim_key = key;
359 }
360 }
361 if let Ok(v) = std::env::var("PGRST_JWT_CACHE_ENABLED") {
362 config.jwt_cache_enabled = env_bool(&v);
363 }
364 if let Ok(v) = std::env::var("PGRST_JWT_CACHE_MAX_LIFETIME") {
367 match v.parse::<u64>() {
368 Ok(n) => config.jwt_cache_max_lifetime = n,
369 Err(_) => warn_ignored(
370 "PGRST_JWT_CACHE_MAX_LIFETIME",
371 &v,
372 "a non-negative integer number of seconds",
373 ),
374 }
375 }
376 for var in ["HASURA_GRAPHQL_ADMIN_SECRET", "PGRST_HASURA_ADMIN_SECRET"] {
382 if let Ok(secret) = std::env::var(var) {
383 if !secret.is_empty() {
384 config.hasura_admin_secret = Some(secret);
385 }
386 }
387 }
388 for var in [
389 "HASURA_GRAPHQL_UNAUTHORIZED_ROLE",
390 "PGRST_HASURA_UNAUTHORIZED_ROLE",
391 ] {
392 if let Ok(role) = std::env::var(var) {
393 if !role.is_empty() {
394 config.hasura_unauthorized_role = Some(role);
395 }
396 }
397 }
398 if let Ok(host) = std::env::var("PGRST_SERVER_HOST") {
399 config.server_host = host;
400 }
401 if let Ok(port) = std::env::var("PGRST_SERVER_PORT") {
402 if let Ok(p) = port.parse() {
403 config.server_port = p;
404 }
405 }
406 if let Ok(port) = std::env::var("PORT") {
407 if let Ok(p) = port.parse() {
408 config.server_port = p;
409 }
410 }
411 if let Ok(origins) = std::env::var("PGRST_SERVER_CORS_ORIGINS") {
415 config.server_cors_origins = if origins.trim() == "*" {
416 Vec::new()
417 } else {
418 origins
419 .split(',')
420 .map(|s| s.trim().to_string())
421 .filter(|s| !s.is_empty())
422 .collect()
423 };
424 }
425 if let Ok(bytes) = std::env::var("PGRST_MAX_BODY_SIZE") {
426 match bytes.parse::<usize>() {
427 Ok(n) if n > 0 => config.max_body_size = n,
428 _ => warn_ignored("PGRST_MAX_BODY_SIZE", &bytes, "a positive integer"),
429 }
430 }
431 if let Ok(level) = std::env::var("PGRST_LOG_LEVEL") {
432 match LogLevel::parse_config(&level) {
433 Some(l) => config.log_level = l,
434 None => warn_ignored(
435 "PGRST_LOG_LEVEL",
436 &level,
437 "one of: crit, error, warn, info, debug",
438 ),
439 }
440 }
441 if let Ok(path) = std::env::var("PGRST_SERVER_UNIX_SOCKET") {
442 if !path.trim().is_empty() {
443 config.server_unix_socket = Some(path.trim().to_string());
444 }
445 }
446 if let Ok(port) = std::env::var("PGRST_ADMIN_SERVER_PORT") {
447 match port.parse::<u16>() {
448 Ok(p) if p > 0 => config.admin_server_port = Some(p),
449 _ => warn_ignored("PGRST_ADMIN_SERVER_PORT", &port, "a port number"),
450 }
451 }
452 if let Ok(uri) = std::env::var("PGRST_OPENAPI_SERVER_PROXY_URI") {
453 if !uri.trim().is_empty() {
454 config.openapi_server_proxy_uri = Some(uri.trim().to_string());
455 }
456 }
457 if let Ok(mode) = std::env::var("PGRST_OPENAPI_MODE") {
458 match OpenApiMode::parse_config(&mode) {
459 Some(m) => config.openapi_mode = m,
460 None => warn_ignored(
461 "PGRST_OPENAPI_MODE",
462 &mode,
463 "one of: disabled, follow-privileges, ignore-privileges",
464 ),
465 }
466 }
467
468 if let Ok(raw) = std::env::var("PGRST_ROLE_SETTINGS") {
472 match serde_json::from_str::<HashMap<String, RoleSettings>>(&raw) {
473 Ok(settings) => config.role_settings = settings,
474 Err(e) => warn_ignored(
475 "PGRST_ROLE_SETTINGS",
476 &raw,
477 &format!("a JSON object of role to settings ({e})"),
478 ),
479 }
480 }
481
482 for (key, value) in std::env::vars() {
485 if let Some(name) = key.strip_prefix("PGRST_APP_SETTINGS_") {
486 if !name.is_empty() {
487 config.app_settings.insert(name.to_ascii_lowercase(), value);
488 }
489 }
490 }
491 for var in ["PGRST_COMPAT_MODE", "POSTRUST_COMPAT_MODE"] {
494 if let Ok(v) = std::env::var(var) {
495 config.compat_mode = env_bool(&v);
496 }
497 }
498
499 config
500 }
501
502 pub fn default_schema(&self) -> &str {
504 self.db_schemas
505 .first()
506 .map(|s| s.as_str())
507 .unwrap_or("public")
508 }
509}
510
511impl Serialize for IsolationLevel {
517 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
518 serializer.serialize_str(self.as_config_str())
519 }
520}
521
522impl<'de> Deserialize<'de> for IsolationLevel {
523 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
524 let raw = String::deserialize(deserializer)?;
525 Self::parse_config(&raw).ok_or_else(|| {
526 serde::de::Error::custom(format!(
527 "unknown isolation level {raw:?}: expected one of \
528 read committed, repeatable read, serializable"
529 ))
530 })
531 }
532}
533
534impl Serialize for OpenApiMode {
535 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
536 serializer.serialize_str(self.as_config_str())
537 }
538}
539
540impl<'de> Deserialize<'de> for OpenApiMode {
541 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
542 let raw = String::deserialize(deserializer)?;
543 Self::parse_config(&raw).ok_or_else(|| {
544 serde::de::Error::custom(format!(
545 "unknown OpenAPI mode {raw:?}: expected one of \
546 disabled, follow-privileges, ignore-privileges"
547 ))
548 })
549 }
550}
551
552impl Serialize for LogLevel {
553 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
554 serializer.serialize_str(self.as_config_str())
555 }
556}
557
558impl<'de> Deserialize<'de> for LogLevel {
559 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
560 let raw = String::deserialize(deserializer)?;
561 Self::parse_config(&raw).ok_or_else(|| {
562 serde::de::Error::custom(format!(
563 "unknown log level {raw:?}: expected one of \
564 crit, error, warn, info, debug"
565 ))
566 })
567 }
568}
569
570#[derive(Clone, Debug, Serialize, Deserialize)]
572pub struct RoleSettings {
573 pub isolation_level: Option<IsolationLevel>,
575 pub statement_timeout: Option<u64>,
577}
578
579#[derive(Clone, Debug, PartialEq, Eq)]
585pub enum IsolationLevel {
586 ReadCommitted,
588 RepeatableRead,
590 Serializable,
594}
595
596impl IsolationLevel {
597 pub fn to_sql(&self) -> &'static str {
599 match self {
600 Self::ReadCommitted => "READ COMMITTED",
601 Self::RepeatableRead => "REPEATABLE READ",
602 Self::Serializable => "SERIALIZABLE",
603 }
604 }
605
606 pub fn parse_config(s: &str) -> Option<Self> {
612 match s
613 .trim()
614 .to_ascii_lowercase()
615 .replace(['-', '_'], " ")
616 .as_str()
617 {
618 "read committed" => Some(Self::ReadCommitted),
619 "repeatable read" => Some(Self::RepeatableRead),
620 "serializable" => Some(Self::Serializable),
621 _ => None,
622 }
623 }
624
625 pub fn as_config_str(&self) -> &'static str {
627 match self {
628 Self::ReadCommitted => "read committed",
629 Self::RepeatableRead => "repeatable read",
630 Self::Serializable => "serializable",
631 }
632 }
633}
634
635#[derive(Clone, Debug, PartialEq, Eq)]
642pub enum OpenApiMode {
643 Disabled,
645 FollowPrivileges,
648 IgnorePrivileges,
650}
651
652impl OpenApiMode {
653 pub fn parse_config(s: &str) -> Option<Self> {
656 match s
657 .trim()
658 .to_ascii_lowercase()
659 .replace(['-', '_'], " ")
660 .as_str()
661 {
662 "disabled" => Some(Self::Disabled),
663 "follow privileges" => Some(Self::FollowPrivileges),
664 "ignore privileges" => Some(Self::IgnorePrivileges),
665 _ => None,
666 }
667 }
668
669 pub fn as_config_str(&self) -> &'static str {
671 match self {
672 Self::Disabled => "disabled",
673 Self::FollowPrivileges => "follow-privileges",
674 Self::IgnorePrivileges => "ignore-privileges",
675 }
676 }
677}
678
679#[derive(Clone, Debug, PartialEq, Eq)]
681pub enum LogLevel {
682 Crit,
685 Error,
687 Warn,
689 Info,
691 Debug,
693}
694
695impl LogLevel {
696 pub fn to_tracing(&self) -> tracing::Level {
701 match self {
702 Self::Crit | Self::Error => tracing::Level::ERROR,
703 Self::Warn => tracing::Level::WARN,
704 Self::Info => tracing::Level::INFO,
705 Self::Debug => tracing::Level::DEBUG,
706 }
707 }
708
709 pub fn parse_config(s: &str) -> Option<Self> {
711 match s.trim().to_ascii_lowercase().as_str() {
712 "crit" | "critical" => Some(Self::Crit),
713 "error" => Some(Self::Error),
714 "warn" | "warning" => Some(Self::Warn),
715 "info" => Some(Self::Info),
716 "debug" => Some(Self::Debug),
717 _ => None,
718 }
719 }
720
721 pub fn as_config_str(&self) -> &'static str {
723 match self {
724 Self::Crit => "crit",
725 Self::Error => "error",
726 Self::Warn => "warn",
727 Self::Info => "info",
728 Self::Debug => "debug",
729 }
730 }
731}
732
733fn default_db_uri() -> String {
735 "postgresql://localhost/postgres".to_string()
736}
737
738fn default_db_schemas() -> Vec<String> {
739 vec!["public".to_string()]
740}
741
742fn default_pool_size() -> u32 {
743 10
744}
745
746fn default_pool_timeout() -> u64 {
747 10
748}
749
750fn default_tx_isolation() -> IsolationLevel {
751 IsolationLevel::ReadCommitted
752}
753
754fn default_max_body_size() -> usize {
755 10 * 1024 * 1024
756}
757
758fn default_extra_search_path() -> Vec<String> {
759 vec!["public".to_string()]
760}
761
762fn default_db_channel() -> String {
763 "pgrst".to_string()
764}
765
766fn default_host() -> String {
767 "127.0.0.1".to_string()
768}
769
770fn default_port() -> u16 {
771 3000
772}
773
774fn default_jwt_role_claim() -> String {
775 "role".to_string()
776}
777
778fn default_jwt_cache_max() -> u64 {
779 3600
780}
781
782fn default_openapi_mode() -> OpenApiMode {
783 OpenApiMode::FollowPrivileges
784}
785
786fn default_log_level() -> LogLevel {
791 LogLevel::Info
792}
793
794fn default_true() -> bool {
795 true
796}
797
798#[cfg(test)]
799mod tests {
800 use super::*;
801
802 #[test]
803 fn test_default_config() {
804 let config = AppConfig::default();
805 assert_eq!(config.server_port, 3000);
806 assert_eq!(config.db_pool_size, 10);
807 assert!(config.db_prepared_statements);
808 }
809
810 #[test]
811 fn test_default_schema() {
812 let mut config = AppConfig::default();
813 assert_eq!(config.default_schema(), "public");
814
815 config.db_schemas = vec!["api".to_string(), "public".to_string()];
816 assert_eq!(config.default_schema(), "api");
817 }
818
819 #[test]
820 fn test_isolation_level_sql() {
821 assert_eq!(IsolationLevel::ReadCommitted.to_sql(), "READ COMMITTED");
822 assert_eq!(IsolationLevel::Serializable.to_sql(), "SERIALIZABLE");
823 }
824
825 #[test]
826 fn isolation_level_accepts_the_documented_spellings() {
827 for s in [
830 "read committed",
831 "READ COMMITTED",
832 "read-committed",
833 " Read_Committed ",
834 ] {
835 assert_eq!(
836 IsolationLevel::parse_config(s),
837 Some(IsolationLevel::ReadCommitted),
838 "{s:?}"
839 );
840 }
841 assert_eq!(
842 IsolationLevel::parse_config("serializable"),
843 Some(IsolationLevel::Serializable)
844 );
845 assert_eq!(
846 IsolationLevel::parse_config("repeatable read"),
847 Some(IsolationLevel::RepeatableRead)
848 );
849 assert_eq!(IsolationLevel::parse_config("dirty read"), None);
852 assert_eq!(IsolationLevel::parse_config(""), None);
853 }
854
855 #[test]
856 fn log_level_accepts_the_documented_spellings() {
857 assert_eq!(LogLevel::parse_config("INFO"), Some(LogLevel::Info));
858 assert_eq!(LogLevel::parse_config("warning"), Some(LogLevel::Warn));
859 assert_eq!(LogLevel::parse_config("crit"), Some(LogLevel::Crit));
860 assert_eq!(LogLevel::parse_config("trace"), None);
863 }
864
865 #[test]
866 fn default_log_level_matches_the_documented_one() {
867 assert_eq!(AppConfig::default().log_level, LogLevel::Info);
870 }
871
872 #[test]
873 fn env_bool_accepts_the_usual_spellings() {
874 for s in ["true", "TRUE", "1", "yes", "on", " On "] {
875 assert!(env_bool(s), "{s:?}");
876 }
877 for s in ["false", "0", "no", "off", "", "maybe"] {
878 assert!(!env_bool(s), "{s:?}");
879 }
880 }
881
882 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
884
885 fn with_env(vars: &[(&str, &str)]) -> AppConfig {
887 let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
888 for (k, v) in vars {
889 std::env::set_var(k, v);
890 }
891 let config = AppConfig::from_env();
892 for (k, _) in vars {
893 std::env::remove_var(k);
894 }
895 config
896 }
897
898 #[test]
899 fn documented_variables_are_read() {
900 let config = with_env(&[
903 ("PGRST_DB_POOL_SIZE", "42"),
904 ("PGRST_DB_POOL_TIMEOUT", "7"),
905 ("PGRST_DB_TX_ISOLATION", "serializable"),
906 ("PGRST_JWT_SECRET_IS_BASE64", "true"),
907 ("PGRST_JWT_ROLE_CLAIM_KEY", "user.role"),
908 ("PGRST_LOG_LEVEL", "warn"),
909 ("PGRST_MAX_BODY_SIZE", "1024"),
910 (
911 "PGRST_SERVER_CORS_ORIGINS",
912 "https://a.example, https://b.example",
913 ),
914 ]);
915
916 assert_eq!(config.db_pool_size, 42);
917 assert_eq!(config.db_pool_timeout, 7);
918 assert_eq!(config.db_tx_isolation, IsolationLevel::Serializable);
919 assert!(config.jwt_secret_is_base64);
920 assert_eq!(config.jwt_role_claim_key, "user.role");
921 assert_eq!(config.log_level, LogLevel::Warn);
922 assert_eq!(config.max_body_size, 1024);
923 assert_eq!(
924 config.server_cors_origins,
925 vec![
926 "https://a.example".to_string(),
927 "https://b.example".to_string()
928 ]
929 );
930 }
931
932 #[test]
933 fn pool_size_accepts_postgrests_own_name() {
934 assert_eq!(with_env(&[("PGRST_DB_POOL", "5")]).db_pool_size, 5);
935 }
936
937 #[test]
938 fn a_star_origin_means_any_origin() {
939 assert!(with_env(&[("PGRST_SERVER_CORS_ORIGINS", "*")])
942 .server_cors_origins
943 .is_empty());
944 assert!(with_env(&[("PGRST_SERVER_CORS_ORIGINS", " , ")])
945 .server_cors_origins
946 .is_empty());
947 }
948
949 #[test]
950 fn an_unusable_value_leaves_the_default() {
951 let config = with_env(&[
952 ("PGRST_DB_POOL_SIZE", "lots"),
953 ("PGRST_DB_POOL_TIMEOUT", "0"),
954 ("PGRST_MAX_BODY_SIZE", "-1"),
955 ("PGRST_DB_TX_ISOLATION", "dirty read"),
956 ("PGRST_LOG_LEVEL", "shout"),
957 ]);
958
959 let d = AppConfig::default();
960 assert_eq!(config.db_pool_size, d.db_pool_size);
961 assert_eq!(config.db_pool_timeout, d.db_pool_timeout);
962 assert_eq!(config.max_body_size, d.max_body_size);
963 assert_eq!(config.db_tx_isolation, d.db_tx_isolation);
964 assert_eq!(config.log_level, d.log_level);
965 }
966
967 #[test]
968 fn the_formerly_dead_fields_are_read() {
969 let config = with_env(&[
972 ("PGRST_DB_PREPARED_STATEMENTS", "false"),
973 ("PGRST_DB_PRE_REQUEST", "auth.check"),
974 ("PGRST_DB_CHANNEL", "my_channel"),
975 ("PGRST_DB_CHANNEL_ENABLED", "yes"),
976 ("PGRST_JWT_CACHE_ENABLED", "false"),
977 ("PGRST_JWT_CACHE_MAX_LIFETIME", "60"),
978 ("PGRST_SERVER_UNIX_SOCKET", "/tmp/postrust.sock"),
979 ("PGRST_ADMIN_SERVER_PORT", "3001"),
980 ("PGRST_OPENAPI_SERVER_PROXY_URI", "https://api.example.com"),
981 ("PGRST_OPENAPI_MODE", "ignore-privileges"),
982 ]);
983
984 assert!(!config.db_prepared_statements);
985 assert_eq!(config.db_pre_request.as_deref(), Some("auth.check"));
986 assert_eq!(config.db_channel, "my_channel");
987 assert!(config.db_channel_enabled);
988 assert!(!config.jwt_cache_enabled);
989 assert_eq!(config.jwt_cache_max_lifetime, 60);
990 assert_eq!(
991 config.server_unix_socket.as_deref(),
992 Some("/tmp/postrust.sock")
993 );
994 assert_eq!(config.admin_server_port, Some(3001));
995 assert_eq!(
996 config.openapi_server_proxy_uri.as_deref(),
997 Some("https://api.example.com")
998 );
999 assert_eq!(config.openapi_mode, OpenApiMode::IgnorePrivileges);
1000 }
1001
1002 #[test]
1003 fn a_zero_jwt_cache_lifetime_is_kept_because_it_turns_the_cache_off() {
1004 assert_eq!(
1007 with_env(&[("PGRST_JWT_CACHE_MAX_LIFETIME", "0")]).jwt_cache_max_lifetime,
1008 0
1009 );
1010 }
1011
1012 #[test]
1013 fn app_settings_come_from_a_prefix() {
1014 let config = with_env(&[
1015 ("PGRST_APP_SETTINGS_JWT_LIFETIME", "3600"),
1016 ("PGRST_APP_SETTINGS_TenantName", "acme"),
1017 ]);
1018 assert_eq!(
1021 config.app_settings.get("jwt_lifetime").map(String::as_str),
1022 Some("3600")
1023 );
1024 assert_eq!(
1025 config.app_settings.get("tenantname").map(String::as_str),
1026 Some("acme")
1027 );
1028 }
1029
1030 #[test]
1031 fn role_settings_parse_from_json_using_the_documented_spellings() {
1032 let config = with_env(&[(
1033 "PGRST_ROLE_SETTINGS",
1034 r#"{"web_user":{"isolation_level":"serializable","statement_timeout":5000}}"#,
1035 )]);
1036 let web_user = config.role_settings.get("web_user").expect("role present");
1037 assert_eq!(web_user.isolation_level, Some(IsolationLevel::Serializable));
1038 assert_eq!(web_user.statement_timeout, Some(5000));
1039 }
1040
1041 #[test]
1042 fn unparseable_role_settings_leave_the_map_empty() {
1043 assert!(with_env(&[("PGRST_ROLE_SETTINGS", "{not json")])
1044 .role_settings
1045 .is_empty());
1046 assert!(with_env(&[(
1049 "PGRST_ROLE_SETTINGS",
1050 r#"{"r":{"isolation_level":"dirty"}}"#
1051 )])
1052 .role_settings
1053 .is_empty());
1054 }
1055
1056 #[test]
1057 fn openapi_mode_accepts_the_documented_spellings() {
1058 assert_eq!(
1059 OpenApiMode::parse_config("follow-privileges"),
1060 Some(OpenApiMode::FollowPrivileges)
1061 );
1062 assert_eq!(
1063 OpenApiMode::parse_config("IGNORE_PRIVILEGES"),
1064 Some(OpenApiMode::IgnorePrivileges)
1065 );
1066 assert_eq!(
1067 OpenApiMode::parse_config("disabled"),
1068 Some(OpenApiMode::Disabled)
1069 );
1070 assert_eq!(OpenApiMode::parse_config("security-definer"), None);
1073 }
1074
1075 #[test]
1076 fn the_config_enums_round_trip_through_json() {
1077 for level in [
1083 IsolationLevel::ReadCommitted,
1084 IsolationLevel::RepeatableRead,
1085 IsolationLevel::Serializable,
1086 ] {
1087 let json = serde_json::to_string(&level).unwrap();
1088 let back: IsolationLevel = serde_json::from_str(&json).unwrap();
1089 assert_eq!(back, level, "{json}");
1090 }
1091
1092 for mode in [
1093 OpenApiMode::Disabled,
1094 OpenApiMode::FollowPrivileges,
1095 OpenApiMode::IgnorePrivileges,
1096 ] {
1097 let json = serde_json::to_string(&mode).unwrap();
1098 assert_eq!(serde_json::from_str::<OpenApiMode>(&json).unwrap(), mode);
1099 }
1100
1101 for level in [
1102 LogLevel::Crit,
1103 LogLevel::Error,
1104 LogLevel::Warn,
1105 LogLevel::Info,
1106 LogLevel::Debug,
1107 ] {
1108 let json = serde_json::to_string(&level).unwrap();
1109 assert_eq!(serde_json::from_str::<LogLevel>(&json).unwrap(), level);
1110 }
1111 }
1112
1113 #[test]
1114 fn a_whole_config_round_trips_through_json() {
1115 let mut config = AppConfig::default();
1117 config.role_settings.insert(
1118 "web_user".to_string(),
1119 RoleSettings {
1120 isolation_level: Some(IsolationLevel::RepeatableRead),
1121 statement_timeout: Some(5000),
1122 },
1123 );
1124
1125 let json = serde_json::to_string(&config).unwrap();
1126 let back: AppConfig = serde_json::from_str(&json).expect("config should round trip");
1127
1128 assert_eq!(back.db_tx_isolation, config.db_tx_isolation);
1129 assert_eq!(back.log_level, config.log_level);
1130 assert_eq!(back.openapi_mode, config.openapi_mode);
1131 assert_eq!(
1132 back.role_settings["web_user"].isolation_level,
1133 Some(IsolationLevel::RepeatableRead)
1134 );
1135 }
1136
1137 #[test]
1138 fn the_serialized_spelling_is_the_configuration_spelling() {
1139 assert_eq!(
1142 serde_json::to_string(&IsolationLevel::ReadCommitted).unwrap(),
1143 "\"read committed\""
1144 );
1145 assert_eq!(
1146 serde_json::to_string(&OpenApiMode::FollowPrivileges).unwrap(),
1147 "\"follow-privileges\""
1148 );
1149 assert_eq!(serde_json::to_string(&LogLevel::Warn).unwrap(), "\"warn\"");
1150 }
1151
1152 #[test]
1153 fn an_empty_role_claim_key_is_not_accepted() {
1154 assert_eq!(
1157 with_env(&[("PGRST_JWT_ROLE_CLAIM_KEY", " ")]).jwt_role_claim_key,
1158 AppConfig::default().jwt_role_claim_key
1159 );
1160 }
1161}