Coverage for users/utils.py: 21%

37 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 18:35 +0000

1from django.conf import settings 

2from django.db.models import Q 

3from social_core.storage import NO_ASCII_REGEX, NO_SPECIAL_REGEX 

4 

5__all__ = ( 

6 'clean_username', 

7 'get_current_pepper', 

8 'user_may_grant_token', 

9) 

10 

11 

12def user_may_grant_token(requesting_user, token_user): 

13 """ 

14 Return True if *requesting_user* has permission to create a token for *token_user*, 

15 respecting ObjectPermission constraints on the users.grant_token permission. 

16 

17 ``has_perm('users.grant_token', obj=None)`` always short-circuits to True when the 

18 permission is present in the cache (obj=None bypasses constraint evaluation in 

19 ObjectPermissionMixin.has_perm). Since the new token does not yet exist in the 

20 database we cannot pass an existing Token as obj. Instead we extract the raw 

21 constraint list, remap Token-field paths to User-field paths, and evaluate them 

22 directly against the target User record — the only variable in a new token creation. 

23 

24 Field remapping rules: 

25 ``user__<field>`` → ``<field>`` (FK traversal into User) 

26 ``user`` → ``pk`` (Token.user FK becomes User.pk) 

27 

28 Constraints referencing other Token fields cannot be evaluated for an unsaved 

29 token; the function returns False (deny) for those. 

30 """ 

31 from users.models import User 

32 

33 # Mirrors ObjectPermissionMixin.has_perm: superusers implicitly have all permissions. 

34 if requesting_user.is_active and requesting_user.is_superuser: 

35 return True 

36 

37 perm = 'users.grant_token' 

38 

39 # get_all_permissions() populates _object_perm_cache as a side effect. 

40 # Guard against missing cache key in case a non-standard backend is in use. 

41 if perm not in requesting_user.get_all_permissions(): 

42 return False 

43 

44 constraints = getattr(requesting_user, '_object_perm_cache', {}).get(perm, []) 

45 

46 # An empty/null constraint means "no restriction" — allow any target. 

47 if any(not c for c in constraints): 

48 return True 

49 

50 # Substitute the $user token so {"user": "$user"} resolves to the requesting user. 

51 resolved_user_id = requesting_user.pk 

52 

53 q = Q() 

54 for constraint in constraints: 

55 user_constraint = {} 

56 for key, raw_val in constraint.items(): 

57 val = resolved_user_id if raw_val == '$user' else raw_val 

58 if key == 'user': 

59 user_constraint['pk'] = val 

60 elif key.startswith('user__'): 

61 user_constraint[key.removeprefix('user__')] = val 

62 else: 

63 # Non-user Token field — cannot evaluate for a new (unsaved) token. 

64 # Fail closed. 

65 return False 

66 q |= Q(**user_constraint) 

67 

68 return User.objects.filter(q, pk=token_user.pk).exists() 

69 

70 

71def clean_username(value): 

72 """Clean username removing any unsupported character""" 

73 value = NO_ASCII_REGEX.sub('', value) 

74 value = NO_SPECIAL_REGEX.sub('', value) 

75 value = value.replace(':', '') 

76 return value 

77 

78 

79def get_current_pepper(): 

80 """ 

81 Return the ID and value of the newest (highest ID) cryptographic pepper. 

82 """ 

83 if not settings.API_TOKEN_PEPPERS: 83 ↛ 84line 83 didn't jump to line 84 because the condition on line 83 was never true

84 raise ValueError("API_TOKEN_PEPPERS is not defined") 

85 newest_id = sorted(settings.API_TOKEN_PEPPERS.keys())[-1] 

86 return newest_id, settings.API_TOKEN_PEPPERS[newest_id]