diff --git a/Dockerfile b/Dockerfile index 8277253b6f3..a219bfd959b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -165,7 +165,8 @@ RUN apk update && apk upgrade && \ tzdata \ libedit \ libldap \ - libcap && \ + libcap \ + su-exec && \ rm -rf /var/cache/apk/* # Copy in the Python packages @@ -197,7 +198,7 @@ RUN /venv/bin/python3 -m pip install --no-cache-dir gunicorn==23.0.0 && \ useradd -r -u 5050 -g root -s /sbin/nologin pgadmin && \ mkdir -p /run/pgadmin /var/lib/pgadmin && \ chown pgadmin:root /run/pgadmin /var/lib/pgadmin && \ - chmod g=u /var/lib/pgadmin && \ + chmod g=u /run/pgadmin /var/lib/pgadmin && \ touch /pgadmin4/config_distro.py && \ chown pgadmin:root /pgadmin4/config_distro.py && \ chmod g=u /pgadmin4/config_distro.py && \ diff --git a/pkg/docker/entrypoint.sh b/pkg/docker/entrypoint.sh index 914a8883307..6a83bec4494 100755 --- a/pkg/docker/entrypoint.sh +++ b/pkg/docker/entrypoint.sh @@ -1,12 +1,78 @@ #!/usr/bin/env bash -# Fixup the passwd file, in case we're on OpenShift -if ! whoami > /dev/null 2>&1; then - if [ "$(id -u)" -ne 5050 ]; then - if [ -w /etc/passwd ]; then - echo "${USER_NAME:-pgadminr}:x:$(id -u):0:${USER_NAME:-pgadminr} user:${HOME}:/sbin/nologin" >> /etc/passwd +######################################################################## +# PUID/PGID support +# +# When the container runs as root (e.g. --user root), the pgadmin user +# is reassigned to the requested UID/GID and all initialization + +# gunicorn run via su-exec as that user. +# +# When the container runs as non-root (default USER 5050, or OpenShift +# random UID), PUID/PGID are ignored and everything runs as the +# current user. +######################################################################## + +PUID=${PUID:-5050} +PGID=${PGID:-0} + +# Validate PUID/PGID are numeric and in acceptable range +if ! echo "$PUID" | grep -qE '^[0-9]+$'; then + echo "ERROR: PUID must be a numeric value, got '$PUID'" + exit 1 +fi +if ! echo "$PGID" | grep -qE '^[0-9]+$'; then + echo "ERROR: PGID must be a numeric value, got '$PGID'" + exit 1 +fi +if [ "$PUID" -eq 0 ]; then + echo "ERROR: PUID=0 (root) is not allowed. Use a non-root UID." + exit 1 +fi + +if [ "$(id -u)" = "0" ]; then + # Ensure a group with the target GID exists + if ! getent group "$PGID" > /dev/null 2>&1; then + if ! addgroup -g "$PGID" pggroup; then + echo "ERROR: Failed to create group with GID=$PGID" + exit 1 + fi + fi + + # Reassign the pgadmin user to the desired UID/GID + if ! usermod -o -u "$PUID" -g "$PGID" pgadmin; then + echo "ERROR: Failed to set pgadmin user to UID=$PUID GID=$PGID" + exit 1 + fi + + # Fix ownership of runtime directories BEFORE any initialization + for dir in /run/pgadmin /var/lib/pgadmin; do + if [ -d "$dir" ]; then + chown -R "$PUID:$PGID" "$dir" + fi + done + + # Fix ownership of individual files (no -R needed) + if [ -e /pgadmin4/config_distro.py ]; then + chown "$PUID:$PGID" /pgadmin4/config_distro.py + fi + + if [ -d /certs ]; then + chown -R "$PUID:$PGID" /certs + fi + + SU_EXEC="su-exec $PUID:$PGID" + echo "pgAdmin will run as UID=$PUID, GID=$PGID" +else + SU_EXEC="" + + # Fixup the passwd file, in case we're on OpenShift + if ! whoami > /dev/null 2>&1; then + if [ "$(id -u)" -ne 5050 ]; then + if [ -w /etc/passwd ]; then + echo "${USER_NAME:-pgadminr}:x:$(id -u):0:${USER_NAME:-pgadminr} user:${HOME}:/sbin/nologin" >> /etc/passwd + fi + fi fi - fi fi # usage: file_env VAR [DEFAULT] ie: file_env 'XYZ_DB_PASSWORD' 'example' @@ -74,12 +140,17 @@ EOF esac echo "${var#PGADMIN_CONFIG_} = $val" >> "${CONFIG_DISTRO_FILE_PATH}" done + + # If running as root with custom config distro path, fix ownership + if [ "$(id -u)" = "0" ] && [ "${CONFIG_DISTRO_FILE_PATH}" != "/pgadmin4/config_distro.py" ]; then + chown "$PUID:$PGID" "${CONFIG_DISTRO_FILE_PATH}" + fi fi # Check whether the external configuration database exists if it is being used. external_config_db_exists="False" if [ -n "${PGADMIN_CONFIG_CONFIG_DATABASE_URI}" ]; then - external_config_db_exists=$(cd /pgadmin4/pgadmin/utils && /venv/bin/python3 -c "from check_external_config_db import check_external_config_db; val = check_external_config_db("${PGADMIN_CONFIG_CONFIG_DATABASE_URI}"); print(val)") + external_config_db_exists=$(cd /pgadmin4/pgadmin/utils && $SU_EXEC /venv/bin/python3 -c "from check_external_config_db import check_external_config_db; val = check_external_config_db(\"${PGADMIN_CONFIG_CONFIG_DATABASE_URI}\"); print(val)") fi # DRY of the code to load the PGADMIN_SERVER_JSON_FILE @@ -96,9 +167,9 @@ function load_server_json_file() { # When running in Desktop mode, no user is created # so we have to import servers anonymously if [ "${PGADMIN_CONFIG_SERVER_MODE}" = "False" ]; then - /venv/bin/python3 /pgadmin4/setup.py load-servers "${PGADMIN_SERVER_JSON_FILE}" ${EXTRA_ARGS} + $SU_EXEC /venv/bin/python3 /pgadmin4/setup.py load-servers "${PGADMIN_SERVER_JSON_FILE}" ${EXTRA_ARGS} else - /venv/bin/python3 /pgadmin4/setup.py load-servers "${PGADMIN_SERVER_JSON_FILE}" --user "${PGADMIN_DEFAULT_EMAIL}" ${EXTRA_ARGS} + $SU_EXEC /venv/bin/python3 /pgadmin4/setup.py load-servers "${PGADMIN_SERVER_JSON_FILE}" --user "${PGADMIN_DEFAULT_EMAIL}" ${EXTRA_ARGS} fi fi } @@ -124,7 +195,7 @@ if [ ! -f /var/lib/pgadmin/pgadmin4.db ] && [ "${external_config_db_exists}" = " fi email_config="{'CHECK_EMAIL_DELIVERABILITY': ${CHECK_EMAIL_DELIVERABILITY}, 'ALLOW_SPECIAL_EMAIL_DOMAINS': ${ALLOW_SPECIAL_EMAIL_DOMAINS}, 'GLOBALLY_DELIVERABLE': ${GLOBALLY_DELIVERABLE}}" echo "email config is ${email_config}" - is_valid_email=$(cd /pgadmin4/pgadmin/utils && /venv/bin/python3 -c "from validation_utils import validate_email; val = validate_email('${PGADMIN_DEFAULT_EMAIL}', ${email_config}); print(val)") + is_valid_email=$(cd /pgadmin4/pgadmin/utils && $SU_EXEC /venv/bin/python3 -c "from validation_utils import validate_email; val = validate_email('${PGADMIN_DEFAULT_EMAIL}', ${email_config}); print(val)") if echo "${is_valid_email}" | grep "False" > /dev/null; then echo "'${PGADMIN_DEFAULT_EMAIL}' does not appear to be a valid email address. Please reset the PGADMIN_DEFAULT_EMAIL environment variable and try again." echo "Validation output: ${is_valid_email}" @@ -140,7 +211,7 @@ if [ ! -f /var/lib/pgadmin/pgadmin4.db ] && [ "${external_config_db_exists}" = " # Initialize DB before starting Gunicorn # Importing pgadmin4 (from this script) is enough - /venv/bin/python3 run_pgadmin.py + $SU_EXEC /venv/bin/python3 run_pgadmin.py export PGADMIN_PREFERENCES_JSON_FILE="${PGADMIN_PREFERENCES_JSON_FILE:-/pgadmin4/preferences.json}" @@ -150,22 +221,30 @@ if [ ! -f /var/lib/pgadmin/pgadmin4.db ] && [ "${external_config_db_exists}" = " # Pre-load any required preferences if [ -f "${PGADMIN_PREFERENCES_JSON_FILE}" ]; then if [ "${PGADMIN_CONFIG_SERVER_MODE}" = "False" ]; then - DESKTOP_USER=$(cd /pgadmin4 && /venv/bin/python3 -c 'import config; print(config.DESKTOP_USER)') - /venv/bin/python3 /pgadmin4/setup.py set-prefs "${DESKTOP_USER}" --input-file "${PGADMIN_PREFERENCES_JSON_FILE}" + DESKTOP_USER=$(cd /pgadmin4 && $SU_EXEC /venv/bin/python3 -c 'import config; print(config.DESKTOP_USER)') + $SU_EXEC /venv/bin/python3 /pgadmin4/setup.py set-prefs "${DESKTOP_USER}" --input-file "${PGADMIN_PREFERENCES_JSON_FILE}" else - /venv/bin/python3 /pgadmin4/setup.py set-prefs "${PGADMIN_DEFAULT_EMAIL}" --input-file "${PGADMIN_PREFERENCES_JSON_FILE}" + $SU_EXEC /venv/bin/python3 /pgadmin4/setup.py set-prefs "${PGADMIN_DEFAULT_EMAIL}" --input-file "${PGADMIN_PREFERENCES_JSON_FILE}" fi fi # Copy the pgpass file passed using secrets - if [ -f "${PGPASS_FILE}" ]; then + if [ -n "${PGPASS_FILE}" ] && [ -f "${PGPASS_FILE}" ]; then if [ "${PGADMIN_CONFIG_SERVER_MODE}" = "False" ]; then - cp ${PGPASS_FILE} /var/lib/pgadmin/.pgpass + cp "${PGPASS_FILE}" /var/lib/pgadmin/.pgpass chmod 600 /var/lib/pgadmin/.pgpass + # Fix ownership when running as root + if [ "$(id -u)" = "0" ]; then + chown "$PUID:$PGID" /var/lib/pgadmin/.pgpass + fi else PGADMIN_USER_CONFIG_DIR=$(echo "${PGADMIN_DEFAULT_EMAIL}" | sed 's/@/_/g') - mkdir -p /var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR} - cp ${PGPASS_FILE} /var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}/.pgpass - chmod 600 /var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}/.pgpass + mkdir -p "/var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}" + cp "${PGPASS_FILE}" "/var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}/.pgpass" + chmod 600 "/var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}/.pgpass" + # Fix ownership when running as root + if [ "$(id -u)" = "0" ]; then + chown -R "$PUID:$PGID" "/var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}" + fi fi fi # If already initialised and PGADMIN_REPLACE_SERVERS_ON_STARTUP is set to true, then load the server json file. @@ -180,7 +259,7 @@ fi # Get the session timeout from the pgAdmin config. We'll use this (in seconds) # to define the Gunicorn worker timeout -TIMEOUT=$(cd /pgadmin4 && /venv/bin/python3 -c 'import config; print(config.SESSION_EXPIRATION_TIME * 60 * 60 * 24)') +TIMEOUT=$(cd /pgadmin4 && $SU_EXEC /venv/bin/python3 -c 'import config; print(config.SESSION_EXPIRATION_TIME * 60 * 60 * 24)') # NOTE: currently pgadmin can run only with 1 worker due to sessions implementation # Using --threads to have multi-threaded single-process worker @@ -196,7 +275,7 @@ else fi if [ -n "${PGADMIN_ENABLE_TLS}" ]; then - exec /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --timeout "${TIMEOUT}" --bind "${BIND_ADDRESS}" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" --keyfile /certs/server.key --certfile /certs/server.cert -c gunicorn_config.py run_pgadmin:app + exec $SU_EXEC /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --timeout "${TIMEOUT}" --bind "${BIND_ADDRESS}" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" --keyfile /certs/server.key --certfile /certs/server.cert -c gunicorn_config.py run_pgadmin:app else - exec /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --limit-request-fields "${GUNICORN_LIMIT_REQUEST_FIELDS:-100}" --limit-request-field_size "${GUNICORN_LIMIT_REQUEST_FIELD_SIZE:-8190}" --timeout "${TIMEOUT}" --bind "${BIND_ADDRESS}" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" -c gunicorn_config.py run_pgadmin:app + exec $SU_EXEC /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --limit-request-fields "${GUNICORN_LIMIT_REQUEST_FIELDS:-100}" --limit-request-field_size "${GUNICORN_LIMIT_REQUEST_FIELD_SIZE:-8190}" --timeout "${TIMEOUT}" --bind "${BIND_ADDRESS}" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" -c gunicorn_config.py run_pgadmin:app fi