Coverage for app/venv/lib/python3.14/site-packages/weblate/auth/utils.py: 70%

82 statements  

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

1# Copyright © Michal Čihař <michal@weblate.org> 

2# 

3# SPDX-License-Identifier: GPL-3.0-or-later 

4 

5from __future__ import annotations 

6 

7from email.errors import HeaderDefect 

8from email.headerregistry import Address 

9from typing import TYPE_CHECKING 

10 

11from django.conf import settings 

12from django.contrib.auth.hashers import make_password 

13from django.db import IntegrityError 

14from social_core.backends.utils import load_backends 

15 

16from weblate.auth.data import ( 

17 GLOBAL_PERM_NAMES, 

18 GLOBAL_PERMISSIONS, 

19 GROUPS, 

20 PERMISSION_NAMES, 

21 PERMISSIONS, 

22 ROLES, 

23 SELECTION_ALL, 

24) 

25 

26if TYPE_CHECKING: 26 ↛ 27line 26 didn't jump to line 27 because the condition on line 26 was never true

27 from weblate.auth.models import Group, Permission, Role 

28 

29 

30def get_auth_backends(): 

31 return load_backends(settings.AUTHENTICATION_BACKENDS) 

32 

33 

34def get_auth_keys() -> set[str]: 

35 return set(get_auth_backends().keys()) 

36 

37 

38def is_django_permission(permission: str): 

39 """ 

40 Check whether permission looks is a Django one. 

41 

42 This is purely based on the list of permissions defined in Weblate. 

43 """ 

44 return ( 

45 permission not in PERMISSION_NAMES 

46 and permission not in GLOBAL_PERM_NAMES 

47 and not permission.startswith(("meta:", "billing:")) 

48 ) 

49 

50 

51def migrate_permissions_list( 

52 model: type[Permission], permissions: tuple[tuple[str, str], ...] 

53) -> set[int]: 

54 ids = set() 

55 # Get all existing permissions 

56 existing_objects = model.objects.filter( 

57 codename__in=[perm[0] for perm in permissions] 

58 ) 

59 existing = {permission.codename: permission for permission in existing_objects} 

60 

61 # Iterate over expected permissions 

62 for code, name in permissions: 

63 try: 

64 instance = existing[code] 

65 except KeyError: 

66 # Missing, create one 

67 instance = model.objects.create(codename=code, name=name) 

68 else: 

69 # Update if needed 

70 if instance.name != name: 

71 instance.name = name 

72 instance.save(update_fields=["name"]) 

73 ids.add(instance.pk) 

74 return ids 

75 

76 

77def migrate_permissions(model: type[Permission]) -> None: 

78 """Create permissions as defined in the data.""" 

79 ids: set[int] = set() 

80 # Per object permissions 

81 ids.update(migrate_permissions_list(model, PERMISSIONS)) 

82 # Global permissions 

83 ids.update(migrate_permissions_list(model, GLOBAL_PERMISSIONS)) 

84 # Delete stale permissions 

85 model.objects.exclude(id__in=ids).delete() 

86 

87 

88def migrate_roles(model: type[Role], perm_model: type[Permission]) -> set[str]: 

89 """Create roles as defined in the data.""" 

90 result = set() 

91 existing: dict[str, Role] = {obj.name: obj for obj in model.objects.all()} 

92 for role, permissions in ROLES: 

93 if role in existing: 93 ↛ 94line 93 didn't jump to line 94 because the condition on line 93 was never true

94 instance = existing[role] 

95 else: 

96 instance = model.objects.create(name=role) 

97 result.add(role) 

98 instance.permissions.set( 

99 perm_model.objects.filter(codename__in=permissions), clear=True 

100 ) 

101 return result 

102 

103 

104def migrate_groups( 

105 model: type[Group], role_model: type[Role], update: bool = False 

106) -> dict[str, Group]: 

107 """Create groups as defined in the data.""" 

108 result: dict[str, Group] = { 

109 obj.name: obj 

110 for obj in model.objects.filter(internal=True, defining_project=None) 

111 } 

112 for group, roles, selection in GROUPS: 

113 if group in result: 113 ↛ 114line 113 didn't jump to line 114 because the condition on line 113 was never true

114 instance = result[group] 

115 created = False 

116 if update and ( 

117 instance.project_selection != selection 

118 or instance.language_selection != SELECTION_ALL 

119 ): 

120 instance.project_selection = selection 

121 instance.language_selection = SELECTION_ALL 

122 instance.save(update_fields=["project_selection", "language_selection"]) 

123 else: 

124 instance = model.objects.create( 

125 name=group, 

126 internal=True, 

127 defining_project=None, 

128 project_selection=selection, 

129 language_selection=SELECTION_ALL, 

130 ) 

131 created = True 

132 result[group] = instance 

133 if created or update: 133 ↛ 112line 133 didn't jump to line 112 because the condition on line 133 was always true

134 instance.roles.set(role_model.objects.filter(name__in=roles), clear=True) 

135 return result 

136 

137 

138def create_anonymous(model, group_model, update=True) -> None: 

139 try: 

140 user, created = model.objects.get_or_create( 

141 username=settings.ANONYMOUS_USER_NAME, 

142 defaults={ 

143 "full_name": "Anonymous", 

144 "email": "noreply@weblate.org", 

145 "is_active": False, 

146 "password": make_password(None), 

147 }, 

148 ) 

149 except IntegrityError as error: 

150 msg = ( 

151 f"Anonymous user ({settings.ANONYMOUS_USER_NAME}) and could not be created, " 

152 f"most likely because other user is using noreply@weblate.org e-mail.: {error}" 

153 ) 

154 raise ValueError(msg) from error 

155 if user.is_active: 155 ↛ 156line 155 didn't jump to line 156 because the condition on line 155 was never true

156 msg = ( 

157 f"Anonymous user ({settings.ANONYMOUS_USER_NAME}) already exists and is " 

158 "active, please change the ANONYMOUS_USER_NAME setting or mark the user " 

159 "as not active in the admin interface." 

160 ) 

161 raise ValueError(msg) 

162 

163 if created or update: 163 ↛ exitline 163 didn't return from function 'create_anonymous' because the condition on line 163 was always true

164 user.set_unusable_password() 

165 user.save() 

166 user.groups.set( 

167 group_model.objects.filter(name__in=("Guests", "Viewers")), clear=True 

168 ) 

169 

170 

171def format_address(display_name: str, email: str) -> str: 

172 """Format e-mail address with display name.""" 

173 # While Address does quote the name following RFC 5322, 

174 # git still doesn't like <> being used in the string 

175 try: 

176 address = Address( 

177 display_name=display_name.replace("<", "").replace(">", ""), 

178 addr_spec=email, 

179 ) 

180 except HeaderDefect as error: 

181 msg = f"Invalid e-mail address '{email}': {error}" 

182 raise ValueError(msg) from error 

183 return str(address)