From 083cb8b9ec0e729604d5f7ce56a7e9528bb67d10 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Mon, 6 Apr 2026 00:41:00 +0200 Subject: [PATCH] feat: add CAMELEER_CORS_ALLOWED_ORIGINS for multi-origin CORS support Behind a reverse proxy the browser sends Origin matching the proxy's public URL, which the single-origin CAMELEER_UI_ORIGIN rejects. New env var accepts comma-separated origins and takes priority over UI_ORIGIN, which remains as a backwards-compatible fallback. Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 2 +- HOWTO.md | 1 + .../server/app/security/SecurityConfig.java | 12 ++++++++---- .../server/app/security/SecurityProperties.java | 3 +++ .../src/main/resources/application.yml | 1 + docs/SERVER-CAPABILITIES.md | 3 ++- 6 files changed, 16 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 43a722ca..70a7527e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,7 +41,7 @@ java -jar cameleer3-server-app/target/cameleer3-server-app-1.0-SNAPSHOT.jar - Multi-tenancy: each server instance serves one tenant (configured via `CAMELEER_TENANT_ID`, default: `"default"`). Environments (dev/staging/prod) are first-class — agents send `environmentId` at registration and in heartbeats. JWT carries `env` claim for environment persistence across token refresh. PostgreSQL isolated via schema-per-tenant (`?currentSchema=tenant_{id}`). ClickHouse shared DB with `tenant_id` + `environment` columns, partitioned by `(tenant_id, toYYYYMM(timestamp))`. - Storage: PostgreSQL for RBAC, config, and audit; ClickHouse for all observability data (executions, search, logs, metrics, stats, diagrams). ClickHouse schema migrations in `clickhouse/*.sql`, run idempotently on startup by `ClickHouseSchemaInitializer`. Use `IF NOT EXISTS` for CREATE and ADD PROJECTION. - Logging: ClickHouse JDBC set to INFO (`com.clickhouse`), HTTP client to WARN (`org.apache.hc.client5`) in application.yml -- Security: JWT auth with RBAC (AGENT/VIEWER/OPERATOR/ADMIN roles), Ed25519 config signing (key derived deterministically from JWT secret via HMAC-SHA256), bootstrap token for registration +- Security: JWT auth with RBAC (AGENT/VIEWER/OPERATOR/ADMIN roles), Ed25519 config signing (key derived deterministically from JWT secret via HMAC-SHA256), bootstrap token for registration. CORS: `CAMELEER_CORS_ALLOWED_ORIGINS` (comma-separated) overrides `CAMELEER_UI_ORIGIN` for multi-origin setups (e.g., reverse proxy). - OIDC: Optional external identity provider support (token exchange pattern). Configured via admin API, stored in database (`server_config` table). Resource server mode: accepts external access tokens (Logto M2M) via JWKS validation when `CAMELEER_OIDC_ISSUER_URI` is set. `CAMELEER_OIDC_JWK_SET_URI` overrides JWKS discovery for container networking. `CAMELEER_OIDC_TLS_SKIP_VERIFY=true` disables TLS cert verification for OIDC calls (self-signed CAs). Scope-based role mapping: `admin`/`operator`/`viewer` scopes map to RBAC roles. - User persistence: PostgreSQL `users` table, admin CRUD at `/api/v1/admin/users` - Usage analytics: ClickHouse `usage_events` table tracks authenticated UI requests, flushed every 5s diff --git a/HOWTO.md b/HOWTO.md index 1eaa91be..e2515f67 100644 --- a/HOWTO.md +++ b/HOWTO.md @@ -389,6 +389,7 @@ Key settings in `cameleer3-server-app/src/main/resources/application.yml`: | `security.ui-user` | `admin` | UI login username (`CAMELEER_UI_USER` env var) | | `security.ui-password` | `admin` | UI login password (`CAMELEER_UI_PASSWORD` env var) | | `security.ui-origin` | `http://localhost:5173` | CORS allowed origin for UI (`CAMELEER_UI_ORIGIN` env var) | +| `security.cors-allowed-origins` | *(empty)* | Comma-separated CORS origins (`CAMELEER_CORS_ALLOWED_ORIGINS`) — overrides `ui-origin` when set | | `security.jwt-secret` | *(random)* | HMAC secret for JWT signing (`CAMELEER_JWT_SECRET`). If set, tokens survive restarts | | `security.oidc.enabled` | `false` | Enable OIDC login (`CAMELEER_OIDC_ENABLED`) | | `security.oidc.issuer-uri` | | OIDC provider issuer URL (`CAMELEER_OIDC_ISSUER`) | diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java index 00954b14..600ecf2a 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java @@ -41,6 +41,7 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.io.InputStream; import java.net.URI; import java.net.URL; +import java.util.Arrays; import java.util.List; import java.util.Set; @@ -217,11 +218,14 @@ public class SecurityConfig { @Bean public CorsConfigurationSource corsConfigurationSource(SecurityProperties properties) { CorsConfiguration config = new CorsConfiguration(); - String origin = properties.getUiOrigin(); - if (origin != null && !origin.isBlank()) { - config.setAllowedOrigins(List.of(origin)); + String corsOrigins = properties.getCorsAllowedOrigins(); + if (corsOrigins != null && !corsOrigins.isBlank()) { + config.setAllowedOrigins(Arrays.stream(corsOrigins.split(",")) + .map(String::trim).filter(s -> !s.isEmpty()).toList()); } else { - config.setAllowedOrigins(List.of("http://localhost:5173")); + String origin = properties.getUiOrigin(); + config.setAllowedOrigins(List.of( + origin != null && !origin.isBlank() ? origin : "http://localhost:5173")); } config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); config.setAllowedHeaders(List.of("*")); diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityProperties.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityProperties.java index 7a9d6e98..79cf970b 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityProperties.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityProperties.java @@ -21,6 +21,7 @@ public class SecurityProperties { private String oidcJwkSetUri; private String oidcAudience; private boolean oidcTlsSkipVerify; + private String corsAllowedOrigins; public long getAccessTokenExpiryMs() { return accessTokenExpiryMs; } public void setAccessTokenExpiryMs(long accessTokenExpiryMs) { this.accessTokenExpiryMs = accessTokenExpiryMs; } @@ -46,4 +47,6 @@ public class SecurityProperties { public void setOidcAudience(String oidcAudience) { this.oidcAudience = oidcAudience; } public boolean isOidcTlsSkipVerify() { return oidcTlsSkipVerify; } public void setOidcTlsSkipVerify(boolean oidcTlsSkipVerify) { this.oidcTlsSkipVerify = oidcTlsSkipVerify; } + public String getCorsAllowedOrigins() { return corsAllowedOrigins; } + public void setCorsAllowedOrigins(String corsAllowedOrigins) { this.corsAllowedOrigins = corsAllowedOrigins; } } diff --git a/cameleer3-server-app/src/main/resources/application.yml b/cameleer3-server-app/src/main/resources/application.yml index ae313df9..f4c1c28d 100644 --- a/cameleer3-server-app/src/main/resources/application.yml +++ b/cameleer3-server-app/src/main/resources/application.yml @@ -54,6 +54,7 @@ security: oidc-jwk-set-uri: ${CAMELEER_OIDC_JWK_SET_URI:} oidc-audience: ${CAMELEER_OIDC_AUDIENCE:} oidc-tls-skip-verify: ${CAMELEER_OIDC_TLS_SKIP_VERIFY:false} + cors-allowed-origins: ${CAMELEER_CORS_ALLOWED_ORIGINS:} springdoc: diff --git a/docs/SERVER-CAPABILITIES.md b/docs/SERVER-CAPABILITIES.md index e7c708eb..c094eb57 100644 --- a/docs/SERVER-CAPABILITIES.md +++ b/docs/SERVER-CAPABILITIES.md @@ -383,7 +383,8 @@ Registry: `gitea.siegeln.net/cameleer/cameleer3-server` | `CAMELEER_TENANT_ID` | No | `default` | Tenant identifier | | `CAMELEER_UI_USER` | No | `admin` | Default admin username | | `CAMELEER_UI_PASSWORD` | No | `admin` | Default admin password | -| `CAMELEER_UI_ORIGIN` | No | `http://localhost:5173` | CORS allowed origin | +| `CAMELEER_UI_ORIGIN` | No | `http://localhost:5173` | CORS allowed origin (single, legacy) | +| `CAMELEER_CORS_ALLOWED_ORIGINS` | No | (empty) | Comma-separated CORS origins — overrides `UI_ORIGIN` when set | | `CLICKHOUSE_URL` | No | `jdbc:clickhouse://localhost:8123/cameleer` | ClickHouse JDBC URL | | `CLICKHOUSE_USERNAME` | No | `default` | ClickHouse user | | `CLICKHOUSE_PASSWORD` | No | (empty) | ClickHouse password |