diff --git a/CLAUDE.md b/CLAUDE.md index f526d6da..141878ef 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,8 +41,8 @@ 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. CORS: `CAMELEER_CORS_ALLOWED_ORIGINS` (comma-separated) overrides `CAMELEER_UI_ORIGIN` for multi-origin setups (e.g., reverse proxy). UI role gating: Admin sidebar/routes hidden for non-ADMIN; diagram toolbar and route control hidden for VIEWER; Config is a main tab (`/config` all apps, `/config/:appId` single app with detail; sidebar clicks stay on config, route clicks resolve to parent app). Read-only for VIEWER, editable for OPERATOR+. Role helpers: `useIsAdmin()`, `useCanControl()` in `auth-store.ts`. Route guard: `RequireAdmin` in `auth/RequireAdmin.tsx`. -- OIDC: Optional external identity provider support (token exchange pattern). Configured via admin API/UI, stored in database (`server_config` table). Configurable `userIdClaim` (default `sub`) determines which id_token claim is used as the user identifier. 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 (case-insensitive): `admin`/`server:admin` → ADMIN, `operator`/`server:operator` → OPERATOR, `viewer`/`server:viewer` → VIEWER. SSO: when OIDC enabled, UI auto-redirects to provider with `prompt=none` for silent sign-in; falls back to `/login?local` on `login_required`, retries without `prompt=none` on `consent_required`. Auto-signup provisions new OIDC users with default roles. System roles synced on every OIDC login (revocations propagate on next login); group memberships are never touched. Supports ES384, ES256, RS256. +- 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). UI role gating: Admin sidebar/routes hidden for non-ADMIN; diagram toolbar and route control hidden for VIEWER; Config is a main tab (`/config` all apps, `/config/:appId` single app with detail; sidebar clicks stay on config, route clicks resolve to parent app). Read-only for VIEWER, editable for OPERATOR+. Role helpers: `useIsAdmin()`, `useCanControl()` in `auth-store.ts`. Route guard: `RequireAdmin` in `auth/RequireAdmin.tsx`. PKCE (S256) enabled on all OIDC authorization requests. +- OIDC: Optional external identity provider support (token exchange pattern). Configured via admin API/UI, stored in database (`server_config` table). Configurable `userIdClaim` (default `sub`) determines which id_token claim is used as the user identifier. 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 via `SystemRole.normalizeScope()` (case-insensitive, strips `server:` prefix): `admin`/`server:admin` → ADMIN, `operator`/`server:operator` → OPERATOR, `viewer`/`server:viewer` → VIEWER. SSO: when OIDC enabled, UI auto-redirects to provider with `prompt=none` + PKCE (S256) for silent sign-in; falls back to `/login?local` on `login_required`, retries without `prompt=none` on `consent_required`. Logout always redirects to `/login?local` (via OIDC end_session or direct fallback) to prevent SSO re-login loops. Auto-signup provisions new OIDC users with default roles. System roles synced on every OIDC login (revocations propagate on next login); group memberships are never touched. Supports ES384, ES256, RS256. Shared OIDC logic in `OidcProviderHelper` (discovery, JWK source, algorithm set). - 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 @@ -57,12 +57,17 @@ java -jar cameleer3-server-app/target/cameleer3-server-app-1.0-SNAPSHOT.jar - Deployment target: k3s at 192.168.50.86, namespace `cameleer` (main), `cam-` (feature branches) - Feature branches: isolated namespace, PG schema; Traefik Ingress at `-api.cameleer.siegeln.net` - Secrets managed in CI deploy step (idempotent `--dry-run=client | kubectl apply`): `cameleer-auth`, `postgres-credentials`, `clickhouse-credentials` -- K8s probes: server uses `/api/v1/health`, PostgreSQL uses `pg_isready` +- K8s probes: server uses `/api/v1/health`, PostgreSQL uses `pg_isready -U "$POSTGRES_USER"` (env var, not hardcoded) +- K8s security: server and database pods run with `securityContext.runAsNonRoot`. UI (nginx) runs without securityContext (needs root for entrypoint setup). +- Docker: server Dockerfile has no default credentials — all DB config comes from env vars at runtime - Docker build uses buildx registry cache + `--provenance=false` for Gitea compatibility +- CI: branch slug sanitization extracted to `.gitea/sanitize-branch.sh`, sourced by docker and deploy-feature jobs ## UI Styling - Always use `@cameleer/design-system` CSS variables for colors (`var(--amber)`, `var(--error)`, `var(--success)`, etc.) — never hardcode hex values. This applies to CSS modules, inline styles, and SVG `fill`/`stroke` attributes. SVG presentation attributes resolve `var()` correctly. +- Brand assets: `@cameleer/design-system/assets/` provides `camel-logo.svg` (currentColor), `cameleer3-{16,32,48,192,512}.png`, and `cameleer3-logo.png`. Copied to `ui/public/` for use as favicon (`favicon-16.png`, `favicon-32.png`) and logo (`camel-logo.svg` — login dialog 36px, sidebar 28x24px). +- Sidebar generates `/exchanges/` paths directly (no legacy `/apps/` redirects). basePath is centralized in `ui/src/config.ts`; router.tsx imports it instead of re-reading `` tag. - Global user preferences (environment selection) use Zustand stores with localStorage persistence — never URL search params. URL params are for page-specific state only (e.g. `?text=` search query). Switching environment resets all filters and remounts pages. ## Disabled Skills diff --git a/HOWTO.md b/HOWTO.md index 0acf7b7f..3ffae61d 100644 --- a/HOWTO.md +++ b/HOWTO.md @@ -39,9 +39,15 @@ PostgreSQL credentials: `cameleer` / `cameleer_dev`, database `cameleer3`. ```bash mvn clean package -DskipTests -CAMELEER_AUTH_TOKEN=my-secret-token java -jar cameleer3-server-app/target/cameleer3-server-app-1.0-SNAPSHOT.jar +SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/cameleer3 \ +SPRING_DATASOURCE_USERNAME=cameleer \ +SPRING_DATASOURCE_PASSWORD=cameleer_dev \ +CAMELEER_AUTH_TOKEN=my-secret-token \ +java -jar cameleer3-server-app/target/cameleer3-server-app-1.0-SNAPSHOT.jar ``` +> **Note:** The Docker image no longer includes default database credentials. When running via `docker run`, pass `-e SPRING_DATASOURCE_URL=...` etc. The docker-compose setup provides these automatically. + The server starts on **port 8081**. The `CAMELEER_AUTH_TOKEN` environment variable is **required** — the server fails fast on startup if it is not set. For token rotation without downtime, set `CAMELEER_AUTH_TOKEN_PREVIOUS` to the old token while rolling out the new one. The server accepts both during the overlap window. 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 fe27af21..ccc74973 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 @@ -94,7 +94,8 @@ public class SecurityConfig { "/", "/index.html", "/config.js", - "/favicon.svg", + "/favicon-*.png", + "/camel-logo.svg", "/assets/**" ).permitAll() diff --git a/docs/SERVER-CAPABILITIES.md b/docs/SERVER-CAPABILITIES.md index 90eabe9d..4800e710 100644 --- a/docs/SERVER-CAPABILITIES.md +++ b/docs/SERVER-CAPABILITIES.md @@ -271,11 +271,11 @@ Server derives an Ed25519 keypair deterministically from the JWT secret. Public ### OIDC Integration -Configured via admin API (`/api/v1/admin/oidc`) or admin UI. Supports any OpenID Connect provider. Features: configurable user ID claim (`userIdClaim`, default `sub` — e.g., `email`, `preferred_username`), role claim extraction (supports nested paths like `realm_access.roles`), auto-signup (auto-provisions new users on first OIDC login), configurable display name claim, constant-time token rotation via dual bootstrap tokens. Supports ES384 (Logto default), ES256, and RS256 for id_token validation. System roles are synced on every OIDC login (not just first) — revoking a scope in the provider takes effect on next login. Group memberships (manually assigned) are never touched by the sync. +Configured via admin API (`/api/v1/admin/oidc`) or admin UI. Supports any OpenID Connect provider. Features: configurable user ID claim (`userIdClaim`, default `sub` — e.g., `email`, `preferred_username`), role claim extraction (supports nested paths like `realm_access.roles`), auto-signup (auto-provisions new users on first OIDC login), configurable display name claim, constant-time token rotation via dual bootstrap tokens, PKCE (S256) on all authorization requests. Supports ES384 (Logto default), ES256, and RS256 for id_token validation. System roles are synced on every OIDC login (not just first) — revoking a scope in the provider takes effect on next login. Group memberships (manually assigned) are never touched by the sync. Role normalization via `SystemRole.normalizeScope()` (case-insensitive, strips `server:` prefix). Shared OIDC infrastructure (discovery, JWK source, algorithm set) centralized in `OidcProviderHelper`. ### SSO Auto-Redirect -When OIDC is configured and enabled, the login page automatically redirects to the OIDC provider with `prompt=none` for silent SSO. If the user has an active provider session, they are signed in without seeing a login form. If `consent_required` is returned (first login, scopes not yet granted), the flow retries without `prompt=none` so the user can grant consent once. If `login_required` (no provider session), falls back to the login form. Bypass auto-redirect with `/login?local`. +When OIDC is configured and enabled, the login page automatically redirects to the OIDC provider with `prompt=none` and PKCE (S256) for silent SSO. If the user has an active provider session, they are signed in without seeing a login form. If `consent_required` is returned (first login, scopes not yet granted), the flow retries without `prompt=none` so the user can grant consent once. If `login_required` (no provider session), falls back to the login form. Bypass auto-redirect with `/login?local`. Logout always redirects to `/login?local` — either via the OIDC `end_session_endpoint` (with `post_logout_redirect_uri`) or as a direct fallback — preventing SSO re-login loops. ### OIDC Resource Server @@ -392,7 +392,7 @@ Stats tables are fed by Materialized Views from base tables. Query with `-Merge( ### Container Image -Multi-stage Docker build: Maven 3.9 + JDK 17 (build) → JRE 17 (runtime). Port 8081. +Multi-stage Docker build: Maven 3.9 + JDK 17 (build) → JRE 17 (runtime). Port 8081. No default credentials baked in — all database config comes from env vars at runtime. Registry: `gitea.siegeln.net/cameleer/cameleer3-server` diff --git a/ui/index.html b/ui/index.html index 70980c1c..639f5a45 100644 --- a/ui/index.html +++ b/ui/index.html @@ -2,7 +2,8 @@ - + + Cameleer3 diff --git a/ui/package-lock.json b/ui/package-lock.json index b9dc6219..fc4e67b2 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -8,7 +8,7 @@ "name": "ui", "version": "0.0.0", "dependencies": { - "@cameleer/design-system": "^0.1.31", + "@cameleer/design-system": "^0.1.32", "@tanstack/react-query": "^5.90.21", "lucide-react": "^1.7.0", "openapi-fetch": "^0.17.0", @@ -278,9 +278,9 @@ } }, "node_modules/@cameleer/design-system": { - "version": "0.1.31", - "resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.31/design-system-0.1.31.tgz", - "integrity": "sha512-yfuMQdLB3RS6loT31YozKCyBUnctxlLPZjpzPzD7UQZhCFU4+ScibMzbPeLFE/MyMkfrpk/j9v8pJCHW7l64TQ==", + "version": "0.1.32", + "resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.32/design-system-0.1.32.tgz", + "integrity": "sha512-ocbTUTUZEpDAprsYD3Ao2ZVZXGSbFg3amL8FS85FoJKdJ3xKZO/kpkTKPl+qmsbh/E030QwdulBR8YnQYlHDBg==", "dependencies": { "lucide-react": "^1.7.0", "react": "^19.0.0", diff --git a/ui/package.json b/ui/package.json index a43d361b..913bd90b 100644 --- a/ui/package.json +++ b/ui/package.json @@ -14,7 +14,7 @@ "generate-api:live": "curl -s http://localhost:8081/api/v1/api-docs -o src/api/openapi.json && openapi-typescript src/api/openapi.json -o src/api/schema.d.ts" }, "dependencies": { - "@cameleer/design-system": "^0.1.31", + "@cameleer/design-system": "^0.1.32", "@tanstack/react-query": "^5.90.21", "lucide-react": "^1.7.0", "openapi-fetch": "^0.17.0", diff --git a/ui/public/camel-logo.svg b/ui/public/camel-logo.svg new file mode 100644 index 00000000..b83d1b07 --- /dev/null +++ b/ui/public/camel-logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/public/favicon-16.png b/ui/public/favicon-16.png new file mode 100644 index 00000000..4cfc9ed5 Binary files /dev/null and b/ui/public/favicon-16.png differ diff --git a/ui/public/favicon-32.png b/ui/public/favicon-32.png new file mode 100644 index 00000000..35257dd1 Binary files /dev/null and b/ui/public/favicon-32.png differ diff --git a/ui/src/auth/LoginPage.tsx b/ui/src/auth/LoginPage.tsx index 2644424b..6a6d06cc 100644 --- a/ui/src/auth/LoginPage.tsx +++ b/ui/src/auth/LoginPage.tsx @@ -131,7 +131,7 @@ export function LoginPage() {
- + cameleer3

{subtitle}

diff --git a/ui/src/components/LayoutShell.tsx b/ui/src/components/LayoutShell.tsx index 199b2b20..0bcbe8eb 100644 --- a/ui/src/components/LayoutShell.tsx +++ b/ui/src/components/LayoutShell.tsx @@ -625,7 +625,7 @@ function LayoutContent() { // --- Render ------------------------------------------------------- const camelLogo = ( - + ); const sidebarElement = (