From cfc4ccf3b10f2a59471f107e0b3cfc4ac568f5c4 Mon Sep 17 00:00:00 2001 From: patlo-iog Date: Wed, 7 Aug 2024 16:54:44 +0700 Subject: [PATCH] fix: improve k8s keycloak bootstrapping script (#1278) Signed-off-by: Pat Losoponkul --- .../identus/api/http/EndpointOutputs.scala | 10 +- infrastructure/charts/.gitignore | 1 + .../charts/agent/templates/configmap.yaml | 183 ++++++++++-------- .../charts/agent/templates/deployment.yaml | 15 +- infrastructure/charts/agent/values.yaml | 16 +- .../service/MockPresentationService.scala | 3 +- 6 files changed, 120 insertions(+), 108 deletions(-) create mode 100644 infrastructure/charts/.gitignore diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/api/http/EndpointOutputs.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/api/http/EndpointOutputs.scala index 4e3390d6d3..119c9597e9 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/api/http/EndpointOutputs.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/api/http/EndpointOutputs.scala @@ -21,11 +21,12 @@ object EndpointOutputs { val basicFailures: EndpointOutput[ErrorResponse] = basicFailuresWith() - val basicFailuresAndForbidden = basicFailuresWith(FailureVariant.forbidden) + val basicFailuresAndForbidden = basicFailuresWith(FailureVariant.unauthorized, FailureVariant.forbidden) val basicFailuresAndNotFound = basicFailuresWith(FailureVariant.notFound) - val basicFailureAndNotFoundAndForbidden = basicFailuresWith(FailureVariant.notFound, FailureVariant.forbidden) + val basicFailureAndNotFoundAndForbidden = + basicFailuresWith(FailureVariant.notFound, FailureVariant.unauthorized, FailureVariant.forbidden) object FailureVariant { val badRequest = oneOfVariantValueMatcher( @@ -57,6 +58,11 @@ object EndpointOutputs { StatusCode.Forbidden, jsonBody[ErrorResponse].description("Forbidden") )(statusCodeMatcher(StatusCode.Forbidden)) + + val unauthorized = oneOfVariantValueMatcher( + StatusCode.Unauthorized, + jsonBody[ErrorResponse].description("Unauthorized") + )(statusCodeMatcher(StatusCode.Unauthorized)) } } diff --git a/infrastructure/charts/.gitignore b/infrastructure/charts/.gitignore new file mode 100644 index 0000000000..25b45693fb --- /dev/null +++ b/infrastructure/charts/.gitignore @@ -0,0 +1 @@ +**/charts/*.tgz diff --git a/infrastructure/charts/agent/templates/configmap.yaml b/infrastructure/charts/agent/templates/configmap.yaml index 2d83f2bd11..2fa395158b 100644 --- a/infrastructure/charts/agent/templates/configmap.yaml +++ b/infrastructure/charts/agent/templates/configmap.yaml @@ -5,92 +5,111 @@ metadata: labels: {{- include "labels.common" . | nindent 4 }} data: - init.sh: | - #!/usr/bin/env bash + init.ts: | + const KEYCLOAK_BASE_URL = process.env.KEYCLOAK_BASE_URL!; + const KEYCLOAK_ADMIN_USER = process.env.KEYCLOAK_ADMIN_USER!; + const KEYCLOAK_ADMIN_PASSWORD = process.env.KEYCLOAK_ADMIN_PASSWORD!; + const REALM_NAME = process.env.REALM_NAME!; + const CLOUD_AGENT_CLIENT_ID = process.env.CLOUD_AGENT_CLIENT_ID!; + const CLOUD_AGENT_CLIENT_SECRET = process.env.CLOUD_AGENT_CLIENT_SECRET!; - set -e - set -u - - KEYCLOAK_BASE_URL=$KEYCLOAK_BASE_URL - KEYCLOAK_ADMIN_USER=$KEYCLOAK_ADMIN_USER - KEYCLOAK_ADMIN_PASSWORD=$KEYCLOAK_ADMIN_PASSWORD - REALM_NAME=$REALM_NAME - CLOUD_AGENT_CLIENT_ID=$CLOUD_AGENT_CLIENT_ID - CLOUD_AGENT_CLIENT_SECRET=$CLOUD_AGENT_CLIENT_SECRET - - function get_admin_token() { - local response=$( - curl --request POST "$KEYCLOAK_BASE_URL/realms/master/protocol/openid-connect/token" \ - --fail -s -v \ - --data-urlencode "grant_type=password" \ - --data-urlencode "client_id=admin-cli" \ - --data-urlencode "username=$KEYCLOAK_ADMIN_USER" \ - --data-urlencode "password=$KEYCLOAK_ADMIN_PASSWORD" - ) - local access_token=$(echo $response | jq -r '.access_token') - echo $access_token - } - - function is_client_exists() { - local access_token=$1 - local client_id=$2 - - local http_status=$( - curl --request GET "$KEYCLOAK_BASE_URL/admin/realms/$REALM_NAME/clients/$client_id" \ - -s -w "%{http_code}" \ - -o /dev/null \ - -H "Authorization: Bearer $access_token" - ) - - if [ $http_status == 200 ]; then - echo "true" - else - echo "false" - fi + async function getAdminToken(): Promise { + const req = new Request(`${KEYCLOAK_BASE_URL}/realms/master/protocol/openid-connect/token`, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + "grant_type": "password", + "client_id": "admin-cli", + "username": KEYCLOAK_ADMIN_USER, + "password": KEYCLOAK_ADMIN_PASSWORD, + }) + }); + const resp = await fetch(req); + const body = await resp.json(); + if (resp.status !== 200) { + throw new Error("Response did not return success status code." + + ` Status: ${resp.status}, Body: ${JSON.stringify(body)}` + ); + } + return body["access_token"]; } - function create_client() { - local access_token=$1 - local client_id=$2 - local client_secret=$3 - - curl --request POST "$KEYCLOAK_BASE_URL/admin/realms/$REALM_NAME/clients" \ - --fail -s \ - -H "Authorization: Bearer $access_token" \ - -H "Content-Type: application/json" \ - --data-raw "{ - \"id\": \"$client_id\", - \"directAccessGrantsEnabled\": true, - \"authorizationServicesEnabled\": true, - \"serviceAccountsEnabled\": true, - \"secret\": \"$client_secret\" - }" + async function createRealm(accessToken: string) { + console.log(`Creating realm ${REALM_NAME} ...`); + const req = new Request(`${KEYCLOAK_BASE_URL}/admin/realms`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${accessToken}` + }, + body: JSON.stringify({ + "realm": REALM_NAME, + "enabled": true, + }) + }); + const resp = await fetch(req); + const resp_body = await resp.json(); + switch (resp.status) { + case 201: + console.log(`Realm ${REALM_NAME} created.`); + break; + case 409: + console.log(`Realm ${REALM_NAME} already exists.`); + break; + default: + throw new Error("Response did not return success status code." + + ` Status: ${resp.status}, Body: ${JSON.stringify(resp_body)}` + ); + } } - echo "Getting admin access token ..." - ADMIN_ACCESS_TOKEN=$(get_admin_token) - - CLIENT_EXIST=$(is_client_exists $ADMIN_ACCESS_TOKEN $CLOUD_AGENT_CLIENT_ID) - if [ $CLIENT_EXIST == "false" ]; then - echo "Creating a new $CLOUD_AGENT_CLIENT_ID client ..." - create_client $ADMIN_ACCESS_TOKEN $CLOUD_AGENT_CLIENT_ID $CLOUD_AGENT_CLIENT_SECRET - fi + async function createClient(accessToken: string, clientId: string, clientSecret?: string) { + console.log(`Creating client ${clientId} ...`); + let req_body = {}; + if (clientSecret) { + req_body = { + "id": clientId, + "directAccessGrantsEnabled": true, + "authorizationServicesEnabled": true, + "serviceAccountsEnabled": true, + "secret": clientSecret + }; + } else { + // public client is created if client secret is not provided + req_body = { + "id": clientId, + "publicClient": true, + "consentRequired": true, + "redirectUris": ["*"] + }; + } -{{- if .Values.keycloak.enabled }} - ---- - -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ include "cloud-agent.name" . }}-realm-import - labels: - {{- include "labels.common" . | nindent 4}} -data: - {{ include "cloud-agent.name" . }}.json: | - { - "realm": {{ .Values.server.keycloak.realm | quote }}, - "enabled": true + const req = new Request(`${KEYCLOAK_BASE_URL}/admin/realms/${REALM_NAME}/clients`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${accessToken}` + }, + body: JSON.stringify(req_body) + }); + const resp = await fetch(req); + const resp_body = await resp.json(); + switch (resp.status) { + case 201: + console.log(`Client ${clientId} created.`); + break; + case 409: + console.log(`Client ${clientId} already exists.`); + break; + default: + throw new Error("Response did not return success status code." + + ` Status: ${resp.status}, Body: ${JSON.stringify(resp_body)}` + ); + } } - -{{- end }} + (async () => { + console.log("Getting admin access token ..."); + const adminToken = await getAdminToken(); + await createRealm(adminToken); + await createClient(adminToken, CLOUD_AGENT_CLIENT_ID, CLOUD_AGENT_CLIENT_SECRET); + })(); diff --git a/infrastructure/charts/agent/templates/deployment.yaml b/infrastructure/charts/agent/templates/deployment.yaml index 69ba3babdf..6125288b86 100644 --- a/infrastructure/charts/agent/templates/deployment.yaml +++ b/infrastructure/charts/agent/templates/deployment.yaml @@ -23,16 +23,13 @@ spec: image: busybox command: ['sh', '-c', "until nc -z {{ .Values.database.postgres.managingTeam }}-{{ include "cloud-agent.name" . }}-postgres-cluster.{{ .Release.Namespace }} 5432; do echo waiting for postgress-operator; sleep 2; done;"] {{- if .Values.server.keycloak.enabled }} - - name: wait-keycloak-ready - image: badouralix/curl-jq:ubuntu - command: ['/bin/bash', '-c', 'until curl http://{{ .Release.Name }}-keycloak/health/ready; do sleep 2; done && echo "Keycloak is ready."'] {{- if .Values.server.keycloak.bootstrap }} - name: keycloak-bootstrap - image: badouralix/curl-jq:ubuntu - command: ['/bin/bash', '-c', '/scripts/init.sh'] + image: oven/bun:1 + command: ['bun', 'run', '/scripts/init.ts'] env: - name: KEYCLOAK_BASE_URL - value: http://{{ .Release.Name }}-keycloak + value: {{ .Values.server.keycloak.url }} - name: KEYCLOAK_ADMIN_USER value: {{ .Values.server.keycloak.admin.username }} - name: KEYCLOAK_ADMIN_PASSWORD @@ -202,7 +199,7 @@ spec: - name: KEYCLOAK_ENABLED value: "true" - name: KEYCLOAK_URL - value: http://{{ .Release.Name }}-keycloak + value: {{ .Values.server.keycloak.url }} - name: KEYCLOAK_REALM value: {{ .Values.server.keycloak.realm }} - name: KEYCLOAK_CLIENT_ID @@ -222,8 +219,8 @@ spec: name: keycloak-bootstrap-script defaultMode: 0500 items: - - key: "init.sh" - path: "init.sh" + - key: "init.ts" + path: "init.ts" {{- end }} affinity: {{- toYaml .Values.affinity | nindent 8 }} diff --git a/infrastructure/charts/agent/values.yaml b/infrastructure/charts/agent/values.yaml index 086453f678..6c67238a7c 100644 --- a/infrastructure/charts/agent/values.yaml +++ b/infrastructure/charts/agent/values.yaml @@ -50,6 +50,7 @@ server: useVault: true keycloak: enabled: false + url: http://agent-keycloak realm: prism-agent bootstrap: true admin: @@ -59,7 +60,7 @@ server: name: keycloak-admin-secret key: password client: - clientId: prism-agent + clientId: cloud-agent clientSecret: secretKeyRef: name: agent-keycloak-client-secret @@ -136,7 +137,7 @@ vault: keycloak: enabled: false # --hostname-url should be the frontend url that user will be logging in with keycloak - extraStartupArgs: "--hostname-url=http://localhost:8080 --import-realm --features=declarative-user-profile" + extraStartupArgs: "--hostname-url=http://localhost:8080 --features=declarative-user-profile" tls: enabled: true autoGenerated: true @@ -154,17 +155,6 @@ keycloak: port: "5432" user: keycloak-admin database: keycloak - extraVolumes: - - name: cloud-agent-realm-import-volume - configMap: - name: cloud-agent-realm-import - items: - - key: cloud-agent.json - path: cloud-agent.json - extraVolumeMounts: - - name: cloud-agent-realm-import-volume - mountPath: /opt/bitnami/keycloak/data/import - readOnly: true # It is configured for deployment and postgresql objects of cloud-agent affinity: diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockPresentationService.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockPresentationService.scala index 60cbe80cbe..fd47d0d48e 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockPresentationService.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/MockPresentationService.scala @@ -15,10 +15,9 @@ import org.hyperledger.identus.pollux.core.service.serdes.{AnoncredCredentialPro import org.hyperledger.identus.pollux.sdjwt.{HolderPrivateKey, PresentationCompact} import org.hyperledger.identus.pollux.vc.jwt.{Issuer, PresentationPayload, W3cCredentialPayload} import org.hyperledger.identus.shared.models.* -import zio.{mock, IO, URLayer, ZIO, ZLayer} +import zio.{mock, IO, UIO, URLayer, ZIO, ZLayer} import zio.json.* import zio.mock.{Mock, Proxy} -import zio.UIO import java.time.Instant import java.util.UUID