mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2026-10-30 16:20:04 -05:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b44bc029b |
No files matched your search
@@ -335,15 +335,7 @@ 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 ==="
|
||||
# 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
|
||||
xcrun stapler validate "${app}"
|
||||
spctl --assess --type execute --verbose=4 "${app}"
|
||||
|
||||
echo "=== signature ==="
|
||||
|
||||
@@ -50,9 +50,8 @@ on:
|
||||
"registries" is the other half: it skips the download server
|
||||
entirely and runs only the container re-tag and the chart push,
|
||||
so those can be exercised against a candidate tag without a
|
||||
release. It does not upload to PyPI, so that a rehearsal cannot
|
||||
publish a candidate's wheel. "pypi" runs only the PyPI upload,
|
||||
for finishing a promotion whose upload failed.
|
||||
release. It does not upload to PyPI, there being no published
|
||||
wheel to upload.
|
||||
type: choice
|
||||
required: true
|
||||
default: release-directory
|
||||
@@ -61,7 +60,6 @@ on:
|
||||
- files
|
||||
- everything
|
||||
- registries
|
||||
- pypi
|
||||
|
||||
concurrency:
|
||||
group: 'promote-release'
|
||||
@@ -126,7 +124,7 @@ jobs:
|
||||
needs: check
|
||||
# Skipped entirely when only the registries are being exercised, so a
|
||||
# rehearsal of those never touches the download server.
|
||||
if: ${{ inputs.scope != 'registries' && inputs.scope != 'pypi' }}
|
||||
if: ${{ inputs.scope != 'registries' }}
|
||||
runs-on: [self-hosted, publish]
|
||||
environment: publishing
|
||||
|
||||
@@ -312,8 +310,7 @@ jobs:
|
||||
if: >-
|
||||
${{ always()
|
||||
&& needs.check.result == 'success'
|
||||
&& (inputs.scope == 'everything' || inputs.scope == 'registries'
|
||||
|| inputs.scope == 'pypi')
|
||||
&& (inputs.scope == 'everything' || inputs.scope == 'registries')
|
||||
&& (needs.promote.result == 'success'
|
||||
|| needs.promote.result == 'skipped') }}
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -344,7 +341,6 @@ jobs:
|
||||
# acceptance test ran against. imagetools works registry-side, so
|
||||
# nothing is pulled or pushed through this runner.
|
||||
- name: Publish the container
|
||||
if: ${{ inputs.scope != 'pypi' }}
|
||||
env:
|
||||
LOCATION: ${{ inputs.location }}
|
||||
TAGS: ${{ inputs.container-tags }}
|
||||
@@ -386,7 +382,6 @@ jobs:
|
||||
# the image would, oci://docker.io/dpage was an odd home for a chart in
|
||||
# the first place, and it needs no stored credential here.
|
||||
- name: Publish the Helm chart
|
||||
if: ${{ inputs.scope != 'pypi' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_ACTOR: ${{ github.actor }}
|
||||
@@ -398,35 +393,33 @@ jobs:
|
||||
helm push pgadmin4-helm-*.tgz oci://ghcr.io/pgadmin-org
|
||||
helm registry logout ghcr.io
|
||||
|
||||
# Fetched from the candidate rather than rebuilt, so what reaches PyPI
|
||||
# is the file the acceptance test installed. Not from the mirrors: they
|
||||
# sync from the download server hourly, so the release is not there
|
||||
# yet when this runs.
|
||||
# Fetched from the published release rather than rebuilt, so what
|
||||
# reaches PyPI is the file the acceptance test installed.
|
||||
- name: Collect the wheel
|
||||
if: ${{ inputs.scope == 'everything' || inputs.scope == 'pypi' }}
|
||||
if: ${{ inputs.scope == 'everything' }}
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
LOCATION: ${{ inputs.location }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p pypi-dist
|
||||
BASE="https://developer.pgadmin.org/builds/${LOCATION}"
|
||||
BASE="https://ftp.postgresql.org/pub/pgadmin/pgadmin4/v${VERSION}"
|
||||
WHEEL="pgadmin4-${VERSION}-py3-none-any.whl"
|
||||
curl -fsS -o "pypi-dist/${WHEEL}" "${BASE}/${WHEEL}"
|
||||
curl -fsS -o "pypi-dist/${WHEEL}" "${BASE}/pip/${WHEEL}"
|
||||
# Beside the wheel rather than in pypi-dist, because everything in
|
||||
# that directory is uploaded and PyPI has not accepted detached
|
||||
# signatures since 2023.
|
||||
curl -fsS -o "${WHEEL}.asc" "${BASE}/${WHEEL}.asc"
|
||||
curl -fsS -o "${WHEEL}.asc" "${BASE}/pip/${WHEEL}.asc"
|
||||
ls -l pypi-dist
|
||||
|
||||
# The wheel arrives over TLS, which says which server sent it and
|
||||
# nothing about how it got there, and the signature that has been
|
||||
# sitting beside the file all along is the only thing that ties these
|
||||
# bytes back to the signing key. Checking it
|
||||
# The wheel arrives over TLS, which says it came from the mirror and
|
||||
# nothing about what the mirror was given: the tree is replicated from
|
||||
# our download server to machines we do not run, and the signature that
|
||||
# has been sitting beside the file all along is the only thing that ties
|
||||
# these bytes back to the key on the publishing server. Checking it
|
||||
# costs a second and turns the last hop of a release into something an
|
||||
# attacker cannot quietly alter.
|
||||
- name: Verify the wheel before uploading it
|
||||
if: ${{ inputs.scope == 'everything' || inputs.scope == 'pypi' }}
|
||||
if: ${{ inputs.scope == 'everything' }}
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
@@ -448,7 +441,7 @@ jobs:
|
||||
# Trusted publishing: PyPI verifies this workflow's OIDC identity, so
|
||||
# there is no API token to hold, rotate or leak.
|
||||
- name: Upload to PyPI
|
||||
if: ${{ inputs.scope == 'everything' || inputs.scope == 'pypi' }}
|
||||
if: ${{ inputs.scope == 'everything' }}
|
||||
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
||||
with:
|
||||
packages-dir: pypi-dist
|
||||
@@ -90,13 +90,6 @@ 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
|
||||
@@ -192,16 +185,10 @@ 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,21 +141,10 @@ jobs:
|
||||
echo "=== gatekeeper assessment ==="
|
||||
spctl --assess --type execute --verbose=4 "${APP}"
|
||||
echo "=== stapled notarisation ==="
|
||||
# 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}"
|
||||
xcrun stapler validate "${APP}"
|
||||
for F in dist/*.dmg; do
|
||||
echo "--- ${F} ---"
|
||||
validate "${F}"
|
||||
xcrun stapler validate "${F}"
|
||||
done
|
||||
|
||||
- name: Archive the disk image and zip
|
||||
|
||||
+1
-8
@@ -59,15 +59,8 @@ 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 \
|
||||
libpq && \
|
||||
make && \
|
||||
apk add --no-cache --virtual build-deps \
|
||||
build-base \
|
||||
openssl-dev \
|
||||
|
||||
@@ -563,16 +563,9 @@ REM and some of them are optional.
|
||||
|
||||
SETLOCAL EnableDelayedExpansion
|
||||
SET "COMPONENTS="
|
||||
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""
|
||||
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""
|
||||
)
|
||||
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/python``, 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/𝜋thon``, 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.
|
||||
|
||||
+1
-1
@@ -75,4 +75,4 @@ urllib3==2.8.*; python_version > '3.9'
|
||||
user-agents==2.2.0
|
||||
Werkzeug==3.1.*
|
||||
WTForms==3.1.*; python_version <= '3.9'
|
||||
WTForms==3.3.*; python_version > '3.9'
|
||||
WTForms==3.2.*; python_version > '3.9'
|
||||
+1
-1
@@ -144,7 +144,7 @@
|
||||
"resolutions": {
|
||||
"rc-resize-observer": "1.4.0",
|
||||
"@xmldom/xmldom": "^0.8.15",
|
||||
"serialize-javascript": "^7.0.5",
|
||||
"serialize-javascript": "^7.1.3",
|
||||
"ip-address": "^10.3.1",
|
||||
"postcss": "^8.5.31",
|
||||
"ws": "^8.20.1",
|
||||
|
||||
-5
@@ -33,11 +33,6 @@ 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]
|
||||
|
||||
+2
-6
@@ -1558,9 +1558,7 @@
|
||||
"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)",
|
||||
"server_type": "pg",
|
||||
"skip_msg": "EPAS can change a view's columns without dropping it."
|
||||
"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)"
|
||||
},
|
||||
"mocking_required": false,
|
||||
"mock_data": {},
|
||||
@@ -1580,9 +1578,7 @@
|
||||
"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)",
|
||||
"server_type": "pg",
|
||||
"skip_msg": "EPAS can change a view's columns without dropping it."
|
||||
"definition": "SELECT id, valid_from FROM (VALUES (1, now()::date, 'a')) t(id, valid_from, name)"
|
||||
},
|
||||
"mocking_required": false,
|
||||
"mock_data": {},
|
||||
|
||||
@@ -1571,13 +1571,7 @@ class TestGetClosestParentStaysInStorage(_StorageOpsMixin, BaseTestGenerator):
|
||||
scenarios = [('default', dict())]
|
||||
|
||||
def runTest(self):
|
||||
_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('\\', '/')
|
||||
|
||||
closest = Filemanager.get_closest_parent
|
||||
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,13 +43,6 @@ 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,13 +45,6 @@ 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,13 +46,6 @@ 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, fs_short_path
|
||||
from pgadmin.utils import NO_CLIENT_FILE
|
||||
from pgadmin.utils.route import BaseTestGenerator
|
||||
|
||||
import pgadmin.utils.driver.psycopg3 as psycopg3_driver
|
||||
@@ -106,8 +106,7 @@ class ConnectionStringClientFilesTestCase(_StorageMixin, BaseTestGenerator):
|
||||
'sslcert': NO_CLIENT_FILE,
|
||||
'sslkey': NO_CLIENT_FILE})),
|
||||
('Server mode passes on a configured file that does not exist',
|
||||
dict(posix_only=True,
|
||||
params={'sslcert': '/missing.crt', 'sslkey': '/missing.key',
|
||||
dict(params={'sslcert': '/missing.crt', 'sslkey': '/missing.key',
|
||||
'sslrootcert': '/missing-root.crt',
|
||||
'sslcrl': '/missing.crl'},
|
||||
expected={'passfile': NO_CLIENT_FILE,
|
||||
@@ -134,25 +133,15 @@ 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, 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('\\', '/')
|
||||
# directory.
|
||||
value = os.path.join(self.storage, value)
|
||||
self.assertEqual(dsn.get(key), value, key)
|
||||
for key in self.absent:
|
||||
self.assertNotIn(key, dsn)
|
||||
|
||||
@@ -144,18 +144,7 @@ class ConnectionStringKerberosTestCase(BaseTestGenerator):
|
||||
shutil.rmtree(self.storage, ignore_errors=True)
|
||||
|
||||
def runTest(self):
|
||||
# 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:
|
||||
if self.real_parser and not LIBPQ_HAS_REQUIRE_AUTH:
|
||||
self.skipTest('The installed libpq cannot parse require_auth.')
|
||||
|
||||
manager = server_manager.ServerManager.__new__(
|
||||
|
||||
@@ -228,12 +228,6 @@ 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,7 +22,6 @@ 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
|
||||
|
||||
@@ -205,12 +204,8 @@ class TestSSHTunnelIdentityFileConfinement(
|
||||
self.assertNotIn(self.alice, result[1])
|
||||
else:
|
||||
self.assertEqual(result, (True, None))
|
||||
# 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('\\', '/'))
|
||||
self.assertEqual(forwarder.call_args.kwargs['ssh_pkey'],
|
||||
os.path.join(self.alice, 'mykey'))
|
||||
|
||||
|
||||
class TestSSHTunnelRealSSHTunnelForwarder(
|
||||
|
||||
@@ -26,19 +26,12 @@ import flask
|
||||
|
||||
import config
|
||||
from pgadmin.utils import (
|
||||
connection_params_to_env, fs_short_path, get_complete_file_path)
|
||||
connection_params_to_env, 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."""
|
||||
|
||||
@@ -145,7 +138,8 @@ 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(_canon(result), _canon(expected))
|
||||
self.assertEqual(os.path.normpath(result),
|
||||
os.path.normpath(expected))
|
||||
|
||||
|
||||
class CompleteFilePathDesktopTestCase(_StorageMixin, BaseTestGenerator):
|
||||
@@ -269,8 +263,8 @@ class CompleteFilePathRealStorageDirTestCase(_StorageMixin,
|
||||
|
||||
def runTest(self):
|
||||
self.assertEqual(
|
||||
_canon(self._resolve_real('/.pgpass')),
|
||||
_canon(os.path.join(self.alice, '.pgpass')))
|
||||
os.path.normpath(self._resolve_real('/.pgpass')),
|
||||
os.path.join(self.alice, '.pgpass'))
|
||||
|
||||
with self.assertRaises(PermissionError) as cm:
|
||||
self._resolve_real('../bob/.pgpass')
|
||||
@@ -278,8 +272,8 @@ class CompleteFilePathRealStorageDirTestCase(_StorageMixin,
|
||||
str(cm.exception))
|
||||
|
||||
self.assertEqual(
|
||||
_canon(self._resolve_real('Team:/.pgpass')),
|
||||
_canon(os.path.join(self.shared, '.pgpass')))
|
||||
os.path.normpath(self._resolve_real('Team:/.pgpass')),
|
||||
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'))
|
||||
@@ -411,8 +405,8 @@ class CompleteFilePathWriteTargetTestCase(_StorageMixin, BaseTestGenerator):
|
||||
else:
|
||||
result = get_complete_file_path('Team:/keylog', validate=False,
|
||||
for_write=True)
|
||||
self.assertEqual(_canon(result),
|
||||
_canon(os.path.join(self.shared, 'keylog')))
|
||||
self.assertEqual(os.path.normpath(result),
|
||||
os.path.join(self.shared, 'keylog'))
|
||||
|
||||
|
||||
def _user(is_admin):
|
||||
|
||||
@@ -19,7 +19,6 @@ 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
|
||||
@@ -33,11 +32,6 @@ 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}
|
||||
@@ -569,9 +563,6 @@ 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)
|
||||
@@ -635,9 +626,6 @@ 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, fs_short_path, NO_CLIENT_FILE)
|
||||
utility_conninfo, connection_params_to_env, 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', posix_only=True,
|
||||
dict(server_mode=True, database='postgres',
|
||||
params={'sslcert': 'missing.crt'},
|
||||
expected={'dbname': 'postgres',
|
||||
'passfile': NO_CLIENT_FILE,
|
||||
@@ -129,23 +129,8 @@ class UtilityConninfoTestCase(BaseTestGenerator):
|
||||
for p in reversed(patches):
|
||||
p.stop()
|
||||
|
||||
# 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()}
|
||||
expected = {k: v.replace('<storage>', self.storage)
|
||||
for k, v in self.expected.items()}
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
|
||||
|
||||
@@ -28,21 +28,13 @@ from psycopg.conninfo import conninfo_to_dict
|
||||
|
||||
from pgadmin.misc.bgprocess.processes import BatchProcess
|
||||
from pgadmin.utils import (
|
||||
NO_CLIENT_FILE, database_conninfo, fs_short_path,
|
||||
utility_connection_params)
|
||||
NO_CLIENT_FILE, database_conninfo, 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)),
|
||||
@@ -113,7 +105,7 @@ class _CallSiteBase:
|
||||
if not self.server_mode:
|
||||
return {'dbname': DATABASE}
|
||||
return {'dbname': DATABASE,
|
||||
'passfile': _resolved(os.path.join(self.storage, '.pgpass')),
|
||||
'passfile': os.path.join(self.storage, '.pgpass'),
|
||||
'sslcert': NO_CLIENT_FILE, 'sslkey': NO_CLIENT_FILE}
|
||||
|
||||
def _assert_dbname(self, args, expected=None):
|
||||
@@ -207,7 +199,7 @@ class ConnectionParamsSourceTestCase(_CallSiteBase, BaseTestGenerator):
|
||||
lambda: utility_conninfo(
|
||||
'postgres', utility_connection_params(manager, server)))
|
||||
self.assertEqual(conninfo_to_dict(conninfo)['passfile'],
|
||||
_resolved(os.path.join(self.storage, '.pgpass')))
|
||||
os.path.join(self.storage, '.pgpass'))
|
||||
|
||||
|
||||
class RestoreArgsTestCase(_CallSiteBase, BaseTestGenerator):
|
||||
|
||||
@@ -304,11 +304,7 @@ class PgDumpallMaintenanceDatabaseTestCase(BaseTestGenerator):
|
||||
f.write('[pgadmin_probe]\npassfile={0}\n'.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, '/'))
|
||||
hostile = 'dbname=postgres passfile={0}'.format(service_passfile)
|
||||
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