from django.contrib.auth import login as auth_login
from django.contrib.auth import logout as auth_logout
from django.contrib.auth.forms import AuthenticationForm
from django.utils.encoding import force_text
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy
from ..core.config import config
from ..utils.rest_api import (
ModelViewSet,
Response,
SimpleMetadata,
ValidationError,
detail_route,
status,
)
from ..utils.views import APIView, PDFView
from .access_permissions import UserAccessPermissions
from .models import Group, User
from .pdf import users_passwords_to_pdf, users_to_pdf
from .serializers import GroupSerializer, UserFullSerializer
# Viewsets for the REST API
class UserViewSet(ModelViewSet):
"""
API endpoint for users.
There are the following views: metadata, list, retrieve, create,
partial_update, update, destroy and reset_password.
"""
access_permissions = UserAccessPermissions()
queryset = User.objects.all()
def check_view_permissions(self):
"""
Returns True if the user has required permissions.
"""
if self.action == 'retrieve':
result = self.get_access_permissions().can_retrieve(self.request.user)
elif self.action in ('metadata', 'list', 'update', 'partial_update'):
result = self.request.user.has_perm('users.can_see_name')
elif self.action in ('create', 'destroy', 'reset_password'):
result = (self.request.user.has_perm('users.can_see_name') and
self.request.user.has_perm('users.can_see_extra_data') and
self.request.user.has_perm('users.can_manage'))
else:
result = False
return result
def get_serializer_class(self):
"""
Returns different serializer classes with respect to action.
"""
if self.action in ('create', 'partial_update', 'update'):
# Return the UserFullSerializer for edit requests.
serializer_class = UserFullSerializer
else:
serializer_class = super().get_serializer_class()
return serializer_class
def list(self, request, *args, **kwargs):
"""
Customized view endpoint to list all user.
Hides the default_password for non admins.
"""
response = super().list(request, *args, **kwargs)
self.extract_default_password(response)
return response
def retrieve(self, request, *args, **kwargs):
"""
Customized view endpoint to retrieve a user.
Hides the default_password for non admins.
"""
response = super().retrieve(request, *args, **kwargs)
self.extract_default_password(response)
return response
def extract_default_password(self, response):
"""
Checks if a user is not a manager. If yes, the default password is
extracted from the response.
"""
if not self.request.user.has_perm('users.can_manage'):
if isinstance(response.data, dict):
try:
del response.data['default_password']
except KeyError:
pass
elif isinstance(response.data, list):
for user in response.data:
try:
del user['default_password']
except KeyError:
pass
def update(self, request, *args, **kwargs):
"""
Customized view endpoint to update an user.
Checks also whether the requesting user can update the user. He
needs at least the permissions 'users.can_see_name' (see
self.check_view_permissions()). Also it is evaluated whether he
wants to update himself or is manager.
"""
# Check manager perms
if (request.user.has_perm('users.can_see_extra_data') and
request.user.has_perm('users.can_manage')):
if request.data.get('is_active') is False and self.get_object() == request.user:
# A user can not deactivate himself.
raise ValidationError({'detail': _('You can not deactivate yourself.')})
response = super().update(request, *args, **kwargs)
else:
# Get user.
user = self.get_object()
# Check permissions only to update yourself.
if request.user != user:
self.permission_denied(request)
# Check permission to send only some data.
whitelist = (
'username',
'title',
'first_name',
'last_name',
'structure_level',
'about_me',)
keys = list(request.data.keys())
for key in keys:
if key not in whitelist:
# Non-staff users are allowed to send only some data. Ignore other data.
del request.data[key]
# Validate data and update user.
serializer = self.get_serializer(
user,
data=request.data,
partial=kwargs.get('partial', False))
serializer.is_valid(raise_exception=True)
serializer.save()
response = Response(serializer.data)
return response
def destroy(self, request, *args, **kwargs):
"""
Customized view endpoint to delete an user.
Ensures that no one can delete himself.
"""
instance = self.get_object()
if instance == self.request.user:
raise ValidationError({'detail': _('You can not delete yourself.')})
self.perform_destroy(instance)
return Response(status=status.HTTP_204_NO_CONTENT)
@detail_route(methods=['post'])
def reset_password(self, request, pk=None):
"""
View to reset the password using the requested password.
"""
user = self.get_object()
if request.data.get('password'):
user.default_password = request.data['password']
user.set_password(user.default_password)
user.save()
return Response({'detail': _('Password successfully reset.')})
class GroupViewSetMetadata(SimpleMetadata):
"""
Customized metadata class for OPTIONS requests.
"""
def get_field_info(self, field):
"""
Customized method to change the display name of permission choices.
"""
field_info = super().get_field_info(field)
if field.field_name == 'permissions':
field_info['choices'] = [
{
'value': choice_value,
'display_name': force_text(choice_name, strings_only=True).split(' | ')[2]
}
for choice_value, choice_name in field.choices.items()
]
return field_info
class GroupViewSet(ModelViewSet):
"""
API endpoint for groups.
There are the following views: metadata, list, retrieve, create,
partial_update, update and destroy.
"""
metadata_class = GroupViewSetMetadata
queryset = Group.objects.all()
serializer_class = GroupSerializer
def check_view_permissions(self):
"""
Returns True if the user has required permissions.
"""
if self.action in ('metadata', 'list', 'retrieve'):
# Every authenticated user can see the metadata and list or
# retrieve groups. Anonymous users can do so if they are enabled.
result = self.request.user.is_authenticated() or config['general_system_enable_anonymous']
elif self.action in ('create', 'partial_update', 'update', 'destroy'):
# Users with all app permissions can edit groups.
result = (self.request.user.has_perm('users.can_see_name') and
self.request.user.has_perm('users.can_see_extra_data') and
self.request.user.has_perm('users.can_manage'))
else:
# Deny request in any other case.
result = False
return result
def destroy(self, request, *args, **kwargs):
"""
Protects builtin groups 'Anonymous' (pk=1) and 'Registered' (pk=2)
from being deleted.
"""
instance = self.get_object()
if instance.pk in (1, 2):
self.permission_denied(request)
self.perform_destroy(instance)
return Response(status=status.HTTP_204_NO_CONTENT)
# Special API views
class UserLoginView(APIView):
"""
Login the user.
"""
http_method_names = ['get', 'post']
def post(self, *args, **kwargs):
form = AuthenticationForm(self.request, data=self.request.data)
if not form.is_valid():
raise ValidationError({'detail': _('Username or password is not correct.')})
self.user = form.get_user()
auth_login(self.request, self.user)
return super().post(*args, **kwargs)
def get_context_data(self, **context):
"""
Adds some context.
For GET requests adds login info text to context. This info text is
taken from the config. If this value is empty, a special text is used
if the admin user has the password 'admin'.
For POST requests adds the id of the current user to the context.
"""
if self.request.method == 'GET':
if config['general_login_info_text']:
context['info_text'] = config['general_login_info_text']
else:
try:
user = User.objects.get(username='admin')
except User.DoesNotExist:
context['info_text'] = ''
else:
if user.check_password('admin'):
context['info_text'] = _(
'Installation was successfully. Use {username} and '
'{password} for first login. Important: Please change '
'your password!').format(
username='admin',
password='admin')
else:
context['info_text'] = ''
else:
# self.request.method == 'POST'
context['user_id'] = self.user.pk
return super().get_context_data(**context)
class UserLogoutView(APIView):
"""
Logout the user.
"""
http_method_names = ['post']
def post(self, *args, **kwargs):
if not self.request.user.is_authenticated():
raise ValidationError({'detail': _('You are not authenticated.')})
auth_logout(self.request)
return super().post(*args, **kwargs)
class WhoAmIView(APIView):
"""
Returns the id of the requesting user.
"""
http_method_names = ['get']
def get_context_data(self, **context):
"""
Appends the user id to the context. Uses None for the anonymous
user. Appends also a flag if guest users are enabled in the config.
"""
return super().get_context_data(
user_id=self.request.user.pk,
guest_enabled=config['general_system_enable_anonymous'],
**context)
class SetPasswordView(APIView):
"""
Users can set a new password for themselves.
"""
http_method_names = ['post']
def post(self, request, *args, **kwargs):
user = request.user
if user.check_password(request.data['old_password']):
user.set_password(request.data['new_password'])
user.save()
else:
raise ValidationError({'detail': _('Old password does not match.')})
return super().post(request, *args, **kwargs)
# Views to generate PDFs
class UsersListPDF(PDFView):
"""
Generate a list of all users as PDF.
"""
required_permission = 'users.can_see_extra_data'
filename = ugettext_lazy('user-list')
document_title = ugettext_lazy('List of users')
def append_to_pdf(self, pdf):
"""
Append PDF objects.
"""
users_to_pdf(pdf)
class UsersPasswordsPDF(PDFView):
"""
Generate the access data welcome paper for all users as PDF.
"""
required_permission = 'users.can_manage'
filename = ugettext_lazy('user-access-data')
top_space = 0
def build_document(self, pdf_document, story):
pdf_document.build(story)
def append_to_pdf(self, pdf):
"""
Append PDF objects.
"""
users_passwords_to_pdf(pdf)