Login required should be the default
If you're building a web-app in Python that requires people to sign in, you are probably pretty familiar with the concept of a @login_required
decorator. For those that might not be, it's pretty straight forward: the decorator wraps a handler method with a check for whether the current request is coming from someone who has been through your sign in process.
When I first saw this however many years ago, I was pretty new to Python decorators and thought it was pretty awesome. I still think the whole concept is pretty awesome. This seems to fit the mold of a "decorator" pretty perfectly... almost as if decorators were built with this in mind...
But what if you forget your @login_required
decorator? It’s so easy to do. When you're testing that "Edit your account" page, you're authenticated, you assume everything should go to plan. You could (and should) add a functional test to check that an unauthenticated request is redirected, but this is just a quick little side project...
So why don’t we just make @login_required
the default? I think that it should be, so here’s some code to save somebody else some time.
It works by wrapping all local methods with your login_required method during object creation (aka, __new__
). If you want a method to be public, you decorate it with the @login_not_required
decorator -- which sets a flag on the method saying... that login is not required.
Here's the BaseHandler
(and Meta class) which does the wrapping:
import types
import webapp2
from auth import login_required
class BaseHandlerMeta(type):
"""Meta class for all request handlers.
This automatically wraps all handler methods with the login_required
decorator. If something should be exposed publicly, it should be wrapped
with the login_not_required decorator.
"""
def __new__(cls, name, bases, local):
if name != 'BaseHandler':
for func_name, func in local.iteritems():
if isinstance(func, types.FunctionType):
local[func_name] = login_required(func)
return type.__new__(cls, name, bases, local)
class BaseHandler(webapp2.RequestHandler):
"""Base class for all RequestHandlers."""
__metaclass__ = BaseHandlerMeta
def user_is_logged_in(self):
# Do some magic here to check if someone is logged in
return False
Here are the login_required
and login_not_required
decorators:
def login_not_required(handler_method):
"""Allows a user to *not* be logged in.
The login_required attribute is inspected by BaseHandlerMeta and is used
as the flag for whether to wrap a method with login_required or not.
"""
handler_method.login_required = False
return handler_method
def login_required(handler_method):
"""Requires that a user be logged in."""
required = getattr(handler_method, 'login_required', True)
already_wrapped = getattr(handler_method, 'wrapped', False)
# If the method doesn't require a login, or has already been wrapped,
# just return the original.
if not required or already_wrapped:
return handler_method
def check_login(self, *args, **kwargs):
if not self.user_is_logged_in():
uri = self.uri_for('login', redirect=self.request.path)
self.redirect(uri, abort=True)
else:
return handler_method(self, *args, **kwargs)
# Let others know that this method is already wrapped to avoid wrapping
# it more than once...
check_login.wrapped = True
return check_login
This would make your request handlers look something like this:
import base
from auth import login_not_required
class MyHandler(base.BaseHandler):
@login_not_required
def my_public_method(self):
self.response.write('Hello world!')
@login_not_required
def my_public_method_with_a_problem(self):
# Calling this should force a redirect to the login page!
self.my_other_handler_that_is_protected()
def my_other_handler_that_is_protected(self):
self.response.write('Hello privately!')
def protected_by_default(self):
self.response.write('Once you log in, you can view this!')
Notice that if you are in a public method (@login_not_required
) and you call a method that does not have that decorator, you’ll get redirected to a login page. If you want something to be public, it and all of the functions it calls (inside the handler) should be explicitly defined as public.