1
0
mirror of synced 2026-10-23 07:08:55 +03:00

Merge remote-tracking branch 'origin/master' into cve-9.16

# Conflicts:
#	docs/en_US/release_notes_9_16.rst
This commit is contained in:
Ashesh Vashi
2026-06-16 11:47:25 +05:30
14 changed files with 1739 additions and 1799 deletions

View File

@@ -63,7 +63,7 @@ and secure.
"OAUTH2_API_BASE_URL", "Oauth2 base URL endpoint to make requests simple, ex: *https://api.github.com/*"
"OAUTH2_USERINFO_ENDPOINT", "User Endpoint, ex: *user* (for github, or *user/emails* if the user's email address is private) and *userinfo* (for google). **For OIDC providers**, this is optional if the ID token contains sufficient claims (email, preferred_username, or sub)."
"OAUTH2_SCOPE", "Oauth scope, ex: 'openid email profile'. **For OIDC providers**, include 'openid' scope to receive an ID token."
"OAUTH2_ICON", "The Font-awesome icon to be placed on the oauth2 button, ex: fa-github"
"OAUTH2_ICON", "The Font Awesome icon to be placed on the oauth2 button. A brand icon name (e.g. fa-github) uses the brands style by default; to use another style, include the style class explicitly (e.g. 'fas fa-key')."
"OAUTH2_BUTTON_COLOR", "Oauth2 button color"
"OAUTH2_USERNAME_CLAIM", "The claim which is used for the username. If the value is empty, **for OIDC providers** pgAdmin will use: 1) email, 2) preferred_username, or 3) sub (in that order). **For OAuth2 providers** without OIDC, email is required. Ex: *oid* (for AzureAD), *email* (for Github), *preferred_username* (for Keycloak)"
"OAUTH2_AUTO_CREATE_USER", "Set the value to *True* if you want to automatically

View File

@@ -27,6 +27,7 @@ New features
************
| `Issue #2431 <https://github.com/pgadmin-org/pgadmin4/issues/2431>`_ - Added an option to colourize panel and tab headers based on the connected server's colour, making it easier to tell which server a tab is connected to at a glance.
| `Issue #7641 <https://github.com/pgadmin-org/pgadmin4/issues/7641>`_ - Allow the OAuth2 login button icon to use any Font Awesome style (e.g. ``fas fa-key``), not only brand icons.
| `Issue #9301 <https://github.com/pgadmin-org/pgadmin4/issues/9301>`_ - Added a "Back to login" link to the Forgot Password and Reset Password pages.
| `Issue #9626 <https://github.com/pgadmin-org/pgadmin4/issues/9626>`_ - Add support for the TOAST tuple target storage parameter in the Materialized View dialog.
| `Issue #9646 <https://github.com/pgadmin-org/pgadmin4/issues/9646>`_ - Make the init container security context in the Helm chart configurable via containerSecurityContext, consistent with the main container.
@@ -46,11 +47,13 @@ Housekeeping
| `Issue #10049 <https://github.com/pgadmin-org/pgadmin4/issues/10049>`_ - Bump vulnerable transitive dependencies (tar, flatted) to clear security advisories.
| `Issue #10050 <https://github.com/pgadmin-org/pgadmin4/issues/10050>`_ - Rebase the version-specific SQL templates so the default targets PostgreSQL 14, the oldest supported server version, dropping the obsolete sub-14 template buckets.
| `Issue #10063 <https://github.com/pgadmin-org/pgadmin4/issues/10063>`_ - Strip the foreign-architecture slice from the macOS bundle so single-arch builds no longer ship the universal2 Python framework's unused arm64/x86_64 code.
| `Issue #10094 <https://github.com/pgadmin-org/pgadmin4/issues/10094>`_ - Update the Italian translation for v9.16.
Bug fixes
*********
| `Issue #6308 <https://github.com/pgadmin-org/pgadmin4/issues/6308>`_ - Fix the infinite loading spinner after an idle database connection is silently dropped, by detecting stale connections and offering a reconnect dialog.
| `Issue #7346 <https://github.com/pgadmin-org/pgadmin4/issues/7346>`_ - Fixed an issue where preferences set via the CLI (setup.py set-prefs) were not validated, so invalid values were stored silently; CLI preference values are now validated against the preference type and rejected (and reported) if invalid.
| `Issue #7596 <https://github.com/pgadmin-org/pgadmin4/issues/7596>`_ - Fix the Query Tool turning into a blank white screen when the runtime has a malformed default locale, by guarding the Query History date/time formatting against the resulting RangeError.
| `Issue #8318 <https://github.com/pgadmin-org/pgadmin4/issues/8318>`_ - Fixed an error ("i.default.find(...) is undefined") that prevented deleting a table or relationship link in the ERD tool when a foreign key referenced a column that had been renamed.
| `Issue #9060 <https://github.com/pgadmin-org/pgadmin4/issues/9060>`_ - Fixed an issue in the Create Table dialog where renaming a column did not update the column references in foreign key and unique constraint definitions for the new table.
@@ -59,6 +62,7 @@ Bug fixes
| `Issue #9595 <https://github.com/pgadmin-org/pgadmin4/issues/9595>`_ - Fix missing ALTER ... SET DEFAULT statements for inherited columns in the generated table SQL/EDIT script.
| `Issue #9677 <https://github.com/pgadmin-org/pgadmin4/issues/9677>`_ - Fix the Unlogged table toggle in table properties not generating any ALTER TABLE ... SET LOGGED/UNLOGGED statement.
| `Issue #9701 <https://github.com/pgadmin-org/pgadmin4/issues/9701>`_ - Ensure pgAdmin uses psycopg3 rather than psycopg2 when connecting to the configuration database via PGADMIN_CONFIG_CONFIG_DATABASE_URI.
| `Issue #9744 <https://github.com/pgadmin-org/pgadmin4/issues/9744>`_ - Fix a View/Edit Data crash when the session contains a transaction object that is not filter-capable (e.g. left by the Query Tool or persisted by an older version), which could prevent the desktop application from loading after an upgrade.
| `Issue #9762 <https://github.com/pgadmin-org/pgadmin4/issues/9762>`_ - Fix the "Cannot read properties of undefined (reading 'map')" crash in the desktop runtime when a menu refresh is triggered before the application menus have been received.
| `Issue #9766 <https://github.com/pgadmin-org/pgadmin4/issues/9766>`_ - Fixed an issue where a server's custom foreground colour was not applied to the object counts (children count) and column type labels shown in the object explorer.
| `Issue #9782 <https://github.com/pgadmin-org/pgadmin4/issues/9782>`_ - Optimise pgAdmin startup time by replacing the fixed server-ping interval with adaptive pinging that backs off when no servers are configured.
@@ -106,7 +110,7 @@ Non-breaking ``dependabot`` updates aggregated for v9.16.
Python:
| ``certifi`` 2026.4.22 -> 2026.5.20
| ``cryptography`` 47.0 -> 48.0
| ``cryptography`` 47.0 -> 49.0
| ``Flask-Security-Too`` 5.4 -> 5.6 (``python_version <= '3.9'``)
| ``google-auth-oauthlib`` 1.3.1 -> 1.4.0 (``python_version > '3.9'``; Python 3.9 stays on 1.3.1)
| ``requests`` >=2.33.1 -> >=2.34.2 (``python_version > '3.9'``)
@@ -124,6 +128,6 @@ JavaScript (``web/``):
JavaScript (``runtime/``):
| ``axios`` 1.16.0 -> 1.17.0
| ``axios`` 1.16.0 -> 1.18.0
| ``electron`` 41.5.0 -> 42.3.3
| ``eslint`` 10.3.0 -> 10.4.1

View File

@@ -1,7 +1,7 @@
apiVersion: v2
name: pgadmin4-helm
version: 9.15.0
appVersion: 9.15.0
version: 9.16.0
appVersion: 9.16.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"]

View File

@@ -22,7 +22,7 @@ bcrypt==5.0.*
boto3==1.42.*; python_version <= '3.9'
boto3==1.43.*; python_version > '3.9'
certifi==2026.5.20
cryptography==48.0.*
cryptography==49.0.*
Flask-Babel==4.0.*
Flask-Compress==1.*
Flask-Login==0.*

View File

@@ -19,7 +19,7 @@
"globals": "^17.6.0"
},
"dependencies": {
"axios": "^1.17.0",
"axios": "^1.18.0",
"electron-context-menu": "^4.1.2",
"electron-store": "^11.0.2"
}

View File

@@ -288,15 +288,15 @@ __metadata:
languageName: node
linkType: hard
"axios@npm:^1.17.0":
version: 1.17.0
resolution: "axios@npm:1.17.0"
"axios@npm:^1.18.0":
version: 1.18.0
resolution: "axios@npm:1.18.0"
dependencies:
follow-redirects: "npm:^1.16.0"
form-data: "npm:^4.0.5"
https-proxy-agent: "npm:^5.0.1"
proxy-from-env: "npm:^2.1.0"
checksum: 10c0/c4fa19ff3a3a63bde48beec03ad816b133b9a6385cccffffe172577ab18c6a70e299280d57f12c80c867fe25df41f92cb91d3a8258708a6d2be3e9e085f92650
checksum: 10c0/f88aae13fd9dd395dea3053c7299178fee62f436f20424a1d6d79ec11ffb7656ba664b4630defbadab2c387b6ea07e29afb9f28b26d8e0e49d45be766983290d
languageName: node
linkType: hard
@@ -1200,7 +1200,7 @@ __metadata:
resolution: "pgadmin4@workspace:."
dependencies:
"@eslint/js": "npm:^10.0.1"
axios: "npm:^1.17.0"
axios: "npm:^1.18.0"
electron: "npm:^42.3.3"
electron-context-menu: "npm:^4.1.2"
electron-store: "npm:^11.0.2"

View File

@@ -46,9 +46,16 @@ export default function LoginPage({userLanguage, langOptions, forgotPassUrl, csr
}
{authSources?.includes?.(authSourcesEnum.OAUTH2) &&
oauth2Config.map((oauth)=>{
// Allow the configured icon to specify its own Font Awesome style
// (e.g. 'fas fa-key'). Default to the brands style ('fab') when
// only an icon name is given, for backward compatibility.
const iconStyles = ['fab', 'fas', 'far', 'fal', 'fat', 'fad',
'fa-brands', 'fa-solid', 'fa-regular', 'fa-light', 'fa-thin', 'fa-duotone'];
const hasStyle = oauth.OAUTH2_ICON?.split(/\s+/).some((c)=>iconStyles.includes(c));
const iconClassName = hasStyle ? oauth.OAUTH2_ICON : 'fab '+oauth.OAUTH2_ICON;
return (
<SecurityButton key={oauth.OAUTH2_NAME} name="oauth2_button" value={oauth.OAUTH2_NAME} style={{backgroundColor: oauth.OAUTH2_BUTTON_COLOR}}>
<Icon className={'fab '+oauth.OAUTH2_ICON} style={{ fontSize: '1.5em', marginRight: '8px' }} />{gettext('Login with %s', oauth.OAUTH2_DISPLAY_NAME)}
<Icon className={iconClassName} style={{ fontSize: '1.5em', marginRight: '8px' }} />{gettext('Login with %s', oauth.OAUTH2_DISPLAY_NAME)}
</SecurityButton>
);
})

View File

@@ -321,7 +321,15 @@ def initialize_viewdata(trans_id, cmd_type, obj_type, sgid, sid, did, obj_id):
if str(trans_id) in sql_grid_data:
old_trans_obj = pickle.loads(
sql_grid_data[str(trans_id)]['command_obj'])
if old_trans_obj.did == did and old_trans_obj.obj_id == obj_id:
# Only restore the filter/sorting when the previously stored object is
# a filter-capable (View/Edit Data) command. The same trans_id may
# have been used by a non-filter command such as the Query Tool, or by
# an incompatible object persisted by an older version - neither
# carries the _row_filter/_data_sorting attributes, and blindly
# accessing them raises an AttributeError that prevents the tool (and,
# in desktop mode, the application) from loading.
if isinstance(old_trans_obj, SQLFilter) and \
old_trans_obj.did == did and old_trans_obj.obj_id == obj_id:
command_obj.set_filter(old_trans_obj._row_filter)
command_obj.set_data_sorting(
dict(data_sorting=old_trans_obj._data_sorting), True)

View File

@@ -0,0 +1,115 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import uuid
import pickle
import secrets
from pgadmin.utils.route import BaseTestGenerator
from pgadmin.browser.server_groups.servers.databases.tests import utils as \
database_utils
from regression import parent_node_dict
from regression.python_test_utils import test_utils
from pgadmin.tools.sqleditor.command import QueryToolCommand
class TestViewDataRestoreStaleTransObj(BaseTestGenerator):
"""
Regression test for issue #9744.
When View/Edit Data initialises a transaction it tries to restore the
filter/sorting from any command object previously stored in the session
under the same trans_id. That stored object is not always a filter-capable
(View/Edit Data) command - the same trans_id may have been used by the
Query Tool, or the session may contain an incompatible object persisted by
an older version after an upgrade. In those cases the object has no
_row_filter/_data_sorting attributes, and blindly accessing them raised an
AttributeError that returned a 500 and, in desktop mode, prevented the
application from loading.
This test seeds the session with a pickled QueryToolCommand (which is what
a restored Query Tool transaction leaves in session['gridData']) under the
trans_id used to initialise View/Edit Data, and asserts the request
succeeds instead of crashing.
"""
scenarios = [
('Initialize View/Edit Data over a stale non-filter trans object',
dict())
]
def setUp(self):
self.server_id = self.server_information['server_id']
self.database_info = parent_node_dict["database"][-1]
self.db_name = self.database_info["db_name"]
self.db_id = self.database_info["db_id"]
self.connection = test_utils.get_db_connection(
self.db_name,
self.server['username'],
self.server['db_password'],
self.server['host'],
self.server['port']
)
db_con = database_utils.connect_database(self, test_utils.SERVER_GROUP,
self.server_id, self.db_id)
if not db_con["info"] == "Database connected.":
raise Exception("Could not connect to the database.")
def runTest(self):
self.table = "test_table_%s" % (str(uuid.uuid4())[1:8])
# Note: do not give the primary key an explicit constraint name -
# other tests in this package create tables in the same database, and
# a hard-coded constraint name would collide across the suite. Letting
# PostgreSQL auto-name it (<table>_pkey) keeps it unique.
table_sql = """Create Table %s(
id integer Not Null Primary Key
);""" % self.table
test_utils.create_table_with_query(self.server, self.db_name,
table_sql)
# Fetch the table OID
pg_cursor = self.connection.cursor()
pg_cursor.execute("""Select oid FROM pg_catalog.pg_class WHERE
relname = '%s' AND relkind IN ('r','s','t')""" % self.table)
table_id = pg_cursor.fetchall()[0][0]
trans_id = str(secrets.choice(range(1, 9999999)))
# Build a QueryToolCommand - the kind of object the Query Tool stores
# in session['gridData'] - and give it the same did/obj_id as the
# table so the restore guard in initialize_viewdata is reached. A
# QueryToolCommand has no _row_filter attribute, which is what
# previously triggered the AttributeError.
stale_obj = QueryToolCommand(
sgid=test_utils.SERVER_GROUP, sid=self.server_id, did=self.db_id)
stale_obj.obj_id = table_id
with self.tester.session_transaction() as sess:
grid_data = sess.get('gridData', {})
grid_data[trans_id] = {
'command_obj': pickle.dumps(stale_obj, -1)
}
sess['gridData'] = grid_data
url = '/sqleditor/initialize/viewdata/{0}/3/table/{1}/{2}/{3}/{4}' \
.format(trans_id, test_utils.SERVER_GROUP, self.server_id,
self.db_id, table_id)
response = self.tester.post(url)
# Before the fix this returned 500 with:
# 'QueryToolCommand' object has no attribute '_row_filter'
self.assertEqual(response.status_code, 200)
def tearDown(self):
self.connection.cursor().execute(
"DROP TABLE IF EXISTS %s" % self.table)
self.connection.commit()
self.connection.close()
database_utils.disconnect_database(self, self.server_id,
self.db_id)

File diff suppressed because it is too large Load Diff

View File

@@ -165,12 +165,15 @@ class _Preference():
return False, None
def set(self, value):
def set(self, value, user_id=None):
"""
set
Set the value into the configuration table for this current user.
Set the value into the configuration table for this current user
(or the given user_id, when called outside a request context).
:param value: Value to be set
:param user_id: User to set the preference for; defaults to the
current user.
:returns: nothing.
"""
@@ -217,14 +220,15 @@ class _Preference():
"Invalid value for {0} option.".format(
error_map.get(self._type, self._type)))
uid = user_id if user_id is not None else current_user.id
pref = UserPrefTable.query.filter_by(
pid=self.pid
).filter_by(uid=current_user.id).first()
).filter_by(uid=uid).first()
value = "{}".format(value)
if pref is None:
pref = UserPrefTable(
uid=current_user.id, pid=self.pid, value=value
uid=uid, pid=self.pid, value=value
)
db.session.add(pref)
else:
@@ -597,30 +601,53 @@ class Preferences():
@classmethod
def save_cli(cls, mid, cid, pid, user_id, value):
"""
save
Update the value for the preference in the configuration database.
save_cli
Validate and update the value for the preference in the
configuration database for the given user (used by the CLI).
:param mid: Module ID
:param cid: Category ID
:param pid: Preference ID
:param user_id: User to set the preference for
:param value: Value for the options
"""
# Find the entry for this module in the configuration database.
module = ModulePrefTable.query.filter_by(id=mid).first()
pref = UserPrefTable.query.filter_by(
pid=pid
).filter_by(uid=user_id).first()
if module is None:
return False, gettext("Could not find the specified module.")
value = "{}".format(value)
if pref is None:
pref = UserPrefTable(
uid=user_id, pid=pid, value=value
)
db.session.add(pref)
else:
pref.value = value
db.session.commit()
m = cls.modules.get(module.name)
if m is None:
return False, gettext(
"Module '{0}' is no longer in use."
).format(module.name)
return True, None
category = None
for c in m.categories:
cat = m.categories[c]
if cid == cat['id']:
category = cat
break
if category is None:
return False, gettext(
"Module '{0}' does not have category with id '{1}'"
).format(module.name, cid)
preference = None
for p in category['preferences']:
pref = (category['preferences'])[p]
if pref.pid == pid:
preference = pref
break
if preference is None:
return False, gettext("Could not find the specified preference.")
# Delegate to set() so the value is validated against the
# preference type, just like the GUI path.
return preference.set(value, user_id=user_id)
@classmethod
def save(cls, mid, cid, pid, value):

View File

@@ -728,6 +728,7 @@ class ManagePreferences:
prefs = ManagePreferences.fetch_prefs(True)
app = create_app(config.APP_NAME + '-cli')
invalid_prefs = []
invalid_value_prefs = []
valid_prefs = []
with app.app_context():
from pgadmin.preferences import save_pref
@@ -749,11 +750,13 @@ class ManagePreferences:
'name': final_opt[2],
'user_id': user_id,
'value': val}
save_pref(_row)
valid_prefs.append(_row)
if save_pref(_row):
valid_prefs.append(_row)
if not json:
table.add_row(jsonlib.dumps(_row))
if not json:
table.add_row(jsonlib.dumps(_row))
else:
invalid_value_prefs.append(f)
else:
invalid_prefs.append(f)
@@ -762,6 +765,11 @@ class ManagePreferences:
(', ').join(
invalid_prefs)))
if len(invalid_value_prefs) >= 1:
print("Invalid value provided for preference(s) "
"[red]{0}[/red].".format(
(', ').join(invalid_value_prefs)))
if not json and console:
print(table)
elif json and console:

View File

@@ -18,7 +18,7 @@
# Application version number components
APP_RELEASE = 9
APP_REVISION = 15
APP_REVISION = 16
# Application version suffix, e.g. 'beta1', 'dev'. Usually an empty string
# for GA releases.
@@ -27,7 +27,7 @@ APP_SUFFIX = ''
# Numeric application version for upgrade checks. Should be in the format:
# [X]XYYZZ, where X is the release version, Y is the revision, with a leading
# zero if needed, and Z represents the suffix, with a leading zero if needed
APP_VERSION_INT = 91500
APP_VERSION_INT = 91600
# DO NOT CHANGE!
# The application version string, constructed from the components