diff -rupN --no-dereference pgadmin4-8.6/web/pgadmin/__init__.py pgadmin4-8.6-new/web/pgadmin/__init__.py
--- pgadmin4-8.6/web/pgadmin/__init__.py 2024-04-29 10:42:02.000000000 +0200
+++ pgadmin4-8.6-new/web/pgadmin/__init__.py 2024-05-04 22:56:10.771329533 +0200
@@ -503,7 +503,7 @@ def create_app(app_name=None):
'SESSION_COOKIE_DOMAIN': config.SESSION_COOKIE_DOMAIN,
# CSRF Token expiration till session expires
'WTF_CSRF_TIME_LIMIT': getattr(config, 'CSRF_TIME_LIMIT', None),
- 'WTF_CSRF_METHODS': ['GET', 'POST', 'PUT', 'DELETE'],
+ 'WTF_CSRF_METHODS': ['POST', 'PUT', 'DELETE'],
# Disable deliverable check for email addresss
'SECURITY_EMAIL_VALIDATOR_ARGS': config.SECURITY_EMAIL_VALIDATOR_ARGS,
# Disable CSRF for unauthenticated endpoints
diff -rupN --no-dereference pgadmin4-8.6/web/pgadmin/__init__.py.orig pgadmin4-8.6-new/web/pgadmin/__init__.py.orig
--- pgadmin4-8.6/web/pgadmin/__init__.py.orig 1970-01-01 01:00:00.000000000 +0100
+++ pgadmin4-8.6-new/web/pgadmin/__init__.py.orig 2024-04-29 10:42:02.000000000 +0200
@@ -0,0 +1,937 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2024, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+"""The main pgAdmin module. This handles the application initialisation tasks,
+such as setup of logging, dynamic loading of modules etc."""
+import logging
+import os
+import sys
+import re
+import ipaddress
+import traceback
+import shutil
+
+from types import MethodType
+from collections import defaultdict
+from importlib import import_module
+
+from flask import Flask, abort, request, current_app, session, url_for
+from flask_socketio import SocketIO
+from werkzeug.exceptions import HTTPException
+from flask_babel import Babel, gettext
+from flask_babel import gettext as _
+from flask_login import user_logged_in, user_logged_out
+from flask_mail import Mail
+from flask_paranoid import Paranoid
+from flask_security import Security, SQLAlchemyUserDatastore, current_user
+from flask_security.utils import login_user, logout_user
+from flask_migrate import Migrate
+from werkzeug.datastructures import ImmutableDict
+from werkzeug.local import LocalProxy
+from werkzeug.utils import find_modules
+from jinja2 import select_autoescape
+
+from pgadmin.model import db, Role, Server, SharedServer, ServerGroup, \
+ User, Keys, Version, SCHEMA_VERSION as CURRENT_SCHEMA_VERSION
+from pgadmin.utils import PgAdminModule, driver, KeyManager, heartbeat
+from pgadmin.utils.preferences import Preferences
+from pgadmin.utils.session import create_session_interface, pga_unauthorised
+from pgadmin.utils.versioned_template_loader import VersionedTemplateLoader
+from datetime import timedelta, datetime
+from pgadmin.setup import get_version, set_version, check_db_tables
+from pgadmin.utils.ajax import internal_server_error, make_json_response
+from pgadmin.utils.csrf import pgCSRFProtect
+from pgadmin import authenticate
+from pgadmin.utils.security_headers import SecurityHeaders
+from pgadmin.utils.constants import KERBEROS, OAUTH2, INTERNAL, LDAP, WEBSERVER
+from jsonformatter import JsonFormatter
+
+# Explicitly set the mime-types so that a corrupted windows registry will not
+# affect pgAdmin 4 to be load properly. This will avoid the issues that may
+# occur due to security fix of X_CONTENT_TYPE_OPTIONS = "nosniff".
+import mimetypes
+
+mimetypes.add_type('application/javascript', '.js')
+mimetypes.add_type('text/css', '.css')
+
+
+winreg = None
+if os.name == 'nt':
+ import winreg
+
+socketio = SocketIO(manage_session=False, async_mode='threading',
+ logger=False, engineio_logger=False, debug=False,
+ ping_interval=25, ping_timeout=120)
+
+_INDEX_PATH = 'browser.index'
+
+
+class PgAdmin(Flask):
+ def __init__(self, *args, **kwargs):
+ # Set the template loader to a postgres-version-aware loader
+ self.jinja_options = ImmutableDict(
+ autoescape=select_autoescape(enabled_extensions=('html', 'xml')),
+ loader=VersionedTemplateLoader(self)
+ )
+ self.logout_hooks = []
+ self.before_app_start = []
+
+ super().__init__(*args, **kwargs)
+
+ def find_submodules(self, basemodule):
+ try:
+ for module_name in find_modules(basemodule, True):
+ if module_name in self.config['MODULE_BLACKLIST']:
+ self.logger.info(
+ 'Skipping blacklisted module: %s' % module_name
+ )
+ continue
+ self.logger.info(
+ 'Examining potential module: %s' % module_name)
+ module = import_module(module_name)
+ for key in list(module.__dict__.keys()):
+ if isinstance(module.__dict__[key], PgAdminModule):
+ yield module.__dict__[key]
+ except Exception:
+ return []
+
+ @property
+ def submodules(self):
+ for blueprint in self.blueprints.values():
+ if isinstance(blueprint, PgAdminModule):
+ yield blueprint
+
+ @property
+ def messages(self):
+ messages = dict()
+ for module in self.submodules:
+ messages.update(getattr(module, "messages", dict()))
+ return messages
+
+ @property
+ def exposed_endpoint_url_map(self):
+ #############################################################
+ # To handle WSGI paths
+ # If user has setup application under WSGI alias
+ # like 'localhost/pgadmin4' then we have to append '/pgadmin4'
+ # into endpoints
+ #############################################################
+ wsgi_root_path = ''
+ if url_for(_INDEX_PATH) != '/browser/':
+ wsgi_root_path = url_for(_INDEX_PATH).replace(
+ '/browser/', ''
+ )
+
+ def get_full_url_path(url):
+ """
+ Generate endpoint URL at per WSGI alias
+ """
+ return wsgi_root_path + url
+
+ # Fetch all endpoints and their respective url
+ for rule in current_app.url_map.iter_rules('static'):
+ yield rule.endpoint, get_full_url_path(rule.rule)
+
+ for module in self.submodules:
+ for endpoint in module.exposed_endpoints:
+ for rule in current_app.url_map.iter_rules(endpoint):
+ yield rule.endpoint, get_full_url_path(rule.rule)
+
+ yield 'pgadmin.root', wsgi_root_path
+
+ @property
+ def menu_items(self):
+ from operator import attrgetter
+
+ menu_items = defaultdict(list)
+ for module in self.submodules:
+ for key, value in module.menu_items.items():
+ menu_items[key].extend(value)
+ menu_items = dict((key, sorted(value, key=attrgetter('priority')))
+ for key, value in menu_items.items())
+ return menu_items
+
+ def register_logout_hook(self, module):
+ if hasattr(module, 'on_logout') and \
+ isinstance(getattr(module, 'on_logout'), MethodType):
+ self.logout_hooks.append(module)
+
+ def register_before_app_start(self, callback):
+ self.before_app_start.append(callback)
+
+ def run_before_app_start(self):
+ # call before app starts or is exported
+ with self.app_context(), self.test_request_context():
+ for callback in self.before_app_start:
+ callback()
+
+
+def _find_blueprint():
+ if request.blueprint:
+ return current_app.blueprints[request.blueprint]
+
+
+current_blueprint = LocalProxy(_find_blueprint)
+
+
+def create_app(app_name=None):
+ # Configuration settings
+ import config
+ if not app_name:
+ app_name = config.APP_NAME
+
+ # Check if app is created for CLI operations or Web
+ cli_mode = False
+ if app_name.endswith('-cli'):
+ cli_mode = True
+
+ # Only enable password related functionality in server mode.
+ if config.SERVER_MODE is True:
+ # Some times we need to access these config params where application
+ # context is not available (we can't use current_app.config in those
+ # cases even with current_app.app_context())
+ # So update these params in config itself.
+ # And also these updated config values will picked up by application
+ # since we are updating config before the application instance is
+ # created.
+
+ config.SECURITY_RECOVERABLE = True
+ config.SECURITY_CHANGEABLE = True
+ # Now we'll open change password page in dialog
+ # 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'
+
+ """Create the Flask application, startup logging and dynamically load
+ additional modules (blueprints) that are found in this directory."""
+ app = PgAdmin(__name__, static_url_path='/static')
+ # Removes unwanted whitespace from render_template function
+ app.jinja_env.trim_blocks = True
+ app.config.from_object(config)
+ app.config.update(dict(PROPAGATE_EXCEPTIONS=True))
+
+ config.SETTINGS_SCHEMA_VERSION = CURRENT_SCHEMA_VERSION
+ ##########################################################################
+ # Setup logging and log the application startup
+ ##########################################################################
+
+ # We won't care about errors in the logging system, we are more
+ # interested in application errors.
+ logging.raiseExceptions = False
+
+ # Add SQL level logging, and set the base logging level
+ logging.addLevelName(25, 'SQL')
+ app.logger.setLevel(logging.DEBUG)
+ app.logger.handlers = []
+
+ # We also need to update the handler on the webserver in order to see
+ # request. Setting the level prevents werkzeug from setting up it's own
+ # stream handler thus ensuring all the logging goes through the pgAdmin
+ # logger.
+ logger = logging.getLogger('werkzeug')
+ logger.setLevel(config.CONSOLE_LOG_LEVEL)
+
+ # Set SQLITE_PATH to TEST_SQLITE_PATH while running test cases
+ if (
+ 'PGADMIN_TESTING_MODE' in os.environ and
+ os.environ['PGADMIN_TESTING_MODE'] == '1'
+ ):
+ config.SQLITE_PATH = config.TEST_SQLITE_PATH
+ config.MASTER_PASSWORD_REQUIRED = False
+ config.UPGRADE_CHECK_ENABLED = False
+
+ if not cli_mode:
+ # Ensure the various working directories exist
+ from pgadmin.setup import create_app_data_directory
+ create_app_data_directory(config)
+
+ # File logging
+ from pgadmin.utils.enhanced_log_rotation import \
+ EnhancedRotatingFileHandler
+ fh = EnhancedRotatingFileHandler(config.LOG_FILE,
+ config.LOG_ROTATION_SIZE,
+ config.LOG_ROTATION_AGE,
+ config.LOG_ROTATION_MAX_LOG_FILES)
+
+ fh.setLevel(config.FILE_LOG_LEVEL)
+
+ if config.JSON_LOGGER:
+ json_formatter = JsonFormatter(config.FILE_LOG_FORMAT_JSON)
+ fh.setFormatter(json_formatter)
+ else:
+ fh.setFormatter(logging.Formatter(config.FILE_LOG_FORMAT))
+
+ app.logger.addHandler(fh)
+ logger.addHandler(fh)
+
+ # Console logging
+ ch = logging.StreamHandler()
+ ch.setLevel(config.CONSOLE_LOG_LEVEL)
+
+ if config.JSON_LOGGER:
+ json_formatter = JsonFormatter(config.CONSOLE_LOG_FORMAT_JSON)
+ ch.setFormatter(json_formatter)
+ else:
+ ch.setFormatter(logging.Formatter(config.CONSOLE_LOG_FORMAT))
+
+ app.logger.addHandler(ch)
+ logger.addHandler(ch)
+
+ # Log the startup
+ app.logger.info('########################################################')
+ app.logger.info('Starting %s v%s...', config.APP_NAME, config.APP_VERSION)
+ app.logger.info('########################################################')
+ app.logger.debug("Python syspath: %s", sys.path)
+
+ ##########################################################################
+ # Setup i18n
+ ##########################################################################
+
+ # Initialise i18n
+ babel = Babel(app)
+
+ def get_locale():
+ """Get the language for the user."""
+ language = 'en'
+ if config.SERVER_MODE is False:
+ # Get the user language preference from the miscellaneous module
+ user_id = None
+ if current_user and current_user.is_authenticated:
+ user_id = current_user.id
+ else:
+ user = user_datastore.find_user(email=config.DESKTOP_USER)
+ if user is not None:
+ user_id = user.id
+ user_language = Preferences.raw_value(
+ 'misc', 'user_language', 'user_language', user_id
+ )
+ if user_language is not None:
+ language = user_language
+ else:
+ # If language is available in get request then return the same
+ # otherwise check the session or cookie
+ data = request.form
+ if 'language' in data:
+ language = data['language'] or language
+ setattr(session, 'PGADMIN_LANGUAGE', language)
+ elif hasattr(session, 'PGADMIN_LANGUAGE'):
+ language = getattr(session, 'PGADMIN_LANGUAGE', language)
+ elif hasattr(request.cookies, 'PGADMIN_LANGUAGE'):
+ language = getattr(
+ request.cookies, 'PGADMIN_LANGUAGE', language
+ )
+
+ return language
+
+ babel.init_app(app, locale_selector=get_locale)
+ ##########################################################################
+ # Setup authentication
+ ##########################################################################
+ if config.CONFIG_DATABASE_URI is not None and \
+ len(config.CONFIG_DATABASE_URI) > 0:
+ app.config['SQLALCHEMY_DATABASE_URI'] = config.CONFIG_DATABASE_URI
+ else:
+ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///{0}?timeout={1}' \
+ .format(config.SQLITE_PATH.replace('\\', '/'),
+ getattr(config, 'SQLITE_TIMEOUT', 500)
+ )
+
+ # Override USER_DOES_NOT_EXIST and INVALID_PASSWORD messages from flask.
+ app.config['SECURITY_MSG_USER_DOES_NOT_EXIST'] = \
+ app.config['SECURITY_MSG_INVALID_PASSWORD'] = \
+ (gettext("Incorrect username or password."), "error")
+ app.config['SECURITY_PASSWORD_LENGTH_MIN'] = config.PASSWORD_LENGTH_MIN
+
+ # Create database connection object and mailer
+ db.init_app(app)
+ Migrate(app, db)
+
+ ##########################################################################
+ # Upgrade the schema (if required)
+ ##########################################################################
+ from config import SQLITE_PATH
+ from pgadmin.setup import db_upgrade
+
+ def backup_db_file():
+ """
+ Create a backup of the current database file
+ and create new database file with default settings.
+ """
+ backup_file_name = "{0}.{1}".format(
+ SQLITE_PATH, datetime.now().strftime('%Y%m%d%H%M%S'))
+ os.rename(SQLITE_PATH, backup_file_name)
+ app.logger.error('Exception in database migration.')
+ app.logger.info('Creating new database file.')
+ try:
+ db_upgrade(app)
+ os.environ[
+ 'CORRUPTED_DB_BACKUP_FILE'] = backup_file_name
+ app.logger.info('Database migration completed.')
+ except Exception:
+ app.logger.error('Database migration failed')
+ app.logger.error(traceback.format_exc())
+ raise RuntimeError('Migration failed')
+
+ def upgrade_db():
+ """
+ Execute the migrations.
+ """
+ try:
+ db_upgrade(app)
+ os.environ['CORRUPTED_DB_BACKUP_FILE'] = ''
+ except Exception:
+ app.logger.error('Database migration failed')
+ app.logger.error(traceback.format_exc())
+ backup_db_file()
+
+ # check all tables are present in the db.
+ is_db_error, invalid_tb_names = check_db_tables()
+ if is_db_error:
+ app.logger.error(
+ 'Table(s) {0} are missing in the'
+ ' database'.format(invalid_tb_names))
+ backup_db_file()
+
+ def run_migration_for_sqlite():
+ with app.app_context():
+ # Run migration for the first time i.e. create database
+ # If version not available, user must have aborted. Tables are not
+ # created and so its an empty db
+ if not os.path.exists(SQLITE_PATH) or get_version() == -1:
+ # If running in cli mode then don't try to upgrade, just raise
+ # the exception
+ if not cli_mode:
+ upgrade_db()
+ else:
+ if not os.path.exists(SQLITE_PATH):
+ raise FileNotFoundError(
+ 'SQLite database file "' + SQLITE_PATH +
+ '" does not exists.')
+ raise RuntimeError(
+ 'The configuration database file is not valid.')
+ else:
+ schema_version = get_version()
+
+ # Run migration if current schema version is greater than the
+ # schema version stored in version table
+ if CURRENT_SCHEMA_VERSION > schema_version:
+ # Take a backup of the old database file.
+ try:
+ prev_database_file_name = \
+ "{0}.prev.bak".format(SQLITE_PATH)
+ shutil.copyfile(SQLITE_PATH, prev_database_file_name)
+ except Exception as e:
+ app.logger.error(e)
+
+ upgrade_db()
+ else:
+ # check all tables are present in the db.
+ is_db_error, invalid_tb_names = check_db_tables()
+ if is_db_error:
+ app.logger.error(
+ 'Table(s) {0} are missing in the'
+ ' database'.format(invalid_tb_names))
+ backup_db_file()
+
+ # Update schema version to the latest
+ if CURRENT_SCHEMA_VERSION > schema_version:
+ set_version(CURRENT_SCHEMA_VERSION)
+ db.session.commit()
+
+ if os.name != 'nt':
+ os.chmod(config.SQLITE_PATH, 0o600)
+
+ def run_migration_for_others():
+ with app.app_context():
+ # Run migration for the first time i.e. create database
+ # If version not available, user must have aborted. Tables are not
+ # created and so its an empty db
+ if get_version() == -1:
+ db_upgrade(app)
+ else:
+ schema_version = get_version()
+
+ # Run migration if current schema version is greater than
+ # the schema version stored in version table.
+ if CURRENT_SCHEMA_VERSION > schema_version:
+ db_upgrade(app)
+ # Update schema version to the latest
+ set_version(CURRENT_SCHEMA_VERSION)
+ db.session.commit()
+
+ # Run the migration as per specified by the user.
+ if config.CONFIG_DATABASE_URI is not None and \
+ len(config.CONFIG_DATABASE_URI) > 0:
+ run_migration_for_others()
+ else:
+ run_migration_for_sqlite()
+
+ Mail(app)
+
+ # Don't bother paths when running in cli mode
+ if not cli_mode:
+ from pgadmin.utils import paths
+ paths.init_app()
+
+ # Setup Flask-Security
+ user_datastore = SQLAlchemyUserDatastore(db, User, Role)
+ security = Security(None, user_datastore)
+
+ ##########################################################################
+ # Setup security
+ ##########################################################################
+ with app.app_context():
+ config.CSRF_SESSION_KEY = Keys.query.filter_by(
+ name='CSRF_SESSION_KEY').first().value
+ config.SECRET_KEY = Keys.query.filter_by(
+ name='SECRET_KEY').first().value
+ config.SECURITY_PASSWORD_SALT = Keys.query.filter_by(
+ name='SECURITY_PASSWORD_SALT').first().value
+
+ # Update the app.config with proper security keyes for signing CSRF data,
+ # signing cookies, and the SALT for hashing the passwords.
+ app.config.update(dict({
+ 'CSRF_SESSION_KEY': config.CSRF_SESSION_KEY,
+ 'SECRET_KEY': config.SECRET_KEY,
+ 'SECURITY_PASSWORD_SALT': config.SECURITY_PASSWORD_SALT,
+ 'SESSION_COOKIE_DOMAIN': config.SESSION_COOKIE_DOMAIN,
+ # CSRF Token expiration till session expires
+ 'WTF_CSRF_TIME_LIMIT': getattr(config, 'CSRF_TIME_LIMIT', None),
+ 'WTF_CSRF_METHODS': ['GET', 'POST', 'PUT', 'DELETE'],
+ # Disable deliverable check for email addresss
+ 'SECURITY_EMAIL_VALIDATOR_ARGS': config.SECURITY_EMAIL_VALIDATOR_ARGS,
+ # Disable CSRF for unauthenticated endpoints
+ 'SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS': True
+ }))
+
+ app.config.update(dict({
+ 'INTERNAL': INTERNAL,
+ 'LDAP': LDAP,
+ 'KERBEROS': KERBEROS,
+ 'OAUTH2': OAUTH2,
+ 'WEBSERVER': WEBSERVER
+ }))
+
+ security.init_app(app, user_datastore)
+
+ # register custom unauthorised handler.
+ if sys.version_info < (3, 8):
+ app.login_manager.unauthorized_handler(pga_unauthorised)
+ else:
+ # Flask-Security-Too > 5.4.* requires custom unauth handeler
+ # to be registeres with it.
+ security.unauthn_handler(pga_unauthorised)
+
+ # Set the permanent session lifetime to the specified value in config file.
+ app.permanent_session_lifetime = timedelta(
+ days=config.SESSION_EXPIRATION_TIME)
+
+ if not cli_mode:
+ app.session_interface = create_session_interface(
+ app, config.SESSION_SKIP_PATHS
+ )
+
+ # Make the Session more secure against XSS & CSRF when running in web mode
+ if config.SERVER_MODE and config.ENHANCED_COOKIE_PROTECTION:
+ paranoid = Paranoid(app)
+ paranoid.redirect_view = _INDEX_PATH
+
+ ##########################################################################
+ # Load all available server drivers
+ ##########################################################################
+ driver.init_app(app)
+ authenticate.init_app(app)
+ heartbeat.init_app(app)
+
+ ##########################################################################
+ # Register language to the preferences after login
+ ##########################################################################
+ @user_logged_in.connect_via(app)
+ def register_language(sender, user):
+ # After logged in, set the language in the preferences if we get from
+ # the login page
+ data = request.form
+ if 'language' in data:
+ language = data['language']
+
+ # Set the user language preference
+ misc_preference = Preferences.module('misc')
+ user_languages = misc_preference.preference(
+ 'user_language'
+ )
+
+ if user_languages and language:
+ language = user_languages.set(language)
+
+ ##########################################################################
+ # Register any local servers we can discover
+ ##########################################################################
+ @user_logged_in.connect_via(app)
+ def on_user_logged_in(sender, user):
+
+ # If Auto Discover servers is turned off then return from the
+ # function.
+ if not config.AUTO_DISCOVER_SERVERS:
+ return
+
+ # Keep hold of the user ID
+ user_id = user.id
+
+ # Get the first server group for the user
+ servergroup_id = 1
+ servergroups = ServerGroup.query.filter_by(
+ user_id=user_id
+ ).order_by("id")
+
+ if int(servergroups.count()) > 0:
+ servergroup = servergroups.first()
+ servergroup_id = servergroup.id
+
+ '''Add a server to the config database'''
+
+ def add_server(user_id, servergroup_id, name, superuser, port,
+ discovery_id, comment):
+ # Create a server object if needed, and store it.
+ servers = Server.query.filter_by(
+ user_id=user_id,
+ discovery_id=svr_discovery_id
+ ).order_by("id")
+
+ if int(servers.count()) > 0:
+ return
+
+ svr = Server(user_id=user_id,
+ servergroup_id=servergroup_id,
+ name=name,
+ host='localhost',
+ port=port,
+ maintenance_db='postgres',
+ username=superuser,
+ connection_params={'sslmode': 'prefer',
+ 'connect_timeout': 10},
+ comment=comment,
+ discovery_id=discovery_id)
+
+ db.session.add(svr)
+ db.session.commit()
+
+ # Figure out what servers are present
+ if winreg is not None:
+ arch_keys = set()
+ proc_arch = os.environ['PROCESSOR_ARCHITECTURE'].lower()
+
+ try:
+ proc_arch64 = os.environ['PROCESSOR_ARCHITEW6432'].lower()
+ except Exception:
+ proc_arch64 = None
+
+ if proc_arch == 'x86' and not proc_arch64:
+ arch_keys.add(0)
+ elif proc_arch == 'x86' or proc_arch == 'amd64':
+ arch_keys.add(winreg.KEY_WOW64_32KEY)
+ arch_keys.add(winreg.KEY_WOW64_64KEY)
+
+ for arch_key in arch_keys:
+ for server_type in ('PostgreSQL', 'EnterpriseDB'):
+ try:
+ root_key = winreg.OpenKey(
+ winreg.HKEY_LOCAL_MACHINE,
+ "SOFTWARE\\" + server_type + "\\Services", 0,
+ winreg.KEY_READ | arch_key
+ )
+ for i in range(0, winreg.QueryInfoKey(root_key)[0]):
+ inst_id = winreg.EnumKey(root_key, i)
+ inst_key = winreg.OpenKey(root_key, inst_id)
+
+ svr_name = winreg.QueryValueEx(
+ inst_key, 'Display Name'
+ )[0]
+ svr_superuser = winreg.QueryValueEx(
+ inst_key, 'Database Superuser'
+ )[0]
+ svr_port = winreg.QueryValueEx(inst_key, 'Port')[0]
+ svr_discovery_id = inst_id
+ svr_comment = gettext(
+ "Auto-detected {0} installation with the data "
+ "directory at {1}").format(
+ winreg.QueryValueEx(
+ inst_key, 'Display Name'
+ )[0],
+ winreg.QueryValueEx(
+ inst_key, 'Data Directory'
+ )[0])
+
+ add_server(
+ user_id, servergroup_id, svr_name,
+ svr_superuser, svr_port,
+ svr_discovery_id, svr_comment
+ )
+
+ inst_key.Close()
+ except Exception:
+ pass
+ else:
+ # We use the postgres-winreg.ini file on non-Windows
+ from configparser import ConfigParser
+
+ registry = ConfigParser()
+
+ try:
+ registry.read('/etc/postgres-reg.ini')
+ sections = registry.sections()
+
+ # Loop the sections, and get the data from any that are PG or PPAS
+ for section in sections:
+ if (
+ section.startswith('PostgreSQL/') or
+ section.startswith('EnterpriseDB/')
+ ):
+ svr_name = registry.get(section, 'Description')
+ svr_superuser = registry.get(section, 'Superuser')
+
+ # getint function throws exception if value is blank.
+ # Ex: Port=
+ # In such case we should handle the exception and continue
+ # to read the next section of the config file.
+ try:
+ svr_port = registry.getint(section, 'Port')
+ except ValueError:
+ continue
+
+ svr_discovery_id = section
+ description = registry.get(section, 'Description')
+ data_directory = registry.get(section, 'DataDirectory')
+ svr_comment = gettext("Auto-detected {0} installation "
+ "with the data directory at {1}"
+ ).format(description, data_directory)
+ add_server(user_id, servergroup_id, svr_name,
+ svr_superuser, svr_port, svr_discovery_id,
+ svr_comment)
+
+ except Exception as e:
+ print(str(e))
+ db.session.rollback()
+
+ @user_logged_in.connect_via(app)
+ @user_logged_out.connect_via(app)
+ def force_session_write(app, user):
+ session.force_write = True
+
+ @user_logged_in.connect_via(app)
+ def store_crypt_key(app, user):
+ # in desktop mode, master password is used to encrypt/decrypt
+ # and is stored in the keyManager memory
+ if config.SERVER_MODE and 'password' in request.form:
+ current_app.keyManager.set(request.form['password'])
+
+ @user_logged_out.connect_via(app)
+ def current_user_cleanup(app, user):
+ from config import PG_DEFAULT_DRIVER
+ from pgadmin.utils.driver import get_driver
+ from flask import current_app
+
+ for mdl in current_app.logout_hooks:
+ try:
+ mdl.on_logout()
+ except Exception as e:
+ current_app.logger.exception(e)
+
+ _driver = get_driver(PG_DEFAULT_DRIVER)
+ _driver.gc_own()
+
+ # remove key
+ current_app.keyManager.reset()
+
+ ##########################################################################
+ # Load plugin modules
+ ##########################################################################
+ from .submodules import get_submodules
+ for module in get_submodules():
+ app.logger.info('Registering blueprint module: %s' % module)
+ if app.blueprints.get(module.name) is None:
+ app.register_blueprint(module)
+ app.register_logout_hook(module)
+
+ @app.before_request
+ def limit_host_addr():
+ """
+ This function validate the hosts from ALLOWED_HOSTS before allowing
+ HTTP request to avoid Host Header Injection attack
+ :return: None/JSON response with 403 HTTP status code
+ """
+ client_host = str(request.host).split(':', maxsplit=1)[0]
+ valid = True
+ allowed_hosts = config.ALLOWED_HOSTS
+
+ if len(allowed_hosts) != 0:
+ regex = re.compile(
+ r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(?:/\d{1,2}|)')
+ # Create separate list for ip addresses and host names
+ ip_set = list(filter(lambda ip: regex.match(ip), allowed_hosts))
+ host_set = list(filter(lambda ip: not regex.match(ip),
+ allowed_hosts))
+ is_ip = regex.match(client_host)
+ if is_ip:
+ ip_address = []
+ for ip in ip_set:
+ ip_address.extend(list(ipaddress.ip_network(ip)))
+ valid = ip_address.__contains__(
+ ipaddress.ip_address(client_host)
+ )
+ else:
+ valid = host_set.__contains__(client_host)
+
+ if not valid:
+ return make_json_response(
+ status=403, success=0,
+ errormsg=_("403 FORBIDDEN")
+ )
+
+ ##########################################################################
+ # Handle the desktop login
+ ##########################################################################
+
+ @app.before_request
+ def before_request():
+ """Login the default user if running in desktop mode"""
+
+ # Check the auth key is valid, if it's set, and we're not in server
+ # mode, and it's not a help file request.
+
+ if not config.SERVER_MODE and app.PGADMIN_INT_KEY != '' and ((
+ 'key' not in request.args or
+ request.args['key'] != app.PGADMIN_INT_KEY) and
+ request.cookies.get('PGADMIN_INT_KEY') != app.PGADMIN_INT_KEY and
+ request.endpoint != 'help.static'
+ ):
+ abort(401)
+
+ if not config.SERVER_MODE and not current_user.is_authenticated:
+ user = user_datastore.find_user(email=config.DESKTOP_USER)
+ # Throw an error if we failed to find the desktop user, to give
+ # the sysadmin a hint. We'll continue to try to login anyway as
+ # that'll through a nice 500 error for us.
+ if user is None:
+ app.logger.error(
+ 'The desktop user %s was not found in the configuration '
+ 'database.'
+ % config.DESKTOP_USER
+ )
+ abort(401)
+ login_user(user)
+ elif config.SERVER_MODE and not current_user.is_authenticated and \
+ request.endpoint in ('redirects.index', 'security.login') and \
+ app.PGADMIN_EXTERNAL_AUTH_SOURCE in [KERBEROS, WEBSERVER]:
+ return authenticate.login()
+ # if the server is restarted the in memory key will be lost
+ # but the user session may still be active. Logout the user
+ # to get the key again when login
+ if config.SERVER_MODE and current_user.is_authenticated and \
+ app.PGADMIN_EXTERNAL_AUTH_SOURCE not in [
+ KERBEROS, OAUTH2, WEBSERVER] and \
+ current_app.keyManager.get() is None and \
+ request.endpoint not in ('security.login', 'security.logout'):
+ logout_user()
+
+ @app.after_request
+ def after_request(response):
+ if 'key' in request.args:
+ domain = dict()
+ if config.COOKIE_DEFAULT_DOMAIN and \
+ config.COOKIE_DEFAULT_DOMAIN != 'localhost':
+ domain['domain'] = config.COOKIE_DEFAULT_DOMAIN
+ response.set_cookie('PGADMIN_INT_KEY', value=request.args['key'],
+ path=config.SESSION_COOKIE_PATH,
+ secure=config.SESSION_COOKIE_SECURE,
+ httponly=config.SESSION_COOKIE_HTTPONLY,
+ samesite=config.SESSION_COOKIE_SAMESITE,
+ **domain)
+
+ SecurityHeaders.set_response_headers(response)
+ return response
+
+ ##########################################################################
+ # Cache busting
+ ##########################################################################
+
+ # Version number to be added to all static file url requests
+ # This is used by url_for function when generating urls
+ # This will solve caching issues when application is upgrading
+ # This is called - Cache Busting
+ @app.url_defaults
+ def add_internal_version(endpoint, values):
+ extensions = config.APP_VERSION_EXTN
+
+ # Add the internal version only if it is set
+ if config.APP_VERSION_PARAM is not None and \
+ config.APP_VERSION_PARAM != '':
+ # If there is a filename, add the version
+ if 'filename' in values \
+ and values['filename'].endswith(extensions):
+ values[config.APP_VERSION_PARAM] = config.APP_VERSION_INT
+ else:
+ # Sometimes there may be direct endpoint for some files
+ # There will be only one rule for such endpoints
+ urls = [url for url in app.url_map.iter_rules(endpoint)]
+ if len(urls) == 1 and urls[0].rule.endswith(extensions):
+ values[config.APP_VERSION_PARAM] = \
+ config.APP_VERSION_INT
+
+ # Strip away internal version param before sending further to app as it was
+ # required for cache busting only
+ @app.url_value_preprocessor
+ def strip_version_number(endpoint, values):
+ if values and config.APP_VERSION_PARAM in values:
+ values.pop(config.APP_VERSION_PARAM)
+
+ ##########################################################################
+ # Minify output. Not required in desktop mode
+ ##########################################################################
+ if not config.DEBUG and config.SERVER_MODE:
+ from flask_compress import Compress
+ Compress(app)
+
+ @app.context_processor
+ def inject_blueprint():
+ """
+ Inject a reference to the current blueprint, if any.
+ """
+
+ return {
+ 'current_app': current_app,
+ 'current_blueprint': current_blueprint,
+ }
+
+ @app.errorhandler(Exception)
+ def all_exception_handler(e):
+ current_app.logger.error(e, exc_info=True)
+ return internal_server_error(errormsg=str(e))
+
+ # Exclude HTTPexception from above handler (all_exception_handler)
+ # HTTPException are user defined exceptions and those should be returned
+ # as is
+ @app.errorhandler(HTTPException)
+ def http_exception_handler(e):
+ current_app.logger.error(e, exc_info=True)
+ return e
+
+ # Intialize the key manager
+ app.keyManager = KeyManager()
+
+ ##########################################################################
+ # Protection against CSRF attacks
+ ##########################################################################
+ with app.app_context():
+ pgCSRFProtect.init_app(app)
+
+ ##########################################################################
+ # All done!
+ ##########################################################################
+ socketio.init_app(app, cors_allowed_origins="*")
+ return app