etesync-server/django_etebase/views.py
Tom Hacohen 267d749c45 Collection: change collections to be an extension of items
Each collection now has an item and the item's UID is the collections
UID. This lets us manipulate collections just like items, and as part of
transactions. This is significant because it lets us change them as part
of transactions!
2020-06-23 13:03:46 +03:00

757 lines
30 KiB
Python

# Copyright © 2017 Tom Hacohen
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, version 3.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import json
from functools import reduce
from django.conf import settings
from django.contrib.auth import get_user_model, user_logged_in, user_logged_out
from django.core.exceptions import PermissionDenied
from django.db import transaction, IntegrityError
from django.db.models import Max, Q
from django.http import HttpResponseBadRequest, HttpResponse, Http404
from django.shortcuts import get_object_or_404
from rest_framework import status
from rest_framework import viewsets
from rest_framework import parsers
from rest_framework.decorators import action as action_decorator
from rest_framework.response import Response
import nacl.encoding
import nacl.signing
import nacl.secret
import nacl.hash
from .token_auth.models import AuthToken
from . import app_settings, permissions
from .models import (
Collection,
CollectionItem,
CollectionItemRevision,
CollectionMember,
CollectionMemberRemoved,
CollectionInvitation,
Stoken,
UserInfo,
)
from .serializers import (
b64encode,
AuthenticationChangePasswordInnerSerializer,
AuthenticationSignupSerializer,
AuthenticationLoginChallengeSerializer,
AuthenticationLoginSerializer,
AuthenticationLoginInnerSerializer,
CollectionSerializer,
CollectionItemSerializer,
CollectionItemBulkGetSerializer,
CollectionItemDepSerializer,
CollectionItemRevisionSerializer,
CollectionItemChunkSerializer,
CollectionMemberSerializer,
CollectionInvitationSerializer,
InvitationAcceptSerializer,
UserInfoPubkeySerializer,
UserSerializer,
)
User = get_user_model()
class BaseViewSet(viewsets.ModelViewSet):
authentication_classes = tuple(app_settings.API_AUTHENTICATORS)
permission_classes = tuple(app_settings.API_PERMISSIONS)
stoken_id_fields = None
def get_serializer_class(self):
serializer_class = self.serializer_class
if self.request.method == 'PUT':
serializer_class = getattr(self, 'serializer_update_class', serializer_class)
return serializer_class
def get_collection_queryset(self, queryset=Collection.objects):
user = self.request.user
return queryset.filter(members__user=user)
def get_stoken_obj(self, request):
stoken = request.GET.get('stoken', None)
if stoken is not None:
return get_object_or_404(Stoken.objects.all(), uid=stoken)
return None
def filter_by_stoken(self, request, queryset):
stoken_rev = self.get_stoken_obj(request)
if stoken_rev is not None:
filter_by_map = map(lambda x: Q(**{x + '__gt': stoken_rev.id}), self.stoken_id_fields)
filter_by = reduce(lambda x, y: x | y, filter_by_map)
queryset = queryset.filter(filter_by).distinct()
return queryset, stoken_rev
def get_queryset_stoken(self, queryset):
aggr_field_names = ['max_{}'.format(i) for i, x in enumerate(self.stoken_id_fields)]
aggr_fields = {name: Max(field) for name, field in zip(aggr_field_names, self.stoken_id_fields)}
aggr = queryset.annotate(**aggr_fields).values(*aggr_field_names)
# FIXME: we are doing it in python instead of SQL because I just couldn't get aggregate to work over the
# annotated values. This should probably be fixed as it could be quite slow
maxid = -1
for row in aggr:
rowmaxid = max(map(lambda x: x or -1, row.values()))
maxid = max(maxid, rowmaxid)
new_stoken = (maxid >= 0) and Stoken.objects.get(id=maxid).uid
return queryset, new_stoken
def filter_by_stoken_and_limit(self, request, queryset):
limit = int(request.GET.get('limit', 50))
queryset, stoken_rev = self.filter_by_stoken(request, queryset)
stoken = stoken_rev.uid if stoken_rev is not None else None
queryset = queryset[:limit]
queryset, new_stoken = self.get_queryset_stoken(queryset)
new_stoken = new_stoken or stoken
# This is not the most efficient way of implementing this, but it's good enough
done = len(queryset) < limit
return queryset, new_stoken, done
# Change how our list works by default
def list(self, request, collection_uid=None):
queryset = self.get_queryset()
serializer = self.get_serializer(queryset, many=True)
ret = {
'data': serializer.data,
'done': True, # we always return all the items, so it's always done
}
return Response(ret)
class CollectionViewSet(BaseViewSet):
allowed_methods = ['GET', 'POST']
permission_classes = BaseViewSet.permission_classes + (permissions.IsCollectionAdminOrReadOnly, )
queryset = Collection.objects.all()
serializer_class = CollectionSerializer
lookup_field = 'main_item__uid'
lookup_url_kwarg = 'uid'
stoken_id_fields = ['items__revisions__stoken__id', 'members__stoken__id']
def get_queryset(self, queryset=None):
if queryset is None:
queryset = type(self).queryset
return self.get_collection_queryset(queryset)
def get_serializer_context(self):
context = super().get_serializer_context()
inline = 'inline' in self.request.query_params
context.update({'request': self.request, 'inline': inline})
return context
def destroy(self, request, uid=None):
# FIXME: implement
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
def partial_update(self, request, uid=None):
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
def update(self, request, *args, **kwargs):
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
try:
serializer.save(owner=self.request.user)
except IntegrityError:
content = {'code': 'integrity_error'}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
return Response({}, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def list(self, request):
queryset = self.get_queryset()
queryset, new_stoken, done = self.filter_by_stoken_and_limit(request, queryset)
serializer = self.get_serializer(queryset, many=True)
ret = {
'data': serializer.data,
'stoken': new_stoken,
'done': done,
}
stoken_obj = self.get_stoken_obj(request)
if stoken_obj is not None:
# FIXME: honour limit? (the limit should be combined for data and this because of stoken)
remed = CollectionMemberRemoved.objects.filter(user=request.user, stoken__id__gt=stoken_obj.id) \
.values_list('collection__main_item__uid', flat=True)
if len(remed) > 0:
ret['removedMemberships'] = [{'uid': x} for x in remed]
return Response(ret)
class CollectionItemViewSet(BaseViewSet):
allowed_methods = ['GET', 'POST', 'PUT']
permission_classes = BaseViewSet.permission_classes + (permissions.HasWriteAccessOrReadOnly, )
queryset = CollectionItem.objects.all()
serializer_class = CollectionItemSerializer
lookup_field = 'uid'
stoken_id_fields = ['revisions__stoken__id']
def get_queryset(self):
collection_uid = self.kwargs['collection_uid']
try:
collection = self.get_collection_queryset(Collection.objects).get(main_item__uid=collection_uid)
except Collection.DoesNotExist:
raise Http404("Collection does not exist")
# XXX Potentially add this for performance: .prefetch_related('revisions__chunks')
queryset = type(self).queryset.filter(collection__pk=collection.pk,
uid__isnull=False,
revisions__current=True)
return queryset
def get_serializer_context(self):
context = super().get_serializer_context()
inline = 'inline' in self.request.query_params
context.update({'request': self.request, 'inline': inline})
return context
def create(self, request, collection_uid=None):
# We create using batch and transaction
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
def destroy(self, request, collection_uid=None, uid=None):
# We can't have destroy because we need to get data from the user (in the body) such as hmac.
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
def update(self, request, collection_uid=None, uid=None):
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
def partial_update(self, request, collection_uid=None, uid=None):
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
def list(self, request, collection_uid=None):
queryset = self.get_queryset()
queryset, new_stoken, done = self.filter_by_stoken_and_limit(request, queryset)
serializer = self.get_serializer(queryset, many=True)
ret = {
'data': serializer.data,
'stoken': new_stoken,
'done': done,
}
return Response(ret)
@action_decorator(detail=True, methods=['GET'])
def revision(self, request, collection_uid=None, uid=None):
# FIXME: need pagination support
col = get_object_or_404(self.get_collection_queryset(Collection.objects), main_item__uid=collection_uid)
col_it = get_object_or_404(col.items, uid=uid)
serializer = CollectionItemRevisionSerializer(col_it.revisions.order_by('-id'), many=True)
ret = {
'data': serializer.data,
'done': True, # we always return all the items, so it's always done
}
return Response(ret)
# FIXME: rename to something consistent with what the clients have - maybe list_updates?
@action_decorator(detail=False, methods=['POST'])
def fetch_updates(self, request, collection_uid=None):
queryset = self.get_queryset()
serializer = CollectionItemBulkGetSerializer(data=request.data, many=True)
if serializer.is_valid():
# FIXME: make configurable?
item_limit = 200
if len(serializer.validated_data) > item_limit:
content = {'code': 'too_many_items',
'detail': 'Request has too many items. Limit: {}'. format(item_limit)}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
queryset, stoken_rev = self.filter_by_stoken(request, queryset)
uids, etags = zip(*[(item['uid'], item.get('etag')) for item in serializer.validated_data])
revs = CollectionItemRevision.objects.filter(uid__in=etags, current=True)
queryset = queryset.filter(uid__in=uids).exclude(revisions__in=revs)
queryset, new_stoken = self.get_queryset_stoken(queryset)
stoken = stoken_rev and stoken_rev.uid
new_stoken = new_stoken or stoken
serializer = self.get_serializer(queryset, many=True)
ret = {
'data': serializer.data,
'stoken': new_stoken,
'done': True, # we always return all the items, so it's always done
}
return Response(ret)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@action_decorator(detail=False, methods=['POST'])
def batch(self, request, collection_uid=None):
return self.transaction(request, collection_uid, validate_etag=False)
@action_decorator(detail=False, methods=['POST'])
def transaction(self, request, collection_uid=None, validate_etag=True):
stoken = request.GET.get('stoken', None)
with transaction.atomic(): # We need this for locking on the collection object
collection_object = get_object_or_404(
self.get_collection_queryset(Collection.objects).select_for_update(), # Lock writes on the collection
main_item__uid=collection_uid)
if stoken is not None and stoken != collection_object.stoken:
content = {'code': 'stale_stoken', 'detail': 'Stoken is too old'}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
items = request.data.get('items')
deps = request.data.get('deps', None)
# FIXME: It should just be one serializer
context = self.get_serializer_context()
context.update({'validate_etag': validate_etag})
serializer = self.get_serializer_class()(data=items, context=context, many=True)
deps_serializer = CollectionItemDepSerializer(data=deps, context=context, many=True)
ser_valid = serializer.is_valid()
deps_ser_valid = (deps is None or deps_serializer.is_valid())
if ser_valid and deps_ser_valid:
try:
items = serializer.save(collection=collection_object)
except IntegrityError:
# FIXME: return the items with a bad token (including deps) so we don't have to fetch them after
content = {'code': 'integrity_error'}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
ret = {
}
return Response(ret, status=status.HTTP_200_OK)
return Response(
{
"items": serializer.errors,
"deps": deps_serializer.errors if deps is not None else [],
},
status=status.HTTP_400_BAD_REQUEST)
class CollectionItemChunkViewSet(viewsets.ViewSet):
allowed_methods = ['GET', 'POST']
parser_classes = (parsers.MultiPartParser, )
authentication_classes = BaseViewSet.authentication_classes
permission_classes = BaseViewSet.permission_classes
serializer_class = CollectionItemChunkSerializer
lookup_field = 'uid'
def get_serializer_class(self):
return self.serializer_class
def get_collection_queryset(self, queryset=Collection.objects):
user = self.request.user
return queryset.filter(members__user=user)
def create(self, request, collection_uid=None, collection_item_uid=None):
col = get_object_or_404(self.get_collection_queryset(), main_item__uid=collection_uid)
col_it = get_object_or_404(col.items, uid=collection_item_uid)
serializer = self.get_serializer_class()(data=request.data)
if serializer.is_valid():
try:
serializer.save(item=col_it)
except IntegrityError:
content = {'code': 'integrity_error'}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
return Response({}, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@action_decorator(detail=True, methods=['GET'])
def download(self, request, collection_uid=None, collection_item_uid=None, uid=None):
import os
from django.views.static import serve
col = get_object_or_404(self.get_collection_queryset(), main_item__uid=collection_uid)
col_it = get_object_or_404(col.items, uid=collection_item_uid)
chunk = get_object_or_404(col_it.chunks, uid=uid)
filename = chunk.chunkFile.path
dirname = os.path.dirname(filename)
basename = os.path.basename(filename)
# FIXME: DO NOT USE! Use django-send file or etc instead.
return serve(request, basename, dirname)
class CollectionMemberViewSet(BaseViewSet):
allowed_methods = ['GET', 'PUT', 'DELETE']
our_base_permission_classes = BaseViewSet.permission_classes
permission_classes = our_base_permission_classes + (permissions.IsCollectionAdmin, )
queryset = CollectionMember.objects.all()
serializer_class = CollectionMemberSerializer
lookup_field = 'user__' + User.USERNAME_FIELD
lookup_url_kwarg = 'username'
stoken_id_fields = ['stoken__id']
# FIXME: need to make sure that there's always an admin, and maybe also don't let an owner remove adm access
# (if we want to transfer, we need to do that specifically)
def get_queryset(self, queryset=None):
collection_uid = self.kwargs['collection_uid']
try:
collection = self.get_collection_queryset(Collection.objects).get(main_item__uid=collection_uid)
except Collection.DoesNotExist:
raise Http404('Collection does not exist')
if queryset is None:
queryset = type(self).queryset
return queryset.filter(collection=collection)
# We override this method because we expect the stoken to be called iterator
def get_stoken_obj(self, request):
stoken = request.GET.get('iterator', None)
if stoken is not None:
return get_object_or_404(Stoken.objects.all(), uid=stoken)
return None
def list(self, request, collection_uid=None):
queryset = self.get_queryset()
queryset, new_stoken, done = self.filter_by_stoken_and_limit(request, queryset)
serializer = self.get_serializer(queryset, many=True)
ret = {
'data': serializer.data,
'iterator': new_stoken, # Here we call it an iterator, it's only stoken for collection/items
'done': done,
}
return Response(ret)
def create(self, request):
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
# FIXME: block leaving if we are the last admins - should be deleted / assigned in this case depending if there
# are other memebers.
def perform_destroy(self, instance):
instance.revoke()
@action_decorator(detail=False, methods=['POST'], permission_classes=our_base_permission_classes)
def leave(self, request, collection_uid=None):
collection_uid = self.kwargs['collection_uid']
col = get_object_or_404(self.get_collection_queryset(Collection.objects), main_item__uid=collection_uid)
member = col.members.get(user=request.user)
self.perform_destroy(member)
return Response({})
class InvitationBaseViewSet(BaseViewSet):
queryset = CollectionInvitation.objects.all()
serializer_class = CollectionInvitationSerializer
lookup_field = 'uid'
lookup_url_kwarg = 'invitation_uid'
def list(self, request, collection_uid=None):
limit = int(request.GET.get('limit', 50))
iterator = request.GET.get('iterator', None)
queryset = self.get_queryset().order_by('id')
if iterator is not None:
iterator = get_object_or_404(queryset, uid=iterator)
queryset = queryset.filter(id__gt=iterator.id)
queryset = queryset[:limit]
serializer = self.get_serializer(queryset, many=True)
# This is not the most efficient way of implementing this, but it's good enough
done = len(queryset) < limit
last_item = len(queryset) > 0 and serializer.data[-1]
ret = {
'data': serializer.data,
'iterator': last_item and last_item['uid'],
'done': done,
}
return Response(ret)
class InvitationOutgoingViewSet(InvitationBaseViewSet):
allowed_methods = ['GET', 'POST', 'PUT', 'DELETE']
def get_queryset(self, queryset=None):
if queryset is None:
queryset = type(self).queryset
return queryset.filter(fromMember__user=self.request.user)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
collection_uid = serializer.validated_data.get('collection', {}).get('uid')
try:
collection = self.get_collection_queryset(Collection.objects).get(main_item__uid=collection_uid)
except Collection.DoesNotExist:
raise Http404('Collection does not exist')
if not permissions.is_collection_admin(collection, request.user):
raise PermissionDenied('User is not an admin of this collection')
serializer.save(collection=collection)
return Response({}, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@action_decorator(detail=False, allowed_methods=['GET'], methods=['GET'])
def fetch_user_profile(self, request):
username = request.GET.get('username')
kwargs = {'owner__' + User.USERNAME_FIELD: username}
user_info = get_object_or_404(UserInfo.objects.all(), **kwargs)
serializer = UserInfoPubkeySerializer(user_info)
return Response(serializer.data)
class InvitationIncomingViewSet(InvitationBaseViewSet):
allowed_methods = ['GET', 'DELETE']
def get_queryset(self, queryset=None):
if queryset is None:
queryset = type(self).queryset
return queryset.filter(user=self.request.user)
@action_decorator(detail=True, allowed_methods=['POST'], methods=['POST'])
def accept(self, request, invitation_uid=None):
invitation = get_object_or_404(self.get_queryset(), uid=invitation_uid)
context = self.get_serializer_context()
context.update({'invitation': invitation})
serializer = InvitationAcceptSerializer(data=request.data, context=context)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(status=status.HTTP_201_CREATED)
class AuthenticationViewSet(viewsets.ViewSet):
allowed_methods = ['POST']
authentication_classes = BaseViewSet.authentication_classes
def get_encryption_key(self, salt):
key = nacl.hash.blake2b(settings.SECRET_KEY.encode(), encoder=nacl.encoding.RawEncoder)
return nacl.hash.blake2b(b'', key=key, salt=salt[:nacl.hash.BLAKE2B_SALTBYTES], person=b'etebase-auth',
encoder=nacl.encoding.RawEncoder)
def get_queryset(self):
return User.objects.all()
def login_response_data(self, user):
return {
'token': AuthToken.objects.create(user=user).key,
'user': UserSerializer(user).data,
}
def list(self, request):
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
@action_decorator(detail=False, methods=['POST'])
def signup(self, request):
serializer = AuthenticationSignupSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.save()
data = self.login_response_data(user)
return Response(data, status=status.HTTP_201_CREATED)
def get_login_user(self, username):
kwargs = {User.USERNAME_FIELD: username}
return get_object_or_404(self.get_queryset(), **kwargs)
def validate_login_request(self, request, validated_data, response_raw, signature, expected_action):
from datetime import datetime
username = validated_data.get('username')
user = self.get_login_user(username)
host = validated_data['host']
challenge = validated_data['challenge']
action = validated_data['action']
salt = bytes(user.userinfo.salt)
enc_key = self.get_encryption_key(salt)
box = nacl.secret.SecretBox(enc_key)
challenge_data = json.loads(box.decrypt(challenge).decode())
now = int(datetime.now().timestamp())
if action != expected_action:
content = {'code': 'wrong_action', 'detail': 'Expected "{}" but got something else'.format(expected_action)}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
elif now - challenge_data['timestamp'] > app_settings.CHALLENGE_VALID_SECONDS:
content = {'code': 'challenge_expired', 'detail': 'Login challange has expired'}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
elif challenge_data['userId'] != user.id:
content = {'code': 'wrong_user', 'detail': 'This challenge is for the wrong user'}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
elif not settings.DEBUG and host != request.get_host():
detail = 'Found wrong host name. Got: "{}" expected: "{}"'.format(host, request.get_host())
content = {'code': 'wrong_host', 'detail': detail}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
verify_key = nacl.signing.VerifyKey(bytes(user.userinfo.loginPubkey), encoder=nacl.encoding.RawEncoder)
try:
verify_key.verify(response_raw, signature)
except nacl.exceptions.BadSignatureError:
return Response({'code': 'login_bad_signature'}, status=status.HTTP_400_BAD_REQUEST)
return None
@action_decorator(detail=False, methods=['POST'])
def login_challenge(self, request):
from datetime import datetime
serializer = AuthenticationLoginChallengeSerializer(data=request.data)
if serializer.is_valid():
username = serializer.validated_data.get('username')
user = self.get_login_user(username)
salt = bytes(user.userinfo.salt)
enc_key = self.get_encryption_key(salt)
box = nacl.secret.SecretBox(enc_key)
challenge_data = {
"timestamp": int(datetime.now().timestamp()),
"userId": user.id,
}
challenge = box.encrypt(json.dumps(
challenge_data, separators=(',', ':')).encode(), encoder=nacl.encoding.RawEncoder)
ret = {
"salt": b64encode(salt),
"challenge": b64encode(challenge),
"version": user.userinfo.version,
}
return Response(ret, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@action_decorator(detail=False, methods=['POST'])
def login(self, request):
outer_serializer = AuthenticationLoginSerializer(data=request.data)
outer_serializer.is_valid(raise_exception=True)
response_raw = outer_serializer.validated_data['response']
response = json.loads(response_raw.decode())
signature = outer_serializer.validated_data['signature']
serializer = AuthenticationLoginInnerSerializer(data=response, context={'host': request.get_host()})
serializer.is_valid(raise_exception=True)
bad_login_response = self.validate_login_request(
request, serializer.validated_data, response_raw, signature, "login")
if bad_login_response is not None:
return bad_login_response
username = serializer.validated_data.get('username')
user = self.get_login_user(username)
data = self.login_response_data(user)
user_logged_in.send(sender=user.__class__, request=request, user=user)
return Response(data, status=status.HTTP_200_OK)
@action_decorator(detail=False, methods=['POST'], permission_classes=BaseViewSet.permission_classes)
def logout(self, request):
request.auth.delete()
user_logged_out.send(sender=request.user.__class__, request=request, user=request.user)
return Response(status=status.HTTP_204_NO_CONTENT)
@action_decorator(detail=False, methods=['POST'], permission_classes=BaseViewSet.permission_classes)
def change_password(self, request):
outer_serializer = AuthenticationLoginSerializer(data=request.data)
outer_serializer.is_valid(raise_exception=True)
response_raw = outer_serializer.validated_data['response']
response = json.loads(response_raw.decode())
signature = outer_serializer.validated_data['signature']
serializer = AuthenticationChangePasswordInnerSerializer(
request.user.userinfo, data=response, context={'host': request.get_host()})
serializer.is_valid(raise_exception=True)
bad_login_response = self.validate_login_request(
request, serializer.validated_data, response_raw, signature, "changePassword")
if bad_login_response is not None:
return bad_login_response
serializer.save()
return Response({}, status=status.HTTP_200_OK)
class TestAuthenticationViewSet(viewsets.ViewSet):
allowed_methods = ['POST']
def list(self, request):
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
@action_decorator(detail=False, methods=['POST'])
def reset(self, request, *args, **kwargs):
# Only run when in DEBUG mode! It's only used for tests
if not settings.DEBUG:
return HttpResponseBadRequest("Only allowed in debug mode.")
user = get_object_or_404(User.objects.all(), username=request.data.get('user').get('username'))
# Only allow test users for extra safety
if not getattr(user, User.USERNAME_FIELD).startswith('test_user'):
return HttpResponseBadRequest("Endpoint not allowed for user.")
if hasattr(user, 'userinfo'):
user.userinfo.delete()
serializer = AuthenticationSignupSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
# Delete all of the journal data for this user for a clear test env
user.collection_set.all().delete()
user.incoming_invitations.all().delete()
# FIXME: also delete chunk files!!!
return HttpResponse()