diff --git a/HISTORY.rst b/HISTORY.rst index fa608f38f3..b6ca8d94c4 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -118,6 +118,7 @@ - Interpret ALLOWED_HOSTS as YAML. - Don't show the document types of an index instance. - Add the tag created and tag edited events. +- Add support for blocking the changing of password for specify users. 3.0.3 (2018-08-17) ================== diff --git a/mayan/apps/authentication/links.py b/mayan/apps/authentication/links.py index cbcccdce1e..45c67515c2 100644 --- a/mayan/apps/authentication/links.py +++ b/mayan/apps/authentication/links.py @@ -7,8 +7,11 @@ from navigation import Link from .icons import icon_logout, icon_password_change -def has_usable_password(context): - return context['request'].user.has_usable_password +def has_usable_password_and_can_change_password(context): + return ( + context['request'].user.has_usable_password and + not context['request'].user.user_options.block_password_change + ) link_logout = Link( @@ -16,6 +19,7 @@ link_logout = Link( text=_('Logout'), view='authentication:logout_view' ) link_password_change = Link( - condition=has_usable_password, icon_class=icon_password_change, - text=_('Change password'), view='authentication:password_change_view' + condition=has_usable_password_and_can_change_password, + icon_class=icon_password_change, text=_('Change password'), + view='authentication:password_change_view' ) diff --git a/mayan/apps/authentication/views.py b/mayan/apps/authentication/views.py index b969c804af..ab2bcadada 100644 --- a/mayan/apps/authentication/views.py +++ b/mayan/apps/authentication/views.py @@ -2,13 +2,14 @@ from __future__ import absolute_import, unicode_literals from django.conf import settings from django.contrib import messages -from django.contrib.auth import REDIRECT_FIELD_NAME +from django.contrib.auth import REDIRECT_FIELD_NAME, get_user_model from django.contrib.auth.views import ( login, password_change, password_reset, password_reset_confirm, password_reset_complete, password_reset_done ) -from django.http import HttpResponseRedirect +from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import redirect, resolve_url +from django.template.loader import render_to_string from django.urls import reverse from django.utils.http import is_safe_url from django.utils.translation import ugettext_lazy as _ @@ -77,6 +78,14 @@ def password_change_view(request): """ extra_context = {'title': _('Current user password change')} + if request.user.user_options.block_password_change: + messages.error( + request, _( + 'Changing the password is not allowed for this account.' + ) + ) + return HttpResponseRedirect(reverse(settings.HOME_VIEW)) + return password_change( request, extra_context=extra_context, template_name='appearance/generic_form.html', diff --git a/mayan/apps/user_management/apps.py b/mayan/apps/user_management/apps.py index 5e1df8d9ae..07bfa27160 100644 --- a/mayan/apps/user_management/apps.py +++ b/mayan/apps/user_management/apps.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from django.apps import apps from django.contrib.auth import get_user_model +from django.db.models.signals import post_save from django.utils.translation import ugettext_lazy as _ from acls import ModelPermission @@ -14,12 +15,13 @@ from metadata import MetadataLookup from navigation import SourceColumn from rest_api.fields import DynamicSerializerField +from .handlers import handler_initialize_new_user_options from .links import ( link_group_add, link_group_delete, link_group_edit, link_group_list, link_group_members, link_group_setup, link_user_add, link_user_delete, link_user_edit, link_user_groups, link_user_list, link_user_multiple_delete, link_user_multiple_set_password, - link_user_set_password, link_user_setup + link_user_set_options, link_user_set_password, link_user_setup ) from .permissions import ( permission_group_delete, permission_group_edit, @@ -116,12 +118,13 @@ class UserManagementApp(MayanAppConfig): sources=(Group,) ) menu_object.bind_links( - links=(link_acl_list, link_group_delete,), position=99, sources=(Group,) + links=(link_acl_list, link_group_delete,), position=99, + sources=(Group,) ) menu_object.bind_links( links=( link_user_edit, link_user_set_password, link_user_groups, - link_acl_list, link_user_delete + link_user_set_options, link_acl_list, link_user_delete ), sources=(User,) ) menu_secondary.bind_links( @@ -140,5 +143,10 @@ class UserManagementApp(MayanAppConfig): ) menu_setup.bind_links(links=(link_user_setup, link_group_setup)) + post_save.connect( + dispatch_uid='user_management_handler_initialize_new_user_options', + receiver=handler_initialize_new_user_options, + sender=User + ) registry.register(Group) registry.register(User) diff --git a/mayan/apps/user_management/forms.py b/mayan/apps/user_management/forms.py index 56d49e1ab1..4cd1634f51 100644 --- a/mayan/apps/user_management/forms.py +++ b/mayan/apps/user_management/forms.py @@ -6,5 +6,7 @@ from django.contrib.auth import get_user_model class UserForm(forms.ModelForm): class Meta: + fields = ( + 'username', 'first_name', 'last_name', 'email', 'is_active', + ) model = get_user_model() - fields = ('username', 'first_name', 'last_name', 'email', 'is_active',) diff --git a/mayan/apps/user_management/handlers.py b/mayan/apps/user_management/handlers.py new file mode 100644 index 0000000000..3cc3354cfc --- /dev/null +++ b/mayan/apps/user_management/handlers.py @@ -0,0 +1,12 @@ +from __future__ import unicode_literals + +from django.apps import apps + + +def handler_initialize_new_user_options(sender, instance, **kwargs): + UserOptions = apps.get_model( + app_label='user_management', model_name='UserOptions' + ) + + if kwargs['created']: + UserOptions.objects.create(user=instance) diff --git a/mayan/apps/user_management/links.py b/mayan/apps/user_management/links.py index 69b4640210..1de2c1715b 100644 --- a/mayan/apps/user_management/links.py +++ b/mayan/apps/user_management/links.py @@ -63,6 +63,10 @@ link_user_multiple_set_password = Link( permissions=(permission_user_edit,), text=_('Set password'), view='user_management:user_multiple_set_password' ) +link_user_set_options = Link( + args='object.id', permissions=(permission_user_edit,), + text=_('User options'), view='user_management:user_options', +) link_user_set_password = Link( args='object.id', permissions=(permission_user_edit,), text=_('Set password'), view='user_management:user_set_password', diff --git a/mayan/apps/user_management/migrations/0001_initial.py b/mayan/apps/user_management/migrations/0001_initial.py new file mode 100644 index 0000000000..5574469c54 --- /dev/null +++ b/mayan/apps/user_management/migrations/0001_initial.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.11 on 2018-08-26 10:01 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +def add_user_options_to_existing_users(apps, schema_editor): + User = apps.get_model(settings.AUTH_USER_MODEL) + UserOptions = apps.get_model( + app_label='user_management', model_name='UserOptions' + ) + + for user in User.objects.all(): + UserOptions.objects.create(user=user) + + +def remove_user_options_from_existing_users(apps, schema_editor): + User = apps.get_model(settings.AUTH_USER_MODEL) + UserOptions = apps.get_model( + app_label='user_management', model_name='UserOptions' + ) + + for user in User.objects.all(): + UserOptions.objects.filter(user=user).delete() + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='UserOptions', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('block_password_change', models.BooleanField(default=False, verbose_name='Forbid this user from changing their password.')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='user_options', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + options={ + 'verbose_name': 'User settings', + 'verbose_name_plural': 'Users settings', + }, + ), + migrations.RunPython( + code=add_user_options_to_existing_users, + reverse_code=remove_user_options_from_existing_users + ), + ] diff --git a/mayan/apps/user_management/migrations/__init__.py b/mayan/apps/user_management/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mayan/apps/user_management/models.py b/mayan/apps/user_management/models.py new file mode 100644 index 0000000000..0542fce604 --- /dev/null +++ b/mayan/apps/user_management/models.py @@ -0,0 +1,24 @@ +from __future__ import unicode_literals + +from django.conf import settings +from django.db import models +from django.utils.translation import ugettext_lazy as _ + + +class UserOptions(models.Model): + user = models.OneToOneField( + on_delete=models.CASCADE, related_name='user_options', + to=settings.AUTH_USER_MODEL, unique=True, verbose_name=_('User') + ) + block_password_change = models.BooleanField( + default=False, + verbose_name=_('Forbid this user from changing their password.') + ) + + class Meta: + verbose_name = _('User settings') + verbose_name_plural = _('Users settings') + + def natural_key(self): + return self.user.natural_key() + natural_key.dependencies = [settings.AUTH_USER_MODEL] diff --git a/mayan/apps/user_management/urls.py b/mayan/apps/user_management/urls.py index 583ad581b5..bfbc33e12e 100644 --- a/mayan/apps/user_management/urls.py +++ b/mayan/apps/user_management/urls.py @@ -9,7 +9,7 @@ from .api_views import ( from .views import ( GroupCreateView, GroupDeleteView, GroupEditView, GroupListView, GroupMembersView, UserCreateView, UserDeleteView, UserEditView, - UserGroupsView, UserListView, UserSetPasswordView + UserGroupsView, UserListView, UserOptionsEditView, UserSetPasswordView ) urlpatterns = [ @@ -51,6 +51,11 @@ urlpatterns = [ r'^user/(?P\d+)/groups/$', UserGroupsView.as_view(), name='user_groups' ), + url( + r'^user/(?P\d+)/options/$', + UserOptionsEditView.as_view(), + name='user_options' + ), ] api_urls = [ diff --git a/mayan/apps/user_management/views.py b/mayan/apps/user_management/views.py index 18fd2109fe..f1a66635c6 100644 --- a/mayan/apps/user_management/views.py +++ b/mayan/apps/user_management/views.py @@ -133,8 +133,10 @@ class UserCreateView(SingleObjectCreateView): class UserDeleteView(MultipleObjectConfirmActionView): - model = get_user_model() object_permission = permission_user_delete + queryset = get_user_model().objects.filter( + is_superuser=False, is_staff=False + ) success_message = _('User delete request performed on %(count)d user') success_message_plural = _( 'User delete request performed on %(count)d users' @@ -216,8 +218,11 @@ class UserGroupsView(AssignRemoveView): 'title': _('Groups of user: %s') % self.get_object() } - def get_object(self): - return get_object_or_404(get_user_model(), pk=self.kwargs['pk']) + return get_object_or_404( + get_user_model().objects.filter( + is_superuser=False, is_staff=False + ), pk=self.kwargs['pk'] + ) def left_list(self): return AssignRemoveView.generate_choices( @@ -248,6 +253,31 @@ class UserListView(SingleObjectListView): ).exclude(is_staff=True).order_by('last_name', 'first_name') +class UserOptionsEditView(SingleObjectEditView): + fields = ('block_password_change',) + object_permission = permission_user_edit + + def get_extra_context(self): + return { + 'title': _( + 'Edit options for user: %s' + ) % self.get_user() + } + + def get_object(self, queryset=None): + return self.get_user().user_options + + def get_post_action_redirect(self): + return reverse('user_management:user_list') + + def get_user(self): + return get_object_or_404( + get_user_model().objects.filter( + is_superuser=False, is_staff=False + ), pk=self.kwargs['pk'] + ) + + class UserSetPasswordView(MultipleObjectFormActionView): form_class = SetPasswordForm model = get_user_model()