2017-11-28 10:47:29 +01:00
|
|
|
import smtplib
|
2018-10-09 22:00:55 +02:00
|
|
|
import textwrap
|
2019-05-24 12:14:21 +02:00
|
|
|
from typing import Iterable, List, Set, Union
|
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
|
2019-05-13 10:17:24 +02:00
|
|
|
from django.contrib.auth.models import Permission
|
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
|
2019-11-26 11:49:06 +01:00
|
|
|
from django.db.utils import IntegrityError
|
2019-02-18 11:26:47 +01:00
|
|
|
from django.http.request import QueryDict
|
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
|
2014-10-11 14:34:49 +02:00
|
|
|
|
2019-08-20 12:00:54 +02:00
|
|
|
from openslides.saml import SAML_ENABLED
|
2020-08-12 11:05:31 +02:00
|
|
|
from openslides.utils import logging
|
2019-08-20 12:00:54 +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,
|
|
|
|
)
|
2019-11-04 14:56:01 +01:00
|
|
|
from ..utils.autoupdate import AutoupdateElement, inform_changed_data, inform_elements
|
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,
|
|
|
|
)
|
2019-11-06 15:55:03 +01:00
|
|
|
from ..utils.validate import validate_json
|
2016-10-01 14:26:28 +02:00
|
|
|
from ..utils.views import APIView
|
2019-03-06 14:53:24 +01:00
|
|
|
from .access_permissions import (
|
|
|
|
GroupAccessPermissions,
|
|
|
|
PersonalNoteAccessPermissions,
|
|
|
|
UserAccessPermissions,
|
|
|
|
)
|
|
|
|
from .models import Group, PersonalNote, User
|
|
|
|
from .serializers import GroupSerializer, PermissionRelatedField
|
2019-08-20 12:00:54 +02:00
|
|
|
from .user_backend import user_backend_manager
|
2011-07-31 10:46:29 +02:00
|
|
|
|
2012-07-07 15:26:00 +02:00
|
|
|
|
2020-10-02 09:57:28 +02:00
|
|
|
demo_mode_users = getattr(settings, "DEMO_USERS", None)
|
2020-08-12 11:05:31 +02:00
|
|
|
is_demo_mode = isinstance(demo_mode_users, list) and len(demo_mode_users) > 0
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
if is_demo_mode:
|
|
|
|
logger.info("OpenSlides started in demo mode. Some features are unavailable.")
|
|
|
|
|
|
|
|
|
|
|
|
def assertNoDemoAndAdmin(user_ids):
|
|
|
|
if isinstance(user_ids, int):
|
|
|
|
user_ids = [user_ids]
|
|
|
|
if is_demo_mode and any(user_id in demo_mode_users for user_id in user_ids):
|
|
|
|
raise ValidationError({"detail": "Not allowed in demo mode"})
|
|
|
|
|
|
|
|
|
|
|
|
def assertNoDemo():
|
|
|
|
if is_demo_mode:
|
|
|
|
raise ValidationError({"detail": "Not allowed in demo mode"})
|
2012-08-10 19:49:46 +02:00
|
|
|
|
2019-01-06 16:22:33 +01: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
|
|
|
"""
|
2019-01-06 16:22:33 +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
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
if self.action in ("list", "retrieve"):
|
2016-09-17 22:26:23 +02:00
|
|
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
2019-01-06 16:22:33 +01:00
|
|
|
elif self.action == "metadata":
|
|
|
|
result = has_perm(self.request.user, "users.can_see_name")
|
|
|
|
elif self.action in ("update", "partial_update"):
|
2018-07-09 23:22:26 +02:00
|
|
|
result = self.request.user.is_authenticated
|
2019-01-06 16:22:33 +01:00
|
|
|
elif self.action in (
|
|
|
|
"create",
|
|
|
|
"destroy",
|
|
|
|
"reset_password",
|
2019-07-17 11:24:01 +02:00
|
|
|
"bulk_generate_passwords",
|
|
|
|
"bulk_reset_passwords_to_default",
|
|
|
|
"bulk_set_state",
|
2019-07-23 11:50:16 +02:00
|
|
|
"bulk_alter_groups",
|
2019-07-17 11:24:01 +02:00
|
|
|
"bulk_delete",
|
2019-01-06 16:22:33 +01:00
|
|
|
"mass_import",
|
|
|
|
"mass_invite_email",
|
|
|
|
):
|
|
|
|
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
|
|
|
|
2019-11-26 11:49:06 +01:00
|
|
|
# catch IntegrityError, probably being caused by a race condition
|
|
|
|
def perform_create(self, serializer):
|
|
|
|
try:
|
|
|
|
super().perform_create(serializer)
|
|
|
|
except IntegrityError as e:
|
|
|
|
raise ValidationError({"detail": str(e)})
|
|
|
|
|
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()
|
2020-08-12 11:05:31 +02:00
|
|
|
assertNoDemoAndAdmin(user.id)
|
2017-02-24 15:04:12 +01:00
|
|
|
# Check permissions.
|
2019-01-06 16:22:33 +01:00
|
|
|
if (
|
|
|
|
has_perm(self.request.user, "users.can_see_name")
|
|
|
|
and has_perm(request.user, "users.can_see_extra_data")
|
|
|
|
and 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.
|
2019-01-06 16:22:33 +01: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.
|
2019-01-12 23:01:42 +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.
|
2019-01-06 16:22:33 +01: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.
|
2019-02-18 11:26:47 +01:00
|
|
|
if isinstance(request.data, QueryDict):
|
|
|
|
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()):
|
2019-08-20 12:00:54 +02:00
|
|
|
if key not in ("username", "about_me", "email"):
|
2017-02-24 15:04:12 +01:00
|
|
|
del request.data[key]
|
2019-08-20 12:00:54 +02:00
|
|
|
|
|
|
|
user_backend = user_backend_manager.get_backend(user.auth_type)
|
|
|
|
if user_backend:
|
|
|
|
disallowed_keys = user_backend.get_disallowed_update_keys()
|
|
|
|
for key in list(request.data.keys()):
|
|
|
|
if key in disallowed_keys:
|
|
|
|
del request.data[key]
|
|
|
|
|
|
|
|
# Hack to make the serializers validation work again if no username, last- or firstname is given:
|
|
|
|
if (
|
|
|
|
"username" not in request.data
|
|
|
|
and "first_name" not in request.data
|
|
|
|
and "last_name" not in request.data
|
|
|
|
):
|
|
|
|
request.data["username"] = user.username
|
|
|
|
|
2020-09-10 12:09:05 +02:00
|
|
|
# check that no chains are created with vote delegation
|
|
|
|
delegate_id = request.data.get("vote_delegated_to_id")
|
|
|
|
if delegate_id:
|
|
|
|
try:
|
|
|
|
delegate = User.objects.get(id=delegate_id)
|
|
|
|
except User.DoesNotExist:
|
|
|
|
raise ValidationError(
|
|
|
|
{
|
|
|
|
"detail": f"Vote delegation: The user with id {delegate_id} does not exist"
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
self.assert_no_self_delegation(user, [delegate_id])
|
|
|
|
self.assert_vote_not_delegated(delegate)
|
|
|
|
self.assert_has_no_delegated_votes(user)
|
|
|
|
|
|
|
|
inform_changed_data(delegate)
|
|
|
|
if user.vote_delegated_to:
|
|
|
|
inform_changed_data(user.vote_delegated_to)
|
|
|
|
|
|
|
|
# handle delegated_from field seperately since its a SerializerMethodField
|
|
|
|
new_delegation_ids = request.data.get("vote_delegated_from_users_id")
|
|
|
|
if "vote_delegated_from_users_id" in request.data:
|
|
|
|
del request.data["vote_delegated_from_users_id"]
|
|
|
|
|
2016-08-31 16:53:02 +02:00
|
|
|
response = super().update(request, *args, **kwargs)
|
2020-09-10 12:09:05 +02:00
|
|
|
|
|
|
|
# after rest of the request succeeded, handle delegation changes
|
|
|
|
if new_delegation_ids:
|
|
|
|
self.assert_no_self_delegation(user, new_delegation_ids)
|
|
|
|
self.assert_vote_not_delegated(user)
|
|
|
|
|
|
|
|
for id in new_delegation_ids:
|
|
|
|
delegation_user = User.objects.get(id=id)
|
|
|
|
self.assert_has_no_delegated_votes(delegation_user)
|
|
|
|
delegation_user.vote_delegated_to = user
|
|
|
|
delegation_user.save()
|
|
|
|
|
|
|
|
delegations_to_remove = user.vote_delegated_from_users.exclude(
|
|
|
|
id__in=(new_delegation_ids or [])
|
|
|
|
)
|
|
|
|
for old_delegation_user in delegations_to_remove:
|
|
|
|
old_delegation_user.vote_delegated_to = None
|
|
|
|
old_delegation_user.save()
|
|
|
|
|
|
|
|
# if only delegated_from was changed, we need an autoupdate for the operator
|
|
|
|
if new_delegation_ids or delegations_to_remove:
|
|
|
|
inform_changed_data(user)
|
|
|
|
|
2015-09-06 10:29:23 +02:00
|
|
|
return response
|
|
|
|
|
2020-09-10 12:09:05 +02:00
|
|
|
def assert_vote_not_delegated(self, user):
|
|
|
|
if user.vote_delegated_to:
|
|
|
|
raise ValidationError(
|
|
|
|
{
|
|
|
|
"detail": "You cannot delegate a vote to a user who has already delegated his vote."
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
def assert_has_no_delegated_votes(self, user):
|
|
|
|
if user.vote_delegated_from_users and len(user.vote_delegated_from_users.all()):
|
|
|
|
raise ValidationError(
|
|
|
|
{
|
|
|
|
"detail": "You cannot delegate a vote of a user who is already a delegate of another user."
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
def assert_no_self_delegation(self, user, delegate_ids):
|
|
|
|
if user.id in delegate_ids:
|
|
|
|
raise ValidationError({"detail": "You cannot delegate a vote to yourself."})
|
|
|
|
|
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.
|
|
|
|
"""
|
2020-08-12 11:05:31 +02:00
|
|
|
assertNoDemo()
|
2016-01-09 11:59:34 +01:00
|
|
|
instance = self.get_object()
|
|
|
|
if instance == self.request.user:
|
2019-01-12 23:01:42 +01:00
|
|
|
raise ValidationError({"detail": "You can not delete yourself."})
|
2016-01-09 11:59:34 +01:00
|
|
|
self.perform_destroy(instance)
|
|
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
@detail_route(methods=["post"])
|
2015-06-18 22:39:58 +02:00
|
|
|
def reset_password(self, request, pk=None):
|
|
|
|
"""
|
2019-07-17 11:24:01 +02:00
|
|
|
View to reset the password of the given user (by url) using a provided password.
|
|
|
|
Expected data: { pasword: <the new password> }
|
2015-06-18 22:39:58 +02:00
|
|
|
"""
|
|
|
|
user = self.get_object()
|
2020-08-12 11:05:31 +02:00
|
|
|
assertNoDemoAndAdmin(user.id)
|
2019-08-20 12:00:54 +02:00
|
|
|
if user.auth_type != "default":
|
|
|
|
raise ValidationError(
|
|
|
|
{
|
|
|
|
"detail": "The user does not have the login information stored in OpenSlides"
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2019-02-08 18:26:37 +01:00
|
|
|
password = request.data.get("password")
|
|
|
|
if not isinstance(password, str):
|
|
|
|
raise ValidationError({"detail": "Password has to be a string."})
|
|
|
|
|
|
|
|
try:
|
|
|
|
validate_password(password, user=request.user)
|
|
|
|
except DjangoValidationError as errors:
|
|
|
|
raise ValidationError({"detail": " ".join(errors)})
|
|
|
|
user.set_password(password)
|
|
|
|
user.save()
|
|
|
|
return Response({"detail": "Password successfully reset."})
|
2015-06-18 22:39:58 +02:00
|
|
|
|
2019-07-17 11:24:01 +02:00
|
|
|
@list_route(methods=["post"])
|
|
|
|
def bulk_generate_passwords(self, request):
|
|
|
|
"""
|
|
|
|
Generates new random passwords for many users. The request user is excluded
|
|
|
|
and the default password will be set to the new generated passwords.
|
|
|
|
Expected data: { user_ids: <list of ids> }
|
|
|
|
"""
|
2020-08-12 11:05:31 +02:00
|
|
|
assertNoDemo()
|
2019-07-17 11:24:01 +02:00
|
|
|
ids = request.data.get("user_ids")
|
|
|
|
self.assert_list_of_ints(ids)
|
|
|
|
|
|
|
|
# Exclude the request user
|
2019-08-20 12:00:54 +02:00
|
|
|
users = self.bulk_get_users(request, ids)
|
2019-07-17 11:24:01 +02:00
|
|
|
for user in users:
|
|
|
|
password = User.objects.make_random_password()
|
|
|
|
user.set_password(password)
|
|
|
|
user.default_password = password
|
|
|
|
user.save()
|
|
|
|
return Response()
|
|
|
|
|
|
|
|
@list_route(methods=["post"])
|
|
|
|
def bulk_reset_passwords_to_default(self, request):
|
|
|
|
"""
|
|
|
|
resets the password of all given users to their default ones. The
|
|
|
|
request user is excluded.
|
|
|
|
Expected data: { user_ids: <list of ids> }
|
|
|
|
"""
|
2020-08-12 11:05:31 +02:00
|
|
|
assertNoDemo()
|
2019-07-17 11:24:01 +02:00
|
|
|
ids = request.data.get("user_ids")
|
|
|
|
self.assert_list_of_ints(ids)
|
|
|
|
|
|
|
|
# Exclude the request user
|
2019-08-20 12:00:54 +02:00
|
|
|
users = self.bulk_get_users(request, ids)
|
2019-07-17 11:24:01 +02:00
|
|
|
# Validate all default passwords
|
|
|
|
for user in users:
|
|
|
|
try:
|
|
|
|
validate_password(user.default_password, user=user)
|
|
|
|
except DjangoValidationError as errors:
|
|
|
|
errors = " ".join(errors)
|
|
|
|
raise ValidationError(
|
|
|
|
{
|
2019-09-02 11:09:03 +02:00
|
|
|
"detail": 'The default password of user "{0}" is not valid: {1}',
|
|
|
|
"args": [user.username, str(errors)],
|
2019-07-17 11:24:01 +02:00
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
# Reset passwords
|
|
|
|
for user in users:
|
|
|
|
user.set_password(user.default_password)
|
|
|
|
user.save()
|
|
|
|
return Response()
|
|
|
|
|
|
|
|
@list_route(methods=["post"])
|
|
|
|
def bulk_set_state(self, request):
|
|
|
|
"""
|
|
|
|
Sets the "state" of may users. The "state" means boolean attributes like active
|
|
|
|
or committee of a user. If 'is_active' is choosen, the request user will be
|
|
|
|
removed from the list of user ids. Expected data:
|
|
|
|
|
|
|
|
{
|
|
|
|
user_ids: <list of ids>
|
|
|
|
field: 'is_active' | 'is_present' | 'is_committee'
|
|
|
|
value: True|False
|
|
|
|
}
|
2020-11-25 15:06:04 +01:00
|
|
|
|
|
|
|
Is_active and is_committee will not be settable for non-default auth type users.
|
2019-07-17 11:24:01 +02:00
|
|
|
"""
|
|
|
|
ids = request.data.get("user_ids")
|
|
|
|
self.assert_list_of_ints(ids)
|
2020-08-12 11:05:31 +02:00
|
|
|
assertNoDemoAndAdmin(ids)
|
2019-07-17 11:24:01 +02:00
|
|
|
|
|
|
|
field = request.data.get("field")
|
|
|
|
if field not in ("is_active", "is_present", "is_committee"):
|
|
|
|
raise ValidationError({"detail": "Unsupported field"})
|
|
|
|
value = request.data.get("value")
|
|
|
|
if not isinstance(value, bool):
|
|
|
|
raise ValidationError({"detail": "value must be true or false"})
|
|
|
|
|
2020-11-25 15:06:04 +01:00
|
|
|
users = User.objects.filter(pk__in=ids)
|
|
|
|
if field != "is_present":
|
|
|
|
users = users.filter(auth_type="default")
|
2019-07-17 11:24:01 +02:00
|
|
|
if field == "is_active":
|
|
|
|
users = users.exclude(pk=request.user.id)
|
2020-11-25 15:06:04 +01:00
|
|
|
|
2019-07-17 11:24:01 +02:00
|
|
|
for user in users:
|
|
|
|
setattr(user, field, value)
|
|
|
|
user.save()
|
|
|
|
|
|
|
|
return Response()
|
|
|
|
|
2019-07-23 11:50:16 +02:00
|
|
|
@list_route(methods=["post"])
|
|
|
|
def bulk_alter_groups(self, request):
|
|
|
|
"""
|
|
|
|
Adds or removes groups from given users. The request user is excluded.
|
|
|
|
Expected data:
|
|
|
|
{
|
|
|
|
user_ids: <list of ids>,
|
|
|
|
action: "add" | "remove",
|
|
|
|
group_ids: <list of ids>
|
|
|
|
}
|
|
|
|
"""
|
|
|
|
user_ids = request.data.get("user_ids")
|
|
|
|
self.assert_list_of_ints(user_ids)
|
2020-08-12 11:05:31 +02:00
|
|
|
assertNoDemoAndAdmin(user_ids)
|
2019-07-23 11:50:16 +02:00
|
|
|
group_ids = request.data.get("group_ids")
|
|
|
|
self.assert_list_of_ints(group_ids, ids_name="groups_id")
|
|
|
|
|
|
|
|
action = request.data.get("action")
|
|
|
|
if action not in ("add", "remove"):
|
|
|
|
raise ValidationError({"detail": "The action must be add or remove"})
|
|
|
|
|
2020-10-25 20:25:43 +01:00
|
|
|
users = self.bulk_get_users(request, user_ids, auth_type=None)
|
2019-07-23 11:50:16 +02:00
|
|
|
groups = list(Group.objects.filter(pk__in=group_ids))
|
|
|
|
|
|
|
|
for user in users:
|
|
|
|
if action == "add":
|
|
|
|
user.groups.add(*groups)
|
|
|
|
else:
|
|
|
|
user.groups.remove(*groups)
|
|
|
|
|
|
|
|
inform_changed_data(users)
|
|
|
|
return Response()
|
|
|
|
|
2019-07-17 11:24:01 +02:00
|
|
|
@list_route(methods=["post"])
|
|
|
|
def bulk_delete(self, request):
|
|
|
|
"""
|
|
|
|
Deletes many users. The request user will be excluded. Expected data:
|
|
|
|
{ user_ids: <list of ids> }
|
|
|
|
"""
|
2020-08-12 11:05:31 +02:00
|
|
|
assertNoDemo()
|
2019-07-17 11:24:01 +02:00
|
|
|
ids = request.data.get("user_ids")
|
|
|
|
self.assert_list_of_ints(ids)
|
|
|
|
|
|
|
|
# Exclude the request user
|
2020-06-10 08:44:26 +02:00
|
|
|
users = self.bulk_get_users(request, ids, auth_type=None)
|
2019-07-17 11:24:01 +02:00
|
|
|
for user in list(users):
|
|
|
|
user.delete()
|
|
|
|
|
|
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
|
|
|
2020-06-10 08:44:26 +02:00
|
|
|
def bulk_get_users(self, request, ids, auth_type="default"):
|
2019-08-20 12:00:54 +02:00
|
|
|
"""
|
2020-06-10 08:44:26 +02:00
|
|
|
Get all users for the given ids. Exludes the request user.
|
|
|
|
If the auth type is given (so it is not None), only these users are included.
|
2019-08-20 12:00:54 +02:00
|
|
|
"""
|
2020-06-10 08:44:26 +02:00
|
|
|
queryset = User.objects
|
|
|
|
if auth_type is not None:
|
|
|
|
queryset = queryset.filter(auth_type=auth_type)
|
|
|
|
return queryset.exclude(pk=request.user.id).filter(pk__in=ids)
|
2019-08-20 12:00:54 +02:00
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
@list_route(methods=["post"])
|
2017-04-19 09:28:21 +02:00
|
|
|
@transaction.atomic
|
|
|
|
def mass_import(self, request):
|
|
|
|
"""
|
|
|
|
API endpoint to create multiple users at once.
|
|
|
|
|
|
|
|
Example: {"users": [{"first_name": "Max"}, {"first_name": "Maxi"}]}
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
users = request.data.get("users")
|
2017-04-19 09:28:21 +02:00
|
|
|
if not isinstance(users, list):
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError({"detail": "Users has to be a list."})
|
2017-04-19 09:28:21 +02:00
|
|
|
|
|
|
|
created_users = []
|
|
|
|
# List of all track ids of all imported users. The track ids are just used in the client.
|
|
|
|
imported_track_ids = []
|
2020-04-30 08:10:39 +02:00
|
|
|
errors = {} # maps imported track ids to errors
|
2017-04-19 09:28:21 +02:00
|
|
|
|
|
|
|
for user in users:
|
|
|
|
serializer = self.get_serializer(data=user)
|
|
|
|
try:
|
|
|
|
serializer.is_valid(raise_exception=True)
|
2020-04-30 08:10:39 +02:00
|
|
|
except ValidationError as e:
|
2017-04-19 09:28:21 +02:00
|
|
|
# Skip invalid users.
|
2020-04-30 08:10:39 +02:00
|
|
|
if "vote_weight" in e.detail and "importTrackId" in user:
|
|
|
|
errors[user["importTrackId"]] = "vote_weight"
|
2017-04-19 09:28:21 +02:00
|
|
|
continue
|
|
|
|
data = serializer.prepare_password(serializer.data)
|
2019-01-06 16:22:33 +01:00
|
|
|
groups = data["groups_id"]
|
|
|
|
del data["groups_id"]
|
2020-09-10 12:09:05 +02:00
|
|
|
del data["vote_delegated_from_users_id"]
|
2017-04-19 09:28:21 +02:00
|
|
|
|
|
|
|
db_user = User(**data)
|
2019-11-26 11:49:06 +01:00
|
|
|
try:
|
|
|
|
db_user.save(skip_autoupdate=True)
|
|
|
|
except IntegrityError:
|
|
|
|
# race condition may happen, so skip double users here again
|
|
|
|
continue
|
2017-04-19 09:28:21 +02:00
|
|
|
db_user.groups.add(*groups)
|
|
|
|
created_users.append(db_user)
|
2019-01-06 16:22:33 +01:00
|
|
|
if "importTrackId" in user:
|
|
|
|
imported_track_ids.append(user["importTrackId"])
|
2017-04-19 09:28:21 +02:00
|
|
|
|
2019-11-01 15:19:05 +01:00
|
|
|
# Now inform all clients and send a response
|
2017-04-19 09:28:21 +02:00
|
|
|
inform_changed_data(created_users)
|
2019-01-06 16:22:33 +01:00
|
|
|
return Response(
|
|
|
|
{
|
2019-09-02 11:09:03 +02:00
|
|
|
"detail": "{0} users successfully imported.",
|
2020-04-30 08:10:39 +02:00
|
|
|
"errors": errors,
|
2019-09-02 11:09:03 +02:00
|
|
|
"args": [len(created_users)],
|
2019-01-06 16:22:33 +01:00
|
|
|
"importedTrackIds": imported_track_ids,
|
|
|
|
}
|
|
|
|
)
|
2017-04-19 09:28:21 +02:00
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
@list_route(methods=["post"])
|
2017-11-28 10:47:29 +01:00
|
|
|
def mass_invite_email(self, request):
|
|
|
|
"""
|
|
|
|
Endpoint to send invitation emails to all given users (by id). Returns the
|
|
|
|
number of emails send.
|
|
|
|
"""
|
2020-08-12 11:05:31 +02:00
|
|
|
assertNoDemo()
|
2019-01-06 16:22:33 +01:00
|
|
|
user_ids = request.data.get("user_ids")
|
2019-07-17 11:24:01 +02:00
|
|
|
self.assert_list_of_ints(user_ids)
|
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.
|
2019-01-06 16:22:33 +01:00
|
|
|
subject = request.data.get("subject")
|
|
|
|
message = request.data.get("message")
|
2018-02-02 12:29:18 +01:00
|
|
|
if not isinstance(subject, str):
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError({"detail": "Subject has to be a string."})
|
2018-02-02 12:29:18 +01:00
|
|
|
if not isinstance(message, str):
|
2019-01-06 16:22:33 +01:00
|
|
|
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:
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError(
|
|
|
|
{
|
2019-09-02 11:09:03 +02:00
|
|
|
"detail": "Cannot connect to SMTP server on {0}:{1}",
|
|
|
|
"args": [settings.EMAIL_HOST, settings.EMAIL_PORT],
|
2019-01-06 16:22:33 +01:00
|
|
|
}
|
|
|
|
)
|
2019-01-12 23:01:42 +01:00
|
|
|
except smtplib.SMTPException as err:
|
2020-10-23 07:22:56 +02:00
|
|
|
if err.errno and err.strerror:
|
|
|
|
detail = f"{err.errno}: {err.strerror}"
|
|
|
|
else:
|
|
|
|
detail = str(err)
|
|
|
|
raise ValidationError({"detail": detail})
|
2017-11-28 10:47:29 +01:00
|
|
|
|
|
|
|
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:
|
2019-01-06 16:22:33 +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)
|
2019-01-12 23:01:42 +01:00
|
|
|
except DjangoValidationError as err:
|
|
|
|
raise ValidationError(err.message_dict)
|
2017-11-28 10:47:29 +01:00
|
|
|
|
|
|
|
connection.close()
|
|
|
|
inform_changed_data(success_users)
|
2019-01-06 16:22:33 +01:00
|
|
|
return Response(
|
|
|
|
{"count": len(success_users), "no_email_ids": user_pks_without_email}
|
|
|
|
)
|
2017-11-28 10:47:29 +01:00
|
|
|
|
2019-07-23 11:50:16 +02:00
|
|
|
def assert_list_of_ints(self, ids, ids_name="user_ids"):
|
2019-07-17 11:24:01 +02:00
|
|
|
""" Asserts, that ids is a list of ints. Raises a ValidationError, if not. """
|
|
|
|
if not isinstance(ids, list):
|
2019-09-02 11:09:03 +02:00
|
|
|
raise ValidationError({"detail": "{0} must be a list", "args": [ids_name]})
|
2019-07-17 11:24:01 +02:00
|
|
|
for id in ids:
|
|
|
|
if not isinstance(id, int):
|
2019-07-23 11:50:16 +02:00
|
|
|
raise ValidationError({"detail": "Every id must be a int"})
|
2019-07-17 11:24:01 +02: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.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
2016-01-25 22:35:23 +01:00
|
|
|
def get_field_info(self, field):
|
|
|
|
"""
|
|
|
|
Customized method to change the display name of permission choices.
|
|
|
|
"""
|
|
|
|
field_info = super().get_field_info(field)
|
2019-01-06 16:22:33 +01:00
|
|
|
if field.field_name == "permissions":
|
|
|
|
field_info["choices"] = [
|
2016-08-29 17:05:06 +02:00
|
|
|
{
|
2019-01-06 16:22:33 +01:00
|
|
|
"value": choice_value,
|
|
|
|
"display_name": force_text(choice_name, strings_only=True).split(
|
|
|
|
" | "
|
|
|
|
)[2],
|
2016-08-29 17:05:06 +02:00
|
|
|
}
|
|
|
|
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
|
|
|
"""
|
2019-01-06 16:22:33 +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
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
if self.action in ("list", "retrieve"):
|
2016-12-17 09:30:20 +01:00
|
|
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
2019-01-06 16:22:33 +01:00
|
|
|
elif self.action == "metadata":
|
2016-12-17 09:30:20 +01:00
|
|
|
# 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()
|
2019-05-13 10:17:24 +02:00
|
|
|
elif self.action in (
|
|
|
|
"create",
|
|
|
|
"partial_update",
|
|
|
|
"update",
|
|
|
|
"destroy",
|
|
|
|
"set_permission",
|
|
|
|
):
|
2015-07-01 23:18:48 +02:00
|
|
|
# Users with all app permissions can edit groups.
|
2019-01-06 16:22:33 +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.
|
2019-01-06 16:22:33 +01:00
|
|
|
old_permissions = list(
|
|
|
|
group.permissions.all()
|
|
|
|
) # Force evaluation so the perms don't change anymore.
|
|
|
|
permission_names = request.data["permissions"]
|
2017-02-21 09:34:24 +01:00
|
|
|
if isinstance(permission_names, str):
|
|
|
|
permission_names = [permission_names]
|
|
|
|
given_permissions = [
|
2019-01-06 16:22:33 +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:
|
2019-05-24 12:14:21 +02:00
|
|
|
changed_permissions = list(
|
|
|
|
set(old_permissions).symmetric_difference(set(given_permissions))
|
|
|
|
)
|
|
|
|
self.inform_permission_change(group, changed_permissions)
|
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
|
2019-01-06 16:22:33 +01:00
|
|
|
affected_users_ids = list(instance.user_set.values_list("pk", flat=True))
|
2018-05-16 13:03:37 +02:00
|
|
|
|
|
|
|
# Delete the group
|
2017-03-06 16:34:20 +01:00
|
|
|
self.perform_destroy(instance)
|
2020-11-24 13:43:05 +01:00
|
|
|
config.remove_group_id_from_all_group_configs(instance.id)
|
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)
|
|
|
|
|
2019-05-13 10:17:24 +02:00
|
|
|
@detail_route(methods=["post"])
|
|
|
|
@transaction.atomic
|
|
|
|
def set_permission(self, request, *args, **kwargs):
|
|
|
|
"""
|
2019-06-05 11:57:29 +02:00
|
|
|
Send {perm: <permissionstring>, set: <True/False>} to set or
|
2019-05-13 10:17:24 +02:00
|
|
|
remove the permission from a group
|
|
|
|
"""
|
|
|
|
perm = request.data.get("perm")
|
|
|
|
if not isinstance(perm, str):
|
|
|
|
raise ValidationError("You have to give a permission as string.")
|
|
|
|
set = request.data.get("set")
|
|
|
|
if not isinstance(set, bool):
|
|
|
|
raise ValidationError("You have to give a set value.")
|
|
|
|
|
|
|
|
# check if perm is a valid permission
|
|
|
|
try:
|
|
|
|
app_label, codename = perm.split(".")
|
|
|
|
except ValueError:
|
|
|
|
raise ValidationError("Incorrect permission string")
|
|
|
|
try:
|
|
|
|
permission = Permission.objects.get(
|
|
|
|
content_type__app_label=app_label, codename=codename
|
|
|
|
)
|
|
|
|
except Permission.DoesNotExist:
|
|
|
|
raise ValidationError("Incorrect permission string")
|
|
|
|
|
|
|
|
# add/remove the permission
|
|
|
|
group = self.get_object()
|
|
|
|
if set:
|
|
|
|
group.permissions.add(permission)
|
|
|
|
else:
|
|
|
|
group.permissions.remove(permission)
|
2019-05-24 12:14:21 +02:00
|
|
|
self.inform_permission_change(group, permission)
|
2019-05-13 10:17:24 +02:00
|
|
|
inform_changed_data(group)
|
|
|
|
|
2019-06-05 11:57:29 +02:00
|
|
|
return Response(
|
2019-09-02 11:09:03 +02:00
|
|
|
{
|
|
|
|
"detail": "Permissions of group {0} successfully changed.",
|
|
|
|
"args": [group.pk],
|
|
|
|
}
|
2019-06-05 11:57:29 +02:00
|
|
|
)
|
2019-05-13 10:17:24 +02:00
|
|
|
|
2019-05-24 12:14:21 +02:00
|
|
|
def inform_permission_change(
|
|
|
|
self,
|
|
|
|
group: Group,
|
|
|
|
changed_permissions: Union[None, Permission, Iterable[Permission]],
|
|
|
|
) -> None:
|
|
|
|
"""
|
|
|
|
Updates every users, if some permission changes. For this, every affected collection
|
|
|
|
is fetched via the permission_change signal and every object of the collection passed
|
2019-11-04 14:56:01 +01:00
|
|
|
into the cache/autoupdate system.
|
2019-05-24 12:14:21 +02:00
|
|
|
"""
|
|
|
|
if isinstance(changed_permissions, Permission):
|
|
|
|
changed_permissions = [changed_permissions]
|
|
|
|
|
|
|
|
if not changed_permissions:
|
|
|
|
return # either None or empty list.
|
|
|
|
|
2019-11-04 14:56:01 +01:00
|
|
|
elements: List[AutoupdateElement] = []
|
2019-05-24 12:14:21 +02:00
|
|
|
signal_results = permission_change.send(None, permissions=changed_permissions)
|
2019-07-29 15:19:59 +02:00
|
|
|
all_full_data = async_to_sync(element_cache.get_all_data_list)()
|
2019-05-24 12:14:21 +02:00
|
|
|
for _, signal_collections in signal_results:
|
|
|
|
for cachable in signal_collections:
|
|
|
|
for full_data in all_full_data.get(
|
|
|
|
cachable.get_collection_string(), {}
|
|
|
|
):
|
|
|
|
elements.append(
|
2019-11-04 14:56:01 +01:00
|
|
|
AutoupdateElement(
|
2019-05-24 12:14:21 +02:00
|
|
|
id=full_data["id"],
|
|
|
|
collection_string=cachable.get_collection_string(),
|
|
|
|
full_data=full_data,
|
|
|
|
disable_history=True,
|
|
|
|
)
|
|
|
|
)
|
2019-11-04 14:56:01 +01:00
|
|
|
inform_elements(elements)
|
2019-05-24 12:14:21 +02:00
|
|
|
|
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.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
2017-05-23 14:07:06 +02:00
|
|
|
access_permissions = PersonalNoteAccessPermissions()
|
|
|
|
queryset = PersonalNote.objects.all()
|
|
|
|
|
|
|
|
def check_view_permissions(self):
|
|
|
|
"""
|
|
|
|
Returns True if the user has required permissions.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
if self.action in ("list", "retrieve"):
|
2017-05-23 14:07:06 +02:00
|
|
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
2019-09-10 11:21:39 +02:00
|
|
|
elif self.action in ("create_or_update", "destroy"):
|
2017-05-23 14:07:06 +02:00
|
|
|
# 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
|
|
|
|
|
2019-09-10 11:21:39 +02:00
|
|
|
@list_route(methods=["post"])
|
|
|
|
@transaction.atomic
|
|
|
|
def create_or_update(self, request, *args, **kwargs):
|
2017-05-23 14:07:06 +02:00
|
|
|
"""
|
|
|
|
Customized method to ensure that every user can change only his own
|
|
|
|
personal notes.
|
2019-09-10 11:21:39 +02:00
|
|
|
|
|
|
|
[{
|
|
|
|
collection: <collection>,
|
|
|
|
id: <id>,
|
|
|
|
content: <Any>,
|
|
|
|
}, ...]
|
|
|
|
"""
|
|
|
|
# verify data:
|
|
|
|
if not isinstance(request.data, list):
|
|
|
|
raise ValidationError({"detail": "Data must be a list"})
|
|
|
|
for data in request.data:
|
|
|
|
if not isinstance(data, dict):
|
|
|
|
raise ValidationError({"detail": "Every entry must be a dict"})
|
|
|
|
if not isinstance(data.get("collection"), str):
|
|
|
|
raise ValidationError({"detail": "The collection must be a string"})
|
|
|
|
if not isinstance(data.get("id"), int):
|
|
|
|
raise ValidationError({"detail": "The id must be an integer"})
|
|
|
|
|
|
|
|
# get note
|
|
|
|
personal_note, _ = PersonalNote.objects.get_or_create(user=request.user)
|
|
|
|
|
|
|
|
# set defaults
|
|
|
|
if not personal_note.notes:
|
|
|
|
personal_note.notes = {}
|
|
|
|
|
|
|
|
for data in request.data:
|
|
|
|
if data["collection"] not in personal_note.notes:
|
|
|
|
personal_note.notes[data["collection"]] = {}
|
2019-11-06 15:55:03 +01:00
|
|
|
content = validate_json(data["content"], 2)
|
|
|
|
personal_note.notes[data["collection"]][data["id"]] = content
|
2019-09-10 11:21:39 +02:00
|
|
|
|
|
|
|
personal_note.save()
|
|
|
|
return Response()
|
2017-05-23 14:07:06 +02:00
|
|
|
|
|
|
|
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
|
|
|
|
2019-01-06 16:22:33 +01:00
|
|
|
|
2020-03-26 11:33:56 +01:00
|
|
|
class SetPresenceView(APIView):
|
|
|
|
http_method_names = ["post"]
|
|
|
|
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
|
|
user = request.user
|
|
|
|
if not config["users_allow_self_set_present"] or not user.is_authenticated:
|
|
|
|
raise ValidationError({"detail": "You cannot set your own presence"})
|
|
|
|
|
|
|
|
present = request.data
|
|
|
|
if present not in (True, False):
|
|
|
|
raise ValidationError({"detail": "Data must be a boolean"})
|
|
|
|
|
|
|
|
user.is_present = present
|
|
|
|
user.save()
|
|
|
|
return Response()
|
|
|
|
|
|
|
|
|
2019-03-07 10:47:03 +01:00
|
|
|
class WhoAmIDataView(APIView):
|
|
|
|
def get_whoami_data(self):
|
|
|
|
"""
|
|
|
|
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.
|
2019-08-20 12:00:54 +02:00
|
|
|
Appends also the serialized user if available and auth_type.
|
2019-03-07 10:47:03 +01:00
|
|
|
"""
|
|
|
|
user_id = self.request.user.pk or 0
|
|
|
|
guest_enabled = anonymous_is_enabled()
|
|
|
|
|
2019-08-20 12:00:54 +02:00
|
|
|
auth_type = "default"
|
2019-03-07 10:47:03 +01:00
|
|
|
if user_id:
|
2019-08-20 12:00:54 +02:00
|
|
|
user_full_data = async_to_sync(element_cache.get_element_data)(
|
|
|
|
self.request.user.get_collection_string(), user_id
|
|
|
|
)
|
2020-10-05 12:07:04 +02:00
|
|
|
if user_full_data is None:
|
|
|
|
return Response(
|
|
|
|
{"detail": "Cache offline, could not fetch user"}, status=500
|
|
|
|
)
|
2019-08-20 12:00:54 +02:00
|
|
|
auth_type = user_full_data["auth_type"]
|
|
|
|
user_data = async_to_sync(element_cache.restrict_element_data)(
|
|
|
|
user_full_data, self.request.user.get_collection_string(), user_id
|
2019-03-07 10:47:03 +01:00
|
|
|
)
|
|
|
|
group_ids = user_data["groups_id"] or [GROUP_DEFAULT_PK]
|
|
|
|
else:
|
|
|
|
user_data = None
|
|
|
|
group_ids = [GROUP_DEFAULT_PK] if guest_enabled else []
|
|
|
|
|
|
|
|
# collect all permissions
|
|
|
|
permissions: Set[str] = set()
|
2019-07-29 15:19:59 +02:00
|
|
|
group_all_data = async_to_sync(element_cache.get_collection_data)("users/group")
|
2019-03-07 10:47:03 +01:00
|
|
|
for group_id in group_ids:
|
|
|
|
permissions.update(group_all_data[group_id]["permissions"])
|
|
|
|
|
|
|
|
return {
|
|
|
|
"user_id": user_id or None,
|
|
|
|
"guest_enabled": guest_enabled,
|
|
|
|
"user": user_data,
|
2019-08-20 12:00:54 +02:00
|
|
|
"auth_type": auth_type,
|
2019-03-07 10:47:03 +01:00
|
|
|
"permissions": list(permissions),
|
|
|
|
}
|
|
|
|
|
|
|
|
def get_context_data(self, **context):
|
|
|
|
context.update(self.get_whoami_data())
|
|
|
|
return super().get_context_data(**context)
|
|
|
|
|
|
|
|
|
|
|
|
class UserLoginView(WhoAmIDataView):
|
2015-02-12 22:42:54 +01:00
|
|
|
"""
|
2015-09-16 00:55:27 +02:00
|
|
|
Login the user.
|
2015-02-12 22:42:54 +01:00
|
|
|
"""
|
2019-01-06 16:22:33 +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)
|
2019-01-06 16:22:33 +01:00
|
|
|
if not self.request.data.get("cookies", True):
|
|
|
|
raise ValidationError(
|
2019-01-12 23:01:42 +01:00
|
|
|
{"detail": "Cookies have to be enabled to use OpenSlides."}
|
2019-01-06 16:22:33 +01:00
|
|
|
)
|
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():
|
2019-01-12 23:01:42 +01:00
|
|
|
raise ValidationError({"detail": "Username or password is not correct."})
|
2015-12-11 16:28:56 +01:00
|
|
|
self.user = form.get_user()
|
2019-08-20 12:00:54 +02:00
|
|
|
if self.user.auth_type != "default":
|
|
|
|
raise ValidationError({"detail": "Please login via your identity provider"})
|
2015-12-11 16:28:56 +01:00
|
|
|
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.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
if self.request.method == "GET":
|
|
|
|
if config["general_login_info_text"]:
|
2019-07-26 11:47:04 +02:00
|
|
|
context["login_info_text"] = config["general_login_info_text"]
|
2016-01-09 01:10:37 +01:00
|
|
|
else:
|
|
|
|
try:
|
2019-01-06 16:22:33 +01:00
|
|
|
user = User.objects.get(username="admin")
|
|
|
|
if user.check_password("admin"):
|
2019-07-26 11:47:04 +02:00
|
|
|
context["login_info_text"] = (
|
2020-05-13 16:16:03 +02:00
|
|
|
"Use <strong>admin</strong> and <strong>admin</strong> for your first login.<br>"
|
2019-02-02 17:06:23 +01:00
|
|
|
"Please change your password to hide this message!"
|
2019-01-06 16:22:33 +01:00
|
|
|
)
|
2019-07-26 11:47:04 +02:00
|
|
|
except User.DoesNotExist:
|
|
|
|
pass
|
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.
|
2019-01-06 16:22:33 +01:00
|
|
|
context["privacy_policy"] = config["general_event_privacy_policy"]
|
|
|
|
context["legal_notice"] = config["general_event_legal_notice"]
|
2019-03-08 09:19:05 +01:00
|
|
|
# Add the theme, so the loginpage is themed correctly
|
|
|
|
context["theme"] = config["openslides_theme"]
|
2019-04-08 10:57:43 +02:00
|
|
|
context["logo_web_header"] = config["logo_web_header"]
|
2019-08-20 12:00:54 +02:00
|
|
|
|
|
|
|
if SAML_ENABLED:
|
|
|
|
from openslides.saml.settings import get_saml_settings
|
|
|
|
|
|
|
|
context["saml_settings"] = get_saml_settings().general_settings
|
2016-01-09 01:10:37 +01:00
|
|
|
else:
|
|
|
|
# self.request.method == 'POST'
|
2019-03-07 10:47:03 +01:00
|
|
|
context.update(self.get_whoami_data())
|
2015-02-12 22:42:54 +01:00
|
|
|
return super().get_context_data(**context)
|
|
|
|
|
|
|
|
|
2019-03-07 10:47:03 +01:00
|
|
|
class UserLogoutView(WhoAmIDataView):
|
2015-02-12 22:42:54 +01:00
|
|
|
"""
|
2015-09-16 00:55:27 +02:00
|
|
|
Logout the user.
|
2015-02-12 22:42:54 +01:00
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
|
|
|
http_method_names = ["post"]
|
2015-02-12 22:42:54 +01:00
|
|
|
|
|
|
|
def post(self, *args, **kwargs):
|
2018-07-09 23:22:26 +02:00
|
|
|
if not self.request.user.is_authenticated:
|
2019-01-12 23:01:42 +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)
|
|
|
|
|
|
|
|
|
2019-03-07 10:47:03 +01:00
|
|
|
class WhoAmIView(WhoAmIDataView):
|
2015-02-12 22:42:54 +01:00
|
|
|
"""
|
2015-07-01 23:18:48 +02:00
|
|
|
Returns the id of the requesting user.
|
2015-02-12 22:42:54 +01:00
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
|
|
|
http_method_names = ["get"]
|
2015-02-12 22:42:54 +01:00
|
|
|
|
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.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
|
|
|
http_method_names = ["post"]
|
2015-11-06 15:44:27 +01:00
|
|
|
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
|
|
user = request.user
|
2019-11-11 07:07:19 +01:00
|
|
|
if (
|
|
|
|
not user.is_authenticated
|
|
|
|
or not has_perm(user, "users.can_change_password")
|
2019-08-20 12:00:54 +02:00
|
|
|
or user.auth_type != "default"
|
2019-01-19 14:32:11 +01:00
|
|
|
):
|
2019-01-19 09:52:13 +01:00
|
|
|
self.permission_denied(request)
|
2020-08-12 11:05:31 +02:00
|
|
|
assertNoDemoAndAdmin(user.id)
|
2019-01-06 16:22:33 +01:00
|
|
|
if user.check_password(request.data["old_password"]):
|
2017-04-13 16:19:20 +02:00
|
|
|
try:
|
2019-01-06 16:22:33 +01:00
|
|
|
validate_password(request.data.get("new_password"), user=user)
|
2017-04-13 16:19:20 +02:00
|
|
|
except DjangoValidationError as errors:
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError({"detail": " ".join(errors)})
|
|
|
|
user.set_password(request.data["new_password"])
|
2015-11-06 15:44:27 +01:00
|
|
|
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:
|
2019-01-12 23:01:42 +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.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
|
|
|
http_method_names = ["post"]
|
2019-01-31 22:41:08 +01:00
|
|
|
use_https = True # TODO: get used protocol from server, see issue #4233
|
2018-10-09 22:00:55 +02:00
|
|
|
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
|
|
"""
|
|
|
|
Loop over all users and send emails.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
to_email = request.data.get("email")
|
2019-09-02 11:09:03 +02:00
|
|
|
users = self.get_users(to_email)
|
|
|
|
|
|
|
|
if len(users) == 0 and getattr(settings, "RESET_PASSWORD_VERBOSE_ERRORS", True):
|
|
|
|
raise ValidationError(
|
|
|
|
{"detail": "No users with email {0} found.", "args": [to_email]}
|
|
|
|
)
|
|
|
|
|
|
|
|
for user in users:
|
2018-10-09 22:00:55 +02:00
|
|
|
current_site = get_current_site(request)
|
|
|
|
site_name = current_site.name
|
2019-01-28 20:53:16 +01:00
|
|
|
if has_perm(user, "users.can_change_password") or has_perm(
|
|
|
|
user, "users.can_manage"
|
|
|
|
):
|
2019-01-20 15:44:03 +01:00
|
|
|
context = {
|
|
|
|
"email": to_email,
|
|
|
|
"site_name": site_name,
|
|
|
|
"protocol": "https" if self.use_https else "http",
|
|
|
|
"domain": current_site.domain,
|
|
|
|
"path": "/login/reset-password-confirm/",
|
2019-05-10 07:35:19 +02:00
|
|
|
"user_id": urlsafe_base64_encode(
|
|
|
|
force_bytes(user.pk)
|
|
|
|
), # urlsafe_base64_encode decodes to ascii
|
2019-01-20 15:44:03 +01:00
|
|
|
"token": default_token_generator.make_token(user),
|
|
|
|
"username": user.get_username(),
|
|
|
|
}
|
|
|
|
body = self.get_email_body(**context)
|
|
|
|
else:
|
|
|
|
# User is not allowed to reset his permission. Send only short message.
|
|
|
|
body = f"""
|
|
|
|
You do not have permission to reset your password at {site_name}.
|
|
|
|
|
|
|
|
Please contact your local administrator.
|
|
|
|
|
|
|
|
Your username, in case you've forgotten: {user.get_username()}
|
|
|
|
"""
|
2018-10-09 22:00:55 +02:00
|
|
|
# Send a django.core.mail.EmailMessage to `to_email`.
|
2019-01-12 23:01:42 +01:00
|
|
|
subject = f"Password reset for {site_name}"
|
2019-01-06 16:22:33 +01:00
|
|
|
subject = "".join(subject.splitlines())
|
2018-10-09 22:00:55 +02:00
|
|
|
from_email = None # TODO: Add nice from_email here.
|
|
|
|
email_message = mail.EmailMessage(subject, body, from_email, [to_email])
|
2019-04-02 16:08:52 +02:00
|
|
|
try:
|
|
|
|
email_message.send()
|
|
|
|
except smtplib.SMTPRecipientsRefused:
|
|
|
|
raise ValidationError(
|
|
|
|
{
|
2019-09-02 11:09:03 +02:00
|
|
|
"detail": "Error: The email to {0} was refused by the server. Please contact your local administrator.",
|
|
|
|
"args": [to_email],
|
2019-04-02 16:08:52 +02:00
|
|
|
}
|
|
|
|
)
|
|
|
|
except smtplib.SMTPAuthenticationError as e:
|
|
|
|
# Nice error message on auth failure
|
|
|
|
raise ValidationError(
|
|
|
|
{
|
2019-09-02 11:09:03 +02:00
|
|
|
"detail": "Error {0}: Authentication failure. Please contact your administrator.",
|
|
|
|
"args": [e.smtp_code],
|
2019-04-02 16:08:52 +02:00
|
|
|
}
|
|
|
|
)
|
2019-07-23 12:03:54 +02:00
|
|
|
except ConnectionRefusedError:
|
|
|
|
raise ValidationError(
|
|
|
|
{
|
|
|
|
"detail": "Connection refused error. Please contact your administrator."
|
|
|
|
}
|
|
|
|
)
|
2019-09-02 11:09:03 +02:00
|
|
|
return Response()
|
2018-10-09 22:00:55 +02:00
|
|
|
|
|
|
|
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.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
active_users = User.objects.filter(
|
2019-08-20 12:00:54 +02:00
|
|
|
**{"email__iexact": email, "is_active": True, "auth_type": "default"}
|
2019-01-06 16:22:33 +01:00
|
|
|
)
|
2019-09-02 11:09:03 +02:00
|
|
|
return [u for u in active_users if u.has_usable_password()]
|
2018-10-09 22:00:55 +02:00
|
|
|
|
|
|
|
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.
|
|
|
|
"""
|
2019-01-06 16:22:33 +01:00
|
|
|
|
|
|
|
http_method_names = ["post"]
|
2018-10-09 22:00:55 +02:00
|
|
|
|
|
|
|
def post(self, request, *args, **kwargs):
|
2019-01-06 16:22:33 +01:00
|
|
|
uidb64 = request.data.get("user_id")
|
|
|
|
token = request.data.get("token")
|
|
|
|
password = request.data.get("password")
|
2018-10-09 22:00:55 +02:00
|
|
|
if not (uidb64 and token and password):
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError(
|
2019-01-12 23:01:42 +01:00
|
|
|
{"detail": "You have to provide user_id, token and password."}
|
2019-01-06 16:22:33 +01:00
|
|
|
)
|
2018-10-09 22:00:55 +02:00
|
|
|
user = self.get_user(uidb64)
|
|
|
|
if user is None:
|
2019-01-12 23:01:42 +01:00
|
|
|
raise ValidationError({"detail": "User does not exist."})
|
2019-01-28 20:53:16 +01:00
|
|
|
if not (
|
|
|
|
has_perm(user, "users.can_change_password")
|
|
|
|
or has_perm(user, "users.can_manage")
|
|
|
|
):
|
2019-01-20 15:44:03 +01:00
|
|
|
self.permission_denied(request)
|
2018-10-09 22:00:55 +02:00
|
|
|
if not default_token_generator.check_token(user, token):
|
2019-01-12 23:01:42 +01:00
|
|
|
raise ValidationError({"detail": "Invalid token."})
|
2018-10-09 22:00:55 +02:00
|
|
|
try:
|
|
|
|
validate_password(password, user=user)
|
|
|
|
except DjangoValidationError as errors:
|
2019-01-06 16:22:33 +01:00
|
|
|
raise ValidationError({"detail": " ".join(errors)})
|
2018-10-09 22:00:55 +02:00
|
|
|
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
|