33import logging
44import re
55from enum import Enum , auto
6- from typing import List , Optional , Union
6+ from typing import Any , List , Optional , Union
77from uuid import uuid4
88
99from django .conf import settings
@@ -54,13 +54,14 @@ def create_claims(authenticator: Authenticator, username: str, attrs: dict, grou
5454 rule_responses = []
5555 # Assume we will have access
5656 access_allowed = True
57- logger .info (f"Creating mapping for user { username } through authenticator { authenticator .name } " )
58- logger .debug (f"{ username } 's groups: { groups } " )
59- logger .debug (f"{ username } 's attrs: { attrs } " )
6057
6158 # debug tracking ID
6259 tracking_id = str (uuid4 ())
6360
61+ logger .info (f"[{ tracking_id } ] Creating mapping for user { username } through authenticator { authenticator .name } " )
62+ logger .debug (f"[{ tracking_id } ] { username } 's groups: { groups } " )
63+ logger .debug (f"[{ tracking_id } ] { username } 's attrs: { attrs } " )
64+
6465 # load the maps
6566 maps = AuthenticatorMap .objects .filter (authenticator = authenticator .pk ).order_by ("order" )
6667 logger .debug (f"Processing { maps .count ()} map(s) for Authenticator ID [{ authenticator .pk } ] ID [{ tracking_id } ]" )
@@ -240,7 +241,7 @@ def process_groups(trigger_condition: dict, groups: list, map_id: int, tracking_
240241
241242 invalid_conditions = set (trigger_condition .keys ()) - set (TRIGGER_DEFINITION ['groups' ]['keys' ].keys ())
242243 if invalid_conditions :
243- logger .warning (f"The conditions { ', ' .join (invalid_conditions )} for groups in mapping { map_id } are invalid and won't be processed" )
244+ logger .warning (f"[ { tracking_id } ] The conditions { ', ' .join (invalid_conditions )} for groups in mapping { map_id } are invalid and won't be processed" )
244245
245246 set_of_user_groups = set (groups )
246247
@@ -283,6 +284,45 @@ def has_access_with_join(current_access: Optional[bool], new_access: bool, condi
283284 return current_access and new_access
284285
285286
287+ def _lowercase_value (value : Any ) -> Any :
288+ """
289+ Convert a value to lowercase, handling different types appropriately.
290+
291+ Args:
292+ value: The value to convert (str, list, or other)
293+
294+ Returns:
295+ The converted value with appropriate case folding applied
296+ """
297+ if isinstance (value , str ):
298+ return value .casefold ()
299+ elif isinstance (value , list ):
300+ # Handle list values (for "in" operator which should only accept arrays)
301+ return [str (item ).casefold () for item in value ]
302+ else :
303+ # Keep other types as-is
304+ return value
305+
306+
307+ def _lowercase_dict (condition : dict ) -> dict :
308+ """
309+ Convert all values in a condition dictionary to lowercase.
310+
311+ Args:
312+ condition: Dictionary of
313+
314+ Returns:
315+ New dictionary with case-folded values (keys will remain the same)
316+ """
317+ if not condition : # empty dict
318+ return {}
319+
320+ updated_condition = {}
321+ for key , value in condition .items ():
322+ updated_condition [key ] = _lowercase_value (value )
323+ return updated_condition
324+
325+
286326def _lowercase_attr_triggers (trigger_condition : dict ) -> dict :
287327 """
288328 Lower case all keys (attribute names) and contained attribute values
@@ -292,61 +332,137 @@ def _lowercase_attr_triggers(trigger_condition: dict) -> dict:
292332 if isinstance (condition , str ):
293333 updated_condition = condition .casefold ()
294334 elif isinstance (condition , dict ):
295- if not condition : # empty dict
296- updated_condition = {}
297- for operator , value in condition .items ():
298- updated_condition = {operator : value .casefold ()}
335+ updated_condition = _lowercase_dict (condition )
299336 else :
300337 updated_condition = condition
301338
302- ci_trigger_condition [attr .casefold ()] = updated_condition # join_condition
339+ ci_trigger_condition [attr .casefold ()] = updated_condition
303340 return ci_trigger_condition
304341
305342
306- def process_user_attributes ( trigger_condition : dict , attributes : dict , map_id : int , tracking_id : str ) -> TriggerResult :
343+ def _validate_join_condition ( join_condition , map_id : int , tracking_id : str ) -> str :
307344 """
308- Looks at a maps trigger for an attribute and the users attributes and determines if the trigger is defined for this user.
309- Attribute names are compared case-insensitively when FEATURE_CASE_INSENSITIVE_AUTH_MAPS is enabled.
345+ Validate and normalize the join condition, defaulting to 'or' if invalid.
346+
347+ Args:
348+ join_condition: The join condition to validate
349+ map_id: Authenticator map ID for logging
350+ tracking_id: Tracking ID for logging
351+
352+ Returns:
353+ Valid join condition ('or' or 'and')
354+ """
355+ if join_condition not in TRIGGER_DEFINITION ['attributes' ]['keys' ]['join_condition' ]['choices' ]:
356+ logger .warning (f"[{ tracking_id } ] Trigger join_condition { join_condition } on authenticator map { map_id } is invalid and will be set to 'or'" )
357+ return 'or'
358+ return join_condition
359+
360+
361+ def _validate_attribute_conditions (attribute : str , condition : dict , map_id : int , tracking_id : str ) -> bool :
362+ """
363+ Validate attribute conditions and log warnings for invalid ones.
364+
365+ Args:
366+ attribute: The attribute name
367+ condition: The condition dictionary for this attribute
368+ map_id: Authenticator map ID for logging
369+ tracking_id: Tracking ID for logging
370+
371+ Returns:
372+ True if conditions are valid and should be processed, False if should be skipped
373+ """
374+ # Warn if there are any invalid conditions, we are just going to ignore them
375+ invalid_conditions = set (condition .keys ()) - set (TRIGGER_DEFINITION ['attributes' ]['keys' ]['*' ]['keys' ].keys ())
376+ if invalid_conditions :
377+ logger .warning (
378+ f"[{ tracking_id } ] The conditions { ', ' .join (invalid_conditions )} for attribute { attribute } "
379+ f"in authenticator map { map_id } are invalid and won't be processed"
380+ )
381+
382+ # Validate that 'in' operator only accepts arrays
383+ if "in" in condition and not isinstance (condition ["in" ], list ):
384+ logger .warning (
385+ f"[{ tracking_id } ] The 'in' operator for attribute { attribute } in authenticator map { map_id } "
386+ f"must use an array, not { type (condition ['in' ]).__name__ } . This condition will be ignored."
387+ )
388+ return False
389+
390+ return True
391+
392+
393+ def _prepare_case_insensitive_data (trigger_condition : dict , attributes : dict , map_id : int , tracking_id : str ) -> tuple [dict , dict ]:
394+ """
395+ Prepare trigger conditions and attributes for case-insensitive comparison if enabled.
396+
397+ Args:
398+ trigger_condition: Original trigger conditions
399+ attributes: Original user attributes
400+ map_id: Authenticator map ID for logging
401+ tracking_id: Tracking ID for logging
402+
403+ Returns:
404+ Tuple of (processed_trigger_condition, processed_attributes)
310405 """
311406 if _is_case_insensitivity_enabled ():
312407 _prefixed_debug (map_id , tracking_id , f"[{ tracking_id } ] Case insensitivity enabled, converting attributes and values to lowercase" )
313408 attributes = {f"{ k } " .casefold (): v for k , v in attributes .items ()}
314409 trigger_condition = _lowercase_attr_triggers (trigger_condition )
315410
411+ return trigger_condition , attributes
412+
413+
414+ def _normalize_user_value (user_value ):
415+ """
416+ Normalize user value to a list format for consistent processing.
417+
418+ Args:
419+ user_value: The user attribute value
420+
421+ Returns:
422+ List containing the user value(s)
423+ """
424+ if type (user_value ) is not list :
425+ # If the value is a string then convert it to a list
426+ return [user_value ]
427+ return user_value
428+
429+
430+ def process_user_attributes (trigger_condition : dict , attributes : dict , map_id : int , tracking_id : str ) -> TriggerResult :
431+ """
432+ Looks at a maps trigger for an attribute and the users attributes and determines if the trigger is defined for this user.
433+ Attribute names are compared case-insensitively when FEATURE_CASE_INSENSITIVE_AUTH_MAPS is enabled.
434+ """
435+ # Prepare data for case-insensitive comparison if needed
436+ trigger_condition , attributes = _prepare_case_insensitive_data (trigger_condition , attributes , map_id , tracking_id )
437+
438+ # Extract and validate join condition
316439 has_access = None
317440 join_condition = trigger_condition .pop ('join_condition' , 'or' )
318- if join_condition not in TRIGGER_DEFINITION ['attributes' ]['keys' ]['join_condition' ]['choices' ]:
319- logger .warning (f"[{ tracking_id } ] Trigger join_condition { join_condition } on authenticator map { map_id } is invalid and will be set to 'or'" )
320- join_condition = 'or'
441+ join_condition = _validate_join_condition (join_condition , map_id , tracking_id )
321442
443+ # Process each attribute in the trigger condition
322444 for attribute in trigger_condition .keys ():
323445 # If we have already determined the result, we can break out and return
324446 if _check_early_exit (has_access , join_condition , map_id , tracking_id ):
325447 break
326448
327- # Warn if there are any invalid conditions, we are just going to ignore them
328- invalid_conditions = set (trigger_condition [attribute ].keys ()) - set (TRIGGER_DEFINITION ['attributes' ]['keys' ]['*' ]['keys' ].keys ())
329- if invalid_conditions :
330- logger .warning (
331- f"[{ tracking_id } ] The conditions { ', ' .join (invalid_conditions )} for attribute { attribute } "
332- f"in authenticator map { map_id } are invalid and won't be processed"
333- )
449+ # Validate attribute conditions
450+ if not _validate_attribute_conditions (attribute , trigger_condition [attribute ], map_id , tracking_id ):
451+ continue
334452
335453 # The attribute is an empty dict we just need to see if the user has the attribute or not
336454 if trigger_condition [attribute ] == {}:
337455 has_access = has_access_with_join (has_access , _check_empty_attribute (attribute , attributes , map_id , tracking_id ), join_condition )
338456 continue
339457
458+ # Check if user has the attribute
340459 user_value = attributes .get (attribute , None )
341- # If the user does not contain the attribute then we can't check any further, don't set has_access and just continue
342460 if user_value is None :
343461 _prefixed_debug (map_id , tracking_id , f"Attr [{ attribute } ] is not present in user attributes, skipping" )
344462 continue
345463
346- if type (user_value ) is not list :
347- # If the value is a string then convert it to a list
348- user_value = [user_value ]
349-
464+ # Normalize user value and process
465+ user_value = _normalize_user_value (user_value )
350466 has_access = _process_user_value (has_access , trigger_condition , user_value , join_condition , attribute , map_id , tracking_id )
351467
352468 return TriggerResult .ALLOW if has_access else TriggerResult .SKIP
@@ -391,7 +507,7 @@ def _evaluate_ends_with(user_value: str, trigger_value: str) -> bool:
391507 return user_value .endswith (trigger_value )
392508
393509
394- def _evaluate_in (user_value : str , trigger_value : str ) -> bool :
510+ def _evaluate_in (user_value : str , trigger_value : list ) -> bool :
395511 """Check if user value is in trigger value list."""
396512 return user_value in trigger_value
397513
0 commit comments