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.
This commit is contained in:
Dave Page
2026-07-25 00:55:45 +05:30
committed by Ashesh Vashi
parent 461c3afba9
commit d36bd8dc96
6 changed files with 32 additions and 8 deletions
+3
View File
@@ -1786,6 +1786,7 @@ def select_frame(trans_id, frame_id):
'/get_arguments/<int:sid>/<int:did>/<int:scid>/<int:func_id>',
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/<int:sid>/<int:did>/<int:scid>/<int:func_id>',
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/<int:sid>/<int:did>/<int:scid>/<int:func_id>',
methods=['POST'], endpoint='clear_arguments'
)
@permissions_required(AllPermissionTypes.tools_debugger)
@pga_login_required
def clear_arguments_sqlite(sid, did, scid, func_id):
"""
+5 -2
View File
@@ -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/<int:trans_id>/<int:sgid>/<int:sid>/<int:did>',
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/<int:trans_id>/<int:sgid>/<int:sid>/<int:did>',
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/<int:trans_id>/<int:sgid>/<int:sid>/<int:did>',
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'])
@@ -248,6 +248,7 @@ def get_node_sql_with_type(node_id, node_type, server_prop,
'/<int:sid>/<int:did>/<int:node_id>/<node_type>/',
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/<int:sid>/<int:did>/',
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(
'/<int:sid>/<int:did>/', methods=['POST'], endpoint='apply'
)
@permissions_required(AllPermissionTypes.tools_grant_wizard)
@pga_login_required
@check_precondition
def save(sid, did):
+9 -3
View File
@@ -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/<int:trans_id>',
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.
+11 -3
View File
@@ -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,
+1
View File
@@ -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):
"""