1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8const EXPECTED_BOOLEAN: &str = "one of: true, false, 1, 0, yes, no, on, off";
9
10#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
12#[error("invalid value for {variable}: {value:?}; expected {expected}")]
13pub struct ConfigError {
14 variable: &'static str,
15 value: String,
16 expected: &'static str,
17}
18
19#[derive(Clone, Debug, Serialize, Deserialize)]
21#[non_exhaustive]
22pub struct AppConfig {
23 #[serde(default = "default_db_uri")]
28 pub db_uri: String,
29
30 #[serde(default = "default_db_schemas")]
32 pub db_schemas: Vec<String>,
33
34 pub db_anon_role: Option<String>,
36
37 #[serde(default = "default_pool_size")]
39 pub db_pool_size: u32,
40
41 #[serde(default = "default_pool_timeout")]
43 pub db_pool_timeout: u64,
44
45 #[serde(default = "default_tx_isolation")]
51 pub db_tx_isolation: IsolationLevel,
52
53 #[serde(default = "default_true")]
55 pub db_prepared_statements: bool,
56
57 #[serde(default = "default_extra_search_path")]
65 pub db_extra_search_path: Vec<String>,
66
67 #[serde(default = "default_db_channel")]
69 pub db_channel: String,
70
71 #[serde(default)]
73 pub db_channel_enabled: bool,
74
75 pub db_pre_request: Option<String>,
78
79 pub db_max_rows: Option<i64>,
81
82 #[serde(default)]
87 pub db_aggregates_enabled: bool,
88
89 #[serde(default = "default_host")]
94 pub server_host: String,
95
96 #[serde(default = "default_port")]
98 pub server_port: u16,
99
100 #[serde(default)]
108 pub server_cors_origins: Vec<String>,
109
110 #[serde(default = "default_max_body_size")]
117 pub max_body_size: usize,
118
119 pub server_unix_socket: Option<String>,
121
122 pub admin_server_port: Option<u16>,
124
125 pub jwt_secret: Option<String>,
130
131 #[serde(default)]
133 pub jwt_secret_is_base64: bool,
134
135 pub jwt_aud: Option<String>,
137
138 #[serde(default = "default_jwt_role_claim")]
140 pub jwt_role_claim_key: String,
141
142 #[serde(default = "default_true")]
144 pub jwt_cache_enabled: bool,
145
146 #[serde(default = "default_jwt_cache_max")]
151 pub jwt_cache_max_lifetime: u64,
152
153 pub hasura_admin_secret: Option<String>,
166
167 pub hasura_unauthorized_role: Option<String>,
171
172 pub openapi_server_proxy_uri: Option<String>,
179
180 #[serde(default = "default_openapi_mode")]
182 pub openapi_mode: OpenApiMode,
183
184 #[serde(default = "default_log_level")]
189 pub log_level: LogLevel,
190
191 #[serde(default)]
196 pub role_settings: HashMap<String, RoleSettings>,
197
198 #[serde(default)]
200 pub app_settings: HashMap<String, String>,
201
202 #[serde(default)]
207 pub graphql_federation: bool,
208
209 #[serde(default)]
211 pub graphql_type_prefix: Option<String>,
212
213 #[serde(default)]
225 pub compat_mode: bool,
226}
227
228impl Default for AppConfig {
229 fn default() -> Self {
230 Self {
231 db_uri: default_db_uri(),
232 db_schemas: default_db_schemas(),
233 db_anon_role: None,
234 db_pool_size: default_pool_size(),
235 db_pool_timeout: default_pool_timeout(),
236 db_tx_isolation: default_tx_isolation(),
237 db_prepared_statements: true,
238 db_extra_search_path: default_extra_search_path(),
239 db_channel: default_db_channel(),
240 db_channel_enabled: false,
241 db_pre_request: None,
242 db_max_rows: None,
243 db_aggregates_enabled: false,
244 server_host: default_host(),
245 server_port: default_port(),
246 server_cors_origins: Vec::new(),
247 max_body_size: default_max_body_size(),
248 server_unix_socket: None,
249 admin_server_port: None,
250 jwt_secret: None,
251 jwt_secret_is_base64: false,
252 jwt_aud: None,
253 jwt_role_claim_key: default_jwt_role_claim(),
254 jwt_cache_enabled: true,
255 jwt_cache_max_lifetime: default_jwt_cache_max(),
256 hasura_admin_secret: None,
257 hasura_unauthorized_role: None,
258 openapi_server_proxy_uri: None,
259 openapi_mode: OpenApiMode::FollowPrivileges,
260 log_level: default_log_level(),
261 role_settings: HashMap::new(),
262 app_settings: HashMap::new(),
263 graphql_federation: false,
264 graphql_type_prefix: None,
265 compat_mode: false,
266 }
267 }
268}
269
270fn warn_ignored(var: &str, value: &str, expected: &str) {
274 tracing::warn!("Ignoring {}={:?}: expected {}", var, value, expected);
275}
276
277fn parse_bool(value: &str) -> Option<bool> {
279 match value.trim().to_ascii_lowercase().as_str() {
280 "true" | "1" | "yes" | "on" => Some(true),
281 "false" | "0" | "no" | "off" => Some(false),
282 _ => None,
283 }
284}
285
286fn env_bool(variable: &'static str, value: &str) -> Result<bool, ConfigError> {
287 parse_bool(value).ok_or_else(|| {
288 let error = ConfigError {
289 variable,
290 value: value.to_string(),
291 expected: EXPECTED_BOOLEAN,
292 };
293 tracing::warn!("{error}");
294 error
295 })
296}
297
298fn parse_graphql_type_prefix(value: &str) -> Result<Option<String>, &'static str> {
300 let value = value.trim();
301 if value.is_empty() {
302 return Ok(None);
303 }
304
305 let mut chars = value.chars();
306 let valid_first = chars
307 .next()
308 .is_some_and(|c| c == '_' || c.is_ascii_alphabetic());
309 if !valid_first || !chars.all(|c| c == '_' || c.is_ascii_alphanumeric()) {
310 return Err("a GraphQL identifier beginning with a letter or underscore");
311 }
312
313 Ok(Some(value.to_string()))
314}
315
316impl AppConfig {
317 pub fn from_env() -> Self {
324 Self::try_from_env().unwrap_or_else(|error| panic!("{error}"))
325 }
326
327 pub fn try_from_env() -> Result<Self, ConfigError> {
330 let mut config = Self::default();
331
332 if let Ok(uri) = std::env::var("PGRST_DB_URI") {
333 config.db_uri = uri;
334 }
335 if let Ok(uri) = std::env::var("DATABASE_URL") {
336 config.db_uri = uri;
337 }
338 if let Ok(schemas) = std::env::var("PGRST_DB_SCHEMAS") {
339 config.db_schemas = schemas.split(',').map(|s| s.trim().to_string()).collect();
340 }
341 if let Ok(role) = std::env::var("PGRST_DB_ANON_ROLE") {
342 config.db_anon_role = Some(role);
343 }
344 for var in ["PGRST_DB_POOL", "PGRST_DB_POOL_SIZE"] {
350 if let Ok(size) = std::env::var(var) {
351 match size.parse::<u32>() {
352 Ok(n) if n > 0 => config.db_pool_size = n,
353 _ => warn_ignored(var, &size, "a positive integer"),
354 }
355 }
356 }
357 if let Ok(secs) = std::env::var("PGRST_DB_POOL_TIMEOUT") {
358 match secs.parse::<u64>() {
359 Ok(n) if n > 0 => config.db_pool_timeout = n,
360 _ => warn_ignored("PGRST_DB_POOL_TIMEOUT", &secs, "a positive integer"),
361 }
362 }
363 if let Ok(level) = std::env::var("PGRST_DB_TX_ISOLATION") {
364 match IsolationLevel::parse_config(&level) {
365 Some(l) => config.db_tx_isolation = l,
366 None => warn_ignored(
367 "PGRST_DB_TX_ISOLATION",
368 &level,
369 "one of: read committed, repeatable read, serializable",
370 ),
371 }
372 }
373 if let Ok(v) = std::env::var("PGRST_DB_EXTRA_SEARCH_PATH") {
374 config.db_extra_search_path = v
375 .split(',')
376 .map(|s| s.trim().to_string())
377 .filter(|s| !s.is_empty())
378 .collect();
379 }
380
381 if let Ok(v) = std::env::var("PGRST_DB_AGGREGATES_ENABLED") {
382 config.db_aggregates_enabled = env_bool("PGRST_DB_AGGREGATES_ENABLED", &v)?;
383 }
384 if let Ok(v) = std::env::var("PGRST_DB_PREPARED_STATEMENTS") {
385 config.db_prepared_statements = env_bool("PGRST_DB_PREPARED_STATEMENTS", &v)?;
386 }
387 if let Ok(v) = std::env::var("PGRST_DB_PRE_REQUEST") {
388 if !v.trim().is_empty() {
389 config.db_pre_request = Some(v.trim().to_string());
390 }
391 }
392 if let Ok(v) = std::env::var("PGRST_DB_CHANNEL") {
393 if !v.trim().is_empty() {
394 config.db_channel = v.trim().to_string();
395 }
396 }
397 if let Ok(v) = std::env::var("PGRST_DB_CHANNEL_ENABLED") {
398 config.db_channel_enabled = env_bool("PGRST_DB_CHANNEL_ENABLED", &v)?;
399 }
400
401 for var in ["PGRST_DB_MAX_ROWS", "PGRST_MAX_ROWS"] {
403 if let Ok(max_rows) = std::env::var(var) {
404 match max_rows.parse::<i64>() {
405 Ok(n) if n >= 0 => config.db_max_rows = Some(n),
406 _ => warn_ignored(var, &max_rows, "a non-negative integer"),
407 }
408 }
409 }
410 if let Ok(secret) = std::env::var("PGRST_JWT_SECRET") {
411 config.jwt_secret = Some(secret);
412 }
413 if let Ok(aud) = std::env::var("PGRST_JWT_AUD") {
414 config.jwt_aud = Some(aud);
415 }
416 if let Ok(v) = std::env::var("PGRST_JWT_SECRET_IS_BASE64") {
421 config.jwt_secret_is_base64 = env_bool("PGRST_JWT_SECRET_IS_BASE64", &v)?;
422 }
423 if let Ok(key) = std::env::var("PGRST_JWT_ROLE_CLAIM_KEY") {
424 if !key.trim().is_empty() {
425 config.jwt_role_claim_key = key;
426 }
427 }
428 if let Ok(v) = std::env::var("PGRST_JWT_CACHE_ENABLED") {
429 config.jwt_cache_enabled = env_bool("PGRST_JWT_CACHE_ENABLED", &v)?;
430 }
431 if let Ok(v) = std::env::var("PGRST_JWT_CACHE_MAX_LIFETIME") {
434 match v.parse::<u64>() {
435 Ok(n) => config.jwt_cache_max_lifetime = n,
436 Err(_) => warn_ignored(
437 "PGRST_JWT_CACHE_MAX_LIFETIME",
438 &v,
439 "a non-negative integer number of seconds",
440 ),
441 }
442 }
443 for var in ["HASURA_GRAPHQL_ADMIN_SECRET", "PGRST_HASURA_ADMIN_SECRET"] {
449 if let Ok(secret) = std::env::var(var) {
450 if !secret.is_empty() {
451 config.hasura_admin_secret = Some(secret);
452 }
453 }
454 }
455 for var in [
456 "HASURA_GRAPHQL_UNAUTHORIZED_ROLE",
457 "PGRST_HASURA_UNAUTHORIZED_ROLE",
458 ] {
459 if let Ok(role) = std::env::var(var) {
460 if !role.is_empty() {
461 config.hasura_unauthorized_role = Some(role);
462 }
463 }
464 }
465 if let Ok(host) = std::env::var("PGRST_SERVER_HOST") {
466 config.server_host = host;
467 }
468 if let Ok(port) = std::env::var("PGRST_SERVER_PORT") {
469 if let Ok(p) = port.parse() {
470 config.server_port = p;
471 }
472 }
473 if let Ok(port) = std::env::var("PORT") {
474 if let Ok(p) = port.parse() {
475 config.server_port = p;
476 }
477 }
478 if let Ok(origins) = std::env::var("PGRST_SERVER_CORS_ORIGINS") {
482 config.server_cors_origins = if origins.trim() == "*" {
483 Vec::new()
484 } else {
485 origins
486 .split(',')
487 .map(|s| s.trim().to_string())
488 .filter(|s| !s.is_empty())
489 .collect()
490 };
491 }
492 if let Ok(bytes) = std::env::var("PGRST_MAX_BODY_SIZE") {
493 match bytes.parse::<usize>() {
494 Ok(n) if n > 0 => config.max_body_size = n,
495 _ => warn_ignored("PGRST_MAX_BODY_SIZE", &bytes, "a positive integer"),
496 }
497 }
498 if let Ok(level) = std::env::var("PGRST_LOG_LEVEL") {
499 match LogLevel::parse_config(&level) {
500 Some(l) => config.log_level = l,
501 None => warn_ignored(
502 "PGRST_LOG_LEVEL",
503 &level,
504 "one of: crit, error, warn, info, debug",
505 ),
506 }
507 }
508 if let Ok(path) = std::env::var("PGRST_SERVER_UNIX_SOCKET") {
509 if !path.trim().is_empty() {
510 config.server_unix_socket = Some(path.trim().to_string());
511 }
512 }
513 if let Ok(port) = std::env::var("PGRST_ADMIN_SERVER_PORT") {
514 match port.parse::<u16>() {
515 Ok(p) if p > 0 => config.admin_server_port = Some(p),
516 _ => warn_ignored("PGRST_ADMIN_SERVER_PORT", &port, "a port number"),
517 }
518 }
519 if let Ok(uri) = std::env::var("PGRST_OPENAPI_SERVER_PROXY_URI") {
520 if !uri.trim().is_empty() {
521 config.openapi_server_proxy_uri = Some(uri.trim().to_string());
522 }
523 }
524 if let Ok(mode) = std::env::var("PGRST_OPENAPI_MODE") {
525 match OpenApiMode::parse_config(&mode) {
526 Some(m) => config.openapi_mode = m,
527 None => warn_ignored(
528 "PGRST_OPENAPI_MODE",
529 &mode,
530 "one of: disabled, follow-privileges, ignore-privileges",
531 ),
532 }
533 }
534
535 if let Ok(raw) = std::env::var("PGRST_ROLE_SETTINGS") {
539 match serde_json::from_str::<HashMap<String, RoleSettings>>(&raw) {
540 Ok(settings) => config.role_settings = settings,
541 Err(e) => warn_ignored(
542 "PGRST_ROLE_SETTINGS",
543 &raw,
544 &format!("a JSON object of role to settings ({e})"),
545 ),
546 }
547 }
548
549 if let Ok(value) = std::env::var("PGRST_GRAPHQL_FEDERATION") {
550 config.graphql_federation = env_bool("PGRST_GRAPHQL_FEDERATION", &value)?;
551 }
552 if let Ok(value) = std::env::var("PGRST_GRAPHQL_TYPE_PREFIX") {
553 match parse_graphql_type_prefix(&value) {
554 Ok(prefix) => config.graphql_type_prefix = prefix,
555 Err(expected) => warn_ignored("PGRST_GRAPHQL_TYPE_PREFIX", &value, expected),
556 }
557 }
558
559 for (key, value) in std::env::vars() {
562 if let Some(name) = key.strip_prefix("PGRST_APP_SETTINGS_") {
563 if !name.is_empty() {
564 config.app_settings.insert(name.to_ascii_lowercase(), value);
565 }
566 }
567 }
568 for var in ["PGRST_COMPAT_MODE", "POSTRUST_COMPAT_MODE"] {
571 if let Ok(v) = std::env::var(var) {
572 config.compat_mode = env_bool(var, &v)?;
573 }
574 }
575
576 Ok(config)
577 }
578
579 pub fn default_schema(&self) -> &str {
581 self.db_schemas
582 .first()
583 .map(|s| s.as_str())
584 .unwrap_or("public")
585 }
586}
587
588impl Serialize for IsolationLevel {
594 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
595 serializer.serialize_str(self.as_config_str())
596 }
597}
598
599impl<'de> Deserialize<'de> for IsolationLevel {
600 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
601 let raw = String::deserialize(deserializer)?;
602 Self::parse_config(&raw).ok_or_else(|| {
603 serde::de::Error::custom(format!(
604 "unknown isolation level {raw:?}: expected one of \
605 read committed, repeatable read, serializable"
606 ))
607 })
608 }
609}
610
611impl Serialize for OpenApiMode {
612 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
613 serializer.serialize_str(self.as_config_str())
614 }
615}
616
617impl<'de> Deserialize<'de> for OpenApiMode {
618 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
619 let raw = String::deserialize(deserializer)?;
620 Self::parse_config(&raw).ok_or_else(|| {
621 serde::de::Error::custom(format!(
622 "unknown OpenAPI mode {raw:?}: expected one of \
623 disabled, follow-privileges, ignore-privileges"
624 ))
625 })
626 }
627}
628
629impl Serialize for LogLevel {
630 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
631 serializer.serialize_str(self.as_config_str())
632 }
633}
634
635impl<'de> Deserialize<'de> for LogLevel {
636 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
637 let raw = String::deserialize(deserializer)?;
638 Self::parse_config(&raw).ok_or_else(|| {
639 serde::de::Error::custom(format!(
640 "unknown log level {raw:?}: expected one of \
641 crit, error, warn, info, debug"
642 ))
643 })
644 }
645}
646
647#[derive(Clone, Debug, Default, Serialize, Deserialize)]
649#[non_exhaustive]
650pub struct RoleSettings {
651 pub isolation_level: Option<IsolationLevel>,
653 pub statement_timeout: Option<u64>,
655}
656
657#[derive(Clone, Debug, PartialEq, Eq)]
663pub enum IsolationLevel {
664 ReadCommitted,
666 RepeatableRead,
668 Serializable,
672}
673
674impl IsolationLevel {
675 pub fn to_sql(&self) -> &'static str {
677 match self {
678 Self::ReadCommitted => "READ COMMITTED",
679 Self::RepeatableRead => "REPEATABLE READ",
680 Self::Serializable => "SERIALIZABLE",
681 }
682 }
683
684 pub fn parse_config(s: &str) -> Option<Self> {
690 match s
691 .trim()
692 .to_ascii_lowercase()
693 .replace(['-', '_'], " ")
694 .as_str()
695 {
696 "read committed" => Some(Self::ReadCommitted),
697 "repeatable read" => Some(Self::RepeatableRead),
698 "serializable" => Some(Self::Serializable),
699 _ => None,
700 }
701 }
702
703 pub fn as_config_str(&self) -> &'static str {
705 match self {
706 Self::ReadCommitted => "read committed",
707 Self::RepeatableRead => "repeatable read",
708 Self::Serializable => "serializable",
709 }
710 }
711}
712
713#[derive(Clone, Debug, PartialEq, Eq)]
720pub enum OpenApiMode {
721 Disabled,
723 FollowPrivileges,
726 IgnorePrivileges,
728}
729
730impl OpenApiMode {
731 pub fn parse_config(s: &str) -> Option<Self> {
734 match s
735 .trim()
736 .to_ascii_lowercase()
737 .replace(['-', '_'], " ")
738 .as_str()
739 {
740 "disabled" => Some(Self::Disabled),
741 "follow privileges" => Some(Self::FollowPrivileges),
742 "ignore privileges" => Some(Self::IgnorePrivileges),
743 _ => None,
744 }
745 }
746
747 pub fn as_config_str(&self) -> &'static str {
749 match self {
750 Self::Disabled => "disabled",
751 Self::FollowPrivileges => "follow-privileges",
752 Self::IgnorePrivileges => "ignore-privileges",
753 }
754 }
755}
756
757#[derive(Clone, Debug, PartialEq, Eq)]
759pub enum LogLevel {
760 Crit,
763 Error,
765 Warn,
767 Info,
769 Debug,
771}
772
773impl LogLevel {
774 pub fn to_tracing(&self) -> tracing::Level {
779 match self {
780 Self::Crit | Self::Error => tracing::Level::ERROR,
781 Self::Warn => tracing::Level::WARN,
782 Self::Info => tracing::Level::INFO,
783 Self::Debug => tracing::Level::DEBUG,
784 }
785 }
786
787 pub fn parse_config(s: &str) -> Option<Self> {
789 match s.trim().to_ascii_lowercase().as_str() {
790 "crit" | "critical" => Some(Self::Crit),
791 "error" => Some(Self::Error),
792 "warn" | "warning" => Some(Self::Warn),
793 "info" => Some(Self::Info),
794 "debug" => Some(Self::Debug),
795 _ => None,
796 }
797 }
798
799 pub fn as_config_str(&self) -> &'static str {
801 match self {
802 Self::Crit => "crit",
803 Self::Error => "error",
804 Self::Warn => "warn",
805 Self::Info => "info",
806 Self::Debug => "debug",
807 }
808 }
809}
810
811fn default_db_uri() -> String {
813 "postgresql://localhost/postgres".to_string()
814}
815
816fn default_db_schemas() -> Vec<String> {
817 vec!["public".to_string()]
818}
819
820fn default_pool_size() -> u32 {
821 10
822}
823
824fn default_pool_timeout() -> u64 {
825 10
826}
827
828fn default_tx_isolation() -> IsolationLevel {
829 IsolationLevel::ReadCommitted
830}
831
832fn default_max_body_size() -> usize {
833 10 * 1024 * 1024
834}
835
836fn default_extra_search_path() -> Vec<String> {
837 vec!["public".to_string()]
838}
839
840fn default_db_channel() -> String {
841 "pgrst".to_string()
842}
843
844fn default_host() -> String {
845 "127.0.0.1".to_string()
846}
847
848fn default_port() -> u16 {
849 3000
850}
851
852fn default_jwt_role_claim() -> String {
853 "role".to_string()
854}
855
856fn default_jwt_cache_max() -> u64 {
857 3600
858}
859
860fn default_openapi_mode() -> OpenApiMode {
861 OpenApiMode::FollowPrivileges
862}
863
864fn default_log_level() -> LogLevel {
869 LogLevel::Info
870}
871
872fn default_true() -> bool {
873 true
874}
875
876#[cfg(test)]
877mod tests {
878 use super::*;
879
880 #[test]
881 fn test_default_config() {
882 let config = AppConfig::default();
883 assert_eq!(config.server_port, 3000);
884 assert_eq!(config.db_pool_size, 10);
885 assert!(config.db_prepared_statements);
886 assert!(!config.graphql_federation);
887 assert_eq!(config.graphql_type_prefix, None);
888 }
889
890 #[test]
891 fn test_default_schema() {
892 let mut config = AppConfig::default();
893 assert_eq!(config.default_schema(), "public");
894
895 config.db_schemas = vec!["api".to_string(), "public".to_string()];
896 assert_eq!(config.default_schema(), "api");
897 }
898
899 #[test]
900 fn test_isolation_level_sql() {
901 assert_eq!(IsolationLevel::ReadCommitted.to_sql(), "READ COMMITTED");
902 assert_eq!(IsolationLevel::Serializable.to_sql(), "SERIALIZABLE");
903 }
904
905 #[test]
906 fn isolation_level_accepts_the_documented_spellings() {
907 for s in [
910 "read committed",
911 "READ COMMITTED",
912 "read-committed",
913 " Read_Committed ",
914 ] {
915 assert_eq!(
916 IsolationLevel::parse_config(s),
917 Some(IsolationLevel::ReadCommitted),
918 "{s:?}"
919 );
920 }
921 assert_eq!(
922 IsolationLevel::parse_config("serializable"),
923 Some(IsolationLevel::Serializable)
924 );
925 assert_eq!(
926 IsolationLevel::parse_config("repeatable read"),
927 Some(IsolationLevel::RepeatableRead)
928 );
929 assert_eq!(IsolationLevel::parse_config("dirty read"), None);
932 assert_eq!(IsolationLevel::parse_config(""), None);
933 }
934
935 #[test]
936 fn log_level_accepts_the_documented_spellings() {
937 assert_eq!(LogLevel::parse_config("INFO"), Some(LogLevel::Info));
938 assert_eq!(LogLevel::parse_config("warning"), Some(LogLevel::Warn));
939 assert_eq!(LogLevel::parse_config("crit"), Some(LogLevel::Crit));
940 assert_eq!(LogLevel::parse_config("trace"), None);
943 }
944
945 #[test]
946 fn default_log_level_matches_the_documented_one() {
947 assert_eq!(AppConfig::default().log_level, LogLevel::Info);
950 }
951
952 #[test]
953 fn parse_bool_accepts_only_the_supported_spellings() {
954 for s in ["true", "TRUE", "1", "yes", "on", " On "] {
955 assert_eq!(parse_bool(s), Some(true), "{s:?}");
956 }
957 for s in ["false", "FALSE", "0", "no", "off", " Off "] {
958 assert_eq!(parse_bool(s), Some(false), "{s:?}");
959 }
960 for s in ["", "maybe", "enabled", "2"] {
961 assert_eq!(parse_bool(s), None, "{s:?}");
962 }
963 }
964
965 #[test]
966 fn graphql_federation_configuration_is_read() {
967 let config = with_env(&[
968 ("PGRST_GRAPHQL_FEDERATION", "true"),
969 ("PGRST_GRAPHQL_TYPE_PREFIX", " test "),
970 ]);
971
972 assert!(config.graphql_federation);
973 assert_eq!(config.graphql_type_prefix.as_deref(), Some("test"));
974 }
975
976 #[test]
977 fn graphql_federation_accepts_an_explicit_false() {
978 assert!(!with_env(&[("PGRST_GRAPHQL_FEDERATION", "false")]).graphql_federation);
979 }
980
981 #[test]
982 fn malformed_graphql_federation_configuration_is_rejected() {
983 let error = with_env_result(&[("PGRST_GRAPHQL_FEDERATION", "sometimes")])
984 .expect_err("an invalid boolean must reject the configuration");
985
986 assert_eq!(
987 error.to_string(),
988 "invalid value for PGRST_GRAPHQL_FEDERATION: \"sometimes\"; expected one of: true, false, 1, 0, yes, no, on, off"
989 );
990 }
991
992 #[test]
993 fn an_empty_graphql_prefix_means_unset() {
994 let config = with_env(&[("PGRST_GRAPHQL_TYPE_PREFIX", " ")]);
995
996 assert_eq!(config.graphql_type_prefix, None);
997 }
998
999 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1001
1002 fn with_env(vars: &[(&str, &str)]) -> AppConfig {
1004 with_env_result(vars).expect("test configuration should be valid")
1005 }
1006
1007 fn with_env_result(vars: &[(&str, &str)]) -> Result<AppConfig, ConfigError> {
1008 let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1009 for (k, v) in vars {
1010 std::env::set_var(k, v);
1011 }
1012 let config = AppConfig::try_from_env();
1013 for (k, _) in vars {
1014 std::env::remove_var(k);
1015 }
1016 config
1017 }
1018
1019 #[test]
1020 fn documented_variables_are_read() {
1021 let config = with_env(&[
1024 ("PGRST_DB_POOL_SIZE", "42"),
1025 ("PGRST_DB_POOL_TIMEOUT", "7"),
1026 ("PGRST_DB_TX_ISOLATION", "serializable"),
1027 ("PGRST_JWT_SECRET_IS_BASE64", "true"),
1028 ("PGRST_JWT_ROLE_CLAIM_KEY", "user.role"),
1029 ("PGRST_LOG_LEVEL", "warn"),
1030 ("PGRST_MAX_BODY_SIZE", "1024"),
1031 (
1032 "PGRST_SERVER_CORS_ORIGINS",
1033 "https://a.example, https://b.example",
1034 ),
1035 ]);
1036
1037 assert_eq!(config.db_pool_size, 42);
1038 assert_eq!(config.db_pool_timeout, 7);
1039 assert_eq!(config.db_tx_isolation, IsolationLevel::Serializable);
1040 assert!(config.jwt_secret_is_base64);
1041 assert_eq!(config.jwt_role_claim_key, "user.role");
1042 assert_eq!(config.log_level, LogLevel::Warn);
1043 assert_eq!(config.max_body_size, 1024);
1044 assert_eq!(
1045 config.server_cors_origins,
1046 vec![
1047 "https://a.example".to_string(),
1048 "https://b.example".to_string()
1049 ]
1050 );
1051 }
1052
1053 #[test]
1054 fn pool_size_accepts_postgrests_own_name() {
1055 assert_eq!(with_env(&[("PGRST_DB_POOL", "5")]).db_pool_size, 5);
1056 }
1057
1058 #[test]
1059 fn a_star_origin_means_any_origin() {
1060 assert!(with_env(&[("PGRST_SERVER_CORS_ORIGINS", "*")])
1063 .server_cors_origins
1064 .is_empty());
1065 assert!(with_env(&[("PGRST_SERVER_CORS_ORIGINS", " , ")])
1066 .server_cors_origins
1067 .is_empty());
1068 }
1069
1070 #[test]
1071 fn an_unusable_value_leaves_the_default() {
1072 let config = with_env(&[
1073 ("PGRST_DB_POOL_SIZE", "lots"),
1074 ("PGRST_DB_POOL_TIMEOUT", "0"),
1075 ("PGRST_MAX_BODY_SIZE", "-1"),
1076 ("PGRST_DB_TX_ISOLATION", "dirty read"),
1077 ("PGRST_LOG_LEVEL", "shout"),
1078 ]);
1079
1080 let d = AppConfig::default();
1081 assert_eq!(config.db_pool_size, d.db_pool_size);
1082 assert_eq!(config.db_pool_timeout, d.db_pool_timeout);
1083 assert_eq!(config.max_body_size, d.max_body_size);
1084 assert_eq!(config.db_tx_isolation, d.db_tx_isolation);
1085 assert_eq!(config.log_level, d.log_level);
1086 }
1087
1088 #[test]
1089 fn the_formerly_dead_fields_are_read() {
1090 let config = with_env(&[
1093 ("PGRST_DB_PREPARED_STATEMENTS", "false"),
1094 ("PGRST_DB_PRE_REQUEST", "auth.check"),
1095 ("PGRST_DB_CHANNEL", "my_channel"),
1096 ("PGRST_DB_CHANNEL_ENABLED", "yes"),
1097 ("PGRST_JWT_CACHE_ENABLED", "false"),
1098 ("PGRST_JWT_CACHE_MAX_LIFETIME", "60"),
1099 ("PGRST_SERVER_UNIX_SOCKET", "/tmp/postrust.sock"),
1100 ("PGRST_ADMIN_SERVER_PORT", "3001"),
1101 ("PGRST_OPENAPI_SERVER_PROXY_URI", "https://api.example.com"),
1102 ("PGRST_OPENAPI_MODE", "ignore-privileges"),
1103 ]);
1104
1105 assert!(!config.db_prepared_statements);
1106 assert_eq!(config.db_pre_request.as_deref(), Some("auth.check"));
1107 assert_eq!(config.db_channel, "my_channel");
1108 assert!(config.db_channel_enabled);
1109 assert!(!config.jwt_cache_enabled);
1110 assert_eq!(config.jwt_cache_max_lifetime, 60);
1111 assert_eq!(
1112 config.server_unix_socket.as_deref(),
1113 Some("/tmp/postrust.sock")
1114 );
1115 assert_eq!(config.admin_server_port, Some(3001));
1116 assert_eq!(
1117 config.openapi_server_proxy_uri.as_deref(),
1118 Some("https://api.example.com")
1119 );
1120 assert_eq!(config.openapi_mode, OpenApiMode::IgnorePrivileges);
1121 }
1122
1123 #[test]
1124 fn a_zero_jwt_cache_lifetime_is_kept_because_it_turns_the_cache_off() {
1125 assert_eq!(
1128 with_env(&[("PGRST_JWT_CACHE_MAX_LIFETIME", "0")]).jwt_cache_max_lifetime,
1129 0
1130 );
1131 }
1132
1133 #[test]
1134 fn app_settings_come_from_a_prefix() {
1135 let config = with_env(&[
1136 ("PGRST_APP_SETTINGS_JWT_LIFETIME", "3600"),
1137 ("PGRST_APP_SETTINGS_TenantName", "acme"),
1138 ]);
1139 assert_eq!(
1142 config.app_settings.get("jwt_lifetime").map(String::as_str),
1143 Some("3600")
1144 );
1145 assert_eq!(
1146 config.app_settings.get("tenantname").map(String::as_str),
1147 Some("acme")
1148 );
1149 }
1150
1151 #[test]
1152 fn role_settings_parse_from_json_using_the_documented_spellings() {
1153 let config = with_env(&[(
1154 "PGRST_ROLE_SETTINGS",
1155 r#"{"web_user":{"isolation_level":"serializable","statement_timeout":5000}}"#,
1156 )]);
1157 let web_user = config.role_settings.get("web_user").expect("role present");
1158 assert_eq!(web_user.isolation_level, Some(IsolationLevel::Serializable));
1159 assert_eq!(web_user.statement_timeout, Some(5000));
1160 }
1161
1162 #[test]
1163 fn unparseable_role_settings_leave_the_map_empty() {
1164 assert!(with_env(&[("PGRST_ROLE_SETTINGS", "{not json")])
1165 .role_settings
1166 .is_empty());
1167 assert!(with_env(&[(
1170 "PGRST_ROLE_SETTINGS",
1171 r#"{"r":{"isolation_level":"dirty"}}"#
1172 )])
1173 .role_settings
1174 .is_empty());
1175 }
1176
1177 #[test]
1178 fn openapi_mode_accepts_the_documented_spellings() {
1179 assert_eq!(
1180 OpenApiMode::parse_config("follow-privileges"),
1181 Some(OpenApiMode::FollowPrivileges)
1182 );
1183 assert_eq!(
1184 OpenApiMode::parse_config("IGNORE_PRIVILEGES"),
1185 Some(OpenApiMode::IgnorePrivileges)
1186 );
1187 assert_eq!(
1188 OpenApiMode::parse_config("disabled"),
1189 Some(OpenApiMode::Disabled)
1190 );
1191 assert_eq!(OpenApiMode::parse_config("security-definer"), None);
1194 }
1195
1196 #[test]
1197 fn the_config_enums_round_trip_through_json() {
1198 for level in [
1204 IsolationLevel::ReadCommitted,
1205 IsolationLevel::RepeatableRead,
1206 IsolationLevel::Serializable,
1207 ] {
1208 let json = serde_json::to_string(&level).unwrap();
1209 let back: IsolationLevel = serde_json::from_str(&json).unwrap();
1210 assert_eq!(back, level, "{json}");
1211 }
1212
1213 for mode in [
1214 OpenApiMode::Disabled,
1215 OpenApiMode::FollowPrivileges,
1216 OpenApiMode::IgnorePrivileges,
1217 ] {
1218 let json = serde_json::to_string(&mode).unwrap();
1219 assert_eq!(serde_json::from_str::<OpenApiMode>(&json).unwrap(), mode);
1220 }
1221
1222 for level in [
1223 LogLevel::Crit,
1224 LogLevel::Error,
1225 LogLevel::Warn,
1226 LogLevel::Info,
1227 LogLevel::Debug,
1228 ] {
1229 let json = serde_json::to_string(&level).unwrap();
1230 assert_eq!(serde_json::from_str::<LogLevel>(&json).unwrap(), level);
1231 }
1232 }
1233
1234 #[test]
1235 fn a_whole_config_round_trips_through_json() {
1236 let mut config = AppConfig::default();
1238 config.role_settings.insert(
1239 "web_user".to_string(),
1240 RoleSettings {
1241 isolation_level: Some(IsolationLevel::RepeatableRead),
1242 statement_timeout: Some(5000),
1243 },
1244 );
1245
1246 let json = serde_json::to_string(&config).unwrap();
1247 let back: AppConfig = serde_json::from_str(&json).expect("config should round trip");
1248
1249 assert_eq!(back.db_tx_isolation, config.db_tx_isolation);
1250 assert_eq!(back.log_level, config.log_level);
1251 assert_eq!(back.openapi_mode, config.openapi_mode);
1252 assert_eq!(
1253 back.role_settings["web_user"].isolation_level,
1254 Some(IsolationLevel::RepeatableRead)
1255 );
1256 }
1257
1258 #[test]
1259 fn the_serialized_spelling_is_the_configuration_spelling() {
1260 assert_eq!(
1263 serde_json::to_string(&IsolationLevel::ReadCommitted).unwrap(),
1264 "\"read committed\""
1265 );
1266 assert_eq!(
1267 serde_json::to_string(&OpenApiMode::FollowPrivileges).unwrap(),
1268 "\"follow-privileges\""
1269 );
1270 assert_eq!(serde_json::to_string(&LogLevel::Warn).unwrap(), "\"warn\"");
1271 }
1272
1273 #[test]
1274 fn an_empty_role_claim_key_is_not_accepted() {
1275 assert_eq!(
1278 with_env(&[("PGRST_JWT_ROLE_CLAIM_KEY", " ")]).jwt_role_claim_key,
1279 AppConfig::default().jwt_role_claim_key
1280 );
1281 }
1282}