Dodanie omyłkowo pominiętego API.
[wolnelektury.git] / apps / piston / authentication.py
diff --git a/apps/piston/authentication.py b/apps/piston/authentication.py
new file mode 100644 (file)
index 0000000..7d09707
--- /dev/null
@@ -0,0 +1,317 @@
+import binascii
+
+import oauth
+from django.http import HttpResponse, HttpResponseRedirect
+from django.contrib.auth.models import User, AnonymousUser
+from django.contrib.auth.decorators import login_required
+from django.template import loader
+from django.contrib.auth import authenticate
+from django.conf import settings
+from django.core.urlresolvers import get_callable
+from django.core.exceptions import ImproperlyConfigured
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+
+from piston import forms
+
+class NoAuthentication(object):
+    """
+    Authentication handler that always returns
+    True, so no authentication is needed, nor
+    initiated (`challenge` is missing.)
+    """
+    def is_authenticated(self, request):
+        return True
+
+class HttpBasicAuthentication(object):
+    """
+    Basic HTTP authenticater. Synopsis:
+    
+    Authentication handlers must implement two methods:
+     - `is_authenticated`: Will be called when checking for
+        authentication. Receives a `request` object, please
+        set your `User` object on `request.user`, otherwise
+        return False (or something that evaluates to False.)
+     - `challenge`: In cases where `is_authenticated` returns
+        False, the result of this method will be returned.
+        This will usually be a `HttpResponse` object with
+        some kind of challenge headers and 401 code on it.
+    """
+    def __init__(self, auth_func=authenticate, realm='API'):
+        self.auth_func = auth_func
+        self.realm = realm
+
+    def is_authenticated(self, request):
+        auth_string = request.META.get('HTTP_AUTHORIZATION', None)
+
+        if not auth_string:
+            return False
+            
+        try:
+            (authmeth, auth) = auth_string.split(" ", 1)
+
+            if not authmeth.lower() == 'basic':
+                return False
+
+            auth = auth.strip().decode('base64')
+            (username, password) = auth.split(':', 1)
+        except (ValueError, binascii.Error):
+            return False
+        
+        request.user = self.auth_func(username=username, password=password) \
+            or AnonymousUser()
+                
+        return not request.user in (False, None, AnonymousUser())
+        
+    def challenge(self):
+        resp = HttpResponse("Authorization Required")
+        resp['WWW-Authenticate'] = 'Basic realm="%s"' % self.realm
+        resp.status_code = 401
+        return resp
+
+    def __repr__(self):
+        return u'<HTTPBasic: realm=%s>' % self.realm
+
+class HttpBasicSimple(HttpBasicAuthentication):
+    def __init__(self, realm, username, password):
+        self.user = User.objects.get(username=username)
+        self.password = password
+
+        super(HttpBasicSimple, self).__init__(auth_func=self.hash, realm=realm)
+    
+    def hash(self, username, password):
+        if username == self.user.username and password == self.password:
+            return self.user
+
+def load_data_store():
+    '''Load data store for OAuth Consumers, Tokens, Nonces and Resources
+    '''
+    path = getattr(settings, 'OAUTH_DATA_STORE', 'piston.store.DataStore')
+
+    # stolen from django.contrib.auth.load_backend
+    i = path.rfind('.')
+    module, attr = path[:i], path[i+1:]
+
+    try:
+        mod = __import__(module, {}, {}, attr)
+    except ImportError, e:
+        raise ImproperlyConfigured, 'Error importing OAuth data store %s: "%s"' % (module, e)
+
+    try:
+        cls = getattr(mod, attr)
+    except AttributeError:
+        raise ImproperlyConfigured, 'Module %s does not define a "%s" OAuth data store' % (module, attr)
+
+    return cls
+
+# Set the datastore here.
+oauth_datastore = load_data_store()
+
+def initialize_server_request(request):
+    """
+    Shortcut for initialization.
+    """
+    if request.method == "POST": #and \
+#       request.META['CONTENT_TYPE'] == "application/x-www-form-urlencoded":
+        params = dict(request.REQUEST.items())
+    else:
+        params = { }
+
+    # Seems that we want to put HTTP_AUTHORIZATION into 'Authorization'
+    # for oauth.py to understand. Lovely.
+    request.META['Authorization'] = request.META.get('HTTP_AUTHORIZATION', '')
+
+    oauth_request = oauth.OAuthRequest.from_request(
+        request.method, request.build_absolute_uri(), 
+        headers=request.META, parameters=params,
+        query_string=request.environ.get('QUERY_STRING', ''))
+        
+    if oauth_request:
+        oauth_server = oauth.OAuthServer(oauth_datastore(oauth_request))
+        oauth_server.add_signature_method(oauth.OAuthSignatureMethod_PLAINTEXT())
+        oauth_server.add_signature_method(oauth.OAuthSignatureMethod_HMAC_SHA1())
+    else:
+        oauth_server = None
+        
+    return oauth_server, oauth_request
+
+def send_oauth_error(err=None):
+    """
+    Shortcut for sending an error.
+    """
+    response = HttpResponse(err.message.encode('utf-8'))
+    response.status_code = 401
+
+    realm = 'OAuth'
+    header = oauth.build_authenticate_header(realm=realm)
+
+    for k, v in header.iteritems():
+        response[k] = v
+
+    return response
+
+def oauth_request_token(request):
+    oauth_server, oauth_request = initialize_server_request(request)
+    
+    if oauth_server is None:
+        return INVALID_PARAMS_RESPONSE
+    try:
+        token = oauth_server.fetch_request_token(oauth_request)
+
+        response = HttpResponse(token.to_string())
+    except oauth.OAuthError, err:
+        response = send_oauth_error(err)
+
+    return response
+
+def oauth_auth_view(request, token, callback, params):
+    form = forms.OAuthAuthenticationForm(initial={
+        'oauth_token': token.key,
+        'oauth_callback': token.get_callback_url() or callback,
+      })
+
+    return render_to_response('piston/authorize_token.html',
+            { 'form': form }, RequestContext(request))
+
+@login_required
+def oauth_user_auth(request):
+    oauth_server, oauth_request = initialize_server_request(request)
+    
+    if oauth_request is None:
+        return INVALID_PARAMS_RESPONSE
+        
+    try:
+        token = oauth_server.fetch_request_token(oauth_request)
+    except oauth.OAuthError, err:
+        return send_oauth_error(err)
+        
+    try:
+        callback = oauth_server.get_callback(oauth_request)
+    except:
+        callback = None
+    
+    if request.method == "GET":
+        params = oauth_request.get_normalized_parameters()
+
+        oauth_view = getattr(settings, 'OAUTH_AUTH_VIEW', None)
+        if oauth_view is None:
+            return oauth_auth_view(request, token, callback, params)
+        else:
+            return get_callable(oauth_view)(request, token, callback, params)
+    elif request.method == "POST":
+        try:
+            form = forms.OAuthAuthenticationForm(request.POST)
+            if form.is_valid():
+                token = oauth_server.authorize_token(token, request.user)
+                args = '?'+token.to_string(only_key=True)
+            else:
+                args = '?error=%s' % 'Access not granted by user.'
+                print "FORM ERROR", form.errors
+            
+            if not callback:
+                callback = getattr(settings, 'OAUTH_CALLBACK_VIEW')
+                return get_callable(callback)(request, token)
+                
+            response = HttpResponseRedirect(callback+args)
+                
+        except oauth.OAuthError, err:
+            response = send_oauth_error(err)
+    else:
+        response = HttpResponse('Action not allowed.')
+            
+    return response
+
+def oauth_access_token(request):
+    oauth_server, oauth_request = initialize_server_request(request)
+    
+    if oauth_request is None:
+        return INVALID_PARAMS_RESPONSE
+        
+    try:
+        token = oauth_server.fetch_access_token(oauth_request)
+        return HttpResponse(token.to_string())
+    except oauth.OAuthError, err:
+        return send_oauth_error(err)
+
+INVALID_PARAMS_RESPONSE = send_oauth_error(oauth.OAuthError('Invalid request parameters.'))
+                
+class OAuthAuthentication(object):
+    """
+    OAuth authentication. Based on work by Leah Culver.
+    """
+    def __init__(self, realm='API'):
+        self.realm = realm
+        self.builder = oauth.build_authenticate_header
+    
+    def is_authenticated(self, request):
+        """
+        Checks whether a means of specifying authentication
+        is provided, and if so, if it is a valid token.
+        
+        Read the documentation on `HttpBasicAuthentication`
+        for more information about what goes on here.
+        """
+        if self.is_valid_request(request):
+            try:
+                consumer, token, parameters = self.validate_token(request)
+            except oauth.OAuthError, err:
+                print send_oauth_error(err)
+                return False
+
+            if consumer and token:
+                request.user = token.user
+                request.consumer = consumer
+                request.throttle_extra = token.consumer.id
+                return True
+            
+        return False
+        
+    def challenge(self):
+        """
+        Returns a 401 response with a small bit on
+        what OAuth is, and where to learn more about it.
+        
+        When this was written, browsers did not understand
+        OAuth authentication on the browser side, and hence
+        the helpful template we render. Maybe some day in the
+        future, browsers will take care of this stuff for us
+        and understand the 401 with the realm we give it.
+        """
+        response = HttpResponse()
+        response.status_code = 401
+        realm = 'API'
+
+        for k, v in self.builder(realm=realm).iteritems():
+            response[k] = v
+
+        tmpl = loader.render_to_string('oauth/challenge.html',
+            { 'MEDIA_URL': settings.MEDIA_URL })
+
+        response.content = tmpl
+
+        return response
+        
+    @staticmethod
+    def is_valid_request(request):
+        """
+        Checks whether the required parameters are either in
+        the http-authorization header sent by some clients,
+        which is by the way the preferred method according to
+        OAuth spec, but otherwise fall back to `GET` and `POST`.
+        """
+        must_have = [ 'oauth_'+s for s in [
+            'consumer_key', 'token', 'signature',
+            'signature_method', 'timestamp', 'nonce' ] ]
+        
+        is_in = lambda l: all([ (p in l) for p in must_have ])
+
+        auth_params = request.META.get("HTTP_AUTHORIZATION", "")
+        req_params = request.REQUEST
+             
+        return is_in(auth_params) or is_in(req_params)
+        
+    @staticmethod
+    def validate_token(request, check_timestamp=True, check_nonce=True):
+        oauth_server, oauth_request = initialize_server_request(request)
+        return oauth_server.verify_request(oauth_request)
+