mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2026-10-27 15:10:05 -05:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
062099ae5b | ||
|
|
0e7c33bee2 | ||
|
|
5a082916f8 | ||
|
|
0eac05b450 | ||
|
|
7bbbb53e69 | ||
|
|
eb66e6a347 | ||
|
|
36122bc29b | ||
|
|
6d035dc4fa | ||
|
|
ea17a4e0ff | ||
|
|
a972ea204e |
No files matched your search
@@ -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 ==="
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 \
|
||||
|
||||
@@ -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 ************************************************************
|
||||
|
||||
@@ -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.
|
||||
|
||||
+5
@@ -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]
|
||||
|
||||
+6
-2
@@ -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')}
|
||||
|
||||
Reference in new issue
Block a user