diff --git a/dusken/api/member.py b/dusken/api/member.py index 89ef5c9..5a3bce2 100644 --- a/dusken/api/member.py +++ b/dusken/api/member.py @@ -12,13 +12,12 @@ from tastypie.resources import ModelResource, ALL from tastypie.validation import CleanedDataFormValidation from dusken.api.groupsbymember import GroupsByMemberResource -from dusken.authentication import MyApiKeyAuthentication, ServiceAuthentication +from dusken.authentication import ServiceAuthentication, OAuth20Authentication from dusken.authorization import MyDjangoAuthorization from dusken.models import * from dusken.utils.api import generate_username, random_string from dusken.forms import MemberCreateForm - class MemberResource(ModelResource): """ This class provides following endpoints: @@ -30,7 +29,7 @@ class MemberResource(ModelResource): resource_name = 'member' list_allowed_methods = [ 'get' ] detail_allowed_methods = [ 'get', 'patch', 'delete' ] - authentication = MyApiKeyAuthentication() # Who are you? + authentication = OAuth20Authentication() # Who are you? authorization = MyDjangoAuthorization() # What are you allowed to do? excludes = [ 'date_joined', 'password', 'is_active', 'is_staff', 'is_superuser', 'last_login' ] filtering = { @@ -95,7 +94,6 @@ class MemberResource(ModelResource): try: country = Country.objects.get(name=new_address['country']) except Country.DoesNotExist, e: - # FIXME change to badrequest? raise ImmediateHttpResponse(HttpForbidden( 'The country "{}" does not exist.'.format(new_address['country'])) ) @@ -167,7 +165,7 @@ class MemberCreateResource(ModelResource): detail_allowed_methods = [] default_format = "application/json" queryset = Member.objects.all() - authentication = ServiceAuthentication() # anyone + authentication = OAuth20Authentication() # anyone authorization = Authorization() # can do what they want with anything always_return_data = True excludes = [ 'date_joined', 'password', 'is_active', 'is_staff', 'is_superuser', 'last_login' ] diff --git a/dusken/api/membership.py b/dusken/api/membership.py index 1498e72..e3fef72 100644 --- a/dusken/api/membership.py +++ b/dusken/api/membership.py @@ -3,7 +3,7 @@ from dusken.api.member import MemberResource from dusken.models import * from tastypie import fields from dusken.authorization import MyDjangoAuthorization -from dusken.authentication import MyApiKeyAuthentication +from dusken.authentication import OAuth20Authentication from tastypie.resources import ModelResource, ALL, ALL_WITH_RELATIONS @@ -21,7 +21,7 @@ class MembershipResource(ModelResource): list_allowed_methods = [ 'get', 'post' ] detail_allowed_methods = [ 'get', 'patch' ] authorization = MyDjangoAuthorization() - authentication = MyApiKeyAuthentication() + authentication = OAuth20Authentication() excludes = [ 'end_date'] filtering = { 'member' : ALL_WITH_RELATIONS, diff --git a/dusken/authentication.py b/dusken/authentication.py index 92d8aff..c8aba44 100644 --- a/dusken/authentication.py +++ b/dusken/authentication.py @@ -1,8 +1,12 @@ +import logging +import provider.oauth2 + +from django.contrib.auth.models import AnonymousUser, User +from django.utils import timezone +from provider.oauth2.models import AccessToken, Client from tastypie.authentication import ApiKeyAuthentication, Authentication from tastypie.http import HttpUnauthorized -# TODO replace with Oauth2 http://django-tastypie.readthedocs.org/en/latest/cookbook.html#creating-a-full-oauth-2-0-api - class MyApiKeyAuthentication(ApiKeyAuthentication): """ Override method below to use our custom User model @@ -53,3 +57,99 @@ class ServiceAuthentication(Authentication): # return True # return False +""" +This is a simple OAuth 2.0 authentication model for tastypie + +Copied nearly verbatim from amrox's example + - https://github.com/amrox/django-tastypie-two-legged-oauth + +Dependencies: + - django-oauth2-provider: https://github.com/caffeinehit/django-oauth2-provider + +Example: + - http://ianalexandr.com +""" + +# stolen from piston +class OAuthError(RuntimeError): + """Generic exception class.""" + def __init__(self, message='OAuth error occured.'): + self.message = message + + +class OAuth20Authentication(Authentication): + """ + OAuth authenticator. + + This Authentication method checks for a provided HTTP_AUTHORIZATION + and looks up to see if this is a valid OAuth Access Token + """ + def __init__(self, realm='API'): + self.realm = realm + + def is_authenticated(self, request, **kwargs): + """ + Verify 2-legged oauth request. Parameters accepted as + values in "Authorization" header, or as a GET request + or in a POST body. + """ + logging.info("OAuth20Authentication") + + try: + key = request.GET.get('oauth_consumer_key') + if not key: + key = request.POST.get('oauth_consumer_key') + if not key: + auth_header_value = request.META.get('HTTP_AUTHORIZATION') + if auth_header_value: + key = auth_header_value.split(': ')[1].split(" ")[1] + if not key: + logging.error('OAuth20Authentication. No consumer_key found.') + return None + """ + If verify_access_token() does not pass, it will raise an error + """ + token = verify_access_token(key) + + # If OAuth authentication is successful, set the request user to the token user for authorization + request.user = token.user + + # If OAuth authentication is successful, set oauth_consumer_key on request in case we need it later + request.META['oauth_consumer_key'] = key + return True + except KeyError, e: + logging.exception("Error in OAuth20Authentication.") + request.user = AnonymousUser() + return False + except Exception, e: + logging.exception("Error in OAuth20Authentication.") + return False + return True + +def verify_access_token(key): + # Check if key is in AccessToken key + try: + token = AccessToken.objects.get(token=key) + + # Check if token has expired + if token.expires < timezone.now(): + raise OAuthError('AccessToken has expired.') + except AccessToken.DoesNotExist, e: + raise OAuthError("AccessToken not found at all.") + + logging.info('Valid access') + return token + +def create_access_token(user): + # Create a Oauth2 Client (each test gets a new one) + oauth2_client = Client.objects.create( + user=user, + name="example.com testclient", + client_type=1, # ?? + url="http://example.com") + # Create a Oauth2 AccessToken (each test gets a new one) + access_token = AccessToken(user=user, client=oauth2_client) + access_token.save() + + # Return HTTP authorization header + return "Authorization: OAuth {}".format(access_token.token) diff --git a/dusken/authorization.py b/dusken/authorization.py index 2b9d0f8..f288418 100644 --- a/dusken/authorization.py +++ b/dusken/authorization.py @@ -1,10 +1,4 @@ -import logging -import provider.oauth2 - from django.http import HttpResponse -from django.contrib.auth.models import AnonymousUser, User -from django.utils import timezone -from provider.oauth2.models import AccessToken from tastypie.authentication import Authentication from tastypie.authorization import DjangoAuthorization from tastypie.exceptions import Unauthorized @@ -80,87 +74,3 @@ class MyDjangoAuthorization(DjangoAuthorization): raise Unauthorized("You are not allowed to access that resource.") return True - - -""" -This is a simple OAuth 2.0 authentication model for tastypie - -Copied nearly verbatim from amrox's example - - https://github.com/amrox/django-tastypie-two-legged-oauth - -Dependencies: - - django-oauth2-provider: https://github.com/caffeinehit/django-oauth2-provider - -Example: - - http://ianalexandr.com -""" - -# stolen from piston -class OAuthError(RuntimeError): - """Generic exception class.""" - def __init__(self, message='OAuth error occured.'): - self.message = message - - -class OAuth20Authentication(Authentication): - """ - OAuth authenticator. - - This Authentication method checks for a provided HTTP_AUTHORIZATION - and looks up to see if this is a valid OAuth Access Token - """ - def __init__(self, realm='API'): - self.realm = realm - - def is_authenticated(self, request, **kwargs): - """ - Verify 2-legged oauth request. Parameters accepted as - values in "Authorization" header, or as a GET request - or in a POST body. - """ - logging.info("OAuth20Authentication") - - try: - key = request.GET.get('oauth_consumer_key') - if not key: - key = request.POST.get('oauth_consumer_key') - if not key: - auth_header_value = request.META.get('HTTP_AUTHORIZATION') - if auth_header_value: - key = auth_header_value.split(' ')[1] - if not key: - logging.error('OAuth20Authentication. No consumer_key found.') - return None - """ - If verify_access_token() does not pass, it will raise an error - """ - token = verify_access_token(key) - - # If OAuth authentication is successful, set the request user to the token user for authorization - request.user = token.user - - # If OAuth authentication is successful, set oauth_consumer_key on request in case we need it later - request.META['oauth_consumer_key'] = key - return True - except KeyError, e: - logging.exception("Error in OAuth20Authentication.") - request.user = AnonymousUser() - return False - except Exception, e: - logging.exception("Error in OAuth20Authentication.") - return False - return True - -def verify_access_token(key): - # Check if key is in AccessToken key - try: - token = AccessToken.objects.get(token=key) - - # Check if token has expired - if token.expires < timezone.now(): - raise OAuthError('AccessToken has expired.') - except AccessToken.DoesNotExist, e: - raise OAuthError("AccessToken not found at all.") - - logging.info('Valid access') - return token diff --git a/dusken/settings-sample.py b/dusken/settings-sample.py index 9f6ea8d..88ed1f8 100644 --- a/dusken/settings-sample.py +++ b/dusken/settings-sample.py @@ -122,6 +122,8 @@ INSTALLED_APPS = ( 'south', 'django_extensions', 'corsheaders', + 'provider', + 'provider.oauth2', ) diff --git a/dusken/tests/member.py b/dusken/tests/member.py index 010e7ca..f7dcac2 100644 --- a/dusken/tests/member.py +++ b/dusken/tests/member.py @@ -1,9 +1,10 @@ import logging -from tastypie.models import ApiKey + +from tastypie.test import ResourceTestCase from dusken.models import Address, Country, Member -from tastypie.test import ResourceTestCase from dusken.utils.tests import test_fixtures_member +from dusken.authentication import create_access_token class MemberTestBase(ResourceTestCase): @@ -16,8 +17,8 @@ class MemberTestBase(ResourceTestCase): # Get the preloaded member, which will be used for # comparison with fetched object. self.member = Member.objects.get(pk=2) - ## create credentials - self.creds = self.create_apikey(username=self.member.username, api_key=ApiKey.objects.get(user=self.member).key) + # Create a OAuth2 Authorization HTTP header + self.creds = create_access_token(self.member) # URIs to the member api self.all_members_url = '/api/v1/member/' diff --git a/dusken/tests/membership.py b/dusken/tests/membership.py index f58a81a..4d918a2 100644 --- a/dusken/tests/membership.py +++ b/dusken/tests/membership.py @@ -1,9 +1,10 @@ -from tastypie.models import ApiKey - +from dusken.authentication import create_access_token from dusken.models import Member -from tastypie.test import ResourceTestCase from dusken.utils.tests import test_fixtures_membership, do_get_request, do_post_request +from tastypie.models import ApiKey +from tastypie.test import ResourceTestCase + class MembershipTest(ResourceTestCase): fixtures = test_fixtures_membership() @@ -17,9 +18,7 @@ class MembershipTest(ResourceTestCase): def setUp(self): self.member = Member.objects.get(pk=2) - self.creds = self.create_apikey( - username=self.member.username, - api_key=ApiKey.objects.get(user=self.member).key) + self.creds = create_access_token(self.member) super(MembershipTest, self).setUp()