There was some talk on the list about homegrown authentication and authorization systems, so I thought I'd draft up a bit on how I do it. The basic idea is this:
- for interactive users, at authentication time, configure the user's session object so all A&A requests can just look at the session.
- for API access, the user must provide an API key and application name with each request.
In my case, interactive user logins are authenticated by a RubyCAS server and authorization is granted by looking up specific attributes in an LDAP directory. API keys are stored locally with an authorization level.
My A&A is decorator based. The app only adds a "username" key to the session object if the authentication completes. So first thing, we have a decorator that looks for a valid username or an API key for automated access:
@decorator
def authenticated(fn, *a, **kw):
'''decorator to redirect to login if user's session isn't valid'''
if session.has_key('username'):
return fn(*a, **kw)
elif request.params.has_key('apiname') and request.params.has_key('apikey'):
k = meta.session.query(model.ApiKey)
apiname = request.params['apiname']
apikey = sha.new(request.params['apikey']).hexdigest()
try:
key = k.filter_by(name=apiname, key=apikey).one()
except:
abort(403)
return fn(*a, **kw)
else:
return redirect_to('/auth')
The app adds session keys according to the authorization stored, so the second thing we need is a decorator that looks at the session authorization and compares it to an argument.
def authorized(*perms, **keywords):
'''decorator to redirect if a user's session isn't authorized'''
def _authorized(fn, *a, **kw):
if session.has_key('username'):
for p in perms:
if session.has_key(p):
return fn(*a, **kw)
if not keywords.has_key('error'):
keywords['error'] = "You are not authorized to access that portion of the application."
session['mesg'] = keywords['error']
session.save()
return redirect_to('/home')
elif request.params.has_key('apiname') and request.params.has_key('apikey'):
k = meta.session.query(model.ApiKey)
apiname = request.params['apiname']
apikey = sha.new(requst.params['apikey']).hexdigest()
try:
key = k.filter_by(name=apiname, key=apikey).one()
except:
abort(403)
if key.authorization in perms:
return fn(*a, **kw)
else:
abort(403)
return decorator(_authorized)
You might need to adjust these based on how you return errors to the user and how you store A&A information.
Finally, to use these, we can stack them up on controller actions:
class SomeController(BaseController):
def public(self):
'''This stuff can be seen by anyone without authenticating'''
@authenticated
def index(self):
'''This stuff can be seen by any authenticated user.'''
@authenticated
@authorized("admin")
def secret(self):
'''If your session doesn't have the key "admin", you'll get kicked out
with the generic error message "You are not authorized to access that
portion of the application."
@authenticated
@authorized("admin", error="Only admins can blow stuff up!")
def explode(self):
'''If your session doesn't have the key "admin" then you'll get kicked
out with the specified error message above.'''
User storage for the API key looks exactly like you think - it has the app name, hashed key, and a field for the authorization level.
You could have saved time using AuthKit authorization's decorators. Reinventing the wheel
is good for learning, but for production you'll want for sure to have a authentication framework
that is build on solid foundations.