Skip to content

Commit bcd48a9

Browse files
Load predefined users from a JSON file through command line. #9229
1 parent 6d0d387 commit bcd48a9

3 files changed

Lines changed: 213 additions & 4 deletions

File tree

docs/en_US/user_management.rst

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,50 @@ username/email address.
270270
/path/to/python /path/to/setup.py get-users --username user1@gmail.com
271271
272272
273+
Load Users
274+
**********
275+
276+
To bulk import users from a JSON file, invoke ``setup.py`` with ``load-users`` command line option,
277+
followed by the path to the JSON file.
278+
279+
.. code-block:: bash
280+
281+
/path/to/python /path/to/setup.py load-users /path/to/users.json
282+
283+
**JSON File Format**
284+
285+
The input JSON file must contain a ``users`` array with user objects:
286+
287+
.. code-block:: json
288+
289+
{
290+
"users": [
291+
{
292+
"username": "admin@example.com",
293+
"email": "admin@example.com",
294+
"password": "securepassword",
295+
"role": "Administrator",
296+
"active": true,
297+
"auth_source": "internal"
298+
},
299+
{
300+
"username": "ldap_user",
301+
"email": "ldap_user@example.com",
302+
"role": "User",
303+
"active": true,
304+
"auth_source": "ldap"
305+
}
306+
]
307+
}
308+
309+
The command handles errors gracefully:
310+
311+
* Users that already exist are skipped
312+
* Invalid roles are reported and skipped
313+
* Missing passwords for internal auth are reported and skipped
314+
* Passwords shorter than 6 characters are reported and skipped
315+
316+
273317
Output
274318
******
275319

web/pgadmin/utils/session.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import config
2727
from uuid import uuid4
2828
from threading import Lock
29-
from flask import current_app, request, flash, redirect
29+
from flask import current_app, request, flash, redirect, has_request_context
3030
from flask_login import login_url
3131

3232
from pickle import dump, load
@@ -115,6 +115,17 @@ def _normalize(self):
115115
while len(self._cache) > (self.num_to_store * 0.8):
116116
self._cache.popitem(False)
117117

118+
def is_session_ready(self, _session):
119+
if not has_request_context() and _session is None:
120+
return False
121+
122+
# Session _id returns the str object
123+
# or None if it hasn't been set yet.
124+
try:
125+
return _session['_id'] is not None
126+
except (AssertionError, RuntimeError, KeyError):
127+
return False
128+
118129
def new_session(self):
119130
session = self.parent.new_session()
120131

@@ -143,16 +154,17 @@ def exists(self, sid):
143154

144155
def get(self, sid, digest):
145156
session = None
146-
with sess_lock:
157+
with (sess_lock):
147158
if sid in self._cache:
148159
session = self._cache[sid]
149-
if session and session.hmac_digest != digest:
160+
if self.is_session_ready(session) and\
161+
session.hmac_digest != digest:
150162
session = None
151163

152164
# reset order in Dict
153165
del self._cache[sid]
154166

155-
if not session:
167+
if not self.is_session_ready(session):
156168
session = self.parent.get(sid, digest)
157169

158170
# Do not store the session if skip paths

web/setup.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,159 @@ def get_role(role: str):
163163

164164
class ManageUsers:
165165

166+
@app.command()
167+
@update_sqlite_path
168+
def load_users(input_file: str,
169+
sqlite_path: Optional[str] = None):
170+
"""Load users from a JSON file.
171+
172+
Expected JSON format:
173+
{
174+
"users": [
175+
{
176+
"username": "user@example.com",
177+
"email": "user@example.com",
178+
"password": "password123",
179+
"role": "User",
180+
"active": true,
181+
"auth_source": "internal"
182+
},
183+
{
184+
"username": "ldap_user",
185+
"email": "ldap@example.com",
186+
"role": "Administrator",
187+
"active": true,
188+
"auth_source": "ldap"
189+
}
190+
]
191+
}
192+
"""
193+
from urllib.parse import unquote
194+
195+
print('----------')
196+
print('Loading users from:', input_file)
197+
print('SQLite pgAdmin config:', config.SQLITE_PATH)
198+
print('----------')
199+
200+
# Parse the input file path
201+
try:
202+
file_path = unquote(input_file)
203+
except Exception as e:
204+
return _handle_error(str(e), True)
205+
206+
# Read and parse JSON file
207+
try:
208+
with open(file_path) as f:
209+
data = jsonlib.load(f)
210+
except jsonlib.decoder.JSONDecodeError as e:
211+
return _handle_error(
212+
gettext("Error parsing input file %s: %s" % (file_path, e)),
213+
True)
214+
except Exception as e:
215+
return _handle_error(
216+
gettext("Error reading input file %s: [%d] %s" %
217+
(file_path, e.errno, e.strerror)), True)
218+
219+
# Validate JSON structure
220+
if 'users' not in data:
221+
return _handle_error(
222+
gettext("Invalid JSON format: 'users' key not found"), True)
223+
224+
users_data = data['users']
225+
if not isinstance(users_data, list):
226+
return _handle_error(
227+
gettext("Invalid JSON format: 'users' must be a list"), True)
228+
229+
created_count = 0
230+
skipped_count = 0
231+
error_count = 0
232+
233+
app = create_app(config.APP_NAME + '-cli')
234+
with (app.test_request_context()):
235+
for user_entry in users_data:
236+
try:
237+
# Validate required fields
238+
if 'username' not in user_entry and\
239+
'email' not in user_entry:
240+
print(f"Skipping user: missing 'username' or 'email'")
241+
error_count += 1
242+
continue
243+
244+
# Determine auth_source (default to internal)
245+
auth_source = user_entry.get('auth_source', INTERNAL)
246+
247+
# Build user data dict
248+
user_data = {
249+
'username': user_entry.get('username',
250+
user_entry.get('email')),
251+
'email': user_entry.get('email'),
252+
'role': user_entry.get('role', 'User'),
253+
'active': user_entry.get('active', True),
254+
'auth_source': auth_source
255+
}
256+
257+
# For internal auth, password is required
258+
if auth_source == INTERNAL:
259+
if 'password' not in user_entry:
260+
print(f"Skipping user '{user_data['username']}': "
261+
f"password required for internal auth")
262+
error_count += 1
263+
continue
264+
user_data['newPassword'] = user_entry['password']
265+
user_data['confirmPassword'] = user_entry['password']
266+
267+
# Check if user already exists
268+
usr = User.query.filter_by(username=user_data['username'],
269+
auth_source=auth_source).first()
270+
271+
uid = usr.id if usr else None
272+
273+
if uid:
274+
print(f"Skipping user '{user_data['username']}': "
275+
f"already exists")
276+
skipped_count += 1
277+
continue
278+
279+
# Get role ID
280+
role = Role.query.filter_by(name=user_data['role']).first()
281+
rid = role.id if role else None
282+
283+
if rid is None:
284+
print(f"Skipping user '{user_data['username']}': "
285+
f"role '{user_data['role']}' does not exist")
286+
error_count += 1
287+
continue
288+
289+
user_data['role'] = rid
290+
291+
# Validate password length for internal users
292+
if auth_source == INTERNAL:
293+
if len(user_data['newPassword']) < 6:
294+
print(f"Skipping user '{user_data['username']}': "
295+
f"password must be at least 6 characters")
296+
error_count += 1
297+
continue
298+
299+
# Create the user
300+
status, msg = create_user(user_data)
301+
if status:
302+
print(f"Created user: {user_data['username']}")
303+
created_count += 1
304+
else:
305+
print(f"Error creating user '{user_data['username']}'"
306+
f": {msg}")
307+
error_count += 1
308+
309+
except Exception as e:
310+
print(f"Error processing user entry: {str(e)}")
311+
error_count += 1
312+
313+
print('----------')
314+
print(f"Users created: {created_count}")
315+
print(f"Users skipped (already exist): {skipped_count}")
316+
print(f"Errors: {error_count}")
317+
print('----------')
318+
166319
@app.command()
167320
@update_sqlite_path
168321
def add_user(email: str, password: str,

0 commit comments

Comments
 (0)