From d36bd8dc96812c716664feac533d240544e70adc Mon Sep 17 00:00:00 2001 From: Dave Page Date: Tue, 16 Jun 2026 16:49:41 +0100 Subject: [PATCH] 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. --- web/pgadmin/tools/debugger/__init__.py | 3 +++ web/pgadmin/tools/erd/__init__.py | 7 +++++-- web/pgadmin/tools/grant_wizard/__init__.py | 3 +++ web/pgadmin/tools/psql/__init__.py | 12 +++++++++--- web/pgadmin/tools/schema_diff/__init__.py | 14 +++++++++++--- web/pgadmin/tools/sqleditor/__init__.py | 1 + 6 files changed, 32 insertions(+), 8 deletions(-) diff --git a/web/pgadmin/tools/debugger/__init__.py b/web/pgadmin/tools/debugger/__init__.py index f5485fac7..3df95383c 100644 --- a/web/pgadmin/tools/debugger/__init__.py +++ b/web/pgadmin/tools/debugger/__init__.py @@ -1786,6 +1786,7 @@ def select_frame(trans_id, frame_id): '/get_arguments////', methods=['GET'], endpoint='get_arguments' ) +@permissions_required(AllPermissionTypes.tools_debugger) @pga_login_required def get_arguments_sqlite(sid, did, scid, func_id): """ @@ -1879,6 +1880,7 @@ def get_array_string(data, i): '/set_arguments////', methods=['POST'], endpoint='set_arguments' ) +@permissions_required(AllPermissionTypes.tools_debugger) @pga_login_required def set_arguments_sqlite(sid, did, scid, func_id): """ @@ -1977,6 +1979,7 @@ def set_arguments_sqlite(sid, did, scid, func_id): '/clear_arguments////', methods=['POST'], endpoint='clear_arguments' ) +@permissions_required(AllPermissionTypes.tools_debugger) @pga_login_required def clear_arguments_sqlite(sid, did, scid, func_id): """ diff --git a/web/pgadmin/tools/erd/__init__.py b/web/pgadmin/tools/erd/__init__.py index b70eb9f63..52c7333c1 100644 --- a/web/pgadmin/tools/erd/__init__.py +++ b/web/pgadmin/tools/erd/__init__.py @@ -32,7 +32,7 @@ from pgadmin.utils.constants import PREF_LABEL_KEYBOARD_SHORTCUTS, \ PREF_LABEL_OPTIONS from .utils import ERDHelper from pgadmin.utils.exception import ConnectionLost -from pgadmin.authenticate import socket_login_required +from pgadmin.authenticate import socket_permissions_required from pgadmin.tools.user_management.PgAdminPermissions import AllPermissionTypes from ... import socketio @@ -585,6 +585,7 @@ def panel(trans_id): '/initialize////', methods=["POST"], endpoint='initialize' ) +@permissions_required(AllPermissionTypes.tools_erd_tool) @pga_login_required def initialize_erd(trans_id, sgid, sid, did): """ @@ -659,6 +660,7 @@ def _get_connection(sid, did, trans_id, db_name=None): @blueprint.route('/prequisite////', methods=["GET"], endpoint='prequisite') +@permissions_required(AllPermissionTypes.tools_erd_tool) @pga_login_required def prequisite(trans_id, sgid, sid, did): conn = _get_connection(sid, did, trans_id) @@ -722,6 +724,7 @@ def translate_foreign_keys(tab_fks, tab_data, all_nodes): @blueprint.route('/sql////', methods=["POST"], endpoint='sql') +@permissions_required(AllPermissionTypes.tools_erd_tool) @pga_login_required def sql(trans_id, sgid, sid, did): data = json.loads(request.data) @@ -776,7 +779,7 @@ def connect(): @socketio.on('tables', namespace=SOCKETIO_NAMESPACE) -@socket_login_required +@socket_permissions_required(AllPermissionTypes.tools_erd_tool) def tables(params): try: helper = ERDHelper(params['trans_id'], params['sid'], params['did']) diff --git a/web/pgadmin/tools/grant_wizard/__init__.py b/web/pgadmin/tools/grant_wizard/__init__.py index f0c5a643c..407b0afcd 100644 --- a/web/pgadmin/tools/grant_wizard/__init__.py +++ b/web/pgadmin/tools/grant_wizard/__init__.py @@ -248,6 +248,7 @@ def get_node_sql_with_type(node_id, node_type, server_prop, '/////', methods=['GET'], endpoint='objects' ) +@permissions_required(AllPermissionTypes.tools_grant_wizard) @pga_login_required @check_precondition def properties(sid, did, node_id, node_type): @@ -404,6 +405,7 @@ def set_priv_for_package(server_prop, data, acls): '/sql///', methods=['POST'], endpoint='modified_sql' ) +@permissions_required(AllPermissionTypes.tools_grant_wizard) @pga_login_required @check_precondition def msql(sid, did): @@ -538,6 +540,7 @@ def parse_priv(data, acls, server_prop): @blueprint.route( '///', methods=['POST'], endpoint='apply' ) +@permissions_required(AllPermissionTypes.tools_grant_wizard) @pga_login_required @check_precondition def save(sid, did): diff --git a/web/pgadmin/tools/psql/__init__.py b/web/pgadmin/tools/psql/__init__.py index ab1e22fd3..e013748ba 100644 --- a/web/pgadmin/tools/psql/__init__.py +++ b/web/pgadmin/tools/psql/__init__.py @@ -20,16 +20,18 @@ from flask import Response, request from flask import render_template, copy_current_request_context, \ current_app as app from flask_babel import gettext -from flask_security import current_user +from flask_security import current_user, permissions_required from pgadmin.user_login_check import pga_login_required from pgadmin.browser.utils import underscore_unescape, underscore_escape from pgadmin.utils import PgAdminModule from pgadmin.utils.driver import get_driver from ... import socketio as sio from pgadmin.utils import get_complete_file_path -from pgadmin.authenticate import socket_login_required +from pgadmin.authenticate import socket_login_required, \ + socket_permissions_required from pgadmin.model import Server from pgadmin.utils.server_access import get_server +from pgadmin.tools.user_management.PgAdminPermissions import AllPermissionTypes if _platform == 'win32': # Check Windows platform support for WinPty api, Disable psql @@ -76,6 +78,7 @@ blueprint = PSQLModule('psql', __name__, static_url_path='/static') @blueprint.route('/panel/', methods=["POST"], endpoint="panel") +@permissions_required(AllPermissionTypes.tools_psql_tool) @pga_login_required def panel(trans_id): """ @@ -292,7 +295,7 @@ def pty_handel_io(connection_data, data, sid): @sio.on('start_process', namespace='/pty') -@socket_login_required +@socket_permissions_required(AllPermissionTypes.tools_psql_tool) def start_process(data): """ Start the pty terminal and execute psql command and emit results to user. @@ -475,6 +478,7 @@ def other_key_press(data): @sio.on('socket_input', namespace='/pty') +@socket_permissions_required(AllPermissionTypes.tools_psql_tool) def socket_input(data): """ This get the user input through socket. @@ -503,6 +507,7 @@ def socket_input(data): @sio.on('socket_set_role', namespace='/pty') +@socket_permissions_required(AllPermissionTypes.tools_psql_tool) def socket_set_role(data): """ This function sets the role used to connect to server. @@ -537,6 +542,7 @@ def socket_set_role(data): @sio.on('resize', namespace='/pty') +@socket_permissions_required(AllPermissionTypes.tools_psql_tool) def resize(data): """ Resize the pty terminal as per the UI terminal. diff --git a/web/pgadmin/tools/schema_diff/__init__.py b/web/pgadmin/tools/schema_diff/__init__.py index c6e5a9185..7136010fe 100644 --- a/web/pgadmin/tools/schema_diff/__init__.py +++ b/web/pgadmin/tools/schema_diff/__init__.py @@ -28,7 +28,7 @@ from pgadmin.utils.driver import get_driver from pgadmin.utils.constants import PREF_LABEL_DISPLAY, \ ERROR_MSG_TRANS_ID_NOT_FOUND from sqlalchemy import or_ -from pgadmin.authenticate import socket_login_required +from pgadmin.authenticate import socket_permissions_required from pgadmin import socketio from pgadmin.tools.user_management.PgAdminPermissions import AllPermissionTypes from pgadmin.utils.server_access import \ @@ -207,6 +207,7 @@ def update_session_diff_transaction(trans_id, session_obj, diff_model_obj): methods=["GET"], endpoint="initialize" ) +@permissions_required(AllPermissionTypes.tools_schema_diff) @pga_login_required def initialize(trans_id): """ @@ -270,6 +271,7 @@ def close(trans_id): methods=["GET"], endpoint="servers" ) +@permissions_required(AllPermissionTypes.tools_schema_diff) @pga_login_required def servers(): """ @@ -323,6 +325,7 @@ def servers(): methods=["GET"], endpoint="get_server" ) +@permissions_required(AllPermissionTypes.tools_schema_diff) @pga_login_required def get_server(sid, did): """ @@ -366,6 +369,7 @@ def get_server(sid, did): methods=["POST"], endpoint="connect_server" ) +@permissions_required(AllPermissionTypes.tools_schema_diff) @pga_login_required def connect_server(sid): # Check if server is already connected then no need to reconnect again. @@ -394,6 +398,7 @@ def connect_server(sid): methods=["POST"], endpoint="connect_database" ) +@permissions_required(AllPermissionTypes.tools_schema_diff) @pga_login_required def connect_database(sid, did): server = get_server_access(sid) @@ -411,6 +416,7 @@ def connect_database(sid, did): methods=["GET"], endpoint="databases" ) +@permissions_required(AllPermissionTypes.tools_schema_diff) @pga_login_required def databases(sid): """ @@ -454,6 +460,7 @@ def databases(sid): methods=["GET"], endpoint="schemas" ) +@permissions_required(AllPermissionTypes.tools_schema_diff) @pga_login_required def schemas(sid, did): """ @@ -478,7 +485,7 @@ def schemas(sid, did): @socketio.on('compare_database', namespace=SOCKETIO_NAMESPACE) -@socket_login_required +@socket_permissions_required(AllPermissionTypes.tools_schema_diff) def compare_database(params): """ This function will compare the two databases. @@ -637,7 +644,7 @@ def compare_database(params): @socketio.on('compare_schema', namespace=SOCKETIO_NAMESPACE) -@socket_login_required +@socket_permissions_required(AllPermissionTypes.tools_schema_diff) def compare_schema(params): """ This function will compare the two schema. @@ -707,6 +714,7 @@ def compare_schema(params): methods=["GET"], endpoint="ddl_compare" ) +@permissions_required(AllPermissionTypes.tools_schema_diff) @pga_login_required def ddl_compare(trans_id, source_sid, source_did, source_scid, target_sid, target_did, target_scid, source_oid, diff --git a/web/pgadmin/tools/sqleditor/__init__.py b/web/pgadmin/tools/sqleditor/__init__.py index 3c61c1edb..8080d220a 100644 --- a/web/pgadmin/tools/sqleditor/__init__.py +++ b/web/pgadmin/tools/sqleditor/__init__.py @@ -193,6 +193,7 @@ def index(): methods=["PUT", "POST"], endpoint="initialize_viewdata" ) +@permissions_required(AllPermissionTypes.tools_query_tool) @pga_login_required def initialize_viewdata(trans_id, cmd_type, obj_type, sgid, sid, did, obj_id): """