BmorricalDjangoWorkflows 2.1.0__tar.gz → 2.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bmorricaldjangoworkflows-2.1.0 → bmorricaldjangoworkflows-2.1.2}/BmorricalDjangoWorkflows.egg-info/PKG-INFO +1 -1
- {bmorricaldjangoworkflows-2.1.0 → bmorricaldjangoworkflows-2.1.2}/PKG-INFO +1 -1
- bmorricaldjangoworkflows-2.1.2/VERSION +1 -0
- {bmorricaldjangoworkflows-2.1.0 → bmorricaldjangoworkflows-2.1.2}/django_workflows/users/services/auth_flow.py +241 -46
- {bmorricaldjangoworkflows-2.1.0 → bmorricaldjangoworkflows-2.1.2}/django_workflows/users/urls.py +2 -0
- {bmorricaldjangoworkflows-2.1.0 → bmorricaldjangoworkflows-2.1.2}/django_workflows/users/views.py +121 -7
- {bmorricaldjangoworkflows-2.1.0 → bmorricaldjangoworkflows-2.1.2}/tests/test_auth_flow.py +646 -41
- bmorricaldjangoworkflows-2.1.2/tests/test_user_views.py +666 -0
- bmorricaldjangoworkflows-2.1.0/VERSION +0 -1
- bmorricaldjangoworkflows-2.1.0/tests/test_user_views.py +0 -358
- {bmorricaldjangoworkflows-2.1.0 → bmorricaldjangoworkflows-2.1.2}/BmorricalDjangoWorkflows.egg-info/SOURCES.txt +0 -0
- {bmorricaldjangoworkflows-2.1.0 → bmorricaldjangoworkflows-2.1.2}/BmorricalDjangoWorkflows.egg-info/dependency_links.txt +0 -0
- {bmorricaldjangoworkflows-2.1.0 → bmorricaldjangoworkflows-2.1.2}/BmorricalDjangoWorkflows.egg-info/requires.txt +0 -0
- {bmorricaldjangoworkflows-2.1.0 → bmorricaldjangoworkflows-2.1.2}/BmorricalDjangoWorkflows.egg-info/top_level.txt +0 -0
- {bmorricaldjangoworkflows-2.1.0 → bmorricaldjangoworkflows-2.1.2}/LICENSE +0 -0
- {bmorricaldjangoworkflows-2.1.0 → bmorricaldjangoworkflows-2.1.2}/MANIFEST.in +0 -0
- {bmorricaldjangoworkflows-2.1.0 → bmorricaldjangoworkflows-2.1.2}/README.md +0 -0
- {bmorricaldjangoworkflows-2.1.0 → bmorricaldjangoworkflows-2.1.2}/django_workflows/__init__.py +0 -0
- {bmorricaldjangoworkflows-2.1.0 → bmorricaldjangoworkflows-2.1.2}/django_workflows/services/__init__.py +0 -0
- {bmorricaldjangoworkflows-2.1.0 → bmorricaldjangoworkflows-2.1.2}/django_workflows/services/email.py +0 -0
- {bmorricaldjangoworkflows-2.1.0 → bmorricaldjangoworkflows-2.1.2}/django_workflows/users/__init__.py +0 -0
- {bmorricaldjangoworkflows-2.1.0 → bmorricaldjangoworkflows-2.1.2}/django_workflows/users/services/__init__.py +0 -0
- {bmorricaldjangoworkflows-2.1.0 → bmorricaldjangoworkflows-2.1.2}/setup.cfg +0 -0
- {bmorricaldjangoworkflows-2.1.0 → bmorricaldjangoworkflows-2.1.2}/setup.py +0 -0
- {bmorricaldjangoworkflows-2.1.0 → bmorricaldjangoworkflows-2.1.2}/tests/__init__.py +0 -0
- {bmorricaldjangoworkflows-2.1.0 → bmorricaldjangoworkflows-2.1.2}/tests/test_email.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.1.2
|
|
@@ -32,6 +32,10 @@ def generate_verification_code(*, length: int = 6) -> str:
|
|
|
32
32
|
return "".join(secrets.choice(_ALLOWED_CODE_CHARS) for _ in range(length))
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
def generate_strong_password(*, length: int = 32) -> str:
|
|
36
|
+
return secrets.token_urlsafe(length)
|
|
37
|
+
|
|
38
|
+
|
|
35
39
|
def _redact_code(code: str) -> str:
|
|
36
40
|
if not code:
|
|
37
41
|
return ""
|
|
@@ -106,6 +110,26 @@ def _send_verification_email_or_compensate(*, user: Any, email: str, html: str,
|
|
|
106
110
|
return False
|
|
107
111
|
|
|
108
112
|
|
|
113
|
+
def _send_admin_registration_email(*, user: Any, html: str, enabled: bool) -> bool:
|
|
114
|
+
response = send_email(
|
|
115
|
+
to=[user.email],
|
|
116
|
+
subject="Your account has been created",
|
|
117
|
+
html=html,
|
|
118
|
+
enabled=enabled,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if _is_delivery_success(response):
|
|
122
|
+
logger.info("Admin registration email sent to %s", user.email)
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
logger.warning(
|
|
126
|
+
"Admin registration email could not be delivered to %s (response_code=%s)",
|
|
127
|
+
user.email,
|
|
128
|
+
getattr(response, "status_code", None),
|
|
129
|
+
)
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
|
|
109
133
|
def _workflows_settings() -> dict[str, Any]:
|
|
110
134
|
if not settings.configured:
|
|
111
135
|
return {}
|
|
@@ -156,6 +180,14 @@ def _get_group_model() -> Type[Any]:
|
|
|
156
180
|
return Group
|
|
157
181
|
|
|
158
182
|
|
|
183
|
+
def _get_registration_username(*, email: str, username: Any = None) -> str:
|
|
184
|
+
if username is None:
|
|
185
|
+
return email
|
|
186
|
+
|
|
187
|
+
normalized_username = str(username).strip()
|
|
188
|
+
return normalized_username or email
|
|
189
|
+
|
|
190
|
+
|
|
159
191
|
def _normalize_group_names(value: Any) -> list[str]:
|
|
160
192
|
if value is None:
|
|
161
193
|
return []
|
|
@@ -183,6 +215,33 @@ def _normalize_group_names(value: Any) -> list[str]:
|
|
|
183
215
|
return normalized
|
|
184
216
|
|
|
185
217
|
|
|
218
|
+
def _normalize_group_payload(groups: Any) -> list[dict[str, Any]]:
|
|
219
|
+
if groups is None:
|
|
220
|
+
return []
|
|
221
|
+
|
|
222
|
+
if not isinstance(groups, list):
|
|
223
|
+
raise ValueError("groups must be a list of objects with name and enabled.")
|
|
224
|
+
|
|
225
|
+
normalized_groups: dict[str, bool] = {}
|
|
226
|
+
for group in groups:
|
|
227
|
+
if not isinstance(group, dict):
|
|
228
|
+
raise ValueError("groups must be a list of objects with name and enabled.")
|
|
229
|
+
|
|
230
|
+
name = str(group.get("name") or "").strip()
|
|
231
|
+
if not name or "enabled" not in group:
|
|
232
|
+
raise ValueError("Each group must include name and enabled.")
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
normalized_groups[name] = _coerce_bool(group["enabled"])
|
|
236
|
+
except ValueError as exc:
|
|
237
|
+
raise ValueError("Each group enabled value must be boolean.") from exc
|
|
238
|
+
|
|
239
|
+
return [
|
|
240
|
+
{"name": name, "enabled": enabled}
|
|
241
|
+
for name, enabled in normalized_groups.items()
|
|
242
|
+
]
|
|
243
|
+
|
|
244
|
+
|
|
186
245
|
def _get_existing_groups(group_names: Any) -> list[Any]:
|
|
187
246
|
normalized_names = _normalize_group_names(group_names)
|
|
188
247
|
if not normalized_names:
|
|
@@ -192,21 +251,49 @@ def _get_existing_groups(group_names: Any) -> list[Any]:
|
|
|
192
251
|
return list(Group.objects.filter(name__in=normalized_names))
|
|
193
252
|
|
|
194
253
|
|
|
195
|
-
def
|
|
196
|
-
|
|
254
|
+
def _sync_user_groups(*, user: Any, groups: Any) -> dict[str, list[str]]:
|
|
255
|
+
normalized_groups = _normalize_group_payload(groups)
|
|
197
256
|
group_manager = getattr(user, "groups", None)
|
|
198
257
|
|
|
199
|
-
if not
|
|
200
|
-
return []
|
|
258
|
+
if not normalized_groups or group_manager is None:
|
|
259
|
+
return {"added": [], "removed": []}
|
|
260
|
+
|
|
261
|
+
existing_groups = {
|
|
262
|
+
group.name: group
|
|
263
|
+
for group in _get_existing_groups([group["name"] for group in normalized_groups])
|
|
264
|
+
}
|
|
265
|
+
groups_to_add = []
|
|
266
|
+
groups_to_remove = []
|
|
201
267
|
|
|
202
|
-
|
|
203
|
-
|
|
268
|
+
for group in normalized_groups:
|
|
269
|
+
existing_group = existing_groups.get(group["name"])
|
|
270
|
+
if existing_group is None:
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
if group["enabled"]:
|
|
274
|
+
groups_to_add.append(existing_group)
|
|
275
|
+
else:
|
|
276
|
+
groups_to_remove.append(existing_group)
|
|
277
|
+
|
|
278
|
+
if groups_to_add and hasattr(group_manager, "add"):
|
|
279
|
+
group_manager.add(*groups_to_add)
|
|
280
|
+
if groups_to_remove and hasattr(group_manager, "remove"):
|
|
281
|
+
group_manager.remove(*groups_to_remove)
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
"added": [group.name for group in groups_to_add],
|
|
285
|
+
"removed": [group.name for group in groups_to_remove],
|
|
286
|
+
}
|
|
204
287
|
|
|
205
288
|
|
|
206
289
|
def _coerce_bool(value: Any) -> bool:
|
|
207
290
|
if isinstance(value, bool):
|
|
208
291
|
return value
|
|
209
292
|
|
|
293
|
+
if isinstance(value, int):
|
|
294
|
+
if value in {0, 1}:
|
|
295
|
+
return bool(value)
|
|
296
|
+
|
|
210
297
|
if isinstance(value, str):
|
|
211
298
|
normalized = value.strip().lower()
|
|
212
299
|
if normalized in {"1", "true", "yes", "on"}:
|
|
@@ -242,18 +329,23 @@ def get_user_details(user: Any) -> dict[str, Any]:
|
|
|
242
329
|
|
|
243
330
|
def list_users(*, exclude_groups: Any = None) -> list[dict[str, Any]]:
|
|
244
331
|
User = get_user_model()
|
|
245
|
-
excluded_group_names =
|
|
332
|
+
excluded_group_names = {"Superuser", *(_normalize_group_names(exclude_groups))}
|
|
246
333
|
users = list(User.objects.all())
|
|
247
|
-
users.sort(key=lambda user: getattr(user, "id", 0))
|
|
248
334
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
for
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
335
|
+
filtered_users = []
|
|
336
|
+
for user in users:
|
|
337
|
+
user_group_names = {group.name for group in user.groups.all()}
|
|
338
|
+
if user_group_names.intersection(excluded_group_names):
|
|
339
|
+
continue
|
|
340
|
+
filtered_users.append(user)
|
|
341
|
+
|
|
342
|
+
users = sorted(
|
|
343
|
+
filtered_users,
|
|
344
|
+
key=lambda user: (
|
|
345
|
+
not bool(getattr(user, "is_active", False)),
|
|
346
|
+
str(getattr(user, "username", "") or "").lower(),
|
|
347
|
+
),
|
|
348
|
+
)
|
|
257
349
|
|
|
258
350
|
return [get_user_details(user) for user in users]
|
|
259
351
|
|
|
@@ -296,7 +388,7 @@ def register_user_and_send_verification_email(
|
|
|
296
388
|
first_name: str,
|
|
297
389
|
last_name: str,
|
|
298
390
|
password: str,
|
|
299
|
-
|
|
391
|
+
username: Any = None,
|
|
300
392
|
groups: Any = None,
|
|
301
393
|
) -> ServiceResult:
|
|
302
394
|
"""
|
|
@@ -310,6 +402,12 @@ def register_user_and_send_verification_email(
|
|
|
310
402
|
enable_email_delivery = _is_enabled(
|
|
311
403
|
_get_workflow_setting(_EMAIL_DELIVERY_ENABLED_SETTING, False)
|
|
312
404
|
)
|
|
405
|
+
registration_username = _get_registration_username(email=email, username=username)
|
|
406
|
+
|
|
407
|
+
try:
|
|
408
|
+
validated_groups = _normalize_group_payload(groups)
|
|
409
|
+
except ValueError as exc:
|
|
410
|
+
return ServiceResult(success=False, message=str(exc))
|
|
313
411
|
|
|
314
412
|
if User.objects.filter(email=email).exists():
|
|
315
413
|
return ServiceResult(
|
|
@@ -325,7 +423,7 @@ def register_user_and_send_verification_email(
|
|
|
325
423
|
with _registration_atomic_context():
|
|
326
424
|
user = User.objects.create_user(
|
|
327
425
|
email=email,
|
|
328
|
-
username=
|
|
426
|
+
username=registration_username,
|
|
329
427
|
first_name=first_name,
|
|
330
428
|
last_name=last_name,
|
|
331
429
|
password=password,
|
|
@@ -340,12 +438,7 @@ def register_user_and_send_verification_email(
|
|
|
340
438
|
meta.verify_time = timezone.now()
|
|
341
439
|
meta.save()
|
|
342
440
|
|
|
343
|
-
|
|
344
|
-
if group is not None:
|
|
345
|
-
requested_groups.extend(
|
|
346
|
-
name for name in _normalize_group_names(group) if name not in requested_groups
|
|
347
|
-
)
|
|
348
|
-
_add_existing_groups_to_user(user=user, group_names=requested_groups)
|
|
441
|
+
_sync_user_groups(user=user, groups=validated_groups)
|
|
349
442
|
|
|
350
443
|
message = (
|
|
351
444
|
f"Hello {first_name} {last_name},<br><br>"
|
|
@@ -402,10 +495,119 @@ def register_user_and_send_verification_email(
|
|
|
402
495
|
)
|
|
403
496
|
|
|
404
497
|
|
|
498
|
+
def admin_register_user_and_send_setup_email(
|
|
499
|
+
email: str,
|
|
500
|
+
first_name: str,
|
|
501
|
+
last_name: str,
|
|
502
|
+
username: Any = None,
|
|
503
|
+
groups: Any = None,
|
|
504
|
+
) -> ServiceResult:
|
|
505
|
+
User = get_user_model()
|
|
506
|
+
|
|
507
|
+
company_name = _get_workflow_setting("COMPANY_NAME", "")
|
|
508
|
+
enable_email_delivery = _is_enabled(
|
|
509
|
+
_get_workflow_setting(_EMAIL_DELIVERY_ENABLED_SETTING, False)
|
|
510
|
+
)
|
|
511
|
+
registration_username = _get_registration_username(email=email, username=username)
|
|
512
|
+
|
|
513
|
+
try:
|
|
514
|
+
validated_groups = _normalize_group_payload(groups)
|
|
515
|
+
except ValueError as exc:
|
|
516
|
+
return ServiceResult(success=False, message=str(exc))
|
|
517
|
+
|
|
518
|
+
if User.objects.filter(email=email).exists():
|
|
519
|
+
return ServiceResult(
|
|
520
|
+
success=False,
|
|
521
|
+
message="A user with this email already exists.",
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
generated_password = generate_strong_password()
|
|
525
|
+
user = None
|
|
526
|
+
message = ""
|
|
527
|
+
|
|
528
|
+
try:
|
|
529
|
+
with _registration_atomic_context():
|
|
530
|
+
user = User.objects.create_user(
|
|
531
|
+
email=email,
|
|
532
|
+
username=registration_username,
|
|
533
|
+
first_name=first_name,
|
|
534
|
+
last_name=last_name,
|
|
535
|
+
password=generated_password,
|
|
536
|
+
is_active=True,
|
|
537
|
+
)
|
|
538
|
+
_sync_user_groups(user=user, groups=validated_groups)
|
|
539
|
+
|
|
540
|
+
message = (
|
|
541
|
+
f"Hello {first_name} {last_name},<br><br>"
|
|
542
|
+
'You have been added as a registered user by an administrator. '
|
|
543
|
+
'Use the "Forgot Password" flow to establish your password and login.<br><br>'
|
|
544
|
+
f"Thank you!<br><br>{company_name}"
|
|
545
|
+
)
|
|
546
|
+
except IntegrityError:
|
|
547
|
+
logger.exception("Admin registration failed due to IntegrityError for email=%s", email)
|
|
548
|
+
return ServiceResult(
|
|
549
|
+
success=False,
|
|
550
|
+
message="Email already exists, login or reset password.",
|
|
551
|
+
)
|
|
552
|
+
except Exception:
|
|
553
|
+
logger.exception("Unexpected error during admin registration for email=%s", email)
|
|
554
|
+
return ServiceResult(
|
|
555
|
+
success=False,
|
|
556
|
+
message="An unexpected error occurred during admin registration.",
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
if enable_email_delivery:
|
|
560
|
+
def _send_after_commit() -> None:
|
|
561
|
+
_send_admin_registration_email(
|
|
562
|
+
user=user,
|
|
563
|
+
html=message,
|
|
564
|
+
enabled=enable_email_delivery,
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
if _register_on_commit(_send_after_commit):
|
|
568
|
+
logger.info(
|
|
569
|
+
"Admin registration email send scheduled after transaction commit for %s",
|
|
570
|
+
user.email,
|
|
571
|
+
)
|
|
572
|
+
else:
|
|
573
|
+
_send_admin_registration_email(
|
|
574
|
+
user=user,
|
|
575
|
+
html=message,
|
|
576
|
+
enabled=enable_email_delivery,
|
|
577
|
+
)
|
|
578
|
+
else:
|
|
579
|
+
logger.info("Email delivery is disabled. Admin registration email not sent.")
|
|
580
|
+
|
|
581
|
+
return ServiceResult(
|
|
582
|
+
success=True,
|
|
583
|
+
message="User created successfully. The user can use the Forgot Password flow to establish a password and login.",
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
|
|
405
587
|
def update_user(user: Any, data: dict[str, Any]) -> UserUpdateResult:
|
|
406
588
|
processed_update = False
|
|
407
589
|
user_fields_updated = False
|
|
408
590
|
|
|
591
|
+
validated_groups = None
|
|
592
|
+
if "groups" in data:
|
|
593
|
+
try:
|
|
594
|
+
validated_groups = _normalize_group_payload(data.get("groups"))
|
|
595
|
+
except ValueError as exc:
|
|
596
|
+
return UserUpdateResult(
|
|
597
|
+
success=False,
|
|
598
|
+
message=str(exc),
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
next_is_active = None
|
|
602
|
+
if "isActive" in data:
|
|
603
|
+
try:
|
|
604
|
+
next_is_active = _coerce_bool(data["isActive"])
|
|
605
|
+
except ValueError:
|
|
606
|
+
return UserUpdateResult(
|
|
607
|
+
success=False,
|
|
608
|
+
message="isActive must be a boolean value.",
|
|
609
|
+
)
|
|
610
|
+
|
|
409
611
|
email_value = data.get("email")
|
|
410
612
|
username_value = data.get("username")
|
|
411
613
|
if email_value is not None or username_value is not None:
|
|
@@ -415,43 +617,36 @@ def update_user(user: Any, data: dict[str, Any]) -> UserUpdateResult:
|
|
|
415
617
|
processed_update = True
|
|
416
618
|
user_fields_updated = True
|
|
417
619
|
|
|
418
|
-
if "
|
|
419
|
-
user.first_name = data["
|
|
620
|
+
if "firstName" in data:
|
|
621
|
+
user.first_name = data["firstName"]
|
|
420
622
|
processed_update = True
|
|
421
623
|
user_fields_updated = True
|
|
422
|
-
if "
|
|
423
|
-
user.last_name = data["
|
|
624
|
+
if "lastName" in data:
|
|
625
|
+
user.last_name = data["lastName"]
|
|
424
626
|
processed_update = True
|
|
425
627
|
user_fields_updated = True
|
|
426
628
|
if "password" in data:
|
|
427
629
|
user.set_password(data["password"])
|
|
428
630
|
processed_update = True
|
|
429
631
|
user_fields_updated = True
|
|
430
|
-
if "
|
|
431
|
-
|
|
432
|
-
user.is_active = _coerce_bool(data["is_active"])
|
|
433
|
-
except ValueError:
|
|
434
|
-
return UserUpdateResult(
|
|
435
|
-
success=False,
|
|
436
|
-
message="is_active must be a boolean value.",
|
|
437
|
-
)
|
|
632
|
+
if "isActive" in data:
|
|
633
|
+
user.is_active = next_is_active
|
|
438
634
|
processed_update = True
|
|
439
635
|
user_fields_updated = True
|
|
440
636
|
|
|
441
|
-
requested_groups = []
|
|
442
637
|
if "groups" in data:
|
|
443
|
-
requested_groups.extend(_normalize_group_names(data.get("groups")))
|
|
444
|
-
processed_update = True
|
|
445
|
-
if "group" in data:
|
|
446
|
-
requested_groups.extend(
|
|
447
|
-
name for name in _normalize_group_names(data.get("group")) if name not in requested_groups
|
|
448
|
-
)
|
|
449
638
|
processed_update = True
|
|
450
|
-
|
|
451
|
-
_add_existing_groups_to_user(user=user, group_names=requested_groups)
|
|
639
|
+
_sync_user_groups(user=user, groups=validated_groups)
|
|
452
640
|
|
|
453
641
|
if user_fields_updated:
|
|
454
|
-
|
|
642
|
+
try:
|
|
643
|
+
user.save()
|
|
644
|
+
except IntegrityError:
|
|
645
|
+
logger.exception("User update failed due to IntegrityError for email=%s", user.email)
|
|
646
|
+
return UserUpdateResult(
|
|
647
|
+
success=False,
|
|
648
|
+
message="A user with this email already exists.",
|
|
649
|
+
)
|
|
455
650
|
|
|
456
651
|
if not processed_update:
|
|
457
652
|
return UserUpdateResult(
|
{bmorricaldjangoworkflows-2.1.0 → bmorricaldjangoworkflows-2.1.2}/django_workflows/users/urls.py
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from django.urls import path
|
|
2
2
|
|
|
3
3
|
from django_workflows.users.views import (
|
|
4
|
+
AdminUserCreateView,
|
|
4
5
|
ChangePasswordView,
|
|
5
6
|
ForgotPasswordView,
|
|
6
7
|
UserCreateView,
|
|
@@ -12,6 +13,7 @@ from django_workflows.users.views import (
|
|
|
12
13
|
urlpatterns = [
|
|
13
14
|
path("user/", UserDetailView.as_view(), name="user-detail"),
|
|
14
15
|
path("users/", UserListView.as_view(), name="user-list"),
|
|
16
|
+
path("admin-register/", AdminUserCreateView.as_view(), name="admin-register"),
|
|
15
17
|
path("forgot-password/", ForgotPasswordView.as_view(), name="forgot-password"),
|
|
16
18
|
path("verify-code/", VerifyCodeView.as_view(), name="verify-code"),
|
|
17
19
|
path("change-password/", ChangePasswordView.as_view(), name="change-password"),
|
{bmorricaldjangoworkflows-2.1.0 → bmorricaldjangoworkflows-2.1.2}/django_workflows/users/views.py
RENAMED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
|
+
from django.contrib.auth import get_user_model
|
|
4
|
+
|
|
3
5
|
from rest_framework import status
|
|
4
|
-
from rest_framework.permissions import IsAuthenticated
|
|
6
|
+
from rest_framework.permissions import BasePermission, IsAuthenticated
|
|
5
7
|
from rest_framework.response import Response
|
|
6
8
|
from rest_framework.views import APIView
|
|
7
9
|
|
|
8
10
|
from django_workflows.users.services.auth_flow import (
|
|
11
|
+
admin_register_user_and_send_setup_email,
|
|
9
12
|
change_password,
|
|
10
13
|
get_user_details,
|
|
11
14
|
list_users,
|
|
@@ -18,6 +21,64 @@ from django_workflows.users.services.auth_flow import (
|
|
|
18
21
|
logger = logging.getLogger(__name__)
|
|
19
22
|
|
|
20
23
|
|
|
24
|
+
def _user_has_group(user, group_name):
|
|
25
|
+
if not user or not getattr(user, "is_authenticated", False):
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
group_manager = getattr(user, "groups", None)
|
|
29
|
+
if group_manager is None:
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
if hasattr(group_manager, "filter"):
|
|
33
|
+
filtered_groups = group_manager.filter(name=group_name)
|
|
34
|
+
if hasattr(filtered_groups, "exists"):
|
|
35
|
+
return filtered_groups.exists()
|
|
36
|
+
|
|
37
|
+
if hasattr(group_manager, "all"):
|
|
38
|
+
return any(getattr(group, "name", None) == group_name for group in group_manager.all())
|
|
39
|
+
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class HasUserManagerGroup(BasePermission):
|
|
44
|
+
message = "User Manager group membership is required."
|
|
45
|
+
|
|
46
|
+
def has_permission(self, request, view):
|
|
47
|
+
return _user_has_group(request.user, "User Manager")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _resolve_patch_target_user(request):
|
|
51
|
+
target_user_id = request.data.get("id")
|
|
52
|
+
if target_user_id is None:
|
|
53
|
+
return request.user, None
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
target_user_id = int(target_user_id)
|
|
57
|
+
except (TypeError, ValueError):
|
|
58
|
+
return None, Response(
|
|
59
|
+
{"detail": "id must be an integer value."},
|
|
60
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if target_user_id == getattr(request.user, "id", None):
|
|
64
|
+
return request.user, None
|
|
65
|
+
|
|
66
|
+
if not _user_has_group(request.user, "User Manager"):
|
|
67
|
+
return None, Response(
|
|
68
|
+
{"detail": HasUserManagerGroup.message},
|
|
69
|
+
status=status.HTTP_403_FORBIDDEN,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
User = get_user_model()
|
|
73
|
+
try:
|
|
74
|
+
return User.objects.get(id=target_user_id), None
|
|
75
|
+
except User.DoesNotExist:
|
|
76
|
+
return None, Response(
|
|
77
|
+
{"detail": "User not found."},
|
|
78
|
+
status=status.HTTP_404_NOT_FOUND,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
21
82
|
class AnonymousWorkflowView(APIView):
|
|
22
83
|
authentication_classes = []
|
|
23
84
|
permission_classes = []
|
|
@@ -52,7 +113,8 @@ class ForgotPasswordView(AnonymousWorkflowView):
|
|
|
52
113
|
|
|
53
114
|
|
|
54
115
|
# Route: POST /register/
|
|
55
|
-
# Body params: firstName, lastName, email, password, optional
|
|
116
|
+
# Body params: firstName, lastName, email, password, optional username, optional company honeypot field, optional groups as [{name, enabled}]
|
|
117
|
+
# Notes: anonymous self-registration flow; user chooses password and receives a verification code by email
|
|
56
118
|
class UserCreateView(AnonymousWorkflowView):
|
|
57
119
|
def post(self, request):
|
|
58
120
|
data = request.data
|
|
@@ -81,7 +143,54 @@ class UserCreateView(AnonymousWorkflowView):
|
|
|
81
143
|
last_name=data.get("lastName"),
|
|
82
144
|
email=data.get("email"),
|
|
83
145
|
password=data.get("password"),
|
|
84
|
-
|
|
146
|
+
username=data.get("username"),
|
|
147
|
+
groups=data.get("groups"),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if not result.success:
|
|
151
|
+
response_status = status.HTTP_409_CONFLICT
|
|
152
|
+
if result.message != "A user with this email already exists.":
|
|
153
|
+
response_status = status.HTTP_400_BAD_REQUEST
|
|
154
|
+
|
|
155
|
+
return Response({"detail": result.message}, status=response_status)
|
|
156
|
+
|
|
157
|
+
return Response({"detail": result.message}, status=status.HTTP_201_CREATED)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# Route: POST /admin-register/
|
|
161
|
+
# Access: authenticated users in the User Manager group
|
|
162
|
+
# Body params: firstName, lastName, email, optional username, optional company honeypot field, optional groups as [{name, enabled}]
|
|
163
|
+
# Notes: server generates a strong password, creates an active user, and sends an informational email without verification code logic
|
|
164
|
+
class AdminUserCreateView(APIView):
|
|
165
|
+
permission_classes = [IsAuthenticated, HasUserManagerGroup]
|
|
166
|
+
|
|
167
|
+
def post(self, request):
|
|
168
|
+
data = request.data
|
|
169
|
+
|
|
170
|
+
if data.get("company") is not None and str(data.get("company")).strip():
|
|
171
|
+
logger.warning(
|
|
172
|
+
"Honeypot triggered for admin registration: company=%s",
|
|
173
|
+
data.get("company"),
|
|
174
|
+
)
|
|
175
|
+
return Response(
|
|
176
|
+
{"detail": "Bot detected. Registration blocked."},
|
|
177
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
required_fields = ["firstName", "lastName", "email"]
|
|
181
|
+
missing_fields = [field for field in required_fields if field not in data]
|
|
182
|
+
if missing_fields:
|
|
183
|
+
logger.error("Admin registration missing fields: %s", missing_fields)
|
|
184
|
+
return Response(
|
|
185
|
+
{"detail": f"Missing required field(s): {', '.join(missing_fields)}"},
|
|
186
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
result = admin_register_user_and_send_setup_email(
|
|
190
|
+
first_name=data.get("firstName"),
|
|
191
|
+
last_name=data.get("lastName"),
|
|
192
|
+
email=data.get("email"),
|
|
193
|
+
username=data.get("username"),
|
|
85
194
|
groups=data.get("groups"),
|
|
86
195
|
)
|
|
87
196
|
|
|
@@ -155,7 +264,7 @@ class ChangePasswordView(AnonymousWorkflowView):
|
|
|
155
264
|
|
|
156
265
|
|
|
157
266
|
# Route: GET /users/
|
|
158
|
-
# Query params: optional exclude_group as comma-separated group names
|
|
267
|
+
# Query params: optional exclude_group as comma-separated group names; users in the Superuser group are always excluded
|
|
159
268
|
class UserListView(APIView):
|
|
160
269
|
permission_classes = [IsAuthenticated]
|
|
161
270
|
|
|
@@ -167,7 +276,8 @@ class UserListView(APIView):
|
|
|
167
276
|
|
|
168
277
|
# Route: GET /user/
|
|
169
278
|
# Route: PATCH /user/
|
|
170
|
-
# PATCH body params: username, email,
|
|
279
|
+
# PATCH body params: optional id, username, email, firstName, lastName, password, isActive, optional groups as [{name, enabled}]
|
|
280
|
+
# Notes: when id is omitted the authenticated user is updated; updating another user by id requires the User Manager group
|
|
171
281
|
class UserDetailView(APIView):
|
|
172
282
|
permission_classes = [IsAuthenticated]
|
|
173
283
|
|
|
@@ -177,7 +287,11 @@ class UserDetailView(APIView):
|
|
|
177
287
|
return Response(data, status=status.HTTP_200_OK)
|
|
178
288
|
|
|
179
289
|
def patch(self, request):
|
|
180
|
-
|
|
290
|
+
target_user, error_response = _resolve_patch_target_user(request)
|
|
291
|
+
if error_response is not None:
|
|
292
|
+
return error_response
|
|
293
|
+
|
|
294
|
+
result = update_user(target_user, request.data)
|
|
181
295
|
|
|
182
296
|
if result.success:
|
|
183
297
|
return Response(
|
|
@@ -185,5 +299,5 @@ class UserDetailView(APIView):
|
|
|
185
299
|
status=status.HTTP_200_OK,
|
|
186
300
|
)
|
|
187
301
|
|
|
188
|
-
logger.error("UserDetailView: %s for user %s", result.message,
|
|
302
|
+
logger.error("UserDetailView: %s for user %s", result.message, target_user.email)
|
|
189
303
|
return Response({"detail": result.message}, status=status.HTTP_400_BAD_REQUEST)
|