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
« 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
5from __future__ import annotations
7from email.errors import HeaderDefect
8from email.headerregistry import Address
9from typing import TYPE_CHECKING
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
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)
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
30def get_auth_backends():
31 return load_backends(settings.AUTHENTICATION_BACKENDS)
34def get_auth_keys() -> set[str]:
35 return set(get_auth_backends().keys())
38def is_django_permission(permission: str):
39 """
40 Check whether permission looks is a Django one.
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 )
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}
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
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()
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
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
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)
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 )
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)