3
3
import logging
4
4
import re
5
5
from enum import Enum , auto
6
- from typing import List , Optional , Union
6
+ from typing import Any , List , Optional , Union
7
7
from uuid import uuid4
8
8
9
9
from django .conf import settings
@@ -54,13 +54,14 @@ def create_claims(authenticator: Authenticator, username: str, attrs: dict, grou
54
54
rule_responses = []
55
55
# Assume we will have access
56
56
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 } " )
60
57
61
58
# debug tracking ID
62
59
tracking_id = str (uuid4 ())
63
60
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
+
64
65
# load the maps
65
66
maps = AuthenticatorMap .objects .filter (authenticator = authenticator .pk ).order_by ("order" )
66
67
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_
240
241
241
242
invalid_conditions = set (trigger_condition .keys ()) - set (TRIGGER_DEFINITION ['groups' ]['keys' ].keys ())
242
243
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" )
244
245
245
246
set_of_user_groups = set (groups )
246
247
@@ -283,6 +284,45 @@ def has_access_with_join(current_access: Optional[bool], new_access: bool, condi
283
284
return current_access and new_access
284
285
285
286
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
+
286
326
def _lowercase_attr_triggers (trigger_condition : dict ) -> dict :
287
327
"""
288
328
Lower case all keys (attribute names) and contained attribute values
@@ -292,61 +332,137 @@ def _lowercase_attr_triggers(trigger_condition: dict) -> dict:
292
332
if isinstance (condition , str ):
293
333
updated_condition = condition .casefold ()
294
334
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 )
299
336
else :
300
337
updated_condition = condition
301
338
302
- ci_trigger_condition [attr .casefold ()] = updated_condition # join_condition
339
+ ci_trigger_condition [attr .casefold ()] = updated_condition
303
340
return ci_trigger_condition
304
341
305
342
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 :
307
344
"""
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)
310
405
"""
311
406
if _is_case_insensitivity_enabled ():
312
407
_prefixed_debug (map_id , tracking_id , f"[{ tracking_id } ] Case insensitivity enabled, converting attributes and values to lowercase" )
313
408
attributes = {f"{ k } " .casefold (): v for k , v in attributes .items ()}
314
409
trigger_condition = _lowercase_attr_triggers (trigger_condition )
315
410
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
316
439
has_access = None
317
440
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 )
321
442
443
+ # Process each attribute in the trigger condition
322
444
for attribute in trigger_condition .keys ():
323
445
# If we have already determined the result, we can break out and return
324
446
if _check_early_exit (has_access , join_condition , map_id , tracking_id ):
325
447
break
326
448
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
334
452
335
453
# The attribute is an empty dict we just need to see if the user has the attribute or not
336
454
if trigger_condition [attribute ] == {}:
337
455
has_access = has_access_with_join (has_access , _check_empty_attribute (attribute , attributes , map_id , tracking_id ), join_condition )
338
456
continue
339
457
458
+ # Check if user has the attribute
340
459
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
342
460
if user_value is None :
343
461
_prefixed_debug (map_id , tracking_id , f"Attr [{ attribute } ] is not present in user attributes, skipping" )
344
462
continue
345
463
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 )
350
466
has_access = _process_user_value (has_access , trigger_condition , user_value , join_condition , attribute , map_id , tracking_id )
351
467
352
468
return TriggerResult .ALLOW if has_access else TriggerResult .SKIP
@@ -391,7 +507,7 @@ def _evaluate_ends_with(user_value: str, trigger_value: str) -> bool:
391
507
return user_value .endswith (trigger_value )
392
508
393
509
394
- def _evaluate_in (user_value : str , trigger_value : str ) -> bool :
510
+ def _evaluate_in (user_value : str , trigger_value : list ) -> bool :
395
511
"""Check if user value is in trigger value list."""
396
512
return user_value in trigger_value
397
513
0 commit comments