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

1"""DRF API serializers for the 'users' app.""" 

2 

3import secrets 

4import string 

5 

6from django.contrib.auth.models import Group, Permission, User 

7from django.db.models import Q 

8from django.utils.translation import gettext_lazy as _ 

9 

10from rest_framework import serializers 

11from rest_framework.exceptions import PermissionDenied 

12 

13from InvenTree.serializers import ( 

14 FilterableSerializerMixin, 

15 InvenTreeModelSerializer, 

16 OptionalField, 

17) 

18 

19from .models import ApiToken, Owner, RuleSet, UserProfile 

20from .permissions import check_user_role, prefetch_rule_sets 

21from .ruleset import RULESET_CHOICES, RULESET_PERMISSIONS, RuleSetEnum 

22 

23 

24class OwnerSerializer(InvenTreeModelSerializer): 

25 """Serializer for an "Owner" (either a "user" or a "group").""" 

26 

27 class Meta: 

28 """Metaclass defines serializer fields.""" 

29 

30 model = Owner 

31 fields = ['pk', 'owner_id', 'owner_model', 'name', 'label'] 

32 

33 name = serializers.CharField(read_only=True) 

34 owner_model = serializers.CharField(read_only=True, source='owner._meta.model_name') 

35 

36 label = serializers.CharField(read_only=True) 

37 

38 

39class RuleSetSerializer(InvenTreeModelSerializer): 

40 """Serializer for a RuleSet.""" 

41 

42 class Meta: 

43 """Metaclass defines serializer fields.""" 

44 

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'] 

57 

58 

59class RoleSerializer(InvenTreeModelSerializer): 

60 """Serializer for a roles associated with a given user.""" 

61 

62 class Meta: 

63 """Metaclass options.""" 

64 

65 model = User 

66 fields = [ 

67 'user', 

68 'username', 

69 'roles', 

70 'permissions', 

71 'is_staff', 

72 'is_superuser', 

73 ] 

74 

75 user = serializers.IntegerField(source='pk') 

76 roles = serializers.SerializerMethodField() 

77 permissions = serializers.SerializerMethodField(allow_null=True) 

78 

79 def get_roles(self, user: User) -> dict: 

80 """Roles associated with the user.""" 

81 roles = {} 

82 

83 # Cache the 'groups' queryset for the user 

84 groups = prefetch_rule_sets(user) 

85 

86 for ruleset in RULESET_CHOICES: 

87 role, _text = ruleset 

88 

89 permissions = [] 

90 

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) 

94 

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 

99 

100 return roles 

101 

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() 

110 

111 return generate_permission_dict(permissions) 

112 

113 

114def generate_permission_dict(permissions) -> dict: 

115 """Generate a dictionary of permissions for a given set of permissions.""" 

116 perms = {} 

117 

118 for permission in permissions: 

119 perm, model = permission.codename.split('_') 

120 

121 if model not in perms: 

122 perms[model] = [] 

123 

124 perms[model].append(perm) 

125 return perms 

126 

127 

128class GetAuthTokenSerializer(serializers.Serializer): 

129 """Serializer for the GetAuthToken API endpoint.""" 

130 

131 class Meta: 

132 """Meta options for GetAuthTokenSerializer.""" 

133 

134 model = ApiToken 

135 fields = ['token', 'name', 'expiry'] 

136 

137 token = serializers.CharField(read_only=True) 

138 name = serializers.CharField() 

139 expiry = serializers.DateField(read_only=True) 

140 

141 

142class BriefUserProfileSerializer(InvenTreeModelSerializer): 

143 """Brief serializer for the UserProfile model.""" 

144 

145 class Meta: 

146 """Meta options for BriefUserProfileSerializer.""" 

147 

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 ] 

160 

161 

162class UserProfileSerializer(BriefUserProfileSerializer): 

163 """Serializer for the UserProfile model.""" 

164 

165 class Meta(BriefUserProfileSerializer.Meta): 

166 """Meta options for UserProfileSerializer.""" 

167 

168 fields = [ 

169 'language', 

170 'theme', 

171 'widgets', 

172 *BriefUserProfileSerializer.Meta.fields, 

173 ] 

174 

175 

176class UserSerializer(InvenTreeModelSerializer): 

177 """Serializer for a User.""" 

178 

179 class Meta: 

180 """Metaclass defines serializer fields.""" 

181 

182 model = User 

183 fields = ['pk', 'username', 'first_name', 'last_name', 'email'] 

184 read_only_fields = ['username', 'email'] 

185 

186 username = serializers.CharField(label=_('Username'), help_text=_('Username')) 

187 

188 first_name = serializers.CharField( 

189 label=_('First Name'), help_text=_('First name of the user'), allow_blank=True 

190 ) 

191 

192 last_name = serializers.CharField( 

193 label=_('Last Name'), help_text=_('Last name of the user'), allow_blank=True 

194 ) 

195 

196 email = serializers.EmailField( 

197 label=_('Email'), help_text=_('Email address of the user'), allow_blank=True 

198 ) 

199 

200 

201class ApiTokenSerializer(InvenTreeModelSerializer): 

202 """Serializer for the ApiToken model.""" 

203 

204 in_use = serializers.SerializerMethodField(read_only=True) 

205 user = serializers.PrimaryKeyRelatedField( 

206 queryset=User.objects.all(), required=False 

207 ) 

208 

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 

212 

213 request = self.context.get('request') 

214 rq_token = get_token_from_request(request) 

215 return token.key == rq_token 

216 

217 class Meta: 

218 """Meta options for ApiTokenSerializer.""" 

219 

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 ] 

234 

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 

242 

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 

245 

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 ) 

251 

252 return super().validate(data) 

253 

254 user_detail = UserSerializer(source='user', read_only=True) 

255 

256 

257class GroupSerializer(FilterableSerializerMixin, InvenTreeModelSerializer): 

258 """Serializer for a 'Group'.""" 

259 

260 class Meta: 

261 """Metaclass defines serializer fields.""" 

262 

263 model = Group 

264 fields = ['pk', 'name', 'permissions', 'roles', 'users'] 

265 

266 permissions = OptionalField( 

267 serializer_class=serializers.SerializerMethodField, 

268 serializer_kwargs={'allow_null': True, 'read_only': True}, 

269 filter_name='permission_detail', 

270 ) 

271 

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()) 

275 

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 ) 

287 

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 ) 

299 

300 

301class ExtendedUserSerializer(UserSerializer): 

302 """Serializer for a User with a bit more info.""" 

303 

304 # from users.serializers import GroupSerializer 

305 

306 class Meta(UserSerializer.Meta): 

307 """Metaclass defines serializer fields.""" 

308 

309 fields = [ 

310 *UserSerializer.Meta.fields, 

311 'groups', 

312 'group_ids', 

313 'is_staff', 

314 'is_superuser', 

315 'is_active', 

316 'profile', 

317 ] 

318 

319 read_only_fields = [*UserSerializer.Meta.read_only_fields, 'groups'] 

320 

321 groups = GroupSerializer(many=True, read_only=True) 

322 

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 ) 

327 

328 is_staff = serializers.BooleanField( 

329 label=_('Administrator'), 

330 help_text=_('Does this user have administrative permissions'), 

331 required=False, 

332 ) 

333 

334 is_superuser = serializers.BooleanField( 

335 label=_('Superuser'), help_text=_('Is this user a superuser'), required=False 

336 ) 

337 

338 is_active = serializers.BooleanField( 

339 label=_('Active'), help_text=_('Is this user account active'), required=False 

340 ) 

341 

342 profile = BriefUserProfileSerializer(many=False, read_only=True) 

343 

344 def validate_is_superuser(self, value): 

345 """Only a superuser account can adjust this value!""" 

346 request_user = self.context['request'].user 

347 

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 }) 

353 

354 return value 

355 

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) 

360 

361 instance = super().update(instance, validated_data) 

362 

363 if groups is not None: 

364 instance.groups.set(groups) 

365 

366 return instance 

367 

368 

369class UserSetPasswordSerializer(serializers.Serializer): 

370 """Serializer for setting a password for a user.""" 

371 

372 class Meta: 

373 """Meta options for UserSetPasswordSerializer.""" 

374 

375 model = User 

376 fields = ['password', 'override_warning'] 

377 

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 ) 

391 

392 

393class MeUserSerializer(ExtendedUserSerializer): 

394 """API serializer specifically for the 'me' endpoint.""" 

395 

396 class Meta(ExtendedUserSerializer.Meta): 

397 """Metaclass options. 

398 

399 Extends the ExtendedUserSerializer.Meta options, 

400 but ensures that certain fields are read-only. 

401 """ 

402 

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'] 

405 

406 read_only_fields = [ 

407 *ExtendedUserSerializer.Meta.read_only_fields, 

408 'is_active', 

409 'is_staff', 

410 'is_superuser', 

411 ] 

412 

413 profile = UserProfileSerializer(many=False, read_only=True) 

414 

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 ) 

422 

423 is_superuser = serializers.BooleanField( 

424 label=_('Superuser'), 

425 help_text=_('Is this user a superuser'), 

426 required=False, 

427 read_only=True, 

428 ) 

429 

430 is_active = serializers.BooleanField( 

431 label=_('Active'), 

432 help_text=_('Is this user account active'), 

433 required=False, 

434 read_only=True, 

435 ) 

436 

437 

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 

450 

451 

452class UserCreateSerializer(ExtendedUserSerializer): 

453 """Serializer for creating a new User.""" 

454 

455 class Meta(ExtendedUserSerializer.Meta): 

456 """Metaclass options for the UserCreateSerializer.""" 

457 

458 # Prevent creation of users with superuser or staff permissions 

459 read_only_fields = ['groups', 'is_staff', 'is_superuser'] 

460 

461 def validate(self, attrs): 

462 """Expanded valiadation for auth.""" 

463 user = self.context['request'].user 

464 

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 ) 

470 

471 # Generate a random password 

472 password = make_random_password(length=14) 

473 attrs.update({'password': password}) 

474 return super().validate(attrs) 

475 

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 

480 

481 base_url = get_base_url() 

482 

483 instance = super().create(validated_data) 

484 

485 # Make sure the user cannot login until they have set a password 

486 instance.set_unusable_password() 

487 

488 message = ( 

489 _('Your account has been created.') 

490 + '\n\n' 

491 + _('Please use the password reset function to login') 

492 ) 

493 

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}' 

496 

497 subject = _('Welcome to InvenTree') 

498 

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 ) 

503 

504 return instance