Compare commits

...
Author SHA1 Message Date
Dave Page 062099ae5b Retry stapler validate in the macOS build and acceptance checks. (#10686)
It looks the notarisation ticket up in Apple's CloudKit, and a dropped
connection there has failed both the arm64 build's verification step,
skipping the rest of a release candidate, and the acceptance test, on
bundles that were notarised and accepted by Gatekeeper.
2026-10-27 00:01:39 +00:00
Dave Page 0e7c33bee2 chore: Sign the versioned OpenSSL, gettext and iconv libraries in the Windows build. (#10684)
FOR treats an item containing a wildcard as a glob against the current
directory and drops it when nothing matches, so libcrypto-*-x64.dll,
libssl-*-x64.dll, libintl-*.dll and libiconv-*.dll never reached the
inner DIR and were shipped unsigned since #10355. Run the loop from the
runtime directory instead, checking each name exists.
2026-10-27 01:46:48 +05:30
Ashesh Vashi 5a082916f8 Collect only the release-candidate build outputs, not the build record.
The assemble job downloaded every artefact of the run, which includes the
'pgadmin-org~pgadmin4~*.dockerbuild' build record that
docker/build-push-action uploads for the container. That artefact is not
stored as a plain zip, so actions/download-artifact fails to extract it,
and because it was downloading everything, the whole step failed after
its retries with 'Artifact download failed after 5 retries' -- while every
real artefact downloaded fine. None of the build record is used here; the
staging tree is assembled only from the pgadmin4-* artefacts. Restrict the
download to pgadmin4-*, which leaves the build record behind.
2026-10-27 00:05:21 +05:30
Ashesh Vashi 0eac05b450 Grant packages: write to the release-candidate container job.
A called workflow's token is capped by the caller's permissions. The
release-candidate workflow sets 'contents: read' at the top level, which
leaves 'packages: none', so when it publishes the container with
publish: true the check-container-build publish-container job's
'packages: write' request exceeded the cap and the workflow was rejected
before starting (startup_failure). Grant packages: write on the container
caller job. The nightly snapshot is unaffected: it builds the container
as a check, with publish-container skipped, so it never requests the
scope.
2026-10-26 22:35:55 +05:30
Ashesh Vashi 7bbbb53e69 Skip the missing-client-file passthrough test on Windows.
Like the equivalent utility_conninfo case, a configured client file that
cannot be found is disabled (NO_CLIENT_FILE) on Windows rather than passed
on as its path; both are unopenable, so the connection still cannot use
another file. fs_short_path() also returns empty for a path that does not
exist on Windows, so the expectation could not be normalised. Run the
passthrough scenario on POSIX only.
2026-10-26 19:33:39 +05:30
Ashesh Vashi eb66e6a347 Make the 9.19 security tests pass on Windows.
The tests added for the 9.19 storage and connection hardening were
written on Linux and failed on Windows, where the behaviour they check
is correct but expressed differently. The production code is unchanged.

- Skip the Backup, Restore and Import/Export descriptor-based
  symlink-race tests on Windows: opening a file with O_NOFOLLOW and
  handing it to the utility as /dev/fd/N is POSIX-only, and the code
  already passes the real path there.
- Compare credential-file paths after fs_short_path()+forward slashes,
  the short (8.3) form libpq is given on Windows; this is the identity
  on POSIX. Covers the --dbname conninfo call sites, get_complete_file_path,
  client-file isolation and the SSH tunnel identity file.
- Normalise the File Manager closest-parent separator to '/'.
- Drop the other inherited credential variable before checking which
  remove_inherited_libpq_credentials() removes, as the runner's own
  environment may hold PGPASSWORD.
- Use forward slashes in the raw hostile --database value, whose
  backslashes libpq would otherwise treat as escapes, and skip the
  'missing configured file passed through' case on Windows, where such
  a file is disabled (NO_CLIENT_FILE) instead.
2026-10-26 19:02:39 +05:30
Ashesh Vashi 36122bc29b Skip the Kerberos require_auth tests where the installed libpq is older than 16.
require_auth needs libpq 16, so pgAdmin only adds it when the installed
libpq is new enough.  These tests mock the connection-parameter libpq
version to exercise that path, but create_connection_string() validates
the connection string, and the injection tests build and parse it, with
the real installed libpq.  On a runner whose libpq is older than 16 that
libpq rejects require_auth, so the scenarios that produce it errored on
macOS and Windows.  Skip a require_auth-producing scenario when the
installed libpq cannot parse it, as the one real-parser scenario in
test_kerberos_conninfo already did.
2026-10-26 17:34:28 +05:30
Kundan 6d035dc4fa Skip the view msql tests that expect DROP VIEW on EPAS. (#10660)
EPAS can change a view's columns with CREATE OR REPLACE VIEW, so pgAdmin
only drops and re-creates the view on PostgreSQL (#5053). The tests
added for #9838 expected DROP VIEW on every server and failed on EPAS.
2026-10-26 17:30:22 +05:30
Ashesh Vashi ea17a4e0ff Install libpq in the Docker env-builder so psycopg's C extension loads.
python:3-alpine now resolves to Python 3.15, whose psycopg[c] wheel links
against the system libpq.  postgresql-dev is removed with build-deps, so
libpq.so.5 was no longer present at runtime and importing psycopg (as the
Sphinx docs build does via config.py) failed with 'no pq wrapper
available', breaking the container build.  Install the runtime libpq
package separately so it survives the build-deps cleanup.
2026-10-26 15:43:59 +05:30
Ashesh Vashi a972ea204e Fix the 9.19 release notes using a math-italic pi where 'py' was meant, which broke the PDF docs build. 2026-10-26 15:43:59 +05:30
21 changed files with 194 additions and 34 deletions

No files matched your search

+9 -1
View File
@@ -335,7 +335,15 @@ jobs:
# on the bundle, set by SetFile -a E in _create_dmg to hide the .app
# extension in the window, and --strict rejects any xattr at all.
echo "=== notarisation and Gatekeeper ==="
xcrun stapler validate "${app}"
# stapler looks the ticket up in Apple's CloudKit, and a dropped
# connection there fails the step with error 68, so it is retried.
# A genuinely broken ticket still fails all three attempts.
for attempt in 1 2 3; do
xcrun stapler validate "${app}" && break
[ "${attempt}" -lt 3 ] || exit 1
echo "stapler validate failed; retrying in $((attempt * 30))s"
sleep $((attempt * 30))
done
spctl --assess --type execute --verbose=4 "${app}"
echo "=== signature ==="
+13
View File
@@ -90,6 +90,13 @@ jobs:
container:
needs: location
if: inputs.publish
# The caller caps a called workflow's token: the top-level
# 'contents: read' leaves 'packages: none', so the publish-container job's
# 'packages: write' request must be granted here or the workflow is
# rejected before it starts.
permissions:
contents: read
packages: write
uses: ./.github/workflows/check-container-build.yml
with:
publish: true
@@ -185,10 +192,16 @@ jobs:
steps:
- uses: actions/checkout@v7
# Only the build outputs, which are all named pgadmin4-*. The container
# build also leaves a 'pgadmin-org~pgadmin4~*.dockerbuild' build-record
# artifact that docker/build-push-action uploads; it is not a release
# file, and it is not stored as a plain zip, so download-artifact fails
# to extract it and, downloading everything, fails the whole step.
- name: Collect the build artefacts
uses: actions/download-artifact@v7
with:
path: artefacts
pattern: pgadmin4-*
# The layout is the one the publishing scripts and the download site
# expect, and is what the wrapper's stage-create verb creates on the
+13 -2
View File
@@ -141,10 +141,21 @@ jobs:
echo "=== gatekeeper assessment ==="
spctl --assess --type execute --verbose=4 "${APP}"
echo "=== stapled notarisation ==="
xcrun stapler validate "${APP}"
# stapler looks the ticket up in Apple's CloudKit, and a dropped
# connection there fails the step with error 68, so it is retried.
# A genuinely broken ticket still fails all three attempts.
validate() {
for attempt in 1 2 3; do
xcrun stapler validate "$1" && return 0
[ "${attempt}" -lt 3 ] || return 1
echo "stapler validate failed; retrying in $((attempt * 30))s"
sleep $((attempt * 30))
done
}
validate "${APP}"
for F in dist/*.dmg; do
echo "--- ${F} ---"
xcrun stapler validate "${F}"
validate "${F}"
done
- name: Archive the disk image and zip
+8 -1
View File
@@ -59,8 +59,15 @@ RUN --mount=type=bind,source=.git,target=/pgadmin4/.git \
FROM python:3-alpine AS env-builder
# Install dependencies
#
# libpq is a runtime dependency of psycopg's C extension: psycopg[c] is built
# from source against postgresql-dev, but that is removed with build-deps
# below, so libpq.so.5 must be installed separately to survive. Without it,
# importing psycopg (which the Sphinx docs build does, via config.py) fails
# with "no pq wrapper available".
RUN apk add --no-cache \
make && \
make \
libpq && \
apk add --no-cache --virtual build-deps \
build-base \
openssl-dev \
+9 -2
View File
@@ -563,9 +563,16 @@ REM and some of them are optional.
SETLOCAL EnableDelayedExpansion
SET "COMPONENTS="
FOR %%p IN (pgAdmin4.exe libpq.dll libcrypto-*-x64.dll libssl-*-x64.dll libintl-*.dll libiconv-*.dll liblz4.dll libzstd.dll zlib1.dll pg_dump.exe pg_dumpall.exe pg_restore.exe psql.exe kinit.exe krb5_64.dll comerr64.dll k5sprt64.dll gssapi64.dll krbcc64.dll ccapiserver.exe) DO (
FOR /F "delims=" %%f IN ('DIR /B "%BUILDROOT%\runtime\%%p" 2^>nul') DO SET "COMPONENTS=!COMPONENTS! "%BUILDROOT%\runtime\%%f""
REM FOR expands an item containing a wildcard against the current directory
REM and silently drops it when nothing matches there, so the loop must run
REM in the runtime directory for the versioned libraries to be found at
REM all. Plain names are passed through whether or not the file exists,
REM hence the IF EXIST for the optional ones.
PUSHD "%BUILDROOT%\runtime" || EXIT /B 1
FOR %%f IN (pgAdmin4.exe libpq.dll libcrypto-*-x64.dll libssl-*-x64.dll libintl-*.dll libiconv-*.dll liblz4.dll libzstd.dll zlib1.dll pg_dump.exe pg_dumpall.exe pg_restore.exe psql.exe kinit.exe krb5_64.dll comerr64.dll k5sprt64.dll gssapi64.dll krbcc64.dll ccapiserver.exe) DO (
IF EXIST "%%f" SET "COMPONENTS=!COMPONENTS! "%BUILDROOT%\runtime\%%f""
)
POPD
IF "!COMPONENTS!" == "" (
ECHO.
ECHO ************************************************************
+1 -1
View File
@@ -141,7 +141,7 @@ Bug fixes
| `Issue #10386 <https://github.com/pgadmin-org/pgadmin4/issues/10386>`_ - Stop the Query Tool spinner, and its idle CPU use, whilst a password prompt is open.
| `Issue #10401 <https://github.com/pgadmin-org/pgadmin4/issues/10401>`_ - Show default privileges in the Database properties panel.
| `Issue #10409 <https://github.com/pgadmin-org/pgadmin4/issues/10409>`_ - Include the authenticated user in the access log on logout.
| `Issue #10414 <https://github.com/pgadmin-org/pgadmin4/issues/10414>`_ - Fix the Linux packages shipping a stray copy of Python as ``venv/bin/𝜋thon``, which stopped the DEB package installing on Ubuntu 26.04.
| `Issue #10414 <https://github.com/pgadmin-org/pgadmin4/issues/10414>`_ - Fix the Linux packages shipping a stray copy of Python as ``venv/bin/python``, which stopped the DEB package installing on Ubuntu 26.04.
| `Issue #10415 <https://github.com/pgadmin-org/pgadmin4/issues/10415>`_ - Prefer the user's email address as the Gravatar identifier.
| `Issue #10418 <https://github.com/pgadmin-org/pgadmin4/issues/10418>`_ - Remove the dead ``usr`` parameter from Query Tool connection URLs.
| `Issue #10419 <https://github.com/pgadmin-org/pgadmin4/issues/10419>`_ - Fix Kerberos login failures returning a JSON 500 response, and first-time automatically created Kerberos logins being rejected.
@@ -33,6 +33,11 @@ class ViewsGetMsqlTestCase(BaseTestGenerator):
# Load test data
self.data = self.test_data
# Check server type
if "server_type" in self.data and \
self.server['type'] != self.data["server_type"]:
self.skipTest(self.data["skip_msg"])
# Create db connection
self.db_name = parent_node_dict["database"][-1]["db_name"]
schema_info = parent_node_dict["schema"][-1]
@@ -1558,7 +1558,9 @@
"query": "\"CREATE OR REPLACE VIEW %s.%s AS SELECT t.id, concat(t.a, ' x ', t.b) AS ab, (t.d)::text AS ds, u.n FROM (VALUES (1, 'a', 'b', now()::date)) t(id, a, b, d) CROSS JOIN (VALUES (2)) u(n); ALTER TABLE %s.%s OWNER TO %s\" % (schema_name, view_name, schema_name, view_name, server['username'])"
},
"test_data": {
"definition": "SELECT t.id, concat(t.a, ' x ', t.b) AS ab, u.n FROM (VALUES (1, 'a', 'b', now()::date)) t(id, a, b, d) CROSS JOIN (VALUES (2)) u(n)"
"definition": "SELECT t.id, concat(t.a, ' x ', t.b) AS ab, u.n FROM (VALUES (1, 'a', 'b', now()::date)) t(id, a, b, d) CROSS JOIN (VALUES (2)) u(n)",
"server_type": "pg",
"skip_msg": "EPAS can change a view's columns without dropping it."
},
"mocking_required": false,
"mock_data": {},
@@ -1578,7 +1580,9 @@
"query": "\"CREATE OR REPLACE VIEW %s.%s AS SELECT id, valid_from, name FROM (VALUES (1, now()::date, 'a')) t(id, valid_from, name); ALTER TABLE %s.%s OWNER TO %s\" % (schema_name, view_name, schema_name, view_name, server['username'])"
},
"test_data": {
"definition": "SELECT id, valid_from FROM (VALUES (1, now()::date, 'a')) t(id, valid_from, name)"
"definition": "SELECT id, valid_from FROM (VALUES (1, now()::date, 'a')) t(id, valid_from, name)",
"server_type": "pg",
"skip_msg": "EPAS can change a view's columns without dropping it."
},
"mocking_required": false,
"mock_data": {},
@@ -1571,7 +1571,13 @@ class TestGetClosestParentStaysInStorage(_StorageOpsMixin, BaseTestGenerator):
scenarios = [('default', dict())]
def runTest(self):
closest = Filemanager.get_closest_parent
_closest = Filemanager.get_closest_parent
# The File Manager uses forward-slash paths; normalise the separator
# so the expectations hold on Windows, where os.path.join uses "\".
def closest(*args):
return _closest(*args).replace('\\', '/')
os.makedirs(os.path.join(self.storage_dir, "a"))
self.assertEqual(closest(self.storage_dir, "/a/"), "/a/")
self.assertEqual(closest(self.storage_dir, "/a/gone/deeper/"), "/a/")
@@ -43,6 +43,13 @@ class BackupSymlinkRaceTest(BaseTestGenerator):
]
def setUp(self):
# Opening the file without following a leaf symbolic link and handing
# it to the utility as /dev/fd/N needs O_NOFOLLOW and descriptor-based
# opens, which exist only on POSIX; on Windows the real path is passed
# and this protection does not apply.
if os.name == 'nt':
self.skipTest(
'Descriptor-based symlink-race protection is POSIX-only.')
if 'default_binary_paths' not in self.server or \
self.server['default_binary_paths'] is None or \
self.server['type'] not in self.server['default_binary_paths'] or \
@@ -45,6 +45,13 @@ class ImportExportSymlinkRaceTest(BaseTestGenerator):
]
def setUp(self):
# Opening the file without following a leaf symbolic link and handing
# it to the utility as /dev/fd/N needs O_NOFOLLOW and descriptor-based
# opens, which exist only on POSIX; on Windows the real path is passed
# and this protection does not apply.
if os.name == 'nt':
self.skipTest(
'Descriptor-based symlink-race protection is POSIX-only.')
if 'default_binary_paths' not in self.server or \
self.server['default_binary_paths'] is None or \
self.server['type'] not in self.server['default_binary_paths'] or \
@@ -46,6 +46,13 @@ class RestoreSymlinkRaceTest(BaseTestGenerator):
]
def setUp(self):
# Opening the file without following a leaf symbolic link and handing
# it to the utility as /dev/fd/N needs O_NOFOLLOW and descriptor-based
# opens, which exist only on POSIX; on Windows the real path is passed
# and this protection does not apply.
if os.name == 'nt':
self.skipTest(
'Descriptor-based symlink-race protection is POSIX-only.')
if 'default_binary_paths' not in self.server or \
self.server['default_binary_paths'] is None or \
self.server['type'] not in self.server['default_binary_paths'] or \
@@ -25,7 +25,7 @@ from unittest.mock import MagicMock, patch
import psycopg
from psycopg.conninfo import conninfo_to_dict, make_conninfo
from pgadmin.utils import NO_CLIENT_FILE
from pgadmin.utils import NO_CLIENT_FILE, fs_short_path
from pgadmin.utils.route import BaseTestGenerator
import pgadmin.utils.driver.psycopg3 as psycopg3_driver
@@ -106,7 +106,8 @@ class ConnectionStringClientFilesTestCase(_StorageMixin, BaseTestGenerator):
'sslcert': NO_CLIENT_FILE,
'sslkey': NO_CLIENT_FILE})),
('Server mode passes on a configured file that does not exist',
dict(params={'sslcert': '/missing.crt', 'sslkey': '/missing.key',
dict(posix_only=True,
params={'sslcert': '/missing.crt', 'sslkey': '/missing.key',
'sslrootcert': '/missing-root.crt',
'sslcrl': '/missing.crl'},
expected={'passfile': NO_CLIENT_FILE,
@@ -133,15 +134,25 @@ class ConnectionStringClientFilesTestCase(_StorageMixin, BaseTestGenerator):
server_mode = True
expected = {}
absent = ()
posix_only = False
def runTest(self):
# On Windows a configured file that cannot be found is disabled
# (NO_CLIENT_FILE), not passed on as its path; both are unopenable.
if self.posix_only and os.name == 'nt':
self.skipTest(
'A missing configured file is disabled on Windows.')
dsn, display = self._connection_string(self.params, self.server_mode)
for key, value in self.expected.items():
if value not in (NO_CLIENT_FILE, 'system'):
# Expected files are given relative to the storage
# directory.
value = os.path.join(self.storage, value)
# directory, and resolved as get_complete_file_path() returns
# them: the short (8.3) form with forward slashes on Windows,
# unchanged on POSIX.
value = fs_short_path(
os.path.join(self.storage, value)).replace('\\', '/')
self.assertEqual(dsn.get(key), value, key)
for key in self.absent:
self.assertNotIn(key, dsn)
@@ -144,7 +144,18 @@ class ConnectionStringKerberosTestCase(BaseTestGenerator):
shutil.rmtree(self.storage, ignore_errors=True)
def runTest(self):
if self.real_parser and not LIBPQ_HAS_REQUIRE_AUTH:
# Any scenario that puts require_auth into the connection string needs
# a libpq that knows it: create_connection_string() validates the
# string, and a real_parser scenario parses it, with the installed
# libpq, which before 16 rejects require_auth. Only the connection
# parameter version is mocked per scenario, not the installed libpq.
produces_require_auth = (
self.real_parser or
getattr(self, 'error', False) or
'require_auth' in self.expected or
'require_auth' in self.params
)
if produces_require_auth and not LIBPQ_HAS_REQUIRE_AUTH:
self.skipTest('The installed libpq cannot parse require_auth.')
manager = server_manager.ServerManager.__new__(
@@ -228,6 +228,12 @@ class InheritedCredentialsTestCase(_ServiceMixin, BaseTestGenerator):
conn.close()
with patch.dict(os.environ, env):
# The runner's own environment may already hold the other
# inherited credential variable (CI sets PGPASSWORD); drop it so
# the scenario sees only the one it set.
for other in ('PGPASSWORD', 'PGSERVICE'):
if other != self.var:
os.environ.pop(other, None)
removed = remove_inherited_libpq_credentials()
remaining = dict(os.environ)
self.assertEqual(removed, [self.var])
@@ -22,6 +22,7 @@ from unittest.mock import MagicMock, patch
import paramiko
from pgadmin.utils import fs_short_path
from pgadmin.utils.route import BaseTestGenerator
from pgadmin.utils.driver.psycopg3 import server_manager as sm
@@ -204,8 +205,12 @@ class TestSSHTunnelIdentityFileConfinement(
self.assertNotIn(self.alice, result[1])
else:
self.assertEqual(result, (True, None))
self.assertEqual(forwarder.call_args.kwargs['ssh_pkey'],
os.path.join(self.alice, 'mykey'))
# get_complete_file_path() returns the short (8.3) form with
# forward slashes on Windows, unchanged on POSIX.
self.assertEqual(
forwarder.call_args.kwargs['ssh_pkey'],
fs_short_path(
os.path.join(self.alice, 'mykey')).replace('\\', '/'))
class TestSSHTunnelRealSSHTunnelForwarder(
@@ -26,12 +26,19 @@ import flask
import config
from pgadmin.utils import (
connection_params_to_env, get_complete_file_path)
connection_params_to_env, fs_short_path, get_complete_file_path)
from pgadmin.utils.route import BaseTestGenerator
import pgadmin.utils.driver.psycopg3.server_manager as server_manager
def _canon(path):
"""Compare paths as get_complete_file_path() returns them: on Windows the
short (8.3) form libpq is given, so an expected long path matches;
fs_short_path() is the identity on POSIX."""
return os.path.normpath(fs_short_path(path))
class _StorageMixin:
"""Builds a storage root holding two users and a shared directory."""
@@ -138,8 +145,7 @@ class CompleteFilePathContainmentTestCase(_StorageMixin, BaseTestGenerator):
else:
# Expected paths are given relative to the temporary root.
expected = os.path.join(self.outer, *self.expected.split('/'))
self.assertEqual(os.path.normpath(result),
os.path.normpath(expected))
self.assertEqual(_canon(result), _canon(expected))
class CompleteFilePathDesktopTestCase(_StorageMixin, BaseTestGenerator):
@@ -263,8 +269,8 @@ class CompleteFilePathRealStorageDirTestCase(_StorageMixin,
def runTest(self):
self.assertEqual(
os.path.normpath(self._resolve_real('/.pgpass')),
os.path.join(self.alice, '.pgpass'))
_canon(self._resolve_real('/.pgpass')),
_canon(os.path.join(self.alice, '.pgpass')))
with self.assertRaises(PermissionError) as cm:
self._resolve_real('../bob/.pgpass')
@@ -272,8 +278,8 @@ class CompleteFilePathRealStorageDirTestCase(_StorageMixin,
str(cm.exception))
self.assertEqual(
os.path.normpath(self._resolve_real('Team:/.pgpass')),
os.path.join(self.shared, '.pgpass'))
_canon(self._resolve_real('Team:/.pgpass')),
_canon(os.path.join(self.shared, '.pgpass')))
# A name that is not configured is just part of a relative path.
self.assertIsNone(self._resolve_real('Nope:/.pgpass'))
@@ -405,8 +411,8 @@ class CompleteFilePathWriteTargetTestCase(_StorageMixin, BaseTestGenerator):
else:
result = get_complete_file_path('Team:/keylog', validate=False,
for_write=True)
self.assertEqual(os.path.normpath(result),
os.path.join(self.shared, 'keylog'))
self.assertEqual(_canon(result),
_canon(os.path.join(self.shared, 'keylog')))
def _user(is_admin):
@@ -19,6 +19,7 @@ from types import SimpleNamespace
from unittest.mock import MagicMock, patch
from flask import session
import psycopg
from psycopg.conninfo import make_conninfo, conninfo_to_dict
import config
@@ -32,6 +33,11 @@ from pgadmin.utils.route import BaseTestGenerator
USER_CACHE = 'FILE:/tmp/pgadmin_cache_alice'
SERVICE_ACCOUNT = {'KRB5CCNAME': 'FILE:/tmp/krb5cc_service_account'}
# The libpq that parses a connection string is the one installed, which may
# be older than 16 and then rejects require_auth. Scenarios that build or
# parse a string carrying require_auth with the real libpq need 16 or later.
LIBPQ_HAS_REQUIRE_AUTH = psycopg.pq.version() >= 160000
def _login(source, ccache=USER_CACHE):
session['auth_source_manager'] = {'current_source': source}
@@ -563,6 +569,9 @@ class ApplyKerberosConnParamsKeyInjectionTestCase(BaseTestGenerator):
def setUp(self):
unittest.TestCase.setUp(self)
# The real libpq builds and parses the string carrying require_auth.
if not LIBPQ_HAS_REQUIRE_AUTH:
self.skipTest('The installed libpq cannot parse require_auth.')
def runTest(self):
params = dict(self.params)
@@ -626,6 +635,9 @@ class ConnectionStringKerberosKeyInjectionTestCase(BaseTestGenerator):
def setUp(self):
unittest.TestCase.setUp(self)
# The real libpq builds and parses the string carrying require_auth.
if not LIBPQ_HAS_REQUIRE_AUTH:
self.skipTest('The installed libpq cannot parse require_auth.')
def runTest(self):
import pgadmin.utils.driver.psycopg3.server_manager as server_manager
@@ -23,7 +23,7 @@ from unittest.mock import patch
from psycopg.conninfo import conninfo_to_dict
from pgadmin.utils import (
utility_conninfo, connection_params_to_env, NO_CLIENT_FILE)
utility_conninfo, connection_params_to_env, fs_short_path, NO_CLIENT_FILE)
from pgadmin.utils.route import BaseTestGenerator
CREDENTIAL_FILES = ('passfile', 'sslcert', 'sslkey')
@@ -55,7 +55,7 @@ class UtilityConninfoTestCase(BaseTestGenerator):
'sslkey': NO_CLIENT_FILE})),
('Server mode, a configured file that does not exist is passed on '
'as the env translation gives it',
dict(server_mode=True, database='postgres',
dict(server_mode=True, database='postgres', posix_only=True,
params={'sslcert': 'missing.crt'},
expected={'dbname': 'postgres',
'passfile': NO_CLIENT_FILE,
@@ -129,8 +129,23 @@ class UtilityConninfoTestCase(BaseTestGenerator):
for p in reversed(patches):
p.stop()
expected = {k: v.replace('<storage>', self.storage)
for k, v in self.expected.items()}
# On Windows a configured file that cannot be found is disabled
# (NO_CLIENT_FILE), not passed through as its resolved path; both are
# unopenable, so the connection still cannot use another file.
if getattr(self, 'posix_only', False) and os.name == 'nt':
self.skipTest(
'A missing configured file is disabled on Windows.')
def _exp(v):
# Storage files resolve as get_complete_file_path() returns them:
# the short (8.3) form with forward slashes on Windows, unchanged
# on POSIX.
if isinstance(v, str) and '<storage>' in v:
return fs_short_path(
v.replace('<storage>', self.storage)).replace('\\', '/')
return v
expected = {k: _exp(v) for k, v in self.expected.items()}
self.assertEqual(result, expected)
@@ -28,13 +28,21 @@ from psycopg.conninfo import conninfo_to_dict
from pgadmin.misc.bgprocess.processes import BatchProcess
from pgadmin.utils import (
NO_CLIENT_FILE, database_conninfo, utility_connection_params)
NO_CLIENT_FILE, database_conninfo, fs_short_path,
utility_connection_params)
from pgadmin.utils.route import BaseTestGenerator
DATABASE = 'host=evil dbname=x'
CONNECTION_PARAMS = {'passfile': '.pgpass'}
def _resolved(path):
"""A storage file path as get_complete_file_path() returns it: on
Windows the short (8.3) form with forward slashes, which libpq is given;
unchanged on POSIX, where fs_short_path() is the identity."""
return fs_short_path(path).replace('\\', '/')
SCENARIOS = [
('Server mode', dict(server_mode=True)),
('Desktop mode', dict(server_mode=False)),
@@ -105,7 +113,7 @@ class _CallSiteBase:
if not self.server_mode:
return {'dbname': DATABASE}
return {'dbname': DATABASE,
'passfile': os.path.join(self.storage, '.pgpass'),
'passfile': _resolved(os.path.join(self.storage, '.pgpass')),
'sslcert': NO_CLIENT_FILE, 'sslkey': NO_CLIENT_FILE}
def _assert_dbname(self, args, expected=None):
@@ -199,7 +207,7 @@ class ConnectionParamsSourceTestCase(_CallSiteBase, BaseTestGenerator):
lambda: utility_conninfo(
'postgres', utility_connection_params(manager, server)))
self.assertEqual(conninfo_to_dict(conninfo)['passfile'],
os.path.join(self.storage, '.pgpass'))
_resolved(os.path.join(self.storage, '.pgpass')))
class RestoreArgsTestCase(_CallSiteBase, BaseTestGenerator):
@@ -304,7 +304,11 @@ class PgDumpallMaintenanceDatabaseTestCase(BaseTestGenerator):
f.write('[pgadmin_probe]\npassfile={0}\n'.format(
service_passfile))
hostile = 'dbname=postgres passfile={0}'.format(service_passfile)
# The raw (control) value is parsed by libpq as a connection string,
# where a backslash escapes the next character; a Windows path must
# use forward slashes, which libpq accepts, to survive it unchanged.
hostile = 'dbname=postgres passfile={0}'.format(
service_passfile.replace(os.sep, '/'))
database = database_conninfo(hostile) if self.fixed else hostile
dbname, server_env = self._utility_dbname()
env = {k: v for k, v in os.environ.items() if not k.startswith('PG')}