diff --git a/docker-jans-keycloak-link/.dockerignore b/docker-jans-keycloak-link/.dockerignore new file mode 100644 index 00000000000..9bc4efd4324 --- /dev/null +++ b/docker-jans-keycloak-link/.dockerignore @@ -0,0 +1,13 @@ +# exclude everything +* + +# include required files/directories +!certs +!conf +!jetty +!libs +!scripts +!LICENSE +!static +!requirements.txt +!templates diff --git a/docker-jans-keycloak-link/.hadolint.yaml b/docker-jans-keycloak-link/.hadolint.yaml new file mode 100644 index 00000000000..428f8174ee2 --- /dev/null +++ b/docker-jans-keycloak-link/.hadolint.yaml @@ -0,0 +1,4 @@ +ignored: + - DL3018 # Pin versions in apk add + - DL3013 # Pin versions in pip + - DL3003 # Use WORKDIR to switch to a directory diff --git a/docker-jans-keycloak-link/CHANGELOG.md b/docker-jans-keycloak-link/CHANGELOG.md new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/docker-jans-keycloak-link/CHANGELOG.md @@ -0,0 +1 @@ + diff --git a/docker-jans-keycloak-link/Dockerfile b/docker-jans-keycloak-link/Dockerfile new file mode 100644 index 00000000000..4cf61d8cd26 --- /dev/null +++ b/docker-jans-keycloak-link/Dockerfile @@ -0,0 +1,249 @@ +FROM bellsoft/liberica-openjre-alpine:17.0.8 + +# =============== +# Alpine packages +# =============== + +RUN apk update \ + && apk upgrade --available \ + && apk add --no-cache openssl python3 tini curl py3-cryptography py3-psycopg2 py3-grpcio \ + && apk add --no-cache --virtual .build-deps wget git zip + +# ===== +# Jetty +# ===== + +ARG JETTY_VERSION=11.0.16 +ARG JETTY_HOME=/opt/jetty +ARG JETTY_BASE=/opt/jans/jetty +ARG JETTY_USER_HOME_LIB=/home/jetty/lib + +# Install jetty +RUN wget -q https://repo1.maven.org/maven2/org/eclipse/jetty/jetty-home/${JETTY_VERSION}/jetty-home-${JETTY_VERSION}.tar.gz -O /tmp/jetty.tar.gz \ + && mkdir -p /opt \ + && tar -xzf /tmp/jetty.tar.gz -C /opt \ + && mv /opt/jetty-home-${JETTY_VERSION} ${JETTY_HOME} \ + && rm -rf /tmp/jetty.tar.gz + +# ====== +# Jython +# ====== + +ARG JYTHON_VERSION=2.7.3 +ARG JYTHON_BUILD_DATE='2022-08-01 17:37' +RUN wget -q https://maven.jans.io/maven/io/jans/jython-installer/${JYTHON_VERSION}/jython-installer-${JYTHON_VERSION}.jar -O /tmp/jython-installer.jar \ + && mkdir -p /opt/jython \ + && java -jar /tmp/jython-installer.jar -v -s -d /opt/jython -e ensurepip \ + && rm -f /tmp/jython-installer.jar /tmp/*.properties + +# ======= +# KC Link +# ======= + +ENV CN_VERSION=1.0.21-SNAPSHOT +ENV CN_BUILD_DATE='2023-11-14 08:13' +ENV CN_SOURCE_URL=https://jenkins.jans.io/maven/io/jans/jans-keycloak-link-server/${CN_VERSION}/jans-keycloak-link-server-${CN_VERSION}.war + +# Install Link +COPY static/jetty-env.xml /tmp/WEB-INF/jetty-env.xml +RUN mkdir -p ${JETTY_BASE}/jans-keycloak-link/webapps \ + && wget -q ${CN_SOURCE_URL} -O /tmp/jans-keycloak-link.war \ + && cd /tmp \ + && zip -d jans-keycloak-link.war WEB-INF/jetty-web.xml \ + && zip -r jans-keycloak-link.war WEB-INF/jetty-env.xml \ + && cp jans-keycloak-link.war ${JETTY_BASE}/jans-keycloak-link/webapps/jans-keycloak-link.war \ + && java -jar ${JETTY_HOME}/start.jar jetty.home=${JETTY_HOME} jetty.base=${JETTY_BASE}/jans-keycloak-link --add-module=server,deploy,resources,http,http-forwarded,threadpool,jsp,cdi-decorate,jmx,stats,logging-log4j2 --approve-all-licenses \ + && rm -rf /tmp/jans-keycloak-link.war /tmp/WEB-INF + +# ===================== +# jans-linux-setup sync +# ===================== + +ENV JANS_SOURCE_VERSION=cc9d64f830ac3a07c7dbcbaafe920386e6fdcb7f +ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup + +# note that as we're pulling from a monorepo (with multiple project in it) +# we are using partial-clone and sparse-checkout to get the jans-linux-setup code +RUN git clone --filter blob:none --no-checkout https://github.com/janssenproject/jans /tmp/jans \ + && cd /tmp/jans \ + && git sparse-checkout init --cone \ + && git checkout ${JANS_SOURCE_VERSION} \ + && git sparse-checkout add ${JANS_SETUP_DIR} + +RUN mkdir -p /etc/jans/conf \ + /app/static/rdbm \ + /app/schema \ + /app/templates/jans-keycloak-link + +# sync static files from linux-setup +RUN cd /tmp/jans \ + && cp ${JANS_SETUP_DIR}/static/rdbm/sql_data_types.json /app/static/rdbm/ \ + && cp ${JANS_SETUP_DIR}/static/rdbm/ldap_sql_data_type_mapping.json /app/static/rdbm/ \ + && cp ${JANS_SETUP_DIR}/static/rdbm/opendj_attributes_syntax.json /app/static/rdbm/ \ + && cp ${JANS_SETUP_DIR}/static/rdbm/sub_tables.json /app/static/rdbm/ \ + && cp ${JANS_SETUP_DIR}/schema/jans_schema.json /app/schema/ \ + && cp ${JANS_SETUP_DIR}/schema/custom_schema.json /app/schema/ \ + && cp ${JANS_SETUP_DIR}/schema/opendj_types.json /app/schema/ \ + && cp -R ${JANS_SETUP_DIR}/templates/jans-keycloak-link/configuration.ldif /app/templates/jans-keycloak-link/ \ + && cp -R ${JANS_SETUP_DIR}/templates/jans-keycloak-link/jans-keycloak-link-config.json /app/templates/jans-keycloak-link/ \ + && cp -R ${JANS_SETUP_DIR}/templates/jans-keycloak-link/jans-keycloak-link-static-config.json /app/templates/jans-keycloak-link/ + +# ====== +# Python +# ====== + +COPY requirements.txt /app/requirements.txt +RUN python3 -m ensurepip \ + && pip3 install --no-cache-dir -U pip wheel setuptools \ + && pip3 install --no-cache-dir -r /app/requirements.txt \ + && pip3 uninstall -y pip wheel + +# ========== +# Prometheus +# ========== + +COPY static/prometheus-config.yaml /opt/prometheus/ + +# ======= +# Cleanup +# ======= + +RUN apk del .build-deps \ + && rm -rf /var/cache/apk/* /tmp/jans + +# ======= +# License +# ======= + +COPY LICENSE /licenses/LICENSE + +# ========== +# Config ENV +# ========== + +ENV CN_CONFIG_ADAPTER=consul \ + CN_CONFIG_CONSUL_HOST=localhost \ + CN_CONFIG_CONSUL_PORT=8500 \ + CN_CONFIG_CONSUL_CONSISTENCY=stale \ + CN_CONFIG_CONSUL_SCHEME=http \ + CN_CONFIG_CONSUL_VERIFY=false \ + CN_CONFIG_CONSUL_CACERT_FILE=/etc/certs/consul_ca.crt \ + CN_CONFIG_CONSUL_CERT_FILE=/etc/certs/consul_client.crt \ + CN_CONFIG_CONSUL_KEY_FILE=/etc/certs/consul_client.key \ + CN_CONFIG_CONSUL_TOKEN_FILE=/etc/certs/consul_token \ + CN_CONFIG_CONSUL_NAMESPACE=jans \ + CN_CONFIG_KUBERNETES_NAMESPACE=default \ + CN_CONFIG_KUBERNETES_CONFIGMAP=jans \ + CN_CONFIG_KUBERNETES_USE_KUBE_CONFIG=false + +# ========== +# Secret ENV +# ========== + +ENV CN_SECRET_ADAPTER=vault \ + CN_SECRET_VAULT_SCHEME=http \ + CN_SECRET_VAULT_HOST=localhost \ + CN_SECRET_VAULT_PORT=8200 \ + CN_SECRET_VAULT_VERIFY=false \ + CN_SECRET_VAULT_ROLE_ID_FILE=/etc/certs/vault_role_id \ + CN_SECRET_VAULT_SECRET_ID_FILE=/etc/certs/vault_secret_id \ + CN_SECRET_VAULT_CERT_FILE=/etc/certs/vault_client.crt \ + CN_SECRET_VAULT_KEY_FILE=/etc/certs/vault_client.key \ + CN_SECRET_VAULT_CACERT_FILE=/etc/certs/vault_ca.crt \ + CN_SECRET_VAULT_NAMESPACE=jans \ + CN_SECRET_KUBERNETES_NAMESPACE=default \ + CN_SECRET_KUBERNETES_SECRET=jans \ + CN_SECRET_KUBERNETES_USE_KUBE_CONFIG=false + +# =============== +# Persistence ENV +# =============== + +ENV CN_PERSISTENCE_TYPE=ldap \ + CN_HYBRID_MAPPING="{}" \ + CN_LDAP_URL=localhost:1636 \ + CN_LDAP_USE_SSL=true \ + CN_COUCHBASE_URL=localhost \ + CN_COUCHBASE_USER=admin \ + CN_COUCHBASE_CERT_FILE=/etc/certs/couchbase.crt \ + CN_COUCHBASE_PASSWORD_FILE=/etc/jans/conf/couchbase_password \ + CN_COUCHBASE_CONN_TIMEOUT=10000 \ + CN_COUCHBASE_CONN_MAX_WAIT=20000 \ + CN_COUCHBASE_SCAN_CONSISTENCY=not_bounded \ + CN_COUCHBASE_BUCKET_PREFIX=jans \ + CN_COUCHBASE_TRUSTSTORE_ENABLE=true \ + CN_COUCHBASE_KEEPALIVE_INTERVAL=30000 \ + CN_COUCHBASE_KEEPALIVE_TIMEOUT=2500 + +# =========== +# Generic ENV +# =========== + +ENV CN_MAX_RAM_PERCENTAGE=75.0 \ + CN_WAIT_MAX_TIME=300 \ + CN_WAIT_SLEEP_DURATION=10 \ + CN_KEYCLOAK_LINK_JAVA_OPTIONS="" \ + GOOGLE_PROJECT_ID="" \ + CN_GOOGLE_SECRET_MANAGER_PASSPHRASE=secret \ + CN_GOOGLE_SECRET_VERSION_ID=latest \ + CN_GOOGLE_SECRET_NAME_PREFIX=jans \ + CN_PROMETHEUS_PORT="" \ + CN_AWS_SECRETS_ENDPOINT_URL="" \ + CN_AWS_SECRETS_PREFIX=jans \ + CN_AWS_SECRETS_REPLICA_FILE="" \ + CN_KEYCLOAK_LINK_JETTY_PORT=9092 \ + CN_KEYCLOAK_LINK_JETTY_HOST=0.0.0.0 + +# ========== +# misc stuff +# ========== + +EXPOSE $CN_KEYCLOAK_LINK_JETTY_PORT + +LABEL org.opencontainers.image.url="ghcr.io/janssenproject/jans/keycloak-link" \ + org.opencontainers.image.authors="Janssen Project " \ + org.opencontainers.image.vendor="Janssen Project" \ + org.opencontainers.image.version="1.0.21" \ + org.opencontainers.image.title="Janssen Keycloak Link" \ + org.opencontainers.image.description="" + +RUN mkdir -p /etc/certs \ + ${JETTY_BASE}/jans-keycloak-link/logs \ + ${JETTY_BASE}/jans-keycloak-link/custom/libs \ + ${JETTY_BASE}/common/libs/spanner \ + ${JETTY_BASE}/common/libs/couchbase \ + ${JETTY_HOME}/temp \ + /usr/share/java \ + /var/jans/cr-snapshots + +COPY templates /app/templates/ +RUN cp /app/templates/jans-keycloak-link/jans-keycloak-link.xml ${JETTY_BASE}/jans-keycloak-link/webapps/ +COPY scripts /app/scripts +RUN chmod +x /app/scripts/entrypoint.sh + +RUN sed -i 's/\(\)/\1\2false<\/Set><\/New>/' /opt/jetty/etc/jetty.xml + +RUN ln -sf /usr/lib/jvm/jre /opt/java + +# create non-root user +RUN adduser -s /bin/sh -h /home/1000 -D -G root -u 1000 jetty + +# adjust ownership and permission +RUN chmod 664 ${JETTY_BASE}/jans-keycloak-link/resources/log4j2.xml \ + && chmod -R g=u ${JETTY_BASE}/jans-keycloak-link/logs \ + && chmod -R g=u /etc/certs \ + && chmod -R g=u /etc/jans \ + && chmod 664 /opt/java/lib/security/cacerts \ + && chown -R 1000:0 ${JETTY_BASE}/common/libs \ + && chown -R 1000:0 /usr/share/java \ + && chown -R 1000:0 /opt/prometheus \ + && chown 1000:0 ${JETTY_BASE}/jans-keycloak-link/webapps/jans-keycloak-link.xml \ + && chown -R 1000:0 /var/jans/cr-snapshots \ + && chown -R 1000:0 ${JETTY_HOME}/temp + +USER 1000 + +RUN mkdir -p $HOME/.config/gcloud + +ENTRYPOINT ["tini", "-e", "143", "-g", "--"] +CMD ["sh", "/app/scripts/entrypoint.sh"] diff --git a/docker-jans-keycloak-link/LICENSE b/docker-jans-keycloak-link/LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/docker-jans-keycloak-link/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docker-jans-keycloak-link/Makefile b/docker-jans-keycloak-link/Makefile new file mode 100644 index 00000000000..a1dd261cc4f --- /dev/null +++ b/docker-jans-keycloak-link/Makefile @@ -0,0 +1,25 @@ +IMAGE_VERSION?=$(shell grep -Po 'org.opencontainers.image.version="\K.*?(?=")' Dockerfile)_dev +IMAGE_URL=$(shell grep -Po 'org.opencontainers.image.url="\K.*?(?=")' Dockerfile) +IMAGE?=${IMAGE_URL}:${IMAGE_VERSION} + +# pass extra args to the targets, for example: +# +# - `make build-dev ARGS="--no-cache"` +# - `make trivy-scan TRIVY_ARGS="-f json"` +# - `make grype-scan GRYPE_ARGS="-o json"` +ARGS?= + +.PHONY: test clean all build-dev trivy-scan grype-scan +.DEFAULT_GOAL := build-dev + +build-dev: + @echo "[I] Building OCI image ${IMAGE}" + @docker build --rm --force-rm ${ARGS} -t ${IMAGE} . + +trivy-scan: + @echo "[I] Scanning OCI image ${IMAGE} using trivy" + @trivy image --security-checks vuln ${ARGS} ${IMAGE} + +grype-scan: + @echo "[I] Scanning OCI image ${IMAGE} using grype" + @grype -v ${ARGS} ${IMAGE} diff --git a/docker-jans-keycloak-link/README.md b/docker-jans-keycloak-link/README.md new file mode 100644 index 00000000000..afd5d15edea --- /dev/null +++ b/docker-jans-keycloak-link/README.md @@ -0,0 +1,169 @@ +--- +tags: +- administration +- reference +- kubernetes +- docker image +--- + +# Overview + +Docker image packaging for Cache Refresh. + +## Versions + +See [Packages](https://github.com/orgs/JanssenProject/packages/container/package/jans%2Flink) for available versions. + +## Environment Variables + +The following environment variables are supported by the container: + +- `CN_CONFIG_ADAPTER`: The config backend adapter, can be `consul` (default) or `kubernetes`. +- `CN_CONFIG_CONSUL_HOST`: hostname or IP of Consul (default to `localhost`). +- `CN_CONFIG_CONSUL_PORT`: port of Consul (default to `8500`). +- `CN_CONFIG_CONSUL_CONSISTENCY`: Consul consistency mode (choose one of `default`, `consistent`, or `stale`). Default to `stale` mode. +- `CN_CONFIG_CONSUL_SCHEME`: supported Consul scheme (`http` or `https`). +- `CN_CONFIG_CONSUL_VERIFY`: whether to verify cert or not (default to `false`). +- `CN_CONFIG_CONSUL_CACERT_FILE`: path to Consul CA cert file (default to `/etc/certs/consul_ca.crt`). This file will be used if it exists and `CN_CONFIG_CONSUL_VERIFY` set to `true`. +- `CN_CONFIG_CONSUL_CERT_FILE`: path to Consul cert file (default to `/etc/certs/consul_client.crt`). +- `CN_CONFIG_CONSUL_KEY_FILE`: path to Consul key file (default to `/etc/certs/consul_client.key`). +- `CN_CONFIG_CONSUL_TOKEN_FILE`: path to file contains ACL token (default to `/etc/certs/consul_token`). +- `CN_CONFIG_KUBERNETES_NAMESPACE`: Kubernetes namespace (default to `default`). +- `CN_CONFIG_KUBERNETES_CONFIGMAP`: Kubernetes configmaps name (default to `jans`). +- `CN_CONFIG_KUBERNETES_USE_KUBE_CONFIG`: Load credentials from `$HOME/.kube/config`, only useful for non-container environment (default to `false`). +- `CN_SECRET_ADAPTER`: The secrets' adapter, can be `vault` (default), `kubernetes`, or `google`. +- `CN_SECRET_VAULT_SCHEME`: supported Vault scheme (`http` or `https`). +- `CN_SECRET_VAULT_HOST`: hostname or IP of Vault (default to `localhost`). +- `CN_SECRET_VAULT_PORT`: port of Vault (default to `8200`). +- `CN_SECRET_VAULT_VERIFY`: whether to verify cert or not (default to `false`). +- `CN_SECRET_VAULT_ROLE_ID_FILE`: path to file contains Vault AppRole role ID (default to `/etc/certs/vault_role_id`). +- `CN_SECRET_VAULT_SECRET_ID_FILE`: path to file contains Vault AppRole secret ID (default to `/etc/certs/vault_secret_id`). +- `CN_SECRET_VAULT_CERT_FILE`: path to Vault cert file (default to `/etc/certs/vault_client.crt`). +- `CN_SECRET_VAULT_KEY_FILE`: path to Vault key file (default to `/etc/certs/vault_client.key`). +- `CN_SECRET_VAULT_CACERT_FILE`: path to Vault CA cert file (default to `/etc/certs/vault_ca.crt`). This file will be used if it exists and `CN_SECRET_VAULT_VERIFY` set to `true`. +- `CN_SECRET_KUBERNETES_NAMESPACE`: Kubernetes namespace (default to `default`). +- `CN_SECRET_KUBERNETES_SECRET`: Kubernetes secrets name (default to `jans`). +- `CN_SECRET_KUBERNETES_USE_KUBE_CONFIG`: Load credentials from `$HOME/.kube/config`, only useful for non-container environment (default to `false`). +- `CN_WAIT_MAX_TIME`: How long the startup "health checks" should run (default to `300` seconds). +- `CN_WAIT_SLEEP_DURATION`: Delay between startup "health checks" (default to `10` seconds). +- `CN_MAX_RAM_PERCENTAGE`: Value passed to Java option `-XX:MaxRAMPercentage`. +- `CN_PERSISTENCE_TYPE`: Persistence backend being used (one of `ldap`, `couchbase`, or `hybrid`; default to `ldap`). +- `CN_HYBRID_MAPPING`: Specify data mapping for each persistence (default to `"{}"`). Note this environment only takes effect when `CN_PERSISTENCE_TYPE` is set to `hybrid`. See [hybrid mapping](#hybrid-mapping) section for details. +- `CN_LDAP_URL`: Address and port of LDAP server (default to `localhost:1636`). +- `CN_LDAP_USE_SSL`: Whether to use SSL connection to LDAP server (default to `true`). +- `CN_COUCHBASE_URL`: Address of Couchbase server (default to `localhost`). +- `CN_COUCHBASE_USER`: Username of Couchbase server (default to `admin`). +- `CN_COUCHBASE_CERT_FILE`: Couchbase root certificate location (default to `/etc/certs/couchbase.crt`). +- `CN_COUCHBASE_PASSWORD_FILE`: Path to file contains Couchbase password (default to `/etc/jans/conf/couchbase_password`). +- `CN_COUCHBASE_CONN_TIMEOUT`: Connect timeout used when a bucket is opened (default to `10000` milliseconds). +- `CN_COUCHBASE_CONN_MAX_WAIT`: Maximum time to wait before retrying connection (default to `20000` milliseconds). +- `CN_COUCHBASE_SCAN_CONSISTENCY`: Default scan consistency; one of `not_bounded`, `request_plus`, or `statement_plus` (default to `not_bounded`). +- `CN_COUCHBASE_BUCKET_PREFIX`: Prefix for Couchbase buckets (default to `jans`). +- `CN_COUCHBASE_TRUSTSTORE_ENABLE`: Enable truststore for encrypted Couchbase connection (default to `true`). +- `CN_COUCHBASE_KEEPALIVE_INTERVAL`: Keep-alive interval for Couchbase connection (default to `30000` milliseconds). +- `CN_COUCHBASE_KEEPALIVE_TIMEOUT`: Keep-alive timeout for Couchbase connection (default to `2500` milliseconds). +- `CN_KEYCLOAK_LINK_JAVA_OPTIONS`: Java options passed to entrypoint, i.e. `-Xmx1024m` (default to empty-string). +- `GOOGLE_APPLICATION_CREDENTIALS`: Optional JSON file (contains Google credentials) that can be injected into container for authentication. Refer to https://cloud.google.com/docs/authentication/provide-credentials-adc#how-to for supported credentials. +- `GOOGLE_PROJECT_ID`: ID of Google project. +- `CN_GOOGLE_SECRET_VERSION_ID`: Janssen secret version ID in Google Secret Manager. Defaults to `latest`, which is recommended. +- `CN_GOOGLE_SECRET_NAME_PREFIX`: Prefix for Janssen secret in Google Secret Manager. Defaults to `jans`. If left `jans-secret` secret will be created. +- `CN_GOOGLE_SECRET_MANAGER_PASSPHRASE`: Passphrase for Janssen secret in Google Secret Manager. This is recommended to be changed and defaults to `secret`. +- `CN_CACHE_REFRESH_APP_LOGGERS`: Custom logging configuration in JSON-string format with hash type (see [Configure app loggers](#configure-app-loggers) section for details). +- `CN_PROMETHEUS_PORT`: Port used by Prometheus JMX agent (default to empty string). To enable Prometheus JMX agent, set the value to a number. See [Exposing metrics](#exposing-metrics) for details. +- `CN_SQL_DB_HOST`: Hostname of the SQL database (default to `localhost`). +- `CN_SQL_DB_PORT`: Port of the SQL database (default to `3306` for MySQL). +- `CN_SQL_DB_NAME`: SQL database name (default to `jans`). +- `CN_SQL_DB_USER`: User name to access the SQL database (default to `jans`). +- `CN_SQL_DB_DIALECT`: Dialect name of the SQL (`mysql` for MySQL or `pgsql` for PostgreSQL; default to `mysql`). +- `CN_SQL_DB_TIMEZONE`: Timezone used by the SQL database (default to `UTC`). +- `CN_SQL_DB_SCHEMA`: Schema name used by SQL database (default to empty-string; if using MySQL, the schema name will be resolved as the database name, whereas in PostgreSQL the schema name will be resolved as `"public"`). +- `CN_AWS_SECRETS_ENDPOINT_URL`: The URL of AWS secretsmanager service (if omitted, will use the one in specified region). +- `CN_AWS_SECRETS_PREFIX`: The prefix name of the secrets (default to `jans`). +- `CN_AWS_SECRETS_REPLICA_FILE`: The location of file contains replica regions definition (if any). This file is mostly used in primary region. Example of contents of the file: `[{"Region": "us-west-1"}]`. +- `AWS_DEFAULT_REGION`: The default AWS Region to use, for example, `us-west-1` or `us-west-2`. +- `AWS_SHARED_CREDENTIALS_FILE`: The location of the shared credentials file used by the client (see https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html). +- `AWS_CONFIG_FILE`: The location of the config file used by the client (see https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html). +- `AWS_PROFILE`: The default profile to use, if any. + +### Configure app loggers + +App loggers can be configured to define where the logs will be redirected and what is the level the logs should be displayed. + +Supported redirect target: + +- `STDOUT` +- `FILE` + +Supported level: + +- `FATAL` +- `ERROR` +- `WARN` +- `INFO` +- `DEBUG` +- `TRACE` + +The following key-value pairs are the defaults: + +```json +{ + "link_log_target": "STDOUT", + "link_log_level": "INFO", + "persistence_log_target": "FILE", + "persistence_log_level": "INFO", + "persistence_duration_log_target": "FILE", + "persistence_duration_log_level": "INFO", + "ldap_stats_log_target": "FILE", + "ldap_stats_log_level": "INFO", + "script_log_target": "FILE", + "script_log_level": "INFO" +} +``` + +To enable prefix on `STDOUT` logging, set the `enable_stdout_log_prefix` key. Example: + +``` +{"link_log_target":"STDOUT","persistence_log_target":"STDOUT","enable_stdout_log_prefix":true} +``` + +### Hybrid mapping + +As per v1.0.1, hybrid persistence supports all available persistence types. To configure hybrid persistence and its data mapping, follow steps below: + +1. Set `CN_PERSISTENCE_TYPE` environment variable to `hybrid` + +2. Set `CN_HYBRID_MAPPING` with the following format: + + ``` + { + "default": "", + "user": "", + "site": "", + "cache": "", + "token": "", + "session": "", + } + ``` + + Example: + + ``` + { + "default": "sql", + "user": "spanner", + "site": "ldap", + "cache": "sql", + "token": "couchbase", + "session": "spanner", + } + ``` + +### Exposing metrics + +As per v1.0.1, certain metrics can be exposed via Prometheus JMX exporter. +To expose the metrics, set the `CN_PROMETHEUS_PORT` environment variable, i.e. `CN_PROMETHEUS_PORT=9093`. +Afterwards, metrics can be scraped by Prometheus or accessed manually by making request to `/metrics` URL, +i.e. `http://container:9093/metrics`. + +Note that Prometheus JMX exporter uses pre-defined config file (see `conf/prometheus-config.yaml`). +To customize the config, mount custom config file to `/opt/prometheus/prometheus-config.yaml` inside the container. diff --git a/docker-jans-keycloak-link/requirements.txt b/docker-jans-keycloak-link/requirements.txt new file mode 100644 index 00000000000..edcebaf160b --- /dev/null +++ b/docker-jans-keycloak-link/requirements.txt @@ -0,0 +1,4 @@ +# pinned to py3-grpcio version to avoid failure on native extension build +grpcio==1.54.2 +libcst<0.4 +git+https://github.com/JanssenProject/jans@9dd82da5c87ee829c73a1135ce8740b8353f8ab5#egg=jans-pycloudlib&subdirectory=jans-pycloudlib diff --git a/docker-jans-keycloak-link/scripts/bootstrap.py b/docker-jans-keycloak-link/scripts/bootstrap.py new file mode 100644 index 00000000000..4a1190676f0 --- /dev/null +++ b/docker-jans-keycloak-link/scripts/bootstrap.py @@ -0,0 +1,235 @@ +from __future__ import annotations + +import json +import logging.config +import os +import typing as _t +from functools import cached_property +from string import Template + +from jans.pycloudlib import get_manager +from jans.pycloudlib.persistence import render_couchbase_properties +from jans.pycloudlib.persistence import render_base_properties +from jans.pycloudlib.persistence import render_hybrid_properties +from jans.pycloudlib.persistence import render_ldap_properties +from jans.pycloudlib.persistence import render_salt +from jans.pycloudlib.persistence import sync_couchbase_truststore +from jans.pycloudlib.persistence import sync_ldap_truststore +from jans.pycloudlib.persistence import render_sql_properties +from jans.pycloudlib.persistence import render_spanner_properties +from jans.pycloudlib.persistence.couchbase import CouchbaseClient +from jans.pycloudlib.persistence.ldap import LdapClient +from jans.pycloudlib.persistence.spanner import SpannerClient +from jans.pycloudlib.persistence.sql import SqlClient +from jans.pycloudlib.persistence.utils import PersistenceMapper +from jans.pycloudlib.utils import cert_to_truststore +from jans.pycloudlib.utils import generate_base64_contents +from jans.pycloudlib.utils import as_boolean + +from settings import LOGGING_CONFIG + +if _t.TYPE_CHECKING: # pragma: no cover + # imported objects for function type hint, completion, etc. + # these won't be executed in runtime + from jans.pycloudlib.manager import Manager + + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("keycloak-link") + +manager = get_manager() + + +def main(): + persistence_type = os.environ.get("CN_PERSISTENCE_TYPE", "ldap") + + render_salt(manager, "/app/templates/salt", "/etc/jans/conf/salt") + render_base_properties("/app/templates/jans.properties", "/etc/jans/conf/jans.properties") + + mapper = PersistenceMapper() + persistence_groups = mapper.groups() + + if persistence_type == "hybrid": + hybrid_prop = "/etc/jans/conf/jans-hybrid.properties" + if not os.path.exists(hybrid_prop): + render_hybrid_properties(hybrid_prop) + + if "ldap" in persistence_groups: + render_ldap_properties( + manager, + "/app/templates/jans-ldap.properties", + "/etc/jans/conf/jans-ldap.properties", + ) + sync_ldap_truststore(manager) + + if "couchbase" in persistence_groups: + render_couchbase_properties( + manager, + "/app/templates/jans-couchbase.properties", + "/etc/jans/conf/jans-couchbase.properties", + ) + sync_couchbase_truststore(manager) + + if "sql" in persistence_groups: + db_dialect = os.environ.get("CN_SQL_DB_DIALECT", "mysql") + + render_sql_properties( + manager, + f"/app/templates/jans-{db_dialect}.properties", + "/etc/jans/conf/jans-sql.properties", + ) + + if "spanner" in persistence_groups: + render_spanner_properties( + manager, + "/app/templates/jans-spanner.properties", + "/etc/jans/conf/jans-spanner.properties", + ) + + if not os.path.isfile("/etc/certs/web_https.crt"): + manager.secret.to_file("ssl_cert", "/etc/certs/web_https.crt") + + cert_to_truststore( + "web_https", + "/etc/certs/web_https.crt", + "/opt/java/lib/security/cacerts", + "changeit", + ) + + configure_logging() + + with manager.lock.create_lock("keycloak-link-setup"): + persistence_setup = PersistenceSetup(manager) + persistence_setup.import_ldif_files() + + +def configure_logging(): + # default config + config = { + "keycloak_link_log_target": "STDOUT", + "keycloak_link_log_level": "INFO", + "persistence_log_target": "FILE", + "persistence_log_level": "INFO", + "persistence_duration_log_target": "FILE", + "persistence_duration_log_level": "INFO", + "ldap_stats_log_target": "FILE", + "ldap_stats_log_level": "INFO", + "script_log_target": "FILE", + "script_log_level": "INFO", + "log_prefix": "", + } + + # pre-populate custom config; format is JSON string of ``dict`` + try: + custom_config = json.loads(os.environ.get("CN_KEYCLOAK_LINK_APP_LOGGERS", "{}")) + except json.decoder.JSONDecodeError as exc: + logger.warning(f"Unable to load logging configuration from environment variable; reason={exc}; fallback to defaults") + custom_config = {} + + # ensure custom config is ``dict`` type + if not isinstance(custom_config, dict): + logger.warning("Invalid data type for CN_KEYCLOAK_LINK_APP_LOGGERS; fallback to defaults") + custom_config = {} + + # list of supported levels; OFF is not supported + log_levels = ("FATAL", "ERROR", "WARN", "INFO", "DEBUG", "TRACE",) + + # list of supported outputs + log_targets = ("STDOUT", "FILE",) + + for k, v in custom_config.items(): + if k not in config: + continue + + if k.endswith("_log_level") and v not in log_levels: + logger.warning(f"Invalid {v} log level for {k}; fallback to defaults") + v = config[k] + + if k.endswith("_log_target") and v not in log_targets: + logger.warning(f"Invalid {v} log output for {k}; fallback to defaults") + v = config[k] + + # update the config + config[k] = v + + # mapping between the ``log_target`` value and their appenders + file_aliases = { + "keycloak_link_log_target": "FILE", + "persistence_log_target": "JANS_KEYCLOAK_PERSISTENCE_FILE", + "persistence_duration_log_target": "JANS_KEYCLOAK_PERSISTENCE_DURATION_FILE", + "ldap_stats_log_target": "JANS_KEYCLOAK_PERSISTENCE_LDAP_STATISTICS_FILE", + "script_log_target": "JANS_KEYCLOAK_SCRIPT_LOG_FILE", + } + for key, value in file_aliases.items(): + if config[key] == "FILE": + config[key] = value + + if any([ + as_boolean(custom_config.get("enable_stdout_log_prefix")), + as_boolean(os.environ.get("CN_ENABLE_STDOUT_LOG_PREFIX")), + ]): + config["log_prefix"] = "${sys:keycloak_link.log.console.prefix}%X{keycloak_link.log.console.group} - " + + with open("/app/templates/jans-keycloak-link/log4j2.xml") as f: + txt = f.read() + + logfile = "/opt/jans/jetty/jans-keycloak-link/resources/log4j2.xml" + tmpl = Template(txt) + with open(logfile, "w") as f: + f.write(tmpl.safe_substitute(config)) + + +class PersistenceSetup: + def __init__(self, manager: Manager) -> None: + self.manager = manager + + client_classes = { + "ldap": LdapClient, + "couchbase": CouchbaseClient, + "spanner": SpannerClient, + "sql": SqlClient, + } + + # determine persistence type + mapper = PersistenceMapper() + self.persistence_type = mapper.mapping["default"] + + # determine persistence client + client_cls = client_classes.get(self.persistence_type) + self.client = client_cls(manager) + + @cached_property + def ctx(self) -> dict[str, _t.Any]: + ctx = { + "ldap_binddn": self.manager.config.get("ldap_binddn"), + "ldap_hostname": self.manager.config.get("ldap_init_host"), + "ldaps_port": self.manager.config.get("ldap_init_port"), + "ldap_bind_encoded_pw": self.manager.secret.get("encoded_ox_ldap_pw"), + "snapshots_dir": "/var/jans/keycloak-link-snapshots", + } + + # pre-populate jans_keycloak_link_config_base64 + with open("/app/templates/jans-keycloak-link/jans-keycloak-link-config.json") as f: + ctx["jans_keycloak_link_config_base64"] = generate_base64_contents(f.read() % ctx) + + # pre-populate jans_keycloak_link_static_conf_base64 + with open("/app/templates/jans-keycloak-link/jans-keycloak-link-static-config.json") as f: + ctx["jans_keycloak_link_static_conf_base64"] = generate_base64_contents(f.read()) + + return ctx + + @cached_property + def ldif_files(self) -> list[str]: + return [ + f"/app/templates/jans-keycloak-link/{file_}" + for file_ in ["configuration.ldif"] + ] + + def import_ldif_files(self) -> None: + for file_ in self.ldif_files: + logger.info(f"Importing {file_}") + self.client.create_from_ldif(file_, self.ctx) + + +if __name__ == "__main__": + main() diff --git a/docker-jans-keycloak-link/scripts/entrypoint.sh b/docker-jans-keycloak-link/scripts/entrypoint.sh new file mode 100644 index 00000000000..31102011c68 --- /dev/null +++ b/docker-jans-keycloak-link/scripts/entrypoint.sh @@ -0,0 +1,69 @@ +#!/bin/sh + +set -e + +# get script directory +basedir=$(dirname "$(readlink -f -- "$0")") + +get_prometheus_opt() { + prom_opt="" + + if [ -n "${CN_PROMETHEUS_PORT}" ]; then + prom_opt=" + -javaagent:/opt/prometheus/jmx_prometheus_javaagent.jar=${CN_PROMETHEUS_PORT}:/opt/prometheus/prometheus-config.yaml + " + fi + echo "${prom_opt}" +} + +get_prometheus_lib() { + if [ -n "${CN_PROMETHEUS_PORT}" ]; then + prom_agent_version="0.17.2" + + if [ ! -f /opt/prometheus/jmx_prometheus_javaagent.jar ]; then + wget -q https://repo1.maven.org/maven2/io/prometheus/jmx/jmx_prometheus_javaagent/${prom_agent_version}/jmx_prometheus_javaagent-${prom_agent_version}.jar -O /opt/prometheus/jmx_prometheus_javaagent.jar + fi + fi +} + +get_java_options() { + if [ -n "${CN_KEYCLOAK_LINK_JAVA_OPTIONS}" ]; then + echo " ${CN_KEYCLOAK_LINK_JAVA_OPTIONS} " + else + # backward-compat + echo " ${CN_JAVA_OPTIONS} " + fi +} + +get_max_ram_percentage() { + if [ -n "${CN_MAX_RAM_PERCENTAGE}" ]; then + echo " -XX:MaxRAMPercentage=$CN_MAX_RAM_PERCENTAGE " + fi +} + +get_prometheus_lib +python3 "$basedir/wait.py" +python3 "$basedir/bootstrap.py" +python3 "$basedir/mod_context.py" jans-keycloak-link +python3 "$basedir/upgrade.py" + +cd /opt/jans/jetty/jans-keycloak-link +# shellcheck disable=SC2046 +exec java \ + -server \ + -XX:+DisableExplicitGC \ + -XX:+UseContainerSupport \ + -Djans.base=/etc/jans \ + -Dserver.base=/opt/jans/jetty/jans-keycloak-link \ + -Dlog.base=/opt/jans/jetty/jans-keycloak-link \ + -Djava.io.tmpdir=/opt/jetty/temp \ + -Dlog4j2.configurationFile=resources/log4j2.xml \ + -Dpython.home=/opt/jython \ + $(get_max_ram_percentage) \ + $(get_prometheus_opt) \ + $(get_java_options) \ + -jar /opt/jetty/start.jar \ + jetty.http.host="${CN_KEYCLOAK_LINK_JETTY_HOST}" \ + jetty.http.port="${CN_KEYCLOAK_LINK_JETTY_PORT}" \ + jetty.deploy.scanInterval=0 \ + jetty.httpConfig.sendServerVersion=false diff --git a/docker-jans-keycloak-link/scripts/healthcheck.py b/docker-jans-keycloak-link/scripts/healthcheck.py new file mode 100644 index 00000000000..a3474f87de0 --- /dev/null +++ b/docker-jans-keycloak-link/scripts/healthcheck.py @@ -0,0 +1,23 @@ +import os +import sys + +import requests + + +def main(): + host = os.environ.get("CN_KEYCLOAK_LINK_JETTY_HOST", "0.0.0.0") # nosec: B104 + port = os.environ.get("CN_KEYCLOAK_LINK_JETTY_PORT", "9092") + req = requests.get(f"http://{host}:{port}/jans-keycloak-link/sys/health-check", timeout=5) + if not req.ok: + sys.exit(1) + + data = req.json() + if data["status"] == "running" and data["db_status"] == "online": + sys.exit(0) + + # any other value will be considered as unhealthy + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/docker-jans-keycloak-link/scripts/mod_context.py b/docker-jans-keycloak-link/scripts/mod_context.py new file mode 100644 index 00000000000..03f3c92d986 --- /dev/null +++ b/docker-jans-keycloak-link/scripts/mod_context.py @@ -0,0 +1,112 @@ +import argparse +import glob +import logging.config +import os +import pathlib +import re +import sys +import zipfile +from collections import namedtuple + +from jans.pycloudlib.persistence import PersistenceMapper +from jans.pycloudlib.utils import exec_cmd + +from settings import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("keycloak-link") + + +Library = namedtuple("Library", ["path", "basename", "meta"]) + +LIB_METADATA_RE = re.compile(r"(?P.*)-(?P\d+.*)(?P\.jar)") + + +def extract_common_libs(persistence_type): + dist_file = f"/usr/share/java/{persistence_type}-libs.zip" + + # download if file is missing + if not os.path.exists(dist_file): + version = os.environ.get("CN_VERSION") + download_url = f"https://jenkins.jans.io/maven/io/jans/jans-orm-{persistence_type}-libs/{version}/jans-orm-{persistence_type}-libs-{version}-distribution.zip" + basename = os.path.basename(download_url) + + logger.info(f"Downloading {basename} as {dist_file}") + + out, err, code = exec_cmd(f"wget -q {download_url} -O {dist_file}") + + if code != 0: + err = out or err + logger.error(f"Unable to download {basename}; reason={err.decode()}") + sys.exit(1) + + # extract + logger.info(f"Extracting {dist_file}") + out, err, code = exec_cmd(f"unzip -q {dist_file} -o -d /opt/jans/jetty/common/libs/{persistence_type}/") + if code != 0: + out = out or err + logger.error(f"Unable to extract {dist_file}; reason={err.decode()}") + sys.exit(1) + + +def get_lib_metadata(path_obj): + return Library(str(path_obj), path_obj.name, LIB_METADATA_RE.search(path_obj.name).groupdict()) + + +def get_archived_libs(app_name): + archive_path = f"/opt/jans/jetty/{app_name}/webapps/{app_name}.war" + with zipfile.ZipFile(archive_path) as zf: + zp = zipfile.Path(zf).joinpath("WEB-INF/lib") + return [get_lib_metadata(po) for po in zp.iterdir()] + + +def get_persistence_common_libs(dirpath): + root_dir = pathlib.Path(dirpath) + return [get_lib_metadata(po) for po in root_dir.rglob("*.jar")] + + +def get_default_custom_libs(app_name): + root = f"/opt/jans/jetty/{app_name}" + return [jar.replace(root, ".") for jar in glob.iglob(f"{root}/custom/libs/*.jar")] + + +def get_registered_common_libs(app_name, persistence_type): + libs = get_persistence_common_libs(f"/opt/jans/jetty/common/libs/{persistence_type}") + archived_libs = get_archived_libs(app_name) + archived_lib_names = [al.meta["name"] for al in archived_libs] + + return [ + lib.path for lib in libs + if lib.meta["name"] not in archived_lib_names + ] + + +def modify_app_xml(app_name): + custom_libs = get_default_custom_libs(app_name) + + mapper = PersistenceMapper() + persistence_groups = mapper.groups().keys() + + for persistence_type in ["spanner", "couchbase"]: + if persistence_type not in persistence_groups: + continue + + extract_common_libs(persistence_type) + custom_libs += get_registered_common_libs(app_name, persistence_type) + + # render custom xml + fn = f"/opt/jans/jetty/{app_name}/webapps/{app_name}.xml" + + with open(fn) as f: + txt = f.read() + + with open(fn, "w") as f: + ctx = {"extra_classpath": ",".join(custom_libs)} + f.write(txt % ctx) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("app_name") + args = parser.parse_args() + modify_app_xml(args.app_name) diff --git a/docker-jans-keycloak-link/scripts/settings.py b/docker-jans-keycloak-link/scripts/settings.py new file mode 100644 index 00000000000..34bac4dcea4 --- /dev/null +++ b/docker-jans-keycloak-link/scripts/settings.py @@ -0,0 +1,26 @@ +LOGGING_CONFIG = { + "version": 1, + "formatters": { + "default": { + "format": "%(levelname)s - %(name)s - %(asctime)s - %(message)s", + }, + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "formatter": "default", + }, + }, + "loggers": { + "jans.pycloudlib": { + "handlers": ["console"], + "level": "INFO", + "propagate": True, + }, + "keycloak-link": { + "handlers": ["console"], + "level": "INFO", + "propagate": False, + }, + }, +} diff --git a/docker-jans-keycloak-link/scripts/upgrade.py b/docker-jans-keycloak-link/scripts/upgrade.py new file mode 100644 index 00000000000..dc9d8691608 --- /dev/null +++ b/docker-jans-keycloak-link/scripts/upgrade.py @@ -0,0 +1,202 @@ +import json +import logging.config +import os +from collections import namedtuple + +from jans.pycloudlib import get_manager +from jans.pycloudlib.persistence import CouchbaseClient +from jans.pycloudlib.persistence import LdapClient +from jans.pycloudlib.persistence import SpannerClient +from jans.pycloudlib.persistence import SqlClient +from jans.pycloudlib.persistence import PersistenceMapper +from jans.pycloudlib.persistence import doc_id_from_dn +from jans.pycloudlib.persistence import id_from_dn + +from settings import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("keycloak-link") + +Entry = namedtuple("Entry", ["id", "attrs"]) + + +class LDAPBackend: + def __init__(self, manager): + self.manager = manager + self.client = LdapClient(manager) + self.type = "ldap" + + def format_attrs(self, attrs): + _attrs = {} + for k, v in attrs.items(): + if len(v) < 2: + v = v[0] + _attrs[k] = v + return _attrs + + def get_entry(self, key, filter_="", attrs=None, **kwargs): + filter_ = filter_ or "(objectClass=*)" + + entry = self.client.get(key, filter_=filter_, attributes=attrs) + if not entry: + return None + return Entry(entry.entry_dn, self.format_attrs(entry.entry_attributes_as_dict)) + + def modify_entry(self, key, attrs=None, **kwargs): + attrs = attrs or {} + del_flag = kwargs.get("delete_attr", False) + + if del_flag: + mod = self.client.MODIFY_DELETE + else: + mod = self.client.MODIFY_REPLACE + + for k, v in attrs.items(): + if not isinstance(v, list): + v = [v] + attrs[k] = [(mod, v)] + return self.client.modify(key, attrs) + + +class SQLBackend: + def __init__(self, manager): + self.manager = manager + self.client = SqlClient(manager) + self.type = "sql" + + def get_entry(self, key, filter_="", attrs=None, **kwargs): + table_name = kwargs.get("table_name") + entry = self.client.get(table_name, key, attrs) + + if not entry: + return None + return Entry(key, entry) + + def modify_entry(self, key, attrs=None, **kwargs): + attrs = attrs or {} + table_name = kwargs.get("table_name") + return self.client.update(table_name, key, attrs), "" + + +class CouchbaseBackend: + def __init__(self, manager): + self.manager = manager + self.client = CouchbaseClient(manager) + self.type = "couchbase" + + def get_entry(self, key, filter_="", attrs=None, **kwargs): + bucket = kwargs.get("bucket") + req = self.client.exec_query( + f"SELECT META().id, {bucket}.* FROM {bucket} USE KEYS '{key}'" # nosec: B608 + ) + if not req.ok: + return None + + try: + _attrs = req.json()["results"][0] + id_ = _attrs.pop("id") + entry = Entry(id_, _attrs) + except IndexError: + entry = None + return entry + + def modify_entry(self, key, attrs=None, **kwargs): + bucket = kwargs.get("bucket") + del_flag = kwargs.get("delete_attr", False) + attrs = attrs or {} + + if del_flag: + kv = ",".join(attrs.keys()) + mod_kv = f"UNSET {kv}" + else: + kv = ",".join([ + "{}={}".format(k, json.dumps(v)) + for k, v in attrs.items() + ]) + mod_kv = f"SET {kv}" + + query = f"UPDATE {bucket} USE KEYS '{key}' {mod_kv}" + req = self.client.exec_query(query) + + if req.ok: + resp = req.json() + status = bool(resp["status"] == "success") + message = resp["status"] + else: + status = False + message = req.text or req.reason + return status, message + + +class SpannerBackend: + def __init__(self, manager): + self.manager = manager + self.client = SpannerClient(manager) + self.type = "spanner" + + def get_entry(self, key, filter_="", attrs=None, **kwargs): + table_name = kwargs.get("table_name") + entry = self.client.get(table_name, key, attrs) + + if not entry: + return None + return Entry(key, entry) + + def modify_entry(self, key, attrs=None, **kwargs): + attrs = attrs or {} + table_name = kwargs.get("table_name") + return self.client.update(table_name, key, attrs), "" + + +BACKEND_CLASSES = { + "sql": SQLBackend, + "couchbase": CouchbaseBackend, + "spanner": SpannerBackend, + "ldap": LDAPBackend, +} + + +class Upgrade: + def __init__(self, manager): + self.manager = manager + + mapper = PersistenceMapper() + + backend_cls = BACKEND_CLASSES[mapper.mapping["default"]] + self.backend = backend_cls(manager) + + def invoke(self): + logger.info("Running upgrade process (if required)") + self.enable_ext_script() + + def enable_ext_script(self): + # default to ldap persistence + kwargs = {} + script_id = "inum=13D3-E7AD,ou=scripts,o=jans" + + if self.backend.type in ("sql", "spanner"): + kwargs = {"table_name": "jansCustomScr"} + script_id = doc_id_from_dn(script_id) + + elif self.backend.type == "couchbase": + kwargs = {"bucket": os.environ.get("CN_COUCHBASE_BUCKET_PREFIX", "jans")} + script_id = id_from_dn(script_id) + + # toggle cache-refresh script + entry = self.backend.get_entry(script_id, **kwargs) + + if entry: + entry.attrs["jansEnabled"] = True + self.backend.modify_entry(entry.id, entry.attrs, **kwargs) + + +def main(): + manager = get_manager() + + with manager.lock.create_lock("keycloak-link-upgrade"): + upgrade = Upgrade(manager) + upgrade.invoke() + + +if __name__ == "__main__": + main() diff --git a/docker-jans-keycloak-link/scripts/wait.py b/docker-jans-keycloak-link/scripts/wait.py new file mode 100644 index 00000000000..1e195e298e3 --- /dev/null +++ b/docker-jans-keycloak-link/scripts/wait.py @@ -0,0 +1,34 @@ +import logging.config +import os + +from jans.pycloudlib import get_manager +from jans.pycloudlib import wait_for +from jans.pycloudlib import wait_for_persistence +from jans.pycloudlib.validators import validate_persistence_type +from jans.pycloudlib.validators import validate_persistence_hybrid_mapping +from jans.pycloudlib.validators import validate_persistence_sql_dialect + +from settings import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) + + +def main(): + persistence_type = os.environ.get("CN_PERSISTENCE_TYPE", "ldap") + validate_persistence_type(persistence_type) + + if persistence_type == "hybrid": + validate_persistence_hybrid_mapping() + + if persistence_type == "sql": + sql_dialect = os.environ.get("CN_SQL_DB_DIALECT", "mysql") + validate_persistence_sql_dialect(sql_dialect) + + manager = get_manager() + deps = ["config", "secret"] + wait_for(manager, deps) + wait_for_persistence(manager) + + +if __name__ == "__main__": + main() diff --git a/docker-jans-keycloak-link/static/jetty-env.xml b/docker-jans-keycloak-link/static/jetty-env.xml new file mode 100644 index 00000000000..228cae0b737 --- /dev/null +++ b/docker-jans-keycloak-link/static/jetty-env.xml @@ -0,0 +1,22 @@ + + + + + + + + + + BeanManager + + + + javax.enterprise.inject.spi.BeanManager + org.jboss.weld.resources.ManagerObjectFactory + + + + + + diff --git a/docker-jans-keycloak-link/static/prometheus-config.yaml b/docker-jans-keycloak-link/static/prometheus-config.yaml new file mode 100644 index 00000000000..6e8a2583872 --- /dev/null +++ b/docker-jans-keycloak-link/static/prometheus-config.yaml @@ -0,0 +1,10 @@ +--- +startDelaySeconds: 0 +ssl: false +lowercaseOutputName: true +lowercaseOutputLabelNames: true +whitelistObjectNames: ["org.eclipse.jetty.server.handler:*"] +rules: + - pattern: ".*xx" + - pattern: ".*requests" + - pattern: ".*requestTimeTotal" diff --git a/docker-jans-keycloak-link/templates/jans-couchbase.properties b/docker-jans-keycloak-link/templates/jans-couchbase.properties new file mode 100644 index 00000000000..d5caad62951 --- /dev/null +++ b/docker-jans-keycloak-link/templates/jans-couchbase.properties @@ -0,0 +1,64 @@ +servers: %(hostname)s + +# The connect timeout is used when a Bucket is opened. +# If you feel the urge to change this value to something higher, there is a good chance that your network is not properly set up. +# Connecting to the server should in practice not take longer than a second on a reasonably fast network. +# Default SDK connectTimeout is 10s +connection.connect-timeout: %(couchbase_conn_timeout)s + +# Enable/disable DNS SRV lookup for the bootstrap nodes +# Default dnsSrvEnabled is true +connection.dns.use-lookup: false + +# Key/value timeout +# Default SDK kvTimeout is 2500ms +connection.kv-timeout: 5000 + +# Query timeout +# Default SDK queryTimeout is 75s +connection.query-timeout: 75000 + +# Configures whether mutation tokens will be returned from the server for all mutation operations +# Default mutationTokensEnabled is true +# connection.mutation-tokens-enabled: false + +# At startup when connection error is occurred persistence layer can make another attempt to open buckets. +# Before make next try it pause process for 5 second. If after that total connection time is less than specified +# in property above new attempt will be executed +connection.connection-max-wait-time: %(couchbase_conn_max_wait)s + +# Default scan consistency. Possible values are: not_bounded, request_plus, statement_plus +connection.scan-consistency: %(couchbase_scan_consistency)s + +# Disable scan consistency in queries. Default value: false +# connection.ignore-attribute-scan-consistency: true + +# Try to execute query with scan consitency specified in connection.scan-consistency first. +# On failure execute query again with scan consistency specified in attributes defintions. Default value: true +# connection.attempt-without-attribute-scan-consistency: false + +# Enable scopes support. Default value: false +# connection.enable-scope-support: true + +# Disable mapping to short attribute names. Default value: false +# connection.disable-attribute-mapping: true + +auth.userName: %(couchbase_server_user)s +auth.userPassword: %(encoded_couchbase_server_pw)s + +buckets: %(couchbase_buckets)s + +bucket.default: %(default_bucket)s +%(couchbase_mappings)s + +password.encryption.method: %(encryption_method)s + +ssl.trustStore.enable: %(ssl_enabled)s +ssl.trustStore.file: %(couchbaseTrustStoreFn)s +ssl.trustStore.pin: %(encoded_couchbaseTrustStorePass)s +ssl.trustStore.type: pkcs12 + +tls.enable: false + +binaryAttributes=objectGUID +certificateAttributes=userCertificate diff --git a/docker-jans-keycloak-link/templates/jans-keycloak-link/jans-keycloak-link.xml b/docker-jans-keycloak-link/templates/jans-keycloak-link/jans-keycloak-link.xml new file mode 100644 index 00000000000..b568bf00153 --- /dev/null +++ b/docker-jans-keycloak-link/templates/jans-keycloak-link/jans-keycloak-link.xml @@ -0,0 +1,11 @@ + + + + + /jans-keycloak-link + + /jans-keycloak-link.war + + true + %(extra_classpath)s + diff --git a/docker-jans-keycloak-link/templates/jans-keycloak-link/log4j2.xml b/docker-jans-keycloak-link/templates/jans-keycloak-link/log4j2.xml new file mode 100644 index 00000000000..604d4f14a29 --- /dev/null +++ b/docker-jans-keycloak-link/templates/jans-keycloak-link/log4j2.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -persistence + + + + + -persistence + + + + -persistence + + + + + -persistence-duration + + + + + -persistence-duration + + + + + -persistence-duration + + + + + -ldap-stats + + + + + -script + + + + + -script + + + + + -script + + + + + -script + + + + + + + + + + diff --git a/docker-jans-keycloak-link/templates/jans-ldap.properties b/docker-jans-keycloak-link/templates/jans-ldap.properties new file mode 100644 index 00000000000..a7ec401fe75 --- /dev/null +++ b/docker-jans-keycloak-link/templates/jans-ldap.properties @@ -0,0 +1,28 @@ +bindDN: %(ldap_binddn)s +bindPassword: %(encoded_ox_ldap_pw)s +servers: %(ldap_hostname)s:%(ldaps_port)s + +useSSL: %(ssl_enabled)s +ssl.trustStoreFile: %(ldapTrustStoreFn)s +ssl.trustStorePin: %(encoded_ldapTrustStorePass)s +ssl.trustStoreFormat: pkcs12 + +maxconnections: 40 + +# Max wait 20 seconds +connection.max-wait-time-millis=20000 + +# Force to recreate polled connections after 30 minutes +connection.max-age-time-millis=1800000 + +# Invoke connection health check after checkout it from pool +connection-pool.health-check.on-checkout.enabled=false + +# Interval to check connections in pool. Value is 3 minutes. Not used when onnection-pool.health-check.on-checkout.enabled=true +connection-pool.health-check.interval-millis=180000 + +# How long to wait during connection health check. Max wait 20 seconds +connection-pool.health-check.max-response-time-millis=20000 + +binaryAttributes=objectGUID +certificateAttributes=userCertificate diff --git a/docker-jans-keycloak-link/templates/jans-mysql.properties b/docker-jans-keycloak-link/templates/jans-mysql.properties new file mode 100644 index 00000000000..ae85a0595f3 --- /dev/null +++ b/docker-jans-keycloak-link/templates/jans-mysql.properties @@ -0,0 +1,37 @@ +db.schema.name=%(rdbm_schema)s + +connection.uri=jdbc:mysql://%(rdbm_host)s:%(rdbm_port)s/%(rdbm_db)s?enabledTLSProtocols=TLSv1.2 + +connection.driver-property.serverTimezone=%(server_time_zone)s +# Prefix connection.driver-property.key=value will be coverterd to key=value JDBC driver properties +#connection.driver-property.driverProperty=driverPropertyValue + +#connection.driver-property.useServerPrepStmts=false +connection.driver-property.cachePrepStmts=false +connection.driver-property.cacheResultSetMetadata=true +connection.driver-property.metadataCacheSize=500 +#connection.driver-property.prepStmtCacheSize=500 +#connection.driver-property.prepStmtCacheSqlLimit=1024 + +auth.userName=%(rdbm_user)s +auth.userPassword=%(rdbm_password_enc)s + +# Password hash method +password.encryption.method=SSHA-256 + +# Connection pool size +connection.pool.max-total=40 +connection.pool.max-idle=15 +connection.pool.min-idle=5 + +# Max time needed to create connection pool in milliseconds +connection.pool.create-max-wait-time-millis=20000 + +# Max wait 20 seconds +connection.pool.max-wait-time-millis=20000 + +# Allow to evict connection in pool after 30 minutes +connection.pool.min-evictable-idle-time-millis=1800000 + +binaryAttributes=objectGUID +certificateAttributes=userCertificate diff --git a/docker-jans-keycloak-link/templates/jans-pgsql.properties b/docker-jans-keycloak-link/templates/jans-pgsql.properties new file mode 100644 index 00000000000..577cabb7149 --- /dev/null +++ b/docker-jans-keycloak-link/templates/jans-pgsql.properties @@ -0,0 +1,29 @@ +db.schema.name=%(rdbm_schema)s + +connection.uri=jdbc:postgresql://%(rdbm_host)s:%(rdbm_port)s/%(rdbm_db)s + +# Prefix connection.driver-property.key=value will be coverterd to key=value JDBC driver properties +#connection.driver-property.driverProperty=driverPropertyValue + +auth.userName=%(rdbm_user)s +auth.userPassword=%(rdbm_password_enc)s + +# Password hash method +password.encryption.method=SSHA-256 + +# Connection pool size +connection.pool.max-total=40 +connection.pool.max-idle=15 +connection.pool.min-idle=5 + +# Max time needed to create connection pool in milliseconds +connection.pool.create-max-wait-time-millis=20000 + +# Max wait 20 seconds +connection.pool.max-wait-time-millis=20000 + +# Allow to evict connection in pool after 30 minutes +connection.pool.min-evictable-idle-time-millis=1800000 + +binaryAttributes=objectGUID +certificateAttributes=userCertificate diff --git a/docker-jans-keycloak-link/templates/jans-spanner.properties b/docker-jans-keycloak-link/templates/jans-spanner.properties new file mode 100644 index 00000000000..73db25b7d54 --- /dev/null +++ b/docker-jans-keycloak-link/templates/jans-spanner.properties @@ -0,0 +1,30 @@ +connection.project=%(spanner_project)s +connection.instance=%(spanner_instance)s +connection.database=%(spanner_database)s + +# Prefix connection.client-property.key=value will be coverterd to key=value +# This is reserved for future usage +#connection.client-property=clientPropertyValue + +# spanner creds or emulator +%(spanner_creds)s + +# Password hash method +password.encryption.method=SSHA-256 + +# Connection pool size +#connection.pool.max-sessions=400 +#connection.pool.min-sessions=100 +#connection.pool.inc-step=25 + +# Max time needed to create connection pool in milliseconds +connection.pool.create-max-wait-time-millis=20000 + +# Maximum allowed statement result set size +statement.limit.default-maximum-result-size=1000 + +# Maximum allowed delete statement result set size +statement.limit.maximum-result-delete-size=10000 + +binaryAttributes=objectGUID +certificateAttributes=userCertificate diff --git a/docker-jans-keycloak-link/templates/jans.properties b/docker-jans-keycloak-link/templates/jans.properties new file mode 100644 index 00000000000..95e378aa65b --- /dev/null +++ b/docker-jans-keycloak-link/templates/jans.properties @@ -0,0 +1,13 @@ +persistence.type=%(persistence_type)s + +jansAuth_ConfigurationEntryDN=ou=jans-auth,ou=configuration,o=jans +fido2_ConfigurationEntryDN=ou=jans-fido2,ou=configuration,o=jans +scim_ConfigurationEntryDN=ou=jans-scim,ou=configuration,o=jans +configApi_ConfigurationEntryDN=ou=jans-config-api,ou=configuration,o=jans +link_ConfigurationEntryDN=ou=jans-link,ou=configuration,o=jans +keycloakLink_ConfigurationEntryDN=ou=jans-keycloak-link,ou=configuration,o=jans +saml_ConfigurationEntryDN=ou=jans-saml,ou=configuration,o=jans + +certsDir=/etc/certs +confDir= +pythonModulesDir=/opt/jans/python/libs:/opt/jython/Lib/site-packages diff --git a/docker-jans-keycloak-link/templates/salt b/docker-jans-keycloak-link/templates/salt new file mode 100644 index 00000000000..ee6c2c330e3 --- /dev/null +++ b/docker-jans-keycloak-link/templates/salt @@ -0,0 +1 @@ +encodeSalt = %(encode_salt)s diff --git a/docker-jans-keycloak-link/version.txt b/docker-jans-keycloak-link/version.txt new file mode 100644 index 00000000000..921168431e4 --- /dev/null +++ b/docker-jans-keycloak-link/version.txt @@ -0,0 +1 @@ +1.0.19-1