Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gateway/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
//! [`crate::module::ServiceHealthError`]
//! * [`services`] — concrete [`crate::module::ServiceModule`] implementations
//! with typed DTOs
//! * [`settings`] — environment-based configuration with redacted [`Debug`]
//! * [`settings`] — secret/auth environment loading plus typed runtime configuration with redacted [`Debug`]
//! and `--dev-keys` ephemeral keypair generation
//! * [`sse`] — Server-Sent Events via [`tokio::sync::broadcast::Sender`]
//! * [`openapi`] — `OpenAPI` schema generation via `utoipa`
Expand Down
12 changes: 4 additions & 8 deletions gateway/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ async fn main() -> anyhow::Result<()> {
let proxy_upstream_url = cfg.gateway.proxy_upstream_url.clone();
let refresh_token_ttl_secs = i64::try_from(cfg.gateway.refresh_token_ttl_secs)
.context("gateway.refresh_token_ttl_secs exceeds i64::MAX")?;
let access_token_ttl_secs = i64::try_from(cfg.gateway.access_token_ttl_secs)
.context("gateway.access_token_ttl_secs exceeds i64::MAX")?;

#[cfg(feature = "otel")]
let provider =
Expand All @@ -40,15 +38,13 @@ async fn main() -> anyhow::Result<()> {
)
.init();

let mut settings = if dev_keys {
let settings = if dev_keys {
gateway_example::settings::Settings::load_dev_keys_from_env()?
} else {
gateway_example::settings::Settings::load()?
};
settings.access_token_ttl_secs = access_token_ttl_secs;
settings.allowed_origins = Arc::from(cfg.gateway.cors.allowed_origins.as_str());
settings.sse_broadcast_buffer = cfg.gateway.sse_broadcast_buffer;
settings.session.cookie_secure = cfg.gateway.session.cookie_secure;
}
.apply_runtime_config(&cfg.gateway)?;
let access_token_ttl_secs = settings.access_token_ttl_secs;

let db_pool = create_db_pool().await?;
run_migrations(&db_pool)
Expand Down
78 changes: 27 additions & 51 deletions gateway/src/settings.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
//! Shared gateway settings.
//!
//! Secret/auth material still comes from deployment environment variables.
//! Non-secret runtime settings are overwritten from `rwf_config::Config` by
//! production `main`, keeping the typed config tree authoritative.
//! Secret/auth material comes from deployment environment variables. Non-secret
//! runtime settings are applied explicitly from `rwf_config::GatewayConfig`.

use std::str::FromStr;
use std::sync::Arc;
Expand All @@ -16,8 +15,6 @@ use crate::pem::{ed25519_spki_der, pem_encode};
pub const JWT_ISS: &str = "gateway-example";
pub const JWT_AUD: &str = "gateway-example-api";
pub const DEFAULT_ADMIN_USER_ID: &str = "00000000-0000-0000-0000-000000000001";
pub const DEFAULT_ALLOWED_ORIGINS: &str =
"http://localhost:3000,http://localhost:3001,http://localhost:3002";

#[must_use]
fn short_fingerprint(bytes: &[u8]) -> String {
Expand All @@ -29,44 +26,6 @@ fn short_fingerprint(bytes: &[u8]) -> String {
format!("{h:016x}")
}

fn bool_env(name: &str, default: bool) -> anyhow::Result<bool> {
match std::env::var(name) {
Ok(value) if value.eq_ignore_ascii_case("true") || value == "1" => Ok(true),
Ok(value) if value.eq_ignore_ascii_case("false") || value == "0" => Ok(false),
Ok(value) => anyhow::bail!("{name} must be true/false or 1/0, got {value:?}"),
Err(std::env::VarError::NotPresent) => Ok(default),
Err(e) => Err(e).with_context(|| format!("failed to read {name}")),
}
}

fn positive_i64_env(name: &str, default: i64) -> anyhow::Result<i64> {
let value = match std::env::var(name) {
Ok(raw) => raw
.parse::<i64>()
.with_context(|| format!("{name} must be an integer, got {raw:?}"))?,
Err(std::env::VarError::NotPresent) => default,
Err(e) => return Err(e).with_context(|| format!("failed to read {name}")),
};
if value <= 0 {
anyhow::bail!("{name} must be positive, got {value}");
}
Ok(value)
}

fn positive_usize_env(name: &str, default: usize) -> anyhow::Result<usize> {
let value = match std::env::var(name) {
Ok(raw) => raw
.parse::<usize>()
.with_context(|| format!("{name} must be a positive integer, got {raw:?}"))?,
Err(std::env::VarError::NotPresent) => default,
Err(e) => return Err(e).with_context(|| format!("failed to read {name}")),
};
if value == 0 {
anyhow::bail!("{name} must be greater than zero");
}
Ok(value)
}

#[derive(Debug, Clone)]
pub struct SessionSettings {
pub cookie_secure: bool,
Expand Down Expand Up @@ -116,8 +75,24 @@ impl std::fmt::Debug for Settings {
}

impl Settings {
/// Load secrets plus legacy environment fallbacks. Production `main`
/// replaces non-secret values with the already-validated typed config.
/// Apply the canonical typed runtime configuration.
///
/// # Errors
/// Returns an error if a validated integer cannot be represented by the
/// gateway's signed duration fields.
pub fn apply_runtime_config(
mut self,
config: &rwf_config::GatewayConfig,
) -> Result<Self, anyhow::Error> {
self.access_token_ttl_secs = i64::try_from(config.access_token_ttl_secs)
.context("gateway.access_token_ttl_secs exceeds i64::MAX")?;
self.allowed_origins = Arc::from(config.cors.allowed_origins.as_str());
self.sse_broadcast_buffer = config.sse_broadcast_buffer;
self.session.cookie_secure = config.session.cookie_secure;
Ok(self)
}

/// Load secret/auth deployment settings.
///
/// # Errors
/// Returns an error for missing/invalid keys, credentials, IDs or values.
Expand Down Expand Up @@ -154,26 +129,25 @@ impl Settings {
.with_context(|| format!("invalid ADMIN_USER_ID {admin_user_id_raw:?}"))?;

let session = SessionSettings {
cookie_secure: bool_env("SESSION_COOKIE_SECURE", true)?,
cookie_name: std::env::var("SESSION_COOKIE_NAME")
.unwrap_or_else(|_| "rwf_session".to_string()),
csrf_cookie_name: std::env::var("CSRF_COOKIE_NAME")
.unwrap_or_else(|_| "rwf_csrf".to_string()),
..SessionSettings::default()
};

Ok(Self {
jwt_private_key_pem: Arc::from(jwt_private_key_pem.as_str()),
jwt_public_key_pem: Arc::from(jwt_public_key_pem.as_str()),
encoding_key,
decoding_key,
access_token_ttl_secs: positive_i64_env("ACCESS_TOKEN_TTL_SECS", 15 * 60)?,
access_token_ttl_secs: 15 * 60,
admin_user_id,
default_admin_password: Arc::from(default_admin_password.as_str()),
allowed_origins: Arc::from(
std::env::var("ALLOWED_ORIGINS")
.unwrap_or_else(|_| DEFAULT_ALLOWED_ORIGINS.to_string()),
"http://localhost:3000,http://localhost:3001,http://localhost:3002",
),
sse_broadcast_buffer: positive_usize_env("SSE_BROADCAST_BUFFER", 256)?,
sse_broadcast_buffer: 256,
session,
})
}
Expand Down Expand Up @@ -224,7 +198,9 @@ impl Settings {
access_token_ttl_secs: 15 * 60,
admin_user_id,
default_admin_password: Arc::from(admin_password),
allowed_origins: Arc::from(DEFAULT_ALLOWED_ORIGINS),
allowed_origins: Arc::from(
"http://localhost:3000,http://localhost:3001,http://localhost:3002",
),
sse_broadcast_buffer: 256,
session: SessionSettings {
cookie_secure: false,
Expand Down
Loading