How to make a python decorator function in Flask with arguments (for authorization)
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.
Related videos on Youtube
Mittenchops
Updated on August 22, 2022Comments
-
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 over 11 yearsTry replacing
if not authorizeowner(hotdog)
withif not authorizeowner(*args, **kwargs)
and removing(hotdog)
from your decorator.
-
Mittenchops over 11 yearsThanks 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 over 11 yearsYep, 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, removeif 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 over 11 yearsAs for the
wraps()
andupdate_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 offunctools
module if you want to learn more about them. -
Mittenchops over 11 yearsWhen 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 over 11 yearsDecorators 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 over 11 yearsI 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 about 7 yearsOwns hotogs is never being used as a decorator in your example and doesn't show it taking arguments.
-
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?