feat: add CAMELEER_CORS_ALLOWED_ORIGINS for multi-origin CORS support
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m7s
CI / docker (push) Successful in 41s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s

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) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-06 00:41:00 +02:00
parent 0609220cdf
commit 083cb8b9ec
6 changed files with 16 additions and 6 deletions

View File

@@ -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

View File

@@ -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`) |

View File

@@ -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("*"));

View File

@@ -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; }
}

View File

@@ -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:

View File

@@ -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 |