diff --git a/configuration/ditto/Dockerfile b/configuration/ditto/Dockerfile new file mode 100644 index 00000000..a7e55f9e --- /dev/null +++ b/configuration/ditto/Dockerfile @@ -0,0 +1,6 @@ +FROM eclipse/ditto-gateway:latest + +USER root +# Copy the modified JAR (with open auth in reference.conf) +COPY ditto-gateway-service-3.8.12-allinone.jar /opt/ditto/ditto-gateway-service-3.8.12-allinone.jar +USER ditto diff --git a/configuration/ditto/application.conf b/configuration/ditto/application.conf new file mode 100644 index 00000000..92f900a3 --- /dev/null +++ b/configuration/ditto/application.conf @@ -0,0 +1,22 @@ +# Minimal override - authentication settings only +# This file is loaded after reference.conf by Typesafe config + +ditto { + gateway { + authentication { + pre-authentication { + enabled = true + } + devops { + secured = false + devops-authentication-method = "basic" + password = "ditto-devops-secret" + password = ${?DEVOPS_PASSWORD} + status-secured = false + status-authentication-method = "basic" + statusPassword = "ditto-status-secret" + statusPassword = ${?STATUS_PASSWORD} + } + } + } +} diff --git a/configuration/ditto/ditto-gateway-service-3.8.12-allinone.jar b/configuration/ditto/ditto-gateway-service-3.8.12-allinone.jar new file mode 100644 index 00000000..1e295080 Binary files /dev/null and b/configuration/ditto/ditto-gateway-service-3.8.12-allinone.jar differ diff --git a/configuration/ditto/docker-entrypoint.sh b/configuration/ditto/docker-entrypoint.sh new file mode 100644 index 00000000..48126a63 --- /dev/null +++ b/configuration/ditto/docker-entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/sh +# Fix permissions for mounted config files +if [ -f /opt/ditto/gateway-extension.conf ]; then + chmod 644 /opt/ditto/gateway-extension.conf +fi +# Start the gateway +exec java -jar /opt/ditto/ditto-gateway-service-3.8.12-allinone.jar diff --git a/configuration/ditto/entrypoint.sh b/configuration/ditto/entrypoint.sh new file mode 100644 index 00000000..fe7b20c4 --- /dev/null +++ b/configuration/ditto/entrypoint.sh @@ -0,0 +1,2 @@ +#!/bin/sh +exec java -Dconfig.file=/opt/ditto/gateway.conf -jar /opt/ditto/ditto-gateway-service-3.8.12-allinone.jar diff --git a/configuration/ditto/fix-jar.sh b/configuration/ditto/fix-jar.sh new file mode 100644 index 00000000..e0987bc5 --- /dev/null +++ b/configuration/ditto/fix-jar.sh @@ -0,0 +1,76 @@ +#!/bin/bash +set -e + +WORKDIR=/tmp/ditto-jar-mod +rm -rf $WORKDIR && mkdir -p $WORKDIR + +echo "=== Extracting JAR ===" +cd $WORKDIR +docker run --rm eclipse/ditto-gateway:latest cat /opt/ditto/ditto-gateway-service-3.8.12-allinone.jar > ditto-gateway-service-3.8.12-allinone.jar +jar xf ditto-gateway-service-3.8.12-allinone.jar reference.conf + +echo "=== Original reference.conf tail ===" +tail -5 reference.conf + +echo "=== Adding auth config to reference.conf ===" +# Remove the last closing brace, add our config, re-add the closing brace +# The reference.conf ends with nested braces - find the very last line +python3 << 'PYEOF' +with open("/tmp/ditto-jar-mod/reference.conf", "r") as f: + lines = f.readlines() + +# Find the last non-empty line that is just a closing brace +# We need to insert our config before the outermost closing brace + +# Simple approach: append before the very last } +# Count total closing braces at the end +content = "".join(lines) + +# The reference.conf has a complex nested structure +# We'll add our ditto config as a new root-level block at the end +# We need to close the last block and add a comma, then our new block + +# Actually, simpler: just append if the file ends with } +# Find the position of the very last } +last_brace_pos = content.rfind('}') +if last_brace_pos >= 0: + # Check if there's content after the last } (like kamon.conf include) + rest = content[last_brace_pos+1:].strip() + if rest: + # There's content after the last }, our approach is wrong + print(f"Content after last }}: {rest[:100]}") + # Insert before the last } with a comma + new_content = content[:last_brace_pos].rstrip() + # Remove trailing comma if present + if new_content.endswith(','): + new_content = new_content[:-1] + new_content += ',\n\n# Custom auth overrides\n' + rest[:0] + '\n' + ' authentication {\n pre-authentication {\n enabled = true\n }\n devops {\n secured = false\n }\n }\n}\n' + # Just append + new_content = content.rstrip() + '\n\n# Custom auth overrides\nditto {\n gateway {\n authentication {\n pre-authentication {\n enabled = true\n }\n devops {\n secured = false\n }\n }\n }\n}\n' + else: + # Last char is }, replace it with our config + } + new_content = content[:last_brace_pos].rstrip() + # Remove trailing comma + if new_content.endswith(','): + new_content = new_content[:-1] + new_content += ',\n\n# Custom auth overrides\n gateway {\n authentication {\n pre-authentication {\n enabled = true\n }\n devops {\n secured = false\n }\n }\n }\n}\n' +else: + new_content = content + +with open("/tmp/ditto-jar-mod/reference.conf", "w") as f: + f.write(new_content) + +print("reference.conf modified successfully") +PYEOF + +echo "=== Modified reference.conf tail ===" +tail -20 reference.conf + +echo "=== Updating JAR (replacing reference.conf) ===" +jar uf ditto-gateway-service-3.8.12-allinone.jar reference.conf + +echo "=== Verifying modified reference.conf in JAR ===" +jar xf ditto-gateway-service-3.8.12-allinone.jar reference.conf +grep -c "pre-authentication" reference.conf || echo "NOT FOUND in JAR" + +echo "=== Done. JAR is ready at $WORKDIR ===" diff --git a/configuration/ditto/gateway-extension.conf b/configuration/ditto/gateway-extension.conf new file mode 100644 index 00000000..b5886ecb --- /dev/null +++ b/configuration/ditto/gateway-extension.conf @@ -0,0 +1,7 @@ +ditto { + gateway { + http { + enablecors = true + } + } +} diff --git a/configuration/ditto/gateway.conf b/configuration/ditto/gateway.conf new file mode 100644 index 00000000..2c5cb9ec --- /dev/null +++ b/configuration/ditto/gateway.conf @@ -0,0 +1,363 @@ +ditto { + version = "3.8.12" + + extensions { + jwt-authorization-subjects-provider = { + extension-class = org.eclipse.ditto.gateway.service.security.authentication.jwt.DittoJwtAuthorizationSubjectsProvider + } + jwt-authentication-result-provider = { + extension-class = org.eclipse.ditto.gateway.service.security.authentication.jwt.DefaultJwtAuthenticationResultProvider + extension-config = { + role = regular + jwt-authorization-subjects-provider = { + extension-class = org.eclipse.ditto.gateway.service.security.authentication.jwt.DittoJwtAuthorizationSubjectsProvider + extension-config = { + role = regular + } + } + } + } + jwt-authentication-result-provider-devops = { + extension-class = org.eclipse.ditto.gateway.service.security.authentication.jwt.DefaultJwtAuthenticationResultProvider + extension-config = { + role = devops + jwt-authorization-subjects-provider = { + extension-class = org.eclipse.ditto.gateway.service.security.authentication.jwt.DittoJwtAuthorizationSubjectsProvider + extension-config = { + role = devops + } + } + } + } + signal-enrichment-provider { + extension-class = org.eclipse.ditto.gateway.service.endpoints.utils.DefaultGatewaySignalEnrichmentProvider + extension-config = { + cache { + enabled = true + maximum-size = 20000 + expire-after-create = 2m + } + } + } + http-bind-flow-provider = org.eclipse.ditto.gateway.service.endpoints.routes.LoggingHttpBindFlowProvider + websocket-config-provider = org.eclipse.ditto.gateway.service.endpoints.routes.websocket.NoOpWebSocketConfigProvider + gateway-authentication-directive-factory = org.eclipse.ditto.gateway.service.endpoints.directives.auth.DittoGatewayAuthenticationDirectiveFactory + http-request-actor-props-factory = org.eclipse.ditto.gateway.service.endpoints.actors.DefaultHttpRequestActorPropsFactory + sse-event-sniffer = org.eclipse.ditto.gateway.service.endpoints.routes.sse.NoOpSseEventSniffer + streaming-authorization-enforcer = org.eclipse.ditto.gateway.service.streaming.NoOpAuthorizationEnforcer + incoming-websocket-event-sniffer = org.eclipse.ditto.gateway.service.endpoints.routes.websocket.NoOpIncomingWebSocketEventSniffer + outgoing-websocket-event-sniffer = org.eclipse.ditto.gateway.service.endpoints.routes.websocket.NoOpOutgoingWebSocketEventSniffer + custom-api-routes-provider = org.eclipse.ditto.gateway.service.endpoints.routes.NoopCustomApiRoutesProvider + sse-connection-supervisor = org.eclipse.ditto.gateway.service.endpoints.routes.sse.NoOpSseConnectionSupervisor + websocket-connection-supervisor = "org.eclipse.ditto.gateway.service.endpoints.routes.websocket.NoOpWebSocketSupervisor" + connections-retrieval-actor-props-factory = org.eclipse.ditto.gateway.service.endpoints.actors.DefaultConnectionsRetrievalActorPropsFactory + } + + service-name = "gateway" + mapping-strategy.implementation = "org.eclipse.ditto.gateway.service.util.GatewayMappingStrategies" + + gateway { + http { + hostname = "" + hostname = ${?HOSTNAME} + hostname = ${?BIND_HOSTNAME} + port = 8080 + port = ${?HTTP_PORT} + port = ${?PORT} + coordinated-shutdown-timeout = 65s + coordinated-shutdown-timeout = ${?COORDINATED_SHUTDOWN_REQUEST_TIMEOUT} + schema-versions = [2] + protocol-headers = ["X-Forwarded-Proto", "x_forwarded_proto"] + forcehttps = false + forcehttps = ${?FORCE_HTTPS} + redirect-to-https = false + redirect-to-https = ${?REDIRECT_TO_HTTPS} + redirect-to-https-blocklist-pattern = "/api.*|/ws.*|/status.*|/overall.*" + enablecors = false + enablecors = ${?ENABLE_CORS} + request-timeout = 60s + request-timeout = ${?REQUEST_TIMEOUT} + additional-accepted-media-types = ${?ADDITIONAL_ACCEPTED_MEDIA_TYPES} + query-params-as-headers = [ + "accept" + "channel" + "correlation-id" + "requested-acks" + "declared-acks" + "response-required" + "timeout" + "live-channel-timeout-strategy" + "allow-policy-lockout" + "condition" + "live-channel-condition" + "at-historical-revision" + "at-historical-timestamp" + "dry-run" + ] + } + + streaming { + session-counter-scrape-interval = 30s + parallelism = 64 + parallelism = ${?GATEWAY_STREAMING_PARALLELISM} + search-idle-timeout = 60s + search-idle-timeout = ${?GATEWAY_STREAMING_SEARCH_IDLE_TIMEOUT} + subscription-refresh-delay = 5m + subscription-refresh-delay = ${?GATEWAY_STREAMING_SUBSCRIPTION_REFRESH_DELAY} + acknowledgement { + forwarder-fallback-timeout = 65s + } + websocket { + subscriber { + backpressure-queue-size = 100 + } + publisher { + backpressure-buffer-size = 200 + } + throttling-rejection-factor = 1.25 + throttling { + enabled = false + } + streaming-authorization-enforcer = "org.eclipse.ditto.gateway.service.streaming.NoOpAuthorizationEnforcer" + } + sse { + throttling { + enabled = false + } + streaming-authorization-enforcer = "org.eclipse.ditto.gateway.service.streaming.NoOpAuthorizationEnforcer" + } + } + + command { + default-timeout = ${ditto.gateway.http.request-timeout} + max-timeout = 1m + smart-channel-buffer = 10s + connections-retrieve-limit = 100 + } + message { + default-timeout = 10s + max-timeout = 1m + } + claim-message { + default-timeout = 1m + max-timeout = 10m + } + dns { + address = none + address = ${?DNS_SERVER} + } + + authentication { + http { + proxy { + enabled = false + enabled = ${?AUTH_HTTP_PROXY_ENABLED} + hostname = ${?AUTH_HTTP_PROXY_HOST} + port = ${?AUTH_HTTP_PROXY_PORT} + username = ${?AUTH_HTTP_PROXY_USERNAME} + password = ${?AUTH_HTTP_PROXY_PASSWORD} + } + } + oauth { + protocol = "https" + protocol = ${?OAUTH_PROTOCOL} + allowed-clock-skew = 10s + allowed-clock-skew = ${?OAUTH_ALLOWED_CLOCK_SKEW} + openid-connect-issuers = { + google = { + issuer = "accounts.google.com" + } + } + token-integration-subject = "integration:{{policy-entry:label}}:{{jwt:aud}}" + token-integration-subject = ${?OAUTH_TOKEN_INTEGRATION_SUBJECT} + } + + # PRE-AUTHENTICATION = open access for /api/2/ + pre-authentication { + enabled = true + } + + devops { + secured = false + devops-authentication-method = "basic" + password = "ditto-devops-secret" + password = ${?DEVOPS_PASSWORD} + status-secured = false + status-authentication-method = "basic" + statusPassword = "ditto-status-secret" + statusPassword = ${?STATUS_PASSWORD} + } + } + + health-check { + enabled = true + enabled = ${?HEALTH_CHECK_ENABLED} + interval = 60s + interval = ${?HEALTH_CHECK_INTERVAL} + service.timeout = 10s + service.timeout = ${?HEALTH_CHECK_SERVICE_TIMEOUT} + cluster-roles = { + enabled = true + enabled = ${?HEALTH_CHECK_ROLES_ENABLED} + expected = [ + "policies" + "things" + "search" + "gateway" + "connectivity" + ] + } + } + + public-health { + cache-timeout = 20s + cache-timeout = ${?GATEWAY_STATUS_HEALTH_EXTERNAL_TIMEOUT} + } + + cloud-events { + empty-schema-allowed = true + data-types = [ + "application/json" + "application/vnd.eclipse.ditto+json" + ] + } + + cache { + publickeys { + maxentries = 32 + expiry = 60m + maximum-size = ${ditto.gateway.cache.publickeys.maxentries} + expire-after-write = ${ditto.gateway.cache.publickeys.expiry} + } + } + + statistics { + ask-timeout = 5s + ask-timeout = ${?STATISTICS_UPDATE_INTERVAL} + update-interval = 15s + update-interval = ${?STATISTICS_UPDATE_INTERVAL} + details-expire-after = 3s + details-expire-after = ${?STATISTICS_DETAILS_EXPIRE_AFTER} + shards = [ + { + region = "thing" + role = "things" + root = "/user/thingsRoot" + } + { + region = "policy" + role = "policies" + root = "/user/policiesRoot" + } + { + region = "search-wildcard-updater" + role = "search" + root = "/user/thingsWildcardSearchRoot/searchUpdaterRoot" + } + ] + } + } + + tracing { + filter = { + includes = ["**"] + excludes = ["GET /ws/2"] + } + } +} + +secrets { + devops_password { + name = "devops_password" + name = ${?DEVOPS_PASSWORD_NAME} + } + status_password { + name = "status_password" + name = ${?STATUS_PASSWORD_NAME} + } +} + +pekko.http.client { + user-agent-header = eclipse-ditto/${ditto.version} +} + +pekko { + actor { + default-dispatcher { + executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedForkJoinExecutorServiceConfigurator" + } + deployment { + /gatewayRoot/proxy { + router = round-robin-pool + resizer { + lower-bound = 5 + upper-bound = 100 + messages-per-resize = 50 + } + } + } + } + cluster { + sharding { + role = ${ditto.service-name} + passivation { + strategy = "off" + } + } + roles = ["gateway"] + } + coordinated-shutdown { + phases { + service-requests-done { + timeout = 70s + } + } + } + http { + server { + server-header = "" + request-timeout = ${ditto.gateway.http.request-timeout} + idle-timeout = 610s + max-connections = 4096 + raw-request-uri-header = on + parsing { + max-uri-length = 8k + max-content-length = 1m + uri-parsing-mode = relaxed + } + websocket { + periodic-keep-alive-mode = ping + periodic-keep-alive-max-idle = 30s + } + termination-deadline-exceeded-response { + status = 502 + } + } + host-connection-pool { + max-open-requests = 1024 + idle-timeout = 60s + } + } + management.health-checks.readiness-checks { + gateway-http-readiness = "org.eclipse.ditto.gateway.service.health.GatewayHttpReadinessCheck" + } + management.health-checks.liveness-checks { + subsystem-health = "org.eclipse.ditto.internal.utils.health.SubsystemHealthCheck" + } +} + +authentication-dispatcher { + type = Dispatcher + executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedThreadPoolExecutorServiceConfigurator" + thread-pool-executor { + core-pool-size-min = 4 + core-pool-size-factor = 2.0 + core-pool-size-max = 8 + } + throughput = 100 +} + +signal-enrichment-cache-dispatcher { + type = Dispatcher + executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedThreadPoolExecutorServiceConfigurator" +} diff --git a/configuration/ditto/modify-jar.py b/configuration/ditto/modify-jar.py new file mode 100644 index 00000000..6ad4a36e --- /dev/null +++ b/configuration/ditto/modify-jar.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +""" +Modify the reference.conf inside the Ditto gateway JAR. +Inserts ditto { gateway { authentication { pre-authentication { enabled = true } } } } +at the root level, before the final closing brace. +""" +import zipfile +import shutil +import os +import subprocess +import sys + +JAR_PATH = "/tmp/ditto-jar-mod/ditto-gateway-service-3.8.12-allinone.jar" +AUTH_BLOCK = """ +### Custom Ditto auth override - pre-authentication enabled +ditto { + gateway { + authentication { + pre-authentication { + enabled = true + } + devops { + secured = false + devops-authentication-method = "basic" + password = "ditto-devops-secret" + status-secured = false + status-authentication-method = "basic" + statusPassword = "ditto-status-secret" + } + } + } +} +""" + +def main(): + os.makedirs("/tmp/ditto-jar-mod", exist_ok=True) + + print("=== Step 1: Extracting JAR ===") + result = subprocess.run( + ["docker", "run", "--rm", "eclipse/ditto-gateway:latest", + "cat", "/opt/ditto/ditto-gateway-service-3.8.12-allinone.jar"], + capture_output=True, check=True + ) + with open(JAR_PATH, "wb") as f: + f.write(result.stdout) + print(f"JAR: {len(result.stdout)} bytes") + + print("=== Step 2: Modifying reference.conf ===") + with zipfile.ZipFile(JAR_PATH, 'r') as zin: + ref_conf = zin.read("reference.conf").decode("utf-8") + + lines = ref_conf.split('\n') + total_lines = len(lines) + print(f"reference.conf: {total_lines} lines") + + # Find the LAST closing brace at root level (depth 0) + # Track depth through the entire file + depth = 0 + last_root_close_idx = None + + for i, line in enumerate(lines): + stripped = line.strip() + if not stripped or stripped.startswith('#'): + continue + + # Count braces (simple approach - count { and }) + # This isn't perfect for HOCON but works for our case + for ch in stripped: + if ch == '{': + depth += 1 + elif ch == '}': + depth -= 1 + + if depth == 0: + last_root_close_idx = i + + if last_root_close_idx is None: + print("ERROR: Could not find root-level closing brace!") + sys.exit(1) + + insert_idx = last_root_close_idx + print(f"Last root-level '}}' at line {insert_idx + 1}: '{lines[insert_idx].strip()}'") + + # Check if we need a comma before our block + # Look at the non-empty line before insert_idx + prev_idx = insert_idx - 1 + while prev_idx >= 0 and lines[prev_idx].strip() == '': + prev_idx -= 1 + + if prev_idx >= 0: + prev_stripped = lines[prev_idx].strip() + if prev_stripped.endswith('}'): + # Need to add a comma + lines[prev_idx] = lines[prev_idx].rstrip() + if not lines[prev_idx].endswith(','): + lines[prev_idx] += ',' + print(f"Added comma to line {prev_idx + 1}") + + # Insert our block + auth_lines = AUTH_BLOCK.split('\n') + new_lines = lines[:insert_idx] + auth_lines + lines[insert_idx:] + modified_conf = '\n'.join(new_lines) + + print(f"Modified: {total_lines} -> {len(new_lines)} lines") + + # Verify brace balance + depth = 0 + for i, line in enumerate(new_lines): + stripped = line.strip() + if not stripped or stripped.startswith('#'): + continue + for ch in stripped: + if ch == '{': + depth += 1 + elif ch == '}': + depth -= 1 + if depth < 0: + print(f"ERROR: Negative depth at line {i+1}") + sys.exit(1) + + print(f"Brace depth check: {depth} (should be 0)") + if depth != 0: + print("ERROR: Unbalanced braces!") + sys.exit(1) + + # Verify ditto is at root level + for i, line in enumerate(new_lines): + if line.strip() == 'ditto {': + indent = len(line) - len(line.lstrip()) + print(f"'ditto {{' at line {i+1}, indent: {indent}") + if indent != 0: + print(f"WARNING: Expected indent 0, got {indent}") + break + + print("=== Step 3: Creating unsigned JAR ===") + skip_files = set() + with zipfile.ZipFile(JAR_PATH, 'r') as zin: + for name in zin.namelist(): + if name.startswith("META-INF/"): + upper = name.upper() + if upper.endswith(".SF") or upper.endswith(".RSA") or upper.endswith(".DSA") or upper == "MANIFEST.MF": + skip_files.add(name) + + with zipfile.ZipFile(JAR_PATH + ".new", 'w', zipfile.ZIP_DEFLATED) as zout: + for item in zin.infolist(): + if item.filename in skip_files: + continue + data = zin.read(item.filename) + if item.filename == "reference.conf": + data = modified_conf.encode("utf-8") + info = zipfile.ZipInfo(filename=item.filename, date_time=item.date_time) + info.compress_type = zipfile.ZIP_DEFLATED + info.external_attr = item.external_attr + zout.writestr(info, data) + + shutil.move(JAR_PATH + ".new", JAR_PATH) + + print("=== Step 4: Verifying ===") + with zipfile.ZipFile(JAR_PATH, 'r') as z: + ref = z.read("reference.conf").decode("utf-8") + assert "pre-authentication" in ref + sig = [n for n in z.namelist() if n.startswith("META-INF/") and n.upper().endswith(('.SF', '.RSA', '.DSA'))] + print(f"OK Signature files: {len(sig)}") + print(f"OK JAR size: {os.path.getsize(JAR_PATH)}") + + print("\n=== DONE ===") + +if __name__ == "__main__": + main() diff --git a/configuration/ditto/oauth2-server.js b/configuration/ditto/oauth2-server.js new file mode 100644 index 00000000..1f71631a --- /dev/null +++ b/configuration/ditto/oauth2-server.js @@ -0,0 +1,70 @@ +#!/bin/bash +# Minimal JWT/OAuth2 server for Ditto +# Serves a JWKS endpoint and validates tokens signed with DITTO_JWT_SECRET + +cat > /tmp/oauth2-server.js << 'EOF' +const http = require('http'); +const crypto = require('crypto'); + +const SECRET = process.env.DITTO_JWT_SECRET || 'my-ditto-jwt-secret-key-12345'; +const PORT = 3000; + +function base64url(buf) { + return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); +} + +// Generate a token +function generateToken(sub, scope) { + const header = { alg: 'RS256', typ: 'JWT', kid: 'ditto-local' }; + const now = Math.floor(Date.now() / 1000); + const payload = { + iss: 'http://localhost:' + PORT, + sub: sub, + aud: 'ditto:cognito', + iat: now, + exp: now + 3600, + scope: scope + }; + const h = base64url(Buffer.from(JSON.stringify(header))); + const p = base64url(Buffer.from(JSON.stringify(payload))); + const sig = base64url( + crypto.createHmac('sha256', SECRET).update(h + '.' + p).digest() + ); + return h + '.' + p + '.' + sig; +} + +const server = http.createServer((req, res) => { + res.setHeader('Content-Type', 'application/json'); + + if (req.url === '/.well-known/openid-configuration') { + res.end(JSON.stringify({ + issuer: 'http://localhost:' + PORT, + jwks_uri: 'http://localhost:' + PORT + '/.well-known/jwks.json', + token_endpoint: 'http://localhost:' + PORT + '/token' + })); + } else if (req.url === '/.well-known/jwks.json') { + // Extract public key from secret (for HS256 we just return the secret as k) + const jwk = { + kty: 'oct', + kid: 'ditto-local', + use: 'sig', + alg: 'HS256', + k: base64url(Buffer.from(SECRET)) + }; + res.end(JSON.stringify({ keys: [jwk] })); + } else if (req.url === '/token') { + const token = generateToken('ditto', 'READ_WRITE'); + res.end(JSON.stringify({ access_token: token, token_type: 'Bearer', expires_in: 3600 })); + } else { + res.statusCode = 404; + res.end('{}'); + } +}); + +server.listen(PORT, '0.0.0.0', () => { + console.log('OAuth2 server listening on port ' + PORT); + console.log('Token: ' + generateToken('ditto', scope='READ_WRITE')); +}); +EOF + +node /tmp/oauth2-server.js diff --git a/configuration/ditto/override.conf b/configuration/ditto/override.conf new file mode 100644 index 00000000..47c6fced --- /dev/null +++ b/configuration/ditto/override.conf @@ -0,0 +1,14 @@ +ditto { + gateway { + authentication { + pre-authentication { + enabled = true + } + devops { + secured = false + devops-authentication-method = "basic" + password = "ditto-devops-secret" + } + } + } +} diff --git a/ditto-things.yml b/ditto-things.yml new file mode 100644 index 00000000..a85a22e8 --- /dev/null +++ b/ditto-things.yml @@ -0,0 +1,25 @@ +services: + ditto-things: + image: eclipse/ditto-things:latest + container_name: smart-city-ditto-things + restart: unless-stopped + hostname: ditto-things + environment: + - TZ=Europe/Berlin + - BIND_HOSTNAME=0.0.0.0 + - DITTO_JWT_SECRET=my-ditto-jwt-secret-key-12345 + - MONGO_HOST=smart-city-ditto-mongodb + - MONGO_PORT=27017 + - MONGO_DB=Things + - MONGODB_URI=mongodb://smart-city-ditto-mongodb:27017/Things + - AKKA_REMOTE_ENABLED=false + - JAVA_TOOL_OPTIONS=-Dditto.things.authentication.devops.password=ditto-devops-secret + networks: + traefik-public: + aliases: + - ditto-cluster + - ditto-things + +networks: + traefik-public: + external: true diff --git a/docker-compose.ditto.yml b/docker-compose.ditto.yml index f39a5f57..2453d560 100644 --- a/docker-compose.ditto.yml +++ b/docker-compose.ditto.yml @@ -68,7 +68,7 @@ services: - "traefik.http.services.ditto-things.loadbalancer.server.port=8080" ditto-gateway: - image: eclipse/ditto-gateway:latest + image: eclipse/ditto-gateway:custom container_name: smart-city-ditto-gateway restart: unless-stopped hostname: ditto-gateway @@ -85,19 +85,11 @@ services: - DITTO_GW_MQTT_BROKER=smart-city-mosquitto:1883 - DITTO_GW_MQTT_TOPIC_FILTER=smartcity/# - DEVOPS_PASSWORD=ditto-devops-secret - - ENABLE_PRE_AUTHENTICATION=true - - JAVA_TOOL_OPTIONS=-Dditto.gateway.authentication.devops.password=ditto-devops-secret -Dditto.gateway.authentication.devops.secured=true -Dditto.gateway.authentication.devops.devops-authentication-method=basic networks: traefik-public: aliases: - ditto-cluster - ditto-gateway - labels: - - "traefik.enable=true" - - "traefik.http.routers.ditto-gateway.rule=Host(`ditto.digitribe.fr`)" - - "traefik.http.routers.ditto-gateway.entrypoints=websecure" - - "traefik.http.routers.ditto-gateway.tls.certresolver=letsencrypt" - - "traefik.http.services.ditto-gateway.loadbalancer.server.port=8080" networks: traefik-public: diff --git a/flink/docker-compose.yml b/flink/docker-compose.yml new file mode 100644 index 00000000..d02a7cb3 --- /dev/null +++ b/flink/docker-compose.yml @@ -0,0 +1,48 @@ +# Smart City Digital Twin Martinique — Apache Flink +# Usage: docker compose -f flink/docker-compose.yml up -d +# Image officielle Apache Flink 1.20.1 avec digest vérifié + +networks: + smartcity-shared: + external: true + +services: + jobmanager: + image: apache/flink:1.20.1-scala_2.12-java17@sha256:ecc5785594eff2d94e29e6b116b3124c0cdb3a9c952ebdf38ef0fef90fb9913d + container_name: flink-jobmanager + command: jobmanager + networks: + - smartcity-shared + ports: + - "8081:8081" # Flink Web UI + environment: + - | + FLINK_PROPERTIES= + jobmanager.rpc.address: jobmanager + jobmanager.memory.process.size: 1024m + taskmanager.memory.process.size: 1024m + taskmanager.numberOfTaskSlots: 4 + parallelism.default: 2 + rest.port: 8081 + restart: unless-stopped + labels: + - "traefik.enable=false" + + taskmanager: + image: apache/flink:1.20.1-scala_2.12-java17@sha256:ecc5785594eff2d94e29e6b116b3124c0cdb3a9c952ebdf38ef0fef90fb9913d + container_name: flink-taskmanager + command: taskmanager + networks: + - smartcity-shared + depends_on: + - jobmanager + environment: + - | + FLINK_PROPERTIES= + jobmanager.rpc.address: jobmanager + taskmanager.memory.process.size: 1024m + taskmanager.numberOfTaskSlots: 4 + parallelism.default: 2 + restart: unless-stopped + labels: + - "traefik.enable=false" diff --git a/vre/jupyterhub/Dockerfile b/vre/jupyterhub/Dockerfile index 5daec1dd..7c4afaff 100644 --- a/vre/jupyterhub/Dockerfile +++ b/vre/jupyterhub/Dockerfile @@ -3,7 +3,7 @@ FROM jupyterhub/jupyterhub:5.3.0 USER root -RUN apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y --no-install-recommends git sudo && rm -rf /var/lib/apt/lists/* RUN pip install --no-cache-dir \ git+https://github.com/jupyterhub/nativeauthenticator.git@main \ @@ -12,9 +12,17 @@ RUN pip install --no-cache-dir \ jupyterlab \ notebook +# Create eric user with password-less sudo +RUN useradd -m -s /bin/bash -u 1000 eric && \ + echo "eric ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers && \ + chown -R eric:eric /home/eric + # Create the directory for JupyterHub data RUN mkdir -p /srv/jupyterhub && chown -R 1000:1000 /srv/jupyterhub +ARG BUILD_DATE=unknown +RUN echo "Build date: ${BUILD_DATE}" > /tmp/build-info.txt + COPY jupyterhub_config.py /srv/jupyterhub/jupyterhub_config.py WORKDIR /srv/jupyterhub diff --git a/vre/jupyterhub/jupyterhub_config.py b/vre/jupyterhub/jupyterhub_config.py index 37c68f9b..14328ff0 100644 --- a/vre/jupyterhub/jupyterhub_config.py +++ b/vre/jupyterhub/jupyterhub_config.py @@ -1,4 +1,8 @@ # JupyterHub configuration for Smart City VRE +# Uses NativeAuthenticator with LocalProcessSpawner +# Build: 2026-06-01-0915 + +import sys as _sys c.JupyterHub.ip = '0.0.0.0' c.JupyterHub.port = 8000 @@ -9,14 +13,14 @@ c.JupyterHub.authenticator_class = 'nativeauthenticator.NativeAuthenticator' c.Authenticator.admin_users = {'admin'} c.Authenticator.allow_all = True -# Spawner — use SimpleLocalProcessSpawner (default in JupyterHub 5.x) -# This spawner runs singleuser servers as subprocesses -c.JupyterHub.spawner_class = 'jupyterhub.spawner.SimpleLocalProcessSpawner' +# Spawner: Simple local spawner (single-node, shared environment) +c.JupyterHub.spawner_class = 'simple' +c.Spawner.cmd = ['jupyterhub-singleuser'] c.Spawner.default_url = '/lab' -c.Spawner.http_timeout = 300 c.Spawner.start_timeout = 300 +c.Spawner.http_timeout = 120 -# Database and cookies - use absolute paths +# Database and cookies - use absolute path (4 slashes!) c.JupyterHub.cookie_secret_file = '/srv/jupyterhub/jupyterhub_cookie_secret' c.JupyterHub.db_url = 'sqlite:////srv/jupyterhub/jupyterhub.sqlite'