diff --git a/charts/janssen/charts/casa/README.md b/charts/janssen/charts/casa/README.md index 1fe2ba52a17..87cf027e8c6 100644 --- a/charts/janssen/charts/casa/README.md +++ b/charts/janssen/charts/casa/README.md @@ -38,12 +38,12 @@ Kubernetes: `>=v1.21.0-0` | image.repository | string | `"janssenproject/casa"` | Image to use for deploying. | | image.tag | string | `"1.0.19_dev"` | Image tag to use for deploying. | | lifecycle | object | `{}` | | -| livenessProbe | object | `{"httpGet":{"path":"/casa/health-check","port":"http-casa"},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5}` | Configure the liveness healthcheck for casa if needed. | -| livenessProbe.httpGet.path | string | `"/casa/health-check"` | http liveness probe endpoint | +| livenessProbe | object | `{"httpGet":{"path":"/jans-casa/health-check","port":"http-casa"},"initialDelaySeconds":25,"periodSeconds":25,"timeoutSeconds":5}` | Configure the liveness healthcheck for casa if needed. | +| livenessProbe.httpGet.path | string | `"/jans-casa/health-check"` | http liveness probe endpoint | | nameOverride | string | `""` | | | podSecurityContext | object | `{}` | | -| readinessProbe | object | `{"httpGet":{"path":"/casa/health-check","port":"http-casa"},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5}` | Configure the readiness healthcheck for the casa if needed. | -| readinessProbe.httpGet.path | string | `"/casa/health-check"` | http readiness probe endpoint | +| readinessProbe | object | `{"httpGet":{"path":"/jans-casa/health-check","port":"http-casa"},"initialDelaySeconds":30,"periodSeconds":30,"timeoutSeconds":5}` | Configure the readiness healthcheck for the casa if needed. | +| readinessProbe.httpGet.path | string | `"/jans-casa/health-check"` | http readiness probe endpoint | | replicas | int | `1` | Service replica number. | | resources | object | `{"limits":{"cpu":"500m","memory":"500Mi"},"requests":{"cpu":"500m","memory":"500Mi"}}` | Resource specs. | | resources.limits.cpu | string | `"500m"` | CPU limit. | diff --git a/charts/janssen/charts/casa/templates/casa-virtual-services.yaml b/charts/janssen/charts/casa/templates/casa-virtual-services.yaml index b3d76594997..3d34e054105 100644 --- a/charts/janssen/charts/casa/templates/casa-virtual-services.yaml +++ b/charts/janssen/charts/casa/templates/casa-virtual-services.yaml @@ -28,7 +28,7 @@ spec: - name: {{ .Release.Name }}-istio-casa match: - uri: - prefix: /casa + prefix: /jans-casa route: - destination: host: {{ .Values.global.casa.casaServiceName }}.{{.Release.Namespace}}.svc.cluster.local diff --git a/charts/janssen/charts/casa/values.yaml b/charts/janssen/charts/casa/values.yaml index 751445a017d..97c6b9ef6b4 100644 --- a/charts/janssen/charts/casa/values.yaml +++ b/charts/janssen/charts/casa/values.yaml @@ -59,7 +59,7 @@ service: livenessProbe: httpGet: # -- http liveness probe endpoint - path: /casa/health-check + path: /jans-casa/health-check port: http-casa initialDelaySeconds: 25 periodSeconds: 25 @@ -68,7 +68,7 @@ livenessProbe: readinessProbe: httpGet: # -- http readiness probe endpoint - path: /casa/health-check + path: /jans-casa/health-check port: http-casa initialDelaySeconds: 30 periodSeconds: 30 @@ -102,4 +102,4 @@ securityContext: {} # -- Additional labels that will be added across all resources definitions in the format of {mylabel: "myapp"} additionalLabels: { } # -- Additional annotations that will be added across all resources in the format of {cert-manager.io/issuer: "letsencrypt-prod"}. key app is taken -additionalAnnotations: { } \ No newline at end of file +additionalAnnotations: { } diff --git a/charts/janssen/charts/nginx-ingress/templates/ingress.yaml b/charts/janssen/charts/nginx-ingress/templates/ingress.yaml index 54e4812db8d..d9ce0dce2af 100644 --- a/charts/janssen/charts/nginx-ingress/templates/ingress.yaml +++ b/charts/janssen/charts/nginx-ingress/templates/ingress.yaml @@ -716,7 +716,7 @@ spec: - host: {{ $host | quote }} http: paths: - - path: /casa + - path: /jans-casa pathType: Prefix backend: service: diff --git a/docker-jans-auth-server/Dockerfile b/docker-jans-auth-server/Dockerfile index 42afad46b99..47d7b6fed0e 100644 --- a/docker-jans-auth-server/Dockerfile +++ b/docker-jans-auth-server/Dockerfile @@ -51,7 +51,7 @@ RUN /opt/jython/bin/pip uninstall -y pip setuptools # =========== ENV CN_VERSION=1.0.19-SNAPSHOT -ENV CN_BUILD_DATE='2023-09-23 10:17' +ENV CN_BUILD_DATE='2023-10-05 08:23' ENV CN_SOURCE_URL=https://jenkins.jans.io/maven/io/jans/jans-auth-server/${CN_VERSION}/jans-auth-server-${CN_VERSION}.war # Install Jans Auth @@ -74,46 +74,25 @@ RUN mkdir -p /usr/share/java \ ARG TWILIO_VERSION=7.17.0 ARG JSMPP_VERSION=2.3.7 -ARG CASA_CONFIG_VERSION=5.0.0-SNAPSHOT +ARG CASA_CONFIG_VERSION=1.0.19-SNAPSHOT ARG CASA_CONFIG_BUILD_DATE="2023-02-13 11:44" ARG FIDO2_CLIENT_VERSION=1.0.19-SNAPSHOT ARG FIDO2_CLIENT_BUILD_DATE="2023-01-31 15:04" RUN wget -q https://repo1.maven.org/maven2/com/twilio/sdk/twilio/${TWILIO_VERSION}/twilio-${TWILIO_VERSION}.jar -P ${JETTY_BASE}/jans-auth/_libs/ \ && wget -q https://repo1.maven.org/maven2/org/jsmpp/jsmpp/${JSMPP_VERSION}/jsmpp-${JSMPP_VERSION}.jar -P ${JETTY_BASE}/jans-auth/_libs/ \ - && wget -q https://jenkins.gluu.org/maven/org/gluu/casa-config/${CASA_CONFIG_VERSION}/casa-config-${CASA_CONFIG_VERSION}.jar -P ${JETTY_BASE}/jans-auth/_libs \ + && wget -q https://jenkins.jans.io/maven/io/jans/casa-config/${CASA_CONFIG_VERSION}/casa-config-${CASA_CONFIG_VERSION}.jar -P ${JETTY_BASE}/jans-auth/_libs \ && wget -q https://jenkins.jans.io/maven/io/jans/jans-fido2-client/${FIDO2_CLIENT_VERSION}/jans-fido2-client-${FIDO2_CLIENT_VERSION}.jar -P ${JETTY_BASE}/jans-auth/_libs # ===================== -# Casa external scripts +# jans-linux-setup sync # ===================== -ARG FLEX_SOURCE_VERSION=3f0281bbf381a63b28229388d6b0ae536902b455 -ARG CASA_EXTRAS_DIR=casa/extras - -RUN mkdir -p /opt/jans/python/libs -RUN git clone --filter blob:none --no-checkout https://github.com/GluuFederation/flex.git /tmp/flex \ - && cd /tmp/flex \ - && git sparse-checkout init --cone \ - && git checkout ${FLEX_SOURCE_VERSION} \ - && git sparse-checkout add ${CASA_EXTRAS_DIR} \ - && cd /opt/jans/python/libs \ - && cp /tmp/flex/${CASA_EXTRAS_DIR}/casa-external_* . \ - && rm -rf /tmp/flex - -# =========== -# Agama files -# =========== - RUN mkdir -p ${JETTY_BASE}/jans-auth/agama/fl \ ${JETTY_BASE}/jans-auth/agama/ftl \ ${JETTY_BASE}/jans-auth/agama/scripts -# ===================== -# jans-linux-setup sync -# ===================== - -ENV JANS_SOURCE_VERSION=14a4ee5d21b788db7bb3e9bb94a1d1caf228f95a +ENV JANS_SOURCE_VERSION=eb4e84a3b7fbf9a3ad778b3cc77b40dec3210e5d # 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 agama code @@ -122,10 +101,12 @@ RUN git clone --filter blob:none --no-checkout https://github.com/janssenproject && git sparse-checkout init --cone \ && git checkout ${JANS_SOURCE_VERSION} \ && git sparse-checkout add agama/misc \ - && git sparse-checkout add jans-linux-setup/jans_setup/static/auth/conf - -RUN cp -R /tmp/jans/agama/misc/* ${JETTY_BASE}/jans-auth/agama/ \ - && cp -R /tmp/jans/jans-linux-setup/jans_setup/static/auth/conf /etc/certs + && git sparse-checkout add jans-linux-setup/jans_setup/static/auth/conf \ + && git sparse-checkout add jans-casa/extras \ + && cp -R agama/misc/* ${JETTY_BASE}/jans-auth/agama/ \ + && cp -R jans-linux-setup/jans_setup/static/auth/conf /etc/certs \ + && mkdir -p /opt/jans/python/libs \ + && cp jans-casa/extras/casa-external_* /opt/jans/python/libs # ====== # Python diff --git a/docker-jans-casa/.dockerignore b/docker-jans-casa/.dockerignore new file mode 100644 index 00000000000..74100673491 --- /dev/null +++ b/docker-jans-casa/.dockerignore @@ -0,0 +1,9 @@ +# exclude everything +* + +# include required files/directories +!scripts +!LICENSE +!requirements.txt +!templates +!static diff --git a/docker-jans-casa/.gitignore b/docker-jans-casa/.gitignore new file mode 100644 index 00000000000..d40b03e27a8 --- /dev/null +++ b/docker-jans-casa/.gitignore @@ -0,0 +1,103 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyprojects + +# Rope project settings +.ropeproject + +# PyCharm project settings +.idea +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/docker-jans-casa/.hadolint.yaml b/docker-jans-casa/.hadolint.yaml new file mode 100644 index 00000000000..428f8174ee2 --- /dev/null +++ b/docker-jans-casa/.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-casa/Dockerfile b/docker-jans-casa/Dockerfile new file mode 100644 index 00000000000..b56ce3dfcc2 --- /dev/null +++ b/docker-jans-casa/Dockerfile @@ -0,0 +1,263 @@ +FROM bellsoft/liberica-openjre-alpine:11.0.16 + +# =============== +# Alpine packages +# =============== + +RUN apk update \ + && apk upgrade --available \ + && apk add --no-cache python3 openssl tini py3-cryptography py3-psycopg2 py3-grpcio \ + && apk add --no-cache --virtual .build-deps git wget 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 + +# ==== +# Casa +# ==== + +ENV CN_VERSION=1.0.19-SNAPSHOT +ENV CN_BUILD_DATE='2023-10-05 08:38' +ENV CN_SOURCE_URL=https://jenkins.jans.io/maven/io/jans/casa/${CN_VERSION}/casa-${CN_VERSION}.war + +# Install Casa +COPY static/jetty-env.xml /tmp/WEB-INF/jetty-env.xml +RUN mkdir -p ${JETTY_BASE}/jans-casa/webapps \ + && wget -q ${CN_SOURCE_URL} -O /tmp/jans-casa.war \ + && cd /tmp \ + && zip -d jans-casa.war WEB-INF/jetty-web.xml \ + && zip -r jans-casa.war WEB-INF/jetty-env.xml \ + && cp jans-casa.war ${JETTY_BASE}/jans-casa/webapps/jans-casa.war \ + && java -jar ${JETTY_HOME}/start.jar jetty.home=${JETTY_HOME} jetty.base=${JETTY_BASE}/jans-casa --add-module=server,deploy,resources,http,jsp,cdi-decorate,jmx,stats,logging-log4j2 --approve-all-licenses \ + && rm -rf /tmp/jans-casa.war /tmp/WEB-INF + +# casa plugins +RUN mkdir -p ${JETTY_BASE}/jans-casa/plugins \ + && for casa_plugin in strong-authn-settings authorized-clients custom-branding; \ + do \ + wget -nv "https://jenkins.jans.io/maven/io/jans/casa/plugins/${casa_plugin}/${CN_VERSION}/${casa_plugin}-${CN_VERSION}-jar-with-dependencies.jar" -O "${JETTY_BASE}/jans-casa/plugins/${casa_plugin}-${CN_VERSION}.jar"; \ + done + +# ===================== +# jans-linux-setup sync +# ===================== + +ENV JANS_SOURCE_VERSION=eb4e84a3b7fbf9a3ad778b3cc77b40dec3210e5d +ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup +ARG JANS_CASA_EXTRAS_DIR=jans-casa/extras + +# 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} \ + && git sparse-checkout add ${JANS_CASA_EXTRAS_DIR} + +RUN mkdir -p /app/static/rdbm \ + /app/schema \ + /app/templates/jans-casa \ + /app/static/extension/person_authentication + +# 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-casa/* /app/templates/jans-casa/ \ + && cp ${JANS_CASA_EXTRAS_DIR}/Casa.py /app/static/extension/person_authentication/ + +# ====== +# 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 \ + CN_GOOGLE_SPANNER_INSTANCE_ID="" \ + CN_GOOGLE_SPANNER_DATABASE_ID="" + +# =========== +# Generic ENV +# =========== + +ENV CN_MAX_RAM_PERCENTAGE=75.0 \ + CN_WAIT_MAX_TIME=300 \ + CN_WAIT_SLEEP_DURATION=10 \ + PYTHON_HOME=/opt/jython \ + CN_DOCUMENT_STORE_TYPE=LOCAL \ + CN_JACKRABBIT_URL=http://localhost:8080 \ + CN_JACKRABBIT_ADMIN_ID=admin \ + CN_JACKRABBIT_ADMIN_PASSWORD_FILE=/etc/jans/conf/jackrabbit_admin_password \ + CN_CASA_JAVA_OPTIONS="" \ + CN_SSL_CERT_FROM_SECRETS=false \ + GOOGLE_PROJECT_ID="" \ + CN_GOOGLE_SECRET_MANAGER_PASSPHRASE=secret \ + CN_GOOGLE_SECRET_VERSION_ID=latest \ + CN_GOOGLE_SECRET_NAME_PREFIX=jans \ + CN_CASA_ADMIN_LOCK_FILE=/opt/jans/jetty/jans-casa/resources/.administrable \ + CN_PROMETHEUS_PORT="" \ + CN_AWS_SECRETS_ENDPOINT_URL="" \ + CN_AWS_SECRETS_PREFIX=jans \ + CN_AWS_SECRETS_REPLICA_FILE="" \ + CN_CASA_JWKS_SIZE_LIMIT=100000 \ + CN_CASA_JETTY_PORT=8080 \ + CN_CASA_JETTY_HOST=0.0.0.0 \ + CN_SHARE_AUTH_CONF=true + +# ========== +# misc stuff +# ========== + +EXPOSE $CN_CASA_JETTY_PORT + +LABEL org.opencontainers.image.url="ghcr.io/janssenproject/jans/casa" \ + org.opencontainers.image.authors="Gluu Inc. " \ + org.opencontainers.image.vendor="Gluu Federation" \ + org.opencontainers.image.version="1.0.19" \ + org.opencontainers.image.title="Janssen Casa" \ + org.opencontainers.image.description="Self-service portal for people to manage their account security preferences in the Janssen, like 2FA" + +RUN mkdir -p /opt/jans/python/libs \ + ${JETTY_BASE}/jans-casa/static \ + ${JETTY_BASE}/jans-casa/plugins \ + ${JETTY_BASE}/jans-casa/logs \ + ${JETTY_BASE}/common/libs/spanner \ + ${JETTY_BASE}/common/libs/couchbase \ + ${JETTY_HOME}/temp \ + /etc/jans/conf/casa \ + /etc/certs \ + /usr/share/java + +COPY templates /app/templates/ +RUN cp /app/templates/jans-casa/jans-casa.xml ${JETTY_BASE}/jans-casa/webapps/ \ + && cp /app/templates/jans-casa/jans-casa_web_resources.xml ${JETTY_BASE}/jans-casa/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 -R g=u ${JETTY_BASE}/jans-casa/static \ + && chmod -R g=u ${JETTY_BASE}/jans-casa/plugins \ + && chown -R 1000:0 ${JETTY_BASE}/jans-casa/resources \ + && chmod 664 ${JETTY_BASE}/jans-casa/resources/log4j2.xml \ + && chmod -R g=u ${JETTY_BASE}/jans-casa/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-casa/webapps/jans-casa.xml \ + && chown -R 1000:0 ${JETTY_HOME}/temp \ + && chown -R 1000:0 /app/templates/jans-casa + +USER 1000 + +RUN mkdir -p $HOME/.config/gcloud + +ENTRYPOINT ["tini", "-e", "143", "-g", "--"] +CMD ["sh", "/app/scripts/entrypoint.sh"] diff --git a/docker-jans-casa/LICENSE b/docker-jans-casa/LICENSE new file mode 100644 index 00000000000..f584424836d --- /dev/null +++ b/docker-jans-casa/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 2020 Gluu, Inc. + + 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-casa/Makefile b/docker-jans-casa/Makefile new file mode 100644 index 00000000000..2ee59219494 --- /dev/null +++ b/docker-jans-casa/Makefile @@ -0,0 +1,27 @@ +CN_VERSION?=$(shell grep -Po 'org.opencontainers.image.version="\K.*?(?=")' Dockerfile) +IMAGE_NAME?=$(shell grep -Po 'org.opencontainers.image.url="\K.*?(?=")' Dockerfile) +DEV_VERSION?=$(shell echo ${CN_VERSION} | cut -d '-' -f 1)_dev + +# pass extra build args, i.e. `make build-dev BUILD_ARGS="--no-cache"` +BUILD_ARGS?= + +# pass extra trivy args, i.e. `make trivy-scan TRIVY_ARGS="-f json"` +TRIVY_ARGS?= + +# pass extra grype args, i.e. `make grype-scan GRYPE_ARGS="-o json"` +GRYPE_ARGS?= + +.PHONY: test clean all build-dev trivy-scan grype-scan +.DEFAULT_GOAL := build-dev + +build-dev: + @echo "[I] Building Docker image ${IMAGE_NAME}:${DEV_VERSION}" + @docker build --rm --force-rm ${BUILD_ARGS} -t ${IMAGE_NAME}:${DEV_VERSION} . + +trivy-scan: + @echo "[I] Scanning Docker image ${IMAGE_NAME}:${DEV_VERSION} using trivy" + @trivy image --scanners vuln ${TRIVY_ARGS} ${IMAGE_NAME}:${DEV_VERSION} + +grype-scan: + @echo "[I] Scanning Docker image ${IMAGE_NAME}:${DEV_VERSION} using grype" + @grype -v ${GRYPE_ARGS} ${IMAGE_NAME}:${DEV_VERSION} diff --git a/docker-jans-casa/README.md b/docker-jans-casa/README.md new file mode 100644 index 00000000000..d76b1ed4962 --- /dev/null +++ b/docker-jans-casa/README.md @@ -0,0 +1,157 @@ +## Overview + +Docker assets for Casa + +## Versions + +See [Packages](https://github.com/orgs/JanssenProject/packages/container/package/jans%2Fcasa) 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` or `kubernetes`. +- `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_CONFIGMAP`: 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`); required if `CN_PERSISTENCE_TYPE` is set to `ldap` or `hybrid`. +- `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`); required if `CN_PERSISTENCE_TYPE` is set to `couchbase` or `hybrid`. +- `CN_COUCHBASE_USER`: Username of Couchbase server (default to `admin`); required if `CN_PERSISTENCE_TYPE` is set to `couchbase` or `hybrid`. +- `CN_COUCHBASE_CERT_FILE`: Couchbase root certificate location (default to `/etc/certs/couchbase.crt`); required if `CN_PERSISTENCE_TYPE` is set to `couchbase` or `hybrid`. +- `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_JAVA_OPTIONS`: Java options passed to entrypoint, i.e. `-Xmx1024m` (default to empty-string). +- `CN_DOCUMENT_STORE_TYPE`: Document store type (one of `LOCAL` or `JCA`; default to `LOCAL`). +- `CN_JACKRABBIT_URL`: URL to remote repository (default to `http://localhost:8080`). +- `CN_JACKRABBIT_SYNC_INTERVAL`: Interval between files sync (default to `300` seconds). +- `CN_JACKRABBIT_ADMIN_ID`: Admin username (default to `admin`). +- `CN_JACKRABBIT_ADMIN_PASSWORD_FILE`: Absolute path to file contains password for admin user (default to `/etc/jans/conf/jackrabbit_admin_password`). +- `CN_SSL_CERT_FROM_SECRETS`: Determine whether to get SSL cert from secrets backend (default to `false`). Note that the flag will take effect only if there's no mounted `/etc/certs/web_https.crt` file. +- `CN_SQL_DB_DIALECT`: Dialect name of SQL backend (one of `mysql`, `pgsql`; default to `mysql`). +- `CN_SQL_DB_HOST`: Host of SQL backend (default to `localhost`). +- `CN_SQL_DB_PORT`: Port of SQL backend (default to `3306`). +- `CN_SQL_DB_NAME`: Database name (default to `jans`) +- `CN_SQL_DB_USER`: Username to interact with SQL backend (default to `jans`). +- `CN_GOOGLE_SPANNER_INSTANCE_ID`: Instance ID of Google Spanner (default to empty string). +- `CN_GOOGLE_SPANNER_DATABASE_ID`: Database ID of Google Spanner (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_CASA_APP_LOGGERS`: Custom logging configuration in JSON-string format with hash type (see [Configure app loggers](#configure-app-loggers) section for details). +- `CN_CASA_ADMIN_LOCK_FILE`: Path to lock file to enable/disable administration feature (default to `/opt/jans/jetty/casa/resources/.administrable`). If file is not exist, the feature is disabled. +- `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_CASA_JWKS_SIZE_LIMIT`: Default HTTP size limit (in bytes) when retrieving remote JWKS (default to `100000`). + +### 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: + +- `OFF` +- `FATAL` +- `ERROR` +- `WARN` +- `INFO` +- `DEBUG` +- `TRACE` + +The following key-value pairs are the defaults: + +```json +{ + "casa_log_target": "STDOUT", + "casa_log_level": "INFO", + "timer_log_target": "FILE", + "timer_log_level": "INFO" +} +``` + +To enable prefix on `STDOUT` logging, set the `enable_stdout_log_prefix` key. Example: + +``` +{"casa_log_target":"STDOUT","timer_log_target":"STDOUT","enable_stdout_log_prefix":true} +``` + +### 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. + +### Hybrid mapping + +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", + } + ``` + diff --git a/docker-jans-casa/requirements.txt b/docker-jans-casa/requirements.txt new file mode 100644 index 00000000000..3053a372a2b --- /dev/null +++ b/docker-jans-casa/requirements.txt @@ -0,0 +1,5 @@ +webdavclient3>=3.14.5 +libcst<0.4 +# pinned to py3-grpcio version to avoid failure on native extension build +grpcio==1.41.0 +git+https://github.com/JanssenProject/jans@36cd1798afaa3c1c05246a4a338804d20713cf9f#egg=jans-pycloudlib&subdirectory=jans-pycloudlib diff --git a/docker-jans-casa/scripts/auth_conf.py b/docker-jans-casa/scripts/auth_conf.py new file mode 100644 index 00000000000..301e0b7bb96 --- /dev/null +++ b/docker-jans-casa/scripts/auth_conf.py @@ -0,0 +1,30 @@ +import os + +from jans.pycloudlib import get_manager +from jans.pycloudlib.utils import as_boolean + +import logging.config +from settings import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("casa") + + +manager = get_manager() + + +def pull_auth_conf(): + conf_files = ( + "otp_configuration.json", + "super_gluu_creds.json", + ) + for conf_file in conf_files: + file_ = f"/etc/certs/{conf_file}" + secret_name = os.path.splitext(conf_file)[0] + logger.info(f"Pulling {file_} from secrets") + manager.secret.to_file(secret_name, file_) + + +if __name__ == "__main__": + if as_boolean(os.environ.get("CN_SHARE_AUTH_CONF", "false")): + pull_auth_conf() diff --git a/docker-jans-casa/scripts/bootstrap.py b/docker-jans-casa/scripts/bootstrap.py new file mode 100644 index 00000000000..8a913bd07be --- /dev/null +++ b/docker-jans-casa/scripts/bootstrap.py @@ -0,0 +1,303 @@ +import contextlib +import json +import logging.config +import os +from uuid import uuid4 +from string import Template +from functools import cached_property + +from ldif import LDIFWriter + +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 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 doc_id_from_dn +from jans.pycloudlib.persistence import id_from_dn +from jans.pycloudlib.persistence.utils import PersistenceMapper +from jans.pycloudlib.utils import cert_to_truststore +from jans.pycloudlib.utils import get_random_chars +from jans.pycloudlib.utils import encode_text +from jans.pycloudlib.utils import generate_base64_contents +from jans.pycloudlib.utils import as_boolean + +from settings import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("casa") + +manager = get_manager() + + +def configure_logging(): + # default config + config = { + "casa_log_target": "STDOUT", + "casa_log_level": "INFO", + "timer_log_target": "FILE", + "timer_log_level": "INFO", + "log_prefix": "", + } + + # pre-populate custom config; format is JSON string of ``dict`` + try: + custom_config = json.loads(os.environ.get("CN_CASA_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_CASA_APP_LOGGERS; fallback to defaults") + custom_config = {} + + # list of supported levels + log_levels = ("OFF", "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 = { + "casa_log_target": "LOG_FILE", + "timer_log_target": "TIMERS_FILE", + } + for key, value in config.items(): + if not key.endswith("_target"): + continue + + if value == "STDOUT": + config[key] = "Console" + else: + config[key] = file_aliases[key] + + 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:casa.log.console.prefix}%X{casa.log.console.group} - " + + with open("/app/templates/jans-casa/log4j2.xml") as f: + txt = f.read() + + logfile = "/opt/jans/jetty/jans-casa/resources/log4j2.xml" + tmpl = Template(txt) + with open(logfile, "w") as f: + f.write(tmpl.safe_substitute(config)) + + +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": + render_hybrid_properties("/etc/jans/conf/jans-hybrid.properties") + + 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() + + persistence_setup = PersistenceSetup(manager) + persistence_setup.import_ldif_files() + + +class PersistenceSetup: + def __init__(self, manager): + 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): + hostname = self.manager.config.get("hostname") + + ctx = { + "hostname": hostname, + "casa_redirect_uri": f"https://{hostname}/jans-casa", + "casa_redirect_logout_uri": f"https://{hostname}/jans-casa/bye.zul", + "casa_frontchannel_logout_uri": f"https://{hostname}/jans-casa/autologout", + } + + with open("/app/static/extension/person_authentication/Casa.py") as f: + ctx["casa_person_authentication_script"] = generate_base64_contents(f.read()) + + # Casa client + ctx["casa_client_id"] = self.manager.config.get("casa_client_id") + if not ctx["casa_client_id"]: + ctx["casa_client_id"] = f"1902.{uuid4()}" + self.manager.config.set("casa_client_id", ctx["casa_client_id"]) + + ctx["casa_client_pw"] = self.manager.secret.get("casa_client_pw") + if not ctx["casa_client_pw"]: + ctx["casa_client_pw"] = get_random_chars() + self.manager.secret.set("casa_client_pw", ctx["casa_client_pw"]) + + ctx["casa_client_encoded_pw"] = self.manager.secret.get("casa_client_encoded_pw") + if not ctx["casa_client_encoded_pw"]: + ctx["casa_client_encoded_pw"] = encode_text( + ctx["casa_client_pw"], self.manager.secret.get("encoded_salt"), + ).decode() + self.manager.secret.set("casa_client_encoded_pw", ctx["casa_client_encoded_pw"]) + + with open("/app/templates/jans-casa/casa-config.json") as f: + ctx["casa_config_base64"] = generate_base64_contents(f.read() % ctx) + + # finalized contexts + return ctx + + @cached_property + def ldif_files(self): + filenames = ["configuration.ldif", "client.ldif"] + # add casa_person_authentication_script.ldif if there's no existing casa script in persistence to avoid error + # java.lang.IllegalStateException: Duplicate key casa (attempted merging values 1 and 1) + if not self._deprecated_script_exists(): + filenames.append("person_authentication_script.ldif") + + # generate extra scopes + self.generate_scopes_ldif() + filenames.append("scopes.ldif") + + return [f"/app/templates/jans-casa/{filename}" for filename in filenames] + + def _deprecated_script_exists(self): + # deprecated Casa script DN + id_ = "inum=BABA-CACA,ou=scripts,o=jans" + + # sql and spanner + if self.persistence_type in ("sql", "spanner"): + return bool(self.client.get("jansCustomScr", doc_id_from_dn(id_))) + + # couchbase + if self.persistence_type == "couchbase": + bucket = os.environ.get("CN_COUCHBASE_BUCKET_PREFIX", "jans") + key = id_from_dn(id_) + req = self.client.exec_query( + f"SELECT META().id, {bucket}.* FROM {bucket} USE KEYS '{key}'" + ) + try: + entry = req.json()["results"][0] + return bool(entry["id"]) + except IndexError: + return False + + # ldap + return bool(self.client.get(id_)) + + def import_ldif_files(self): + for file_ in self.ldif_files: + logger.info(f"Importing {file_}") + self.client.create_from_ldif(file_, self.ctx) + + def generate_scopes_ldif(self): + # prepare required scopes (if any) + with open("/app/templates/jans-casa/scopes.json") as f: + scopes = json.loads(f.read()) + + with open("/app/templates/jans-casa/scopes.ldif", "wb") as fd: + writer = LDIFWriter(fd, cols=1000) + + for scope in scopes: + writer.unparse( + f"inum={scope['inum']},ou=scopes,o=jans", + { + "objectClass": ["top", "jansScope"], + "description": [scope["description"]], + "displayName": [scope["displayName"]], + "inum": [scope["inum"]], + "jansDefScope": [str(scope["jansDefScope"]).lower()], + "jansId": [scope["jansId"]], + "jansScopeTyp": [scope["jansScopeTyp"]], + "jansAttrs": [json.dumps({ + "spontaneousClientId": None, + "spontaneousClientScopes": [], + "showInConfigurationEndpoint": False, + })], + }, + ) + + +if __name__ == "__main__": + main() diff --git a/docker-jans-casa/scripts/entrypoint.sh b/docker-jans-casa/scripts/entrypoint.sh new file mode 100644 index 00000000000..eb6535b0547 --- /dev/null +++ b/docker-jans-casa/scripts/entrypoint.sh @@ -0,0 +1,74 @@ +#!/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_CASA_JAVA_OPTIONS}" ]; then + echo " ${CN_CASA_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 +} + +touch "$CN_CASA_ADMIN_LOCK_FILE" +get_prometheus_lib +python3 "$basedir/wait.py" +python3 "$basedir/bootstrap.py" +python3 "$basedir/mod_context.py" jans-casa +python3 "$basedir/upgrade.py" +# python3 "$basedir/jca_sync.py" & +python3 "$basedir/auth_conf.py" + +cd /opt/jans/jetty/jans-casa +# shellcheck disable=SC2046 +exec java \ + -server \ + -XX:+DisableExplicitGC \ + -XX:+UseContainerSupport \ + -Djans.base=/etc/jans \ + -Dserver.base=/opt/jans/jetty/jans-casa \ + -Dlog.base=/opt/jans/jetty/jans-casa \ + -Djava.io.tmpdir=/opt/jetty/temp \ + -Dlog4j2.configurationFile=resources/log4j2.xml \ + -Dpython.home=/opt/jython \ + -Dadmin.lock=${CN_CASA_ADMIN_LOCK_FILE} \ + -Dcom.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpSizeLimit=${CN_CASA_JWKS_SIZE_LIMIT} \ + $(get_max_ram_percentage) \ + $(get_prometheus_opt) \ + $(get_java_options) \ + -jar /opt/jetty/start.jar \ + jetty.http.host="${CN_CASA_JETTY_HOST}" \ + jetty.http.port="${CN_CASA_JETTY_PORT}" \ + jetty.deploy.scanInterval=0 \ + jetty.httpConfig.sendServerVersion=false diff --git a/docker-jans-casa/scripts/jca_sync.py b/docker-jans-casa/scripts/jca_sync.py new file mode 100644 index 00000000000..6e1d7d88e32 --- /dev/null +++ b/docker-jans-casa/scripts/jca_sync.py @@ -0,0 +1,114 @@ +import contextlib +import logging.config +import os +import shutil +import time +import filecmp + +from webdav3.client import Client +from webdav3.exceptions import RemoteResourceNotFound +from webdav3.exceptions import NoConnection + +from settings import LOGGING_CONFIG + +ROOT_DIR = "/repository/default" +SYNC_DIR = "/opt/jans/jetty/casa" +TMP_DIR = "/tmp/webdav" + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("casa") + + +def sync_from_webdav(url, username, password): + client = Client({ + "webdav_hostname": url, + "webdav_login": username, + "webdav_password": password, + "webdav_root": ROOT_DIR, + }) + client.verify = False + + try: + logger.info(f"Sync files from {url}{ROOT_DIR}{SYNC_DIR}") + # download files to temporary directory to avoid `/opt/gluu/jetty/casa` + # getting deleted + client.download(SYNC_DIR, TMP_DIR) + + # copy all downloaded files to /opt/gluu/jetty/casa + for subdir, _, files in os.walk(TMP_DIR): + for file_ in files: + src = os.path.join(subdir, file_) + dest = src.replace(TMP_DIR, SYNC_DIR) + + if not os.path.exists(os.path.dirname(dest)): + os.makedirs(os.path.dirname(dest)) + + # if destination path exists, compare the contents with the source; + # if both have same contents, do not copy the file + if os.path.exists(dest) and filecmp.cmp(src, dest, shallow=False): + continue + # logger.info(f"Copying {src} to {dest}") + shutil.copyfile(src, dest) + except (RemoteResourceNotFound, NoConnection) as exc: + logger.warning(f"Unable to sync files from {url}{ROOT_DIR}{SYNC_DIR}; reason={exc}") + + files = ( + "/etc/certs/otp_configuration.json", + "/etc/certs/super_gluu_creds.json", + ) + + for file_ in files: + try: + logger.info(f"Sync {file_} from {url}{ROOT_DIR}{file_}") + client.download_file(file_, file_) + except (RemoteResourceNotFound, NoConnection) as exc: + logger.warning(f"Unable to sync {file_} from {url}{ROOT_DIR}{file_}; reason={exc}") + + +def get_sync_interval(): + default = 5 * 60 # 5 minutes + + env_name = "CN_JACKRABBIT_SYNC_INTERVAL" + + try: + interval = int(os.environ.get(env_name, default)) + except ValueError: + interval = default + return interval + + +def get_jackrabbit_url(): + return os.environ.get("CN_JACKRABBIT_URL", "http://localhost:8080") + + +def main(): + store_type = os.environ.get("CN_DOCUMENT_STORE_TYPE", "LOCAL") + if store_type != "JCA": + logger.warning(f"Using {store_type} document store; sync is disabled ...") + return + + url = get_jackrabbit_url() + + username = os.environ.get("CN_JACKRABBIT_ADMIN_ID", "admin") + password = "" + + password_file = os.environ.get( + "CN_JACKRABBIT_ADMIN_PASSWORD_FILE", + "/etc/jans/conf/jackrabbit_admin_password", + ) + with contextlib.suppress(FileNotFoundError): + with open(password_file) as f: + password = f.read().strip() + password = password or username + + sync_interval = get_sync_interval() + try: + while True: + sync_from_webdav(url, username, password) + time.sleep(sync_interval) + except KeyboardInterrupt: + logger.warning("Canceled by user; exiting ...") + + +if __name__ == "__main__": + main() diff --git a/docker-jans-casa/scripts/mod_context.py b/docker-jans-casa/scripts/mod_context.py new file mode 100644 index 00000000000..4ac5b010603 --- /dev/null +++ b/docker-jans-casa/scripts/mod_context.py @@ -0,0 +1,113 @@ +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("casa") + + +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] + + reg_libs = [ + lib.path for lib in libs + if lib.meta["name"] not in archived_lib_names + ] + return reg_libs + + +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-casa/scripts/settings.py b/docker-jans-casa/scripts/settings.py new file mode 100644 index 00000000000..ee7d8ec2967 --- /dev/null +++ b/docker-jans-casa/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, + }, + "casa": { + "handlers": ["console"], + "level": "INFO", + "propagate": False, + }, + }, +} diff --git a/docker-jans-casa/scripts/upgrade.py b/docker-jans-casa/scripts/upgrade.py new file mode 100644 index 00000000000..789388ad8e8 --- /dev/null +++ b/docker-jans-casa/scripts/upgrade.py @@ -0,0 +1,314 @@ +import json +import logging.config +import os +from collections import namedtuple +from contextlib import suppress +from urllib.parse import urlparse +from urllib.parse import urlunparse + +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("casa") + +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}'" + ) + 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.update_client_scopes() + self.update_client_uris() + self.update_conf_app() + + def update_client_scopes(self): + kwargs = {} + client_id = self.manager.config.get("casa_client_id") + id_ = f"inum={client_id},ou=clients,o=jans" + + if self.backend.type in ("sql", "spanner"): + kwargs = {"table_name": "jansClnt"} + id_ = doc_id_from_dn(id_) + elif self.backend.type == "couchbase": + kwargs = {"bucket": os.environ.get("CN_COUCHBASE_BUCKET_PREFIX", "jans")} + id_ = id_from_dn(id_) + + entry = self.backend.get_entry(id_, **kwargs) + + if not entry: + return + + if self.backend.type == "sql" and self.backend.client.dialect == "mysql": + client_scopes = entry.attrs["jansScope"]["v"] + else: + client_scopes = entry.attrs["jansScope"] + + if not isinstance(client_scopes, list): + client_scopes = [client_scopes] + + # all potential new scopes for client + with open("/app/templates/jans-casa/scopes.json") as f: + new_client_scopes = [ + f"inum={scope['inum']},ou=scopes,o=jans" + for scope in json.loads(f.read()) + ] + + # find missing scopes from the client + diff = list(set(new_client_scopes).difference(client_scopes)) + + if diff: + if self.backend.type == "sql" and self.backend.client.dialect == "mysql": + entry.attrs["jansScope"]["v"] = client_scopes + diff + else: + entry.attrs["jansScope"] = client_scopes + diff + self.backend.modify_entry(entry.id, entry.attrs, **kwargs) + + def update_conf_app(self): + kwargs = {} + id_ = "ou=casa,ou=configuration,o=jans" + + if self.backend.type in ("sql", "spanner"): + kwargs = {"table_name": "jansAppConf"} + id_ = doc_id_from_dn(id_) + elif self.backend.type == "couchbase": + kwargs = {"bucket": os.environ.get("CN_COUCHBASE_BUCKET_PREFIX", "jans")} + id_ = id_from_dn(id_) + + entry = self.backend.get_entry(id_, **kwargs) + + if not entry: + return + + should_update = False + + if self.backend.type != "couchbase": + with suppress(json.decoder.JSONDecodeError): + entry.attrs["jansConfApp"] = json.loads(entry.attrs["jansConfApp"]) + + for key in ["authz_redirect_uri", "post_logout_uri", "frontchannel_logout_uri"]: + parsed_url = urlparse(entry.attrs["jansConfApp"]["oidc_config"][key]) + + url_paths = [ + pth for pth in parsed_url.path.rsplit("/") + if pth + ] + + if url_paths[0] != "jans-casa": + url_paths[0] = "jans-casa" + parsed_url = parsed_url._replace(path="/".join(url_paths)) + entry.attrs["jansConfApp"]["oidc_config"][key] = urlunparse(parsed_url) + should_update = True + + if should_update: + if self.backend.type != "couchbase": + entry.attrs["jansConfApp"] = json.dumps(entry.attrs["jansConfApp"]) + self.backend.modify_entry(entry.id, entry.attrs, **kwargs) + + def update_client_uris(self): + kwargs = {} + client_id = self.manager.config.get("casa_client_id") + id_ = f"inum={client_id},ou=clients,o=jans" + + if self.backend.type in ("sql", "spanner"): + kwargs = {"table_name": "jansClnt"} + id_ = doc_id_from_dn(id_) + elif self.backend.type == "couchbase": + kwargs = {"bucket": os.environ.get("CN_COUCHBASE_BUCKET_PREFIX", "jans")} + id_ = id_from_dn(id_) + + entry = self.backend.get_entry(id_, **kwargs) + + if not entry: + return + + should_update = False + hostname = self.manager.config.get("hostname") + uri_mapping = { + "jansLogoutURI": f"https://{hostname}/jans-casa/autologout", + "jansPostLogoutRedirectURI": f"https://{hostname}/jans-casa/bye.zul", + "jansRedirectURI": f"https://{hostname}/jans-casa", + } + + for key, uri in uri_mapping.items(): + if self.backend.type == "sql" and self.backend.client.dialect == "mysql": + client_uris = entry.attrs[key]["v"] + else: + client_uris = entry.attrs[key] + + if not isinstance(client_uris, list): + client_uris = [client_uris] + + if uri not in client_uris: + client_uris.append(uri) + + if self.backend.type == "sql" and self.backend.client.dialect == "mysql": + entry.attrs[key]["v"] = client_uris + else: + entry.attrs[key] = client_uris + should_update = True + + if should_update: + self.backend.modify_entry(entry.id, entry.attrs, **kwargs) + + +def main(): + manager = get_manager() + upgrade = Upgrade(manager) + upgrade.invoke() + + +if __name__ == "__main__": + main() diff --git a/docker-jans-casa/scripts/wait.py b/docker-jans-casa/scripts/wait.py new file mode 100644 index 00000000000..1e195e298e3 --- /dev/null +++ b/docker-jans-casa/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-casa/static/jetty-env.xml b/docker-jans-casa/static/jetty-env.xml new file mode 100644 index 00000000000..228cae0b737 --- /dev/null +++ b/docker-jans-casa/static/jetty-env.xml @@ -0,0 +1,22 @@ + + + + + + + + + + BeanManager + + + + javax.enterprise.inject.spi.BeanManager + org.jboss.weld.resources.ManagerObjectFactory + + + + + + diff --git a/docker-jans-casa/static/prometheus-config.yaml b/docker-jans-casa/static/prometheus-config.yaml new file mode 100644 index 00000000000..e16bce27c2a --- /dev/null +++ b/docker-jans-casa/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" \ No newline at end of file diff --git a/docker-jans-casa/templates/jans-casa/jans-casa.xml b/docker-jans-casa/templates/jans-casa/jans-casa.xml new file mode 100644 index 00000000000..f27e32580b2 --- /dev/null +++ b/docker-jans-casa/templates/jans-casa/jans-casa.xml @@ -0,0 +1,11 @@ + + + + + /jans-casa + + /jans-casa.war + + true + %(extra_classpath)s + diff --git a/docker-jans-casa/templates/jans-casa/jans-casa_web_resources.xml b/docker-jans-casa/templates/jans-casa/jans-casa_web_resources.xml new file mode 100644 index 00000000000..fe40f2a0d7a --- /dev/null +++ b/docker-jans-casa/templates/jans-casa/jans-casa_web_resources.xml @@ -0,0 +1,11 @@ + + + + /jans-casa/custom + + + /opt/jans/jetty/jans-casa/static + false + + + diff --git a/docker-jans-casa/templates/jans-casa/log4j2.xml b/docker-jans-casa/templates/jans-casa/log4j2.xml new file mode 100644 index 00000000000..d27fad212a3 --- /dev/null +++ b/docker-jans-casa/templates/jans-casa/log4j2.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + -timer + + + + + + + + + + + + diff --git a/docker-jans-casa/templates/jans-couchbase.properties b/docker-jans-casa/templates/jans-couchbase.properties new file mode 100644 index 00000000000..d5caad62951 --- /dev/null +++ b/docker-jans-casa/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-casa/templates/jans-ldap.properties b/docker-jans-casa/templates/jans-ldap.properties new file mode 100644 index 00000000000..a7ec401fe75 --- /dev/null +++ b/docker-jans-casa/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-casa/templates/jans-mysql.properties b/docker-jans-casa/templates/jans-mysql.properties new file mode 100644 index 00000000000..ae85a0595f3 --- /dev/null +++ b/docker-jans-casa/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-casa/templates/jans-pgsql.properties b/docker-jans-casa/templates/jans-pgsql.properties new file mode 100644 index 00000000000..577cabb7149 --- /dev/null +++ b/docker-jans-casa/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-casa/templates/jans-spanner.properties b/docker-jans-casa/templates/jans-spanner.properties new file mode 100644 index 00000000000..73db25b7d54 --- /dev/null +++ b/docker-jans-casa/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-casa/templates/jans.properties b/docker-jans-casa/templates/jans.properties new file mode 100644 index 00000000000..e9d142a485a --- /dev/null +++ b/docker-jans-casa/templates/jans.properties @@ -0,0 +1,9 @@ +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 + +certsDir=/etc/certs +confDir= +pythonModulesDir=/opt/jans/python/libs:/opt/jython/Lib/site-packages diff --git a/docker-jans-casa/templates/salt b/docker-jans-casa/templates/salt new file mode 100644 index 00000000000..ee6c2c330e3 --- /dev/null +++ b/docker-jans-casa/templates/salt @@ -0,0 +1 @@ +encodeSalt = %(encode_salt)s diff --git a/docker-jans-casa/version.txt b/docker-jans-casa/version.txt new file mode 100644 index 00000000000..82b4de0f2e7 --- /dev/null +++ b/docker-jans-casa/version.txt @@ -0,0 +1 @@ +1.0.18-1