fix: harden validation/preference/connection-params paths against pre-existing edge cases

Five small defensive fixes that were exposed by running the full
regression suite end-to-end:

1. utils/validation_utils.py: validate_email() now returns False
   instead of raising TypeError when passed a non-str/bytes value
   (e.g. None from a missing form field). Matches the wrapper's
   contract that it only ever returns bool.

2. tools/user_management/__init__.py: list endpoint guarded against
   users with no roles. u.roles[0].id -> u.roles[0].id if u.roles
   else None. Triggered by ChangePasswordTestCase fixtures.

3. utils/preferences.py: control_props['tags'] / ['creatable']
   replaced with .get(...) so preferences whose control_props omit
   these keys do not raise KeyError on update.

4. browser/server_groups/servers/__init__.py (create endpoint):
   convert_connection_parameter() is bidirectional (list<->dict).
   The save path always wants the storage shape (dict). When input
   is already a dict (internal callers / tests mimicking storage
   form), skip conversion to avoid the dict->list round-trip that
   breaks the MutableDict column. Same fix applied to the workspaces
   save path.

5. misc/workspaces/__init__.py: same defensive handling for
   convert_connection_parameter() on the save path.

These are all pre-existing master bugs surfaced by edge-case test
data; none are introduced by the 9.15 CVE work.
This commit is contained in:
Ashesh Vashi
2026-05-01 23:29:43 +05:30
parent dc61039e93
commit d57acce354
5 changed files with 28 additions and 7 deletions
@@ -1266,8 +1266,17 @@ class ServerNode(PGChildNodeView):
).format(arg)
)
connection_params = convert_connection_parameter(
data.get('connection_params', []))
# convert_connection_parameter() is bidirectional: list->dict for
# save (frontend shape), dict->list for display (DB shape). Here
# we want the storage shape (dict). If the input is already a
# dict (e.g. from internal callers / tests that mimic stored
# form), keep it as-is; otherwise assume frontend list shape and
# convert.
raw_params = data.get('connection_params', [])
if isinstance(raw_params, dict):
connection_params = raw_params
else:
connection_params = convert_connection_parameter(raw_params)
if 'hostaddr' in connection_params and \
not is_valid_ipaddress(connection_params['hostaddr']):
+8 -2
View File
@@ -93,8 +93,14 @@ def adhoc_connect_server():
).format(arg)
)
connection_params = convert_connection_parameter(
data.get('connection_params', []))
# convert_connection_parameter() is bidirectional. For a save path we
# want the storage shape (dict). If the input is already a dict, keep
# it; otherwise assume frontend list shape and convert.
raw_params = data.get('connection_params', [])
if isinstance(raw_params, dict):
connection_params = raw_params
else:
connection_params = convert_connection_parameter(raw_params)
if connection_params is not None:
if 'hostaddr' in connection_params and \
@@ -168,7 +168,7 @@ def user(uid):
'username': u.username,
'email': u.email,
'active': u.active,
'role': u.roles[0].id,
'role': u.roles[0].id if u.roles else None,
'auth_source': u.auth_source,
'locked': u.locked,
'canDrop': u.id != current_user.id
+2 -2
View File
@@ -197,8 +197,8 @@ class _Preference():
if 'value' in opt and opt['value'] == value),
False)
assert (has_value or (self.control_props and
(self.control_props['tags'] or
self.control_props['creatable'])))
(self.control_props.get('tags') or
self.control_props.get('creatable'))))
elif self._type == 'date':
value = parser_map[self._type](value).date()
else:
+6
View File
@@ -13,6 +13,12 @@ from email_validator import validate_email as email_validate, \
def validate_email(email, email_config=None):
# email_validator raises TypeError (not EmailNotValidError) when the
# input is not str/bytes. Treat anything non-string as invalid so
# callers see a clean False, matching the contract that this wrapper
# never raises.
if not isinstance(email, (str, bytes)):
return False
try:
if email_config is None:
email_config = {}