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 pub jwt_iss: Option<String>,
141
142 #[serde(default = "default_jwt_role_claim")]
144 pub jwt_role_claim_key: String,
145
146 #[serde(default = "default_true")]
148 pub jwt_cache_enabled: bool,
149
150 #[serde(default = "default_jwt_cache_max")]
155 pub jwt_cache_max_lifetime: u64,
156
157 pub jwt_jwks_url: Option<String>,
164
165 #[serde(default = "default_jwt_jwks_refresh")]
168 pub jwt_jwks_refresh: u64,
169
170 pub hasura_admin_secret: Option<String>,
183
184 pub hasura_unauthorized_role: Option<String>,
188
189 pub openapi_server_proxy_uri: Option<String>,
196
197 #[serde(default = "default_openapi_mode")]
199 pub openapi_mode: OpenApiMode,
200
201 #[serde(default = "default_log_level")]
206 pub log_level: LogLevel,
207
208 #[serde(default)]
217 pub log_format: LogFormat,
218
219 #[serde(default)]
227 pub otel_db_statement: bool,
228
229 #[serde(default)]
234 pub role_settings: HashMap<String, RoleSettings>,
235
236 #[serde(default)]
238 pub app_settings: HashMap<String, String>,
239
240 #[serde(default)]
245 pub graphql_federation: bool,
246
247 #[serde(default)]
249 pub graphql_type_prefix: Option<String>,
250
251 #[serde(default)]
263 pub compat_mode: bool,
264}
265
266impl Default for AppConfig {
267 fn default() -> Self {
268 Self {
269 db_uri: default_db_uri(),
270 db_schemas: default_db_schemas(),
271 db_anon_role: None,
272 db_pool_size: default_pool_size(),
273 db_pool_timeout: default_pool_timeout(),
274 db_tx_isolation: default_tx_isolation(),
275 db_prepared_statements: true,
276 db_extra_search_path: default_extra_search_path(),
277 db_channel: default_db_channel(),
278 db_channel_enabled: false,
279 db_pre_request: None,
280 db_max_rows: None,
281 db_aggregates_enabled: false,
282 server_host: default_host(),
283 server_port: default_port(),
284 server_cors_origins: Vec::new(),
285 max_body_size: default_max_body_size(),
286 server_unix_socket: None,
287 admin_server_port: None,
288 jwt_secret: None,
289 jwt_secret_is_base64: false,
290 jwt_aud: None,
291 jwt_iss: None,
292 jwt_role_claim_key: default_jwt_role_claim(),
293 jwt_cache_enabled: true,
294 jwt_cache_max_lifetime: default_jwt_cache_max(),
295 jwt_jwks_url: None,
296 jwt_jwks_refresh: default_jwt_jwks_refresh(),
297 hasura_admin_secret: None,
298 hasura_unauthorized_role: None,
299 openapi_server_proxy_uri: None,
300 openapi_mode: OpenApiMode::FollowPrivileges,
301 log_level: default_log_level(),
302 log_format: LogFormat::Text,
303 otel_db_statement: false,
304 role_settings: HashMap::new(),
305 app_settings: HashMap::new(),
306 graphql_federation: false,
307 graphql_type_prefix: None,
308 compat_mode: false,
309 }
310 }
311}
312
313fn warn_ignored(var: &str, value: &str, expected: &str) {
317 tracing::warn!("Ignoring {}={:?}: expected {}", var, value, expected);
318}
319
320fn parse_bool(value: &str) -> Option<bool> {
322 match value.trim().to_ascii_lowercase().as_str() {
323 "true" | "1" | "yes" | "on" => Some(true),
324 "false" | "0" | "no" | "off" => Some(false),
325 _ => None,
326 }
327}
328
329fn env_bool(variable: &'static str, value: &str) -> Result<bool, ConfigError> {
330 parse_bool(value).ok_or_else(|| {
331 let error = ConfigError {
332 variable,
333 value: value.to_string(),
334 expected: EXPECTED_BOOLEAN,
335 };
336 tracing::warn!("{error}");
337 error
338 })
339}
340
341fn parse_graphql_type_prefix(value: &str) -> Result<Option<String>, &'static str> {
343 let value = value.trim();
344 if value.is_empty() {
345 return Ok(None);
346 }
347
348 let mut chars = value.chars();
349 let valid_first = chars
350 .next()
351 .is_some_and(|c| c == '_' || c.is_ascii_alphabetic());
352 if !valid_first || !chars.all(|c| c == '_' || c.is_ascii_alphanumeric()) {
353 return Err("a GraphQL identifier beginning with a letter or underscore");
354 }
355
356 Ok(Some(value.to_string()))
357}
358
359fn env_name(name: String) -> String {
365 if cfg!(windows) {
366 upper_env_name(name)
367 } else {
368 name
369 }
370}
371
372fn upper_env_name(mut name: String) -> String {
373 name.make_ascii_uppercase();
374 name
375}
376
377impl AppConfig {
378 pub fn from_env() -> Self {
385 Self::try_from_env().unwrap_or_else(|error| panic!("{error}"))
386 }
387
388 pub fn try_from_env() -> Result<Self, ConfigError> {
391 Self::try_from_vars(
394 std::env::vars_os().filter_map(|(k, v)| {
395 Some((env_name(k.into_string().ok()?), v.into_string().ok()?))
396 }),
397 )
398 }
399
400 pub fn try_from_vars<I, K, V>(vars: I) -> Result<Self, ConfigError>
405 where
406 I: IntoIterator<Item = (K, V)>,
407 K: Into<String>,
408 V: Into<String>,
409 {
410 let pairs: Vec<(String, String)> = vars
415 .into_iter()
416 .map(|(k, v)| (k.into(), v.into()))
417 .collect();
418 let vars: HashMap<&str, &str> = pairs
419 .iter()
420 .map(|(k, v)| (k.as_str(), v.as_str()))
421 .collect();
422 let get = |key: &str| vars.get(key).map(|v| v.to_string()).ok_or(());
423 let mut config = Self::default();
424
425 if let Ok(uri) = get("PGRST_DB_URI") {
426 config.db_uri = uri;
427 }
428 if let Ok(uri) = get("DATABASE_URL") {
429 config.db_uri = uri;
430 }
431 if let Ok(schemas) = get("PGRST_DB_SCHEMAS") {
432 config.db_schemas = schemas.split(',').map(|s| s.trim().to_string()).collect();
433 }
434 if let Ok(role) = get("PGRST_DB_ANON_ROLE") {
435 config.db_anon_role = Some(role);
436 }
437 for var in ["PGRST_DB_POOL", "PGRST_DB_POOL_SIZE"] {
443 if let Ok(size) = get(var) {
444 match size.parse::<u32>() {
445 Ok(n) if n > 0 => config.db_pool_size = n,
446 _ => warn_ignored(var, &size, "a positive integer"),
447 }
448 }
449 }
450 if let Ok(secs) = get("PGRST_DB_POOL_TIMEOUT") {
451 match secs.parse::<u64>() {
452 Ok(n) if n > 0 => config.db_pool_timeout = n,
453 _ => warn_ignored("PGRST_DB_POOL_TIMEOUT", &secs, "a positive integer"),
454 }
455 }
456 if let Ok(level) = get("PGRST_DB_TX_ISOLATION") {
457 match IsolationLevel::parse_config(&level) {
458 Some(l) => config.db_tx_isolation = l,
459 None => warn_ignored(
460 "PGRST_DB_TX_ISOLATION",
461 &level,
462 "one of: read committed, repeatable read, serializable",
463 ),
464 }
465 }
466 if let Ok(v) = get("PGRST_DB_EXTRA_SEARCH_PATH") {
467 config.db_extra_search_path = v
468 .split(',')
469 .map(|s| s.trim().to_string())
470 .filter(|s| !s.is_empty())
471 .collect();
472 }
473
474 if let Ok(v) = get("PGRST_DB_AGGREGATES_ENABLED") {
475 config.db_aggregates_enabled = env_bool("PGRST_DB_AGGREGATES_ENABLED", &v)?;
476 }
477 if let Ok(v) = get("PGRST_DB_PREPARED_STATEMENTS") {
478 config.db_prepared_statements = env_bool("PGRST_DB_PREPARED_STATEMENTS", &v)?;
479 }
480 if let Ok(v) = get("PGRST_DB_PRE_REQUEST") {
481 if !v.trim().is_empty() {
482 config.db_pre_request = Some(v.trim().to_string());
483 }
484 }
485 if let Ok(v) = get("PGRST_DB_CHANNEL") {
486 if !v.trim().is_empty() {
487 config.db_channel = v.trim().to_string();
488 }
489 }
490 if let Ok(v) = get("PGRST_DB_CHANNEL_ENABLED") {
491 config.db_channel_enabled = env_bool("PGRST_DB_CHANNEL_ENABLED", &v)?;
492 }
493
494 for var in ["PGRST_DB_MAX_ROWS", "PGRST_MAX_ROWS"] {
496 if let Ok(max_rows) = get(var) {
497 match max_rows.parse::<i64>() {
498 Ok(n) if n >= 0 => config.db_max_rows = Some(n),
499 _ => warn_ignored(var, &max_rows, "a non-negative integer"),
500 }
501 }
502 }
503 if let Ok(secret) = get("PGRST_JWT_SECRET") {
504 config.jwt_secret = Some(secret);
505 }
506 if let Ok(aud) = get("PGRST_JWT_AUD") {
507 config.jwt_aud = Some(aud);
508 }
509 if let Ok(iss) = get("PGRST_JWT_ISS") {
512 if !iss.trim().is_empty() {
513 config.jwt_iss = Some(iss);
514 }
515 }
516 if let Ok(v) = get("PGRST_JWT_SECRET_IS_BASE64") {
521 config.jwt_secret_is_base64 = env_bool("PGRST_JWT_SECRET_IS_BASE64", &v)?;
522 }
523 if let Ok(key) = get("PGRST_JWT_ROLE_CLAIM_KEY") {
524 if !key.trim().is_empty() {
525 config.jwt_role_claim_key = key;
526 }
527 }
528 if let Ok(v) = get("PGRST_JWT_CACHE_ENABLED") {
529 config.jwt_cache_enabled = env_bool("PGRST_JWT_CACHE_ENABLED", &v)?;
530 }
531 if let Ok(v) = get("PGRST_JWT_CACHE_MAX_LIFETIME") {
534 match v.parse::<u64>() {
535 Ok(n) => config.jwt_cache_max_lifetime = n,
536 Err(_) => warn_ignored(
537 "PGRST_JWT_CACHE_MAX_LIFETIME",
538 &v,
539 "a non-negative integer number of seconds",
540 ),
541 }
542 }
543 if let Ok(url) = get("PGRST_JWT_JWKS_URL") {
545 if !url.trim().is_empty() {
546 config.jwt_jwks_url = Some(url.trim().to_string());
547 }
548 }
549 if let Ok(secs) = get("PGRST_JWT_JWKS_REFRESH") {
550 match secs.parse::<u64>() {
551 Ok(n) if n > 0 => config.jwt_jwks_refresh = n,
552 _ => warn_ignored(
553 "PGRST_JWT_JWKS_REFRESH",
554 &secs,
555 "a positive integer number of seconds",
556 ),
557 }
558 }
559 for var in ["HASURA_GRAPHQL_ADMIN_SECRET", "PGRST_HASURA_ADMIN_SECRET"] {
565 if let Ok(secret) = get(var) {
566 if !secret.is_empty() {
567 config.hasura_admin_secret = Some(secret);
568 }
569 }
570 }
571 for var in [
572 "HASURA_GRAPHQL_UNAUTHORIZED_ROLE",
573 "PGRST_HASURA_UNAUTHORIZED_ROLE",
574 ] {
575 if let Ok(role) = get(var) {
576 if !role.is_empty() {
577 config.hasura_unauthorized_role = Some(role);
578 }
579 }
580 }
581 if let Ok(host) = get("PGRST_SERVER_HOST") {
582 config.server_host = host;
583 }
584 if let Ok(port) = get("PGRST_SERVER_PORT") {
585 if let Ok(p) = port.parse() {
586 config.server_port = p;
587 }
588 }
589 if let Ok(port) = get("PORT") {
590 if let Ok(p) = port.parse() {
591 config.server_port = p;
592 }
593 }
594 if let Ok(origins) = get("PGRST_SERVER_CORS_ORIGINS") {
598 config.server_cors_origins = if origins.trim() == "*" {
599 Vec::new()
600 } else {
601 origins
602 .split(',')
603 .map(|s| s.trim().to_string())
604 .filter(|s| !s.is_empty())
605 .collect()
606 };
607 }
608 if let Ok(bytes) = get("PGRST_MAX_BODY_SIZE") {
609 match bytes.parse::<usize>() {
610 Ok(n) if n > 0 => config.max_body_size = n,
611 _ => warn_ignored("PGRST_MAX_BODY_SIZE", &bytes, "a positive integer"),
612 }
613 }
614 if let Ok(level) = get("PGRST_LOG_LEVEL") {
615 match LogLevel::parse_config(&level) {
616 Some(l) => config.log_level = l,
617 None => warn_ignored(
618 "PGRST_LOG_LEVEL",
619 &level,
620 "one of: crit, error, warn, info, debug",
621 ),
622 }
623 }
624 if let Ok(format) = get("PGRST_LOG_FORMAT") {
625 match LogFormat::parse_config(&format) {
626 Some(f) => config.log_format = f,
627 None => warn_ignored("PGRST_LOG_FORMAT", &format, "one of: text, json"),
628 }
629 }
630 if let Ok(v) = get("PGRST_OTEL_DB_STATEMENT") {
631 config.otel_db_statement = env_bool("PGRST_OTEL_DB_STATEMENT", &v)?;
632 }
633 if let Ok(path) = get("PGRST_SERVER_UNIX_SOCKET") {
634 if !path.trim().is_empty() {
635 config.server_unix_socket = Some(path.trim().to_string());
636 }
637 }
638 if let Ok(port) = get("PGRST_ADMIN_SERVER_PORT") {
639 match port.parse::<u16>() {
640 Ok(p) if p > 0 => config.admin_server_port = Some(p),
641 _ => warn_ignored("PGRST_ADMIN_SERVER_PORT", &port, "a port number"),
642 }
643 }
644 if let Ok(uri) = get("PGRST_OPENAPI_SERVER_PROXY_URI") {
645 if !uri.trim().is_empty() {
646 config.openapi_server_proxy_uri = Some(uri.trim().to_string());
647 }
648 }
649 if let Ok(mode) = get("PGRST_OPENAPI_MODE") {
650 match OpenApiMode::parse_config(&mode) {
651 Some(m) => config.openapi_mode = m,
652 None => warn_ignored(
653 "PGRST_OPENAPI_MODE",
654 &mode,
655 "one of: disabled, follow-privileges, ignore-privileges",
656 ),
657 }
658 }
659
660 if let Ok(raw) = get("PGRST_ROLE_SETTINGS") {
664 match serde_json::from_str::<HashMap<String, RoleSettings>>(&raw) {
665 Ok(settings) => config.role_settings = settings,
666 Err(e) => warn_ignored(
667 "PGRST_ROLE_SETTINGS",
668 &raw,
669 &format!("a JSON object of role to settings ({e})"),
670 ),
671 }
672 }
673
674 if let Ok(value) = get("PGRST_GRAPHQL_FEDERATION") {
675 config.graphql_federation = env_bool("PGRST_GRAPHQL_FEDERATION", &value)?;
676 }
677 if let Ok(value) = get("PGRST_GRAPHQL_TYPE_PREFIX") {
678 match parse_graphql_type_prefix(&value) {
679 Ok(prefix) => config.graphql_type_prefix = prefix,
680 Err(expected) => warn_ignored("PGRST_GRAPHQL_TYPE_PREFIX", &value, expected),
681 }
682 }
683
684 for (key, value) in &pairs {
690 if let Some(name) = key.strip_prefix("PGRST_APP_SETTINGS_") {
691 if !name.is_empty() {
692 config
693 .app_settings
694 .insert(name.to_ascii_lowercase(), value.clone());
695 }
696 }
697 }
698 for var in ["PGRST_COMPAT_MODE", "POSTRUST_COMPAT_MODE"] {
701 if let Ok(v) = get(var) {
702 config.compat_mode = env_bool(var, &v)?;
703 }
704 }
705
706 Ok(config)
707 }
708
709 pub fn default_schema(&self) -> &str {
711 self.db_schemas
712 .first()
713 .map(|s| s.as_str())
714 .unwrap_or("public")
715 }
716}
717
718impl Serialize for IsolationLevel {
724 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
725 serializer.serialize_str(self.as_config_str())
726 }
727}
728
729impl<'de> Deserialize<'de> for IsolationLevel {
730 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
731 let raw = String::deserialize(deserializer)?;
732 Self::parse_config(&raw).ok_or_else(|| {
733 serde::de::Error::custom(format!(
734 "unknown isolation level {raw:?}: expected one of \
735 read committed, repeatable read, serializable"
736 ))
737 })
738 }
739}
740
741impl Serialize for OpenApiMode {
742 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
743 serializer.serialize_str(self.as_config_str())
744 }
745}
746
747impl<'de> Deserialize<'de> for OpenApiMode {
748 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
749 let raw = String::deserialize(deserializer)?;
750 Self::parse_config(&raw).ok_or_else(|| {
751 serde::de::Error::custom(format!(
752 "unknown OpenAPI mode {raw:?}: expected one of \
753 disabled, follow-privileges, ignore-privileges"
754 ))
755 })
756 }
757}
758
759impl Serialize for LogLevel {
760 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
761 serializer.serialize_str(self.as_config_str())
762 }
763}
764
765impl<'de> Deserialize<'de> for LogLevel {
766 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
767 let raw = String::deserialize(deserializer)?;
768 Self::parse_config(&raw).ok_or_else(|| {
769 serde::de::Error::custom(format!(
770 "unknown log level {raw:?}: expected one of \
771 crit, error, warn, info, debug"
772 ))
773 })
774 }
775}
776
777impl Serialize for LogFormat {
778 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
779 serializer.serialize_str(self.as_config_str())
780 }
781}
782
783impl<'de> Deserialize<'de> for LogFormat {
784 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
785 let raw = String::deserialize(deserializer)?;
786 Self::parse_config(&raw).ok_or_else(|| {
787 serde::de::Error::custom(format!(
788 "unknown log format {raw:?}: expected one of text, json"
789 ))
790 })
791 }
792}
793
794#[derive(Clone, Debug, Default, Serialize, Deserialize)]
796#[non_exhaustive]
797pub struct RoleSettings {
798 pub isolation_level: Option<IsolationLevel>,
800 pub statement_timeout: Option<u64>,
802}
803
804#[derive(Clone, Debug, PartialEq, Eq)]
810pub enum IsolationLevel {
811 ReadCommitted,
813 RepeatableRead,
815 Serializable,
819}
820
821impl IsolationLevel {
822 pub fn to_sql(&self) -> &'static str {
824 match self {
825 Self::ReadCommitted => "READ COMMITTED",
826 Self::RepeatableRead => "REPEATABLE READ",
827 Self::Serializable => "SERIALIZABLE",
828 }
829 }
830
831 pub fn parse_config(s: &str) -> Option<Self> {
837 match s
838 .trim()
839 .to_ascii_lowercase()
840 .replace(['-', '_'], " ")
841 .as_str()
842 {
843 "read committed" => Some(Self::ReadCommitted),
844 "repeatable read" => Some(Self::RepeatableRead),
845 "serializable" => Some(Self::Serializable),
846 _ => None,
847 }
848 }
849
850 pub fn as_config_str(&self) -> &'static str {
852 match self {
853 Self::ReadCommitted => "read committed",
854 Self::RepeatableRead => "repeatable read",
855 Self::Serializable => "serializable",
856 }
857 }
858}
859
860#[derive(Clone, Debug, PartialEq, Eq)]
867pub enum OpenApiMode {
868 Disabled,
870 FollowPrivileges,
873 IgnorePrivileges,
875}
876
877impl OpenApiMode {
878 pub fn parse_config(s: &str) -> Option<Self> {
881 match s
882 .trim()
883 .to_ascii_lowercase()
884 .replace(['-', '_'], " ")
885 .as_str()
886 {
887 "disabled" => Some(Self::Disabled),
888 "follow privileges" => Some(Self::FollowPrivileges),
889 "ignore privileges" => Some(Self::IgnorePrivileges),
890 _ => None,
891 }
892 }
893
894 pub fn as_config_str(&self) -> &'static str {
896 match self {
897 Self::Disabled => "disabled",
898 Self::FollowPrivileges => "follow-privileges",
899 Self::IgnorePrivileges => "ignore-privileges",
900 }
901 }
902}
903
904#[derive(Clone, Debug, PartialEq, Eq)]
906pub enum LogLevel {
907 Crit,
910 Error,
912 Warn,
914 Info,
917 Debug,
920}
921
922impl LogLevel {
923 pub fn to_tracing(&self) -> tracing::Level {
928 match self {
929 Self::Crit | Self::Error => tracing::Level::ERROR,
930 Self::Warn => tracing::Level::WARN,
931 Self::Info => tracing::Level::INFO,
932 Self::Debug => tracing::Level::DEBUG,
933 }
934 }
935
936 pub fn parse_config(s: &str) -> Option<Self> {
938 match s.trim().to_ascii_lowercase().as_str() {
939 "crit" | "critical" => Some(Self::Crit),
940 "error" => Some(Self::Error),
941 "warn" | "warning" => Some(Self::Warn),
942 "info" => Some(Self::Info),
943 "debug" => Some(Self::Debug),
944 _ => None,
945 }
946 }
947
948 pub fn as_config_str(&self) -> &'static str {
950 match self {
951 Self::Crit => "crit",
952 Self::Error => "error",
953 Self::Warn => "warn",
954 Self::Info => "info",
955 Self::Debug => "debug",
956 }
957 }
958}
959
960#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
962pub enum LogFormat {
963 #[default]
966 Text,
967 Json,
970}
971
972impl LogFormat {
973 pub fn parse_config(s: &str) -> Option<Self> {
975 match s.trim().to_ascii_lowercase().as_str() {
976 "text" => Some(Self::Text),
977 "json" => Some(Self::Json),
978 _ => None,
979 }
980 }
981
982 pub fn as_config_str(&self) -> &'static str {
984 match self {
985 Self::Text => "text",
986 Self::Json => "json",
987 }
988 }
989}
990
991fn default_db_uri() -> String {
993 "postgresql://localhost/postgres".to_string()
994}
995
996fn default_db_schemas() -> Vec<String> {
997 vec!["public".to_string()]
998}
999
1000fn default_pool_size() -> u32 {
1001 10
1002}
1003
1004fn default_pool_timeout() -> u64 {
1005 10
1006}
1007
1008fn default_tx_isolation() -> IsolationLevel {
1009 IsolationLevel::ReadCommitted
1010}
1011
1012fn default_max_body_size() -> usize {
1013 10 * 1024 * 1024
1014}
1015
1016fn default_extra_search_path() -> Vec<String> {
1017 vec!["public".to_string()]
1018}
1019
1020fn default_db_channel() -> String {
1021 "pgrst".to_string()
1022}
1023
1024fn default_host() -> String {
1025 "127.0.0.1".to_string()
1026}
1027
1028fn default_port() -> u16 {
1029 3000
1030}
1031
1032fn default_jwt_role_claim() -> String {
1033 "role".to_string()
1034}
1035
1036fn default_jwt_cache_max() -> u64 {
1037 3600
1038}
1039
1040fn default_jwt_jwks_refresh() -> u64 {
1041 300
1042}
1043
1044fn default_openapi_mode() -> OpenApiMode {
1045 OpenApiMode::FollowPrivileges
1046}
1047
1048fn default_log_level() -> LogLevel {
1053 LogLevel::Info
1054}
1055
1056fn default_true() -> bool {
1057 true
1058}
1059
1060#[cfg(test)]
1061mod tests {
1062 use super::*;
1063
1064 #[test]
1065 fn test_default_config() {
1066 let config = AppConfig::default();
1067 assert_eq!(config.server_port, 3000);
1068 assert_eq!(config.db_pool_size, 10);
1069 assert!(config.db_prepared_statements);
1070 assert!(!config.graphql_federation);
1071 assert_eq!(config.graphql_type_prefix, None);
1072 }
1073
1074 #[test]
1075 fn test_default_schema() {
1076 let mut config = AppConfig::default();
1077 assert_eq!(config.default_schema(), "public");
1078
1079 config.db_schemas = vec!["api".to_string(), "public".to_string()];
1080 assert_eq!(config.default_schema(), "api");
1081 }
1082
1083 #[test]
1084 fn test_isolation_level_sql() {
1085 assert_eq!(IsolationLevel::ReadCommitted.to_sql(), "READ COMMITTED");
1086 assert_eq!(IsolationLevel::Serializable.to_sql(), "SERIALIZABLE");
1087 }
1088
1089 #[test]
1090 fn isolation_level_accepts_the_documented_spellings() {
1091 for s in [
1094 "read committed",
1095 "READ COMMITTED",
1096 "read-committed",
1097 " Read_Committed ",
1098 ] {
1099 assert_eq!(
1100 IsolationLevel::parse_config(s),
1101 Some(IsolationLevel::ReadCommitted),
1102 "{s:?}"
1103 );
1104 }
1105 assert_eq!(
1106 IsolationLevel::parse_config("serializable"),
1107 Some(IsolationLevel::Serializable)
1108 );
1109 assert_eq!(
1110 IsolationLevel::parse_config("repeatable read"),
1111 Some(IsolationLevel::RepeatableRead)
1112 );
1113 assert_eq!(IsolationLevel::parse_config("dirty read"), None);
1116 assert_eq!(IsolationLevel::parse_config(""), None);
1117 }
1118
1119 #[test]
1120 fn log_level_accepts_the_documented_spellings() {
1121 assert_eq!(LogLevel::parse_config("INFO"), Some(LogLevel::Info));
1122 assert_eq!(LogLevel::parse_config("warning"), Some(LogLevel::Warn));
1123 assert_eq!(LogLevel::parse_config("crit"), Some(LogLevel::Crit));
1124 assert_eq!(LogLevel::parse_config("trace"), None);
1127 }
1128
1129 #[test]
1130 fn default_log_level_matches_the_documented_one() {
1131 assert_eq!(AppConfig::default().log_level, LogLevel::Info);
1134 }
1135
1136 #[test]
1137 fn parse_bool_accepts_only_the_supported_spellings() {
1138 for s in ["true", "TRUE", "1", "yes", "on", " On "] {
1139 assert_eq!(parse_bool(s), Some(true), "{s:?}");
1140 }
1141 for s in ["false", "FALSE", "0", "no", "off", " Off "] {
1142 assert_eq!(parse_bool(s), Some(false), "{s:?}");
1143 }
1144 for s in ["", "maybe", "enabled", "2"] {
1145 assert_eq!(parse_bool(s), None, "{s:?}");
1146 }
1147 }
1148
1149 #[test]
1150 fn graphql_federation_configuration_is_read() {
1151 let config = with_env(&[
1152 ("PGRST_GRAPHQL_FEDERATION", "true"),
1153 ("PGRST_GRAPHQL_TYPE_PREFIX", " test "),
1154 ]);
1155
1156 assert!(config.graphql_federation);
1157 assert_eq!(config.graphql_type_prefix.as_deref(), Some("test"));
1158 }
1159
1160 #[test]
1161 fn graphql_federation_accepts_an_explicit_false() {
1162 assert!(!with_env(&[("PGRST_GRAPHQL_FEDERATION", "false")]).graphql_federation);
1163 }
1164
1165 #[test]
1166 fn malformed_graphql_federation_configuration_is_rejected() {
1167 let error = with_env_result(&[("PGRST_GRAPHQL_FEDERATION", "sometimes")])
1168 .expect_err("an invalid boolean must reject the configuration");
1169
1170 assert_eq!(
1171 error.to_string(),
1172 "invalid value for PGRST_GRAPHQL_FEDERATION: \"sometimes\"; expected one of: true, false, 1, 0, yes, no, on, off"
1173 );
1174 }
1175
1176 #[test]
1177 fn an_empty_graphql_prefix_means_unset() {
1178 let config = with_env(&[("PGRST_GRAPHQL_TYPE_PREFIX", " ")]);
1179
1180 assert_eq!(config.graphql_type_prefix, None);
1181 }
1182
1183 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1185
1186 fn with_env(vars: &[(&str, &str)]) -> AppConfig {
1188 with_env_result(vars).expect("test configuration should be valid")
1189 }
1190
1191 fn with_env_result(vars: &[(&str, &str)]) -> Result<AppConfig, ConfigError> {
1192 let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1193 for (k, v) in vars {
1194 std::env::set_var(k, v);
1195 }
1196 let config = AppConfig::try_from_env();
1197 for (k, _) in vars {
1198 std::env::remove_var(k);
1199 }
1200 config
1201 }
1202
1203 #[test]
1204 fn documented_variables_are_read() {
1205 let config = with_env(&[
1208 ("PGRST_DB_POOL_SIZE", "42"),
1209 ("PGRST_DB_POOL_TIMEOUT", "7"),
1210 ("PGRST_DB_TX_ISOLATION", "serializable"),
1211 ("PGRST_JWT_SECRET_IS_BASE64", "true"),
1212 ("PGRST_JWT_ROLE_CLAIM_KEY", "user.role"),
1213 ("PGRST_LOG_LEVEL", "warn"),
1214 ("PGRST_MAX_BODY_SIZE", "1024"),
1215 (
1216 "PGRST_SERVER_CORS_ORIGINS",
1217 "https://a.example, https://b.example",
1218 ),
1219 ]);
1220
1221 assert_eq!(config.db_pool_size, 42);
1222 assert_eq!(config.db_pool_timeout, 7);
1223 assert_eq!(config.db_tx_isolation, IsolationLevel::Serializable);
1224 assert!(config.jwt_secret_is_base64);
1225 assert_eq!(config.jwt_role_claim_key, "user.role");
1226 assert_eq!(config.log_level, LogLevel::Warn);
1227 assert_eq!(config.max_body_size, 1024);
1228 assert_eq!(
1229 config.server_cors_origins,
1230 vec![
1231 "https://a.example".to_string(),
1232 "https://b.example".to_string()
1233 ]
1234 );
1235 }
1236
1237 #[test]
1238 fn the_jwt_issuer_is_read_and_an_empty_one_is_unset() {
1239 let read = |value: &str| {
1240 AppConfig::try_from_vars([("PGRST_JWT_ISS", value)])
1241 .unwrap()
1242 .jwt_iss
1243 };
1244 assert_eq!(
1245 read("https://issuer.example").as_deref(),
1246 Some("https://issuer.example")
1247 );
1248 assert_eq!(read(" "), None);
1249 assert_eq!(
1250 AppConfig::try_from_vars(Vec::<(String, String)>::new())
1251 .unwrap()
1252 .jwt_iss,
1253 None
1254 );
1255 }
1256
1257 #[test]
1258 fn the_jwks_url_and_its_refresh_interval_are_read() {
1259 let read = |vars: &[(&str, &str)]| AppConfig::try_from_vars(vars.to_vec()).unwrap();
1260 let config = read(&[
1261 ("PGRST_JWT_JWKS_URL", " https://idp.example/jwks "),
1262 ("PGRST_JWT_JWKS_REFRESH", "60"),
1263 ]);
1264 assert_eq!(
1265 config.jwt_jwks_url.as_deref(),
1266 Some("https://idp.example/jwks")
1267 );
1268 assert_eq!(config.jwt_jwks_refresh, 60);
1269
1270 let defaults = read(&[("PGRST_JWT_JWKS_URL", ""), ("PGRST_JWT_JWKS_REFRESH", "0")]);
1271 assert_eq!(defaults.jwt_jwks_url, None);
1272 assert_eq!(defaults.jwt_jwks_refresh, 300);
1273 assert_eq!(
1274 read(&[("PGRST_JWT_JWKS_REFRESH", "soon")]).jwt_jwks_refresh,
1275 300
1276 );
1277 }
1278
1279 #[test]
1280 fn variables_can_come_from_somewhere_other_than_the_environment() {
1281 let config = AppConfig::try_from_vars([
1285 ("PGRST_DB_SCHEMAS", "api, extra"),
1286 ("PGRST_DB_ANON_ROLE", "web_anon"),
1287 ("PGRST_JWT_SECRET", "a-secret"),
1288 ("PGRST_DB_MAX_ROWS", "100"),
1289 ("PGRST_APP_SETTINGS_Tenant", "acme"),
1290 ])
1291 .unwrap();
1292
1293 assert_eq!(config.db_schemas, ["api", "extra"]);
1294 assert_eq!(config.db_anon_role.as_deref(), Some("web_anon"));
1295 assert_eq!(config.jwt_secret.as_deref(), Some("a-secret"));
1296 assert_eq!(config.db_max_rows, Some(100));
1297 assert_eq!(
1298 config.app_settings.get("tenant").map(String::as_str),
1299 Some("acme")
1300 );
1301
1302 let refused = AppConfig::try_from_vars([("PGRST_DB_AGGREGATES_ENABLED", "maybe")]);
1303 assert!(refused.is_err());
1304 }
1305
1306 #[test]
1307 fn app_settings_differing_only_in_case_take_the_later_value() {
1308 for _ in 0..16 {
1311 let config = AppConfig::try_from_vars([
1312 ("PGRST_APP_SETTINGS_Tenant", "first"),
1313 ("PGRST_APP_SETTINGS_TENANT", "second"),
1314 ("PGRST_APP_SETTINGS_tenant", "third"),
1315 ])
1316 .unwrap();
1317 assert_eq!(
1318 config.app_settings.get("tenant").map(String::as_str),
1319 Some("third")
1320 );
1321
1322 let config = AppConfig::try_from_vars([
1323 ("PGRST_APP_SETTINGS_tenant", "third"),
1324 ("PGRST_APP_SETTINGS_TENANT", "second"),
1325 ])
1326 .unwrap();
1327 assert_eq!(
1328 config.app_settings.get("tenant").map(String::as_str),
1329 Some("second")
1330 );
1331 }
1332 }
1333
1334 #[test]
1335 fn windows_environment_names_are_matched_whatever_their_case() {
1336 let config = AppConfig::try_from_vars([
1338 (upper_env_name("pgrst_db_schemas".into()), "api"),
1339 (upper_env_name("Pgrst_App_Settings_Tenant".into()), "acme"),
1340 ])
1341 .unwrap();
1342 assert_eq!(config.db_schemas, ["api"]);
1343 assert_eq!(
1344 config.app_settings.get("tenant").map(String::as_str),
1345 Some("acme")
1346 );
1347 assert_eq!(env_name("PGRST_DB_URI".into()), "PGRST_DB_URI");
1348 assert_eq!(
1349 env_name("pgrst_db_uri".into()) == "PGRST_DB_URI",
1350 cfg!(windows)
1351 );
1352 }
1353
1354 #[test]
1355 fn pool_size_accepts_postgrests_own_name() {
1356 assert_eq!(with_env(&[("PGRST_DB_POOL", "5")]).db_pool_size, 5);
1357 }
1358
1359 #[test]
1360 fn a_star_origin_means_any_origin() {
1361 assert!(with_env(&[("PGRST_SERVER_CORS_ORIGINS", "*")])
1364 .server_cors_origins
1365 .is_empty());
1366 assert!(with_env(&[("PGRST_SERVER_CORS_ORIGINS", " , ")])
1367 .server_cors_origins
1368 .is_empty());
1369 }
1370
1371 #[test]
1372 fn an_unusable_value_leaves_the_default() {
1373 let config = with_env(&[
1374 ("PGRST_DB_POOL_SIZE", "lots"),
1375 ("PGRST_DB_POOL_TIMEOUT", "0"),
1376 ("PGRST_MAX_BODY_SIZE", "-1"),
1377 ("PGRST_DB_TX_ISOLATION", "dirty read"),
1378 ("PGRST_LOG_LEVEL", "shout"),
1379 ]);
1380
1381 let d = AppConfig::default();
1382 assert_eq!(config.db_pool_size, d.db_pool_size);
1383 assert_eq!(config.db_pool_timeout, d.db_pool_timeout);
1384 assert_eq!(config.max_body_size, d.max_body_size);
1385 assert_eq!(config.db_tx_isolation, d.db_tx_isolation);
1386 assert_eq!(config.log_level, d.log_level);
1387 }
1388
1389 #[test]
1390 fn the_formerly_dead_fields_are_read() {
1391 let config = with_env(&[
1394 ("PGRST_DB_PREPARED_STATEMENTS", "false"),
1395 ("PGRST_DB_PRE_REQUEST", "auth.check"),
1396 ("PGRST_DB_CHANNEL", "my_channel"),
1397 ("PGRST_DB_CHANNEL_ENABLED", "yes"),
1398 ("PGRST_JWT_CACHE_ENABLED", "false"),
1399 ("PGRST_JWT_CACHE_MAX_LIFETIME", "60"),
1400 ("PGRST_SERVER_UNIX_SOCKET", "/tmp/postrust.sock"),
1401 ("PGRST_ADMIN_SERVER_PORT", "3001"),
1402 ("PGRST_OPENAPI_SERVER_PROXY_URI", "https://api.example.com"),
1403 ("PGRST_OPENAPI_MODE", "ignore-privileges"),
1404 ]);
1405
1406 assert!(!config.db_prepared_statements);
1407 assert_eq!(config.db_pre_request.as_deref(), Some("auth.check"));
1408 assert_eq!(config.db_channel, "my_channel");
1409 assert!(config.db_channel_enabled);
1410 assert!(!config.jwt_cache_enabled);
1411 assert_eq!(config.jwt_cache_max_lifetime, 60);
1412 assert_eq!(
1413 config.server_unix_socket.as_deref(),
1414 Some("/tmp/postrust.sock")
1415 );
1416 assert_eq!(config.admin_server_port, Some(3001));
1417 assert_eq!(
1418 config.openapi_server_proxy_uri.as_deref(),
1419 Some("https://api.example.com")
1420 );
1421 assert_eq!(config.openapi_mode, OpenApiMode::IgnorePrivileges);
1422 }
1423
1424 #[test]
1425 fn tracing_settings_are_read() {
1426 let d = AppConfig::default();
1427 assert_eq!(d.log_format, LogFormat::Text);
1428 assert!(!d.otel_db_statement);
1429
1430 let config = with_env(&[
1431 ("PGRST_LOG_FORMAT", "JSON"),
1432 ("PGRST_OTEL_DB_STATEMENT", "on"),
1433 ]);
1434 assert_eq!(config.log_format, LogFormat::Json);
1435 assert!(config.otel_db_statement);
1436
1437 assert_eq!(
1440 with_env(&[("PGRST_LOG_FORMAT", "yaml")]).log_format,
1441 LogFormat::Text
1442 );
1443 assert!(with_env_result(&[("PGRST_OTEL_DB_STATEMENT", "maybe")]).is_err());
1444 }
1445
1446 #[test]
1447 fn a_zero_jwt_cache_lifetime_is_kept_because_it_turns_the_cache_off() {
1448 assert_eq!(
1451 with_env(&[("PGRST_JWT_CACHE_MAX_LIFETIME", "0")]).jwt_cache_max_lifetime,
1452 0
1453 );
1454 }
1455
1456 #[test]
1457 fn app_settings_come_from_a_prefix() {
1458 let config = with_env(&[
1459 ("PGRST_APP_SETTINGS_JWT_LIFETIME", "3600"),
1460 ("PGRST_APP_SETTINGS_TenantName", "acme"),
1461 ]);
1462 assert_eq!(
1465 config.app_settings.get("jwt_lifetime").map(String::as_str),
1466 Some("3600")
1467 );
1468 assert_eq!(
1469 config.app_settings.get("tenantname").map(String::as_str),
1470 Some("acme")
1471 );
1472 }
1473
1474 #[test]
1475 fn role_settings_parse_from_json_using_the_documented_spellings() {
1476 let config = with_env(&[(
1477 "PGRST_ROLE_SETTINGS",
1478 r#"{"web_user":{"isolation_level":"serializable","statement_timeout":5000}}"#,
1479 )]);
1480 let web_user = config.role_settings.get("web_user").expect("role present");
1481 assert_eq!(web_user.isolation_level, Some(IsolationLevel::Serializable));
1482 assert_eq!(web_user.statement_timeout, Some(5000));
1483 }
1484
1485 #[test]
1486 fn unparseable_role_settings_leave_the_map_empty() {
1487 assert!(with_env(&[("PGRST_ROLE_SETTINGS", "{not json")])
1488 .role_settings
1489 .is_empty());
1490 assert!(with_env(&[(
1493 "PGRST_ROLE_SETTINGS",
1494 r#"{"r":{"isolation_level":"dirty"}}"#
1495 )])
1496 .role_settings
1497 .is_empty());
1498 }
1499
1500 #[test]
1501 fn openapi_mode_accepts_the_documented_spellings() {
1502 assert_eq!(
1503 OpenApiMode::parse_config("follow-privileges"),
1504 Some(OpenApiMode::FollowPrivileges)
1505 );
1506 assert_eq!(
1507 OpenApiMode::parse_config("IGNORE_PRIVILEGES"),
1508 Some(OpenApiMode::IgnorePrivileges)
1509 );
1510 assert_eq!(
1511 OpenApiMode::parse_config("disabled"),
1512 Some(OpenApiMode::Disabled)
1513 );
1514 assert_eq!(OpenApiMode::parse_config("security-definer"), None);
1517 }
1518
1519 #[test]
1520 fn the_config_enums_round_trip_through_json() {
1521 for level in [
1527 IsolationLevel::ReadCommitted,
1528 IsolationLevel::RepeatableRead,
1529 IsolationLevel::Serializable,
1530 ] {
1531 let json = serde_json::to_string(&level).unwrap();
1532 let back: IsolationLevel = serde_json::from_str(&json).unwrap();
1533 assert_eq!(back, level, "{json}");
1534 }
1535
1536 for mode in [
1537 OpenApiMode::Disabled,
1538 OpenApiMode::FollowPrivileges,
1539 OpenApiMode::IgnorePrivileges,
1540 ] {
1541 let json = serde_json::to_string(&mode).unwrap();
1542 assert_eq!(serde_json::from_str::<OpenApiMode>(&json).unwrap(), mode);
1543 }
1544
1545 for level in [
1546 LogLevel::Crit,
1547 LogLevel::Error,
1548 LogLevel::Warn,
1549 LogLevel::Info,
1550 LogLevel::Debug,
1551 ] {
1552 let json = serde_json::to_string(&level).unwrap();
1553 assert_eq!(serde_json::from_str::<LogLevel>(&json).unwrap(), level);
1554 }
1555 }
1556
1557 #[test]
1558 fn a_whole_config_round_trips_through_json() {
1559 let mut config = AppConfig::default();
1561 config.role_settings.insert(
1562 "web_user".to_string(),
1563 RoleSettings {
1564 isolation_level: Some(IsolationLevel::RepeatableRead),
1565 statement_timeout: Some(5000),
1566 },
1567 );
1568
1569 let json = serde_json::to_string(&config).unwrap();
1570 let back: AppConfig = serde_json::from_str(&json).expect("config should round trip");
1571
1572 assert_eq!(back.db_tx_isolation, config.db_tx_isolation);
1573 assert_eq!(back.log_level, config.log_level);
1574 assert_eq!(back.openapi_mode, config.openapi_mode);
1575 assert_eq!(
1576 back.role_settings["web_user"].isolation_level,
1577 Some(IsolationLevel::RepeatableRead)
1578 );
1579 }
1580
1581 #[test]
1582 fn the_serialized_spelling_is_the_configuration_spelling() {
1583 assert_eq!(
1586 serde_json::to_string(&IsolationLevel::ReadCommitted).unwrap(),
1587 "\"read committed\""
1588 );
1589 assert_eq!(
1590 serde_json::to_string(&OpenApiMode::FollowPrivileges).unwrap(),
1591 "\"follow-privileges\""
1592 );
1593 assert_eq!(serde_json::to_string(&LogLevel::Warn).unwrap(), "\"warn\"");
1594 }
1595
1596 #[test]
1597 fn an_empty_role_claim_key_is_not_accepted() {
1598 assert_eq!(
1601 with_env(&[("PGRST_JWT_ROLE_CLAIM_KEY", " ")]).jwt_role_claim_key,
1602 AppConfig::default().jwt_role_claim_key
1603 );
1604 }
1605}