WIP: idhub-docker #5

Draft
pedro wants to merge 3 commits from idhub-docker into release
8 changed files with 279 additions and 2 deletions

34
.env.example Normal file
View file

@ -0,0 +1,34 @@
# IDHUB
####
IDHUB_DOMAIN=localhost
IDHUB_ALLOWED_HOSTS=${IDHUB_DOMAIN},${IDHUB_DOMAIN}:9001,127.0.0.1,127.0.0.1:9001
IDHUB_TIME_ZONE='Europe/Madrid'
#IDHUB_SECRET_KEY='uncomment-it-and-fill-this'
# enable dev flags when DEVELOPMENT deployment
# adapt to your domain in a production/reverse proxy env
IDHUB_CSRF_TRUSTED_ORIGINS='https://idhub.example.org'
# fill this section with your email credentials
IDHUB_DEFAULT_FROM_EMAIL="user@example.org"
IDHUB_EMAIL_HOST="smtp.example.org"
IDHUB_EMAIL_HOST_USER="smtp_user"
IDHUB_EMAIL_HOST_PASSWORD="smtp_passwd"
IDHUB_EMAIL_PORT=25
IDHUB_EMAIL_USE_TLS=True
IDHUB_EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend"
# replace with production data
# this is used when IDHUB_DEPLOYMENT is not equal to DEVELOPMENT
IDHUB_ADMIN_USER='admin'
IDHUB_ADMIN_PASSWD='admin'
IDHUB_ADMIN_EMAIL='admin@example.org'
# this option needs to be set to 'n' to be able to make work idhub in docker
# by default it is set to 'y' to facilitate idhub dev when outside docker
IDHUB_SYNC_ORG_DEV='n'
# TODO that is only for testing
IDHUB_ENABLE_EMAIL=false
IDHUB_ENABLE_2FACTOR_AUTH=false
IDHUB_ENABLE_DOMAIN_CHECKER=false

33
docker-compose.yml Normal file
View file

@ -0,0 +1,33 @@
services:
idhub:
init: true
build:
dockerfile: docker/idhub.Dockerfile
environment:
- DOMAIN=${IDHUB_DOMAIN:-localhost}
- ALLOWED_HOSTS=${IDHUB_ALLOWED_HOSTS:-$IDHUB_DOMAIN}
- DEBUG=true
- INITIAL_ADMIN_EMAIL=${IDHUB_ADMIN_EMAIL}
- INITIAL_ADMIN_PASSWORD=${IDHUB_ADMIN_PASSWD}
- CREATE_TEST_USERS=true
- ENABLE_EMAIL=${IDHUB_ENABLE_EMAIL:-true}
- ENABLE_2FACTOR_AUTH=${IDHUB_ENABLE_2FACTOR_AUTH:-true}
- ENABLE_DOMAIN_CHECKER=${IDHUB_ENABLE_DOMAIN_CHECKER:-true}
- SECRET_KEY=${IDHUB_SECRET_KEY:-publicsecretisnotsecureVtmKBfxpVV47PpBCF2Nzz2H6qnbd}
- STATIC_ROOT=${IDHUB_STATIC_ROOT:-/static/}
- MEDIA_ROOT=${IDHUB_MEDIA_ROOT:-/media/}
- PORT=${IDHUB_PORT:-9001}
- DEFAULT_FROM_EMAIL=${IDHUB_DEFAULT_FROM_EMAIL}
- EMAIL_HOST=${IDHUB_EMAIL_HOST}
- EMAIL_HOST_USER=${IDHUB_EMAIL_HOST_USER}
- EMAIL_HOST_PASSWORD=${IDHUB_EMAIL_HOST_PASSWORD}
- EMAIL_PORT=${IDHUB_EMAIL_PORT}
- EMAIL_USE_TLS=${IDHUB_EMAIL_USE_TLS}
- EMAIL_BACKEND=${IDHUB_EMAIL_BACKEND}
- SUPPORTED_CREDENTIALS=['CourseCredential', 'EOperatorClaim', 'FederationMembership', 'FinancialVulnerabilityCredential', 'MembershipCard']
- SYNC_ORG_DEV=${IDHUB_SYNC_ORG_DEV}
ports:
- 9001:9001
volumes:
- .:/opt/idhub

35
docker-reset.sh Executable file
View file

@ -0,0 +1,35 @@
#!/bin/sh
# SPDX-License-Identifier: AGPL-3.0-or-later
set -e
set -u
# DEBUG
set -x
main() {
cd "$(dirname "${0}")"
rm -fv ./db.sqlite3
if [ ! -f .env ]; then
cp -v .env.example .env
echo "WARNING: .env was not there, .env.example was copied, this only happens once"
fi
docker compose down -v
docker compose build
docker compose up ${detach_arg:-}
# TODO docker registry
#project=dkr-dsg.ac.upc.edu/trustchain-oc1-orchestral
#idhub_image=${project}/idhub:${idhub_tag}
#idhub_branch=$(git -C IdHub branch --show-current)
# docker build -f docker/idhub.Dockerfile -t ${idhub_image} -t ${project}/idhub:${idhub_branch}__latest .
#docker tag hello-world:latest farga.pangea.org/pedro/test/hello-world
#docker push farga.pangea.org/pedro/test/hello-world:latest
}
main "${@}"
# written in emacs
# -*- mode: shell-scrip; -*-t

34
docker/idhub.Dockerfile Normal file
View file

@ -0,0 +1,34 @@
FROM python:3.11.7-slim-bookworm
# last line is dependencies for weasyprint (for generating pdfs in lafede pilot) https://doc.courtbouillon.org/weasyprint/stable/first_steps.html#debian-11
RUN apt update && \
apt-get install -y \
git \
sqlite3 \
jq \
libpango-1.0-0 libpangoft2-1.0-0 \
&& pip install cffi brotli \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /opt/idhub
# reduce size (python specifics) -> src https://stackoverflow.com/questions/74616667/removing-pip-cache-after-installing-dependencies-in-docker-image
ENV PYTHONDONTWRITEBYTECODE=1
# here document in dockerfile src https://stackoverflow.com/questions/40359282/launch-a-cat-command-unix-into-dockerfile
RUN cat > /etc/pip.conf <<END
[install]
compile = no
[global]
no-cache-dir = True
END
RUN pip install --upgrade pip
# not needed anymore?
#COPY ssikit_trustchain/didkit-0.3.2-cp311-cp311-manylinux_2_34_x86_64.whl /opt/idhub
COPY ./requirements.txt /opt/idhub
RUN pip install -r requirements.txt
COPY docker/idhub.entrypoint.sh /
ENTRYPOINT sh /idhub.entrypoint.sh

139
docker/idhub.entrypoint.sh Executable file
View file

@ -0,0 +1,139 @@
#!/bin/sh
set -e
set -u
set -x
usage() {
cat <<END
ERROR: you need to map your idhub git repo volume to docker, suggested volume mapping is:
volumes:
- ./IdHub:/opt/idhub
END
exit 1
}
inject_env_vars() {
# related https://www.kenmuse.com/blog/avoiding-dubious-ownership-in-dev-containers/
git config --global --add safe.directory "${idhub_dir}"
export COMMIT="commit: $(git log --pretty=format:'%h' -n 1)"
cat > status_data <<END
DOMAIN=${DOMAIN}
END
}
deployment_strategy() {
# detect if existing deployment (TODO only works with sqlite)
if [ -f "${idhub_dir}/db.sqlite3" ]; then
echo "INFO: detected EXISTING deployment"
./manage.py migrate
# warn admin that it should re-enter password to keep the service working
./manage.py send_mail_admins
else
# this file helps all docker containers to guess number of hosts involved
# right now is only needed by new deployment for oidc
if [ -d "/sharedsecret" ]; then
touch /sharedsecret/${DOMAIN}
fi
# move the migrate thing in docker entrypoint
# inspired by https://medium.com/analytics-vidhya/django-with-docker-and-docker-compose-python-part-2-8415976470cc
echo "INFO detected NEW deployment"
./manage.py migrate
printf "This is DEVELOPMENT/PILOTS_EARLY DEPLOYMENT: including demo hardcoded data\n creating initial Datas\n" >&2
./manage.py initial_datas
if [ "${OIDC_ORGS:-}" ]; then
config_oidc4vp
else
echo "Note: skipping oidc4vp config"
fi
fi
}
_set() {
key="${1}"
value="${2}"
domain="${3}"
sqlite3 db.sqlite3 "update oidc4vp_organization set ${key}='${value}' where domain='${domain}';"
}
_get() {
sqlite3 -json db.sqlite3 "select * from oidc4vp_organization;"
}
_lines () {
local myfile="${1}"
cat "${myfile}" | wc -l
}
config_oidc4vp() {
# populate your config
data="$(_get)"
echo "${data}" | jq --arg domain "${DOMAIN}" '{ ($domain): .}' > /sharedsecret/${DOMAIN}
while true; do
echo wait the other idhubs to write, this is the only oportunity to sync with other idhubs in the docker compose
## break when no empty files left
if ! wc -l /sharedsecret/* | awk '{print $1;}' | grep -qE '^0$'; then
break
fi
sleep 1
done
# get other configs
for host in /sharedsecret/*; do
# we are flexible on querying for DOMAIN: the first one based on regex
target_domain="$(cat "${host}" | jq -r 'keys[0]')"
if [ "${target_domain}" != "${DOMAIN}" ]; then
filtered_data="$(cat "${host}" | jq --arg domain "${DOMAIN}" 'first(.[][] | select(.domain | test ($domain)))')"
client_id="$(echo "${filtered_data}" | jq -r '.client_id')"
client_secret="$(echo "${filtered_data}" | jq -r '.client_secret')"
_set my_client_id ${client_id} ${target_domain}
_set my_client_secret ${client_secret} ${target_domain}
fi
done
}
runserver() {
PORT="${PORT:-8000}"
if [ ! "${DEBUG:-}" = "true" ]; then
./manage.py collectstatic
if [ "${EXPERIMENTAL:-}" = "true" ]; then
# reloading on source code changing is a debugging future, maybe better then use debug
# src https://stackoverflow.com/questions/12773763/gunicorn-autoreload-on-source-change/24893069#24893069
# gunicorn with 1 worker, with more than 1 worker this is not expected to work
gunicorn --access-logfile - --error-logfile - -b :${PORT} trustchain_idhub.wsgi:application
else
./manage.py runserver 0.0.0.0:${PORT}
fi
else
./manage.py runserver 0.0.0.0:${PORT}
fi
}
check_app_is_there() {
if [ ! -f "./manage.py" ]; then
usage
fi
}
main() {
idhub_dir='/opt/idhub'
cd "${idhub_dir}"
check_app_is_there
deployment_strategy
inject_env_vars
runserver
}
main "${@}"

View file

@ -33,7 +33,7 @@ class UserView(LoginRequiredMixin):
] ]
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
if not settings.DEVELOPMENT: if settings.ENABLE_DOMAIN_CHECKER:
err_txt = "User domain is {} which does not match server domain {}".format( err_txt = "User domain is {} which does not match server domain {}".format(
request.get_host(), settings.DOMAIN request.get_host(), settings.DOMAIN
) )
@ -56,7 +56,7 @@ class UserView(LoginRequiredMixin):
return url or response return url or response
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
if not settings.DEVELOPMENT: if settings.ENABLE_DOMAIN_CHECKER:
err_txt = "User domain is {} which does not match server domain {}".format( err_txt = "User domain is {} which does not match server domain {}".format(
request.get_host(), settings.DOMAIN request.get_host(), settings.DOMAIN
) )

View file

@ -6,6 +6,7 @@ black==23.9.1
python-decouple==3.8 python-decouple==3.8
jsonschema[format]==4.19.1 jsonschema[format]==4.19.1
pandas==2.1.1 pandas==2.1.1
numpy>=1.21,<2.0
xlrd==2.0.1 xlrd==2.0.1
odfpy==1.4.1 odfpy==1.4.1
requests==2.31.0 requests==2.31.0

View file

@ -241,5 +241,6 @@ OIDC_ORGS = config('OIDC_ORGS', '')
ENABLE_EMAIL = config('ENABLE_EMAIL', default=True, cast=bool) ENABLE_EMAIL = config('ENABLE_EMAIL', default=True, cast=bool)
CREATE_TEST_USERS = config('CREATE_TEST_USERS', default=False, cast=bool) CREATE_TEST_USERS = config('CREATE_TEST_USERS', default=False, cast=bool)
ENABLE_2FACTOR_AUTH = config('ENABLE_2FACTOR_AUTH', default=True, cast=bool) ENABLE_2FACTOR_AUTH = config('ENABLE_2FACTOR_AUTH', default=True, cast=bool)
ENABLE_DOMAIN_CHECKER = config('ENABLE_DOMAIN_CHECKER', default=True, cast=bool)
COMMIT = config('COMMIT', default='') COMMIT = config('COMMIT', default='')