From eff54c00d514e1edd74fbc789f9064d09db40b02 Mon Sep 17 00:00:00 2001 From: "tom christie tom@tomchristie.com" Date: Mon, 24 Jan 2011 18:59:23 +0000 Subject: [PATCH] Added authenicators. Awesome. --- docs/index.rst | 10 +++++-- flywheel/authenticators.py | 44 +++++++++++++++++++++++++++++++ flywheel/resource.py | 54 ++++++++++++++++++++++---------------- 3 files changed, 84 insertions(+), 24 deletions(-) create mode 100644 flywheel/authenticators.py diff --git a/docs/index.rst b/docs/index.rst index 4788c205b..bad8b1004 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,9 +3,15 @@ FlyWheel Documentation This is the online documentation for FlyWheel - A REST framework for Django. +Some of FlyWheel's features: + * Clean, simple, class-based views for Resources. -* Easy input validation using Forms and ModelForms. -* Self describing APIs, with HTML and Plain Text outputs. +* Support for ModelResources with nice default implementations and input validation. +* Automatically provides a browse-able self-documenting API. +* Pluggable Emitters, Parsers and Authenticators - Easy to customise. +* Content type negotiation using Accept headers. +* Optional support for forms as input validation. +* Modular architecture - Easy to extend and modify. .. toctree:: :maxdepth: 1 diff --git a/flywheel/authenticators.py b/flywheel/authenticators.py new file mode 100644 index 000000000..8de182deb --- /dev/null +++ b/flywheel/authenticators.py @@ -0,0 +1,44 @@ +from django.contrib.auth import authenticate +import base64 + +class BaseAuthenticator(object): + """All authenticators should extend BaseAuthenticator.""" + + def __init__(self, resource): + """Initialise the authenticator with the Resource instance as state, + in case the authenticator needs to access any metadata on the Resource object.""" + self.resource = resource + + def authenticate(self, request): + """Authenticate the request and return the authentication context or None. + + The default permission checking on Resource will use the allowed_methods attribute + for permissions if the authentication context is not None, and use anon_allowed_methods otherwise. + + The authentication context is passed to the method calls eg Resource.get(request, auth) in order to + allow them to apply any more fine grained permission checking at the point the response is being generated. + + This function must be overridden to be implemented.""" + return None + + +class BasicAuthenticator(BaseAuthenticator): + """Use HTTP Basic authentication""" + def authenticate(self, request): + if 'HTTP_AUTHORIZATION' in request.META: + auth = request.META['HTTP_AUTHORIZATION'].split() + if len(auth) == 2 and auth[0].lower() == "basic": + uname, passwd = base64.b64decode(auth[1]).split(':') + user = authenticate(username=uname, password=passwd) + if user is not None and user.is_active: + return user + return None + + +class UserLoggedInAuthenticator(BaseAuthenticator): + """Use Djagno's built-in request session for authentication.""" + def authenticate(self, request): + if request.user and request.user.is_active: + return request.user + return None + diff --git a/flywheel/resource.py b/flywheel/resource.py index 1b76b98aa..8c20e14f0 100644 --- a/flywheel/resource.py +++ b/flywheel/resource.py @@ -2,7 +2,7 @@ from django.contrib.sites.models import Site from django.core.urlresolvers import reverse from django.http import HttpResponse -from flywheel import emitters, parsers +from flywheel import emitters, parsers, authenticators from flywheel.response import status, Response, ResponseException from decimal import Decimal @@ -48,6 +48,10 @@ class Resource(object): parsers = ( parsers.JSONParser, parsers.XMLParser, parsers.FormParser ) + + # List of all authenticating methods to attempt + authenticators = ( authenticators.UserLoggedInAuthenticator, + authenticators.BasicAuthenticator ) # Optional form for input validation and presentation of HTML formatted responses. form = None @@ -81,7 +85,6 @@ class Resource(object): """""" # Setup the resource context self.request = request - self.auth_context = None self.response = None self.form_instance = None @@ -123,7 +126,7 @@ class Resource(object): # """Return an list of all the media types that this resource can emit.""" # return [parser.media_type for parser in self.parsers] - #def deafult_parser(self): + #def default_parser(self): # return self.parsers[0] @@ -133,31 +136,22 @@ class Resource(object): return self.add_domain(reverse(view, args=args, kwargs=kwargs)) - def authenticate(self, request): - """TODO""" - return None - # user = ... - # if DEBUG and request is from localhost - # if anon_user and not anon_allowed_methods raise PermissionDenied - # return auth_context - - - def get(self, request, *args, **kwargs): + def get(self, request, auth, *args, **kwargs): """Must be subclassed to be implemented.""" self.not_implemented('GET') - def post(self, request, content, *args, **kwargs): + def post(self, request, auth, content, *args, **kwargs): """Must be subclassed to be implemented.""" self.not_implemented('POST') - def put(self, request, content, *args, **kwargs): + def put(self, request, auth, content, *args, **kwargs): """Must be subclassed to be implemented.""" self.not_implemented('PUT') - def delete(self, request, *args, **kwargs): + def delete(self, request, auth, *args, **kwargs): """Must be subclassed to be implemented.""" self.not_implemented('DELETE') @@ -196,12 +190,28 @@ class Resource(object): return method - def check_method_allowed(self, method): + def authenticate(self, request): + """Attempt to authenticate the request, returning an authentication context or None""" + for authenticator in self.authenticators: + auth_context = authenticator(self).authenticate(request) + if auth_context: + return auth_context + return None + + + def check_method_allowed(self, method, auth): """Ensure the request method is acceptable for this resource.""" + + # If anonoymous check permissions and bail with no further info if disallowed + if auth is None and not method in self.anon_allowed_methods: + raise ResponseException(status.HTTP_403_FORBIDDEN, + {'detail': 'You do not have permission to access this resource. ' + + 'You may need to login or otherwise authenticate the request.'}) + if not method in self.callmap.keys(): raise ResponseException(status.HTTP_501_NOT_IMPLEMENTED, {'detail': 'Unknown or unsupported method \'%s\'' % method}) - + if not method in self.allowed_methods: raise ResponseException(status.HTTP_405_METHOD_NOT_ALLOWED, {'detail': 'Method \'%s\' not allowed on this resource.' % method}) @@ -376,10 +386,10 @@ class Resource(object): # Typically the context will be a user, or None if this is an anonymous request, # but it could potentially be more complex (eg the context of a request key which # has been signed against a particular set of permissions) - self.auth_context = self.authenticate(request) + auth_context = self.authenticate(request) # Ensure the requested operation is permitted on this resource - self.check_method_allowed(method) + self.check_method_allowed(method, auth_context) # Get the appropriate create/read/update/delete function func = getattr(self, self.callmap.get(method, None)) @@ -391,10 +401,10 @@ class Resource(object): data = parser(self).parse(request.raw_post_data) self.form_instance = self.get_form(data) data = self.cleanup_request(data, self.form_instance) - response = func(request, data, *args, **kwargs) + response = func(request, auth_context, data, *args, **kwargs) else: - response = func(request, *args, **kwargs) + response = func(request, auth_context, *args, **kwargs) # Allow return value to be either Response, or an object, or None if isinstance(response, Response):