How to make a python decorator function in Flask with arguments (for authorization)

16,946

Here's how to do it:

from functools import update_wrapper

def owns_hotdog(hotdog):
    def decorator(fn):
        def wrapped_function(*args, **kwargs):
            # First check if user is authenticated.
            if not logged_in():
                return redirect(url_for('login'))
            # For authorization error it is better to return status code 403
            # and handle it in errorhandler separately, because the user could
            # be already authenticated, but lack the privileges.
            if not authorizeowner(hotdog):
                abort(403)
            return fn(*args, **kwargs)
        return update_wrapper(wrapped_function, fn)
    return decorator

@app.errorhandler(403)
def forbidden_403(exception):
    return 'No hotdogs for you!', 403

When decorator takes arguments, it's not really a decorator, but a factory function which returns the real decorator.

But if I were you, I would use Flask-Login for authentication and augment it with custom decorators and functions as yours to handle authorization.

I looked into Flask-Principal, but found it overly complicated for my tastes. Haven't checked Flask-Security, but I believe it uses Flask-Principal for authorization. Overall I think that Flask-Login with some custom code is enough most of the time.

Share:
16,946

Related videos on Youtube

Mittenchops
Author by

Mittenchops

Updated on August 22, 2022

Comments

  • Mittenchops
    Mittenchops over 1 year

    I used a flask snippet for my flask-login that checks that a user is logged in:

    from functools import wraps
    
    def logged_in(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            if session.get('logged_in') is not None:
                return f(*args, **kwargs)
            else:
                flash('Please log in first.', 'error')
                return redirect(url_for('login'))
        return decorated_function
    

    And I decorate views like so:

    @app.route('/secrets', methods=['GET', 'POST'])
    @logged_in
    def secrets():
        error = None
    

    I'd like to do something similar for authorization, too. Right now, I have many views to check that a user owns a resource, let's say the hotdogs resource.

    If the logged_in user is the owner of that particular hotdog, he can edit and manage his hotdogs. If he isn't, I kick him out to the unauthorized screen.

    @app.route('/<hotdog>/addmustard/',methods=["GET"])
    @logged_in
    def addmustard(hotdog):
        if not (authorizeowner(hotdog)):
            return redirect(url_for('unauthorized'))
        do_stuff()
    

    authorizeowner() takes a hotdog as input and checks that the recorded hotdog owner matches the owner name listed in the session variable.

    I tried making a owns_hotdog wrapper/decorator function similar to my logged in one, but it complained that it didn't accept arguments. How can I achieve something similar? Something like...

    def owns_hotdog(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            if not authorizeowner(hotdog):
                return f(*args, **kwargs)
            else:
                flash('Please log in first.', 'error')
                return redirect(url_for('login'))
        return decorated_function
    

    From the error message, decorator seems not to be receiving the hotdog argument that Flask views have access to from the variable in the route. My hope is for something like...

    @app.route('/<hotdog>/addmustard/',methods=["GET"])
    @logged_in
    @owns_hotdog(hotdog)
    def addmustard(hotdog):
        do_stuff()
    

    Everything works with my current authorizeowner(hotdog) function, but it just seems cleaner to have this in place as a wrapper on top of my route, rather than as the first line inside the route.

    Some other notes:

    • I know that Flask-Security and Flask-Principal can manage authorization for me. Unfortunately, I'm using an unsupported database back-end and am unable to use these extensions. So, I'm forced to do authentication without them.
    • If you see any glaring holes in doing authorization this way, please let me know!
    • Blender
      Blender over 11 years
      Try replacing if not authorizeowner(hotdog) with if not authorizeowner(*args, **kwargs) and removing (hotdog) from your decorator.
  • Mittenchops
    Mittenchops over 11 years
    Thanks Audrius, for the code and double for the advice on flask-login. I don't quite understand the update_wrapper versus wraps. Would I still call this in the view as before, like.. @app.route('/<hotdog>/addmustard/',methods=["GET"]) @logged_in @owns_hotdog(hotdog) def addmustard(hotdog): do_stuff()
  • Audrius Kažukauskas
    Audrius Kažukauskas over 11 years
    Yep, you'll use @owns_hotdog(hotdog) as a decorator. My example also covers authentication part, so you don't need @logged_in decorator, but if you want to use it, remove if logged_in()... part from my code. Also, logged_in() function in my example is not the same as your decorator, I just used the name to imply that it needs to check if user is logged in and chose the same name. Sorry if that caused any confusion.
  • Audrius Kažukauskas
    Audrius Kažukauskas over 11 years
    As for the wraps() and update_wrapper(), those functions aren't required, but it's a good idea to use them. When decorator returns a different function from the one it got as its argument, those functions make the returned function look like the original one, i.e. they copy the docstring and the name. Check the docs of functools module if you want to learn more about them.
  • Mittenchops
    Mittenchops over 11 years
    When I run this with @owns_hotdog(hotdog) I get an error that keeps flask from launching. The view file says @owns_hotdog(hotdog) NameError: name 'hotdog' is not defined It seems to not let me pass the argument to the factory function for some reason?
  • Audrius Kažukauskas
    Audrius Kažukauskas over 11 years
    Decorators are executed at function definition time, so the name hotdog must be defined by then. I get an impression, that you're trying to access the value of <hotdog> part from your route. If that's the case, check stackoverflow.com/a/13932942/1870151 where you'll find an example that demonstrates how to do this.
  • Mittenchops
    Mittenchops over 11 years
    I see. That link is perfect, it's exactly what I was going for, but I learned a ton about helpers versus factories from your help above. Thanks for your help!
  • swade
    swade about 7 years
    Owns hotogs is never being used as a decorator in your example and doesn't show it taking arguments.
  • alkanschtein
    alkanschtein over 3 years
    @AudriusKažukauskas where should I keep this chunk of code? I want to make it in another package should I use init_app like structure? It will be great if you can explain the file structure as well. Also, can I append log_in decorator inside of this one?