Skip to content

Commit 1cfd88f

Browse files
authored
Merge branch 'master' into passfile-fix
2 parents ba2ce6c + d112dc3 commit 1cfd88f

454 files changed

Lines changed: 19225 additions & 14306 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/run-python-tests-epas.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,17 @@ jobs:
3333
matrix:
3434
os: [ubuntu-22.04, windows-latest]
3535
pgver: [14, 15, 16, 17, 18]
36+
postgisver: [34]
37+
exclude:
38+
- pgver: 18
39+
postgisver: 34
40+
include:
41+
- os: ubuntu-22.04
42+
pgver: 18
43+
postgisver: 36
44+
- os: windows-latest
45+
pgver: 18
46+
postgisver: 36
3647

3748
runs-on: ${{ matrix.os }}
3849

@@ -53,7 +64,7 @@ jobs:
5364
if: ${{ matrix.os == 'ubuntu-22.04' }}
5465
run: |
5566
sudo apt update
56-
sudo apt install -y libpq-dev libffi-dev libssl-dev libkrb5-dev zlib1g-dev edb-as${{ matrix.pgver }}-server edb-as${{ matrix.pgver }}-server-pldebugger edb-as${{ matrix.pgver }}-postgis34
67+
sudo apt install -y libpq-dev libffi-dev libssl-dev libkrb5-dev zlib1g-dev edb-as${{ matrix.pgver }}-server edb-as${{ matrix.pgver }}-server-pldebugger edb-as${{ matrix.pgver }}-postgis${{ matrix.postgisver }}
5768
5869
- name: Install pgagent on Linux
5970
if: ${{ matrix.os == 'ubuntu-22.04' && matrix.pgver <= 16 }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,6 @@ auditpy.txt
5959
web/coverage/
6060
web/.coverage
6161
web/regression/.coverage
62+
.worktree/
6263
web/regression/covhtml/
6364
web/regression/htmlcov/

Dockerfile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,8 @@ RUN apk update && apk upgrade && \
165165
tzdata \
166166
libedit \
167167
libldap \
168-
libcap && \
168+
libcap \
169+
su-exec && \
169170
rm -rf /var/cache/apk/*
170171

171172
# Copy in the Python packages
@@ -197,7 +198,7 @@ RUN /venv/bin/python3 -m pip install --no-cache-dir gunicorn==23.0.0 && \
197198
useradd -r -u 5050 -g root -s /sbin/nologin pgadmin && \
198199
mkdir -p /run/pgadmin /var/lib/pgadmin && \
199200
chown pgadmin:root /run/pgadmin /var/lib/pgadmin && \
200-
chmod g=u /var/lib/pgadmin && \
201+
chmod g=u /run/pgadmin /var/lib/pgadmin && \
201202
touch /pgadmin4/config_distro.py && \
202203
chown pgadmin:root /pgadmin4/config_distro.py && \
203204
chmod g=u /pgadmin4/config_distro.py && \

pkg/docker/entrypoint.sh

Lines changed: 101 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,78 @@
11
#!/usr/bin/env bash
22

3-
# Fixup the passwd file, in case we're on OpenShift
4-
if ! whoami > /dev/null 2>&1; then
5-
if [ "$(id -u)" -ne 5050 ]; then
6-
if [ -w /etc/passwd ]; then
7-
echo "${USER_NAME:-pgadminr}:x:$(id -u):0:${USER_NAME:-pgadminr} user:${HOME}:/sbin/nologin" >> /etc/passwd
3+
########################################################################
4+
# PUID/PGID support
5+
#
6+
# When the container runs as root (e.g. --user root), the pgadmin user
7+
# is reassigned to the requested UID/GID and all initialization +
8+
# gunicorn run via su-exec as that user.
9+
#
10+
# When the container runs as non-root (default USER 5050, or OpenShift
11+
# random UID), PUID/PGID are ignored and everything runs as the
12+
# current user.
13+
########################################################################
14+
15+
PUID=${PUID:-5050}
16+
PGID=${PGID:-0}
17+
18+
# Validate PUID/PGID are numeric and in acceptable range
19+
if ! echo "$PUID" | grep -qE '^[0-9]+$'; then
20+
echo "ERROR: PUID must be a numeric value, got '$PUID'"
21+
exit 1
22+
fi
23+
if ! echo "$PGID" | grep -qE '^[0-9]+$'; then
24+
echo "ERROR: PGID must be a numeric value, got '$PGID'"
25+
exit 1
26+
fi
27+
if [ "$PUID" -eq 0 ]; then
28+
echo "ERROR: PUID=0 (root) is not allowed. Use a non-root UID."
29+
exit 1
30+
fi
31+
32+
if [ "$(id -u)" = "0" ]; then
33+
# Ensure a group with the target GID exists
34+
if ! getent group "$PGID" > /dev/null 2>&1; then
35+
if ! addgroup -g "$PGID" pggroup; then
36+
echo "ERROR: Failed to create group with GID=$PGID"
37+
exit 1
38+
fi
39+
fi
40+
41+
# Reassign the pgadmin user to the desired UID/GID
42+
if ! usermod -o -u "$PUID" -g "$PGID" pgadmin; then
43+
echo "ERROR: Failed to set pgadmin user to UID=$PUID GID=$PGID"
44+
exit 1
45+
fi
46+
47+
# Fix ownership of runtime directories BEFORE any initialization
48+
for dir in /run/pgadmin /var/lib/pgadmin; do
49+
if [ -d "$dir" ]; then
50+
chown -R "$PUID:$PGID" "$dir"
51+
fi
52+
done
53+
54+
# Fix ownership of individual files (no -R needed)
55+
if [ -e /pgadmin4/config_distro.py ]; then
56+
chown "$PUID:$PGID" /pgadmin4/config_distro.py
57+
fi
58+
59+
if [ -d /certs ]; then
60+
chown -R "$PUID:$PGID" /certs
61+
fi
62+
63+
SU_EXEC="su-exec $PUID:$PGID"
64+
echo "pgAdmin will run as UID=$PUID, GID=$PGID"
65+
else
66+
SU_EXEC=""
67+
68+
# Fixup the passwd file, in case we're on OpenShift
69+
if ! whoami > /dev/null 2>&1; then
70+
if [ "$(id -u)" -ne 5050 ]; then
71+
if [ -w /etc/passwd ]; then
72+
echo "${USER_NAME:-pgadminr}:x:$(id -u):0:${USER_NAME:-pgadminr} user:${HOME}:/sbin/nologin" >> /etc/passwd
73+
fi
74+
fi
875
fi
9-
fi
1076
fi
1177

1278
# usage: file_env VAR [DEFAULT] ie: file_env 'XYZ_DB_PASSWORD' 'example'
@@ -74,12 +140,17 @@ EOF
74140
esac
75141
echo "${var#PGADMIN_CONFIG_} = $val" >> "${CONFIG_DISTRO_FILE_PATH}"
76142
done
143+
144+
# If running as root with custom config distro path, fix ownership
145+
if [ "$(id -u)" = "0" ] && [ "${CONFIG_DISTRO_FILE_PATH}" != "/pgadmin4/config_distro.py" ]; then
146+
chown "$PUID:$PGID" "${CONFIG_DISTRO_FILE_PATH}"
147+
fi
77148
fi
78149

79150
# Check whether the external configuration database exists if it is being used.
80151
external_config_db_exists="False"
81152
if [ -n "${PGADMIN_CONFIG_CONFIG_DATABASE_URI}" ]; then
82-
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)")
153+
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)")
83154
fi
84155

85156
# DRY of the code to load the PGADMIN_SERVER_JSON_FILE
@@ -96,9 +167,9 @@ function load_server_json_file() {
96167
# When running in Desktop mode, no user is created
97168
# so we have to import servers anonymously
98169
if [ "${PGADMIN_CONFIG_SERVER_MODE}" = "False" ]; then
99-
/venv/bin/python3 /pgadmin4/setup.py load-servers "${PGADMIN_SERVER_JSON_FILE}" ${EXTRA_ARGS}
170+
$SU_EXEC /venv/bin/python3 /pgadmin4/setup.py load-servers "${PGADMIN_SERVER_JSON_FILE}" ${EXTRA_ARGS}
100171
else
101-
/venv/bin/python3 /pgadmin4/setup.py load-servers "${PGADMIN_SERVER_JSON_FILE}" --user "${PGADMIN_DEFAULT_EMAIL}" ${EXTRA_ARGS}
172+
$SU_EXEC /venv/bin/python3 /pgadmin4/setup.py load-servers "${PGADMIN_SERVER_JSON_FILE}" --user "${PGADMIN_DEFAULT_EMAIL}" ${EXTRA_ARGS}
102173
fi
103174
fi
104175
}
@@ -124,7 +195,7 @@ if [ ! -f /var/lib/pgadmin/pgadmin4.db ] && [ "${external_config_db_exists}" = "
124195
fi
125196
email_config="{'CHECK_EMAIL_DELIVERABILITY': ${CHECK_EMAIL_DELIVERABILITY}, 'ALLOW_SPECIAL_EMAIL_DOMAINS': ${ALLOW_SPECIAL_EMAIL_DOMAINS}, 'GLOBALLY_DELIVERABLE': ${GLOBALLY_DELIVERABLE}}"
126197
echo "email config is ${email_config}"
127-
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)")
198+
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)")
128199
if echo "${is_valid_email}" | grep "False" > /dev/null; then
129200
echo "'${PGADMIN_DEFAULT_EMAIL}' does not appear to be a valid email address. Please reset the PGADMIN_DEFAULT_EMAIL environment variable and try again."
130201
echo "Validation output: ${is_valid_email}"
@@ -140,7 +211,7 @@ if [ ! -f /var/lib/pgadmin/pgadmin4.db ] && [ "${external_config_db_exists}" = "
140211

141212
# Initialize DB before starting Gunicorn
142213
# Importing pgadmin4 (from this script) is enough
143-
/venv/bin/python3 run_pgadmin.py
214+
$SU_EXEC /venv/bin/python3 run_pgadmin.py
144215

145216
export PGADMIN_PREFERENCES_JSON_FILE="${PGADMIN_PREFERENCES_JSON_FILE:-/pgadmin4/preferences.json}"
146217

@@ -150,22 +221,30 @@ if [ ! -f /var/lib/pgadmin/pgadmin4.db ] && [ "${external_config_db_exists}" = "
150221
# Pre-load any required preferences
151222
if [ -f "${PGADMIN_PREFERENCES_JSON_FILE}" ]; then
152223
if [ "${PGADMIN_CONFIG_SERVER_MODE}" = "False" ]; then
153-
DESKTOP_USER=$(cd /pgadmin4 && /venv/bin/python3 -c 'import config; print(config.DESKTOP_USER)')
154-
/venv/bin/python3 /pgadmin4/setup.py set-prefs "${DESKTOP_USER}" --input-file "${PGADMIN_PREFERENCES_JSON_FILE}"
224+
DESKTOP_USER=$(cd /pgadmin4 && $SU_EXEC /venv/bin/python3 -c 'import config; print(config.DESKTOP_USER)')
225+
$SU_EXEC /venv/bin/python3 /pgadmin4/setup.py set-prefs "${DESKTOP_USER}" --input-file "${PGADMIN_PREFERENCES_JSON_FILE}"
155226
else
156-
/venv/bin/python3 /pgadmin4/setup.py set-prefs "${PGADMIN_DEFAULT_EMAIL}" --input-file "${PGADMIN_PREFERENCES_JSON_FILE}"
227+
$SU_EXEC /venv/bin/python3 /pgadmin4/setup.py set-prefs "${PGADMIN_DEFAULT_EMAIL}" --input-file "${PGADMIN_PREFERENCES_JSON_FILE}"
157228
fi
158229
fi
159230
# Copy the pgpass file passed using secrets
160-
if [ -f "${PGPASS_FILE}" ]; then
231+
if [ -n "${PGPASS_FILE}" ] && [ -f "${PGPASS_FILE}" ]; then
161232
if [ "${PGADMIN_CONFIG_SERVER_MODE}" = "False" ]; then
162-
cp ${PGPASS_FILE} /var/lib/pgadmin/.pgpass
233+
cp "${PGPASS_FILE}" /var/lib/pgadmin/.pgpass
163234
chmod 600 /var/lib/pgadmin/.pgpass
235+
# Fix ownership when running as root
236+
if [ "$(id -u)" = "0" ]; then
237+
chown "$PUID:$PGID" /var/lib/pgadmin/.pgpass
238+
fi
164239
else
165240
PGADMIN_USER_CONFIG_DIR=$(echo "${PGADMIN_DEFAULT_EMAIL}" | sed 's/@/_/g')
166-
mkdir -p /var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}
167-
cp ${PGPASS_FILE} /var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}/.pgpass
168-
chmod 600 /var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}/.pgpass
241+
mkdir -p "/var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}"
242+
cp "${PGPASS_FILE}" "/var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}/.pgpass"
243+
chmod 600 "/var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}/.pgpass"
244+
# Fix ownership when running as root
245+
if [ "$(id -u)" = "0" ]; then
246+
chown -R "$PUID:$PGID" "/var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}"
247+
fi
169248
fi
170249
fi
171250
# If already initialised and PGADMIN_REPLACE_SERVERS_ON_STARTUP is set to true, then load the server json file.
@@ -180,7 +259,7 @@ fi
180259

181260
# Get the session timeout from the pgAdmin config. We'll use this (in seconds)
182261
# to define the Gunicorn worker timeout
183-
TIMEOUT=$(cd /pgadmin4 && /venv/bin/python3 -c 'import config; print(config.SESSION_EXPIRATION_TIME * 60 * 60 * 24)')
262+
TIMEOUT=$(cd /pgadmin4 && $SU_EXEC /venv/bin/python3 -c 'import config; print(config.SESSION_EXPIRATION_TIME * 60 * 60 * 24)')
184263

185264
# NOTE: currently pgadmin can run only with 1 worker due to sessions implementation
186265
# Using --threads to have multi-threaded single-process worker
@@ -196,7 +275,7 @@ else
196275
fi
197276

198277
if [ -n "${PGADMIN_ENABLE_TLS}" ]; then
199-
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
278+
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
200279
else
201-
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
280+
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
202281
fi

requirements.txt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@
1212
# relevant packages.
1313
###############################################################################
1414

15-
Authlib==1.6.9
15+
Authlib==1.6.*; python_version <= '3.9'
16+
Authlib==1.7.*; python_version > '3.9'
1617
azure-identity==1.25.3
1718
azure-mgmt-rdbms==10.1.1
1819
azure-mgmt-resource==25.0.0
1920
azure-mgmt-subscription==3.1.1
2021
bcrypt==5.0.*
2122
boto3==1.42.*
22-
certifi==2026.2.25
23+
certifi==2026.4.22
2324
cryptography==46.0.*
2425
Flask-Babel==4.0.*
2526
Flask-Compress==1.*
@@ -28,13 +29,14 @@ Flask-Mail==0.*
2829
Flask-Migrate==4.*
2930
Flask-Paranoid==0.*
3031
Flask-Security-Too==5.4.*; python_version <= '3.9'
31-
Flask-Security-Too==5.7.*; python_version > '3.9'
32+
Flask-Security-Too==5.8.*; python_version > '3.9'
3233
Flask-SocketIO==5.6.*
3334
Flask-SQLAlchemy==3.1.*
34-
Flask-WTF==1.2.*
35+
Flask-WTF==1.2.*; python_version <= '3.9'
36+
Flask-WTF==1.3.*; python_version > '3.9'
3537
Flask==3.1.*
3638
google-api-python-client==2.*
37-
google-auth-oauthlib==1.3.0
39+
google-auth-oauthlib==1.3.1
3840
gssapi==1.11.*
3941
jsonformatter~=0.3.4
4042
keyring==25.*
@@ -62,4 +64,4 @@ urllib3==2.6.*; python_version > '3.9'
6264
user-agents==2.2.0
6365
Werkzeug==3.1.*
6466
WTForms==3.1.*; python_version <= '3.9'
65-
WTForms==3.2.*; python_version > '3.9'
67+
WTForms==3.2.*; python_version > '3.9'

runtime/package.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212
},
1313
"packageManager": "yarn@4.9.2",
1414
"devDependencies": {
15-
"electron": "41.0.2",
16-
"eslint": "^9.39.2",
17-
"eslint-plugin-unused-imports": "^4.4.1"
15+
"@eslint/js": "^10.0.1",
16+
"electron": "^41.3.0",
17+
"eslint": "^10.2.1",
18+
"eslint-plugin-unused-imports": "^4.4.1",
19+
"globals": "^17.5.0"
1820
},
1921
"dependencies": {
20-
"axios": "^1.13.5",
21-
"electron-context-menu": "^4.1.0",
22+
"axios": "^1.15.2",
23+
"electron-context-menu": "^4.1.2",
2224
"electron-store": "^11.0.2"
2325
}
2426
}

runtime/src/js/downloader.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ function updateProgress(callerWindow) {
4747
return;
4848
}
4949
setBadge(Object.keys(downloadQueue).length);
50-
let progress = 0;
50+
let progress;
5151
if(Object.values(downloadQueue).some((item) => item.total === null)) {
5252
// If any of the items in the queue does not have a total, we cannot calculate progress
5353
// so we return 2 to indicate that the progress is indeterminate.

runtime/src/js/misc.js

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -100,17 +100,13 @@ const serverLogFile = path.join(getLocalAppDataPath(), 'pgadmin4.' + (new Date()
100100

101101
// This function is used to read the file and return the content
102102
export const readServerLog = () => {
103-
let data = null;
104-
105103
if (fs.existsSync(serverLogFile)) {
106-
data = fs.readFileSync(serverLogFile, 'utf8');
107-
} else {
108-
let errMsg = 'Unable to read file ' + serverLogFile + '.';
109-
console.warn(errMsg);
110-
return errMsg;
104+
return fs.readFileSync(serverLogFile, 'utf8');
111105
}
112106

113-
return data;
107+
let errMsg = 'Unable to read file ' + serverLogFile + '.';
108+
console.warn(errMsg);
109+
return errMsg;
114110
};
115111

116112
// This function is used to write the data into the log file

0 commit comments

Comments
 (0)