From 0f943b5f1017c8f7c7f268637d417a7230c97189 Mon Sep 17 00:00:00 2001 From: Khushboo Vashi Date: Fri, 20 Feb 2026 10:49:16 +0530 Subject: [PATCH 1/7] Load predefined users from a JSON file through command line. #9229 --- docs/en_US/user_management.rst | 44 ++++++++++ web/setup.py | 150 +++++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+) diff --git a/docs/en_US/user_management.rst b/docs/en_US/user_management.rst index d34711a07c6..d4c181fe256 100644 --- a/docs/en_US/user_management.rst +++ b/docs/en_US/user_management.rst @@ -270,6 +270,50 @@ username/email address. /path/to/python /path/to/setup.py get-users --username user1@gmail.com +Load Users +********** + +To bulk import users from a JSON file, invoke ``setup.py`` with ``load-users`` command line option, +followed by the path to the JSON file. + +.. code-block:: bash + + /path/to/python /path/to/setup.py load-users /path/to/users.json + +**JSON File Format** + +The input JSON file must contain a ``users`` array with user objects: + +.. code-block:: json + + { + "users": [ + { + "username": "admin@example.com", + "email": "admin@example.com", + "password": "securepassword", + "role": "Administrator", + "active": true, + "auth_source": "internal" + }, + { + "username": "ldap_user", + "email": "ldap_user@example.com", + "role": "User", + "active": true, + "auth_source": "ldap" + } + ] + } + +The command handles errors gracefully: + +* Users that already exist are skipped +* Invalid roles are reported and skipped +* Missing passwords for internal auth are reported and skipped +* Passwords shorter than 6 characters are reported and skipped + + Output ****** diff --git a/web/setup.py b/web/setup.py index 6b664f4fd03..b616f5659bc 100644 --- a/web/setup.py +++ b/web/setup.py @@ -163,6 +163,156 @@ def get_role(role: str): class ManageUsers: + @app.command() + @update_sqlite_path + def load_users(input_file: str, + sqlite_path: Optional[str] = None, + json: Optional[bool] = False): + """Load users from a JSON file. + + Expected JSON format: + { + "users": [ + { + "username": "user@example.com", + "email": "user@example.com", + "password": "password123", + "role": "User", + "active": true, + "auth_source": "internal" + }, + { + "username": "ldap_user", + "email": "ldap@example.com", + "role": "Administrator", + "active": true, + "auth_source": "ldap" + } + ] + } + """ + from urllib.parse import unquote + + print('----------') + print('Loading users from:', input_file) + print('SQLite pgAdmin config:', config.SQLITE_PATH) + print('----------') + + # Parse the input file path + try: + file_path = unquote(input_file) + except Exception as e: + print(str(e)) + return _handle_error(str(e), True) + + # Read and parse JSON file + try: + with open(file_path) as f: + data = jsonlib.load(f) + except jsonlib.decoder.JSONDecodeError as e: + return _handle_error( + gettext("Error parsing input file %s: %s" % (file_path, e)), + True) + except Exception as e: + return _handle_error( + gettext("Error reading input file %s: [%d] %s" % + (file_path, e.errno, e.strerror)), True) + + # Validate JSON structure + if 'users' not in data: + return _handle_error( + gettext("Invalid JSON format: 'users' key not found"), True) + + users_data = data['users'] + if not isinstance(users_data, list): + return _handle_error( + gettext("Invalid JSON format: 'users' must be a list"), True) + + created_count = 0 + skipped_count = 0 + error_count = 0 + + app = create_app(config.APP_NAME + '-cli') + with app.test_request_context(): + for user_entry in users_data: + try: + # Validate required fields + if 'username' not in user_entry and 'email' not in user_entry: + print(f"Skipping user: missing 'username' or 'email'") + error_count += 1 + continue + + # Determine auth_source (default to internal) + auth_source = user_entry.get('auth_source', INTERNAL) + + # Build user data dict + user_data = { + 'username': user_entry.get('username', + user_entry.get('email')), + 'email': user_entry.get('email'), + 'role': user_entry.get('role', 'User'), + 'active': user_entry.get('active', True), + 'auth_source': auth_source + } + + # For internal auth, password is required + if auth_source == INTERNAL: + if 'password' not in user_entry: + print(f"Skipping user '{user_data['username']}': " + f"password required for internal auth") + error_count += 1 + continue + user_data['newPassword'] = user_entry['password'] + user_data['confirmPassword'] = user_entry['password'] + + # Check if user already exists + uid = ManageUsers.get_user( + username=user_data['username'], + auth_source=auth_source) + if uid: + print(f"Skipping user '{user_data['username']}': " + f"already exists") + skipped_count += 1 + continue + + # Get role ID + rid = ManageRoles.get_role(user_data['role']) + if rid is None: + print(f"Skipping user '{user_data['username']}': " + f"role '{user_data['role']}' does not exist") + error_count += 1 + continue + + user_data['role'] = rid + + # Validate password length for internal users + if auth_source == INTERNAL: + if len(user_data['newPassword']) < 6: + print(f"Skipping user '{user_data['username']}': " + f"password must be at least 6 characters") + error_count += 1 + continue + + # Create the user + status, msg = create_user(user_data) + if status: + print(f"Created user: {user_data['username']}") + created_count += 1 + else: + print(f"Error creating user '{user_data['username']}'" + f": {msg}") + error_count += 1 + + except Exception as e: + print(f"Error processing user entry: {str(e)}") + error_count += 1 + + print('----------') + print(f"Users created: {created_count}") + print(f"Users skipped (already exist): {skipped_count}") + print(f"Errors: {error_count}") + print('----------') + @app.command() @update_sqlite_path def add_user(email: str, password: str, From 1f66714576698831c2360935026e3575a6677a71 Mon Sep 17 00:00:00 2001 From: Khushboo Vashi Date: Fri, 20 Feb 2026 10:56:00 +0530 Subject: [PATCH 2/7] Fix PEP8 issue. --- web/setup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/setup.py b/web/setup.py index b616f5659bc..63fe2f307bd 100644 --- a/web/setup.py +++ b/web/setup.py @@ -233,11 +233,12 @@ def load_users(input_file: str, error_count = 0 app = create_app(config.APP_NAME + '-cli') - with app.test_request_context(): + with (app.test_request_context()): for user_entry in users_data: try: # Validate required fields - if 'username' not in user_entry and 'email' not in user_entry: + if 'username' not in user_entry and\ + 'email' not in user_entry: print(f"Skipping user: missing 'username' or 'email'") error_count += 1 continue From b1fe14f57ba07748ecc07fe410d69b196daac9b7 Mon Sep 17 00:00:00 2001 From: Khushboo Vashi Date: Mon, 23 Feb 2026 10:35:32 +0530 Subject: [PATCH 3/7] Fix session isssue due to Flask upgrade. --- web/pgadmin/utils/session.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/web/pgadmin/utils/session.py b/web/pgadmin/utils/session.py index 6b050858576..7ffe6a37a6a 100644 --- a/web/pgadmin/utils/session.py +++ b/web/pgadmin/utils/session.py @@ -38,6 +38,8 @@ from werkzeug.security import safe_join from werkzeug.exceptions import InternalServerError +from flask import has_request_context + from pgadmin.utils.ajax import make_json_response @@ -100,7 +102,6 @@ def put(self, session): 'Store a managed session' raise NotImplementedError - class CachingSessionManager(SessionManager): def __init__(self, parent, num_to_store, skip_paths=None): self.parent = parent @@ -115,6 +116,17 @@ def _normalize(self): while len(self._cache) > (self.num_to_store * 0.8): self._cache.popitem(False) + def is_session_ready(self, _session): + if not has_request_context(): + return False + + # ._get_current_object() returns the actual dict-like object + # or None if it hasn't been set yet. + try: + return _session._get_current_object() is not None + except (AssertionError, RuntimeError): + return False + def new_session(self): session = self.parent.new_session() @@ -146,13 +158,13 @@ def get(self, sid, digest): with sess_lock: if sid in self._cache: session = self._cache[sid] - if session and session.hmac_digest != digest: + if self.is_session_ready(session) and session.hmac_digest != digest: session = None # reset order in Dict del self._cache[sid] - if not session: + if not self.is_session_ready(session): session = self.parent.get(sid, digest) # Do not store the session if skip paths From d22a079f9fd672ade961e8b789f51b3d2cca5796 Mon Sep 17 00:00:00 2001 From: Khushboo Vashi Date: Mon, 23 Feb 2026 17:05:23 +0530 Subject: [PATCH 4/7] Fix Python test failure. --- web/pgadmin/utils/session.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/web/pgadmin/utils/session.py b/web/pgadmin/utils/session.py index 7ffe6a37a6a..a34a122fe86 100644 --- a/web/pgadmin/utils/session.py +++ b/web/pgadmin/utils/session.py @@ -102,6 +102,7 @@ def put(self, session): 'Store a managed session' raise NotImplementedError + class CachingSessionManager(SessionManager): def __init__(self, parent, num_to_store, skip_paths=None): self.parent = parent @@ -120,10 +121,10 @@ def is_session_ready(self, _session): if not has_request_context(): return False - # ._get_current_object() returns the actual dict-like object + # Session _id returns the str object # or None if it hasn't been set yet. try: - return _session._get_current_object() is not None + return _session['_id'] is not None except (AssertionError, RuntimeError): return False @@ -155,10 +156,11 @@ def exists(self, sid): def get(self, sid, digest): session = None - with sess_lock: + with (sess_lock): if sid in self._cache: session = self._cache[sid] - if self.is_session_ready(session) and session.hmac_digest != digest: + if self.is_session_ready(session) and\ + session.hmac_digest != digest: session = None # reset order in Dict From da3454ec13f3cbecb4dbaf74f7786ab7df7e5969 Mon Sep 17 00:00:00 2001 From: Khushboo Vashi Date: Mon, 23 Feb 2026 17:10:22 +0530 Subject: [PATCH 5/7] Fix Python test failure. --- web/pgadmin/utils/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/pgadmin/utils/session.py b/web/pgadmin/utils/session.py index a34a122fe86..a2b726c73e3 100644 --- a/web/pgadmin/utils/session.py +++ b/web/pgadmin/utils/session.py @@ -125,7 +125,7 @@ def is_session_ready(self, _session): # or None if it hasn't been set yet. try: return _session['_id'] is not None - except (AssertionError, RuntimeError): + except (AssertionError, RuntimeError, KeyError): return False def new_session(self): From fd58e01ad28d9e802ad863bdbd4282b1ec79c9f9 Mon Sep 17 00:00:00 2001 From: Khushboo Vashi Date: Tue, 24 Feb 2026 11:45:25 +0530 Subject: [PATCH 6/7] Fix Python test case. --- web/pgadmin/utils/session.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/web/pgadmin/utils/session.py b/web/pgadmin/utils/session.py index a2b726c73e3..65fc2393877 100644 --- a/web/pgadmin/utils/session.py +++ b/web/pgadmin/utils/session.py @@ -26,7 +26,7 @@ import config from uuid import uuid4 from threading import Lock -from flask import current_app, request, flash, redirect +from flask import current_app, request, flash, redirect, has_request_context from flask_login import login_url from pickle import dump, load @@ -38,8 +38,6 @@ from werkzeug.security import safe_join from werkzeug.exceptions import InternalServerError -from flask import has_request_context - from pgadmin.utils.ajax import make_json_response @@ -118,7 +116,7 @@ def _normalize(self): self._cache.popitem(False) def is_session_ready(self, _session): - if not has_request_context(): + if not has_request_context() and _session is None: return False # Session _id returns the str object From e30bfebe5a500bf59f73d85b86fb3b1407269ac9 Mon Sep 17 00:00:00 2001 From: Khushboo Vashi Date: Tue, 24 Feb 2026 15:17:30 +0530 Subject: [PATCH 7/7] Optimize the code. --- web/setup.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/web/setup.py b/web/setup.py index 63fe2f307bd..6fec576659b 100644 --- a/web/setup.py +++ b/web/setup.py @@ -166,8 +166,7 @@ class ManageUsers: @app.command() @update_sqlite_path def load_users(input_file: str, - sqlite_path: Optional[str] = None, - json: Optional[bool] = False): + sqlite_path: Optional[str] = None): """Load users from a JSON file. Expected JSON format: @@ -202,7 +201,6 @@ def load_users(input_file: str, try: file_path = unquote(input_file) except Exception as e: - print(str(e)) return _handle_error(str(e), True) # Read and parse JSON file @@ -267,9 +265,11 @@ def load_users(input_file: str, user_data['confirmPassword'] = user_entry['password'] # Check if user already exists - uid = ManageUsers.get_user( - username=user_data['username'], - auth_source=auth_source) + usr = User.query.filter_by(username=user_data['username'], + auth_source=auth_source).first() + + uid = usr.id if usr else None + if uid: print(f"Skipping user '{user_data['username']}': " f"already exists") @@ -277,7 +277,9 @@ def load_users(input_file: str, continue # Get role ID - rid = ManageRoles.get_role(user_data['role']) + role = Role.query.filter_by(name=user_data['role']).first() + rid = role.id if role else None + if rid is None: print(f"Skipping user '{user_data['username']}': " f"role '{user_data['role']}' does not exist")