Compare commits

..
145 Commits
Author SHA1 Message Date
Ashesh Vashi 5c10c322d1 chore(deps): bump @mui/material 7 -> 9 and @mui/x-date-pickers 8 -> 9
Supersedes dependabot #9843 and #9888, which must land together
because date-pickers v9 requires @mui/material v9 as a peer.
@mui/material skipped v8 on npm (7.x -> 9.x), so only v9's
breaking changes apply.

## Manifest changes

- @mui/material         ^7.3.10 -> ^9.0.0  (resolved 9.0.1)
- @mui/icons-material   ^7.3.10 -> ^9.0.0  (resolved 9.0.1)
- @mui/x-date-pickers   ^8.28.3 -> ^9.0.2  (resolved 9.2.0)

## Source changes (98 files, mostly mechanical)

1. Two legacy icon imports renamed (v9 removed these in favor of
   the *Outlined variants):

     -import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
     +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutlined';
     -import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
     +import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutlined';

   Both in web/pgadmin/static/js/Explain/AIInsights.jsx.

2. `styled(Box)` -> `styled('div')` across 50 files. v9's `Box`
   stopped filtering CSS-property-named keys (flexDirection,
   marginLeft, etc.) out of the props it forwarded to the DOM, so
   any `styled(Box)(({theme}) => ({ flexDirection: 'column', ... }))`
   leaked those CSS keys to the underlying `<div>` as React props.
   Using `styled('div')` keeps the CSS-in-JS layer working as
   expected and removes the now-redundant Box layer (every affected
   styled wrapper was a pure styling container, not using Box's
   sx-passthrough or component-swap behavior).

3. Box / Stack / Typography / Link / Grid / DialogContentText /
   TimelineContent / TimelineOppositeContent system props moved to
   `sx`. v9 removed individual system-prop syntax from these
   components; `<Box flexGrow={1}>` is now `<Box sx={{flexGrow: 1}}>`.
   Applied via the official codemod:

     npx @mui/codemod@latest v9.0.0/system-props pgadmin

   65 files transformed, 6 Jinja-templated `.js` files skipped
   (parse errors, not real JS; no MUI usage in them).

4. One test updated for v9's class-name split. Button's
   variant+color combo class (`MuiButton-containedPrimary`) is now
   emitted as two separate classes (`MuiButton-contained` +
   `MuiButton-colorPrimary`).

   web/regression/javascript/components/Buttons.spec.js

## Test plan

- [x] `yarn install` resolved cleanly
- [x] `yarn run test:js-once` -> 140/0/0 suites, 824/0/0 tests
- [x] `yarn run linter` -> clean (silent)
- [ ] CI matrix (Linux/Mac/Windows) passes
- [ ] Visual sweep of main UI surfaces in `yarn start`:
        login screen, browser tree, query tool, ERD, schema
        dialogs (table/function/trigger/index), theme switcher
        (light/dark/high-contrast), file manager, debugger panel,
        backup/restore dialogs.

## Notes for reviewers

The major risk surface for this PR is not API breakage (all caught
by jest) but **visual regression**. v9's "sx prop performance" and
"deprecated API cleanup" work shifts a few pixels in places — most
notably ListItemIcon spacing (which v9 changed from hardcoded
`minWidth` to theme spacing). Worth a careful look at menus and
toolbars.

The other Box-related changes (system props -> sx) preserve
behavior 1:1 — they're a pure syntactic migration.

`@mui/x-date-pickers`'s legacy `LocalizationProvider` API was
removed, but we already use the modern `dateAdapter={AdapterDateFns}`
form in `Theme/index.jsx`, so no change needed there.
2026-05-20 17:18:22 +05:30
Ashesh Vashi aad2dfd725 chore: Apply non-breaking dependency updates from open dependabot PRs (#9954)
Python:
- requirements.txt: google-auth-oauthlib 1.3.1 -> 1.4.0
  (#9929 / #9931), gated so Python 3.9 stays on 1.3.1 (1.4.0
  requires python_version >= 3.10). Mirrors the existing
  boto3 1.42.*/1.43.* split.
- tools/requirements.txt: requests >=2.33.1 -> >=2.34.2 on
  python_version > '3.9' (#9943 / #9944).
- web/regression/requirements.txt: selenium 4.43.0 -> 4.44.0
  (#9946). The selenium pin already requires Python >=3.10 in
  master, so the bump introduces no new 3.9 gap.

JavaScript (web/package.json, web/yarn.lock):
- postcss 8.5.12 -> 8.5.14 (#9874 / #9889)
- @tanstack/react-query 5.100.5 -> 5.100.9 (#9878)
- ip-address 10.1.0 -> 10.1.1 (#9918)
- packageManager pin yarn@4.14.0 -> yarn@4.15.0 and regenerate
  yarn.lock at lockfile __metadata.version 10. CI runs yarn
  4.15.0 with hardened mode on public PRs and refuses to migrate
  the lockfile from version 9 (yarn 4.14.x) to 10; master passes
  today only because hardened mode is PR-only.

Electron runtime (runtime/package.json, runtime/yarn.lock):
- axios 1.16.0 -> 1.16.1 (#9948)
- eslint 10.3.0 -> 10.4.0 (#9947)

Skipped (genuine breaking changes, deferred to follow-up PRs):
- @mui/material 7 -> 9 (#9843)
- @mui/x-date-pickers 8 -> 9 (#9888)
- cryptography 47.0.* -> 48.0.* (#9926 / #9932)
- paramiko 3.5.1 -> 5.0.0 (#9927 / #9930)
- electron 41.5.0 -> 42.1.0 (#9945)

Verified in an isolated worktree:

  - jest:        140/0/0 suites, 824/0/0 tests
  - eslint:      clean (web + runtime, both silent)
  - pycodestyle: 0 violations project-wide

Each version was cross-checked against the corresponding
dependabot PR diff via `gh pr diff`. Each Python bump was
cross-checked against PyPI's requires_python so Python 3.9
support stays intact.
2026-05-20 14:53:04 +05:30
Dave Page af888027a4 fix(docker): ship libpq-oauth-18.so and libcurl for PostgreSQL 18 OAuth (#9952)
libpq 18 dlopens libpq-oauth-18.so (the SASL OAUTHBEARER flow plugin)
when connecting to a server with an `oauth` pg_hba.conf rule. The
container previously copied only libpq.so.5.18 from postgres:18-alpine
and omitted both the plugin and its libcurl runtime dependency, so
OAuth connections failed with "no OAuth flows are available (try
installing the libpq-oauth package)" before any token exchange could
begin.

Add libpq-oauth-18.so to the existing pg18-builder COPY (it sits next
to libpq.so.5.18 in /usr/local/lib in postgres:18-alpine) and install
the libcurl apk package so the plugin can dlopen libcurl.so.4 at
runtime.

Closes #9951
2026-05-20 12:39:42 +05:30
Ashesh Vashi 86a8b165ab fix(test): mock does_utility_exist in maintenance accept-valid tests
MaintenanceInputValidationAcceptsValidTest asserted batch_process_mock
was called, but the route short-circuits with success=0 (HTTP 200)
when does_utility_exist() returns a missing-binary error. On Windows
CI psql.exe is not at the path pgAdmin probes, so BatchProcess was
never reached and the five 'accepted' scenarios failed with
'AssertionError: False is not true'. Stub does_utility_exist to None
in this test so it focuses on input-validation acceptance, not on the
runtime PostgreSQL binary layout.
2026-05-06 15:03:18 +05:30
Ashesh Vashi bdbca57332 docs: note Python 3.9 boto3 split in v9.15 release notes
Annotate the boto3 1.42 -> 1.43 dependency line to reflect that 1.43
requires Python >=3.10, so 3.9 stays on the 1.42.* series.
2026-05-06 12:46:58 +05:30
Ashesh Vashi dda6d0e3a8 fix(deps): pin boto3 to 1.42.* on Python 3.9
boto3 1.43.0 requires Python >=3.10, which breaks installs on Python
3.9. Add a python_version gate so 3.9 stays on the 1.42.x series (the
last to support 3.9) while newer interpreters track 1.43.*.
2026-05-06 12:43:59 +05:30
Ashesh Vashi 411d718fa7 docs: add v9.15 release notes to toctree
Sphinx ran with -W (warnings as errors) and failed both build-docs and
build-python-package CI jobs because release_notes_9_15.rst was not
referenced from any toctree.
2026-05-06 12:03:29 +05:30
Ashesh Vashi d17616eda8 docs: round out v9.15 release notes with stragglers
Adds entries for items that landed after the release notes were first
written:

  - Housekeeping: #9906 (Italian translation update, merged from
    origin/master).
  - Test-suite stability: 208541cc4 (ImportExportServersTestCase
    sys.executable + subprocess error surfacing).
  - Documentation (new subsection): 9923eefca (clarification that
    MAX_LOGIN_ATTEMPTS applies only to INTERNAL auth; LDAP / OAuth2 /
    Kerberos / Webserver brute-force protection is the upstream IdP's
    and reverse-proxy's responsibility).
  - Dependencies (new subsection): the cumulative non-breaking
    dependabot updates aggregated for v9.15, organized by Python /
    JavaScript (web/) / JavaScript (runtime/), listing the
    package-level diffs without commit-keyed framing. Covers both
    aggregating commits (d55ffe405 and 4330a688a) so a reader looking
    for "what versions did 9.15 ship with" finds it in one place.

Sphinx build remains clean for release_notes_9_15.rst.
2026-05-05 19:33:16 +05:30
Ashesh Vashi c6a6d46283 Updated version for release v9.15 2026-05-05 19:07:40 +05:30
Ashesh Vashi 4330a688aa chore: Apply non-breaking dependency updates from open dependabot PRs
Python (requirements.txt):
- boto3 1.42.* -> 1.43.* (#9908)
- psycopg 3.3.3 -> 3.3.4 (#9911) for python_version >= '3.10'

JavaScript (web/package.json, web/yarn.lock):
- axios 1.15.2 -> 1.16.0 (matches dependabot's #9907 in /runtime,
  applied to /web for cross-package consistency)

Electron runtime (runtime/package.json, runtime/yarn.lock):
- axios 1.15.2 -> 1.16.0 (#9907)
- electron 41.3.0 -> 41.5.0 (#9910)
- eslint 10.2.1 -> 10.3.0 (#9912)
- globals 17.5.0 -> 17.6.0 (#9909)
  follow-redirects 1.15.11 -> 1.16.0 transitively

Skipped (genuine breaking changes, deferred to a future minor):
- @mui/material 7 -> 9 (#9843)
- @mui/x-date-pickers 8 -> 9 (#9888)

Verified in an isolated worktree:

  - jest:                140/0/0 suites, 824/0/0 tests
  - eslint:              clean (silent)
  - pycodestyle:         0 violations project-wide
  - python regression:   1879/0/308 (PG18, --exclude feature_tests)

The axios 1.16.0 release notes call out three observable changes; only
the first is potentially relevant to pgAdmin and is a bugfix:

  - Fetch adapter now enforces maxBodyLength / maxContentLength (these
    were silently ignored on the fetch adapter before 1.16.0). pgAdmin
    does not set these limits, so behaviour is unchanged.
  - Proxy requests preserve user-supplied Host headers — pgAdmin does
    not proxy through axios.
  - Basic-auth credentials embedded in URLs are URL-decoded — pgAdmin
    does not construct credential-embedded URLs.

psycopg 3.3.4 brings three bugfixes: spurious connection-timeout in C
extension on long-uptime systems, client-side adaptation of enums whose
names need quoting, and consistent Cursor.statusmessage after
executemany().

electron 41.5.0 is a patch within the 41.x line carrying Chromium
security backports plus a Windows frameless-window resize regression
fix and a low-level mouse-hook teardown fix.
2026-05-05 18:47:55 +05:30
Ashesh Vashi 96e6528243 Merge remote-tracking branch 'origin/master' into cve-9.15 2026-05-05 16:22:33 +05:30
Ashesh Vashi ba9ff51aaa docs: assign CVE IDs to v9.15 release-notes placeholders
Replaces the "(CVE pending)" markers on the seven placeholder issues
with their assigned identifiers:

  #9830 -> CVE-2026-7813   (cross-user data access / shared-server escalation)
  #9865 -> CVE-2026-7814   (stored XSS via crafted PostgreSQL object names)
  #9898 -> CVE-2026-7815   (SQL injection in Maintenance tool option values)
  #9899 -> CVE-2026-7816   (OS command injection in Import/Export query export)
  #9900 -> CVE-2026-7817   (LFI/SSRF in LLM API configuration endpoints)
  #9901 -> CVE-2026-7818   (unsafe deserialization in session manager)
  #9902 -> CVE-2026-7819   (symlink path traversal in file manager)
  #9904 -> CVE-2026-7820   (account-lockout bypass via Flask-Security /login)

#9835 is a follow-up to #9830 and shares CVE-2026-7813, so its
parenthetical is dropped rather than replaced with a separate ID.

To be revealed publicly when this branch is pushed for the 9.15 release.
2026-05-05 16:14:02 +05:30
Domenico Sgarbossa 4eb899c92c Updated Italian translation for v9.15 (#9906) 2026-05-05 16:07:08 +05:30
Ashesh Vashi 6469b69565 style: fix pre-existing pycodestyle violations across CVE-9.15 test files
24 violations had accumulated on the cve-9.15 branch from the #9901 and
#9902 CVE-fix work and a couple of older spots. Surfaced when the full
suite was run after the #9904 work; no functional change.

  - 22 x E501 (line too long > 79):
      - 15 in pgadmin/misc/file_manager/tests/test_filemanager_security.py
        (13 class declarations using two mixin parents,
        2 docstrings)
      - 6  in pgadmin/utils/tests/test_session_file_format.py
        (5 class declarations, 1 docstring)
      - 1  in pgadmin/browser/tests/test_kerberos_with_mocking.py
        (extracted self.app.url_map._rules_by_endpoint into a local
        before the membership test)
  - 2  x E305 (expected 2 blank lines after class/function):
      - pgadmin/misc/file_manager/__init__.py (after _open_upload_target)
      - pgadmin/browser/__init__.py (after _first_form_error)

Class declarations are wrapped via parenthesised continuation, the
standard pgAdmin convention; docstrings are either shortened or wrapped
across two lines preserving the same meaning. Verified:

  - pycodestyle clean project-wide (24 -> 0).
  - Affected tests still pass: test_filemanager_security 17/0/0,
    test_session_file_format 18/0/0, test_kerberos_with_mocking 2/0/3
    (skips are pre-existing and unrelated -- Kerberos blueprint not
    loaded in default config).
2026-05-05 15:03:00 +05:30
Ashesh Vashi 208541cc48 fix: ImportExportServersTestCase uses sys.executable + surfaces subprocess errors
Two latent bugs in pgadmin/setup/tests/test_export_import_servers.py
caused this test to fail on macOS / modern Linux distros:

1. The test invoked the subprocesses with a hardcoded "python" via
   os.system. macOS and many modern Linux distros do not provide a
   "python" alias — only "python3" or a venv-specific binary. The
   subprocess fails with "sh: python: command not found", exits non-
   zero, and writes nothing to the dump-servers output file.

   Same root cause that a50a553b0 ("chore: feature tests use
   sys.executable") fixed for AppStarter; this is the same fix in the
   regression test.

2. Both os.system calls redirected stderr to /dev/null. When the
   subprocess failed the empty output file then tripped json.loads
   with the misleading "Expecting value: line 1 column 1 (char 0)"
   instead of surfacing the underlying "command not found" error.

Switch to subprocess.run with a list (so there is no shell quoting and
no command-injection surface), use sys.executable, capture stderr, and
self.fail() with the captured stderr if the subprocess exits non-zero.

Verified: setup.tests goes from 4 pass / 1 fail / 1 skip to 5 pass / 0
fail / 1 skip on PG18.
2026-05-05 15:03:00 +05:30
Ashesh Vashi 9923eefcab docs: clarify scope of MAX_LOGIN_ATTEMPTS and document brute-force protection for LDAP
The "Avoiding a bruteforce attack" section in login.rst implied that
MAX_LOGIN_ATTEMPTS protected all logins; in fact it only applies to the
INTERNAL authentication source (the /authenticate/login view filters by
auth_source=INTERNAL). Operators using LDAP / OAUTH2 / KERBEROS /
WEBSERVER got no signal that brute-force protection lives at a different
layer.

Adds:

  - login.rst: the bruteforce section now states that MAX_LOGIN_ATTEMPTS
    is INTERNAL-only and points operators of external sources to the
    upstream identity provider's lockout policy and to the reverse proxy
    for IP-based throttling.
  - ldap.rst: a new "Brute-force protection" section calling out that
    LDAP credential lockout is the directory's responsibility (ppolicy /
    AD account-lockout GPO) and that request rate-limiting belongs on
    the proxy. Cross-references the login-page section.

No code change. Closes the documentation gap that surfaced during the
analysis of the #9904 lockout-bypass fix without inheriting the
maintenance burden of duplicating directory lockout in pgAdmin.
2026-05-05 15:03:00 +05:30
Ashesh Vashi 32d194691e docs: add release-notes line for #9904 lockout-bypass fix
Mirrors the CVE-pending entries for #9898-#9902 with reporter credit.
The detailed CVE record (CVSS, description, affected files) is held in
the local docs/CVEs/ working draft and submitted to MITRE/Vulnogram
externally; only the release-notes summary lands in git pre-disclosure.
2026-05-05 15:03:00 +05:30
Ashesh Vashi d336c1e786 fix: enforce account lockout on Flask-Security's /login endpoint (#9904)
pgAdmin enforces MAX_LOGIN_ATTEMPTS only inside its own /authenticate/login
view. Flask-Security's default /login view (registered automatically by
security.init_app) was reachable but never consulted the locked field:

  - User inherited Flask-Security's UserMixin.is_locked(), which always
    returns True (= "not locked, proceed").
  - User inherited Flask-Login's is_active, which only checks the active
    column, not locked.

So an attacker with valid credentials could bypass a lockout by posting
to /login after triggering the counter at /authenticate/login.

Override both contracts on the User model so every auth path inherits
them:

  - is_active: False when active=False OR locked=True, so
    flask_security.login_user() refuses to mint a session.
  - is_locked(form_error): returns False (= "locked") and appends the
    same "Your account is locked" message /authenticate/login uses, so
    LoginForm.validate() rejects the submission cleanly.

Only INTERNAL accounts are reachable by the bypass (LDAP/OAuth2/Kerberos/
Webserver users have no local password, which LoginForm.validate rejects
before the locked check) and lockout itself is internal-only (auth_source
filter at authenticate/__init__.py:124), so the overrides only take
effect for INTERNAL accounts.

Adds a unit test covering the four (active, locked) combinations of the
contract LoginForm.validate() depends on.

Also includes a SQLite-only data-cleanup migration. Migration 6650c52670c2
added the locked column with server_default='false'. SQLite has no native
BOOLEAN affinity, so the literal 'false' was stored as TEXT both in the
column DEFAULT and on existing rows backfilled by ALTER TABLE.
SQLAlchemy's Boolean processor then reads that TEXT back as Python True
(because bool('false') is True for any non-empty string), which means
the new is_active override would otherwise see every legacy row as
locked and refuse login. The new migration normalize_locked_text_default
rewrites every SQLite row's locked to integer 0/1 (NULL -> 0); on a
PostgreSQL config DB the column was always stored as a proper BOOLEAN
so the migration is a no-op there (and integer literals would fail
on a BOOLEAN column anyway). SCHEMA_VERSION is bumped from 51 to 52.

Reported-by: Fernando Bortotti <fernando.bortotti@bsd.com.br>
2026-05-05 15:00:19 +05:30
Ashesh Vashi 7f230d05fa docs: add release notes for v9.15
Drafts the user-facing release notes for the 9.15 release. Covers all
47 non-merge commits since REL-9_14:

- 18 issue-linked entries under New features / Housekeeping / Bug fixes,
  with reporter credits (names only) for the six external CVE reports
- #9901 absorbs 14 follow-up commits (session encryption + 0o600,
  SHA-256 digest, drop of live AuthSourceManager / cloud provider
  instances, DATA_DIR perms, log file mode, log handler hardening,
  user_info_server prompt-loop bound) via "Also..."
- #9830 absorbs the @with_object_filters extension to ServerNode.list
- 14 commits without an associated GitHub issue listed under
  "Additional changes" (bug fixes / test stability / refactoring /
  housekeeping) for transparency

CVE IDs are placeholders ("CVE pending") and will be filled in once
MITRE assigns them. Release date and bundled-utility version are
also placeholders pending the actual release.
2026-05-02 12:59:54 +05:30
Ashesh Vashi fcb3db7f54 Merge remote-tracking branch 'origin/master' into cve-9.15 2026-05-02 12:12:20 +05:30
Ashesh Vashi 2f211dc04c Updated message catalogs for v9.15 2026-05-02 12:11:32 +05:30
Ashesh Vashi a11d289bd9 test: harden click_modal backdrop wait and open_query_tool stale-element retry
click_modal: wait up to 5s for the MuiDialog-backdrop to become
invisible after clicking the modal button. MUI v7 leaves the backdrop
in the DOM during the ~300ms close animation, which intercepts the
next click in tests that chain modal->modal interactions.

open_query_tool: the execute-query toolbar button can re-render between
the visibility wait and the ActionChains.move_to_element call
(Firefox/geckodriver hits this regularly). Retry the move up to 3
times on StaleElementReferenceException, refetching the element each
attempt rather than reusing a stale handle.

Both changes are pure test-side stability fixes; no production code
or behavior is affected.
2026-05-02 12:10:51 +05:30
Ashesh Vashi a50a553b0a chore: feature tests use sys.executable; sync yarn.lock to package.json
1. AppStarter.start_app() spawned the pgAdmin subprocess with bare
   "python" via subprocess.Popen. macOS (and many modern Linux
   distros) do not provide a "python" symlink — only "python3" or a
   venv-specific binary. The result was an Errno 2 "No such file or
   directory" the moment a feature test tried to launch pgAdmin.

   Switch to sys.executable so the spawned pgAdmin uses the same
   interpreter and venv as the test runner. Works regardless of how
   the venv is named or whether "python" is on PATH.

2. yarn.lock churn from `yarn install` resolving the @tanstack /
   moment-timezone / postcss peer-dep ranges. Snapshot the resolved
   versions for reproducibility (post the dependency bumps in
   d55ffe405).
2026-05-02 11:02:30 +05:30
Ashesh Vashi e7eaef0dc0 Merge remote-tracking branch 'origin/master' into cve-9.15 2026-05-02 11:00:24 +05:30
Ashesh Vashi 849c679fc6 Updated message catalog post merging few translations 2026-05-02 10:59:22 +05:30
rx500 f43c781e98 Update messages.po (es) (#9893)
Word translation corrected.
2026-05-02 10:56:59 +05:30
Daniel Nylander 119a99375d Update Swedish translation. (#9764)
- Database terminology corrections (bord → tabell, Visa → Vy, etc.)
- Standardize Tabellrymd, Sammansatt utlösare
- Fix punctuation in placeholder lists (Oxford-comma alignment)
- Drop incorrect fuzzy flag from .po header

Note: msgid "on" (from ai_tools.js) is missing here because the PR's
.po was regenerated against a slightly older source tree; it will be
re-added on the next `make messages` run.
2026-05-02 10:53:37 +05:30
MichalBartos ff0fc9e7eb Fix translation for 'Refresh' in Czech (#9832) 2026-05-02 10:37:12 +05:30
Degit22 aec3ea117f Update Russian translation. (#9839) 2026-05-02 10:19:18 +05:30
Ashesh Vashi ddc371adf7 fix: bound user_info_server prompt loops + allow .local in deliverability test
Two related issues, both surfaced when running the regression suite
non-interactively:

1. user_info_server()'s while-not-validate retry loops had no upper
   bound. With a mocked or closed stdin (test mocks, EOF in CI/cron,
   typo'd PGADMIN_SETUP_EMAIL=''), the loop would call input()/pprompt()
   forever, printing 'Invalid email address. Please try again.' on
   every iteration. We saw this manifest as a 13.5 million-line
   25 GB log when test_no_email_deliverability hit the case.

   Cap each loop at MAX_PROMPT_ATTEMPTS=5 and raise RuntimeError with a
   pointer to PGADMIN_SETUP_EMAIL/PASSWORD env vars on exhaustion.

2. test_no_email_deliverability included pg@postgres.local in its
   "should be accepted with deliverability=False" data set. The .local
   suffix is in email_validator.SPECIAL_USE_DOMAIN_NAMES which fails
   syntactic validation regardless of the deliverability flag, so
   validate_email always rejected it - looping forever pre-fix #1.

   Set config.ALLOW_SPECIAL_EMAIL_DOMAINS = ['local'] in the test's
   try/finally block so .local is allowed for this scenario, restored
   afterward to avoid leaking state into other tests.

With both fixes in place, the test can be removed from the regression
runtests --exclude list.
2026-05-02 01:52:29 +05:30
Ashesh Vashi 0fad04de85 fix: PSQL socket tests use authenticated tester; role-deps test skips on auth failure
Two test-infra issues exposed by the full regression suite:

1. PSQL socket tests (test_backend_task, test_psql_input,
   test_resize_terminal, test_socket_disconnect, test_start_process)
   created a fresh, unauthenticated app.test_client() and built a
   socketio test client around it. The /pty connect handler is
   wrapped with @socket_login_required, so the unauthenticated client
   was rejected and is_connected('/pty') returned False.

   Switch to the authenticated self.tester from BaseTestGenerator so
   the connect handler accepts the connection. Matches the pattern
   already used by BaseSocketTestGenerator (test_socket_connect,
   test_psql_disabled).

2. test_role_dependencies_sql creates a temporary LOGIN role and
   calls create_table as that role. On clusters where pg_hba.conf
   does not allow arbitrary roles to connect from 127.0.0.1, the
   create_table swallows the connection error via try/except and
   the subsequent pg_class lookup returns no row, surfacing as an
   opaque "NoneType is not subscriptable" error.

   Detect the empty fetchone() and skipTest with a clear message
   pointing at pg_hba.conf so the test fails gracefully on
   environmentally-restricted clusters instead of erroring.
2026-05-02 01:39:00 +05:30
Ashesh Vashi 1f7194924f fix: harden 6 regression tests against environmental drift
Tests that worked on legacy CI envs but failed under newer
email_validator / non-postgres-named superuser / no-app-context
on tearDown / etc.:

1. test_import_export_create_job_unit_test.py - E-string scenario
   was labelled "Rejected: ... (false positive, safe)" expecting
   parser over-rejection. The parser actually correctly identifies
   the query as balanced (close paren is inside the literal). Update
   expected to True and rename scenario to reflect the real
   handling.

2. test_sg_data_isolation.py - tearDown does ORM query/delete after
   the @create_user_wise_test_client wrapper has popped the test
   client/context. Wrap tearDown's ORM ops in an explicit
   app_context().

3. test_validate_user_email.py - the "with deliverability" scenario
   used postgres@local.dev expecting rejection. Newer email_validator
   no longer rejects this. Switch to postgres@nonexistent.invalid,
   which is RFC-reserved and rejected regardless of DNS state. Also
   align expected message with the actual format string used by
   validate_user.

4-5. test_grant_wizard_save_permissions.py and
   test_grant_wizard_get_sql.py - test data hardcoded 'postgres' /
   'enterprisedb' as grantee/grantor. On clusters where the
   superuser is named differently (e.g. dev Homebrew installs use
   the local OS account), the GRANT statement fails with
   "role X does not exist". Use self.server['username'] instead.

6. test_role_dependencies_sql.py - the test creates a fresh LOGIN
   role and tries to CREATE TABLE as that role; without explicit
   CREATE-on-database grant, the create silently fails and the
   subsequent pg_class lookup returns no row. Mark the test role
   SUPERUSER so it can create the table.

All are test-infra fixes; none of the production code paths under
test were modified.
2026-05-02 01:29:01 +05:30
Ashesh Vashi d78a7cc3bf fix: ConnectionLocker session safety + batch_process tests need request context
Two related issues that surfaced in BatchProcessTest:

1. ConnectionLocker.__enter__ accessed Flask session (via 'in' check)
   AFTER acquiring the lock. When called outside a request context,
   session access raises RuntimeError. Python's 'with' semantics skip
   __exit__ if __enter__ raises, so the lock leaked - any subsequent
   call to ConnectionLocker hung forever waiting on a lock that
   would never be released.

   Wrap session access in try/except RuntimeError so missing-request-
   context falls through cleanly and the lock is released normally
   on with-block exit. Also use .get() chains so partial session
   shapes do not raise KeyError.

2. The four batch_process unit tests (backup, import_export,
   maintenance, restore) used app_context() instead of
   test_request_context(). flask-babel's gettext() in
   BackupMessage.details() and similar code paths require a request
   context; ConnectionLocker.__enter__ also touches session as above.
   Switching to test_request_context('/') gives both the request
   binding they need.

Verified: tools.backup.tests.test_batch_process now runs 4/4 passing
(was 3 ERROR + 1 hang before fix #1; 3 FAIL before fix #2).

Both are pre-existing issues exposed by Python 3.14 / flask 3.1 /
flask-babel stricter context enforcement; not introduced by 9.15
CVE work.
2026-05-02 01:10:33 +05:30
Ashesh Vashi 657529280e fix: guard session['auth_source_manager'] access in BatchProcess.start
processes.py:322 unconditionally indexed session['auth_source_manager']
to gate the KRB5CCNAME copy. When the Flask session does not have the
auth_source_manager key (test request contexts that bypass the login
flow, or sessions whose login hasn't populated this key yet), the
indexing raises KeyError, aborting BatchProcess.start() before it ever
spawns the subprocess. The error has no obvious traceback in the
runtests harness because it bubbles up through BackupCreateJob's mock
infrastructure - the test just records ERROR with no message.

Replace [...] with .get(...) chains so a missing auth_source_manager
yields None and the Kerberos branch is skipped, matching the original
intent.

Surfaced by BatchProcessTest "When backup server" / "When backup
globals" scenarios which mock current_user but leave session in its
default unauthenticated state. Same shape on master; the fix is a
one-line robustness improvement.
2026-05-02 00:54:48 +05:30
Ashesh Vashi d55ffe405b fix: Apply non-breaking dependency updates from open dependabot PRs
Python (requirements.txt, tools/, web/regression/):
- cryptography 46.0 -> 47.0 (move CFB8 import to decrepit module to
  silence the 47.0 deprecation warning and survive 49.0 removal)
- typer 0.24 -> 0.25 for python>3.9 (drop removed [all] extra)
- safety >=1.9.0 -> >=3.7.0 (CI audit tool)
- requests >=2.21.0 -> >=2.33.1
- testtools 2.8.7 -> 2.9.1
- pycodestyle >=2.5.0 -> >=2.14.0

JavaScript (web/package.json, web/yarn.lock):
- postcss 8.5.6 -> 8.5.12
- moment-timezone 0.6.0 -> 0.6.2
- @tanstack/react-query 5.90 -> 5.100.5

Skipped (genuine breaking changes): @mui/material 7->9 (#9843),
@mui/x-date-pickers 8->9 (#9888).
2026-05-02 00:18:47 +05:30
Ashesh Vashi 4eb184739a fix: replace 'e.message' with str(e) and add @with_object_filters to ServerNode.list
Two pre-existing master bugs surfaced by the data-isolation regression
tests:

1. 'e.message' on caught exceptions (13 sites). Exception.message was
   removed in Python 3 (it lived on BaseException in Py2). When the
   except handler runs, accessing e.message raises AttributeError,
   masking the original exception. Werkzeug HTTPException doesn't
   expose .message either - it uses .description / .__str__.

   Replace 'e.message' with str(e) across:
   - browser/server_groups/__init__.py (delete/update/create handlers)
   - browser/server_groups/servers/__init__.py (5 handlers)
   - browser/server_groups/servers/databases/__init__.py
   - misc/cloud/__init__.py
   - tools/debugger/__init__.py
   - tools/grant_wizard/__init__.py (2 sites)

   Sites guarded by hasattr(e, 'message') first (psycopg3 driver) are
   left as-is; module.messages dict access (utils/__init__) is also
   unrelated.

2. ServerNode.list() missing @with_object_filters decorator. PR #8917
   (99b822e47) added object_filters as a required positional arg to
   list() but only added the @with_object_filters decorator to
   get_nodes(), leaving list() with a signature the Flask routing
   couldn't satisfy. Surfaced by SharedServersGetTestCase 'Get a all
   shared server' test, which calls the list endpoint directly.
2026-05-01 23:57:57 +05:30
Ashesh Vashi d57acce354 fix: harden validation/preference/connection-params paths against pre-existing edge cases
Five small defensive fixes that were exposed by running the full
regression suite end-to-end:

1. utils/validation_utils.py: validate_email() now returns False
   instead of raising TypeError when passed a non-str/bytes value
   (e.g. None from a missing form field). Matches the wrapper's
   contract that it only ever returns bool.

2. tools/user_management/__init__.py: list endpoint guarded against
   users with no roles. u.roles[0].id -> u.roles[0].id if u.roles
   else None. Triggered by ChangePasswordTestCase fixtures.

3. utils/preferences.py: control_props['tags'] / ['creatable']
   replaced with .get(...) so preferences whose control_props omit
   these keys do not raise KeyError on update.

4. browser/server_groups/servers/__init__.py (create endpoint):
   convert_connection_parameter() is bidirectional (list<->dict).
   The save path always wants the storage shape (dict). When input
   is already a dict (internal callers / tests mimicking storage
   form), skip conversion to avoid the dict->list round-trip that
   breaks the MutableDict column. Same fix applied to the workspaces
   save path.

5. misc/workspaces/__init__.py: same defensive handling for
   convert_connection_parameter() on the save path.

These are all pre-existing master bugs surfaced by edge-case test
data; none are introduced by the 9.15 CVE work.
2026-05-01 23:29:43 +05:30
Ashesh Vashi dc61039e93 fix: quote username in views/mview test helper for dotted local roles
Same dot-username bug as 504775de8 / 9b29bc203 but in a different shape:
view_test_data.json query strings are Python expressions interpolating
server['username'] into ALTER TABLE ... OWNER TO and GRANT ... TO
clauses. views/tests/utils.py:create_view evaluates these expressions,
producing SQL with an unquoted identifier. A local PG role with a dot
(e.g. 'ashesh.vashi') was rejected with 'syntax error at or near "."'.

Substitute server['username'] with Driver.qtIdent(None, server['username'])
in the query template before evaluating, so the resulting SQL contains
the properly-quoted identifier. The substitution covers every
occurrence in the template (some queries have OWNER TO and GRANT TO in
the same string).

Accounts for the remaining 72 'syntax error at or near "."' cases that
504775de8 + 9b29bc203 did not reach.
2026-05-01 23:09:09 +05:30
Ashesh Vashi 9b29bc2033 fix: quote username in types/compound_triggers test helpers for dotted local roles
Two more test setUp helpers had the same dot-username bug as
user_mappings (504775de8): server['username'] was substituted into
the OWNER TO clause unquoted, so a local PG role with a dot (e.g.
'ashesh.vashi') was rejected with 'syntax error at or near "."'.

- types/tests/utils.py - 'ALTER TYPE ... OWNER TO %s' now uses
  Driver.qtIdent() for the username.
- compound_triggers/tests/utils.py - sql_query template substitution
  now passes the qtIdent-quoted username.

Same Driver.qtIdent(None, server['username']) approach as in 504775de8.
2026-05-01 23:00:48 +05:30
Ashesh Vashi 504775de80 fix: quote username in user_mappings test helper for dotted local roles
The setUp helper in user_mappings/tests/utils.py interpolated
server['username'] directly into the CREATE USER MAPPING DDL, so a
local PostgreSQL role containing a dot (e.g. 'ashesh.vashi') was
parsed by PG as a schema-qualified identifier and rejected with
'syntax error at or near "."'. This blocked every test whose setUp
created a user mapping (UserMappingGetSQLTestCase, etc.).

Wrap the FOR target with Driver.qtIdent() so the identifier is
double-quoted when it contains special characters. Mirrors the
approach used by the resql framework's _normalize_owner() / <OWNER>
substitution introduced in d112dc3b9 - that fix covered SQL
expectation comparison; this fix covers test setUp DDL generation,
which the resql logic does not reach.

The OPTIONS user/password are left as raw '%s' since they are SQL
string literals (single-quoted), where dots are syntactically safe.

Verified: 33/33 user_mappings tests pass; zero 'syntax error at or
near "."' occurrences in the targeted run.
2026-05-01 22:54:09 +05:30
Ashesh Vashi 044355c5e0 fix: harden EnhancedRotatingFileHandler._open and add regression tests
Found by aggressive review of 83abb1c4f:

1. fd leak on os.fdopen failure — os.open returns a raw fd; if
   os.fdopen() raises after, the fd is orphaned. Wrap in try/except
   and close on failure. Built-in open() doesn't have this issue
   because it manages the fd internally.

2. Silent regression of close-on-exec — Python's built-in open() sets
   fds non-inheritable by default since PEP 446 (3.4+). os.open()
   does not. Replacing the default open path therefore made the log
   fd inheritable across fork/exec — mostly harmless in pgAdmin's
   subprocess paths (they default close_fds=True) but still a
   silent behavioral diff. OR in O_CLOEXEC to match.

Also adds web/pgadmin/utils/tests/test_enhanced_log_rotation.py
covering: new file mode, rotated archive + new active file mode,
pre-existing file mode unchanged, and fd non-inheritable. Skipped on
Windows where POSIX mode bits and PEP 446 semantics differ. Verified
all 4 cases pass via web/regression/runtests.py --pkg utils.
2026-05-01 16:38:49 +05:30
Ashesh Vashi 85366aa128 fix: create pgadmin4.log with mode 0o600
Override EnhancedRotatingFileHandler._open() so the active log file
and rotated backups are created owner-only on POSIX. The parent
DATA_DIR is already 0o700, so this is defense-in-depth — but the
log file can contain sensitive context (auth events, query
fragments, paths) and should not be world/group readable on its own.

Pre-existing log files keep their current permissions; new installs
and rotated files pick up the tighter mode. Windows uses the default
open path since os.open mode is ignored there.
2026-05-01 16:38:49 +05:30
Ashesh Vashi 6f4f28def7 refactor: factor wtforms-error-to-JSON into helper, drop dead import
Two endpoints (change_password, forgot_password) had previously been
patched ad-hoc to handle wtforms validators that emit Babel
LazyString (LazyProxy) error messages — those instances report as
iterable, so json.dumps fails with "Circular reference detected" if
they reach the encoder. Both fixes lived inline with slightly
different shapes and a shared trap waiting to bite the next caller.

Extract `_first_form_error_message(form, default=None)`: walks
form.errors, force-resolves the first LazyString to plain str, returns
the default when the form has no errors. Use it from both endpoints.

Also drop the now-dead `from flask_security.views import
default_render_json` import — the only callsite in browser/__init__.py
was replaced when that function's signature drifted; the import was
left behind.

No behavior change beyond consolidating the two ad-hoc patterns; both
endpoint test suites still pass.
2026-05-01 16:38:49 +05:30
Ashesh Vashi fb9ce563fb fix: tighten DATA_DIR file/dir permissions at creation
Two related hardening changes around the pgAdmin data directory:

1. Atomic 0o600 for pgadmin4.db
   ----------------------------
   pgadmin/__init__.py:run_migration_for_sqlite() and
   setup.py:setup_db()'s run_migration_for_sqlite() previously chmod'd
   pgadmin4.db to 0o600 *after* SQLAlchemy/SQLite created it via
   db_upgrade(). With a typical 0o022 umask, the file existed at 0o644
   between SQLite's create and pgAdmin's chmod — a TOCTOU window.
   Practically narrow because the parent dir is 0o700, but easy to
   close: wrap the migration call in `os.umask(0o077)` so the file is
   born 0o600. Both code paths use try/finally so the prior umask is
   restored even on migration failure (a raise during db_upgrade no
   longer leaves the rest of the worker running with 0o077 in effect).
   The post-hoc chmod is kept as belt-and-suspenders for the case where
   the file already existed at a wider mode from an older install.

2. 0o700 for sensitive DATA_DIR subdirectories
   --------------------------------------------
   setup/data_directory.py previously chmod'd only SESSION_DB_PATH and
   the parent dir of SQLITE_PATH to 0o700. STORAGE_DIR (user uploads
   including saved cloud-deployment certs/keys), AZURE_CREDENTIAL_CACHE
   _DIR (MSAL token cache files — real Azure credentials), KERBEROS_
   CCACHE_DIR (Kerberos credential caches), and the directory holding
   pgadmin4.log (stack traces frequently capture sensitive context)
   were left at umask-default (typically 0o755 — directory listing
   readable by any local user).

   Refactor the create-loop to track which directories were newly
   created on this invocation, then apply chmod 0o700 uniformly to all
   sensitive dirs at once. Errors (e.g., chmod on a mounted volume in
   OpenShift) emit a WARNING but don't abort — same lenient pattern the
   existing SQLITE_PATH-dir chmod already used.

   pgadmin4.log itself is still 0o644 (umask-default) because Python's
   logging.FileHandler doesn't take a mode argument; tracked as a
   follow-up task to subclass the handler.
2026-05-01 16:38:49 +05:30
Ashesh Vashi 3b36dd3964 fix: encrypt session-file body (Fernet) for confidentiality at rest
The session file contains OAuth access/refresh tokens, AWS / Google /
Azure / BigAnimal cloud credentials, the Kerberos cache path, MFA OTP
material, and pass_enc_key — the symmetric KEK that decrypts the user's
saved Postgres server passwords. The HMAC header introduced earlier in
this branch protects integrity but not confidentiality: a leak of
sessions/<sid> alone exposes every secret in plaintext.

Wrap the pickle body in Fernet (AES-128-CBC + HMAC-SHA256, AEAD) before
the on-disk HMAC computation — encrypt-then-MAC, so pickle.loads is
unreachable on the read path until both the file HMAC verifies and
Fernet authenticates the ciphertext.

File format becomes:

    +----------------------------------------------------------+
    | _HMAC_HEX_LEN bytes : hex HMAC over the ciphertext      |
    +----------------------------------------------------------+
    | N bytes : Fernet(pickle((randval, digest, data)))       |
    +----------------------------------------------------------+

Fernet key is derived from SECRET_KEY via HKDF-SHA256 with a fixed,
versioned salt and info string (`pga-session-body-v1`,
`pgadmin session body encryption v1`) so multiple workers produce the
same key deterministically and a future format swap can derive a fresh
key without reusing bytes already in flight on disk.

Caveat (not theoretical): SECRET_KEY currently lives in pgadmin4.db in
the same DATA_DIR. A leak that includes BOTH sessions/ AND pgadmin4.db
recovers the derived Fernet key and decrypts session bodies. Closing
that gap requires moving SECRET_KEY out of DATA_DIR (e.g., into the OS
keychain via USE_OS_SECRET_STORAGE) — tracked as a Layer-2 follow-up.

Backwards compat: pre-Layer-1 session files (HMAC over plain pickle, no
Fernet) pass the HMAC check but raise InvalidToken on Fernet.decrypt.
That's caught and logged as "legacy unencrypted body"; users see a
one-time re-login on upgrade. (The same upgrade also flips
SESSION_DIGEST_METHOD's default from sha1 to sha256 and changes the
file format, so the re-login is unavoidable regardless.)

Three new tests:

* TestSessionBodyIsEncryptedOnDisk: place a sentinel in the session,
  read raw bytes, assert the sentinel does NOT appear on disk.
* TestSessionBodyRejectedWithDifferentSecret: write under SECRET_KEY=A,
  read under SECRET_KEY=B, confirm rejection (rules out hard-coded-key
  bugs).
* TestLegacyHmacOnlyFileRejected: pre-Layer-1 file is rejected with the
  expected "legacy unencrypted body" log message.

Pre-existing tests that built bodies directly (TestCorruptedHmacHeader,
TestCookieHmacMismatchWithValidFile) updated to wrap the body via the
new make_encrypted_body() helper so they exercise the realistic file
shape.
2026-05-01 16:38:49 +05:30
Ashesh Vashi bee80fe943 fix: write session files with mode 0o600 (was umask-default 0644)
$DATA_DIR/sessions/<sid> contains OAuth access_tokens / refresh_tokens
(via session['oauth2_token']), AWS / Google / Azure / BigAnimal cloud
credentials (via the cloud refactors earlier in this branch), the
Kerberos credential cache path, MFA OTP material, and pass_enc_key —
the symmetric KEK that decrypts the user's saved Postgres server
passwords.

The HMAC header added earlier in this branch protects integrity but
not confidentiality: anyone with read access to the file gets the
secrets. Default `open(path, 'wb')` uses the process umask, which on
typical systems leaves files 0o644 (world-readable). Switch
new_session() and put() to a new _open_session_file() helper that
opens with `os.open(... O_WRONLY | O_CREAT | O_TRUNC, 0o600)`, mirroring
the upload helper introduced in PR 1.

The directory itself is already 0o700, so this is defense-in-depth for
container scenarios where the data volume might be mounted under shared
uids, or for misconfigurations of the directory mode.

NB: Existing session files retain their old mode until next write, then
adopt 0o600. Operators who want to forcibly tighten existing files can
chmod the sessions directory recursively post-upgrade.

Adds a positive test asserting both put() and new_session() produce
0o600 files (skipped on Windows where POSIX mode bits are not
meaningful).
2026-05-01 16:38:49 +05:30
Ashesh Vashi ccfbd2c457 fix: SESSION_DIGEST_METHOD default to sha256, follow-up review polish
Default SESSION_DIGEST_METHOD from hashlib.sha1 to hashlib.sha256.
HMAC-SHA1 is still cryptographically acceptable for the cookie's
(sid, randval) signature, but SHA-256 is the modern default and aligns
with the file-HMAC header introduced earlier in this branch. The session
file format already invalidates all existing sessions on upgrade (the
new HMAC header is required), so flipping this default at the same time
is a free hardening rather than an additional break.

Test polish from the post-merge hostile review:

* Tighten the "no unsafe deserializer imported" assertion in the four
  cloud-module test files (RDS, Google, BigAnimal, Azure) to a regex
  anchored at line start with a word boundary, so it catches
  `from pickle import dumps, loads`, `import pickle as p`, and indented
  imports — not just bare `import pickle`.

* test_login.py: drop a sid-rotation assertion that would have given
  false confidence. Flask-Paranoid does NOT rotate the session id on
  login (it binds a `_paranoid_token` to UA+IP and validates per
  request), so an `assertNotEqual(pre_sid, post_sid)` would always fail
  for the wrong reason. Comment the limitation in the test for the next
  reviewer; stronger fixation testing is owed as a follow-up.

* docs/proposals: spec line numbers in §4.2 had drifted ~10 lines from
  the implemented branch (helper extraction, etc.); refresh them and
  the audit-summary table to point at HEAD-of-branch lines. Append a
  §1.6 entry enumerating the residual `pickle.loads` callsites in
  sqleditor / schema_diff / bgprocess that PR 7 / Phase 2 will close,
  so future reviewers see the surface.
2026-05-01 16:38:49 +05:30
Ashesh Vashi 1518b0828d fix: SERVER_MODE python-test path and two endpoint regressions
CI runs the python test suite with SERVER_MODE=False (DESKTOP) which
explicitly skips OAuth2 / LDAP / Kerberos / change-password / forgot-
password tests via runtime guards. Running the suite with
SERVER_MODE=True surfaced two real pgAdmin endpoint regressions and a
batch of stale test fixtures that this commit addresses.

Test-infra fixes (regression/python_test_utils/csrf_test_client.py):

* fetch_csrf was looking for <input id="csrf_token" ...> which the
  React SPA login no longer renders. The token is exposed as
  "csrfToken": "..." in a JSON config block embedded in /login HTML
  (camelCase) and as "csrf_token": "..." in JSON API responses
  (snake_case). Match both, fall back to the legacy hidden-input form.

* login() captured the CSRF from GET /login, but Flask-Paranoid
  regenerates the session on login (anti-fixation), which drops the
  csrf_token from the new session. Subsequent state-changing API calls
  failed with "The CSRF session token is missing." Refresh the token
  from a post-login GET /browser/ when SERVER_MODE is True.

Endpoint regressions (web/pgadmin/browser/__init__.py):

* change_password JSON path: bad_request(list(form.errors.values())[0][0])
  passed a Babel LazyString (LazyProxy) to the JSON encoder, producing
  500 with "Circular reference detected." Force resolution to plain
  str.

* forgot_password JSON path: default_render_json(form, include_user=
  False) used a Flask-Security API that no longer accepts include_user,
  yielding 500 on every JSON POST. Replace with pgAdmin's standard
  make_json_response/bad_request envelope.

Test-fixture maintenance:

* test_change_password.py / browser/tests/utils.py: send JSON to the
  JSON-only change_password endpoint (was sending form data, which the
  endpoint silently ignored); use DELETE /user_management/save/<id>
  for cleanup; update respdata strings to match current pgAdmin output.

* test_login.py, test_gravatar_image_display.py,
  test_ldap_with_mocking.py, test_kerberos_with_mocking.py,
  test_webserver_with_mocking.py: drop the
  'Gravatar image for X' assertion (server-rendered HTML no longer
  exists; React renders the gravatar client-side) and verify successful
  authentication via session._user_id instead.

* test_ldap_login.py: skip when LDAP config is template placeholders
  (no live LDAP server reachable in dev/CI).

* test_kerberos_with_mocking.py: skip when authenticate.kerberos_login
  is not registered (the blueprint loads only when KERBEROS is in
  AUTHENTICATION_SOURCES at app-init time).

* test_reset_password.py: switch to JSON POST against the JSON-only
  forgot_password endpoint; parametrize expected status; drop HTML
  pre-check.

* test_validate_email.py: drop pg@postgres.local (modern email-
  validator rejects RFC 6761 special-use TLDs); switch deliverability
  scenario to .invalid/.test domains for stable DNS.

Net result: 296 tests pass, 0 failures, 12 skipped (LDAP/Kerberos
require infra; 1 pgcrypto scenario; cloud-wizard real-credential
tests).
2026-05-01 16:38:49 +05:30
Ashesh Vashi 93206710f7 fix: drop live Azure instance from session, persist auth state only
The azure cloud-deployment module wrote a live Azure class instance
directly into session['azure']['azure_obj'] (no pickle.dumps -- it
relied on the session backend to serialize anything). That is an
implicit pickle dependency: any session-format change would crash, and
the live class instance carries Azure SDK credential objects with
mutable state.

Add Azure.to_state()/from_state() that round-trip the persistable
fields (tenant_id, session_token, use_interactive_credential,
authentication_record_json, region, subscription_id, availability_zone,
available_capabilities_list, azure_cache_name, azure_cache_location)
through a plain dict. Live SDK objects (_clients, _credentials,
_cli_credentials) are intentionally NOT in to_state -- they're rebuilt
lazily from authentication_record_json on first credential use.

from_state bypasses __init__ (which references current_user.username)
via cls.__new__(cls) so unit tests work without a Flask login context.

Module-level _get_azure_from_session()/_save_azure_to_session() helpers
replace the 18 session['azure']['azure_obj'] sites across 12 endpoints.

Worker-restart UX trade-off: today's behavior pickles the populated
Azure SDK client cache, surviving a worker recycle. After this change
the in-memory cache is gone on restart, but the persisted
authentication_record_json is sufficient for the SDK to silently rebuild
the credential without re-prompting for device code -- the class was
designed for this replay. Verification owed in PR review.

Seven new tests cover: round-trip of persistable fields, helper from
session state, lazy SDK objects after from_state, missing-session
graceful return, defaults for partial state, regression assertion that
no live instance leaks into 'azure_obj', and that the unsafe
deserializer is not used.
2026-05-01 16:38:49 +05:30
Ashesh Vashi 2b0d44a33f fix: drop pickled BigAnimal provider instance from session
The biganimal cloud-deployment module pickled the live BigAnimalProvider
across 15 sites (verify, polling-for-token, projects, providers,
regions, db_types, db_versions, instance_types, volume_types,
volume_properties, deploy). Same pattern, same risk as the other
cloud providers.

Add BigAnimalProvider.to_state()/from_state() and
_get_biganimal_from_session()/_save_biganimal_to_session() helpers.
from_state uses cls.__new__(cls) to bypass __init__'s
get_auth_provider() HTTP call -- the persisted `provider` dict already
holds that data.

Five new tests cover: round-trip preservation, helper construction,
defaults for partial state, missing-session graceful return, and a
regression assertion that pickle is gone.
2026-05-01 16:38:49 +05:30
Ashesh Vashi fffc3cc242 fix: drop pickled Google cloud instance from session
The google cloud-deployment module persisted the live Google class via
pickle.dumps/pickle.loads across 14 sites: the verify_credentials
handler, the OAuth2 callback, and every region/zone/version/instance-
type lookup. Each call paid a pickle round-trip and re-introduced the
deserialization risk inside session storage.

Add Google.to_state()/Google.from_state() that round-trip the
persistable fields (client_config, credentials_json, redirect_url,
project_id, regions, availability_zones, verification flags) through
a plain dict, and rebuild the google.oauth2.credentials.Credentials
SDK object lazily from credentials_json. Module-level
_get_google_from_session() / _save_google_to_session() helpers replace
the per-site pickle dance.

Six new tests cover: round-trip of all persistable fields, credential
rebuild from token dict, helper construction from session state,
helper returning None for missing/empty session, defaults for partial
state, and a regression assertion that pickle is gone from the module.
2026-05-01 16:38:48 +05:30
Ashesh Vashi adbb249604 fix: drop pickled RDS instance from session
verify_credentials previously stored pickle.dumps(rds, -1) under
session['aws']['aws_rds_obj']; subsequent endpoints unpickled it via
pickle.loads. That pickle round-trip was an in-session deserialization
vector and forced the session storage to carry binary blobs.

The RDS class is a thin wrapper over boto3 clients: all of its mutable
state is the credential dict already saved at session['aws']['secret'].
Drop the pickled blob and reconstruct RDS per request via a new
_get_rds_from_session() helper. boto3 client construction is cheap
(microseconds) and clients are cached on the per-request instance.

Also fixes a pre-existing latent bug in verify_credentials where status
was undefined when the cached creds matched the new ones (only the
inside of the cache-miss branch assigned it), now explicitly set to
True for the cache-hit path.

Five new tests cover: full creds, missing session_token, no aws key,
no secret key, and a regression assertion that the cloud.rds module
no longer imports the unsafe deserializer.
2026-05-01 16:38:48 +05:30
Ashesh Vashi 64a232bc85 fix: drop live AuthSourceManager from session, store provider name only
The OAuth2 login redirect flow stashed the live AuthSourceManager
instance in flask.session via session['auth_obj'] = auth_obj, requiring
the session storage to handle arbitrary Python objects. That coupled the
auth flow to the pickle-backed session format and presented a second-
order deserialization vector if any path could influence session
contents.

The only piece of state that genuinely needs to cross the OAuth2
provider redirect is the provider name. Persist that as
session['oauth2_current_client'] (a plain str) and reconstruct a fresh
AuthSourceManager in oauth_authorize from current_app's auth-source
registry. The OAuth2 source's per-instance oauth2_current_client is
restored before login() so client selection still works on the callback.

Defensive: oauth_authorize now redirects with a flash error when the
session-state key is missing (e.g., session expired between login and
callback), instead of KeyError'ing.

Two new scenarios in test_oauth2_with_mocking.py verify (a) post-redirect
session contains the new minimal state and not a live class instance,
and (b) the callback handles missing provider state without 500.
2026-05-01 16:38:48 +05:30
Ashesh Vashi 435752b83c fix: symlink-based path traversal in file_manager (CWE-61/CWE-22) (#9902)
CWE-61 / CWE-22 in file_manager: check_access_permission used
os.path.abspath, which resolves '..' but not symbolic links, while
the subsequent kernel write follows symlinks. An authenticated user
could plant a symlink inside their storage area pointing outside it
and write to any path the pgAdmin process could reach.

Fix: switch to os.path.realpath for both orig_path and in_dir, and
add a new _open_upload_target helper that opens with O_NOFOLLOW
(and mode 0o600) to close the leaf-component TOCTOU between the
access check and the open. Drops the redundant post-write
check_access_permission call in add().

Mode change for uploaded files (0o644 -> 0o600) is intentional
hardening; release notes will call this out.

Tests: 16 file-manager security tests covering realpath enforcement
on all five access-check consumers and O_NOFOLLOW leaf-symlink
rejection.

The shared design proposal lives in docs/proposals/ and was added
in the preceding pickle-RCE commit.

Reported-by: Fernando Bortotti <fernando.bortotti@bsd.com.br>
2026-05-01 16:38:48 +05:30
Ashesh Vashi 30a8903374 fix: pickle deserialization RCE in session manager (CWE-502) (#9901)
CWE-502 in FileBackedSessionManager.get(): pickle.load was called on
the session file before the HMAC integrity check. Any file dropped
in the sessions directory was deserialized unconditionally, allowing
an authenticated user with sessions-dir write access to achieve
OS-level RCE.

Fix: prepend a 64-byte hex SHA-256 HMAC header over the pickle body,
computed with the SECRET_KEY, and verify it via hmac.compare_digest
before any deserialization. Also raises (not asserts) on empty
SECRET_KEY so -O does not strip the check, and narrows the
post-deserialize except clause to surface programming errors instead
of masking them.

Tests: 13 new session-format tests covering round-trip, malicious-
pickle rejection, header tampering, empty/truncated files,
cookie-HMAC mismatch, unsafe sids, empty SECRET_KEY, MFA-shaped data,
and SERVER_MODE=False direct upload.

Includes docs/proposals/2026-04-30-eliminate-rce-and-symlink-escape.md,
the shared design doc covering this fix and the symlink-traversal fix
in the following commit.

Reported-by: Fernando Bortotti <fernando.bortotti@bsd.com.br>
2026-05-01 16:38:48 +05:30
Ashesh Vashi 24485fe964 fix: prevent LFI and SSRF in LLM API configuration endpoints (#9900)
User-supplied api_key_file and api_url preferences fed pgAdmin's LLM
provider clients without validation. An authenticated user could read
arbitrary server-side files (LFI) or coerce pgAdmin into requesting
internal targets such as 169.254.169.254 (SSRF) via the chat path and
model-list endpoints.

- validate_api_key_path() restricts user-supplied paths to the user's
  private storage directory in server mode (covering both old- and
  new-style names) or the home directory in desktop mode; resolves
  symlinks and rejects null bytes. Shared storage is intentionally
  excluded since API keys are per-user secrets.
- _read_api_key_from_file() caps reads at 1024 bytes and enforces a
  printable-ASCII no-whitespace key shape so it cannot be repurposed
  as an arbitrary file reader.
- validate_api_url() enforces config.ALLOWED_LLM_API_URLS by exact
  scheme://host:port match, applied at refresh endpoints, accessor
  fallbacks, and provider client constructors so the chat path is
  also covered. Logs a startup warning if the allowlist is empty.
- Adds test coverage for path validation, URL validation, refresh-
  endpoint rejection paths, and refresh-endpoint happy paths.

Reported-by: j3seer <jasserchebbi@outlook.com>
2026-05-01 16:38:48 +05:30
Ashesh Vashi 13badc62c3 fix: Prevent OS command injection in Import/Export query export (CWE-78) (#9899)
User-supplied input was interpolated directly into a psql \copy
metacommand template without sanitization. An authenticated pgAdmin
user could inject ") TO PROGRAM 'cmd'" to achieve RCE on the pgAdmin
server, or ") TO '/path'" for arbitrary file write, by breaking out
of the \copy (...) context.

Add _is_query_parens_balanced() which tracks parenthesis depth in the
query, modeled on psql's strtokx tokenizer used by parse_slash_copy.
Only single-quoted strings ('...' with '' and \' escaping) and
double-quoted identifiers ("..." with "" escaping) are recognized;
line/block comments and dollar-quoting are deliberately NOT skipped
because psql's \copy parser does not recognize them either, so any
unbalanced ')' inside those constructs must remain visible to the
validator.

Whitelist-validate format, on_error, and log_verbosity which were
also raw-interpolated, and write the normalized lowercase value back
to data so downstream template equality checks (e.g. data.format ==
'csv') match regardless of input case.

Reject queries containing null bytes (which could cause a C-string
truncation mismatch between Python validation and psql execution)
and normalize \r\n / \r / \n to spaces (required for Windows; also
prevents psql metacommand termination).

Type-check 'query' before string operations so malformed payloads
return a clean 400 instead of a 500. Tighten is_query_export gating
to 'is True' in both update_data_for_import_export and the validation
block so a client cannot exploit a truthy-but-not-True value to
bypass one check while satisfying the other.

Add unit tests covering the parens parser (balanced cases, RCE
attempts, escape edge cases) and route-level tests asserting
BatchProcess is not invoked for malicious payloads.

Reported-by: chungkn (Chung Kim), OneMount Group <kimngocchung.k2a@gmail.com>
2026-05-01 16:38:48 +05:30
Ashesh Vashi cf53953d9a fix: prevent SQL injection in Maintenance tool option values (#9898)
The Maintenance tool concatenated four user-supplied JSON fields
(buffer_usage_limit, vacuum_parallel, vacuum_index_cleanup,
reindex_tablespace) directly into the rendered VACUUM/ANALYZE/REINDEX
command, which was passed to psql --command. An authenticated user
with the tools_maintenance permission could break out of the option
syntax and execute arbitrary SQL on the connected server, escalating
to RCE on the database host via COPY ... TO PROGRAM.

Fix:
- Allow-list values server-side before rendering: vacuum_index_cleanup
  in {AUTO, ON, OFF}; vacuum_parallel a non-negative integer up to
  1024; buffer_usage_limit matching ^\d+\s*(kB|MB|GB|TB)$
  (case-insensitive). Reject with HTTP 400 on failure.
- Switch reindex_tablespace from manual double-quote wrapping to the
  qtIdent filter used elsewhere in the same template, which both
  escapes embedded quotes and only quotes when required.
- Type-guard against unhashable values so non-string payloads return
  400 instead of triggering a 500.

Reported-by: j3seer <jasserchebbi@outlook.com>
2026-05-01 16:38:48 +05:30
Ashesh Vashi d112dc3b96 fix: Fixed the resql tests - use <OWNER> placeholder instead of hardcoded 'postgres' role (#9873)
The reverse-engineered SQL tests hardcoded 'postgres' as the role name
in test JSON data and expected SQL files. This fails on systems where
the superuser is not 'postgres' (e.g. 'ashesh.vashi').

Changes:
- Replace hardcoded 'postgres' with '<OWNER>' placeholder in ~400 test
  JSON, SQL, and MSQL files across all browser module tests
- Extend test_resql.py to resolve '<OWNER>' in scenario data sent to
  API endpoints (not just in expected SQL)
- Handle identifier quoting for usernames containing special characters
  (dots, etc.) by normalizing both expected and actual SQL
- Fix test_domain_sql.py to use qtIdent for username in expected SQL
- Split Authlib version pin: 1.6.x for Python <=3.9, 1.7.x for >3.9
2026-04-24 21:18:38 +05:30
Ashesh Vashi b70d4c9857 fix: Bump runtime and development dependencies (#9870)
Python (requirements.txt):
- Authlib 1.6.9 -> 1.7.0
- Flask-Security-Too 5.7.* -> 5.8.* (py>3.9)
- certifi 2026.2.25 -> 2026.4.22
- Flask-WTF 1.2.* -> 1.3.*

Python test (web/regression/requirements.txt):
- selenium 4.41.0 -> 4.43.0
- testscenarios 0.5.0 -> 0.6.1
- testtools 2.8.7 -> 2.9.0

JavaScript web (web/package.json):
- @mui/material ^7.3.7 -> ^7.3.10
- @mui/x-date-pickers ^8.27.2 -> ^8.28.3
- @mui/icons-material ^7.3.6 -> ^7.3.10
- dompurify ^3.3.3 -> ^3.4.1
- axios ^1.13.5 -> ^1.15.2
- react ^19.2.3 -> ^19.2.5
- react-dom ^19.2.3 -> ^19.2.5
- typescript ^5.9.2 -> ^6.0.3
- marked ^17.0.1 -> ^18.0.2
- react-checkbox-tree ^1.7.2 -> ^2.0.1
- eslint ^9.39.2 -> ^9.39.4
- and ~60 more minor/patch bumps across build, test, and runtime deps

JavaScript runtime (runtime/package.json):
- electron 41.2.0 -> 41.3.0
- axios ^1.15.0 -> ^1.15.2
- eslint ^10.2.0 -> ^10.2.1
- globals ^17.4.0 -> ^17.5.0
2026-04-24 14:28:36 +05:30
Ashesh Vashi 3294e74197 fix: prevent XSS via innerHTML in browser tree and explain visualizer (#9865)
Use textContent instead of innerHTML when setting DOM element text
to prevent stored XSS through crafted PostgreSQL object names.
2026-04-23 14:21:04 +05:30
Aditya Toshniwal 79e490c5fa Updated yarn.lock file to use yarn v4.14.0 and fix the builds 2026-04-17 17:13:19 +05:30
Ashesh Vashi e4edcf2253 fix: SharedServer feature parity columns and write guards (#9835)
Add passexec_cmd, passexec_expiration, kerberos_conn, tags, and
post_connection_sql to SharedServer so non-owners get their own
per-user values instead of inheriting the owner's.  Drop the unused
db_res column which was never overlaid or writable by non-owners.

Key changes:
- New Alembic migration (sharedserver_feature_parity) adds 5 columns,
  drops db_res, cleans up orphaned records.  All operations idempotent.
- Overlay copies new fields from SharedServer instead of suppressing
- _owner_only_fields guard blocks non-owners from setting passexec_cmd,
  passexec_expiration, db_res, db_res_type via API
- Non-owners can set post_connection_sql (runs under their own creds)
- update_tags and flag_modified use sharedserver for non-owners
- update() response returns sharedserver tags for non-owners
- ServerManager passexec suppression with config.SERVER_MODE guard
- UI: post_connection_sql editable for non-owners (readonly only when
  connected, not when shared)
- SCHEMA_VERSION bumped to 51
- Comprehensive unit tests for overlay, write guards, and tag deltas
2026-04-13 15:03:31 +05:30
Ashesh Vashi 4ddb16f47a fix: customize container user permissions using PUID and PGID. #9657 (#9833)
Add support for custom container user permissions via PUID and PGID
environment variables. When the container is started as root
(--user root), the pgadmin user is reassigned to the requested UID/GID
and all initialization runs under that user via su-exec, ensuring
files are created with correct ownership from the start.

Key changes:
- Dockerfile: add su-exec package, add chmod g=u for /run/pgadmin
  (fixes OpenShift random UID access)
- entrypoint.sh: add PUID/PGID validation and privilege dropping
  before initialization (not after), preserving OpenShift compatibility

Three modes supported:
- Default (USER 5050): unchanged behavior
- Custom UID (--user root -e PUID=N -e PGID=N): drops to target user
  before any init
- OpenShift (random UID, GID 0): passwd fixup + group permissions
2026-04-13 14:34:18 +05:30
Ashesh Vashi a3a0537277 fix: Bump runtime dependencies and upgrade ESLint to v10 (#9834)
- electron: 41.0.2 → 41.2.0
- eslint: ^9.39.2 → ^10.2.0
- axios: ^1.13.5 → ^1.15.0
- electron-context-menu: ^4.1.0 → ^4.1.2
- Added @eslint/js and globals as explicit devDependencies (required by ESLint 10)
- Fixed no-useless-assignment lint errors in downloader.js and misc.js
2026-04-09 21:31:17 +05:30
Ashesh Vashi 9a76ed80bb fix: enforce data isolation and harden shared servers in server mode (#9830)
pgAdmin 4 in server mode had no data isolation between users — any
authenticated user could access other users' private servers,
background processes, and debugger state by guessing object IDs.
The shared server feature had 21 vulnerabilities including credential
leaks, privilege escalation via passexec_cmd, and owner data
corruption via SQLAlchemy session mutations.

Centralized access control:
- New server_access.py with get_server(), get_server_group(),
  get_user_server_query() replacing ~20 unfiltered queries
- connection_manager() raises ObjectGone (HTTP 410) in server mode
  when access is denied — fixes 155+ unguarded callers
- UserScopedMixin.for_user() on 10 models replaces scattered
  user_id filters

Shared server isolation (all 21 audit issues):
- Expunge server from session before property merge to prevent
  owner data corruption
- Suppress passexec_cmd, post_connection_sql for non-owners in
  merge, API response, and ServerManager
- Override all 6 SSL/passfile connection_params keys from
  SharedServer; strip owner-only keys; sanitize on creation
- _is_non_owner() helper centralises 15+ inline ownership checks
- SharedServer lookup uses (osid, user_id) not name
- Unique constraint on SharedServer(osid, user_id)
- Tunnel/DB password save, change_password, clear_saved_password,
  clear_sshtunnel_password all branch on ownership
- Only owner can unshare (delete_shared_server guard)
- Session restore includes shared servers
- tunnel_port/tunnel_keep_alive copied from owner, not hardcoded

Tool/module hardening:
- All tool endpoints use get_server()
- Debugger function arguments scoped by user_id
- Background processes use Process.for_user()
- Workspace adhoc servers scoped to current user

Migration (schema version 49 -> 50):
- Add user_id to debugger_function_arguments composite PK
- Add indexes on server, sharedserver, servergroup
- Add unique constraint on sharedserver(osid, user_id)
2026-04-09 18:32:59 +05:30
Ashesh Vashi 872d5ac0b3 fix: Bump python & javascript dependencies (#9827)
* Update Python dependencies:
   - google-auth-oauthlib 1.3.0 → 1.3.1
   - sphinxcontrib-youtube 1.4.1 → 1.5.0
   - fixtures 4.3.1 → 4.3.2
   - Add missing newline at end of requirements files

* Update JavaScript dependencies:
   - Replace deprecated @babel/plugin-proposal-* packages with @babel/plugin-transform-* equivalents
   - Remove unused @types/classnames dependency
   - Update yarn.lock
   - Pin the 'react-frame-component' to '~5.2.6'

* fix: Use PostGIS 36 for EPAS 18 in CI workflow
   - EPAS 18 ships with edb-as18-postgis36 instead of postgis34.
   - Add a postgisver matrix variable to support mixed versions.
   - Add exclude+include for EPAS 18 PostGIS matrix to fix empty runs-on

* Moving '@babel/plugin-transform-class-properties', '@babel/preset-react' to devDependencies section
2026-04-08 17:31:56 +05:30
rztrainlocalandrztrainlocal d59fcf3459 fix(9656): Use absolute paths for a2enmod, a2enconf for debain setup script (#9815)
Reason: On debian, it does not have `/usr/sbin` in the path environment variable anymore.

Co-authored-by: rztrainlocal <ke@KE-U758.HOME>
2026-04-06 21:48:19 +05:30
Akshay Joshi d8a078af53 Updated version for release v9.14 2026-03-30 17:44:32 +05:30
Dave PageandClaude Opus 4.6 9bb96360dd Support /v1/responses for OpenAI models. #9795
* Support /v1/responses for OpenAI models. #9795

* Address CodeRabbit review feedback on OpenAI provider.

- Preserve exception chains with 'raise ... from e' in all
  exception handlers for better debugging tracebacks.
- Use f-string !s conversion instead of str() calls.
- Extract duplicated max_tokens error handling into a shared
  _raise_max_tokens_error() helper method.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Validate api_url and use incomplete_details from Responses API.

- Strip known endpoint suffixes (/chat/completions, /responses) from
  api_url in __init__ to prevent doubled paths if a user provides a
  full endpoint URL instead of a base URL.
- Use incomplete_details.reason from the Responses API to properly
  distinguish between max_output_tokens and content_filter when the
  response status is 'incomplete', in both the non-streaming and
  streaming parsers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:16:22 +05:30
Libor M. 2228575564 Czech translation for version 9.14 2026-03-30 12:43:22 +05:30
Khushboo Vashi 5dd0de9e0f Revert "Update JavaScript dependencies."
This reverts commit 92bb092718.
2026-03-30 12:29:48 +05:30
Khushboo Vashi aa3dc389e4 Update Release Notes. 2026-03-27 14:27:15 +05:30
Akshay Joshi 92bb092718 Update JavaScript dependencies. 2026-03-27 13:23:18 +05:30
Anil Sahoo 3598840203 Fixed an issue where ALT+F5 for executing a query in the Query Tool shows a crosshair cursor icon for rectangular selection. #9570 2026-03-27 12:59:35 +05:30
Domenico Sgarbossa 792441ab95 Updated Italian translation for v9.14. 2026-03-27 11:30:59 +05:30
Anil Sahoo 2c626d1181 Fixed Geometry Viewer not auto-updating on first query execution after View/Edit Data to Query Tool promotion. #9392 2026-03-27 11:23:47 +05:30
Pravesh Sharma 278e812293 Fixed an issue where the default fillfactor value for B-tree indexes was incorrect. #9648 2026-03-27 11:20:40 +05:30
Pravesh Sharma 1d51a1a943 Fixed an issue where the Query tool kept prompting for a password when using a shared server. #9789 2026-03-27 11:03:15 +05:30
Dave Page bf649420b7 Fix a couple of related issues in the Query Tool layout.
- Hide the AI Assistant tab if AI is disabled or unconfigured. #9696
- Ensure the AI Assistant tab is not the first one shown.
2026-03-27 11:01:42 +05:30
Anil Sahoo 1c93f93a6a Fixed Python & feature test failures caused by Werkzeug 3.1.7 rejecting empty Host header in CSRF token generation. 2026-03-26 17:59:58 +05:30
Akshay Joshi 3ba887001a Updated message catalogs for v9.14 2026-03-26 17:06:45 +05:30
Akshay Joshi 257654849b Updated Javascript and Python dependencies. 2026-03-17 12:12:00 +05:30
Dave Page d7f462c22c Clarify where the LLM API key files should be. #9758 2026-03-17 11:44:17 +05:30
Dave Page 6296016b67 Fix an issue where LLM responses are not streamed or rendered properly in the AI Assistant. #9734
* Address CodeRabbit review feedback for streaming and SQL extraction.

- Anthropic: preserve separators between text blocks in streaming to
  match _parse_response() behavior.
- Docker: validate that the API URL points to a loopback address to
  constrain the request surface.
- Docker/OpenAI: raise LLMClientError on empty streams instead of
  yielding blank LLMResponse objects, matching non-streaming behavior.
- SQL extraction: strip trailing semicolons before joining blocks to
  avoid double semicolons in output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address remaining CodeRabbit review feedback for streaming and rendering.

- Use distinct 3-tuple ('complete', text, messages) for completion events
  to avoid ambiguity with ('tool_use', [...]) 2-tuples in chat streaming.
- Pass conversation history from request into chat_with_database_stream()
  so follow-up NLQ turns retain context.
- Add re.IGNORECASE to SQL fence regex for case-insensitive matching.
- Render MarkdownContent as block element instead of span to avoid
  invalid DOM when response contains paragraphs, lists, or tables.
- Keep stop notice as a separate message instead of appending to partial
  markdown, preventing it from being swallowed by open code fences.
- Snapshot streamingIdRef before setMessages in error handler to avoid
  race condition where ref is cleared before React executes the updater.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address CodeRabbit review feedback for streaming providers and history.

- Fix critical NameError: use self._api_url instead of undefined API_URL
  in anthropic and openai streaming _process_stream() methods.
- Match sync path auth handling: conditionally set API key headers in
  streaming paths for both anthropic and openai providers.
- Remove unconditional temperature from openai streaming payload to
  match sync path compatibility approach.
- Add URL scheme validation to OllamaClient.__init__ to prevent unsafe
  local/resource access via non-http schemes.
- Guard ollama streaming finalizer: raise error when stream drops
  without a done frame and no content was received.
- Update chat.py type hint and docstring for 3-tuple completion event.
- Serialize and return filtered conversation history in the complete
  SSE event so the client can round-trip it on follow-up turns.
- Store and send conversation history from NLQChatPanel, clear on
  conversation reset.
- Fix JSON-fallback SQL render path: clear content when SQL was
  extracted without fenced blocks so ChatMessage uses sql-only renderer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix missing closing brace in NLQChatPanel switch statement.

Adding block scoping to the error case introduced an unmatched brace
that prevented the switch statement from closing properly, causing
an eslint parse error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix missing compaction module and SQL extraction test.

- Replace compaction module imports with inline history deserialization
  and filtering since compaction.py is on a different branch.
- Add rstrip(';') to SQL extraction test to match production code,
  fixing double-semicolon assertion failure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix SQL extraction test expected values after rstrip(';') change.

The rstrip(';') applied to each block before joining means single
blocks and the last block in multi-block joins no longer have
trailing semicolons. Update expected values to match.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Strictly guard Ollama stream: raise if no terminal done frame received.

Truncated content from a dropped connection should not be treated as
a complete response, even if partial text was streamed. Always raise
when final_data is None, matching CodeRabbit's recommendation.
2026-03-17 11:41:57 +05:30
Akshay Joshi e3472103bb Updated release note. 2026-03-16 19:20:18 +05:30
Dave Page 990b4f5e54 Fix an issue where the AI Assistant was not retaining conversation context between messages, with chat history compaction to manage token budgets.
* Address CodeRabbit review feedback for chat context and compaction.

- Track tool-use turns as groups instead of one-to-one pairs, so
  multi-tool assistant messages don't leave orphaned results.
- Add fallback to shrink the recent window when protected messages
  alone exceed the token budget, preventing compaction no-ops.
- Fix low-value test fixtures to keep transient messages short so
  they actually classify as low-importance.
- Guard Clear button against in-flight stream race conditions by
  adding a clearedRef flag and cancelling active streams.
- Assert that conversation history is actually passed through to
  chat_with_database in the "With History" test.

* Address remaining CodeRabbit review feedback for compaction module.

- Expand protected set to cover full tool groups, preventing orphaned
  tool call/result messages when a turn straddles the recent window.
- Add input validation in deserialize_history() for non-list/non-dict data.
- Strengthen test assertion for preserved recent window tail.


* Fix CI test failures in compaction and NLQ chat tests.

- Lower max_tokens budget in test_drops_low_value to reliably force
  compaction (500 was borderline, use 200).
- Consume SSE response data before asserting mock calls in NLQ chat
  test, since Flask's streaming generator only executes on iteration.


* Clarify mock patch target in NLQ chat test.

Add comment explaining why we patch the source module rather than the
use site: the endpoint uses a local import inside the function body,
so there is no module-level binding to patch.
2026-03-16 19:02:36 +05:30
Rohit Bhati 74b1a3bcc4 Fixed Process Watcher garbled text on Windows with non-UTF-8 locales. #9457 2026-03-16 19:00:41 +05:30
Anil Sahoo 01c2d12b51 Fixed an issue where selecting all in the Query Tool's Messages tab would select the entire page content. #8992 2026-03-16 14:39:42 +05:30
Dave Page e5cf456fae Fixed an issue where AI Reports are grayed out after setting an API key by auto-selecting the default provider. #9694
* Don't let auto-selection override an explicit default_provider choice.

If the same save payload includes a default_provider update (including
setting it to empty/disabled), skip the auto-selection logic so the
user's explicit choice is respected.
2026-03-16 14:24:09 +05:30
Dave Page da55da4d32 Replace misleading AI thinking messages with fun elephant-themed ones. #9702
The previous messages like "Vacuuming the catalog..." and "Analyzing
table statistics..." could be mistaken for actual database operations.
Replace them with clearly whimsical elephant-themed messages, expand
the pool to 32 messages, and consolidate them into a single shared
module with gettext() support.
2026-03-16 14:23:05 +05:30
Akshay Joshi b301bdcecd Updated release note 2026-03-16 12:43:29 +05:30
Dave Page 23bd3359c1 Fixed an issue where AI features are visible in the UI even when LLM_ENABLED is set to False. 2026-03-16 12:35:16 +05:30
Dave Page 0bc4edbab5 Improve the AI Assistant user prompt to be more descriptive of the actual functionality. 2026-03-16 12:34:44 +05:30
Dave Page 34aa360830 Allow copying of text from the AI Assistant chat panel. #9738 2026-03-16 12:16:45 +05:30
Dave PageandClaude Opus 4.6 7e39fc8477 Fix flakey SchemaDialogView 'change text' JS test
Add a wait for the FormView autofocus timer (200ms) to complete before
typing, preventing a race condition where the autofocus moves focus away
from the target field on slow CI machines. This matches the pattern
already used by simulateValidData in the same test file.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 12:14:10 +05:30
Dave PageandClaude Opus 4.6 8f74b2e6d4 Fixed an issue where AI Reports fail with OpenAI models that do not support the temperature parameter. #9719
Removed the temperature parameter from all LLM provider clients and
pipeline calls, allowing each model to use its default. This fixes
compatibility with GPT-5-mini/nano and future models that don't
support user-configurable temperature.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 12:13:04 +05:30
Dave PageandClaude Opus 4.6 c8cb744ffd Fix model tool prompt
* Fix NLQ system prompt to work with models that prioritize text instructions over tool calls.

The previous prompt told the model to "Return ONLY the JSON object, nothing else"
while also providing tool definitions. Models like Qwen 3.5 would follow the text
instruction and never use tools. The updated prompt clearly separates the tool-use
phase from the final JSON response phase, and explicitly instructs the model to
call tools directly rather than describing them in text.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update release notes for NLQ prompt fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix issue number in release notes for NLQ prompt fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 12:10:02 +05:30
Dave PageandClaude Opus 4.6 b3aa78c0a5 Added support for custom LLM provider URLs for OpenAI and Anthropic, allowing use of OpenAI-compatible providers such as LM Studio, EXO, and LiteLLM. #9703
- Add configurable API URL fields for OpenAI and Anthropic providers
- Make API keys optional when using custom URLs (for local providers)
- Auto-clear model dropdown when provider settings change
- Refresh button uses current unsaved form values
- Update documentation and release notes

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 12:08:55 +05:30
Dave Page 78e3b67eb1 Fix an issue where the AI Assistant input textbox sometimes swallows the first character of input. #9740 2026-03-16 12:06:47 +05:30
Muhammad Aqeel c8bd75c9a8 Fix apparmor_parser not found error in desktop postinst script (#9728)
Add a runtime guard in the postinst so apparmor_parser is only called
  when available. Previously, packages built on Ubuntu 24+ would fail to
  install on headless servers or systems without AppArmor tools. A warning
  is printed when the profile load is skipped to aid debugging.
2026-03-10 15:29:40 +00:00
Akshay Joshi a0e6da0772 1) Updated JavaScripts and Python dependecies.
2) Updated release note.
2026-03-10 13:41:19 +05:30
Rohit Bhati 8ca765478f Fixed an issue where permissions page is not completely accessible on full scroll. #9721 2026-03-10 12:59:42 +05:30
Pravesh Sharma f49c967bfd Added support to download binary data from result grid. #4011 2026-03-10 12:25:21 +05:30
Khushboo Vashi 0a539c32d9 Fixed an issue where OAuth2 authentication fails with 'object has no attribute' if OAUTH2_AUTO_CREATE_USER is False. #9279 2026-03-10 12:24:55 +05:30
Anil Sahoo 395ff36322 Ensure that the Geometry Viewer refreshes when re-running queries or switching geometry columns, preventing stale data from being displayed. #9392 2026-03-09 16:08:14 +05:30
Florian 00a44a5d76 fix TypeError Exception in session.py
Signed-off-by: Florian Brandes <florian.brandes@posteo.de>
2026-03-09 12:21:36 +05:30
Daniel NylanderandDaniel Nylander 3ecdc57a59 i18n: Complete Swedish translation (3625/3625 strings)
- Translate all 357 previously untranslated strings
- Fix 1 duplicate entry ('on' with empty msgstr)
- Fix format string mismatch (%(provider)s)
- Total: 3625 translated strings (100% coverage)

Co-authored-by: Daniel Nylander <daniel@danielnylander.se>
2026-03-09 12:21:18 +05:30
Akshay Joshi 08b0f6fe7b 1) Updated Python and Javascripts dependencies.
2) Created new release note files for v9.14
2026-03-05 17:56:33 +05:30
Khushboo Vashi 6d18741f1a Ensure the Kerberos mock test handles OS errors correctly. 2026-03-05 17:44:12 +05:30
Akshay Joshi dc47bf48f7 Remove PG/EPAS 13 from the github runners. 2026-03-05 16:52:41 +05:30
Khushboo Vashi 5b231ddd3f Revert "Customize container user permissions using PUID and PGID. #9657"
This reverts commit 97c90f1e69.
2026-03-02 18:05:26 +05:30
Akshay Joshi 3e48c21fe5 Remove #9657 from release notes due to revert. 2026-03-02 17:06:53 +05:30
Akshay Joshi 4cc74c13f7 Updated version for release v9.13 2026-03-02 14:11:36 +05:30
Akshay Joshi 2cf5d39a01 Fixed one translation issue. 2026-03-02 14:09:25 +05:30
Akshay Joshi f4112c9a0a Updated yarn.lock 2026-03-02 14:08:49 +05:30
Domenico Sgarbossa 6f69120af5 Updated Italian translation for v9.13 2026-03-02 11:48:06 +05:30
Libor M. cf6d7f3b89 Czech translation for version 9.13. 2026-03-02 11:38:56 +05:30
Aditya Toshniwal b78fc884e3 Fixed an issue in file dialog where rename was not working. #9651 2026-02-27 18:24:54 +05:30
Rohit Bhati caafc6bb1b Fix broken checkbox selection in backup dialog objects tree. #9649 2026-02-27 17:01:11 +05:30
Akshay Joshi e9e52905cc Updated message catalogs for v9.13 2026-02-26 18:56:11 +05:30
Akshay Joshi cae00a1dc6 1) Fixed one issue in session.py
2) Updated release note.
2026-02-25 14:26:30 +05:30
Pravesh Sharma 5dd63ca16b Fixed an issue where modifying a shared server incorrectly updated the original server details. #9258 2026-02-25 11:22:44 +05:30
kon-foo 97c90f1e69 Customize container user permissions using PUID and PGID. #9657 2026-02-24 16:49:43 +05:30
Khushboo Vashi bcd48a92b9 Load predefined users from a JSON file through command line. #9229 2026-02-24 16:46:31 +05:30
Lance J.andChristian P. 6d0d387f53 ERD Tool: Insert table with relations via drag-and-drop. #5578 #8198
* Add preference for insert with relations

Co-authored-by: Christian P. <pirnichristian@gmail.com>

* Insert tables with relations on drag and drop

Co-authored-by: Christian P. <pirnichristian@gmail.com>

* Fix test mock not returning Erd Supported Data

Co-authored-by: Christian P. <pirnichristian@gmail.com>

---------

Co-authored-by: Christian P. <pirnichristian@gmail.com>
2026-02-24 12:15:54 +05:30
Rohit Bhati 3995ba9a95 Add support for 'ONLY' in Index creation dialog. #6386 2026-02-24 12:14:36 +05:30
Akshay Joshi 4bb6b72377 Updated Python and JavaScript dependencies. 2026-02-24 11:54:19 +05:30
balodis 080fcc1cfb Fix column ordering in unique/primary key constraint properties
The fromRaw formatter for the Columns field in unique constraint and
primary key properties used _.filter(allOptions, ...), which preserved
the order of allOptions (table column position) rather than the
constraint-defined column order from backendVal. Replaced with _.find
mapped over backendVal to preserve the correct constraint column order.

Added unit tests for cell and type formatter functions to verify
column ordering is preserved.
2026-02-23 17:11:38 +05:30
Dave Page 93e2f23a48 Update the release notes to properly describe the new AI features. 2026-02-23 15:07:36 +05:30
Akshay Joshi 965a27db39 typer[all]==0.24.* drop support for Python 3.9. Added conditional requirement. 2026-02-18 17:41:13 +05:30
Akshay Joshi 7b377ff3da Pin ESLint to 9.39.2 as 10.0.0 not working. 2026-02-18 17:19:05 +05:30
Akshay Joshi 2dedb6e69a Updated Python and Javascript dependencies. 2026-02-18 13:02:46 +05:30
kon-foo b15f15960f Fix misleading error when _FILE secret is not readable. 2026-02-18 12:10:23 +05:30
Khushboo Vashi 50f78bf210 Fix translation compilation. #9583 2026-02-18 11:09:06 +05:30
Akshay Joshi 0842f36f16 Updated release note 2026-02-17 17:41:42 +05:30
Khushboo Vashi 5002419611 Fixed an issue where the 'Quote strings only' configuration was ignored when downloading the result set. #7578 2026-02-17 17:19:41 +05:30
Dave Page f2756a3dcf Core LLM integration infrastructure to allow pgAdmin to connect to AI providers. #9641
* Core infrastructure for LLM integration.
* Add support for a number of different AI generated reports on security, performance, and schema design on servers, databases, and schemas, as appropriate.
* Add a Natural Language AI assistant to the Query Tool.
* Add an AI Insights panel to the EXPLAIN tool in the Query Tool, to analyse and report on issues in query plans.
2026-02-17 17:16:06 +05:30
Khushboo Vashi 2715932464 Update Release Notes. 2026-02-10 14:39:34 +05:30
Murtuza Zabuawala b366d210dc Allow users to customize "OF TYPE" columns during table creation. #229 2026-02-10 14:33:22 +05:30
Simon Randby fbbc12a0a3 Fix an issue where deployment of helm chart crashing with operation not permitted. #9572 2026-02-09 17:13:43 +05:30
Pravesh Sharma 9039080214 Fixed an issue where a long name in ERD table node was not breaking into multiple lines. 2026-02-09 14:53:51 +05:30
Pravesh Sharma f52584eef8 Fixed an issue where column comments were not displayed in the SQL tab for materialised views. #9486 2026-02-09 14:46:15 +05:30
Pravesh Sharma 5abd9e3c5b Fixed an issue where tools settings changed by the users were not restored on application relaunch. #8988 2026-02-09 10:54:53 +05:30
Akshay Joshi 3e05f7a14f Updated Javascript and Python dependencies. 2026-02-06 13:30:09 +05:30
792 changed files with 79158 additions and 36036 deletions

No files matched your search

+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
strategy:
fail-fast: false
matrix:
pgver: [13, 14, 15, 16, 17]
pgver: [14, 15, 16, 17, 18]
runs-on: ubuntu-22.04
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
strategy:
fail-fast: false
matrix:
pgver: [13, 14, 15, 16, 17, 18]
pgver: [14, 15, 16, 17, 18]
runs-on: ubuntu-22.04
+13 -2
View File
@@ -32,7 +32,18 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-22.04, windows-latest]
pgver: [13, 14, 15, 16, 17]
pgver: [14, 15, 16, 17, 18]
postgisver: [34]
exclude:
- pgver: 18
postgisver: 34
include:
- os: ubuntu-22.04
pgver: 18
postgisver: 36
- os: windows-latest
pgver: 18
postgisver: 36
runs-on: ${{ matrix.os }}
@@ -53,7 +64,7 @@ jobs:
if: ${{ matrix.os == 'ubuntu-22.04' }}
run: |
sudo apt update
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
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 }}
- name: Install pgagent on Linux
if: ${{ matrix.os == 'ubuntu-22.04' && matrix.pgver <= 16 }}
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-22.04, windows-latest]
pgver: [13, 14, 15, 16, 17, 18]
pgver: [14, 15, 16, 17, 18]
runs-on: ${{ matrix.os }}
+1
View File
@@ -59,5 +59,6 @@ auditpy.txt
web/coverage/
web/.coverage
web/regression/.coverage
.worktree/
web/regression/covhtml/
web/regression/htmlcov/
+5 -3
View File
@@ -159,13 +159,15 @@ RUN apk update && apk upgrade && \
bash \
postfix \
krb5-libs \
libcurl \
libjpeg-turbo \
shadow \
sudo \
tzdata \
libedit \
libldap \
libcap && \
libcap \
su-exec && \
rm -rf /var/cache/apk/*
# Copy in the Python packages
@@ -173,7 +175,7 @@ COPY --from=env-builder /venv /venv
# Copy in the tools
COPY --from=tool-builder /usr/local/pgsql /usr/local/
COPY --from=pg18-builder /usr/local/lib/libpq.so.5.18 /usr/lib/liblz4.so.1.10.0 /usr/lib/
COPY --from=pg18-builder /usr/local/lib/libpq.so.5.18 /usr/local/lib/libpq-oauth-18.so /usr/lib/liblz4.so.1.10.0 /usr/lib/
RUN ln -s libpq.so.5.18 /usr/lib/libpq.so.5 && \
ln -s libpq.so.5.18 /usr/lib/libpq.so && \
@@ -197,7 +199,7 @@ RUN /venv/bin/python3 -m pip install --no-cache-dir gunicorn==23.0.0 && \
useradd -r -u 5050 -g root -s /sbin/nologin pgadmin && \
mkdir -p /run/pgadmin /var/lib/pgadmin && \
chown pgadmin:root /run/pgadmin /var/lib/pgadmin && \
chmod g=u /var/lib/pgadmin && \
chmod g=u /run/pgadmin /var/lib/pgadmin && \
touch /pgadmin4/config_distro.py && \
chown pgadmin:root /pgadmin4/config_distro.py && \
chmod g=u /pgadmin4/config_distro.py && \
+249
View File
@@ -0,0 +1,249 @@
.. _ai_tools:
*******************
`AI Reports`:index:
*******************
**AI Reports** is a feature that provides AI-powered database analysis and insights
using Large Language Models (LLMs). Use the *Tools → AI Reports* menu to access
the various AI-powered reports.
The AI Reports feature allows you to:
* Generate security reports to identify potential security vulnerabilities and configuration issues.
* Create performance reports with optimization recommendations for queries and configurations.
* Perform design reviews to analyze database schema structure and suggest improvements.
**Prerequisites:**
Before using AI Reports, you must:
1. Ensure AI features are enabled in the server configuration (set ``LLM_ENABLED`` to ``True`` in ``config.py``).
2. Configure an LLM provider in :ref:`Preferences → AI <preferences>`.
**Note:**
* AI Reports using cloud providers (Anthropic, OpenAI) require an active internet connection.
Local providers (Ollama, Docker Model Runner) do not require internet access.
* API usage may incur costs depending on your LLM provider's pricing model.
Local providers (Ollama, Docker Model Runner) are free to use.
* The quality and accuracy of reports depend on the LLM provider and model configured.
Configuring AI Reports
**********************
To configure AI Reports, navigate to *File → Preferences → AI* (or click the *Settings*
button and select *AI*).
.. image:: images/preferences_ai.png
:alt: AI preferences
:align: center
Select your preferred LLM provider from the dropdown:
**Anthropic**
Use Claude models from Anthropic, or any Anthropic-compatible API provider.
* **API URL**: Custom API endpoint URL (leave empty for default: https://api.anthropic.com/v1).
* **API Key File**: Path to a file containing your Anthropic API key (obtain from https://console.anthropic.com/). This path refers to the filesystem where the pgAdmin server is running (e.g., inside the container if using Docker). The ``~`` prefix is expanded to the home directory of the user running the pgAdmin server process. Optional when using a custom URL with a provider that does not require authentication.
* **Model**: Select from available Claude models (e.g., claude-sonnet-4-20250514).
**OpenAI**
Use GPT models from OpenAI, or any OpenAI-compatible API provider (e.g.,
LiteLLM, LM Studio, EXO, or other local inference servers).
* **API URL**: Custom API endpoint URL (leave empty for default: https://api.openai.com/v1). Include the ``/v1`` path prefix if required by your provider.
* **API Key File**: Path to a file containing your OpenAI API key (obtain from https://platform.openai.com/). This path refers to the filesystem where the pgAdmin server is running (e.g., inside the container if using Docker). The ``~`` prefix is expanded to the home directory of the user running the pgAdmin server process. Optional when using a custom URL with a provider that does not require authentication.
* **Model**: Select from available GPT models (e.g., gpt-4).
**Ollama**
Use locally-hosted open-source models via Ollama. Requires a running Ollama instance.
* **API URL**: The URL of your Ollama server (default: http://localhost:11434).
* **Model**: Enter the name of the Ollama model to use (e.g., llama2, mistral).
**Docker Model Runner**
Use models running in Docker Desktop's built-in model runner (available in Docker Desktop 4.40+).
No API key is required.
* **API URL**: The URL of the Docker Model Runner API (default: http://localhost:12434).
* **Model**: Select from available models or enter a custom model name.
.. note:: You can also use the *OpenAI* provider with a custom API URL for any
OpenAI-compatible endpoint, including Docker Model Runner and other local
inference servers.
After configuring your provider, click *Save* to apply the changes.
Security Reports
****************
Security Reports analyze your PostgreSQL server, database, or schema for potential
security vulnerabilities and configuration issues.
To generate a security report:
1. In the *Browser* tree, select a server, database, or schema.
2. Choose *Tools → AI Reports → Security* from the menu, or right-click the
object and select *Security* from the context menu.
3. The report will be generated and displayed in a new tab.
.. image:: images/ai_security_report.png
:alt: AI security report
:align: center
**Security Report Scope:**
* **Server Level**: Analyzes server configuration, authentication settings, roles, and permissions.
* **Database Level**: Reviews database-specific security settings, roles with database access, and object permissions.
* **Schema Level**: Examines schema permissions, object ownership, and access controls.
Each report includes:
* **Security Findings**: Identified vulnerabilities or security concerns.
* **Risk Assessment**: Severity levels for each finding (Critical, High, Medium, Low).
* **Recommendations**: Specific actions to remediate security issues.
* **Best Practices**: General security recommendations for PostgreSQL.
Performance Reports
*******************
Performance Reports analyze query performance, configuration settings, and provide
optimization recommendations.
To generate a performance report:
1. In the *Browser* tree, select a server or database.
2. Choose *Tools → AI Reports → Performance* from the menu, or right-click the
object and select *Performance* from the context menu.
3. The report will be generated and displayed in a new tab.
**Performance Report Scope:**
* **Server Level**: Analyzes server configuration parameters, resource utilization, and overall server performance metrics.
* **Database Level**: Reviews database-specific configuration, query performance, index usage, and table statistics.
Each report includes:
* **Performance Metrics**: Key performance indicators and statistics.
* **Configuration Analysis**: Review of relevant configuration parameters.
* **Query Optimization**: Recommendations for improving slow queries.
* **Index Recommendations**: Suggestions for adding, removing, or modifying indexes.
* **Capacity Planning**: Resource utilization trends and recommendations.
Design Review Reports
*********************
Design Review Reports analyze your database schema structure and suggest
improvements for normalization, naming conventions, and best practices.
To generate a design review report:
1. In the *Browser* tree, select a database or schema.
2. Choose *Tools → AI Reports → Design* from the menu, or right-click the
object and select *Design* from the context menu.
3. The report will be generated and displayed in a new tab.
**Design Review Scope:**
* **Database Level**: Reviews overall database structure, schema organization, and cross-schema dependencies.
* **Schema Level**: Analyzes tables, views, functions, and other objects within the schema.
Each report includes:
* **Schema Structure Analysis**: Review of table structures, relationships, and constraints.
* **Normalization Review**: Recommendations for database normalization (1NF, 2NF, 3NF, etc.).
* **Naming Conventions**: Suggestions for consistent naming patterns.
* **Data Type Usage**: Review of data type choices and recommendations.
* **Index Design**: Analysis of indexing strategy.
* **Best Practices**: General PostgreSQL schema design recommendations.
Working with Reports
********************
All AI reports are displayed in a dedicated panel with the following features:
**Report Display**
Reports are formatted as Markdown and rendered with syntax highlighting for SQL code.
**Toolbar Actions**
* **Stop** - Cancel the current report generation. This is useful if the report
is taking too long or if you want to change parameters.
* **Regenerate** - Generate a new report for the same object. Useful when you
want to get a fresh analysis or if data has changed.
* **Download** - Download the report as a Markdown (.md) file. The filename
includes the report type, object name, and date for easy identification.
**Multiple Reports**
You can generate and view multiple reports simultaneously. Each report opens in
a new tab, allowing you to compare reports across different servers, databases,
or schemas.
**Report Management**
Each report tab can be closed individually by clicking the *X* in the tab.
Panel titles show the object name and report type for easy identification.
**Copying Content**
You can select and copy text from reports to use in documentation or share with
your team.
Troubleshooting
***************
**"AI features are disabled in the server configuration"**
The administrator has disabled AI features on the server. Contact your
pgAdmin administrator to enable the ``LLM_ENABLED`` configuration option.
**"Please configure an LLM provider in Preferences"**
You need to configure an LLM provider before using AI Reports. See *Configuring AI Reports* above.
**"Please connect to the server/database first"**
You must establish a connection to the server or database before generating reports.
**API Connection Errors**
* Verify your API key is correct (for Anthropic and OpenAI).
* Check your internet connection (for cloud providers).
* For Ollama, ensure the Ollama server is running and accessible.
* For Docker Model Runner, ensure Docker Desktop 4.40+ is running with the model runner enabled.
* Check that your firewall allows connections to the LLM provider's API.
**Report Generation Fails**
* Check the pgAdmin logs for detailed error messages.
* Verify the database connection is still active.
* Ensure the selected model is available for your account/subscription.
+1
View File
@@ -17,3 +17,4 @@ PL/SQL code.
schema_diff
erd_tool
psql_tool
ai_tools
Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

+22 -12
View File
@@ -51,6 +51,28 @@ Use the fields in the *Definition* tab to define the index:
* Use the drop-down listbox next to *Depends on extensions* to select the extension
that this index depends on (for example, edbspl). If set, dropping the extension
will automatically drop the index as well.
* Move the switch next to *Only Table?* to the *Yes* position to create the index
only on the parent table without recursing to its partitions. This button will be
enabled for partition tables only. The default is *No*.
* Move the switch next to *Unique?* to the *Yes* position to check for duplicate values
in the table when the index is created and when data is added. The default is *No*.
* Move the switch next to *NULLs not distinct?* to the *Yes* position to treat null values as not distinct.
The default is *No*. This option is available only on PostgreSQL 15 and above.
* Move the *Clustered?* switch to the *Yes* position to instruct the server to
cluster the table.
* Move the *Concurrent build?* switch to the *Yes* position to build the index
without taking any locks that prevent concurrent inserts, updates, or deletes
on the table.
* Use the *Constraint* field to provide a constraint expression; a constraint
expression limits the entries in the index to those rows that satisfy the
constraint.
Click the *With* tab to continue.
.. image:: images/index_with.png
:alt: Index dialog with tab
:align: center
* Use the *Fill Factor* field to specify a fill factor for the index. The fill
factor specifies how full the selected method will try to fill each index
page.
@@ -66,18 +88,6 @@ Use the fields in the *Definition* tab to define the index:
The default is *Yes*.
* Move the switch next to *Autosummarize* to the *Yes* position to define whether a summarization run is
queued for the previous page range whenever an insertion is detected on the next one. The default is *No*
* Move the switch next to *Unique?* to the *Yes* position to check for duplicate values
in the table when the index is created and when data is added. The default is *No*.
* Move the switch next to *NULLs not distinct?* to the *Yes* position to treat null values as not distinct.
The default is *No*. This option is available only on PostgreSQL 15 and above.
* Move the *Clustered?* switch to the *Yes* position to instruct the server to
cluster the table.
* Move the *Concurrent build?* switch to the *Yes* position to build the index
without taking any locks that prevent concurrent inserts, updates, or deletes
on the table.
* Use the *Constraint* field to provide a constraint expression; a constraint
expression limits the entries in the index to those rows that satisfy the
constraint.
Click the *Columns* tab to continue.
+25
View File
@@ -110,3 +110,28 @@ There are 3 ways to configure LDAP:
"LDAP_BIND_PASSWORD", "Password for simple bind.
Specify the value if you have set the LDAP_BIND_USER parameter."
Brute-force protection
**********************
pgAdmin's built-in account-lockout setting (``MAX_LOGIN_ATTEMPTS``) applies
only to accounts using the ``INTERNAL`` authentication source. It does
**not** rate-limit or lock LDAP-authenticated logins.
When LDAP authentication is enabled, brute-force protection for LDAP
credentials is the responsibility of:
- **The directory.** Configure account-lockout policy on the LDAP server
itself (for example, OpenLDAP's ``ppolicy`` overlay, Active Directory's
account-lockout GPO, or the equivalent on FreeIPA / 389 Directory
Server). The directory is the credential authority and is the correct
place to enforce per-account lockout.
- **The reverse proxy or WAF in front of pgAdmin.** Configure
request-rate limiting on the login endpoint (for example, ``limit_req``
in nginx, ``stick-table`` rules in HAProxy, or the equivalent in your
WAF) to bound login-attempt volume per source IP. This is independent
of pgAdmin and protects against attackers who would otherwise reach the
LDAP server through pgAdmin.
See :ref:`Avoiding a bruteforce attack <login>` for the equivalent note
on the login page.
+15 -3
View File
@@ -60,6 +60,18 @@ asking you to contact the LDAP administrator to recover your LDAP password.
Avoiding a bruteforce attack
****************************
You have the possibility to lock an account by setting ``MAX_LOGIN_ATTEMPTS``
once it has reached the maximum number of login attempts.
You can disable this feature by setting the value to zero.
For accounts using the ``INTERNAL`` authentication source, pgAdmin locks
the account after ``MAX_LOGIN_ATTEMPTS`` consecutive failed logins. The
default is 3; set it to zero to disable.
For accounts authenticated against an external identity provider
(``LDAP``, ``OAUTH2``, ``KERBEROS``, ``WEBSERVER``), brute-force protection
is **not** handled by pgAdmin and ``MAX_LOGIN_ATTEMPTS`` does not apply.
Operators using these sources should:
- Configure account-lockout policy on the upstream provider (for example,
Active Directory's account-lockout GPO, OpenLDAP's ``ppolicy`` overlay,
or the OAuth2 provider's rate limits).
- Configure request rate-limiting (for example, ``limit_req`` in nginx)
on the reverse proxy in front of pgAdmin to bound login-attempt volume
per source IP.
+6
View File
@@ -156,6 +156,12 @@ Use the *Tools* menu to access the following options (in alphabetical order):
+------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------+
| *Search Objects...* | Click to open the :ref:`Search Objects... <search_objects>` and start searching any kind of objects in a database. |
+------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------+
| *AI Reports* | Click to access a submenu with AI-powered analysis options (requires :ref:`AI configuration <ai_tools>`): |
| | |
| | - *Security Report* - Generate an AI-powered security analysis for the selected server, database, or schema. |
| | - *Performance Report* - Generate an AI-powered performance analysis for the selected server or database. |
| | - *Design Report* - Generate an AI-powered design review for the selected database or schema. |
+------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------+
| *Add named restore point* | Click to open the :ref:`Add named restore point... <add_restore_point_dialog>` dialog to take a point-in-time snapshot of the current |
| | server state. |
+------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------+
+85 -2
View File
@@ -27,6 +27,83 @@ The left pane of the *Preferences* tab displays a tree control; each node of
the tree control provides access to options that are related to the node under
which they are displayed.
The AI Node
***********
Use preferences found in the *AI* node of the tree control to configure
AI-powered features and LLM (Large Language Model) providers.
.. image:: images/preferences_ai.png
:alt: Preferences AI section
:align: center
**Note:** AI features must be enabled in the server configuration (``LLM_ENABLED = True``
in ``config.py``) for these preferences to be available.
Use the fields on the *AI* panel to configure your LLM provider:
* Use the *Default Provider* drop-down to select your LLM provider. Options include:
*Anthropic*, *OpenAI*, *Ollama*, or *Docker Model Runner*.
**Anthropic Settings:**
* Use the *API URL* field to set a custom API endpoint URL. Leave empty to use
the default Anthropic API (``https://api.anthropic.com/v1``). Set a custom URL
to use an Anthropic-compatible API provider.
* Use the *API Key File* field to specify the path to a file containing your
Anthropic API key. This path refers to the filesystem where the pgAdmin
server is running (e.g., inside the container if using Docker). The ``~``
prefix is expanded to the home directory of the user running the pgAdmin
server process. The API key may be optional when using a custom API URL
with a provider that does not require authentication.
* Use the *Model* field to select from the available Claude models. Click the
refresh button to fetch the latest available models from your configured
endpoint.
**OpenAI Settings:**
* Use the *API URL* field to set a custom API endpoint URL. Leave empty to use
the default OpenAI API (``https://api.openai.com/v1``). Set a custom URL to
use any OpenAI-compatible API provider (e.g., LiteLLM, LM Studio, EXO).
Include the ``/v1`` path prefix if required by your provider
(e.g., ``http://localhost:1234/v1``).
* Use the *API Key File* field to specify the path to a file containing your
OpenAI API key. This path refers to the filesystem where the pgAdmin
server is running (e.g., inside the container if using Docker). The ``~``
prefix is expanded to the home directory of the user running the pgAdmin
server process. The API key may be optional when using a custom API URL
with a provider that does not require authentication.
* Use the *Model* field to select from the available GPT models. Click the
refresh button to fetch the latest available models from your configured
endpoint.
**Ollama Settings:**
* Use the *API URL* field to specify the Ollama server URL
(default: ``http://localhost:11434``).
* Use the *Model* field to select from the available models or enter a custom
model name (e.g., ``llama2``, ``mistral``). Click the refresh button to fetch
the latest available models from your Ollama server.
**Docker Model Runner Settings:**
* Use the *API URL* field to specify the Docker Model Runner API URL
(default: ``http://localhost:12434``). Available in Docker Desktop 4.40+.
* Use the *Model* field to select from the available models or enter a custom
model name. Click the refresh button to fetch the latest available models
from your Docker Model Runner.
.. note:: You can also use the *OpenAI* provider with a custom API URL for any
OpenAI-compatible endpoint, including Docker Model Runner, LM Studio, EXO,
and other local inference servers. This can be useful when you want to use
a provider that isn't explicitly listed but supports the OpenAI API format.
The Browser Node
****************
@@ -392,11 +469,17 @@ Use the fields on the *File Downloads* panel to manage file downloads related pr
* When the *Automatically open downloaded files?* switch is set to *True*
the downloaded file will automatically open in the system's default
application associated with that file type.
application associated with that file type. **Note:** This option is applicable and
visible only in desktop mode.
* When the *Enable binary data download?* switch is set to *True*,
binary data can be downloaded from the result grid. Default is set to *False*
to prevent excessive memory usage on the server.
* When the *Prompt for the download location?* switch is set to *True*
a prompt will appear after clicking the download button, allowing you
to choose the download location.
to choose the download location. **Note:** This option is applicable and
visible only in desktop mode.
**Note:** File Downloads related settings are applicable and visible only in desktop mode.
+84 -2
View File
@@ -32,8 +32,9 @@ The Query Tool features two panels:
* The upper panel displays the *SQL Editor*. You can use the panel to enter,
edit, or execute a query or a script. It also shows the *History* tab which can be used
to view the queries that have been executed in the session, and a *Scratch Pad*
which can be used to hold text snippets during editing. If the Scratch Pad is
to view the queries that have been executed in the session, a *Scratch Pad*
which can be used to hold text snippets during editing, and an *AI Assistant*
tab for generating SQL from natural language (when AI is configured). If the Scratch Pad is
closed, it can be re-opened (or additional ones opened) by right-clicking in
the SQL Editor and other panels and adding a new panel.
* The lower panel displays the *Data Output* panel. The tabbed panel displays
@@ -201,6 +202,49 @@ can be adjusted in ``config_local.py`` or ``config_system.py`` (see the
`MAX_QUERY_HIST_STORED` value. See the :ref:`Deployment <deployment>` section
for more information.
AI Assistant Panel
******************
The *AI Assistant* tab provides a chat-style interface for generating SQL queries
from natural language descriptions. This feature requires an AI provider to be
configured in *Preferences > AI*. For configuration details, see the
:ref:`preferences` documentation.
.. image:: images/query_ai_assistant.png
:alt: Query tool AI Assistant panel
:align: center
To use the AI Assistant:
1. Click on the *AI Assistant* tab in the upper panel, or use the *AI Assistant*
toolbar button.
2. Type a description of the SQL query you need in natural language.
3. Press Enter or click the send button to submit your request.
4. The AI will analyze your database schema and generate appropriate SQL.
The AI Assistant displays conversations with your messages and AI responses. When
the AI generates SQL, it appears in a syntax-highlighted code block with action
buttons:
* **Insert** - Insert the SQL at the current cursor position in the SQL Editor.
* **Replace** - Replace all content in the SQL Editor with the generated SQL.
* **Copy** - Copy the SQL to the clipboard.
The AI Assistant maintains conversation context, allowing you to refine queries
iteratively. For example, you can ask for a query and then follow up with
"also add a filter for active users" to modify the previous result.
**Tips for effective use:**
* Be specific about table and column names if you know them.
* Describe the desired output format (e.g., "show count by category").
* For complex queries, break down requirements step by step.
* Use the *Clear* button to start a fresh conversation.
**Note:** The AI Assistant uses database schema inspection tools to understand
your database structure. It supports SELECT, INSERT, UPDATE, DELETE, and DDL
statements. All generated queries should be reviewed before execution.
The Data Output Panel
*********************
@@ -335,6 +379,44 @@ If planner mis-estimated number of rows (actual vs planned) by
:alt: Query tool explain plan statistics
:align: center
* AI Insights
The *AI Insights* tab provides AI-powered analysis of query execution plans,
identifying performance bottlenecks and suggesting optimizations. This tab is
only available when an AI provider is configured in *Preferences > AI*.
.. image:: images/query_explain_ai_insights.png
:alt: Query tool explain plan AI insights
:align: center
When you switch to the AI Insights tab, the AI analyzes the execution plan and
provides:
**Performance Bottlenecks** - Issues identified in the query plan, such as:
* Sequential scans on large tables that could benefit from indexes
* Significant differences between estimated and actual row counts
* Expensive sort or hash operations
* Nested loops with high iteration counts
**Recommendations** - Concrete suggestions to improve query performance:
* Index creation statements with appropriate columns
* ANALYZE commands to update table statistics
* Configuration parameter adjustments
* Query restructuring suggestions
Each recommendation that includes SQL (such as CREATE INDEX statements) has
action buttons to *Copy* the SQL to the clipboard or *Insert* it into the
Query Editor.
Click the *Regenerate* button to request a fresh analysis of the current plan.
**Note:** AI analysis is generated on-demand when you first click the AI Insights
tab or when a new explain plan is generated while the tab is active. The analysis
provides guidance but all suggested changes should be carefully evaluated before
applying to production databases.
Messages Panel
**************
+3
View File
@@ -12,6 +12,9 @@ notes for it.
:maxdepth: 1
release_notes_9_15
release_notes_9_14
release_notes_9_13
release_notes_9_12
release_notes_9_11
release_notes_9_10
+44
View File
@@ -0,0 +1,44 @@
************
Version 9.13
************
Release date: 2026-03-05
This release contains a number of bug fixes and new features since the release of pgAdmin 4 v9.12.
Supported Database Servers
**************************
**PostgreSQL**: 13, 14, 15, 16, 17 and 18
**EDB Advanced Server**: 13, 14, 15, 16, 17 and 18
Bundled PostgreSQL Utilities
****************************
**psql**, **pg_dump**, **pg_dumpall**, **pg_restore**: 18.0
New features
************
| `Issue #229 <https://github.com/pgadmin-org/pgadmin4/issues/229>`_ - Allow users to customize "OF TYPE" columns during table creation.
| `Issue #5578 <https://github.com/pgadmin-org/pgadmin4/issues/5578>`_ - No FK are shown in diagram created from existing tables in the ERD Tool.
| `Issue #6386 <https://github.com/pgadmin-org/pgadmin4/issues/6386>`_ - Add support for 'ONLY' in Index creation dialog.
| `Issue #8198 <https://github.com/pgadmin-org/pgadmin4/issues/8198>`_ - Allow "drag-n-drop" for only user chosen tables, and show relations between them.
| `Issue #9229 <https://github.com/pgadmin-org/pgadmin4/issues/9229>`_ - Load predefined users from a JSON file through command line.
| `Issue #9641 <https://github.com/pgadmin-org/pgadmin4/issues/9641>`_ - Core LLM integration infrastructure, AI reports for security, schema, and performance, AI chat for the Query Tool, and AI Insights for EXPLAIN.
Housekeeping
************
Bug fixes
*********
| `Issue #7578 <https://github.com/pgadmin-org/pgadmin4/issues/7578>`_ - Fixed an issue where the 'Quote strings only' configuration was ignored when downloading the result set.
| `Issue #8988 <https://github.com/pgadmin-org/pgadmin4/issues/8988>`_ - Fixed an issue where tools settings changed by the users were not restored on application relaunch.
| `Issue #9258 <https://github.com/pgadmin-org/pgadmin4/issues/9258>`_ - Fixed an issue where modifying a shared server incorrectly updated the original server details.
| `Issue #9484 <https://github.com/pgadmin-org/pgadmin4/issues/9484>`_ - Fixed an issue where a long name in ERD table node was not breaking into multiple lines.
| `Issue #9486 <https://github.com/pgadmin-org/pgadmin4/issues/9486>`_ - Fixed an issue where column comments were not displayed in the SQL tab for materialised views.
| `Issue #9572 <https://github.com/pgadmin-org/pgadmin4/issues/9572>`_ - Fix an issue where deployment of helm chart crashing with operation not permitted.
| `Issue #9583 <https://github.com/pgadmin-org/pgadmin4/issues/9583>`_ - Fix translation compilation.
| `Issue #9649 <https://github.com/pgadmin-org/pgadmin4/issues/9649>`_ - Fix broken checkbox selection in backup dialog objects tree.
| `Issue #9651 <https://github.com/pgadmin-org/pgadmin4/issues/9651>`_ - Fixed an issue in file dialog where rename was not working.
+52
View File
@@ -0,0 +1,52 @@
************
Version 9.14
************
Release date: 2026-04-02
This release contains a number of bug fixes and new features since the release of pgAdmin 4 v9.13.
Supported Database Servers
**************************
**PostgreSQL**: 13, 14, 15, 16, 17 and 18
**EDB Advanced Server**: 13, 14, 15, 16, 17 and 18
Bundled PostgreSQL Utilities
****************************
**psql**, **pg_dump**, **pg_dumpall**, **pg_restore**: 18.2
New features
************
| `Issue #4011 <https://github.com/pgadmin-org/pgadmin4/issues/4011>`_ - Added support to download binary data from result grid.
| `Issue #9703 <https://github.com/pgadmin-org/pgadmin4/issues/9703>`_ - Added support for custom LLM provider URLs for OpenAI and Anthropic, allowing use of OpenAI-compatible providers such as LM Studio, EXO, and LiteLLM.
| `Issue #9709 <https://github.com/pgadmin-org/pgadmin4/issues/9709>`_ - Fixed an issue where AI features (AI Assistant tab, AI Reports menus, and AI Preferences) were visible in the UI even when LLM_ENABLED is set to False.
| `Issue #9738 <https://github.com/pgadmin-org/pgadmin4/issues/9738>`_ - Allow copying of text from the AI Assistant chat panel.
Housekeeping
************
Bug fixes
*********
| `Issue #8992 <https://github.com/pgadmin-org/pgadmin4/issues/8992>`_ - Fixed an issue where selecting all in the Query Tool's Messages tab would select the entire page content.
| `Issue #9279 <https://github.com/pgadmin-org/pgadmin4/issues/9279>`_ - Fixed an issue where OAuth2 authentication fails with 'object has no attribute' if OAUTH2_AUTO_CREATE_USER is False.
| `Issue #9392 <https://github.com/pgadmin-org/pgadmin4/issues/9392>`_ - Ensure that the Geometry Viewer refreshes when re-running queries or switching geometry columns, preventing stale data from being displayed.
| `Issue #9457 <https://github.com/pgadmin-org/pgadmin4/issues/9457>`_ - Fixed Process Watcher garbled text on Windows with non-UTF-8 locales.
| `Issue #9570 <https://github.com/pgadmin-org/pgadmin4/issues/9570>`_ - Fixed an issue where ALT+F5 for executing a query in the Query Tool shows a crosshair cursor icon for rectangular selection.
| `Issue #9648 <https://github.com/pgadmin-org/pgadmin4/issues/9648>`_ - Fixed an issue where the default fillfactor value for B-tree indexes was incorrect.
| `Issue #9694 <https://github.com/pgadmin-org/pgadmin4/issues/9694>`_ - Fixed an issue where AI Reports are grayed out after setting an API key by auto-selecting the default provider.
| `Issue #9696 <https://github.com/pgadmin-org/pgadmin4/issues/9696>`_ - Fixed an issue where AI Assistant does not notify that No API Key or Provider is Set.
| `Issue #9702 <https://github.com/pgadmin-org/pgadmin4/issues/9702>`_ - Fixed misleading AI activity messages that could be mistaken for actual database operations.
| `Issue #9719 <https://github.com/pgadmin-org/pgadmin4/issues/9719>`_ - Fixed an issue where AI Reports fail with OpenAI models that do not support the temperature parameter.
| `Issue #9721 <https://github.com/pgadmin-org/pgadmin4/issues/9721>`_ - Fixed an issue where permissions page is not completely accessible on full scroll.
| `Issue #9729 <https://github.com/pgadmin-org/pgadmin4/issues/9729>`_ - Fixed an issue where some LLM models would not use database tools in the AI assistant, instead returning text descriptions of tool calls.
| `Issue #9732 <https://github.com/pgadmin-org/pgadmin4/issues/9732>`_ - Improve the AI Assistant user prompt to be more descriptive of the actual functionality.
| `Issue #9734 <https://github.com/pgadmin-org/pgadmin4/issues/9734>`_ - Fixed an issue where LLM responses are not streamed or rendered properly in the AI Assistant.
| `Issue #9736 <https://github.com/pgadmin-org/pgadmin4/issues/9736>`_ - Fix an issue where the AI Assistant was not retaining conversation context between messages, with chat history compaction to manage token budgets.
| `Issue #9740 <https://github.com/pgadmin-org/pgadmin4/issues/9740>`_ - Fixed an issue where the AI Assistant input textbox sometimes swallows the first character of input.
| `Issue #9758 <https://github.com/pgadmin-org/pgadmin4/issues/9758>`_ - Clarify where the LLM API key files should be.
| `Issue #9789 <https://github.com/pgadmin-org/pgadmin4/issues/9789>`_ - Fixed an issue where the Query tool kept prompting for a password when using a shared server.
| `Issue #9795 <https://github.com/pgadmin-org/pgadmin4/issues/9795>`_ - Support /v1/responses for OpenAI models.
+113
View File
@@ -0,0 +1,113 @@
************
Version 9.15
************
Release date: 2026-05-11
This release contains a number of bug fixes and new features since the release of pgAdmin 4 v9.14.
Supported Database Servers
**************************
**PostgreSQL**: 13, 14, 15, 16, 17 and 18
**EDB Advanced Server**: 13, 14, 15, 16, 17 and 18
Bundled PostgreSQL Utilities
****************************
**psql**, **pg_dump**, **pg_dumpall**, **pg_restore**: 18.2
New features
************
| `Issue #9657 <https://github.com/pgadmin-org/pgadmin4/issues/9657>`_ - Allow the container image to run as a non-default user via the PUID and PGID environment variables.
Housekeeping
************
| `Issue #9764 <https://github.com/pgadmin-org/pgadmin4/issues/9764>`_ - Update the Swedish translation.
| `Issue #9827 <https://github.com/pgadmin-org/pgadmin4/issues/9827>`_ - Bump Python and JavaScript dependencies.
| `Issue #9832 <https://github.com/pgadmin-org/pgadmin4/issues/9832>`_ - Fix the Czech translation for 'Refresh'.
| `Issue #9834 <https://github.com/pgadmin-org/pgadmin4/issues/9834>`_ - Bump runtime dependencies and upgrade ESLint to v10.
| `Issue #9839 <https://github.com/pgadmin-org/pgadmin4/issues/9839>`_ - Update the Russian translation.
| `Issue #9870 <https://github.com/pgadmin-org/pgadmin4/issues/9870>`_ - Bump runtime and development dependencies.
| `Issue #9873 <https://github.com/pgadmin-org/pgadmin4/issues/9873>`_ - Use an ``<OWNER>`` placeholder in resql tests instead of a hardcoded 'postgres' role to support non-default superuser names.
| `Issue #9893 <https://github.com/pgadmin-org/pgadmin4/issues/9893>`_ - Update the Spanish translation.
| `Issue #9906 <https://github.com/pgadmin-org/pgadmin4/issues/9906>`_ - Update the Italian translation.
Bug fixes
*********
| `Issue #9656 <https://github.com/pgadmin-org/pgadmin4/issues/9656>`_ - Use absolute paths for ``a2enmod`` and ``a2enconf`` in the Debian setup script so it works when ``/usr/sbin`` is not on PATH.
| `Issue #9830 <https://github.com/pgadmin-org/pgadmin4/issues/9830>`_ - Fix cross-user data access and shared-server privilege escalation in server mode (CVE-2026-7813). Also applies the ``@with_object_filters`` access-control decorator to ``ServerNode.list``.
| `Issue #9835 <https://github.com/pgadmin-org/pgadmin4/issues/9835>`_ - Tighten Shared Server feature parity, owner-only field handling, and write guards as a follow-up to the data-isolation hardening.
| `Issue #9865 <https://github.com/pgadmin-org/pgadmin4/issues/9865>`_ - Fix stored cross-site scripting (XSS) via crafted PostgreSQL object names rendered in the Browser Tree and Explain Visualizer (CVE-2026-7814). Reported by Fahar Abbas.
| `Issue #9898 <https://github.com/pgadmin-org/pgadmin4/issues/9898>`_ - Fix SQL injection in Maintenance tool option values (CVE-2026-7815). Reported by j3seer.
| `Issue #9899 <https://github.com/pgadmin-org/pgadmin4/issues/9899>`_ - Fix OS command injection in Import/Export query export (CVE-2026-7816). Reported by Chung Kim (chungkn), OneMount Group.
| `Issue #9900 <https://github.com/pgadmin-org/pgadmin4/issues/9900>`_ - Fix local-file inclusion and server-side request forgery in LLM API configuration endpoints (CVE-2026-7817). Reported by j3seer.
| `Issue #9901 <https://github.com/pgadmin-org/pgadmin4/issues/9901>`_ - Fix unsafe deserialization in the session manager that could lead to remote code execution (CVE-2026-7818). Also encrypts session files at rest using Fernet, restricts session-file permissions to 0o600, switches the session-digest default from SHA-1 to SHA-256, drops several non-roundtrippable live objects from the session (``AuthSourceManager`` and the Azure, RDS, Google Cloud, and BigAnimal cloud-provider instances), tightens DATA_DIR file and directory permissions at creation, creates ``pgadmin4.log`` with mode 0o600, hardens ``EnhancedRotatingFileHandler._open`` against rotation failures, and bounds the ``user_info_server`` prompt retry loop so a non-interactive caller cannot spin forever. Reported by Fernando Bortotti.
| `Issue #9902 <https://github.com/pgadmin-org/pgadmin4/issues/9902>`_ - Fix symlink-based path traversal in the file manager (CVE-2026-7819). Reported by Fernando Bortotti.
| `Issue #9904 <https://github.com/pgadmin-org/pgadmin4/issues/9904>`_ - Fix account-lockout bypass on Flask-Security's default ``/login`` view by overriding ``User.is_active`` and ``User.is_locked()`` so the ``locked`` field is honored on every authentication path (CVE-2026-7820). Reported by Fernando Bortotti.
Additional changes (no associated issue)
****************************************
The commits below did not have a dedicated GitHub issue. They are listed here for transparency.
Bug fixes
---------
| ``1518b0828`` - Restore the SERVER_MODE python-test path and fix two endpoint regressions surfaced by it.
| ``d57acce35`` - Harden validation, preference, and connection-params paths against pre-existing edge cases.
Test-suite stability
--------------------
| ``a11d289bd`` - Harden ``click_modal`` backdrop wait and ``open_query_tool`` stale-element retry in feature tests.
| ``a50a553b0`` - Feature tests use ``sys.executable``; sync ``yarn.lock`` to ``package.json``.
| ``0fad04de8`` - PSQL socket tests use the authenticated tester; the role-dependencies test skips cleanly on auth failure.
| ``1f7194924`` - Harden six regression tests against environmental drift.
| ``dc61039e9`` - Quote the username in the views/mview test helper for dotted local roles.
| ``9b29bc203`` - Quote the username in the types/compound-triggers test helpers for dotted local roles.
| ``504775de8`` - Quote the username in the user-mappings test helper for dotted local roles.
| ``208541cc4`` - ``ImportExportServersTestCase`` uses ``sys.executable`` instead of a bare ``python`` and surfaces subprocess errors instead of swallowing them as a misleading JSON-parse failure.
Refactoring
-----------
| ``6f4f28def`` - Factor the WTForms-error-to-JSON conversion into a helper and drop a dead import.
Documentation
-------------
| ``9923eefca`` - Clarify in ``login.rst`` and ``ldap.rst`` that ``MAX_LOGIN_ATTEMPTS`` applies only to the ``INTERNAL`` authentication source. Operators using LDAP, OAuth2, Kerberos, or Webserver auth should rely on the upstream identity provider's lockout policy and reverse-proxy request rate-limiting.
Dependencies
------------
Non-breaking ``dependabot`` updates aggregated for v9.15.
Python:
| ``boto3`` 1.42 -> 1.43 (``python_version > '3.9'``; Python 3.9 stays on 1.42)
| ``cryptography`` 46.0 -> 47.0
| ``psycopg`` 3.3.3 -> 3.3.4 (``python_version >= '3.10'``)
| ``pycodestyle`` >=2.5.0 -> >=2.14.0
| ``requests`` >=2.21.0 -> >=2.33.1
| ``safety`` >=1.9.0 -> >=3.7.0
| ``testtools`` 2.8.7 -> 2.9.1
| ``typer`` 0.24 -> 0.25 (``python > '3.9'``)
JavaScript (``web/``):
| ``@tanstack/react-query`` 5.90 -> 5.100.5
| ``axios`` 1.15.2 -> 1.16.0
| ``moment-timezone`` 0.6.0 -> 0.6.2
| ``postcss`` 8.5.6 -> 8.5.12
JavaScript (``runtime/``):
| ``axios`` 1.15.2 -> 1.16.0
| ``electron`` 41.3.0 -> 41.5.0
| ``eslint`` 10.2.1 -> 10.3.0
| ``globals`` 17.5.0 -> 17.6.0
+44
View File
@@ -270,6 +270,50 @@ username/email address.
/path/to/python /path/to/setup.py get-users --username user1@gmail.com
Load Users
**********
To bulk import users from a JSON file, invoke ``setup.py`` with ``load-users`` command line option,
followed by the path to the JSON file.
.. code-block:: bash
/path/to/python /path/to/setup.py load-users /path/to/users.json
**JSON File Format**
The input JSON file must contain a ``users`` array with user objects:
.. code-block:: json
{
"users": [
{
"username": "admin@example.com",
"email": "admin@example.com",
"password": "securepassword",
"role": "Administrator",
"active": true,
"auth_source": "internal"
},
{
"username": "ldap_user",
"email": "ldap_user@example.com",
"role": "User",
"active": true,
"auth_source": "ldap"
}
]
}
The command handles errors gracefully:
* Users that already exist are skipped
* Invalid roles are reported and skipped
* Missing passwords for internal auth are reported and skipped
* Passwords shorter than 6 characters are reported and skipped
Output
******
@@ -0,0 +1,872 @@
# Proposal: Eliminate Insecure Deserialization RCE and Symlink-based Path Traversal
**Status:** Draft (v5 — addresses review findings of 2026-04-30, including end-to-end coherence pass)
> **Note on line numbers:** Citations in this proposal are accurate as of `HEAD` of
> `fix/pickle-rce` at v5 publication time. If upstream changes land in `session.py` or
> `file_manager/__init__.py` before PR 1 merges, the implementing PR will recompute line
> references against the rebase base.
**Date:** 2026-04-30
**Author:** Ashesh Vashi
**Tracking branch:** `fix/pickle-rce`
**Related:** Independent security report dated 2026-04-29; CVE-2024-2044 (prior RCE in
SERVER_MODE=False)
---
## 1. Problem Statement
A security report dated 2026-04-29 identified two complementary vulnerabilities in pgAdmin 4
(confirmed on v9.14) that, when combined, allow an authenticated user to achieve OS-level Remote
Code Execution on the pgAdmin host or container.
### 1.1 Vulnerability 1 — Insecure Deserialization Before HMAC Verification (CWE-502)
**Location:** `web/pgadmin/utils/session.py:240-267` — `FileBackedSessionManager.get()`
The session manager deserializes the session file with `pickle.load()` **before** verifying the
HMAC integrity signature stored alongside it. Any file an attacker can place in the sessions
directory is therefore executed unconditionally — `pickle.load()` fires arbitrary
`__reduce__` hooks during reads, before any of the integrity checks run.
The vulnerable sequence:
1. `open(fname, 'rb')` and read all bytes.
2. `pickle.load(f)` — RCE here.
3. Compare the in-body HMAC against the cookie HMAC — too late.
### 1.2 Vulnerability 2 — Symlink Escape in the File Manager (CWE-61 / CWE-22)
**Location:** `web/pgadmin/misc/file_manager/__init__.py` — `check_access_permission()` at
line 894 and the upload handler `add()` at line 1062.
Both code paths use `os.path.abspath()`, which resolves `.` and `..` but does **not** resolve
symbolic links. The subsequent `open(path, 'wb')` is performed by the OS kernel, which **does**
follow symlinks. If a symlink exists inside the user's storage directory pointing outside the
sandbox, the access check (path-string semantics) passes while the kernel write
(filesystem semantics) follows the symlink.
The same path-string-vs-kernel mismatch applies to `rename()` (line 990), `delete()` (line
1037), and `download()` (line 1263), which all call `check_access_permission()` and then
operate on the literal path via syscall.
A separate timing bug in `add()`: the inline access check runs *before* `open()` (good) but
`Filemanager.check_access_permission()` is *also* called *after* the file write at line 1104 —
pointless, because the file is already on disk by then.
### 1.3 Threat Model
- Attacker has a valid pgAdmin login (any role suffices — no admin required).
- Attacker can reach the file manager and session cookie surfaces.
- Goal: OS-level command execution as the user running gunicorn/pgAdmin (root in the official
Docker image).
- Out of scope: attackers who already have host shell or Docker volume write access; supply-chain
compromise.
### 1.4 Severity
CVSS v3.1 base score (per the report): **8.8 (High)**.
| Attribute | Value |
|---|---|
| Attack vector | Network |
| Privileges required | Low (any authenticated user) |
| User interaction | None |
| Confidentiality / Integrity / Availability | High / High / High |
### 1.5 Attack chain feasibility — what the report does and does not prove
The report demonstrates two scenarios. Both rely on a primitive the report **does not prove
exists inside pgAdmin**:
- **Scenario A (SERVER_MODE=False):** in this mode `check_access_permission()` returns
immediately without any check (see line 895). An authenticated user can directly upload a
payload to the sessions directory via the file manager. No symlink needed. **This is a
complete in-pgAdmin attack chain when `SERVER_MODE=False`** — and it's the likely vector for
the prior CVE-2024-2044.
- **Scenario B (SERVER_MODE=True):** requires a symlink to be planted inside the user's
storage directory. The report says "create a symlink" but does not describe how to do this
through pgAdmin alone.
A grep over the **entire `web/pgadmin/` tree** (not just file_manager) for symlink-creating
primitives returns zero matches across all of:
| Audit pattern | Matches in `web/pgadmin/` |
|---|---|
| `os.symlink` / `os.link(` / `symbolic_link` / `symlink_to` / `Path.symlink` / `.symlink(` | 0 |
| `tarfile` / `zipfile` / `unpack_archive` / `extractall` / `extract_archive` / `shutil.copytree` (archive extraction can preserve symlinks from a malicious archive) | 0 |
| `subprocess` / `os.popen` / shell-invocation primitives in `file_manager/` | 0 |
The download endpoint serves files; the upload endpoint writes file *bodies*; no
rename/move/extract handler creates links. **Therefore Scenario B is only exploitable when the
attacker has an out-of-band way to plant a symlink** — host shell, Docker volume mount with
shared storage, container escape, or a future pgAdmin feature that introduces a symlink
primitive (e.g., archive extraction). This audit excludes the bundled
`docker-entrypoint.sh`/runtime startup scripts, which run before user authentication and are
not part of the user-facing attack surface.
We close Vulnerability 2 anyway, as defense in depth: (a) prevents future regressions if such a
primitive is introduced, (b) closes the path for deployments where the storage directory is
shared with another writer, (c) the change is small and low-risk. But the spec does **not**
claim Vulnerability 2 alone is a current authenticated-user RCE primitive in
`SERVER_MODE=True`.
### 1.6 Known residual issues not introduced or fixed by this proposal
- **Session-file replay after logout.** A user who saves a copy of their valid, HMAC'd session
file and the matching cookie can `cp` the file back into the sessions directory after
logout-driven deletion, regaining their session. The current code has this property; the
Phase 1 design preserves it. Closing it requires a per-session-revision nonce that's
invalidated server-side on logout — out of scope for this proposal, tracked as a follow-up.
- **Read-side information disclosure via symlink.** `download()` at line 1263 calls
`check_access_permission()` and `send_from_directory()`. Once `check_access_permission()`
uses `realpath()`, symlink-based reads are also blocked at the access-check layer. The
underlying TOCTOU caveat (§4.2.3) applies symmetrically to read.
- **CSRF-token theft via session-file read (Scenario B step 3 of the report).** The report's
Scenario B requires the attacker to read the victim's session file from disk to extract the
raw `csrf_token`. This requires file-read access to the sessions directory — either via the
same out-of-band primitive needed to plant a symlink (host shell, Docker volume mount), or
via the file manager's `download()` endpoint pointed at the sessions directory through a
symlink. The realpath fix in §4.2.1 closes the symlink-based read path. Out-of-band
file-read access remains out of scope (§1.3 threat model). After this proposal lands, the
attacker's only remaining path to a session file's contents is host-level access, which
also yields easier RCE primitives directly.
- **Residual `pickle.loads` callsites that operate on session-stored bytes (Phase 2 scope).**
After PR 1's HMAC-header gate on the session file, no attacker-controlled bytes can reach
`pickle.loads` via the session-on-disk path. However, several modules unpickle values that
were themselves placed inside `flask.session` by pgAdmin's own write paths. These are safe
*as long as the write paths cannot be influenced by attackers*, but they remain
insecure-deserialization vectors that should be eliminated in Phase 2 / PR 7. Enumerated
here so future reviewers see the surface:
- `web/pgadmin/tools/sqleditor/__init__.py`: `import pickle` at line 12; `pickle.loads`
at lines 320, 795, 854, 1857, 1935; `pickle.dumps` at lines 334, 628, 962, 1178, 1235,
1348, 1642, 1698, 1739, 1783, 1992 (the `command_obj` round-trip).
- `web/pgadmin/tools/sqleditor/utils/start_running_query.py`: lines 12, 53, 207.
- `web/pgadmin/tools/sqleditor/utils/query_tool_connection_check.py`: lines 11, 33.
- `web/pgadmin/tools/sqleditor/utils/filter_dialog.py`: lines 11, 97.
- `web/pgadmin/tools/schema_diff/__init__.py`: `import pickle` at line 12; `pickle.loads`
at line 184; `pickle.dumps` at lines 197, 225 (the `diff_model_obj` round-trip).
- `web/pgadmin/misc/bgprocess/processes.py`: `from pickle import dumps, loads` at line 20;
used at lines 162, 164, 251, 704, 706 to round-trip the BatchProcess descriptor stored
in the SQLite `process` table. This one is interesting because it deserializes from the
database, not the session — same risk class but a different threat surface (host-local
DB write access).
---
## 2. Goals
1. **Close both vulnerabilities** in shippable, independently reviewable units of work.
2. **Eliminate `pickle` from the pgAdmin web codebase entirely** as a long-term defence-in-depth
measure, not just from `session.py`. The same module is also used inside the cloud provider
handlers and the SQL editor's grid state.
3. **No regressions** in: login (internal, OAuth2, LDAP, Kerberos, Webserver, MFA), cloud
deployment wizards (RDS, Azure, Google, BigAnimal), Query Tool, Schema Diff, Debugger.
4. **No silent data loss** for active sessions on upgrade where avoidable. Sessions written by
pre-fix code will be invalidated (one-time re-login). Operators are alerted via a single
warning log line per rejected sid (see §4.3).
## 3. Non-goals
- Changing the cookie format or HMAC algorithm of the cookie.
- Changing the session storage backend (still file-backed with HMAC).
- Refactoring the `Filemanager` class beyond what's needed for the symlink fix.
- Deprecating `SERVER_MODE=False` (orthogonal concern).
- Closing session-file replay-after-logout (§1.6).
---
## 4. Design
### 4.1 Vulnerability 1 — Two-phase fix
#### Phase 1 (urgent): HMAC-first verification with a plaintext header
`pickle` stays as the on-disk format **temporarily**, but is gated behind an HMAC verified from
a plaintext header that lives outside the serialized blob.
**File format (new):**
```
+----------------------------------------------------+
| H bytes : hex HMAC over the body, ASCII-encoded |
+----------------------------------------------------+
| N bytes : pickle body — (randval, digest, data) |
+----------------------------------------------------+
```
`H` is determined by the chosen digest algorithm: `len(hmac.new(...).hexdigest()) ==
2 * digest_size`. For SHA-256, `H = 64`. We use **SHA-256** for the file HMAC regardless of
`config.SESSION_DIGEST_METHOD` (which defaults to SHA-1 — see §8 Open Question 1). The HMAC is
computed with `app.config['SECRET_KEY']` over the body bytes.
**Read path (new):**
```python
import hmac
import hashlib
import pickle
import logging
_FILE_HMAC = hashlib.sha256
_HMAC_HEX_LEN = _FILE_HMAC().digest_size * 2 # 64 for SHA-256
logger = logging.getLogger(__name__)
def _compute_file_hmac(secret, body: bytes) -> bytes:
# Flask's SECRET_KEY may be str or bytes; normalize before HMAC.
key = secret.encode() if isinstance(secret, str) else secret
return hmac.new(key, body, _FILE_HMAC).hexdigest().encode()
def get(self, sid, digest):
fname = safe_join(self.path, sid)
if fname is None or not os.path.exists(fname):
return self.new_session()
try:
with open(fname, 'rb') as f:
header = f.read(_HMAC_HEX_LEN)
body = f.read()
if len(header) != _HMAC_HEX_LEN or len(body) == 0:
# 0-byte file from new_session(), or pre-fix session file. Silently
# discard; Phase 1 deployment expects one-time re-login churn.
return self.new_session()
expected = _compute_file_hmac(self.secret, body)
if not hmac.compare_digest(header, expected):
logger.warning(
"session file rejected: bad file-HMAC for sid=%s", sid[:8]
)
return self.new_session()
randval, hmac_digest, data = pickle.loads(body) # safe: body is verified
except (pickle.UnpicklingError, EOFError, OSError, MemoryError, ValueError, TypeError):
# Narrow catch: silently invalidate corrupted/unreadable session files.
# Programming errors (AttributeError, NameError, etc.) propagate intentionally
# so they show up in tests and logs rather than being masked as new_session().
return self.new_session()
if hmac_digest != digest:
return self.new_session()
return ManagedSession(data, sid=sid, randval=randval, hmac_digest=hmac_digest)
```
**Write path (new):**
```python
def put(self, session):
...
# The existing disk_write_delay and skip_paths skip logic at lines 271-285
# is preserved verbatim — only the format step below changes.
body = pickle.dumps(
(session.randval, session.hmac_digest, dict(session)), -1
)
header = _compute_file_hmac(self.secret, body)
fname = safe_join(self.path, session.sid)
with open(fname, 'wb') as f:
f.write(header)
f.write(body)
```
**Defensive precondition (`__init__`):** raise on empty `SECRET_KEY`:
```python
if not self.secret:
raise RuntimeError("SECRET_KEY must be non-empty")
```
`assert` is **not** acceptable here — Python's `-O` flag strips assertions, which would silently
disable the security-critical check in optimized production builds. Pre-fix code shares the
implicit non-empty assumption, but an empty `SECRET_KEY` reduces the file-HMAC to a known-key
MAC, and a security-driven redesign is the right time to fail fast.
**Performance.** HMAC-SHA256 over typical session bodies (<10 KB) costs <100 µs per request on
modern x86-64. Sessions exceeding 1 MB (rare; large `gridData` with cloud-handler bytes blobs)
will see sub-millisecond overhead. No measurable production impact expected. Confirmed by
inspection — the read path was already `f.read()`-bound on full file bytes, so the additional
work is one `hmac.new(...).hexdigest()` per request.
**Important note on the report's Fix 1.** The report's suggested "HMAC-first" code calls
`pickle.loads(raw)` once just to read the stored HMAC — which is the exact line that fires
the RCE. The plaintext-header design above avoids that: the HMAC is verified against raw bytes
before any deserialization.
**Migration:** files written by the old code (no header) fail the header check and are silently
discarded — affected sessions get a fresh `new_session()`. One-time re-login on upgrade.
Operators see a transient burst of `session file rejected` warnings during rollout, then they
quiesce.
**Empty-file behavior.** `new_session()` at line 218-238 creates a 0-byte placeholder. The next
`get()` reads short (< 64 bytes) header + empty body, falls through to `new_session()`.
Subsequent `put()` writes the full header + body. This matches the existing behavior of the old
code (where `pickle.load` on 0 bytes raised, was caught, and fell through to `new_session()`).
No regression.
**Concurrent `put()` from multiple workers.** Two workers writing the same sid race on the
file. `open(fname, 'wb')` truncates atomically; the worst case is interleaved writes producing
a header/body mismatch. The next `get()` recomputes the file-HMAC, fails verification, logs
the warning, and returns `new_session()`. **Race-tolerant by design** — partial writes can
never produce an executable payload because they cannot satisfy the HMAC. The pre-fix code
shared the same race shape but with `pickle.load` instead of HMAC validation; not a
regression.
**Caching layer.** `CachingSessionManager` (session.py:104-196) wraps
`FileBackedSessionManager`. Cache hits use the cached `ManagedSession.hmac_digest` for the
cookie compare; cache misses go through `parent.get()`, which performs the new file-HMAC
check. The cache is only populated from a verified disk read (or from `new_session()`), so the
file-HMAC check is enforced **at every cache miss** — including LRU evictions (`_normalize`
trims to 80% capacity once size exceeds `num_to_store=1000`) and writes (`put()` invalidates
the cached entry). No additional change needed here.
#### Phase 2 (defence in depth): switch to plain JSON
After Phase 1 ships, refactor every site that stashes a Python object or `pickle.dumps(...)`
bytes inside `flask.session` so that the session contains only data primitives. Once the
session contents are pure JSON-safe types, the on-disk format switches from pickle to plain
JSON, and `pickle` is removed from the web codebase entirely.
The session's purpose is to hold **identity and state pointers**, not live SDK clients. The
existing `__pgsql_server_managers` pattern (`web/pgadmin/utils/driver/psycopg3/server_manager.py:519-530` —
stores `as_dict()` in session, keeps live `ServerManager` instances in module-global state) is
the template.
**Per-site refactor (verified by grep):**
| Site (file:line) | Current | Replacement |
|---|---|---|
| `web/pgadmin/authenticate/__init__.py:113` | `session['auth_obj'] = AuthSourceManager(...)` (live object) | Store source name; reconstruct `AuthSourceManager` in OAuth2 callback (`oauth2.py:55`) |
| `web/pgadmin/misc/cloud/azure/__init__.py:89` | `session['azure']['azure_obj'] = Azure(...)` (live object) | Store `authentication_record_json` + tenant/auth_type/cache_name; rebuild `Azure` per request via process-local cache keyed by sid. **See §5.4 for an unverified-claims caveat.** |
| `web/pgadmin/misc/cloud/rds/__init__.py:73` | `session['aws']['aws_rds_obj'] = pickle.dumps(_rds, -1)` | Store credentials dict; rebuild `RDS` from process-local cache. 1 write, 2 reads (lines 73, 100, 129). |
| `web/pgadmin/misc/cloud/google/__init__.py:110` | `session['google']['google_obj'] = pickle.dumps(...)` (4 write sites: 110, 129, 145, 181) | Store credentials/state dict; rebuild `Google` from process-local cache. |
| `web/pgadmin/misc/cloud/biganimal/__init__.py:73` | `session['biganimal']['provider_obj'] = pickle.dumps(...)` (4 write sites: 60, 73, 95, 106) | Same pattern. |
| `web/pgadmin/tools/sqleditor/__init__.py` (lines 338, 632, 699, 740 write `session['gridData']`; the per-trans `command_obj` field holds `pickle.dumps(transaction, -1)` produced in `tools/sqleditor/utils/start_running_query.py:204-208`, where the local `session` parameter is later persisted via `update_session_grid_transaction`) | serialized `Command` bytes inside a session-resident dict | Store command kind + identifying inputs; rebuild `Command` via `from_state()`; cache PG metadata in process-local TTL cache. **High risk — see §5.4.** |
**Process-local cache pattern (illustrative):**
```python
# Module-level
_azure_cache: dict[str, tuple[float, Azure]] = {}
_AZURE_CACHE_TTL = 600 # seconds
def get_azure_for_session(sid: str) -> Azure:
now = time.monotonic()
cached = _azure_cache.get(sid)
if cached and (now - cached[0]) < _AZURE_CACHE_TTL:
return cached[1]
state = session['azure'] # JSON-safe dict only
obj = Azure.from_state(state) # new constructor
_azure_cache[sid] = (now, obj)
return obj
```
Eviction: lazy on read (TTL check) and on logout (`_azure_cache.pop(sid, None)`).
**Worker-restart UX trade-off (Azure).** Today's behavior pickles the live `Azure` (with its
populated SDK client cache) into the session file, so a worker restart preserves Azure auth
state. After Phase 2, the in-memory client cache is gone on restart. Whether the user is
re-prompted for device-code on the next request depends on whether
`authentication_record_json` is sufficient to silently rebuild the credential. **This is
unverified** (see §5.4) and must be confirmed during the Azure refactor PR before merge. If
re-prompting is unavoidable, that is a UX regression — Phase 1 ships independently and is not
gated on resolving this.
#### Final state
- `web/pgadmin/utils/session.py`: `from json import dumps, loads` replaces `from pickle import
dump, load`. HMAC header retained.
- All `import pickle` and `from pickle import ...` lines in the web tree removed (except in
test fixtures, by deliberate exception).
- CI guard (proposed): `grep -rEn '^\s*(import pickle\b|from pickle\b)' web/pgadmin/ | grep -v
'/tests/'` must return empty.
### 4.2 Vulnerability 2 — `realpath`, syscall hardening, and ordering fix
#### 4.2.1 `check_access_permission` — realpath both sides
At `web/pgadmin/misc/file_manager/__init__.py:913-940` (definition starts at line 913 in the
implemented branch), replace `os.path.abspath()` with `os.path.realpath()` for both
`orig_path` and `in_dir`. The `in_dir` argument must also be
resolved through `realpath()` — otherwise a symlinked storage root would compare unequal to a
resolved target path, breaking legitimate access.
Python 3.9+ semantics (pgAdmin's documented floor — see `web/pgAdmin4.wsgi:14` and
`README.md:23`): `os.path.realpath(path)` resolves symlinks for components that exist and
leaves not-yet-existing tail components as literal text. This is the correct semantics for
upload paths.
**Windows behavior is believed correct in Python 3.9+ but unverified by this proposal.**
CPython release notes indicate `ntpath.realpath` resolves NTFS junctions and symbolic links via
`GetFinalPathNameByHandle`, with several bug fixes landed across 3.10/3.11. Verification on
Windows is a **PR-1 merge gate**: a smoke test creates a junction, calls `realpath`, and asserts
the resolved path matches the target. This test must pass on the lowest supported Python
(3.9.x) before PR 1 merges.
#### 4.2.2 Upload handler `add()` — realpath, ordering, and `O_NOFOLLOW`
Three changes at `web/pgadmin/misc/file_manager/__init__.py:1087-1132` (definition at line
1087 in the implemented branch):
1. Replace `os.path.abspath` with `os.path.realpath` in the inline check (line 1112), and
resolve `the_dir` through `realpath` too (line 1114).
2. Delete the dead post-write `Filemanager.check_access_permission(the_dir, path)` call.
The inline check above already covers it, the file is on disk by the time it
runs, and replicating the same check at two granularities is bug-prone.
3. **Replace `open(new_name, 'wb')` with `os.open(...)` + `O_NOFOLLOW`** (now extracted to
the module-level helper `_open_upload_target` at line 52) to close the leaf-component
TOCTOU gap between the access check and the syscall:
```python
fd = os.open(
new_name,
os.O_WRONLY | os.O_CREAT | os.O_TRUNC
| getattr(os, 'O_NOFOLLOW', 0),
0o600,
)
with os.fdopen(fd, 'wb') as f:
while True:
data = file_obj.read(4194304)
if not data:
break
f.write(data)
```
`O_NOFOLLOW` causes the kernel to refuse to open the path if its **final** component is a
symlink. Combined with `realpath`-based directory resolution at check time, this closes the
**leaf-component race** where another worker plants a symlink at the leaf between check and
open. **Intermediate-component races remain unmitigated** and rely on §1.5 reasoning (no
in-pgAdmin primitive can plant the directory symlinks needed to exploit them).
Windows: `os.O_NOFOLLOW` is not defined on Windows. The `getattr(..., 0)` fallback above makes
the flag a no-op on Windows. This leaves a small residual leaf-component TOCTOU on Windows,
mitigated by the absence of the symlink-creation primitive.
**Intentional mode change (0o644 → 0o600).** The pseudocode passes mode `0o600` to `os.open`,
restricting uploaded files to owner-only. The current code uses `open(name, 'wb')` which
applies the umask-default (typically `0o644`, world-readable). The change is intentional
hardening — uploaded files are user-private session artifacts and have no need for
world-read access — but it **is** a behavioral change. Release notes must call this out so
operators relying on cross-user readability of uploaded files (uncommon) can adjust.
#### 4.2.3 Cover `rename`, `delete`, `download`
These all call `check_access_permission()` and then operate on the literal path via
`os.rename`, `os.rmdir`/`os.remove`, and `send_from_directory` respectively. Once
`check_access_permission()` is realpath-based:
- **`rename` (definition at line 1015 in the implemented branch):** `os.rename` does not follow symlinks at the leaf for either source
or destination — it renames the link itself, not the target. Intermediate-component symlinks
in the destination path *would* route through the link at the kernel level (same shape as
upload TOCTOU). Python's stdlib does not expose `renameat2` flags, so there is no clean
`O_NOFOLLOW` analog for rename. **Residual TOCTOU on intermediate components is accepted**
under §1.5 reasoning (no in-pgAdmin symlink primitive). Realpath-based access check at T1 is
the only layer of defense.
- **`delete` (definition at line 1062):** `os.rmdir` and `os.remove` operate on the leaf,
not the target. No additional change required.
- **`download` (definition at line 1287):** Werkzeug 3.1.* (`requirements.txt:65`) provides
`send_from_directory`, which uses `werkzeug.security.safe_join` for `..`-traversal
protection. `safe_join` does NOT call `realpath` and does NOT protect against symlinks —
verified by reading the werkzeug 3.x source. Our `check_access_permission()` (post-§4.2.1)
is therefore the **sole** symlink defense for downloads. TOCTOU between check and
`send_from_directory`'s file open is symmetric to upload (§4.2.2) and not closeable from
Python without bypassing `send_from_directory`. Residual TOCTOU accepted under §1.5.
A separate latent bug — `Filemanager.check_access_permission(the_dir,
"{}{}".format(path, path))` in `download` — concatenates `path` with itself. This is
unrelated to the security fix and is tracked as a separate cleanup ticket.
#### 4.2.4 `addfolder` (mkdir endpoint)
`addfolder` (definition at line 1256 in the implemented branch) calls
`Filemanager.check_access_permission(user_dir, path+name)` before `os.mkdir(create_path)`. Once §4.2.1 fixes `check_access_permission`, intermediate-
symlink resolution is caught at the access-check layer. `os.mkdir` itself does not create
symlinks — it creates a real directory at the literal path — so even a TOCTOU race cannot
escalate to symlink creation. No additional change required for this endpoint.
#### 4.2.5 Audit — sites covered by the Vulnerability 2 fix
This table enumerates the sites involved in the symlink-escape fix, not every path-handling
site in `file_manager/__init__.py`. Other `open()` / `os.path.*` calls in the file (e.g., file
encoding detection at line 438, `read_file_generator` at line 108) are not security-relevant
to Vuln 2 and are out of scope.
Line numbers below are as of the implemented branch (`fix/pickle-rce`).
| Line | Function / Site | Resolution |
|---|---|---|
| 924 | `check_access_permission` — was `os.path.abspath`, now `os.path.realpath` | §4.2.1 |
| 936 | `check_access_permission` — `in_dir` realpath | §4.2.1 |
| 1112 | `add` (inline upload check) — was `os.path.abspath`, now `os.path.realpath` | §4.2.2 |
| 1119 | `add` — was `open(new_name, 'wb')`, now `_open_upload_target(new_name)` | §4.2.2 |
| 52 | new module-level `_open_upload_target` helper — `os.open` + `O_NOFOLLOW` + 0o600 | §4.2.2 |
| (in `rename`, line 1015) | `os.rename(oldpath_sys, newpath_sys)` | Covered by §4.2.1 realpath; intermediate-component TOCTOU accepted (§4.2.3) |
| (in `delete`, line 1062) | `os.rmdir` / `os.remove` | Leaf-only operation, no additional change (§4.2.3) |
| (in `download`, line 1287) | `send_from_directory` | Covered by §4.2.1 realpath at access check (§4.2.3) |
| (in `addfolder`, line 1256) | `os.mkdir` | Covered by §4.2.1 realpath (§4.2.4) |
All identified sites are covered. **Out-of-scope for this proposal:** `os.path.abspath`
callsites elsewhere in `web/pgadmin/` have not been exhaustively audited. A follow-up
security review is in scope for tracking, but is not gated on this proposal.
### 4.3 Tests
#### 4.3.1 New regression tests
1. **Unsafe-payload session rejection (Vuln 1):** drop a hand-crafted file in the sessions
directory containing a `pickle` payload whose `__reduce__` writes a sentinel file. Issue a
request with that sid in the cookie. Assert (a) the sentinel is **not** created, (b) the
request gets a fresh session, (c) a single `session file rejected: bad file-HMAC`
warning is logged.
2. **Header-tampered session rejection:** write a session with a corrupted HMAC header (flip
one byte). Assert `get()` returns a fresh session, no exception leaks, warning logged.
3. **Empty-file passthrough:** create a 0-byte file at `sessions/<sid>` (the post-`new_session()`
state). Assert `get()` returns a fresh session, **no warning** (this case is expected).
4. **Symlink escape rejection (Vuln 2, SERVER_MODE=True):** create a symlink inside the user's
storage dir pointing to `/tmp` (or another out-of-sandbox path). Attempt an upload. Assert
the upload is denied **and** no file is created at the symlink target.
5. **`O_NOFOLLOW` leaf-symlink rejection (Linux/macOS):** pre-plant a symlink at the upload
target leaf path (no race needed — test the *property*, not the timing). Attempt upload.
Assert `OSError` is raised with `errno.ELOOP` (Linux) or `errno.EMLINK` (macOS) and no file
exists at the symlink target afterward. *Skip on Windows* (no `O_NOFOLLOW`).
6. **Upload happy path:** confirm legitimate uploads still succeed in both `SERVER_MODE=False`
and `SERVER_MODE=True`.
7. **MFA login flow regression:** cover at least one full MFA email and authenticator login
round-trip. The MFA fields are JSON-safe but the test suite should exercise them.
8. **Cookie-HMAC mismatch with valid file-HMAC:** write a session file with a valid file-HMAC
header but a `hmac_digest` inside the body that does NOT match the cookie-supplied digest.
Assert `get()` returns a fresh session, **and** assert the `session file rejected:
bad file-HMAC` warning is **not** logged (this is a cookie-binding mismatch, not a
file-tamper event; current proposal does not log cookie mismatches to avoid noise from
normal cookie-tampering attempts).
9. **Round-trip:** construct a `ManagedSession`, call `session.sign(secret)` to populate
`randval` and `hmac_digest` (a `put()` precondition; `put()` itself calls `sign()` on
first write but a deterministic test should pre-sign), call `put()`, then `get()` with the
matching cookie. Assert the returned `ManagedSession` has the same `data`, `randval`,
`hmac_digest` as the input. Without this, a `put()` regression silently breaks every
session save.
10. **SERVER_MODE=False direct upload to sessions dir:** unit test against
`FileBackedSessionManager.get()` directly (preferred over a full integration test to
avoid Flask/CSRF setup). Write a malicious pickle file to the sessions path that
`safe_join(self.path, sid)` would resolve to. Call `get(sid, fake_digest)`. Assert
(a) `pickle.loads` does not execute the payload (sentinel not created), (b) `get()`
returns a fresh session, (c) the file-HMAC warning is logged. This validates the
Scenario A chain from §1.5 is closed at the session-read layer regardless of whether
`SERVER_MODE` permits the upload.
#### 4.3.2 Existing tests to update
- `web/pgadmin/browser/tests/test_oauth2_with_mocking.py` — uses `session['oauth2_token']`
(already a dict); verify it continues to pass.
- Cloud-wizard tests using legacy serialized fixtures — update to dict fixtures during the
per-provider PR.
#### 4.3.3 Manual verification
- All four cloud deployment wizards end-to-end (RDS, Azure, Google, BigAnimal).
- OAuth2 login round-trip with a real provider.
- Query Tool: execute, page, edit cell, transactions, savepoint, BYTEA result.
- Debugger: at least one direct-debug session.
---
## 5. Blast Radius
### 5.1 Phase 1 (PR 1)
| Aspect | Estimate |
|---|---|
| Source files modified | 2 (`web/pgadmin/utils/session.py`, `web/pgadmin/misc/file_manager/__init__.py`) |
| Test files added/modified | ~2 |
| LOC changed | Source: ~80–110 (session HMAC logic ~50–80, file_manager realpath/O_NOFOLLOW/coverage ~30). Tests: ~250–400 (10 regression tests in §4.3.1, ~25–40 LOC each plus shared fixtures). **Total ~350–500.** |
| Behavioural change | Sessions written by old code become unreadable → users re-login once on upgrade. One transient warning logged per rejected sid. |
| Risk | **Low.** Self-contained, well-bounded, easy to review. |
### 5.2 Phase 2 — verified site counts
Per-feature breakdown (all numbers verified by grep):
| Feature | Files | `pickle.dumps` writes | `pickle.loads` reads | Risk | Rationale |
|---|---|---|---|---|---|
| `auth_obj` (OAuth2 login) | `authenticate/__init__.py`, `authenticate/oauth2.py` | 0 (live object stored directly) | 0 (live object read directly) | Medium | Login path — regression locks users out. Test against a real OAuth2 provider before merge. |
| Azure cloud | `misc/cloud/azure/__init__.py` | 0 (live object stored directly at line 89) | 0 (read directly at lines 117, 138, 152, 164, 175, 189, 202, 216, 228, 692, …) | **High (verification gated)** | See §5.4. |
| AWS RDS cloud | `misc/cloud/rds/__init__.py` | 1 (line 73) | 2 (lines 100, 129) | Medium | Few sites; boto3 client recreation should be cheap (assumed — verify per §5.4). |
| Google cloud | `misc/cloud/google/__init__.py` | 4 (lines 110, 129, 145, 181) | 10 (lines 95, 127, 143, 161, 179, 199, 220, 241, 307, …) | Medium | Refactor: 1 `from_state()` constructor + ~10 callsites converted from `pickle.loads(...)` to `get_google_for_session(sid)`. |
| BigAnimal cloud | `misc/cloud/biganimal/__init__.py` | 4 (lines 60, 73, 95, 106) | 11 (lines 57, 83, 93, 104, 115, 125, 137, 150, 162, 528, …) | Medium | Same shape as Google. |
| SQL editor — `command_obj` producer | `tools/sqleditor/utils/start_running_query.py:204-208` | 1 (`pickle.dumps` at line 207, into a per-trans dict later persisted via `update_session_grid_transaction`) | 0 | **High (verification gated)** | See §5.4 item 4. |
| SQL editor — Flask-session reads/writes | `tools/sqleditor/__init__.py` | gridData writes at lines 338, 632, 699, 740 | 1 (`pickle.loads` at line 1935) | **High (verification gated)** | The per-trans dict containing `command_obj` lives inside `session['gridData']`. Refactor: store command kind + identifying inputs; rebuild `Command` via `from_state()`; cache PG metadata in process-local TTL cache. |
| Final swap (`session.py` to JSON) | `utils/session.py` | n/a | n/a | Low | Trivial once Phase 2 lands. |
**Total source files touched (Phase 1 + Phase 2):** 8 (files), 2 of which are big files
(`misc/cloud/google/__init__.py`, `misc/cloud/biganimal/__init__.py`).
**Total LOC changed (rough):** 600–900 across Phase 2.
**PR count target:** 6–7 (one per feature plus the final swap).
> **Note on the two SQL-editor rows above:** they describe the same logical refactor but split
> by file. The producer row covers where `pickle.dumps` is called; the Flask-session row
> covers how that pickled blob enters and exits the `flask.session['gridData']` dict. Both
> rows are addressed by a single PR (PR 7).
### 5.3 Failure modes considered
- **Existing session files lost on upgrade:** users re-login once. Expected; called out in
release notes.
- **Process restart drops in-memory caches (cloud SDKs):** users may need to re-authenticate
against the cloud provider on the next request after a worker recycle. **Differs from
today's behavior for Azure** (see §5.4 and §4.1 Phase 2).
- **`session['azure']` grows because state replaces object:** dict holds ~5 primitive fields,
far below the file-write size threshold.
### 5.4 Unverified claims that gate merge of specific PRs
These claims appear in the spec or its rationale and are **not yet evidence-backed**. They must
be verified during the relevant PR before merge:
1. **Azure: `authentication_record_json` is sufficient to silently rebuild the credential
without re-prompting the user for device code.** The class clearly persists the auth record
and supports rebuilding the credential from it (`misc/cloud/azure/__init__.py:298-322`),
*but* whether **all** ~12 Azure endpoints work after a per-request rebuild has not been
end-to-end tested. Verification: run the Azure cloud wizard end-to-end after Phase 2 with
the in-memory cache disabled; confirm no re-prompt.
2. **boto3 RDS clients are cheap to recreate from credentials dict.** Generally true; SSO/STS
token-cache state may complicate it. Verification: run the RDS wizard end-to-end across a
simulated worker restart.
3. **Google Cloud SDK clients are reconstructable from refresh-token credentials.**
Refresh-token flows typically work; OAuth-installed-app flows with one-time auth codes do
not. Verification: run the Google wizard end-to-end across a simulated worker restart.
4. **The `Command` subclass tree in sqleditor is amenable to a `from_state()` constructor.**
The proposal author has not yet read the Command hierarchy. The refactor PR (PR 7) starts
with an audit that may reveal additional design work; the spec marks this PR as **High
risk, scope-revisit on audit**.
**Fallback if PR 7 is intractable:** If the audit concludes the refactor is more than ~2
weeks of work (e.g., the `Command` tree has deep coupling to live cursor state),
the spec resolves the conflict as follows:
- **Option A (preferred):** ship Phase 2 without sqleditor. The CI guard at PR 8 carves
out `web/pgadmin/tools/sqleditor/` as a documented exception, with a TODO and tracking
issue. Concrete encoding: sqleditor stores the pickled `Command` bytes
**base64-encoded into a JSON string field** inside `session['gridData'][trans_id]`.
The outer JSON session body is HMAC-protected by Phase 1's file-HMAC, so the encoded
bytes cannot be tampered with from the disk side. **Caveat:** `pickle.loads` still runs
on those bytes inside sqleditor on every read. If any other code path can write to
`gridData` with attacker-controlled content (an unrelated bug), the RCE primitive
returns. This is a strictly weaker guarantee than full pickle removal — Option A is a
scope-management choice, not a security equivalence.
- **Option B (rejected unless A is infeasible):** keep pickle in `session.py` itself, do
not migrate to JSON, accept the ongoing footgun across the entire session surface.
PR 7 author chooses A or B based on the audit; spec is amended in the PR description with
the rationale.
---
## 6. Implementation Plan
### 6.1 PR sequencing
1. **PR 1 — Close the CVEs.** HMAC-header redesign in `session.py`; in `file_manager`:
`realpath` (§4.2.1), `O_NOFOLLOW` and dead-check removal in upload (§4.2.2),
access-check coverage for `rename`/`delete`/`download` (§4.2.3) and `addfolder` (§4.2.4).
Tests: **all 10 items** in §4.3.1. *Ships first, separately, with a security advisory.*
2. **PR 2 — `auth_obj` data-only refactor.** Removes the live `AuthSourceManager` from the
session.
3. **PR 3 — Azure cloud refactor.** Includes verification step §5.4 item 1.
4. **PR 4 — AWS RDS cloud refactor.** Includes verification §5.4 item 2.
5. **PR 5 — Google cloud refactor.** Includes verification §5.4 item 3.
6. **PR 6 — BigAnimal cloud refactor.**
7. **PR 7 — SQL editor `command_obj` refactor.** Begins with a Command-tree audit
(verification §5.4 item 4); scope may grow.
8. **PR 8 — Final swap: `session.py` to plain JSON, delete `pickle` imports, add CI guard.**
PRs 2–7 are independent and can land in any order. **PR 8 has a hard dependency on PRs 2–7
all having merged** — until every `pickle` site is removed, the JSON swap will fail to
serialize live objects or pickled-bytes blobs.
### 6.2 Rollback strategy
Each PR is reverted independently if a regression is discovered post-merge. PR 1 has no
in-codebase compatibility shim — a revert of PR 1 restores the pre-fix vulnerable behavior,
which is unacceptable. **The team commits to fix-forward only for PR 1: any post-merge
regression is resolved by a same-day hotfix, never by a revert.** This commitment requires
on-call availability during the release window; the security advisory cannot publish without
this commitment in place.
PRs 2–7 each remove a `pickle` site without changing the on-disk format; reverting any of them
is safe and does not re-open RCE (since PR 1 already gates the on-disk read).
PR 8 (the JSON swap) carries upgrade risk for sessions written by PRs 1-7's pickle-with-header
format. Format-detection on read after PR 8: the file-HMAC header is still validated first
(same algorithm), then `json.loads(body)` is attempted. A pre-PR-8 pickle body fails JSON parse
with `JSONDecodeError`, falls through to `new_session()`, and a warning is logged. **No
execution path through `pickle.loads` remains in `session.py` post-PR-8** — a malicious or
legitimate pickle file is rejected at JSON parse, never deserialized. One-time re-login at PR 8
release; warning logged per dropped sid.
### 6.3 Multi-instance / HA note
pgAdmin is typically deployed single-instance. If two replicas share a sessions directory, both
must upgrade together: in **both directions**, the receiver fails to parse the format and
silently invalidates the session (old code reading a header'd file fails on `pickle.load` of
ASCII hex; new code reading a header-less file fails the HMAC length check). No data loss
beyond a one-time re-login per affected user, but operators must coordinate the upgrade window.
Document in release notes.
**Mixed-version Python workers (rare):** `pickle.dumps(..., -1)` selects `HIGHEST_PROTOCOL`
which varies by Python version (3.9 → protocol 5; 3.13 → protocol 5; 3.14 → 5). A protocol
written by a newer worker and read by an older worker may fail to deserialize. Pre-existing
assumption that all workers in a deployment run the same Python; not introduced by this
proposal. After PR 8 (JSON swap), this concern disappears entirely.
### 6.4 Acceptance criteria
**PR 1 (security fix):**
- [ ] All 10 regression tests in §4.3.1 added and passing.
- [ ] Defensive non-empty-`SECRET_KEY` check added in `FileBackedSessionManager.__init__`
(raise, not assert).
- [ ] `_compute_file_hmac` accepts both `str` and `bytes` for `secret`.
- [ ] Round-trip test (§4.3.1 test 9) passes — confirms `put()` produces a file readable by
`get()`.
- [ ] Windows realpath smoke test (§4.2.1) passes on Python 3.9.x.
- [ ] CVE assigned and security advisory drafted; advisory publishes on the PR-1 release.
- [ ] On-call coverage confirmed for PR-1 release window (per §6.2 fix-forward commitment).
**Each of PR 2-7 (per-feature refactor):**
- [ ] Corresponding §5.4 verification item passes (where applicable: Azure → item 1, RDS →
item 2, Google → item 3, sqleditor → item 4; auth_obj and BigAnimal have no §5.4 entry).
- [ ] No regression in the relevant feature's existing test suite.
- [ ] Manual verification per §4.3.3 for the affected feature (cloud wizard end-to-end,
OAuth2 round-trip, or Query Tool sweep).
- [ ] No new `pickle` site introduced in any other file (regression check).
**PR 8 (final pickle removal):**
- [ ] `grep -rEn '^\s*(import pickle\b|from pickle\b)' web/pgadmin/ | grep -v '/tests/'`
returns empty (or returns only the documented sqleditor exception per §5.4 item 4
Option A).
- [ ] All four cloud wizards verified end-to-end in `SERVER_MODE=True` and `SERVER_MODE=False`.
- [ ] OAuth2 round-trip verified against a real provider.
- [ ] Query Tool regression sweep passed (pagination, edit, transactions, BYTEA).
- [ ] Audit table in §9 re-run on the merge commit and the result recorded in the PR
description; any drift from the table in this proposal explained.
---
## 7. Alternatives Considered
### 7.1 Big-bang refactor in one PR
Rejected. Combines an urgent security fix with a multi-feature refactor; a regression in any
cloud wizard would block the security fix from shipping. Review surface is too large for
adequate scrutiny.
### 7.2 Keep pickle, only fix HMAC ordering
Rejected as the long-term answer. Closes the immediate CVE but leaves `pickle` as the de-facto
session format and inside cloud handlers. Any future bug that lets an attacker influence
session contents re-opens RCE. Adopted as **Phase 1** only — buys time for Phase 2.
### 7.3 Use `flask.json.tag.TaggedJSONSerializer`
Rejected for the on-disk session format. `TaggedJSONSerializer` handles `bytes`, `datetime`,
`UUID`, `tuple`, `set` — which would let the in-session serialized blobs (the cloud-handler
`pickle.dumps` outputs) silently survive the JSON switch. We want those *gone*, not preserved.
Plain JSON forces the refactor.
### 7.4 Switch to `itsdangerous.URLSafeTimedSerializer` only (no file backend)
Rejected. Cookie size limits make this impractical for pgAdmin's session contents (especially
`gridData` and server manager state). File-backed sessions stay.
### 7.5 chroot or seccomp-style sandboxing of the worker process
Out of scope. Requires deployment-level changes that vary by environment
(Docker, Kubernetes, Snap, AppImage, systemd, native pip, conda, etc.) and that operators
must opt into per-deployment. Worth tracking separately as a defense-in-depth follow-up.
---
## 8. Open Questions (resolved before approval)
1. **HMAC algorithm for the file header: SHA-256 vs `config.SESSION_DIGEST_METHOD` (default
SHA-1)?**
*Resolution:* hard-code SHA-256 in the file header. The primary rationale is **header-length
predictability**: the file format needs a fixed prefix length decoupled from operator config,
so that file parsing is deterministic. Secondary rationale: SHA-256 is the modern default
and avoids a future deprecation step. The cookie HMAC remains tied to
`SESSION_DIGEST_METHOD` for backward compatibility — that's an unrelated mechanism.
2. **TTL for the process-local SDK client cache?**
*Resolution:* match `permanent_session_lifetime` (Flask config), with passive eviction on
each access.
3. **Migration strategy for in-flight sessions:** silent drop vs. warning per dropped sid.
*Resolution:* warning logged per rejected sid (sid prefix only, to avoid log-leaking
active sids). Provides operator visibility without information disclosure.
4. **CI guard for `pickle`:** add a lint check that fails CI if `import pickle` reappears
outside `web/pgadmin/tests/`?
*Resolution:* yes. Implementation: a single grep-based check in the existing test
makefile, gated on PR 8.
## 9. Audit summary (referenced from §1.5 and §4.2)
This proposal relies on the following grep audits run against the v9.14 branch
(commit-of-record: head of `fix/pickle-rce` at proposal time). Patterns are listed as
ripgrep-compatible regex.
**Reproducer:** `rg --type py 'PATTERN' PATH` (or equivalently
`grep -rEn --include='*.py' 'PATTERN' PATH`). Re-running these audits on the merge commit is
a PR-8 acceptance criterion (§6.4).
**Audit 1 — symlink-creating primitives:**
```regex
os\.symlink|os\.link\(|symbolic_link|symlink_to|Path\.symlink|\.symlink\(
```
Path: `web/pgadmin/`. Result: **0 matches.**
**Audit 2 — archive extraction (could preserve symlinks from a malicious archive):**
```regex
tarfile\.|zipfile\.|shutil\.unpack_archive|shutil\.copytree|extractall|extract_archive
```
Path: `web/pgadmin/`. Result: **0 matches.**
**Audit 3 — shell-invocation primitives in file_manager:**
```regex
subprocess|os\.popen|os\.system|shell=True
```
Path: `web/pgadmin/misc/file_manager/`. Result: **0 matches.**
**Audit 4 — `os.path.abspath` callers in file_manager:**
```regex
os\.path\.abspath
```
Path: `web/pgadmin/misc/file_manager/__init__.py`. Result: **2 matches** at lines 903 and 1086.
**Audit 5 — directory-creation handlers in file_manager:**
```regex
def (addfolder|add_folder|new_folder|mkdir|create_directory|createfolder)
```
Path: `web/pgadmin/misc/file_manager/`. Result: **1 match** — `addfolder` at line 1232.
**Audit 6 — `os.mkdir` / `os.makedirs` in file_manager:**
```regex
os\.mkdir|os\.makedirs
```
Path: `web/pgadmin/misc/file_manager/`. Result: **1 match** at line 1250 (inside `addfolder`).
These results justify the §1.5 conclusion (no in-pgAdmin symlink primitive) and the §4.2.5
conclusion (exhaustive audit of file_manager path-handling sites).
## 10. References
- Security report: 2026-04-29 (private)
- CVE-2024-2044 — pgAdmin RCE in `SERVER_MODE=False`, earlier versions
- CWE-502 — Deserialization of Untrusted Data
- CWE-61 — UNIX Symbolic Link (Symlink) Following
- CWE-22 — Improper Limitation of a Pathname to a Restricted Directory
- OWASP Top 10 — A08:2021 Software and Data Integrity Failures
- Existing pattern: `web/pgadmin/utils/driver/psycopg3/server_manager.py:519-530` —
`as_dict()` + module-global live state
- Linux `open(2)` — `O_NOFOLLOW`: refuse to follow symbolic links at the leaf component
+5 -1
View File
@@ -86,7 +86,11 @@ EOF
#!/bin/sh
echo "Load apparmor pgAdmin profile..."
apparmor_parser -r /etc/apparmor.d/pgadmin4
if command -v apparmor_parser >/dev/null 2>&1; then
apparmor_parser -r /etc/apparmor.d/pgadmin4
else
echo "Warning: apparmor_parser not found, skipping profile load. pgAdmin desktop may not work on Ubuntu 24+ with userns restrictions."
fi
EOF
chmod 755 "${DESKTOPROOT}/DEBIAN/postinst"
fi
+105 -22
View File
@@ -1,12 +1,78 @@
#!/usr/bin/env bash
# Fixup the passwd file, in case we're on OpenShift
if ! whoami > /dev/null 2>&1; then
if [ "$(id -u)" -ne 5050 ]; then
if [ -w /etc/passwd ]; then
echo "${USER_NAME:-pgadminr}:x:$(id -u):0:${USER_NAME:-pgadminr} user:${HOME}:/sbin/nologin" >> /etc/passwd
########################################################################
# PUID/PGID support
#
# When the container runs as root (e.g. --user root), the pgadmin user
# is reassigned to the requested UID/GID and all initialization +
# gunicorn run via su-exec as that user.
#
# When the container runs as non-root (default USER 5050, or OpenShift
# random UID), PUID/PGID are ignored and everything runs as the
# current user.
########################################################################
PUID=${PUID:-5050}
PGID=${PGID:-0}
# Validate PUID/PGID are numeric and in acceptable range
if ! echo "$PUID" | grep -qE '^[0-9]+$'; then
echo "ERROR: PUID must be a numeric value, got '$PUID'"
exit 1
fi
if ! echo "$PGID" | grep -qE '^[0-9]+$'; then
echo "ERROR: PGID must be a numeric value, got '$PGID'"
exit 1
fi
if [ "$PUID" -eq 0 ]; then
echo "ERROR: PUID=0 (root) is not allowed. Use a non-root UID."
exit 1
fi
if [ "$(id -u)" = "0" ]; then
# Ensure a group with the target GID exists
if ! getent group "$PGID" > /dev/null 2>&1; then
if ! addgroup -g "$PGID" pggroup; then
echo "ERROR: Failed to create group with GID=$PGID"
exit 1
fi
fi
# Reassign the pgadmin user to the desired UID/GID
if ! usermod -o -u "$PUID" -g "$PGID" pgadmin; then
echo "ERROR: Failed to set pgadmin user to UID=$PUID GID=$PGID"
exit 1
fi
# Fix ownership of runtime directories BEFORE any initialization
for dir in /run/pgadmin /var/lib/pgadmin; do
if [ -d "$dir" ]; then
chown -R "$PUID:$PGID" "$dir"
fi
done
# Fix ownership of individual files (no -R needed)
if [ -e /pgadmin4/config_distro.py ]; then
chown "$PUID:$PGID" /pgadmin4/config_distro.py
fi
if [ -d /certs ]; then
chown -R "$PUID:$PGID" /certs
fi
SU_EXEC="su-exec $PUID:$PGID"
echo "pgAdmin will run as UID=$PUID, GID=$PGID"
else
SU_EXEC=""
# Fixup the passwd file, in case we're on OpenShift
if ! whoami > /dev/null 2>&1; then
if [ "$(id -u)" -ne 5050 ]; then
if [ -w /etc/passwd ]; then
echo "${USER_NAME:-pgadminr}:x:$(id -u):0:${USER_NAME:-pgadminr} user:${HOME}:/sbin/nologin" >> /etc/passwd
fi
fi
fi
fi
fi
# usage: file_env VAR [DEFAULT] ie: file_env 'XYZ_DB_PASSWORD' 'example'
@@ -23,6 +89,10 @@ function file_env() {
local val="$def"
if [ "${!var:-}" ]; then
val="${!var}"
elif [ "${!fileVar:-}" ] && [ ! -r "${!fileVar}" ]; then
printf >&2 'error: %s is set to "%s" but the file does not exist or is not readable\n' \
"$fileVar" "${!fileVar}"
exit 1
elif [ "${!fileVar:-}" ]; then
val="$(< "${!fileVar}")"
fi
@@ -70,12 +140,17 @@ EOF
esac
echo "${var#PGADMIN_CONFIG_} = $val" >> "${CONFIG_DISTRO_FILE_PATH}"
done
# If running as root with custom config distro path, fix ownership
if [ "$(id -u)" = "0" ] && [ "${CONFIG_DISTRO_FILE_PATH}" != "/pgadmin4/config_distro.py" ]; then
chown "$PUID:$PGID" "${CONFIG_DISTRO_FILE_PATH}"
fi
fi
# Check whether the external configuration database exists if it is being used.
external_config_db_exists="False"
if [ -n "${PGADMIN_CONFIG_CONFIG_DATABASE_URI}" ]; then
external_config_db_exists=$(cd /pgadmin4/pgadmin/utils && /venv/bin/python3 -c "from check_external_config_db import check_external_config_db; val = check_external_config_db("${PGADMIN_CONFIG_CONFIG_DATABASE_URI}"); print(val)")
external_config_db_exists=$(cd /pgadmin4/pgadmin/utils && $SU_EXEC /venv/bin/python3 -c "from check_external_config_db import check_external_config_db; val = check_external_config_db(\"${PGADMIN_CONFIG_CONFIG_DATABASE_URI}\"); print(val)")
fi
# DRY of the code to load the PGADMIN_SERVER_JSON_FILE
@@ -92,9 +167,9 @@ function load_server_json_file() {
# When running in Desktop mode, no user is created
# so we have to import servers anonymously
if [ "${PGADMIN_CONFIG_SERVER_MODE}" = "False" ]; then
/venv/bin/python3 /pgadmin4/setup.py load-servers "${PGADMIN_SERVER_JSON_FILE}" ${EXTRA_ARGS}
$SU_EXEC /venv/bin/python3 /pgadmin4/setup.py load-servers "${PGADMIN_SERVER_JSON_FILE}" ${EXTRA_ARGS}
else
/venv/bin/python3 /pgadmin4/setup.py load-servers "${PGADMIN_SERVER_JSON_FILE}" --user "${PGADMIN_DEFAULT_EMAIL}" ${EXTRA_ARGS}
$SU_EXEC /venv/bin/python3 /pgadmin4/setup.py load-servers "${PGADMIN_SERVER_JSON_FILE}" --user "${PGADMIN_DEFAULT_EMAIL}" ${EXTRA_ARGS}
fi
fi
}
@@ -120,7 +195,7 @@ if [ ! -f /var/lib/pgadmin/pgadmin4.db ] && [ "${external_config_db_exists}" = "
fi
email_config="{'CHECK_EMAIL_DELIVERABILITY': ${CHECK_EMAIL_DELIVERABILITY}, 'ALLOW_SPECIAL_EMAIL_DOMAINS': ${ALLOW_SPECIAL_EMAIL_DOMAINS}, 'GLOBALLY_DELIVERABLE': ${GLOBALLY_DELIVERABLE}}"
echo "email config is ${email_config}"
is_valid_email=$(cd /pgadmin4/pgadmin/utils && /venv/bin/python3 -c "from validation_utils import validate_email; val = validate_email('${PGADMIN_DEFAULT_EMAIL}', ${email_config}); print(val)")
is_valid_email=$(cd /pgadmin4/pgadmin/utils && $SU_EXEC /venv/bin/python3 -c "from validation_utils import validate_email; val = validate_email('${PGADMIN_DEFAULT_EMAIL}', ${email_config}); print(val)")
if echo "${is_valid_email}" | grep "False" > /dev/null; then
echo "'${PGADMIN_DEFAULT_EMAIL}' does not appear to be a valid email address. Please reset the PGADMIN_DEFAULT_EMAIL environment variable and try again."
echo "Validation output: ${is_valid_email}"
@@ -136,7 +211,7 @@ if [ ! -f /var/lib/pgadmin/pgadmin4.db ] && [ "${external_config_db_exists}" = "
# Initialize DB before starting Gunicorn
# Importing pgadmin4 (from this script) is enough
/venv/bin/python3 run_pgadmin.py
$SU_EXEC /venv/bin/python3 run_pgadmin.py
export PGADMIN_PREFERENCES_JSON_FILE="${PGADMIN_PREFERENCES_JSON_FILE:-/pgadmin4/preferences.json}"
@@ -146,22 +221,30 @@ if [ ! -f /var/lib/pgadmin/pgadmin4.db ] && [ "${external_config_db_exists}" = "
# Pre-load any required preferences
if [ -f "${PGADMIN_PREFERENCES_JSON_FILE}" ]; then
if [ "${PGADMIN_CONFIG_SERVER_MODE}" = "False" ]; then
DESKTOP_USER=$(cd /pgadmin4 && /venv/bin/python3 -c 'import config; print(config.DESKTOP_USER)')
/venv/bin/python3 /pgadmin4/setup.py set-prefs "${DESKTOP_USER}" --input-file "${PGADMIN_PREFERENCES_JSON_FILE}"
DESKTOP_USER=$(cd /pgadmin4 && $SU_EXEC /venv/bin/python3 -c 'import config; print(config.DESKTOP_USER)')
$SU_EXEC /venv/bin/python3 /pgadmin4/setup.py set-prefs "${DESKTOP_USER}" --input-file "${PGADMIN_PREFERENCES_JSON_FILE}"
else
/venv/bin/python3 /pgadmin4/setup.py set-prefs "${PGADMIN_DEFAULT_EMAIL}" --input-file "${PGADMIN_PREFERENCES_JSON_FILE}"
$SU_EXEC /venv/bin/python3 /pgadmin4/setup.py set-prefs "${PGADMIN_DEFAULT_EMAIL}" --input-file "${PGADMIN_PREFERENCES_JSON_FILE}"
fi
fi
# Copy the pgpass file passed using secrets
if [ -f "${PGPASS_FILE}" ]; then
if [ -n "${PGPASS_FILE}" ] && [ -f "${PGPASS_FILE}" ]; then
if [ "${PGADMIN_CONFIG_SERVER_MODE}" = "False" ]; then
cp ${PGPASS_FILE} /var/lib/pgadmin/.pgpass
cp "${PGPASS_FILE}" /var/lib/pgadmin/.pgpass
chmod 600 /var/lib/pgadmin/.pgpass
# Fix ownership when running as root
if [ "$(id -u)" = "0" ]; then
chown "$PUID:$PGID" /var/lib/pgadmin/.pgpass
fi
else
PGADMIN_USER_CONFIG_DIR=$(echo "${PGADMIN_DEFAULT_EMAIL}" | sed 's/@/_/g')
mkdir -p /var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}
cp ${PGPASS_FILE} /var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}/.pgpass
chmod 600 /var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}/.pgpass
mkdir -p "/var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}"
cp "${PGPASS_FILE}" "/var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}/.pgpass"
chmod 600 "/var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}/.pgpass"
# Fix ownership when running as root
if [ "$(id -u)" = "0" ]; then
chown -R "$PUID:$PGID" "/var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}"
fi
fi
fi
# If already initialised and PGADMIN_REPLACE_SERVERS_ON_STARTUP is set to true, then load the server json file.
@@ -176,7 +259,7 @@ fi
# Get the session timeout from the pgAdmin config. We'll use this (in seconds)
# to define the Gunicorn worker timeout
TIMEOUT=$(cd /pgadmin4 && /venv/bin/python3 -c 'import config; print(config.SESSION_EXPIRATION_TIME * 60 * 60 * 24)')
TIMEOUT=$(cd /pgadmin4 && $SU_EXEC /venv/bin/python3 -c 'import config; print(config.SESSION_EXPIRATION_TIME * 60 * 60 * 24)')
# NOTE: currently pgadmin can run only with 1 worker due to sessions implementation
# Using --threads to have multi-threaded single-process worker
@@ -192,7 +275,7 @@ else
fi
if [ -n "${PGADMIN_ENABLE_TLS}" ]; then
exec /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --timeout "${TIMEOUT}" --bind "${BIND_ADDRESS}" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" --keyfile /certs/server.key --certfile /certs/server.cert -c gunicorn_config.py run_pgadmin:app
exec $SU_EXEC /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --timeout "${TIMEOUT}" --bind "${BIND_ADDRESS}" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" --keyfile /certs/server.key --certfile /certs/server.cert -c gunicorn_config.py run_pgadmin:app
else
exec /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --limit-request-fields "${GUNICORN_LIMIT_REQUEST_FIELDS:-100}" --limit-request-field_size "${GUNICORN_LIMIT_REQUEST_FIELD_SIZE:-8190}" --timeout "${TIMEOUT}" --bind "${BIND_ADDRESS}" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" -c gunicorn_config.py run_pgadmin:app
exec $SU_EXEC /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --limit-request-fields "${GUNICORN_LIMIT_REQUEST_FIELDS:-100}" --limit-request-field_size "${GUNICORN_LIMIT_REQUEST_FIELD_SIZE:-8190}" --timeout "${TIMEOUT}" --bind "${BIND_ADDRESS}" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" -c gunicorn_config.py run_pgadmin:app
fi
+2 -2
View File
@@ -1,7 +1,7 @@
apiVersion: v2
name: pgadmin4-helm
version: 9.12.0
appVersion: 9.12.0
version: 9.15.0
appVersion: 9.15.0
description: pgAdmin is the most popular and feature rich Open Source administration and development platform for PostgreSQL, the most advanced Open Source database in the world.
home: https://pgadmin.org
sources: ["https://github.com/pgadmin-org/pgadmin4/tree/master/helm"]
+2 -2
View File
@@ -140,7 +140,7 @@ spec:
mountPath: /pgadmin4/config_distro.py
subPath: config_distro.py
- name: empty-dir
mountPath: /usr/bin/python3
mountPath: /usr/local/bin/python3
subPath: python3
- name: empty-dir
mountPath: /tmp
@@ -214,7 +214,7 @@ spec:
image: {{ template "pgadmin4.image" . }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
command: ["sh", "-x", "-c"]
args: ['ls /usr/bin/python3.* | sort -V -r | head -n 1 | xargs -i cp {} python3']
args: ['ls /venv/bin/python3.* | sort -V -r | head -n 1 | xargs -i cp {} python3']
workingDir: /emptyDir
volumeMounts:
- name: empty-dir
+2 -2
View File
@@ -112,8 +112,8 @@ if [ ${IS_DEBIAN} == 1 ]; then
case ${RESPONSE} in
y|Y )
a2enmod wsgi 1> /dev/null
a2enconf pgadmin4 1> /dev/null
/usr/sbin/a2enmod wsgi 1> /dev/null
/usr/sbin/a2enconf pgadmin4 1> /dev/null
;;
* )
exit 1;;
+20 -15
View File
@@ -12,15 +12,17 @@
# relevant packages.
###############################################################################
Authlib==1.6.6
azure-identity==1.25.1
Authlib==1.6.*; python_version <= '3.9'
Authlib==1.7.*; python_version > '3.9'
azure-identity==1.25.3
azure-mgmt-rdbms==10.1.1
azure-mgmt-resource==24.0.0
azure-mgmt-resource==25.0.0
azure-mgmt-subscription==3.1.1
bcrypt==5.0.*
boto3==1.42.*
certifi==2026.1.4
cryptography==46.0.*
boto3==1.42.*; python_version <= '3.9'
boto3==1.43.*; python_version > '3.9'
certifi==2026.4.22
cryptography==47.0.*
Flask-Babel==4.0.*
Flask-Compress==1.*
Flask-Login==0.*
@@ -28,14 +30,16 @@ Flask-Mail==0.*
Flask-Migrate==4.*
Flask-Paranoid==0.*
Flask-Security-Too==5.4.*; python_version <= '3.9'
Flask-Security-Too==5.7.*; python_version > '3.9'
Flask-Security-Too==5.8.*; python_version > '3.9'
Flask-SocketIO==5.6.*
Flask-SQLAlchemy==3.1.*
Flask-WTF==1.2.*
Flask-WTF==1.2.*; python_version <= '3.9'
Flask-WTF==1.3.*; python_version > '3.9'
Flask==3.1.*
google-api-python-client==2.*
google-auth-oauthlib==1.2.4
gssapi==1.10.*
google-auth-oauthlib==1.3.1; python_version <= '3.9'
google-auth-oauthlib==1.4.0; python_version > '3.9'
gssapi==1.11.*
jsonformatter~=0.3.4
keyring==25.*
ldap3==2.*
@@ -44,21 +48,22 @@ paramiko==3.5.1
passlib==1.*
psutil==7.2.*
psycopg[c]==3.2.13; python_version < '3.10'
psycopg[c]==3.3.2; python_version >= '3.10'
psycopg[c]==3.3.4; python_version >= '3.10'
pyotp==2.*
python-dateutil==2.*
pytz==2025.*
pytz==2026.*
# pinned for https://github.com/andfoy/pywinpty/issues/545
pywinpty==2.0.*; sys_platform=="win32"
qrcode[pil]==8.*
setuptools==80.*
setuptools==82.*
SQLAlchemy==2.*
sqlparse==0.*
sshtunnel==0.*
typer[all]==0.21.*
typer[all]==0.23.*; python_version <= '3.9'
typer==0.25.*; python_version > '3.9'
urllib3==1.26.*; python_version <= '3.9'
urllib3==2.6.*; python_version > '3.9'
user-agents==2.2.0
Werkzeug==3.1.*
WTForms==3.1.*; python_version <= '3.9'
WTForms==3.2.*; python_version > '3.9'
WTForms==3.2.*; python_version > '3.9'
+7 -5
View File
@@ -12,13 +12,15 @@
},
"packageManager": "yarn@4.9.2",
"devDependencies": {
"electron": "40.0.0",
"eslint": "^9.39.2",
"eslint-plugin-unused-imports": "^4.2.0"
"@eslint/js": "^10.0.1",
"electron": "^41.5.0",
"eslint": "^10.4.0",
"eslint-plugin-unused-imports": "^4.4.1",
"globals": "^17.6.0"
},
"dependencies": {
"axios": "^1.13.3",
"electron-context-menu": "^4.1.0",
"axios": "^1.16.1",
"electron-context-menu": "^4.1.2",
"electron-store": "^11.0.2"
}
}
+11 -2
View File
@@ -1,3 +1,12 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2026, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { autoUpdater, ipcMain } from 'electron';
import { refreshMenus } from './menu.js';
import * as misc from './misc.js';
@@ -76,7 +85,7 @@ function handleSendDataForAppUpdate({
data.product_name
) {
const ftpUrl = encodeURIComponent(
`${data.auto_update_url}/pgadmin4-${data.upgrade_version}-${process.arch}.zip`
`${data.auto_update_url}/pgadmin4-${data.upgrade_version}-${process.arch}.zip`,
);
let serverUrl = `${baseUrl}/misc/auto_update/${data.current_version_int}/${data.upgrade_version}/${data.upgrade_version_int}/${data.product_name}/${ftpUrl}/?key=${UUID}`;
@@ -122,7 +131,7 @@ export function setupAutoUpdater({
baseUrl,
UUID,
forceQuitAndInstallUpdate,
})
}),
);
}
}
+1 -1
View File
@@ -47,7 +47,7 @@ function updateProgress(callerWindow) {
return;
}
setBadge(Object.keys(downloadQueue).length);
let progress = 0;
let progress;
if(Object.values(downloadQueue).some((item) => item.total === null)) {
// If any of the items in the queue does not have a total, we cannot calculate progress
// so we return 2 to indicate that the progress is indeterminate.
+6 -6
View File
@@ -135,11 +135,11 @@ function buildMenu(pgadminMenus, pgAdminMainScreen, configStore, callbacks) {
{ type: 'separator' },
...(isMac
? [
{ role: 'hide' },
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
]
{ role: 'hide' },
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
]
: []),
{ role: 'quit' },
],
@@ -172,7 +172,7 @@ function buildMenu(pgadminMenus, pgAdminMainScreen, configStore, callbacks) {
{ type: 'separator' },
].concat(isLinux ? [] : [{ role: 'togglefullscreen' }]),
},
{ role: 'windowMenu' }
{ role: 'windowMenu' },
);
template.push(pgadminMenus[pgadminMenus.length - 1]);
+4 -8
View File
@@ -100,17 +100,13 @@ const serverLogFile = path.join(getLocalAppDataPath(), 'pgadmin4.' + (new Date()
// This function is used to read the file and return the content
export const readServerLog = () => {
let data = null;
if (fs.existsSync(serverLogFile)) {
data = fs.readFileSync(serverLogFile, 'utf8');
} else {
let errMsg = 'Unable to read file ' + serverLogFile + '.';
console.warn(errMsg);
return errMsg;
return fs.readFileSync(serverLogFile, 'utf8');
}
return data;
let errMsg = 'Unable to read file ' + serverLogFile + '.';
console.warn(errMsg);
return errMsg;
};
// This function is used to write the data into the log file
+2 -2
View File
@@ -372,7 +372,7 @@ function launchPgAdminWindow() {
setupMenu(pgAdminMainScreen, configStore, menuCallbacks);
setupDownloader()
setupDownloader();
pgAdminMainScreen.loadURL(startPageUrl);
@@ -452,7 +452,7 @@ function notifyUpdateInstalled() {
}, 10000);
} else {
// If main screen not ready, wait and send after it's created
app.once('browser-window-created', (event, window) => {
app.once('browser-window-created', (_event, _window) => {
misc.writeServerLog('[Auto-Updater]: Update installed successfully.');
setTimeout(() => {
pgAdminMainScreen.webContents.send('notifyAppAutoUpdate', {update_installed: true});
+209 -311
View File
@@ -35,80 +35,68 @@ __metadata:
languageName: node
linkType: hard
"@eslint-community/regexpp@npm:^4.12.1":
"@eslint-community/regexpp@npm:^4.12.2":
version: 4.12.2
resolution: "@eslint-community/regexpp@npm:4.12.2"
checksum: 10c0/fddcbc66851b308478d04e302a4d771d6917a0b3740dc351513c0da9ca2eab8a1adf99f5e0aa7ab8b13fa0df005c81adeee7e63a92f3effd7d367a163b721c2d
languageName: node
linkType: hard
"@eslint/config-array@npm:^0.21.1":
version: 0.21.1
resolution: "@eslint/config-array@npm:0.21.1"
"@eslint/config-array@npm:^0.23.5":
version: 0.23.5
resolution: "@eslint/config-array@npm:0.23.5"
dependencies:
"@eslint/object-schema": "npm:^2.1.7"
"@eslint/object-schema": "npm:^3.0.5"
debug: "npm:^4.3.1"
minimatch: "npm:^3.1.2"
checksum: 10c0/2f657d4edd6ddcb920579b72e7a5b127865d4c3fb4dda24f11d5c4f445a93ca481aebdbd6bf3291c536f5d034458dbcbb298ee3b698bc6c9dd02900fe87eec3c
minimatch: "npm:^10.2.4"
checksum: 10c0/b24833c4c76e78ee075d306cd3f095db46b2db0f90cc13a6ee6e4275f9889731c05bf5403ab5fefb79c756e07ac9184ed0e04570341382f9eccbccc80e6d1a0c
languageName: node
linkType: hard
"@eslint/config-helpers@npm:^0.4.2":
version: 0.4.2
resolution: "@eslint/config-helpers@npm:0.4.2"
"@eslint/config-helpers@npm:^0.6.0":
version: 0.6.0
resolution: "@eslint/config-helpers@npm:0.6.0"
dependencies:
"@eslint/core": "npm:^0.17.0"
checksum: 10c0/92efd7a527b2d17eb1a148409d71d80f9ac160b565ac73ee092252e8bf08ecd08670699f46b306b94f13d22e88ac88a612120e7847570dd7cdc72f234d50dcb4
"@eslint/core": "npm:^1.2.1"
checksum: 10c0/f9af20e8b60b0ba27edb74b8eb40c0c5d51a9bf9baf9e053bb57833a87cb0a1c49b4dfaad88fc24d49c907ad1324c8a0b668684fa9c321351dac4bc9155ec10a
languageName: node
linkType: hard
"@eslint/core@npm:^0.17.0":
version: 0.17.0
resolution: "@eslint/core@npm:0.17.0"
"@eslint/core@npm:^1.2.1":
version: 1.2.1
resolution: "@eslint/core@npm:1.2.1"
dependencies:
"@types/json-schema": "npm:^7.0.15"
checksum: 10c0/9a580f2246633bc752298e7440dd942ec421860d1946d0801f0423830e67887e4aeba10ab9a23d281727a978eb93d053d1922a587d502942a713607f40ed704e
checksum: 10c0/10979b40588ecfef771fcb5013a542a35fb30692cc95a65f3481b0b36fbd89f5679efeb30d57f4eed35203d859aabace2a620177d6c536f71b299a1af2f3398f
languageName: node
linkType: hard
"@eslint/eslintrc@npm:^3.3.1":
version: 3.3.3
resolution: "@eslint/eslintrc@npm:3.3.3"
"@eslint/js@npm:^10.0.1":
version: 10.0.1
resolution: "@eslint/js@npm:10.0.1"
peerDependencies:
eslint: ^10.0.0
peerDependenciesMeta:
eslint:
optional: true
checksum: 10c0/9f3fcaf71ba7fdf65d82e8faad6ecfe97e11801cc3c362b306a88ea1ed1344ae0d35330dddb0e8ad18f010f6687a70b75491b9e01c8af57acd7987cee6b3ec6c
languageName: node
linkType: hard
"@eslint/object-schema@npm:^3.0.5":
version: 3.0.5
resolution: "@eslint/object-schema@npm:3.0.5"
checksum: 10c0/1db337431f520b99e9edda64ef5fafd7ec6a029843eeb608753025125b6649d861d843cffafafd3c4e37926d7d5f9ec0c6a8e3665c13c3da2144e8132892e92e
languageName: node
linkType: hard
"@eslint/plugin-kit@npm:^0.7.1":
version: 0.7.1
resolution: "@eslint/plugin-kit@npm:0.7.1"
dependencies:
ajv: "npm:^6.12.4"
debug: "npm:^4.3.2"
espree: "npm:^10.0.1"
globals: "npm:^14.0.0"
ignore: "npm:^5.2.0"
import-fresh: "npm:^3.2.1"
js-yaml: "npm:^4.1.1"
minimatch: "npm:^3.1.2"
strip-json-comments: "npm:^3.1.1"
checksum: 10c0/532c7acc7ddd042724c28b1f020bd7bf148fcd4653bb44c8314168b5f772508c842ce4ee070299cac51c5c5757d2124bdcfcef5551c8c58ff9986e3e17f2260d
languageName: node
linkType: hard
"@eslint/js@npm:9.39.2":
version: 9.39.2
resolution: "@eslint/js@npm:9.39.2"
checksum: 10c0/00f51c52b04ac79faebfaa65a9652b2093b9c924e945479f1f3945473f78aee83cbc76c8d70bbffbf06f7024626575b16d97b66eab16182e1d0d39daff2f26f5
languageName: node
linkType: hard
"@eslint/object-schema@npm:^2.1.7":
version: 2.1.7
resolution: "@eslint/object-schema@npm:2.1.7"
checksum: 10c0/936b6e499853d1335803f556d526c86f5fe2259ed241bc665000e1d6353828edd913feed43120d150adb75570cae162cf000b5b0dfc9596726761c36b82f4e87
languageName: node
linkType: hard
"@eslint/plugin-kit@npm:^0.4.1":
version: 0.4.1
resolution: "@eslint/plugin-kit@npm:0.4.1"
dependencies:
"@eslint/core": "npm:^0.17.0"
"@eslint/core": "npm:^1.2.1"
levn: "npm:^0.4.1"
checksum: 10c0/51600f78b798f172a9915dffb295e2ffb44840d583427bc732baf12ecb963eb841b253300e657da91d890f4b323d10a1bd12934bf293e3018d8bb66fdce5217b
checksum: 10c0/335b0c1c46fd906cb50bd5ce442b9cee18dc44342ce35c718ba4a63d1aa51d2797f16a517b2f4fe371ccd777b6862fafb2dc8195e00e69197ef4cb17ab32c01b
languageName: node
linkType: hard
@@ -171,7 +159,14 @@ __metadata:
languageName: node
linkType: hard
"@types/estree@npm:^1.0.6":
"@types/esrecurse@npm:^4.3.1":
version: 4.3.1
resolution: "@types/esrecurse@npm:4.3.1"
checksum: 10c0/90dad74d5da3ad27606d8e8e757322f33171cfeaa15ad558b615cf71bb2a516492d18f55f4816384685a3eb2412142e732bbae9a4a7cd2cf3deb7572aa4ebe03
languageName: node
linkType: hard
"@types/estree@npm:^1.0.6, @types/estree@npm:^1.0.8":
version: 1.0.8
resolution: "@types/estree@npm:1.0.8"
checksum: 10c0/39d34d1afaa338ab9763f37ad6066e3f349444f9052b9676a7cc0252ef9485a41c6d81c9c4e0d26e9077993354edf25efc853f3224dd4b447175ef62bdcc86a5
@@ -179,9 +174,9 @@ __metadata:
linkType: hard
"@types/http-cache-semantics@npm:*":
version: 4.0.4
resolution: "@types/http-cache-semantics@npm:4.0.4"
checksum: 10c0/51b72568b4b2863e0fe8d6ce8aad72a784b7510d72dc866215642da51d84945a9459fa89f49ec48f1e9a1752e6a78e85a4cda0ded06b1c73e727610c925f9ce6
version: 4.2.0
resolution: "@types/http-cache-semantics@npm:4.2.0"
checksum: 10c0/82dd33cbe7d4843f1e884a251c6a12d385b62274353b9db167462e7fbffdbb3a83606f9952203017c5b8cabbd7b9eef0cf240a3a9dedd20f69875c9701939415
languageName: node
linkType: hard
@@ -202,20 +197,20 @@ __metadata:
linkType: hard
"@types/node@npm:*":
version: 25.0.10
resolution: "@types/node@npm:25.0.10"
version: 25.5.0
resolution: "@types/node@npm:25.5.0"
dependencies:
undici-types: "npm:~7.16.0"
checksum: 10c0/9edc3c812b487c32c76eebac7c87acae1f69515a0bc3f6b545806d513eb9e918c3217bf751dc93da39f60e06bf1b0caa92258ef3a6dd6457124b2e761e54f61f
undici-types: "npm:~7.18.0"
checksum: 10c0/70c508165b6758c4f88d4f91abca526c3985eee1985503d4c2bd994dbaf588e52ac57e571160f18f117d76e963570ac82bd20e743c18987e82564312b3b62119
languageName: node
linkType: hard
"@types/node@npm:^24.9.0":
version: 24.10.9
resolution: "@types/node@npm:24.10.9"
version: 24.12.0
resolution: "@types/node@npm:24.12.0"
dependencies:
undici-types: "npm:~7.16.0"
checksum: 10c0/e9e436fcd2136bddb1bbe3271a89f4653910bcf6ee8047c4117f544c7905a106c039e2720ee48f28505ef2560e22fb9ead719f28bf5e075fdde0c1120e38e3b2
checksum: 10c0/8b31c0af5b5474f13048a4e77c57f22cd4f8fe6e58c4b6fde9456b0c13f46a5bfaf5744ff88fd089581de9f0d6e99c584e022681de7acb26a58d258c654c4843
languageName: node
linkType: hard
@@ -246,12 +241,21 @@ __metadata:
languageName: node
linkType: hard
"acorn@npm:^8.15.0":
version: 8.15.0
resolution: "acorn@npm:8.15.0"
"acorn@npm:^8.16.0":
version: 8.16.0
resolution: "acorn@npm:8.16.0"
bin:
acorn: bin/acorn
checksum: 10c0/dec73ff59b7d6628a01eebaece7f2bdb8bb62b9b5926dcad0f8931f2b8b79c2be21f6c68ac095592adb5adb15831a3635d9343e6a91d028bbe85d564875ec3ec
checksum: 10c0/c9c52697227661b68d0debaf972222d4f622aa06b185824164e153438afa7b08273432ca43ea792cadb24dada1d46f6f6bb1ef8de9956979288cc1b96bf9914e
languageName: node
linkType: hard
"agent-base@npm:6":
version: 6.0.2
resolution: "agent-base@npm:6.0.2"
dependencies:
debug: "npm:4"
checksum: 10c0/dc4f757e40b5f3e3d674bc9beb4f1048f4ee83af189bae39be99f57bf1f48dde166a8b0a5342a84b5944ee8e6ed1e5a9d801858f4ad44764e84957122fe46261
languageName: node
linkType: hard
@@ -269,46 +273,37 @@ __metadata:
languageName: node
linkType: hard
"ajv@npm:^6.12.4":
version: 6.12.6
resolution: "ajv@npm:6.12.6"
"ajv@npm:^6.14.0":
version: 6.14.0
resolution: "ajv@npm:6.14.0"
dependencies:
fast-deep-equal: "npm:^3.1.1"
fast-json-stable-stringify: "npm:^2.0.0"
json-schema-traverse: "npm:^0.4.1"
uri-js: "npm:^4.2.2"
checksum: 10c0/41e23642cbe545889245b9d2a45854ebba51cda6c778ebced9649420d9205f2efb39cb43dbc41e358409223b1ea43303ae4839db682c848b891e4811da1a5a71
checksum: 10c0/a2bc39b0555dc9802c899f86990eb8eed6e366cddbf65be43d5aa7e4f3c4e1a199d5460fd7ca4fb3d864000dbbc049253b72faa83b3b30e641ca52cb29a68c22
languageName: node
linkType: hard
"ajv@npm:^8.0.0, ajv@npm:^8.17.1":
version: 8.17.1
resolution: "ajv@npm:8.17.1"
version: 8.18.0
resolution: "ajv@npm:8.18.0"
dependencies:
fast-deep-equal: "npm:^3.1.3"
fast-uri: "npm:^3.0.1"
json-schema-traverse: "npm:^1.0.0"
require-from-string: "npm:^2.0.2"
checksum: 10c0/ec3ba10a573c6b60f94639ffc53526275917a2df6810e4ab5a6b959d87459f9ef3f00d5e7865b82677cb7d21590355b34da14d1d0b9c32d75f95a187e76fff35
checksum: 10c0/e7517c426173513a07391be951879932bdf3348feaebd2199f5b901c20f99d60db8cd1591502d4d551dc82f594e82a05c4fe1c70139b15b8937f7afeaed9532f
languageName: node
linkType: hard
"ansi-regex@npm:^6.0.1":
"ansi-regex@npm:^6.2.2":
version: 6.2.2
resolution: "ansi-regex@npm:6.2.2"
checksum: 10c0/05d4acb1d2f59ab2cf4b794339c7b168890d44dda4bf0ce01152a8da0213aca207802f930442ce8cd22d7a92f44907664aac6508904e75e038fa944d2601b30f
languageName: node
linkType: hard
"ansi-styles@npm:^4.1.0":
version: 4.3.0
resolution: "ansi-styles@npm:4.3.0"
dependencies:
color-convert: "npm:^2.0.1"
checksum: 10c0/895a23929da416f2bd3de7e9cb4eabd340949328ab85ddd6e484a637d8f6820d485f53933446f5291c3b760cbc488beb8e88573dd0f9c7daf83dccc8fe81b041
languageName: node
linkType: hard
"ansi-styles@npm:^6.0.0":
version: 6.2.3
resolution: "ansi-styles@npm:6.2.3"
@@ -316,13 +311,6 @@ __metadata:
languageName: node
linkType: hard
"argparse@npm:^2.0.1":
version: 2.0.1
resolution: "argparse@npm:2.0.1"
checksum: 10c0/c5640c2d89045371c7cedd6a70212a04e360fd34d6edeae32f6952c63949e3525ea77dbec0289d8213a99bbaeab5abfa860b5c12cf88a2e6cf8106e90dd27a7e
languageName: node
linkType: hard
"async-function@npm:^1.0.0":
version: 1.0.0
resolution: "async-function@npm:1.0.0"
@@ -345,30 +333,31 @@ __metadata:
linkType: hard
"atomically@npm:^2.0.3":
version: 2.1.0
resolution: "atomically@npm:2.1.0"
version: 2.1.1
resolution: "atomically@npm:2.1.1"
dependencies:
stubborn-fs: "npm:^2.0.0"
when-exit: "npm:^2.1.4"
checksum: 10c0/c352ce2e247e4f9aec4e5f46b9720a3598a8241ba4ac7d067060cfd418051268976fec5c958b9d13cc1139b9f11076a8cbdb7837c2e15ca34d3dcceb9c1000b5
checksum: 10c0/8813decdea834eab9b95c63ae3762355e9182e718b49be50153539bb52f727851f5096ef180f84901572dac31c51cb113a3bf3dda12fa633a16bc58f49ba003d
languageName: node
linkType: hard
"axios@npm:^1.13.3":
version: 1.13.3
resolution: "axios@npm:1.13.3"
"axios@npm:^1.16.1":
version: 1.16.1
resolution: "axios@npm:1.16.1"
dependencies:
follow-redirects: "npm:^1.15.6"
form-data: "npm:^4.0.4"
proxy-from-env: "npm:^1.1.0"
checksum: 10c0/86f0770624d9f14a3f8f8738c8b8f7f7fbb7b0d4ad38757db1de2d71007a0311bc597661c5ff4b4a9ee6350c6956a7282e3a281fcdf7b5b32054e35a8801e2ce
follow-redirects: "npm:^1.16.0"
form-data: "npm:^4.0.5"
https-proxy-agent: "npm:^5.0.1"
proxy-from-env: "npm:^2.1.0"
checksum: 10c0/2f77e37e6552bbff8a772d058fb09500198e9188c6b20dc799d82dbe12a8cb506f6eed4e4e62a9ba612a35cbab496faa26d68f9bff14a53af6d15c3e136391a7
languageName: node
linkType: hard
"balanced-match@npm:^1.0.0":
version: 1.0.2
resolution: "balanced-match@npm:1.0.2"
checksum: 10c0/9308baf0a7e4838a82bbfd11e01b1cb0f0cf2893bc1676c27c2a8c0e70cbae1c59120c3268517a8ae7fb6376b4639ef81ca22582611dbee4ed28df945134aaee
"balanced-match@npm:^4.0.2":
version: 4.0.4
resolution: "balanced-match@npm:4.0.4"
checksum: 10c0/07e86102a3eb2ee2a6a1a89164f29d0dbaebd28f2ca3f5ca786f36b8b23d9e417eb3be45a4acf754f837be5ac0a2317de90d3fcb7f4f4dc95720a1f36b26a17b
languageName: node
linkType: hard
@@ -379,13 +368,12 @@ __metadata:
languageName: node
linkType: hard
"brace-expansion@npm:^1.1.7":
version: 1.1.12
resolution: "brace-expansion@npm:1.1.12"
"brace-expansion@npm:^5.0.5":
version: 5.0.5
resolution: "brace-expansion@npm:5.0.5"
dependencies:
balanced-match: "npm:^1.0.0"
concat-map: "npm:0.0.1"
checksum: 10c0/975fecac2bb7758c062c20d0b3b6288c7cc895219ee25f0a64a9de662dbac981ff0b6e89909c3897c1f84fa353113a721923afdec5f8b2350255b097f12b1f73
balanced-match: "npm:^4.0.2"
checksum: 10c0/4d238e14ed4f5cc9c07285550a41cef23121ca08ba99fa9eb5b55b580dcb6bf868b8210aa10526bdc9f8dc97f33ca2a7259039c4cc131a93042beddb424c48e3
languageName: node
linkType: hard
@@ -428,23 +416,6 @@ __metadata:
languageName: node
linkType: hard
"callsites@npm:^3.0.0":
version: 3.1.0
resolution: "callsites@npm:3.1.0"
checksum: 10c0/fff92277400eb06c3079f9e74f3af120db9f8ea03bad0e84d9aede54bbe2d44a56cccb5f6cf12211f93f52306df87077ecec5b712794c5a9b5dac6d615a3f301
languageName: node
linkType: hard
"chalk@npm:^4.0.0":
version: 4.1.2
resolution: "chalk@npm:4.1.2"
dependencies:
ansi-styles: "npm:^4.1.0"
supports-color: "npm:^7.1.0"
checksum: 10c0/4a3fef5cc34975c898ffe77141450f679721df9dde00f6c304353fa9c8b571929123b26a0e4617bde5018977eb655b31970c297b91b63ee83bb82aeb04666880
languageName: node
linkType: hard
"cli-truncate@npm:^4.0.0":
version: 4.0.0
resolution: "cli-truncate@npm:4.0.0"
@@ -464,22 +435,6 @@ __metadata:
languageName: node
linkType: hard
"color-convert@npm:^2.0.1":
version: 2.0.1
resolution: "color-convert@npm:2.0.1"
dependencies:
color-name: "npm:~1.1.4"
checksum: 10c0/37e1150172f2e311fe1b2df62c6293a342ee7380da7b9cfdba67ea539909afbd74da27033208d01d6d5cfc65ee7868a22e18d7e7648e004425441c0f8a15a7d7
languageName: node
linkType: hard
"color-name@npm:~1.1.4":
version: 1.1.4
resolution: "color-name@npm:1.1.4"
checksum: 10c0/a1a3f914156960902f46f7f56bc62effc6c94e84b2cae157a526b1c1f74b677a47ec602bf68a61abfa2b42d15b7c5651c6dbe72a43af720bc588dff885b10f95
languageName: node
linkType: hard
"combined-stream@npm:^1.0.8":
version: 1.0.8
resolution: "combined-stream@npm:1.0.8"
@@ -489,16 +444,9 @@ __metadata:
languageName: node
linkType: hard
"concat-map@npm:0.0.1":
version: 0.0.1
resolution: "concat-map@npm:0.0.1"
checksum: 10c0/c996b1cfdf95b6c90fee4dae37e332c8b6eb7d106430c17d538034c0ad9a1630cb194d2ab37293b1bdd4d779494beee7786d586a50bd9376fd6f7bcc2bd4c98f
languageName: node
linkType: hard
"conf@npm:^15.0.2":
version: 15.0.2
resolution: "conf@npm:15.0.2"
version: 15.1.0
resolution: "conf@npm:15.1.0"
dependencies:
ajv: "npm:^8.17.1"
ajv-formats: "npm:^3.0.1"
@@ -509,7 +457,7 @@ __metadata:
json-schema-typed: "npm:^8.0.1"
semver: "npm:^7.7.2"
uint8array-extras: "npm:^1.5.0"
checksum: 10c0/77713ffe61e1ec26713fba972b6a4f589a3c45872cf07f0e6499a3e54fe855d2c88bf80e148346b28abef0174e4bffcb98cc0a58dd42d451c936a045bd319113
checksum: 10c0/ba3c74891f48232a7d32e473508c147a431a7bfdd266ebdeaf4e737227568345ea632a42d02e35f13740cb9a54076d79572d31591d6debae3b6bee089fa91c8a
languageName: node
linkType: hard
@@ -533,7 +481,7 @@ __metadata:
languageName: node
linkType: hard
"debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2":
"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2":
version: 4.4.3
resolution: "debug@npm:4.4.3"
dependencies:
@@ -624,14 +572,14 @@ __metadata:
languageName: node
linkType: hard
"electron-context-menu@npm:^4.1.0":
version: 4.1.1
resolution: "electron-context-menu@npm:4.1.1"
"electron-context-menu@npm:^4.1.2":
version: 4.1.2
resolution: "electron-context-menu@npm:4.1.2"
dependencies:
cli-truncate: "npm:^4.0.0"
electron-dl: "npm:^4.0.0"
electron-is-dev: "npm:^3.0.1"
checksum: 10c0/3e2eb5298a311f320734427fadb1cf22ef6685c8d86387d1a2d558b698b079de8eabb4d7a8790cab100cf5eba3607df887846067f5a805d1c011016b9eccdf70
checksum: 10c0/30fc6f1370dd24b1f089911f40895d13fbfc683f534e79a2c17ded832b76a6554180bfa2f165528c5ffea19dcbda6c4ff77ccd63c5ddef382cbaa83fd47f153f
languageName: node
linkType: hard
@@ -663,16 +611,16 @@ __metadata:
languageName: node
linkType: hard
"electron@npm:40.0.0":
version: 40.0.0
resolution: "electron@npm:40.0.0"
"electron@npm:^41.5.0":
version: 41.5.0
resolution: "electron@npm:41.5.0"
dependencies:
"@electron/get": "npm:^2.0.0"
"@types/node": "npm:^24.9.0"
extract-zip: "npm:^2.0.1"
bin:
electron: cli.js
checksum: 10c0/acd42835a64f007817ba43969d7bdd1281f109f26fbf4c6639208a8262b80ee1b89dfb844a30de694baa152ff4afc1be1d1e09b0a69629a57be03f9bbf3f9416
checksum: 10c0/5d311e7d411cc5372a6f1eed644776353a781c63122bfc8e70ef00bf5aaf78293e4bca8ccbbacbd7217c7ce72b35e25e4f9f97ced974d97e24e9912e570ee7c5
languageName: node
linkType: hard
@@ -769,26 +717,28 @@ __metadata:
languageName: node
linkType: hard
"eslint-plugin-unused-imports@npm:^4.2.0":
version: 4.3.0
resolution: "eslint-plugin-unused-imports@npm:4.3.0"
"eslint-plugin-unused-imports@npm:^4.4.1":
version: 4.4.1
resolution: "eslint-plugin-unused-imports@npm:4.4.1"
peerDependencies:
"@typescript-eslint/eslint-plugin": ^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0
eslint: ^9.0.0 || ^8.0.0
eslint: ^10.0.0 || ^9.0.0 || ^8.0.0
peerDependenciesMeta:
"@typescript-eslint/eslint-plugin":
optional: true
checksum: 10c0/aa4d8ebd8bea5fc0ac8d3fdeb0d0ed2dd0fccb96a1f5569bca772f71b4c45edd1b3c32d9754fb175c45ccf4d123725f333bf99380b9896bb678a8d599e290aa3
checksum: 10c0/bef630eedc3c239ca1c0a11c6af60485310e3934bd0819d3eb51e0acabdafc722c97d35457750a957541f5cc6a99aa78abb359eb3837d3702d836b6d24cbd573
languageName: node
linkType: hard
"eslint-scope@npm:^8.4.0":
version: 8.4.0
resolution: "eslint-scope@npm:8.4.0"
"eslint-scope@npm:^9.1.2":
version: 9.1.2
resolution: "eslint-scope@npm:9.1.2"
dependencies:
"@types/esrecurse": "npm:^4.3.1"
"@types/estree": "npm:^1.0.8"
esrecurse: "npm:^4.3.0"
estraverse: "npm:^5.2.0"
checksum: 10c0/407f6c600204d0f3705bd557f81bd0189e69cd7996f408f8971ab5779c0af733d1af2f1412066b40ee1588b085874fc37a2333986c6521669cdbdd36ca5058e0
checksum: 10c0/9fb8bca5a73e5741efb6cec84467027b6cb6f4203ff9b43a938e272c5cd30800bde46a5c20dfd1609f840225f0b62b7673be391b20acadf8658ca9fa4729b3dd
languageName: node
linkType: hard
@@ -799,38 +749,35 @@ __metadata:
languageName: node
linkType: hard
"eslint-visitor-keys@npm:^4.2.1":
version: 4.2.1
resolution: "eslint-visitor-keys@npm:4.2.1"
checksum: 10c0/fcd43999199d6740db26c58dbe0c2594623e31ca307e616ac05153c9272f12f1364f5a0b1917a8e962268fdecc6f3622c1c2908b4fcc2e047a106fe6de69dc43
"eslint-visitor-keys@npm:^5.0.1":
version: 5.0.1
resolution: "eslint-visitor-keys@npm:5.0.1"
checksum: 10c0/16190bdf2cbae40a1109384c94450c526a79b0b9c3cb21e544256ed85ac48a4b84db66b74a6561d20fe6ab77447f150d711c2ad5ad74df4fcc133736bce99678
languageName: node
linkType: hard
"eslint@npm:^9.39.2":
version: 9.39.2
resolution: "eslint@npm:9.39.2"
"eslint@npm:^10.4.0":
version: 10.4.0
resolution: "eslint@npm:10.4.0"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.8.0"
"@eslint-community/regexpp": "npm:^4.12.1"
"@eslint/config-array": "npm:^0.21.1"
"@eslint/config-helpers": "npm:^0.4.2"
"@eslint/core": "npm:^0.17.0"
"@eslint/eslintrc": "npm:^3.3.1"
"@eslint/js": "npm:9.39.2"
"@eslint/plugin-kit": "npm:^0.4.1"
"@eslint-community/regexpp": "npm:^4.12.2"
"@eslint/config-array": "npm:^0.23.5"
"@eslint/config-helpers": "npm:^0.6.0"
"@eslint/core": "npm:^1.2.1"
"@eslint/plugin-kit": "npm:^0.7.1"
"@humanfs/node": "npm:^0.16.6"
"@humanwhocodes/module-importer": "npm:^1.0.1"
"@humanwhocodes/retry": "npm:^0.4.2"
"@types/estree": "npm:^1.0.6"
ajv: "npm:^6.12.4"
chalk: "npm:^4.0.0"
ajv: "npm:^6.14.0"
cross-spawn: "npm:^7.0.6"
debug: "npm:^4.3.2"
escape-string-regexp: "npm:^4.0.0"
eslint-scope: "npm:^8.4.0"
eslint-visitor-keys: "npm:^4.2.1"
espree: "npm:^10.4.0"
esquery: "npm:^1.5.0"
eslint-scope: "npm:^9.1.2"
eslint-visitor-keys: "npm:^5.0.1"
espree: "npm:^11.2.0"
esquery: "npm:^1.7.0"
esutils: "npm:^2.0.2"
fast-deep-equal: "npm:^3.1.3"
file-entry-cache: "npm:^8.0.0"
@@ -840,8 +787,7 @@ __metadata:
imurmurhash: "npm:^0.1.4"
is-glob: "npm:^4.0.0"
json-stable-stringify-without-jsonify: "npm:^1.0.1"
lodash.merge: "npm:^4.6.2"
minimatch: "npm:^3.1.2"
minimatch: "npm:^10.2.4"
natural-compare: "npm:^1.4.0"
optionator: "npm:^0.9.3"
peerDependencies:
@@ -851,22 +797,22 @@ __metadata:
optional: true
bin:
eslint: bin/eslint.js
checksum: 10c0/bb88ca8fd16bb7e1ac3e13804c54d41c583214460c0faa7b3e7c574e69c5600c7122295500fb4b0c06067831111db740931e98da1340329527658e1cf80073d3
checksum: 10c0/6bf644dc08fa5a6b23157d23a4a4638d45823d03a67da1daac8dc1085b03934fa98013efd2eac2cd6ec90fe88d36b336bdf38d5f000325f22d823a15f2031426
languageName: node
linkType: hard
"espree@npm:^10.0.1, espree@npm:^10.4.0":
version: 10.4.0
resolution: "espree@npm:10.4.0"
"espree@npm:^11.2.0":
version: 11.2.0
resolution: "espree@npm:11.2.0"
dependencies:
acorn: "npm:^8.15.0"
acorn: "npm:^8.16.0"
acorn-jsx: "npm:^5.3.2"
eslint-visitor-keys: "npm:^4.2.1"
checksum: 10c0/c63fe06131c26c8157b4083313cb02a9a54720a08e21543300e55288c40e06c3fc284bdecf108d3a1372c5934a0a88644c98714f38b6ae8ed272b40d9ea08d6b
eslint-visitor-keys: "npm:^5.0.1"
checksum: 10c0/cf87e18ffd9dc113eb8d16588e7757701bc10c9934a71cce8b89c2611d51672681a918307bd6b19ac3ccd0e7ba1cbccc2f815b36b52fa7e73097b251014c3d81
languageName: node
linkType: hard
"esquery@npm:^1.5.0":
"esquery@npm:^1.7.0":
version: 1.7.0
resolution: "esquery@npm:1.7.0"
dependencies:
@@ -1001,23 +947,23 @@ __metadata:
linkType: hard
"flatted@npm:^3.2.9":
version: 3.3.3
resolution: "flatted@npm:3.3.3"
checksum: 10c0/e957a1c6b0254aa15b8cce8533e24165abd98fadc98575db082b786b5da1b7d72062b81bfdcd1da2f4d46b6ed93bec2434e62333e9b4261d79ef2e75a10dd538
version: 3.4.1
resolution: "flatted@npm:3.4.1"
checksum: 10c0/3987a7f1e39bc7215cece001354313b462cdb4fb2dde0df4f7acd9e5016fbae56ee6fb3f0870b2150145033be8bda4f01af6f87a00946049651131bbfca7dfa6
languageName: node
linkType: hard
"follow-redirects@npm:^1.15.6":
version: 1.15.11
resolution: "follow-redirects@npm:1.15.11"
"follow-redirects@npm:^1.16.0":
version: 1.16.0
resolution: "follow-redirects@npm:1.16.0"
peerDependenciesMeta:
debug:
optional: true
checksum: 10c0/d301f430542520a54058d4aeeb453233c564aaccac835d29d15e050beb33f339ad67d9bddbce01739c5dc46a6716dbe3d9d0d5134b1ca203effa11a7ef092343
checksum: 10c0/a1e2900163e6f1b4d1ed5c221b607f41decbab65534c63fe7e287e40a5d552a6496e7d9d7d976fa4ba77b4c51c11e5e9f683f10b43011ea11e442ff128d0e181
languageName: node
linkType: hard
"form-data@npm:^4.0.4":
"form-data@npm:^4.0.5":
version: 4.0.5
resolution: "form-data@npm:4.0.5"
dependencies:
@@ -1056,9 +1002,9 @@ __metadata:
linkType: hard
"get-east-asian-width@npm:^1.0.0":
version: 1.4.0
resolution: "get-east-asian-width@npm:1.4.0"
checksum: 10c0/4e481d418e5a32061c36fbb90d1b225a254cc5b2df5f0b25da215dcd335a3c111f0c2023ffda43140727a9cafb62dac41d022da82c08f31083ee89f714ee3b83
version: 1.5.0
resolution: "get-east-asian-width@npm:1.5.0"
checksum: 10c0/bff8bbc8d81790b9477f7aa55b1806b9f082a8dc1359fff7bd8b96939622c86b729685afc2bfeb22def1fc6ef1e5228e4d87dd4e6da60bc43a5edfb03c4ee167
languageName: node
linkType: hard
@@ -1125,10 +1071,10 @@ __metadata:
languageName: node
linkType: hard
"globals@npm:^14.0.0":
version: 14.0.0
resolution: "globals@npm:14.0.0"
checksum: 10c0/b96ff42620c9231ad468d4c58ff42afee7777ee1c963013ff8aabe095a451d0ceeb8dcd8ef4cbd64d2538cef45f787a78ba3a9574f4a634438963e334471302d
"globals@npm:^17.6.0":
version: 17.6.0
resolution: "globals@npm:17.6.0"
checksum: 10c0/cf94fb4329cc5c68cf81018fd68324f413181ee169f0235b0b33b82bc93fe7825a21beea951f83a80e8e4bbdad9c0c80515a145b5fd4b5cb52f2a80db899a93f
languageName: node
linkType: hard
@@ -1175,13 +1121,6 @@ __metadata:
languageName: node
linkType: hard
"has-flag@npm:^4.0.0":
version: 4.0.0
resolution: "has-flag@npm:4.0.0"
checksum: 10c0/2e789c61b7888d66993e14e8331449e525ef42aac53c627cc53d1c3334e768bcb6abdc4f5f0de1478a25beec6f0bd62c7549058b7ac53e924040d4f301f02fd1
languageName: node
linkType: hard
"has-property-descriptors@npm:^1.0.0":
version: 1.0.2
resolution: "has-property-descriptors@npm:1.0.2"
@@ -1233,6 +1172,16 @@ __metadata:
languageName: node
linkType: hard
"https-proxy-agent@npm:^5.0.1":
version: 5.0.1
resolution: "https-proxy-agent@npm:5.0.1"
dependencies:
agent-base: "npm:6"
debug: "npm:4"
checksum: 10c0/6dd639f03434003577c62b27cafdb864784ef19b2de430d8ae2a1d45e31c4fd60719e5637b44db1a88a046934307da7089e03d6089ec3ddacc1189d8de8897d1
languageName: node
linkType: hard
"ignore@npm:^5.2.0":
version: 5.3.2
resolution: "ignore@npm:5.3.2"
@@ -1240,16 +1189,6 @@ __metadata:
languageName: node
linkType: hard
"import-fresh@npm:^3.2.1":
version: 3.3.1
resolution: "import-fresh@npm:3.3.1"
dependencies:
parent-module: "npm:^1.0.0"
resolve-from: "npm:^4.0.0"
checksum: 10c0/bf8cc494872fef783249709385ae883b447e3eb09db0ebd15dcead7d9afe7224dad7bd7591c6b73b0b19b3c0f9640eb8ee884f01cfaf2887ab995b0b36a0cbec
languageName: node
linkType: hard
"imurmurhash@npm:^0.1.4":
version: 0.1.4
resolution: "imurmurhash@npm:0.1.4"
@@ -1294,17 +1233,6 @@ __metadata:
languageName: node
linkType: hard
"js-yaml@npm:^4.1.1":
version: 4.1.1
resolution: "js-yaml@npm:4.1.1"
dependencies:
argparse: "npm:^2.0.1"
bin:
js-yaml: bin/js-yaml.js
checksum: 10c0/561c7d7088c40a9bb53cc75becbfb1df6ae49b34b5e6e5a81744b14ae8667ec564ad2527709d1a6e7d5e5fa6d483aa0f373a50ad98d42fde368ec4a190d4fae7
languageName: node
linkType: hard
"json-buffer@npm:3.0.1":
version: 3.0.1
resolution: "json-buffer@npm:3.0.1"
@@ -1387,13 +1315,6 @@ __metadata:
languageName: node
linkType: hard
"lodash.merge@npm:^4.6.2":
version: 4.6.2
resolution: "lodash.merge@npm:4.6.2"
checksum: 10c0/402fa16a1edd7538de5b5903a90228aa48eb5533986ba7fa26606a49db2572bf414ff73a2c9f5d5fd36b31c46a5d5c7e1527749c07cbcf965ccff5fbdf32c506
languageName: node
linkType: hard
"lowercase-keys@npm:^2.0.0":
version: 2.0.0
resolution: "lowercase-keys@npm:2.0.0"
@@ -1461,12 +1382,12 @@ __metadata:
languageName: node
linkType: hard
"minimatch@npm:^3.1.2":
version: 3.1.2
resolution: "minimatch@npm:3.1.2"
"minimatch@npm:^10.2.4":
version: 10.2.5
resolution: "minimatch@npm:10.2.5"
dependencies:
brace-expansion: "npm:^1.1.7"
checksum: 10c0/0262810a8fc2e72cca45d6fd86bd349eee435eb95ac6aa45c9ea2180e7ee875ef44c32b55b5973ceabe95ea12682f6e3725cbb63d7a2d1da3ae1163c8b210311
brace-expansion: "npm:^5.0.5"
checksum: 10c0/6bb058bd6324104b9ec2f763476a35386d05079c1f5fe4fbf1f324a25237cd4534d6813ecd71f48208f4e635c1221899bef94c3c89f7df55698fe373aaae20fd
languageName: node
linkType: hard
@@ -1546,15 +1467,6 @@ __metadata:
languageName: node
linkType: hard
"parent-module@npm:^1.0.0":
version: 1.0.1
resolution: "parent-module@npm:1.0.1"
dependencies:
callsites: "npm:^3.0.0"
checksum: 10c0/c63d6e80000d4babd11978e0d3fee386ca7752a02b035fd2435960ffaa7219dc42146f07069fb65e6e8bf1caef89daf9af7535a39bddf354d78bf50d8294f556
languageName: node
linkType: hard
"path-exists@npm:^4.0.0":
version: 4.0.0
resolution: "path-exists@npm:4.0.0"
@@ -1587,12 +1499,14 @@ __metadata:
version: 0.0.0-use.local
resolution: "pgadmin4@workspace:."
dependencies:
axios: "npm:^1.13.3"
electron: "npm:40.0.0"
electron-context-menu: "npm:^4.1.0"
"@eslint/js": "npm:^10.0.1"
axios: "npm:^1.16.1"
electron: "npm:^41.5.0"
electron-context-menu: "npm:^4.1.2"
electron-store: "npm:^11.0.2"
eslint: "npm:^9.39.2"
eslint-plugin-unused-imports: "npm:^4.2.0"
eslint: "npm:^10.4.0"
eslint-plugin-unused-imports: "npm:^4.4.1"
globals: "npm:^17.6.0"
languageName: unknown
linkType: soft
@@ -1610,20 +1524,20 @@ __metadata:
languageName: node
linkType: hard
"proxy-from-env@npm:^1.1.0":
version: 1.1.0
resolution: "proxy-from-env@npm:1.1.0"
checksum: 10c0/fe7dd8b1bdbbbea18d1459107729c3e4a2243ca870d26d34c2c1bcd3e4425b7bcc5112362df2d93cc7fb9746f6142b5e272fd1cc5c86ddf8580175186f6ad42b
"proxy-from-env@npm:^2.1.0":
version: 2.1.0
resolution: "proxy-from-env@npm:2.1.0"
checksum: 10c0/ed01729fd4d094eab619cd7e17ce3698b3413b31eb102c4904f9875e677cd207392795d5b4adee9cec359dfd31c44d5ad7595a3a3ad51c40250e141512281c58
languageName: node
linkType: hard
"pump@npm:^3.0.0":
version: 3.0.3
resolution: "pump@npm:3.0.3"
version: 3.0.4
resolution: "pump@npm:3.0.4"
dependencies:
end-of-stream: "npm:^1.1.0"
once: "npm:^1.3.1"
checksum: 10c0/ada5cdf1d813065bbc99aa2c393b8f6beee73b5de2890a8754c9f488d7323ffd2ca5f5a0943b48934e3fcbd97637d0337369c3c631aeb9614915db629f1c75c9
checksum: 10c0/2780e66b5471c19e3e3e1063b84f3f6a3a08367f24c5ed552f98cd5901e6ada27c7ad6495d4244f553fd03b01884a4561933064f053f47c8994d84fd352768ea
languageName: node
linkType: hard
@@ -1664,13 +1578,6 @@ __metadata:
languageName: node
linkType: hard
"resolve-from@npm:^4.0.0":
version: 4.0.0
resolution: "resolve-from@npm:4.0.0"
checksum: 10c0/8408eec31a3112ef96e3746c37be7d64020cda07c03a920f5024e77290a218ea758b26ca9529fd7b1ad283947f34b2291c1c0f6aa0ed34acfdda9c6014c8d190
languageName: node
linkType: hard
"responselike@npm:^2.0.0":
version: 2.0.1
resolution: "responselike@npm:2.0.1"
@@ -1711,11 +1618,11 @@ __metadata:
linkType: hard
"semver@npm:^7.3.2, semver@npm:^7.7.2":
version: 7.7.3
resolution: "semver@npm:7.7.3"
version: 7.7.4
resolution: "semver@npm:7.7.4"
bin:
semver: bin/semver.js
checksum: 10c0/4afe5c986567db82f44c8c6faef8fe9df2a9b1d98098fc1721f57c696c4c21cebd572f297fc21002f81889492345b8470473bc6f4aff5fb032a6ea59ea2bc45e
checksum: 10c0/5215ad0234e2845d4ea5bb9d836d42b03499546ddafb12075566899fc617f68794bb6f146076b6881d755de17d6c6cc73372555879ec7dce2c2feee947866ad2
languageName: node
linkType: hard
@@ -1791,18 +1698,11 @@ __metadata:
linkType: hard
"strip-ansi@npm:^7.1.0":
version: 7.1.2
resolution: "strip-ansi@npm:7.1.2"
version: 7.2.0
resolution: "strip-ansi@npm:7.2.0"
dependencies:
ansi-regex: "npm:^6.0.1"
checksum: 10c0/0d6d7a023de33368fd042aab0bf48f4f4077abdfd60e5393e73c7c411e85e1b3a83507c11af2e656188511475776215df9ca589b4da2295c9455cc399ce1858b
languageName: node
linkType: hard
"strip-json-comments@npm:^3.1.1":
version: 3.1.1
resolution: "strip-json-comments@npm:3.1.1"
checksum: 10c0/9681a6257b925a7fa0f285851c0e613cc934a50661fa7bb41ca9cbbff89686bb4a0ee366e6ecedc4daafd01e83eee0720111ab294366fe7c185e935475ebcecd
ansi-regex: "npm:^6.2.2"
checksum: 10c0/544d13b7582f8254811ea97db202f519e189e59d35740c46095897e254e4f1aa9fe1524a83ad6bc5ad67d4dd6c0281d2e0219ed62b880a6238a16a17d375f221
languageName: node
linkType: hard
@@ -1831,15 +1731,6 @@ __metadata:
languageName: node
linkType: hard
"supports-color@npm:^7.1.0":
version: 7.2.0
resolution: "supports-color@npm:7.2.0"
dependencies:
has-flag: "npm:^4.0.0"
checksum: 10c0/afb4c88521b8b136b5f5f95160c98dee7243dc79d5432db7efc27efb219385bbc7d9427398e43dd6cc730a0f87d5085ce1652af7efbe391327bc0a7d0f7fc124
languageName: node
linkType: hard
"tagged-tag@npm:^1.0.0":
version: 1.0.0
resolution: "tagged-tag@npm:1.0.0"
@@ -1864,11 +1755,11 @@ __metadata:
linkType: hard
"type-fest@npm:^5.0.0, type-fest@npm:^5.0.1":
version: 5.4.1
resolution: "type-fest@npm:5.4.1"
version: 5.4.4
resolution: "type-fest@npm:5.4.4"
dependencies:
tagged-tag: "npm:^1.0.0"
checksum: 10c0/500386d690e634499e6fb765a1e33909a75fea0258acdd56a2d1c9565d6810e222f6d95bd80daa5498377c7ea976af46f518185b5dbd67ff8454562544808aa1
checksum: 10c0/bf9c6d7df5383fd720aac71da8ce8690ff1c554459d19cf3c72d61eac98255dba57abe20c628f91f4116f66211791462fdafa90b2be2d7405a5a4c295e4d849d
languageName: node
linkType: hard
@@ -1886,6 +1777,13 @@ __metadata:
languageName: node
linkType: hard
"undici-types@npm:~7.18.0":
version: 7.18.2
resolution: "undici-types@npm:7.18.2"
checksum: 10c0/85a79189113a238959d7a647368e4f7c5559c3a404ebdb8fc4488145ce9426fcd82252a844a302798dfc0e37e6fb178ff481ed03bc4caf634c5757d9ef43521d
languageName: node
linkType: hard
"universalify@npm:^0.1.0":
version: 0.1.2
resolution: "universalify@npm:0.1.2"
+6 -4
View File
@@ -1,5 +1,7 @@
requests>=2.21.0
requests[security]>=2.21.0
safety>=1.9.0
requests>=2.32.5,<2.33; python_version <= '3.9'
requests>=2.34.2; python_version > '3.9'
requests[security]>=2.32.5,<2.33; python_version <= '3.9'
requests[security]>=2.34.2; python_version > '3.9'
safety>=3.7.0
Sphinx==7.4.7
sphinxcontrib-youtube==1.4.1
sphinxcontrib-youtube==1.5.0
+1 -1
View File
@@ -42,7 +42,7 @@ module.exports = [
'babelOptions': {
'plugins': [
'@babel/plugin-syntax-jsx',
'@babel/plugin-proposal-class-properties',
'@babel/plugin-transform-class-properties',
],
},
...reactjs.configs.recommended.parserOptions,
+5
View File
@@ -1,5 +1,10 @@
approvedGitRepositories:
- "**"
checksumBehavior: update
enableScripts: true
logFilters:
- code: YN0013
level: discard
+1 -1
View File
@@ -2,5 +2,5 @@
"presets": [["@babel/preset-env", {"modules": "commonjs", "useBuiltIns": "usage", "corejs": 3}], ["@babel/preset-react", {
"runtime": "automatic"
}], "@babel/preset-typescript"],
"plugins": ["@babel/plugin-proposal-class-properties", "@babel/proposal-object-rest-spread"]
"plugins": ["@babel/plugin-transform-class-properties", "@babel/plugin-transform-object-rest-spread"]
}
+103 -2
View File
@@ -376,8 +376,16 @@ SESSION_DB_PATH = os.path.join(DATA_DIR, 'sessions')
SESSION_COOKIE_NAME = 'pga4_session'
# Session digest method
SESSION_DIGEST_METHOD = 'hashlib.sha1'
# Session digest method.
# Used as the HMAC algorithm for the session cookie (signing the
# (sid, randval) pair). HMAC-SHA1 is still cryptographically acceptable
# for authentication, but SHA-256 is the modern default and aligns with
# the file-HMAC header introduced for session-on-disk integrity.
# Operators on existing deployments will see all sessions invalidated on
# upgrade regardless (the file-HMAC header is new), so we flip this
# default at the same time rather than leaving SHA-1 as a long-term
# liability.
SESSION_DIGEST_METHOD = 'hashlib.sha256'
##########################################################################
# Mail server settings
@@ -970,6 +978,99 @@ MAX_SERVER_TAGS_ALLOWED = 5
ON_DEMAND_LOG_COUNT = 10000
##########################################################################
# AI/LLM Settings
##########################################################################
# Master switch to enable/disable LLM features entirely.
# When False, all AI/LLM features are disabled and cannot be enabled
# by users through preferences. When True, users can configure their
# preferred LLM provider in preferences.
LLM_ENABLED = True
# Default LLM Provider
# Specifies which LLM provider to use by default when LLM_ENABLED is True.
# Users can override this in their preferences.
# Valid values: 'anthropic', 'openai', 'ollama', 'docker', or '' (disabled)
DEFAULT_LLM_PROVIDER = ''
# Anthropic Configuration
# URL for the Anthropic API endpoint. Leave empty to use the default
# (https://api.anthropic.com/v1). Set a custom URL to use an
# Anthropic-compatible API provider.
ANTHROPIC_API_URL = ''
# Path to a file containing the Anthropic API key. The file should contain
# only the API key with no additional whitespace or formatting.
# Default: ~/.anthropic-api-key
# Note: The API key may be optional when using a custom API URL with a
# provider that does not require authentication.
ANTHROPIC_API_KEY_FILE = '~/.anthropic-api-key'
# The Anthropic model to use for AI features.
# Examples: claude-sonnet-4-20250514, claude-3-5-haiku-20241022
ANTHROPIC_API_MODEL = ''
# OpenAI Configuration
# URL for the OpenAI API endpoint. Leave empty to use the default
# (https://api.openai.com/v1). Set a custom URL to use any
# OpenAI-compatible API provider (e.g., LiteLLM, LM Studio, EXO).
# Include the /v1 path prefix if required by your provider
# (e.g., http://localhost:1234/v1).
OPENAI_API_URL = ''
# Path to a file containing the OpenAI API key. The file should contain
# only the API key with no additional whitespace or formatting.
# Default: ~/.openai-api-key
# Note: The API key may be optional when using a custom API URL with a
# provider that does not require authentication.
OPENAI_API_KEY_FILE = '~/.openai-api-key'
# The OpenAI model to use for AI features.
# Examples: gpt-4o, gpt-4o-mini, gpt-4-turbo
OPENAI_API_MODEL = ''
# Ollama Configuration
# URL for the Ollama API endpoint. Leave empty to disable Ollama.
# Typical value: http://localhost:11434
OLLAMA_API_URL = ''
# The Ollama model to use for AI features.
# Examples: llama3.2, codellama, mistral
OLLAMA_API_MODEL = ''
# Docker Model Runner Configuration
# Docker Desktop 4.40+ includes a built-in model runner with an
# OpenAI-compatible API. No API key is required.
# URL for the Docker Model Runner API endpoint. Leave empty to disable.
# Typical value: http://localhost:12434
# Tip: You can also use the OpenAI provider with a custom API URL for any
# OpenAI-compatible endpoint, including Docker Model Runner.
DOCKER_API_URL = ''
# The Docker Model Runner model to use for AI features.
# Examples: ai/qwen3-coder, ai/llama3.2
DOCKER_API_MODEL = ''
# Allowed LLM API URLs
# A list of scheme://host:port entries that LLM API requests are allowed
# to connect to. Only URLs matching an entry in this list will be permitted.
# This prevents SSRF attacks via user-controlled API URL fields.
# Set to an empty list to disable URL restriction (not recommended).
# Add entries for custom providers (LiteLLM, LM Studio, corporate proxies).
ALLOWED_LLM_API_URLS = [
'https://api.anthropic.com:443',
'https://api.openai.com:443',
'http://localhost:11434', # Ollama default
'http://localhost:12434', # Docker Model Runner default
]
# Maximum Tool Iterations
# The maximum number of tool call iterations allowed during an AI conversation.
# This prevents runaway conversations that could consume excessive resources.
# Users can override this in their preferences.
MAX_LLM_TOOL_ITERATIONS = 20
#############################################################################
# Patch the default config with custom config and other manipulations
#############################################################################
+1 -1
View File
@@ -52,7 +52,7 @@ module.exports = {
],
'testEnvironment': 'jsdom',
'transformIgnorePatterns': [
'[/\\\\]node_modules[/\\\\](?!react-dnd|dnd-core|@react-dnd|react-resize-detector|react-data-grid).+\\.(js|jsx|mjs|cjs|ts|tsx)$',
'[/\\\\]node_modules[/\\\\](?!react-dnd|dnd-core|@react-dnd|react-resize-detector|react-data-grid|marked).+\\.(js|jsx|mjs|cjs|ts|tsx)$',
'^.+\\.module\\.(css|sass|scss)$'
]
};
@@ -0,0 +1,58 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""Add tools_ai permission to existing roles
Revision ID: add_tools_ai_perm
Revises: 018e16dad6aa
Create Date: 2025-12-01
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'add_tools_ai_perm'
down_revision = '018e16dad6aa'
branch_labels = None
depends_on = None
def upgrade():
# Get metadata from current connection
meta = sa.MetaData()
meta.reflect(op.get_bind(), only=('role',))
role_table = sa.Table('role', meta)
# Get all roles with permissions
conn = op.get_bind()
result = conn.execute(
sa.select(role_table.c.id, role_table.c.permissions)
.where(role_table.c.permissions.isnot(None))
)
# Add tools_ai permission to each role that has permissions
for row in result:
role_id = row[0]
permissions = row[1]
if permissions:
perms_list = permissions.split(',')
if 'tools_ai' not in perms_list:
perms_list.append('tools_ai')
new_permissions = ','.join(perms_list)
conn.execute(
role_table.update()
.where(role_table.c.id == role_id)
.values(permissions=new_permissions)
)
def downgrade():
# pgAdmin only upgrades, downgrade not implemented.
pass
@@ -0,0 +1,149 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""Add user_id to debugger_function_arguments and indexes for data isolation
Revision ID: add_user_id_dbg_args
Revises: add_tools_ai_perm
Create Date: 2026-04-08
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'add_user_id_dbg_args'
down_revision = 'add_tools_ai_perm'
branch_labels = None
depends_on = None
def upgrade():
conn = op.get_bind()
dialect = conn.dialect.name
# --- DebuggerFunctionArguments: add user_id to composite PK ---
if dialect == 'sqlite':
# SQLite cannot ALTER composite PKs. Recreate the table.
# Existing debugger argument data is ephemeral (cached function
# args) so dropping is acceptable.
op.execute(
'DROP TABLE IF EXISTS debugger_function_arguments'
)
op.create_table(
'debugger_function_arguments',
sa.Column('user_id', sa.Integer(),
sa.ForeignKey('user.id'), nullable=False),
sa.Column('server_id', sa.Integer(), nullable=False),
sa.Column('database_id', sa.Integer(), nullable=False),
sa.Column('schema_id', sa.Integer(), nullable=False),
sa.Column('function_id', sa.Integer(), nullable=False),
sa.Column('arg_id', sa.Integer(), nullable=False),
sa.Column('is_null', sa.Integer(), nullable=False),
sa.Column('is_expression', sa.Integer(), nullable=False),
sa.Column('use_default', sa.Integer(), nullable=False),
sa.Column('value', sa.String(), nullable=True),
sa.PrimaryKeyConstraint(
'user_id', 'server_id', 'database_id',
'schema_id', 'function_id', 'arg_id'
),
sa.CheckConstraint('is_null >= 0 AND is_null <= 1'),
sa.CheckConstraint(
'is_expression >= 0 AND is_expression <= 1'),
sa.CheckConstraint(
'use_default >= 0 AND use_default <= 1'),
)
else:
# PostgreSQL: add column, backfill from server owner, recreate
# PK using batch_alter_table for portability.
op.add_column(
'debugger_function_arguments',
sa.Column('user_id', sa.Integer(),
sa.ForeignKey('user.id'), nullable=True)
)
# Backfill: assign user_id from the server's owner
op.execute(
'UPDATE debugger_function_arguments '
'SET user_id = s.user_id '
'FROM server s '
'WHERE debugger_function_arguments.server_id = s.id'
)
# Delete orphans (rows with no matching server)
op.execute(
'DELETE FROM debugger_function_arguments '
'WHERE user_id IS NULL'
)
op.alter_column(
'debugger_function_arguments', 'user_id', nullable=False
)
# Recreate PK with user_id using batch_alter_table
with op.batch_alter_table(
'debugger_function_arguments'
) as batch:
batch.drop_constraint(
'debugger_function_arguments_pkey', type_='primary'
)
batch.create_primary_key(
'debugger_function_arguments_pkey',
['user_id', 'server_id', 'database_id',
'schema_id', 'function_id', 'arg_id']
)
# --- Indexes for data isolation query performance ---
# Only create indexes on tables that exist (sharedserver may be
# absent in older schemas that haven't run all prior migrations).
inspector = sa.inspect(conn)
index_stmts = [
('server',
'CREATE INDEX IF NOT EXISTS ix_server_user_id '
'ON server (user_id)'),
('server',
'CREATE INDEX IF NOT EXISTS ix_server_servergroup_id '
'ON server (servergroup_id)'),
('sharedserver',
'CREATE INDEX IF NOT EXISTS ix_sharedserver_user_id '
'ON sharedserver (user_id)'),
('sharedserver',
'CREATE INDEX IF NOT EXISTS ix_sharedserver_osid '
'ON sharedserver (osid)'),
('servergroup',
'CREATE INDEX IF NOT EXISTS ix_servergroup_user_id '
'ON servergroup (user_id)'),
]
for table_name, stmt in index_stmts:
if inspector.has_table(table_name):
op.execute(stmt)
# --- Unique constraint on SharedServer(osid, user_id) ---
# Prevents duplicate SharedServer records from TOCTOU race.
# First remove duplicates (keep lowest id per osid+user_id).
if inspector.has_table('sharedserver'):
if dialect == 'sqlite':
op.execute(
'DELETE FROM sharedserver WHERE id NOT IN '
'(SELECT MIN(id) FROM sharedserver '
'GROUP BY osid, user_id)'
)
else:
op.execute(
'DELETE FROM sharedserver s1 USING '
'sharedserver s2 WHERE s1.osid = s2.osid '
'AND s1.user_id = s2.user_id '
'AND s1.id > s2.id'
)
with op.batch_alter_table('sharedserver') as batch:
batch.create_unique_constraint(
'uq_sharedserver_osid_user',
['osid', 'user_id']
)
def downgrade():
# pgAdmin only upgrades, downgrade not implemented.
pass
+4 -7
View File
@@ -15,8 +15,6 @@ Create Date: 2018-08-29 15:33:57.855491
"""
from alembic import op
from sqlalchemy.orm.session import Session
from pgadmin.model import DebuggerFunctionArguments
# revision identifiers, used by Alembic.
revision = 'ca00ec32581b'
@@ -26,11 +24,10 @@ depends_on = None
def upgrade():
session = Session(bind=op.get_bind())
debugger_records = session.query(DebuggerFunctionArguments).all()
if debugger_records:
session.delete(debugger_records)
# Use raw SQL instead of importing the model class, because
# model changes in later migrations (e.g. adding user_id) would
# cause this migration to fail on fresh databases.
op.execute('DELETE FROM debugger_function_arguments')
def downgrade():
@@ -0,0 +1,72 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""Normalize User.locked column data from TEXT to integer 0/1 (SQLite only)
Migration 6650c52670c2 added the locked column with
``server_default='false'``. SQLite has no native BOOLEAN affinity, so the
literal 'false' was stored as TEXT both in the column DEFAULT and on
existing rows backfilled by the ALTER TABLE. SQLAlchemy's Boolean
processor then reads that TEXT back as Python ``True`` (because
``bool('false')`` is ``True`` for any non-empty string), which means any
code that branches on ``User.locked`` sees a locked-out account where
the data should be unlocked.
This was harmless for years because the only writer (the lockout block in
``/authenticate/login``) writes Python ``True``/``False`` (stored as
integer 1/0), and no reader branched on the field. The ``User.is_active``
and ``User.is_locked`` overrides added for the lockout-bypass fix are
the first readers, so they trip over every legacy row.
The data corruption is SQLite-specific. PostgreSQL accepts the string
``'false'`` for a BOOLEAN column and coerces it on insert, so existing
rows on a PostgreSQL config DB already hold proper boolean values and
need no fix. Restrict the data normalization to SQLite — the integer
literals below would also fail on PostgreSQL because the locked column
is BOOLEAN, not INTEGER.
Revision ID: normalize_locked_text_default
Revises: sharedserver_feature_parity
Create Date: 2026-05-05
"""
from alembic import op
# revision identifiers, used by Alembic.
revision = 'normalize_locked_text_default'
down_revision = 'sharedserver_feature_parity'
branch_labels = None
depends_on = None
def upgrade():
bind = op.get_bind()
if bind.dialect.name != 'sqlite':
# On PostgreSQL the locked column was always stored as a proper
# BOOLEAN, so there is nothing to normalize. Skip — UPDATEs with
# integer literals against a BOOLEAN column would otherwise raise
# "column locked is of type boolean but expression is of type
# integer" on PostgreSQL.
return
op.execute(
"UPDATE \"user\" SET locked = 1 "
"WHERE LOWER(CAST(locked AS TEXT)) IN ('true', 't', 'yes', 'y', '1')"
)
op.execute(
"UPDATE \"user\" SET locked = 0 "
"WHERE locked IS NULL OR LOWER(CAST(locked AS TEXT)) NOT IN "
"('true', 't', 'yes', 'y', '1')"
)
def downgrade():
# pgAdmin only upgrades, downgrade not implemented.
pass
@@ -0,0 +1,87 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""SharedServer feature parity columns, orphan cleanup, and db_res removal
Adds passexec_cmd, passexec_expiration, kerberos_conn, tags, and
post_connection_sql to the sharedserver table so non-owners get their
own per-user values instead of inheriting the owner's. Drops db_res
which was never overlaid or writable by non-owners. Also cleans up
orphaned records whose parent server was deleted.
Revision ID: sharedserver_feature_parity
Revises: add_user_id_dbg_args
Create Date: 2026-04-13
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'sharedserver_feature_parity'
down_revision = 'add_user_id_dbg_args'
branch_labels = None
depends_on = None
def upgrade():
conn = op.get_bind()
inspector = sa.inspect(conn)
if not inspector.has_table('sharedserver'):
return
# Clean up orphaned SharedServer records whose osid
# references a Server that no longer exists.
op.execute(
"DELETE FROM sharedserver WHERE osid NOT IN "
"(SELECT id FROM server)"
)
# Add missing columns (idempotent — guard against re-runs).
existing_cols = {
c['name'] for c in inspector.get_columns('sharedserver')
}
new_columns = [
('passexec_cmd',
sa.Column('passexec_cmd', sa.Text(),
nullable=True)),
('passexec_expiration',
sa.Column('passexec_expiration', sa.Integer(),
nullable=True)),
('kerberos_conn',
sa.Column('kerberos_conn', sa.Boolean(),
nullable=False,
server_default=sa.false())),
('tags',
sa.Column('tags', sa.JSON(), nullable=True)),
('post_connection_sql',
sa.Column('post_connection_sql', sa.String(),
nullable=True)),
]
cols_to_add = [
col for name, col in new_columns
if name not in existing_cols
]
if cols_to_add:
with op.batch_alter_table('sharedserver') as batch_op:
for col in cols_to_add:
batch_op.add_column(col)
# Drop db_res — database restrictions are an owner-level
# concept; the column was never overlaid or writable by
# non-owners and its presence invites accidental leakage.
if 'db_res' in existing_cols:
with op.batch_alter_table('sharedserver') as batch_op:
batch_op.drop_column('db_res')
def downgrade():
# pgAdmin only upgrades, downgrade not implemented.
pass
+92 -91
View File
@@ -7,62 +7,64 @@
],
"license": "PostgreSQL",
"devDependencies": {
"@babel/core": "^7.28.3",
"@babel/eslint-parser": "^7.26.5",
"@babel/eslint-plugin": "^7.26.10",
"@babel/plugin-proposal-object-rest-spread": "^7.10.1",
"@babel/plugin-syntax-jsx": "^7.16.0",
"@babel/plugin-transform-runtime": "^7.28.3",
"@babel/preset-env": "^7.28.0",
"@babel/preset-typescript": "^7.24.7",
"@babel/core": "^7.29.0",
"@babel/eslint-parser": "^7.28.6",
"@babel/eslint-plugin": "^7.27.1",
"@babel/plugin-syntax-jsx": "^7.28.6",
"@babel/plugin-transform-class-properties": "^7.28.6",
"@babel/plugin-transform-object-rest-spread": "^7.28.6",
"@babel/plugin-transform-runtime": "^7.29.0",
"@babel/preset-env": "^7.29.2",
"@babel/preset-react": "^7.28.5",
"@babel/preset-typescript": "^7.28.5",
"@emotion/memoize": "^0.9.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@emotion/utils": "^1.0.0",
"@emotion/utils": "^1.4.2",
"@svgr/webpack": "^8.1.0",
"@testing-library/dom": "10.4.1",
"@testing-library/jest-dom": "^6.7.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "16.3.2",
"@testing-library/user-event": "^14.5.2",
"@testing-library/user-event": "^14.6.1",
"@types/jest": "^30.0.0",
"autoprefixer": "^10.4.23",
"autoprefixer": "^10.5.0",
"axios-mock-adapter": "^2.1.0",
"babel-loader": "^10.0.0",
"browserify": "^17.0.0",
"babel-loader": "^10.1.1",
"browserify": "^17.0.1",
"buffer": "^6.0.3",
"copy-webpack-plugin": "^13.0.1",
"cross-env": "^10.0.0",
"css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.3",
"eslint": "^9.39.2",
"eslint-plugin-jest": "^29.9.0",
"copy-webpack-plugin": "^14.0.0",
"cross-env": "^10.1.0",
"css-loader": "^7.1.4",
"css-minimizer-webpack-plugin": "^8.0.0",
"eslint": "^9.39.4",
"eslint-plugin-jest": "^29.15.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-unused-imports": "^4.2.0",
"eslint-plugin-unused-imports": "^4.4.1",
"exports-loader": "^5.0.0",
"globals": "^17.0.0",
"html-react-parser": "^5.2.12",
"globals": "^17.5.0",
"html-react-parser": "^5.2.17",
"image-minimizer-webpack-plugin": "^4.1.4",
"imports-loader": "^5.0.0",
"jest": "^30.2.0",
"jest-environment-jsdom": "^30.2.0",
"loader-utils": "^3.2.1",
"mini-css-extract-plugin": "^2.10.0",
"postcss-loader": "^8.1.1",
"jest": "^30.3.0",
"jest-environment-jsdom": "^30.3.0",
"loader-utils": "^3.3.1",
"mini-css-extract-plugin": "^2.10.2",
"postcss-loader": "^8.2.1",
"process": "^0.11.10",
"prop-types": "^15.7.2",
"prop-types": "^15.8.1",
"resize-observer-polyfill": "^1.5.1",
"shim-loader": "^1.0.1",
"style-loader": "^4.0.0",
"stylis": "^4.3.4",
"svgo": "^4.0.0",
"stylis": "^4.4.0",
"svgo": "^4.0.1",
"svgo-loader": "^4.0.0",
"terser-webpack-plugin": "^5.3.12",
"typescript": "^5.9.2",
"typescript-eslint": "^8.54.0",
"terser-webpack-plugin": "^5.5.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.0",
"url-loader": "^4.1.1",
"webfonts-loader": "^8.0.1",
"webpack": "^5.103.0",
"webpack-bundle-analyzer": "^5.2.0",
"webfonts-loader": "^8.1.1",
"webpack": "^5.106.2",
"webpack-bundle-analyzer": "^5.3.0",
"webpack-cli": "^6.0.1",
"yarn-audit-html": "7.3.2"
},
@@ -70,90 +72,89 @@
"sharp": "^0.34.4"
},
"dependencies": {
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/preset-react": "^7.27.1",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-sql": "^6.10.0",
"@date-io/core": "^3.0.0",
"@date-io/core": "^3.2.0",
"@date-io/date-fns": "3.x",
"@emotion/sheet": "^1.0.1",
"@emotion/sheet": "^1.4.0",
"@fluentui/keyboard-key": "^0.4.23",
"@fortawesome/fontawesome-free": "latest",
"@mui/icons-material": "^7.3.6",
"@mui/material": "^7.3.7",
"@mui/x-date-pickers": "^8.26.0",
"@mui/icons-material": "^9.0.0",
"@mui/material": "^9.0.0",
"@mui/x-date-pickers": "^9.0.2",
"@nozbe/microfuzz": "^1.0.0",
"@projectstorm/react-diagrams": "^7.0.4",
"@simonwep/pickr": "^1.5.1",
"@simonwep/pickr": "^1.9.1",
"@szhsin/react-menu": "^4.5.1",
"@tanstack/react-query": "^5.90.19",
"@tanstack/react-table": "^8.16.0",
"@tanstack/react-virtual": "^3.13.13",
"@types/classnames": "^2.3.4",
"@types/react": "^19.2.9",
"@types/react-dom": "^19.0.0",
"@tanstack/react-query": "^5.100.9",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.24",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@xterm/addon-fit": "^0.11.0",
"@xterm/addon-search": "^0.16.0",
"@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0",
"ajv": "^8.8.2",
"anti-trojan-source": "^1.4.0",
"aspen-decorations": "^1.0.2",
"axios": "^1.13.3",
"ajv": "^8.18.0",
"anti-trojan-source": "^1.8.1",
"aspen-decorations": "^1.1.1",
"axios": "^1.16.0",
"babelify": "~10.0.0",
"bignumber.js": "^9.2.0",
"bignumber.js": "^10.0.2",
"brace": "^0.11.1",
"chart.js": "^4.4.7",
"chartjs-plugin-zoom": "^2.0.1",
"codemirror": "^6.0.1",
"chart.js": "^4.5.1",
"chartjs-plugin-zoom": "^2.2.0",
"codemirror": "^6.0.2",
"convert-units": "^2.3.4",
"date-fns": "^4.1.0",
"diff-arrays-of-objects": "^1.1.8",
"hotkeys-js": "^4.0.0",
"diff-arrays-of-objects": "^1.1.10",
"dompurify": "^3.4.1",
"hotkeys-js": "^4.0.3",
"html-to-image": "1.11.11",
"immutability-helper": "^3.0.0",
"insert-if": "^1.1.0",
"ip-address": "^10.1.0",
"json-bignumber": "^1.0.1",
"immutability-helper": "^3.1.1",
"insert-if": "^1.2.0",
"ip-address": "^10.1.1",
"json-bignumber": "^1.1.1",
"leaflet": "^1.9.4",
"lodash": "4.*",
"moment": "^2.29.4",
"moment-timezone": "^0.6.0",
"marked": "^18.0.2",
"moment": "^2.30.1",
"moment-timezone": "^0.6.2",
"notificar": "^1.0.1",
"notistack": "^3.0.1",
"papaparse": "^5.5.2",
"path-fx": "^2.0.0",
"postcss": "^8.5.6",
"notistack": "^3.0.2",
"papaparse": "^5.5.3",
"path-fx": "^2.1.2",
"postcss": "^8.5.14",
"rc-dock": "^4.0.0-alpha.2",
"react": "^19.2.3",
"react-arborist": "^3.2.0",
"react-aspen": "^1.1.0",
"react-checkbox-tree": "^1.7.2",
"react": "^19.2.5",
"react-arborist": "^3.5.0",
"react-aspen": "^1.2.0",
"react-checkbox-tree": "^2.0.1",
"react-data-grid": "https://github.com/pgadmin-org/react-data-grid.git#8ddaa7bed99c8329e39b8ff37ba156f7fc8e85d4",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^19.2.3",
"react-draggable": "^4.4.6",
"react-dropzone": "^14.2.1",
"react-frame-component": "^5.2.6",
"react-dom": "^19.2.5",
"react-draggable": "^4.5.0",
"react-dropzone": "^15.0.0",
"react-frame-component": "~5.2.6",
"react-leaflet": "^4.2.1",
"react-new-window": "^1.0.1",
"react-resize-detector": "^12.3.0",
"react-rnd": "^10.5.1",
"react-select": "^5.7.2",
"react-timer-hook": "^4.0.1",
"react-virtualized-auto-sizer": "^2.0.2",
"react-rnd": "^10.5.3",
"react-select": "^5.10.2",
"react-timer-hook": "^4.0.5",
"react-virtualized-auto-sizer": "^2.0.3",
"snapsvg-cjs": "^0.0.6",
"socket.io-client": "^4.5.0",
"split.js": "^1.5.10",
"sql-formatter": "^15.6.10",
"socket.io-client": "^4.8.3",
"split.js": "^1.6.5",
"sql-formatter": "^15.7.3",
"uplot": "^1.6.32",
"uplot-react": "^1.1.4",
"uplot-react": "^1.2.4",
"use-resize-observer": "^9.1.0",
"valid-filename": "^4.0.0",
"vanilla-jsoneditor": "^3.3.1",
"vanilla-jsoneditor": "^3.12.0",
"wkx": "^0.5.0",
"zustand": "^5.0.9"
"zustand": "^5.0.12"
},
"resolutions": {
"rc-resize-observer": "1.4.0"
@@ -179,7 +180,7 @@
"auditpy": "safety check --full-report -i 51668 -i 52495",
"audit-all": "yarn run auditjs && yarn run auditpy"
},
"packageManager": "yarn@4.9.2",
"packageManager": "yarn@4.15.0",
"browserslist": [
"defaults and fully supports es6-module"
]
+56 -39
View File
@@ -403,50 +403,67 @@ def create_app(app_name=None):
backup_db_file()
def run_migration_for_sqlite():
# Run migration for the first time i.e. create database
# If version not available, user must have aborted. Tables are not
# created and so its an empty db
if not os.path.exists(SQLITE_PATH) or get_version() == -1:
# If running in cli mode then don't try to upgrade, just raise
# the exception
if not cli_mode:
upgrade_db()
# Tighten the process umask while we may be creating the SQLite
# DB file. The post-hoc chmod below sets 0o600, but only AFTER
# SQLAlchemy/SQLite has already created the file with the
# umask-default mode (0o644 on a typical 0o022 umask). That's a
# TOCTOU window — small in practice because the parent dir is
# 0o700, but easy to close. With umask 0o077 in effect during
# creation, the file is born 0o600.
_saved_umask = os.umask(0o077)
try:
# Run migration for the first time i.e. create database
# If version not available, user must have aborted. Tables
# are not created and so its an empty db
if not os.path.exists(SQLITE_PATH) or get_version() == -1:
# If running in cli mode then don't try to upgrade, just
# raise the exception
if not cli_mode:
upgrade_db()
else:
if not os.path.exists(SQLITE_PATH):
raise FileNotFoundError(
'SQLite database file "' + SQLITE_PATH +
'" does not exists.')
raise RuntimeError(
'The configuration database file is not valid.')
else:
if not os.path.exists(SQLITE_PATH):
raise FileNotFoundError(
'SQLite database file "' + SQLITE_PATH +
'" does not exists.')
raise RuntimeError(
'The configuration database file is not valid.')
else:
schema_version = get_version()
schema_version = get_version()
# Run migration if current schema version is greater than the
# schema version stored in version table
if CURRENT_SCHEMA_VERSION > schema_version:
# Take a backup of the old database file.
try:
prev_database_file_name = \
"{0}.prev.bak".format(SQLITE_PATH)
shutil.copyfile(SQLITE_PATH, prev_database_file_name)
except Exception as e:
app.logger.error(e)
# Run migration if current schema version is greater than
# the schema version stored in version table
if CURRENT_SCHEMA_VERSION > schema_version:
# Take a backup of the old database file.
try:
prev_database_file_name = \
"{0}.prev.bak".format(SQLITE_PATH)
shutil.copyfile(
SQLITE_PATH, prev_database_file_name)
except Exception as e:
app.logger.error(e)
upgrade_db()
else:
# check all tables are present in the db.
is_db_error, invalid_tb_names = check_db_tables()
if is_db_error:
app.logger.error(
'Table(s) {0} are missing in the'
' database'.format(invalid_tb_names))
backup_db_file()
upgrade_db()
else:
# check all tables are present in the db.
is_db_error, invalid_tb_names = check_db_tables()
if is_db_error:
app.logger.error(
'Table(s) {0} are missing in the'
' database'.format(invalid_tb_names))
backup_db_file()
# Update schema version to the latest
if CURRENT_SCHEMA_VERSION > schema_version:
set_version(CURRENT_SCHEMA_VERSION)
db.session.commit()
# Update schema version to the latest
if CURRENT_SCHEMA_VERSION > schema_version:
set_version(CURRENT_SCHEMA_VERSION)
db.session.commit()
finally:
# Always restore the prior umask — including the exception
# paths above, so a migration failure doesn't leave the
# whole process running with 0o077.
os.umask(_saved_umask)
# Belt-and-suspenders: covers the case where the file already
# existed at a wider mode from an older install.
if os.name != 'nt':
os.chmod(config.SQLITE_PATH, 0o600)
+13 -2
View File
@@ -122,11 +122,22 @@ export default function AboutComponent() {
</Grid>
}
{ aboutData.settings &&
<Box flexGrow="1" display="flex" flexDirection="column" minHeight="0">
<Box
sx={{
flexGrow: '1',
display: 'flex',
flexDirection: 'column',
minHeight: '0'
}}>
<Box>
<InputLabel style={{fontWeight: 'bold'}}>{gettext('pgAdmin Server Configuration')}</InputLabel>
</Box>
<Box flexGrow="1" paddingTop="1px" minHeight="0">
<Box
sx={{
flexGrow: '1',
paddingTop: '1px',
minHeight: '0'
}}>
<InputSQL value={aboutData.settings}
controlProps={{
readonly: true,
+6 -5
View File
@@ -110,7 +110,10 @@ def _login():
# Sending empty form as oauth2 does not require form attribute
auth_obj = AuthSourceManager({}, copy.deepcopy(
config.AUTHENTICATION_SOURCES))
session['auth_obj'] = auth_obj
# Persist only the OAuth2 provider selection across the redirect.
# The auth-source instance lives on current_app's registry so we
# re-look-up rather than persist a live class instance in session.
session['oauth2_current_client'] = request.form.get('oauth2_button')
else:
auth_obj = AuthSourceManager(form, copy.deepcopy(
config.AUTHENTICATION_SOURCES))
@@ -183,16 +186,14 @@ def _login():
user.login_attempts = 0
db.session.commit()
if 'auth_obj' in session:
session.pop('auth_obj')
session.pop('oauth2_current_client', None)
return redirect(pga_utils.get_safe_post_login_redirect())
elif isinstance(msg, Response):
return msg
elif 'oauth2_button' in request.form and not isinstance(msg, str):
return msg
if 'auth_obj' in session:
session.pop('auth_obj')
session.pop('oauth2_current_client', None)
flash(msg, MessageType.ERROR)
form_class = _security.forms.get('login_form').cls
form = form_class()
+52 -27
View File
@@ -52,16 +52,37 @@ def init_app(app):
methods=['GET', 'POST'])
@pgCSRFProtect.exempt
def oauth_authorize():
auth_obj = session['auth_obj']
auth_obj.set_current_source(auth_obj.source.get_source_name())
# Reconstruct the minimal AuthSourceManager state from the session.
# The previous design stored a live AuthSourceManager instance,
# which required serializable-anything session storage and
# presented a deserialization vector. Now we persist only the
# OAuth2 provider name and re-look-up the auth-source from
# current_app's registry.
from pgadmin.authenticate import AuthSourceManager, get_auth_sources
oauth2_client = session.get('oauth2_current_client')
if not oauth2_client:
current_app.logger.warning(
"OAuth2 callback received without provider context; "
"session may have expired between login and callback")
flash(gettext("Login session expired. Please try again."),
MessageType.ERROR)
return redirect(get_safe_post_logout_redirect())
oauth2_source = get_auth_sources(OAUTH2)
# Restore the provider selection onto the (process-global) source
# instance so login() picks the right OAuth2 client.
oauth2_source.oauth2_current_client = oauth2_client
auth_obj = AuthSourceManager({}, [OAUTH2])
auth_obj.set_source(oauth2_source)
auth_obj.set_current_source(oauth2_source.get_source_name())
status, msg = auth_obj.login()
if status:
session['auth_source_manager'] = auth_obj.as_dict()
if 'auth_obj' in session:
session.pop('auth_obj')
session.pop('oauth2_current_client', None)
return redirect(get_safe_post_login_redirect())
if 'auth_obj' in session:
session.pop('auth_obj')
session.pop('oauth2_current_client', None)
logout_user()
flash(msg, MessageType.ERROR)
return redirect(get_safe_post_login_redirect())
@@ -437,9 +458,9 @@ class OAuth2Authentication(BaseAuthentication):
def login(self, form):
if not self.oauth2_current_client:
error_msg = 'No OAuth2 provider available.'
error_msg = gettext('No OAuth2 provider available.')
current_app.logger.error(error_msg)
return False, gettext(error_msg)
return False, error_msg
profile = self.get_user_profile()
profile_dict = self.get_profile_dict(profile)
@@ -468,35 +489,35 @@ class OAuth2Authentication(BaseAuthentication):
not id_token_claims and
not profile_dict
):
error_msg = "No profile data found from OIDC provider."
error_msg = gettext("No profile data found from OIDC provider.")
current_app.logger.error(error_msg)
return False, gettext(error_msg)
return False, error_msg
# For non-OIDC providers, profile is required
if not self._is_oidc_provider() and not profile_dict:
error_msg = "No profile data found."
error_msg = gettext("No profile data found.")
current_app.logger.error(error_msg)
return False, gettext(error_msg)
return False, error_msg
# Resolve username using OIDC-aware logic
username, email = self._resolve_username(id_token_claims, profile_dict)
if not username:
if self._is_oidc_provider():
error_msg = (
error_msg = gettext(
'Could not extract username from OIDC claims. '
'Please ensure your OIDC provider returns standard '
'claims (email, preferred_username, or sub).'
)
else:
error_msg = (
error_msg = gettext(
'An email id or OAUTH2_USERNAME_CLAIM is required to '
'login into pgAdmin. Please update your OAuth2 profile '
'for email id or set OAUTH2_USERNAME_CLAIM config '
'parameter.'
)
current_app.logger.error(error_msg)
return False, gettext(error_msg)
return False, error_msg
additional_claims = None
if 'OAUTH2_ADDITIONAL_CLAIMS' in self.oauth2_config[
@@ -678,10 +699,12 @@ class OAuth2Authentication(BaseAuthentication):
self.oauth2_current_client].authorize_redirect(redirect_url)
def __auto_create_user(self, username, email):
if config.OAUTH2_AUTO_CREATE_USER:
user = User.query.filter_by(username=username,
auth_source=OAUTH2).first()
if not user:
user = User.query.filter_by(username=username,
auth_source=OAUTH2).first()
if user:
return True, {'username': username}
else:
if config.OAUTH2_AUTO_CREATE_USER:
create_msg = ("Creating user {0} with email {1} "
"from auth source OAUTH2.")
current_app.logger.info(create_msg.format(username,
@@ -693,18 +716,20 @@ class OAuth2Authentication(BaseAuthentication):
'active': True,
'auth_source': OAUTH2
})
return True, {'username': username}
else:
return False, gettext('No Email/Username found.'
' Please contact your administrator.')
def __is_any_claim_valid(self, identity, additional_claims):
if additional_claims is None:
reason = "Additional claim config is None, no check to do."
reason = gettext("Additional claim config is None,"
" no check to do.")
return (True, reason)
if not isinstance(additional_claims, dict):
reason = "Additional claim check config is not a dict."
reason = gettext("Additional claim check config is not a dict.")
return (False, reason)
if additional_claims.keys() is None:
reason = "Additional claim check config dict is empty."
if len(additional_claims.keys()) == 0:
reason = gettext("Additional claim check config dict is empty.")
return (False, reason)
for key in additional_claims.keys():
claim = identity.get(key)
@@ -716,7 +741,7 @@ class OAuth2Authentication(BaseAuthentication):
if not isinstance(authorized_claims, list):
authorized_claims = [authorized_claims]
if any(item in authorized_claims for item in claim):
reason = "Claim match found. Authorized access."
reason = gettext("Claim match found. Authorized access.")
return True, reason
reason = "No match was found."
reason = gettext("No match was found.")
return False, reason
+37 -4
View File
@@ -68,11 +68,31 @@ from pgadmin.authenticate import AuthSourceManager
from pgadmin.utils.exception import CryptKeyMissing
from pgadmin.user_login_check import pga_login_required
from flask_security.views import default_render_json
MODULE_NAME = 'browser'
BROWSER_STATIC = 'browser.static'
BROWSER_INDEX = 'browser.index'
def _first_form_error_message(form, default=None):
"""Extract the first validator error message from a wtforms form,
coerced to a plain str.
Returns `default` if there are no errors. Babel `LazyString`
(`LazyProxy`) instances — emitted by wtforms validators with
translatable messages — must be force-resolved before they reach
`json.dumps`, which otherwise fails with "Circular reference
detected" because `LazyProxy` reports itself as iterable. Use this
helper any time you return a form-validation error in a JSON
envelope.
"""
for errors in form.errors.values():
for error in errors:
if error:
return str(error)
return default
PGADMIN_BROWSER = 'pgAdmin.Browser'
PASS_ERROR_MSG = gettext('Your password has not been changed.')
SMTP_SOCKET_ERROR = gettext(
@@ -494,6 +514,9 @@ def utils():
except Exception:
pg_libpq_version = 0
# Check if LLM features are enabled (system-level AND provider configured)
from pgadmin.llm.utils import is_llm_enabled
for submodule in current_blueprint.submodules:
snippets.extend(submodule.jssnippets)
@@ -538,6 +561,7 @@ def utils():
"Administrator") else restricted_shared_storage_list,
enable_server_passexec_cmd=config.ENABLE_SERVER_PASS_EXEC_CMD,
max_server_tags_allowed=config.MAX_SERVER_TAGS_ALLOWED,
llm_enabled=is_llm_enabled(),
), 200)
response.headers['Content-Type'] = MIMETYPE_APP_JS
response.headers['Cache-Control'] = NO_CACHE_CONTROL
@@ -914,7 +938,8 @@ if hasattr(config, 'SECURITY_CHANGEABLE') and config.SECURITY_CHANGEABLE:
elif errormsg is not None:
return internal_server_error(errormsg)
else:
return bad_request(list(form.errors.values())[0][0])
return bad_request(_first_form_error_message(
form, gettext('Invalid input.')))
return make_json_response(
success=1,
@@ -1003,8 +1028,16 @@ if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE:
do_flash(*get_message('PASSWORD_RESET_REQUEST',
email=form.user.email))
if request.get_json(silent=True) and not has_error:
return default_render_json(form, include_user=False)
if request.get_json(silent=True) is not None:
# Flask-Security's default_render_json signature changed
# (no longer accepts `include_user`); return our standard
# JSON envelope.
if has_error or form.errors:
return bad_request(_first_form_error_message(
form, gettext('Password reset failed.')))
return make_json_response(
success=1,
info=gettext('Password reset instructions sent.'))
for errors in form.errors.values():
for error in errors:
@@ -445,7 +445,7 @@ def register_browser_preferences(self):
'text', '%FUNCTION%(%ARGS%)',
category_label=PREF_LABEL_DISPLAY,
help_str=gettext(
'Supported placeholders are %FUNCTION%, %ARGS%, %SCHEMA% and'
'Supported placeholders are %FUNCTION%, %ARGS%, %SCHEMA%, and'
' %DATABASE%. Users can provide any string with or '
'without placeholders of their choice. A blank title will revert '
'to the default.'
+23 -18
View File
@@ -25,6 +25,8 @@ from sqlalchemy import exc
from pgadmin.model import db, ServerGroup, Server
import config
from pgadmin.utils.preferences import Preferences
from pgadmin.utils.server_access import get_server_group, \
get_server_groups_for_user
def get_icon_css_class(group_id, group_user_id,
@@ -228,7 +230,7 @@ class ServerGroupView(NodeView):
except Exception as e:
db.session.rollback()
return make_json_response(
status=410, success=0, errormsg=e.message
status=410, success=0, errormsg=str(e)
)
return make_json_response(result=request.form)
@@ -265,7 +267,7 @@ class ServerGroupView(NodeView):
except Exception as e:
db.session.rollback()
return make_json_response(
status=410, success=0, errormsg=e.message
status=410, success=0, errormsg=str(e)
)
icon_class, is_shared = get_icon_css_class(gid, servergroup.user_id)
@@ -286,7 +288,7 @@ class ServerGroupView(NodeView):
def properties(self, gid):
"""Update the server-group properties"""
sg = ServerGroup.query.filter(ServerGroup.id == gid).first()
sg = get_server_group(gid)
if sg is None:
return make_json_response(
@@ -296,7 +298,8 @@ class ServerGroupView(NodeView):
)
else:
return ajax_response(
response={'id': sg.id, 'name': sg.name, 'user_id': sg.user_id},
response={'id': sg.id, 'name': sg.name,
'user_id': sg.user_id},
status=200
)
@@ -342,7 +345,7 @@ class ServerGroupView(NodeView):
return make_json_response(
status=410,
success=0,
errormsg=e.message)
errormsg=str(e))
else:
return make_json_response(
@@ -373,8 +376,9 @@ class ServerGroupView(NodeView):
@staticmethod
def get_all_server_groups():
"""
Returns the list of server groups to show in server mode and
if there is any shared server in the group.
Returns the list of server groups to show in server mode.
Includes groups owned by the user and groups containing
shared servers accessible to this user.
:return: server groups
"""
@@ -383,17 +387,18 @@ class ServerGroupView(NodeView):
pref = Preferences.module('browser')
hide_shared_server = pref.preference('hide_shared_server').get()
server_groups = ServerGroup.query.all()
groups = []
for group in server_groups:
if hide_shared_server and \
ServerGroupModule.has_shared_server(group.id) and \
group.user_id != current_user.id:
continue
if group.user_id == current_user.id or \
ServerGroupModule.has_shared_server(group.id):
server_groups = get_server_groups_for_user()
if hide_shared_server:
groups = []
for group in server_groups:
if group.user_id != current_user.id and \
ServerGroupModule.has_shared_server(group.id):
continue
groups.append(group)
return groups
return groups
return server_groups
@pga_login_required
def nodes(self, gid=None):
@@ -421,7 +426,7 @@ class ServerGroupView(NodeView):
)
)
else:
group = ServerGroup.query.filter(ServerGroup.id == gid).first()
group = get_server_group(gid)
if not group:
return gone(
@@ -39,11 +39,30 @@ from pgadmin.browser.server_groups.servers.utils import \
from pgadmin.utils.constants import UNAUTH_REQ, MIMETYPE_APP_JS, \
SERVER_CONNECTION_CLOSED, RESTRICTION_TYPE_SQL
from sqlalchemy import or_
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import object_session
from sqlalchemy.orm.attributes import flag_modified
from pgadmin.utils.preferences import Preferences
from .... import socketio as sio
from pgadmin.utils import get_complete_file_path
from pgadmin.settings.utils import with_object_filters
from pgadmin.utils.server_access import get_server, \
get_user_server_query, get_server_group
# File-path keys in connection_params that are per-user and must
# not be copied from the owner to a new SharedServer or leaked
# through the property merge.
SENSITIVE_CONN_KEYS = frozenset({
'passfile', 'sslcert', 'sslkey',
'sslrootcert', 'sslcrl', 'sslcrldir',
})
def _is_non_owner(server):
"""True if the server is shared and the current user is not
the owner. Centralises the check used in 15+ places."""
return server.shared and server.user_id != current_user.id
def has_any(data, keys):
@@ -150,15 +169,31 @@ class ServerModule(sg.ServerGroupPluginModule):
@staticmethod
def get_shared_server_properties(server, sharedserver):
"""
Return shared server properties
Return shared server properties.
Overlays per-user SharedServer values onto the owner's Server
object so each non-owner sees their own customizations.
The server is expunged from the SQLAlchemy session before
mutation so that the owner's record is never dirtied.
:param server:
:param sharedserver:
:return: shared server
:return: shared server (detached)
"""
if sharedserver is None:
return server
# Detach from session so in-place mutations are never
# flushed back to the owner's Server row.
sess = object_session(server)
if sess is not None:
sess.expunge(server)
server.bgcolor = sharedserver.bgcolor
server.fgcolor = sharedserver.fgcolor
server.name = sharedserver.name
server.role = sharedserver.role
server.service = sharedserver.service
server.use_ssh_tunnel = sharedserver.use_ssh_tunnel
server.tunnel_host = sharedserver.tunnel_host
server.tunnel_port = sharedserver.tunnel_port
@@ -168,23 +203,33 @@ class ServerModule(sg.ServerGroupPluginModule):
server.save_password = sharedserver.save_password
server.tunnel_identity_file = sharedserver.tunnel_identity_file
server.tunnel_prompt_password = sharedserver.tunnel_prompt_password
if hasattr(server, 'connection_params') and \
hasattr(sharedserver, 'connection_params') and \
'passfile' in server.connection_params and \
'passfile' in sharedserver.connection_params:
server.connection_params['passfile'] = \
sharedserver.connection_params['passfile']
# Override per-user connection_params keys. Use the
# SharedServer value whenever it is present, regardless of
# whether the owner's Server has the same key.
s_conn = getattr(server, 'connection_params', None) \
or {}
ss_conn = getattr(sharedserver, 'connection_params',
None) or {}
for key in SENSITIVE_CONN_KEYS:
if key in ss_conn:
s_conn[key] = ss_conn[key]
elif key in s_conn:
# Owner has this key but non-owner doesn't —
# remove it so the owner's path doesn't leak.
del s_conn[key]
server.connection_params = s_conn
server.servergroup_id = sharedserver.servergroup_id
if hasattr(server, 'connection_params') and \
hasattr(sharedserver, 'connection_params') and \
'sslcert' in server.connection_params and \
'sslcert' in sharedserver.connection_params:
server.connection_params['sslcert'] = \
sharedserver.connection_params['sslcert']
server.username = sharedserver.username
server.server_owner = sharedserver.server_owner
server.password = sharedserver.password
server.prepare_threshold = sharedserver.prepare_threshold
server.passexec_cmd = sharedserver.passexec_cmd
server.passexec_expiration = sharedserver.passexec_expiration
server.kerberos_conn = sharedserver.kerberos_conn
server.tags = sharedserver.tags
server.post_connection_sql = sharedserver.post_connection_sql
return server
@@ -202,12 +247,13 @@ class ServerModule(sg.ServerGroupPluginModule):
if server.discovery_id and \
not server.shared and \
config.SERVER_MODE and \
len(SharedServer.query.filter_by(
SharedServer.query.filter_by(
user_id=current_user.id,
name=server.name).all()) > 0 and not hide_shared_server:
osid=server.id).first() is not None \
and not hide_shared_server:
continue
if server.shared and server.user_id != current_user.id:
if _is_non_owner(server):
shared_server = self.get_shared_server(server, gid)
@@ -244,8 +290,7 @@ class ServerModule(sg.ServerGroupPluginModule):
"""Return a JSON document listing the server groups for the user"""
hide_shared_server = get_preferences()
servers = Server.query.filter(
or_(Server.user_id == current_user.id, Server.shared),
servers = get_user_server_query().filter(
Server.servergroup_id == gid, Server.is_adhoc == 0)
driver = get_driver(PG_DEFAULT_DRIVER)
@@ -391,6 +436,18 @@ class ServerModule(sg.ServerGroupPluginModule):
try:
db.session.rollback()
user = User.query.filter_by(id=data.user_id).first()
# Strip owner's sensitive file paths from
# connection_params — each user should configure
# their own SSL/passfile paths.
safe_conn_params = {}
if data.connection_params:
safe_conn_params = {
k: v for k, v in
data.connection_params.items()
if k not in SENSITIVE_CONN_KEYS
}
shared_server = SharedServer(
osid=data.id,
user_id=current_user.id,
@@ -409,43 +466,62 @@ class ServerModule(sg.ServerGroupPluginModule):
service=data.service if data.service else None,
use_ssh_tunnel=data.use_ssh_tunnel,
tunnel_host=data.tunnel_host,
tunnel_port=22,
tunnel_port=data.tunnel_port
if data.tunnel_port else 22,
tunnel_username=None,
tunnel_authentication=0,
tunnel_identity_file=None,
tunnel_keep_alive=0,
tunnel_keep_alive=data.tunnel_keep_alive
if data.tunnel_keep_alive else 0,
tunnel_prompt_password=0,
shared=True,
connection_params=data.connection_params,
prepare_threshold=data.prepare_threshold
connection_params=safe_conn_params,
prepare_threshold=data.prepare_threshold,
passexec_cmd=None,
passexec_expiration=None,
kerberos_conn=False,
tags=None,
post_connection_sql=None
)
db.session.add(shared_server)
db.session.commit()
except Exception as e:
if shared_server:
db.session.delete(shared_server)
db.session.commit()
db.session.rollback()
raise e
@staticmethod
def get_shared_server(server, gid):
"""
return the shared server
Return the SharedServer record for the current user,
creating one lazily if it doesn't exist. The unique
constraint on (osid, user_id) prevents duplicates from
concurrent requests.
:param server:
:param gid:
:return: shared_server
:return: shared_server (never None)
:raises: Exception if SharedServer cannot be created
"""
shared_server = SharedServer.query.filter_by(
name=server.name, user_id=current_user.id,
servergroup_id=int(gid), osid=server.id).first()
user_id=current_user.id,
osid=server.id).first()
if shared_server is None:
ServerModule.create_shared_server(server, int(gid))
try:
ServerModule.create_shared_server(
server, int(gid))
except IntegrityError:
# Unique constraint violation from a concurrent
# request — the record now exists.
db.session.rollback()
shared_server = SharedServer.query.filter_by(
name=server.name, user_id=current_user.id,
servergroup_id=int(gid), osid=server.id).first()
user_id=current_user.id,
osid=server.id).first()
if shared_server is None:
raise Exception(
"Failed to create shared server record "
"for server {0}".format(server.id))
return shared_server
@@ -494,17 +570,28 @@ class ServerNode(PGChildNodeView):
'clear_sshtunnel_password': [{'put': 'clear_sshtunnel_password'}],
})
def update_connection_parameter(self, data, server):
def update_connection_parameter(self, data, server, sharedserver=None):
"""
This function is used to update the connection parameters.
"""
if 'connection_params' in data and \
hasattr(server, 'connection_params'):
existing_conn_params = getattr(server, 'connection_params')
# For shared servers accessed by non-owners, apply changes
# to the SharedServer's connection_params (a copy) so we
# don't mutate the owner's Server record in-place.
if sharedserver is not None and \
server.shared and \
server.user_id != current_user.id:
existing_conn_params = dict(
sharedserver.connection_params or {})
else:
existing_conn_params = getattr(
server, 'connection_params')
new_conn_params = data['connection_params']
if 'deleted' in new_conn_params:
for item in new_conn_params['deleted']:
del existing_conn_params[item['name']]
if item['name'] in existing_conn_params:
del existing_conn_params[item['name']]
if 'added' in new_conn_params:
for item in new_conn_params['added']:
existing_conn_params[item['name']] = item['value']
@@ -559,15 +646,13 @@ class ServerNode(PGChildNodeView):
Return a JSON document listing the servers under this server group
for the user.
"""
servers = Server.query.filter(
or_(Server.user_id == current_user.id,
Server.shared),
servers = get_user_server_query().filter(
Server.servergroup_id == gid, Server.is_adhoc == 0)
driver = get_driver(PG_DEFAULT_DRIVER)
for server in servers:
if server.shared and server.user_id != current_user.id:
if _is_non_owner(server):
shared_server = ServerModule.get_shared_server(server, gid)
server = \
ServerModule.get_shared_server_properties(server,
@@ -626,24 +711,22 @@ class ServerNode(PGChildNodeView):
@pga_login_required
def node(self, gid, sid):
"""Return a JSON document listing the server groups for the user"""
server = Server.query.filter_by(id=sid).first()
if server.shared and server.user_id != current_user.id:
shared_server = ServerModule.get_shared_server(server, gid)
server = ServerModule.get_shared_server_properties(server,
shared_server)
server = get_server(sid)
if server is None:
return make_json_response(
status=410,
success=0,
errormsg=gettext(
gettext(
"Could not find the server with id# {0}."
).format(sid)
)
"Could not find the server with id# {0}."
).format(sid)
)
if _is_non_owner(server):
shared_server = ServerModule.get_shared_server(server, gid)
server = ServerModule.get_shared_server_properties(server,
shared_server)
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(server.id)
conn = manager.connection()
connected = conn.connected()
@@ -692,16 +775,20 @@ class ServerNode(PGChildNodeView):
),
)
def delete_shared_server(self, server_name, gid, osid):
def delete_shared_server(self, gid, osid, user_id=None):
"""
Delete the shared server
:param server_name:
:return:
Delete SharedServer records for a given original server.
:param gid: Server group ID
:param osid: Original server ID
:param user_id: If set, only delete for this user.
If None, delete for ALL users (owner unshare/delete).
"""
try:
shared_server = SharedServer.query.filter_by(name=server_name,
servergroup_id=gid,
osid=osid)
filters = dict(servergroup_id=gid, osid=osid)
if user_id is not None:
filters['user_id'] = user_id
shared_server = SharedServer.query.filter_by(
**filters)
for s in shared_server:
get_driver(PG_DEFAULT_DRIVER).delete_manager(s.id)
db.session.delete(s)
@@ -711,7 +798,7 @@ class ServerNode(PGChildNodeView):
current_app.logger.exception(e)
return make_json_response(
success=0,
errormsg=e.message)
errormsg=str(e))
@pga_login_required
def delete(self, gid, sid):
@@ -737,14 +824,14 @@ class ServerNode(PGChildNodeView):
get_driver(PG_DEFAULT_DRIVER).delete_manager(s.id)
db.session.delete(s)
db.session.commit()
self.delete_shared_server(server_name, gid, sid)
self.delete_shared_server(gid, sid)
QueryHistory.clear_history(current_user.id, sid)
except Exception as e:
current_app.logger.exception(e)
return make_json_response(
success=0,
errormsg=e.message)
errormsg=str(e))
return make_json_response(success=1,
info=gettext("Server deleted"))
@@ -753,7 +840,7 @@ class ServerNode(PGChildNodeView):
@pga_login_required
def update(self, gid, sid):
"""Update the server settings"""
server = Server.query.filter_by(id=sid).first()
server = get_server(sid)
sharedserver = None
if server is None:
@@ -820,8 +907,8 @@ class ServerNode(PGChildNodeView):
data['db_res'] = ','.join(data['db_res'])
# Update connection parameter if any.
self.update_connection_parameter(data, server)
self.update_tags(data, server)
self.update_connection_parameter(data, server, sharedserver)
self.update_tags(data, sharedserver or server)
if 'connection_params' in data and \
'hostaddr' in data['connection_params'] and \
@@ -854,7 +941,7 @@ class ServerNode(PGChildNodeView):
# tags is JSON type, sqlalchemy sometimes will not detect change
if 'tags' in data:
flag_modified(server, 'tags')
flag_modified(sharedserver or server, 'tags')
try:
db.session.commit()
@@ -862,7 +949,7 @@ class ServerNode(PGChildNodeView):
current_app.logger.exception(e)
return make_json_response(
success=0,
errormsg=e.message
errormsg=str(e)
)
# When server is connected, we don't require to update the connection
@@ -870,6 +957,10 @@ class ServerNode(PGChildNodeView):
# which will affect the connections.
if not conn.connected():
manager.update(server)
# Suppress passexec for non-owners so the manager
# never holds the owner's password-exec command.
if _is_non_owner(server):
manager.passexec = None
return jsonify(
node=self.blueprint.generate_browser_node(
@@ -877,7 +968,7 @@ class ServerNode(PGChildNodeView):
server.name,
server_icon_and_background(
connected, manager, sharedserver)
if server.shared and server.user_id != current_user.id
if _is_non_owner(server)
else server_icon_and_background(
connected, manager, server),
True,
@@ -891,7 +982,7 @@ class ServerNode(PGChildNodeView):
role=server.role,
is_password_saved=bool(server.save_password),
description=server.comment,
tags=server.tags
tags=(sharedserver or server).tags
)
)
@@ -901,7 +992,7 @@ class ServerNode(PGChildNodeView):
if value == '':
value = None
if server.shared and server.user_id != current_user.id:
if _is_non_owner(server):
setattr(sharedserver, config_param_map[arg], value)
else:
setattr(server, config_param_map[arg], value)
@@ -915,22 +1006,37 @@ class ServerNode(PGChildNodeView):
if not crypt_key_present:
raise CryptKeyMissing
# Fields that non-owners must never set on their
# SharedServer — they enable command/SQL execution
# or are owner-level concepts not on SharedServer.
_owner_only_fields = frozenset({
'passexec_cmd', 'passexec_expiration',
'db_res', 'db_res_type',
})
for arg in config_param_map:
if arg in data:
# Non-owners cannot set dangerous fields.
if _is_non_owner(server) and \
arg in _owner_only_fields:
continue
value = data[arg]
if arg == 'password':
value = encrypt(data[arg], crypt_key)
# sqlite3 do not have boolean type so we need to convert
# it manually to integer
if 'shared' in data and not data['shared']:
# Delete the shared server from DB if server
# owner uncheck shared property
self.delete_shared_server(server.name, gid, server.id)
# sqlite3 do not have boolean type so we need to
# convert it manually to integer.
# Only the owner may unshare — this deletes ALL
# users' SharedServer records.
if 'shared' in data and not data['shared'] \
and not _is_non_owner(server):
self.delete_shared_server(gid, server.id)
if arg in ('sslcompression', 'use_ssh_tunnel',
'tunnel_authentication', 'kerberos_conn', 'shared'):
'tunnel_authentication',
'kerberos_conn', 'shared'):
value = 1 if value else 0
self._update_server_details(server, sharedserver,
config_param_map, arg, value)
config_param_map, arg,
value)
idx += 1
return idx
@@ -951,23 +1057,21 @@ class ServerNode(PGChildNodeView):
)
@pga_login_required
@with_object_filters
def list(self, gid, object_filters):
"""
Return list of attributes of all servers.
"""
servers = Server.query.filter(
or_(Server.user_id == current_user.id, Server.shared),
servers = get_user_server_query().filter(
Server.servergroup_id == gid,
Server.is_adhoc == 0).order_by(Server.name)
sg = ServerGroup.query.filter_by(
id=gid
).first()
sg = get_server_group(gid)
res = []
driver = get_driver(PG_DEFAULT_DRIVER)
for server in servers:
if server.shared and server.user_id != current_user.id:
if _is_non_owner(server):
shared_server = ServerModule.get_shared_server(server, gid)
server = \
ServerModule.get_shared_server_properties(server,
@@ -1001,8 +1105,7 @@ class ServerNode(PGChildNodeView):
def properties(self, gid, sid):
"""Return list of attributes of a server"""
server = Server.query.filter_by(
id=sid).first()
server = get_server(sid)
if server is None:
return make_json_response(
@@ -1025,7 +1128,7 @@ class ServerNode(PGChildNodeView):
# port and user when server is connected
display_connection_str = self.update_connection_string(manager, server)
if server.shared and server.user_id != current_user.id:
if _is_non_owner(server):
shared_server = ServerModule.get_shared_server(server, gid)
server = ServerModule.get_shared_server_properties(server,
shared_server)
@@ -1077,11 +1180,8 @@ class ServerNode(PGChildNodeView):
'fgcolor': server.fgcolor,
'db_res': get_db_restriction(server.db_res_type, server.db_res),
'db_res_type': server.db_res_type,
'passexec_cmd':
server.passexec_cmd if server.passexec_cmd else None,
'passexec_expiration':
server.passexec_expiration if server.passexec_expiration
else None,
'passexec_cmd': server.passexec_cmd,
'passexec_expiration': server.passexec_expiration,
'service': server.service if server.service else None,
'use_ssh_tunnel': use_ssh_tunnel,
'tunnel_host': tunnel_host,
@@ -1167,8 +1267,17 @@ class ServerNode(PGChildNodeView):
).format(arg)
)
connection_params = convert_connection_parameter(
data.get('connection_params', []))
# convert_connection_parameter() is bidirectional: list->dict for
# save (frontend shape), dict->list for display (DB shape). Here
# we want the storage shape (dict). If the input is already a
# dict (e.g. from internal callers / tests that mimic stored
# form), keep it as-is; otherwise assume frontend list shape and
# convert.
raw_params = data.get('connection_params', [])
if isinstance(raw_params, dict):
connection_params = raw_params
else:
connection_params = convert_connection_parameter(raw_params)
if 'hostaddr' in connection_params and \
not is_valid_ipaddress(connection_params['hostaddr']):
@@ -1394,7 +1503,12 @@ class ServerNode(PGChildNodeView):
def connect_status(self, gid, sid):
"""Check and return the connection status."""
server = Server.query.filter_by(id=sid).first()
server = get_server(sid)
if server is None:
return make_json_response(
status=410, success=0,
errormsg=self.not_found_error_msg()
)
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
conn = manager.connection()
connected = conn.connected()
@@ -1463,15 +1577,16 @@ class ServerNode(PGChildNodeView):
# function in that case no need to fetch the server detail based on
# sid.
if server is None:
server = Server.query.filter_by(id=sid).first()
server = get_server(sid)
if server is None:
return bad_request(self.not_found_error_msg())
shared_server = None
if server.shared and server.user_id != current_user.id:
if _is_non_owner(server):
shared_server = ServerModule.get_shared_server(server, gid)
server = ServerModule.get_shared_server_properties(server,
shared_server)
if server is None:
return bad_request(self.not_found_error_msg())
# Return if username is blank and the server is shared
if server.username is None and not server.service and \
@@ -1513,6 +1628,12 @@ class ServerNode(PGChildNodeView):
# the API call is not made from SQL Editor or View/Edit Data tool
if not manager.connection().connected() and not is_qt:
manager.update(server)
# Re-suppress passexec after update() which rebuilds
# from the (overlaid) server object. Belt-and-suspenders:
# the overlay already defaults passexec to None, but this
# guards against direct DB edits.
if _is_non_owner(server):
manager.passexec = None
conn = manager.connection()
# Get enc key
@@ -1613,12 +1734,8 @@ class ServerNode(PGChildNodeView):
else:
if save_password and config.ALLOW_SAVE_PASSWORD:
try:
# If DB server is running in trust mode then password may
# not be available but we don't need to ask password
# every time user try to connect
# 1 is True in SQLite as no boolean type
setattr(server, 'save_password', 1)
if server.shared and server.user_id != current_user.id:
if _is_non_owner(server):
setattr(shared_server, 'save_password', 1)
else:
setattr(server, 'save_password', 1)
@@ -1626,7 +1743,7 @@ class ServerNode(PGChildNodeView):
# Save the encrypted password using the user's login
# password key, if there is any password to save
if password:
if server.shared and server.user_id != current_user.id:
if _is_non_owner(server):
setattr(shared_server, 'password', password)
else:
setattr(server, 'password', password)
@@ -1637,12 +1754,16 @@ class ServerNode(PGChildNodeView):
manager.release(database=server.maintenance_db)
conn = None
return internal_server_error(errormsg=e.message)
return internal_server_error(errormsg=str(e))
if save_tunnel_password and config.ALLOW_SAVE_TUNNEL_PASSWORD:
try:
# Save the encrypted tunnel password.
setattr(server, 'tunnel_password', tunnel_password)
if _is_non_owner(server):
setattr(shared_server, 'tunnel_password',
tunnel_password)
else:
setattr(server, 'tunnel_password', tunnel_password)
db.session.commit()
except Exception as e:
# Release Connection
@@ -1650,7 +1771,7 @@ class ServerNode(PGChildNodeView):
manager.release(database=server.maintenance_db)
conn = None
return internal_server_error(errormsg=e.message)
return internal_server_error(errormsg=str(e))
current_app.logger.info('Connection Established for server: \
%s - %s' % (server.id, server.name))
@@ -1689,7 +1810,7 @@ class ServerNode(PGChildNodeView):
def disconnect(self, gid, sid):
"""Disconnect the Server."""
server = Server.query.filter_by(id=sid).first()
server = get_server(sid)
if server is None:
return bad_request(self.not_found_error_msg())
@@ -1814,7 +1935,7 @@ class ServerNode(PGChildNodeView):
raise CryptKeyMissing
# Fetch Server Details
server = Server.query.filter_by(id=sid).first()
server = get_server(sid, only_owned=False)
if server is None:
return bad_request(self.not_found_error_msg())
@@ -1901,11 +2022,24 @@ class ServerNode(PGChildNodeView):
# Store password in sqlite only if no pgpass file
if not is_passfile:
password = encrypt(data['newPassword'], crypt_key)
# Check if old password was stored in pgadmin4 sqlite database.
# If yes then update that password.
if server.password is not None and config.ALLOW_SAVE_PASSWORD:
setattr(server, 'password', password)
db.session.commit()
# Check if old password was stored in pgadmin4
# sqlite database. If yes then update that password.
# For non-owners of shared servers, check the
# SharedServer record (not the owner's Server).
if config.ALLOW_SAVE_PASSWORD:
if server.shared and \
server.user_id != current_user.id:
shared_server = \
ServerModule.get_shared_server(
server, gid)
if shared_server and \
shared_server.password is not None:
setattr(shared_server, 'password',
password)
db.session.commit()
elif server.password is not None:
setattr(server, 'password', password)
db.session.commit()
# Also update password in connection manager.
manager.password = password
manager.update_session()
@@ -1925,9 +2059,7 @@ class ServerNode(PGChildNodeView):
"""
Utility function for wal_replay for resume/pause.
"""
server = Server.query.filter_by(
user_id=current_user.id, id=sid
).first()
server = get_server(sid)
if server is None:
return make_json_response(
@@ -2011,9 +2143,7 @@ class ServerNode(PGChildNodeView):
sid: Server id
"""
is_pgpass = False
server = Server.query.filter_by(
user_id=current_user.id, id=sid
).first()
server = get_server(sid)
if server is None:
return make_json_response(
@@ -2104,38 +2234,22 @@ class ServerNode(PGChildNodeView):
:return:
"""
try:
server = Server.query.filter_by(id=sid).first()
shared_server = None
server = get_server(sid, only_owned=False)
if server is None:
return make_json_response(
success=0,
info=self.not_found_error_msg()
)
if server.shared and server.user_id != current_user.id:
shared_server = SharedServer.query.filter_by(
name=server.name, user_id=current_user.id,
servergroup_id=gid, osid=server.id).first()
if shared_server is None:
return make_json_response(
success=0,
info=gettext("Could not find the required server.")
)
server = ServerModule. \
get_shared_server_properties(server, shared_server)
if server.shared and server.user_id != current_user.id:
if _is_non_owner(server):
shared_server = ServerModule.get_shared_server(
server, gid)
setattr(shared_server, 'password', None)
if shared_server.save_password:
setattr(shared_server, 'save_password', 0)
else:
setattr(server, 'password', None)
# If password was saved then clear the flag also
# 0 is False in SQLite db
if server.save_password:
if server.shared and server.user_id != current_user.id:
setattr(shared_server, 'save_password', 0)
else:
if server.save_password:
setattr(server, 'save_password', 0)
db.session.commit()
except Exception as e:
@@ -2161,13 +2275,19 @@ class ServerNode(PGChildNodeView):
:return:
"""
try:
server = Server.query.filter_by(id=sid).first()
server = get_server(sid, only_owned=False)
if server is None:
return make_json_response(
success=0,
info=self.not_found_error_msg()
)
setattr(server, 'tunnel_password', None)
if _is_non_owner(server):
shared_server = ServerModule.get_shared_server(
server, gid)
setattr(shared_server, 'tunnel_password', None)
else:
setattr(server, 'tunnel_password', None)
db.session.commit()
except Exception as e:
current_app.logger.error(
@@ -34,6 +34,7 @@ from pgadmin.tools.sqleditor.utils.query_history import QueryHistory
from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry
from pgadmin.model import db, Server, Database
from pgadmin.utils.server_access import get_server
from pgadmin.browser.utils import underscore_escape
from pgadmin.utils.constants import TWO_PARAM_STRING
@@ -579,7 +580,9 @@ class DatabaseView(PGChildNodeView):
'already_connected': already_connected,
'connected': True,
'info_prefix': TWO_PARAM_STRING.
format(Server.query.filter_by(id=sid)[0].name, conn.db)
format(getattr(
get_server(sid), 'name', None) or
_('Unknown'), conn.db)
}
)
@@ -602,7 +605,9 @@ class DatabaseView(PGChildNodeView):
'icon': 'icon-database-not-connected',
'connected': False,
'info_prefix': TWO_PARAM_STRING.
format(Server.query.filter_by(id=sid)[0].name, conn.db)
format(getattr(
get_server(sid), 'name', None) or
_('Unknown'), conn.db)
}
)
@@ -866,7 +871,7 @@ class DatabaseView(PGChildNodeView):
db.session.commit()
except Exception as e:
current_app.logger.exception(e)
return True, e.message, False
return True, str(e), False
return False, '', can_drop
def _get_data_from_request(self):
@@ -7,7 +7,7 @@ CREATE SERVER "FS_$%{}[]()&*^!@""'`\/#"
FOREIGN DATA WRAPPER test_fdw_for_fs;
ALTER SERVER "FS_$%{}[]()&*^!@""'`\/#"
OWNER TO postgres;
OWNER TO <OWNER>;
COMMENT ON SERVER "FS_$%{}[]()&*^!@""'`\/#"
IS 'Test comment';
@@ -8,11 +8,11 @@ CREATE SERVER "FS_$%{}[]()&*^!@""'`\/#"
OPTIONS (host '192.168.1.1', dbname 'edb', port '5450');
ALTER SERVER "FS_$%{}[]()&*^!@""'`\/#"
OWNER TO postgres;
OWNER TO <OWNER>;
COMMENT ON SERVER "FS_$%{}[]()&*^!@""'`\/#"
IS 'Test comment';
GRANT USAGE ON FOREIGN SERVER "FS_$%{}[]()&*^!@""'`\/#" TO PUBLIC;
GRANT USAGE ON FOREIGN SERVER "FS_$%{}[]()&*^!@""'`\/#" TO postgres;
GRANT USAGE ON FOREIGN SERVER "FS_$%{}[]()&*^!@""'`\/#" TO <OWNER>;
@@ -7,9 +7,9 @@ CREATE SERVER "FS_$%{}[]()&*^!@""'`\/#"
FOREIGN DATA WRAPPER test_fdw_for_fs;
ALTER SERVER "FS_$%{}[]()&*^!@""'`\/#"
OWNER TO postgres;
OWNER TO <OWNER>;
COMMENT ON SERVER "FS_$%{}[]()&*^!@""'`\/#"
IS 'Test comment';
GRANT USAGE ON FOREIGN SERVER "FS_$%{}[]()&*^!@""'`\/#" TO postgres;
GRANT USAGE ON FOREIGN SERVER "FS_$%{}[]()&*^!@""'`\/#" TO <OWNER>;
@@ -9,10 +9,10 @@ CREATE SERVER "FS_$%{}[]()&*^!@""'`\/#"
OPTIONS (host '192.168.1.1', dbname 'edb', port '5450');
ALTER SERVER "FS_$%{}[]()&*^!@""'`\/#"
OWNER TO postgres;
OWNER TO <OWNER>;
COMMENT ON SERVER "FS_$%{}[]()&*^!@""'`\/#"
IS 'Test comment';
GRANT USAGE ON FOREIGN SERVER "FS_$%{}[]()&*^!@""'`\/#" TO PUBLIC;
GRANT USAGE ON FOREIGN SERVER "FS_$%{}[]()&*^!@""'`\/#" TO postgres;
GRANT USAGE ON FOREIGN SERVER "FS_$%{}[]()&*^!@""'`\/#" TO <OWNER>;
@@ -5,7 +5,7 @@ CREATE SERVER "FS_$%{}[]()&*^!@""'`\/#"
OPTIONS (host '192.168.1.1', dbname 'edb', port '5450');
ALTER SERVER "FS_$%{}[]()&*^!@""'`\/#"
OWNER TO postgres;
OWNER TO <OWNER>;
COMMENT ON SERVER "FS_$%{}[]()&*^!@""'`\/#"
IS 'Test comment';
@@ -32,11 +32,11 @@
"fsrvoption":"port",
"fsrvvalue":"5450"
}],
"fsrvowner":"postgres",
"fsrvowner":"<OWNER>",
"description":"Test comment",
"fsrvacl":[{
"grantee":"PUBLIC",
"grantor":"postgres",
"grantor":"<OWNER>",
"privileges":[{
"privilege_type":"U",
"privilege":true,
@@ -60,7 +60,7 @@
"sql_endpoint": "NODE-foreign_server.sql_id",
"data": {
"name":"FS_$%{}[]()&*^!@\"'`\\/#",
"fsrvowner":"postgres"
"fsrvowner":"<OWNER>"
}
}, {
"type": "alter",
@@ -96,7 +96,7 @@
"fsrvacl":{
"added": [{
"grantee":"PUBLIC",
"grantor":"postgres",
"grantor":"<OWNER>",
"privileges":[{
"privilege_type":"U",
"privilege":true,
@@ -129,7 +129,7 @@
"fsrvacl":{
"deleted": [{
"grantee":"PUBLIC",
"grantor":"postgres",
"grantor":"<OWNER>",
"privileges":[{
"privilege_type":"U",
"privilege":true,
@@ -1,6 +1,6 @@
-- User Mapping : postgres
-- User Mapping : <OWNER>
-- DROP USER MAPPING IF EXISTS FOR postgres SERVER test_fs_for_user_mapping
-- DROP USER MAPPING IF EXISTS FOR <OWNER> SERVER test_fs_for_user_mapping
CREATE USER MAPPING FOR postgres SERVER test_fs_for_user_mapping
CREATE USER MAPPING FOR <OWNER> SERVER test_fs_for_user_mapping
OPTIONS ("user" 'test_user12', password 'secret123');
@@ -1,5 +1,5 @@
-- User Mapping : postgres
-- User Mapping : <OWNER>
-- DROP USER MAPPING IF EXISTS FOR postgres SERVER test_fs_for_user_mapping
-- DROP USER MAPPING IF EXISTS FOR <OWNER> SERVER test_fs_for_user_mapping
CREATE USER MAPPING FOR postgres SERVER test_fs_for_user_mapping;
CREATE USER MAPPING FOR <OWNER> SERVER test_fs_for_user_mapping;
@@ -1,2 +1,2 @@
ALTER USER MAPPING FOR postgres SERVER test_fs_for_user_mapping
ALTER USER MAPPING FOR <OWNER> SERVER test_fs_for_user_mapping
OPTIONS (DROP "user", DROP password);
@@ -1,2 +1,2 @@
ALTER USER MAPPING FOR postgres SERVER test_fs_for_user_mapping
ALTER USER MAPPING FOR <OWNER> SERVER test_fs_for_user_mapping
OPTIONS (SET "user" 'test_user12', SET password 'secret123');
@@ -1,6 +1,6 @@
-- User Mapping : postgres
-- User Mapping : <OWNER>
-- DROP USER MAPPING IF EXISTS FOR postgres SERVER test_fs_for_user_mapping
-- DROP USER MAPPING IF EXISTS FOR <OWNER> SERVER test_fs_for_user_mapping
CREATE USER MAPPING FOR postgres SERVER test_fs_for_user_mapping
CREATE USER MAPPING FOR <OWNER> SERVER test_fs_for_user_mapping
OPTIONS ("user" 'test_user', password 'secret');
@@ -1,2 +1,2 @@
CREATE USER MAPPING FOR postgres SERVER test_fs_for_user_mapping
CREATE USER MAPPING FOR <OWNER> SERVER test_fs_for_user_mapping
OPTIONS ("user" 'test_user', password 'secret');
@@ -27,7 +27,7 @@
"sql_endpoint": "NODE-user_mapping.sql_id",
"msql_endpoint": "NODE-user_mapping.msql",
"data": {
"name": "postgres",
"name": "<OWNER>",
"umoptions":[{
"umoption":"user",
"umvalue":"test_user"
@@ -13,6 +13,7 @@ import sys
import traceback
import json
from pgadmin.utils.driver.psycopg3 import Driver
from regression.python_test_utils.test_utils import get_db_connection,\
set_isolation_level
@@ -61,8 +62,12 @@ def create_user_mapping(server, db_name, fsrv_name):
old_isolation_level = connection.isolation_level
set_isolation_level(connection, 0)
pg_cursor = connection.cursor()
# Quote the username as an SQL identifier so usernames containing
# special characters (e.g. dots like 'ashesh.vashi') do not parse
# as schema-qualified names.
quoted_username = Driver.qtIdent(None, server['username'])
query = "CREATE USER MAPPING FOR %s SERVER %s OPTIONS" \
" (user '%s', password '%s')" % (server['username'],
" (user '%s', password '%s')" % (quoted_username,
fsrv_name,
server['username'],
server['db_password']
@@ -13,4 +13,4 @@ COMMENT ON FOREIGN DATA WRAPPER "Fdw2_$%{}[]()&*^!@""'`\/#"
IS 'a comment';
GRANT USAGE ON FOREIGN DATA WRAPPER "Fdw2_$%{}[]()&*^!@""'`\/#" TO PUBLIC;
GRANT USAGE ON FOREIGN DATA WRAPPER "Fdw2_$%{}[]()&*^!@""'`\/#" TO postgres WITH GRANT OPTION;
GRANT USAGE ON FOREIGN DATA WRAPPER "Fdw2_$%{}[]()&*^!@""'`\/#" TO <OWNER> WITH GRANT OPTION;
@@ -103,7 +103,7 @@
"fdwacl": {
"added":[{
"grantee": "PUBLIC",
"grantor": "postgres",
"grantor": "<OWNER>",
"privileges":[
{
"privilege_type": "U",
@@ -111,8 +111,8 @@
"with_grant": false
}]
},{
"grantee": "postgres",
"grantor": "postgres",
"grantee": "<OWNER>",
"grantor": "<OWNER>",
"privileges":[{
"privilege_type": "U",
"privilege": true,
@@ -262,7 +262,7 @@
"lanproc": "plpgsql_call_handler",
"lanval": null,
"description": null,
"lanowner": "postgres",
"lanowner": "<OWNER>",
"laninl": null,
"seclabels": null
}
@@ -15,4 +15,4 @@ COMMENT ON LANGUAGE "Lan2_$%{}[]()&*^!@""'`\/#"
GRANT USAGE ON LANGUAGE "Lan2_$%{}[]()&*^!@""'`\/#" TO PUBLIC;
GRANT USAGE ON LANGUAGE "Lan2_$%{}[]()&*^!@""'`\/#" TO postgres WITH GRANT OPTION;
GRANT USAGE ON LANGUAGE "Lan2_$%{}[]()&*^!@""'`\/#" TO <OWNER> WITH GRANT OPTION;
@@ -3,4 +3,4 @@ COMMENT ON LANGUAGE "Lan2_$%{}[]()&*^!@""'`\/#"
GRANT USAGE ON LANGUAGE "Lan2_$%{}[]()&*^!@""'`\/#" TO PUBLIC;
GRANT USAGE ON LANGUAGE "Lan2_$%{}[]()&*^!@""'`\/#" TO postgres WITH GRANT OPTION;
GRANT USAGE ON LANGUAGE "Lan2_$%{}[]()&*^!@""'`\/#" TO <OWNER> WITH GRANT OPTION;
@@ -1,2 +1,2 @@
REVOKE ALL ON LANGUAGE "Lan2_$%{}[]()&*^!@""'`\/#" FROM PUBLIC;
REVOKE ALL ON LANGUAGE "Lan2_$%{}[]()&*^!@""'`\/#" FROM postgres;
REVOKE ALL ON LANGUAGE "Lan2_$%{}[]()&*^!@""'`\/#" FROM <OWNER>;
@@ -40,7 +40,7 @@
"lanacl": {
"added":[{
"grantee": "PUBLIC",
"grantor": "postgres",
"grantor": "<OWNER>",
"privileges":[
{
"privilege_type": "U",
@@ -48,8 +48,8 @@
"with_grant": false
}]
},{
"grantee": "postgres",
"grantor": "postgres",
"grantee": "<OWNER>",
"grantor": "<OWNER>",
"privileges":[{
"privilege_type": "U",
"privilege": true,
@@ -70,7 +70,7 @@
"lanacl": {
"deleted":[{
"grantee": "PUBLIC",
"grantor": "postgres",
"grantor": "<OWNER>",
"privileges":[
{
"privilege_type": "U",
@@ -78,8 +78,8 @@
"with_grant": false
}]
},{
"grantee": "postgres",
"grantor": "postgres",
"grantee": "<OWNER>",
"grantor": "<OWNER>",
"privileges":[{
"privilege_type": "U",
"privilege": true,
@@ -64,7 +64,7 @@
"evnt_update": true,
"evnt_delete": false,
"evnt_truncate": false,
"pubowner": "postgres",
"pubowner": "<OWNER>",
"publish_via_partition_root": false,
"all_table": true,
"pubtable": ""
@@ -116,11 +116,13 @@
"evnt_update": true,
"evnt_delete": true,
"evnt_truncate": true,
"pubowner": "postgres",
"pubowner": "<OWNER>",
"publish_via_partition_root": false,
"all_table": false,
"only_table": false,
"pubtable": ["public.test_table_publication"]
"pubtable": [
"public.test_table_publication"
]
},
"expected_sql_file": "create_publication_few_tables.sql",
"expected_msql_file": "create_publication_few_tables_msql.sql"
@@ -132,7 +134,10 @@
"sql_endpoint": "NODE-publication.sql_id",
"msql_endpoint": "NODE-publication.msql_id",
"data": {
"pubtable": ["public.test_table_publication", "public.test_table_publication_2"]
"pubtable": [
"public.test_table_publication",
"public.test_table_publication_2"
]
},
"expected_sql_file": "alter_publication_add_tables.sql",
"expected_msql_file": "alter_publication_add_tables_msql.sql"
@@ -144,7 +149,9 @@
"sql_endpoint": "NODE-publication.sql_id",
"msql_endpoint": "NODE-publication.msql_id",
"data": {
"pubtable": ["public.test_table_publication_2"]
"pubtable": [
"public.test_table_publication_2"
]
},
"expected_sql_file": "alter_publication_drop_tables.sql",
"expected_msql_file": "alter_publication_drop_tables_msql.sql"
@@ -84,7 +84,7 @@
"evnt_update": true,
"evnt_delete": false,
"evnt_truncate": false,
"pubowner": "postgres",
"pubowner": "<OWNER>",
"publish_via_partition_root": false,
"all_table": true,
"only_table": false,
@@ -139,13 +139,15 @@
"evnt_update": true,
"evnt_delete": true,
"evnt_truncate": true,
"pubowner": "postgres",
"pubowner": "<OWNER>",
"publish_via_partition_root": false,
"all_table": false,
"only_table": false,
"pubtable": [{
"table_name": "public.test_table_publication"
}],
"pubtable": [
{
"table_name": "public.test_table_publication"
}
],
"pubschema": ""
},
"expected_sql_file": "create_publication_few_tables.sql",
@@ -159,9 +161,11 @@
"msql_endpoint": "NODE-publication.msql_id",
"data": {
"pubtable": {
"added": [{
"table_name": "public.test_table_publication_2"
}]
"added": [
{
"table_name": "public.test_table_publication_2"
}
]
}
},
"expected_sql_file": "alter_publication_add_tables.sql",
@@ -175,9 +179,11 @@
"msql_endpoint": "NODE-publication.msql_id",
"data": {
"pubtable": {
"deleted": [{
"table_name": "public.test_table_publication_2"
}]
"deleted": [
{
"table_name": "public.test_table_publication_2"
}
]
}
},
"expected_sql_file": "alter_publication_drop_tables.sql",
@@ -191,10 +197,14 @@
"msql_endpoint": "NODE-publication.msql_id",
"data": {
"pubtable": {
"added": [{
"table_name": "public.test_table_publication_2",
"columns": ["dept_id"]
}]
"added": [
{
"table_name": "public.test_table_publication_2",
"columns": [
"dept_id"
]
}
]
}
},
"expected_sql_file": "alter_publication_add_tables_columns.sql",
@@ -208,10 +218,14 @@
"msql_endpoint": "NODE-publication.msql_id",
"data": {
"pubtable": {
"changed": [{
"table_name": "public.test_table_publication_2",
"columns": ["dept_name"]
}]
"changed": [
{
"table_name": "public.test_table_publication_2",
"columns": [
"dept_name"
]
}
]
}
},
"expected_sql_file": "alter_publication_set_tables_columns.sql",
@@ -225,9 +239,11 @@
"msql_endpoint": "NODE-publication.msql_id",
"data": {
"pubtable": {
"deleted": [{
"table_name": "public.test_table_publication_2"
}]
"deleted": [
{
"table_name": "public.test_table_publication_2"
}
]
}
},
"expected_sql_file": "alter_publication_drop_tables.sql",
@@ -241,10 +257,12 @@
"msql_endpoint": "NODE-publication.msql_id",
"data": {
"pubtable": {
"added": [{
"table_name": "public.test_table_publication_2",
"where": "dept_id=2"
}]
"added": [
{
"table_name": "public.test_table_publication_2",
"where": "dept_id=2"
}
]
}
},
"expected_sql_file": "alter_publication_add_tables_where.sql",
@@ -258,10 +276,12 @@
"msql_endpoint": "NODE-publication.msql_id",
"data": {
"pubtable": {
"changed": [{
"table_name": "public.test_table_publication_2",
"where": "dept_name='test'"
}]
"changed": [
{
"table_name": "public.test_table_publication_2",
"where": "dept_name='test'"
}
]
}
},
"expected_sql_file": "alter_publication_set_tables_where.sql",
@@ -275,9 +295,11 @@
"msql_endpoint": "NODE-publication.msql_id",
"data": {
"pubtable": {
"deleted": [{
"table_name": "public.test_table_publication_2"
}]
"deleted": [
{
"table_name": "public.test_table_publication_2"
}
]
}
},
"expected_sql_file": "alter_publication_drop_tables.sql",
@@ -291,11 +313,15 @@
"msql_endpoint": "NODE-publication.msql_id",
"data": {
"pubtable": {
"added": [{
"table_name": "public.test_table_publication_2",
"columns": ["dept_id"],
"where": "dept_id=2"
}]
"added": [
{
"table_name": "public.test_table_publication_2",
"columns": [
"dept_id"
],
"where": "dept_id=2"
}
]
}
},
"expected_sql_file": "alter_publication_add_tables_columns_where.sql",
@@ -309,11 +335,15 @@
"msql_endpoint": "NODE-publication.msql_id",
"data": {
"pubtable": {
"changed": [{
"table_name": "public.test_table_publication_2",
"columns": ["dept_name"],
"where": "dept_name='test'"
}]
"changed": [
{
"table_name": "public.test_table_publication_2",
"columns": [
"dept_name"
],
"where": "dept_name='test'"
}
]
}
},
"expected_sql_file": "alter_publication_set_tables_columns_where.sql",
@@ -339,12 +369,14 @@
"evnt_update": true,
"evnt_delete": true,
"evnt_truncate": true,
"pubowner": "postgres",
"pubowner": "<OWNER>",
"publish_via_partition_root": false,
"all_table": false,
"only_table": false,
"pubtable": "",
"pubschema": ["test_schema_publication"]
"pubschema": [
"test_schema_publication"
]
},
"expected_sql_file": "create_publication_few_schemas.sql",
"expected_msql_file": "create_publication_few_schemas_msql.sql"
@@ -356,7 +388,10 @@
"sql_endpoint": "NODE-publication.sql_id",
"msql_endpoint": "NODE-publication.msql_id",
"data": {
"pubschema": ["test_schema_publication","test_schema_publication_2"]
"pubschema": [
"test_schema_publication",
"test_schema_publication_2"
]
},
"expected_sql_file": "alter_publication_add_schemas.sql",
"expected_msql_file": "alter_publication_add_schemas_msql.sql"
@@ -368,7 +403,9 @@
"sql_endpoint": "NODE-publication.sql_id",
"msql_endpoint": "NODE-publication.msql_id",
"data": {
"pubschema": ["test_schema_publication_2"]
"pubschema": [
"test_schema_publication_2"
]
},
"expected_sql_file": "alter_publication_drop_schemas.sql",
"expected_msql_file": "alter_publication_drop_schemas_msql.sql"
@@ -393,13 +430,15 @@
"evnt_update": true,
"evnt_delete": true,
"evnt_truncate": true,
"pubowner": "postgres",
"pubowner": "<OWNER>",
"publish_via_partition_root": false,
"all_table": false,
"only_table": true,
"pubtable": [{
"table_name": "public.test_table_publication"
}],
"pubtable": [
{
"table_name": "public.test_table_publication"
}
],
"pubschema": ""
},
"expected_sql_file": "create_publication_few_tables_only.sql",
@@ -425,14 +464,18 @@
"evnt_update": true,
"evnt_delete": true,
"evnt_truncate": true,
"pubowner": "postgres",
"pubowner": "<OWNER>",
"publish_via_partition_root": false,
"all_table": false,
"only_table": false,
"pubtable": [{
"table_name": "public.test_table_publication"
}],
"pubschema": ["test_schema_publication"]
"pubtable": [
{
"table_name": "public.test_table_publication"
}
],
"pubschema": [
"test_schema_publication"
]
},
"expected_sql_file": "create_publication_few_tables_schemas.sql",
"expected_msql_file": "create_publication_few_tables_schemas_msql.sql"
@@ -457,14 +500,19 @@
"evnt_update": true,
"evnt_delete": true,
"evnt_truncate": true,
"pubowner": "postgres",
"pubowner": "<OWNER>",
"publish_via_partition_root": false,
"all_table": false,
"only_table": false,
"pubtable": [{
"table_name": "public.test_table_publication",
"columns": ["emp_id", "name"]
}],
"pubtable": [
{
"table_name": "public.test_table_publication",
"columns": [
"emp_id",
"name"
]
}
],
"pubschema": ""
},
"expected_sql_file": "create_publication_few_tables_columns.sql",
@@ -490,14 +538,16 @@
"evnt_update": true,
"evnt_delete": true,
"evnt_truncate": true,
"pubowner": "postgres",
"pubowner": "<OWNER>",
"publish_via_partition_root": false,
"all_table": false,
"only_table": false,
"pubtable": [{
"table_name": "public.test_table_publication",
"where": "emp_id=2 and name='test'"
}],
"pubtable": [
{
"table_name": "public.test_table_publication",
"where": "emp_id=2 and name='test'"
}
],
"pubschema": ""
},
"expected_sql_file": "create_publication_few_tables_where.sql",
@@ -523,15 +573,20 @@
"evnt_update": true,
"evnt_delete": true,
"evnt_truncate": true,
"pubowner": "postgres",
"pubowner": "<OWNER>",
"publish_via_partition_root": false,
"all_table": false,
"only_table": false,
"pubtable": [{
"table_name": "public.test_table_publication",
"columns": ["emp_id", "name"],
"where": "emp_id=2 and name='test'"
}],
"pubtable": [
{
"table_name": "public.test_table_publication",
"columns": [
"emp_id",
"name"
],
"where": "emp_id=2 and name='test'"
}
],
"pubschema": ""
},
"expected_sql_file": "create_publication_few_tables_columns_where.sql",
@@ -557,15 +612,19 @@
"evnt_update": true,
"evnt_delete": true,
"evnt_truncate": true,
"pubowner": "postgres",
"pubowner": "<OWNER>",
"publish_via_partition_root": false,
"all_table": false,
"only_table": false,
"pubtable": [{
"table_name": "public.test_table_publication",
"where": "emp_id=2 and name='test'"
}],
"pubschema": ["test_schema_publication"]
"pubtable": [
{
"table_name": "public.test_table_publication",
"where": "emp_id=2 and name='test'"
}
],
"pubschema": [
"test_schema_publication"
]
},
"expected_sql_file": "create_publication_few_tables_where_schemas.sql",
"expected_msql_file": "create_publication_few_tables_where_schemas_msql.sql"
@@ -64,7 +64,7 @@
"evnt_update": true,
"evnt_delete": false,
"evnt_truncate": false,
"pubowner": "postgres",
"pubowner": "<OWNER>",
"all_table": true,
"pubtable": ""
},
@@ -115,10 +115,12 @@
"evnt_update": true,
"evnt_delete": true,
"evnt_truncate": true,
"pubowner": "postgres",
"pubowner": "<OWNER>",
"all_table": false,
"only_table": false,
"pubtable": ["public.test_table_publication"]
"pubtable": [
"public.test_table_publication"
]
},
"expected_sql_file": "create_publication_few_tables.sql",
"expected_msql_file": "create_publication_few_tables_msql.sql"
@@ -130,7 +132,10 @@
"sql_endpoint": "NODE-publication.sql_id",
"msql_endpoint": "NODE-publication.msql_id",
"data": {
"pubtable": ["public.test_table_publication", "public.test_table_publication_2"]
"pubtable": [
"public.test_table_publication",
"public.test_table_publication_2"
]
},
"expected_sql_file": "alter_publication_add_tables.sql",
"expected_msql_file": "alter_publication_add_tables_msql.sql"
@@ -142,7 +147,9 @@
"sql_endpoint": "NODE-publication.sql_id",
"msql_endpoint": "NODE-publication.msql_id",
"data": {
"pubtable": ["public.test_table_publication_2"]
"pubtable": [
"public.test_table_publication_2"
]
},
"expected_sql_file": "alter_publication_drop_tables.sql",
"expected_msql_file": "alter_publication_drop_tables_msql.sql"
@@ -44,7 +44,7 @@
"data": {
"name": "Cl1_$%{}[]()&*^!@\"'`\\/#b",
"schema": "testschema",
"owner": "postgres",
"owner": "<OWNER>",
"description": "Description for extra params",
"locale": "locale",
"provider": "icu",
@@ -70,7 +70,7 @@
"data": {
"name": "Cl1_$%{}[]()&*^!@\"'`\\/#b",
"schema": "testschema",
"owner": "postgres",
"owner": "<OWNER>",
"description": "libc",
"locale": "C",
"provider": "libc",
@@ -44,7 +44,7 @@
"data": {
"name": "Cl1_$%{}[]()&*^!@\"'`\\/#b",
"schema": "testschema",
"owner": "postgres",
"owner": "<OWNER>",
"description": "Description for extra params",
"locale": "locale",
"provider": "icu",
@@ -70,7 +70,7 @@
"data": {
"name": "Cl1_$%{}[]()&*^!@\"'`\\/#b",
"schema": "testschema",
"owner": "postgres",
"owner": "<OWNER>",
"description": "libc",
"locale": "C",
"provider": "libc",
@@ -95,7 +95,7 @@
"data": {
"name": "Cl1_$%{}[]()&*^!@\"'`\\/#b",
"schema": "testschema",
"owner": "postgres",
"owner": "<OWNER>",
"description": "builtin",
"locale": "C",
"provider": "builtin",
@@ -8,7 +8,7 @@ CREATE DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#"
DEFAULT 5
NOT NULL;
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#" OWNER TO postgres;
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#" OWNER TO <OWNER>;
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#"
ADD CONSTRAINT constraint_1 CHECK (3 < 5);
@@ -7,7 +7,7 @@ CREATE DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#"
COLLATE pg_catalog."C"
DEFAULT 3;
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#" OWNER TO postgres;
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#" OWNER TO <OWNER>;
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#"
ADD CONSTRAINT constraint_1 CHECK (3 < 5);
@@ -7,7 +7,7 @@ CREATE DOMAIN public."Dom2_$%{}[]()&*^!@""'`\/#"
COLLATE pg_catalog."C"
DEFAULT 3;
ALTER DOMAIN public."Dom2_$%{}[]()&*^!@""'`\/#" OWNER TO postgres;
ALTER DOMAIN public."Dom2_$%{}[]()&*^!@""'`\/#" OWNER TO <OWNER>;
ALTER DOMAIN public."Dom2_$%{}[]()&*^!@""'`\/#"
ADD CONSTRAINT constraint_1 CHECK (3 < 5);
@@ -7,7 +7,7 @@ CREATE DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#"
COLLATE pg_catalog."C"
DEFAULT 5;
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#" OWNER TO postgres;
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#" OWNER TO <OWNER>;
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#"
ADD CONSTRAINT constraint_1 CHECK (3 < 5);
@@ -8,7 +8,7 @@ CREATE DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#"
DEFAULT 5
NOT NULL;
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#" OWNER TO postgres;
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#" OWNER TO <OWNER>;
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#"
ADD CONSTRAINT constraint_1 CHECK (3 < 5);
@@ -7,7 +7,7 @@ CREATE DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#"
DEFAULT 3
NOT NULL;
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#" OWNER TO postgres;
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#" OWNER TO <OWNER>;
COMMENT ON DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#"
IS 'test_comment';
@@ -4,7 +4,7 @@ CREATE DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#"
DEFAULT 5
NOT NULL;
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#" OWNER TO postgres;
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#" OWNER TO <OWNER>;
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#"
ADD CONSTRAINT constraint_1 CHECK (3 < 5);
@@ -3,7 +3,7 @@ CREATE DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#"
DEFAULT 3
NOT NULL;
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#" OWNER TO postgres;
ALTER DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#" OWNER TO <OWNER>;
COMMENT ON DOMAIN public."Dom1_$%{}[]()&*^!@""'`\/#"
IS 'test_comment';
@@ -9,7 +9,7 @@
"name": "Dom1_$%{}[]()&*^!@\"'`\\/#",
"schema": "public",
"schema_id": "<SCHEMA_ID>",
"owner": "postgres",
"owner": "<OWNER>",
"basensp": "public",
"description": "test_comment",
"basetype": "text",
@@ -90,7 +90,7 @@
"name": "Dom1_$%{}[]()&*^!@\"'`\\/#",
"schema": "public",
"schema_id": "<SCHEMA_ID>",
"owner": "postgres",
"owner": "<OWNER>",
"basensp": "public",
"description": "test_comment",
"basetype": "numeric",
@@ -101,13 +101,16 @@ class DomainReverseEngineeredSQLTestCase(BaseTestGenerator):
# Replace multiple spaces with one space and check the expected sql
sql = re.sub(r'\s+', ' ', orig_sql).strip()
from pgadmin.utils.driver.psycopg3 import Driver
expected_sql = '-- DOMAIN: {0}.{1} -- DROP DOMAIN IF EXISTS ' \
'{0}.{1}; CREATE DOMAIN {0}.{1} {2} ' \
'ALTER DOMAIN {0}.{1} OWNER' \
' TO {3};'.format(self.schema_name,
self.test_data['domain_name'],
self.test_data['domain_sql'],
self.server["username"])
Driver.qtIdent(
None,
self.server["username"]))
self.assertEqual(sql, expected_sql)
@@ -18,7 +18,7 @@
"endpoint": "NODE-foreign_server.obj",
"sql_endpoint": "NODE-foreign_server.sql_id",
"data": {
"name":"test_fs_for_foreign_table_column"
"name": "test_fs_for_foreign_table_column"
},
"store_object_id": "True"
},
@@ -33,7 +33,7 @@
"schema": "public",
"basensp": "public",
"ftsrvname": "test_fs_for_foreign_table_column",
"owner": "postgres",
"owner": "<OWNER>",
"ftoptions": [],
"inherits": [],
"relacl": [],
@@ -51,9 +51,9 @@
"name": "col_1_$%{}[]()&*^!@\"'`\\/#",
"description": "Comment for create",
"cltype": "numeric",
"attlen":"10",
"attprecision":"5",
"colconstype":"n",
"attlen": "10",
"attprecision": "5",
"colconstype": "n",
"attnotnull": true,
"attoptions": [],
"seclabels": [],
@@ -73,7 +73,14 @@
"name": "new_col_1_$%{}[]()&*^!@\"'`\\/#",
"description": "Comment for alter",
"cltype": "real",
"coloptions":{"added":[{"option":"column_name","value":"test"}]}
"coloptions": {
"added": [
{
"option": "column_name",
"value": "test"
}
]
}
},
"expected_sql_file": "alter_column_numeric.sql",
"expected_msql_file": "alter_column_numeric.msql"
@@ -153,7 +160,7 @@
"data": {
"attlen": "10",
"attnotnull": false,
"attnum":3,
"attnum": 3,
"description": "test comment modification"
},
"expected_sql_file": "alter_column_timestamp_with_default_value_using_function.sql",
@@ -174,16 +181,28 @@
"endpoint": "NODE-foreign_table_column.obj",
"sql_endpoint": "NODE-foreign_table_column.sql_id",
"msql_endpoint": "NODE-foreign_table_column.msql",
"data": {"name":"col_4_$%{}[]()&*^!@\"'`\\/#",
"description":"test comment",
"cltype":"bigint",
"attnotnull":true,
"coloptions":[{"option":"column_name","value":"test_options"}],
"colconstype":"g",
"is_tlength":false,
"is_precision":false,
"genexpr":"1000+1",
"attoptions":[{"name":"n_distinct","value":"1"}]},
"data": {
"name": "col_4_$%{}[]()&*^!@\"'`\\/#",
"description": "test comment",
"cltype": "bigint",
"attnotnull": true,
"coloptions": [
{
"option": "column_name",
"value": "test_options"
}
],
"colconstype": "g",
"is_tlength": false,
"is_precision": false,
"genexpr": "1000+1",
"attoptions": [
{
"name": "n_distinct",
"value": "1"
}
]
},
"expected_sql_file": "create_column_with_integer_generated_always_column_option_variables.sql",
"expected_msql_file": "create_column_with_integer_generated_always_column_option_variables.msql"
},
@@ -193,11 +212,25 @@
"endpoint": "NODE-foreign_table_column.obj_id",
"sql_endpoint": "NODE-foreign_table_column.sql_id",
"msql_endpoint": "NODE-foreign_table_column.msql_id",
"data":{
"attoptions":{
"changed":[{"name":"n_distinct","value":"111"}]},
"coloptions":{"changed":[{"option":"column_name","value":"test_options_update"}]},
"attnum":4},
"data": {
"attoptions": {
"changed": [
{
"name": "n_distinct",
"value": "111"
}
]
},
"coloptions": {
"changed": [
{
"option": "column_name",
"value": "test_options_update"
}
]
},
"attnum": 4
},
"expected_sql_file": "alter_column_with_integer_generated_always_column_option_variables.sql",
"expected_msql_file": "alter_column_with_integer_generated_always_column_option_variables.msql"
},
@@ -207,7 +240,8 @@
"endpoint": "NODE-foreign_table_column.obj_id",
"sql_endpoint": "NODE-foreign_table_columnn.sql_id",
"data": {
"name":"col_4_$%{}[]()&*^!@\"'`\\/#"
"name": "col_4_$%{}[]()&*^!@\"'`\\/#"
}
}
]}
]
}
@@ -18,7 +18,7 @@
"endpoint": "NODE-foreign_server.obj",
"sql_endpoint": "NODE-foreign_server.sql_id",
"data": {
"name":"test_fs_for_foreign_table_column"
"name": "test_fs_for_foreign_table_column"
},
"store_object_id": "True"
},
@@ -33,7 +33,7 @@
"schema": "public",
"basensp": "public",
"ftsrvname": "test_fs_for_foreign_table_column",
"owner": "postgres",
"owner": "<OWNER>",
"ftoptions": [],
"inherits": [],
"relacl": [],
@@ -51,10 +51,10 @@
"name": "col_1_$%{}[]()&*^!@\"'`\\/#",
"description": "Comment for create",
"cltype": "numeric",
"attlen":"10",
"attprecision":"5",
"coloptions":[],
"colconstype":"n",
"attlen": "10",
"attprecision": "5",
"coloptions": [],
"colconstype": "n",
"attnotnull": true,
"attoptions": [],
"seclabels": [],
@@ -74,7 +74,14 @@
"name": "new_col_1_$%{}[]()&*^!@\"'`\\/#",
"description": "Comment for alter",
"cltype": "real",
"coloptions":{"added":[{"option":"column_name","value":"test"}]}
"coloptions": {
"added": [
{
"option": "column_name",
"value": "test"
}
]
}
},
"expected_sql_file": "alter_column_numeric.sql",
"expected_msql_file": "alter_column_numeric.msql"
@@ -127,4 +134,5 @@
"name": "col_2_$%{}[]()&*^!@\"'`\\/#"
}
}
]}
]
}
Loaded 100 of 792 files, more files were not shown because too many files have changed in this diff. Show more