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 endmasquerade(self, request): from hijack.helpers import release_hijack if request.method != 'POST': raise BinderMethodNotAllowed() self._require_model_perm('unmasquerade', request) release_hijack(request) # Ignore returned redirect response object return self.respond_with_user(request, request.user.id)
def login(self, request): data = jsonloads(request.body) username = data.get('username', '').lower() password = data.get('password', '') if request.method != 'POST': raise BinderMethodNotAllowed() user = auth.authenticate(username=username, password=password) if user is None: raise BinderNotAuthenticated() auth.login(request, user) return JsonResponse(UserView()._get_obj(user.id, request=request))
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 masquerade(self, request, pk=None): from hijack.helpers import login_user if request.method != 'POST': raise BinderMethodNotAllowed() try: user = self.model._default_manager.get(pk=pk) except self.model.DoesNotExist: raise BinderNotFound() self._require_model_perm('masquerade', request) login_user(request, user) # Ignore returned redirect response object return self.respond_with_user(request, user.id)
def soft_delete(self, user, undelete=False, request=None): """ Allows the user to be soft deleted, and undeleted. What actually needs to be done on soft deletion can be implemented in _after_soft_delete """ try: if not user.is_active and not undelete: raise BinderIsDeleted() if not not user.is_active and undelete: raise BinderIsNotDeleted() except AttributeError: raise BinderMethodNotAllowed() user.is_active = undelete user.save() self._after_soft_delete(request, user, undelete)
def login(self, request): """ Login the user Request: POST user/login/ { "username": "******", "password": "******" } Response: returns the same parameters as GET user/{id}/ """ if request.method != 'POST': raise BinderMethodNotAllowed() try: decoded = request.body.decode() body = json.loads(decoded) username = body.get(self.model.USERNAME_FIELD, '') password = body.get('password', '') except Exception: username = request.POST.get(self.model.USERNAME_FIELD, '') password = request.POST.get('password', '') user = self.authenticate( request, **{ self.model.USERNAME_FIELD: username.lower(), 'password': password, }) self._require_model_perm('login', request) if user is None: logger.info('login failed for "{}"'.format(username)) raise BinderNotAuthenticated() else: self.auth_login(request, user) logger.info('login for {}/{}'.format(user.id, user)) return self.respond_with_user(request, user.id)
def logout(self, request): """ Logout the user Request: POST /user/logout/ {} Response: 204 {} """ if request.method != 'POST': raise BinderMethodNotAllowed() self._require_model_perm('logout', request) logger.info('logout for {}/{}'.format(request.user.id, request.user)) self._logout(request) return HttpResponse(status=204)
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 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 logout(self, request): if request.method != 'POST': raise BinderMethodNotAllowed() auth.logout(request) return HttpResponse(status=204)
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