Coverage for src/backend/InvenTree/users/serializers.py: 89%
174 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
1"""DRF API serializers for the 'users' app."""
3import secrets
4import string
6from django.contrib.auth.models import Group, Permission, User
7from django.db.models import Q
8from django.utils.translation import gettext_lazy as _
10from rest_framework import serializers
11from rest_framework.exceptions import PermissionDenied
13from InvenTree.serializers import (
14 FilterableSerializerMixin,
15 InvenTreeModelSerializer,
16 OptionalField,
17)
19from .models import ApiToken, Owner, RuleSet, UserProfile
20from .permissions import check_user_role, prefetch_rule_sets
21from .ruleset import RULESET_CHOICES, RULESET_PERMISSIONS, RuleSetEnum
24class OwnerSerializer(InvenTreeModelSerializer):
25 """Serializer for an "Owner" (either a "user" or a "group")."""
27 class Meta:
28 """Metaclass defines serializer fields."""
30 model = Owner
31 fields = ['pk', 'owner_id', 'owner_model', 'name', 'label']
33 name = serializers.CharField(read_only=True)
34 owner_model = serializers.CharField(read_only=True, source='owner._meta.model_name')
36 label = serializers.CharField(read_only=True)
39class RuleSetSerializer(InvenTreeModelSerializer):
40 """Serializer for a RuleSet."""
42 class Meta:
43 """Metaclass defines serializer fields."""
45 model = RuleSet
46 fields = [
47 'pk',
48 'name',
49 'label',
50 'group',
51 'can_view',
52 'can_add',
53 'can_change',
54 'can_delete',
55 ]
56 read_only_fields = ['pk', 'name', 'label', 'group']
59class RoleSerializer(InvenTreeModelSerializer):
60 """Serializer for a roles associated with a given user."""
62 class Meta:
63 """Metaclass options."""
65 model = User
66 fields = [
67 'user',
68 'username',
69 'roles',
70 'permissions',
71 'is_staff',
72 'is_superuser',
73 ]
75 user = serializers.IntegerField(source='pk')
76 roles = serializers.SerializerMethodField()
77 permissions = serializers.SerializerMethodField(allow_null=True)
79 def get_roles(self, user: User) -> dict:
80 """Roles associated with the user."""
81 roles = {}
83 # Cache the 'groups' queryset for the user
84 groups = prefetch_rule_sets(user)
86 for ruleset in RULESET_CHOICES:
87 role, _text = ruleset
89 permissions = []
91 for permission in RULESET_PERMISSIONS:
92 if check_user_role(user, role, permission, groups=groups): 92 ↛ 91line 92 didn't jump to line 91 because the condition on line 92 was always true
93 permissions.append(permission)
95 if len(permissions) > 0: 95 ↛ 98line 95 didn't jump to line 98 because the condition on line 95 was always true
96 roles[role] = permissions
97 else:
98 roles[role] = None # pragma: no cover
100 return roles
102 def get_permissions(self, user: User) -> dict:
103 """Permissions associated with the user."""
104 if user.is_superuser: 104 ↛ 107line 104 didn't jump to line 107 because the condition on line 104 was always true
105 permissions = Permission.objects.all()
106 else:
107 permissions = Permission.objects.filter(
108 Q(user=user) | Q(group__user=user)
109 ).distinct()
111 return generate_permission_dict(permissions)
114def generate_permission_dict(permissions) -> dict:
115 """Generate a dictionary of permissions for a given set of permissions."""
116 perms = {}
118 for permission in permissions:
119 perm, model = permission.codename.split('_')
121 if model not in perms:
122 perms[model] = []
124 perms[model].append(perm)
125 return perms
128class GetAuthTokenSerializer(serializers.Serializer):
129 """Serializer for the GetAuthToken API endpoint."""
131 class Meta:
132 """Meta options for GetAuthTokenSerializer."""
134 model = ApiToken
135 fields = ['token', 'name', 'expiry']
137 token = serializers.CharField(read_only=True)
138 name = serializers.CharField()
139 expiry = serializers.DateField(read_only=True)
142class BriefUserProfileSerializer(InvenTreeModelSerializer):
143 """Brief serializer for the UserProfile model."""
145 class Meta:
146 """Meta options for BriefUserProfileSerializer."""
148 model = UserProfile
149 fields = [
150 'displayname',
151 'position',
152 'status',
153 'location',
154 'active',
155 'contact',
156 'type',
157 'organisation',
158 'primary_group',
159 ]
162class UserProfileSerializer(BriefUserProfileSerializer):
163 """Serializer for the UserProfile model."""
165 class Meta(BriefUserProfileSerializer.Meta):
166 """Meta options for UserProfileSerializer."""
168 fields = [
169 'language',
170 'theme',
171 'widgets',
172 *BriefUserProfileSerializer.Meta.fields,
173 ]
176class UserSerializer(InvenTreeModelSerializer):
177 """Serializer for a User."""
179 class Meta:
180 """Metaclass defines serializer fields."""
182 model = User
183 fields = ['pk', 'username', 'first_name', 'last_name', 'email']
184 read_only_fields = ['username', 'email']
186 username = serializers.CharField(label=_('Username'), help_text=_('Username'))
188 first_name = serializers.CharField(
189 label=_('First Name'), help_text=_('First name of the user'), allow_blank=True
190 )
192 last_name = serializers.CharField(
193 label=_('Last Name'), help_text=_('Last name of the user'), allow_blank=True
194 )
196 email = serializers.EmailField(
197 label=_('Email'), help_text=_('Email address of the user'), allow_blank=True
198 )
201class ApiTokenSerializer(InvenTreeModelSerializer):
202 """Serializer for the ApiToken model."""
204 in_use = serializers.SerializerMethodField(read_only=True)
205 user = serializers.PrimaryKeyRelatedField(
206 queryset=User.objects.all(), required=False
207 )
209 def get_in_use(self, token: ApiToken) -> bool:
210 """Return True if the token is currently used to call the endpoint."""
211 from InvenTree.middleware import get_token_from_request
213 request = self.context.get('request')
214 rq_token = get_token_from_request(request)
215 return token.key == rq_token
217 class Meta:
218 """Meta options for ApiTokenSerializer."""
220 model = ApiToken
221 fields = [
222 'created',
223 'expiry',
224 'id',
225 'last_seen',
226 'name',
227 'token',
228 'active',
229 'revoked',
230 'user',
231 'user_detail',
232 'in_use',
233 ]
235 def validate(self, data):
236 """Validate the data for the serializer."""
237 request_user = self.context['request'].user
238 if not request_user: 238 ↛ 239line 238 didn't jump to line 239 because the condition on line 238 was never true
239 raise serializers.ValidationError(
240 _('User must be authenticated')
241 ) # pragma: no cover
243 if 'user' not in data: 243 ↛ 247line 243 didn't jump to line 247 because the condition on line 243 was always true
244 data['user'] = request_user
246 # Only superusers can create tokens for other users
247 if data['user'] != request_user and not request_user.is_superuser: 247 ↛ 248line 247 didn't jump to line 248 because the condition on line 247 was never true
248 raise serializers.ValidationError(
249 _('Only a superuser can create a token for another user')
250 )
252 return super().validate(data)
254 user_detail = UserSerializer(source='user', read_only=True)
257class GroupSerializer(FilterableSerializerMixin, InvenTreeModelSerializer):
258 """Serializer for a 'Group'."""
260 class Meta:
261 """Metaclass defines serializer fields."""
263 model = Group
264 fields = ['pk', 'name', 'permissions', 'roles', 'users']
266 permissions = OptionalField(
267 serializer_class=serializers.SerializerMethodField,
268 serializer_kwargs={'allow_null': True, 'read_only': True},
269 filter_name='permission_detail',
270 )
272 def get_permissions(self, group: Group) -> dict:
273 """Return a list of permissions associated with the group."""
274 return generate_permission_dict(group.permissions.all())
276 roles = OptionalField(
277 serializer_class=RuleSetSerializer,
278 serializer_kwargs={
279 'source': 'rule_sets',
280 'many': True,
281 'read_only': True,
282 'allow_null': True,
283 },
284 filter_name='role_detail',
285 prefetch_fields=['rule_sets'],
286 )
288 users = OptionalField(
289 serializer_class=UserSerializer,
290 serializer_kwargs={
291 'source': 'user_set',
292 'many': True,
293 'read_only': True,
294 'allow_null': True,
295 },
296 filter_name='user_detail',
297 prefetch_fields=['user_set'],
298 )
301class ExtendedUserSerializer(UserSerializer):
302 """Serializer for a User with a bit more info."""
304 # from users.serializers import GroupSerializer
306 class Meta(UserSerializer.Meta):
307 """Metaclass defines serializer fields."""
309 fields = [
310 *UserSerializer.Meta.fields,
311 'groups',
312 'group_ids',
313 'is_staff',
314 'is_superuser',
315 'is_active',
316 'profile',
317 ]
319 read_only_fields = [*UserSerializer.Meta.read_only_fields, 'groups']
321 groups = GroupSerializer(many=True, read_only=True)
323 # Write-only field, for updating the groups associated with the user
324 group_ids = serializers.PrimaryKeyRelatedField(
325 queryset=Group.objects.all(), many=True, write_only=True, required=False
326 )
328 is_staff = serializers.BooleanField(
329 label=_('Administrator'),
330 help_text=_('Does this user have administrative permissions'),
331 required=False,
332 )
334 is_superuser = serializers.BooleanField(
335 label=_('Superuser'), help_text=_('Is this user a superuser'), required=False
336 )
338 is_active = serializers.BooleanField(
339 label=_('Active'), help_text=_('Is this user account active'), required=False
340 )
342 profile = BriefUserProfileSerializer(many=False, read_only=True)
344 def validate_is_superuser(self, value):
345 """Only a superuser account can adjust this value!"""
346 request_user = self.context['request'].user
348 if 'is_superuser' in self.context['request'].data:
349 if not request_user.is_superuser: 349 ↛ 350line 349 didn't jump to line 350 because the condition on line 349 was never true
350 raise PermissionDenied({
351 'is_superuser': _('Only a superuser can adjust this field')
352 })
354 return value
356 def update(self, instance, validated_data):
357 """Update the user instance with the provided data."""
358 # Update the groups associated with the user
359 groups = validated_data.pop('group_ids', None)
361 instance = super().update(instance, validated_data)
363 if groups is not None:
364 instance.groups.set(groups)
366 return instance
369class UserSetPasswordSerializer(serializers.Serializer):
370 """Serializer for setting a password for a user."""
372 class Meta:
373 """Meta options for UserSetPasswordSerializer."""
375 model = User
376 fields = ['password', 'override_warning']
378 password = serializers.CharField(
379 label=_('Password'),
380 help_text=_('Password for the user'),
381 write_only=True,
382 required=True,
383 style={'input_type': 'password'},
384 )
385 override_warning = serializers.BooleanField(
386 label=_('Override warning'),
387 help_text=_('Override the warning about password rules'),
388 write_only=True,
389 required=False,
390 )
393class MeUserSerializer(ExtendedUserSerializer):
394 """API serializer specifically for the 'me' endpoint."""
396 class Meta(ExtendedUserSerializer.Meta):
397 """Metaclass options.
399 Extends the ExtendedUserSerializer.Meta options,
400 but ensures that certain fields are read-only.
401 """
403 # Remove the 'group_ids' field, as this is not relevant for the 'me' endpoint
404 fields = [f for f in ExtendedUserSerializer.Meta.fields if f != 'group_ids']
406 read_only_fields = [
407 *ExtendedUserSerializer.Meta.read_only_fields,
408 'is_active',
409 'is_staff',
410 'is_superuser',
411 ]
413 profile = UserProfileSerializer(many=False, read_only=True)
415 # Redefine the fields from ExtendedUserSerializer, to ensure they are marked as read-only
416 is_staff = serializers.BooleanField(
417 label=_('Staff'),
418 help_text=_('Does this user have staff permissions'),
419 required=False,
420 read_only=True,
421 )
423 is_superuser = serializers.BooleanField(
424 label=_('Superuser'),
425 help_text=_('Is this user a superuser'),
426 required=False,
427 read_only=True,
428 )
430 is_active = serializers.BooleanField(
431 label=_('Active'),
432 help_text=_('Is this user account active'),
433 required=False,
434 read_only=True,
435 )
438def make_random_password(length=14):
439 """Generate a random password of given length."""
440 alphabet = string.ascii_letters + string.digits
441 while True:
442 password = ''.join(secrets.choice(alphabet) for i in range(length))
443 if (
444 any(c.islower() for c in password)
445 and any(c.isupper() for c in password)
446 and sum(c.isdigit() for c in password) >= 3
447 ):
448 break
449 return password
452class UserCreateSerializer(ExtendedUserSerializer):
453 """Serializer for creating a new User."""
455 class Meta(ExtendedUserSerializer.Meta):
456 """Metaclass options for the UserCreateSerializer."""
458 # Prevent creation of users with superuser or staff permissions
459 read_only_fields = ['groups', 'is_staff', 'is_superuser']
461 def validate(self, attrs):
462 """Expanded valiadation for auth."""
463 user = self.context['request'].user
465 # Check that the user trying to create a new user is a superuser
466 if not user.is_staff or not check_user_role(user, RuleSetEnum.ADMIN, 'add'): 466 ↛ 467line 466 didn't jump to line 467 because the condition on line 466 was never true
467 raise serializers.ValidationError( # pragma: no cover # Handled by permissions already
468 _('You do not have permission to create users')
469 )
471 # Generate a random password
472 password = make_random_password(length=14)
473 attrs.update({'password': password})
474 return super().validate(attrs)
476 def create(self, validated_data):
477 """Send an e email to the user after creation."""
478 from InvenTree.helpers_model import get_base_url
479 from InvenTree.tasks import email_user, offload_task
481 base_url = get_base_url()
483 instance = super().create(validated_data)
485 # Make sure the user cannot login until they have set a password
486 instance.set_unusable_password()
488 message = (
489 _('Your account has been created.')
490 + '\n\n'
491 + _('Please use the password reset function to login')
492 )
494 if base_url: 494 ↛ 497line 494 didn't jump to line 497 because the condition on line 494 was always true
495 message += f'\n\nURL: {base_url}'
497 subject = _('Welcome to InvenTree')
499 # Send the user an onboarding email (from current site)
500 offload_task(
501 email_user, instance.pk, str(subject), str(message), force_async=True
502 )
504 return instance