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.
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
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.