openwisp-users 1.3.1__tar.gz → 1.3.3__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.
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/CHANGES.rst +78 -0
- {openwisp_users-1.3.1/openwisp_users.egg-info → openwisp_users-1.3.3}/PKG-INFO +1 -1
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/__init__.py +1 -1
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/static/openwisp-users/accounts/css/non-admin.css +5 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/admin.py +108 -14
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/api/permissions.py +27 -1
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/api/serializers.py +16 -2
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/api/views.py +17 -8
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/apps.py +20 -4
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/base/models.py +6 -6
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/password_validation.py +2 -1
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/tasks.py +5 -2
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/tests/test_admin.py +636 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/tests/test_api/test_api.py +341 -10
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/tests/test_models.py +131 -8
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/utils.py +9 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3/openwisp_users.egg-info}/PKG-INFO +1 -1
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/LICENSE +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/MANIFEST.in +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/README.rst +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/__init__.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/adapter.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/locale/de/LC_MESSAGES/django.po +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/locale/fur/LC_MESSAGES/django.po +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/locale/it/LC_MESSAGES/django.po +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/locale/ru/LC_MESSAGES/django.po +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/locale/sl/LC_MESSAGES/django.po +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/static/openwisp-users/accounts/css/change-password.css +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/templates/account/email/account_expiration_reminder_message.html +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/templates/account/email/account_expiration_reminder_message.txt +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/templates/account/email/account_expired_message.html +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/templates/account/email/account_expired_message.txt +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/templates/account/email/base_message.txt +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/templates/account/email/email_confirmation_message.html +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/templates/account/email/email_confirmation_message.txt +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/templates/account/email/email_confirmation_signup_message.html +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/templates/account/email/password_expiration_message.html +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/templates/account/email/password_expiration_message.txt +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/templates/account/email_confirm.html +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/templates/account/email_verification_success.html +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/templates/account/login.html +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/templates/account/logout_success.html +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/templates/account/password_change.html +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/templates/account/password_change_success.html +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/templates/account/password_not_required.html +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/templates/allauth/layouts/base.html +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/urls.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/accounts/views.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/api/__init__.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/api/authentication.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/api/filters.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/api/mixins.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/api/swagger.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/api/throttling.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/api/urls.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/auth.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/backends.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/base/__init__.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/base/forms.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/locale/de/LC_MESSAGES/django.po +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/locale/fur/LC_MESSAGES/django.po +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/locale/it/LC_MESSAGES/django.po +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/locale/ru/LC_MESSAGES/django.po +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/locale/sl/LC_MESSAGES/django.po +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/management/__init__.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/management/commands/__init__.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/management/commands/export_users.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/middleware.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/migrations/0001_initial.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/migrations/0002_auto_20180508_2017.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/migrations/0003_default_organization.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/migrations/0004_default_groups.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/migrations/0005_user_phone_number.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/migrations/0006_id_email_index_together.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/migrations/0007_unique_email.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/migrations/0008_update_admins_permissions.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/migrations/0009_create_organization_owners.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/migrations/0010_allow_admins_change_organization.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/migrations/0011_user_first_name_150_max_length.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/migrations/0012_user_location_max_length_256.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/migrations/0013_user_birth_date.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/migrations/0014_user_notes.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/migrations/0015_alter_organization_users_alter_organizationuser_user.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/migrations/0016_organizationinvitation.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/migrations/0017_user_language.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/migrations/0018_allow_operator_view_organisation.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/migrations/0019_user_password_updated.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/migrations/0020_populate_password_updated_field.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/migrations/0021_rename_user_id_email_openwisp_us_id_06c07a_idx.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/migrations/0022_user_expiration_date.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/migrations/0023_add_auth_token_permissions_to_admins.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/migrations/0024_apikey.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/migrations/0025_alter_organizationuser_is_admin.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/migrations/0026_user_password_based_token.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/migrations/__init__.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/mixins.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/models.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/multitenancy.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/settings.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/static/openwisp-users/css/admin.css +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/static/openwisp-users/css/change-password.css +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/static/openwisp-users/js/addform.js +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/static/openwisp-users/js/org-autocomplete.js +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/templates/admin/action_confirmation.html +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/templates/admin/login.html +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/templates/admin/openwisp_users/action_confirmation.html +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/templates/admin/openwisp_users/login.html +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/templates/admin/openwisp_users/user/change_form.html +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/templates/custom_password_reset_email.html +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/templates/custom_password_reset_email.txt +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/tests/__init__.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/tests/test_accounts.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/tests/test_adapter.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/tests/test_api/__init__.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/tests/test_api/test_authentication.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/tests/test_api/test_password_reset.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/tests/test_api/test_throttling.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/tests/test_api/test_urls.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/tests/test_api/test_views.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/tests/test_auth.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/tests/test_backends.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/tests/test_commands.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/tests/test_middlewares.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/tests/utils.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/views.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users/widgets.py +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users.egg-info/SOURCES.txt +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users.egg-info/dependency_links.txt +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users.egg-info/not-zip-safe +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users.egg-info/requires.txt +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/openwisp_users.egg-info/top_level.txt +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/pyproject.toml +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/requirements.txt +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/setup.cfg +0 -0
- {openwisp_users-1.3.1 → openwisp_users-1.3.3}/setup.py +0 -0
|
@@ -1,6 +1,84 @@
|
|
|
1
1
|
Changelog
|
|
2
2
|
=========
|
|
3
3
|
|
|
4
|
+
Version 1.3.3 [2026-11-13]
|
|
5
|
+
--------------------------
|
|
6
|
+
|
|
7
|
+
Bugfixes
|
|
8
|
+
~~~~~~~~
|
|
9
|
+
|
|
10
|
+
- Prevented unauthorized changes to owner memberships
|
|
11
|
+
|
|
12
|
+
Prevent non-superusers from editing owner memberships in
|
|
13
|
+
OrganizationUserAdmin.
|
|
14
|
+
|
|
15
|
+
Superusers can still update owner memberships, and administrators can
|
|
16
|
+
still edit other memberships.
|
|
17
|
+
|
|
18
|
+
**Security Advisory**: `GHSA-7h4r-39v5-q7cq
|
|
19
|
+
<https://github.com/openwisp/openwisp-users/security/advisories/GHSA-7h4r-39v5-q7cq>`_.
|
|
20
|
+
|
|
21
|
+
- Enforced password validators in user password API
|
|
22
|
+
|
|
23
|
+
Apply configured password validators when creating users and changing
|
|
24
|
+
their passwords through the API.
|
|
25
|
+
|
|
26
|
+
**Security Advisory**: `GHSA-qx7x-g6rx-5g3g
|
|
27
|
+
<https://github.com/openwisp/openwisp-users/security/advisories/GHSA-qx7x-g6rx-5g3g>`_.
|
|
28
|
+
|
|
29
|
+
- Aligned organization owner permissions across admin and API
|
|
30
|
+
|
|
31
|
+
Use consistent organization owner permission checks for account,
|
|
32
|
+
password, and email management in the admin and REST API.
|
|
33
|
+
|
|
34
|
+
Include owners of inactive organizations in ownership checks. Existing
|
|
35
|
+
access for superusers and owners managing their own accounts remains
|
|
36
|
+
unchanged.
|
|
37
|
+
|
|
38
|
+
**Security Advisory**: `GHSA-97jh-j6hw-4389
|
|
39
|
+
<https://github.com/openwisp/openwisp-users/security/advisories/GHSA-97jh-j6hw-4389>`_.
|
|
40
|
+
|
|
41
|
+
- Prevented stale memberships after organization status changes
|
|
42
|
+
|
|
43
|
+
Clear cached memberships after the organization status change commits,
|
|
44
|
+
preventing stale memberships from being cached during the transaction.
|
|
45
|
+
|
|
46
|
+
**Security Advisory**: `GHSA-x794-gqv9-rffh
|
|
47
|
+
<https://github.com/openwisp/openwisp-users/security/advisories/GHSA-x794-gqv9-rffh>`_.
|
|
48
|
+
|
|
49
|
+
- Prevented unauthorized changes to organization memberships
|
|
50
|
+
|
|
51
|
+
When adding or editing an organization membership in
|
|
52
|
+
OrganizationUserAdmin, organization admins can only select users who
|
|
53
|
+
already belong to an organization they manage and cannot select
|
|
54
|
+
superusers.
|
|
55
|
+
|
|
56
|
+
**Security Advisory**: `GHSA-p9mp-fxhh-hmfw
|
|
57
|
+
<https://github.com/openwisp/openwisp-users/security/advisories/GHSA-p9mp-fxhh-hmfw>`_.
|
|
58
|
+
|
|
59
|
+
- Improved permission checks for bulk user actions
|
|
60
|
+
|
|
61
|
+
Organization administrators can only activate or deactivate users they
|
|
62
|
+
have permission to edit. If the selection includes a user they cannot
|
|
63
|
+
edit, the action is stopped and a message is displayed.
|
|
64
|
+
|
|
65
|
+
**Security advisory**: `GHSA-q66j-wp8j-5hf5
|
|
66
|
+
<https://github.com/openwisp/openwisp-users/security/advisories/GHSA-q66j-wp8j-5hf5>`_.
|
|
67
|
+
|
|
68
|
+
Version 1.3.2 [2026-10-23]
|
|
69
|
+
--------------------------
|
|
70
|
+
|
|
71
|
+
Bugfixes
|
|
72
|
+
~~~~~~~~
|
|
73
|
+
|
|
74
|
+
- Restored logo padding on mobile screens
|
|
75
|
+
|
|
76
|
+
Rules in `openwisp.css` removed the logo padding on non-admin screens,
|
|
77
|
+
making it appear cramped.
|
|
78
|
+
|
|
79
|
+
- Corrected password expiration dates and duplicate notifications `#375
|
|
80
|
+
<https://github.com/openwisp/openwisp-users/issues/375>`_
|
|
81
|
+
|
|
4
82
|
Version 1.3.1 [2026-09-09]
|
|
5
83
|
--------------------------
|
|
6
84
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openwisp-users
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.3
|
|
4
4
|
Summary: Provides basic multi-tenancy features for OpenWISP (using the Django web-framework)
|
|
5
5
|
Home-page: http://openwisp.org
|
|
6
6
|
Download-URL: https://github.com/openwisp/openwisp-users/releases
|
|
@@ -8,19 +8,19 @@ from django.apps import apps
|
|
|
8
8
|
from django.conf import settings
|
|
9
9
|
from django.contrib import admin, messages
|
|
10
10
|
from django.contrib.admin.actions import delete_selected
|
|
11
|
-
from django.contrib.admin.utils import model_ngettext
|
|
11
|
+
from django.contrib.admin.utils import model_ngettext, quote
|
|
12
12
|
from django.contrib.auth import get_user_model
|
|
13
13
|
from django.contrib.auth.admin import GroupAdmin as BaseGroupAdmin
|
|
14
14
|
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
|
15
15
|
from django.contrib.auth.forms import UserChangeForm as BaseUserChangeForm
|
|
16
16
|
from django.contrib.auth.forms import UserCreationForm as BaseUserCreationForm
|
|
17
|
-
from django.core.exceptions import ValidationError
|
|
18
|
-
from django.db.models import Q
|
|
17
|
+
from django.core.exceptions import PermissionDenied, ValidationError
|
|
18
|
+
from django.db.models import OuterRef, Q, Subquery
|
|
19
19
|
from django.forms.models import BaseInlineFormSet
|
|
20
20
|
from django.http import HttpResponseRedirect
|
|
21
21
|
from django.template.response import TemplateResponse
|
|
22
22
|
from django.urls import reverse
|
|
23
|
-
from django.utils.html import format_html
|
|
23
|
+
from django.utils.html import format_html, format_html_join
|
|
24
24
|
from django.utils.timezone import localdate
|
|
25
25
|
from django.utils.translation import gettext_lazy as _
|
|
26
26
|
from django.utils.translation import ngettext
|
|
@@ -37,7 +37,7 @@ from openwisp_utils.admin import CopyableFieldsAdmin
|
|
|
37
37
|
|
|
38
38
|
from . import settings as app_settings
|
|
39
39
|
from .multitenancy import MultitenantAdminMixin, MultitenantOrgFilter
|
|
40
|
-
from .utils import BaseAdmin
|
|
40
|
+
from .utils import BaseAdmin, user_not_allowed_to_change_owner
|
|
41
41
|
|
|
42
42
|
Group = load_model("openwisp_users", "Group")
|
|
43
43
|
Organization = load_model("openwisp_users", "Organization")
|
|
@@ -47,15 +47,6 @@ User = get_user_model()
|
|
|
47
47
|
logger = logging.getLogger(__name__)
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-
def user_not_allowed_to_change_owner(user, obj):
|
|
51
|
-
return (
|
|
52
|
-
obj
|
|
53
|
-
and not user.is_superuser
|
|
54
|
-
and user.pk != obj.pk
|
|
55
|
-
and obj.is_owner_of_any_organization
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
|
|
59
50
|
class EmailAddressInline(admin.StackedInline):
|
|
60
51
|
model = EmailAddress
|
|
61
52
|
extra = 0
|
|
@@ -237,6 +228,41 @@ class UserAdmin(MultitenantAdminMixin, BaseUserAdmin, BaseAdmin):
|
|
|
237
228
|
class Media:
|
|
238
229
|
css = {"all": ("openwisp-users/css/admin.css",)}
|
|
239
230
|
|
|
231
|
+
def _format_permission_denied(self, users, has_more):
|
|
232
|
+
display_names = []
|
|
233
|
+
url_name = f"{self.opts.app_label}_{self.opts.model_name}_change"
|
|
234
|
+
for user in users:
|
|
235
|
+
url = reverse(
|
|
236
|
+
f"{self.admin_site.name}:{url_name}",
|
|
237
|
+
args=[quote(user.pk)],
|
|
238
|
+
)
|
|
239
|
+
display_names.append(format_html('<a href="{}">{}</a>', url, user.username))
|
|
240
|
+
count = len(users)
|
|
241
|
+
if has_more:
|
|
242
|
+
formatted_names = format_html_join(
|
|
243
|
+
", ", "{}", ((name,) for name in display_names)
|
|
244
|
+
)
|
|
245
|
+
elif count > 1:
|
|
246
|
+
preceding = format_html_join(
|
|
247
|
+
", ", "{}", ((name,) for name in display_names[:-1])
|
|
248
|
+
)
|
|
249
|
+
formatted_names = format_html(
|
|
250
|
+
"{} {} {}", preceding, _("and"), display_names[-1]
|
|
251
|
+
)
|
|
252
|
+
else:
|
|
253
|
+
formatted_names = display_names[0]
|
|
254
|
+
return format_html(
|
|
255
|
+
ngettext(
|
|
256
|
+
"No users were updated because you do not have permission "
|
|
257
|
+
"to modify this user: {usernames}{ending}",
|
|
258
|
+
"No users were updated because you do not have permission "
|
|
259
|
+
"to modify these users: {usernames}{ending}",
|
|
260
|
+
count,
|
|
261
|
+
),
|
|
262
|
+
usernames=formatted_names,
|
|
263
|
+
ending=" …" if has_more else ".",
|
|
264
|
+
)
|
|
265
|
+
|
|
240
266
|
def require_confirmation(func):
|
|
241
267
|
"""
|
|
242
268
|
Decorator to lead to a confirmation page.
|
|
@@ -247,6 +273,40 @@ class UserAdmin(MultitenantAdminMixin, BaseUserAdmin, BaseAdmin):
|
|
|
247
273
|
"""
|
|
248
274
|
|
|
249
275
|
def wrapper(modeladmin, request, queryset):
|
|
276
|
+
# Bulk updates bypass the object checks used by the change form.
|
|
277
|
+
unauthorized_users = []
|
|
278
|
+
has_more_unauthorized_users = False
|
|
279
|
+
checked_pks = []
|
|
280
|
+
for obj in queryset.iterator(chunk_size=1000):
|
|
281
|
+
if not modeladmin.has_change_permission(request, obj):
|
|
282
|
+
if len(unauthorized_users) < 10:
|
|
283
|
+
unauthorized_users.append(obj)
|
|
284
|
+
else:
|
|
285
|
+
has_more_unauthorized_users = True
|
|
286
|
+
# An eleventh denial confirms truncation. The action cannot
|
|
287
|
+
# proceed, so checking further users would not change it.
|
|
288
|
+
break
|
|
289
|
+
else:
|
|
290
|
+
checked_pks.append(obj.pk)
|
|
291
|
+
if unauthorized_users:
|
|
292
|
+
modeladmin.message_user(
|
|
293
|
+
request,
|
|
294
|
+
modeladmin._format_permission_denied(
|
|
295
|
+
unauthorized_users, has_more_unauthorized_users
|
|
296
|
+
),
|
|
297
|
+
messages.ERROR,
|
|
298
|
+
)
|
|
299
|
+
return HttpResponseRedirect(request.get_full_path())
|
|
300
|
+
queryset = queryset.filter(pk__in=checked_pks)
|
|
301
|
+
if not request.user.is_superuser:
|
|
302
|
+
protected_owner_ids = (
|
|
303
|
+
OrganizationOwner.objects.filter(
|
|
304
|
+
organization_user__user_id=OuterRef("pk")
|
|
305
|
+
)
|
|
306
|
+
.exclude(organization_user__user_id=request.user.pk)
|
|
307
|
+
.values("organization_user__user_id")
|
|
308
|
+
)
|
|
309
|
+
queryset = queryset.exclude(pk__in=Subquery(protected_owner_ids))
|
|
250
310
|
opts = modeladmin.model._meta
|
|
251
311
|
if request.POST.get("confirmation") is None:
|
|
252
312
|
request.current_app = modeladmin.admin_site.name
|
|
@@ -596,6 +656,40 @@ class OrganizationUserAdmin(
|
|
|
596
656
|
fields = ("user", "organization", "is_admin")
|
|
597
657
|
search_fields = ["user__username", "organization__name"]
|
|
598
658
|
|
|
659
|
+
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
|
660
|
+
if db_field.name == "user" and not request.user.is_superuser:
|
|
661
|
+
# Filter only non-superusers belonging to organizations
|
|
662
|
+
# managed by the requester.
|
|
663
|
+
queryset = User.objects.filter(
|
|
664
|
+
id__in=OrganizationUser.objects.filter(
|
|
665
|
+
organization_id__in=request.user.organizations_managed
|
|
666
|
+
).values_list("user_id"),
|
|
667
|
+
is_superuser=False,
|
|
668
|
+
)
|
|
669
|
+
self._check_user(request, queryset)
|
|
670
|
+
kwargs["queryset"] = queryset
|
|
671
|
+
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
|
672
|
+
|
|
673
|
+
def _check_user(self, request, queryset):
|
|
674
|
+
user_id = request.POST.get("user")
|
|
675
|
+
if request.method != "POST" or not user_id:
|
|
676
|
+
return
|
|
677
|
+
try:
|
|
678
|
+
# Detect a submitted user ID outside the requester's scope.
|
|
679
|
+
allowed = queryset.filter(pk=user_id).exists()
|
|
680
|
+
except (ValidationError, ValueError, TypeError, OverflowError):
|
|
681
|
+
return
|
|
682
|
+
if not allowed:
|
|
683
|
+
# Django logs this potentially malicious request as a warning.
|
|
684
|
+
raise PermissionDenied(
|
|
685
|
+
f"Rejected unavailable membership user: actor={request.user.pk}"
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
def has_change_permission(self, request, obj=None):
|
|
689
|
+
if obj and not request.user.is_superuser and hasattr(obj, "organizationowner"):
|
|
690
|
+
return False
|
|
691
|
+
return super().has_change_permission(request, obj)
|
|
692
|
+
|
|
599
693
|
def get_readonly_fields(self, request, obj=None):
|
|
600
694
|
# retrieve readonly fields
|
|
601
695
|
fields = super().get_readonly_fields(request, obj)
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from django.utils.translation import gettext_lazy as _
|
|
2
|
-
from rest_framework.permissions import BasePermission
|
|
2
|
+
from rest_framework.permissions import SAFE_METHODS, BasePermission
|
|
3
3
|
from rest_framework.permissions import (
|
|
4
4
|
DjangoModelPermissions as BaseDjangoModelPermissions,
|
|
5
5
|
)
|
|
6
6
|
from swapper import load_model
|
|
7
7
|
|
|
8
|
+
from ..utils import user_not_allowed_to_change_owner
|
|
9
|
+
|
|
8
10
|
Organization = load_model("openwisp_users", "Organization")
|
|
9
11
|
|
|
10
12
|
|
|
@@ -95,6 +97,30 @@ class IsOrganizationOwner(BaseOrganizationPermission):
|
|
|
95
97
|
return org and (user.is_superuser or user.is_owner(org))
|
|
96
98
|
|
|
97
99
|
|
|
100
|
+
class CanChangeUser(BasePermission):
|
|
101
|
+
message = _(
|
|
102
|
+
"You do not have permission to edit or delete this user "
|
|
103
|
+
"because they are owner of an organization."
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def has_object_permission(self, request, view, obj):
|
|
107
|
+
return request.method in SAFE_METHODS or not user_not_allowed_to_change_owner(
|
|
108
|
+
request.user, obj
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class CanChangeUserEmail(CanChangeUser):
|
|
113
|
+
def has_permission(self, request, view):
|
|
114
|
+
if request.method in SAFE_METHODS or request.user.is_superuser:
|
|
115
|
+
return True
|
|
116
|
+
return super().has_object_permission(
|
|
117
|
+
request, view, view.get_parent_queryset().first()
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def has_object_permission(self, request, view, obj):
|
|
121
|
+
return super().has_object_permission(request, view, obj.user)
|
|
122
|
+
|
|
123
|
+
|
|
98
124
|
class DjangoModelPermissions(ObjectOrganizationMixin, BaseDjangoModelPermissions):
|
|
99
125
|
perms_map = {
|
|
100
126
|
"GET": ["%(app_label)s.view_%(model_name)s"],
|
|
@@ -9,9 +9,11 @@ from dj_rest_auth.serializers import (
|
|
|
9
9
|
PasswordResetSerializer as BasePasswordResetSerializer,
|
|
10
10
|
)
|
|
11
11
|
from django.conf import settings
|
|
12
|
-
from django.contrib.auth import get_user_model
|
|
12
|
+
from django.contrib.auth import get_user_model, password_validation
|
|
13
|
+
from django.contrib.auth.hashers import check_password
|
|
13
14
|
from django.contrib.auth.models import Permission
|
|
14
15
|
from django.contrib.sites.shortcuts import get_current_site
|
|
16
|
+
from django.core.exceptions import ValidationError as DjangoValidationError
|
|
15
17
|
from django.db import transaction
|
|
16
18
|
from django.db.models import Q
|
|
17
19
|
from django.utils.module_loading import import_string
|
|
@@ -275,6 +277,17 @@ class SuperUserListSerializer(BaseSuperUserSerializer):
|
|
|
275
277
|
raise serializers.ValidationError(_("This field cannot be blank."))
|
|
276
278
|
return value
|
|
277
279
|
|
|
280
|
+
def validate(self, data):
|
|
281
|
+
data = super().validate(data)
|
|
282
|
+
values = dict(data)
|
|
283
|
+
for field in self._skip_validation_fields + ["password"]:
|
|
284
|
+
values.pop(field, None)
|
|
285
|
+
try:
|
|
286
|
+
password_validation.validate_password(data["password"], user=User(**values))
|
|
287
|
+
except DjangoValidationError as error:
|
|
288
|
+
raise serializers.ValidationError({"password": error.messages})
|
|
289
|
+
return data
|
|
290
|
+
|
|
278
291
|
def create(self, validated_data):
|
|
279
292
|
group_data = validated_data.pop("groups", None)
|
|
280
293
|
org_user_data = validated_data.pop("organization_users", None)
|
|
@@ -457,7 +470,7 @@ class ChangePasswordSerializer(serializers.Serializer):
|
|
|
457
470
|
else:
|
|
458
471
|
if self.initial_data.get("new_password"):
|
|
459
472
|
to_change_user = self.context["user"]
|
|
460
|
-
if not
|
|
473
|
+
if not check_password(value, to_change_user.password):
|
|
461
474
|
raise serializers.ValidationError(
|
|
462
475
|
_(
|
|
463
476
|
"Your old password was entered incorrectly."
|
|
@@ -473,6 +486,7 @@ class ChangePasswordSerializer(serializers.Serializer):
|
|
|
473
486
|
raise serializers.ValidationError(
|
|
474
487
|
_("New password cannot be the same as your old password.")
|
|
475
488
|
)
|
|
489
|
+
password_validation.validate_password(value, user=self.context["user"])
|
|
476
490
|
return value
|
|
477
491
|
|
|
478
492
|
def save(self, **kwargs):
|
|
@@ -20,7 +20,11 @@ from rest_framework.response import Response
|
|
|
20
20
|
from rest_framework.settings import api_settings
|
|
21
21
|
from swapper import load_model
|
|
22
22
|
|
|
23
|
-
from openwisp_users.api.permissions import
|
|
23
|
+
from openwisp_users.api.permissions import (
|
|
24
|
+
CanChangeUser,
|
|
25
|
+
CanChangeUserEmail,
|
|
26
|
+
DjangoModelPermissions,
|
|
27
|
+
)
|
|
24
28
|
from openwisp_users.auth import create_auth_token
|
|
25
29
|
from openwisp_users.backends import UsersAuthenticationBackend
|
|
26
30
|
from openwisp_utils.api.pagination import OpenWispPagination
|
|
@@ -150,6 +154,8 @@ class OrganizationDetailView(BaseOrganizationView, RetrieveUpdateDestroyAPIView)
|
|
|
150
154
|
|
|
151
155
|
|
|
152
156
|
class BaseUserView(ProtectedAPIMixin):
|
|
157
|
+
permission_classes = ProtectedAPIMixin.permission_classes + (CanChangeUser,)
|
|
158
|
+
|
|
153
159
|
def get_queryset(self):
|
|
154
160
|
user = self.request.user
|
|
155
161
|
if user.is_superuser:
|
|
@@ -221,7 +227,11 @@ class ChangePasswordView(BaseUserView, UpdateAPIView):
|
|
|
221
227
|
if self.request.user.id == self.kwargs["pk"]:
|
|
222
228
|
self.permission_classes = [IsAuthenticated]
|
|
223
229
|
else:
|
|
224
|
-
self.permission_classes = [
|
|
230
|
+
self.permission_classes = [
|
|
231
|
+
IsAuthenticated,
|
|
232
|
+
DjangoModelPermissions,
|
|
233
|
+
CanChangeUser,
|
|
234
|
+
]
|
|
225
235
|
return super().get_permissions()
|
|
226
236
|
|
|
227
237
|
def get_object(self):
|
|
@@ -241,15 +251,13 @@ class ChangePasswordView(BaseUserView, UpdateAPIView):
|
|
|
241
251
|
"id": self.kwargs["pk"],
|
|
242
252
|
}
|
|
243
253
|
obj = get_object_or_404(qs, **filter_kwargs)
|
|
254
|
+
self.check_object_permissions(self.request, obj)
|
|
244
255
|
return obj
|
|
245
256
|
|
|
246
|
-
def get_serializer_context(self):
|
|
247
|
-
context = super().get_serializer_context()
|
|
248
|
-
context["user"] = self.get_object()
|
|
249
|
-
return context
|
|
250
|
-
|
|
251
257
|
def update(self, request, *args, **kwargs):
|
|
252
|
-
|
|
258
|
+
context = self.get_serializer_context()
|
|
259
|
+
context["user"] = self.get_object()
|
|
260
|
+
serializer = self.get_serializer(data=request.data, context=context)
|
|
253
261
|
serializer.is_valid(raise_exception=True)
|
|
254
262
|
serializer.save()
|
|
255
263
|
return Response(
|
|
@@ -271,6 +279,7 @@ class PasswordChangeView(ProtectedAPIMixin, BasePasswordChangeView):
|
|
|
271
279
|
|
|
272
280
|
|
|
273
281
|
class BaseEmailView(ProtectedAPIMixin, FilterByParent, GenericAPIView):
|
|
282
|
+
permission_classes = ProtectedAPIMixin.permission_classes + (CanChangeUserEmail,)
|
|
274
283
|
model = EmailAddress
|
|
275
284
|
serializer_class = EmailAddressSerializer
|
|
276
285
|
|
|
@@ -113,6 +113,11 @@ class OpenwispUsersConfig(AppConfig):
|
|
|
113
113
|
sender=Organization,
|
|
114
114
|
dispatch_uid="handle_org_is_active_change",
|
|
115
115
|
)
|
|
116
|
+
post_save.connect(
|
|
117
|
+
self.invalidate_org_membership_cache,
|
|
118
|
+
sender=Organization,
|
|
119
|
+
dispatch_uid="invalidate_org_membership_cache",
|
|
120
|
+
)
|
|
116
121
|
|
|
117
122
|
for model in [OrganizationUser, OrganizationOwner]:
|
|
118
123
|
for signal, name in signal_tuples:
|
|
@@ -174,19 +179,30 @@ class OpenwispUsersConfig(AppConfig):
|
|
|
174
179
|
record_password_based_login(request, False)
|
|
175
180
|
|
|
176
181
|
@classmethod
|
|
177
|
-
def handle_org_is_active_change(cls, instance, **kwargs):
|
|
182
|
+
def handle_org_is_active_change(cls, instance, using, **kwargs):
|
|
178
183
|
if instance._state.adding:
|
|
179
184
|
# If it's a new organization, we don't need to update any cache
|
|
180
185
|
return
|
|
181
186
|
Organization = instance._meta.model
|
|
182
187
|
try:
|
|
183
|
-
old_instance =
|
|
188
|
+
old_instance = (
|
|
189
|
+
Organization.objects.using(using).only("is_active").get(pk=instance.pk)
|
|
190
|
+
)
|
|
184
191
|
except Organization.DoesNotExist:
|
|
185
192
|
return
|
|
193
|
+
instance._is_active_changed = instance.is_active != old_instance.is_active
|
|
194
|
+
|
|
195
|
+
@classmethod
|
|
196
|
+
def invalidate_org_membership_cache(cls, instance, using, **kwargs):
|
|
197
|
+
if not getattr(instance, "_is_active_changed", False):
|
|
198
|
+
return
|
|
199
|
+
instance._is_active_changed = False
|
|
186
200
|
from .tasks import invalidate_org_membership_cache
|
|
187
201
|
|
|
188
|
-
|
|
189
|
-
|
|
202
|
+
pk = instance.pk
|
|
203
|
+
transaction.on_commit(
|
|
204
|
+
lambda: invalidate_org_membership_cache.delay(pk), using=using
|
|
205
|
+
)
|
|
190
206
|
|
|
191
207
|
@classmethod
|
|
192
208
|
def pre_save_update_organizations_dict(cls, instance, **kwargs):
|
|
@@ -129,7 +129,7 @@ class AbstractUser(BaseUser):
|
|
|
129
129
|
return str(pk)
|
|
130
130
|
|
|
131
131
|
def set_password(self, *args, **kwargs):
|
|
132
|
-
self.password_updated =
|
|
132
|
+
self.password_updated = localdate()
|
|
133
133
|
return super().set_password(*args, **kwargs)
|
|
134
134
|
|
|
135
135
|
def has_password_expired(self):
|
|
@@ -145,7 +145,7 @@ class AbstractUser(BaseUser):
|
|
|
145
145
|
)
|
|
146
146
|
else:
|
|
147
147
|
return False
|
|
148
|
-
return expiry_date <
|
|
148
|
+
return expiry_date < localdate()
|
|
149
149
|
|
|
150
150
|
def is_member(self, organization):
|
|
151
151
|
return self._get_pk(organization) in self.organizations_dict
|
|
@@ -160,10 +160,10 @@ class AbstractUser(BaseUser):
|
|
|
160
160
|
|
|
161
161
|
@cached_property
|
|
162
162
|
def is_owner_of_any_organization(self):
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return
|
|
163
|
+
OrganizationOwner = load_model("openwisp_users", "OrganizationOwner")
|
|
164
|
+
# Query ownership directly because organizations_dict excludes inactive
|
|
165
|
+
# organizations, which can still have owners.
|
|
166
|
+
return OrganizationOwner.objects.filter(organization_user__user=self).exists()
|
|
167
167
|
|
|
168
168
|
@property
|
|
169
169
|
def organizations_dict(self):
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from django.contrib.auth.hashers import check_password
|
|
1
2
|
from django.core.exceptions import ValidationError
|
|
2
3
|
from django.utils.translation import gettext_lazy as _
|
|
3
4
|
|
|
@@ -11,7 +12,7 @@ class PasswordReuseValidator:
|
|
|
11
12
|
def validate(self, password, user=None):
|
|
12
13
|
if user is None:
|
|
13
14
|
return
|
|
14
|
-
if
|
|
15
|
+
if check_password(password, user.password):
|
|
15
16
|
# The new password is same as the current password
|
|
16
17
|
raise ValidationError(
|
|
17
18
|
_("You cannot re-use your current password. Enter a new password.")
|
|
@@ -6,7 +6,7 @@ from django.db.models import Q
|
|
|
6
6
|
from django.template.loader import render_to_string
|
|
7
7
|
from django.urls import reverse
|
|
8
8
|
from django.utils import translation
|
|
9
|
-
from django.utils.timezone import
|
|
9
|
+
from django.utils.timezone import localdate, timedelta
|
|
10
10
|
from django.utils.translation import gettext_lazy as _
|
|
11
11
|
from swapper import load_model
|
|
12
12
|
|
|
@@ -30,7 +30,7 @@ def password_expiration_email():
|
|
|
30
30
|
):
|
|
31
31
|
# The password expiration feature is not enabled
|
|
32
32
|
return
|
|
33
|
-
expiry_date =
|
|
33
|
+
expiry_date = localdate() + timedelta(days=7)
|
|
34
34
|
query = Q()
|
|
35
35
|
if app_settings.USER_PASSWORD_EXPIRATION:
|
|
36
36
|
query |= Q(
|
|
@@ -54,6 +54,9 @@ def password_expiration_email():
|
|
|
54
54
|
emailaddress__verified=True,
|
|
55
55
|
)
|
|
56
56
|
.filter(query)
|
|
57
|
+
# a user can own more than one verified email address:
|
|
58
|
+
# without distinct() the join would send one email per address
|
|
59
|
+
.distinct()
|
|
57
60
|
)
|
|
58
61
|
email_count = 0
|
|
59
62
|
for user in qs.iterator():
|