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
316fn env_name(name: String) -> String {
322 if cfg!(windows) {
323 upper_env_name(name)
324 } else {
325 name
326 }
327}
328
329fn upper_env_name(mut name: String) -> String {
330 name.make_ascii_uppercase();
331 name
332}
333
334impl AppConfig {
335 pub fn from_env() -> Self {
342 Self::try_from_env().unwrap_or_else(|error| panic!("{error}"))
343 }
344
345 pub fn try_from_env() -> Result<Self, ConfigError> {
348 Self::try_from_vars(
351 std::env::vars_os().filter_map(|(k, v)| {
352 Some((env_name(k.into_string().ok()?), v.into_string().ok()?))
353 }),
354 )
355 }
356
357 pub fn try_from_vars<I, K, V>(vars: I) -> Result<Self, ConfigError>
362 where
363 I: IntoIterator<Item = (K, V)>,
364 K: Into<String>,
365 V: Into<String>,
366 {
367 let pairs: Vec<(String, String)> = vars
372 .into_iter()
373 .map(|(k, v)| (k.into(), v.into()))
374 .collect();
375 let vars: HashMap<&str, &str> = pairs
376 .iter()
377 .map(|(k, v)| (k.as_str(), v.as_str()))
378 .collect();
379 let get = |key: &str| vars.get(key).map(|v| v.to_string()).ok_or(());
380 let mut config = Self::default();
381
382 if let Ok(uri) = get("PGRST_DB_URI") {
383 config.db_uri = uri;
384 }
385 if let Ok(uri) = get("DATABASE_URL") {
386 config.db_uri = uri;
387 }
388 if let Ok(schemas) = get("PGRST_DB_SCHEMAS") {
389 config.db_schemas = schemas.split(',').map(|s| s.trim().to_string()).collect();
390 }
391 if let Ok(role) = get("PGRST_DB_ANON_ROLE") {
392 config.db_anon_role = Some(role);
393 }
394 for var in ["PGRST_DB_POOL", "PGRST_DB_POOL_SIZE"] {
400 if let Ok(size) = get(var) {
401 match size.parse::<u32>() {
402 Ok(n) if n > 0 => config.db_pool_size = n,
403 _ => warn_ignored(var, &size, "a positive integer"),
404 }
405 }
406 }
407 if let Ok(secs) = get("PGRST_DB_POOL_TIMEOUT") {
408 match secs.parse::<u64>() {
409 Ok(n) if n > 0 => config.db_pool_timeout = n,
410 _ => warn_ignored("PGRST_DB_POOL_TIMEOUT", &secs, "a positive integer"),
411 }
412 }
413 if let Ok(level) = get("PGRST_DB_TX_ISOLATION") {
414 match IsolationLevel::parse_config(&level) {
415 Some(l) => config.db_tx_isolation = l,
416 None => warn_ignored(
417 "PGRST_DB_TX_ISOLATION",
418 &level,
419 "one of: read committed, repeatable read, serializable",
420 ),
421 }
422 }
423 if let Ok(v) = get("PGRST_DB_EXTRA_SEARCH_PATH") {
424 config.db_extra_search_path = v
425 .split(',')
426 .map(|s| s.trim().to_string())
427 .filter(|s| !s.is_empty())
428 .collect();
429 }
430
431 if let Ok(v) = get("PGRST_DB_AGGREGATES_ENABLED") {
432 config.db_aggregates_enabled = env_bool("PGRST_DB_AGGREGATES_ENABLED", &v)?;
433 }
434 if let Ok(v) = get("PGRST_DB_PREPARED_STATEMENTS") {
435 config.db_prepared_statements = env_bool("PGRST_DB_PREPARED_STATEMENTS", &v)?;
436 }
437 if let Ok(v) = get("PGRST_DB_PRE_REQUEST") {
438 if !v.trim().is_empty() {
439 config.db_pre_request = Some(v.trim().to_string());
440 }
441 }
442 if let Ok(v) = get("PGRST_DB_CHANNEL") {
443 if !v.trim().is_empty() {
444 config.db_channel = v.trim().to_string();
445 }
446 }
447 if let Ok(v) = get("PGRST_DB_CHANNEL_ENABLED") {
448 config.db_channel_enabled = env_bool("PGRST_DB_CHANNEL_ENABLED", &v)?;
449 }
450
451 for var in ["PGRST_DB_MAX_ROWS", "PGRST_MAX_ROWS"] {
453 if let Ok(max_rows) = get(var) {
454 match max_rows.parse::<i64>() {
455 Ok(n) if n >= 0 => config.db_max_rows = Some(n),
456 _ => warn_ignored(var, &max_rows, "a non-negative integer"),
457 }
458 }
459 }
460 if let Ok(secret) = get("PGRST_JWT_SECRET") {
461 config.jwt_secret = Some(secret);
462 }
463 if let Ok(aud) = get("PGRST_JWT_AUD") {
464 config.jwt_aud = Some(aud);
465 }
466 if let Ok(v) = get("PGRST_JWT_SECRET_IS_BASE64") {
471 config.jwt_secret_is_base64 = env_bool("PGRST_JWT_SECRET_IS_BASE64", &v)?;
472 }
473 if let Ok(key) = get("PGRST_JWT_ROLE_CLAIM_KEY") {
474 if !key.trim().is_empty() {
475 config.jwt_role_claim_key = key;
476 }
477 }
478 if let Ok(v) = get("PGRST_JWT_CACHE_ENABLED") {
479 config.jwt_cache_enabled = env_bool("PGRST_JWT_CACHE_ENABLED", &v)?;
480 }
481 if let Ok(v) = get("PGRST_JWT_CACHE_MAX_LIFETIME") {
484 match v.parse::<u64>() {
485 Ok(n) => config.jwt_cache_max_lifetime = n,
486 Err(_) => warn_ignored(
487 "PGRST_JWT_CACHE_MAX_LIFETIME",
488 &v,
489 "a non-negative integer number of seconds",
490 ),
491 }
492 }
493 for var in ["HASURA_GRAPHQL_ADMIN_SECRET", "PGRST_HASURA_ADMIN_SECRET"] {
499 if let Ok(secret) = get(var) {
500 if !secret.is_empty() {
501 config.hasura_admin_secret = Some(secret);
502 }
503 }
504 }
505 for var in [
506 "HASURA_GRAPHQL_UNAUTHORIZED_ROLE",
507 "PGRST_HASURA_UNAUTHORIZED_ROLE",
508 ] {
509 if let Ok(role) = get(var) {
510 if !role.is_empty() {
511 config.hasura_unauthorized_role = Some(role);
512 }
513 }
514 }
515 if let Ok(host) = get("PGRST_SERVER_HOST") {
516 config.server_host = host;
517 }
518 if let Ok(port) = get("PGRST_SERVER_PORT") {
519 if let Ok(p) = port.parse() {
520 config.server_port = p;
521 }
522 }
523 if let Ok(port) = get("PORT") {
524 if let Ok(p) = port.parse() {
525 config.server_port = p;
526 }
527 }
528 if let Ok(origins) = get("PGRST_SERVER_CORS_ORIGINS") {
532 config.server_cors_origins = if origins.trim() == "*" {
533 Vec::new()
534 } else {
535 origins
536 .split(',')
537 .map(|s| s.trim().to_string())
538 .filter(|s| !s.is_empty())
539 .collect()
540 };
541 }
542 if let Ok(bytes) = get("PGRST_MAX_BODY_SIZE") {
543 match bytes.parse::<usize>() {
544 Ok(n) if n > 0 => config.max_body_size = n,
545 _ => warn_ignored("PGRST_MAX_BODY_SIZE", &bytes, "a positive integer"),
546 }
547 }
548 if let Ok(level) = get("PGRST_LOG_LEVEL") {
549 match LogLevel::parse_config(&level) {
550 Some(l) => config.log_level = l,
551 None => warn_ignored(
552 "PGRST_LOG_LEVEL",
553 &level,
554 "one of: crit, error, warn, info, debug",
555 ),
556 }
557 }
558 if let Ok(path) = get("PGRST_SERVER_UNIX_SOCKET") {
559 if !path.trim().is_empty() {
560 config.server_unix_socket = Some(path.trim().to_string());
561 }
562 }
563 if let Ok(port) = get("PGRST_ADMIN_SERVER_PORT") {
564 match port.parse::<u16>() {
565 Ok(p) if p > 0 => config.admin_server_port = Some(p),
566 _ => warn_ignored("PGRST_ADMIN_SERVER_PORT", &port, "a port number"),
567 }
568 }
569 if let Ok(uri) = get("PGRST_OPENAPI_SERVER_PROXY_URI") {
570 if !uri.trim().is_empty() {
571 config.openapi_server_proxy_uri = Some(uri.trim().to_string());
572 }
573 }
574 if let Ok(mode) = get("PGRST_OPENAPI_MODE") {
575 match OpenApiMode::parse_config(&mode) {
576 Some(m) => config.openapi_mode = m,
577 None => warn_ignored(
578 "PGRST_OPENAPI_MODE",
579 &mode,
580 "one of: disabled, follow-privileges, ignore-privileges",
581 ),
582 }
583 }
584
585 if let Ok(raw) = get("PGRST_ROLE_SETTINGS") {
589 match serde_json::from_str::<HashMap<String, RoleSettings>>(&raw) {
590 Ok(settings) => config.role_settings = settings,
591 Err(e) => warn_ignored(
592 "PGRST_ROLE_SETTINGS",
593 &raw,
594 &format!("a JSON object of role to settings ({e})"),
595 ),
596 }
597 }
598
599 if let Ok(value) = get("PGRST_GRAPHQL_FEDERATION") {
600 config.graphql_federation = env_bool("PGRST_GRAPHQL_FEDERATION", &value)?;
601 }
602 if let Ok(value) = get("PGRST_GRAPHQL_TYPE_PREFIX") {
603 match parse_graphql_type_prefix(&value) {
604 Ok(prefix) => config.graphql_type_prefix = prefix,
605 Err(expected) => warn_ignored("PGRST_GRAPHQL_TYPE_PREFIX", &value, expected),
606 }
607 }
608
609 for (key, value) in &pairs {
615 if let Some(name) = key.strip_prefix("PGRST_APP_SETTINGS_") {
616 if !name.is_empty() {
617 config
618 .app_settings
619 .insert(name.to_ascii_lowercase(), value.clone());
620 }
621 }
622 }
623 for var in ["PGRST_COMPAT_MODE", "POSTRUST_COMPAT_MODE"] {
626 if let Ok(v) = get(var) {
627 config.compat_mode = env_bool(var, &v)?;
628 }
629 }
630
631 Ok(config)
632 }
633
634 pub fn default_schema(&self) -> &str {
636 self.db_schemas
637 .first()
638 .map(|s| s.as_str())
639 .unwrap_or("public")
640 }
641}
642
643impl Serialize for IsolationLevel {
649 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
650 serializer.serialize_str(self.as_config_str())
651 }
652}
653
654impl<'de> Deserialize<'de> for IsolationLevel {
655 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
656 let raw = String::deserialize(deserializer)?;
657 Self::parse_config(&raw).ok_or_else(|| {
658 serde::de::Error::custom(format!(
659 "unknown isolation level {raw:?}: expected one of \
660 read committed, repeatable read, serializable"
661 ))
662 })
663 }
664}
665
666impl Serialize for OpenApiMode {
667 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
668 serializer.serialize_str(self.as_config_str())
669 }
670}
671
672impl<'de> Deserialize<'de> for OpenApiMode {
673 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
674 let raw = String::deserialize(deserializer)?;
675 Self::parse_config(&raw).ok_or_else(|| {
676 serde::de::Error::custom(format!(
677 "unknown OpenAPI mode {raw:?}: expected one of \
678 disabled, follow-privileges, ignore-privileges"
679 ))
680 })
681 }
682}
683
684impl Serialize for LogLevel {
685 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
686 serializer.serialize_str(self.as_config_str())
687 }
688}
689
690impl<'de> Deserialize<'de> for LogLevel {
691 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
692 let raw = String::deserialize(deserializer)?;
693 Self::parse_config(&raw).ok_or_else(|| {
694 serde::de::Error::custom(format!(
695 "unknown log level {raw:?}: expected one of \
696 crit, error, warn, info, debug"
697 ))
698 })
699 }
700}
701
702#[derive(Clone, Debug, Default, Serialize, Deserialize)]
704#[non_exhaustive]
705pub struct RoleSettings {
706 pub isolation_level: Option<IsolationLevel>,
708 pub statement_timeout: Option<u64>,
710}
711
712#[derive(Clone, Debug, PartialEq, Eq)]
718pub enum IsolationLevel {
719 ReadCommitted,
721 RepeatableRead,
723 Serializable,
727}
728
729impl IsolationLevel {
730 pub fn to_sql(&self) -> &'static str {
732 match self {
733 Self::ReadCommitted => "READ COMMITTED",
734 Self::RepeatableRead => "REPEATABLE READ",
735 Self::Serializable => "SERIALIZABLE",
736 }
737 }
738
739 pub fn parse_config(s: &str) -> Option<Self> {
745 match s
746 .trim()
747 .to_ascii_lowercase()
748 .replace(['-', '_'], " ")
749 .as_str()
750 {
751 "read committed" => Some(Self::ReadCommitted),
752 "repeatable read" => Some(Self::RepeatableRead),
753 "serializable" => Some(Self::Serializable),
754 _ => None,
755 }
756 }
757
758 pub fn as_config_str(&self) -> &'static str {
760 match self {
761 Self::ReadCommitted => "read committed",
762 Self::RepeatableRead => "repeatable read",
763 Self::Serializable => "serializable",
764 }
765 }
766}
767
768#[derive(Clone, Debug, PartialEq, Eq)]
775pub enum OpenApiMode {
776 Disabled,
778 FollowPrivileges,
781 IgnorePrivileges,
783}
784
785impl OpenApiMode {
786 pub fn parse_config(s: &str) -> Option<Self> {
789 match s
790 .trim()
791 .to_ascii_lowercase()
792 .replace(['-', '_'], " ")
793 .as_str()
794 {
795 "disabled" => Some(Self::Disabled),
796 "follow privileges" => Some(Self::FollowPrivileges),
797 "ignore privileges" => Some(Self::IgnorePrivileges),
798 _ => None,
799 }
800 }
801
802 pub fn as_config_str(&self) -> &'static str {
804 match self {
805 Self::Disabled => "disabled",
806 Self::FollowPrivileges => "follow-privileges",
807 Self::IgnorePrivileges => "ignore-privileges",
808 }
809 }
810}
811
812#[derive(Clone, Debug, PartialEq, Eq)]
814pub enum LogLevel {
815 Crit,
818 Error,
820 Warn,
822 Info,
824 Debug,
826}
827
828impl LogLevel {
829 pub fn to_tracing(&self) -> tracing::Level {
834 match self {
835 Self::Crit | Self::Error => tracing::Level::ERROR,
836 Self::Warn => tracing::Level::WARN,
837 Self::Info => tracing::Level::INFO,
838 Self::Debug => tracing::Level::DEBUG,
839 }
840 }
841
842 pub fn parse_config(s: &str) -> Option<Self> {
844 match s.trim().to_ascii_lowercase().as_str() {
845 "crit" | "critical" => Some(Self::Crit),
846 "error" => Some(Self::Error),
847 "warn" | "warning" => Some(Self::Warn),
848 "info" => Some(Self::Info),
849 "debug" => Some(Self::Debug),
850 _ => None,
851 }
852 }
853
854 pub fn as_config_str(&self) -> &'static str {
856 match self {
857 Self::Crit => "crit",
858 Self::Error => "error",
859 Self::Warn => "warn",
860 Self::Info => "info",
861 Self::Debug => "debug",
862 }
863 }
864}
865
866fn default_db_uri() -> String {
868 "postgresql://localhost/postgres".to_string()
869}
870
871fn default_db_schemas() -> Vec<String> {
872 vec!["public".to_string()]
873}
874
875fn default_pool_size() -> u32 {
876 10
877}
878
879fn default_pool_timeout() -> u64 {
880 10
881}
882
883fn default_tx_isolation() -> IsolationLevel {
884 IsolationLevel::ReadCommitted
885}
886
887fn default_max_body_size() -> usize {
888 10 * 1024 * 1024
889}
890
891fn default_extra_search_path() -> Vec<String> {
892 vec!["public".to_string()]
893}
894
895fn default_db_channel() -> String {
896 "pgrst".to_string()
897}
898
899fn default_host() -> String {
900 "127.0.0.1".to_string()
901}
902
903fn default_port() -> u16 {
904 3000
905}
906
907fn default_jwt_role_claim() -> String {
908 "role".to_string()
909}
910
911fn default_jwt_cache_max() -> u64 {
912 3600
913}
914
915fn default_openapi_mode() -> OpenApiMode {
916 OpenApiMode::FollowPrivileges
917}
918
919fn default_log_level() -> LogLevel {
924 LogLevel::Info
925}
926
927fn default_true() -> bool {
928 true
929}
930
931#[cfg(test)]
932mod tests {
933 use super::*;
934
935 #[test]
936 fn test_default_config() {
937 let config = AppConfig::default();
938 assert_eq!(config.server_port, 3000);
939 assert_eq!(config.db_pool_size, 10);
940 assert!(config.db_prepared_statements);
941 assert!(!config.graphql_federation);
942 assert_eq!(config.graphql_type_prefix, None);
943 }
944
945 #[test]
946 fn test_default_schema() {
947 let mut config = AppConfig::default();
948 assert_eq!(config.default_schema(), "public");
949
950 config.db_schemas = vec!["api".to_string(), "public".to_string()];
951 assert_eq!(config.default_schema(), "api");
952 }
953
954 #[test]
955 fn test_isolation_level_sql() {
956 assert_eq!(IsolationLevel::ReadCommitted.to_sql(), "READ COMMITTED");
957 assert_eq!(IsolationLevel::Serializable.to_sql(), "SERIALIZABLE");
958 }
959
960 #[test]
961 fn isolation_level_accepts_the_documented_spellings() {
962 for s in [
965 "read committed",
966 "READ COMMITTED",
967 "read-committed",
968 " Read_Committed ",
969 ] {
970 assert_eq!(
971 IsolationLevel::parse_config(s),
972 Some(IsolationLevel::ReadCommitted),
973 "{s:?}"
974 );
975 }
976 assert_eq!(
977 IsolationLevel::parse_config("serializable"),
978 Some(IsolationLevel::Serializable)
979 );
980 assert_eq!(
981 IsolationLevel::parse_config("repeatable read"),
982 Some(IsolationLevel::RepeatableRead)
983 );
984 assert_eq!(IsolationLevel::parse_config("dirty read"), None);
987 assert_eq!(IsolationLevel::parse_config(""), None);
988 }
989
990 #[test]
991 fn log_level_accepts_the_documented_spellings() {
992 assert_eq!(LogLevel::parse_config("INFO"), Some(LogLevel::Info));
993 assert_eq!(LogLevel::parse_config("warning"), Some(LogLevel::Warn));
994 assert_eq!(LogLevel::parse_config("crit"), Some(LogLevel::Crit));
995 assert_eq!(LogLevel::parse_config("trace"), None);
998 }
999
1000 #[test]
1001 fn default_log_level_matches_the_documented_one() {
1002 assert_eq!(AppConfig::default().log_level, LogLevel::Info);
1005 }
1006
1007 #[test]
1008 fn parse_bool_accepts_only_the_supported_spellings() {
1009 for s in ["true", "TRUE", "1", "yes", "on", " On "] {
1010 assert_eq!(parse_bool(s), Some(true), "{s:?}");
1011 }
1012 for s in ["false", "FALSE", "0", "no", "off", " Off "] {
1013 assert_eq!(parse_bool(s), Some(false), "{s:?}");
1014 }
1015 for s in ["", "maybe", "enabled", "2"] {
1016 assert_eq!(parse_bool(s), None, "{s:?}");
1017 }
1018 }
1019
1020 #[test]
1021 fn graphql_federation_configuration_is_read() {
1022 let config = with_env(&[
1023 ("PGRST_GRAPHQL_FEDERATION", "true"),
1024 ("PGRST_GRAPHQL_TYPE_PREFIX", " test "),
1025 ]);
1026
1027 assert!(config.graphql_federation);
1028 assert_eq!(config.graphql_type_prefix.as_deref(), Some("test"));
1029 }
1030
1031 #[test]
1032 fn graphql_federation_accepts_an_explicit_false() {
1033 assert!(!with_env(&[("PGRST_GRAPHQL_FEDERATION", "false")]).graphql_federation);
1034 }
1035
1036 #[test]
1037 fn malformed_graphql_federation_configuration_is_rejected() {
1038 let error = with_env_result(&[("PGRST_GRAPHQL_FEDERATION", "sometimes")])
1039 .expect_err("an invalid boolean must reject the configuration");
1040
1041 assert_eq!(
1042 error.to_string(),
1043 "invalid value for PGRST_GRAPHQL_FEDERATION: \"sometimes\"; expected one of: true, false, 1, 0, yes, no, on, off"
1044 );
1045 }
1046
1047 #[test]
1048 fn an_empty_graphql_prefix_means_unset() {
1049 let config = with_env(&[("PGRST_GRAPHQL_TYPE_PREFIX", " ")]);
1050
1051 assert_eq!(config.graphql_type_prefix, None);
1052 }
1053
1054 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1056
1057 fn with_env(vars: &[(&str, &str)]) -> AppConfig {
1059 with_env_result(vars).expect("test configuration should be valid")
1060 }
1061
1062 fn with_env_result(vars: &[(&str, &str)]) -> Result<AppConfig, ConfigError> {
1063 let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1064 for (k, v) in vars {
1065 std::env::set_var(k, v);
1066 }
1067 let config = AppConfig::try_from_env();
1068 for (k, _) in vars {
1069 std::env::remove_var(k);
1070 }
1071 config
1072 }
1073
1074 #[test]
1075 fn documented_variables_are_read() {
1076 let config = with_env(&[
1079 ("PGRST_DB_POOL_SIZE", "42"),
1080 ("PGRST_DB_POOL_TIMEOUT", "7"),
1081 ("PGRST_DB_TX_ISOLATION", "serializable"),
1082 ("PGRST_JWT_SECRET_IS_BASE64", "true"),
1083 ("PGRST_JWT_ROLE_CLAIM_KEY", "user.role"),
1084 ("PGRST_LOG_LEVEL", "warn"),
1085 ("PGRST_MAX_BODY_SIZE", "1024"),
1086 (
1087 "PGRST_SERVER_CORS_ORIGINS",
1088 "https://a.example, https://b.example",
1089 ),
1090 ]);
1091
1092 assert_eq!(config.db_pool_size, 42);
1093 assert_eq!(config.db_pool_timeout, 7);
1094 assert_eq!(config.db_tx_isolation, IsolationLevel::Serializable);
1095 assert!(config.jwt_secret_is_base64);
1096 assert_eq!(config.jwt_role_claim_key, "user.role");
1097 assert_eq!(config.log_level, LogLevel::Warn);
1098 assert_eq!(config.max_body_size, 1024);
1099 assert_eq!(
1100 config.server_cors_origins,
1101 vec![
1102 "https://a.example".to_string(),
1103 "https://b.example".to_string()
1104 ]
1105 );
1106 }
1107
1108 #[test]
1109 fn variables_can_come_from_somewhere_other_than_the_environment() {
1110 let config = AppConfig::try_from_vars([
1114 ("PGRST_DB_SCHEMAS", "api, extra"),
1115 ("PGRST_DB_ANON_ROLE", "web_anon"),
1116 ("PGRST_JWT_SECRET", "a-secret"),
1117 ("PGRST_DB_MAX_ROWS", "100"),
1118 ("PGRST_APP_SETTINGS_Tenant", "acme"),
1119 ])
1120 .unwrap();
1121
1122 assert_eq!(config.db_schemas, ["api", "extra"]);
1123 assert_eq!(config.db_anon_role.as_deref(), Some("web_anon"));
1124 assert_eq!(config.jwt_secret.as_deref(), Some("a-secret"));
1125 assert_eq!(config.db_max_rows, Some(100));
1126 assert_eq!(
1127 config.app_settings.get("tenant").map(String::as_str),
1128 Some("acme")
1129 );
1130
1131 let refused = AppConfig::try_from_vars([("PGRST_DB_AGGREGATES_ENABLED", "maybe")]);
1132 assert!(refused.is_err());
1133 }
1134
1135 #[test]
1136 fn app_settings_differing_only_in_case_take_the_later_value() {
1137 for _ in 0..16 {
1140 let config = AppConfig::try_from_vars([
1141 ("PGRST_APP_SETTINGS_Tenant", "first"),
1142 ("PGRST_APP_SETTINGS_TENANT", "second"),
1143 ("PGRST_APP_SETTINGS_tenant", "third"),
1144 ])
1145 .unwrap();
1146 assert_eq!(
1147 config.app_settings.get("tenant").map(String::as_str),
1148 Some("third")
1149 );
1150
1151 let config = AppConfig::try_from_vars([
1152 ("PGRST_APP_SETTINGS_tenant", "third"),
1153 ("PGRST_APP_SETTINGS_TENANT", "second"),
1154 ])
1155 .unwrap();
1156 assert_eq!(
1157 config.app_settings.get("tenant").map(String::as_str),
1158 Some("second")
1159 );
1160 }
1161 }
1162
1163 #[test]
1164 fn windows_environment_names_are_matched_whatever_their_case() {
1165 let config = AppConfig::try_from_vars([
1167 (upper_env_name("pgrst_db_schemas".into()), "api"),
1168 (upper_env_name("Pgrst_App_Settings_Tenant".into()), "acme"),
1169 ])
1170 .unwrap();
1171 assert_eq!(config.db_schemas, ["api"]);
1172 assert_eq!(
1173 config.app_settings.get("tenant").map(String::as_str),
1174 Some("acme")
1175 );
1176 assert_eq!(env_name("PGRST_DB_URI".into()), "PGRST_DB_URI");
1177 assert_eq!(
1178 env_name("pgrst_db_uri".into()) == "PGRST_DB_URI",
1179 cfg!(windows)
1180 );
1181 }
1182
1183 #[test]
1184 fn pool_size_accepts_postgrests_own_name() {
1185 assert_eq!(with_env(&[("PGRST_DB_POOL", "5")]).db_pool_size, 5);
1186 }
1187
1188 #[test]
1189 fn a_star_origin_means_any_origin() {
1190 assert!(with_env(&[("PGRST_SERVER_CORS_ORIGINS", "*")])
1193 .server_cors_origins
1194 .is_empty());
1195 assert!(with_env(&[("PGRST_SERVER_CORS_ORIGINS", " , ")])
1196 .server_cors_origins
1197 .is_empty());
1198 }
1199
1200 #[test]
1201 fn an_unusable_value_leaves_the_default() {
1202 let config = with_env(&[
1203 ("PGRST_DB_POOL_SIZE", "lots"),
1204 ("PGRST_DB_POOL_TIMEOUT", "0"),
1205 ("PGRST_MAX_BODY_SIZE", "-1"),
1206 ("PGRST_DB_TX_ISOLATION", "dirty read"),
1207 ("PGRST_LOG_LEVEL", "shout"),
1208 ]);
1209
1210 let d = AppConfig::default();
1211 assert_eq!(config.db_pool_size, d.db_pool_size);
1212 assert_eq!(config.db_pool_timeout, d.db_pool_timeout);
1213 assert_eq!(config.max_body_size, d.max_body_size);
1214 assert_eq!(config.db_tx_isolation, d.db_tx_isolation);
1215 assert_eq!(config.log_level, d.log_level);
1216 }
1217
1218 #[test]
1219 fn the_formerly_dead_fields_are_read() {
1220 let config = with_env(&[
1223 ("PGRST_DB_PREPARED_STATEMENTS", "false"),
1224 ("PGRST_DB_PRE_REQUEST", "auth.check"),
1225 ("PGRST_DB_CHANNEL", "my_channel"),
1226 ("PGRST_DB_CHANNEL_ENABLED", "yes"),
1227 ("PGRST_JWT_CACHE_ENABLED", "false"),
1228 ("PGRST_JWT_CACHE_MAX_LIFETIME", "60"),
1229 ("PGRST_SERVER_UNIX_SOCKET", "/tmp/postrust.sock"),
1230 ("PGRST_ADMIN_SERVER_PORT", "3001"),
1231 ("PGRST_OPENAPI_SERVER_PROXY_URI", "https://api.example.com"),
1232 ("PGRST_OPENAPI_MODE", "ignore-privileges"),
1233 ]);
1234
1235 assert!(!config.db_prepared_statements);
1236 assert_eq!(config.db_pre_request.as_deref(), Some("auth.check"));
1237 assert_eq!(config.db_channel, "my_channel");
1238 assert!(config.db_channel_enabled);
1239 assert!(!config.jwt_cache_enabled);
1240 assert_eq!(config.jwt_cache_max_lifetime, 60);
1241 assert_eq!(
1242 config.server_unix_socket.as_deref(),
1243 Some("/tmp/postrust.sock")
1244 );
1245 assert_eq!(config.admin_server_port, Some(3001));
1246 assert_eq!(
1247 config.openapi_server_proxy_uri.as_deref(),
1248 Some("https://api.example.com")
1249 );
1250 assert_eq!(config.openapi_mode, OpenApiMode::IgnorePrivileges);
1251 }
1252
1253 #[test]
1254 fn a_zero_jwt_cache_lifetime_is_kept_because_it_turns_the_cache_off() {
1255 assert_eq!(
1258 with_env(&[("PGRST_JWT_CACHE_MAX_LIFETIME", "0")]).jwt_cache_max_lifetime,
1259 0
1260 );
1261 }
1262
1263 #[test]
1264 fn app_settings_come_from_a_prefix() {
1265 let config = with_env(&[
1266 ("PGRST_APP_SETTINGS_JWT_LIFETIME", "3600"),
1267 ("PGRST_APP_SETTINGS_TenantName", "acme"),
1268 ]);
1269 assert_eq!(
1272 config.app_settings.get("jwt_lifetime").map(String::as_str),
1273 Some("3600")
1274 );
1275 assert_eq!(
1276 config.app_settings.get("tenantname").map(String::as_str),
1277 Some("acme")
1278 );
1279 }
1280
1281 #[test]
1282 fn role_settings_parse_from_json_using_the_documented_spellings() {
1283 let config = with_env(&[(
1284 "PGRST_ROLE_SETTINGS",
1285 r#"{"web_user":{"isolation_level":"serializable","statement_timeout":5000}}"#,
1286 )]);
1287 let web_user = config.role_settings.get("web_user").expect("role present");
1288 assert_eq!(web_user.isolation_level, Some(IsolationLevel::Serializable));
1289 assert_eq!(web_user.statement_timeout, Some(5000));
1290 }
1291
1292 #[test]
1293 fn unparseable_role_settings_leave_the_map_empty() {
1294 assert!(with_env(&[("PGRST_ROLE_SETTINGS", "{not json")])
1295 .role_settings
1296 .is_empty());
1297 assert!(with_env(&[(
1300 "PGRST_ROLE_SETTINGS",
1301 r#"{"r":{"isolation_level":"dirty"}}"#
1302 )])
1303 .role_settings
1304 .is_empty());
1305 }
1306
1307 #[test]
1308 fn openapi_mode_accepts_the_documented_spellings() {
1309 assert_eq!(
1310 OpenApiMode::parse_config("follow-privileges"),
1311 Some(OpenApiMode::FollowPrivileges)
1312 );
1313 assert_eq!(
1314 OpenApiMode::parse_config("IGNORE_PRIVILEGES"),
1315 Some(OpenApiMode::IgnorePrivileges)
1316 );
1317 assert_eq!(
1318 OpenApiMode::parse_config("disabled"),
1319 Some(OpenApiMode::Disabled)
1320 );
1321 assert_eq!(OpenApiMode::parse_config("security-definer"), None);
1324 }
1325
1326 #[test]
1327 fn the_config_enums_round_trip_through_json() {
1328 for level in [
1334 IsolationLevel::ReadCommitted,
1335 IsolationLevel::RepeatableRead,
1336 IsolationLevel::Serializable,
1337 ] {
1338 let json = serde_json::to_string(&level).unwrap();
1339 let back: IsolationLevel = serde_json::from_str(&json).unwrap();
1340 assert_eq!(back, level, "{json}");
1341 }
1342
1343 for mode in [
1344 OpenApiMode::Disabled,
1345 OpenApiMode::FollowPrivileges,
1346 OpenApiMode::IgnorePrivileges,
1347 ] {
1348 let json = serde_json::to_string(&mode).unwrap();
1349 assert_eq!(serde_json::from_str::<OpenApiMode>(&json).unwrap(), mode);
1350 }
1351
1352 for level in [
1353 LogLevel::Crit,
1354 LogLevel::Error,
1355 LogLevel::Warn,
1356 LogLevel::Info,
1357 LogLevel::Debug,
1358 ] {
1359 let json = serde_json::to_string(&level).unwrap();
1360 assert_eq!(serde_json::from_str::<LogLevel>(&json).unwrap(), level);
1361 }
1362 }
1363
1364 #[test]
1365 fn a_whole_config_round_trips_through_json() {
1366 let mut config = AppConfig::default();
1368 config.role_settings.insert(
1369 "web_user".to_string(),
1370 RoleSettings {
1371 isolation_level: Some(IsolationLevel::RepeatableRead),
1372 statement_timeout: Some(5000),
1373 },
1374 );
1375
1376 let json = serde_json::to_string(&config).unwrap();
1377 let back: AppConfig = serde_json::from_str(&json).expect("config should round trip");
1378
1379 assert_eq!(back.db_tx_isolation, config.db_tx_isolation);
1380 assert_eq!(back.log_level, config.log_level);
1381 assert_eq!(back.openapi_mode, config.openapi_mode);
1382 assert_eq!(
1383 back.role_settings["web_user"].isolation_level,
1384 Some(IsolationLevel::RepeatableRead)
1385 );
1386 }
1387
1388 #[test]
1389 fn the_serialized_spelling_is_the_configuration_spelling() {
1390 assert_eq!(
1393 serde_json::to_string(&IsolationLevel::ReadCommitted).unwrap(),
1394 "\"read committed\""
1395 );
1396 assert_eq!(
1397 serde_json::to_string(&OpenApiMode::FollowPrivileges).unwrap(),
1398 "\"follow-privileges\""
1399 );
1400 assert_eq!(serde_json::to_string(&LogLevel::Warn).unwrap(), "\"warn\"");
1401 }
1402
1403 #[test]
1404 fn an_empty_role_claim_key_is_not_accepted() {
1405 assert_eq!(
1408 with_env(&[("PGRST_JWT_ROLE_CLAIM_KEY", " ")]).jwt_role_claim_key,
1409 AppConfig::default().jwt_role_claim_key
1410 );
1411 }
1412}