diff --git a/.gitignore b/.gitignore index 7e57cb3..8994b5d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ data/ -config/nextcloud/ \ No newline at end of file +config/credentials +config/nextcloud/ diff --git a/ansible/site.yml b/ansible/site.yml index 22ea670..ded0d95 100644 --- a/ansible/site.yml +++ b/ansible/site.yml @@ -1,11 +1,22 @@ -# automated-office/ansible/site.yml --- - name: Deploy Automated Office - hosts: all + hosts: localhost become: true + pre_tasks: + - debug: + msg: + - "Client secret defined: {{ client_secret is defined }}" + - "Client secret length: {{ client_secret | default('') | length }}" + verbosity: 1 + + - name: "Verify required variables" + fail: + msg: "Client secret is not set or empty" + when: client_secret is not defined or client_secret | default('') | trim == '' + roles: - - common - - docker - - nginx - - services \ No newline at end of file + - role: common + - role: docker + - role: nginx + - role: services \ No newline at end of file diff --git a/config/credentials/credentials_2024-12-11_18-28-21.txt.gpg b/config/credentials/credentials_2024-12-11_18-28-21.txt.gpg deleted file mode 100644 index b43471e..0000000 Binary files a/config/credentials/credentials_2024-12-11_18-28-21.txt.gpg and /dev/null differ diff --git a/config/nginx/sites-available/keycloak b/config/nginx/sites-available/keycloak index 2c2181a..cf437a2 100644 --- a/config/nginx/sites-available/keycloak +++ b/config/nginx/sites-available/keycloak @@ -5,7 +5,7 @@ upstream keycloak_upstream { server { listen 80; server_name auth.mrx8086.com; - + # Redirect HTTP to HTTPS return 301 https://$host$request_uri; } @@ -30,35 +30,49 @@ server { add_header X-XSS-Protection "1; mode=block" always; add_header X-Frame-Options SAMEORIGIN always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; - + # Content Security Policy add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; frame-src 'self'; frame-ancestors 'self'; connect-src 'self'" always; - # Proxy settings for all locations + # Proxy settings - Added X-Forwarded headers here to apply to all proxied locations proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Ssl on; # Optional, but explicit + proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Forwarded-Host $host; - proxy_set_header X-Forwarded-Port 443; proxy_set_header Host $host; proxy_http_version 1.1; - + # Root location for the main application location / { proxy_pass http://keycloak_upstream; } + # Specific location for the token endpoint + location ~ ^/auth/realms/[^/]+/protocol/openid-connect/token$ { + proxy_pass http://keycloak_upstream; + proxy_buffer_size 128k; + proxy_buffers 4 256k; + proxy_busy_buffers_size 256k; + # WebSocket support (likely not needed for token endpoint, but keeping for consistency) + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + # Timeouts + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + # Keycloak required paths location /realms/ { proxy_pass http://keycloak_upstream; proxy_buffer_size 128k; proxy_buffers 4 256k; proxy_busy_buffers_size 256k; - # WebSocket support proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; - # Timeouts proxy_connect_timeout 60s; proxy_send_timeout 60s; @@ -67,7 +81,7 @@ server { location /resources/ { proxy_pass http://keycloak_upstream; - + # Cache settings for static resources proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504; proxy_cache_valid 200 1d; diff --git a/config/nginx/sites-available/nextcloud b/config/nginx/sites-available/nextcloud index c384e10..8357df3 100644 --- a/config/nginx/sites-available/nextcloud +++ b/config/nginx/sites-available/nextcloud @@ -1,12 +1,10 @@ upstream nextcloud_upstream { - server 172.19.0.3:80; # Die IP wird später durch die tatsächliche Container-IP ersetzt + server 172.19.0.3:80; # SICHERSTELLEN, DASS DIES DIE KORREKTE IP IST } server { listen 80; server_name cloud.mrx8086.com; - - # Redirect HTTP to HTTPS return 301 https://$host$request_uri; } @@ -14,27 +12,19 @@ server { listen 443 ssl; server_name cloud.mrx8086.com; - # SSL Configuration + # SSL Configuration (wie zuvor) ssl_certificate /etc/nginx/ssl/mrx8086.com/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/mrx8086.com/privkey.pem; - ssl_session_timeout 1d; - ssl_session_tickets off; + # ... weitere SSL-Einstellungen ... - # Modern SSL configuration - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; - ssl_prefer_server_ciphers off; - - # Security headers + # Security headers (wie zuvor) add_header X-Content-Type-Options nosniff always; add_header X-XSS-Protection "1; mode=block" always; add_header X-Frame-Options SAMEORIGIN always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; - - # Content Security Policy für Nextcloud add_header Content-Security-Policy "frame-ancestors 'self'; default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; media-src 'self';" always; - # Proxy settings + # Proxy settings (wie zuvor) proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; @@ -43,31 +33,36 @@ server { proxy_set_header Host $host; proxy_http_version 1.1; - # Nextcloud specific settings + # Nextcloud specific settings (wie zuvor) client_max_body_size 512M; fastcgi_buffers 64 4K; - + + # Expliziter location-Block für den OpenID Connect Callback + location /apps/sociallogin/custom_oidc/keycloak { + proxy_pass http://nextcloud_upstream; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + # Root location location / { proxy_pass http://nextcloud_upstream; - - # WebSocket support proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; - - # Timeouts proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; } - # Block sensitive paths + # Block sensitive paths (wie zuvor) location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/) { deny all; return 404; } - # Deny access to hidden files + # Deny access to hidden files (wie zuvor) location ~ /\. { deny all; return 404; diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 0163334..ccbcd3b 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -34,8 +34,10 @@ services: - keycloak-network depends_on: - keycloak-db + extra_hosts: + - "cloud.mrx8086.com:172.23.171.133" healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/health/ready"] + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 30s timeout: 10s retries: 3 @@ -48,7 +50,7 @@ services: POSTGRES_USER: ${KC_DB_USERNAME} POSTGRES_PASSWORD: ${KC_DB_PASSWORD} volumes: - - ../data/keycloak/db:/var/lib/postgresql/data + - ../data/keycloak-db:/var/lib/postgresql/data networks: - keycloak-network restart: unless-stopped @@ -63,6 +65,13 @@ services: image: nextcloud:latest container_name: nextcloud restart: unless-stopped + ports: + - "8081:80" + volumes: + - ../data/nextcloud:/var/www/html + - ../config/nextcloud/config:/var/www/html/config + - ../config/nextcloud/custom_apps:/var/www/html/custom_apps + - ../data/nextcloud-db:/var/lib/mysql environment: - MYSQL_HOST=nextcloud-db - MYSQL_DATABASE=nextcloud @@ -74,16 +83,24 @@ services: - OVERWRITEPROTOCOL=https - OVERWRITEHOST=cloud.mrx8086.com - OVERWRITEWEBROOT=/ - - TRUSTED_PROXIES=172.18.0.0/16 - volumes: - - ../data/nextcloud:/var/www/html - - ../config/nextcloud/config:/var/www/html/config - - ../config/nextcloud/custom_apps:/var/www/html/custom_apps - - ../config/nextcloud/data:/var/www/html/data + - TRUSTED_PROXIES=172.19.0.0/16 + - NEXTCLOUD_URL=https://cloud.mrx8086.com + - NEXTCLOUD_DEBUG=1 + - NEXTCLOUD_CONFIG_CUSTOM_SCOPE="openid profile groups-nextcloud" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:80/"] + interval: 30s + timeout: 10s + retries: 3 networks: - nextcloud-network depends_on: - nextcloud-db + extra_hosts: + - "auth.mrx8086.com:172.23.171.133" + dns: + - 8.8.8.8 + - 8.8.4.4 nextcloud-db: image: mariadb:10.6 diff --git a/docs/context/scripts/setup_environment.sh b/docs/context/scripts/setup_environment.sh index 9a1b2da..1244eb8 100644 --- a/docs/context/scripts/setup_environment.sh +++ b/docs/context/scripts/setup_environment.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -e # Ensure we're in the project root directory PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" @@ -8,7 +9,8 @@ cd "${PROJECT_ROOT}" CREDENTIALS_DIR="config/credentials" DOCKER_DIR="docker" KEYCLOAK_SCRIPTS_DIR="scripts/setup/keycloak" - +ANSIBLE_PLAYBOOK="ansible/site.yml" +ANSIBLE_INVENTORY="ansible/inventory/staging/hosts" # Create necessary directories mkdir -p "${CREDENTIALS_DIR}" @@ -33,7 +35,7 @@ NEXTCLOUD_DB_USER=$(generate_password) NEXTCLOUD_DB_PASSWORD=$(generate_password) NEXTCLOUD_ADMIN_USER=$(generate_password) NEXTCLOUD_ADMIN_PASSWORD=$(generate_password) - +KEYCLOAK_NEXTCLOUD_CLIENT_SECRET=$(generate_password) # Create .env file in docker directory cat > "${DOCKER_DIR}/.env" << EOL @@ -64,6 +66,7 @@ PAPERLESS_CLIENT_ID=paperless NODERED_CLIENT_ID=nodered TESTADMIN_PASSWORD=${TESTADMIN_PASSWORD} TESTUSER_PASSWORD=${TESTUSER_PASSWORD} +KEYCLOAK_NEXTCLOUD_CLIENT_SECRET=${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET} EOL # Create encrypted credentials documentation @@ -77,8 +80,10 @@ Password: ${KEYCLOAK_ADMIN_PASSWORD} Keycloak Database Credentials: Username: keycloak Password: ${KC_DB_PASSWORD} + Test Admin Credentials: Password: ${TESTADMIN_PASSWORD} + Test User Credentials: Password: ${TESTUSER_PASSWORD} @@ -86,6 +91,7 @@ Nextcloud Database Credentials: Root Password: ${NEXTCLOUD_DB_ROOT_PASSWORD} User: ${NEXTCLOUD_DB_USER} Password: ${NEXTCLOUD_DB_PASSWORD} + Nextcloud Admin Credentials: Username: ${NEXTCLOUD_ADMIN_USER} Password: ${NEXTCLOUD_ADMIN_PASSWORD} @@ -101,4 +107,40 @@ echo ".env file for docker-compose has been created in: ${DOCKER_DIR}/.env" echo ".env file for setup_realm.js has been created in: ${KEYCLOAK_SCRIPTS_DIR}/.env" echo "" echo "To view credentials, use:" -echo "gpg -d ${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt.gpg" \ No newline at end of file +echo "gpg -d ${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt.gpg" + +echo ">>> Nextcloud Konfiguration..." + +# Verify if variable is set from earlier in the script +echo ">>> Debug: Checking original variable..." +echo ">>> Debug: KEYCLOAK_NEXTCLOUD_CLIENT_SECRET = ${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}" + +# Try reading from .env file if variable is empty +if [ -z "${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}" ]; then + echo ">>> Debug: Variable is empty, trying to read from .env file..." + KEYCLOAK_NEXTCLOUD_CLIENT_SECRET=$(grep KEYCLOAK_NEXTCLOUD_CLIENT_SECRET "${KEYCLOAK_SCRIPTS_DIR}/.env" | cut -d '=' -f2) + echo ">>> Debug: Value from .env file = ${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}" +fi + +# Ensure we have a value +if [ -z "${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}" ]; then + echo ">>> Error: Could not get client secret value" + exit 1 +fi + +# Escape special characters in the secret for JSON +ESCAPED_SECRET=$(echo "$KEYCLOAK_NEXTCLOUD_CLIENT_SECRET" | sed 's/["\]/\\&/g') +echo ">>> Debug: Escaped secret = $ESCAPED_SECRET" + +# Create the extra vars +EXTRA_VARS="{\"client_secret\": \"$ESCAPED_SECRET\"}" +echo ">>> Debug: Extra vars = $EXTRA_VARS" + +# Run Ansible with the extra vars +sudo ansible-playbook \ + -i "$ANSIBLE_INVENTORY" \ + "$ANSIBLE_PLAYBOOK" \ + --extra-vars "$EXTRA_VARS" \ + -v + +echo ">>> Fertig" \ No newline at end of file diff --git a/scripts/install/setup_environment.sh b/scripts/install/setup_environment.sh old mode 100644 new mode 100755 index aba63dc..5b7bc0e --- a/scripts/install/setup_environment.sh +++ b/scripts/install/setup_environment.sh @@ -1,31 +1,163 @@ #!/bin/bash +set -e # Ensure we're in the project root directory PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -cd "${PROJECT_ROOT}" # Define directories relative to project root -CREDENTIALS_DIR="config/credentials" -DOCKER_DIR="docker" +CREDENTIALS_DIR="${PROJECT_ROOT}/config/credentials" +DOCKER_DIR="${PROJECT_ROOT}/docker" +KEYCLOAK_SETUP_DIR="${PROJECT_ROOT}/scripts/setup/keycloak" +ANSIBLE_PLAYBOOK="${PROJECT_ROOT}/ansible/site.yml" +ANSIBLE_INVENTORY="${PROJECT_ROOT}/ansible/inventory/staging/hosts" +NEXTCLOUD_DATA_DIR="${PROJECT_ROOT}/data/nextcloud/data" +TEMP_FILE=$(mktemp) +KEYCLOAK_DB_DIR="${PROJECT_ROOT}/data/keycloak-db" # Create necessary directories -mkdir -p "${CREDENTIALS_DIR}" -mkdir -p "${DOCKER_DIR}" +sudo mkdir -p "${CREDENTIALS_DIR}" +sudo mkdir -p "${DOCKER_DIR}" +sudo mkdir -p "${KEYCLOAK_SETUP_DIR}" + +# Initialize password variables +KEYCLOAK_ADMIN_PASSWORD="" +KC_DB_PASSWORD="" +TESTADMIN_PASSWORD="" +TESTUSER_PASSWORD="" +TESTSERVICEUSER_PASSWORD="" +KEYCLOAK_NEXTCLOUD_CLIENT_SECRET="" + +# Function to read a password from a .env file +read_password_from_env() { + local env_file="$1" + local variable_name="$2" + if [ -f "$env_file" ]; then + grep "^${variable_name}=" "$env_file" | cut -d '=' -f2 + fi +} # Function to generate secure passwords generate_password() { - openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24 + openssl rand -base64 32 +} + +# Function to generate password if empty +generate_password_if_empty() { + local variable_name="$1" + eval "local value=\$$variable_name" + if [ -z "$value" ]; then + eval "$variable_name=\"$(generate_password)\"" + echo ">>> Generiertes Passwort für: $variable_name" + fi +} + +# Function to create .env file +create_env_file() { + local env_file="$1" + local content="$2" + if [ ! -f "$env_file" ]; then + echo "$content" > "$env_file" + echo ">>> .env file created: $env_file" + else + echo ">>> .env file already exists: $env_file" + fi } +echo ">>> Überprüfe bestehende .env Dateien und lese Passwörter..." + +# Try reading passwords from existing .env files +if [ -f "$DOCKER_DIR/.env" ]; then + KC_DB_PASSWORD=$(read_password_from_env "$DOCKER_DIR/.env" "KC_DB_PASSWORD") + KEYCLOAK_ADMIN_PASSWORD=$(read_password_from_env "$DOCKER_DIR/.env" "KEYCLOAK_ADMIN_PASSWORD") +fi + +if [ -f "$KEYCLOAK_SETUP_DIR/.env" ]; then + KEYCLOAK_ADMIN_PASSWORD=$(read_password_from_env "$KEYCLOAK_SETUP_DIR/.env" "KEYCLOAK_ADMIN_PASSWORD") # Überschreibt ggf. den Wert aus docker/.env + TESTADMIN_PASSWORD=$(read_password_from_env "$KEYCLOAK_SETUP_DIR/.env" "TESTADMIN_PASSWORD") + TESTUSER_PASSWORD=$(read_password_from_env "$KEYCLOAK_SETUP_DIR/.env" "TESTUSER_PASSWORD") + TESTSERVICEUSER_PASSWORD=$(read_password_from_env "$KEYCLOAK_SETUP_DIR/.env" "TESTSERVICEUSER_PASSWORD") + KEYCLOAK_NEXTCLOUD_CLIENT_SECRET=$(read_password_from_env "$KEYCLOAK_SETUP_DIR/.env" "KEYCLOAK_NEXTCLOUD_CLIENT_SECRET") +fi + +echo ">>> Generiere neue Passwörter für fehlende Werte..." + +# Generate passwords if they are still empty +generate_password_if_empty KEYCLOAK_ADMIN_PASSWORD +generate_password_if_empty KC_DB_PASSWORD +generate_password_if_empty TESTADMIN_PASSWORD +generate_password_if_empty TESTUSER_PASSWORD +generate_password_if_empty TESTSERVICEUSER_PASSWORD +generate_password_if_empty KEYCLOAK_NEXTCLOUD_CLIENT_SECRET + # Date for documentation SETUP_DATE=$(date '+%Y-%m-%d_%H-%M-%S') -# Generate passwords -KEYCLOAK_ADMIN_PASSWORD=$(generate_password) -KC_DB_PASSWORD=$(generate_password) +# Create credentials content +CREDENTIALS_CONTENT=$(cat < "${CREDENTIALS_DIR}/credentials_hash.txt" +echo ">>> Credentials hash stored in: ${CREDENTIALS_DIR}/credentials_hash.txt" + +# Set GPG PASSPHRASE +export GPG_PASSPHRASE=$(generate_password) +# Set GPG agent environment variable +export GPG_TTY=$(tty) + +echo ">>> Trying openssl encryption first" +# Alternative Verschlüsselung mit Openssl +echo "$CREDENTIALS_CONTENT" > "$TEMP_FILE" + if openssl enc -aes-256-cbc -pbkdf2 -salt -in "$TEMP_FILE" -out "${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt.enc" -k "$GPG_PASSPHRASE" ; then + echo ">>> Credentials encrypted successfully using openssl" + mv "${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt.enc" "${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt.gpg" + else + echo ">>> Openssl encryption failed, trying gpg" + + # Attempt to kill existing gpg agent + gpgconf --kill gpg-agent 2>/dev/null + echo ">>> Attempting to manually start gpg-agent with pinentry-curses" + gpg-agent --daemon --pinentry-program /usr/bin/pinentry-curses + gpg-connect-agent /bye 2>/dev/null + eval $(gpg-agent --daemon) + gpg-connect-agent updatestartuptty /bye 2>/dev/null + + # Attempt to encrypt credentials using GPG with error handling + if echo "$CREDENTIALS_CONTENT" | gpg --symmetric --cipher-algo AES256 -vvv -o "${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt.gpg" ; then + echo ">>> Credentials encrypted successfully using gpg." + else + echo ">>> GPG encryption failed. Attempting GPG encryption with password workaround." + # Attempt encryption with passphrase workaround + if echo "$CREDENTIALS_CONTENT" | gpg --batch --passphrase "$GPG_PASSPHRASE" --symmetric --cipher-algo AES256 -vvv -o "${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt.gpg"; then + echo ">>> Credentials encrypted successfully using gpg with passphrase workaround." + else + echo ">>> GPG encryption with passphrase workaround failed" + exit 1 + fi + fi + fi +rm "$TEMP_FILE" # Create .env file in docker directory -cat > "${DOCKER_DIR}/.env" << EOL +DOCKER_ENV_CONTENT=$(cat < "${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt" << EOL -Setup Date: ${SETUP_DATE} +# Create .env file in scripts/setup/keycloak directory +KEYCLOAK_ENV_CONTENT=$(cat <>> Environment setup completed!" -Keycloak Database Credentials: -Username: keycloak -Password: ${KC_DB_PASSWORD} -EOL +# --------------- KEYCLOAK KONFIGURATION --------------- +echo ">>> Keycloak Konfiguration..." +cd "$KEYCLOAK_SETUP_DIR" + +echo ">>> Starte setup_realm.js" +node setup_realm.js + +cd "$PROJECT_ROOT" + +# --------------- NEXTCLOUD KONFIGURATION --------------- +echo ">>> Nextcloud Konfiguration..." + +# Verify if variable is set from earlier in the script +echo ">>> Debug: Checking original variable..." +echo ">>> Debug: KEYCLOAK_NEXTCLOUD_CLIENT_SECRET = ${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}" + +# Try reading from .env file if variable is empty +if [ -z "${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}" ]; then + echo ">>> Debug: Variable is empty, trying to read from .env file..." + KEYCLOAK_NEXTCLOUD_CLIENT_SECRET=$(grep KEYCLOAK_NEXTCLOUD_CLIENT_SECRET "${KEYCLOAK_SETUP_DIR}/.env" | cut -d '=' -f2) + echo ">>> Debug: Value from .env file = ${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}" +fi + +# Ensure we have a value +if [ -z "${KEYCLOAK_NEXTCLOUD_CLIENT_SECRET}" ]; then + echo ">>> Error: Could not get client secret value" + exit 1 +fi + +# Escape special characters in the secret for JSON +ESCAPED_SECRET=$(echo "$KEYCLOAK_NEXTCLOUD_CLIENT_SECRET" | sed 's/["\]/\\&/g') +echo ">>> Debug: Escaped secret = $ESCAPED_SECRET" + +# Create the extra vars +EXTRA_VARS="{\"client_secret\": \"$ESCAPED_SECRET\"}" +echo ">>> Debug: Extra vars = $EXTRA_VARS" -# Encrypt credentials file -gpg --symmetric --cipher-algo AES256 "${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt" -rm "${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt" +# Run Ansible with the extra vars +sudo ansible-playbook \ + -i "$ANSIBLE_INVENTORY" \ + "$ANSIBLE_PLAYBOOK" \ + --extra-vars "$EXTRA_VARS" \ + -v -echo "Environment setup completed!" -echo "Credentials have been saved and encrypted in: ${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt.gpg" -echo ".env file has been created in: ${DOCKER_DIR}/.env" -echo "" -echo "To view credentials, use:" -echo "gpg -d ${CREDENTIALS_DIR}/credentials_${SETUP_DATE}.txt.gpg" \ No newline at end of file +echo ">>> Fertig" \ No newline at end of file diff --git a/scripts/setup/keycloak/.env b/scripts/setup/keycloak/.env index 8c6b3c3..0087b01 100644 --- a/scripts/setup/keycloak/.env +++ b/scripts/setup/keycloak/.env @@ -6,4 +6,6 @@ PAPERLESS_CLIENT_ID=paperless NODERED_CLIENT_ID=nodered TESTADMIN_PASSWORD=TestAdminPwd TESTUSER_PASSWORD=TestUserPwd -KEYCLOAK_NEXTCLOUD_CLIENT_SECRET=D939xgzoxi58T2XZShdUPZP4gsI0kBOu \ No newline at end of file +TESTSERVICEUSER_PASSWORD=TestServiceUserPwd +KEYCLOAK_NEXTCLOUD_CLIENT_SECRET=OSbJ08zyjBWChwBR7S6c1q4sU0d8zvEK +NEXTCLOUD_URL=https://cloud.mrx8086.com \ No newline at end of file diff --git a/scripts/setup/keycloak/setup_realm.js b/scripts/setup/keycloak/setup_realm.js index 9f67b1e..784d543 100644 --- a/scripts/setup/keycloak/setup_realm.js +++ b/scripts/setup/keycloak/setup_realm.js @@ -1,19 +1,26 @@ import dotenv from 'dotenv'; import axios from 'axios'; -// Lade Umgebungsvariablen +// Load environment variables dotenv.config(); +console.log('Environment variables loaded.'); -// Konfigurationskonstanten +// Configuration constants const KEYCLOAK_URL = process.env.KEYCLOAK_URL || 'https://auth.mrx8086.com'; const ADMIN_USERNAME = process.env.KEYCLOAK_ADMIN_USER; const ADMIN_PASSWORD = process.env.KEYCLOAK_ADMIN_PASSWORD; const REALM_NAME = 'office-automation'; -// Client IDs und deren Konfiguration +console.log('Configuration constants set:', { KEYCLOAK_URL, ADMIN_USERNAME, REALM_NAME }); + +// Client IDs and their configuration const CLIENTS = { [process.env.NEXTCLOUD_CLIENT_ID || 'nextcloud']: { - redirectUris: ["https://cloud.mrx8086.com/*"] + redirectUris: [ + `https://cloud.mrx8086.com/apps/sociallogin/custom_oidc/keycloak`, + `https://cloud.mrx8086.com/apps/user_oidc/code` + ], + postLogoutRedirectUris: ["https://cloud.mrx8086.com/*"] }, [process.env.PAPERLESS_CLIENT_ID || 'paperless']: { redirectUris: ["https://docs.mrx8086.com/*"] @@ -23,16 +30,18 @@ const CLIENTS = { } }; -// Hilfsfunktion für API-Fehlerbehandlung +console.log('CLIENTS configuration:', CLIENTS); + +// Helper function for API error handling const handleAxiosError = (error, operation, config, response) => { console.error(`Error during ${operation}:`); if (config) { - console.error('Request:', { - method: config.method, - url: config.url, - headers: config.headers, - data: config.data, - }); + console.error('Request:', { + method: config.method, + url: config.url, + headers: config.headers, + data: config.data, + }); } if (error.response) { console.error('Response:', { @@ -45,8 +54,9 @@ const handleAxiosError = (error, operation, config, response) => { throw error; }; -// Admin Token abrufen +// Get Admin Token async function getAdminToken() { + console.log('Getting admin token...'); try { const response = await axios.post( `${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token`, @@ -62,165 +72,146 @@ async function getAdminToken() { } } ); + console.log('Admin token received.'); return response.data.access_token; } catch (error) { - handleAxiosError(error, 'getting admin token'); + handleAxiosError(error, 'getting admin token'); } } -// Prüfen ob Realm existiert +// Check if Realm exists async function checkRealmExists(token) { + console.log(`Checking if realm ${REALM_NAME} exists...`); try { - await axios.get( - `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}`, - { - headers: { - 'Authorization': `Bearer ${token}` - } - } - ); + await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}`, { + headers: { 'Authorization': `Bearer ${token}` } + }); + console.log(`Realm ${REALM_NAME} exists.`); return true; } catch (error) { if (error.response?.status === 404) { + console.log(`Realm ${REALM_NAME} does not exist.`); return false; } handleAxiosError(error, 'checking realm existence'); } } - -// Funktion um Client Infos abzufragen -async function getClient(token, clientId) { +// Function to get client information by clientId +async function getClientByClientId(token, clientId) { + console.log(`Getting client information for ${clientId}...`); try { - const response = await axios.get( - `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients`, - { - headers: { - 'Authorization': `Bearer ${token}` - }, - params: { - clientId: clientId - } - } - ); + const response = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients`, { + headers: { 'Authorization': `Bearer ${token}` }, + params: { clientId } + }); if (response.data.length === 0) { - console.error(`Client ${clientId} not found`); + console.log(`Client ${clientId} not found`); return null; } - + console.log(`Client ${clientId} found.`); return response.data[0]; } catch (error) { - handleAxiosError(error, `getting client ${clientId}`); + handleAxiosError(error, `getting client ${clientId}`); + return null; } } -// Prüfen ob Client existiert -async function checkClientExists(token, clientId) { - const client = await getClient(token, clientId); - return !!client; -} - +// Check if client exists +const checkClientExists = async (token, clientId) => !!await getClientByClientId(token, clientId); +// Get client mappers by client ID async function getClientMappers(token, clientId) { + console.log(`Getting client mappers for ${clientId}...`); + const client = await getClientByClientId(token, clientId); + if (!client) { + console.log(`Client ${clientId} not found, no mappers to get.`); + return []; + } try { - const client = await getClient(token, clientId); - if (!client) { - return []; - } const response = await axios.get( `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/protocol-mappers/models`, - { - headers: { - 'Authorization': `Bearer ${token}` - } - } + { headers: { 'Authorization': `Bearer ${token}` } } ); + console.log(`Client mappers for ${clientId} retrieved.`); return response.data; } catch (error) { - handleAxiosError(error, `getting client mappers for ${clientId}`, error.config, error.response); + handleAxiosError(error, `getting client mappers for ${clientId}`, error.config, error.response); return []; } } -async function getClientScopes(token, clientId){ +// Get client scopes for a client +async function getClientScopes(token, clientId) { + console.log(`Getting client scopes for ${clientId}...`); + const client = await getClientByClientId(token, clientId); + if (!client) { + console.log(`Client ${clientId} not found, no client scopes to get.`); + return []; + } try { - const client = await getClient(token, clientId); - if(!client) - return []; - const response = await axios.get( `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/client-scopes`, - { - headers: { - 'Authorization': `Bearer ${token}` - } - } + { headers: { 'Authorization': `Bearer ${token}` } } ); - + console.log(`Client scopes for ${clientId} retrieved.`); return response.data; - - } catch(error){ - handleAxiosError(error, `getting client scopes for ${clientId}`, error.config, error.response); + } catch (error) { + handleAxiosError(error, `getting client scopes for ${clientId}`, error.config, error.response); return []; } } +// Get a specific client scope by name async function getClientScope(token, scopeName) { + console.log(`Getting client scope "${scopeName}"...`); try { const response = await axios.get( `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes`, - { - headers: { - 'Authorization': `Bearer ${token}` - }, - params: { - name: scopeName - } - } + { headers: { 'Authorization': `Bearer ${token}` } } ); - - if(response.data.length === 0){ - console.error(`Client Scope ${scopeName} not found`); + const foundScope = response.data.find(scope => scope.name === scopeName); + if (!foundScope) { + console.log(`Client Scope "${scopeName}" not found`); return null; } - - return response.data[0] - } catch (error){ + console.log(`Client scope "${scopeName}" found:`, foundScope); + return foundScope; + } catch (error) { handleAxiosError(error, `getting client scope ${scopeName}`, error.config, error.response); return null; } } -async function addDefaultClientScope(token, clientId, scopeName){ +// Add a default client scope to a client +async function addDefaultClientScope(token, clientId, scopeName) { + console.log(`Adding client scope "${scopeName}" as default for client "${clientId}"...`); + const client = await getClientByClientId(token, clientId); + const scope = await getClientScope(token, scopeName); + if (!client || !scope) { + console.log(`Client "${clientId}" or scope "${scopeName}" not found, cannot add as default scope.`); + return; + } try { - const client = await getClient(token, clientId); - const scope = await getClientScope(token, scopeName); - if(!client || !scope){ - return null; - } - - await axios.put( - `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/default-client-scopes/${scope.id}`, - null, - { - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - } - } + `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/default-client-scopes/${scope.id}`, + null, + { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } ); - - console.log(`Client scope ${scopeName} added as default scope for client ${clientId}`) - - } catch(error){ - handleAxiosError(error, `adding client scope ${scopeName} as default for client ${clientId}`); + console.log(`Client scope "${scopeName}" added as default scope for client "${clientId}"`); + } catch (error) { + handleAxiosError(error, `adding client scope "${scopeName}" as default for client "${clientId}"`); } } - -// Realm erstellen +// Create Realm async function createRealm(token) { + console.log(`Creating realm ${REALM_NAME}...`); const realmConfig = { realm: REALM_NAME, enabled: true, @@ -250,67 +241,47 @@ async function createRealm(token) { webAuthnPolicyUserVerificationRequirement: "preferred", webAuthnPolicyCreateTimeout: 0, webAuthnPolicyAvoidSameAuthenticatorRegister: false, - defaultDefaultClientScopes: [ - "email", - "profile", - "roles", - "web-origins" - ], - defaultOptionalClientScopes: [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] + defaultDefaultClientScopes: ["email", "profile", "roles", "web-origins"], + defaultOptionalClientScopes: ["address", "phone", "offline_access", "microprofile-jwt"] }; - try { - await axios.post( - `${KEYCLOAK_URL}/admin/realms`, - realmConfig, - { - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - } + await axios.post(`${KEYCLOAK_URL}/admin/realms`, realmConfig, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' } - ); + }); console.log('Realm created successfully'); } catch (error) { handleAxiosError(error, 'creating realm'); } } -// Client erstellen +// Create client and manage mappers async function createClient(token, clientId, clientName, redirectUris) { - let client; - const clientExists = await checkClientExists(token, clientId); + console.log(`Creating client "${clientId}"...`); + let client = await getClientByClientId(token, clientId); - if (!clientExists) { + if (!client) { const clientConfig = { clientId: clientId, name: clientName, enabled: true, protocol: "openid-connect", publicClient: false, - authorizationServicesEnabled: true, - serviceAccountsEnabled: true, + authorizationServicesEnabled: false, + serviceAccountsEnabled: false, standardFlowEnabled: true, implicitFlowEnabled: false, directAccessGrantsEnabled: true, redirectUris: redirectUris, - webOrigins: ["+"], - defaultClientScopes: [ - "roles" - ], - optionalClientScopes: [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] + webOrigins: ["+"], + defaultClientScopes: ["roles"], + optionalClientScopes: ["address", "phone", "offline_access", "microprofile-jwt"], + rootUrl: process.env.NEXTCLOUD_URL, + baseUrl: process.env.NEXTCLOUD_URL, + adminUrl: process.env.NEXTCLOUD_URL, }; - try { const response = await axios.post( `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients`, @@ -322,21 +293,34 @@ async function createClient(token, clientId, clientName, redirectUris) { } } ); - console.log(`Client ${clientId} created successfully`); + console.log(`Client "${clientId}" created successfully`); client = response.data; } catch (error) { handleAxiosError(error, `creating client: ${clientId}`); return; } } else { - client = await getClient(token, clientId); - console.log(`Client ${clientId} already exists, checking mappers`); + console.log(`Client "${clientId}" already exists, checking mappers`); } - if (client) { - - const existingMappers = await getClientMappers(token, clientId) + if (client) { + try { + await axios.put( + `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}`, + { ...client, secret: process.env[`KEYCLOAK_${clientId.replace(/[^a-zA-Z0-9]/g, '').toUpperCase()}_CLIENT_SECRET`] }, + { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + ); + console.log(`Set client secret for client: ${clientId}`); + } catch (error) { + handleAxiosError(error, `setting client secret for client: ${clientId}`, error.config, error.response); + } + const existingMappers = await getClientMappers(token, clientId); const requiredMappers = [ { name: "groups", @@ -367,10 +351,8 @@ async function createClient(token, clientId, clientName, redirectUris) { for (const mapper of requiredMappers) { const existingMapper = existingMappers.find(m => m.name === mapper.name); - try { if (existingMapper) { - // Update existierenden Mapper await axios.put( `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/protocol-mappers/models/${existingMapper.id}`, { ...existingMapper, ...mapper }, @@ -381,9 +363,8 @@ async function createClient(token, clientId, clientName, redirectUris) { } } ); - console.log(`Mapper ${mapper.name} updated for client ${clientId}`); + console.log(`Mapper "${mapper.name}" updated for client "${clientId}"`); } else { - // Erstelle neuen Mapper await axios.post( `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/protocol-mappers/models`, mapper, @@ -394,70 +375,69 @@ async function createClient(token, clientId, clientName, redirectUris) { } } ); - console.log(`Mapper ${mapper.name} created for client ${clientId}`); + console.log(`Mapper "${mapper.name}" created for client "${clientId}"`); } } catch (error) { - handleAxiosError(error, `managing mapper ${mapper.name} for client ${clientId}`, error.config, error.response); - // Wir werfen den Fehler nicht weiter, damit andere Mapper noch verarbeitet werden können + handleAxiosError(error, `managing mapper "${mapper.name}" for client "${clientId}"`, error.config, error.response); + } + } + + if (clientId.includes("nextcloud")) { + await addDefaultClientScope(token, clientId, "openid"); + await addDefaultClientScope(token, clientId, "profile"); + await addDefaultClientScope(token, clientId, "groups-nextcloud"); + + try { + await axios.put( + `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}`, + { ...client, defaultClientScopes: client.defaultClientScopes.filter(c => c !== "nextcloud-dedicated") }, + { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + ); + console.log(`Removed client scope nextcloud-dedicated from client: ${clientId}`); + } catch (error) { + handleAxiosError(error, `removing client scope nextcloud-dedicated from client: ${clientId}`, error.config, error.response); } } } } - -// Gruppen erstellen +// Create default groups async function createDefaultGroups(token) { + console.log('Creating default groups...'); const groups = [ - { - name: "Administrators", - path: "/Administrators", - attributes: { - "description": ["Full system access"] - } - }, - { - name: "Users", - path: "/Users", - attributes: { - "description": ["Regular system users"] - } - } + { name: "nextcloud-admins", path: "/nextcloud-admins", attributes: { "description": ["Nextcloud administrators"] } }, + { name: "nextcloud-users", path: "/nextcloud-users", attributes: { "description": ["Nextcloud regular users"] } }, + { name: "nextcloud-youpi", path: "/nextcloud-youpi", attributes: { "description": ["Nextcloud youpi"] } }, + { name: "nextcloud-service", path: "/nextcloud-service", attributes: { "description": ["Nextcloud service user"] } } ]; for (const group of groups) { try { - // Prüfen ob Gruppe existiert - const existingGroup = await axios.get( - `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/groups`, - { - headers: { - 'Authorization': `Bearer ${token}` - }, - params: { - search: group.name, - } - } - ); + const existingGroups = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/groups`, { + headers: { 'Authorization': `Bearer ${token}` }, + params: { search: group.name, exact: true } // Added exact: true for precise matching + }); - if (existingGroup.data.length > 0) { - console.log(`Group ${group.name} already exists`); + if (existingGroups.data.length > 0) { + console.log(`Group "${group.name}" already exists`); continue; } - await axios.post( - `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/groups`, - group, - { - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - } + await axios.post(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/groups`, group, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' } - ); - console.log(`Group ${group.name} created successfully`); + }); + console.log(`Group "${group.name}" created successfully`); } catch (error) { if (error.response?.status === 409) { - console.log(`Group ${group.name} already exists`); + console.log(`Group "${group.name}" already exists`); } else { handleAxiosError(error, `creating group: ${group.name}`); } @@ -465,44 +445,44 @@ async function createDefaultGroups(token) { } } - +// Create a test token for a user async function createTestToken(token, username) { + console.log(`Creating test token for user "${username}"...`); try { - const nextcloudClientId = Object.keys(CLIENTS).find(key => key.includes('nextcloud')) || 'nextcloud'; - const client = await getClient(token, nextcloudClientId); + const nextcloudClientId = Object.keys(CLIENTS).find(key => key.includes('nextcloud')) || 'nextcloud'; + const client = await getClientByClientId(token, nextcloudClientId); - if (!client) + if (!client) { + console.log(`Client "${nextcloudClientId}" not found, cannot create test token.`); return null; + } const response = await axios.post( `${KEYCLOAK_URL}/realms/${REALM_NAME}/protocol/openid-connect/token`, new URLSearchParams({ 'client_id': nextcloudClientId, - 'client_secret': process.env.KEYCLOAK_NEXTCLOUD_CLIENT_SECRET, + 'client_secret': process.env[`KEYCLOAK_${nextcloudClientId.replace(/[^a-zA-Z0-9]/g, '').toUpperCase()}_CLIENT_SECRET`], 'username': username, 'password': process.env.TESTADMIN_PASSWORD || "initial123!", 'grant_type': 'password' }), - { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - } - } + { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } ); + console.log(`Test token for user "${username}" created.`); return response.data.access_token; } catch (error) { handleAxiosError(error, `getting test token for ${username}`, error.config, error.response); + return null; } } -// Funktion zum Decodieren eines JWT-Tokens +// Function to decode a JWT token function decodeToken(token) { try { const base64Url = token.split('.')[1]; const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); - const jsonPayload = decodeURIComponent(atob(base64).split('').map(function (c) { - return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); - }).join('')); - + const jsonPayload = decodeURIComponent(atob(base64).split('').map(c => + '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2) + ).join('')); return JSON.parse(jsonPayload); } catch (error) { console.error("Error decoding token:", error.message); @@ -510,87 +490,128 @@ function decodeToken(token) { } } -// Test-User erstellen +// Create initial users async function createInitialUsers(token) { + console.log('Creating initial users...'); const users = [ - { - username: "testadmin", - enabled: true, - emailVerified: true, - firstName: "Test", - lastName: "Admin", - email: "testadmin@mrx8086.com", - credentials: [{ - type: "password", - value: process.env.TESTADMIN_PASSWORD || "initial123!", - temporary: true - }], - groups: ["/Administrators"] - }, - { - username: "testuser", - enabled: true, - emailVerified: true, - firstName: "Test", - lastName: "User", - email: "testuser@mrx8086.com", - credentials: [{ - type: "password", - value: process.env.TESTUSER_PASSWORD || "initial123!", - temporary: true - }], - groups: ["/Users"] - } + { username: "testadmin", enabled: true, emailVerified: true, firstName: "Test", lastName: "Admin", email: "testadmin@mrx8086.com", credentials: [{ type: "password", value: process.env.TESTADMIN_PASSWORD || "initial123!", temporary: false }], groups: ["nextcloud-admins", "nextcloud-users"] }, + { username: "testuser", enabled: true, emailVerified: true, firstName: "Test", lastName: "User", email: "testuser@mrx8086.com", credentials: [{ type: "password", value: process.env.TESTUSER_PASSWORD || "initial123!", temporary: false }], groups: ["nextcloud-users", "nextcloud-youpi"] }, + { username: "testserviceuser", enabled: true, emailVerified: true, firstName: "Test", lastName: "Service User", email: "testserviceuser@mrx8086.com", credentials: [{ type: "password", value: process.env.TESTSERVICEUSER_PASSWORD || "initial123!", temporary: false }], groups: ["nextcloud-service"] } ]; for (const user of users) { try { - // Prüfen ob User existiert - const existingUsers = await axios.get( - `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/users`, - { - headers: { - 'Authorization': `Bearer ${token}` - }, - params: { - username: user.username, - exact: true - } - } - ); + const existingUsers = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/users`, { + headers: { 'Authorization': `Bearer ${token}` }, + params: { username: user.username, exact: true } // Added exact: true for precise matching + }); if (existingUsers.data.length > 0) { - console.log(`User ${user.username} already exists`); + console.log(`User "${user.username}" already exists`); continue; } - // User erstellen - await axios.post( - `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/users`, - user, + await axios.post(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/users`, user, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + console.log(`User "${user.username}" created successfully`); + + } catch (error) { + handleAxiosError(error, `creating user: ${user.username}`, error.config, error.response); + } + } +} + +async function createGroupsNextcloudScope(token) { + const scopeName = "groups-nextcloud"; + const mapperName = "groups-mapper"; + console.log(`Starting createGroupsNextcloudScope`); + let clientScope = await getClientScope(token, scopeName); + + if (!clientScope) { + try { + console.log(`Creating client scope "${scopeName}"`); + clientScope = await axios.post(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes`, + { + "name": scopeName, + "protocol": "openid-connect", + "description": "Provides access to user group information for Nextcloud.", // Hinzugefügt: Beschreibung + "attributes": {}, + "consentScreenText": "Grant access to user groups in Nextcloud", + "includeInTokenScope": true + }, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } - } - ); - console.log(`User ${user.username} created successfully`); + }); + console.log(`Client scope "${scopeName}" created successfully`); + clientScope = response.data; + } catch (error) { + console.error(`Error creating client scope "${scopeName}":`, error); + handleAxiosError(error, `creating ${scopeName} client scope`, error.config, error.response); + return; + } + } else { + console.log(`Client scope "${scopeName}" exists, skipping creation`); + } + console.log("Client scope creation step finished"); + if (clientScope) { + console.log(`Check for mapper "${mapperName}" in scope "${scopeName}"`); + try { + const mappersResponse = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes/${clientScope.id}/protocol-mappers/models`, + { headers: { 'Authorization': `Bearer ${token}` } } + ); + if (!mappersResponse.data.find(m => m.name === mapperName)) { + try { + await axios.post(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes/${clientScope.id}/protocol-mappers/models`, + { + "name": mapperName, + "protocol": "openid-connect", + "protocolMapper": "oidc-group-membership-mapper", + "config": { + "full.path": "false", + "id.token.claim": "false", + "access.token.claim": "true", + "userinfo.token.claim": "true", + "claim.name": "groups", + "add.to.introspection": "false" + } + }, + { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + console.log(`Mapper "${mapperName}" created for client scope "${scopeName}"`); + } catch (error) { + console.error(`Error creating mapper "${mapperName}" for scope "${scopeName}":`, error); + handleAxiosError(error, `creating mapper for ${scopeName}`, error.config, error.response); + } + } else { + console.log(`Mapper "${mapperName}" exists in client scope "${scopeName}", skipping creation`); + } } catch (error) { - handleAxiosError(error, `creating user: ${user.username}`, error.config, error.response); + console.error("Error checking for mappers:", error); + handleAxiosError(error, `checking mappers for ${scopeName}`, error.config, error.response); } } + console.log("Finished createGroupsNextcloudScope"); } - -// Hauptfunktion +// Main function async function setupRealm() { try { console.log('Starting Keycloak setup...'); const token = await getAdminToken(); - // Prüfe ob Realm existiert + // Check if realm exists const realmExists = await checkRealmExists(token); if (!realmExists) { @@ -600,41 +621,37 @@ async function setupRealm() { console.log('Realm already exists, skipping base setup'); } - // Clients erstellen + // Create client scope groups-nextcloud + await createGroupsNextcloudScope(token); + + // Create clients for (const clientId in CLIENTS) { await createClient(token, clientId, clientId, CLIENTS[clientId].redirectUris); } - const nextcloudClientId = Object.keys(CLIENTS).find(key => key.includes('nextcloud')) || 'nextcloud'; - await addDefaultClientScope(token, nextcloudClientId, "openid"); - - // Gruppen erstellen + // Create groups await createDefaultGroups(token); - // Test User erstellen + // Create test users await createInitialUsers(token); - - // Konfiguration des Office-Automation Realms mit Admin Token auslesen + // Read the configuration of the Office-Automation realm with Admin Token if (token) { console.log("Master Realm Admin Token:", token); - try { const realmConfig = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}`, { - headers: { - 'Authorization': `Bearer ${token}` - } + headers: { 'Authorization': `Bearer ${token}` } }); - console.log("Office Automation Realm Configuration:", realmConfig.data) + console.log("Office Automation Realm Configuration:", realmConfig.data); } catch (error) { handleAxiosError(error, 'getting office realm configuration', error.config, error.response); } } else { - console.error("Error getting Master Realm admin token") + console.error("Error getting Master Realm admin token"); } - // Test Token erstellen - const testToken = await createTestToken(token, "testadmin@mrx8086.com"); + // Create Test Token + const testToken = await createTestToken(token, "testadmin"); if (testToken) { console.log("Test Token generated successfully!"); @@ -644,7 +661,6 @@ async function setupRealm() { } else { console.error("Error generating Test Token"); } - console.log('Setup completed successfully'); } catch (error) { @@ -653,5 +669,5 @@ async function setupRealm() { } } -// Script ausführen +// Execute the script setupRealm(); \ No newline at end of file diff --git a/scripts/setup/keycloak/test_realm.js b/scripts/setup/keycloak/test_realm.js index 78c03a4..11e3254 100644 --- a/scripts/setup/keycloak/test_realm.js +++ b/scripts/setup/keycloak/test_realm.js @@ -6,7 +6,7 @@ dotenv.config(); const KEYCLOAK_URL = process.env.KEYCLOAK_URL || 'https://auth.mrx8086.com'; const NEXTCLOUD_CLIENT_ID = process.env.NEXTCLOUD_CLIENT_ID || 'nextcloud'; const TESTADMIN_USERNAME = "testadmin@mrx8086.com"; -const TESTADMIN_PASSWORD = process.env.TESTADMIN_PASSWORD || "initial123!"; +const TESTADMIN_PASSWORD = process.env.TESTADMIN_PASSWORD; const REALM_NAME = 'office-automation'; const CLIENT_SECRET = process.env.KEYCLOAK_NEXTCLOUD_CLIENT_SECRET; @@ -74,28 +74,33 @@ function decodeToken(token) { } } +// Prüfe ob ein Admin Token korrekt generiert werden kann async function testKeycloakLogin() { - try { - const accessToken = await getAccessToken(TESTADMIN_USERNAME, TESTADMIN_PASSWORD); + try { + const accessToken = await getAccessToken(TESTADMIN_USERNAME, TESTADMIN_PASSWORD); - if (!accessToken) { - console.error('Failed to get access token.'); - return; - } - console.log('Access Token:', accessToken); - const decodedToken = decodeToken(accessToken); - if (decodedToken) { - console.log('Decoded Access Token:', decodedToken); - if (decodedToken.groups.includes('/Administrators')){ - console.log("Admin Group is set correctly!") - } else { - console.error("Admin Group is not set correctly!") - } - } + if (!accessToken) { + console.error('Failed to get access token.'); + return; + } + console.log('Access Token:', accessToken); + const decodedToken = decodeToken(accessToken); + if(decodedToken) { + console.log('Decoded Access Token:', decodedToken); + if (Array.isArray(decodedToken.groups) && decodedToken.groups.includes('/nextcloud-admins')){ + console.log("Admin Group is set correctly!") + } else if (typeof decodedToken.groups === 'string' && decodedToken.groups.includes('/nextcloud-admins')) { + console.log("Admin Group is set correctly!") + } + else { + console.error("Admin Group is not set correctly!") + } + } - } catch (error) { - console.error('An error occurred:', error); - } + + } catch (error) { + console.error('An error occurred:', error); + } } testKeycloakLogin(); \ No newline at end of file