def get_q(self, qualifier, value, invert, partial=''): self.check_qualifier(qualifier) # TODO: Try to make the splitting and cleaning more re-usable if qualifier in ('in', 'range'): values = value.split(',') if qualifier == 'range': if len(values) != 2: raise BinderRequestError( 'Range requires exactly 2 values for {}.'.format( self.field_description())) else: values = [value] if qualifier == 'isnull': cleaned_value = True elif qualifier in ('in', 'range'): cleaned_value = [self.clean_value(qualifier, v) for v in values] else: try: cleaned_value = self.clean_value(qualifier, values[0]) except IndexError: raise ValidationError( 'Value for filter {{{}}}.{{{}}} may not be empty.'.format( self.field.model.__name__, self.field.name)) suffix = '__' + qualifier if qualifier else '' if invert: return ~Q(**{partial + self.field.name + suffix: cleaned_value}) else: return Q(**{partial + self.field.name + suffix: cleaned_value})
def change_password(self, request): """ Change the password from an old password Request: POST user/change_password/ { "old_password": str, "new_password": str } Response: Same as GET user/{id}/ """ if request.method != 'PUT': raise BinderMethodNotAllowed() self._require_model_perm('change_own_password', request) decoded = request.body.decode() try: body = json.loads(decoded) except ValueError: raise BinderRequestError( _('Invalid request body: not a JSON document.')) user = request.user errors = {} for item in ['old_password', 'new_password']: if body.get(item) is None: errors[item] = ['missing'] if not user.check_password(body.get('old_password')): errors['old_password'] = ['incorrect'] if len(errors) != 0: raise BinderValidationError(errors) password = body.get('new_password') try: password_validation.validate_password(password, user) except ValidationError as ve: validation_errors = {'new_password': ve.messages} raise BinderValidationError(validation_errors) user.set_password(password) user.save() logger.info('password changed for {}/{}'.format(user.id, user)) if user == request.user: """ No need to change the password of an user that is not our own """ update_session_auth_hash(request, user) return self.respond_with_user(request, user.id)
def multi_request_view(request): if request.method == 'GET': allowed_methods = ['GET'] elif request.method == 'POST': allowed_methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] else: try: raise BinderMethodNotAllowed() except BinderException as e: e.log() return e.response(request) requests = jsonloads(request.body) responses = [] key_responses = {} if not isinstance(requests, list): try: raise BinderRequestError('requests should be a list') except BinderException as e: e.log() return e.response(request) handler = RequestHandler() try: with transaction.atomic(): for i, data in enumerate(requests): key = data.pop('key', i) # Get response from request try: req = parse_request( data, allowed_methods, key_responses, request, ) except BinderException as e: e.log() res = e.response(request) else: res = handler.get_response(req) # Serialize and add to responses res_data = serialize_response(res) responses.append(res_data) # Add by key so that we can reference it in other requests key_responses[key] = res_data # Rollback the transaction if the request has a failing code if res.status_code >= 400: raise ErrorStatus(res.status_code) except ErrorStatus as e: status = e.status else: status = 200 return JsonResponse(responses, safe=False, status=status)
def handle_exceptions_view(request): # We create a model so we can test transaction rollback on error Zoo.objects.create(name='Test zoo') try: res = request.GET['res'] except KeyError: raise BinderRequestError('no res provided') else: return HttpResponse(res)
def filter_deleted(self, queryset, pk, deleted, request=None): """ Can be used to filter deleted users, or unfilter them. """ if pk or deleted == 'true': return queryset if deleted is None: return queryset.filter(is_active=True) if deleted == 'only': return queryset.filter(is_active=False) raise BinderRequestError( _('Invalid value: deleted=%s.') % request.GET.get('deleted'))
def activate(self, request, pk=None): """ Adds an endpoint to activate an user. Also logs in the user Request: PUT user/{id}/activate/ { "activation_code": string } Response: Same as GET user/{id}/ """ if request.method != 'PUT': raise BinderMethodNotAllowed() self._require_model_perm('activate', request) decoded = request.body.decode() try: body = json.loads(decoded) except ValueError: raise BinderRequestError( _('Invalid request body: not a JSON document.')) errors = {} for item in ['activation_code']: if body.get(item) is None: errors[item] = ['missing'] if len(errors) != 0: raise BinderValidationError(errors) try: user = self.model._default_manager.get(pk=pk) except (TypeError, ValueError, OverflowError, self.model.DoesNotExist): user = None if user is None or not self.token_generator.check_token( user, body.get('activation_code')): raise BinderNotFound() logger.info('login for {}/{} via successful activation'.format( user.id, user)) user.is_active = True user.save() self.auth_login(request, user) return self.respond_with_user(request, user.id)
def clean_qualifier(self, qualifier, value): if qualifier == 'isnull': cleaned_value = value not in ['0', 'false', 'False'] elif qualifier in ('in', 'range'): values = value.split(',') if qualifier == 'range' and len(values) != 2: raise BinderRequestError( 'Range requires exactly 2 values for {}.'.format( self.field_description())) cleaned_value = [ self.clean_value(qualifier, value) for value in values ] else: cleaned_value = self.clean_value(qualifier, value) return qualifier, cleaned_value
def reset_request(self, request): """ Adds an endpoint to do a reset request. Generates a token, and calls the _send_reset_mail callback if the reset request is successful Request: POST user/reset_request/ { 'username': '******' } Response: 204 { } """ if request.method != 'POST': raise BinderMethodNotAllowed() self._require_model_perm('reset_password', request) decoded = request.body.decode() try: body = json.loads(decoded) except ValueError: raise BinderRequestError( _('Invalid request body: not a JSON document.')) logger.info('password reset attempt for {}'.format( body.get(self.model.USERNAME_FIELD, ''))) for user in self.get_users( request, body.get(self.model.USERNAME_FIELD, '').lower()): token = self.token_generator.make_token(user) self._send_reset_mail(request, user, token) return HttpResponse(status=204)
def reset_password(self, request, pk=None): """ Resets the password from an reset code Request: POST user/reset_password/ { "reset_code": str, "password": str } Response: Same as GET user/{id}/ """ self._require_model_perm('reset_password', request) decoded = request.body.decode() try: body = json.loads(decoded) except ValueError: raise BinderRequestError( _('Invalid request body: not a JSON document.')) errors = { item: 'missing' for item in ['reset_code', 'password'] if item not in body } if errors: raise BinderValidationError(errors) return self._reset_pass_for_user(request, int(pk), body['reset_code'], body['password'])
def check_qualifier(self, qualifier): if qualifier not in self.allowed_qualifiers: raise BinderRequestError( 'Qualifier {} not supported for type {} ({}).'.format( qualifier, self.__class__.__name__, self.field_description()))
def parse_request(data, allowed_methods, responses, request): if not isinstance(data, dict): raise BinderRequestError('requests should be dicts') # Transform data transforms = data.pop('transforms', []) str_params = defaultdict(dict) if not isinstance(transforms, list): raise BinderRequestError('transforms should be a list') for transform in transforms: if 'source' not in transform: raise BinderRequestError('transforms require the field source') if 'target' not in transform: raise BinderRequestError('transforms require the field target') if (not isinstance(transform['source'], list) or len(transform['source']) < 1): raise BinderRequestError('source must be a non empty list') if (not isinstance(transform['target'], list) or len(transform['target']) < 2): raise BinderRequestError('target must be a non empty list') # Get value through source value = responses for key in transform['source']: if not isinstance(value, (list, dict)): raise BinderRequestError( 'source can only iterate through lists and dicts') try: value = value[key] except (KeyError, IndexError): raise BinderRequestError( 'invalid source {}, error at key {}'.format( transform['source'], key)) # Set value according to target target = data target_key = transform['target'][0] for i, key in enumerate(transform['target'][1:]): if isinstance(target, (list, dict)): try: target = target[target_key] except (KeyError, IndexError): raise BinderRequestError( 'invalid target {}, error at key {}'.format( transform['target'], target_key)) target_key = key else: raise BinderRequestError( 'target can only iterate through lists and dicts') if isinstance(target, (list, dict)): try: target[target_key] = value except IndexError: raise BinderRequestError( 'invalid target {}, error at key {}'.format( transform['target'], target_key)) elif isinstance(target, str): str_params[tuple( transform['target'][:-1])][transform['target'][-1]] = value else: raise BinderRequestError( 'target can only modify lists, dicts and strs') try: for keys, params in str_params.items(): target = data target_key = keys[0] for key in keys[1:]: target = target[target_key] target_key = key s = target[target_key] try: s = s.format(**params) except KeyError as e: raise BinderRequestError( 'str formatting at {}, missing key: {}'.format( keys, e.args[0])) target[target_key] = target[target_key].format(**params) except Exception: # All kind of things can go wrong when the data is altered through # a transform that occured after the str_params where determined # causing the target not to exist anymore or not be a str raise BinderRequestError( 'transforms altered data in such a way that str params became ' 'invalid') # Validate request if 'method' not in data: raise BinderRequestError('requests require the field method') if 'path' not in data: raise BinderRequestError('requests require the field path') # Validate method is allowed if data['method'] not in allowed_methods: raise BinderMethodNotAllowed() # Create request req = HttpRequest() req.method = data['method'] req.path = data['path'] req.path_info = req.path req.COOKIES = request.COOKIES req.META = request.META req.content_type = 'application/json' if 'body' in data: req._body = jsondumps(data['body']).encode() else: req._body = b'' return req
def multi_request_view(request): if request.method == 'GET': allowed_methods = ['GET'] elif request.method == 'POST': allowed_methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] else: try: raise BinderMethodNotAllowed() except BinderException as e: e.log() return e.response(request) requests = jsonloads(request.body) responses = [] key_responses = {} if not isinstance(requests, list): try: raise BinderRequestError('requests should be a list') except BinderException as e: e.log() return e.response(request) handler = RequestHandler() try: with transaction.atomic(): for i, data in enumerate(requests): key = data.pop('key', i) # Get response from request try: req = parse_request( data, allowed_methods, key_responses, request, ) except BinderException as e: e.log() res = e.response(request) else: # There's a "sneaky little hack" in Django test client # to avoid csrf checks in tests. This needs to be passed # down to make sure tests for multi request are also skipping # csrf checks. See also: https://github.com/django/django/blob/ebb08d19424c314c75908bc6048ff57c2f872269/django/test/client.py#L142 req._dont_enforce_csrf_checks = request._dont_enforce_csrf_checks res = handler.get_response(req) # Serialize and add to responses res_data = serialize_response(res) responses.append(res_data) # Add by key so that we can reference it in other requests key_responses[key] = res_data # Rollback the transaction if the request has a failing code if res.status_code >= 400: raise ErrorStatus(res.status_code) except ErrorStatus as e: status = e.status else: status = 200 return JsonResponse(responses, safe=False, status=status)
def send_activation_email(self, request): """ Endpoint that can be used to send an activation mail for an user. Calls the _send_activation_email callback if the user is succesfully activated Request: POST { "email": "email" } Response: { "code": code } Possible codes: sent Mail is send sucessfully already active User is already active, no mail was send blacklisted User was not activated """ if request.method != 'PUT': raise BinderMethodNotAllowed() # For lack of a better check self._require_model_perm('reset_password', request) decoded = request.body.decode() try: body = json.loads(decoded) except ValueError: raise BinderRequestError( _('Invalid request body: not a JSON document.')) logger.info('activation email attempt for {}'.format( body.get('email', ''))) if body.get('email') is None: raise BinderValidationError({'email': ['missing']}) try: user = self.model._default_manager.get(email=body.get('email')) except self.model.DoesNotExist: raise BinderNotFound() if user.is_active: if user.last_login is None: # TODO: Figure out a way to make this customisable without # allowing injection of arbitrary URLs (phishing!) self._send_activation_email(request, user) response = JsonResponse({'code': 'sent'}) response.status_code = 201 else: response = JsonResponse({'code': 'already active'}) else: response = JsonResponse({'code': 'blacklisted'}) response.status_code = 400 return response