Compare commits

...
160 Commits
Author SHA1 Message Date
Ashesh Vashi f09776c22d test(backup, restore): fix stale XSS check after PGDATABASE dbname fix
Backup/restore now pass the target database via PGDATABASE instead of
a CLI/positional argument, so the XSS regression test's check against
the displayed command string could never find the db name (escaped or
not) and always failed. Point Backup's check at the message field,
where the db name is still interpolated; for Restore, where the db
name no longer surfaces anywhere (command shows an always-empty
--dbname, message omits it entirely), assert its absence instead.
2026-09-15 18:11:25 +05:30
Ashesh Vashi d64bc8c24c Updated message catalogs for v9.18
Also drop the fuzzy marker from the Korean catalog header. Babel skips a
catalog whose header is fuzzy, and ko's header still carried the
unfilled template placeholders that make babel mark it so, with the
result that ko/LC_MESSAGES/messages.mo had not been regenerated since
November 2025 -- 83KB against the 263KB the .po compiles to, so most of
the 89% of Korean strings that are translated never reached users.
2026-09-15 17:19:11 +05:30
Ashesh Vashi 1c66edd12d docs: cite a GitHub issue for every 9.18 release note entry
The file mixed issue-backed entries with commit-id ones. Make every
entry issue-backed:

  - The LLM API redirect hardening had no issue, and its entry was the
    only record of Ziya Abdullayev's report outside the commit message.
    It gets #10420, written up with the mechanism, the affected range
    (>= 9.15, since ALLOWED_LLM_API_URLS did not exist before then) and
    the fix commit, and noting that it is hardening rather than an
    exploitable flaw, so no CVE was requested.

  - The Table dialog fix closed two issues, #10179 and #10180, so it
    becomes two entries. That also covers the Definition-tab type
    restriction, which the single commit-id entry had omitted.

  - The remaining commit-id entries are dropped.
2026-09-15 17:02:00 +05:30
Ashesh Vashi 36e08d6d8a fix(maintenance): drop a duplicated comment block above the database guard
The four-line comment explaining why an empty database must be rejected
was pasted twice above the guard in create_maintenance_job(). Comment
only; no behaviour change.
2026-09-14 23:08:27 +05:30
hiteshjambhale 59f8d82aa9 fix(restore, maintenance): reject empty/missing database to close role-name fallback
The PGDATABASE fix only set the env var when the database value was
truthy, so an empty string left both PGDATABASE and --dbname unset.
libpq then falls back down its chain to a database named after the
login role, silently restoring into / vacuuming the wrong database
instead of failing. Reject an empty or missing database with a 400
before any args are built, in both tools, and add regression tests
covering the empty-value path.
2026-09-14 17:33:20 +00:00
hiteshjambhale 3922cdfcbf test(restore, maintenance): harden arg-injection guards per review
- Scan every argv token for the forbidden value (forbidden_arg_substr)
  instead of list-membership only, so a value smuggled into a single
  token (e.g. "--dbname=host=...") is caught.
- Assert pg_restore keeps the required "--dbname=" flag, guarding against
  a regression that drops it (pg_restore errors without -d/--dbname).
2026-09-14 17:33:20 +00:00
hiteshjambhale 038a2619fa test(restore, maintenance): add arg-injection regression tests; fix pg_restore --dbname display
- Add regression scenarios asserting the database value is absent from
  argv and carried in PGDATABASE (maintenance: 1, restore: 2). They fail
  if the fix is reverted.
- Use the attached form "--dbname=" (a single argv token) for pg_restore
  instead of ['--dbname', ''] so the process-details command renders
  correctly -- an empty standalone token was dropped from the display.
2026-09-14 17:33:20 +00:00
hiteshjambhale e3a10c53cb fix(restore, maintenance): pass database via PGDATABASE to close connection-string injection
The restore and maintenance tools passed the user-controlled database
value to --dbname. libpq expands a --dbname value containing "=" (e.g.
"host=evil port=5433 dbname=postgres") into a connection string, letting
a user redirect the connection -- and the exported PGPASSWORD credential
-- to an arbitrary server, regardless of the --host/--port pgAdmin sets.

Pass the target database via the PGDATABASE environment variable instead
(libpq treats it as a literal name and never expands it):
- maintenance (psql) and restore plain/SQL (psql): drop --dbname; the
  tool connects using PGDATABASE.
- restore custom/tar/directory (pg_restore): pg_restore requires
  -d/--dbname, so pass an empty --dbname '' (no "=", not expanded) while
  the real database name comes from PGDATABASE.
2026-09-14 17:33:20 +00:00
Ashesh Vashi cf22515efc Merge remote-tracking branch 'origin/master' into cve-9.18 2026-09-14 22:52:21 +05:30
Ashesh Vashi 81edb68b27 fix(deps): pin psycopg[c] back to 3.3.4 (#10399)
The macOS x64 packaging job fails while installing requirements.txt:

    Preparing metadata (pyproject.toml): finished with status 'error'
    error: subprocess-exited-with-error
    exit code: -10

Exit code -10 is SIGBUS. The only output before the crash is
setuptools' own `[tool.setuptools.ext-modules] in pyproject.toml is
still experimental` warning, so it dies while setuptools reads the
project config, well before pg_config is ever invoked.

psycopg-c ships no wheels, so every platform compiles it from the
sdist. The only build-tooling change between 3.3.4 and 3.3.5 is the
strictly pinned build requirement:

    3.3.0 - 3.3.4    setuptools == 80.3.1
    3.3.5            setuptools == 83.0.0

Because that pin is an equality, PIP_CONSTRAINT cannot override the
version inside the isolated build environment - the only ways to keep
3.3.5 are --no-build-isolation with a pre-seeded setuptools (which
would fork the macOS build procedure from Linux's) or an upstream
fix. Linux builds 3.3.5 fine, so this looks specific to the
relocatable-python framework the macOS bundle is built against.

Pin to 3.3.4 to unblock the build. The encoding.py rewrite that
shipped alongside the 3.3.5 bump stays untouched: it reads the
derived py_codecs/pg_codecs dicts, which are plain dicts in both
versions.

One test expectation was 3.3.5-specific. psycopg 3.3.5 added an
ISO88591 alias to its encoding table, so inverting py_codecs resolves
iso8859-1 to ISO88591; on 3.3.4 only LATIN1 exists, so
get_encoding('LATIN1') returns ['LATIN1', 'iso8859-1'] instead of
['ISO88591', 'iso8859-1']. Both are valid PostgreSQL names for the
same encoding, so the scenario now accepts either and the test passes
on 3.3.4 and 3.3.5 alike.
2026-09-14 22:46:45 +05:30
dev-hari-prasad fd9179f25a chore: configure 3-day release age gate for npm packages (#10390)
Set npmMinimalAgeGate to 3d in web/.yarnrc.yml and runtime/.yarnrc.yml to prevent immediately installing freshly-published packages, providing a grace period against npm supply-chain compromises.
2026-09-10 16:10:12 +01:00
dev-hari-prasad 6acbf875ae chore(security): restore checksumBehavior to throw in web and runtime (#10395)
Revert checksumBehavior from update back to Yarn's default (throw) in web/.yarnrc.yml and runtime/.yarnrc.yml to restore tamper detection and ensure builds fail if downloaded package checksums do not match lockfiles.
2026-09-10 16:10:05 +01:00
dev-hari-prasad 809e812a17 chore(security): disable install scripts by default and narrow approved git repos in /web (#10391)
Revert global enableScripts in web/.yarnrc.yml to Yarn's secure default (false) to prevent arbitrary lifecycle scripts from running on install. Allowlist only ttf2woff2 and unrs-resolver via dependenciesMeta.built in web/package.json, keeping promotional/unneeded scripts blocked. Narrow approvedGitRepositories to pgadmin-org/react-data-grid.
2026-09-10 16:09:54 +01:00
dev-hari-prasad d84d3ce9e3 chore(deps): pin and bump @fortawesome/fontawesome-free to 7.3.1 in /web (#10372)
Replace 'latest' with exact '7.3.1' in web/package.json and update web/yarn.lock.
2026-09-10 16:09:43 +01:00
Ashesh Vashi 155ebe5c28 docs: add assigned CVE numbers to the 9.18 release notes
Fill in the identifiers allocated by the PostgreSQL CNA for the three
security entries that carried a placeholder:

  #10383  webserver Remote-User auth bypass      CVE-2026-86863
  #10384  backup argument/conninfo injection     CVE-2026-86864
  #10385  File Manager save_file symlink follow  CVE-2026-86861

Add the missing entry for #10388, connection-string injection in the
Restore and Maintenance tools (CVE-2026-86862). The fix was written but
never got a release-note line.

Move the release date out to 2026-09-17.
2026-09-10 09:03:59 +05:30
Dave Page 153e3273d8 Fix inherited-column read-only state and Definition-tab type restriction in the Table dialog (#10332)
Backend properties fetch marks a column already inherited from a
parent table with inheritedfromtable, while a column fetched
interactively via 'Inherited from table(s)' carries inheritedfrom
instead and has no attnum yet. inSchemaWithColumnCheck only checked
inheritedfrom, and did so after an isNew() short-circuit that treated
the attnum-less interactive rows as new, so inherited columns ended up
editable and deletable in both cases. Check both fields, and check
them before the isNew() short-circuit, and extend
canEditDeleteRowColumns the same way so the row's edit/delete buttons
are disabled too.

Fix: restrict Definition-tab Data type options to edit_types (#10180)

The expanded row's Definition tab and the inline grid-cell editor each
defined their own copy of the edit_types filter for the 'cltype'
field, but the tab's version received the whole table's data as
'state' rather than the row, since MappedControl resolves a field's
'type' callback against the top-level schema, not the field's own row
(unlike 'cell', which already gets the full row). That made isNew()
and edit_types resolve against the wrong object, so the filter always
no-opped and the tab showed every type instead of the restricted set.

Have MappedControl also resolve a field's declared 'deps' against its
own row (listenDepChanges already does this correctly for 'cell') and
forward them as a 2nd argument to 'type', mirroring what 'cell'
already receives. column.ui.js declares 'edit_types'/'attnum' as deps
on 'cltype' and factors the filter into one shared editTypesFilter()
used by both 'cell' and 'type', so the two stay in sync by
construction.

Fx: honour inheritedfromtable in attlen/attprecision editable checks

The Length/Precision and Scale grid-cell editors only checked
inheritedfrom when deciding whether to allow editing, so a column
loaded with only inheritedfromtable set (i.e. already inherited when
the table was opened) could be left incorrectly editable, unlike the
other inherited-column checks in this schema. Add a shared
isInheritedColumn() helper covering both fields and reuse it in both
editable callbacks and in inSchemaWithColumnCheck.
2026-09-09 15:45:50 +05:30
Dave Page 6c70d7ccb8 fix(pkg): stop shipping the JavaScript build configuration (#10403)
The Windows installer and the pip wheel were both shipping web/yarn.lock,
web/package.json and the rest of the JavaScript build configuration into
the installed tree, even though none of it is used at runtime: only the
webpack output under pgadmin/static/js/generated is. Because node_modules
is stripped, the lockfile that remains describes packages that are not
present, so vulnerability scanners run against an installation report the
whole build-time dependency tree, devDependencies included, as findings
that cannot be acted on and will recur on every release no matter how
current the versions are.

The Linux, macOS and Docker builds already removed these files, so this
brings Windows and pip into line with them, and adds webpack.* to the
Linux and macOS lists so that all of the packagers now strip the same
set. For pip the cleanup runs before syft, so the SBOM describes what
actually ships.

Verified by assembling a web tree with the cleanup applied and starting
pgAdmin from it: /misc/ping and /browser/ both return 200 and every
generated bundle is served.
2026-09-09 14:37:38 +05:30
Dave Page 9d0a61a828 test: drop proc1 from the Schema Diff KNOWN_DIFFERENCES list
The whitespace-only difference that CREATE OR REPLACE left in procedure
bodies was fixed in abf926428, so proc1 now settles once the generated
script has been applied and the test fails on the stale entry, exactly
as it is designed to: the list fails when something on it starts working
so that it cannot quietly rot. Take the entry off.
2026-09-07 22:40:55 +01:00
dev-hari-prasad 50d76765a1 perf: defer the cloud SDK imports until a cloud wizard is used (#10362)
boto3, azure.*, googleapiclient and google_auth_oauthlib were imported at
the top of the AWS RDS, Azure and Google Cloud modules, so every pgAdmin
start paid for all three SDKs whether or not anyone went near the cloud
deployment wizards. Move those imports into the functions that need them,
leaving the blueprints, their menu entries and the exposed endpoint map
eagerly registered, which is the property that makes this viable where
the earlier lazy-blueprint idea was not; repeat calls cost nothing, since
sys.modules caches each module after the first import.

The modules with several call sites get a small _azure_sdk() or
_google_sdk() helper handing back the symbols in a namespace, whilst the
one and two site cases carry a plain inline import with a comment saying
why it sits there. The oauth2client sentinel stays at the top of the
Google module, where it still runs before anything can import
googleapiclient, and _google_sdk() now carries a note pointing back at it
so that a later tidy-up does not move it and quietly reintroduce #10110.

Deferring the imports moves the failure mode for a broken or missing SDK
from startup, where it is obvious, to the first click in a wizard, so
each affected path now fails cleanly rather than leaking a traceback into
the UI: the Google and Azure lookups return their usual (data, error)
tuple, or an empty list with the reason logged; Google.callback() reports
a missing google_auth_oauthlib as a verification error; and the RDS
client helper logs the ImportError and lets its callers answer with empty
metadata instead of a 500.

Also fixes a pre-existing bug in the Google instance_types endpoint,
which assigned the (instance_types, error) tuple returned by
Google.get_instance_types() to a single name and then called .get() on
it, raising AttributeError for anyone who got that far in the wizard.

Measured on Linux with a warm page cache, timing create_app() in desktop
mode over five runs and taking the median, this takes application startup
from around 1567 ms to around 1354 ms; on Windows the saving was reported
as larger, at around 2131 ms down to around 1515 ms.

Partially addresses #10221.
2026-09-07 15:59:36 +01:00
Ashesh VashiandDave Page 0b8e8db0cc fix(pkg): stop the Windows build overwriting the venv's pip (#10396)
* fix(pkg): stop the Windows build overwriting the venv's pip

CREATE_VIRTUAL_ENV creates a virtualenv and then copies the whole of the
system Python's Lib over it, so that the relocated interpreter has a
standard library to use. That copy includes Lib\site-packages, and XCOPY
/Y overwrites the files that exist on both sides without removing the
ones that only exist in the venv, so the venv's freshly seeded pip ends
up spliced together with whatever pip C:\Python313 happens to have.

That was harmless while the two layouts agreed. pip 26.2 turned
pip/_internal/build_env from a module into a package, so on a builder
whose system pip predates 26.2 the new build_env/ directory survives the
copy whilst utils/misc.py is overwritten by the older one, and every
subsequent pip invocation dies before it does anything:

  File "...\pip\_internal\build_env\installer.py", line 22, in <module>
    from pip._internal.utils.misc import get_runnable_pip
  ImportError: cannot import name 'get_runnable_pip' from
  'pip._internal.utils.misc'

It cannot recover on its own either, since the next line of the script
is the pip invocation that upgrades pip.

Copy the standard library with ROBOCOPY and exclude site-packages, which
the venv is entitled to keep to itself. ROBOCOPY rather than XCOPY
because /XD takes a directory to skip and the existing
:CHECK_ROBOCOPY_ERROR helper already handles its exit codes; /R:3 /W:5
so a locked file fails the build rather than retrying for a million
attempts.

* fix(pkg): make the Windows build's ROBOCOPY failures diagnosable

Follow-up review fixes to the site-packages exclusion.

Exclude site-packages by name rather than by full path. The full path is
built from PGADMIN_PYTHON_DIR, so if that is ever set with a trailing
backslash the source and the /XD argument both gain a doubled separator;
should the match then fail, site-packages is copied over the venv again
and the build fails with the same ImportError this was meant to remove.
No directory inside the standard library is legitimately called
site-packages, so matching by name is both simpler and unconditional.

Drop "> nul" from both ROBOCOPY calls in favour of /NFL /NDL /NP.
ROBOCOPY writes its errors to stdout, so with the redirect in place a
copy that returns 8 or more aborted the build with nothing in the log to
say which file or share was at fault; the switches suppress the per-file
noise whilst keeping the header, the summary and any errors.

Guard the copy of the web directory with "|| EXIT /B 1" as well, since
EXIT /B inside :CHECK_ROBOCOPY_ERROR returns from the label rather than
from the caller and a failed copy was therefore ignored, leaving the
build to package an incomplete tree.

Guard virtualenv.exe likewise: with the standard library now copied by
ROBOCOPY, which creates its destination, a failure there would otherwise
surface later at the activate step instead of where it happened.

---------

Co-authored-by: Dave Page <dpage@pgadmin.org>
2026-09-07 15:02:06 +01:00
Ashesh Vashi 0e9c3eab8f Add the master changes merged since the 9.18 notes were written
Covers the ten commits merged from master: the CSP per-request nonce,
the Object Explorer keyboard shortcut (folded into the existing #9631
entry, since it is the same feature reached another way), the Schema
Diff dependency ordering, function-body whitespace, foreign table and
sequence fixes surfaced by the now-asserting regression test, the
server-cursor prepare TypeError, the partitioned index TABLESPACE
clause, the UDF argument grid, the Helm existingSecret path and the
Validate binary path dialog.

Entries within each section are now ordered by issue number, with the
SHA-keyed ones last.
2026-09-07 16:50:01 +05:30
Ashesh Vashi 7edb9c012c Merge remote-tracking branch 'origin/master' into cve-9.18 2026-09-07 16:47:57 +05:30
Kundan 7c2177241f fix: render Validate binary path modal as HTML instead of raw markup (#10394)
The endpoint returned a pre-built <b>/<br/> HTML string, and the frontend
switched to the escaping alertText() during the XSS hardening pass
(9e370d3cb), so the modal showed literal tags instead of formatted text.
Return structured {utility, version} data from the backend and compose
the modal content as React elements on the frontend, keeping the
untrusted binary version output routed through SafeMessage.

- Fall back to the not-found message on empty version output too

|| instead of ?? — an empty-string version from an unexpected
'--version' output shape should still show the fallback message.
2026-09-07 16:46:41 +05:30
hiteshjambhale fb0ca5c4c8 feat: Use per-request nonce in CSP to remove 'unsafe-inline' / 'unsafe-eval' (#10308)
Harden the default Content-Security-Policy so inline scripts run only via a
per-request nonce instead of a blanket 'unsafe-inline', and drop 'unsafe-eval'.

- Generate a per-request nonce (secrets.token_urlsafe) cached on flask.g so the
  exact same value is emitted in templates and the CSP response header.
- Substitute a {nonce} placeholder in CONTENT_SECURITY_POLICY at runtime.
- Tag inline <script>/<style> tags, and set window.__webpack_nonce__ so webpack's
  dynamically injected assets carry the nonce too.
- New default: script-src 'self' 'nonce-{nonce}' (no 'unsafe-inline'/'unsafe-eval').
- style-src keeps 'unsafe-inline': MUI/React inject un-nonced runtime styles and
  inline style="" attributes that cannot be nonced.
- 'unsafe-eval' is not needed by production bundles; the dev ('eval' devtool)
  bundles add it via config_local.py (documented in config.py).

Setting window.__webpack_nonce__ in HTML is a no-op: webpack only substitutes the __webpack_nonce__ identifier inside compiled modules, and MUI/emotion does not read it either (it requires an explicit createCache nonce). Same-origin webpack chunks are already allowed by script-src 'self', so the block did nothing. Remove it; the resourceBasePath inline script keeps its nonce.

Development bundles are built with webpack's 'eval' devtool, which the strict nonce policy blocks, forcing developers to manually add 'unsafe-eval' via config_local.py. Handle it automatically in get_content_security_policy() (which runs per request and therefore sees a DEBUG value overridden in config_local): when config.DEBUG is set and the policy uses a nonce, append 'unsafe-eval' to the script-src directive, without duplicating it. Production is unaffected and custom (non-nonce) policies pass through untouched.

Adds unit tests covering nonce generation/caching/per-request freshness, {nonce} substitution, pass-through of custom/None/empty policies, header emission, and the dev-mode behaviour (including the no-script-src edge case, exact script-src name matching, and no-duplication).

The dev-mode 'unsafe-eval' behaviour is now implemented, so these are ordinary regression tests. Remove the 'PLANNED'/'EXPECTED TO FAIL until impl' banners (which incorrectly told maintainers not to make the source pass) and an unused MagicMock import.
2026-09-07 16:44:38 +05:30
Dave Page d0210a534d fix: correct existingSecret path in Helm deployment template (#10214) (#10313)
The annotation conditions referenced the non-existent top-level
.Values.existingSecret instead of .Values.auth.existingSecret, so the
secret checksum annotation and the empty-secret gating never worked as
intended when an existing secret wasn't supplied.
2026-09-07 16:42:50 +05:30
Dave Page 006fc6bd88 fix: omit redundant TABLESPACE pg_default from generated index SQL (#10342)
PostgreSQL rejects an explicit TABLESPACE clause naming the database's
own default tablespace when the index is on a partitioned table
('cannot specify default tablespace for partitioned relations'), so
the SQL pgAdmin generated for a new index could not be executed
against a partitioned table whenever no non-default tablespace was
chosen. The index_constraint and exclusion_constraint create.sql
templates already guard against emitting this redundant clause;
extend the same guard to the plain index create.sql templates.

Closes #10341
2026-09-07 16:42:15 +05:30
Dave Page abf926428e fix: stop Schema Diff injecting whitespace into applied function/procedure bodies (#10302) (#10317)
Generating a script for a function or procedure that only exists on the
source ("Source Only") routed through FunctionView.sql(), which always
calls reformat_prosrc_code() to wrap the body with a leading and trailing
newline around $BODY$...$BODY$ purely for readability in the SQL panel.
That reformatted body was then executed verbatim against the target, so
PostgreSQL stored the extra newlines in prosrc: the target's body no
longer matched the source's byte for byte, and re-comparing kept
reporting a whitespace-only difference unless "Ignore whitespace" was
ticked.

Schema Diff's update path (CREATE OR REPLACE against an existing target
object) already suppressed this formatting via allow_code_formatting,
added for #7102; this extends the same guard to the create/"Source
Only" path so Schema Diff never adds whitespace of its own to the SQL
it actually applies.

Added a dedicated regression test covering both the "Source Only" and
"Different" cases for a function and a procedure: applying the
generated script now leaves every object comparing as Identical
without needing "Ignore whitespace".
2026-09-07 16:41:35 +05:30
Dave Page f2fdef5ba5 Schema Diff: reinstate dependency ordering of the generated script (#10319)
dependLevel drove the order in which generateFinalScript() writes
objects, with higher levels emitted first so dependencies land before
whatever needs them, but nothing has assigned it since the React port
deleted schema_diff_dependency.js: every row got Object.keys' default
undefined bucket, so the script came out in whatever order the rows
happened to be in.

computeDependLevels() rebuilds the levels for the full result set from
each row's dependencies, walking the reverse graph so a row that other
rows depend on always ends up with the highest of their levels plus
one, with a cycle guard for the circular dependencies the script header
already warns about.

Fixes #10295.
2026-09-07 16:40:27 +05:30
Dave Page 972332ae59 feat: add a keyboard shortcut for collapsing the Object Explorer (#10296)
#10254 made the Object Explorer collapsible by clicking the current
workspace icon, but left that as the only way to do it. This adds a
shortcut, Ctrl+Alt+B by default, registered as its own
toggle_object_explorer preference so anyone can change it.

Ctrl+Alt+B rather than anything in the Shift+Alt family, which is full: b,
f, o, l, h, q, v, s, n, e, d and g are all taken, b by the existing Object
Explorer shortcut, which keeps its meaning of focusing the tree. Ctrl+Alt+B
is close enough to VS Code's Ctrl+B for the side bar to be guessable, and
keeps a family resemblance to the bracket keys used for tab navigation.
Ctrl+Shift+B would have been the other obvious candidate, but Chrome and
Firefox both use it for the bookmarks bar.

Worth noting for anyone reviewing: on keyboard layouts where AltGr is
Ctrl+Alt, this combination can be swallowed by the layout and produce a
character instead. That is true of any Ctrl+Alt binding, the preference is
user-configurable, and Shift+Ctrl+Alt+B is available if the default turns
out to annoy people.

The shortcut itself only fires an event, since the workspace provider owns
the visibility state, and the provider ignores it in classic layout where
there is no workspace toolbar and the Object Explorer is always shown.

Tested in a browser as well as in the unit tests: from the workspace layout
the key collapses and restores the panel, and the preference is registered
alongside the existing shortcuts.
2026-09-07 16:39:33 +05:30
Dave Page bc04db39d5 fix: stop AsyncDictServerCursor forwarding prepare to the server cursor (#10343)
psycopg's AsyncServerCursor.execute() has never accepted a prepare kwarg
(a server-side DECLARE CURSOR can't be a prepared statement) and raises
TypeError on any unexpected keyword, even one whose value is None.

PR #10030 widened AsyncDictCursor.execute()/_execute() to forward
prepare/binary to whatever cursor it holds, fixing a real
psycopg_pool.ConnectionPool.check_connection breakage. AsyncDictServerCursor
inherits that same execute()/_execute() without overriding it, so every
server-cursor query now unconditionally forwards prepare=None straight into
AsyncServerCursor.execute() and fails with
"TypeError: keyword not supported: prepare".

That TypeError isn't a psycopg.Error, so execute_async()'s
"except psycopg.Error" doesn't catch it. It propagates into the background
QueryThread's generic exception handler, which logs it and builds an
internal_server_error response that is discarded (the thread's return value
goes nowhere) - so the query silently never runs, invisible to the user,
and the async cursor is left with no query having actually executed on it.

Give AsyncDictServerCursor its own _execute() that drops prepare before
delegating, leaving DictCursor/AsyncDictCursor's forwarding for the
pool-checkout case it was written for untouched.

Strengthen test_server_cursor.py's existing scenario to assert the poll
response's actual status/result instead of only the HTTP status code and
the echoed server_cursor flag, and add a fast, DB-less regression test
asserting AsyncDictServerCursor._execute never forwards prepare.
2026-09-07 16:38:46 +05:30
Dave Page 3d310d4656 Fix UDF/procedure argument grid delete (and add) in edit mode (#10333)
`canDeleteRow for the function/procedure Arguments grid checked whether the
whole function was new rather than whether the individual row was new,
so once a function was saved the delete icon was disabled for every
argument row, including ones added but not yet saved (same bug pattern
already fixed for enum values in #8208). canAdd had the same whole-object
gate, hiding the "+" button entirely once a function was saved, so there
was no way to add a row in the first place.

Pre-existing (already persisted) arguments remain non-deletable, since
PostgreSQL has no way to remove an argument from a function via
CREATE OR REPLACE.

Also fixes _update_arguments_for_get_sql, which only merged the
'changed' key of the arguments diff and silently dropped (or, without
a 'changed' key at all, raised a 500) any newly added argument, so a
row added via the now-enabled "+" button actually survives into the
generated SQL.

Closes #10252
2026-09-07 16:37:12 +05:30
Dave Page 8c6fd211a7 Schema Diff: make the regression test assert its generated script, and fix what that found (#10305)
The Schema Diff comparison test wrapped applying its generated script, and
the comparison that follows it, in a bare `except Exception` that discarded
both. It reported a pass whatever the script did, which is why it printed
`syntax error at or near ")"` on every run whilst claiming two tests passed.
It now fails when an object's SQL does not apply, and when applying the lot
leaves the two databases different, with a short list of the differences
that are known not to settle yet so that the list cannot quietly rot.

Objects are applied one at a time and retried rather than as a single
script, because the script is no longer ordered by dependency (#10295), so
an object can fail purely because something it needs comes later on;
retrying tells that apart from SQL that is simply wrong.

Turning the assertions on found the following, each of which is fixed here:

* A range type being dropped and recreated because its kind changed lost
  its subtype, because directory_diff() drops a plain value that only one
  side of the comparison has, and rendered `CREATE TYPE ... AS RANGE ()`.
  Once that was fixed it wrote the catalogue's `-` placeholder out as
  `CANONICAL = -`, which the reverse-engineered SQL path already avoids.
  Both are now handled where the comparison data is built (#10304).

* The constructor functions, casts and multirange types that PostgreSQL
  creates for a range type were compared as though a user had written them,
  so the script tried to recreate objects that come into being with their
  parent type: 47 of 151 objects in the test's fixtures were these.
  Internal dependencies are now excluded alongside extension ones, matching
  what pg_dump does.

* Recreating a foreign table declared any column that also differed twice,
  because a changed column was appended to the table's existing columns
  rather than replacing the entry already there (#10297).

* Raising a sequence's MINVALUE above the value it currently sits at, or
  lowering MAXVALUE below it, generated a statement PostgreSQL rejects
  outright, taking every other change to that sequence with it. Such a
  change is now accompanied by the RESTART it requires (#10298).

* A foreign table column added by Schema Diff lost its collation, because
  get_columns.sql calls it collname whilst the column templates render
  collspcname (#10300).

* A comparison that threw part way through emitted its failure and then
  reported success as well, handing the client a fraction of the databases
  as though it were a complete result (#10303).

Two differences remain listed as known: a rebuilt partitioned table keeps
the default partition used as scaffolding for the data copy (#10301), and
CREATE OR REPLACE wraps a function body in newlines, leaving a
whitespace-only difference (#10302).

Fixes #10293

* Stop the partition rebuild script copying from a temporary table

Asserting that the generated script actually applies turned up a
partitioned table rebuild whose script refers to a relation that has
never existed, along the lines of:

    CREATE TABLE schema.temp_partitioned_2 (
        LIKE schema.temp_partitioned_1 INCLUDING ALL
    ) PARTITION BY RANGE (col1);

so that applying it fails with 'relation "schema.temp_partitioned_1"
does not exist'.

There are two things going on here, and both are fixed.

PgAdminModule.register() is called once per application instance, whilst
the blueprint objects themselves are module level singletons, so each
sub-class that appends its sub-modules from its own register() (most of
them do, TableModule included) leaves a duplicate entry behind every
time a second application is created in the same process. Nothing in
production notices, because production creates a single app, but the
regression suite creates several, and anything walking self.submodules
then does its work once per duplicate: with four copies of the partition
sub-module, get_sql_from_submodule_diff generated the same partition
rebuild four times over. self.submodules is now de-duplicated as the
blueprint registers, and parentmodules likewise only gains an entry it
does not already hold.

The second call was only harmful because PartitionsView.get_sql_from_diff
stashed its temporary names on the caller's own dictionaries, replacing
the table's real name with a temporary one, so a subsequent call read the
first call's temporary name back as the original. It now works on copies
and leaves the comparison data it is handed alone, which makes it safe to
call more than once regardless of how it is reached.

* Take a mutable copy of request.form, and close cursors on a failed chunk

Two things picked up in review.

Sequence update() passed request.form straight through to get_SQL(), and
request.form is immutable, so as soon as _add_restart_for_new_bounds had
a restart to add (which is the whole point of it) a form encoded update
raised TypeError. It now takes a dict copy, as the msql path already
effectively did.

apply_sql_chunks() left the cursor open when a statement failed, and a
failed statement is retried on the next pass, so a run could accumulate
one open cursor per attempt. The cursor is now closed in a finally,
whether the statement worked or not.
2026-09-07 16:35:13 +05:30
Ashesh Vashi de71c83d8d Bump version to 9.18 and add the 9.18 release notes
Update APP_REVISION/APP_VERSION_INT in web/version.py and the Helm
chart's version/appVersion, and add docs/en_US/release_notes_9_18.rst
with a toctree entry.

The notes cover everything merged since REL-9_17 (ee7109d40..bd7cde585)
plus the three CVE fixes on this branch: the Webserver auth header
bypass (#10383), Backup argument and connection-string injection
(#10384) and the File Manager save_file symlink TOCTOU (#10385). The
CVE identifiers are still placeholders (CVE-2026-XXXXX) pending
assignment, and #10388 has no entry because its fix is not on this
branch yet.

Changes with no associated issue are listed under Housekeeping and Bug
fixes keyed by commit SHA, following the 9.15 convention, so the
"Additional changes" wrapper is dropped and Dependencies becomes a
top-level section.
2026-09-07 15:02:01 +05:30
Kundan SableandClaude Sonnet 5 8ed0ecc06b webserver auth: address round-3 review follow-ups
Two non-blocking items from asheshv/pgAdmin4-cve#66's approval review:

- The auth_source lockout remedy was circular for the deployments most
  likely to hit it: telling a locked-out admin to "sign in through
  another authentication source" doesn't help when
  AUTHENTICATION_SOURCES = ['webserver'] and there is no other source.
  Documented the actual way out - temporarily add 'internal' back,
  fix the affected users, remove it again.
- A malformed WEBSERVER_REMOTE_USER (not a non-empty string) was
  demoted to info logging along with the client-triggered rejections
  in the previous round, but it's a static operator misconfiguration,
  not something a client varies per request - at info it would go
  unnoticed while every header-based login silently fails. Moved back
  to warning, consistent with the other config-error checks.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 19:06:51 +00:00
Kundan SableandClaude Sonnet 5 c0bd0d8665 webserver auth: fix CI-breaking test isolation bug, address round-2 review items
Round 2 of asheshv/pgAdmin4-cve#66's review:

- test_webserver_remote_user_trust.py bound WebserverAuthentication at
  module-import time. test_auth_gating.py (which sorts and runs first)
  evicts pgadmin.authenticate.webserver from sys.modules to observe its
  re-import and never restores it, so the trust test held a discarded
  copy of the module while patch('...webserver.User') patched the fresh
  one - the patch silently missed and hit a real, unconfigured database.
  Reproduced the reviewer's "27 passed, 3 failed" package-level run, then
  fixed it by importing WebserverAuthentication inside each runTest()
  instead, matching this repo's existing deferred-import convention.
  Full authenticate package now passes 30/30.
- Documented the auth_source lockout: login() refusing any non-webserver
  auth_source protects existing accounts, but a site with pre-existing
  internal-auth users (including the default admin) will find nobody can
  log in after enabling this. Added a warning to webserver.rst with the
  fix (change Authentication source in User Management).
- login()'s user-is-None branch called logger.exception() with no active
  exception, writing a useless NoneType traceback and never naming the
  failed username. Now a plain warning naming it.
- Demoted per-request rejection logging (untrusted peer, secret mismatch,
  misconfigured name) from warning to info, since an unauthenticated
  client can trigger any of them on every request; kept genuine config
  errors (a malformed WEBSERVER_TRUSTED_PROXIES entry, or the whole
  setting not being a list/tuple) at warning.
- Guarded against WEBSERVER_TRUSTED_PROXIES being set to a plain string,
  which iterated character-by-character and logged one warning per
  character while still safely rejecting the request.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 19:06:51 +00:00
Kundan SableandClaude Sonnet 5 90e94da9db webserver auth: address CVE fix review comments
Fixes four issues from asheshv/pgAdmin4-cve#66's review:

- HTTP_-prefixed WEBSERVER_REMOTE_USER names (e.g.
  HTTP_X_FORWARDED_USER) were permanently unresolvable via the header
  path: Werkzeug normalizes '_' to '-' in header lookups but does not
  strip the HTTP_ prefix. Normalize the name to its header form before
  looking it up.
- A non-str/bytes or non-ASCII WEBSERVER_SHARED_SECRET made
  hmac.compare_digest raise, turning every login attempt into an
  unhandled 500. Encode both operands and catch the failure, degrading
  to a rejected match with a logged warning instead.
- Added a test exercising the real ProxyFix/X-Forwarded-For bypass via
  app.test_client(), which the existing scenarios (bare
  test_request_context, no ProxyFix in the stack) could not catch.
- Added positive-path tests for login() (a genuine WEBSERVER user still
  logs in; a header-asserted name with no matching User row fails
  gracefully) plus a few low-priority coverage gaps the review flagged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 19:06:51 +00:00
Kundan SableandClaude Sonnet 5 02e327c1b9 Fix authentication bypass in Webserver auth via client-controlled header
WebserverAuthentication.get_user() fell back to request.headers when the
WSGI environ lookup missed, letting any client set Remote-User (or an
equivalent HTTP_-prefixed/hyphenated name) and log in as an arbitrary
existing user, including an Administrator, with no credential check.

Gate the header fallback behind an explicit opt-in
(WEBSERVER_REMOTE_USER_FROM_HEADER), a trusted-proxy IP/CIDR allow-list
(WEBSERVER_TRUSTED_PROXIES, checked against the raw pre-ProxyFix peer so
X-Forwarded-For can't spoof it), and an optional shared secret
(WEBSERVER_SHARED_SECRET). Also refuse to log a header-asserted identity
into any account whose auth_source isn't webserver, so a forged header
can never take over an internal-auth admin even if the gate above is
misconfigured.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 19:06:51 +00:00
hiteshjambhale 5082834a27 fix(backup): pass database via PGDATABASE env to close connection-string injection
The "--" end-of-options marker (previous commit) stops pg_dump from
reading the database value as an option, but libpq still expands a dbname
containing "=" (e.g. "host=evil port=5433 dbname=postgres") into a
connection string -- letting a user redirect the connection, and the
exported PGPASSWORD credential, to an arbitrary server.

Pass the target database via the PGDATABASE environment variable instead
of as a command-line argument (as import_export already does). libpq
treats PGDATABASE as a literal database name and never expands it, so
both the file-write and connection-redirect vectors are closed. Also
fixes a latent bug where a database name legitimately containing "=" was
broken.

Update the regression tests: assert the database name is absent from
argv and carried in PGDATABASE (order-blind assertIn is insufficient).
2026-09-04 13:57:13 +00:00
hiteshjambhale a14469290e test(backup): add regression tests for database-field argument injection
Adds two scenarios to the object-backup unit test:
- normal dbname lands as the trailing positional after '--'
- a malicious '--file=...' value stays a trailing positional after '--'
  and never overrides the storage-confined '--file'

Uses an order-sensitive assertion on the last args (the existing assertIn
harness is order-blind and would pass even if '--' came after the dbname).
2026-09-04 13:57:13 +00:00
hiteshjambhale 82a88b74f9 fix(backup): prevent argument injection via database field
The object-backup path appended the request 'database' field to the
pg_dump argv as a raw positional argument. Under getopt_long argument
permutation, a value like --file=/absolute/path is parsed as an option
and overrides the storage-confined --file, letting a low-privilege user
write pg_dump output outside their storage sandbox.

Insert the '--' end-of-options marker before the database name so
pg_dump always treats it strictly as the positional dbname.
2026-09-04 13:57:13 +00:00
Ashesh Vashi d773db699f test: make save_file symlink test harness-native, add positive control
Follow-up to the previous commit; the fix itself is unchanged.

The new test called create_app() inside runTest(). That is redundant --
runtests.py already calls setApp()/setTestClient() on every test object,
independent of setUp() -- and expensive: it did not complete in over two
minutes locally, while the two existing tests in this file run in 1.1s
total. It also routes through create_app_data_directory(), which calls
sys.exit(1) on any data-directory problem, surfacing as SystemExit
mid-test. Use the harness's already-authenticated test client instead,
which also removes the save_file.__wrapped__() unwrapping; that worked
only because pga_login_required applies @wraps outermost over
@mfa_required, so reordering those two lines would have silently pointed
__wrapped__ at the MFA wrapper.

The old test also stubbed check_access_permission to a no-op, which
tests the write guard with the containment check absent rather than the
actual threat. Let the real check run and plant the symlink from inside
it, which reproduces the check-to-open race deterministically.

Assert on the error message rather than a bare status code. 500 is also
returned by the check_access_permission branch, and "outside file
unchanged" holds whenever any error occurs, so the previous assertions
would pass even if the write never happened for an unrelated reason.

Add a positive control. Nothing asserted that an ordinary save still
succeeds, so a broken _open_upload_target() would have left the negative
test green while every Query Tool and ERD save failed. It also pins the
0o600 mode so that documented behavioral change cannot regress silently.

Report the leaf-symlink rejection specifically instead of leaking the
raw strerror. A user whose save is refused saw "Error: Too many levels
of symbolic links", which explains nothing. Note this is a real
behavioral change: a symlink inside the storage root pointing at a
target inside the storage root passed check_access_permission and used
to work, and is now refused -- the same tradeoff already accepted for
uploads.

Verified with runtests.py against PostgreSQL 18.6 in desktop mode:

  --pkg misc.file_manager            19/19 pass
  --exclude feature_tests            2037 pass, 491 skipped, 1 failure

The single failure is utils.driver.psycopg3 test_encoding (LATIN1 maps
to the iso8859-1 python codec), which reproduces identically on the
unmodified base commit bd7cde585 and is unrelated to this branch.

Both new tests were confirmed non-vacuous by reverting the
_open_upload_target() call to the bare open(): TestSaveFileRejectsLeafSymlink
then fails with "AssertionError: 200 != 500" (the symlink is followed and
the outside target is overwritten) and TestSaveFileWritesRegularFile with
"AssertionError: 420 != 384" (0o644 vs 0o600).
2026-09-04 05:55:58 +00:00
Dave Page 83a31a350c fix: close save_file() leaf-symlink TOCTOU in File Manager
check_access_permission() resolves symlinks via realpath() and validates
containment (CVE-2026-7813), but save_file() re-joins the path afterwards
and wrote through a bare open(), which follows a symlink planted at the
leaf between the check and the write. The upload endpoint already closed
this exact gap with _open_upload_target()'s O_NOFOLLOW-protected open;
save_file() never got the same treatment.

Route save_file()'s write through _open_upload_target() too, so both
File Manager write sinks share the same kernel-enforced protection.
Reported privately per SECURITY.md.
2026-09-04 05:55:58 +00:00
Ashesh Vashi bd7cde5851 chore(deps): bump third-party JS and Python dependencies (#10370)
JS (yarn, patch/minor only): babel toolchain, MUI, react/react-dom,
testing-library, axios, webpack ecosystem, jest, eslint plugins, and
misc small libs. Excluded @simonwep/pickr (webpack prod-bundle
interop break on 1.10.x), @testing-library/user-event@14.6.7 and
webpack@5.110.3 (both npm-quarantined at bump time).

Python (>3.9 requirements branch only, Python 3.9 pins untouched):
Authlib 1.8, google-auth-oauthlib 1.4.1, gssapi 1.12 (ungated),
psycopg[c] 3.3.5, selenium 4.48.0, testscenarios 0.7.0.

Skipped: paramiko 3->5 (blocked on sshtunnel's paramiko.DSSKey
usage), azure-mgmt-resource 26.0.0 (drops Python 3.9 support and
conflicts with the <26 upper bound kept for Azure CLI distro
packaging, see #10247).

Two bumped packages required source fixes:

- psycopg 3.3.5 restructured the private
  psycopg._encodings._py_codecs attribute from a dict to a tuple of
  alias groups, breaking our monkeypatch in
  pgadmin/utils/driver/psycopg3/encoding.py with TypeError: 'tuple'
  object does not support item assignment. Rewrote get_encoding()/
  configure_driver_encodings() to use the derived py_codecs/
  pg_codecs dicts instead, which stay plain dicts across psycopg
  3.2.x-3.3.5 regardless of _py_codecs's internal shape. Verified
  byte-for-byte identical behavior against 3.3.4 for every
  encode_dict entry (SQL_ASCII, EUC_TW, UNICODE, ascii) plus
  UTF8/LATIN1. Added a DB-free unit test
  (pgadmin/utils/driver/psycopg3/tests/test_encoding.py) so a future
  psycopg internals change fails fast here instead of only surfacing
  as an app-wide import crash in the full regression suite.

- react-frame-component 5.3.2 breaks LayoutIframeTab.jsx (the iframe
  wrapper every tool panel - Query Tool, Debugger, PSQL, ERD, Schema
  Diff - renders into): the panel's iframe never mounts, hanging any
  of those tools indefinitely (React error #130 in the production
  bundle). This caused every run-feature-tests-pg CI job to fail.
  Reverted to 5.2.6.
2026-09-03 14:58:04 +05:30
Dave Page bc58657d3d ci: stop fetching an unpinned Yarn before the pinned one (#10306)
Everywhere we build the JS, we asked Yarn to install itself twice:

    yarn set version berry
    yarn set version <the version from packageManager>

The first call fetches whichever release is "berry" today and the second
immediately replaces it, so every build made two downloads to end up with one
Yarn, and the one that could not be reproduced tomorrow was fetched first.

That first fetch broke CI outright on 17 August 2026, during a GitHub
incident, when it returned an empty body and Yarn 1 died writing it out:

    error TypeError [ERR_INVALID_ARG_TYPE]: The "data" argument must be of
    type string or an instance of Buffer, TypedArray, or DataView
        at writeFile (node:fs:2373:5)

The unpinned call is dropped everywhere it appeared: Make.bat, the macOS and
pip package builds, and the four workflows. The packaging scripts keep their
pinned "yarn set version ${YARN_VERSION}", which still reads packageManager
from package.json, so their behaviour is unchanged beyond losing a download.

The workflows go further and use Corepack, which reads packageManager itself
and provisions exactly that Yarn without writing .yarnrc.yml and
.yarn/releases into the repository root, where the JS project is not. The
Dockerfile already installed and enabled Corepack immediately before both
"set version" calls, so both were redundant there and are simply removed.

CONTRIBUTING.md and README.md already tell contributors to start with
"corepack enable", so this brings the automation in line with the documented
setup. Note that Corepack ships with Node.js only up to (but not including)
25.0.0, so it will need installing explicitly once the runners move past it,
as the Dockerfile already does.
2026-08-21 18:39:32 +05:30
Dave Page 5bd4f3bde6 Javascript dependency: Bump use-resize-observer from 9.1.0 to 10.0.0 (#10265) (#10299)
v10 removes the default export in favour of a named one, so the import in
PgTreeView had to change with it; left as it was, useResizeObserver resolves
to undefined and the Object Explorer dies when the hook is called. The rest of
the v10 changes cost us nothing: it drops the /polyfilled entrypoint and its
@juggle/resize-observer dependency, no longer takes react-dom as a peer, and
requires React >= 18.2, which is comfortably met on React 19. The hook still
returns the { ref, width, height } shape the tree destructures.

Verified by hand in Chrome and Safari with the tree expanded through
databases, schemas, tables and tablespaces, resizing the panel and the window.
2026-08-21 18:37:43 +05:30
Dave Page 9a009fd2b6 Don't follow HTTP redirects on LLM API requests (#10281)
* Don't follow HTTP redirects on LLM API requests

ALLOWED_LLM_API_URLS is checked against the URL pgAdmin is configured
with, but the providers then issued the request through
urllib.request.urlopen(), whose default handler would follow a Location
header on to a destination that check was never applied to.

No legitimate LLM API replies with a redirect, so refuse them outright
rather than re-validating each hop: urlopen_no_redirect() installs a
handler that raises HTTPError instead, and the outbound calls in all
four providers and in the model-refresh endpoints now go through it.

This is hardening rather than a fix for an exploitable flaw, since
returning the redirect in the first place requires control of a host
that is already on the allowlist. Reported by Ziya Abdullayev.

* Handle 308 redirects on Python 3.10 as well

HTTPRedirectHandler only gained http_error_308() in Python 3.11, so on
the 3.9 and 3.10 interpreters we still support (and which CI runs) a 308
was never recognised as a redirect: it bypassed _NoRedirectHandler's
redirect_request() entirely and surfaced from http_error_default() as a
bare 'HTTP Error 308: Permanent Redirect'. The redirect was still not
followed, so this was never a security gap, but the caller got no
explanation of why the request had failed, and the test asserting our
message failed on every CI job as a result.

Mapping http_error_308 onto http_error_301, exactly as 3.11 itself does,
routes 308 back through redirect_request() so that every redirect status
behaves identically on every version we support.
2026-08-21 18:37:15 +05:30
Dave Page 1f02466324 fix: reject empty or null Username on non-shared server import (#10309) (#10320)
validate_json_data() only checked that the Username key was present on
a non-shared server, not that it held anything useful, so an empty or
null value imported cleanly and left behind a server that libpq would
silently authenticate as the OS account running pgAdmin rather than
reject outright. Check the value, matching the truthiness check already
used for shared servers and the "Username must be specified" rule
enforced by the server dialog.
2026-08-21 18:35:35 +05:30
Dave Page c0fa2a18d8 Relax azure-mgmt-resource pin to allow 24.0.0 (#10323)
The exact ==25.0.0 pin was an incidental side effect of a mass
dependency bump and isn't required by pgAdmin's own code: the cloud
wizard only uses the top-level ResourceManagementClient re-export and
resource_groups.list(), both unchanged between 24.0.0 and 25.0.0.
Pinning to >=24.0.0,<26.0.0 lets distro packagers keep pgAdmin
installable alongside Azure CLI, which still requires
azure-mgmt-resource<25.

Closes #10247
2026-08-21 18:34:55 +05:30
Dave Page 46c092cc5d Remove redundant passlib pin that conflicts with libpass (#10325)
Flask-Security-Too >= 5.7 depends on libpass, a fork of passlib that
installs its files under the same passlib/ import path. Our own
unconditional passlib==1.* pin in requirements.txt therefore causes
both packages to be installed into the same site-packages/passlib/
directory, and parallel installers such as uv can end up with a
mixed or overwritten set of files depending on install order.

pgAdmin's own code never imports passlib directly, and the older
Flask-Security-Too 5.6.x branch (used on Python 3.9) already pulls in
real passlib as its own transitive dependency, so dropping our direct
pin does not change what gets installed there. On newer Python
versions, only libpass is installed, resolving the conflict.

Closes #10239
2026-08-21 18:34:06 +05:30
Dave Page f739036066 fix: guard PasswordExec against None host/port/username (#10326)
Service-only connections (pg_service.conf) leave host, port and username
unset, so a per-server Password Exec Command crashed in get() with
TypeError: replace() argument 2 must be str, not None. Fall back to an
empty string for each unset placeholder instead.

Closes #10237
2026-08-21 18:33:16 +05:30
Thomas Han 0ebefaf80e fix: accept SharedUsername when importing shared servers (#9971)
Importing a servers.json definition insisted on a Username attribute for
every server, so a shared server that carried only SharedUsername was
rejected with "'Username' attribute not found for server ...", even though
the dialog treats SharedUsername as the default username for a shared
server. Validation now accepts either attribute when the server is shared,
whilst non-shared servers still require Username.

The loader derives the username it stores in the same way. Username wins
when both are present, since it is the owner's own login and the per-user
override, and SharedUsername is used when Username is absent. That matches
what creating a shared server through the dialog produces, so an imported
shared server prompts for a password on the first connection rather than
sitting on a loading spinner with no username to connect with.
SharedUsername itself does not inherit a legacy Username, because it means
"the default username for the other users of this server" rather than a
fallback for the owner.

Folded in during review:

Validation now looks at the values rather than merely at the keys. Checking
for the presence of a key alone let {"Shared": true, "SharedUsername": null}
and its empty-string equivalent through, and the loader would then store
that as the server's username: on a schema created from the model that is an
integrity error surfaced as a raw "Error creating server" string, whilst on
a database migrated over the years, where the column carries no NOT NULL, it
imports silently and leaves behind a server that cannot connect. The
equivalent pre-existing hole on the non-shared path, where {"Username": ""}
is still accepted, is left for a separate change covering empty usernames in
general.

Tests in web/pgadmin/utils/tests/test_validate_json_data.py cover the
username requirements for shared and non-shared servers, the Service escape
hatch, the integer port check, and shared servers being skipped rather than
rejected for a non-admin user. The JSON format documentation notes that
SharedUsername may stand in for Username.

Fixes #9226
2026-08-18 10:11:58 +01:00
hiteshjambhale 104d018677 fix: dedupe concurrent getNodeAjaxOptions requests (#10307)
getNodeAjaxOptions() in web/pgadmin/browser/static/js/node_ajax.js, the
shared helper behind every AJAX-backed dropdown, cached responses only
once they had resolved, so when many components asked for the same URL
before the first response landed each of them saw an empty cache and
fired its own identical GET. Every column row's Data Type dropdown
mounting at once on a wide table's Columns tab therefore produced one
get_types request per row: around 150 concurrent duplicates on a
150-column table.

Concurrent GETs are now shared in the Axios wrapper. A module-level Map
tracks requests in flight, keyed on the request axios will actually make:
api.getUri() resolves the URI exactly as the request will, honouring the
instance baseURL, plain-object params, URLSearchParams and any custom
paramsSerializer, whilst the instance's own header configuration and the
per-request headers are hashed in alongside it, so an instance built with
extra headers never shares with a plain one. Params are canonicalised
first so that callers passing the same values in a different order still
share. The entry is removed once the request settles, success or failure,
so nothing leaks and later callers fetch fresh data.

Beyond params and headers there is plenty in an axios config that cannot
be captured in a key and that changes either what a caller gets back or
how it can be aborted: transformResponse, responseType, validateStatus,
timeout, signal and so on. Rather than let those share silently, a
request carrying anything outside the shareable set goes straight through
to api.get().

Each caller still runs its own caching and transform on the shared
response, so cached behaviour and response-shape handling are unchanged.

Fixes #10155.
2026-08-18 10:02:30 +01:00
Dave Page 81ac80321e fix: make the auth gating test independent of import order
The test added in #10240 fails in a full run, though it passes when the
authenticate package is tested on its own:

    FAIL: runTest (pgadmin.authenticate.tests.test_auth_gating
                   .AuthSourceGatingTestCase)
    AssertionError: 'oauth2' not found in
                    ['internal', 'kerberos', 'ldap', 'webserver']

It cleared sys.modules for each provider and expected load_modules(), which
reaches them with "from . import <provider>", to import and hence re-register
them. That is not enough: IMPORT_FROM finds the attribute already set on the
parent package by whichever earlier test imported the module, and returns the
previous module object without re-executing it, so the provider never
re-registers itself.

Only oauth2 was affected, because it is the one provider imported earlier in a
full run, by the tests under browser/tests. Nothing pre-imports it when the
authenticate package is run alone, which is why the narrow run passed and CI
did not.

Drop the attribute from the parent package alongside the sys.modules entry, so
the import genuinely happens again.

Verified with the full suite as CI runs it, python regression/runtests.py
--exclude feature_tests: both scenarios pass and the runner's own exit status
is clean, with the only remaining failure a local config-directory permissions
check that depends on the developer's config_local.py.
2026-08-17 16:54:47 +01:00
agu2347 01e5bf0993 Schema Diff: fix false SERIAL/BIGSERIAL differences and invalid ALTER SQL (#10248)
Schema Diff compares tables whose serial columns have been
reverse-engineered back onto the SERIAL/BIGSERIAL/SMALLSERIAL pseudo-type,
and that reprojection left two problems behind for any column declared as
SERIAL.

First, the raw oid of the sequence behind the column's nextval() default
(defseqrelid) was still being compared, and two independently created
databases assign that sequence completely different oids, so structurally
identical columns were reported as different; it is now ignored alongside
its sibling seqrelid.

Second, the SQL generated for a serial column that genuinely does differ
read the column's old properties straight from the catalogue without the
same reprojection, so the real integer type and the real nextval() default
were compared against the pseudo-type and its emptied default. Any genuine
difference, such as a comment or a NOT NULL, therefore also rendered an
invalid ALTER COLUMN ... TYPE bigserial, a spurious DROP DEFAULT, and a set
of sequence options that only an identity column accepts. The changed
column is now normalised back onto the type that ALTER COLUMN can actually
be given, whilst the sequence it owns is left to the sequence object's own
comparison.

Covered by a unit test for the column comparison and by a Schema Diff test
that applies the generated SQL to the target and re-compares.

Fixes #10236
2026-08-17 14:58:21 +01:00
byshy 549d71a203 feat: toggle Object Explorer by re-clicking Default workspace icon (#10254)
Clicking the Default workspace icon whilst that workspace is already
current now collapses the Object Explorer, and clicking it again brings it
back, in the manner of the VS Code side bar. The choice is remembered in a
new Browser/ObjectExplorerVisible setting, so it survives a refresh, and
File → Reset Layout restores the panel along with the rest of the layout.

The panel is hidden with CSS rather than unmounted, so the tree keeps its
expansion and selection state whilst it is out of the way, and because
rc-dock sizes box children with flex, the main panel takes the freed width
with no gap left behind.

Folded in during review:

The Object Explorer keyboard shortcut no longer does nothing when the
sidebar is collapsed. It asks for the panel to be shown first, through a
pgadmin:object-explorer:show event on pgAdmin.Browser.Events. The focus()
call in bindLeftTree() was already inert, since the panel is a div with no
tabindex, so the useful part of that shortcut is the tree selection that
follows it.

LayoutDocker now fires a RESET event which BrowserComponent listens for,
rather than resetLayout() being monkey-patched onto the shared docker
instance. That removes a mutation of an object other code holds a
reference to.

A failed attempt to persist the setting is logged rather than silently
discarded, since otherwise the sidebar could disagree with the server after
the next refresh with nothing to explain why.

The icon's tooltip says Show or Hide Object Explorer while it acts as a
toggle, instead of describing a workspace switch that will not happen, and
aria-pressed exposes the state to assistive technology.

Tests in web/regression/javascript/workspaces/WorkspaceProvider.spec.jsx
cover the default, the saved setting being honoured, each toggle persisting
its new value, two toggles in one tick resolving in order, and the reveal
event.
2026-08-17 14:49:49 +01:00
dependabot[bot] 7b592f2e82 Javascript dependency: Bump image-minimizer-webpack-plugin in /web (#10288)
Bumps [image-minimizer-webpack-plugin](https://github.com/webpack/image-minimizer-webpack-plugin) from 4.1.4 to 5.0.0.
- [Release notes](https://github.com/webpack/image-minimizer-webpack-plugin/releases)
- [Changelog](https://github.com/webpack/image-minimizer-webpack-plugin/blob/main/CHANGELOG.md)
- [Commits](https://github.com/webpack/image-minimizer-webpack-plugin/compare/v4.1.4...v5.0.0)

---
updated-dependencies:
- dependency-name: image-minimizer-webpack-plugin
  dependency-version: 5.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-17 14:48:25 +01:00
Dave Page efc0dea335 docs: describe what AI features transmit to LLM providers (#10291)
The AI documentation covered how to configure each provider but said
nothing about what actually leaves the server once one is configured,
which is the first question anyone working under data residency or
procurement constraints will ask.

This adds a provider-neutral section to the AI Reports documentation
setting out what may be transmitted, which is schema definitions,
configuration settings read from pg_settings, query text, EXPLAIN plan
output and, because the Query Tool assistant can run read-only queries,
row data where the assistant judges it necessary; when nothing is
transmitted at all, which is the default, since no provider is
configured out of the box; and the fact that each provider processes
that data under its own terms and in locations of its own choosing, so
the reader knows to check both those terms and their own organisation's
policies before enabling a cloud provider.

It also makes explicit that the list of providers reflects the APIs
pgAdmin can speak to rather than a recommendation, and cross-references
the new section from the AI Assistant notes in the Query Tool
documentation.
2026-08-17 14:25:47 +01:00
dev-hari-prasad 2de30f25c0 Gate external auth providers behind SERVER_MODE in desktop mode (#10240)
Desktop mode has no use for the kerberos, ldap, mfa, oauth2 or webserver
authentication providers: AUTHENTICATION_SOURCES defaults to internal, MFA is
inert because mfa_enabled() requires SERVER_MODE, and get_logout_url() already
guards its kerberos and oauth2 branches on SERVER_MODE. So skip importing and
initialising them unless SERVER_MODE is set, leaving desktop mode with
internal authentication alone.

The accompanying test needed wiring into our own runner before it would ever
execute: web/pgadmin/authenticate/tests/ had no __init__.py, so pkgutil did
not report it and find_modules() never reached the module, and the test class
derived from unittest.TestCase rather than BaseTestGenerator, so the
TestsGeneratorRegistry metaclass never registered it and get_suite() could not
pick it up. Both are fixed here, and the two scenarios now run and pass under
regression/runtests.py.

The test no longer asserts that pgadmin.authenticate.mfa is absent from
sys.modules in desktop mode, because it is not: both pgadmin/browser/__init__
and pgadmin/user_login_check import pgadmin.authenticate.mfa.utils
unconditionally. The original assertion only held because the test itself
popped the module first.
2026-08-17 13:39:59 +01:00
Dave Page c2398d5b08 chore: stop Dependabot re-proposing pickr 1.10 and Babel 8 (#10286)
Two exclusions, both for bumps we have already established we cannot take.

pickr 1.10 switched to a dual CJS/ESM build whose CommonJS entry point exports
a namespace object rather than the class, so Babel's interop leaves
_pickr.default as an object and every dialog carrying a colour field dies on
mount. That took master's feature tests down for three days (#10264, reverted
in #10278). Because 1.9 -> 1.10 is a minor bump it rejoined the grouped
minor-and-patch PR immediately afterwards, buried among twenty other packages,
so minor updates are excluded here as well as majors until withColorPicker.js
is adapted to the new export shape.

Babel 8 requires Node ^22.18.0 || >=24.11.0, whilst Node 20 remains our
minimum, and every 8.x package peer-depends on @babel/core ^8.0.0 whilst we
are on 7.x. Taking any of them individually, as Dependabot proposed for
preset-env, eslint-parser and plugin-syntax-jsx, leaves an unsatisfiable peer
requirement and a mixed 7/8 tree. Babel 8 needs a coordinated migration across
all ten @babel/* packages once we are on Node 22.
2026-08-17 12:13:56 +01:00
dependabot[bot] 98fda1cbb0 Javascript dependency: Bump the runtime-javascript-minor-and-patch group across 1 directory with 2 updates (#10279)
Bumps the runtime-javascript-minor-and-patch group with 2 updates in the /runtime directory: [electron](https://github.com/electron/electron) and [globals](https://github.com/sindresorhus/globals).


Updates `electron` from 43.3.0 to 43.4.0
- [Release notes](https://github.com/electron/electron/releases)
- [Commits](https://github.com/electron/electron/compare/v43.3.0...v43.4.0)

Updates `globals` from 17.9.0 to 17.11.0
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v17.9.0...v17.11.0)

---
updated-dependencies:
- dependency-name: electron
  dependency-version: 43.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: runtime-javascript-minor-and-patch
- dependency-name: globals
  dependency-version: 17.11.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: runtime-javascript-minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-17 12:08:25 +01:00
Dave Page c20f0e33bb chore: stop Dependabot proposing major jest-dom bumps (#10280)
@testing-library/jest-dom 7 declares "engines": {"node": ">=22"}, whilst Node
20 remains our minimum and is what most of the buildfarm runs. The bump passes
our JS tests on Node 20 in practice, so this is a deliberate choice not to
depend on an officially unsupported combination rather than a reaction to a
failure.

The "^6.9.1" constraint in web/package.json already prevents 7.x from being
installed; what Dependabot proposes is widening that constraint, which is the
part we do not want, so ignore major updates for this package until the
buildfarm moves to Node 22. See #10271 and #10210.
2026-08-17 10:35:22 +01:00
dependabot[bot] e2ca7bb447 Javascript dependency: Bump @tanstack/react-virtual in /web (#10270)
Bumps [@tanstack/react-virtual](https://github.com/TanStack/virtual/tree/HEAD/packages/react-virtual) from 3.13.24 to 3.14.9.
- [Release notes](https://github.com/TanStack/virtual/releases)
- [Changelog](https://github.com/TanStack/virtual/blob/main/packages/react-virtual/CHANGELOG.md)
- [Commits](https://github.com/TanStack/virtual/commits/@tanstack/react-virtual@3.14.9/packages/react-virtual)

---
updated-dependencies:
- dependency-name: "@tanstack/react-virtual"
  dependency-version: 3.14.9
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-17 10:29:57 +01:00
dependabot[bot] e3626c4918 Python dependency: Bump selenium in /web/regression (#10267)
Bumps [selenium](https://github.com/SeleniumHQ/Selenium) from 4.46.0 to 4.47.0.
- [Release notes](https://github.com/SeleniumHQ/Selenium/releases)
- [Commits](https://github.com/SeleniumHQ/Selenium/compare/selenium-4.46.0...selenium-4.47.0)

---
updated-dependencies:
- dependency-name: selenium
  dependency-version: 4.47.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-17 10:29:46 +01:00
Dave Page 7790264acb chore: batch minor and patch Dependabot updates into one PR per manifest (#10277)
We were carrying 27 open Dependabot PRs, the great majority of them single
patch bumps of transitive packages, and the review cost of that queue is
entirely out of proportion to its risk. Every genuine problem found whilst
clearing it (paramiko 5.0 breaking sshtunnel, use-resize-observer 10.0 dropping
its default export, jest-dom 7.0 requiring a newer Node) was a major bump.

Group minor and patch updates into a single weekly PR per manifest, and leave
major updates arriving individually so each still gets its own review. Grouping
applies to version updates only, so security updates are unaffected and
continue to arrive as separate PRs.
2026-08-17 10:23:20 +01:00
Dave Page 8ceffe310b Revert "Javascript dependency: Bump @simonwep/pickr from 1.9.1 to 1.10.1 in /web (#10264)" (#10278)
This reverts commit 34dba5740e0e0e3bb6dd06afe4d84f9e4c0a9a6e.

pickr 1.10.1 breaks every dialog carrying a colour field, which includes
Register - Server, so the dialog renders as "Something went wrong." and all
feature tests time out waiting for its fields.

The cause is a dual-package interop change rather than anything in our code.
In 1.9.1 the UMD factory ended with "return e = e.default", so module.exports
was the Pickr class itself; Babel's _interopRequireDefault saw no __esModule
marker, wrapped it, and _pickr.default was the constructor. In 1.10.1 the
factory instead returns a namespace object carrying a default getter, still
without an __esModule marker, so the wrapper nests it one level deeper and
_pickr.default is that namespace object:

  TypeError: _pickr.default is not a constructor
      at initPickr (pgadmin/static/js/helpers/withColorPicker.js:69)

Nothing in the JS unit tests, eslint or the webpack build can see this,
because it only fails when the component mounts, which is why the bump went in
with a green board and only the feature tests caught it.

Moving to 1.10.1 later is perfectly possible, but it needs the import in
withColorPicker.js adapted to the new shape, and it must be validated by
opening a dialog rather than by a green Jest run.
2026-08-17 10:22:50 +01:00
dependabot[bot] 34dba57409 Javascript dependency: Bump @simonwep/pickr from 1.9.1 to 1.10.1 in /web (#10264)
Bumps [@simonwep/pickr](https://github.com/simonwep/pickr) from 1.9.1 to 1.10.1.
- [Release notes](https://github.com/simonwep/pickr/releases)
- [Commits](https://github.com/simonwep/pickr/compare/1.9.1...v1.10.1)

---
updated-dependencies:
- dependency-name: "@simonwep/pickr"
  dependency-version: 1.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 13:12:02 +01:00
dependabot[bot] 6fac4f0cbe Javascript dependency: Bump eslint from 10.8.0 to 10.8.1 in /runtime (#10260)
Bumps [eslint](https://github.com/eslint/eslint) from 10.8.0 to 10.8.1.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.8.0...v10.8.1)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.8.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 13:08:56 +01:00
dependabot[bot] 12830a7be8 Python dependency: Update setuptools requirement from ==83.* to ==84.* (#10261)
Updates the requirements on [setuptools](https://github.com/pypa/setuptools) to permit the latest version.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v83.0.0...v84.0.0)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-version: 84.0.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 13:08:31 +01:00
dependabot[bot] 1b48d616f9 Javascript dependency: Bump @testing-library/user-event in /web (#10262)
Bumps [@testing-library/user-event](https://github.com/testing-library/user-event) from 14.6.1 to 14.6.3.
- [Release notes](https://github.com/testing-library/user-event/releases)
- [Changelog](https://github.com/testing-library/user-event/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/user-event/compare/v14.6.1...v14.6.3)

---
updated-dependencies:
- dependency-name: "@testing-library/user-event"
  dependency-version: 14.6.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 13:08:22 +01:00
dependabot[bot] 39a56a1074 Python dependency: Update cryptography requirement (#10263)
Updates the requirements on [cryptography](https://github.com/pyca/cryptography) to permit the latest version.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/49.0.0...50.0.0)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 50.0.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 13:08:19 +01:00
dependabot[bot] 6de909b2d5 Javascript dependency: Bump globals from 17.8.0 to 17.9.0 in /runtime (#10259)
Bumps [globals](https://github.com/sindresorhus/globals) from 17.8.0 to 17.9.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v17.8.0...v17.9.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 17.9.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 12:04:02 +01:00
dependabot[bot] 1908d468f1 Javascript dependency: Bump electron from 43.2.0 to 43.3.0 in /runtime (#10258)
Bumps [electron](https://github.com/electron/electron) from 43.2.0 to 43.3.0.
- [Release notes](https://github.com/electron/electron/releases)
- [Commits](https://github.com/electron/electron/compare/v43.2.0...v43.3.0)

---
updated-dependencies:
- dependency-name: electron
  dependency-version: 43.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 12:03:59 +01:00
Flipper 9cd0e35f47 Add backendRefs group and kind to httproute.yaml (#10222) 2026-08-14 11:34:31 +01:00
Martin Manka 3cb33475ec Helm Chart: Add existingClaim to persistence (#10230) 2026-08-14 11:33:39 +01:00
byshyandCursor e6e35bb4c4 fix(pkg): remove trailing quote from Windows ProductVersion (#10250)
rcedit was passed "%APP_VERSION%"", which stamped ProductVersion as
e.g. 9.17" on Windows installer builds.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-14 11:31:43 +01:00
dependabot[bot] 1dd4d00246 Javascript dependency: Bump ip-address from 10.2.0 to 10.3.1 in /web (#10225)
Bumps [ip-address](https://github.com/beaugunderson/ip-address) from 10.2.0 to 10.3.1.
- [Release notes](https://github.com/beaugunderson/ip-address/releases)
- [Commits](https://github.com/beaugunderson/ip-address/compare/v10.2.0...v10.3.1)

---
updated-dependencies:
- dependency-name: ip-address
  dependency-version: 10.3.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 11:27:59 +01:00
Dave Page 9338129183 chore: stop Dependabot proposing major paramiko bumps (#10257)
paramiko 5.0 removed DSSKey entirely, whilst sshtunnel 0.4.0 still refers to
paramiko.DSSKey in SSHTunnelForwarder.get_keys(), which _consolidate_auth()
calls from the constructor. A major bump therefore does not merely drop DSA
key support, it raises AttributeError before any SSH tunnelled connection can
be established, and nothing in CI covers SSH tunnels so it looks green.
sshtunnel has had no release since 0.4.0 in 2019, so there is nothing newer to
move to on that side.

Ignore major paramiko updates until sshtunnel is fixed or replaced. The
exclusion is repeated under the /web/regression entry because
web/regression/requirements.txt starts with "-r ../../requirements.txt", so
that entry sees the root pins too.
2026-08-14 11:26:51 +01:00
dependabot[bot] 2d989238a4 Javascript dependency: Bump tar from 7.5.16 to 7.5.21 in /web (#10182)
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.16 to 7.5.21.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.16...v7.5.21)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.21
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 11:21:06 +01:00
dependabot[bot] aa16a12182 Javascript dependency: Bump shell-quote from 1.8.4 to 1.10.0 in /web (#10185)
Bumps [shell-quote](https://github.com/ljharb/shell-quote) from 1.8.4 to 1.10.0.
- [Changelog](https://github.com/ljharb/shell-quote/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/shell-quote/compare/v1.8.4...v1.10.0)

---
updated-dependencies:
- dependency-name: shell-quote
  dependency-version: 1.10.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 11:21:03 +01:00
dependabot[bot] d9635cd2cd Javascript dependency: Bump svgo-loader from 4.0.0 to 5.0.0 in /web (#10207)
Bumps [svgo-loader](https://github.com/svg/svgo-loader) from 4.0.0 to 5.0.0.
- [Release notes](https://github.com/svg/svgo-loader/releases)
- [Commits](https://github.com/svg/svgo-loader/compare/v4.0.0...v5.0.0)

---
updated-dependencies:
- dependency-name: svgo-loader
  dependency-version: 5.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 11:21:00 +01:00
dependabot[bot] 904bb2e1e2 Javascript dependency: Bump anti-trojan-source in /web (#10211)
Bumps [anti-trojan-source](https://github.com/lirantal/anti-trojan-source) from 1.12.0 to 1.12.1.
- [Release notes](https://github.com/lirantal/anti-trojan-source/releases)
- [Changelog](https://github.com/lirantal/anti-trojan-source/blob/main/RELEASE.md)
- [Commits](https://github.com/lirantal/anti-trojan-source/compare/v1.12.0...v1.12.1)

---
updated-dependencies:
- dependency-name: anti-trojan-source
  dependency-version: 1.12.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 11:20:56 +01:00
dependabot[bot] 02c8b3748e Javascript dependency: Bump brace-expansion in /web (#10220)
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.16 to 1.1.18.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/commits)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.18
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 11:20:53 +01:00
dependabot[bot] 6696db11be Javascript dependency: Bump socket.io-parser from 4.2.6 to 4.2.7 in /web (#10229)
Bumps [socket.io-parser](https://github.com/socketio/socket.io) from 4.2.6 to 4.2.7.
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/socket.io-parser@4.2.6...socket.io-parser@4.2.7)

---
updated-dependencies:
- dependency-name: socket.io-parser
  dependency-version: 4.2.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 11:20:50 +01:00
dependabot[bot] 109706d8f8 Javascript dependency: Bump fast-uri from 3.1.4 to 3.1.5 in /web (#10232)
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.4 to 3.1.5.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.4...v3.1.5)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 11:20:47 +01:00
dependabot[bot] 8cc69cf71c Javascript dependency: Bump dompurify from 3.4.12 to 3.4.13 in /web (#10241)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.12 to 3.4.13.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.4.12...3.4.13)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.4.13
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 11:20:43 +01:00
dependabot[bot] 1a87ee14d9 Javascript dependency: Bump js-yaml from 3.15.0 to 3.15.1 in /web (#10244)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.15.0 to 3.15.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/3.15.1/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.15.0...3.15.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 3.15.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 11:20:39 +01:00
dependabot[bot] 73c5e01309 Javascript dependency: Bump postcss from 8.5.21 to 8.5.23 in /web (#10246)
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.21 to 8.5.23.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.21...8.5.23)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.23
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 11:20:35 +01:00
dependabot[bot] 1cca96b62c Javascript dependency: Bump eslint from 10.7.0 to 10.8.0 in /runtime (#10204)
Bumps [eslint](https://github.com/eslint/eslint) from 10.7.0 to 10.8.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.7.0...v10.8.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 11:20:31 +01:00
dependabot[bot] 74687c9ddf Javascript dependency: Bump brace-expansion in /runtime (#10219)
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 5.0.7 to 5.0.9.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v5.0.7...v5.0.9)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 5.0.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 11:20:28 +01:00
dependabot[bot] 0d6973c042 Javascript dependency: Bump globals from 17.7.0 to 17.8.0 in /runtime (#10223)
Bumps [globals](https://github.com/sindresorhus/globals) from 17.7.0 to 17.8.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v17.7.0...v17.8.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 17.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 11:20:25 +01:00
dependabot[bot] 0b9fb6fc06 Javascript dependency: Bump axios from 1.18.1 to 1.19.0 in /runtime (#10224)
Bumps [axios](https://github.com/axios/axios) from 1.18.1 to 1.19.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.18.1...v1.19.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 11:20:21 +01:00
dependabot[bot] 60d4dd8c5a Javascript dependency: Bump undici from 7.28.0 to 7.29.0 in /runtime (#10227)
Bumps [undici](https://github.com/nodejs/undici) from 7.28.0 to 7.29.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v7.28.0...v7.29.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 7.29.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 11:20:18 +01:00
dependabot[bot] df7c256b53 Javascript dependency: Bump fast-uri from 3.1.4 to 3.1.5 in /runtime (#10231)
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.4 to 3.1.5.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.4...v3.1.5)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 11:20:12 +01:00
dependabot[bot] db7cbc7900 Python dependency: Update typer requirement from ==0.26.* to ==0.27.* (#10203)
Updates the requirements on [typer](https://github.com/fastapi/typer) to permit the latest version.
- [Release notes](https://github.com/fastapi/typer/releases)
- [Changelog](https://github.com/fastapi/typer/blob/master/docs/release-notes.md)
- [Commits](https://github.com/fastapi/typer/compare/0.26.0...0.27.0)

---
updated-dependencies:
- dependency-name: typer
  dependency-version: 0.27.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 11:20:08 +01:00
dependabot[bot] aac7d33441 Python dependency: Bump certifi from 2026.6.17 to 2026.7.22 (#10205)
Bumps [certifi](https://github.com/certifi/python-certifi) from 2026.6.17 to 2026.7.22.
- [Commits](https://github.com/certifi/python-certifi/compare/2026.06.17...2026.07.22)

---
updated-dependencies:
- dependency-name: certifi
  dependency-version: 2026.7.22
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 11:20:05 +01:00
dependabot[bot] 9e0a847410 Python dependency: Bump selenium in /web/regression (#10206)
Bumps [selenium](https://github.com/SeleniumHQ/Selenium) from 4.45.0 to 4.46.0.
- [Release notes](https://github.com/SeleniumHQ/Selenium/releases)
- [Commits](https://github.com/SeleniumHQ/Selenium/compare/selenium-4.45.0...selenium-4.46.0)

---
updated-dependencies:
- dependency-name: selenium
  dependency-version: 4.46.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 11:20:02 +01:00
Dave Page 35d68c47eb fix: follow Flask-Security-Too 5.8.2's corrected is_locked() semantics (#10256)
Flask-Security-Too 5.8.2, released on 12 August 2026, fixed a long-standing
inversion in its login forms: `LoginForm.validate()` previously read a `True`
return from `UserMixin.is_locked()` as "not locked, carry on", and the base
implementation unconditionally returned `True`. Our `User.is_locked()` was
written against that inverted convention, so as soon as CI began resolving
5.8.2 through the loose `Flask-Security-Too==5.8.*` pin, an unlocked user
returned `True`, form validation failed, the login POST redirected with a 302
and every subsequent request arrived as `AnonymousUser`. The server-mode data
isolation tests caught it, though the breakage is not limited to tests: on
5.8.2 nobody could log in at all.

`User.is_locked()` now returns `True` when the account is locked, matching the
corrected upstream contract, and the dependency is floored at 5.8.2 so that we
cannot silently resolve a release which reads the value backwards. The two
conventions are mutually exclusive, hence a floor rather than a version check
in the model. The regression tests are updated to assert the fixed contract.
2026-08-14 11:17:09 +01:00
Dave Page 6487f2a93f Add support for Python 3.14 (#10255)
Python 3.14 is now supported, so add the trove classifier for it to the
pip packaging metadata and move the desktop builds onto it: the macOS
bundle now defaults to 3.14.7, and the Windows build looks for an
interpreter in C:\Python314 by default, with both build READMEs updated
to match. The minimum supported version is unchanged at 3.9.

Whilst here, the SonarQube scanner's Python compatibility list had drifted
somewhat, still naming 3.7 and 3.8 and stopping at 3.11, so it has been
brought into line with the versions we actually support.
2026-08-14 11:16:25 +01:00
Ashesh Vashi 067f7af227 revert(pkg): back out all _fixup_imports path-handling attempts
pkg/mac/build-functions.sh reverted to bd841b882's state - keeps the
notarization diagnostic fix (print REQUEST_STATUS + fetch notarytool
log on failure), drops everything from 0296de48e through 6891b5e12.

Five iterations trying to fix _fixup_imports's otool handling of
"pgAdmin 4 Helper (GPU)" (newline-based path list, otool-classic
fallback via bare name, via resolved absolute path, absolute-path
retries for otool/install_name_tool) all failed to get the appbundle
build past this file. Confirmed along the way: the path itself is
byte-for-byte correct and the file verifiably exists at the moment
otool runs (od -c / ls -la evidence from a live buildfarm run); both
otool and otool-classic, called directly with correct quoting and a
resolved absolute path, still fail identically on it. That's a real
bug/limitation in Apple's tooling on this build node, not fixable by
further changes to how this script constructs or passes the path.

Reverting rather than continuing to guess. The stale nspkg.pth /
--system-site-packages fix in pkg/linux/build-functions.sh (verified
working, pgadmin4-rpm-build #180 succeeded) is untouched by this.
2026-07-30 12:30:49 +05:30
Ashesh Vashi 6891b5e12f fix(pkg): retry otool/install_name_tool with absolute path before giving up
pgadmin4-appbundle-build #116 (macos-arm64) failed even with
otool-classic invoked directly via its real resolved path
(669c2f244's fix) - identical error, byte-for-byte, to calling plain
`otool -L`:

  can't open file: .../pgAdmin 4 Helper  (No such file or directory)

That rules out PATH resolution and otool's internal re-exec dispatch
as the cause too. Both otool -L and otool-classic -L, invoked directly
with correct argv quoting and a verified-correct, verified-present
path, fail identically on this one binary. Whatever's actually broken
is inside these tools' own handling of "(" in a path they're given.

Untested variable so far: this script pushd's into the bundle dir and
passes relative paths ("./Contents/...") throughout. Try a plain
absolute path with each tool before falling back to the diagnostic
dump - cheap to test, and applies equally to the two
`install_name_tool -change` calls further down that edit this same
file in place, since they could hit the identical issue.

Still not proven - if this also fails, the diagnostic dump (retained
as final fallback) will at least confirm parens themselves are the
blocker regardless of path form, which would mean sidestepping via a
temporary parens-free symlink is the next thing to try.
2026-07-30 12:10:59 +05:30
Ashesh Vashi 669c2f2442 fix(pkg): resolve otool-classic's real path, bare name isn't on PATH
pgadmin4-appbundle-build #115 (macos-arm64) failed even with the
otool-classic fallback from 2d17e9e15:

  otool-classic output: .../build-functions.sh: line 260:
    otool-classic: command not found

otool-classic only exists inside the active Xcode toolchain
(/Applications/Xcode.app/.../XcodeDefault.xctoolchain/usr/bin/
otool-classic - the exact path visible in otool's own internal error
message), not as a bare command on $PATH. The previous fallback never
actually tested whether the real otool-classic binary can open the
file - it just failed on a missing command.

Resolve the real path via `xcrun -f otool-classic` (falling back to
deriving it from `xcode-select -p` if xcrun's lookup is unavailable)
and invoke that directly.

Not yet verified against a real buildfarm run.
2026-07-30 12:01:52 +05:30
Ashesh Vashi 2d17e9e15d fix(pkg): retry via otool-classic directly, now with hard evidence
pgadmin4-appbundle-build #114 (macos-arm64) failed on the same
"pgAdmin 4 Helper (GPU)" binary as before, but this time the
diagnostics from d4364555a actually ran and proved the path itself is
not the problem:

  Raw bytes of the path (od -c): plain ASCII throughout, single
  spaces, no non-breaking/unicode look-alikes, no trailing garbage.
  ls -la of the exact path: file exists, correct size/perms,
  timestamped seconds before otool ran - not a race condition either.

otool -L still failed via its internal otool-classic re-exec on that
exact, verified-correct, verified-present file. That's conclusively a
bug in otool's own internal dispatch, not anything this script builds
or passes to it - we can't fix Apple's tool, but we can avoid
triggering its broken code path.

Retry via `otool-classic -L "${TODO_OBJ}"` called directly (our own
argv-based quoting, not otool's internal re-exec, whatever that does
differently) when the initial `otool -L` fails. Every other binary in
the bundle keeps going through plain `otool -L` unchanged - this only
engages for the specific case that's already failing anyway. The
diagnostic dump (od -c / ls -la / otool-classic's own output) is kept
as the final fallback in case even the direct call fails too.

Not yet verified against a real buildfarm run.
2026-07-30 11:55:29 +05:30
Ashesh Vashi d4364555ae fix(pkg): diagnostics never ran - set -e ate the failure before $? check
pgadmin4-appbundle-build #113 (macos-arm64) failed again on the same
otool -L call, but none of the diagnostic output from 401afbfa3 showed
up. Instead:

  The command "OTOOL_OUTPUT=$(otool -L "${TODO_OBJ}" 2>&1)" failed in
  "_fixup_imports" with exit code 1.

That's this script's own ERR trap firing on the assignment statement
itself. Under set -e, `VAR=$(cmd)` still propagates cmd's non-zero
exit to the trap immediately - the previous diagnostic's separate
`OTOOL_STATUS=$?` line right after never got a chance to run, so
nothing was ever printed, same as the original bare failure.

Move the assignment into the condition of the `if` itself
(`if ! OTOOL_OUTPUT=$(...); then`) - bash explicitly exempts a
command being tested by if/while/until (or negated with !) from
errexit, so this time the diagnostic block will actually execute.

Still not verified - watching the next buildfarm run.
2026-07-30 11:28:36 +05:30
Ashesh Vashi 401afbfa31 fix(pkg): add diagnostics for otool -L failures instead of guessing
pgadmin4-appbundle-build #112 (macos-arm64) failed with otool-classic
unable to open a path that our own logging showed as correct
immediately beforehand ("pgAdmin 4 Helper (GPU)"). Rather than work
around it blindly (an otool-classic fallback was tried and reverted -
unverified, and risks masking a real problem instead of fixing it),
capture real evidence the next time this happens:

- raw byte dump (od -c) of ${TODO_OBJ} right before otool runs, and
  again on failure - catches non-ASCII or invisible whitespace that
  a plain `echo` can't show
- ls -la of the exact path and its containing directory on failure -
  confirms whether the file actually exists at that exact moment
  (rules out/in a race with something still writing it)
- otool's actual stdout/stderr and exit code, instead of letting its
  own error text be the only signal

No fallback behavior - fails loudly (exit 1) with this diagnostic
dump so we can tell what's actually happening from the next real
buildfarm run instead of speculating further.
2026-07-30 11:20:38 +05:30
Ashesh Vashi 0296de48ee fix(pkg): _fixup_imports truncates paths with spaces, breaking otool
pgadmin4-appbundle-build #110 (macos-x64) failed with:
  error: otool-classic: can't open file: ./Contents/Frameworks/pgAdmin
  error: otool-classic: can't open file: ./Contents/Frameworks/Electron

_fixup_imports built its worklist with `awk -F':| '`, a regex
alternation that splits on a literal ':' OR any bare space. `file`'s
actual output format is "path: description" (colon-space as one
token), so any space *inside* the path itself - "pgAdmin 4 Helper
(Plugin).app", "Electron Framework.framework", both routine in macOS
app bundles - also got treated as a split point, truncating $1 to
whatever preceded the first space.

The resulting list was then joined with a single space (ORS=" ") and
iterated via an unquoted `for x in $list`, which word-splits on
spaces again - doubly ambiguous between a path's own spaces and the
list's separator, unrecoverable however it's parsed.

Fix: split only on the literal ": " token file emits (`awk -F': '`,
not `-F ':| '`), and switch the whole worklist to newline-separated
instead of space-separated, iterated with `while IFS= read -r`
instead of `for x in $unquoted_list`, at both the outer (executable)
level and inner (library-copy) level (`TODO="${TODO}"$'\n'"..."`).

Not yet verified against a real buildfarm run - can't reproduce the
macOS codesigning environment locally. Watching the next
pgadmin4-appbundle-build run.
2026-07-30 09:48:03 +05:30
Ashesh Vashi 689c969f95 fix(pkg): stop using --system-site-packages, use a scoped .pth instead
The previous fix (bd841b882) tried to delete the specific broken
sphinxcontrib-jsmath nspkg.pth file from the system site-packages
before creating the venv. It didn't work: pgadmin4-rpm-build #177
failed identically on el-10. The delete silently no-opped - that
directory is root-owned on the build node and the build user doesn't
have write access there, so `find -delete` failed while `-print` (which
runs first) still logged the match, making the fix look like it ran.

Root cause is the --system-site-packages flag itself: it uses Python's
site.addsitedir() internally, which does not just add a directory to
sys.path - it also scans that directory for every *.pth file and
executes any "import ..." lines found in them. That's what runs the
broken nspkg.pth's stale namespace-package bootstrap code and corrupts
sys.path before core stdlib resolves, breaking pip's own subprocess.

We only need --system-site-packages for OS-provided packages that
don't have reliable pip wheels (e.g. dbus-python, a hard runtime
dependency per pkg/debian/build.sh's python3-dbus dep, needs
libdbus-1-dev to build from source). A *plain path line* (no "import")
in a .pth file only appends that directory to sys.path - it does not
trigger a further .pth scan of it. So: create the venv without
--system-site-packages, then write the system site-packages
directories as plain lines into a .pth file inside the venv's own
site-packages (which the build user does own). Same OS-package
availability, without ever asking Python to treat the system directory
as a site directory - the broken nspkg.pth is simply never read.

Not yet verified against a real buildfarm run - can't reproduce the
el-10 environment locally. Watching the next pgadmin4-rpm-build run.
2026-07-30 09:36:44 +05:30
Ashesh Vashi bd841b8827 fix(pkg): surface real notarization failures, guard against stale nspkg.pth in venv
macOS build (build-functions.sh): _notarize_pkg only printed
"Notarization failed." on rejection, giving no indication why. Print
the actual REQUEST_STATUS and fetch the full notary log via
`notarytool log` so future failures are actually diagnosable from the
Jenkins console instead of just "status: Invalid".

Linux build (build-functions.sh): _create_python_virtualenv creates
venvs with --system-site-packages, which pulls in the entire system
site-packages dir - including any stale, improperly-uninstalled
package's namespace-package .pth hook. That legacy pip/setuptools
mechanism runs at interpreter startup, before core stdlib is
guaranteed to resolve; a leftover sphinxcontrib-jsmath nspkg.pth on
the el-10 build node corrupted sys.path early enough to break pip's
own subprocess bootstrap, failing the whole build with a misleading
"No module named 'importlib'"/"'traceback'" error
(pgadmin4-rpm-build #176). Remove that exact known-broken file before
creating the venv - not every *-nspkg.pth, since this touches the
*system* Python install on a build node shared by other jobs, and any
other such file could still be load-bearing for something unrelated.
--system-site-packages itself is left untouched - it's required so
venvs can see OS-provided packages not available as clean pip wheels
on every target platform.
2026-07-30 09:21:22 +05:30
Ashesh Vashi 3f99454199 fix: unbound keyring probe at config-import time can hang desktop startup
evaluate_and_patch_config() called keyring.get_password() synchronously
at `import config` time to detect a selected-but-unusable OS keyring
backend. On Debian 13 (and similar headless/RDP sessions with no live
D-Bus/GNOME-Keyring session), that call - and even the keyring import
itself - can block forever, freezing the whole desktop app before it
ever starts.

Revert evaluate_config.py to the pre-9.17 synchronous check (backend
name only, no get_password call). Move the usability probe into
pgadmin.utils.keyring_probe, run from create_app() in a background
daemon thread so it never delays startup, isolated in its own process
via subprocess.Popen(sys.executable, '-c', ...) so a hang can actually
be killed - a thread-only timeout can't do this, it would leave
CPython's per-module import lock held forever and wedge any later
`import keyring` in the parent process too.

subprocess.Popen (plain fork+exec), not multiprocessing.Process, is
required here: create_app() runs at the top level of pgAdmin4.py while
it's still being imported, and multiprocessing's spawn start method
refuses to start a child before the current process finishes
bootstrapping its __main__ module. An earlier version of this fix used
multiprocessing and crashed the probe thread with RuntimeError on every
single test run, which corrupted the SQLAlchemy/sqlite session for the
rest of app init and surfaced as an unrelated-looking "attempt to write
a readonly database" error on module_preference inserts.

config.USE_OS_SECRET_STORAGE is only ever read from request handlers
requiring an authenticated session, never at import time or inside
create_app() itself, so the async resolution race is safe in practice.

Tests mock subprocess.Popen for the timeout/kill/config-fallback
orchestration (deterministic, no real 3s wait or backend dependency),
plus one test class that runs the real probe script in a real
subprocess against a fake keyring module injected via PYTHONPATH, so
the script body itself has real coverage.

Verified with the full regression suite (--exclude feature_tests) in
both desktop mode (2134 passed, 0 failed) and server mode (2251
passed, 0 failed); remaining skips are pre-existing pgAgent-dependent
job tests.
2026-07-30 00:00:33 +05:30
Ashesh Vashi 6e029ab6e5 docs: correct reporter credit for #10193/#10194 to full name
Hung Tran Quoc (GitHub handle @rampage0010) — release notes only
listed the given name.
2026-07-28 12:40:25 +05:30
Ashesh Vashi ee7109d400 Updated version for release v9.17 2026-07-28 12:08:09 +05:30
Ashesh Vashi 78f7a5f6ab test: restore app.PGADMIN_EXTERNAL_AUTH_SOURCE in auth-mocking tearDown
Each of the Kerberos/LDAP/OAuth2/webserver login-mocking test classes
sets self.app.PGADMIN_EXTERNAL_AUTH_SOURCE (the Flask app instance
attribute before_request() actually reads) in setUp, but tearDownClass
only restored app_config.PGADMIN_EXTERNAL_AUTH_SOURCE -- a different
object (the config module). Whichever of these classes ran last
(test_webserver_with_mocking, alphabetically) left the live app
attribute stuck at WEBSERVER for the rest of the suite, making
before_request() route every subsequent unauthenticated /login request
through authenticate.login()'s webserver auto-auth path instead of
rendering the login form -- surfacing as an unrelated CSRF-harvest
failure in test_close_requires_auth much later in the run.

Full server-mode regression: 2507 tests, 0 failures/errors (was 2).
2026-07-28 11:07:37 +05:30
Ashesh Vashi 689d200e9f test: fix server-mode test-harness bugs found running the full suite
get_test_user() created secondary test clients via app.test_client()
but never called setApp() on them, so any test using a second user
crashed with 'NoneType has no attribute config' the first time it
needed to fetch a CSRF token (44 errors in a full server-mode run).

TestSqlEditorCloseRequiresAuth.setUp() logs out the shared
class-level tester before asserting a CSRF token was harvested; when
that assertion failed, unittest skipped tearDown() entirely (by
design, tearDown only runs if setUp succeeds), leaving the tester
logged out for the rest of the suite and cascading into 188 unrelated
failures. Switched to addCleanup(), which always runs.

Full server-mode regression: 2507 tests, was 232 failures/errors,
now 2 (both isolated to this same test's own CSRF-harvest timing in
full-suite order, unrelated to these two fixes).
2026-07-28 10:12:18 +05:30
Ashesh Vashi 339b4f9cd3 Merge branch 'cve-9.17-rebase' into master 2026-07-28 09:12:21 +05:30
Domenico Sgarbossa 5fc319551d Updated message catalogs for v9.17 (#10212) 2026-07-28 09:00:14 +05:30
Ashesh Vashi 9ef9a7136f docs: add 9.17 release notes for CVE-2026-17346 through 17351, 17566
Adds bug-fix bullets for issues #10190-10194, #10200 (previously
assigned CVE-2026-17346..17351) and #10213 (CVE-2026-17566, the
import/export \copy backslash-escape RCE reported by Arpit Jain).
2026-07-28 08:56:41 +05:30
Ashesh Vashi 1496fabe28 fix(security): reject ambiguous backslash-escape in import/export query guard
_is_query_parens_balanced() always treated \' inside a single-quoted
string as an escaped quote, matching psql only when
standard_conforming_strings=off. Under the actual default (scs=on,
every supported PostgreSQL version), psql treats \ as a literal
character, so 'a\' closes the string right there. A crafted query
export payload like SELECT 'a\') TO PROGRAM 'cmd' x' passed the
balance check while the real ) it hid closed the wrapping \copy (...)
context in psql, exposing a live TO PROGRAM clause for RCE.

Since the correct interpretation depends on a server setting we
can't reliably know, reject any backslash inside a single-quoted
string outright instead of guessing.

Reported by Arpit Jain (arpitjain099).
2026-07-27 19:04:29 +05:30
Kundan ef76102bcd fix(llm): close lexer-differential bypass in AI Assistant read-only guard
sqlparse's string-literal lexing can disagree with PostgreSQL's: under
standard_conforming_strings = on (the default), a backslash before a
quote is an ordinary character to PostgreSQL but sqlparse treats it as
escaping the quote, so a payload like
SELECT '\';COMMIT;CREATE TABLE pwn(x int);SELECT 1 --'
passes _validate_readonly_query as a single SELECT while PostgreSQL
executes it as four statements -- the smuggled COMMIT ends the wrapping
BEGIN TRANSACTION READ ONLY and the trailing ROLLBACK is a no-op,
reintroducing the write/RCE bypass the bf47924444 fix was meant to
close (reported by Kai Aizen / SnailSploit).

Run the LLM-supplied query with prepare=True, forcing psycopg3's
extended query protocol. PostgreSQL's own Parse step -- not a
client-side approximation of it -- rejects any text containing more
than one statement, independent of how it's lexed. Threaded through
execute_2darray as an opt-in parameter (default None) so no other
caller's behavior changes. Also set SESSION CHARACTERISTICS AS
TRANSACTION READ ONLY as defense-in-depth against a smuggled COMMIT.

prepare=True alone is not sufficient: psycopg3's PrepareManager.get()
returns Prepare.NO -- checked before it even inspects the prepare
argument -- whenever the connection's prepare_threshold is None, which
is pgAdmin's per-server default ("Prepare threshold" is blank unless an
administrator sets it). On a default-configured server the extended
protocol never actually engaged, so the bypass stayed live. Force
prepare_threshold=0 on the LLM's single-use connection in
_connect_readonly() so the extended protocol -- and PostgreSQL's
single-statement Parse-step guarantee -- is unconditional on this
connection, without touching the server-wide setting or any other
session/caller.

Adds regression coverage at three levels: unit tests pinning that
prepare=True is always passed and that prepare_threshold=0 is forced
on the connection; and an end-to-end test driving the real
/sqleditor/nlq/chat/<trans_id>/stream route (LLM client mocked, real
tool-dispatch path) with the exact smuggled-COMMIT payload, confirming
the guarantee holds from the HTTP entry point down to the driver call.
2026-07-27 16:51:46 +05:30
Ashesh Vashi 2b4307f321 feat: add system-wide default for Geometry Viewer custom tile provider (#10198)
Follow-up to #10142. Geometry Viewer's custom tile provider was only
configurable per-user (Preferences), with no way for an administrator
to set an organization-wide default (e.g. an internal tile server)
that applies out of the box for every user.

config.DEFAULT_GEOMETRY_VIEWER_PROVIDER is a plain dict; the five
custom_tile_* preference defaults (url, name, crs, attribution,
max_zoom) are sourced from it instead of hardcoded literals.
Preference.get() only reads a per-user DB row if the user has
explicitly saved one, otherwise it falls back to this default --
override semantics, one active provider, no naming-conflict surface.

config_local.py/config_distro.py/PGADMIN_CONFIG_* replace this config
variable wholesale, not merged key-by-key, so a naive
DEFAULT_GEOMETRY_VIEWER_PROVIDER['name'] lookup would KeyError-crash
preference registration (and app startup) on any partial admin
override, or AttributeError on a wrong-typed value.
resolve_geometry_viewer_provider_defaults() guards against this:
falls back per-field on missing/wrong-type values, validates crs
against the 3 supported choices, validates max_zoom is a non-bool int
in [0, 25], degrading to the original hardcoded defaults instead of
crashing.

No new attacker-reachable surface: DEFAULT_GEOMETRY_VIEWER_PROVIDER is
filesystem/deployment-level admin config, same trust boundary as
DEFAULT_BINARY_PATHS/OAUTH2_CONFIG. Existing DOMPurify sanitization on
name/attribution and the http(s):// + {x}/{y}/{z} URL validation in
GeometryViewerUtils.js (from #10142) apply uniformly to config-sourced
and per-user values alike.

Adds test_geometry_viewer_provider_defaults.py (9 scenarios: fully
valid, partial override, non-dict, invalid CRS, out-of-range/non-int/
bool max_zoom, non-string fields, empty dict).
2026-07-27 16:07:17 +05:30
Kundan Sable 362c202b25 fix: close remaining front-door-only tool-RBAC gaps (tools_ai, import_export_servers, search_objects, change_password)
Extends the tool-RBAC fix (permission enforced only on a blueprint's
"front door" route, with the rest relying on @pga_login_required alone)
to four permission types the original fix didn't cover:

* tools_ai ("AI Reports"): none of the 14 Security/Performance/Design
  report routes in the llm blueprint checked this permission at all --
  only the frontend menu was hidden behind AllPermissionTypes.TOOLS_AI
  (llm/static/js/ai_tools.js). A user an admin had denied AI access to
  could call the report/report-stream routes directly and have real
  server/database/schema content sent to whichever external LLM
  provider is configured, bypassing both the access control and the
  data-exposure/cost boundary the permission exists to enforce.
* tools_import_export_servers: only 'save' was gated; 'get_servers' and
  'load_servers' relied on @pga_login_required alone. Lower impact than
  the above: get_servers only returns the calling user's own servers,
  and load_servers only previews an already-uploaded file without
  persisting (the actual write stays gated at 'save').
* tools_search_objects: only 'search' was gated; 'types' (a static list
  of supported object-type names for the dropdown, no DB content) was
  not.
* change_password: the self-service password-change route
  (browser.change_password) never checked this permission, so revoking
  "Change Password" from a role in Role management had no effect --
  users under that role could still change their own password. Lower
  severity than the others since the route is inherently self-scoped
  (can only ever affect the caller's own password).

Extends tools/tests/test_tool_permissions_required.py with 17 new
scenarios for the first three, and adds a dedicated
browser/tests/test_change_password_permission_required.py for the last
(different blueprint, different permission category).

Verified against a live PostgreSQL server in SERVER mode: all 40
scenarios (37 + 3) pass with the fix applied. Negative control:
reverting the four fixes reproduces exactly the reported gaps -- 17 of
the 20 pre-existing scenarios still pass unaffected, the 17 new ones
fail, and change_password returns 200 instead of 403 -- confirming the
tests actually catch the regression rather than passing vacuously.
2026-07-27 05:28:57 +00:00
Ashesh Vashi 84b93c268d chore(deps): consolidated dependency bumps (setuptools, fast-uri, tar, shell-quote, svgo) (#10199)
Supersedes 7 open Dependabot PRs by applying the safe ones and
properly fixing the one with a broken lockfile, in one CI cycle:

- setuptools ==82.* -> ==83.* (#10144, #10145 - duplicate PRs, same
  patch). Only touches the `python_version > '3.9'` line; the
  `<82; python_version <= '3.9'` gate for Python 3.9 is untouched,
  so this doesn't affect Python 3.9 support.
- fast-uri 3.1.2 -> 3.1.4 in both /web and /runtime (#10183, #10195)
  - fixes two real CVEs (GHSA-v2hh-gcrm-f6hx, GHSA-4c8g-83qw-93j6).
- tar 7.5.16 -> 7.5.21 (#10182) - patch series, decompression-bomb
  and unbounded-recursion hardening only.
- shell-quote 1.8.4 -> 1.10.0 (#10185) - additive opt-in option +
  parser fixes, no breaking changes.
- svgo 3.3.3 -> 4.0.2 (#10184) - Dependabot's own PR left yarn.lock
  internally inconsistent (dropped the workspace-level `svgo` entry
  while merging version-range blocks), so `yarn install --immutable`
  failed in CI with "the lockfile would have been modified by this
  install". Regenerated properly via `yarn up`/`yarn dedupe` here.
  The direct `svgo`/`svgo-loader` deps are not actually wired into
  any webpack rule (verified via grep) - the real SVG pipeline is
  `@svgr/webpack` -> `@svgr/plugin-svgo` -> svgo 3.3.3, which this
  bump does not touch - so the major version jump has no build
  impact. `yarn.lock` now correctly keeps that separate 3.3.3
  resolution alongside the deduped 4.0.2 one.

Not included (structurally blocked, tracked separately):
- paramiko 3.5.1 -> 5.0.0 (#9927): paramiko 5 removes DSSKey
  entirely; sshtunnel 0.4.0 (dormant since 2021) still references
  paramiko.DSSKey, so `import sshtunnel` would crash immediately.
- pywinpty 2.0.* -> 3.0.* (#10082, #10084): the existing pin cites
  https://github.com/andfoy/pywinpty/issues/545, confirmed still
  open ("process read and write not working as expected in 3.x").

Verified: `yarn install --immutable` clean in both /web and
/runtime, `yarn run linter` clean, full `yarn run bundle:dev`
compiles successfully.
2026-07-25 01:46:48 +05:30
Ashesh Vashi 1e7407bbf6 Merge remote-tracking branch 'origin/master' into cve-9.17-rebase 2026-07-25 01:04:16 +05:30
Ashesh Vashi 9bcc0ff8de docs: fix RST emphasis error in v9.17 release notes
Trailing "SELECT *" broke sphinx's inline emphasis parser (bare *
without matching close), failing the macOS appbundle docs build.
2026-07-25 01:02:36 +05:30
Kundan Sable f75452bfd0 test(security): behavioral regression test for name-literal SQL escaping
Add test_name_literal_sql_escaping.py covering the templates fixed in the
CVE-2026-12044 follow-up: index Statistics (coll_stats.sql, both
dialects), publications (pg + ppas), and subscriptions dependency/
get-position lookups.

Each scenario renders the real template with a stacked-statement
apostrophe payload and asserts (1) the object name appears exactly as
qtLiteral escapes it and (2) the rendered SQL parses as exactly one
statement -- the property that actually prevents statement smuggling.
Verified the semantic assertion fails on the pre-patch raw-interpolation
form (parses as 2 statements) and passes on the fixed form, so the test
genuinely guards the fix rather than trivially passing.

Complements test_stats_template_regclass_cast.py (single-index
pgstatindex path) and the lint guard in test_sql_string_literal_lint.py.
Pure template-render test, no DB required.

Also independently verified during review: the fix's escaping neutralises
a live stacked-statement injection on PostgreSQL 16 (pre-patch slept 5s,
post-patch 0.0s) for both index-stats and pub/sub paths, and confirmed
the browser tree label and Statistics grid render object names as
React-escaped JSX text, so the HTML/XSS probe in an object name does not
execute in those paths. EPAS/ppas runtime path not exercised (ppas
publication templates are byte-identical to the pg variants and are
covered at render level by this test).
2026-07-25 00:55:46 +05:30
Kundan Sable 73b3218992 fix(security): escape index-stats/publication/subscription name sinks (CVE-2026-12044 follow-up)
The 9.16 fix for CVE-2026-12044 hardened qtLiteral and switched sixteen
COMMENT ON / pgstattuple / pgstatindex templates to it, but the fix
missed several sinks that were previously excused in
test_sql_string_literal_lint.py's ALLOWLIST on the (incorrect)
assumption that schema/table/publication/subscription names sourced
from pg_catalog via the browser tree could never contain apostrophes.
They can, since PostgreSQL permits arbitrary characters in quoted
identifiers, so a low-privileged user able to CREATE TABLE, CREATE
PUBLICATION, or CREATE SUBSCRIPTION can plant a name that breaks out of
the unescaped '{{ name }}' interpolation once any user views that
object's Statistics or Dependencies tab.

Switch the following templates to qtLiteral(conn):
- schemas/tables/templates/indexes/sql/{16_plus,default}/coll_stats.sql
  (Index Statistics -- reported as "SQL injection in pgAdmin index
  Statistics (incomplete fix for CVE-2026-12044)")
- publications/templates/publications/{pg,ppas}/default/sql/
  {dependencies,get_position}.sql
- subscriptions/templates/subscriptions/sql/default/
  {dependencies,get_position}.sql

publications/__init__.py and subscriptions/__init__.py now pass
conn=self.conn into the dependencies.sql render_template call so the
qtLiteral filter has a connection to quote against. Remove the
corresponding ALLOWLIST entries in test_sql_string_literal_lint.py now
that these sinks are properly escaped instead of merely assumed safe.

Patch supplied pre-written; manual testing by the reporter reported
both the SQL-injection and HTML-injection probes as positive, but the
exact pre/post-fix state tested was not fully disambiguated in this
session -- treat as reported-but-not-independently-verified pending
follow-up confirmation.
2026-07-25 00:55:46 +05:30
Dave Page 24fdcf0f58 fix(security): add missing auth decorators to Constraints/preferences/debugger/schema_diff routes (CVE-2026-12046 follow-up)
In SERVER mode pgAdmin enforces authentication per route via
@pga_login_required; the before_request hook only handles desktop
auto-login and the Kerberos/Webserver redirect, so a route shipped
without the decorator is reachable unauthenticated (CWE-306). This is
the same defect class as CVE-2026-12046 (the sqleditor close/
update_connection routes).

A sweep found further omissions, now fixed with @pga_login_required:

* Constraints blueprint: nodes, proplist (obj), delete -- the routes
  named in the "SQL injection in pgAdmin index Statistics" follow-up
  report as an incomplete fix for CVE-2026-12046. delete is a
  state-mutating DELETE (removes table constraints); nodes/proplist
  return object information. Adds the missing pga_login_required import
  to this module.
* preferences.get_all_cli (GET)
* debugger.close (DELETE)
* schema_diff.close (DELETE)
2026-07-25 00:55:46 +05:30
Ashesh Vashi e7a8576731 Fix MASTER_PASSWORD_HOOK tokenisation on Windows
posix=False in shlex.split() never strips quote characters, so any
Windows hook path quoted to handle spaces (or any quoted argument)
came out with literal quotes still attached, and unquoted spaced
paths were split into multiple argv elements either way.

Use a shlex.shlex instance with posix=True (correct quote-stripping)
and escape='' (so backslashes in Windows paths are not treated as
escape characters). Document the quoting requirement in config.py
and add regression tests for quoted/unquoted spaced paths.
2026-07-25 00:55:46 +05:30
Dave Page ea7e798aac Fix OS command injection in the MASTER_PASSWORD_HOOK feature
The MASTER_PASSWORD_HOOK setting lets administrators specify an external
command that returns a per-user encryption key, with %u in the configured
string replaced by the current user's name. The previous implementation
substituted the username into the command string and executed the result
with subprocess.Popen(..., shell=True). Because the username can originate
from an external authentication source (OAuth/OIDC claims, Kerberos,
webserver auth), a username containing shell metacharacters allowed an
authenticated user to execute arbitrary commands as the pgAdmin service
account in deployments where the hook uses %u.

Tokenise the trusted hook string into an argument vector first, substitute
the untrusted username into the individual arguments, and execute with
shell=False. The username is therefore always confined to a single argv
element and any shell metacharacters it contains are inert.

Note for administrators: hooks that previously relied on shell features
(pipes, redirection, environment-variable expansion, globbing) in the
MASTER_PASSWORD_HOOK string itself will no longer have those interpreted;
such logic should be moved into the hook script. The documented form,
'<PATH>/script.sh %u', is unaffected.

Adds regression tests covering usernames containing ';', '$()', backticks,
pipes, '&&' and newlines, plus an end-to-end marker-file proof that no
shell execution occurs.

Reported-by: B1gN0Se
2026-07-25 00:55:46 +05:30
Ashesh Vashi 64a9cdbd6a fix: close remaining tool-RBAC gaps found in review
Extends the tool-permission enforcement pattern from the reported CVE
fix to the backup/restore/maintenance/import_export blueprints, which
had the identical front-door-only gap: only create_*_job was gated,
while objects()/check_utility_exists()/get_import_export_settings()
relied on @pga_login_required alone.

Also clears password/save_password/tunnel_password on adhoc server
clones, not just ownership fields - Server.clone() copies every
column, so a non-owner cloning another user's shared server was still
persisting that user's stored credentials into a row the non-owner
now owns.

Adds missing socket test scenarios (erd tables, psql socket_input/
socket_set_role/resize) that the existing test docstring claimed to
cover but didn't.
2026-07-25 00:55:46 +05:30
Dave Page ba1984718a Add RBAC regression tests for tool routes, sockets and adhoc ownership
Add regression coverage for the authorisation fixes:

  * test_tool_permissions_required: a consolidated, per-blueprint check
    that logs in as a user with no roles (hence no tool permissions) and
    asserts every gated backend HTTP route across the query tool, grant
    wizard, schema diff, ERD, PSQL and debugger returns 403. This catches
    any future route added to these blueprints without the decorator.
  * test_tool_socket_permissions_required: asserts the schema diff
    compare_database/compare_schema and psql start_process Socket.IO
    handlers refuse a user lacking the tool permission.
  * test_adhoc_connect_server_ownership: asserts that an adhoc connect
    triggered by a non-owner against an administrator-owned shared server
    persists a server row owned by the caller and not shared.

All three are skipped in DESKTOP mode, where every request is
auto-authenticated as the all-permissions DESKTOP_USER.
2026-07-25 00:55:45 +05:30
Dave Page a7e74a6ed6 Re-home adhoc cloned servers to the calling user
When /misc/workspace/adhoc_connect_server is given a sid, it clones the
existing server. Server.clone() copies every column of the source row,
including user_id, shared and shared_username. When a non-owner triggered
an adhoc connect against an administrator-owned shared server, the clone
inherited the administrator's ownership and shared flag, so pgAdmin
persisted a new, administrator-owned, shared adhoc server row created at
the behest of another user; the row is committed before the connection is
attempted, so it survived even when the connection failed.

Force the cloned adhoc record to belong to the current user and to be
private (user_id, shared, shared_username) before committing, mirroring
the new-server branch, so a non-owner can no longer persist a
cross-tenant server record.
2026-07-25 00:55:45 +05:30
Dave Page d36bd8dc96 Enforce tool RBAC on every backend route, not just the entry point
The tool permissions (tools_query_tool, tools_grant_wizard,
tools_schema_diff, tools_erd_tool, tools_psql_tool, tools_debugger) were
enforced only on a single "front door" route per tool, whilst the rest of
each tool's backend workflow relied on pga_login_required alone. An
authenticated user who had been denied a tool could therefore still drive
the tool through its other routes and Socket.IO handlers:

  * Query Tool: View/Edit Data via sqleditor.initialize_viewdata and the
    rest of the view-data chain, bypassing the gate on initialize_sqleditor.
  * Grant Wizard: object discovery (objects), SQL preview (modified_sql)
    and the actual privilege change (apply) were ungated; only acl_list
    was protected. This allowed real GRANTs to be generated and applied.
  * Schema Diff: initialize, servers, get_server, connect_server,
    connect_database, databases, schemas, ddl_compare and the
    compare_database/compare_schema socket handlers were ungated; only
    panel was protected.
  * ERD: initialize, prequisite, sql (table DDL generation) and the
    tables socket handler were ungated; only panel was protected.
  * PSQL: the panel route and the entire /pty Socket.IO namespace
    (start_process, socket_input, socket_set_role, resize) had no tool
    permission check at all, so a denied user could still obtain an
    interactive psql session.
  * Debugger: the directly addressable get_arguments/set_arguments/
    clear_arguments routes were ungated.

Apply permissions_required to the HTTP routes and socket_permissions_required
to the Socket.IO handlers so the tool permission is enforced consistently
across each tool's surface. The permission check is the outermost
decorator, so it runs before any connection or transaction lookup.
2026-07-25 00:55:45 +05:30
Dave Page 461c3afba9 Add socket_permissions_required decorator for Socket.IO RBAC
Flask-Security's permissions_required only guards HTTP routes; pgAdmin's
Socket.IO event handlers had no permission-aware equivalent and relied on
socket_login_required, which checks authentication but not the tool
permission. Add socket_permissions_required as the socket counterpart of
permissions_required so that event handlers can enforce the same
tool-level RBAC as the routes.

It reads the user's permissions via has_permission() rather than
flask_principal's Permission().can(), so it does not depend on the
principal identity having been loaded onto the socket request context,
and it honours pgAdmin's Administrator bypass. On failure it disconnects
and raises ConnectionRefusedError, mirroring socket_login_required.
2026-07-25 00:55:45 +05:30
Ashesh Vashi 54ff7b4dce Updated message catalogs for v9.17 2026-07-25 00:54:55 +05:30
Ashesh Vashi 5d39a52a73 docs: add release notes for v9.17 2026-07-25 00:21:27 +05:30
Niv Greenstein 108797d48a feat: add custom tile provider settings for Geometry Viewer (#10142)
Geometry Viewer's base-layer choices were hardcoded (Empty, Street,
Topography, Gray Style, Light Color/Dark Matter), with no way to
point at a private/internal tile server or a provider not in that
built-in list.

Add a geometry_viewer preferences category (custom_tile_url, _name,
_crs, _attribution, _max_zoom) under sqleditor. When a valid custom
tile provider URL is configured (must be http(s) and contain {x},
{y}, {z}), it's used as the default base layer; built-in tiled
layers are hidden unless the custom provider's CRS is also
EPSG:3857 (Web Mercator), since they can't be reprojected together.
Non-mercator custom providers (e.g. EPSG:4326) only apply to SRID
4326 geometry data; other SRIDs still render on the existing blank
Cartesian plane.

Custom name/attribution are DOMPurify-sanitized before being passed
to Leaflet (both are innerHTML sinks). URL is not sanitized but is
gated by the http(s):// prefix check, so javascript:/data: URLs are
rejected; it's consumed as a tile-image src template, not an HTML
sink.

Adds GeometryViewerUtils.spec.js covering URL validation, defaults,
sanitization, CRS resolution/fallback, and base-layer composition.
2026-07-24 22:17:51 +05:30
Ashesh Vashi 7339eb8dcc fix: report the correct error message on trigger node.sql failure (#10197)
The failure path for the second query (node.sql, resolving the
trigger function's node data) reported errormsg=rset — the result
dict from the first, already-successful get_function_oid.sql query
— instead of res, the actual error from the failed query. Any
failure here (permissions, catalog access) surfaced a stale,
misleading payload instead of the real cause.
2026-07-24 21:59:06 +05:30
Ashesh Vashi 34108aa50f fix: remove unused sqlalchemy exists import in server_access.py (#10196)
'exists' was imported but never called; the .exists() at line 165 is
the query-builder method on a Query instance, not this function.
2026-07-24 21:58:00 +05:30
lkmatsumura 1e1240038a refactor(server_groups): centralize shared-group visibility and access logic (#10077)
Replace the per-server ServerGroupModule.has_shared_server() loop and
scattered ad hoc shared-server checks with two centralized helpers,
get_server_groups_for_user() and get_server_groups_for_user_query(),
in server_access.py — a single SQL EXISTS query instead of iterating
servers per group. get_server_group() reuses the same shared-access
logic, and returned groups now carry is_shared_group metadata so the
browser tree can render the correct icon and derive edit/delete
permissions consistently.

update() now resolves the target group via get_server_group(gid,
hide_shared=True), so a non-owner can no longer rename another
user's shared group; delete() already blocked deleting shared or
first-user groups and now reports clearer errors for both cases.
Desktop mode's non-SERVER_MODE query path uses
literal(0).label('is_shared_group') (a bare int has no .label()).

Adds test_sg_permissions.py covering owned/shared/desktop group
visibility and access.
2026-07-24 19:11:27 +05:30
Hari Prasad 966a8ae99a docs: add basic starter contributing guide (#10093)
* docs: add basic starter contributing guide

Add a CONTRIBUTING.md file to document the contribution workflow for pgAdmin 4.

Includes:
- Getting started instructions
- Issue reporting guidelines
- Development workflow and coding practices
- Pull request requirements
- Development environment setup
- Database migration guidance
- Security reporting process
- Community support resources

This provides a single reference for new and existing contributors.

* add language tag on the opening fence of the Windows command block

* Add architecture overview, define $PGADMIN4_SRC, and link GitHub URLs

* docs: update contribution guidance

Clarify contributor workflow, fix the runtime link, and add references to existing contributor docs.
2026-07-24 19:05:11 +05:30
Dave Page 75dd304d25 fix(cloud): stop googleapiclient pulling in the system oauth2client (#10110) (#10112)
On packaged Linux installs the bundled venv is created with
--system-site-packages (issue #7173) so it can reach system packages such
as dbus-python. That also exposes the deprecated system oauth2client and
whatever pyOpenSSL ships alongside it. googleapiclient imports oauth2client
optionally, and on Ubuntu 24.04 that drags in a pyOpenSSL too old for our
bundled cryptography, aborting startup at blueprint registration with
"AttributeError: module 'lib' has no attribute 'GEN_EMAIL'".

pgAdmin only ever authenticates to Google via google-auth and
google-auth-oauthlib, so park a None sentinel under
sys.modules['oauth2client'] before importing googleapiclient in both the
web module and the standalone pgacloud provider. The optional import then
fails cleanly and googleapiclient falls back to google-auth.

Closes #10110
2026-07-24 19:02:29 +05:30
n0099 67755809a6 zh_Hans_CN translate: truncate 截断 -> 清空 (#10138)
this also sync with phpmyadmin: https://github.com/phpmyadmin/phpmyadmin/blob/c605fd63bc2e48b682e06d6d1041de544ddf861c/resources/po/zh_CN.po#L12154
2026-07-24 19:01:55 +05:30
Kobi Hikri e65edd4669 Pin sonarqube-scan-action to a full commit SHA (v8.2.1) (#10154) 2026-07-24 19:01:03 +05:30
Kundan 9bf83b4489 fix: return Response as-is and pass conn when expanding a trigger's function (#10177)
Expanding a Trigger node under a Table to view its function threw
'Response' object is not iterable, crashing the tree.

get_children_nodes() can legitimately return a Flask Response instead
of a list (e.g. gone("Could not find the specified trigger
function") when the trigger's function has no matching row in the
trigger-function node query, as happens for a genuine internal-
language function such as suppress_redundant_updates_trigger).
NodeView.children() / PGChildNodeView.children() unconditionally
sorted whatever get_children_nodes() returned, crashing on a
Response instead of passing it through.

Separately, TriggerView.get_children_nodes() rendered the trigger-
function node.sql template (which quotes fnid via qtLiteral(conn))
without passing conn into the template context. This call site
predates qtLiteral requiring a connection; after qtLiteral was
hardened in 658bb585d to raise ValueError instead of silently
degrading when conn is missing, this broke node.sql rendering for
every trigger, not just the internal-language case.

Fix both: check isinstance(children, flask.Response) in children()
before sorting/iterating and return it as-is, and pass
conn=self.conn into the node.sql render call, matching every other
render_template call site in the file.

Fixes #10117
2026-07-24 19:00:31 +05:30
Regina Obe 58e5fc7aed Fix for #10187 (#10188)
Extension UI gives error with PG 19
column reference "comment" is ambiguous LINE 5: e.comment

Issue caused because in pg19 :

SELECT * FROM pg_catalog.pg_available_extensions();

now outputs an additional column `location`
after default_version and comment is now 4th column
2026-07-24 18:38:40 +05:30
Ashesh Vashi 577a518fcb fix: scan all Mach-O binaries for bundle linkage, not just .so/.dylib (#10189)
_verify_bundle_linkage's host-library scan only matched files by
.so/.dylib name suffix, so a shipped executable (Contents/MacOS/*)
or a Python.framework payload with a host-linked dependency but no
matching suffix could slip through unchecked — the same class of
bug #10135 was added to catch.

Detect Mach-O executables/libraries by content via `file` instead
(same pattern the codesign step in this file already uses),
regardless of extension or executable permission bits, so readable
non-executable dylibs are covered too.
2026-07-24 18:34:03 +05:30
Dave Page 1884356245 fix: fail the macOS build if any bundled library links outside the bundle (#10135)
9.16 failed to start on Intel Macs (#10123): cryptography had no
prebuilt Intel macOS wheel, pip compiled it from source, and its
openssl-sys build linked the builder's Homebrew OpenSSL into
_rust.abi3.so instead of the bundled one. _fixup_imports deliberately
skips _rust.abi3.so, so the dangling reference shipped unnoticed and
the app died on startup for anyone without that external dylib.

Add _verify_bundle_linkage, run after the bundle is assembled and
relocated but before code-signing: walks every .so/.dylib and fails
the build if any install-name points at a build-host prefix
(/usr/local, /opt/homebrew, /opt/local, $SLAVE_HOME). OS libraries
and @loader_path/@rpath/@executable_path references pass untouched.
Turns this class of bug into a build-time failure instead of a
runtime one.
2026-07-24 18:22:08 +05:30
Kundan b85fb6c650 fix: use pg_depend ownership instead of name guessing for SERIAL detection (#10167)
Column-owns-sequence was detected by guessing the sequence name as
<table>_<col>_seq and string-matching it in nextval(...). Renaming
a table/column broke the guess (false negative, #10100); an
unrelated sequence with the same guessed name broke it the other
way (false positive, #10101).

Check col.get('seqrelid') (real pg_depend ownership) instead, and
exclude identity columns. SERIAL is now emitted iff a genuine
ownership dependency exists, independent of naming.

Fixes #10100, #10101
2026-07-24 18:15:20 +05:30
Kundan 5ff4f694fd fix: Schema Diff filter chip toggle broadcasting stale empty selection (#10172)
selectFilterOption() computed newOptions inside the functional
setSelectedFilters(prev => {...}) updater, then used that same
variable to dispatch TRIGGER_CHANGE_FILTER synchronously. Since the
updater runs lazily on next render, every chip toggle broadcast an
empty filter, so the tree always showed "No difference found" until
a full re-Compare.

Compute the new selection synchronously from current state and use
it for both the state update and the dispatched event.

Fixes #10102
2026-07-24 18:14:22 +05:30
hiteshjambhale f9935b3da5 fix: honor selected EOL sequence when copying query text to clipboard (#10160)
getSelectionFromState() joined ranges with the chosen EOL but sliced
each range via state.sliceDoc(), which always uses the document's
own \n regardless of the LF/CRLF preference. Introduced in #8691
when the copy path switched from doc.sliceString(..., lineSep) to
sliceDoc(...) to fix multi-cursor copying.

Slice with state.doc.sliceString(range.from, range.to, lineSep) so
the chosen EOL applies within a range too; the #8691 multi-cursor
join behavior is unchanged.

Fixes #10158
2026-07-24 18:13:27 +05:30
Dave Page cec1b38b2f fix: warn when OAuth2 provider settings are misplaced at the top level of config (#10113)
OAuth2 provider settings are only read from the OAUTH2_CONFIG list;
bare top-level keys like OAUTH2_CLIENT_ID or
OAUTH2_SSL_CERT_VERIFICATION are silently ignored. This is an easy
trap in container deployments where every other option is set via
an individual PGADMIN_CONFIG_<KEY> env var.

Add warn_on_misplaced_oauth2_config(), called once at OAuth2 module
startup, that logs when per-provider OAUTH2_* keys exist at the top
level with no provider configured in OAUTH2_CONFIG. No behavior
change for correctly configured or unconfigured deployments.
Documents the supported container approach in oauth2.rst.

Closes #10053
2026-07-24 18:12:22 +05:30
Kundan 6f8be955a1 fix: render disconnect tree label via className, not embedded HTML (#10169)
Disconnect handlers in server.js/database.js built the transitional
tree-node label as an HTML string and passed it to setLabel(), which
writes via label.textContent — correctly escaping markup, so the
literal "<span class='text-muted'>[Disconnecting...]</span>" text
was shown instead of a greyed-out label.

Pass plain text plus a className: 'text-muted' instead; setLabel()
applies the className to the label span (default file-name class
otherwise), so muted styling comes from CSS, not injected markup.

Fixes #10106
2026-07-24 18:11:22 +05:30
Kundan 49e955a855 feat: add preference for default View Data row limit (#10170)
The unqualified "View Data" action always ran with no LIMIT, unlike
"First/Last 100 Rows" which hardcode limit=100 — unusable on large
tables with no way to cap it.

Add view_data_default_row_limit preference (int, default 0 =
unlimited) under sqleditor Options. GridCommand.__init__ applies it
as self.limit when cmd_type == VIEW_ALL_ROWS and the value is
positive; objectquery.sql already renders LIMIT only when > 0, so
no template changes needed. First/Last 100 Rows untouched.

Closes #10104
2026-07-24 18:10:13 +05:30
Kundan 6ad6024bca fix: detect a selected-but-unusable OS keyring and fall back gracefully (#10168)
evaluate_and_patch_config() only disabled USE_OS_SECRET_STORAGE when
keyring.get_keyring().name == 'fail Keyring'. On Debian 13 over RDP, a
real backend (e.g. SecretService) is selected because no D-Bus/GNOME
Keyring session is running, so the name check passes even though every
actual keyring call fails — leaving the crypt key unset and every
operation raising a bare CryptKeyMissing.

Probe the selected backend with a harmless read of a never-existing
entry after selection. A healthy backend returns None without
prompting; an unusable one raises, and we disable
USE_OS_SECRET_STORAGE so the app falls back to the master-password /
in-app crypt key mechanism instead of failing outright.

Fixes #10107
2026-07-24 18:08:27 +05:30
Kundan aeef9c2d80 fix: ALT+F5 not falling back to statement under cursor (#10173)
triggerExecution() only fell back to getQueryAt(cursor) when
getSelection() was falsy. Since #7293/#8691 (non-continuous
highlighted-block support), getSelection() flattens and joins all
ranges, so multiple empty cursor ranges can produce a truthy but
whitespace-only string (e.g. "\n"), skipping the fallback entirely —
ALT+F5 silently tried to execute whitespace. Same stale check existed
in checkUnderlineQueryCursorWarning(). Also fixes a latent
state.selection.head reference (EditorSelection has no such
property; correct field is state.selection.main.head).

Treat a whitespace-only selection as empty before the fallback check
in both functions. Real highlighted selections always contain
non-whitespace, so #7293/#9570 behavior is unaffected.

Fixes #10109
2026-07-24 18:07:26 +05:30
Ashesh Vashi 8812025a8a chore(deps): Bump patch/minor dependency bumps (#10176)
Consolidates 20 open dependabot PRs (JS + Python) into one bump, applied
directly rather than cherry-picked (PR branches were stale and would have
reverted unrelated fixes like the yarn packageManager pin). Adds further
same-major patch/minor bumps found by auditing beyond dependabot's own PR
list.

JS (web + runtime): axios, brace-expansion, form-data, undici, js-yaml,
dompurify, @babel/core, webpack, sharp, electron, eslint, react-checkbox-tree,
autoprefixer, eslint-plugin-jest, globals, jest, jest-environment-jsdom, svgo,
terser-webpack-plugin, typescript-eslint, webpack-bundle-analyzer,
@date-io/date-fns, @szhsin/react-menu, @tanstack/react-query, @types/react,
ajv, anti-trojan-source, ip-address, marked, moment-timezone, papaparse,
postcss, react, react-dom, react-draggable, react-timer-hook, sql-formatter,
zustand.

Python: certifi, selenium (version-gated: 4.45.0 requires Python >=3.10,
4.44.0 kept for <=3.9 to preserve Python 3.9 support).

Reverted / excluded, with reasons:
- azure-mgmt-resource 26.0.0: moved ResourceManagementClient from
  azure.mgmt.resource to azure.mgmt.resource.resources, breaking
  pgadmin/misc/cloud/azure/__init__.py at import time. Caught by the Python
  regression suite. Kept at 25.0.0.
- @simonwep/pickr 1.10.0: switched its build tool to tsup, which marks its
  UMD bundle as an ES module via Symbol.toStringTag instead of the
  __esModule flag Babel's interop helper checks for. Babel double-wraps the
  export, so `new Pickr(...)` resolves to a non-constructor and crashes
  every dialog that mounts a color picker -- reproduces only in the
  production/minified webpack build, not the dev bundle or Jest. Pinned to
  ~1.9.1 (tilde, not caret) so a future install can't silently float back to
  1.10.x.
- paramiko 3->5 (#9927): structurally blocked by sshtunnel 0.4.0 still
  referencing paramiko.DSSKey.
- @mui/material / @mui/x-date-pickers 7/8->9 (#10091, #10092): known
  UI-breaking, needs the accompanying component fixes tracked on a separate
  branch, not a bare version bump.
- react-arborist, @tanstack/react-virtual, react-frame-component: same-major
  bumps available but excluded -- core object-browser tree, already-fragile
  virtualization code, or tilde-pinned range respectively.
- A handful of JS packages hit Yarn 4.15's registry quarantine gate (blocks
  just-published versions); backed off to the next-older version instead of
  forcing through.

Added core-js as an explicit devDependency: it was never declared despite
webpack's Babel config (useBuiltIns: 'usage', corejs: 3) requiring it --
it only worked because @simonwep/pickr 1.9.1 happened to pull it in
transitively, which broke when pickr was briefly bumped.

Verified: eslint (web + runtime), full JS test suite (149/149 suites,
916/916 tests), webpack production build compiles clean, Python regression
suite (2388/2388, excl. Selenium), Selenium feature_tests (17/19 pass; the
2 failures trace to a local pldbgapi-extension gap, unrelated to any
bumped dependency).
2026-07-23 15:28:38 +05:30
Ashesh Vashi 1993109475 fix: pin Yarn version from packageManager field in remaining build scripts (#10175)
#10156 fixed CI build failures caused by Yarn 4.x fetching a newer patch
with different builtin compat hashes, breaking --immutable lockfile
validation, but only patched pkg/linux/build-functions.sh and the GHA
workflows. Make.bat, pkg/mac/build-functions.sh, pkg/pip/build.sh, and
Dockerfile still hardcoded 'yarn set version 4', which is what broke the
Windows Jenkins snapshot build (job 1669).
2026-07-23 13:06:29 +05:30
Ashesh Vashi b15c745dc1 fix: pin Yarn version from packageManager field in build scripts (#10156)
* fix: pin Yarn version from packageManager field in build scripts

Replace hardcoded 'yarn set version 4' in build-functions.sh with a
dynamic lookup from each workspace's package.json packageManager field.
Also syncs runtime yarn version to 4.15.0 to match web/package.json.

Fixes CI build failures caused by Yarn 4.x fetching a newer patch that
produces different builtin compat hashes, breaking --immutable lockfile
validation.
2026-07-16 11:48:36 +05:30
Luiz K Matsumura dfc4ef37d0 refactor: clarify ServerGroup relationships to Server and SharedServer (#10076)
The Server and SharedServer models each declared a relationship named
'servers' that actually pointed at the parent ServerGroup, with backrefs
('server'/'sharedserver') providing the collections. The naming was the
inverse of what it modelled and made the call sites read oddly.

Redefine the relationships in the natural direction using explicit
back_populates: ServerGroup gains 'servers' and 'sharedservers'
collections (carrying the existing delete-orphan cascade), and Server and
SharedServer each gain a 'servergroup' reference. The two call sites that
read the group name are updated from server.servers.name to
server.servergroup.name accordingly.

This is a naming/modelling cleanup with no functional or schema change, so
no migration is required.
2026-06-19 10:32:02 +01:00
somaz 7e76fb4eb0 Add Gateway API HTTPRoute support to the Helm chart (#10095)
Add an opt-in Gateway API HTTPRoute template to the Helm chart as an
alternative to the existing Ingress, addressing #9942. It is disabled by
default (httpRoute.enabled: false) so existing installs are unaffected.

The template mirrors the existing ingress.yaml conventions: the
pgadmin4.fullname backend, commonLabels/commonAnnotations propagation, and
a hostname that falls back to ingress.hostname when httpRoute.hostnames is
unset. parentRefs is required (a fail guard fires when it is empty),
apiVersion defaults to gateway.networking.k8s.io/v1, and the default rule
forwards "/" (PathPrefix) to the pgAdmin service. Custom hostnames and
rules can be supplied for full control. The new values are documented in
the chart README.
2026-06-19 09:41:54 +01:00
Jeremy Schneider 0c6740f146 feat: include authenticated user identity in HTTP access log (#9991)
Set an X-Remote-User response header containing the authenticated username
on every request when the LOG_AUTHENTICATED_USER config option is enabled
(disabled by default). This allows the HTTP access log to be configured to
include user identity via standard log format directives
(%({x-remote-user}o)s in gunicorn, %{X-Remote-User}o in Apache) without
requiring any changes to pgAdmin's session or authentication behaviour. The
default gunicorn access log format is updated to surface the header.

The username is sanitised to a header-safe value: it is transliterated to
Latin-1 (HTTP headers are Latin-1 only) and any non-printable characters,
including CR/LF, are stripped, so unusual usernames cannot cause a 500 on
every response.
2026-06-19 09:26:32 +01:00
284 changed files with 28327 additions and 14773 deletions

No files matched your search

+82
View File
@@ -1,4 +1,11 @@
version: 2
# A note on the "groups" blocks below: minor and patch updates are batched into
# a single PR per manifest per week, whilst major updates continue to arrive
# individually. Majors are where the breakage lives and each one wants its own
# review, whereas a dozen separate PRs for patch bumps is pure overhead.
# Grouping applies to version updates only, so Dependabot security updates are
# unaffected and still arrive as their own PRs.
updates:
- package-ecosystem: "docker"
directory: "/"
@@ -17,6 +24,20 @@ updates:
- "Dependencies"
commit-message:
prefix: "Python dependency"
groups:
python-minor-and-patch:
patterns:
- "*"
update-types:
- "minor"
- "patch"
# paramiko 5.0 removed DSSKey, which sshtunnel 0.4.0 still references in
# SSHTunnelForwarder.get_keys(); a major bump therefore breaks every SSH
# tunnelled connection at construction time. sshtunnel has had no release
# since 2019, so this stays put until it is fixed or replaced. See #9927.
ignore:
- dependency-name: "paramiko"
update-types: ["version-update:semver-major"]
- package-ecosystem: "pip"
directory: "/tools"
@@ -26,7 +47,17 @@ updates:
- "Dependencies"
commit-message:
prefix: "Python dependency"
groups:
tools-python-minor-and-patch:
patterns:
- "*"
update-types:
- "minor"
- "patch"
# Note that web/regression/requirements.txt begins with
# "-r ../../requirements.txt", so this entry also sees everything pinned in
# the root file and the paramiko exclusion has to be repeated here.
- package-ecosystem: "pip"
directory: "/web/regression"
schedule:
@@ -35,6 +66,16 @@ updates:
- "Dependencies"
commit-message:
prefix: "Python dependency"
groups:
regression-python-minor-and-patch:
patterns:
- "*"
update-types:
- "minor"
- "patch"
ignore:
- dependency-name: "paramiko"
update-types: ["version-update:semver-major"]
- package-ecosystem: "npm"
directory: "/runtime"
@@ -44,6 +85,13 @@ updates:
- "Dependencies"
commit-message:
prefix: "Javascript dependency"
groups:
runtime-javascript-minor-and-patch:
patterns:
- "*"
update-types:
- "minor"
- "patch"
- package-ecosystem: "npm"
directory: "/web"
@@ -53,3 +101,37 @@ updates:
- "Dependencies"
commit-message:
prefix: "Javascript dependency"
groups:
web-javascript-minor-and-patch:
patterns:
- "*"
update-types:
- "minor"
- "patch"
ignore:
# @testing-library/jest-dom 7 requires Node >= 22, whilst Node 20 is the
# minimum we support and what most of the buildfarm runs. Revisit once
# the buildfarm moves to Node 22. See #10271 and #10210.
- dependency-name: "@testing-library/jest-dom"
update-types: ["version-update:semver-major"]
# Babel 8 requires Node ^22.18.0 || >=24.11.0, and every 8.x package
# peer-depends on @babel/core ^8.0.0 whilst we are on 7.x. Taking any of
# them individually leaves an unsatisfiable peer requirement and a mixed
# 7/8 tree, so Babel 8 needs a coordinated migration across all ten
# @babel/* packages once we are on Node 22. See #10283, #10284, #10285.
- dependency-name: "@babel/*"
update-types: ["version-update:semver-major"]
# pickr 1.10 switched to a dual CJS/ESM build whose CommonJS entry point
# exports a namespace object rather than the class, so Babel's interop
# leaves _pickr.default as an object and every dialog with a colour field
# dies on mount. That took master's feature tests down for three days
# (#10264, reverted in #10278). Note 1.9 -> 1.10 is a *minor* bump, so it
# otherwise rejoins the grouped PR and comes back around; minor updates
# are excluded here as well as majors until withColorPicker.js is adapted
# to the new export shape.
- dependency-name: "@simonwep/pickr"
update-types:
- "version-update:semver-major"
- "version-update:semver-minor"
+5 -4
View File
@@ -19,10 +19,11 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Upgrade yarn
run: |
yarn set version berry
yarn set version 4
# Corepack reads packageManager from web/package.json and provisions
# exactly that Yarn: a single pinned download, with nothing written
# into the checkout.
- name: Enable Corepack
run: corepack enable
- name: Install Node modules
run: |
+5 -4
View File
@@ -141,10 +141,11 @@ jobs:
}
EOF
- name: Upgrade yarn
run: |
yarn set version berry
yarn set version 4
# Corepack reads packageManager from web/package.json and provisions
# exactly that Yarn: a single pinned download, with nothing written
# into the checkout.
- name: Enable Corepack
run: corepack enable
- name: Build the JS bundle
run: |
+5 -4
View File
@@ -151,10 +151,11 @@ jobs:
}
EOF
- name: Upgrade yarn
run: |
yarn set version berry
yarn set version 4
# Corepack reads packageManager from web/package.json and provisions
# exactly that Yarn: a single pinned download, with nothing written
# into the checkout.
- name: Enable Corepack
run: corepack enable
- name: Build the JS bundle
run: |
+5 -4
View File
@@ -28,10 +28,11 @@ jobs:
- uses: actions/checkout@v4
- name: Upgrade yarn
run: |
yarn set version berry
yarn set version 4
# Corepack reads packageManager from web/package.json and provisions
# exactly that Yarn: a single pinned download, with nothing written
# into the checkout.
- name: Enable Corepack
run: corepack enable
- name: Install Node modules
run: |
+2 -2
View File
@@ -43,7 +43,7 @@ jobs:
sonar.test.inclusions=**/tests/**, web/regression
# Python compatibility
sonar.python.version=3.7, 3.8, 3.9, 3.10, 3.11
sonar.python.version=3.9, 3.10, 3.11, 3.12, 3.13, 3.14
EOF
APP_RELEASE=`grep "^APP_RELEASE" web/config.py | cut -d"=" -f2 | sed 's/ //g'`
@@ -52,7 +52,7 @@ jobs:
sed -i "s/%VERSION%/${APP_LONG_VERSION}/g" sonar-project.properties
- name: SonarQube Scan
uses: sonarsource/sonarqube-scan-action@master
uses: sonarsource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8.2.1
env:
SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONARQUBE_HOST }}
+341
View File
@@ -0,0 +1,341 @@
# Contributing to pgAdmin 4
Thank you for your interest in contributing to pgAdmin 4.
## Contents
- Getting Started
- Reporting Issues
- Making Changes
- Pull Requests
- AI-assisted Contributions
- Development Setup
- Database Migrations
- Security Issues
- Community and Support
- License
## Getting Started
Before starting work, please review the project documentation and familiarize yourself with the architecture and development workflow.
### Architecture
pgAdmin 4 is written as a web application with Python (Flask) on the server side and ReactJS, HTML5 with CSS for the client side processing and UI.
Although developed using web technologies, pgAdmin 4 can be deployed either on a web server using a browser, or standalone on a workstation. The [runtime/](runtime/) subdirectory contains an Electron-based runtime application intended to allow this, which will fork a Python server process and display the UI.
For a fuller introduction to the codebase, see the [code overview](docs/en_US/code_overview.rst).
If you are new to the codebase, consider starting with smaller bug fixes or documentation improvements before working on larger changes.
The Sphinx documentation also includes contributor guidance for [coding standards](docs/en_US/coding_standards.rst), [submitting pull requests](docs/en_US/submitting_pull_requests.rst), [code review](docs/en_US/code_review.rst), and [translations](docs/en_US/translations.rst).
## Reporting Issues
When reporting a bug:
- Search existing issues first.
- If a similar issue exists, add any additional information or context you have. Do not create a duplicate issue.
- Provide clear steps to reproduce the issue.
- Include relevant environment and version information.
- Attach screenshots, logs, or error messages when appropriate.
For feature requests, describe the problem being solved and the expected behavior.
## Making Changes
When working on a contribution:
- Keep changes focused and easy to review.
- Follow existing coding patterns and conventions, including the [coding standards](docs/en_US/coding_standards.rst).
- Avoid unrelated refactoring and changes in the same commit.
- Keep commit messages clear and descriptive.
- Update tests or documentation when necessary.
## Pull Requests
Before opening a pull request:
- Ensure your changes build successfully.
- Test your changes locally.
- Review your code before submission.
- Update your branch with the latest changes from `master` upstream when appropriate.
When opening a pull request:
- Clearly describe the change.
- Explain the reason for the change.
- Include screenshots or recordings for UI-related changes when applicable.
- Submit any changes as Pull Requests against the `master` branch of the [pgadmin-org/pgadmin4](https://github.com/pgadmin-org/pgadmin4) repository.
Use a clear pull request title that describes the change. Short prefixes such as `feat:`, `fix:`, `docs:`, `test:`, `refactor:`, and `chore:` are useful when they match the change being made. Branch names are contributor-local and do not need to follow a project-specific naming convention.
For more detail, see the [pull request submission guide](docs/en_US/submitting_pull_requests.rst) and [code review guide](docs/en_US/code_review.rst).
## AI-assisted Contributions
Contributors are responsible for understanding, testing, and standing behind everything they submit, whether or not AI tools were used. Please do not open large numbers of auto-generated issues or pull requests without first discussing that work with the maintainers.
## Development Setup
In the following documentation and examples, `$PGADMIN4_SRC` is used to denote the top-level directory of your copy of the pgAdmin source tree (either from a tarball or a git checkout).
### Prerequisites
1. Install Node.js 20 and above (https://nodejs.org/en/download)
2. yarn (https://yarnpkg.com/getting-started/install)
3. Python 3.9 and above (https://www.python.org/downloads/)
4. PostgreSQL server (https://www.postgresql.org/download)
Start by enabling Corepack, if it isn't already; this will add the yarn binary to your PATH:
```bash
corepack enable
```
### Building Web Assets
pgAdmin is dependent on a number of third-party JavaScript libraries. These, along with its own JavaScript code, CSS code and images must be compiled into a "bundle" which is transferred to the browser for execution and rendering. This is far more efficient than simply requesting each asset as it is needed by the client.
To create the bundle, you will need the 'yarn' package management tool to be installed. Then, you can run the following commands on a *nix system to download the required packages and build the bundle:
```bash
$ cd $PGADMIN4_SRC
$ make install-node
$ make bundle
```
On Windows systems (where "make" is not available), the following commands can be used:
```bat
C:\> cd $PGADMIN4_SRC\web
C:\$PGADMIN4_SRC\web> yarn install
C:\$PGADMIN4_SRC\web> yarn run bundle
```
### Python Environment
In order to run the Python code, a suitable runtime environment is required. Python version 3.9 and later are currently supported. It is recommended that a Python virtual environment is set up for this purpose, rather than using the system Python environment. On Linux and Mac systems, the process is fairly simple - adapt as required for your distribution:
1. Create a virtual environment in an appropriate directory. The last argument is the name of the environment; that can be changed as desired:
```bash
$ python3 -m venv venv
```
2. Now activate the virtual environment:
```bash
$ source venv/bin/activate
```
On Windows:
```bash
venv\Scripts\activate
```
3. Some of the components used by pgAdmin require a very recent version of *pip*, so update that to the latest:
```bash
(venv) $ pip install --upgrade pip
```
4. Ensure that a PostgreSQL installation's bin/ directory is in the path (so pg_config can be found for building psycopg3), and install the required packages:
```bash
(venv) $ PATH=$PATH:/usr/local/pgsql/bin pip install -r $PGADMIN4_SRC/requirements.txt
```
If you are planning to run the regression tests, you also need to install additional requirements from `web/regression/requirements.txt`:
```bash
(venv) $ pip install -r $PGADMIN4_SRC/web/regression/requirements.txt
```
### Configuration
Create a local configuration file for pgAdmin. Edit `$PGADMIN4_SRC/web/config_local.py` and add any desired configuration options (use the `config.py` file as a reference - any settings duplicated in `config_local.py` will override those in `config.py`). A typical development configuration may look like:
```python
import os
import logging
# Change pgAdmin data directory
DATA_DIR = '/Users/myuser/.pgadmin_dev'
# Change pgAdmin server and port
DEFAULT_SERVER = '127.0.0.1'
DEFAULT_SERVER_PORT = 5051
# Switch between server and desktop mode
SERVER_MODE = True
# Change pgAdmin config DB path in case an external DB is used.
CONFIG_DATABASE_URI="postgresql://postgres:postgres@localhost:5436/pgadmin"
# Set up SMTP
MAIL_SERVER = 'smtp.gmail.com'
MAIL_PORT = 465
MAIL_USE_SSL = True
MAIL_USERNAME = 'user@gmail.com'
MAIL_PASSWORD = 'xxxxxxxxxx'
# Change log level
CONSOLE_LOG_LEVEL = logging.INFO
FILE_LOG_LEVEL = logging.INFO
# Use a different config DB for each server mode.
if SERVER_MODE == False:
SQLITE_PATH = os.path.join(
DATA_DIR,
'pgadmin4-desktop.db'
)
else:
SQLITE_PATH = os.path.join(
DATA_DIR,
'pgadmin4-server.db'
)
```
This configuration allows easy switching between server and desktop modes for testing.
### Running pgAdmin
The initial setup of the configuration database is interactive in server mode, and non-interactive in desktop mode. You can run it either by running:
```bash
(venv) $ python3 $PGADMIN4_SRC/web/setup.py
```
or by starting pgAdmin 4:
```bash
(venv) $ python3 $PGADMIN4_SRC/web/pgAdmin4.py
```
Whilst it is possible to automatically run setup in desktop mode by running the runtime, that will not work in server mode as the runtime doesn't allow command line interaction with the setup program.
At this point you will be able to run pgAdmin 4 from the command line in either server or desktop mode, and access it from a web browser using the URL shown in the terminal once pgAdmin has started up.
Setup of an environment on Windows is somewhat more complicated unfortunately, please see `pkg/win32/README.md` for complete details.
### Building the documentation
In order to build the docs, an additional Python package is required in the virtual environment. This can be installed with the pip package manager:
```bash
$ source venv/bin/activate
(venv) $ pip install Sphinx
(venv) $ pip install sphinxcontrib-youtube
```
The docs can then be built using the Makefile in `$PGADMIN4_SRC`, e.g.
```bash
(venv) $ make docs
```
The output can be found in `$PGADMIN4_SRC/docs/en_US/_build/html/index.html`
### Runtime Development
For Electron runtime development:
1. Navigate to the `runtime` directory.
2. Install dependencies:
```bash
yarn install
```
3. Copy:
```text
dev_config.json.in
```
to:
```text
dev_config.json
```
4. Update the paths to your Python executable and `pgAdmin4.py`.
Start the runtime:
```bash
yarn run start
```
### Building packages
Most packages can be built using the Makefile in `$PGADMIN4_SRC`, provided all the setup and configuration above has been completed.
To build a source tarball:
```bash
(venv) $ make src
```
To build a PIP Wheel, activate either a Python 3 virtual environment, configured with all the required packages, and then run:
```bash
(venv) $ make pip
```
To build the macOS AppBundle, please see `pkg/mac/README.md`.
To build the Windows installer, please see `pkg/win32/README.md`.
## Database Migrations
In order to make changes to the SQLite DB, navigate to the 'web' directory:
```bash
(venv) $ cd $PGADMIN4_SRC/web
```
Create a migration file with the following command:
```bash
(venv) $ FLASK_APP=pgAdmin4.py flask db revision
```
This will create a file in `$PGADMIN4_SRC/web/migrations/versions/`. Add any changes to the `upgrade` function.
When making schema changes, update the `SCHEMA_VERSION` value in:
```text
web/pgadmin/model/__init__.py
```
There is no need to increment the `SETTINGS_SCHEMA_VERSION`.
## Security Issues
**Do not report security vulnerabilities through GitHub issues.**
Potential security issues should be reported to:
`security@pgadmin.org`.
Note that this **address should only be used for reporting security issues** that you believe you've found in the design or code of pgAdmin, pgAgent, and the pgAdmin website. It **should not be used to ask security questions**.
If you are unsure whether an issue is a security vulnerability, please lean on the side of caution and report it to the above address. The pgAdmin team will review the report and respond as appropriate.
## Community and Support
For development discussions, use:
`pgadmin-hackers@postgresql.org`
For support options, visit the [pgAdmin support page](https://www.pgadmin.org/support/).
Use the [GitHub issue tracker](https://github.com/pgadmin-org/pgadmin4/issues) for reporting bugs and requesting features, but please follow the guidelines outlined in the "Reporting Issues" section above.
## License
By contributing to pgAdmin 4, you agree that your contributions will be licensed under the project's existing and future license terms.
-2
View File
@@ -42,8 +42,6 @@ RUN --mount=type=bind,source=.git,target=/pgadmin4/.git \
export CPPFLAGS="-DPNG_ARM_NEON_OPT=0" && \
npm install -g corepack && \
corepack enable && \
yarn set version berry && \
yarn set version 4 && \
yarn install && \
yarn run bundle && \
rm -rf yarn.lock \
+35 -11
View File
@@ -52,7 +52,7 @@ REM Main build sequence Ends
:SET_ENVIRONMENT
ECHO Configuring the environment...
IF "%PGADMIN_PYTHON_DIR%" == "" SET "PGADMIN_PYTHON_DIR=C:\Python313"
IF "%PGADMIN_PYTHON_DIR%" == "" SET "PGADMIN_PYTHON_DIR=C:\Python314"
IF "%PGADMIN_KRB5_DIR%" == "" SET "PGADMIN_KRB5_DIR=C:\Program Files\MIT\Kerberos"
IF "%PGADMIN_POSTGRES_DIR%" == "" SET "PGADMIN_POSTGRES_DIR=C:\Program Files\PostgreSQL\17"
IF "%PGADMIN_INNOTOOL_DIR%" == "" SET "PGADMIN_INNOTOOL_DIR=C:\Program Files (x86)\Inno Setup 6"
@@ -161,10 +161,14 @@ REM Main build sequence Ends
CD "%TMPDIR%"
REM Note that we must use virtualenv.exe here, as the venv module doesn't allow python.exe to relocate.
"%PGADMIN_PYTHON_DIR%\Scripts\virtualenv.exe" venv
"%PGADMIN_PYTHON_DIR%\Scripts\virtualenv.exe" venv || EXIT /B 1
XCOPY /S /I /E /H /Y "%PGADMIN_PYTHON_DIR%\DLLs" "%TMPDIR%\venv\DLLs" > nul || EXIT /B 1
XCOPY /S /I /E /H /Y "%PGADMIN_PYTHON_DIR%\Lib" "%TMPDIR%\venv\Lib" > nul || EXIT /B 1
REM Copy the standard library, but NOT site-packages: the venv already has its
REM own seeded pip there, and overwriting only the files the system Python also
REM has leaves a mix of two pip versions behind.
ROBOCOPY /E /R:3 /W:5 /NFL /NDL /NP "%PGADMIN_PYTHON_DIR%\Lib" "%TMPDIR%\venv\Lib" /XD site-packages
CALL :CHECK_ROBOCOPY_ERROR || EXIT /B 1
ECHO Activating virtual environment - %TMPDIR%\venv...
CALL "%TMPDIR%\venv\Scripts\activate" || EXIT /B 1
@@ -216,13 +220,18 @@ REM Main build sequence Ends
RD /Q /S "%WD%\web\pgadmin\static\js\generated\.cache" 1> nul 2>&1
ECHO Copying web directory...
ROBOCOPY /S "%WD%\web" "%BUILDROOT%\web" > nul
CALL :CHECK_ROBOCOPY_ERROR
ROBOCOPY /S /NFL /NDL /NP "%WD%\web" "%BUILDROOT%\web"
CALL :CHECK_ROBOCOPY_ERROR || EXIT /B 1
ECHO Installing javascript dependencies...
CD "%BUILDROOT%\web"
CALL yarn set version berry || EXIT /B 1
CALL yarn set version 4 || EXIT /B 1
SET "YARN_VERSION="
FOR /f "delims=" %%v IN ('node -p "require('./package.json').packageManager.split('@')[1]"') DO SET "YARN_VERSION=%%v"
IF "%YARN_VERSION%"=="" (
ECHO ERROR: Could not determine Yarn version from package.json packageManager field.
EXIT /B 1
)
CALL yarn set version %YARN_VERSION% || EXIT /B 1
CALL yarn install || EXIT /B 1
CALL npm rebuild || EXIT /B 1
@@ -237,8 +246,18 @@ REM Main build sequence Ends
RD /Q /S "%BUILDROOT%\web\regression" 1> nul 2>&1
ECHO Removing tools...
RD /Q /S "%BUILDROOT%\web\tools" 1> nul 2>&1
ECHO Removing yarn cache...
ECHO Removing the JavaScript build configuration...
RD /Q /S "%BUILDROOT%\web\.yarn" 1> nul 2>&1
DEL /q "%BUILDROOT%\web\yarn.lock" 1> nul 2>&1
DEL /q "%BUILDROOT%\web\.yarnrc.yml" 1> nul 2>&1
DEL /q "%BUILDROOT%\web\package.json" 1> nul 2>&1
DEL /q "%BUILDROOT%\web\jest.config.js" 1> nul 2>&1
DEL /q "%BUILDROOT%\web\babel.cfg" 1> nul 2>&1
DEL /q "%BUILDROOT%\web\babel.config.json" 1> nul 2>&1
DEL /q "%BUILDROOT%\web\webpack.config.js" 1> nul 2>&1
DEL /q "%BUILDROOT%\web\webpack.shim.js" 1> nul 2>&1
DEL /q "%BUILDROOT%\web\.eslintrc.js" 1> nul 2>&1
DEL /q "%BUILDROOT%\web\.editorconfig" 1> nul 2>&1
ECHO Removing any existing configurations...
DEL /q "%BUILDROOT%\web\pgadmin4.db" 1> nul 2>&1
DEL /q "%BUILDROOT%\web\config_local.py" 1> nul 2>&1
@@ -276,8 +295,13 @@ REM Main build sequence Ends
CD "%BUILDROOT%\runtime\resources\app\"
CALL yarn set version berry || EXIT /B 1
CALL yarn set version 4 || EXIT /B 1
SET "YARN_VERSION="
FOR /f "delims=" %%v IN ('node -p "require('./package.json').packageManager.split('@')[1]"') DO SET "YARN_VERSION=%%v"
IF "%YARN_VERSION%"=="" (
ECHO ERROR: Could not determine Yarn version from package.json packageManager field.
EXIT /B 1
)
CALL yarn set version %YARN_VERSION% || EXIT /B 1
CALL yarn workspaces focus --production || EXIT /B 1
ECHO Removing yarn cache...
@@ -310,7 +334,7 @@ REM Main build sequence Ends
%TMPDIR%\rcedit-x64.exe "%BUILDROOT%\runtime\pgAdmin4.exe" --set-icon "%WD%\pkg\win32\Resources\pgAdmin4.ico"
%TMPDIR%\rcedit-x64.exe "%BUILDROOT%\runtime\pgAdmin4.exe" --set-version-string "FileDescription" "%APP_NAME%"
%TMPDIR%\rcedit-x64.exe "%BUILDROOT%\runtime\pgAdmin4.exe" --set-version-string "ProductName" "%APP_NAME%"
%TMPDIR%\rcedit-x64.exe "%BUILDROOT%\runtime\pgAdmin4.exe" --set-product-version "%APP_VERSION%""
%TMPDIR%\rcedit-x64.exe "%BUILDROOT%\runtime\pgAdmin4.exe" --set-product-version "%APP_VERSION%"
IF NOT "%PGADMIN_WINDOWS_CSC%" == "" (
ECHO Attempting to sign the pgAdmin4.exe...
+32
View File
@@ -82,6 +82,38 @@ Select your preferred LLM provider from the dropdown:
After configuring your provider, click *Save* to apply the changes.
.. _ai_data_handling:
Data Handling and Provider Selection
************************************
When a cloud LLM provider is configured, pgAdmin transmits information about
your database to that provider's API. Depending on the feature in use, this may
include schema definitions such as table, column, index and constraint names
and data types; server and database configuration settings read from
``pg_settings``; query text; and EXPLAIN plan output. The *AI Assistant* in the
Query Tool is also able to run queries against your database, within a read-only
transaction and limited to 1000 rows, so row data may be included where the
assistant determines it is needed to answer a question.
None of this is transmitted unless you invoke an AI feature, and none of it is
transmitted at all when no provider has been configured, which is the default;
when AI features have been disabled by the administrator through the
``LLM_ENABLED`` setting; or when a locally hosted provider such as Ollama or
Docker Model Runner is configured.
Each provider processes this data under its own terms of service, privacy policy
and legal jurisdiction, and operates its infrastructure in locations of its own
choosing. If you work in an environment subject to data residency requirements,
procurement policies, or sector-specific rules governing the handling of
database metadata, review both the provider's terms and your own organisation's
policies before enabling a cloud provider.
The providers listed in pgAdmin reflect the APIs that pgAdmin is able to
communicate with. Their inclusion is not a recommendation, and no provider is
enabled by default.
Security Reports
****************
+5
View File
@@ -101,6 +101,11 @@ properties of the geometries directly in map, just click the specific geometry:
viewer will render geometries with the same SRID in the map. If SRID=4326 the
OSM tile layer will be added into the map.
- *Custom tile provider:* A custom map tile provider ({z}/{x}/{y} URL template,
in EPSG:3857, EPSG:4326 or EPSG:3395) can be configured as a base layer via
*Preferences > Query Tool > Geometry Viewer*. When configured, it is
selected as the default base layer.
- *Data size:* For performance reasons, the viewer will render no more than
100000 geometries, totaling up to 20MB.
+3 -1
View File
@@ -132,7 +132,9 @@ The JSON file format used when importing or exporting servers is quite
straightforward and simply contains a list of servers, with a number of
attributes. The following attributes are required to be present in every server
definition: Name, Group, Port, Username, SSLMode, MaintenanceDB and one of Host,
HostAddr or Service.
HostAddr or Service. A shared server may supply SharedUsername in place of
Username, in which case the shared username is also used as the username of the
imported server.
Password fields cannot be imported or exported.
+2
View File
@@ -22,6 +22,8 @@ When using main browser window, the following keyboard shortcuts are available:
+----------------------------+--------------------+------------------------------------+
| Shift + Alt + b | Shift + Option + b | Object explorer |
+----------------------------+--------------------+------------------------------------+
| Ctrl + Alt + b | Ctrl + Option + b | Toggle Object Explorer |
+----------------------------+--------------------+------------------------------------+
| Shift + Alt + n | Shift + Option + n | Create object |
+----------------------------+--------------------+------------------------------------+
| Shift + Alt + d | Shift + Option + d | Delete object |
+19
View File
@@ -11,6 +11,25 @@ file (see the :ref:`config.py <config_py>` documentation) on the system where
pgAdmin is installed in Server mode. You can copy these settings from *config.py*
file and modify the values for the following parameters.
.. warning::
pgAdmin reads OAuth2 provider settings **only** from the ``OAUTH2_CONFIG``
list; each provider is a single dictionary entry within that list. Settings
such as ``OAUTH2_CLIENT_ID`` or ``OAUTH2_SSL_CERT_VERIFICATION`` defined at
the top level of the configuration are ignored.
This matters in container deployments. Although most options can be set with
an individual ``PGADMIN_CONFIG_<KEY>`` environment variable, OAuth2 is the
exception: individual variables such as ``PGADMIN_CONFIG_OAUTH2_CLIENT_ID``
or ``PGADMIN_CONFIG_OAUTH2_SSL_CERT_VERIFICATION`` will **not** configure a
provider. Instead, configure the whole provider list through a single
``PGADMIN_CONFIG_OAUTH2_CONFIG`` variable, for example::
PGADMIN_CONFIG_OAUTH2_CONFIG="[{'OAUTH2_NAME': 'my-provider', 'OAUTH2_DISPLAY_NAME': 'My Provider', 'OAUTH2_CLIENT_ID': '...', 'OAUTH2_CLIENT_SECRET': '...', 'OAUTH2_SERVER_METADATA_URL': 'https://provider.example.com/.well-known/openid-configuration', 'OAUTH2_SCOPE': 'openid email profile', 'OAUTH2_SSL_CERT_VERIFICATION': False}]"
If pgAdmin detects per-provider OAuth2 settings at the top level while no
provider is configured in ``OAUTH2_CONFIG``, it logs a warning at startup
to highlight the misconfiguration.
OAuth2 vs OpenID Connect (OIDC)
================================
+33
View File
@@ -635,6 +635,39 @@ a graphical EXPLAIN.
* When the *Verbose output?* switch is set to *True*, graphical explain details
will include extended information about the query execution plan.
Use the fields on the *Geometry Viewer* panel to configure a custom map tile
provider used as a base layer when viewing geometry data.
* Use the *Custom tile provider URL* field to specify the URL template of a
custom XYZ tile provider, e.g.
``https://myserver.example.com/tiles/{z}/{x}/{y}.png``. The template must
contain the ``{x}``, ``{y}`` and ``{z}`` placeholders, and may contain
``{s}`` for subdomains (a, b, c). Leave the field empty to disable the
custom tile provider.
* Use the *Custom tile provider name* field to specify the display name of
the custom tile provider in the layer selector of the Geometry Viewer.
* Use the *Custom tile provider CRS* field to specify the coordinate
reference system of the custom tile provider. If it is not EPSG:3857
(Web Mercator), the built-in tile layers will be hidden as they cannot be
mixed with other coordinate systems.
* Use the *Custom tile provider attribution* field to specify the
attribution text shown on the map. It may contain HTML links.
* Use the *Custom tile provider max zoom* field to specify the maximum zoom
level of the custom tile provider.
When a custom tile provider is configured, it is selected as the default
base layer of the Geometry Viewer.
An administrator may set a system-wide default for these fields via the
``DEFAULT_GEOMETRY_VIEWER_PROVIDER`` setting in :ref:`config_py`. It applies
to any user who has not saved their own values for the fields above; a
user's own preferences, once saved, always take precedence over the
system-wide default.
.. image:: images/preferences_graph_visualiser.png
:alt: Preferences sqleditor graph visualiser section
:align: center
+3 -1
View File
@@ -243,7 +243,9 @@ iteratively. For example, you can ask for a query and then follow up with
**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.
statements. All generated queries should be reviewed before execution. See
:ref:`ai_data_handling` for details of the information that is transmitted to
your configured LLM provider.
The Data Output Panel
*********************
+2
View File
@@ -12,6 +12,8 @@ notes for it.
:maxdepth: 1
release_notes_9_18
release_notes_9_17
release_notes_9_16
release_notes_9_15
release_notes_9_14
+119
View File
@@ -0,0 +1,119 @@
************
Version 9.17
************
Release date: 2026-07-30
This release contains a number of bug fixes and new features since the release of pgAdmin 4 v9.16.
Supported Database Servers
**************************
**PostgreSQL**: 14, 15, 16, 17 and 18
**EDB Advanced Server**: 14, 15, 16, 17 and 18
Bundled PostgreSQL Utilities
****************************
**psql**, **pg_dump**, **pg_dumpall**, **pg_restore**: 18.4
New features
************
| `Issue #9990 <https://github.com/pgadmin-org/pgadmin4/issues/9990>`_ - Include the authenticated user's identity in the HTTP access log.
| `Issue #9942 <https://github.com/pgadmin-org/pgadmin4/issues/9942>`_ - Add an opt-in Gateway API HTTPRoute template to the Helm chart as an alternative to the existing Ingress.
| `Issue #10104 <https://github.com/pgadmin-org/pgadmin4/issues/10104>`_ - Add a preference to cap the row count fetched by the plain "View Data" action, so it is usable on large tables without always doing a full ``SELECT *``.
| `Issue #10142 <https://github.com/pgadmin-org/pgadmin4/issues/10142>`_ - Add support for a custom XYZ tile provider (URL, name, CRS, attribution, max zoom) in the Geometry Viewer, alongside the existing built-in base layers.
Housekeeping
************
| `Issue #10077 <https://github.com/pgadmin-org/pgadmin4/issues/10077>`_ - Centralize shared-server-group visibility and access-control logic, and adjust the ServerGroup-to-Server/SharedServer model relationships.
| `Issue #10093 <https://github.com/pgadmin-org/pgadmin4/issues/10093>`_ - Add a basic starter contributing guide.
| `Issue #10135 <https://github.com/pgadmin-org/pgadmin4/issues/10135>`_ - Fail the macOS appbundle build if any bundled library links outside the bundle, and scan all Mach-O binaries for bundle linkage rather than only files named ``.so``/``.dylib``.
| `Issue #10154 <https://github.com/pgadmin-org/pgadmin4/issues/10154>`_ - Pin the sonarqube-scan-action GitHub workflow to a full commit SHA instead of a mutable branch ref, to harden against supply-chain tampering.
| `Issue #10138 <https://github.com/pgadmin-org/pgadmin4/issues/10138>`_ - Update the Simplified Chinese (zh_Hans_CN) translation to match the wording used in the interface.
| `Issue #10156 <https://github.com/pgadmin-org/pgadmin4/issues/10156>`_ - Pin the Yarn version used by the build scripts to the ``packageManager`` field, rather than whatever Yarn happens to be on the builder's PATH.
| `Issue #10176 <https://github.com/pgadmin-org/pgadmin4/issues/10176>`_ - Bump JavaScript and Python third-party dependencies, including axios, webpack, react, electron, and certifi.
Bug fixes
*********
| `Issue #10053 <https://github.com/pgadmin-org/pgadmin4/issues/10053>`_ - Warn when OAuth2 provider settings are misplaced at the top level of the config instead of under ``OAUTH2_CONFIG``.
| `Issue #10100 <https://github.com/pgadmin-org/pgadmin4/issues/10100>`_ - Fix Schema Diff's "Generate Script" and the browser tree's CREATE Script view emitting wrong SQL for SERIAL/identity columns, by detecting column-owned sequences via pg_depend instead of guessing the sequence name.
| `Issue #10102 <https://github.com/pgadmin-org/pgadmin4/issues/10102>`_ - Fix a Schema Diff result-status filter chip showing "No difference found" after being toggled off and back on, even when real differences exist.
| `Issue #10106 <https://github.com/pgadmin-org/pgadmin4/issues/10106>`_ - Fix the Object Explorer briefly showing the literal HTML markup instead of a greyed-out "[Disconnecting...]" label when disconnecting a server or database.
| `Issue #10107 <https://github.com/pgadmin-org/pgadmin4/issues/10107>`_ - Detect a selected-but-unusable OS keyring and fall back gracefully instead of failing.
| `Issue #10109 <https://github.com/pgadmin-org/pgadmin4/issues/10109>`_ - Fix ALT+F5 ("Execute query at cursor") doing nothing when the cursor is on or near a statement that is not highlighted, in a Query Tool tab with multiple statements separated by blank lines.
| `Issue #10110 <https://github.com/pgadmin-org/pgadmin4/issues/10110>`_ - Stop the googleapiclient cloud-deployment dependency from pulling in the system oauth2client package.
| `Issue #10117 <https://github.com/pgadmin-org/pgadmin4/issues/10117>`_ - Fix a "'Response' object is not iterable" crash when expanding a Trigger node under a Table to view its trigger function, and report the correct error message on a node.sql failure in that flow.
| `Issue #10158 <https://github.com/pgadmin-org/pgadmin4/issues/10158>`_ - Honor the selected EOL sequence when copying query text to the clipboard.
| `Issue #10187 <https://github.com/pgadmin-org/pgadmin4/issues/10187>`_ - Fix the object browser's extension UI breaking under PostgreSQL 19's extension catalog changes.
| `Issue #10190 <https://github.com/pgadmin-org/pgadmin4/issues/10190>`_ - Fix a tool-permission bypass where a user denied the Query Tool, Grant Wizard, or Schema Diff permission could still drive that tool's backend routes and Socket.IO handlers directly, since the permission check was applied only to a single "front door" route per tool. Also fixes a non-owner triggering an adhoc connection against another user's shared server persisting a new server record still owned by that other user (CVE-2026-17350). Reported by LXY.
| `Issue #10191 <https://github.com/pgadmin-org/pgadmin4/issues/10191>`_ - Fix OS command injection in the MASTER_PASSWORD_HOOK feature, where an externally-sourced username (e.g. via OAuth2/OIDC, Kerberos, or webserver authentication) containing shell metacharacters could execute arbitrary commands as the pgAdmin service account when the configured hook string uses ``%u`` (CVE-2026-17347). Reported by Thiago Pereira.
| `Issue #10192 <https://github.com/pgadmin-org/pgadmin4/issues/10192>`_ - Fix a lexer-differential bypass of the AI Assistant's read-only transaction guard, where sqlparse's string-literal lexing disagreed with PostgreSQL's own parser under ``standard_conforming_strings = on``, letting a crafted multi-statement payload smuggle a COMMIT past the intended read-only wrapper, an incomplete fix for CVE-2026-12045 (CVE-2026-17351). Reported by Kai Aizen.
| `Issue #10193 <https://github.com/pgadmin-org/pgadmin4/issues/10193>`_ - Fix SQL injection in the Index Statistics all-indexes listing and the Publications/Subscriptions Dependencies views, where an apostrophe in a table, index, publication, or subscription name broke out of an unescaped template interpolation, an incomplete fix for CVE-2026-12044 (CVE-2026-17346). Reported by Hung Tran Quoc (@rampage0010).
| `Issue #10194 <https://github.com/pgadmin-org/pgadmin4/issues/10194>`_ - Fix several Constraints, Preferences, Debugger, and Schema Diff routes missing the ``@pga_login_required`` decorator, making them reachable without authentication in server mode, an incomplete fix for CVE-2026-12046 (CVE-2026-17348). Reported by Hung Tran Quoc (@rampage0010).
| `Issue #10200 <https://github.com/pgadmin-org/pgadmin4/issues/10200>`_ - Fix an adhoc server connection cloning another user's stored database credentials (password, save password flag, tunnel password) alongside ownership, letting a non-owner who cloned another user's shared server connect using that user's saved database password (CVE-2026-17349).
| `Issue #10213 <https://github.com/pgadmin-org/pgadmin4/issues/10213>`_ - Fix OS command injection in the Import/Export Data tool, where a query-based export could pass a crafted query string past the ``\copy (...)`` parenthesis-balance guard by exploiting a backslash-escape mismatch with psql's default ``standard_conforming_strings = on`` behaviour, exposing a live ``TO PROGRAM`` clause for arbitrary command execution (CVE-2026-17566). Reported by Arpit Jain.
Additional changes (no associated issue)
****************************************
Dependencies
------------
Non-breaking ``dependabot`` and audit-driven bumps aggregated for v9.17.
Python:
| ``certifi`` 2026.5.20 -> 2026.6.17
JavaScript (``web/``):
| ``@babel/core`` 7.29.0 -> 7.29.6
| ``@date-io/date-fns`` 3.x -> 3.2.1
| ``@szhsin/react-menu`` 4.5.1 -> 4.5.2
| ``@tanstack/react-query`` 5.100.9 -> 5.101.4
| ``@types/react`` 19.2.14 -> 19.2.17
| ``ajv`` 8.18.0 -> 8.20.0
| ``anti-trojan-source`` 1.8.1 -> 1.12.0
| ``autoprefixer`` 10.5.0 -> 10.5.4
| ``axios`` 1.16.0 -> 1.18.1
| ``core-js`` added as an explicit direct dependency (3.49.0), previously relied on transitively via pickr
| ``date-fns`` 4.1.0 -> 4.4.0
| ``dompurify`` 3.4.1 -> 3.4.12
| ``eslint-plugin-jest`` 29.15.2 -> 29.15.5
| ``globals`` 17.5.0 -> 17.7.0
| ``hotkeys-js`` 4.0.3 -> 4.0.4
| ``html-to-image`` 1.11.11 -> 1.11.13
| ``ip-address`` 10.1.1 -> 10.2.0
| ``jest`` / ``jest-environment-jsdom`` 30.3.0 -> 30.4.2 / 30.4.1
| ``marked`` 18.0.2 -> 18.0.7
| ``moment-timezone`` 0.6.2 -> 0.6.3
| ``papaparse`` 5.5.3 -> 5.5.4
| ``postcss`` 8.5.10/8.5.14 -> 8.5.20
| ``react`` / ``react-dom`` 19.2.5 -> 19.2.7
| ``react-checkbox-tree`` 2.0.1 -> 2.0.2
| ``react-draggable`` 4.5.0 -> 4.7.0
| ``react-timer-hook`` 4.0.5 -> 4.0.6
| ``sharp`` 0.34.4 -> 0.35.3
| ``sql-formatter`` 15.7.3 -> 15.8.2
| ``svgo`` 4.0.1 -> 4.0.2
| ``terser-webpack-plugin`` 5.5.0 -> 5.6.1
| ``typescript-eslint`` 8.59.0 -> 8.65.0
| ``webpack`` 5.106.2 -> 5.108.4
| ``webpack-bundle-analyzer`` 5.3.0 -> 5.3.1
| ``zustand`` 5.0.12 -> 5.0.14
| Transitive advisory bumps: ``brace-expansion``, ``form-data``, ``undici``, ``js-yaml``
| Reverted ``azure-mgmt-resource`` to avoid a breaking import-path change in ``pgadmin/misc/cloud/azure``
JavaScript (``runtime/``):
| ``electron`` 42.3.3 -> 43.1.1
| ``eslint`` 10.4.1 -> 10.7.0
| ``globals`` 17.6.0 -> 17.7.0
| ``axios`` 1.18.0 -> 1.18.1
| ``ip-address`` 10.1.1 -> 10.2.0
| ``postcss`` 8.5.10 -> 8.5.20
| Pinned ``packageManager`` to ``yarn@4.15.0``
+136
View File
@@ -0,0 +1,136 @@
************
Version 9.18
************
Release date: 2026-09-17
This release contains a number of bug fixes and new features since the release of pgAdmin 4 v9.17.
Supported Database Servers
**************************
**PostgreSQL**: 14, 15, 16, 17 and 18
**EDB Advanced Server**: 14, 15, 16, 17 and 18
Bundled PostgreSQL Utilities
****************************
**psql**, **pg_dump**, **pg_dumpall**, **pg_restore**: 18.4
New features
************
| `Issue #9631 <https://github.com/pgadmin-org/pgadmin4/issues/9631>`_ - Collapse and restore the Object Explorer by re-clicking the current workspace icon, in the manner of the VS Code side bar, remembering the choice across refreshes. A keyboard shortcut, Ctrl+Alt+B by default, does the same thing and can be changed through the new ``toggle_object_explorer`` preference.
Housekeeping
************
| `Issue #10221 <https://github.com/pgadmin-org/pgadmin4/issues/10221>`_ - Skip importing and initialising the kerberos, ldap, mfa, oauth2 and webserver authentication providers unless ``SERVER_MODE`` is set, leaving desktop mode with internal authentication alone.
| `Issue #10247 <https://github.com/pgadmin-org/pgadmin4/issues/10247>`_ - Relax the ``azure-mgmt-resource`` pin to allow 24.0.0, and aggregate the third-party JavaScript and Python dependency bumps for this release.
| `Issue #10293 <https://github.com/pgadmin-org/pgadmin4/issues/10293>`_ - Make the Schema Diff regression test assert its own generated script, which it previously swallowed, so invalid generated SQL can no longer pass silently.
Bug fixes
*********
| `Issue #9226 <https://github.com/pgadmin-org/pgadmin4/issues/9226>`_ - Accept ``SharedUsername`` when importing a shared server from a servers.json definition, instead of insisting on ``Username`` for every server.
| `Issue #10155 <https://github.com/pgadmin-org/pgadmin4/issues/10155>`_ - Share concurrent identical GET requests behind ``getNodeAjaxOptions()`` so a wide table's Columns tab no longer fires one duplicate ``get_types`` request per column row.
| `Issue #10179 <https://github.com/pgadmin-org/pgadmin4/issues/10179>`_ - Fix inherited columns in the Table dialog being editable and deletable: a column already inherited from a parent carries ``inheritedfromtable`` while one fetched interactively through 'Inherited from table(s)' carries ``inheritedfrom`` and has no ``attnum`` yet, and only the latter was checked, after an ``isNew()`` short-circuit that treated the attnum-less rows as new.
| `Issue #10180 <https://github.com/pgadmin-org/pgadmin4/issues/10180>`_ - Fix the Data type dropdown on the expanded Definition tab of the Table dialog offering every type, ignoring the allowed-type restriction already applied to the inline editor.
| `Issue #10214 <https://github.com/pgadmin-org/pgadmin4/issues/10214>`_ - Fix the ``existingSecret`` path in the Helm deployment template.
| `Issue #10235 <https://github.com/pgadmin-org/pgadmin4/issues/10235>`_ - Remove a trailing quote from the Windows installer's ``ProductVersion``, which was stamped as e.g. ``9.17"``.
| `Issue #10236 <https://github.com/pgadmin-org/pgadmin4/issues/10236>`_ - Fix Schema Diff reporting false differences for SERIAL/BIGSERIAL columns by ignoring the owned sequence's oid, and fix the invalid ``ALTER COLUMN ... TYPE bigserial`` SQL generated when such a column genuinely differs.
| `Issue #10237 <https://github.com/pgadmin-org/pgadmin4/issues/10237>`_ - Fix a ``replace() argument 2 must be str, not None`` crash when a per-server Password Exec Command is used with a service-only (pg_service.conf) connection, which leaves host, port and username unset.
| `Issue #10239 <https://github.com/pgadmin-org/pgadmin4/issues/10239>`_ - Remove a redundant ``passlib`` pin that conflicts with the indirectly required ``libpass``.
| `Issue #10252 <https://github.com/pgadmin-org/pgadmin4/issues/10252>`_ - Fix the argument grid on the Definition tab of a user-defined function or procedure refusing to delete, or add, a row in edit mode.
| `Issue #10295 <https://github.com/pgadmin-org/pgadmin4/issues/10295>`_ - Reinstate dependency ordering of the script Schema Diff generates, which had been lost since the React port left ``dependLevel`` unset.
| `Issue #10297 <https://github.com/pgadmin-org/pgadmin4/issues/10297>`_ - Fix Schema Diff duplicating any column that also differs when it recreates a foreign table.
| `Issue #10298 <https://github.com/pgadmin-org/pgadmin4/issues/10298>`_ - Fix Schema Diff generating SQL that PostgreSQL rejects when a sequence's MINVALUE is raised above its current value.
| `Issue #10300 <https://github.com/pgadmin-org/pgadmin4/issues/10300>`_ - Fix a foreign table column added by Schema Diff losing its collation.
| `Issue #10302 <https://github.com/pgadmin-org/pgadmin4/issues/10302>`_ - Fix Schema Diff injecting whitespace into an applied function or procedure body, which left a whitespace-only difference behind.
| `Issue #10303 <https://github.com/pgadmin-org/pgadmin4/issues/10303>`_ - Report a Schema Diff comparison that fails part way through as a failure, rather than as a success.
| `Issue #10309 <https://github.com/pgadmin-org/pgadmin4/issues/10309>`_ - Reject an empty or null ``Username`` when importing a non-shared server, which previously imported cleanly and left a server libpq would silently authenticate as the OS account running pgAdmin.
| `Issue #10311 <https://github.com/pgadmin-org/pgadmin4/issues/10311>`_ - Fix login being impossible against Flask-Security-Too 5.8.2, which corrected a long-standing inversion in ``UserMixin.is_locked()`` that ``User.is_locked()`` had been written against.
| `Issue #10341 <https://github.com/pgadmin-org/pgadmin4/issues/10341>`_ - Omit the redundant ``TABLESPACE pg_default`` clause from generated index SQL, which was invalid on a partitioned table.
| `Issue #10383 <https://github.com/pgadmin-org/pgadmin4/issues/10383>`_ - Fix an authentication bypass in Webserver authentication mode, where ``get_user()`` fell back to reading the configured ``WEBSERVER_REMOTE_USER`` name from the request headers when it was absent from the WSGI environment, letting any client that could reach pgAdmin assert any identity, including an administrator's (CVE-2026-86863). Header-asserted identity is now opt-in, restricted to a configured list of trusted proxies with an optional shared secret, and refused for accounts whose authentication source is not ``webserver``. Reported by Sanghyeon Lee (@h9e0n).
| `Issue #10384 <https://github.com/pgadmin-org/pgadmin4/issues/10384>`_ - Fix argument and connection-string injection in the Backup tool, where the client-supplied database name was appended to the pg_dump argument vector as a bare positional argument: because getopt_long permutes arguments, a value beginning with a dash supplied further options such as ``--file``, overriding the storage-confined output path, and because libpq expands a database name containing an equals sign into a full connection string, one could also redirect the connection to an attacker-controlled host and exfiltrate the stored password (CVE-2026-86864). The database name is now passed through the ``PGDATABASE`` environment variable instead. Reported by Sanghyeon Lee (@h9e0n) and Hitesh Jambhale.
| `Issue #10385 <https://github.com/pgadmin-org/pgadmin4/issues/10385>`_ - Fix a time-of-check to time-of-use flaw in the File Manager's ``save_file`` endpoint, which backs saving from the Query Tool and ERD: the requested path was validated with ``check_access_permission()`` and then opened with a plain ``open()``, so a symbolic link planted in between was followed, writing outside the storage directory. This is the sink CVE-2026-7819's hardening of the separate upload path did not cover (CVE-2026-86861). Reported by sec-rex.
| `Issue #10388 <https://github.com/pgadmin-org/pgadmin4/issues/10388>`_ - Fix connection-string injection in the Restore and Maintenance tools, where the client-supplied database name was passed straight to ``--dbname``: libpq expands a database name containing an equals sign into a full connection string, whose embedded keywords override the ``--host`` and ``--port`` pgAdmin supplies, so the connection - and the stored password exported in ``PGPASSWORD`` - could be redirected to a server of the caller's choosing (CVE-2026-86862). The database name is now passed through the ``PGDATABASE`` environment variable, which libpq never expands. Found by Hitesh Jambhale.
| `Issue #10393 <https://github.com/pgadmin-org/pgadmin4/issues/10393>`_ - Render the Validate binary path dialog as HTML, instead of showing the raw markup.
| `Issue #10420 <https://github.com/pgadmin-org/pgadmin4/issues/10420>`_ - Refuse HTTP redirects on LLM API requests, rather than following a ``Location`` header on to a destination ``ALLOWED_LLM_API_URLS`` was never applied to. This is hardening rather than a fix for an exploitable flaw, since returning the redirect at all requires control of a host already on the allowlist. Reported by Ziya Abdullayev.
Dependencies
************
Non-breaking ``dependabot`` and audit-driven bumps aggregated for v9.18.
Python:
| ``Authlib`` 1.7.* -> 1.8.*
| ``azure-mgmt-resource`` ==25.0.0 -> >=24.0.0,<26.0.0
| ``certifi`` 2026.6.17 -> 2026.7.22
| ``cryptography`` 49.0.* -> 50.0.*
| ``Flask-Security-Too`` 5.8.* -> >=5.8.2,<5.9
| ``google-auth-oauthlib`` 1.4.0 -> 1.4.1
| ``gssapi`` 1.11.* -> 1.12.*
| ``passlib`` 1.* pin removed (redundant, and conflicting with ``libpass``)
| ``psycopg[c]`` 3.3.4 -> 3.3.5
| ``selenium`` (``web/regression``) 4.45.0 -> 4.48.0
| ``setuptools`` 83.* -> 84.*
| ``testscenarios`` (``web/regression``) 0.6.2 -> 0.7.0
| ``typer`` 0.26.* -> 0.27.*
| Skipped: ``paramiko`` 3 -> 5 (blocked on ``sshtunnel``'s ``paramiko.DSSKey`` usage) and ``azure-mgmt-resource`` 26.0.0 (drops Python 3.9 support)
JavaScript (``web/``):
| ``@babel/*`` toolchain (core, eslint-parser, eslint-plugin, plugin-syntax-jsx, plugin-transform-class-properties, plugin-transform-object-rest-spread, plugin-transform-runtime, preset-env, preset-react, preset-typescript) -> 7.29.7
| ``@mui/icons-material`` / ``@mui/material`` 7.3.10 -> 7.3.11
| ``@mui/x-date-pickers`` 8.28.3 -> 8.29.3
| ``@tanstack/react-query`` 5.101.4 -> 5.102.8
| ``@tanstack/react-virtual`` 3.13.24 -> 3.14.10
| ``@testing-library/jest-dom`` 6.9.1 -> 6.10.0
| ``@testing-library/react`` 16.3.2 -> 16.3.3
| ``@testing-library/user-event`` 14.6.1 -> 14.6.3
| ``@types/react`` 19.2.17 -> 19.2.18
| ``@types/react-dom`` 19.2.3 -> 19.2.5
| ``anti-trojan-source`` 1.12.0 -> 1.12.2
| ``axios`` 1.18.1 -> 1.20.0
| ``codemirror`` 6.0.2 -> 6.65.7
| ``core-js`` 3.49.0 -> 3.50.0
| ``css-loader`` 7.1.4 -> 7.1.5
| ``diff-arrays-of-objects`` 1.1.10 -> 1.1.11
| ``dompurify`` 3.4.12 -> 3.4.14
| ``eslint`` 9.39.4 -> 9.39.5
| ``eslint-plugin-jest`` 29.15.5 -> 29.16.6
| ``globals`` 17.7.0 -> 17.12.0
| ``hotkeys-js`` 4.0.4 -> 4.0.7
| ``image-minimizer-webpack-plugin`` 4.1.4 -> 5.0.0
| ``ip-address`` 10.2.0 -> 10.7.0
| ``jest`` / ``jest-environment-jsdom`` 30.4.2 / 30.4.1 -> 30.5.1
| ``lossless-json`` 4.3.0 -> 4.3.1
| ``marked`` 18.0.7 -> 18.0.11
| ``papaparse`` 5.5.4 -> 5.7.0
| ``postcss`` 8.5.20 -> 8.5.26
| ``rc-dock`` 4.0.0-alpha.2 -> 4.0.0-alpha.3
| ``react`` / ``react-dom`` 19.2.7 -> 19.2.8
| ``react-arborist`` 3.5.0 -> 3.16.0
| ``react-draggable`` 4.7.0 -> 4.7.1
| ``sharp`` 0.35.3 -> 0.35.4
| ``svgo`` 4.0.2 -> 4.1.0
| ``svgo-loader`` 4.0.0 -> 5.0.0
| ``typescript-eslint`` 8.65.0 -> 8.69.0
| ``use-resize-observer`` 9.1.0 -> 10.0.0
| ``vanilla-jsoneditor`` 3.12.0 -> 3.13.0
| ``webpack-bundle-analyzer`` 5.3.1 -> 5.3.2
| ``zustand`` 5.0.14 -> 5.0.15
| Transitive advisory bumps: ``brace-expansion``, ``fast-uri``, ``js-yaml``, ``shell-quote``, ``socket.io-parser``, ``tar``
| Held back: ``@simonwep/pickr`` 1.10.x (webpack production-bundle interop break), Babel 8, and ``webpack`` 5.110.3 (npm-quarantined at bump time)
JavaScript (``runtime/``):
| ``axios`` 1.18.1 -> 1.19.0
| ``electron`` 43.1.1 -> 43.4.0
| ``eslint`` 10.7.0 -> 10.8.1
| ``globals`` 17.7.0 -> 17.11.0
| ``ip-address`` 10.2.0 -> 10.3.1
| ``postcss`` 8.5.20 -> 8.5.23
| Transitive advisory bumps: ``brace-expansion``, ``fast-uri``, ``undici``
+50 -1
View File
@@ -36,8 +36,57 @@ and modify the values for the following parameters:
environment variable name which comes from the web server after webserver authentication.
The default value is REMOTE_USER and the possible values are REMOTE_USER,
HTTP_X_FORWARDED_USER, X-Forwarded-User."
"WEBSERVER_REMOTE_USER_FROM_HEADER", "Set to *True* only if your reverse proxy passes the
authenticated identity as an HTTP request header rather than the REMOTE_USER CGI variable.
Defaults to *False*. See the warning below before enabling this."
"WEBSERVER_TRUSTED_PROXIES", "A list of IP addresses/CIDR ranges of the reverse proxies
that are allowed to assert the identity header. Required (non-empty) for
WEBSERVER_REMOTE_USER_FROM_HEADER to take effect. Defaults to an empty list."
"WEBSERVER_SHARED_SECRET", "An optional shared secret that the trusted proxy must inject
into every request, as additional proof that the identity header was set by the proxy
and not the client. Defaults to *None* (not checked)."
"WEBSERVER_SHARED_SECRET_HEADER", "The header name carrying WEBSERVER_SHARED_SECRET.
Defaults to *X-Pgadmin-Webserver-Secret*."
.. note:: If REMOTE_USER does not work, try replacing it with either REMOTE-USER or Remote-user.
.. warning::
Setting WEBSERVER_REMOTE_USER to a header-derived name (such as
HTTP_X_FORWARDED_USER, or REMOTE-USER/Remote-user) means the identity is
read from a client-controlled HTTP request header, not necessarily a
value set by your webserver's authentication plug-in. Any client that
can reach pgAdmin can set this header itself and impersonate any user,
including an existing Administrator, unless you:
* Set ``WEBSERVER_REMOTE_USER_FROM_HEADER = True``.
* List every reverse proxy allowed to assert the identity in
``WEBSERVER_TRUSTED_PROXIES``.
* Configure your reverse proxy to strip *every* inbound spelling of the
header (any hyphen/underscore variant, in any case) before it sets its
own, so a client cannot smuggle a value past it.
* Optionally configure ``WEBSERVER_SHARED_SECRET`` /
``WEBSERVER_SHARED_SECRET_HEADER`` as an additional proof that the
header was set by your proxy.
Without all of the above, pgAdmin does not trust a header-asserted
identity and login through it is rejected.
.. warning::
A user matched via Webserver authentication is only logged in if their
*Authentication source* is *webserver*. This prevents a header-asserted
identity from ever logging into an account that was created some other
way - most importantly, the default administrator account created by
pgAdmin's own setup, whose *Authentication source* is *internal*.
If you are enabling Webserver authentication on a site that already has
users (including that default administrator) with an *Authentication
source* other than *webserver*, none of them will be able to log in
through Webserver authentication until you change it. To do so, sign in
through another configured authentication source and, for each such
user, set *Authentication source* to *webserver* on the
:ref:`User Management <user_management>` page. If ``AUTHENTICATION_SOURCES``
lists only *webserver*, there is no other source to sign in with -
temporarily add *internal* back to ``AUTHENTICATION_SOURCES``, sign in
with the internal administrator's password, fix the affected users'
*Authentication source*, then remove *internal* again.
Master Password
===============
+8
View File
@@ -5,6 +5,14 @@ from config import JSON_LOGGER, CONSOLE_LOG_LEVEL, CONSOLE_LOG_FORMAT_JSON
gunicorn.SERVER_SOFTWARE = "Python"
# Include the authenticated user identity in the access log.
# %({x-remote-user}o)s reads the X-Remote-User response header set by pgAdmin
# for authenticated requests; unauthenticated requests log '-'.
access_log_format = (
'%(h)s %(l)s %({x-remote-user}o)s %(t)s "%(r)s" %(s)s %(b)s '
'"%(f)s" "%(a)s"'
)
if JSON_LOGGER:
logconfig_dict = {
"version": 1,
+2 -2
View File
@@ -1,7 +1,7 @@
apiVersion: v2
name: pgadmin4-helm
version: 9.16.0
appVersion: 9.16.0
version: 9.18.0
appVersion: 9.18.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"]
+7
View File
@@ -35,11 +35,18 @@ The chart should dump its version and appVersion in the Chart.yaml file every re
| `preferences.data` | Preferences to load | `{}` |
| `resources.*` | Allocated requests and limits resources | `{"requests": {...}, "limits": {...}}` |
| `persistence.enabled` | PVC resource creation | `true` |
| `persistence.existingClaim` | Provide existing PVC instead of creating one | `""` |
| `service.type` | Service type | `"ClusterIP"` |
| `service.loadBalancerIP` | Load balancer IP (Only if service.type is LoadBalancer) | `""` |
| `ingress.enabled` | Ingress resource creation | `false` |
| `ingress.hostname` | Ingress resource hostname | `"pgadmin4.local"` |
| `ingress.tlsSecret` | Ingress tls secret name | `""` |
| `httpRoute.enabled` | Gateway API HTTPRoute resource creation | `false` |
| `httpRoute.apiVersion` | HTTPRoute apiVersion (override for v1beta1 implementations) | `"gateway.networking.k8s.io/v1"` |
| `httpRoute.parentRefs` | Gateway(s) the route attaches to (required when `httpRoute.enabled` is true) | `[]` |
| `httpRoute.hostnames` | HTTPRoute hostnames (falls back to `ingress.hostname` when empty) | `[]` |
| `httpRoute.annotations` | HTTPRoute annotations | `{}` |
| `httpRoute.rules` | Custom routing rules (defaults to a single `/` PathPrefix rule to the service) | `[]` |
| `strategy.type` | Deployment strategy type (RollingUpdate or Recreate) | Kubernetes default (RollingUpdate) |
| `strategy.rollingUpdate.maxSurge` | Maximum number of pods that can be created over the desired replicas | Kubernetes default (25%) |
| `strategy.rollingUpdate.maxUnavailable` | Maximum number of pods that can be unavailable during the update | Kubernetes default (25%) |
+3 -3
View File
@@ -32,12 +32,12 @@ spec:
{{- with omit .Values.commonLabels "app" }}
{{- . | toYaml | nindent 8 }}
{{- end }}
{{- if or (not (empty .Values.commonAnnotations)) (not .Values.existingSecret) .Values.preferences.enabled .Values.serverDefinitions.enabled }}
{{- if or (not (empty .Values.commonAnnotations)) (empty .Values.auth.existingSecret) .Values.preferences.enabled .Values.serverDefinitions.enabled }}
annotations:
{{- with .Values.commonAnnotations }}
{{- . | toYaml | nindent 8 }}
{{- end }}
{{- if not .Values.existingSecret }}
{{- if empty .Values.auth.existingSecret }}
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
{{- end }}
{{- if and .Values.config_local.enabled (empty .Values.config_local.existingSecret) }}
@@ -66,7 +66,7 @@ spec:
{{- if .Values.persistence.enabled }}
- name: data
persistentVolumeClaim:
claimName: {{ template "pgadmin4.fullname" . }}
claimName: {{ .Values.persistence.existingClaim | default (include "pgadmin4.fullname" .) }}
{{- end }}
{{- if .Values.config_local.enabled }}
- name: config-local
+39
View File
@@ -0,0 +1,39 @@
{{- if .Values.httpRoute.enabled }}
{{- if not .Values.httpRoute.parentRefs }}
{{- fail "httpRoute.parentRefs must be set when httpRoute.enabled is true" }}
{{- end }}
apiVersion: {{ .Values.httpRoute.apiVersion | default "gateway.networking.k8s.io/v1" }}
kind: HTTPRoute
metadata:
name: {{ template "pgadmin4.fullname" . }}
{{- with .Values.commonLabels }}
labels: {{ . | toYaml | nindent 4 }}
{{- end }}
{{- if or .Values.httpRoute.annotations .Values.commonAnnotations }}
annotations: {{ merge .Values.httpRoute.annotations .Values.commonAnnotations | toYaml | nindent 4 }}
{{- end }}
spec:
parentRefs: {{- toYaml .Values.httpRoute.parentRefs | nindent 4 }}
{{- $hostnames := .Values.httpRoute.hostnames }}
{{- if and (not $hostnames) .Values.ingress.hostname }}
{{- $hostnames = list (tpl .Values.ingress.hostname .) }}
{{- end }}
{{- with $hostnames }}
hostnames: {{- toYaml . | nindent 4 }}
{{- end }}
rules:
{{- if .Values.httpRoute.rules }}
{{- toYaml .Values.httpRoute.rules | nindent 4 }}
{{- else }}
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- group: ''
kind: Service
name: {{ template "pgadmin4.fullname" . }}
port: {{ .Values.service.port }}
weight: 1
{{- end }}
{{- end }}
+2 -2
View File
@@ -1,4 +1,4 @@
{{- if .Values.persistence.enabled }}
{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
@@ -17,4 +17,4 @@ spec:
requests:
storage: {{ .Values.persistence.size }}
accessModes: {{ .Values.persistence.accessModes }}
{{- end }}
{{- end }}
+20 -1
View File
@@ -88,6 +88,7 @@ persistence:
size: 1Gi
storageClass: ""
accessModes: ["ReadWriteOnce"]
existingClaim: ""
service:
type: ClusterIP
@@ -106,6 +107,25 @@ ingress:
annotations: {}
# If set, enables TLS configuration in the Ingress resource.
tlsSecret: ""
httpRoute:
# Enable a Gateway API HTTPRoute as an alternative to the Ingress above.
enabled: false
# apiVersion for the HTTPRoute resource. Override if your Gateway API
# implementation still serves the resource under v1beta1.
apiVersion: gateway.networking.k8s.io/v1
# parentRefs is required when httpRoute.enabled is true. It references the
# Gateway (and optionally a listener) that should route to pgAdmin.
parentRefs: []
# - name: my-gateway
# namespace: gateway-system
# sectionName: https
# hostnames for the route. When empty, falls back to ingress.hostname.
hostnames: []
# - pgadmin4.example.com
annotations: {}
# Custom routing rules. When empty, a single rule forwards all traffic
# ("/" PathPrefix) to the pgAdmin service.
rules: []
startupProbe:
enabled: false
httpGet:
@@ -160,4 +180,3 @@ containerSecurityContext:
type: RuntimeDefault
windowsOptions:
hostProcess: false
+52 -7
View File
@@ -68,11 +68,48 @@ _create_python_virtualenv() {
mkdir -p "usr/${APP_NAME}"
cd "usr/${APP_NAME}" || exit
# Create the blank venv
"${SYSTEM_PYTHON_PATH}" -m venv --system-site-packages venv
# Capture the system site-packages dirs *before* creating the venv -
# we need OS-provided packages not available as clean pip wheels on
# every target platform (e.g. dbus-python: needs libdbus-1-dev to
# build from source, no reliable prebuilt wheel, but is a hard
# runtime dependency - see pkg/debian/build.sh's python3-dbus dep).
SYSTEM_SITE_DIRS=$("${SYSTEM_PYTHON_PATH}" -c \
"import site; print('\n'.join(site.getsitepackages()))")
# Create the blank venv WITHOUT --system-site-packages. That flag
# uses Python's site.addsitedir() under the hood, which doesn't just
# add the directory to sys.path - it also scans it for every *.pth
# file and executes any "import ..." lines found inside them. A
# stale, improperly-uninstalled package's namespace-package .pth
# hook (legacy pip/setuptools mechanism) left in the system
# site-packages runs arbitrary code at interpreter startup, before
# core stdlib is guaranteed to resolve. A leftover
# sphinxcontrib-jsmath nspkg.pth on the el-10 build node corrupted
# sys.path early enough to break pip's own subprocess bootstrap,
# failing the whole build with a misleading "No module named
# 'importlib'"/"'traceback'" error (pgadmin4-rpm-build #176/#177).
#
# We can't just delete that file - it's in a root-owned system
# directory on a build node shared by other jobs, and the build
# user isn't guaranteed to have write access there (confirmed: our
# first attempt at deleting it silently no-opped on el-10).
#
# Instead, get the same "OS-provided packages are importable" result
# via a mechanism that doesn't trigger a .pth scan: a *plain path
# line* (no "import") in a .pth file only appends that directory to
# sys.path - it does NOT recursively scan it for further .pth files.
# Write the system site-packages dirs as plain lines into a .pth
# file inside the venv's own site-packages (which we do own), added
# once pip/wheel are installed below.
"${SYSTEM_PYTHON_PATH}" -m venv venv
# shellcheck disable=SC1091
. venv/bin/activate
VENV_SITE_PACKAGES=$(python3 -c \
"import sysconfig; print(sysconfig.get_path('purelib'))")
echo "${SYSTEM_SITE_DIRS}" > \
"${VENV_SITE_PACKAGES}/system-site-packages.pth"
# Make sure we have the wheel package present, as well as the latest pip
pip3 install --upgrade pip
pip3 install wheel
@@ -209,8 +246,12 @@ _build_runtime() {
# Install the runtime node_modules
pushd "${BUNDLEDIR}/resources/app" > /dev/null || exit
yarn set version berry
yarn set version 4
YARN_VERSION=$(node -p "require('./package.json').packageManager.split('@')[1]")
if [ -z "${YARN_VERSION}" ]; then
echo "ERROR: Could not determine Yarn version from package.json packageManager field."
exit 1
fi
yarn set version "${YARN_VERSION}"
yarn workspaces focus --production
# remove the yarn cache
@@ -257,8 +298,12 @@ _copy_code() {
find "${SERVERROOT}/usr/${APP_NAME}/venv/" -name "_tkinter*" -print0 | xargs -0 rm -rf
pushd "${SOURCEDIR}/web" > /dev/null || exit
yarn set version berry
yarn set version 4
YARN_VERSION=$(node -p "require('./package.json').packageManager.split('@')[1]")
if [ -z "${YARN_VERSION}" ]; then
echo "ERROR: Could not determine Yarn version from package.json packageManager field."
exit 1
fi
yarn set version "${YARN_VERSION}"
yarn install
yarn run bundle
popd > /dev/null || exit
@@ -268,7 +313,7 @@ _copy_code() {
cp "${SOURCEDIR}/pkg/linux/config_distro.py" "${SERVERROOT}/usr/${APP_NAME}/web/"
cd "${SERVERROOT}/usr/${APP_NAME}/web/" || exit
rm -f pgadmin4.db config_local.*
rm -rf jest.config.js babel.* package.json .yarn* yarn* .editorconfig .eslint* node_modules/ regression/ tools/ pgadmin/static/js/generated/.cache
rm -rf jest.config.js babel.* package.json .yarn* yarn* webpack.* .editorconfig .eslint* node_modules/ regression/ tools/ pgadmin/static/js/generated/.cache
find . -name "tests" -type d -print0 | xargs -0 rm -rf
find . -name "feature_tests" -type d -print0 | xargs -0 rm -rf
find . -name "__pycache__" -type d -print0 | xargs -0 rm -rf
+1 -1
View File
@@ -13,7 +13,7 @@ Either build the sources or get them from macports or similar:
## Building
1. To bundle a different version of Python from the default of 3.13.1, set the
1. To bundle a different version of Python from the default of 3.14.7, set the
*PGADMIN_PYTHON_VERSION* environment variable, e.g:
export PGADMIN_PYTHON_VERSION=3.13.11
+84 -6
View File
@@ -66,8 +66,12 @@ _build_runtime() {
# Install the runtime node_modules, then replace the package.json
pushd "${BUNDLE_DIR}/Contents/Resources/app/" > /dev/null || exit
yarn set version berry
yarn set version 4
YARN_VERSION=$(node -p "require('./package.json').packageManager.split('@')[1]")
if [ -z "${YARN_VERSION}" ]; then
echo "ERROR: Could not determine Yarn version from package.json packageManager field."
exit 1
fi
yarn set version "${YARN_VERSION}"
yarn workspaces focus --production
# remove the yarn cache
@@ -298,8 +302,12 @@ _complete_bundle() {
# Build node modules
pushd "${SOURCE_DIR}/web" > /dev/null || exit
yarn set version berry
yarn set version 4
YARN_VERSION=$(node -p "require('./package.json').packageManager.split('@')[1]")
if [ -z "${YARN_VERSION}" ]; then
echo "ERROR: Could not determine Yarn version from package.json packageManager field."
exit 1
fi
yarn set version "${YARN_VERSION}"
yarn install 2>&1
# Record the source commit hash before the heavy lint/webpack
@@ -338,7 +346,7 @@ _complete_bundle() {
cp -r "${SOURCE_DIR}/web" "${BUNDLE_DIR}/Contents/Resources/"
cd "${BUNDLE_DIR}/Contents/Resources/web" || exit
rm -f pgadmin4.db config_local.*
rm -rf jest.config.js babel.* package.json .yarn* yarn* .editorconfig .eslint* node_modules/ regression/ tools/ pgadmin/static/js/generated/.cache
rm -rf jest.config.js babel.* package.json .yarn* yarn* webpack.* .editorconfig .eslint* node_modules/ regression/ tools/ pgadmin/static/js/generated/.cache
find . -name "tests" -type d -print0 | xargs -0 rm -rf
find . -name "feature_tests" -type d -print0 | xargs -0 rm -rf
find . -name "__pycache__" -type d -print0 | xargs -0 rm -rf
@@ -457,6 +465,71 @@ _prune_dangling_symlinks() {
fi
}
_verify_bundle_linkage() {
# Belt and braces: make sure nothing in the finished bundle links against
# a library that lives outside it on the build host. Such a reference
# resolves on the build machine but the path is absent on an end user's
# Mac (or, for a Homebrew dylib, present-but-rejected by hardened-runtime
# library validation for having a different Team ID), so the app dies on
# startup before it can even import config.
#
# The known offender is the Python cryptography module. When no binary
# wheel is published for the build architecture (cryptography 49 dropped
# the Intel/universal2 macOS wheel, leaving arm64 only), pip compiles it
# from source, and its openssl-sys crate links whatever OpenSSL it
# discovers on the build host rather than the one we ship: it ignores the
# CFLAGS/LDFLAGS the build exports and falls back to Homebrew, baking
# e.g. /usr/local/opt/openssl@3/lib/libssl.3.dylib into _rust.abi3.so.
# _fixup_imports deliberately skips _rust.abi3.so, so the dangling
# reference would otherwise sail through to a shipped DMG. See issue
# #10123.
#
# Everything legitimate in the bundle is either an OS library (/usr/lib,
# /System) or a bundle-relative reference (@loader_path, @rpath,
# @executable_path), so any absolute install-name under a build-host
# prefix is, by definition, wrong.
#
# NB: run after _complete_bundle (which does the install-name rewriting
# via _fixup_imports) so we validate the final, relocated state.
echo "Verifying bundle library references..."
# Build-host prefixes that must never appear in a shipped bundle. SLAVE_HOME
# (the Jenkins workspace root, under which the self-built OpenSSL and
# PostgreSQL live) is only added when set, i.e. on the CI builders.
local PREFIXES='/usr/local|/opt/homebrew|/opt/local'
if [ -n "${SLAVE_HOME}" ]; then
PREFIXES="${PREFIXES}|${SLAVE_HOME}"
fi
local found="" f deps
while IFS= read -r f; do
# otool prints the filename header then one line per dependency; drop
# the header, take the install-name column, and keep only build-host
# paths. grep exits non-zero (no match) for a clean binary, which is
# the common case, so fall through to the next file.
deps=$(otool -L "${f}" 2>/dev/null | tail -n +2 | awk '{print $1}' | \
grep -E "^(${PREFIXES})") || continue
echo "ERROR: ${f} links against build-host libraries:" >&2
echo "${deps}" | sed 's/^/ /' >&2
found="yes"
done < <(find "${BUNDLE_DIR}" -type f -exec file "{}" \; | \
grep -v "(for architecture" | \
grep -E "Mach-O executable|Mach-O 64-bit executable|Mach-O 64-bit bundle|Mach-O 64-bit dynamically linked shared library" | \
awk -F":" '{print $1}' | uniq)
if [ -n "${found}" ]; then
echo "ERROR: the bundle links against libraries outside it; those paths" >&2
echo " will not exist (or will fail library validation) on end-user" >&2
echo " machines and the app will not start. See issue #10123." >&2
echo " Ensure the affected module links the bundled OpenSSL, e.g." >&2
echo " set OPENSSL_DIR (and OPENSSL_STATIC) for the cryptography" >&2
echo " build so openssl-sys does not pick up Homebrew's copy." >&2
exit 1
fi
echo "Bundle library references OK."
}
_generate_sbom() {
echo "Generating SBOM..."
syft "${BUNDLE_DIR}/Contents/" -o cyclonedx-json > "${BUNDLE_DIR}/Contents/sbom.json"
@@ -615,7 +688,12 @@ _notarize_pkg() {
awk -F ': ' '/status:/ { print $2; }')
if [[ "${REQUEST_STATUS}" != "Accepted" ]]; then
echo "Notarization failed."
echo "Notarization failed with status: ${REQUEST_STATUS}"
echo "Fetching notary log for details..."
xcrun notarytool log "${SUBMISSION_ID}" \
--team-id "${DEVELOPER_TEAM_ID}" \
--apple-id "${DEVELOPER_USER}" \
--password "${DEVELOPER_ASP}"
exit 1
fi
+3 -2
View File
@@ -53,8 +53,8 @@ if [ "${PGADMIN_POSTGRES_DIR}" == "" ]; then
fi
if [ "${PGADMIN_PYTHON_VERSION}" == "" ]; then
echo "PGADMIN_PYTHON_VERSION not set. Setting it to the default: 3.13.11"
export PGADMIN_PYTHON_VERSION=3.13.11
echo "PGADMIN_PYTHON_VERSION not set. Setting it to the default: 3.14.7"
export PGADMIN_PYTHON_VERSION=3.14.7
fi
# Initialize variables
@@ -94,6 +94,7 @@ _build_docs
_complete_bundle
_strip_architecture
_prune_dangling_symlinks
_verify_bundle_linkage
_generate_sbom
_codesign_binaries
_codesign_bundle
+15 -2
View File
@@ -56,8 +56,12 @@ do
tar cf - "${FILE}" | (cd ../pip-build/pgadmin4; tar xf -)
done
yarn set version berry
yarn set version 4
YARN_VERSION=$(node -p "require('./package.json').packageManager.split('@')[1]")
if [ -z "${YARN_VERSION}" ]; then
echo "ERROR: Could not determine Yarn version from package.json packageManager field."
exit 1
fi
yarn set version "${YARN_VERSION}"
yarn install
yarn run bundle
@@ -72,6 +76,15 @@ do
tar cf - "${FILE}" | (cd ../pip-build/pgadmin4; tar xf -)
done
# Remove the JavaScript build configuration from the tree we're shipping. None
# of it is used at runtime (only the bundle under pgadmin/static/js/generated
# is), and leaving yarn.lock in place means vulnerability scanners report the
# entire build-time dependency tree against the installed package. This happens
# before the SBOM is generated so that the SBOM describes what actually ships.
echo Removing the JavaScript build configuration...
(cd ../pip-build/pgadmin4 && rm -rf jest.config.js babel.* package.json \
.yarn* yarn* webpack.* .editorconfig .eslint*)
cd ../docs || exit
for FILE in $(git ls-files)
do
+2 -1
View File
@@ -84,7 +84,8 @@ setup(
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13'
'Programming Language :: Python :: 3.13',
'Programming Language :: Python :: 3.14'
],
keywords='pgadmin4,postgresql,postgres',
+2 -2
View File
@@ -204,7 +204,7 @@ examples shown below are the defaults for the build system, so if they match
your requirements you don't need to set them. For PostgreSQL 16 and below:
SET "PGADMIN_POSTGRES_DIR=C:\build64\pgsql"
SET "PGADMIN_PYTHON_DIR=C:\Python313"
SET "PGADMIN_PYTHON_DIR=C:\Python314"
SET "PGADMIN_KRB5_DIR=C:\build64\krb5"
SET "PGADMIN_INNOTOOL_DIR=C:\Program Files (x86)\Inno Setup 6"
SET "PGADMIN_SIGNTOOL_DIR=C:\Program Files (x86)\Windows Kits\10\bin\10.0.17763.0\x64"
@@ -214,7 +214,7 @@ your requirements you don't need to set them. For PostgreSQL 16 and below:
For PostgreSQL 17 and later:
SET "PGADMIN_POSTGRES_DIR=C:\build64"
SET "PGADMIN_PYTHON_DIR=C:\Python313"
SET "PGADMIN_PYTHON_DIR=C:\Python314"
SET "PGADMIN_KRB5_DIR=C:\build64"
SET "PGADMIN_INNOTOOL_DIR=C:\Program Files (x86)\Inno Setup 6"
SET "PGADMIN_SIGNTOOL_DIR=C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64"
+15 -10
View File
@@ -13,16 +13,19 @@
###############################################################################
Authlib==1.6.*; python_version <= '3.9'
Authlib==1.7.*; python_version > '3.9'
Authlib==1.8.*; python_version > '3.9'
azure-identity==1.25.3
azure-mgmt-rdbms==10.1.1
azure-mgmt-resource==25.0.0
# Azure CLI (a common companion package on Linux distros) still pins
# azure-mgmt-resource<25, so keep the lower bound at 24.0.0 to avoid
# conflicts for distro packagers; see pgadmin-org/pgadmin4#10247.
azure-mgmt-resource>=24.0.0,<26.0.0
azure-mgmt-subscription==3.1.1
bcrypt==5.0.*
boto3==1.42.*; python_version <= '3.9'
boto3==1.43.*; python_version > '3.9'
certifi==2026.5.20
cryptography==49.0.*
certifi==2026.7.22
cryptography==50.0.*
Flask-Babel==4.0.*
Flask-Compress==1.*
Flask-Login==0.*
@@ -30,7 +33,10 @@ Flask-Mail==0.*
Flask-Migrate==4.*
Flask-Paranoid==0.*
Flask-Security-Too==5.6.*; python_version <= '3.9'
Flask-Security-Too==5.8.*; python_version > '3.9'
# 5.8.2 corrected the inverted UserMixin.is_locked() test in LoginForm;
# User.is_locked() in web/pgadmin/model/__init__.py follows the fixed
# semantics, so anything earlier in the 5.8 series would refuse all logins.
Flask-Security-Too>=5.8.2,<5.9; python_version > '3.9'
Flask-SocketIO==5.6.*
Flask-SQLAlchemy==3.1.*
Flask-WTF==1.2.*; python_version <= '3.9'
@@ -38,14 +44,13 @@ Flask-WTF==1.3.*; python_version > '3.9'
Flask==3.1.*
google-api-python-client==2.*
google-auth-oauthlib==1.3.1; python_version <= '3.9'
google-auth-oauthlib==1.4.0; python_version > '3.9'
gssapi==1.11.*
google-auth-oauthlib==1.4.1; python_version > '3.9'
gssapi==1.12.*
jsonformatter~=0.3.4
keyring==25.*
ldap3==2.*
libgravatar==1.0.*
paramiko==3.5.1
passlib==1.*
psutil==7.2.*
psycopg[c]==3.2.13; python_version < '3.10'
psycopg[c]==3.3.4; python_version >= '3.10'
@@ -56,12 +61,12 @@ pytz==2026.*
pywinpty==2.0.*; sys_platform=="win32"
qrcode[pil]==8.*
setuptools<82; python_version <= '3.9'
setuptools==82.*; python_version > '3.9'
setuptools==84.*; python_version > '3.9'
SQLAlchemy==2.*
sqlparse==0.*
sshtunnel==0.*
typer[all]==0.23.*; python_version <= '3.9'
typer==0.26.*; python_version > '3.9'
typer==0.27.*; python_version > '3.9'
urllib3==1.26.*; python_version <= '3.9'
urllib3==2.7.*; python_version > '3.9'
user-agents==2.2.0
+3 -1
View File
@@ -1,7 +1,9 @@
checksumBehavior: update
checksumBehavior: throw
logFilters:
- code: YN0013
level: discard
nodeLinker: node-modules
npmMinimalAgeGate: 3d
+10 -5
View File
@@ -10,17 +10,22 @@
"start": "electron .",
"linter": "yarn run eslint -c .eslintrc.js ."
},
"packageManager": "yarn@4.9.2",
"packageManager": "yarn@4.15.0",
"devDependencies": {
"@eslint/js": "^10.0.1",
"electron": "^42.3.3",
"eslint": "^10.4.1",
"electron": "^43.4.0",
"eslint": "^10.8.1",
"eslint-plugin-unused-imports": "^4.4.1",
"globals": "^17.6.0"
"globals": "^17.11.0"
},
"dependencies": {
"axios": "^1.18.0",
"axios": "^1.19.0",
"electron-context-menu": "^4.1.2",
"electron-store": "^11.0.2"
},
"resolutions": {
"brace-expansion@npm:^5.0.5": "^5.0.7",
"undici@npm:^7.24.4": "^7.28.0",
"form-data@npm:^4.0.5": "^4.0.6"
}
}
+64 -167
View File
@@ -2,9 +2,16 @@
# Manual changes might be lost - proceed with caution!
__metadata:
version: 8
version: 10
cacheKey: 10c0
"@electron-internal/extract-zip@npm:^1.0.1":
version: 1.0.4
resolution: "@electron-internal/extract-zip@npm:1.0.4"
checksum: 10c0/e841b38b08317ff5345e3454d0417c406d2258f0de5e654b55b49b28c46d3c0666febc5e14044a4ce110b8a9dd5d4d2b6d7e546a658d073094b3e335ca6db252
languageName: node
linkType: hard
"@electron/get@npm:^5.0.0":
version: 5.0.0
resolution: "@electron/get@npm:5.0.0"
@@ -52,12 +59,12 @@ __metadata:
languageName: node
linkType: hard
"@eslint/config-helpers@npm:^0.6.0":
version: 0.6.0
resolution: "@eslint/config-helpers@npm:0.6.0"
"@eslint/config-helpers@npm:^0.7.0":
version: 0.7.0
resolution: "@eslint/config-helpers@npm:0.7.0"
dependencies:
"@eslint/core": "npm:^1.2.1"
checksum: 10c0/f9af20e8b60b0ba27edb74b8eb40c0c5d51a9bf9baf9e053bb57833a87cb0a1c49b4dfaad88fc24d49c907ad1324c8a0b668684fa9c321351dac4bc9155ec10a
checksum: 10c0/fd40d57d6f1db49f7b647048b88a433dc7f6522ef3edf855a43cb526ef4fc40622ceed0dc8de2e03d254f30f8e035370570de1d4bd8e7c2b1200131451e0d331
languageName: node
linkType: hard
@@ -151,15 +158,6 @@ __metadata:
languageName: node
linkType: hard
"@types/node@npm:*":
version: 25.5.0
resolution: "@types/node@npm:25.5.0"
dependencies:
undici-types: "npm:~7.18.0"
checksum: 10c0/70c508165b6758c4f88d4f91abca526c3985eee1985503d4c2bd994dbaf588e52ac57e571160f18f117d76e963570ac82bd20e743c18987e82564312b3b62119
languageName: node
linkType: hard
"@types/node@npm:^24.9.0":
version: 24.12.0
resolution: "@types/node@npm:24.12.0"
@@ -169,15 +167,6 @@ __metadata:
languageName: node
linkType: hard
"@types/yauzl@npm:^2.9.1":
version: 2.10.3
resolution: "@types/yauzl@npm:2.10.3"
dependencies:
"@types/node": "npm:*"
checksum: 10c0/f1b7c1b99fef9f2fe7f1985ef7426d0cebe48cd031f1780fcdc7451eec7e31ac97028f16f50121a59bcf53086a1fc8c856fd5b7d3e00970e43d92ae27d6b43dc
languageName: node
linkType: hard
"acorn-jsx@npm:^5.3.2":
version: 5.3.2
resolution: "acorn-jsx@npm:5.3.2"
@@ -288,15 +277,15 @@ __metadata:
languageName: node
linkType: hard
"axios@npm:^1.18.0":
version: 1.18.0
resolution: "axios@npm:1.18.0"
"axios@npm:^1.19.0":
version: 1.19.0
resolution: "axios@npm:1.19.0"
dependencies:
follow-redirects: "npm:^1.16.0"
form-data: "npm:^4.0.5"
form-data: "npm:^4.0.6"
https-proxy-agent: "npm:^5.0.1"
proxy-from-env: "npm:^2.1.0"
checksum: 10c0/f88aae13fd9dd395dea3053c7299178fee62f436f20424a1d6d79ec11ffb7656ba664b4630defbadab2c387b6ea07e29afb9f28b26d8e0e49d45be766983290d
checksum: 10c0/559fe7d51291787def61566a3db78b87510c8faf9c8a8c006d9d8b933808628ff0d8eca7756b40ae25e07da96d946c68c1ffba3da9075ed0b5d3661801d76869
languageName: node
linkType: hard
@@ -307,19 +296,12 @@ __metadata:
languageName: node
linkType: hard
"brace-expansion@npm:^5.0.5":
version: 5.0.5
resolution: "brace-expansion@npm:5.0.5"
"brace-expansion@npm:^5.0.7":
version: 5.0.9
resolution: "brace-expansion@npm:5.0.9"
dependencies:
balanced-match: "npm:^4.0.2"
checksum: 10c0/4d238e14ed4f5cc9c07285550a41cef23121ca08ba99fa9eb5b55b580dcb6bf868b8210aa10526bdc9f8dc97f33ca2a7259039c4cc131a93042beddb424c48e3
languageName: node
linkType: hard
"buffer-crc32@npm:~0.2.3":
version: 0.2.13
resolution: "buffer-crc32@npm:0.2.13"
checksum: 10c0/cb0a8ddf5cf4f766466db63279e47761eb825693eeba6a5a95ee4ec8cb8f81ede70aa7f9d8aeec083e781d47154290eb5d4d26b3f7a465ec57fb9e7d59c47150
checksum: 10c0/3dea38884a1c3c8b1c9c44a7402a0c76fca460f70cffb3127242b0b4cbf4472019e022ade021eec44838ff19f1dac2625dfd11dd459d7e1e055b0698a8d52fec
languageName: node
linkType: hard
@@ -474,17 +456,17 @@ __metadata:
languageName: node
linkType: hard
"electron@npm:^42.3.3":
version: 42.3.3
resolution: "electron@npm:42.3.3"
"electron@npm:^43.4.0":
version: 43.4.0
resolution: "electron@npm:43.4.0"
dependencies:
"@electron-internal/extract-zip": "npm:^1.0.1"
"@electron/get": "npm:^5.0.0"
"@types/node": "npm:^24.9.0"
extract-zip: "npm:^2.0.1"
bin:
electron: cli.js
install-electron: install.js
checksum: 10c0/9fdb34c23097986e4ed9f328ec36356d78481133a17fdba78312be6dddb5e513f91e727a7802f91ff7302d86f794998085ef6fbc2c2b8ae9bc2d5f8b38711759
checksum: 10c0/8b03553d1a5d186d4d95f80195ff634f6afd4ea9db7e09fdc67f251f93e7e2447da9a233e05281289450335ad8a01630609ffb5f3467ab38e71b65be489c9342
languageName: node
linkType: hard
@@ -495,15 +477,6 @@ __metadata:
languageName: node
linkType: hard
"end-of-stream@npm:^1.1.0":
version: 1.4.5
resolution: "end-of-stream@npm:1.4.5"
dependencies:
once: "npm:^1.4.0"
checksum: 10c0/b0701c92a10b89afb1cb45bf54a5292c6f008d744eb4382fa559d54775ff31617d1d7bc3ef617575f552e24fad2c7c1a1835948c66b3f3a4be0a6c1f35c883d8
languageName: node
linkType: hard
"env-paths@npm:^3.0.0":
version: 3.0.0
resolution: "env-paths@npm:3.0.0"
@@ -606,14 +579,14 @@ __metadata:
languageName: node
linkType: hard
"eslint@npm:^10.4.1":
version: 10.4.1
resolution: "eslint@npm:10.4.1"
"eslint@npm:^10.8.1":
version: 10.8.1
resolution: "eslint@npm:10.8.1"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.8.0"
"@eslint-community/regexpp": "npm:^4.12.2"
"@eslint/config-array": "npm:^0.23.5"
"@eslint/config-helpers": "npm:^0.6.0"
"@eslint/config-helpers": "npm:^0.7.0"
"@eslint/core": "npm:^1.2.1"
"@eslint/plugin-kit": "npm:^0.7.2"
"@humanfs/node": "npm:^0.16.6"
@@ -637,7 +610,7 @@ __metadata:
imurmurhash: "npm:^0.1.4"
is-glob: "npm:^4.0.0"
json-stable-stringify-without-jsonify: "npm:^1.0.1"
minimatch: "npm:^10.2.4"
minimatch: "npm:^10.2.5"
natural-compare: "npm:^1.4.0"
optionator: "npm:^0.9.3"
peerDependencies:
@@ -647,7 +620,7 @@ __metadata:
optional: true
bin:
eslint: bin/eslint.js
checksum: 10c0/c5e6bb5158fb0d62f090c8e2671de5c98283bbb37a58b6d871bada63af3018e683483c96c1a92272fedc8f4e5279a273a0451acf0da67c487406639daa05aedb
checksum: 10c0/0c4720a43ef2052829db18e37aa79adb5f0a62137bdf4a4f8e9507bbbbf888ba0669fc03e4b7139d8f27b59b051bbbcd0a3988b068c30fa934a766d60a81f599
languageName: node
linkType: hard
@@ -713,23 +686,6 @@ __metadata:
languageName: node
linkType: hard
"extract-zip@npm:^2.0.1":
version: 2.0.1
resolution: "extract-zip@npm:2.0.1"
dependencies:
"@types/yauzl": "npm:^2.9.1"
debug: "npm:^4.1.1"
get-stream: "npm:^5.1.0"
yauzl: "npm:^2.10.0"
dependenciesMeta:
"@types/yauzl":
optional: true
bin:
extract-zip: cli.js
checksum: 10c0/9afbd46854aa15a857ae0341a63a92743a7b89c8779102c3b4ffc207516b2019337353962309f85c66ee3d9092202a83cdc26dbf449a11981272038443974aee
languageName: node
linkType: hard
"fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3":
version: 3.1.3
resolution: "fast-deep-equal@npm:3.1.3"
@@ -752,18 +708,9 @@ __metadata:
linkType: hard
"fast-uri@npm:^3.0.1":
version: 3.1.2
resolution: "fast-uri@npm:3.1.2"
checksum: 10c0/5b35641895959f3f7ab7a7b1b5542bded159346f25ec9f256817b206d50b64eda5828e90d605a2e2fc645c90519a7259c2bab2c942ee728c88b88e5be21b090d
languageName: node
linkType: hard
"fd-slicer@npm:~1.1.0":
version: 1.1.0
resolution: "fd-slicer@npm:1.1.0"
dependencies:
pend: "npm:~1.2.0"
checksum: 10c0/304dd70270298e3ffe3bcc05e6f7ade2511acc278bc52d025f8918b48b6aa3b77f10361bddfadfe2a28163f7af7adbdce96f4d22c31b2f648ba2901f0c5fc20e
version: 3.1.5
resolution: "fast-uri@npm:3.1.5"
checksum: 10c0/2bf60eb800dd610c65e17be436425dcb21c92aff3a87d442a8bccab0b7b071e88cf1a5d7d1ea946370b937e6fc0375c405c0296c10587e57de4f78be4646d1d0
languageName: node
linkType: hard
@@ -813,16 +760,16 @@ __metadata:
languageName: node
linkType: hard
"form-data@npm:^4.0.5":
version: 4.0.5
resolution: "form-data@npm:4.0.5"
"form-data@npm:^4.0.6":
version: 4.0.6
resolution: "form-data@npm:4.0.6"
dependencies:
asynckit: "npm:^0.4.0"
combined-stream: "npm:^1.0.8"
es-set-tostringtag: "npm:^2.1.0"
hasown: "npm:^2.0.2"
mime-types: "npm:^2.1.12"
checksum: 10c0/dd6b767ee0bbd6d84039db12a0fa5a2028160ffbfaba1800695713b46ae974a5f6e08b3356c3195137f8530dcd9dfcb5d5ae1eeff53d0db1e5aad863b619ce3b
hasown: "npm:^2.0.4"
mime-types: "npm:^2.1.35"
checksum: 10c0/43947a77bf0ff45c6ceed789778982d47a3f3e720a74b71721174ebf3310a5f1a8be1d6b38a3ee3688e8a18a2c4273073ec0844cd37efda3eaf46d41c9c318ff
languageName: node
linkType: hard
@@ -878,15 +825,6 @@ __metadata:
languageName: node
linkType: hard
"get-stream@npm:^5.1.0":
version: 5.2.0
resolution: "get-stream@npm:5.2.0"
dependencies:
pump: "npm:^3.0.0"
checksum: 10c0/43797ffd815fbb26685bf188c8cfebecb8af87b3925091dd7b9a9c915993293d78e3c9e1bce125928ff92f2d0796f3889b92b5ec6d58d1041b574682132e0a80
languageName: node
linkType: hard
"glob-parent@npm:^6.0.2":
version: 6.0.2
resolution: "glob-parent@npm:6.0.2"
@@ -896,10 +834,10 @@ __metadata:
languageName: node
linkType: hard
"globals@npm:^17.6.0":
version: 17.6.0
resolution: "globals@npm:17.6.0"
checksum: 10c0/cf94fb4329cc5c68cf81018fd68324f413181ee169f0235b0b33b82bc93fe7825a21beea951f83a80e8e4bbdad9c0c80515a145b5fd4b5cb52f2a80db899a93f
"globals@npm:^17.11.0":
version: 17.11.0
resolution: "globals@npm:17.11.0"
checksum: 10c0/5e1be3d816e04d4d0d01b1cdd40e46b5fb6b5e9f15aece9a5919b060e0fb1b3f1b9e7327dce97ef19e05513a6d65872e0834999ddea251557556dddd0f5fde30
languageName: node
linkType: hard
@@ -942,6 +880,15 @@ __metadata:
languageName: node
linkType: hard
"hasown@npm:^2.0.4":
version: 2.0.4
resolution: "hasown@npm:2.0.4"
dependencies:
function-bind: "npm:^1.1.2"
checksum: 10c0/2d8de939e270b70618f8cebb69746620db10617dbb495bc66ddad326955ea24d3ca4af133aff3eb7c1853e0218f867bc2b050ec26fe02e3aea58f880ffc5e506
languageName: node
linkType: hard
"https-proxy-agent@npm:^5.0.1":
version: 5.0.1
resolution: "https-proxy-agent@npm:5.0.1"
@@ -1087,7 +1034,7 @@ __metadata:
languageName: node
linkType: hard
"mime-types@npm:^2.1.12":
"mime-types@npm:^2.1.35":
version: 2.1.35
resolution: "mime-types@npm:2.1.35"
dependencies:
@@ -1103,7 +1050,7 @@ __metadata:
languageName: node
linkType: hard
"minimatch@npm:^10.2.4":
"minimatch@npm:^10.2.4, minimatch@npm:^10.2.5":
version: 10.2.5
resolution: "minimatch@npm:10.2.5"
dependencies:
@@ -1126,15 +1073,6 @@ __metadata:
languageName: node
linkType: hard
"once@npm:^1.3.1, once@npm:^1.4.0":
version: 1.4.0
resolution: "once@npm:1.4.0"
dependencies:
wrappy: "npm:1"
checksum: 10c0/5d48aca287dfefabd756621c5dfce5c91a549a93e9fdb7b8246bc4c4790aa2ec17b34a260530474635147aeb631a2dcc8b32c613df0675f96041cbb8244517d0
languageName: node
linkType: hard
"optionator@npm:^0.9.3":
version: 0.9.4
resolution: "optionator@npm:0.9.4"
@@ -1188,25 +1126,18 @@ __metadata:
languageName: node
linkType: hard
"pend@npm:~1.2.0":
version: 1.2.0
resolution: "pend@npm:1.2.0"
checksum: 10c0/8a87e63f7a4afcfb0f9f77b39bb92374afc723418b9cb716ee4257689224171002e07768eeade4ecd0e86f1fa3d8f022994219fb45634f2dbd78c6803e452458
languageName: node
linkType: hard
"pgadmin4@workspace:.":
version: 0.0.0-use.local
resolution: "pgadmin4@workspace:."
dependencies:
"@eslint/js": "npm:^10.0.1"
axios: "npm:^1.18.0"
electron: "npm:^42.3.3"
axios: "npm:^1.19.0"
electron: "npm:^43.4.0"
electron-context-menu: "npm:^4.1.2"
electron-store: "npm:^11.0.2"
eslint: "npm:^10.4.1"
eslint: "npm:^10.8.1"
eslint-plugin-unused-imports: "npm:^4.4.1"
globals: "npm:^17.6.0"
globals: "npm:^17.11.0"
languageName: unknown
linkType: soft
@@ -1231,16 +1162,6 @@ __metadata:
languageName: node
linkType: hard
"pump@npm:^3.0.0":
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/2780e66b5471c19e3e3e1063b84f3f6a3a08367f24c5ed552f98cd5901e6ada27c7ad6495d4244f553fd03b01884a4561933064f053f47c8994d84fd352768ea
languageName: node
linkType: hard
"punycode@npm:^2.1.0":
version: 2.3.1
resolution: "punycode@npm:2.3.1"
@@ -1410,17 +1331,10 @@ __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
"undici@npm:^7.24.4":
version: 7.25.0
resolution: "undici@npm:7.25.0"
checksum: 10c0/02a0b45dc14eb91bc488948750232450fe52f27a6b08086d6ac6736bb47908d600fe3a96d346f12eab24729c782e5c2f693bc8e8eca6696d4e4c09b1ed4cb4ec
"undici@npm:^7.28.0":
version: 7.29.0
resolution: "undici@npm:7.29.0"
checksum: 10c0/85ea96e91e7f3de24de678cbbc54230fdd641a3b0643005f2aef044af1f1a2db8fbecb88b28e6fa78bb41ffeafda4479b57075b6b6ab92beac4b7b5ab53f9b10
languageName: node
linkType: hard
@@ -1468,23 +1382,6 @@ __metadata:
languageName: node
linkType: hard
"wrappy@npm:1":
version: 1.0.2
resolution: "wrappy@npm:1.0.2"
checksum: 10c0/56fece1a4018c6a6c8e28fbc88c87e0fbf4ea8fd64fc6c63b18f4acc4bd13e0ad2515189786dd2c30d3eec9663d70f4ecf699330002f8ccb547e4a18231fc9f0
languageName: node
linkType: hard
"yauzl@npm:^2.10.0":
version: 2.10.0
resolution: "yauzl@npm:2.10.0"
dependencies:
buffer-crc32: "npm:~0.2.3"
fd-slicer: "npm:~1.1.0"
checksum: 10c0/f265002af7541b9ec3589a27f5fb8f11cf348b53cc15e2751272e3c062cd73f3e715bc72d43257de71bbaecae446c3f1b14af7559e8ab0261625375541816422
languageName: node
linkType: hard
"yocto-queue@npm:^0.1.0":
version: 0.1.0
resolution: "yocto-queue@npm:0.1.0"
+4 -4
View File
@@ -1,12 +1,12 @@
approvedGitRepositories:
- "**"
- "https://github.com/pgadmin-org/react-data-grid.git"
checksumBehavior: update
enableScripts: true
checksumBehavior: throw
logFilters:
- code: YN0013
level: discard
nodeLinker: node-modules
npmMinimalAgeGate: 3d
+85 -2
View File
@@ -144,8 +144,40 @@ CROSS_ORIGIN_OPENER_POLICY = "same-origin"
# such as JavaScript, CSS, or pretty much anything that the browser loads.
# see https://content-security-policy.com/#source_list for more info
# e.g. "default-src https: data: 'unsafe-inline' 'unsafe-eval';"
CONTENT_SECURITY_POLICY = "default-src ws: http: data: blob: 'unsafe-inline'" \
" 'unsafe-eval';"
#
# A per-request nonce is used in place of 'unsafe-inline' for scripts. The
# literal token {nonce} anywhere in the policy is replaced at runtime with a
# freshly generated nonce that is also emitted on pgAdmin's inline <script>
# tags. To go back to the old permissive policy, set:
# CONTENT_SECURITY_POLICY = "default-src ws: http: data: blob:" \
# " 'unsafe-inline' 'unsafe-eval';"
# Notes:
# - 'unsafe-inline' is retained for style-src because pgAdmin's UI (React/MUI)
# injects runtime styles and inline style="" attributes that are not (and
# cannot be) nonce tagged. WARNING: do NOT add 'nonce-{nonce}' to style-src
# to tighten this -- per the CSP spec a nonce silently DISABLES
# 'unsafe-inline', which blocks all of MUI's runtime styles and leaves the
# UI unstyled.
# - 'unsafe-eval' is NOT listed: production bundles do not need it. This holds
# only because JsonEditor.jsx passes neither 'queryLanguages' nor a
# 'validator' to vanilla-jsoneditor: its default query language (JSONQuery)
# composes closures rather than evaluating source, so the eval-capable
# paths (jsonpath-plus, ajv's runtime compiler and the Lodash query
# language) stay unreachable. Enabling schema validation or registering
# another query language would make 'unsafe-eval' necessary again.
# - Development webpack bundles ARE built with the 'eval' devtool and need
# 'unsafe-eval'. security_headers.py adds it automatically when DEBUG is
# True (for a nonce based policy), so no manual change is needed for the
# dev server. Security note: turning DEBUG on in a server-mode deployment
# therefore relaxes the policy (drops eval protection), and this is not
# logged.
CONTENT_SECURITY_POLICY = (
"default-src 'self' ws: http: data: blob:;"
" script-src 'self' 'nonce-{nonce}';"
" style-src 'self' 'unsafe-inline';"
" object-src 'none';"
" base-uri 'self';"
)
# STRICT_TRANSPORT_SECURITY_ENABLED when set to True will set the
# Strict-Transport-Security header
@@ -301,6 +333,9 @@ else:
LOG_ROTATION_SIZE = 10 # In MBs
LOG_ROTATION_AGE = 1440 # In minutes
LOG_ROTATION_MAX_LOG_FILES = 90 # Maximum number of backups to retain
# Include the authenticated username in the X-Remote-User response header so
# it can be captured in the HTTP access log. Disabled by default.
LOG_AUTHENTICATED_USER = False
##########################################################################
# Server Connection Driver Settings
##########################################################################
@@ -543,6 +578,26 @@ SQLALCHEMY_TRACK_MODIFICATIONS = False
##########################################################################
DATA_RESULT_ROWS_PER_PAGE = 1000
##########################################################################
# System-wide default for the Geometry Viewer's custom tile provider
# (Query Tool > Data Output > Geometry Viewer). Applies to any user who
# has not saved their own "Custom tile provider ..." preference; a
# per-user preference, once set, always takes precedence over this
# default. Leave "url" empty to keep using the built-in tile layers by
# default (the pre-existing behaviour).
#
# "crs" must be one of "EPSG:3857" (Web Mercator), "EPSG:4326", or
# "EPSG:3395" - the only coordinate reference systems the Geometry
# Viewer's tile layer selector supports.
##########################################################################
DEFAULT_GEOMETRY_VIEWER_PROVIDER = {
"url": "",
"name": "Custom",
"crs": "EPSG:3857",
"attribution": "",
"max_zoom": 18
}
##########################################################################
# Allow users to display Gravatar image for their username in Server mode
##########################################################################
@@ -633,6 +688,14 @@ USE_OS_SECRET_STORAGE = True
# You can pass the current username as an argument to the external script
# by specifying %u in config value.
# E.g. - MASTER_PASSWORD_HOOK = '<PATH>/passwdgen_script.sh %u'
#
# The command is split into arguments and executed directly, without a
# shell, so shell features (pipes, redirection, environment-variable
# expansion, globbing) in this value are not interpreted; put any such
# logic inside the hook script itself. On Windows, a batch file must be
# invoked via an executable wrapper rather than directly. If the path to
# the script/program contains spaces, quote it, e.g. -
# MASTER_PASSWORD_HOOK = '"<PATH WITH SPACES>/passwdgen_script.exe" %u'
##########################################################################
MASTER_PASSWORD_HOOK = None
@@ -896,6 +959,26 @@ WEBSERVER_AUTO_CREATE_USER = True
WEBSERVER_REMOTE_USER = 'REMOTE_USER'
# Accept the remote user identity from an inbound HTTP request header (not
# just the WSGI/CGI environment). This is required only for reverse proxies
# that pass the identity as a header rather than setting the REMOTE_USER
# CGI variable.
# SECURITY: only enable this when the proxy in front of pgAdmin OVERWRITES
# this header on every request before it reaches pgAdmin; otherwise any
# client that can reach pgAdmin can assert any identity it likes.
WEBSERVER_REMOTE_USER_FROM_HEADER = False
# IP addresses/CIDR ranges of the reverse proxies that are allowed to assert
# the identity header above. An empty list means no peer is trusted, so
# WEBSERVER_REMOTE_USER_FROM_HEADER stays inert even if enabled.
WEBSERVER_TRUSTED_PROXIES = []
# Optional shared secret that the trusted proxy must inject into the request
# (in the header named by WEBSERVER_SHARED_SECRET_HEADER) for the header-based
# identity to be accepted. Leave as None to skip this additional check.
WEBSERVER_SHARED_SECRET = None
WEBSERVER_SHARED_SECRET_HEADER = 'X-Pgadmin-Webserver-Secret'
##########################################################################
# Two-factor Authentication Configuration
##########################################################################
+82 -69
View File
@@ -7,46 +7,47 @@
],
"license": "PostgreSQL",
"devDependencies": {
"@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",
"@babel/core": "^7.29.7",
"@babel/eslint-parser": "^7.29.7",
"@babel/eslint-plugin": "^7.29.7",
"@babel/plugin-syntax-jsx": "^7.29.7",
"@babel/plugin-transform-class-properties": "^7.29.7",
"@babel/plugin-transform-object-rest-spread": "^7.29.7",
"@babel/plugin-transform-runtime": "^7.29.7",
"@babel/preset-env": "^7.29.7",
"@babel/preset-react": "^7.29.7",
"@babel/preset-typescript": "^7.29.7",
"@emotion/memoize": "^0.9.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@emotion/utils": "^1.4.2",
"@svgr/webpack": "^8.1.0",
"@testing-library/dom": "10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "16.3.2",
"@testing-library/user-event": "^14.6.1",
"@testing-library/jest-dom": "^6.10.0",
"@testing-library/react": "16.3.3",
"@testing-library/user-event": "^14.6.3",
"@types/jest": "^30.0.0",
"autoprefixer": "^10.5.0",
"autoprefixer": "^10.5.4",
"axios-mock-adapter": "^2.1.0",
"babel-loader": "^10.1.1",
"browserify": "^17.0.1",
"buffer": "^6.0.3",
"copy-webpack-plugin": "^14.0.0",
"core-js": "^3.50.0",
"cross-env": "^10.1.0",
"css-loader": "^7.1.4",
"css-loader": "^7.1.5",
"css-minimizer-webpack-plugin": "^8.0.0",
"eslint": "^9.39.4",
"eslint-plugin-jest": "^29.15.2",
"eslint": "^9.39.5",
"eslint-plugin-jest": "^29.16.6",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-unused-imports": "^4.4.1",
"exports-loader": "^5.0.0",
"globals": "^17.5.0",
"globals": "^17.12.0",
"html-react-parser": "^5.2.17",
"image-minimizer-webpack-plugin": "^4.1.4",
"image-minimizer-webpack-plugin": "^5.0.0",
"imports-loader": "^5.0.0",
"jest": "^30.3.0",
"jest-environment-jsdom": "^30.3.0",
"jest": "^30.5.1",
"jest-environment-jsdom": "^30.5.1",
"loader-utils": "^3.3.1",
"mini-css-extract-plugin": "^2.10.2",
"postcss-loader": "^8.2.1",
@@ -56,86 +57,86 @@
"shim-loader": "^1.0.1",
"style-loader": "^4.0.0",
"stylis": "^4.4.0",
"svgo": "^4.0.1",
"svgo-loader": "^4.0.0",
"terser-webpack-plugin": "^5.5.0",
"svgo": "4.1.0",
"svgo-loader": "^5.0.0",
"terser-webpack-plugin": "^5.6.1",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.0",
"typescript-eslint": "^8.69.0",
"url-loader": "^4.1.1",
"webfonts-loader": "^8.1.1",
"webpack": "^5.106.2",
"webpack-bundle-analyzer": "^5.3.0",
"webpack": "^5.108.4",
"webpack-bundle-analyzer": "^5.3.2",
"webpack-cli": "^6.0.1",
"yarn-audit-html": "7.3.2"
},
"optionalDependencies": {
"sharp": "^0.34.4"
"sharp": "^0.35.4"
},
"dependencies": {
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-sql": "^6.10.0",
"@date-io/core": "^3.2.0",
"@date-io/date-fns": "3.x",
"@date-io/date-fns": "^3.2.1",
"@emotion/sheet": "^1.4.0",
"@fluentui/keyboard-key": "^0.4.23",
"@fortawesome/fontawesome-free": "latest",
"@mui/icons-material": "^7.3.10",
"@mui/material": "^7.3.10",
"@mui/x-date-pickers": "^8.28.3",
"@fortawesome/fontawesome-free": "7.3.1",
"@mui/icons-material": "^7.3.11",
"@mui/material": "^7.3.11",
"@mui/x-date-pickers": "^8.29.3",
"@nozbe/microfuzz": "^1.0.0",
"@projectstorm/react-diagrams": "^7.0.4",
"@simonwep/pickr": "^1.9.1",
"@szhsin/react-menu": "^4.5.1",
"@tanstack/react-query": "^5.100.9",
"@simonwep/pickr": "~1.9.1",
"@szhsin/react-menu": "^4.5.2",
"@tanstack/react-query": "^5.102.8",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.24",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@tanstack/react-virtual": "^3.14.10",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.5",
"@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.18.0",
"anti-trojan-source": "^1.8.1",
"ajv": "^8.20.0",
"anti-trojan-source": "^1.12.2",
"aspen-decorations": "^1.1.1",
"axios": "^1.16.0",
"axios": "^1.20.0",
"babelify": "~10.0.0",
"bignumber.js": "^10.0.2",
"brace": "^0.11.1",
"chart.js": "^4.5.1",
"chartjs-plugin-zoom": "^2.2.0",
"codemirror": "^6.0.2",
"codemirror": "^6.65.7",
"convert-units": "^2.3.4",
"date-fns": "^4.1.0",
"diff-arrays-of-objects": "^1.1.10",
"dompurify": "^3.4.1",
"hotkeys-js": "^4.0.3",
"html-to-image": "1.11.11",
"date-fns": "^4.4.0",
"diff-arrays-of-objects": "^1.1.11",
"dompurify": "^3.4.14",
"hotkeys-js": "^4.0.7",
"html-to-image": "1.11.13",
"immutability-helper": "^3.1.1",
"insert-if": "^1.2.0",
"ip-address": "^10.1.1",
"ip-address": "^10.7.0",
"json-bignumber": "^1.1.1",
"leaflet": "^1.9.4",
"lodash": "4.*",
"lossless-json": "^4.3.0",
"marked": "^18.0.2",
"lossless-json": "^4.3.1",
"marked": "^18.0.11",
"moment": "^2.30.1",
"moment-timezone": "^0.6.2",
"moment-timezone": "^0.6.3",
"notificar": "^1.0.1",
"notistack": "^3.0.2",
"papaparse": "^5.5.3",
"papaparse": "^5.7.0",
"path-fx": "^2.1.2",
"postcss": "^8.5.14",
"rc-dock": "^4.0.0-alpha.2",
"react": "^19.2.5",
"react-arborist": "^3.5.0",
"postcss": "^8.5.26",
"rc-dock": "^4.0.0-alpha.3",
"react": "^19.2.8",
"react-arborist": "^3.16.0",
"react-aspen": "^1.2.0",
"react-checkbox-tree": "^2.0.1",
"react-checkbox-tree": "^2.0.2",
"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.5",
"react-draggable": "^4.5.0",
"react-dom": "^19.2.8",
"react-draggable": "^4.7.1",
"react-dropzone": "^15.0.0",
"react-frame-component": "~5.2.6",
"react-leaflet": "^4.2.1",
@@ -143,31 +144,43 @@
"react-resize-detector": "^12.3.0",
"react-rnd": "^10.5.3",
"react-select": "^5.10.2",
"react-timer-hook": "^4.0.5",
"react-timer-hook": "^4.0.6",
"react-virtualized-auto-sizer": "^2.0.3",
"snapsvg-cjs": "^0.0.6",
"socket.io-client": "^4.8.3",
"split.js": "^1.6.5",
"sql-formatter": "^15.7.3",
"sql-formatter": "^15.8.2",
"uplot": "^1.6.32",
"uplot-react": "^1.2.4",
"use-resize-observer": "^9.1.0",
"use-resize-observer": "^10.0.0",
"valid-filename": "^4.0.0",
"vanilla-jsoneditor": "^3.12.0",
"vanilla-jsoneditor": "^3.13.0",
"wkx": "^0.5.0",
"zustand": "^5.0.12"
"zustand": "^5.0.15"
},
"dependenciesMeta": {
"ttf2woff2": {
"built": true
},
"unrs-resolver": {
"built": true
}
},
"resolutions": {
"rc-resize-observer": "1.4.0",
"@xmldom/xmldom": "^0.8.13",
"serialize-javascript": "^7.0.5",
"ip-address": "^10.1.1",
"postcss": "^8.5.10",
"ip-address": "^10.3.1",
"postcss": "^8.5.23",
"ws": "^8.20.1",
"qs": "^6.15.2",
"@tootallnate/once": "^2.0.1",
"tar@npm:^7.5.4": "^7.5.15",
"ttf2woff2/node-gyp": "^11.2.0"
"tar@npm:^7.5.4": "^7.5.21",
"ttf2woff2/node-gyp": "^11.2.0",
"js-yaml@npm:^3.13.1": "^3.15.0",
"form-data@npm:^4.0.5": "^4.0.6",
"brace-expansion@npm:^1.1.7": "^1.1.16",
"brace-expansion@npm:^5.0.5": "^5.0.7"
},
"scripts": {
"linter": "yarn run eslint -c .eslintrc.js .",
+11
View File
@@ -8,12 +8,23 @@
# ##########################################################################
import json
import os
import sys
import time
from utils.io import debug, error, output
from utils.misc import get_my_ip, get_random_id
from providers._abstract import AbsProvider
# pgAdmin only authenticates to Google via google-auth, never the
# long-deprecated oauth2client. googleapiclient still tries to import
# oauth2client optionally, and on packaged installs the venv inherits the
# system site-packages (--system-site-packages, see issue #7173) where a
# stale oauth2client and an incompatible pyOpenSSL may be present. That
# optional import drags in the broken pyOpenSSL and crashes with "module
# 'lib' has no attribute 'GEN_EMAIL'". Blocking the module here makes
# googleapiclient fall back to google-auth cleanly. See issue #10110.
sys.modules.setdefault('oauth2client', None)
from googleapiclient import discovery
from googleapiclient.errors import HttpError
from google.auth.transport.requests import Request
+23
View File
@@ -210,6 +210,12 @@ def create_app(app_name=None):
# we don't want it to redirect to main page after password
# change operation so we will open the same password change page again.
config.SECURITY_POST_CHANGE_VIEW = 'browser.change_password'
else:
# Desktop mode: re-check the keyring backend picked at config-load
# time is actually usable, without blocking startup on a possible
# hang (see pgadmin/keyring_probe.py for why).
from pgadmin.utils.keyring_probe import start_async_probe
start_async_probe(config.__dict__)
"""Create the Flask application, startup logging and dynamically load
additional modules (blueprints) that are found in this directory."""
@@ -873,6 +879,20 @@ def create_app(app_name=None):
@app.after_request
def after_request(response):
if config.LOG_AUTHENTICATED_USER:
if current_user.is_authenticated and current_user.username:
# HTTP headers are latin-1 only, so transliterate anything
# outside that range to avoid gunicorn 500s for unicode names.
safe = current_user.username.encode(
'latin-1', 'replace').decode('latin-1')
# CR/LF and other control chars are valid latin-1 but Werkzeug
# rejects them in header values (would 500 every request for
# that user), so drop any non-printable characters too.
safe = ''.join(c for c in safe if c.isprintable())
response.headers['X-Remote-User'] = safe
else:
response.headers.pop('X-Remote-User', None)
if 'key' in request.args:
domain = dict()
if config.COOKIE_DEFAULT_DOMAIN and \
@@ -938,6 +958,9 @@ def create_app(app_name=None):
return {
'current_app': current_app,
'current_blueprint': current_blueprint,
# Per-request Content-Security-Policy nonce, for use on inline
# <script>/<style> tags when a nonce based CSP is configured.
'csp_nonce': SecurityHeaders.get_nonce(),
}
@app.errorhandler(Exception)
+34
View File
@@ -82,6 +82,40 @@ def socket_login_required(f):
return wrapped
def socket_permissions_required(*fsperms):
"""Socket.IO equivalent of flask_security's permissions_required.
Refuses the event unless the current user is authenticated and holds
all of the named pgAdmin permissions (via their roles). This mirrors
the HTTP @permissions_required decorator so that Socket.IO handlers
enforce the same tool-level RBAC as the routes: an authenticated user
must not be able to reach a sensitive handler (e.g. the schema diff
comparison or the PSQL pseudo-terminal) for a tool whose permission
they have been denied.
has_permission() is used in preference to flask_principal's
Permission().can() because it reads the user's roles directly and
does not depend on the principal identity having been loaded onto the
socket request context; it also honours pgAdmin's Administrator
bypass (see CustomUserMixin).
"""
def wrapper(f):
@functools.wraps(f)
def wrapped(*args, **kwargs):
if not current_user.is_authenticated:
disconnect()
raise ConnectionRefusedError("Unauthorised !")
for fsperm in fsperms:
if not current_user.has_permission(fsperm):
disconnect()
raise ConnectionRefusedError("Forbidden !")
return f(*args, **kwargs)
return wrapped
return wrapper
class AuthenticateModule(pga_utils.PgAdminModule):
def get_exposed_url_endpoints(self):
return ['authenticate.login']
+54
View File
@@ -31,6 +31,58 @@ from pgadmin.model import db
OAUTH2_LOGOUT = 'oauth2.logout'
OAUTH2_AUTHORIZE = 'oauth2.authorize'
# OAuth2 settings that legitimately live at the top level of the
# configuration. Every other OAUTH2_* setting belongs *inside* an entry
# of the OAUTH2_CONFIG list and is ignored if set at the top level.
_TOP_LEVEL_OAUTH2_SETTINGS = {'OAUTH2_CONFIG', 'OAUTH2_AUTO_CREATE_USER'}
def warn_on_misplaced_oauth2_config(app):
"""Warn when per-provider OAuth2 settings have been supplied as
top-level configuration variables instead of inside an OAUTH2_CONFIG
entry.
pgAdmin only reads provider settings from the OAUTH2_CONFIG list, so
values such as a top-level OAUTH2_CLIENT_ID or OAUTH2_SSL_CERT_VERIFICATION
are silently ignored. This is an easy trap in container deployments:
every other setting is configured as PGADMIN_CONFIG_<KEY>, so it is
natural to assume PGADMIN_CONFIG_OAUTH2_CLIENT_ID and friends will work
the same way, when in fact OAuth2 must be configured through a single
PGADMIN_CONFIG_OAUTH2_CONFIG holding the provider list. Surfacing this
loudly turns a silent misconfiguration into a diagnosable one (see
issue #10053).
"""
misplaced = sorted(
key for key in dir(config)
if key.startswith('OAUTH2_') and
key not in _TOP_LEVEL_OAUTH2_SETTINGS
)
if not misplaced:
return
# If at least one provider is actually configured in OAUTH2_CONFIG we
# assume the deployment knows what it is doing and stay quiet, even if
# some stray top-level keys are also present.
providers_configured = any(
isinstance(provider, dict) and provider.get('OAUTH2_NAME')
for provider in (getattr(config, 'OAUTH2_CONFIG', None) or [])
)
if providers_configured:
return
app.logger.warning(
"OAuth2: the following settings are defined at the top level of the "
"configuration but pgAdmin reads OAuth2 provider settings only from "
"the OAUTH2_CONFIG list, so they are being ignored: %s. If you are "
"configuring OAuth2 through individual PGADMIN_CONFIG_OAUTH2_* "
"environment variables, this will not work; configure a single "
"PGADMIN_CONFIG_OAUTH2_CONFIG holding the provider list instead, "
"e.g. PGADMIN_CONFIG_OAUTH2_CONFIG='[{\"OAUTH2_NAME\": \"...\", "
"\"OAUTH2_CLIENT_ID\": \"...\"}]'. See the OAuth2 documentation for "
"details.",
", ".join(misplaced)
)
class Oauth2Module(PgAdminModule):
def register(self, app, options):
@@ -114,6 +166,8 @@ def init_app(app):
app.register_blueprint(blueprint)
app.login_manager.logout_view = OAUTH2_LOGOUT
warn_on_misplaced_oauth2_config(app)
class OAuth2Authentication(BaseAuthentication):
"""OAuth Authentication Class"""
+12 -10
View File
@@ -10,6 +10,7 @@
"""External Authentication Registry."""
import config
from pgadmin.utils.dynamic_registry import create_registry_metaclass
@@ -19,20 +20,21 @@ def load_modules(cls, app=None):
from . import internal as module
submodules.append(module)
from . import kerberos as module
submodules.append(module)
if config.SERVER_MODE:
from . import kerberos as module
submodules.append(module)
from . import ldap as module
submodules.append(module)
from . import ldap as module
submodules.append(module)
from . import mfa as module
submodules.append(module)
from . import mfa as module
submodules.append(module)
from . import oauth2 as module
submodules.append(module)
from . import oauth2 as module
submodules.append(module)
from . import webserver as module
submodules.append(module)
from . import webserver as module
submodules.append(module)
for module in submodules:
if "init_app" in module.__dict__.keys():
@@ -0,0 +1,8 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
@@ -0,0 +1,130 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""Verify that external authentication providers are only loaded in server
mode, and that desktop mode is left with internal authentication alone.
"""
import sys
import config
from pgadmin.authenticate.registry import AuthSourceRegistry
from pgadmin.utils.route import BaseTestGenerator
# Providers registered as authentication sources. MFA is deliberately absent:
# it is loaded by the same registry but registers no auth source of its own.
EXTERNAL_SOURCES = ['kerberos', 'ldap', 'oauth2', 'webserver']
# Modules whose import can be asserted on. mfa is excluded because
# pgadmin/browser/__init__.py and pgadmin/user_login_check.py both import
# pgadmin.authenticate.mfa.utils unconditionally, so the mfa package is
# present in sys.modules in desktop mode whatever this registry does.
EXTERNAL_MODULES = [
'pgadmin.authenticate.{0}'.format(name) for name in EXTERNAL_SOURCES
]
class AuthSourceGatingTestCase(BaseTestGenerator):
"""Checks AuthSourceRegistry.load_modules() honours config.SERVER_MODE."""
scenarios = [
('desktop mode registers internal authentication only',
dict(server_mode=False)),
('server mode registers every external provider',
dict(server_mode=True)),
]
# Pure registry-contract test - no Postgres server interaction needed, so
# skip BaseTestGenerator.setUp's connect_server().
def setUp(self):
self._orig_server_mode = config.SERVER_MODE
self._orig_registry = dict(AuthSourceRegistry._registry or {})
self._orig_objects = dict(AuthSourceRegistry._objects or {})
def tearDown(self):
config.SERVER_MODE = self._orig_server_mode
AuthSourceRegistry._registry = self._orig_registry
AuthSourceRegistry._objects = self._orig_objects
def _reset_registry(self):
"""Empty the registry and drop the external provider modules, so that
load_modules() is observed importing them (or not) from scratch.
"""
import pgadmin.authenticate as auth_package
for source, module in zip(EXTERNAL_SOURCES, EXTERNAL_MODULES):
sys.modules.pop(module, None)
# Clearing sys.modules alone is not enough. load_modules() reaches
# these with "from . import <provider>", which finds the attribute
# already set on the parent package by whichever earlier test
# imported it, and so returns the previous module object without
# re-executing it. The provider then never re-registers itself and
# the registry looks empty for that one alone. Dropping the
# attribute as well forces the real import.
if hasattr(auth_package, source):
delattr(auth_package, source)
AuthSourceRegistry._registry = dict()
AuthSourceRegistry._objects = dict()
internal = sys.modules.get('pgadmin.authenticate.internal')
if internal is not None and \
hasattr(internal, 'InternalAuthentication'):
AuthSourceRegistry._registry['internal'] = \
internal.InternalAuthentication
def runTest(self):
config.SERVER_MODE = self.server_mode
self._reset_registry()
AuthSourceRegistry.load_modules(_MockApp() if self.server_mode
else None)
registered = list(AuthSourceRegistry._registry.keys())
self.assertIn(
'internal', registered,
"Internal authentication must be registered in both modes")
for source, module in zip(EXTERNAL_SOURCES, EXTERNAL_MODULES):
if self.server_mode:
self.assertIn(
source, registered,
"{0} must be registered in server mode".format(source))
self.assertIn(
module, sys.modules,
"{0} must be imported in server mode".format(module))
else:
self.assertNotIn(
source, registered,
"{0} must not be registered in desktop mode".format(
source))
self.assertNotIn(
module, sys.modules,
"{0} must not be imported in desktop mode".format(module))
class _MockApp:
"""The minimum Flask-like surface the providers' init_app() touches."""
class _LoginManager:
logout_view = None
class _Logger:
def warning(self, *args, **kwargs):
pass
def __init__(self):
self.login_manager = self._LoginManager()
self.logger = self._Logger()
def register_blueprint(self, *args, **kwargs):
pass
def register_logout_hook(self, *args, **kwargs):
pass
@@ -0,0 +1,360 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""Verify that WebserverAuthentication only trusts an identity asserted via
an inbound HTTP header when the operator has explicitly opted in, and that
it never logs a header-asserted identity into a non-Webserver account.
These are pure request/config-logic tests - no Postgres server or SQLite
database is required, so setUp() below skips
BaseTestGenerator.setUp()'s connect_server().
WebserverAuthentication is deliberately imported inside each runTest(),
not at module level: test_auth_gating.py (which sorts and therefore runs
before this module) evicts pgadmin.authenticate.webserver from
sys.modules and never restores it, to observe load_modules() re-import
it from scratch. A module-level import here would bind the discarded
module's class, while unittest.mock.patch('pgadmin.authenticate.webserver
.User') resolves through sys.modules and patches the fresh one - the
patch would silently miss and hit a real, unconfigured database.
"""
from unittest.mock import patch
import flask
from werkzeug.middleware.proxy_fix import ProxyFix
import config
from pgadmin.utils.constants import WEBSERVER, INTERNAL
from pgadmin.utils.route import BaseTestGenerator
class WebserverRemoteUserTrustTestCase(BaseTestGenerator):
"""Checks get_user()'s trusted-environ vs. gated-header split, the
trusted-proxy allow-list, and the shared-secret check.
"""
scenarios = [
('header present, opt-in off returns nothing',
dict(remote_user='REMOTE_USER', from_header=False,
trusted_proxies=['127.0.0.1'], shared_secret=None,
peer='127.0.0.1', secret_header=None, expected=None)),
('header present, opt-in on, peer not allow-listed returns nothing',
dict(remote_user='REMOTE_USER', from_header=True,
trusted_proxies=['10.0.0.1'], shared_secret=None,
peer='127.0.0.1', secret_header=None, expected=None)),
('header present, opt-in on, peer allowed, secret mismatch',
dict(remote_user='REMOTE_USER', from_header=True,
trusted_proxies=['127.0.0.1'], shared_secret='s3cr3t',
peer='127.0.0.1', secret_header='wrong', expected=None)),
('header present, fully configured returns the header value',
dict(remote_user='REMOTE_USER', from_header=True,
trusted_proxies=['127.0.0.1/32'], shared_secret='s3cr3t',
peer='127.0.0.1', secret_header='s3cr3t',
expected='header-user')),
('bare REMOTE_USER in environ is always trusted',
dict(remote_user='REMOTE_USER', from_header=False,
trusted_proxies=[], shared_secret=None,
peer='127.0.0.1', secret_header=None,
expected='environ-user', set_environ_remote_user=True)),
('HTTP_-prefixed WEBSERVER_REMOTE_USER with opt-in off is rejected',
dict(remote_user='HTTP_X_FORWARDED_USER', from_header=False,
trusted_proxies=['127.0.0.1'], shared_secret=None,
peer='127.0.0.1', secret_header=None, expected=None,
header_name='X-Forwarded-User')),
('HTTP_-prefixed WEBSERVER_REMOTE_USER, fully configured, '
'resolves via the equivalent header',
dict(remote_user='HTTP_X_FORWARDED_USER', from_header=True,
trusted_proxies=['127.0.0.1'], shared_secret=None,
peer='127.0.0.1', secret_header=None,
expected='header-user', header_name='X-Forwarded-User')),
('hyphenated WEBSERVER_REMOTE_USER resolves via header when '
'opted in',
dict(remote_user='Remote-user', from_header=True,
trusted_proxies=['127.0.0.1'], shared_secret=None,
peer='127.0.0.1', secret_header=None,
expected='header-user')),
('shared secret configured, header absent returns nothing',
dict(remote_user='REMOTE_USER', from_header=True,
trusted_proxies=['127.0.0.1'], shared_secret='s3cr3t',
peer='127.0.0.1', secret_header=None, expected=None)),
]
# Pure logic test - no Postgres server interaction needed.
def setUp(self):
self._orig = dict(
WEBSERVER_REMOTE_USER=config.WEBSERVER_REMOTE_USER,
WEBSERVER_REMOTE_USER_FROM_HEADER=getattr(
config, 'WEBSERVER_REMOTE_USER_FROM_HEADER', False),
WEBSERVER_TRUSTED_PROXIES=getattr(
config, 'WEBSERVER_TRUSTED_PROXIES', []),
WEBSERVER_SHARED_SECRET=getattr(
config, 'WEBSERVER_SHARED_SECRET', None),
WEBSERVER_SHARED_SECRET_HEADER=getattr(
config, 'WEBSERVER_SHARED_SECRET_HEADER',
'X-Pgadmin-Webserver-Secret'),
)
def tearDown(self):
for key, value in self._orig.items():
setattr(config, key, value)
def runTest(self):
from pgadmin.authenticate.webserver import WebserverAuthentication
config.WEBSERVER_REMOTE_USER = self.remote_user
config.WEBSERVER_REMOTE_USER_FROM_HEADER = self.from_header
config.WEBSERVER_TRUSTED_PROXIES = self.trusted_proxies
config.WEBSERVER_SHARED_SECRET = self.shared_secret
headers = {}
# Simulate the client sending a header. request.headers.get(name)
# is looked up by the literal name pgAdmin is configured to use
# (config.WEBSERVER_REMOTE_USER), so the header key here matches
# that name, or the explicit header_name override for the scenario
# where WEBSERVER_REMOTE_USER is an HTTP_-prefixed CGI-style name
# (a real client would still send an ordinary hyphenated header).
if not getattr(self, 'set_environ_remote_user', False):
header_name = getattr(self, 'header_name', self.remote_user)
headers[header_name] = 'header-user'
if self.secret_header is not None:
headers[config.WEBSERVER_SHARED_SECRET_HEADER] = \
self.secret_header
environ_overrides = {'REMOTE_ADDR': self.peer}
if getattr(self, 'set_environ_remote_user', False):
environ_overrides['REMOTE_USER'] = 'environ-user'
app = flask.Flask(__name__)
with app.test_request_context(
'/', headers=headers, environ_overrides=environ_overrides):
auth = WebserverAuthentication()
username = auth.get_user()
self.assertEqual(username, self.expected)
class WebserverLoginAuthSourceTestCase(BaseTestGenerator):
"""login() must refuse a matched user whose auth_source is not
Webserver, even when get_user() returns a name - defense in depth
against a misconfigured/forged trust gate.
"""
# Pure logic test - no Postgres server interaction needed.
def setUp(self):
pass
def tearDown(self):
pass
def runTest(self):
from pgadmin.authenticate.webserver import WebserverAuthentication
app = flask.Flask(__name__)
with app.test_request_context('/'):
auth = WebserverAuthentication()
class _FakeUser:
auth_source = INTERNAL
with patch(
'pgadmin.authenticate.webserver.User') as mock_user, \
patch.object(auth, 'get_user',
return_value='admin@example.com'):
mock_user.query.filter_by.return_value.first \
.return_value = _FakeUser()
status, message = auth.login(None)
self.assertFalse(
status,
"login() must refuse a user whose auth_source is not "
"{0}".format(WEBSERVER))
class WebserverLoginPositiveTestCase(BaseTestGenerator):
"""login() must still succeed for a matched user whose auth_source
genuinely is Webserver - the auth_source guard in
WebserverLoginAuthSourceTestCase must not reject everyone.
"""
def setUp(self):
pass
def tearDown(self):
pass
def runTest(self):
from pgadmin.authenticate.webserver import WebserverAuthentication
app = flask.Flask(__name__)
with app.test_request_context('/'):
auth = WebserverAuthentication()
class _FakeUser:
auth_source = WEBSERVER
with patch(
'pgadmin.authenticate.webserver.User') as mock_user, \
patch('pgadmin.authenticate.webserver.login_user',
return_value=True), \
patch.object(auth, 'get_user',
return_value='webserver-user'):
mock_user.query.filter_by.return_value.first \
.return_value = _FakeUser()
status, message = auth.login(None)
self.assertTrue(
status,
"login() must accept a matched user whose auth_source "
"is {0}".format(WEBSERVER))
self.assertIsNone(message)
class WebserverLoginNoMatchingUserTestCase(BaseTestGenerator):
"""login() must fail gracefully (LOGIN_FAILED), not raise, when
get_user() returns a name with no matching User row.
"""
def setUp(self):
pass
def tearDown(self):
pass
def runTest(self):
from pgadmin.authenticate.webserver import WebserverAuthentication
app = flask.Flask(__name__)
with app.test_request_context('/'):
auth = WebserverAuthentication()
with patch(
'pgadmin.authenticate.webserver.User') as mock_user, \
patch.object(auth, 'get_user',
return_value='no-such-user'):
mock_user.query.filter_by.return_value.first \
.return_value = None
status, message = auth.login(None)
self.assertFalse(
status,
"login() must fail when no User row matches the "
"asserted identity")
self.assertEqual(message, auth.messages('LOGIN_FAILED'))
class WebserverProxyFixBypassTestCase(BaseTestGenerator):
"""A spoofed X-Forwarded-For naming a trusted proxy must not grant
trust for the real, untrusted socket peer. See
_get_untrusted_peer_addr()'s docstring: request.remote_addr is NOT
safe to use here, because ProxyFix rewrites it from a client-
controlled X-Forwarded-For header. Unlike WebserverRemoteUserTrustTest
Case above, this exercises the real WSGI middleware stack via
app.test_client(), so ProxyFix actually runs.
"""
def setUp(self):
self._orig = dict(
WEBSERVER_REMOTE_USER=config.WEBSERVER_REMOTE_USER,
WEBSERVER_REMOTE_USER_FROM_HEADER=getattr(
config, 'WEBSERVER_REMOTE_USER_FROM_HEADER', False),
WEBSERVER_TRUSTED_PROXIES=getattr(
config, 'WEBSERVER_TRUSTED_PROXIES', []),
WEBSERVER_SHARED_SECRET=getattr(
config, 'WEBSERVER_SHARED_SECRET', None),
)
def tearDown(self):
for key, value in self._orig.items():
setattr(config, key, value)
def runTest(self):
from pgadmin.authenticate.webserver import WebserverAuthentication
config.WEBSERVER_REMOTE_USER = 'REMOTE_USER'
config.WEBSERVER_REMOTE_USER_FROM_HEADER = True
# The trusted proxy list names the address the attacker will
# spoof via X-Forwarded-For, not the real, untrusted peer below.
config.WEBSERVER_TRUSTED_PROXIES = ['10.9.9.9']
config.WEBSERVER_SHARED_SECRET = None
app = flask.Flask(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1)
result = {}
@app.route('/')
def _probe():
result['username'] = WebserverAuthentication().get_user()
return ''
client = app.test_client()
response = client.get(
'/',
headers={
'REMOTE_USER': 'header-user',
'X-Forwarded-For': '10.9.9.9',
},
environ_overrides={'REMOTE_ADDR': '127.0.0.1'})
# Flask turns an exception inside the probe into a 500 rather than
# re-raising it, so without these two assertions a get_user() that
# blew up would leave result empty and pass this test vacuously.
self.assertEqual(response.status_code, 200)
self.assertIn('username', result,
"the probe route did not run, so nothing was tested")
self.assertIsNone(
result['username'],
"a spoofed X-Forwarded-For naming a trusted proxy must not "
"grant trust for the real, untrusted socket peer")
class WebserverAuthenticateMailTestCase(BaseTestGenerator):
"""authenticate()'s _get_trusted_value('mail') call still reads the
'mail' CGI variable from the environ.
"""
def setUp(self):
self._orig_remote_user = config.WEBSERVER_REMOTE_USER
def tearDown(self):
config.WEBSERVER_REMOTE_USER = self._orig_remote_user
def runTest(self):
from pgadmin.authenticate.webserver import WebserverAuthentication
config.WEBSERVER_REMOTE_USER = 'REMOTE_USER'
app = flask.Flask(__name__)
app.secret_key = 'test-secret-key'
with app.test_request_context(
'/', environ_overrides={
'REMOTE_USER': 'environ-user',
'mail': 'environ-user@example.com',
}):
auth = WebserverAuthentication()
with patch.object(
auth, '_WebserverAuthentication__auto_create_user',
return_value=(True, None)) as mock_create:
auth.authenticate(None)
# The empty email is pre-existing upstream behaviour:
# authenticate() reads 'mail' but passes '' on to
# __auto_create_user(). Pinned here as-is - fixing it is
# out of scope for this security fix.
mock_create.assert_called_once_with(
'environ-user', '')
+178 -4
View File
@@ -9,6 +9,8 @@
"""A blueprint module implementing the Webserver authentication."""
import hmac
import ipaddress
import secrets
import string
import config
@@ -26,6 +28,161 @@ from flask_security.utils import logout_user
from pgadmin.utils.master_password import set_crypt_key
def _is_cgi_var_name(name):
"""Return True only for a genuine CGI/WSGI environment variable name,
e.g. REMOTE_USER. Anything HTTP_-prefixed or hyphenated is, in practice,
a client-supplied HTTP header (e.g. HTTP_X_FORWARDED_USER or
X-Forwarded-User), so it must never be trusted as-is. A name that is
not a non-empty string at all (a misconfigured WEBSERVER_REMOTE_USER)
is not a CGI variable name either.
"""
return isinstance(name, str) and bool(name) and \
not name.startswith('HTTP_') and '-' not in name
def _get_untrusted_peer_addr():
"""Return the raw peer address of the socket that connected to pgAdmin,
ignoring anything ProxyFix derived from a client-controlled
X-Forwarded-For header. request.remote_addr is NOT safe for this
purpose: with the default PROXY_X_FOR_COUNT = 1, ProxyFix rewrites it
from X-Forwarded-For even when nothing is actually in front of pgAdmin.
"""
orig = request.environ.get('werkzeug.proxy_fix.orig', {})
return orig.get('REMOTE_ADDR') or request.environ.get('REMOTE_ADDR')
def _peer_is_trusted_proxy():
"""Check the raw peer address against WEBSERVER_TRUSTED_PROXIES. Never
raises - a malformed config entry or an unparseable/missing peer address
is treated as untrusted. A per-request rejection (no proxy in front, an
untrusted peer) is logged at info, since an unauthenticated client can
trigger it on every request; a malformed WEBSERVER_TRUSTED_PROXIES
itself is an operator misconfiguration and stays at warning.
"""
if not isinstance(config.WEBSERVER_TRUSTED_PROXIES, (list, tuple)):
current_app.logger.warning(
"Webserver auth: WEBSERVER_TRUSTED_PROXIES must be a list or "
"tuple of IP/CIDR strings; rejecting the header-asserted "
"identity.")
return False
peer_addr = _get_untrusted_peer_addr()
if not peer_addr:
current_app.logger.info(
"Webserver auth: could not determine the peer address; "
"rejecting the header-asserted identity.")
return False
try:
peer = ipaddress.ip_address(peer_addr)
except ValueError:
current_app.logger.info(
"Webserver auth: peer address {0} is not a valid IP; "
"rejecting the header-asserted identity.".format(peer_addr))
return False
for entry in config.WEBSERVER_TRUSTED_PROXIES:
try:
network = ipaddress.ip_network(entry, strict=False)
except ValueError:
current_app.logger.warning(
"Webserver auth: WEBSERVER_TRUSTED_PROXIES entry {0} is "
"not a valid IP/CIDR; ignoring it.".format(entry))
continue
if peer in network:
return True
current_app.logger.info(
"Webserver auth: peer {0} is not in WEBSERVER_TRUSTED_PROXIES; "
"rejecting the header-asserted identity.".format(peer_addr))
return False
def _shared_secret_matches():
"""When a shared secret is configured, require the proxy to have sent
it back in the configured header, compared with hmac.compare_digest to
avoid a timing side-channel. When no secret is configured, this check is
skipped (returns True). Never raises - a non-str/bytes or non-ASCII
secret is treated as a mismatch, with a warning logged for the operator.
"""
if not config.WEBSERVER_SHARED_SECRET:
return True
supplied = request.headers.get(config.WEBSERVER_SHARED_SECRET_HEADER)
if not supplied:
return False
# Compare as bytes: hmac.compare_digest rejects a str carrying any
# non-ASCII character, so an accented passphrase would otherwise raise
# rather than simply match or not.
secret = config.WEBSERVER_SHARED_SECRET
if isinstance(secret, str):
secret = secret.encode('utf-8')
try:
return hmac.compare_digest(supplied.encode('utf-8'), secret)
except TypeError:
current_app.logger.warning(
"Webserver auth: WEBSERVER_SHARED_SECRET must be a str or "
"bytes value; rejecting the header-asserted identity.")
return False
def _get_trusted_value(name):
"""Read a value that the client cannot forge: the WSGI/CGI environment,
but only for a genuine CGI variable name (see _is_cgi_var_name). Any
HTTP_-prefixed or hyphenated name is, under this WSGI server, indistin-
guishable from a client-supplied header, so it is never read here.
"""
if not _is_cgi_var_name(name):
return None
return request.environ.get(name)
def _header_name_for(name):
"""Translate a CGI-style HTTP_FOO_BAR name to the HTTP header name a
client/proxy actually sends (Foo-Bar), so an operator can set
WEBSERVER_REMOTE_USER to either spelling and have the header path
resolve it. Werkzeug's header lookup normalizes '_' to '-' but does not
strip an HTTP_ prefix, so that translation has to happen here. Any
other name (e.g. an ordinary hyphenated header name) is returned as-is.
"""
if name.startswith('HTTP_'):
return '-'.join(
part.capitalize() for part in name[len('HTTP_'):].split('_'))
return name
def _get_header_value(name):
"""Read a value from an inbound HTTP request header - a client-
controlled source. Only returned when the operator has explicitly
opted in via WEBSERVER_REMOTE_USER_FROM_HEADER, the request came from a
peer listed in WEBSERVER_TRUSTED_PROXIES, and (if configured) the
shared secret matches. A client-triggered rejection here (untrusted
peer, secret mismatch) is logged at info, since an unauthenticated
client can trigger it on every request and warning-level logging
would let it flood the log. A malformed WEBSERVER_REMOTE_USER is a
static operator misconfiguration, not something a client varies per
request, and stays at warning so it doesn't go unnoticed while every
login silently fails.
"""
if not config.WEBSERVER_REMOTE_USER_FROM_HEADER:
return None
if not isinstance(name, str) or not name:
current_app.logger.warning(
"Webserver auth: WEBSERVER_REMOTE_USER is not set to a "
"non-empty name; rejecting the header-asserted identity.")
return None
if not _peer_is_trusted_proxy():
return None
if not _shared_secret_matches():
current_app.logger.info(
"Webserver auth: shared secret mismatch; rejecting the "
"header-asserted identity.")
return None
return request.headers.get(_header_name_for(name))
class WebserverModule(PgAdminModule):
def register(self, app, options):
# Do not look for the sub_modules,
@@ -77,10 +234,12 @@ class WebserverAuthentication(BaseAuthentication):
return True, None
def get_user(self):
username = request.environ.get(config.WEBSERVER_REMOTE_USER)
username = _get_trusted_value(config.WEBSERVER_REMOTE_USER)
if not username:
# One more try to get the Remote User from the hearders
username = request.headers.get(config.WEBSERVER_REMOTE_USER)
# Gated fallback: only for deployments that have explicitly
# opted in to trusting a header-asserted identity. See
# _get_header_value() for the checks this requires.
username = _get_header_value(config.WEBSERVER_REMOTE_USER)
return username
def authenticate(self, form):
@@ -93,7 +252,7 @@ class WebserverAuthentication(BaseAuthentication):
session['pass_enc_key'] = ''.join(
(secrets.choice(string.ascii_lowercase) for _ in range(10)))
useremail = request.environ.get('mail')
useremail = _get_trusted_value('mail')
if not useremail:
useremail = ''
return self.__auto_create_user(username, '')
@@ -102,6 +261,21 @@ class WebserverAuthentication(BaseAuthentication):
username = self.get_user()
if username:
user = User.query.filter_by(username=username).first()
if user is None:
current_app.logger.warning(
"Webserver auth: no User row matches the asserted "
"identity {0}.".format(username))
return False, self.messages('LOGIN_FAILED')
# Defense in depth: a header-asserted identity must never be
# able to log in an account that was not created via Webserver
# authentication (e.g. the internal default admin), even if the
# trust gate above is misconfigured.
if user.auth_source != WEBSERVER:
current_app.logger.warning(
"Webserver auth: rejecting login for {0}, whose "
"auth_source is {1}, not {2}.".format(
username, user.auth_source, WEBSERVER))
return False, self.messages('LOGIN_FAILED')
status = login_user(user)
if not status:
current_app.logger.exception(self.messages('LOGIN_FAILED'))
+4 -1
View File
@@ -27,7 +27,7 @@ from flask import current_app, render_template, url_for, make_response, \
flash, Response, request, redirect, session
from flask_babel import gettext
from libgravatar import Gravatar
from flask_security import current_user
from flask_security import current_user, permissions_required
from flask_login.utils import login_url
from flask_security.changeable import send_password_changed_notice
from flask_security.decorators import anonymous_user_required
@@ -50,6 +50,8 @@ from pgadmin.utils.ajax import make_json_response, internal_server_error, \
bad_request
from pgadmin.utils.csrf import pgCSRFProtect
from pgadmin.utils.preferences import Preferences
from pgadmin.tools.user_management.PgAdminPermissions import \
AllPermissionTypes
from pgadmin.browser.register_browser_preferences import \
register_browser_preferences
from pgadmin.browser.register_editor_preferences import \
@@ -891,6 +893,7 @@ if hasattr(config, 'SECURITY_CHANGEABLE') and config.SECURITY_CHANGEABLE:
@blueprint.route("/change_password", endpoint="change_password",
methods=['GET', 'POST'])
@pgCSRFProtect.exempt
@permissions_required(AllPermissionTypes.change_password)
@pga_login_required
def change_password():
"""View function which handles a change password request."""
@@ -163,6 +163,21 @@ def register_browser_preferences(self):
fields=fields
)
self.preference.register(
'keyboard_shortcuts',
'toggle_object_explorer',
gettext('Toggle Object Explorer'),
'keyboardshortcut',
{
'alt': True,
'shift': False,
'control': True,
'key': {'key_code': 66, 'char': 'b'}
},
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
fields=fields
)
self.preference.register(
'keyboard_shortcuts',
'tabbed_panel_backward',
+66 -60
View File
@@ -29,18 +29,14 @@ from pgadmin.utils.server_access import get_server_group, \
get_server_groups_for_user
def get_icon_css_class(group_id, group_user_id,
default_val='icon-server_group'):
def get_icon_css_class(is_shared_group, default_val='icon-server_group'):
"""
Returns css value
:param group_id:
:param group_user_id:
:param is_shared_group:
:param default_val:
:return: default_val
"""
if (config.SERVER_MODE and
group_user_id != current_user.id and
ServerGroupModule.has_shared_server(group_id)):
if (config.SERVER_MODE and is_shared_group):
default_val = 'icon-server_group_shared'
return default_val, True
@@ -66,39 +62,24 @@ class ServerGroupModule(BrowserPluginModule):
return snippets
@staticmethod
def has_shared_server(gid):
"""
To check whether given server group contains shared server or not
:param gid:
:return: True if servergroup contains shared server else false
"""
servers = Server.query.filter_by(servergroup_id=gid)
for s in servers:
if s.shared:
return True
return False
def get_nodes(self, *arg, **kwargs):
"""Return a JSON document listing the server groups for the user"""
if config.SERVER_MODE:
groups = ServerGroupView.get_all_server_groups()
else:
groups = ServerGroup.query.filter_by(
user_id=current_user.id
).order_by("id")
groups = ServerGroupView.get_all_server_groups()
for idx, group in enumerate(groups):
icon_class, is_shared = get_icon_css_class(group.id, group.user_id)
for group in groups:
icon_class, is_shared = get_icon_css_class(group.is_shared_group)
yield self.generate_browser_node(
"%d" % (group.id), None,
"%d" % (group.id),
None,
group.name,
icon_class,
True,
self.node_type,
can_delete=True if idx > 0 else False,
user_id=group.user_id,
can_delete=(
not (group.is_first_user_group or group.is_shared_group)
),
can_edit=(not group.is_shared_group),
is_shared=is_shared
)
@@ -202,6 +183,7 @@ class ServerGroupView(NodeView):
success=0,
errormsg=gettext(
'The specified server group cannot be deleted.'
' Shared servers are present in this group.'
)
)
@@ -211,6 +193,7 @@ class ServerGroupView(NodeView):
success=0,
errormsg=gettext(
'The specified server group cannot be deleted.'
' The first server group is not deletable.'
)
)
@@ -239,10 +222,8 @@ class ServerGroupView(NodeView):
def update(self, gid):
"""Update the server-group properties"""
# There can be only one record at most
servergroup = ServerGroup.query.filter_by(
user_id=current_user.id,
id=gid).first()
# There can be only one record at most (only owned)
servergroup = get_server_group(gid, hide_shared=True)
data = request.form if request.form else json.loads(
request.data
@@ -259,18 +240,20 @@ class ServerGroupView(NodeView):
if 'name' in data:
servergroup.name = data['name']
db.session.commit()
except exc.IntegrityError:
db.session.rollback()
return bad_request(gettext(
"The specified server group already exists."
))
except Exception as e:
db.session.rollback()
return make_json_response(
status=410, success=0, errormsg=str(e)
)
icon_class, is_shared = get_icon_css_class(gid, servergroup.user_id)
icon_class, is_shared = get_icon_css_class(servergroup.is_shared_group)
return jsonify(
node=self.blueprint.generate_browser_node(
gid,
@@ -279,7 +262,13 @@ class ServerGroupView(NodeView):
icon_class,
True,
self.node_type,
can_delete=True, # This is user created hence can delete
can_delete=(
not (
servergroup.is_first_user_group or
servergroup.is_shared_group
)
),
can_edit=(not servergroup.is_shared_group),
is_shared=is_shared
)
)
@@ -315,12 +304,17 @@ class ServerGroupView(NodeView):
user_id=current_user.id,
name=data['name'])
db.session.add(sg)
db.session.flush()
group_id = sg.id
db.session.commit()
# Reload with access metadata attached
sg = get_server_group(group_id)
data['id'] = sg.id
data['name'] = sg.name
icon_class, is_shared = get_icon_css_class(sg.id, sg.user_id)
icon_class, is_shared = get_icon_css_class(sg.is_shared_group)
return jsonify(
node=self.blueprint.generate_browser_node(
"%d" % sg.id,
@@ -329,8 +323,13 @@ class ServerGroupView(NodeView):
icon_class,
True,
self.node_type,
# This is user created hence can deleted
can_delete=True,
can_delete=(
not (
sg.is_first_user_group or
sg.is_shared_group
)
),
can_edit=(not sg.is_shared_group),
is_shared=is_shared
)
)
@@ -385,20 +384,12 @@ class ServerGroupView(NodeView):
# Don't display shared server if user has
# selected 'Hide shared server'
pref = Preferences.module('browser')
hide_shared_server = pref.preference('hide_shared_server').get()
pref_item = pref.preference('hide_shared_server')
hide_shared_server = (
pref_item.get() if pref_item is not None else False
)
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 server_groups
return get_server_groups_for_user(hide_shared_server)
@pga_login_required
def nodes(self, gid=None):
@@ -406,14 +397,15 @@ class ServerGroupView(NodeView):
nodes = []
if gid is None:
if config.SERVER_MODE:
groups = self.get_all_server_groups()
else:
groups = ServerGroup.query.filter_by(user_id=current_user.id)
groups = get_server_groups_for_user(hide_shared=True)
for group in groups:
icon_class, is_shared = get_icon_css_class(group.id,
group.user_id)
icon_class, is_shared = get_icon_css_class(
group.is_shared_group
)
nodes.append(
self.blueprint.generate_browser_node(
"%d" % group.id,
@@ -422,6 +414,13 @@ class ServerGroupView(NodeView):
icon_class,
True,
self.node_type,
can_delete=(
not (
group.is_first_user_group or
group.is_shared_group
)
),
can_edit=(not group.is_shared_group),
is_shared=is_shared
)
)
@@ -433,14 +432,21 @@ class ServerGroupView(NodeView):
errormsg=gettext("Could not find the server group.")
)
icon_class, is_shared = get_icon_css_class(group.id,
group.user_id)
icon_class, is_shared = get_icon_css_class(group.is_shared_group)
nodes = self.blueprint.generate_browser_node(
"%d" % (group.id), None,
"%d" % (group.id),
None,
group.name,
icon_class,
True,
self.node_type,
can_delete=(
not (
group.is_first_user_group or
group.is_shared_group
)
),
can_edit=(not group.is_shared_group),
is_shared=is_shared
)
@@ -24,6 +24,6 @@
{% endif %}
{% if schema_diff %}
AND CASE WHEN (SELECT COUNT(*) FROM pg_catalog.pg_depend
WHERE objid = ca.oid AND deptype = 'e') > 0 THEN FALSE ELSE TRUE END
WHERE objid = ca.oid AND deptype IN ('e', 'i')) > 0 THEN FALSE ELSE TRUE END
{% endif %}
ORDER BY st.typname, tt.typname
@@ -7,7 +7,7 @@ SELECT
FROM
pg_catalog.pg_extension x
LEFT JOIN pg_catalog.pg_namespace n ON x.extnamespace=n.oid
JOIN pg_catalog.pg_available_extensions() e(name, default_version, comment) ON x.extname=e.name
JOIN pg_catalog.pg_available_extensions() e ON x.extname=e.name
{%- if eid %}
WHERE x.oid = {{eid}}::oid
{% elif ename %}
@@ -1048,7 +1048,7 @@ class PublicationView(PGChildNodeView, SchemaDiffObjectCompare):
status, pname = self.conn.execute_scalar(get_name_sql)
table_sql = render_template(
"/".join([self.template_path, 'dependencies.sql']),
pname=pname
pname=pname, conn=self.conn
)
status, res = self.conn.execute_dict(table_sql)
if not status:
@@ -2,4 +2,4 @@ SELECT cl.oid AS oid,
pg_catalog.quote_ident(pgb_table.schemaname)||'.'||pg_catalog.quote_ident(pgb_table.tablename) AS pubtable
FROM pg_catalog.pg_publication_tables pgb_table
LEFT JOIN pg_catalog.pg_class cl ON pgb_table.tablename = cl.relname
WHERE pubname = '{{ pname }}' AND pgb_table.schemaname NOT LIKE 'pgagent';
WHERE pubname = {{ pname|qtLiteral(conn) }} AND pgb_table.schemaname NOT LIKE 'pgagent';
@@ -1 +1 @@
SELECT oid, pubname AS name FROM pg_catalog.pg_publication WHERE pubname = '{{ pubname }}';
SELECT oid, pubname AS name FROM pg_catalog.pg_publication WHERE pubname = {{ pubname|qtLiteral(conn) }};
@@ -2,4 +2,4 @@ SELECT cl.oid AS oid,
pg_catalog.quote_ident(pgb_table.schemaname)||'.'||pg_catalog.quote_ident(pgb_table.tablename) AS pubtable
FROM pg_catalog.pg_publication_tables pgb_table
LEFT JOIN pg_catalog.pg_class cl ON pgb_table.tablename = cl.relname
WHERE pubname = '{{ pname }}' AND pgb_table.schemaname NOT LIKE 'pgagent';
WHERE pubname = {{ pname|qtLiteral(conn) }} AND pgb_table.schemaname NOT LIKE 'pgagent';
@@ -1 +1 @@
SELECT oid, pubname AS name FROM pg_catalog.pg_publication WHERE pubname = '{{ pubname }}';
SELECT oid, pubname AS name FROM pg_catalog.pg_publication WHERE pubname = {{ pubname|qtLiteral(conn) }};
@@ -1194,6 +1194,8 @@ class ForeignTableView(BaseTableView, DataTypeReader,
# Parse the data coming from client
data = column_utils.parse_format_columns(data, mode='edit')
ForeignTableView._normalise_column_collation(data['columns'])
columns = data['columns']
column_sql = '\n'
@@ -1229,6 +1231,31 @@ class ForeignTableView(BaseTableView, DataTypeReader,
conn=self.conn)
return sql, data['name']
@staticmethod
def _normalise_column_collation(columns):
"""
Give a column's collation the name the column templates expect.
The dialog calls a column's collation ``collspcname``, and that is
what foreign_table_columns' create and update templates render,
whilst get_columns.sql calls it ``collname``, which is what Schema
Diff hands us. Without reconciling the two, a column added or
retyped by Schema Diff silently loses its collation and the two
databases stay different however many times the script is applied
(#10300).
:param columns: The column difference, modified in place
"""
# A create carries a plain list of columns; only an update carries
# the added/changed/deleted difference this applies to.
if not isinstance(columns, dict):
return
for action in ('added', 'changed'):
for column in columns.get(action) or []:
if not column.get('collspcname') and column.get('collname'):
column['collspcname'] = column['collname']
def _check_for_column_delete(self, columns, data, column_sql):
# If column(s) is/are deleted
if 'deleted' in columns:
@@ -1826,15 +1853,31 @@ class ForeignTableView(BaseTableView, DataTypeReader,
:param data: Data for columns.
:param tmp_columns: tmp_columns list.
"""
def index_of(name):
for index, column in enumerate(tmp_columns):
if column.get('name') == name:
return index
return None
if 'added' in data['columns']:
for item in data['columns']['added']:
tmp_columns.append(item)
if 'changed' in data['columns']:
# tmp_columns holds the table as it stands, so a changed column
# is already in it in its old form and has to be replaced;
# appending it would declare the column twice and PostgreSQL
# would reject the recreated table outright (#10297).
for item in data['columns']['changed']:
tmp_columns.append(item)
index = index_of(item.get('name'))
if index is None:
tmp_columns.append(item)
else:
tmp_columns[index] = item
if 'deleted' in data['columns']:
for item in data['columns']['deleted']:
tmp_columns.remove(item)
index = index_of(item.get('name'))
if index is not None:
tmp_columns.pop(index)
@staticmethod
def _modify_constraints_data(data):
@@ -913,6 +913,7 @@ class FunctionView(PGChildNodeView, DataTypeReader, SchemaDiffObjectCompare):
"""
json_resp = kwargs.get('json_resp', True)
target_schema = kwargs.get('target_schema', None)
allow_code_formatting = kwargs.get('allow_code_formatting', True)
resp_data = self._fetch_properties(gid, sid, did, scid, fnid)
# Most probably this is due to error
@@ -936,7 +937,8 @@ class FunctionView(PGChildNodeView, DataTypeReader, SchemaDiffObjectCompare):
resp_data['func_args_without'] = ', '.join(args_without_name)
self.reformat_prosrc_code(resp_data)
if allow_code_formatting:
self.reformat_prosrc_code(resp_data)
if self.node_type == 'procedure':
object_type = 'procedure'
@@ -1035,7 +1037,7 @@ class FunctionView(PGChildNodeView, DataTypeReader, SchemaDiffObjectCompare):
:return:
"""
if 'arguments' in data and len(data['arguments']) > 0:
for arg in data['arguments']['changed']:
for arg in data['arguments'].get('changed', []):
for old_arg in old_data['arguments']:
if arg['argid'] == old_arg['argid']:
old_arg.update(arg)
@@ -1194,6 +1196,36 @@ class FunctionView(PGChildNodeView, DataTypeReader, SchemaDiffObjectCompare):
data[arg]) > 0) or arg in data:
data['change_func'] = True
# PostgreSQL cannot add an argument to an existing function/
# procedure via CREATE OR REPLACE. Adding an IN/INOUT/VARIADIC
# argument changes the routine's signature, so PostgreSQL
# creates a new, separate overloaded routine instead of
# replacing this one. Adding an OUT argument does not affect
# the signature, but it changes the shape of the returned row,
# which PostgreSQL rejects outright (SQLSTATE 42P13). Reject
# both cases explicitly, rather than silently leaving an
# orphaned routine behind or letting the database error surface.
if 'arguments' in data and isinstance(data['arguments'], dict) \
and data['arguments'].get('added'):
added_args = data['arguments']['added']
if any(
(a.get('argmode') or 'IN') != 'OUT' for a in added_args
):
return False, gettext(
"Adding a new IN/INOUT/VARIADIC argument to an "
"existing function/procedure is not supported, as "
"PostgreSQL would create a separate, overloaded "
"routine rather than replacing this one. Please "
"create a new function/procedure instead."
), ''
else:
return False, gettext(
"Adding a new OUT argument to an existing function/"
"procedure is not supported, as it would change the "
"shape of the returned row. Please create a new "
"function/procedure instead."
), ''
# If Function Definition/Arguments are changed then merge old
# Arguments with changed ones for Create/Replace Function
# SQL statement
@@ -1670,8 +1702,18 @@ class FunctionView(PGChildNodeView, DataTypeReader, SchemaDiffObjectCompare):
sql = self.delete(gid=gid, sid=sid, did=did,
scid=scid, fnid=oid, only_sql=True)
elif target_schema:
# This SQL is the one that will actually be executed
# against the target (e.g. to create a "Source Only"
# object there), so it must reproduce the body exactly
# as stored, without the cosmetic newlines added around
# $BODY$ for readability in the SQL panel. Otherwise the
# target's prosrc would differ from the source's by
# nothing but pgAdmin's own formatting, and a subsequent
# comparison would keep reporting a (whitespace-only)
# difference (see #10302).
sql = self.sql(gid=gid, sid=sid, did=did, scid=scid, fnid=oid,
target_schema=target_schema, json_resp=False)
target_schema=target_schema, json_resp=False,
allow_code_formatting=False)
else:
sql = self.sql(gid=gid, sid=sid, did=did, scid=scid, fnid=oid,
json_resp=False)
@@ -304,16 +304,16 @@ export default class FunctionSchema extends BaseUISchema {
},
{
id: 'arguments', label: gettext('Arguments'), cell: 'string',
group: gettext('Definition'), type: 'collection', canAdd: function(){
return obj.isNew();
},
group: gettext('Definition'), type: 'collection',
canDelete: true, mode: ['create', 'edit'],
columns: ['argtype', 'argmode', 'argname', 'argdefval'],
schema : new DefaultArgumentSchema(this.node_info, this.fieldOptions.getTypes),
disabled: obj.inCatalog(),
canDeleteRow: function() {
return obj.isNew();
},
// Existing (already saved) arguments cannot be removed here, as
// PostgreSQL has no way to drop an argument from a function via
// CREATE OR REPLACE. Only rows added in the current session (not
// yet saved) can be deleted.
canDeleteRow: (state) => (this.isNew(state)),
},{
id: 'prosrc', label: gettext('Code'), cell: 'text',
type: 'sql', mode: ['properties', 'create', 'edit'],
@@ -19,7 +19,7 @@ WHERE
{% endif %}
{% if schema_diff %}
AND CASE WHEN (SELECT COUNT(*) FROM pg_catalog.pg_depend
WHERE objid = pr.oid AND deptype = 'e') > 0 THEN FALSE ELSE TRUE END
WHERE objid = pr.oid AND deptype IN ('e', 'i')) > 0 THEN FALSE ELSE TRUE END
{% endif %}
AND typname NOT IN ('trigger', 'event_trigger')
ORDER BY
@@ -20,7 +20,7 @@ WHERE
{% endif %}
{% if schema_diff %}
AND CASE WHEN (SELECT COUNT(*) FROM pg_catalog.pg_depend
WHERE objid = pr.oid AND deptype = 'e') > 0 THEN FALSE ELSE TRUE END
WHERE objid = pr.oid AND deptype IN ('e', 'i')) > 0 THEN FALSE ELSE TRUE END
{% endif %}
AND typname NOT IN ('trigger', 'event_trigger')
ORDER BY
@@ -142,6 +142,87 @@ class FunctionGetmsqlTestCase(BaseTestGenerator):
}
)
),
(
'Fetch Function msql with newly added IN argument is rejected',
dict(
url='/browser/function/msql/',
is_positive_test=True,
mocking_required=False,
with_function_id=True,
is_mock_local_function=False,
test_data={
"name": "Test Function",
"funcowner": "",
"pronamespace": 2200,
"prorettypename": "character varying",
"lanname": "sql",
"prosrc": "select '1'",
"probin": "$libdir/",
"variables": [],
"seclabels": [],
"acl": [],
# PostgreSQL cannot add an IN argument to an existing
# function via CREATE OR REPLACE (it would create a
# separate, overloaded routine instead), so this must
# be rejected with a clear error rather than silently
# producing SQL that orphans a routine.
"arguments": json.dumps({
"added": [{
"argname": "new_arg",
"argtype": "integer",
"argmode": "IN",
"argdefval": "1"
}]
})
},
mock_data={},
expected_data={
"status_code": 500,
"check_errormsg": "overloaded"
}
),
),
(
'Fetch Function msql with newly added OUT argument is '
'rejected',
dict(
url='/browser/function/msql/',
is_positive_test=True,
mocking_required=False,
with_function_id=True,
is_mock_local_function=False,
test_data={
"name": "Test Function",
"funcowner": "",
"pronamespace": 2200,
"prorettypename": "character varying",
"lanname": "sql",
"prosrc": "select '1'",
"probin": "$libdir/",
"variables": [],
"seclabels": [],
"acl": [],
# Unlike an added IN/INOUT/VARIADIC argument, an added
# OUT argument does not change the function's
# identity/signature, but it does change the shape of
# the returned row, which PostgreSQL rejects outright
# (SQLSTATE 42P13). This must be rejected with a
# distinct, accurate error message.
"arguments": json.dumps({
"added": [{
"argname": "new_out_arg",
"argtype": "integer",
"argmode": "OUT"
}]
})
},
mock_data={},
expected_data={
"status_code": 500,
"check_errormsg": "returned row"
}
),
),
(
'Fetch Function msql fetch properties not found',
dict(
@@ -222,5 +303,11 @@ class FunctionGetmsqlTestCase(BaseTestGenerator):
self.assertEqual(response.status_code,
self.expected_data['status_code'])
if 'check_string' in self.expected_data:
self.assertIn(self.expected_data['check_string'],
response.json['data'])
if 'check_errormsg' in self.expected_data:
self.assertIn(self.expected_data['check_errormsg'],
response.json['errormsg'])
# Disconnect the database
database_utils.disconnect_database(self, self.server_id, self.db_id)
@@ -21,10 +21,77 @@ class ProcedurePutTestCase(BaseTestGenerator):
""" This class will update new procedure under schema node. """
scenarios = [
# Fetching default URL for procedure node.
('Fetch Procedure Node URL',
dict(url='/browser/procedure/obj/'))
('Fetch Procedure Node URL', dict(
url='/browser/procedure/obj/',
is_add_argument=False,
expected_data={
"status_code": 200
}
)),
(
'Fetch Procedure update with newly added IN argument is '
'rejected',
dict(
url='/browser/procedure/obj/',
# PostgreSQL cannot add an IN argument to an existing
# procedure via CREATE OR REPLACE (it would create a
# separate, overloaded routine instead), so this must be
# rejected with a clear error rather than silently
# producing SQL that orphans a routine.
is_add_argument=True,
test_data={
"arguments": {
"added": [{
"argname": "new_arg",
"argtype": "integer",
"argmode": "IN",
}]
}
},
expected_data={
"status_code": 500,
"check_errormsg": "overloaded"
}
),
),
(
'Fetch Procedure update with newly added OUT argument is '
'rejected',
dict(
url='/browser/procedure/obj/',
# Unlike an added IN/INOUT/VARIADIC argument, an added
# OUT argument does not change the procedure's
# identity/signature, but it does change the shape of the
# returned row, which PostgreSQL rejects outright
# (SQLSTATE 42P13). This must be rejected with a
# distinct, accurate error message.
is_add_argument=True,
test_data={
"arguments": {
"added": [{
"argname": "new_out_arg",
"argtype": "integer",
"argmode": "OUT",
}]
}
},
expected_data={
"status_code": 500,
"check_errormsg": "returned row"
}
),
),
]
def update_procedure(self, proc_id, data):
return self.tester.put(
self.url + str(utils.SERVER_GROUP) +
'/' + str(self.server_id) + '/' + str(self.db_id) + '/' +
str(self.schema_id) + '/' +
str(proc_id),
data=json.dumps(data),
follow_redirects=True)
def runTest(self):
""" This function will update procedure under database node. """
super().setUp()
@@ -47,14 +114,15 @@ class ProcedurePutTestCase(BaseTestGenerator):
"dependsonextensions": ["plpgsql"]
}
put_response = self.tester.put(
self.url + str(utils.SERVER_GROUP) +
'/' + str(self.server_id) + '/' + str(self.db_id) + '/' +
str(self.schema_id) + '/' +
str(proc_id),
data=json.dumps(data),
follow_redirects=True)
self.assertEqual(put_response.status_code, 200)
if getattr(self, 'is_add_argument', False):
data['arguments'] = self.test_data['arguments']
response = self.update_procedure(proc_id, data)
self.assertEqual(response.status_code,
self.expected_data['status_code'])
if 'check_errormsg' in self.expected_data:
self.assertIn(self.expected_data['check_errormsg'],
response.json['errormsg'])
# Disconnect the database
database_utils.disconnect_database(self, self.server_id, self.db_id)
@@ -540,7 +540,10 @@ class SequenceView(PGChildNodeView, SchemaDiffObjectCompare):
Returns:
"""
data = request.form if request.form else json.loads(
# request.form is immutable, and get_SQL adds a 'restart' of its
# own when the new bounds have left the sequence outside them, so
# take a mutable copy of it.
data = dict(request.form) if request.form else json.loads(
request.data
)
sql, _ = self.get_SQL(gid, sid, did, data, scid, seid)
@@ -628,6 +631,50 @@ class SequenceView(PGChildNodeView, SchemaDiffObjectCompare):
status=200
)
def _add_restart_for_new_bounds(self, data, old_data):
"""
Ask for the sequence to be repositioned when the bounds being set
would leave it outside them.
PostgreSQL will not raise a sequence's MINVALUE above, or lower its
MAXVALUE below, the value the sequence currently sits at: it
rejects the whole statement with "RESTART value (n) cannot be less
than MINVALUE (m)", so every other change in it is lost too.
Repositioning onto the nearest value the new bounds allow is the
only way such a change can be applied, so ask for it rather than
generating a statement that cannot run (#10298). A sequence already
within its new bounds is left where it is, because handing out
values that have been used already would be worse than either.
:param data: The change being applied, modified in place
:param old_data: The sequence as it stands
"""
minimum = data.get('minimum')
maximum = data.get('maximum')
if minimum is None and maximum is None:
return
current = data.get('current_value')
if current is None:
sql = render_template(
"/".join([self.template_path, 'get_def.sql']),
data=old_data, conn=self.conn
)
status, res = self.conn.execute_dict(sql)
if not status or not res['rows']:
return
current = res['rows'][0]['last_value']
if current is None:
return
if minimum is not None and int(minimum) > int(current):
data['restart'] = int(minimum)
elif maximum is not None and int(maximum) < int(current):
data['restart'] = int(maximum)
def get_SQL(self, gid, sid, did, data, scid, seid=None,
add_not_exists_clause=False):
"""
@@ -667,6 +714,9 @@ class SequenceView(PGChildNodeView, SchemaDiffObjectCompare):
for arg in required_args:
if arg not in data:
data[arg] = old_data[arg]
self._add_restart_for_new_bounds(data, old_data)
sql = render_template(
"/".join([self.template_path, self._UPDATE_SQL]),
data=data, o_data=old_data, conn=self.conn
@@ -41,6 +41,9 @@ ALTER SEQUENCE IF EXISTS {{ conn|qtIdent(o_data.schema, data.name) }}
{% if data.maximum is defined %}
{% set defquery = defquery+'\n MAXVALUE '+data.maximum|string %}
{% endif %}
{% if data.restart is defined %}
{% set defquery = defquery+'\n RESTART '+data.restart|string %}
{% endif %}
{% if data.cache is defined %}
{% set defquery = defquery+'\n CACHE '+data.cache|string %}
{% endif %}
@@ -36,6 +36,9 @@ SELECT setval({{ seqname|qtLiteral(conn) }}, {{ data.current_value }}, false);
{% if data.maximum is defined %}
{% set defquery = defquery+'\n MAXVALUE '+data.maximum|string %}
{% endif %}
{% if data.restart is defined %}
{% set defquery = defquery+'\n RESTART '+data.restart|string %}
{% endif %}
{% if data.cache is defined %}
{% set defquery = defquery+'\n CACHE '+data.cache|string %}
{% endif %}
@@ -88,15 +88,21 @@ export default class ColumnSchema extends BaseUISchema {
}
if(this.nodeInfo && ('schema' in this.nodeInfo)) {
if(this.isNew(state)) {
return false;
// inheritedfrom/inheritedfromtable check is useful when we use this
// schema in table node. A column inherited from a parent table should
// always be read-only, whether it was already present when the table
// was opened (inheritedfromtable, set on the properties fetch) or was
// just added interactively via 'Inherited from table(s)'
// (inheritedfrom, set on the freshly fetched column). This must be
// checked before the isNew() check below, as interactively added
// inherited columns don't carry an attnum yet and would otherwise be
// (wrongly) treated as new, editable rows.
if (this.isInheritedColumn(state)) {
return true;
}
// We will disable control if it's system columns
// inheritedfrom check is useful when we use this schema in table node
// inheritedfrom has value then we should disable it
if (!isEmptyString(state.inheritedfrom)){
return true;
if(this.isNew(state)) {
return false;
}
// ie: it's position is less than 1
@@ -164,6 +170,33 @@ export default class ColumnSchema extends BaseUISchema {
return !isEmptyString(state.inheritedfromtype);
}
// A column inherited from a parent table should be treated as read-only,
// whether it was already present when the table was opened
// (inheritedfromtable, set on the properties fetch) or was just added
// interactively via 'Inherited from table(s)' (inheritedfrom, set on the
// freshly fetched column). Shared by any editable/disabled check that
// needs to special-case inherited columns.
isInheritedColumn(state) {
return !isEmptyString(state.inheritedfrom) ||
!isEmptyString(state.inheritedfromtable);
}
// Shared by the inline grid-cell 'Data type' editor and the expanded
// Definition tab's 'Data type' dropdown, so both apply the exact same
// edit_types restriction for the same column. isRowNew must be computed
// by the caller against the *row's* own state (not the enclosing table's
// or the field's own scalar state), since new columns can be set to any
// type whilst existing ones may only be altered to one of edit_types.
editTypesFilter(edit_types, isRowNew) {
return (options)=>{
if (isRowNew || this.inErd) {
return options;
}
let allowed = edit_types || [];
return _.filter(options, (o)=>allowed.indexOf(o.value) > -1);
};
}
get baseFields() {
let obj = this;
@@ -241,20 +274,21 @@ export default class ColumnSchema extends BaseUISchema {
group: gettext('Definition'), noEmpty: true,
editable: this.editableCheckForTable,
options: this.cltypeOptions, optionsLoaded: (options)=>{obj.datatypes = options;},
type: (state)=>{
// 'edit_types'/'attnum' are declared as deps purely so that the
// schema view resolves them against this row (not the whole table),
// and passes them through as the 2nd (depVals) argument below. This
// is what lets the expanded Definition tab's dropdown apply the same
// edit_types restriction as the inline grid-cell editor, whose
// 'cell' callback already receives the full row.
deps: ['edit_types', 'attnum'],
type: (state, depVals)=>{
let [edit_types, attnum] = depVals || [];
return {
type: 'select',
options: this.cltypeOptions,
controlProps: {
allowClear: false,
filter: (options)=>{
let result = options;
let edit_types = state?.edit_types || [];
if(!obj.isNew(state) && !this.inErd) {
result = _.filter(options, (o)=>edit_types.indexOf(o.value) > -1);
}
return result;
},
filter: obj.editTypesFilter(edit_types, obj.isNew({attnum})),
}
};
},
@@ -264,14 +298,7 @@ export default class ColumnSchema extends BaseUISchema {
options: this.cltypeOptions,
controlProps: {
allowClear: false,
filter: (options)=>{
let result = options;
let edit_types = row?.edit_types || [];
if(!obj.isNew(row) && !this.inErd) {
result = _.filter(options, (o)=>edit_types.indexOf(o.value) > -1);
}
return result;
},
filter: obj.editTypesFilter(row?.edit_types, obj.isNew(row)),
}
};
}
@@ -370,8 +397,8 @@ export default class ColumnSchema extends BaseUISchema {
return !obj.attlenRange(state);
},
editable: function(state) {
// inheritedfrom has value then we should disable it
if (!isEmptyString(state.inheritedfrom)) {
// A column inherited from a parent table should stay read-only.
if (obj.isInheritedColumn(state)) {
return false;
}
return Boolean(obj.attlenRange(state));
@@ -402,11 +429,11 @@ export default class ColumnSchema extends BaseUISchema {
return !this.attprecisionRange(state);
},
editable: function(state) {
// inheritedfrom has value then we should disable it
if (!isEmptyString(state.inheritedfrom)) {
// A column inherited from a parent table should stay read-only.
if (obj.isInheritedColumn(state)) {
return false;
}
return Boolean(this.attprecisionRange(state));
return Boolean(obj.attprecisionRange(state));
},
},{
id: 'min_val_attprecision', skipChange: true, visible: false, type: '',
@@ -0,0 +1,109 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""Unit tests for get_formatted_columns()'s SERIAL-column detection using
mocks.
These verify the ownership-vs-default-sequence comparison (#10100,
#10101) without requiring a running PostgreSQL server.
"""
from unittest.mock import MagicMock, patch
from pgadmin.utils.route import BaseTestGenerator
UTILS_MODULE = ('pgadmin.browser.server_groups.servers.databases.schemas.'
'tables.columns.utils')
def _make_column(**overrides):
"""A properties.sql result row, as returned for a single column."""
defaults = dict(
name='id', atttypid=23, attlen=4, attnum=1, attndims=0,
atttypmod=-1, attacl=None, attnotnull=False, attoptions=None,
attfdwoptions=None, attstattarget=-1, attstorage='p',
attidentity='', defval=None, typname='integer',
displaytypname='integer', cltype='integer',
inheritedfrom=None, inheritedid=None, elemoid=23,
typnspname='pg_catalog',
defaultstorage='p', description=None, indkey=None, isdup=False,
collspcname='', is_fk=False, seclabels=None, is_sys_column=False,
colconstype='n', genexpr=None, relname='t', is_view_only=False,
attcompression=None, seqrelid=None, defseqrelid=None,
)
defaults.update(overrides)
return defaults
class TestSerialColumnDetection(BaseTestGenerator):
"""Unit tests for ServerModule.get_formatted_columns() SERIAL
detection using mock rows."""
scenarios = [
('Split ownership/default keeps explicit nextval default',
dict(test_method='test_split_ownership_default_not_serial')),
('Genuine SERIAL column is reprojected',
dict(test_method='test_genuine_serial_is_detected')),
('Identity column is never reprojected',
dict(test_method='test_identity_column_not_serial')),
]
@patch(UTILS_MODULE + '.render_template', return_value='SELECT 1;')
def runTest(self, mock_render):
getattr(self, self.test_method)()
def _run(self, column_row):
from pgadmin.browser.server_groups.servers.databases.schemas.\
tables.columns.utils import get_formatted_columns
conn = MagicMock()
conn.execute_dict.return_value = (True, {'rows': [column_row]})
conn.execute_2darray.return_value = (True, {'rows': []})
with patch(UTILS_MODULE + '.column_formatter'):
data = get_formatted_columns(
conn, tid=1, data={}, other_columns=[],
table_or_type='table', template_path='columns/sql/default')
return data['columns'][0]
def test_split_ownership_default_not_serial(self):
# Column owns sequence 100 (pg_depend deptype='a'), but its
# DEFAULT's nextval() call references a different sequence, 200
# (pg_depend deptype='n' from the pg_attrdef entry). SERIAL must
# not be applied - ownership and default disagree.
col = _make_column(
defval="nextval('seq_used'::regclass)",
seqrelid=100, defseqrelid=200,
)
result = self._run(col)
self.assertEqual(result['typname'], 'integer')
self.assertEqual(result['cltype'], 'integer')
self.assertEqual(result['defval'], "nextval('seq_used'::regclass)")
def test_genuine_serial_is_detected(self):
# Ownership and default both point at the same sequence (100) -
# this is what a real SERIAL column looks like.
col = _make_column(
defval="nextval('t_id_seq'::regclass)",
seqrelid=100, defseqrelid=100,
)
result = self._run(col)
self.assertEqual(result['typname'], 'serial')
self.assertEqual(result['cltype'], 'serial')
self.assertEqual(result['defval'], '')
def test_identity_column_not_serial(self):
# Identity columns can carry an internal sequence dependency on
# both sides, but must never be reprojected as SERIAL.
col = _make_column(
defval=None, seqrelid=100, defseqrelid=100, attidentity='a',
)
result = self._run(col)
self.assertEqual(result['typname'], 'integer')
self.assertEqual(result['defval'], None)
@@ -17,8 +17,6 @@ from pgadmin.browser.server_groups.servers.databases.schemas.utils \
import DataTypeReader
from pgadmin.browser.server_groups.servers.utils import parse_priv_from_db, \
parse_priv_to_db
from pgadmin.browser.server_groups.servers.databases.utils \
import make_object_name
from functools import wraps
import re
@@ -226,6 +224,67 @@ def parse_options_for_column(db_variables):
return variables_lst
# The integer types a SERIAL declaration reverse-engineers into, mapped to
# the pseudo-type that produced them, and the reverse mapping for callers
# that need the real, alterable type behind a reprojected column.
SERIAL_TYPES = {
'integer': 'serial',
'smallint': 'smallserial',
'bigint': 'bigserial'
}
UNDERLYING_SERIAL_TYPES = {v: k for k, v in SERIAL_TYPES.items()}
def is_serial_column(col):
"""
Report whether a column is the reverse-engineered form of a SERIAL
declaration, which is the case only when it owns the very sequence
that its own nextval() default references.
Ownership comes from pg_depend (deptype='a', surfaced by
properties.sql as ``seqrelid``), whilst the default's own dependency
(deptype='n', from the pg_attrdef entry) is surfaced as
``defseqrelid``. The two are independent, so a column may own one
sequence whilst defaulting from another, and only the case where both
point at the same sequence is a SERIAL. Identity columns carry an
internal dependency too, but never a nextval() default, and are
excluded via attidentity regardless. Issues #9896, #10100, #10101.
:param col: Column properties, as returned by properties.sql
:return: True when the column is a SERIAL/SMALLSERIAL/BIGSERIAL
"""
defval = col.get('defval', '') or ''
return bool(col.get('seqrelid')) and defval.startswith("nextval('") \
and not col.get('attidentity') \
and col.get('seqrelid') == col.get('defseqrelid') \
and col.get('typname') in SERIAL_TYPES
def reproject_serial_column(col):
"""
Rewrite a column that owns the sequence behind its nextval() default
back into the SERIAL/SMALLSERIAL/BIGSERIAL pseudo-type it was declared
with, so that callers can emit round-trippable DDL. Columns that are
not SERIAL, including ones already reprojected, are left untouched.
:param col: Column properties, modified in place
:return: The same column
"""
if not is_serial_column(col):
return col
serial_type = SERIAL_TYPES[col['typname']]
col['displaytypname'] = serial_type
col['cltype'] = serial_type
col['typname'] = serial_type
col['defval'] = ''
return col
@get_template_path
def get_formatted_columns(conn, tid, data, other_columns,
table_or_type, template_path=None,
@@ -234,14 +293,18 @@ def get_formatted_columns(conn, tid, data, other_columns,
This function will iterate and return formatted data for all
the columns.
Serial-column detection is on by default: a column whose default
is ``nextval('<table>_<col>_seq'...)`` with an integer/smallint/
bigint type is the reverse-engineered form of a SERIAL declaration,
and we reproject it back to ``serial`` / ``smallserial`` /
``bigserial`` so callers can emit valid round-trippable DDL. Pass
``with_serial=False`` only if you genuinely need the raw libpq
representation (e.g. a low-level introspection caller that handles
the sequence itself). Issue #9896.
Serial-column detection is on by default: a column that owns the
sequence referenced by its ``nextval(...)`` default (with an
integer/smallint/bigint type) is the reverse-engineered form of a
SERIAL declaration, and we reproject it back to ``serial`` /
``smallserial`` / ``bigserial`` so callers can emit valid
round-trippable DDL. Ownership is determined from ``pg_depend`` (the
``seqrelid`` exposed by properties.sql), not by guessing the
sequence name, so it stays correct across renames and never rewrites
an unrelated column. Pass ``with_serial=False`` only if you genuinely
need the raw libpq representation (e.g. a low-level introspection
caller that handles the sequence itself). Issues #9896, #10100,
#10101.
:param conn: Connection Object
:param tid: Table ID
@@ -269,26 +332,7 @@ def get_formatted_columns(conn, tid, data, other_columns,
other_col['inheritedfrom']
if with_serial:
# Here we assume if a column is serial
serial_seq_name = make_object_name(
data['name'], col['name'], 'seq')
# replace the escaped quotes for comparison
defval = (col.get('defval', '') or '').replace("''", "'").\
replace('""', '"')
if serial_seq_name in defval and defval.startswith("nextval('")\
and col['typname'] in ('integer', 'smallint', 'bigint'):
serial_type = {
'integer': 'serial',
'smallint': 'smallserial',
'bigint': 'bigserial'
}[col['typname']]
col['displaytypname'] = serial_type
col['cltype'] = serial_type
col['typname'] = serial_type
col['defval'] = ''
reproject_serial_column(col)
data['columns'] = all_columns
@@ -19,6 +19,7 @@ from flask_babel import gettext
from pgadmin.browser.collection import CollectionNodeModule
from pgadmin.utils.ajax import make_json_response, \
make_response as ajax_response, internal_server_error
from pgadmin.user_login_check import pga_login_required
from config import PG_DEFAULT_DRIVER
from .type import ConstraintRegistry
@@ -112,6 +113,7 @@ blueprint = ConstraintsModule(__name__)
@blueprint.route('/nodes/<int:gid>/<int:sid>/<int:did>/<int:scid>/<int:tid>/')
@pga_login_required
def nodes(**kwargs):
"""
Returns all constraint as a tree node.
@@ -137,6 +139,7 @@ def nodes(**kwargs):
@blueprint.route('/obj/<int:gid>/<int:sid>/<int:did>/<int:scid>/<int:tid>/')
@pga_login_required
def proplist(**kwargs):
"""
Returns all constraint with properties.
@@ -164,6 +167,7 @@ def proplist(**kwargs):
methods=['DELETE'])
@blueprint.route('/delete/<int:gid>/<int:sid>/<int:did>/<int:scid>/<int:tid>/',
methods=['DELETE'])
@pga_login_required
def delete(**kwargs):
"""
Delete multiple constraints under the table.
@@ -8,7 +8,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS "Idx_$%{}[]()&*^!@""'`\/#"
INCLUDE(name, id)
NULLS NOT DISTINCT
WITH (fillfactor=10, deduplicate_items=False)
TABLESPACE pg_default
WHERE id < 100;
ALTER INDEX public."Idx_$%{}[]()&*^!@""'`\/#"
@@ -8,7 +8,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS "Idx1_$%{}[]()&*^!@""'`\/#"
INCLUDE(name, id)
NULLS NOT DISTINCT
WITH (fillfactor=10)
TABLESPACE pg_default;
;
ALTER TABLE IF EXISTS public.test_table_for_indexes
CLUSTER ON "Idx1_$%{}[]()&*^!@""'`\/#";
@@ -7,7 +7,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS "Idx1_$%{}[]()&*^!@""'`\/#"
(id DESC NULLS FIRST, name COLLATE pg_catalog."POSIX" text_pattern_ops DESC NULLS FIRST)
INCLUDE(name, id)
NULLS NOT DISTINCT
TABLESPACE pg_default;
;
COMMENT ON INDEX public."Idx1_$%{}[]()&*^!@""'`\/#"
IS 'Test Comment';
@@ -8,7 +8,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS "Idx_$%{}[]()&*^!@""'`\/#"
INCLUDE(name, id)
NULLS NOT DISTINCT
WITH (fillfactor=10)
TABLESPACE pg_default
WHERE id < 100;
COMMENT ON INDEX public."Idx_$%{}[]()&*^!@""'`\/#"
@@ -4,7 +4,7 @@ CREATE UNIQUE INDEX "Idx_$%{}[]()&*^!@""'`\/#"
INCLUDE(name, id)
NULLS NOT DISTINCT
WITH (fillfactor=10)
TABLESPACE pg_default
WHERE id < 100;
COMMENT ON INDEX public."Idx_$%{}[]()&*^!@""'`\/#"
@@ -8,7 +8,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS "Idx_$%{}[]()&*^!@""'`\/#"
INCLUDE(name, id)
NULLS NOT DISTINCT
WITH (fillfactor=10)
TABLESPACE pg_default
WHERE id < 100;
COMMENT ON INDEX public."Idx_$%{}[]()&*^!@""'`\/#"
@@ -4,7 +4,7 @@ CREATE UNIQUE INDEX "Idx_$%{}[]()&*^!@""'`\/#"
INCLUDE(name, id)
NULLS NOT DISTINCT
WITH (fillfactor=10)
TABLESPACE pg_default
WHERE id < 100;
COMMENT ON INDEX public."Idx_$%{}[]()&*^!@""'`\/#"
@@ -7,4 +7,4 @@ CREATE UNIQUE INDEX IF NOT EXISTS "Idx_$%{}[]()&*^!@""'`\/#"
(id DESC NULLS FIRST, name COLLATE pg_catalog."POSIX" text_pattern_ops DESC NULLS FIRST)
INCLUDE(name, id)
NULLS NOT DISTINCT
TABLESPACE pg_default;
;
@@ -3,4 +3,4 @@ CREATE UNIQUE INDEX "Idx_$%{}[]()&*^!@""'`\/#"
(id DESC NULLS FIRST, name COLLATE pg_catalog."POSIX" text_pattern_ops DESC NULLS FIRST)
INCLUDE(name, id)
NULLS NOT DISTINCT
TABLESPACE pg_default;
;
@@ -8,7 +8,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS "Idx_$%{}[]()&*^!@""'`\/#"
INCLUDE(name, id)
NULLS NOT DISTINCT
WITH (fillfactor=10)
TABLESPACE pg_default
WHERE id < 100;
COMMENT ON INDEX public."Idx_$%{}[]()&*^!@""'`\/#"
@@ -4,7 +4,7 @@ CREATE UNIQUE INDEX "Idx_$%{}[]()&*^!@""'`\/#"
INCLUDE(name, id)
NULLS NOT DISTINCT
WITH (fillfactor=10)
TABLESPACE pg_default
WHERE id < 100;
COMMENT ON INDEX public."Idx_$%{}[]()&*^!@""'`\/#"
@@ -4,7 +4,7 @@ CREATE UNIQUE INDEX "Idx_$%{}[]()&*^!@""'`\/#"
INCLUDE(name, id)
NULLS NOT DISTINCT
WITH (fillfactor=10, deduplicate_items=False)
TABLESPACE pg_default
WHERE id < 100;
ALTER INDEX public."Idx_$%{}[]()&*^!@""'`\/#"
@@ -8,7 +8,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS "Idx_$%{}[]()&*^!@""'`\/#"
INCLUDE(name, id)
NULLS NOT DISTINCT
WITH (fillfactor=10, deduplicate_items=False)
TABLESPACE pg_default
WHERE id < 100;
ALTER INDEX public."Idx_$%{}[]()&*^!@""'`\/#"
@@ -7,7 +7,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS "Idx3_$%{}[]()&*^!@""'`\/#"
(id ASC NULLS LAST, lower(name) COLLATE pg_catalog."POSIX" text_pattern_ops ASC NULLS LAST)
INCLUDE(name, id)
WITH (fillfactor=10)
TABLESPACE pg_default
WHERE id < 100;
COMMENT ON INDEX public."Idx3_$%{}[]()&*^!@""'`\/#"
@@ -7,7 +7,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS "Idx_$%{}[]()&*^!@""'`\/#"
(id ASC NULLS LAST, name COLLATE pg_catalog."POSIX" text_pattern_ops ASC NULLS LAST)
INCLUDE(name, id)
WITH (fillfactor=10, deduplicate_items=False)
TABLESPACE pg_default
WHERE id < 100;
ALTER INDEX public."Idx_$%{}[]()&*^!@""'`\/#"
@@ -7,7 +7,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS "Idx1_$%{}[]()&*^!@""'`\/#"
(id DESC NULLS FIRST, name COLLATE pg_catalog."POSIX" text_pattern_ops DESC NULLS FIRST)
INCLUDE(name, id)
WITH (fillfactor=10)
TABLESPACE pg_default;
;
ALTER TABLE IF EXISTS public.test_table_for_indexes
CLUSTER ON "Idx1_$%{}[]()&*^!@""'`\/#";
@@ -6,7 +6,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS "Idx1_$%{}[]()&*^!@""'`\/#"
ON public.test_table_for_indexes USING btree
(id DESC NULLS FIRST, name COLLATE pg_catalog."POSIX" text_pattern_ops DESC NULLS FIRST)
INCLUDE(name, id)
TABLESPACE pg_default;
;
COMMENT ON INDEX public."Idx1_$%{}[]()&*^!@""'`\/#"
IS 'Test Comment';
@@ -7,7 +7,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS "Idx_$%{}[]()&*^!@""'`\/#"
(id ASC NULLS FIRST, name COLLATE pg_catalog."POSIX" text_pattern_ops ASC NULLS FIRST)
INCLUDE(name, id)
WITH (fillfactor=10, deduplicate_items=False)
TABLESPACE pg_default
WHERE id < 100;
COMMENT ON INDEX public."Idx_$%{}[]()&*^!@""'`\/#"
@@ -3,7 +3,7 @@ CREATE UNIQUE INDEX "Idx_$%{}[]()&*^!@""'`\/#"
(id ASC NULLS FIRST, name COLLATE pg_catalog."POSIX" text_pattern_ops ASC NULLS FIRST)
INCLUDE(name, id)
WITH (fillfactor=10, deduplicate_items=False)
TABLESPACE pg_default
WHERE id < 100;
COMMENT ON INDEX public."Idx_$%{}[]()&*^!@""'`\/#"
@@ -7,7 +7,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS "Idx_$%{}[]()&*^!@""'`\/#"
(id ASC NULLS LAST, name COLLATE pg_catalog."POSIX" text_pattern_ops ASC NULLS LAST)
INCLUDE(name, id)
WITH (fillfactor=10, deduplicate_items=False)
TABLESPACE pg_default
WHERE id < 100;
COMMENT ON INDEX public."Idx_$%{}[]()&*^!@""'`\/#"
@@ -3,7 +3,7 @@ CREATE UNIQUE INDEX "Idx_$%{}[]()&*^!@""'`\/#"
(id ASC NULLS LAST, name COLLATE pg_catalog."POSIX" text_pattern_ops ASC NULLS LAST)
INCLUDE(name, id)
WITH (fillfactor=10, deduplicate_items=False)
TABLESPACE pg_default
WHERE id < 100;
COMMENT ON INDEX public."Idx_$%{}[]()&*^!@""'`\/#"
@@ -6,4 +6,4 @@ CREATE UNIQUE INDEX IF NOT EXISTS "Idx_$%{}[]()&*^!@""'`\/#"
ON public.test_table_for_indexes USING btree
(id DESC NULLS FIRST, name COLLATE pg_catalog."POSIX" text_pattern_ops DESC NULLS FIRST)
INCLUDE(name, id)
TABLESPACE pg_default;
;
@@ -2,4 +2,4 @@ CREATE UNIQUE INDEX "Idx_$%{}[]()&*^!@""'`\/#"
ON public.test_table_for_indexes USING btree
(id DESC NULLS FIRST, name COLLATE pg_catalog."POSIX" text_pattern_ops DESC NULLS FIRST)
INCLUDE(name, id)
TABLESPACE pg_default;
;
@@ -7,7 +7,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS "Idx_$%{}[]()&*^!@""'`\/#"
(id DESC NULLS LAST, name COLLATE pg_catalog."POSIX" text_pattern_ops DESC NULLS LAST)
INCLUDE(name, id)
WITH (fillfactor=10, deduplicate_items=False)
TABLESPACE pg_default
WHERE id < 100;
COMMENT ON INDEX public."Idx_$%{}[]()&*^!@""'`\/#"
@@ -3,7 +3,7 @@ CREATE UNIQUE INDEX "Idx_$%{}[]()&*^!@""'`\/#"
(id DESC NULLS LAST, name COLLATE pg_catalog."POSIX" text_pattern_ops DESC NULLS LAST)
INCLUDE(name, id)
WITH (fillfactor=10, deduplicate_items=False)
TABLESPACE pg_default
WHERE id < 100;
COMMENT ON INDEX public."Idx_$%{}[]()&*^!@""'`\/#"
@@ -7,7 +7,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS "Idx3_$%{}[]()&*^!@""'`\/#"
(id ASC NULLS LAST, lower(name) COLLATE pg_catalog."POSIX" text_pattern_ops ASC NULLS LAST)
INCLUDE(name, id)
WITH (fillfactor=10)
TABLESPACE pg_default
WHERE id < 100;
COMMENT ON INDEX public."Idx3_$%{}[]()&*^!@""'`\/#"
Loaded 100 of 284 files, more files were not shown because too many files have changed in this diff. Show more