Coverage for users/utils.py: 21%
37 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« 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
5__all__ = (
6 'clean_username',
7 'get_current_pepper',
8 'user_may_grant_token',
9)
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.
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.
24 Field remapping rules:
25 ``user__<field>`` → ``<field>`` (FK traversal into User)
26 ``user`` → ``pk`` (Token.user FK becomes User.pk)
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
33 # Mirrors ObjectPermissionMixin.has_perm: superusers implicitly have all permissions.
34 if requesting_user.is_active and requesting_user.is_superuser:
35 return True
37 perm = 'users.grant_token'
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
44 constraints = getattr(requesting_user, '_object_perm_cache', {}).get(perm, [])
46 # An empty/null constraint means "no restriction" — allow any target.
47 if any(not c for c in constraints):
48 return True
50 # Substitute the $user token so {"user": "$user"} resolves to the requesting user.
51 resolved_user_id = requesting_user.pk
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)
68 return User.objects.filter(q, pk=token_user.pk).exists()
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
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]