diff --git a/CLAUDE.md b/CLAUDE.md index 1a222ac9..528bfcf8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -165,7 +165,7 @@ java -jar cameleer3-server-app/target/cameleer3-server-app-1.0-SNAPSHOT.jar - Communication: receives HTTP POST data from agents (executions, diagrams, metrics, logs), serves SSE event streams for config push/commands (config-update, deep-trace, replay, route-control) - Environment filtering: all data queries (exchanges, dashboard stats, route metrics, agent events, correlation) filter by the selected environment. All commands (config-update, route-control, set-traced-processors, replay) target only agents in the selected environment when one is selected. `AgentRegistryService.findByApplicationAndEnvironment()` for environment-scoped command dispatch. Backend endpoints accept optional `environment` query parameter; null = all environments (backward compatible). - Maintains agent instance registry (in-memory) with states: LIVE -> STALE -> DEAD. Auto-heals from JWT `env` claim + heartbeat body on heartbeat/SSE after server restart (priority: heartbeat `environmentId` > JWT `env` claim > `"default"`). Capabilities and route states updated on every heartbeat (protocol v2). Route catalog falls back to ClickHouse stats for route discovery when registry has incomplete data. -- Multi-tenancy: each server instance serves one tenant (configured via `CAMELEER_SERVER_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))`. +- Multi-tenancy: each server instance serves one tenant (configured via `CAMELEER_SERVER_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}`) and `ApplicationName=tenant_{id}` on the JDBC URL for `pg_stat_activity` scoping — the `DatabaseAdminController` filters active queries and kill-query by `application_name` so tenants sharing a PG instance cannot see each other's queries. The installer must include both `currentSchema` and `ApplicationName` in `SPRING_DATASOURCE_URL`. 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. CORS: `CAMELEER_SERVER_SECURITY_CORSALLOWEDORIGINS` (comma-separated) overrides `CAMELEER_SERVER_SECURITY_UIORIGIN` for multi-origin setups (e.g., reverse proxy). Infrastructure access: `CAMELEER_SERVER_SECURITY_INFRASTRUCTUREENDPOINTS=false` disables Database and ClickHouse admin endpoints (set by SaaS provisioner on tenant servers). Health endpoint exposes the flag for UI tab visibility. UI role gating: Admin sidebar/routes hidden for non-ADMIN; diagram toolbar and route control hidden for VIEWER. Read-only for VIEWER, editable for OPERATOR+. Role helpers: `useIsAdmin()`, `useCanControl()` in `auth-store.ts`. Route guard: `RequireAdmin` in `auth/RequireAdmin.tsx`. Last-ADMIN guard: system prevents removal of the last ADMIN role (409 Conflict on role removal, user deletion, group role removal). Password policy: min 12 chars, 3-of-4 character classes, no username match (enforced on user creation and admin password reset). Brute-force protection: 5 failed attempts -> 15 min lockout (tracked via `failed_login_attempts` / `locked_until` on users table). Token revocation: `token_revoked_before` column on users, checked in `JwtAuthenticationFilter`, set on password change. @@ -397,7 +397,7 @@ Mean processing time = `camel.route.policy.total_time / camel.route.policy.count # GitNexus — Code Intelligence -This project is indexed by GitNexus as **cameleer3-server** (6298 symbols, 15882 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **cameleer3-server** (6295 symbols, 15883 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/DatabaseAdminController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/DatabaseAdminController.java index c432199f..b8b3eec8 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/DatabaseAdminController.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/DatabaseAdminController.java @@ -102,6 +102,7 @@ public class DatabaseAdminController { state, query FROM pg_stat_activity WHERE state != 'idle' AND pid != pg_backend_pid() AND datname = current_database() + AND application_name = current_setting('application_name') ORDER BY query_start ASC """, (rs, row) -> new ActiveQueryResponse( rs.getInt("pid"), rs.getDouble("duration_seconds"), @@ -113,7 +114,7 @@ public class DatabaseAdminController { @Operation(summary = "Terminate a query by PID") public ResponseEntity killQuery(@PathVariable int pid, HttpServletRequest request) { var exists = jdbc.queryForObject( - "SELECT EXISTS(SELECT 1 FROM pg_stat_activity WHERE pid = ? AND pid != pg_backend_pid())", + "SELECT EXISTS(SELECT 1 FROM pg_stat_activity WHERE pid = ? AND pid != pg_backend_pid() AND application_name = current_setting('application_name'))", Boolean.class, pid); if (!Boolean.TRUE.equals(exists)) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, "No active query with PID " + pid); diff --git a/cameleer3-server-app/src/main/resources/application.yml b/cameleer3-server-app/src/main/resources/application.yml index 3aa6d335..a7b2ef5b 100644 --- a/cameleer3-server-app/src/main/resources/application.yml +++ b/cameleer3-server-app/src/main/resources/application.yml @@ -7,7 +7,7 @@ spring: max-file-size: 200MB max-request-size: 200MB datasource: - url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/cameleer3?currentSchema=tenant_${cameleer.server.tenant.id}} + url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/cameleer3?currentSchema=tenant_${cameleer.server.tenant.id}&ApplicationName=tenant_${cameleer.server.tenant.id}} username: ${SPRING_DATASOURCE_USERNAME:cameleer} password: ${SPRING_DATASOURCE_PASSWORD:cameleer_dev} driver-class-name: org.postgresql.Driver diff --git a/deploy/overlays/feature/kustomization.yaml b/deploy/overlays/feature/kustomization.yaml index 8ecf91cc..4b88fd13 100644 --- a/deploy/overlays/feature/kustomization.yaml +++ b/deploy/overlays/feature/kustomization.yaml @@ -24,7 +24,7 @@ patches: - name: server env: - name: SPRING_DATASOURCE_URL - value: "jdbc:postgresql://cameleer-postgres.cameleer.svc.cluster.local:5432/cameleer3?currentSchema=BRANCH_SCHEMA" + value: "jdbc:postgresql://cameleer-postgres.cameleer.svc.cluster.local:5432/cameleer3?currentSchema=BRANCH_SCHEMA&ApplicationName=BRANCH_SCHEMA" - name: CAMELEER_SERVER_SECURITY_UIORIGIN value: "http://BRANCH_SLUG.cameleer.siegeln.net" # UI ConfigMap: branch-specific API URL diff --git a/deploy/overlays/main/kustomization.yaml b/deploy/overlays/main/kustomization.yaml index 1c0e9898..6cb8a93d 100644 --- a/deploy/overlays/main/kustomization.yaml +++ b/deploy/overlays/main/kustomization.yaml @@ -41,7 +41,7 @@ patches: - name: CAMELEER_SERVER_SECURITY_UIORIGIN value: "http://192.168.50.86:30090" - name: SPRING_DATASOURCE_URL - value: "jdbc:postgresql://cameleer-postgres:5432/cameleer3?currentSchema=public" + value: "jdbc:postgresql://cameleer-postgres:5432/cameleer3?currentSchema=public&ApplicationName=tenant_default" # UI ConfigMap: production API URL - target: kind: ConfigMap