Chapter 7.  Improving and Adding Authentication to an API with Flask

In this chapter, we will improve the RESTful API that we started in the previous chapter and we will add authentication related security to it. We will:

  • Improve unique constraints in the models
  • Update fields for a resource with the PATCH method
  • Code a generic pagination class
  • Add pagination features to the API
  • Understand the steps to add authentication and permissions
  • Add a user model
  • Create a schema to validate, serialize and deserialize users
  • Add authentication to resources
  • Create resource classes to handle users
  • Run migrations to generate the user table
  • Compose requests with the necessary authentication

Improving unique constraints in the models

When we created the Category model, we specified the True value for the unique argument when we created the db.Column instance named name. As a result, the migrations generated the necessary unique constraint to make sure that the name field has unique values in the category table. This way, the database won't allow us to insert duplicate values for category.name. However, the error message generated when we try to do so is not clear.

Run the following command to create a category with a duplicate name. There is already an existing category with the name equal to 'Information':

http POST :5000/api/categories/ name='Information'

The following is the equivalent curl command:

curl -iX POST -H "Content-Type: application/json" -d '{"name":"Information"}'
    :5000/api/categories/

The previous command will compose and send a POST HTTP request with the specified JSON key-value pair. The unique constraint in the category.name field won't allow the database table to persist the new category. Thus, the request will return an HTTP 400 Bad Request status code with an integrity error message. The following lines show a sample response:

HTTP/1.0 400 BAD REQUEST
Content-Length: 282
Content-Type: application/json
Date: Mon, 15 Aug 2016 03:53:27 GMT
Server: Werkzeug/0.11.10 Python/3.5.1
{
    "error": "(psycopg2.IntegrityError) duplicate key value violates unique
         constraint "category_name_key"
DETAIL:  Key (name)=(Information)
         already exists.
 [SQL: 'INSERT INTO category (name) VALUES (%(name)s)
         RETURNING category.id'] [parameters: {'name': 'Information'}]"
}

Obviously, the error message is extremely technical and provides too many details about the database and the query that failed. We might parse the error message to automatically generate a more user friendly error message. However, instead of doing so, we want to avoid trying to insert a row that we know will fail. We will add code to make sure that a category is unique before we try to persist it. Of course, there is still a chance to receive the previously shown error if somebody inserts a category with the same name between the time we run our code, indicating that a category name is unique, and persist the changes in the database. However, the chances are lower and we can reduce the changes of the previously shown error message to be shown.

Tip

In a production-ready REST API we should never return the error messages returned by SQLAlchemy or any other database-related data, as it might include sensitive data that we don't want the users of our API to be able to retrieve. In this case, we are returning all the errors for debugging purposes and to be able to improve our API.

Now, we will add a new class method to the Category class to allow us to determine whether a name is unique or not. Open the api/models.py file and add the following lines within the declaration of the Category class. The code file for the sample is included in the restful_python_chapter_07_01 folder:

    @classmethod 
    def is_unique(cls, id, name): 
        existing_category = cls.query.filter_by(name=name).first() 
        if existing_category is None: 
            return True 
        else: 
            if existing_category.id == id: 
                return True 
            else: 
                return False 

The new Category.is_unique class method receives the id and the name for the category that we want to make sure that has a unique name. If the category is a new one that hasn't been saved yet, we will receive a 0 for the id value. Otherwise, we will receive the category id in the argument.

The method calls the query.filter_by method for the current class to retrieve a category whose name matches the other category name. In case there is a category that matches the criteria, the method will return True only if the id is the same one than the one received in the argument. In case no category matches the criteria, the method will return True.

We will use the previously created class method to check whether a category is unique or not before creating and persisting it in the CategoryListResource.post method. Open the api/views.py file and replace the existing post method declared in the CategoryListResource class with the following lines. The lines that have been added or modified are highlighted. The code file for the sample is included in the restful_python_chapter_07_01 folder:

    def post(self): 
        request_dict = request.get_json() 
        if not request_dict: 
            resp = {'message': 'No input data provided'} 
            return resp, status.HTTP_400_BAD_REQUEST 
        errors = category_schema.validate(request_dict) 
        if errors: 
            return errors, status.HTTP_400_BAD_REQUEST 
        category_name = request_dict['name'] 
        if not Category.is_unique(id=0, name=category_name): 
            response = {'error': 'A category with the same name already exists'} 
            return response, status.HTTP_400_BAD_REQUEST 
        try:  
            category = Category(category_name) 
            category.add(category) 
            query = Category.query.get(category.id) 
            result = category_schema.dump(query).data 
            return result, status.HTTP_201_CREATED 
            except SQLAlchemyError as e: 
            db.session.rollback() 
            resp = {"error": str(e)} 
            return resp, status.HTTP_400_BAD_REQUEST 

Now, we will perform the same validation in the CategoryResource.patch method. Open the api/views.py file and replace the existing patch method declared in the CategoryResource class with the following lines. The lines that have been added or modified are highlighted. The code file for the sample is included in the restful_python_chapter_07_01 folder:

    def patch(self, id): 
        category = Category.query.get_or_404(id) 
        category_dict = request.get_json() 
        if not category_dict: 
            resp = {'message': 'No input data provided'} 
            return resp, status.HTTP_400_BAD_REQUEST 
        errors = category_schema.validate(category_dict) 
        if errors: 
            return errors, status.HTTP_400_BAD_REQUEST 
        try: 
            if 'name' in category_dict: 
                category_name = category_dict['name']  
                if Category.is_unique(id=id, name=category_name): 
                    category.name = category_name 
                else: 
                    response = {'error': 'A category with the same name already
                     exists'} 
                    return response, status.HTTP_400_BAD_REQUEST 
            category.update() 
            return self.get(id) 
                except SQLAlchemyError as e: 
                db.session.rollback() 
                resp = {"error": str(e)} 
                return resp, status.HTTP_400_BAD_REQUEST 

Run the following command to again create a category with a duplicate name:

http POST :5000/api/categories/ name='Information'

The following is the equivalent curl command:

curl -iX POST -H "Content-Type: application/json" -d '{"name":"Information"}'
    :5000/api/categories/

The previous command will compose and send a POST HTTP request with the specified JSON key-value pair. The changes we made will generate a response with a user friendly error message and will avoid trying to persist the changes. The request will return an HTTP 400 Bad Request status code with the error message in the JSON body. The following lines show a sample response:

HTTP/1.0 400 BAD REQUEST
Content-Length: 64
Content-Type: application/json
Date: Mon, 15 Aug 2016 04:38:43 GMT
Server: Werkzeug/0.11.10 Python/3.5.1
{
    "error": "A category with the same name already exists"
}

Now, we will add a new class method to the Message class to allow us to determine whether a message is unique or not. Open the api/models.py file and add the following lines within the declaration of the Message class. The code file for the sample is included in the restful_python_chapter_07_01 folder:

    @classmethod 
    def is_unique(cls, id, message): 
        existing_message = cls.query.filter_by(message=message).first() 
        if existing_message is None: 
            return True 
        else: 
            if existing_message.id == id: 
                return True 
            else: 
                return False 

The new Message.is_unique class method receives the id and the message for the message that we want to make sure that has a unique value for the message field. If the message is a new one that hasn't been saved yet, we will receive a 0 for the id value. Otherwise, we will receive the message id in the argument.

The method calls the query.filter_by method for the current class to retrieve a message whose message field matches the other message's message. In case there is a message that matches the criteria, the method will return True only if the id is the same one than the one received in the argument. In case no message matches the criteria, the method will return True.

We will use the previously created class method to check whether a message is unique or not before creating and persisting it in the MessageListResource.post method. Open the api/views.py file and replace the existing post method declared in the MessageListResource class with the following lines. The lines that have been added or modified are highlighted. The code file for the sample is included in the restful_python_chapter_07_01 folder:

    def post(self): 
        request_dict = request.get_json() 
        if not request_dict: 
            response = {'message': 'No input data provided'} 
            return response, status.HTTP_400_BAD_REQUEST 
        errors = message_schema.validate(request_dict) 
        if errors: 
            return errors, status.HTTP_400_BAD_REQUEST 
        message_message = request_dict['message'] 
        if not Message.is_unique(id=0, message=message_message): 
            response = {'error': 'A message with the same message already
            exists'} 
            return response, status.HTTP_400_BAD_REQUEST 
        try: 
            category_name = request_dict['category']['name'] 
            category = Category.query.filter_by(name=category_name).first() 
            if category is None: 
                # Create a new Category 
                category = Category(name=category_name) 
                db.session.add(category) 
            # Now that we are sure we have a category 
            # create a new Message 
            message = Message( 
                message=message_message, 
                duration=request_dict['duration'], 
                category=category) 
            message.add(message) 
            query = Message.query.get(message.id) 
            result = message_schema.dump(query).data 
            return result, status.HTTP_201_CREATED 
        except SQLAlchemyError as e: 
            db.session.rollback() 
            resp = {"error": str(e)} 
            return resp, status.HTTP_400_BAD_REQUEST 

Now, we will perform the same validation in the MessageResource.patch method. Open the api/views.py file and replace the existing patch method declared in the MessageResource class with the following lines. The lines that have been added or modified are highlighted. The code file for the sample is included in the restful_python_chapter_07_01 folder:

    def patch(self, id): 
        message = Message.query.get_or_404(id) 
        message_dict = request.get_json(force=True) 
        if 'message' in message_dict: 
            message_message = message_dict['message']  
            if Message.is_unique(id=id, message=message_message): 
                message.message = message_message 
            else: 
                response = {'error': 'A message with the same message already
                exists'} 
                return response, status.HTTP_400_BAD_REQUEST 
        if 'duration' in message_dict: 
            message.duration = message_dict['duration'] 
        if 'printed_times' in message_dict: 
            message.printed_times = message_dict['printed_times'] 
        if 'printed_once' in message_dict: 
            message.printed_once = message_dict['printed_once'] 
        dumped_message, dump_errors = message_schema.dump(message) 
        if dump_errors: 
            return dump_errors, status.HTTP_400_BAD_REQUEST 
        validate_errors = message_schema.validate(dumped_message) 
        if validate_errors: 
            return validate_errors, status.HTTP_400_BAD_REQUEST 
        try: 
            message.update() 
            return self.get(id) 
        except SQLAlchemyError as e: 
                db.session.rollback() 
                resp = {"error": str(e)} 
                return resp, status.HTTP_400_BAD_REQUEST 

Run the following command to create a message with a duplicate value for the message field:

http POST :5000/api/messages/ message='Checking temperature sensor' duration=25 category="Information"

The following is the equivalent curl command:

curl -iX POST -H "Content-Type: application/json" -d '{"message":"Checking temperature sensor", "duration":25, "category": "Information"}' :5000/api/messages/

The previous command will compose and send a POST HTTP request with the specified JSON key-value pair. The changes we made will generate a response with a user friendly error message and will avoid trying to persist the changes in the message. The request will return an HTTP 400 Bad Request status code with the error message in the JSON body. The following lines show a sample response:

HTTP/1.0 400 BAD REQUEST
Content-Length: 66
Content-Type: application/json
Date: Mon, 15 Aug 2016 04:55:46 GMT
Server: Werkzeug/0.11.10 Python/3.5.1
{
    "error": "A message with the same message already exists"
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.172.115