2017-11-28 10:47:29 +01:00
|
|
|
import smtplib
|
2018-10-09 22:00:55 +02:00
|
|
|
import textwrap
|
2018-08-22 22:00:08 +02:00
|
|
|
from typing import List
|
2017-09-04 00:25:45 +02:00
|
|
|
|
2018-07-09 23:22:26 +02:00
|
|
|
from asgiref.sync import async_to_sync
|
2017-11-28 10:47:29 +01:00
|
|
|
from django.conf import settings
|
2018-07-09 23:22:26 +02:00
|
|
|
from django.contrib.auth import (
|
|
|
|
login as auth_login,
|
|
|
|
logout as auth_logout,
|
|
|
|
update_session_auth_hash,
|
|
|
|
)
|
2015-06-16 10:37:23 +02:00
|
|
|
from django.contrib.auth.forms import AuthenticationForm
|
2017-04-13 16:19:20 +02:00
|
|
|
from django.contrib.auth.password_validation import validate_password
|
2018-10-09 22:00:55 +02:00
|
|
|
from django.contrib.auth.tokens import default_token_generator
|
|
|
|
from django.contrib.sites.shortcuts import get_current_site
|
2017-11-28 10:47:29 +01:00
|
|
|
from django.core import mail
|
2017-04-13 16:19:20 +02:00
|
|
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
2017-04-19 09:28:21 +02:00
|
|
|
from django.db import transaction
|
2018-10-09 22:00:55 +02:00
|
|
|
from django.utils.encoding import force_bytes, force_text
|
|
|
|
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
|
2015-06-18 22:39:58 +02:00
|
|
|
from django.utils.translation import ugettext as _
|
2014-10-11 14:34:49 +02:00
|
|
|
|
2015-09-16 00:55:27 +02:00
|
|
|
from ..core.config import config
|
2017-02-21 09:34:24 +01:00
|
|
|
from ..core.signals import permission_change
|
2018-07-09 23:22:26 +02:00
|
|
|
from ..utils.auth import (
|
2018-10-09 13:44:38 +02:00
|
|
|
GROUP_ADMIN_PK,
|
|
|
|
GROUP_DEFAULT_PK,
|
2018-07-09 23:22:26 +02:00
|
|
|
anonymous_is_enabled,
|
|
|
|
has_perm,
|
|
|
|
)
|
2017-04-19 09:28:21 +02:00
|
|
|
from ..utils.autoupdate import (
|
2018-11-03 23:40:20 +01:00
|
|
|
Element,
|
2017-04-19 09:28:21 +02:00
|
|
|
inform_changed_data,
|
2018-11-03 23:40:20 +01:00
|
|
|
inform_changed_elements,
|
2017-04-19 09:28:21 +02:00
|
|
|
)
|
2018-07-09 23:22:26 +02:00
|
|
|
from ..utils.cache import element_cache
|
2015-11-06 15:44:27 +01:00
|
|
|
from ..utils.rest_api import (
|
|
|
|
ModelViewSet,
|
|
|
|
Response,
|
2016-01-25 22:35:23 +01:00
|
|
|
SimpleMetadata,
|
2015-11-06 15:44:27 +01:00
|
|
|
ValidationError,
|
|
|
|
detail_route,
|
2017-04-19 09:28:21 +02:00
|
|
|
list_route,
|
2015-11-06 15:44:27 +01:00
|
|
|
status,
|
|
|
|
)
|
2016-10-01 14:26:28 +02:00
|
|
|
from ..utils.views import APIView
|
2017-05-23 14:07:06 +02:00
|
|
|
from .access_permissions import (
|
|
|
|
GroupAccessPermissions,
|
|
|
|
PersonalNoteAccessPermissions,
|
|
|
|
UserAccessPermissions,
|
|
|
|
)
|
|
|
|
from .models import Group, PersonalNote, User
|
2017-02-21 09:34:24 +01:00
|
|
|
from .serializers import GroupSerializer, PermissionRelatedField
|
2011-07-31 10:46:29 +02:00
|
|
|
|
2012-07-07 15:26:00 +02:00
|
|
|
|
2015-07-01 23:18:48 +02:00
|
|
|
# Viewsets for the REST API
|
2012-08-10 19:49:46 +02:00
|
|
|
|
2015-02-12 18:48:14 +01:00
|
|
|
class UserViewSet(ModelViewSet):
|
2015-01-06 00:11:22 +01:00
|
|
|
"""
|
2015-07-01 23:18:48 +02:00
|
|
|
API endpoint for users.
|
|
|
|
|
2015-08-31 14:07:24 +02:00
|
|
|
There are the following views: metadata, list, retrieve, create,
|
|
|
|
partial_update, update, destroy and reset_password.
|
2015-01-06 00:11:22 +01:00
|
|
|
"""
|
2016-02-11 22:58:32 +01:00
|
|
|
access_permissions = UserAccessPermissions()
|
2015-01-06 00:11:22 +01:00
|
|
|
queryset = User.objects.all()
|
|
|
|
|
2015-07-01 23:18:48 +02:00
|
|
|
def check_view_permissions(self):
|
2015-01-06 00:11:22 +01:00
|
|
|
"""
|
2015-07-01 23:18:48 +02:00
|
|
|
Returns True if the user has required permissions.
|
2015-01-06 00:11:22 +01:00
|
|
|
"""
|
2016-09-17 22:26:23 +02:00
|
|
|
if self.action in ('list', 'retrieve'):
|
|
|
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
2017-02-10 14:51:44 +01:00
|
|
|
elif self.action == 'metadata':
|
2017-01-26 15:34:24 +01:00
|
|
|
result = has_perm(self.request.user, 'users.can_see_name')
|
2017-02-10 14:51:44 +01:00
|
|
|
elif self.action in ('update', 'partial_update'):
|
2018-07-09 23:22:26 +02:00
|
|
|
result = self.request.user.is_authenticated
|
2017-11-28 10:47:29 +01:00
|
|
|
elif self.action in ('create', 'destroy', 'reset_password', 'mass_import', 'mass_invite_email'):
|
2017-01-26 15:34:24 +01:00
|
|
|
result = (has_perm(self.request.user, 'users.can_see_name') and
|
|
|
|
has_perm(self.request.user, 'users.can_see_extra_data') and
|
|
|
|
has_perm(self.request.user, 'users.can_manage'))
|
2015-07-01 23:18:48 +02:00
|
|
|
else:
|
|
|
|
result = False
|
|
|
|
return result
|
2015-01-06 00:11:22 +01:00
|
|
|
|
2015-09-06 10:29:23 +02:00
|
|
|
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.
|
|
|
|
"""
|
2018-05-16 07:51:40 +02:00
|
|
|
user = self.get_object()
|
2017-02-24 15:04:12 +01:00
|
|
|
# Check permissions.
|
|
|
|
if (has_perm(self.request.user, 'users.can_see_name') and
|
|
|
|
has_perm(request.user, 'users.can_see_extra_data') and
|
2017-01-26 15:34:24 +01:00
|
|
|
has_perm(request.user, 'users.can_manage')):
|
2017-02-24 15:04:12 +01:00
|
|
|
# The user has all permissions so he may update every user.
|
2018-05-16 07:51:40 +02:00
|
|
|
if request.data.get('is_active') is False and user == request.user:
|
2017-02-24 15:04:12 +01:00
|
|
|
# But a user can not deactivate himself.
|
2016-01-09 11:59:34 +01:00
|
|
|
raise ValidationError({'detail': _('You can not deactivate yourself.')})
|
2015-09-06 10:29:23 +02:00
|
|
|
else:
|
2017-02-24 15:04:12 +01:00
|
|
|
# The user does not have all permissions so he may only update himself.
|
2016-08-31 16:53:02 +02:00
|
|
|
if str(request.user.pk) != self.kwargs['pk']:
|
2015-09-06 10:29:23 +02:00
|
|
|
self.permission_denied(request)
|
2018-07-09 23:22:26 +02:00
|
|
|
|
|
|
|
# This is a hack to make request.data mutable. Otherwise fields can not be deleted.
|
|
|
|
request.data._mutable = True
|
2017-02-24 15:04:12 +01:00
|
|
|
# Remove fields that the user is not allowed to change.
|
|
|
|
# The list() is required because we want to use del inside the loop.
|
|
|
|
for key in list(request.data.keys()):
|
|
|
|
if key not in ('username', 'about_me'):
|
|
|
|
del request.data[key]
|
2016-08-31 16:53:02 +02:00
|
|
|
response = super().update(request, *args, **kwargs)
|
2018-05-16 07:51:40 +02:00
|
|
|
# Maybe some group assignments have changed. Better delete the restricted user cache
|
2018-11-03 23:40:20 +01:00
|
|
|
async_to_sync(element_cache.del_user)(user.pk)
|
2015-09-06 10:29:23 +02:00
|
|
|
return response
|
|
|
|
|
2016-01-09 11:59:34 +01:00
|
|
|
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)
|
|
|
|
|
2015-06-18 22:39:58 +02:00
|
|
|
@detail_route(methods=['post'])
|
|
|
|
def reset_password(self, request, pk=None):
|
|
|
|
"""
|
2016-01-10 13:47:59 +01:00
|
|
|
View to reset the password using the requested password.
|
2015-06-18 22:39:58 +02:00
|
|
|
"""
|
|
|
|
user = self.get_object()
|
2016-08-25 11:40:37 +02:00
|
|
|
if isinstance(request.data.get('password'), str):
|
2017-04-13 16:19:20 +02:00
|
|
|
try:
|
|
|
|
validate_password(request.data.get('password'), user=request.user)
|
|
|
|
except DjangoValidationError as errors:
|
|
|
|
raise ValidationError({'detail': ' '.join(errors)})
|
2016-08-25 11:40:37 +02:00
|
|
|
user.set_password(request.data.get('password'))
|
|
|
|
user.save()
|
|
|
|
return Response({'detail': _('Password successfully reset.')})
|
|
|
|
else:
|
|
|
|
raise ValidationError({'detail': 'Password has to be a string.'})
|
2015-06-18 22:39:58 +02:00
|
|
|
|
2017-04-19 09:28:21 +02:00
|
|
|
@list_route(methods=['post'])
|
|
|
|
@transaction.atomic
|
|
|
|
def mass_import(self, request):
|
|
|
|
"""
|
|
|
|
API endpoint to create multiple users at once.
|
|
|
|
|
|
|
|
Example: {"users": [{"first_name": "Max"}, {"first_name": "Maxi"}]}
|
|
|
|
"""
|
|
|
|
users = request.data.get('users')
|
|
|
|
if not isinstance(users, list):
|
|
|
|
raise ValidationError({'detail': 'Users has to be a list.'})
|
|
|
|
|
|
|
|
created_users = []
|
|
|
|
# List of all track ids of all imported users. The track ids are just used in the client.
|
|
|
|
imported_track_ids = []
|
|
|
|
|
|
|
|
for user in users:
|
|
|
|
serializer = self.get_serializer(data=user)
|
|
|
|
try:
|
|
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
except ValidationError:
|
|
|
|
# Skip invalid users.
|
|
|
|
continue
|
|
|
|
data = serializer.prepare_password(serializer.data)
|
|
|
|
groups = data['groups_id']
|
|
|
|
del data['groups_id']
|
|
|
|
|
|
|
|
db_user = User(**data)
|
|
|
|
db_user.save(skip_autoupdate=True)
|
|
|
|
db_user.groups.add(*groups)
|
|
|
|
created_users.append(db_user)
|
|
|
|
if 'importTrackId' in user:
|
|
|
|
imported_track_ids.append(user['importTrackId'])
|
|
|
|
|
|
|
|
# Now infom all clients and send a response
|
|
|
|
inform_changed_data(created_users)
|
|
|
|
return Response({
|
|
|
|
'detail': _('{number} users successfully imported.').format(number=len(created_users)),
|
|
|
|
'importedTrackIds': imported_track_ids})
|
|
|
|
|
2017-11-28 10:47:29 +01:00
|
|
|
@list_route(methods=['post'])
|
|
|
|
def mass_invite_email(self, request):
|
|
|
|
"""
|
|
|
|
Endpoint to send invitation emails to all given users (by id). Returns the
|
|
|
|
number of emails send.
|
|
|
|
"""
|
|
|
|
user_ids = request.data.get('user_ids')
|
|
|
|
if not isinstance(user_ids, list):
|
|
|
|
raise ValidationError({'detail': 'User_ids has to be a list.'})
|
|
|
|
for user_id in user_ids:
|
|
|
|
if not isinstance(user_id, int):
|
|
|
|
raise ValidationError({'detail': 'User_id has to be an int.'})
|
2018-02-02 12:29:18 +01:00
|
|
|
# Get subject and body from the response. Do not use the config values
|
|
|
|
# because they might not be translated.
|
|
|
|
subject = request.data.get('subject')
|
|
|
|
message = request.data.get('message')
|
|
|
|
if not isinstance(subject, str):
|
|
|
|
raise ValidationError({'detail': 'Subject has to be a string.'})
|
|
|
|
if not isinstance(message, str):
|
|
|
|
raise ValidationError({'detail': 'Message has to be a string.'})
|
2017-11-28 10:47:29 +01:00
|
|
|
users = User.objects.filter(pk__in=user_ids)
|
|
|
|
|
|
|
|
# Sending Emails. Keep track, which users gets an email.
|
|
|
|
# First, try to open the connection to the smtp server.
|
|
|
|
connection = mail.get_connection(fail_silently=False)
|
|
|
|
try:
|
|
|
|
connection.open()
|
|
|
|
except ConnectionRefusedError:
|
|
|
|
raise ValidationError({'detail': 'Cannot connect to SMTP server on {}:{}'.format(
|
|
|
|
settings.EMAIL_HOST,
|
|
|
|
settings.EMAIL_PORT)})
|
|
|
|
except smtplib.SMTPException as e:
|
|
|
|
raise ValidationError({'detail': '{}: {}'.format(e.errno, e.strerror)})
|
|
|
|
|
|
|
|
success_users = []
|
2018-01-09 11:01:51 +01:00
|
|
|
user_pks_without_email = []
|
2017-11-28 10:47:29 +01:00
|
|
|
try:
|
|
|
|
for user in users:
|
2018-01-09 11:01:51 +01:00
|
|
|
if user.email:
|
2018-02-02 12:29:18 +01:00
|
|
|
if user.send_invitation_email(connection, subject, message, skip_autoupdate=True):
|
2018-01-09 11:01:51 +01:00
|
|
|
success_users.append(user)
|
|
|
|
else:
|
|
|
|
user_pks_without_email.append(user.pk)
|
2017-11-28 10:47:29 +01:00
|
|
|
except DjangoValidationError as e:
|
|
|
|
raise ValidationError(e.message_dict)
|
|
|
|
|
|
|
|
connection.close()
|
|
|
|
inform_changed_data(success_users)
|
2018-01-09 11:01:51 +01:00
|
|
|
return Response({
|
|
|
|
'count': len(success_users),
|
|
|
|
'no_email_ids': user_pks_without_email})
|
2017-11-28 10:47:29 +01:00
|
|
|
|
2015-01-06 00:11:22 +01:00
|
|
|
|
2016-01-25 22:35:23 +01:00
|
|
|
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':
|
2016-08-29 17:05:06 +02:00
|
|
|
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()
|
|
|
|
]
|
2016-01-25 22:35:23 +01:00
|
|
|
return field_info
|
|
|
|
|
|
|
|
|
2015-02-12 18:48:14 +01:00
|
|
|
class GroupViewSet(ModelViewSet):
|
2015-02-04 00:08:38 +01:00
|
|
|
"""
|
2015-07-01 23:18:48 +02:00
|
|
|
API endpoint for groups.
|
|
|
|
|
2015-08-31 14:07:24 +02:00
|
|
|
There are the following views: metadata, list, retrieve, create,
|
|
|
|
partial_update, update and destroy.
|
2015-02-04 00:08:38 +01:00
|
|
|
"""
|
2016-01-25 22:35:23 +01:00
|
|
|
metadata_class = GroupViewSetMetadata
|
2016-12-17 09:30:20 +01:00
|
|
|
queryset = Group.objects.all()
|
2015-02-04 00:08:38 +01:00
|
|
|
serializer_class = GroupSerializer
|
2016-12-17 09:30:20 +01:00
|
|
|
access_permissions = GroupAccessPermissions()
|
2015-02-04 00:08:38 +01:00
|
|
|
|
2015-07-01 23:18:48 +02:00
|
|
|
def check_view_permissions(self):
|
2015-02-04 00:08:38 +01:00
|
|
|
"""
|
2015-07-01 23:18:48 +02:00
|
|
|
Returns True if the user has required permissions.
|
2015-02-04 00:08:38 +01:00
|
|
|
"""
|
2016-12-17 09:30:20 +01:00
|
|
|
if self.action in ('list', 'retrieve'):
|
|
|
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
|
|
|
elif self.action == 'metadata':
|
|
|
|
# Every authenticated user can see the metadata.
|
|
|
|
# Anonymous users can do so if they are enabled.
|
2018-07-09 23:22:26 +02:00
|
|
|
result = self.request.user.is_authenticated or anonymous_is_enabled()
|
2015-07-01 23:18:48 +02:00
|
|
|
elif self.action in ('create', 'partial_update', 'update', 'destroy'):
|
|
|
|
# Users with all app permissions can edit groups.
|
2017-01-26 15:34:24 +01:00
|
|
|
result = (has_perm(self.request.user, 'users.can_see_name') and
|
|
|
|
has_perm(self.request.user, 'users.can_see_extra_data') and
|
|
|
|
has_perm(self.request.user, 'users.can_manage'))
|
2015-07-01 23:18:48 +02:00
|
|
|
else:
|
|
|
|
# Deny request in any other case.
|
|
|
|
result = False
|
|
|
|
return result
|
2015-02-04 00:08:38 +01:00
|
|
|
|
2017-03-06 16:34:20 +01:00
|
|
|
def update(self, request, *args, **kwargs):
|
2015-02-17 00:45:53 +01:00
|
|
|
"""
|
2017-03-06 16:34:20 +01:00
|
|
|
Customized endpoint to update a group. Send the signal
|
|
|
|
'permission_change' if group permissions change.
|
2015-02-17 00:45:53 +01:00
|
|
|
"""
|
2017-03-06 16:34:20 +01:00
|
|
|
group = self.get_object()
|
2015-02-17 00:45:53 +01:00
|
|
|
|
2017-03-06 16:34:20 +01:00
|
|
|
# Collect old and new (given) permissions to get the difference.
|
|
|
|
old_permissions = list(group.permissions.all()) # Force evaluation so the perms don't change anymore.
|
2017-02-21 09:34:24 +01:00
|
|
|
permission_names = request.data['permissions']
|
|
|
|
if isinstance(permission_names, str):
|
|
|
|
permission_names = [permission_names]
|
|
|
|
given_permissions = [
|
2017-03-06 16:34:20 +01:00
|
|
|
PermissionRelatedField(read_only=True).to_internal_value(data=perm) for perm in permission_names]
|
2017-02-21 09:34:24 +01:00
|
|
|
|
2017-03-06 16:34:20 +01:00
|
|
|
# Run super to update the group.
|
2017-02-21 09:34:24 +01:00
|
|
|
response = super().update(request, *args, **kwargs)
|
|
|
|
|
2017-03-06 16:34:20 +01:00
|
|
|
# Check status code and send 'permission_change' signal.
|
2017-02-21 09:34:24 +01:00
|
|
|
if response.status_code == 200:
|
2017-03-06 16:34:20 +01:00
|
|
|
|
2018-05-16 07:51:40 +02:00
|
|
|
# Delete the user chaches of all affected users
|
|
|
|
for user in group.user_set.all():
|
2018-11-03 23:40:20 +01:00
|
|
|
async_to_sync(element_cache.del_user)(user.pk)
|
2018-05-16 07:51:40 +02:00
|
|
|
|
2017-03-06 16:34:20 +01:00
|
|
|
def diff(full, part):
|
|
|
|
"""
|
|
|
|
This helper function calculates the difference of two lists:
|
|
|
|
The result is a list of all elements of 'full' that are
|
|
|
|
not in 'part'.
|
|
|
|
"""
|
|
|
|
part = set(part)
|
|
|
|
return [item for item in full if item not in part]
|
|
|
|
|
2017-02-21 09:34:24 +01:00
|
|
|
new_permissions = diff(given_permissions, old_permissions)
|
|
|
|
|
2017-03-06 16:34:20 +01:00
|
|
|
# Some permissions are added.
|
|
|
|
if len(new_permissions) > 0:
|
2018-11-03 23:40:20 +01:00
|
|
|
elements: List[Element] = []
|
2017-03-06 16:34:20 +01:00
|
|
|
signal_results = permission_change.send(None, permissions=new_permissions, action='added')
|
2018-11-01 17:30:18 +01:00
|
|
|
all_full_data = async_to_sync(element_cache.get_all_full_data)()
|
2017-03-06 16:34:20 +01:00
|
|
|
for receiver, signal_collections in signal_results:
|
2018-07-09 23:22:26 +02:00
|
|
|
for cachable in signal_collections:
|
2018-11-03 23:40:20 +01:00
|
|
|
for full_data in all_full_data.get(cachable.get_collection_string(), {}):
|
|
|
|
elements.append(Element(id=full_data['id'], collection_string=cachable.get_collection_string(), full_data=full_data))
|
|
|
|
inform_changed_elements(elements)
|
2017-03-06 16:34:20 +01:00
|
|
|
|
|
|
|
# TODO: Some permissions are deleted.
|
2017-02-21 09:34:24 +01:00
|
|
|
|
|
|
|
return response
|
|
|
|
|
2017-03-06 16:34:20 +01:00
|
|
|
def destroy(self, request, *args, **kwargs):
|
|
|
|
"""
|
2018-10-09 13:44:38 +02:00
|
|
|
Protects builtin groups 'Default' (pk=1) and 'Admin' (pk=2) from being deleted.
|
2017-03-06 16:34:20 +01:00
|
|
|
"""
|
|
|
|
instance = self.get_object()
|
2018-10-09 13:44:38 +02:00
|
|
|
if instance.pk in (GROUP_DEFAULT_PK, GROUP_ADMIN_PK):
|
2017-03-06 16:34:20 +01:00
|
|
|
self.permission_denied(request)
|
2018-05-16 13:03:37 +02:00
|
|
|
# The list() is required to evaluate the query
|
|
|
|
affected_users_ids = list(instance.user_set.values_list('pk', flat=True))
|
|
|
|
|
|
|
|
# Delete the group
|
2017-03-06 16:34:20 +01:00
|
|
|
self.perform_destroy(instance)
|
2018-05-16 13:03:37 +02:00
|
|
|
|
|
|
|
# Get the updated user data from the DB.
|
|
|
|
affected_users = User.objects.filter(pk__in=affected_users_ids)
|
|
|
|
inform_changed_data(affected_users)
|
2017-03-06 16:34:20 +01:00
|
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
|
|
|
2015-02-04 00:08:38 +01:00
|
|
|
|
2017-05-23 14:07:06 +02:00
|
|
|
class PersonalNoteViewSet(ModelViewSet):
|
|
|
|
"""
|
|
|
|
API endpoint for personal notes.
|
|
|
|
|
|
|
|
There are the following views: metadata, list, retrieve, create,
|
|
|
|
partial_update, update, and destroy.
|
|
|
|
"""
|
|
|
|
access_permissions = PersonalNoteAccessPermissions()
|
|
|
|
queryset = PersonalNote.objects.all()
|
|
|
|
|
|
|
|
def check_view_permissions(self):
|
|
|
|
"""
|
|
|
|
Returns True if the user has required permissions.
|
|
|
|
"""
|
|
|
|
if self.action in ('list', 'retrieve'):
|
|
|
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
|
|
|
elif self.action in ('metadata', 'create', 'partial_update', 'update', 'destroy'):
|
|
|
|
# Every authenticated user can see metadata and create personal
|
|
|
|
# notes for himself and can manipulate only his own personal notes.
|
|
|
|
# See self.perform_create(), self.update() and self.destroy().
|
2018-07-09 23:22:26 +02:00
|
|
|
result = self.request.user.is_authenticated
|
2017-05-23 14:07:06 +02:00
|
|
|
else:
|
|
|
|
result = False
|
|
|
|
return result
|
|
|
|
|
|
|
|
def perform_create(self, serializer):
|
|
|
|
"""
|
|
|
|
Customized method to inject the request.user into serializer's save
|
|
|
|
method so that the request.user can be saved into the model field.
|
|
|
|
"""
|
|
|
|
serializer.save(user=self.request.user)
|
|
|
|
|
|
|
|
def update(self, request, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
Customized method to ensure that every user can change only his own
|
|
|
|
personal notes.
|
|
|
|
"""
|
|
|
|
if self.get_object().user != self.request.user:
|
|
|
|
self.permission_denied(request)
|
|
|
|
return super().update(request, *args, **kwargs)
|
|
|
|
|
|
|
|
def destroy(self, request, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
Customized method to ensure that every user can delete only his own
|
|
|
|
personal notes.
|
|
|
|
"""
|
|
|
|
if self.get_object().user != self.request.user:
|
|
|
|
self.permission_denied(request)
|
|
|
|
return super().destroy(request, *args, **kwargs)
|
|
|
|
|
|
|
|
|
2015-07-01 23:18:48 +02:00
|
|
|
# Special API views
|
2015-02-12 22:42:54 +01:00
|
|
|
|
|
|
|
class UserLoginView(APIView):
|
|
|
|
"""
|
2015-09-16 00:55:27 +02:00
|
|
|
Login the user.
|
2015-02-12 22:42:54 +01:00
|
|
|
"""
|
2016-01-09 01:10:37 +01:00
|
|
|
http_method_names = ['get', 'post']
|
2015-02-12 22:42:54 +01:00
|
|
|
|
|
|
|
def post(self, *args, **kwargs):
|
2016-12-19 14:14:46 +01:00
|
|
|
# If the client tells that cookies are disabled, do not continue as guest (if enabled)
|
|
|
|
if not self.request.data.get('cookies', True):
|
|
|
|
raise ValidationError({'detail': _('Cookies have to be enabled to use OpenSlides.')})
|
2015-02-12 22:42:54 +01:00
|
|
|
form = AuthenticationForm(self.request, data=self.request.data)
|
2015-12-11 16:28:56 +01:00
|
|
|
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)
|
2015-02-12 22:42:54 +01:00
|
|
|
return super().post(*args, **kwargs)
|
|
|
|
|
|
|
|
def get_context_data(self, **context):
|
2016-01-09 01:10:37 +01:00
|
|
|
"""
|
|
|
|
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 '
|
2016-01-30 10:04:20 +01:00
|
|
|
'your password!').format(
|
2016-01-09 01:10:37 +01:00
|
|
|
username='<strong>admin</strong>',
|
2016-01-30 10:04:20 +01:00
|
|
|
password='<strong>admin</strong>')
|
2016-01-09 01:10:37 +01:00
|
|
|
else:
|
|
|
|
context['info_text'] = ''
|
2018-09-06 07:14:55 +02:00
|
|
|
# Add the privacy policy and legal notice, so the client can display it
|
|
|
|
# even, it is not logged in.
|
2018-08-31 12:37:47 +02:00
|
|
|
context['privacy_policy'] = config['general_event_privacy_policy']
|
2018-09-06 07:14:55 +02:00
|
|
|
context['legal_notice'] = config['general_event_legal_notice']
|
2016-01-09 01:10:37 +01:00
|
|
|
else:
|
|
|
|
# self.request.method == 'POST'
|
|
|
|
context['user_id'] = self.user.pk
|
2018-11-01 17:30:18 +01:00
|
|
|
context['user'] = async_to_sync(element_cache.get_element_restricted_data)(
|
2018-11-03 23:40:20 +01:00
|
|
|
self.user.pk or 0,
|
2018-11-01 17:30:18 +01:00
|
|
|
self.user.get_collection_string(),
|
|
|
|
self.user.pk)
|
2015-02-12 22:42:54 +01:00
|
|
|
return super().get_context_data(**context)
|
|
|
|
|
|
|
|
|
|
|
|
class UserLogoutView(APIView):
|
|
|
|
"""
|
2015-09-16 00:55:27 +02:00
|
|
|
Logout the user.
|
2015-02-12 22:42:54 +01:00
|
|
|
"""
|
|
|
|
http_method_names = ['post']
|
|
|
|
|
|
|
|
def post(self, *args, **kwargs):
|
2018-07-09 23:22:26 +02:00
|
|
|
if not self.request.user.is_authenticated:
|
2015-12-11 16:28:56 +01:00
|
|
|
raise ValidationError({'detail': _('You are not authenticated.')})
|
2015-02-12 22:42:54 +01:00
|
|
|
auth_logout(self.request)
|
|
|
|
return super().post(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
class WhoAmIView(APIView):
|
|
|
|
"""
|
2015-07-01 23:18:48 +02:00
|
|
|
Returns the id of the requesting user.
|
2015-02-12 22:42:54 +01:00
|
|
|
"""
|
|
|
|
http_method_names = ['get']
|
|
|
|
|
|
|
|
def get_context_data(self, **context):
|
|
|
|
"""
|
2015-12-10 00:02:16 +01:00
|
|
|
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.
|
2017-01-14 09:14:42 +01:00
|
|
|
Appends also the serialized user if available.
|
2015-02-12 22:42:54 +01:00
|
|
|
"""
|
2018-11-03 23:40:20 +01:00
|
|
|
user_id = self.request.user.pk or 0
|
|
|
|
if user_id:
|
2018-11-01 17:30:18 +01:00
|
|
|
user_data = async_to_sync(element_cache.get_element_restricted_data)(
|
2018-11-03 23:40:20 +01:00
|
|
|
user_id,
|
2018-11-01 17:30:18 +01:00
|
|
|
self.request.user.get_collection_string(),
|
|
|
|
user_id)
|
2017-01-14 09:14:42 +01:00
|
|
|
else:
|
|
|
|
user_data = None
|
2015-02-12 22:42:54 +01:00
|
|
|
return super().get_context_data(
|
2018-11-03 23:40:20 +01:00
|
|
|
user_id=user_id or None,
|
2017-01-15 13:33:54 +01:00
|
|
|
guest_enabled=anonymous_is_enabled(),
|
2017-01-14 09:14:42 +01:00
|
|
|
user=user_data,
|
2015-02-12 22:42:54 +01:00
|
|
|
**context)
|
2015-07-01 23:18:48 +02:00
|
|
|
|
|
|
|
|
2015-11-06 15:44:27 +01:00
|
|
|
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']):
|
2017-04-13 16:19:20 +02:00
|
|
|
try:
|
|
|
|
validate_password(request.data.get('new_password'), user=user)
|
|
|
|
except DjangoValidationError as errors:
|
|
|
|
raise ValidationError({'detail': ' '.join(errors)})
|
2015-11-06 15:44:27 +01:00
|
|
|
user.set_password(request.data['new_password'])
|
|
|
|
user.save()
|
2017-02-10 14:51:44 +01:00
|
|
|
update_session_auth_hash(request, user)
|
2015-11-06 15:44:27 +01:00
|
|
|
else:
|
2015-12-11 16:28:56 +01:00
|
|
|
raise ValidationError({'detail': _('Old password does not match.')})
|
2015-11-06 15:44:27 +01:00
|
|
|
return super().post(request, *args, **kwargs)
|
2018-10-09 22:00:55 +02:00
|
|
|
|
|
|
|
|
|
|
|
class PasswordResetView(APIView):
|
|
|
|
"""
|
|
|
|
Users can send an email to themselves to get a password reset email.
|
|
|
|
|
|
|
|
Send POST request with {'email': <email addresss>} and all users with this
|
|
|
|
address will receive an email (means Django sends one or more emails to
|
|
|
|
this address) with a one-use only link.
|
|
|
|
"""
|
|
|
|
http_method_names = ['post']
|
2018-10-16 08:08:59 +02:00
|
|
|
use_https = False # TODO: Do we use https?
|
2018-10-09 22:00:55 +02:00
|
|
|
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
Loop over all users and send emails.
|
|
|
|
"""
|
|
|
|
to_email = request.data.get('email')
|
|
|
|
for user in self.get_users(to_email):
|
|
|
|
current_site = get_current_site(request)
|
|
|
|
site_name = current_site.name
|
|
|
|
context = {
|
|
|
|
'email': to_email,
|
|
|
|
'site_name': site_name,
|
|
|
|
'protocol': 'https' if self.use_https else 'http',
|
|
|
|
'domain': current_site.domain,
|
2018-10-16 08:08:59 +02:00
|
|
|
'path': '/login/reset-password-confirm/',
|
2018-10-09 22:00:55 +02:00
|
|
|
'user_id': urlsafe_base64_encode(force_bytes(user.pk)).decode(),
|
|
|
|
'token': default_token_generator.make_token(user),
|
|
|
|
'username': user.get_username(),
|
|
|
|
}
|
|
|
|
# Send a django.core.mail.EmailMessage to `to_email`.
|
|
|
|
subject = _('Password reset for {}').format(site_name)
|
|
|
|
subject = ''.join(subject.splitlines())
|
|
|
|
body = self.get_email_body(**context)
|
|
|
|
from_email = None # TODO: Add nice from_email here.
|
|
|
|
email_message = mail.EmailMessage(subject, body, from_email, [to_email])
|
|
|
|
email_message.send()
|
|
|
|
return super().post(request, *args, **kwargs)
|
|
|
|
|
|
|
|
def get_users(self, email):
|
|
|
|
"""Given an email, return matching user(s) who should receive a reset.
|
|
|
|
|
|
|
|
This allows subclasses to more easily customize the default policies
|
|
|
|
that prevent inactive users and users with unusable passwords from
|
|
|
|
resetting their password.
|
|
|
|
"""
|
|
|
|
active_users = User.objects.filter(**{
|
|
|
|
'email__iexact': email,
|
|
|
|
'is_active': True,
|
|
|
|
})
|
|
|
|
return (u for u in active_users if u.has_usable_password())
|
|
|
|
|
|
|
|
def get_email_body(self, **context):
|
|
|
|
"""
|
|
|
|
Add context to email template and return the complete body.
|
|
|
|
"""
|
|
|
|
return textwrap.dedent(
|
|
|
|
"""
|
|
|
|
You're receiving this email because you requested a password reset for your user account at {site_name}.
|
|
|
|
|
|
|
|
Please go to the following page and choose a new password:
|
|
|
|
|
|
|
|
{protocol}://{domain}{path}?user_id={user_id}&token={token}
|
|
|
|
|
|
|
|
Your username, in case you've forgotten: {username}
|
|
|
|
|
|
|
|
Thanks for using our site!
|
|
|
|
|
|
|
|
The {site_name} team.
|
|
|
|
"""
|
|
|
|
).format(**context)
|
|
|
|
|
|
|
|
|
|
|
|
class PasswordResetConfirmView(APIView):
|
|
|
|
"""
|
|
|
|
View to reset the password.
|
|
|
|
|
|
|
|
Send POST request with {'user_id': <encoded user id>, 'token': <token>,
|
|
|
|
'password' <new password>} to set password of this user to the new one.
|
|
|
|
"""
|
|
|
|
http_method_names = ['post']
|
|
|
|
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
|
|
uidb64 = request.data.get('user_id')
|
|
|
|
token = request.data.get('token')
|
|
|
|
password = request.data.get('password')
|
|
|
|
if not (uidb64 and token and password):
|
|
|
|
raise ValidationError({'detail': _('You have to provide user_id, token and password.')})
|
|
|
|
user = self.get_user(uidb64)
|
|
|
|
if user is None:
|
|
|
|
raise ValidationError({'detail': _('User does not exist.')})
|
|
|
|
if not default_token_generator.check_token(user, token):
|
|
|
|
raise ValidationError({'detail': _('Invalid token.')})
|
|
|
|
try:
|
|
|
|
validate_password(password, user=user)
|
|
|
|
except DjangoValidationError as errors:
|
|
|
|
raise ValidationError({'detail': ' '.join(errors)})
|
|
|
|
user.set_password(password)
|
|
|
|
user.save()
|
|
|
|
return super().post(request, *args, **kwargs)
|
|
|
|
|
|
|
|
def get_user(self, uidb64):
|
|
|
|
try:
|
|
|
|
# urlsafe_base64_decode() decodes to bytestring
|
|
|
|
uid = urlsafe_base64_decode(uidb64).decode()
|
|
|
|
user = User.objects.get(pk=uid)
|
|
|
|
except (TypeError, ValueError, OverflowError, User.DoesNotExist):
|
|
|
|
user = None
|
|
|
|
return user
|