Custom authentication and parameter checking in Flask

1k Views Asked by At

I am currently refactoring a Flask app I wrote about two years ago and I suspect having done a few things not as elegantly and clean as it would have been possible using the library. I would therefore ask for some advice on how to improve upon the status quo:

  1. The app provides a number of API-Endpoints each reachable via a route of the form /<category>/<endpoint> where <category> is one of 10 distinct categories.
  2. For each <category> I created a distinct Python module <category>.py, placed it an api/ subdirectory and a distinct flask.Blueprint (actually a subclass thereof, see later in the text) and registered that with the main app = Flask(__name__).
  3. Each module <category>.py contains a number of functions that serve as endpoints for the respective category and that are each decorated with a route-Decorator.

So far so good.

Each endpoint function accepts a number of parameters that may either be passed as parameters of a GET-request or as part of a parameter field in the JSON-payload of a POST-request. So before calling the respective endpoint function it is checked whether or not the correct number of parameters with the correct names are provided.

Also, the app needs to check whether or not the client is allowed to call a certain function. To do this, the environment variable SSL_CLIENT_CERT that was set by the webserver (lighttpd over FCGI in my case) is read and its fingerprint compared to some internal permission file.

Since I did not quite know where to put the logic to do the above I subclassed flask.Blueprint and wrote my own (modified) route decorator (custom_route). This decorator now either returns a custom made error response (flask.Response object) or a custom-made success response (thereby calling the endpoint function with the parameters passed from the client).

So a module category_xy.py looks something like this:

category_xy = CustomBlueprint('category_xy', __name__)    

@category_xy.custom_route('/category_xy/some_endpoint',
                          auth_required=True,
                          methods=['GET', 'POST'])
def some_endpoint(foo, bar):
    # do stuff 
    return '123'

With CustomBlueprint defined in a separate file as (partly pseudoish-code):

from flask import Blueprint, Response, g, request
from functools import wraps


class CustomBlueprint(Blueprint):
    def custom_route(self, rule, **options):
        def decorator(f):

            # don't pass the custom parameter 'auth_required' on to
            # self.route
            modified_options = dict(options)
            if 'auth_required' in modified_options:
                del modified_options['auth_required']

            @self.route(rule, **modified_options)
            @wraps(f)
            def wrapper():
                # do some authentication checks...
                if not authenticiated():
                    return Response(...)

                # check if correct paramters have been passed
                if not correct_paramters():
                    return Response(...)

                # extract parameter values from either GET or POST request
                arg_values = get_arg_values(request)

                # call the decorated function with these parameters
                result = f(*arg_values)

                return Response(result, ...)
            return wrapper
        return decorator

This works but it does not feel clean at all and I think that there should be a better and cleaner way of doing this. Putting all this logic in a custom decorator feels quite wrong.

Could somebody more experienced with Flask provide some thoughts and/or best practice?

1

There are 1 best solutions below

0
Sergey Shubin On

As a start you can switch to Pluggable Views — API that allows you to write your view functions as classes:

from flask import Blueprint, Response, render_template
from flask.views import View

from models import db, CategoryXY # Let`s have here a database model

category_blueprint = Blueprint('category_xy', __name__)

class CategoryXYView(View):
    def get_list(self):
        items = db.session.query(CategoryXY).all()
        return render_template('category_xy_list.html', items=items)

    def get_one(self, item_id):
        item = db.session.query(CategoryXY).first()
        if item is None:
            return Response(status=404)
        return render_template('category_xy_edit.html', item=item)

    def dispatch_request(self, item_id):
        """Required method which handles incoming requests"""
        if item_id is None:
            return self.get_list()
        return self.get_one(item_id)

category_xy_view = CategoryXYView.as_view('category_xy')
category_blueprint.add_url_rule(
    'category_xy',
    view_func=category_xy_view,
    defaults={'item_id': None}
)
category_blueprint.add_url_rule(
    'category_xy/<item_id>',
    view_func=category_xy_view
)

It becomes really helpful when you have base view class with common methods (database objects handling, validation, authorization checking) and "category" classes are inherited from it:

class BaseView(View):
    model = None
    edit_template = None
    list_template = None

    def get_one(self, item_id):
        item = db.session.query(self.model).filter_by(id=item_id).first()
        if item is None:
            return Response(status=404)
        return render_template(self.edit_template, item=item)

    def get_list(self):
        items = db.session.query(self.model).all()
        return render_template(self.list_template, items=items)

    def dispatch_request(self, item_id):
        if item_id is None:
            return self.get_list()
        return self.get_one(item_id)

class CategoryXYView(BaseView):
    model = CategoryXY
    edit_template = 'category_xy_edit.html'
    list_template = 'category_xy_list.html'

category_xy_view = CategoryXYView.as_view('category_xy')
category_blueprint.add_url_rule(
    'category_xy',
    view_func=category_xy_view,
    defaults={'item_id': None}
)
category_blueprint.add_url_rule(
    'category_xy/<item_id>',
    view_func=category_xy_view
)

If one of categories has extended functionality you can just override one of the common methods:

class CategoryXXView(BaseView):
    model = CategoryXX
    edit_template = 'category_xx_edit.html'
    list_template = 'category_xx_list.html'

    def get_one(self, item_id):
        item = db.session.query(self.model).first()
        if item is None:
            return Response(status=404)
        xy_items = db.session.query(CategoryXY).all()
        return render_template(self.edit_template, item=item, xy_items=xy_items)

In order to automate routes generation you can write a metaclass for BaseView. Routes for every new class will be generated after class definition:

class ViewMeta(type):
    def __new__(mcs, name, bases, attributes):
        cls = type(name, bases, attributes)

        if cls.route is not None:
            view_function = cls.as_view(cls.route)
            category_blueprint.add_url_rule(
                cls.route,
                view_func=view_function,
                defaults={'item_id': None}
            )
            category_blueprint.add_url_rule(
                '{}/<item_id>'.format(cls.route),
                view_func=view_function
            )
        return cls

class BaseView(View):
    __metaclass__ = ViewMeta
    route = None
    model = None
    # Other common attributes

class CategoryXYView(BaseView):
    route = 'category_xy'
    model = CategoryXY