From 4a50b780a6093b86dee8daf1a173ae6c6d41e297 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Fri, 28 Nov 2025 00:37:44 -0800 Subject: [PATCH] KL-1 Configurable CORS support --- bin/core/src/config.rs | 38 ++++++++++++++++++++++ bin/core/src/main.rs | 12 ++----- client/core/rs/src/entities/config/core.rs | 24 ++++++++++++++ config/core.config.toml | 17 ++++++++++ 4 files changed, 81 insertions(+), 10 deletions(-) diff --git a/bin/core/src/config.rs b/bin/core/src/config.rs index dfa152afe..94355e276 100644 --- a/bin/core/src/config.rs +++ b/bin/core/src/config.rs @@ -1,6 +1,7 @@ use std::{path::PathBuf, sync::OnceLock}; use anyhow::Context; +use axum::http::HeaderValue; use colored::Colorize; use config::ConfigLoader; use environment_file::{ @@ -14,6 +15,7 @@ use komodo_client::entities::{ logger::LogConfig, }; use noise::key::{RotatableKeyPair, SpkiPublicKey}; +use tower_http::cors::CorsLayer; /// Should call in startup to ensure Core errors without valid private key. pub fn core_keys() -> &'static RotatableKeyPair { @@ -89,6 +91,36 @@ pub fn periphery_public_keys() -> Option<&'static [SpkiPublicKey]> { .as_deref() } +/// Creates a CORS layer based on the Core configuration. +/// +/// - If `cors_allowed_origins` is empty: Allows all origins (backward compatibility) +/// - If `cors_allowed_origins` is set: Only allows the specified origins +/// - Methods and headers are always allowed (Any) +/// - Credentials are only allowed if `cors_allow_credentials` is true +pub fn cors_layer() -> CorsLayer { + let config = core_config(); + let allowed_origins = if config.cors_allowed_origins.is_empty() { + vec![HeaderValue::from_static("*")] + } else { + config + .cors_allowed_origins + .iter() + .filter_map(|origin| { + HeaderValue::from_str(origin) + .inspect_err(|e| { + warn!("Invalid CORS allowed origin: {origin} | {e:?}") + }) + .ok() + }) + .collect() + }; + CorsLayer::new() + .allow_origin(allowed_origins) + .allow_methods(tower_http::cors::Any) + .allow_headers(tower_http::cors::Any) + .allow_credentials(config.cors_allow_credentials) +} + pub fn core_config() -> &'static CoreConfig { static CORE_CONFIG: OnceLock = OnceLock::new(); CORE_CONFIG.get_or_init(|| { @@ -281,6 +313,12 @@ pub fn core_config() -> &'static CoreConfig { .komodo_frontend_path .unwrap_or(config.frontend_path), jwt_ttl: env.komodo_jwt_ttl.unwrap_or(config.jwt_ttl), + cors_allowed_origins: env + .komodo_cors_allowed_origins + .unwrap_or(config.cors_allowed_origins), + cors_allow_credentials: env + .komodo_cors_allow_credentials + .unwrap_or(config.cors_allow_credentials), sync_directory: env .komodo_sync_directory .unwrap_or(config.sync_directory), diff --git a/bin/core/src/main.rs b/bin/core/src/main.rs index 16d57e9d7..b118b6a1f 100644 --- a/bin/core/src/main.rs +++ b/bin/core/src/main.rs @@ -8,10 +8,7 @@ use std::{net::SocketAddr, str::FromStr}; use anyhow::Context; use axum::{Router, routing::get}; use axum_server::{Handle, tls_rustls::RustlsConfig}; -use tower_http::{ - cors::{Any, CorsLayer}, - services::{ServeDir, ServeFile}, -}; +use tower_http::services::{ServeDir, ServeFile}; use tracing::Instrument; use crate::config::{core_config, core_keys}; @@ -108,12 +105,7 @@ async fn app() -> anyhow::Result<()> { .nest("/ws", ws::router()) .nest("/client", ts_client::router()) .fallback_service(serve_frontend) - .layer( - CorsLayer::new() - .allow_origin(Any) - .allow_methods(Any) - .allow_headers(Any), - ) + .layer(config::cors_layer()) .into_make_service(); let addr = diff --git a/client/core/rs/src/entities/config/core.rs b/client/core/rs/src/entities/config/core.rs index 8bc9ff610..11ae8fdfb 100644 --- a/client/core/rs/src/entities/config/core.rs +++ b/client/core/rs/src/entities/config/core.rs @@ -218,6 +218,11 @@ pub struct Env { /// Override `github_oauth.secret` from file pub komodo_github_oauth_secret_file: Option, + /// Override `cors_allowed_origins` + pub komodo_cors_allowed_origins: Option>, + /// Override `cors_allow_credentials` + pub komodo_cors_allow_credentials: Option, + /// Override `database.uri` #[serde(alias = "komodo_mongo_uri")] pub komodo_database_uri: Option, @@ -511,6 +516,21 @@ pub struct CoreConfig { #[serde(default)] pub github_oauth: OauthCredentials, + // ======= + // = CORS = + // ======= + /// List of CORS allowed origins. + /// If empty, allows all origins (`*`). + /// Production setups should configure this explicitly. + /// Example: `["https://komodo.example.com", "https://app.example.com"]`. + #[serde(default)] + pub cors_allowed_origins: Vec, + + /// Tell CORS to allow credentials in requests. + /// Used if needed for authentication proxy. + #[serde(default)] + pub cors_allow_credentials: bool, + // ============ // = Webhooks = // ============ @@ -757,6 +777,8 @@ impl Default for CoreConfig { oidc_additional_audiences: Default::default(), google_oauth: Default::default(), github_oauth: Default::default(), + cors_allowed_origins: Default::default(), + cors_allow_credentials: Default::default(), webhook_secret: Default::default(), webhook_base_url: Default::default(), logging: Default::default(), @@ -853,6 +875,8 @@ impl CoreConfig { id: empty_or_redacted(&config.github_oauth.id), secret: empty_or_redacted(&config.github_oauth.id), }, + cors_allowed_origins: config.cors_allowed_origins, + cors_allow_credentials: config.cors_allow_credentials, webhook_secret: empty_or_redacted(&config.webhook_secret), webhook_base_url: config.webhook_base_url, database: config.database.sanitized(), diff --git a/config/core.config.toml b/config/core.config.toml index 903298da2..35d98380a 100644 --- a/config/core.config.toml +++ b/config/core.config.toml @@ -294,6 +294,23 @@ github_oauth.id = "" ## Required if github_oauth is enabled. github_oauth.secret = "" +######## +# CORS # +######## + +## Specifically set list of CORS allowed origins. +## If empty, allows all origins (`*`). +## Production setups should configure this explicitly. +## Env: KOMODO_CORS_ALLOWED_ORIGINS +## Default: empty +cors_allowed_origins = ["*"] + +## Tell CORS to allow credentials in requests. +## Set true only if needed for authentication proxy. +## Env: KOMODO_CORS_ALLOW_CREDENTIALS +## Default: false +cors_allow_credentials = false + ################## # POLL INTERVALS # ##################