diff --git a/automation/dbildungs-iam-server/Chart.lock b/automation/dbildungs-iam-server/Chart.lock index 914bf7129..17bbda516 100644 --- a/automation/dbildungs-iam-server/Chart.lock +++ b/automation/dbildungs-iam-server/Chart.lock @@ -3,4 +3,4 @@ dependencies: repository: https://charts.bitnami.com/bitnami version: 11.0.6 digest: sha256:790bafa04fe9c1cc9f772dc12fada16eb847c282f738fd23df09f665af93ec74 -generated: "2024-11-20T10:46:55.616226106Z" +generated: "2024-11-20T10:56:41.410289808Z" diff --git a/automation/dbildungs-iam-server/Chart.yaml b/automation/dbildungs-iam-server/Chart.yaml index db4326885..6e05cbef7 100644 --- a/automation/dbildungs-iam-server/Chart.yaml +++ b/automation/dbildungs-iam-server/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v2 -appVersion: SPSH-1288 +appVersion: DBP-1022 dependencies: - condition: redis-cluster.enabled name: redis-cluster @@ -8,4 +8,4 @@ dependencies: description: dBildungs-IAM-server name: dbildungs-iam-server type: application -version: 0.0.0-spsh-1288-20241120-1046 +version: 0.0.0-dbp-1022-20241120-1056 diff --git a/automation/dbildungs-iam-server/cron/Dockerfile b/automation/dbildungs-iam-server/cron/Dockerfile new file mode 100644 index 000000000..7f7575d79 --- /dev/null +++ b/automation/dbildungs-iam-server/cron/Dockerfile @@ -0,0 +1,23 @@ +FROM alpine:3.19 + +# Install necessary packages +RUN apk update && \ + apk add --no-cache bash cronie curl jq openssl vim + +# Create a new user and group +RUN addgroup -S appgroup && adduser -S appuser -G appgroup + +# Copy scripts into the image +COPY scripts/ /scripts/ + +# Set execute permissions for all .sh scripts in /scripts/ and create a log file +RUN chmod +x /scripts/*.sh \ + && touch /var/log/cron.log \ + && chmod 644 /var/log/cron.log \ + && chown -R appuser:appgroup /scripts /var/log/cron.log + +# Switch to the new user +USER appuser + +# Start the cron service in foreground +CMD ["/usr/sbin/crond", "-f"] diff --git a/automation/dbildungs-iam-server/cron/keys/dummy_jwks.json b/automation/dbildungs-iam-server/cron/keys/dummy_jwks.json new file mode 100644 index 000000000..85b1e9d25 --- /dev/null +++ b/automation/dbildungs-iam-server/cron/keys/dummy_jwks.json @@ -0,0 +1,15 @@ +{ + "keys": [ + { + "kty": "RSA", + "n": "23mzd3v4YjgWMzO7XYYwD92NqCm436ErU1-NPTVok9aaVXx5mjZKfh_Xoyp5BEgjQU042MKOhl1Ri17HfOOf6k4cpBpvBQhENp0yfNPv_-kSy4OgdA3qk9-kZyvuRX1-0LKvJMwlrCLCLEfiv_yn8YQLpQeqgdIj1AlX37fcnSxxL3qukM_-Hm8dCB2mbUzANT_uRkSCHFQWVDxcbocKAmhr0808CmpiINWEIVv7AhS_HVSliaeB-iteAKN3W9Am3tCtGZaWKUlioKueQux7OTKxHm5fM-jZ9ZPnb7_RQlOGV9vu-TTMO8pKYkqn15LcnYuBKKHmFEBO8vRxI9_8Lw", + "e": "AQAB", + "d": "CdcSByhlbC9BkjgejW89FmkDjJJE-gR63HkV7F70T7SOejNjga4vdtTXUTclR94yyR8SORMNWtQyRMJnb_UGBXZNGG_K9yR2EntyeQrzjBDCHqJ0fjTlheMVYZQkUbSdC_RcSpUQl1V-STKhOvmz-e3Gq-Evxt70wPFOTEyCYAA5zTSgF7vwoxtKChfOb3NvkLUmD4JrBEb0vzapTgVvoyB158glUGEibpHBaVvVnA98qEI5hqJE2jhhtaoGyvErIkWDOummb1WPN2D0Nqsvr-sfwH3mxKFLDogHIfjMLxDaP9Y3I7Wwie9pbpsg6zK66s6EB27hkZnbRLlwaK4ImQ", + "p": "_KCzohdV8BpnvfDxyL-Zjj8paJB5RBLkewf7xl-sqLHykjn-_nR1OGfEr8Gc0zwYD6FtTAJ9JN-h730vBacUVZDrgnKOW0NbQPIwNXCSisyChhbkSVXLBi94r_-t92ieJ8wPbchynF6Z1UyH0m4rieKnAPcxuio9iLuXdQrRNEs", + "q": "3me1bHQ_GO5mPKwUf-kSZDguninq98ERMOAYdr__yUM1fc8QJ_3FSkZsSFr91Fi5kPvP9gthPRYhlfKeix61ibypLnLpyx6A298VIdG8VFjPrXzlme5CGSPYN9-YRSQq31e-xSdkn3lKiJlqPZzlRARyHveJlSWu07LuS91AgS0", + "dp": "--U1GEOSchWyKaeNPrElaLu8C0I7WFBKOA7u0o9ldtPwXjOr-Yaftz1o1iMEv29lQnigpbC5ncHLEyRMdaNyWBtnaSvWnFNeMzUKMs7rn7Bp2VAMEr-T77f36-3SRiavxFjpbXr4JMkDNLbZm0405Yj1IrZYhBtIPgVm8NJ3ZV8", + "dq": "ofqgbKvBZLQEq_2cNIiYh3tPoIvhAK6Riao8xwgREBEt_UH4f1fY_76IkK4MnkI8bHapwIYLPQVIUsBQbfxgtT89bIHu-qttqDUyW944Lqo8HxuO0WxwoYS0rgTgDsNHokByxX5qT6dz_EbX1KXXaJFgWGNqxcCbMr3nxkMO_sU", + "qi": "r8ZslmjXzZJUv6IoN6nUT12UpzmhbriRXxjTcLNSwZBuSXz8QV_7F8ViNyEcot20aDo35t8IssLnDD9nxDAGTCL68FkXTJaAsUE2beGfkX9Sz5r_Gzlcer_Gjhl5aNHeZYgIMsYciPhM4laBzKD3d51xQuDFMMX1RQUvyDHDIog" + } + ] +} diff --git a/automation/dbildungs-iam-server/cron/scripts/cron_trigger.sh b/automation/dbildungs-iam-server/cron/scripts/cron_trigger.sh new file mode 100644 index 000000000..0f4487541 --- /dev/null +++ b/automation/dbildungs-iam-server/cron/scripts/cron_trigger.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +# Check if BACKEND_ENDPOINT_URL is set +if [ -z "$BACKEND_ENDPOINT_URL" ]; then + echo "Error: BACKEND_ENDPOINT_URL is not set." + exit 1 +fi + +# Check if HTTP_METHOD is set +if [ -z "$HTTP_METHOD" ]; then + echo "Error: HTTP_METHOD is not set." + exit 1 +fi + +endpoint_url="${BACKEND_ENDPOINT_URL}" + +echo "Triggering $endpoint_url with $HTTP_METHOD at $(date)" + +# Call get_access_token.sh and capture the access token +access_token=$(./get_access_token.sh) + +# Make the request with JWT authorization and capture the HTTP status code and response body +response=$(curl -s -w "\n%{http_code}" -X "$HTTP_METHOD" "$endpoint_url" \ + -H "Authorization: Bearer $access_token" \ + -H "Content-Type: application/json") + +# Split the response into body and status code +response_body=$(echo "$response" | sed '$d') +http_status=$(echo "$response" | tail -n1) + +# Output the response details +echo "Finished triggering $endpoint_url with $HTTP_METHOD at $(date) +HTTP Status: $http_status +Response Body: $response_body" + +# Exit with status 1 if the HTTP status code is not 200 +if [ "$http_status" -ne 200 ]; then + echo "Error: HTTP request failed with status code $http_status" + exit 1 +fi diff --git a/automation/dbildungs-iam-server/cron/scripts/get_access_token.sh b/automation/dbildungs-iam-server/cron/scripts/get_access_token.sh new file mode 100644 index 000000000..366643e24 --- /dev/null +++ b/automation/dbildungs-iam-server/cron/scripts/get_access_token.sh @@ -0,0 +1,172 @@ +#!/bin/bash + +# Ensure the script exits on any error +set -e + +# Function to perform base64 URL encoding +base64url_encode() { + # Base64 encode the input, replace '+' with '-', '/' with '_', and remove padding '=' + echo -n "$1" | openssl enc -base64 -A | tr '+/' '-_' | tr -d '=' +} + +base64url_decode() { + local input=$1 + # Replace '-' with '+', '_' with '/' + input=$(echo "$input" | sed 's/-/+/g; s/_/\//g') + # Calculate the required padding + local padding=$(( (4 - ${#input} % 4) % 4 )) + # Add padding if necessary + input="$input$(printf '=%.0s' $(seq 1 $padding))" + echo "$input" | base64 -d +} + +# Function to decode base64url and convert to hex, preserving leading zeros +decode_to_hex() { + base64url_decode "$1" | hexdump -v -e '/1 "%02x"' +} + +# Generate a random string for 'jti' claim +generate_jti() { + head /dev/urandom | tr -dc A-Za-z0-9 | head -c 13 +} + +# Load environment variables +clientId="${KC_CLIENT_ID}" +kc_token_url="${KC_TOKEN_URL}" + +# Load JWKS from environment variable or file +if [ -n "$JWKS" ]; then + # JWKS is set in the environment, use it directly + jwks="$JWKS" +elif [ -n "$JWKS_FILE_PATH" ] && [ -f "$JWKS_FILE_PATH" ]; then + # JWKS_FILE_PATH is set, use the file + jwks=$(cat "$JWKS_FILE_PATH") +else + echo "Error: No JWKS environment variable or JWKS file found." >> /var/log/cron.log + exit 1 +fi + +# Check if environment variables are set +if [[ -z "$clientId" || -z "$kc_token_url" || -z "$jwks" ]]; then + echo "Error: CLIENT_ID, TOKEN_URL, and JWKS environment variables must be set." >> /var/log/cron.log + exit 1 +fi + +# Extract the first key from the JWKS +key_json=$(echo "$jwks" | jq -c '.keys[0]') + +# Check if key_json is empty +if [[ -z "$key_json" ]]; then + echo "Error: No keys found in JWKS." >> /var/log/cron.log + exit 1 +fi + +# Extract RSA components from JWK +n=$(echo "$key_json" | jq -r '.n') +e=$(echo "$key_json" | jq -r '.e') +d=$(echo "$key_json" | jq -r '.d') +p=$(echo "$key_json" | jq -r '.p') +q=$(echo "$key_json" | jq -r '.q') +dp=$(echo "$key_json" | jq -r '.dp') +dq=$(echo "$key_json" | jq -r '.dq') +qi=$(echo "$key_json" | jq -r '.qi') + +# Decode the base64url-encoded components and convert to hex +n_dec=$(decode_to_hex "$n") +e_dec=$(decode_to_hex "$e") +d_dec=$(decode_to_hex "$d") +p_dec=$(decode_to_hex "$p") +q_dec=$(decode_to_hex "$q") +dp_dec=$(decode_to_hex "$dp") +dq_dec=$(decode_to_hex "$dq") +qi_dec=$(decode_to_hex "$qi") + +# Create an ASN.1 structure for the RSA private key +asn1_structure=$(mktemp) + +cat > "$asn1_structure" <> /var/log/cron.log + +# Generate the PEM-formatted private key +temp_key_file=$(mktemp) +openssl asn1parse -genconf "$asn1_structure" -out "$temp_key_file" > /dev/null 2>&1 +openssl rsa -in "$temp_key_file" -inform DER -outform PEM -out "$temp_key_file.pem" > /dev/null 2>&1 + +echo "Ending to generate PEM-formatted private key" >> /var/log/cron.log + +# Remove temporary files +rm "$asn1_structure" "$temp_key_file" + +# Create JWT header +header='{"alg":"RS256","typ":"JWT"}' +header_base64=$(base64url_encode "$header") + +# Create JWT payload +current_time=$(date +%s) +exp_time=$((current_time + 300)) # Token valid for 5 minutes +jti=$(generate_jti) + +payload=$(cat <> /var/log/cron.log + +# Sign the JWT +signature=$(echo -n "$header_payload" | \ + openssl dgst -sha256 -sign "$temp_key_file.pem" | \ + openssl enc -base64 -A | tr '+/' '-_' | tr -d '=') + +echo "Signed the JWT" >> /var/log/cron.log + +# Remove the temporary PEM key file +rm "$temp_key_file.pem" + +# Create the JWT assertion +jwt_assertion="$header_payload.$signature" + +# Make the POST request to Keycloak to get the access token +response=$(curl -s -X POST "$kc_token_url" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials" \ + -d "client_id=$clientId" \ + -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ + -d "client_assertion=$jwt_assertion") + +echo "Access token requested" >> /var/log/cron.log + +# Check if the response contains an access token +if echo "$response" | grep -q '"access_token"'; then + # Extract the access token from the response + access_token=$(echo "$response" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p') + echo "$access_token" +else + echo "Failed to retrieve access token. Response:" >> /var/log/cron.log + echo "$response" >> /var/log/cron.log + exit 1 +fi diff --git a/automation/dbildungs-iam-server/templates/configmap.yaml b/automation/dbildungs-iam-server/templates/configmap.yaml index 6b8d8ac73..d98ee1a8d 100644 --- a/automation/dbildungs-iam-server/templates/configmap.yaml +++ b/automation/dbildungs-iam-server/templates/configmap.yaml @@ -17,4 +17,4 @@ data: FRONTEND_LOGOUT_REDIRECT: "https://{{ .Values.backendHostname }}/" BACKEND_HOSTNAME: "{{ .Values.backendHostname }}" LDAP_URL: '{{ .Values.ldap.url | replace "spsh-xxx" .Release.Namespace }}' - LDAP_BIND_DN: "{{ .Values.ldap.bindDN }}" + LDAP_BIND_DN: "{{ .Values.ldap.bindDN }}" \ No newline at end of file diff --git a/automation/dbildungs-iam-server/templates/cronjob-envs-configmap.yaml b/automation/dbildungs-iam-server/templates/cronjob-envs-configmap.yaml new file mode 100644 index 000000000..2d7158c75 --- /dev/null +++ b/automation/dbildungs-iam-server/templates/cronjob-envs-configmap.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "common.names.name" . }}-cronjob-envs-configmap + namespace: {{ template "common.names.namespace" . }} + labels: + {{- include "common.labels" . | nindent 4 }} +data: + KC_TOKEN_URL: "https://{{ $.Values.keycloakHostname }}{{ $.Values.cronjobs.keycloakTokenUrl }}" + JWKS_FILE_PATH: "{{ $.Values.cronjobs.jwksFilePath }}" + KC_CLIENT_ID: "{{ $.Values.cronjobs.keycloakClientId }}" \ No newline at end of file diff --git a/automation/dbildungs-iam-server/templates/cronjob-scripts-configmap.yaml b/automation/dbildungs-iam-server/templates/cronjob-scripts-configmap.yaml new file mode 100644 index 000000000..4c2278c5c --- /dev/null +++ b/automation/dbildungs-iam-server/templates/cronjob-scripts-configmap.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "common.names.name" . }}-cronjob-scripts-configmap + namespace: {{ template "common.names.namespace" . }} + labels: {{- include "common.labels" . | nindent 4 }} +type: Opaque +data: + get_access_token.sh: |- + {{ .Files.Get "cron/scripts/get_access_token.sh" | nindent 4 }} + cron_trigger.sh: |- + {{ .Files.Get "cron/scripts/cron_trigger.sh" | nindent 4 }} diff --git a/automation/dbildungs-iam-server/templates/cronjob.yaml b/automation/dbildungs-iam-server/templates/cronjob.yaml new file mode 100644 index 000000000..82639d7e1 --- /dev/null +++ b/automation/dbildungs-iam-server/templates/cronjob.yaml @@ -0,0 +1,87 @@ +{{- if .Values.cronjobs.enabled }} +{{- range $job_name, $job_options := .Values.cronjobs.jobs }} +apiVersion: batch/v1 +kind: CronJob +metadata: + name: {{ template "common.names.name" $ }}-{{ $job_name}} + namespace: {{ template "common.names.namespace" $ }} +spec: + schedule: {{ $job_options.schedule }} + startingDeadlineSeconds: 300 + jobTemplate: + spec: + backoffLimit: 0 + template: + metadata: + labels: + cron: {{ $job_name }} + spec: + automountServiceAccountToken: false + containers: + - name: {{ $job_name }} + image: "{{ $.Values.cronjobs.image.repository }}:{{ $.Values.cronjobs.image.tag }}" + imagePullPolicy: {{ $.Values.cronjobs.image.pullPolicy | default "Always"}} + securityContext: + # not yet possible since we need to install some tools + # privileged: false + # runAsUser: 1000 + # runAsNonRoot: true + capabilities: + drop: + - ALL + readOnlyRootFilesystem: false + allowPrivilegeEscalation: false + seccompProfile: + type: RuntimeDefault + envFrom: + - configMapRef: + name: {{ template "common.names.name" $ }}-cronjob-envs-configmap + env: + - name: BACKEND_ENDPOINT_URL + value: "https://{{ $.Values.backendHostname }}{{ $job_options.endpoint }}" + - name: HTTP_METHOD + value: "{{ $job_options.httpMethod }}" + resources: + limits: + memory: "128Mi" + cpu: "200m" + requests: + memory: "64Mi" + cpu: "50m" + command: + - "sh" + - "-c" + - | + mkdir /scripts && + cp /scripts_tmp/*.sh /scripts/ && + chmod +x /scripts/*.sh && + chmod 600 /etc/crontabs/root && + touch /var/log/cron.log && + chmod 644 /var/log/cron.log && + cd {{ $.Values.cronjobs.scriptDir }} && + ./{{ $job_options.script }} + volumeMounts: + - name: secret-volume-jwks + mountPath: /keys/jwks.json + subPath: jwks.json + readOnly: true + - name: script-volume + mountPath: /scripts_tmp + readOnly: false + ports: + - containerPort: {{ $.Values.cronjobs.port }} + name: cron-pod + volumes: + - name: script-volume + configMap: + name: {{ template "common.names.name" $ }}-cronjob-scripts-configmap + - name: secret-volume-jwks + secret: + secretName: dbildungs-iam-server + items: + - key: service-account-private-jwks + path: jwks.json + restartPolicy: Never +--- +{{- end}} +{{- end }} \ No newline at end of file diff --git a/automation/dbildungs-iam-server/values.yaml b/automation/dbildungs-iam-server/values.yaml index 32331b899..579894985 100644 --- a/automation/dbildungs-iam-server/values.yaml +++ b/automation/dbildungs-iam-server/values.yaml @@ -172,4 +172,35 @@ autoscaling: enabled: false minReplicas: 1 maxReplicas: 5 - targetCPUUtilizationPercentage: 60 \ No newline at end of file + targetCPUUtilizationPercentage: 60 + +cronjobs: + enabled: true + image: + tag: latest + #docker pull ghcr.io/hpi-schul-cloud/cron-tools:docops-1-latest + #repository: schulcloud/infra-tools/cron-tools + repository: ghcr.io/hpi-schul-cloud/cron-tools + pullPolicy: IfNotPresent + port: 5656 + keycloakTokenUrl: '/realms/SPSH/protocol/openid-connect/token' + keycloakClientId: spsh-service + jwksFilePath: /keys/jwks.json + backendHostname: '{{ $.Values.frontendHostname }}' + scriptDir: scripts + jobs: + cron-trigger-1: + schedule: 20 0 * * * + endpoint: '/api/cron/kopers-lock' + httpMethod: 'PUT' + script: 'cron_trigger.sh' + cron-trigger-2: + schedule: 40 0 * * * + endpoint: '/api/cron/kontext-expired' + httpMethod: 'PUT' + script: 'cron_trigger.sh' + cron-trigger-3: + schedule: 50 0 * * * + endpoint: '/api/cron/person-without-org' + httpMethod: 'PUT' + script: 'cron_trigger.sh' \ No newline at end of file