mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2026-08-17 16:34:44 -05:00
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.
651 lines
21 KiB
Python
651 lines
21 KiB
Python
##########################################################################
|
|
#
|
|
# pgAdmin 4 - PostgreSQL Tools
|
|
#
|
|
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
|
|
# This software is released under the PostgreSQL Licence
|
|
#
|
|
##########################################################################
|
|
import json
|
|
import os
|
|
import select
|
|
import struct
|
|
|
|
import config
|
|
import re
|
|
import subprocess
|
|
from sys import platform as _platform
|
|
from config import PG_DEFAULT_DRIVER
|
|
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, 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, \
|
|
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
|
|
# if not supporting
|
|
try:
|
|
from winpty import PtyProcess
|
|
except ImportError as error:
|
|
config.ENABLE_PSQL = False
|
|
else:
|
|
import fcntl
|
|
import termios
|
|
import pty
|
|
|
|
session_input = dict()
|
|
pdata = dict()
|
|
cdata = dict()
|
|
open_psql_connections = dict()
|
|
|
|
|
|
class PSQLModule(PgAdminModule):
|
|
"""
|
|
class PSQLModule(PgAdminModule)
|
|
A module class for PSQL derived from PgAdminModule.
|
|
"""
|
|
|
|
LABEL = gettext("PSQL")
|
|
|
|
def get_own_menuitems(self):
|
|
return {}
|
|
|
|
def get_exposed_url_endpoints(self):
|
|
"""
|
|
Returns:
|
|
list: URL endpoints for PSQL module
|
|
"""
|
|
return [
|
|
'psql.panel'
|
|
]
|
|
|
|
|
|
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):
|
|
"""
|
|
Return panel template for PSQL tools.
|
|
:param trans_id:
|
|
"""
|
|
params = {'trans_id': trans_id,
|
|
'is_enable':config.ENABLE_PSQL,
|
|
'platform': _platform
|
|
}
|
|
if request.args:
|
|
params.update({k: v for k, v in request.args.items()})
|
|
if request.form:
|
|
for key, val in request.form.items():
|
|
params[key] = val
|
|
|
|
params['title'] = underscore_escape(params['title'])
|
|
if 'user' in params:
|
|
params['user'] = underscore_escape(params['user'])
|
|
|
|
if 'sid_soid_mapping' not in app.config:
|
|
app.config['sid_soid_mapping'] = dict()
|
|
|
|
s = get_server(int(params['sid']))
|
|
if s:
|
|
data = _get_database_role(params['sid'], params['did'])
|
|
if data:
|
|
params['db'] = underscore_escape(data['db_name']) \
|
|
if 'db_name' in data else 'postgres'
|
|
params['role'] = underscore_escape(data['role'])
|
|
set_env_variables(is_win=_platform == 'win32')
|
|
return render_template("psql/index.html",
|
|
params=json.dumps(params))
|
|
else:
|
|
params['error'] = 'The server was not found.'
|
|
return render_template(
|
|
"psql/index.html",
|
|
params=json.dumps(params))
|
|
|
|
|
|
def set_env_variables(is_win=False):
|
|
# Set TERM env for xterm.
|
|
os.environ['TERM'] = 'xterm'
|
|
if is_win:
|
|
os.environ['PYWINPTY_BACKEND'] = '1'
|
|
# If psql is enabled in server mode, set psqlrc and hist paths
|
|
# to individual user storage.
|
|
if config.ENABLE_PSQL and config.SERVER_MODE:
|
|
psql_data = {
|
|
'PSQLRC': get_complete_file_path('.psqlrc', False),
|
|
'PSQL_HISTORY': get_complete_file_path('.psql_history', False)
|
|
}
|
|
os.environ[current_user.username] = json.dumps(psql_data)
|
|
|
|
|
|
def set_term_size(fd, row, col, xpix=0, ypix=0):
|
|
"""
|
|
Set the terminal size as per UI xterm size.
|
|
:param fd:
|
|
:param row:
|
|
:param col:
|
|
:param xpix:
|
|
:param ypix:
|
|
"""
|
|
if _platform == 'win32':
|
|
app.config['sessions'][request.sid].setwinsize(row, col)
|
|
else:
|
|
term_size = struct.pack('HHHH', row, col, xpix, ypix)
|
|
fcntl.ioctl(fd, termios.TIOCSWINSZ, term_size)
|
|
|
|
|
|
@sio.on('connect', namespace='/pty')
|
|
@socket_login_required
|
|
def connect():
|
|
"""
|
|
Connect to the server through socket.
|
|
:return:
|
|
:rtype:
|
|
"""
|
|
if config.ENABLE_PSQL:
|
|
sio.emit('connected', {'sid': request.sid}, namespace='/pty',
|
|
to=request.sid)
|
|
else:
|
|
sio.emit('conn_not_allow', {'sid': request.sid}, namespace='/pty',
|
|
to=request.sid)
|
|
|
|
|
|
def get_user_env():
|
|
env = os.environ
|
|
if config.ENABLE_PSQL and config.SERVER_MODE:
|
|
user_env = json.loads(os.environ[current_user.username])
|
|
env['PSQLRC'] = user_env['PSQLRC']
|
|
env['PSQL_HISTORY'] = user_env['PSQL_HISTORY']
|
|
return env
|
|
|
|
|
|
def create_pty_terminal(connection_data, server_id):
|
|
# Create the pty terminal process, parent and fd are file descriptors
|
|
# for parent and child.
|
|
parent, fd = pty.openpty()
|
|
p = None
|
|
if parent is not None:
|
|
# Child process
|
|
p = subprocess.Popen(connection_data,
|
|
preexec_fn=os.setsid,
|
|
stdin=fd,
|
|
stdout=fd,
|
|
stderr=fd,
|
|
universal_newlines=True,
|
|
env=get_user_env()
|
|
)
|
|
|
|
app.config['sessions'][request.sid] = parent
|
|
pdata[request.sid] = p
|
|
cdata[request.sid] = fd
|
|
open_psql_connections[request.sid] = server_id
|
|
else:
|
|
app.config['sessions'][request.sid] = parent
|
|
cdata[request.sid] = fd
|
|
set_term_size(fd, 50, 50)
|
|
|
|
return p, parent, fd
|
|
|
|
|
|
def read_terminal_data(parent, data_ready, max_read_bytes, sid):
|
|
"""
|
|
Read the terminal output.
|
|
:param parent:
|
|
:param data_ready:
|
|
:param max_read_bytes:
|
|
:param sid:
|
|
:return:
|
|
"""
|
|
if parent in data_ready:
|
|
# Read the output from parent fd (terminal).
|
|
output = os.read(parent, max_read_bytes)
|
|
try:
|
|
decode_data = output.decode()
|
|
except Exception:
|
|
try:
|
|
decode_data = output.decode('UTF-8')
|
|
except Exception:
|
|
decode_data = output.decode('UTF-8', errors='replace')
|
|
|
|
sio.emit('pty-output',
|
|
{'result': decode_data,
|
|
'error': False},
|
|
namespace='/pty', room=sid)
|
|
|
|
|
|
def read_stdout(process, sid, max_read_bytes, win_emit_output=True):
|
|
(data_ready, _, _) = select.select([process.fd], [], [], 0)
|
|
if process.fd in data_ready:
|
|
output = process.read(max_read_bytes)
|
|
if win_emit_output:
|
|
sio.emit('pty-output',
|
|
{'result': output,
|
|
'error': False},
|
|
namespace='/pty', room=sid)
|
|
|
|
sio.sleep(0.01)
|
|
|
|
|
|
def windows_platform(connection_data, sid, max_read_bytes, server_id):
|
|
process = PtyProcess.spawn('cmd.exe', env=get_user_env())
|
|
|
|
process.write(r'"{0}" "{1}" 2>>&1'.format(connection_data[0],
|
|
connection_data[1]))
|
|
process.write("\r\n")
|
|
app.config['sessions'][request.sid] = process
|
|
pdata[request.sid] = process
|
|
cdata[request.sid] = process.fd
|
|
open_psql_connections[request.sid] = server_id
|
|
set_term_size(process, 50, 50)
|
|
|
|
while process.isalive():
|
|
read_stdout(process, sid, max_read_bytes,
|
|
win_emit_output=True)
|
|
|
|
|
|
def non_windows_platform(parent, p, fd, data, max_read_bytes, sid):
|
|
while p and p.poll() is None:
|
|
if request.sid in app.config['sessions']:
|
|
# This code is added to make this unit testable.
|
|
if "is_test" not in data:
|
|
sio.sleep(0.01)
|
|
else:
|
|
data['count'] += 1
|
|
if data['count'] == 5:
|
|
break
|
|
|
|
timeout = 0
|
|
# module provides access to platform-specific I/O
|
|
# monitoring functions
|
|
try:
|
|
(data_ready, _, _) = select.select([parent, fd], [], [],
|
|
timeout)
|
|
|
|
read_terminal_data(parent, data_ready, max_read_bytes, sid)
|
|
except OSError:
|
|
# If the process is killed, bad file descriptor exception may
|
|
# occur. Handle it gracefully
|
|
pass
|
|
|
|
|
|
def pty_handel_io(connection_data, data, sid):
|
|
max_read_bytes = 1024 * 20
|
|
if _platform == 'win32':
|
|
windows_platform(connection_data, sid, max_read_bytes,
|
|
int(data['sid']))
|
|
else:
|
|
p, parent, fd = create_pty_terminal(connection_data, int(data['sid']))
|
|
non_windows_platform(parent, p, fd, data, max_read_bytes, sid)
|
|
|
|
|
|
@sio.on('start_process', namespace='/pty')
|
|
@socket_permissions_required(AllPermissionTypes.tools_psql_tool)
|
|
def start_process(data):
|
|
"""
|
|
Start the pty terminal and execute psql command and emit results to user.
|
|
:param data:
|
|
:return:
|
|
"""
|
|
@copy_current_request_context
|
|
def read_and_forward_pty_output(sid, data):
|
|
pty_handel_io(connection_data, data, sid)
|
|
|
|
# Check user is authenticated and PSQL is enabled in config.
|
|
if current_user.is_authenticated and config.ENABLE_PSQL:
|
|
connection_data = []
|
|
connection_successful = False
|
|
try:
|
|
db = ''
|
|
if data['db']:
|
|
db = underscore_unescape(data['db'])
|
|
|
|
data['db'] = db
|
|
|
|
_, manager = _get_connection(int(data['sid']), data)
|
|
psql_utility = manager.utility('sql')
|
|
if psql_utility is None or not os.path.exists(psql_utility):
|
|
sio.emit('pty-output',
|
|
{
|
|
'result': gettext(
|
|
'PSQL utility not found. Specify the valid '
|
|
'binary path in the preferences for the '
|
|
'appropriate server version, or select '
|
|
'"Set as default" to use an existing binary '
|
|
'path.'),
|
|
'error': True},
|
|
namespace='/pty', room=request.sid)
|
|
return
|
|
connection_data = get_connection_str(psql_utility, db,
|
|
manager)
|
|
connection_successful = True
|
|
except Exception as e:
|
|
# If any error raised during the start the PSQL emit error to UI.
|
|
# request.sid: This sid is socket id.
|
|
error_msg = 'Error while running psql command: {0}'.format(e)
|
|
if str(e) == 'Server is not connected.':
|
|
error_msg = 'Error while opening psql tool: {0}'.format(e)
|
|
|
|
sio.emit('conn_error',
|
|
{'error': error_msg},
|
|
namespace='/pty',
|
|
room=request.sid)
|
|
|
|
if connection_successful:
|
|
try:
|
|
if str(data['sid']) not in app.config['sid_soid_mapping']:
|
|
# request.sid: refer request.sid as socket id.
|
|
app.config['sid_soid_mapping'][str(data['sid'])] = list()
|
|
app.config['sid_soid_mapping'][str(data['sid'])].append(
|
|
request.sid)
|
|
else:
|
|
app.config['sid_soid_mapping'][str(data['sid'])].append(
|
|
request.sid)
|
|
|
|
sio.start_background_task(read_and_forward_pty_output,
|
|
request.sid, data)
|
|
except Exception as e:
|
|
sio.emit(
|
|
'conn_error',
|
|
{'error':'Error while running psql command: {0}'.
|
|
format(e)},
|
|
namespace='/pty',
|
|
room=request.sid)
|
|
else:
|
|
# Show error if user is not authenticated.
|
|
sio.emit('conn_not_allow', {'sid': request.sid}, namespace='/pty',
|
|
to=request.sid)
|
|
|
|
|
|
def _get_connection(sid, data):
|
|
"""
|
|
Get the connection object of ERD.
|
|
:param sid:
|
|
:param did:
|
|
:param trans_id:
|
|
:return:
|
|
"""
|
|
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
|
|
if not manager:
|
|
msg = 'Server is not connected.'
|
|
app.logger.error(msg)
|
|
raise RuntimeError(msg)
|
|
try:
|
|
conn = manager.connection()
|
|
# This is added for unit test only, no use in normal execution.
|
|
if 'pwd' in data:
|
|
kwargs = {'password': data['pwd'], "user": data['user']}
|
|
status, msg = conn.connect(**kwargs)
|
|
else:
|
|
status, msg = conn.connect()
|
|
if not status:
|
|
app.logger.error(msg)
|
|
raise RuntimeError('Server is not connected.')
|
|
|
|
return conn, manager
|
|
except Exception as e:
|
|
app.logger.error(e)
|
|
raise
|
|
|
|
|
|
def get_connection_str(psql_utility, db, manager):
|
|
"""
|
|
Get connection string(through connection dsn)
|
|
:param psql_utility: PostgreSQL binary path.
|
|
:param db: database name to connect specific db.
|
|
:return: connection attribute list for PSQL connection.
|
|
"""
|
|
manager.export_password_env('PGPASSWORD')
|
|
database = db if db != '' else 'postgres'
|
|
user = underscore_unescape(manager.user) if manager.user else None
|
|
conn_attr = manager.create_connection_string(database, user)
|
|
|
|
conn_attr_list = list()
|
|
conn_attr_list.append(psql_utility)
|
|
conn_attr_list.append(conn_attr)
|
|
return conn_attr_list
|
|
|
|
|
|
def enter_key_press(data):
|
|
"""
|
|
Handel the Enter key press event.
|
|
:param data:
|
|
"""
|
|
user_input = data['input']
|
|
|
|
if user_input == r'\q' or user_input == 'q\\q' or user_input in\
|
|
[r'\quit', 'exit', 'exit;']:
|
|
# If user enter \q to terminate the PSQL, emit the msg to
|
|
# notify user connection is terminated.
|
|
sio.emit('pty-output',
|
|
{
|
|
'result': gettext(
|
|
'Connection terminated. To create a new '
|
|
'connection, please open another psql'
|
|
' tool.'),
|
|
'error': True},
|
|
namespace='/pty', room=request.sid)
|
|
|
|
if _platform == 'win32':
|
|
app.config['sessions'][request.sid].write('\n')
|
|
del app.config['sessions'][request.sid]
|
|
else:
|
|
os.write(app.config['sessions'][request.sid],
|
|
'\n'.encode())
|
|
|
|
else:
|
|
if _platform == 'win32':
|
|
app.config['sessions'][request.sid].write(
|
|
"{0}".format(data['input']))
|
|
else:
|
|
os.write(app.config['sessions'][request.sid],
|
|
data['input'].encode())
|
|
session_input[request.sid] = ''
|
|
|
|
|
|
def other_key_press(data):
|
|
"""
|
|
Handel the other key press from psql tool.
|
|
:param data:
|
|
:type data:
|
|
:return:
|
|
:rtype:
|
|
"""
|
|
session_input[request.sid] = data['input']
|
|
|
|
if _platform == 'win32':
|
|
app.config['sessions'][request.sid].write(
|
|
"{0}".format(data['input']))
|
|
else:
|
|
# Write user input to terminal parent fd.
|
|
os.write(app.config['sessions'][request.sid],
|
|
data['input'].encode())
|
|
|
|
|
|
@sio.on('socket_input', namespace='/pty')
|
|
@socket_permissions_required(AllPermissionTypes.tools_psql_tool)
|
|
def socket_input(data):
|
|
"""
|
|
This get the user input through socket.
|
|
:param data: User input from socket.
|
|
"""
|
|
try:
|
|
# request.sid: refer request.sid as socket id.
|
|
# Check PSQL enabled setting from config.
|
|
enable_psql = True if config.ENABLE_PSQL else False
|
|
|
|
if request.sid in app.config['sessions']:
|
|
if data['key_name'] == 'Enter' and enable_psql:
|
|
enter_key_press(data)
|
|
else:
|
|
other_key_press(data)
|
|
except Exception:
|
|
# Delete socket id from sessions.
|
|
# request.sid: refer request.sid as socket id.
|
|
sio.emit('pty-output',
|
|
{
|
|
'result': gettext('Invalid session.\r\n'),
|
|
'error': True
|
|
},
|
|
namespace='/pty', room=request.sid)
|
|
del app.config['sessions'][request.sid]
|
|
|
|
|
|
@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.
|
|
:param data: User input from socket.
|
|
"""
|
|
try:
|
|
if request.sid in app.config['sessions']:
|
|
# checking if role contains special characters and quoting it.
|
|
if re.search('[^a-z0-9_]', data['role']):
|
|
data['role'] = data['role'].replace('"', '""')
|
|
data['role'] = '"{0}"'.format(data['role'])
|
|
|
|
input_data = "SET ROLE {0};".format(data['role'])
|
|
if _platform == 'win32':
|
|
app.config['sessions'][request.sid].write(
|
|
"{0}".format(input_data))
|
|
app.config['sessions'][request.sid].write("\r\n")
|
|
else:
|
|
os.write(app.config['sessions'][request.sid],
|
|
input_data.encode())
|
|
os.write(app.config['sessions'][request.sid], '\n'.encode())
|
|
except Exception:
|
|
# Delete socket id from sessions.
|
|
# request.sid: refer request.sid as socket id.
|
|
sio.emit('pty-output',
|
|
{
|
|
'result': gettext('Invalid session.\r\n'),
|
|
'error': True
|
|
},
|
|
namespace='/pty', room=request.sid)
|
|
del app.config['sessions'][request.sid]
|
|
|
|
|
|
@sio.on('resize', namespace='/pty')
|
|
@socket_permissions_required(AllPermissionTypes.tools_psql_tool)
|
|
def resize(data):
|
|
"""
|
|
Resize the pty terminal as per the UI terminal.
|
|
:param data: UI terminal rows and cols data
|
|
"""
|
|
# request.sid: refer request.sid as socket id.
|
|
if request.sid in app.config['sessions']:
|
|
set_term_size(app.config['sessions'][request.sid], data['rows'],
|
|
data['cols'])
|
|
|
|
|
|
@sio.on('disconnect', namespace='/pty')
|
|
def disconnect():
|
|
"""
|
|
Disconnect the socket and terminate the process
|
|
"""
|
|
# request.sid: refer request.sid as socket id.
|
|
if request.sid in pdata:
|
|
# On disconnect socket manually exit the psql terminal and close the
|
|
# parend and child fd then kill the subprocess.
|
|
disconnect_socket()
|
|
|
|
|
|
@sio.on('server-disconnect', namespace='/pty')
|
|
def server_disconnect(data):
|
|
"""
|
|
Disconnect the socket and terminate the process after user disconnect
|
|
the server. we can't use disconnect event name as it is reserved for socket
|
|
internal use.
|
|
"""
|
|
# request.sid: refer request.sid as socket id.
|
|
if request.sid in pdata and request.sid in app.config['sid_soid_mapping'][
|
|
data['sid']]:
|
|
# On disconnect socket manually exit the psql terminal and close the
|
|
# parend and child fd then kill the subprocess.
|
|
app.config['sid_soid_mapping'][data['sid']] = [soid for soid in
|
|
app.config[
|
|
'sid_soid_mapping'][
|
|
data['sid']] if
|
|
soid != request.sid]
|
|
disconnect_socket()
|
|
|
|
|
|
def cleanup_globals():
|
|
del pdata[request.sid]
|
|
del cdata[request.sid]
|
|
server_id = open_psql_connections[request.sid]
|
|
del open_psql_connections[request.sid]
|
|
# Check if all the connections of the adhoc server is closed
|
|
# then delete the server from the pgadmin database.
|
|
from pgadmin.misc.workspaces import check_and_delete_adhoc_server
|
|
check_and_delete_adhoc_server(server_id)
|
|
|
|
|
|
def disconnect_socket():
|
|
if _platform == 'win32':
|
|
if request.sid in app.config['sessions']:
|
|
process = app.config['sessions'][request.sid]
|
|
process.terminate()
|
|
del app.config['sessions'][request.sid]
|
|
cleanup_globals()
|
|
else:
|
|
os.write(app.config['sessions'][request.sid], r'\q\n'.encode())
|
|
sio.sleep(1)
|
|
os.close(app.config['sessions'][request.sid])
|
|
os.close(cdata[request.sid])
|
|
del app.config['sessions'][request.sid]
|
|
cleanup_globals()
|
|
|
|
|
|
def get_connection_status(conn):
|
|
if conn.connected():
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def _get_database_role(sid, did):
|
|
"""
|
|
This method is used to get database based on sid, did.
|
|
"""
|
|
try:
|
|
from pgadmin.utils.driver import get_driver
|
|
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(int(sid))
|
|
conn = manager.connection(did=int(did))
|
|
|
|
is_connected = get_connection_status(conn)
|
|
|
|
if not is_connected:
|
|
conn.connect()
|
|
|
|
db_name = conn.db
|
|
role = manager.role if manager.role else None
|
|
return {'db_name': db_name, 'role': role}
|
|
except Exception as e:
|
|
print(str(e))
|
|
return None
|
|
|
|
|
|
def get_open_psql_connections():
|
|
"""
|
|
This function returns open connections
|
|
"""
|
|
|
|
return open_psql_connections
|