App Engine + External Authentication: Exposing Handlers to Cron, Tasks, and Admins
Since Google is deprecating OpenID 2.0 support, I decided to update LibraryHippo to authenticate via OAuth 2.0, which is a story in itself, but I'm here to talk about what happened next.
LibraryHippo has a set of handlers that are accessed primarily via the Cron and Task Queue mechanisms, but every once in a while need to be triggered ad hoc by a human administrator. Until now, these request handlers were protected from the rabble by requiring administrator status via the application's app.yaml. Unfortunately, externally-authenticated users have no special standing within App Engine, so this restriction had to be relaxed.
My first thought was to remove the restriction from
app.yaml and check
for access in the handler like so:
if (users.is_current_user_admin() or self.is_external_user_admin()) # application code that understands the logged-in users # do stuff else: self.abort(403)
Unfortunately, this fails miserably. When the handler is executed by a
task or cron job,
This behaviour seems not to be widely reported; I couldn't find it mentioned in the App Engine issues list, but a web search eventually turned up App Engine: Google fails users.is_current_user_admin() test by Ben Davies, an article written nearly 5 years ago.
In this article, Mr. Davies suggests that the best alternative to
users.is_current_user_admin is to "check the easily spoofed request
user-agent". I was skittish of this approach, especially since Google
is now recommending checking
securing URLS for cron. The App Engine documentation
explains how X-AppEngine-Cron is protected against spoofing, but I'm
I ended up taking a different approach. I added two routes for the
affected handlers. One route is in the old
(subpath?) and the other in a new one for system commands,
system. The latter is secured in the app.yaml, just as before. Thus
# in app.yaml - url: /system/.* script: libraryhippo.application login: admin
# in the application's Python source handlers = [ # other handlers ('/admin/notify/(.*)$', Notify), ('/system/notify/(.*)$', Notify), ] # and later, in the Notify handler request_path = urlparse.urlsplit(self.request.url).path if (self.is_external_user_admin() or not request_path.startswith('/admin/')) # do stuff else: self.abort(403)
Thus the handler is executed if the user has admin rights or the URL
isn't locked down by virtue of being below
URLs are all assumed to be protected by the app.yaml setting.
Perhaps this is technically no better than checking a header in the request, but it works for me, at least until I see what happens with Issue 11576: have users.is_current_user_admin return true for tasks and cron jobs.