Adds oauth support up until authorization
authorxray7224 <xray7224@googlemail.com>
Mon, 8 Jul 2013 19:35:03 +0000 (20:35 +0100)
committerxray7224 <jessica@megworld.co.uk>
Thu, 11 Jul 2013 17:21:43 +0000 (18:21 +0100)
docs/source/api/client_register.rst
mediagoblin/db/models.py
mediagoblin/federation/routing.py
mediagoblin/federation/views.py
mediagoblin/tools/request.py
mediagoblin/tools/response.py
setup.py

index 088eb51d5fbbff7526194103d114c0d9cfcdc292..4ad7908eb0b3e205c0c051d2ff73c09ef5dd539a 100644 (file)
@@ -113,8 +113,8 @@ Errors
 
 There are a number of errors you could get back, This explains what could cause some of them:
 
-Could not decode JSON
-    This is caused when you have an error in your JSON, you may want to use a JSON validator to ensure that your JSON is correct.
+Could not decode data
+    This is caused when you have an error in the encoding of your data.
 
 Unknown Content-Type
     You should sent a Content-Type header with when you make a request, this should be either application/json or www-form-urlencoded. This is caused when a unknown Content-Type is used.
index daee92953ce26a4266838f163a796c69e7f7dc19..8a71aa09975e9ebea72a60a0d61a074e1c154987 100644 (file)
@@ -130,7 +130,36 @@ class Client(Base):
         else:
             return "<Client {0}>".format(self.id)
 
+class RequestToken(Base):
+    """
+        Model for representing the request tokens
+    """
+    __tablename__ = "core__request_tokens"
 
+    token = Column(Unicode, primary_key=True)
+    secret = Column(Unicode, nullable=False)
+    client = Column(Unicode, ForeignKey(Client.id))
+    user = Column(Integer, ForeignKey(User.id), nullable=True)
+    used = Column(Boolean, default=False)
+    authenticated = Column(Boolean, default=False)
+    verifier = Column(Unicode, nullable=True)
+    callback = Column(Unicode, nullable=True)
+    created = Column(DateTime, nullable=False, default=datetime.datetime.now)
+    updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
+    
+class AccessToken(Base):
+    """
+        Model for representing the access tokens
+    """
+    __tablename__ = "core__access_tokens"
+
+    token = Column(Unicode, nullable=False, primary_key=True)
+    secret = Column(Unicode, nullable=False)
+    user = Column(Integer, ForeignKey(User.id))
+    request_token = Column(Unicode, ForeignKey(RequestToken.token))
+    created = Column(DateTime, nullable=False, default=datetime.datetime.now)
+    updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
 
 class MediaEntry(Base, MediaEntryMixin):
     """
@@ -607,10 +636,10 @@ with_polymorphic(
     [ProcessingNotification, CommentNotification])
 
 MODELS = [
-    User, Client, MediaEntry, Tag, MediaTag, MediaComment, Collection, CollectionItem,
-    MediaFile, FileKeynames, MediaAttachmentFile, ProcessingMetaData,
-    Notification, CommentNotification, ProcessingNotification,
-    CommentSubscription]
+    User, Client, RequestToken, AccessToken, MediaEntry, Tag, MediaTag, 
+    MediaComment, Collection, CollectionItem, MediaFile, FileKeynames, 
+    MediaAttachmentFile, ProcessingMetaData, Notification, CommentNotification,
+    ProcessingNotification, CommentSubscription]
 
 
 ######################################################
index 6a75628e6f8fdb5dea7b68235f67a54cccca3450..f7e6f72c13ee3d6912d6fafb8f7d1d0a06a6c233 100644 (file)
 
 from mediagoblin.tools.routing import add_route
 
-add_route("mediagoblin.federation", "/api/client/register",  "mediagoblin.federation.views:client_register")
+# client registration & oauth
+add_route(
+        "mediagoblin.federation",
+        "/api/client/register",
+        "mediagoblin.federation.views:client_register"
+        )
+
+
+add_route(
+        "mediagoblin.federation",
+        "/oauth/request_token",
+        "mediagoblin.federation.views:request_token"
+        )
+
+add_route(
+        "mediagoblin.federation",
+        "/oauth/authorize",
+        "mediagoblin.federation.views:authorize",
+        )
+
+add_route(
+        "mediagoblin.federation",
+        "/oauth/access_token",
+        "mediagoblin.federation.views:access_token"
+        )
index 743fd142507f7ecc40971052bed4012f32adae1e..6c0008555ddace81a9dfe80886f3c4f32a863dce 100644 (file)
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-import json
+from oauthlib.oauth1 import RequestValidator, RequestTokenEndpoint
 
+from mediagoblin.tools.translate import pass_to_ugettext
 from mediagoblin.meddleware.csrf import csrf_exempt
-from mediagoblin.tools.response import json_response 
+from mediagoblin.tools.request import decode_request
+from mediagoblin.tools.response import json_response, render_400
 from mediagoblin.tools.crypto import random_string
 from mediagoblin.tools.validator import validate_email, validate_url
-from mediagoblin.db.models import Client
+from mediagoblin.db.models import Client, RequestToken, AccessToken
 
 # possible client types
 client_types = ["web", "native"] # currently what pump supports
@@ -28,38 +30,53 @@ client_types = ["web", "native"] # currently what pump supports
 @csrf_exempt
 def client_register(request):
     """ Endpoint for client registration """
-    data = request.get_data()
-    if request.content_type == "application/json":
-        try:
-            data = json.loads(data)
-        except ValueError:
-            return json_response({"error":"Could not decode JSON"})
-    elif request.content_type == "" or request.content_type == "application/x-www-form-urlencoded":
-        data = request.form
-    else:
-        return json_response({"error":"Unknown Content-Type"}, status=400)
+    try:
+        data = decode_request(request) 
+    except ValueError:
+        error = "Could not decode data."
+        return json_response({"error": error}, status=400)
+
+    if data is "":
+        error = "Unknown Content-Type"
+        return json_response({"error": error}, status=400)
 
     if "type" not in data:
-        return json_response({"error":"No registration type provided"}, status=400)
-    if "application_type" not in data or data["application_type"] not in client_types:
-        return json_response({"error":"Unknown application_type."}, status=400)
+        error = "No registration type provided."
+        return json_response({"error": error}, status=400)
+    if data.get("application_type", None) not in client_types:
+        error = "Unknown application_type."
+        return json_response({"error": error}, status=400)
     
     client_type = data["type"]
 
     if client_type == "client_update":
         # updating a client
         if "client_id" not in data:
-            return json_response({"error":"client_id is required to update."}, status=400)
+            error = "client_id is requried to update."
+            return json_response({"error": error}, status=400)
         elif "client_secret" not in data:
-            return json_response({"error":"client_secret is required to update."}, status=400)
+            error = "client_secret is required to update."
+            return json_response({"error": error}, status=400)
 
-        client = Client.query.filter_by(id=data["client_id"], secret=data["client_secret"]).first()
+        client = Client.query.filter_by(
+                id=data["client_id"], 
+                secret=data["client_secret"]
+                ).first()
 
         if client is None:
-            return json_response({"error":"Unauthorized."}, status=403)
+            error = "Unauthorized."
+            return json_response({"error": error}, status=403)
+
+        client.application_name = data.get(
+                "application_name", 
+                client.application_name
+                )
+
+        client.application_type = data.get(
+                "application_type",
+                client.application_type
+                )
 
-        client.application_name = data.get("application_name", client.application_name)
-        client.application_type = data.get("application_type", client.application_type)
         app_name = ("application_type", client.application_name)
         if app_name in client_types:
             client.application_name = app_name
@@ -67,11 +84,14 @@ def client_register(request):
     elif client_type == "client_associate":
         # registering
         if "client_id" in data:
-            return json_response({"error":"Only set client_id for update."}, status=400)
+            error = "Only set client_id for update."
+            return json_response({"error": error}, status=400)
         elif "access_token" in data:
-            return json_response({"error":"access_token not needed for registration."}, status=400)
+            error = "access_token not needed for registration."
+            return json_response({"error": error}, status=400)
         elif "client_secret" in data:
-            return json_response({"error":"Only set client_secret for update."}, status=400)
+            error = "Only set client_secret for update."
+            return json_response({"error": error}, status=400)
 
         # generate the client_id and client_secret
         client_id = random_string(22) # seems to be what pump uses
@@ -85,14 +105,19 @@ def client_register(request):
                 secret=client_secret, 
                 expirey=expirey_db,
                 application_type=data["application_type"],
-        )
+                )
 
     else:
-        return json_response({"error":"Invalid registration type"}, status=400)
+        error = "Invalid registration type"
+        return json_response({"error": error}, status=400)
 
     logo_url = data.get("logo_url", client.logo_url)
     if logo_url is not None and not validate_url(logo_url):
-        return json_response({"error":"Logo URL {0} is not a valid URL".format(logo_url)}, status=400)
+        error = "Logo URL {0} is not a valid URL.".format(logo_url)
+        return json_response(
+                {"error": error}, 
+                status=400
+                )
     else:
         client.logo_url = logo_url
     application_name=data.get("application_name", None)
@@ -100,13 +125,15 @@ def client_register(request):
     contacts = data.get("contact", None)
     if contacts is not None:
         if type(contacts) is not unicode:
-            return json_response({"error":"contacts must be a string of space-separated email addresses."}, status=400)
+            error = "Contacts must be a string of space-seporated email addresses."
+            return json_response({"error": error}, status=400)
 
         contacts = contacts.split()
         for contact in contacts:
             if not validate_email(contact):
                 # not a valid email
-                return json_response({"error":"Email {0} is not a valid email".format(contact)}, status=400)
+                error = "Email {0} is not a valid email.".format(contact)
+                return json_response({"error": error}, status=400)
      
         
         client.contacts = contacts
@@ -114,14 +141,16 @@ def client_register(request):
     request_uri = data.get("request_uris", None)
     if request_uri is not None:
         if type(request_uri) is not unicode:
-            return json_respinse({"error":"redirect_uris must be space-separated URLs."}, status=400)
+            error = "redirect_uris must be space-seporated URLs."
+            return json_respinse({"error": error}, status=400)
 
         request_uri = request_uri.split()
 
         for uri in request_uri:
             if not validate_url(uri):
                 # not a valid uri
-                return json_response({"error":"URI {0} is not a valid URI".format(uri)}, status=400)
+                error = "URI {0} is not a valid URI".format(uri)
+                return json_response({"error": error}, status=400)
 
         client.request_uri = request_uri
 
@@ -132,7 +161,85 @@ def client_register(request):
 
     return json_response(
         {
-            "client_id":client.id,
-            "client_secret":client.secret,
-            "expires_at":expirey,
+            "client_id": client.id,
+            "client_secret": client.secret,
+            "expires_at": expirey,
         })
+
+class ValidationException(Exception):
+    pass
+
+class GMGRequestValidator(RequestValidator):
+
+    def __init__(self, data):
+        self.POST = data
+
+    def save_request_token(self, token, request):
+        """ Saves request token in db """
+        client_id = self.POST[u"Authorization"][u"oauth_consumer_key"]
+
+        request_token = RequestToken(
+                token=token["oauth_token"],
+                secret=token["oauth_token_secret"],
+                )
+        request_token.client = client_id
+        request_token.save()
+
+
+@csrf_exempt
+def request_token(request):
+    """ Returns request token """
+    try:
+        data = decode_request(request) 
+    except ValueError:
+        error = "Could not decode data."
+        return json_response({"error": error}, status=400)
+
+    if data is "":
+        error = "Unknown Content-Type"
+        return json_response({"error": error}, status=400)
+
+
+    # Convert 'Authorization' to a dictionary
+    authorization = {}
+    for item in data["Authorization"].split(","):
+        key, value = item.split("=", 1)
+        authorization[key] = value
+    data[u"Authorization"] = authorization
+
+    # check the client_id
+    client_id = data[u"Authorization"][u"oauth_consumer_key"]
+    client = Client.query.filter_by(id=client_id).first()
+    if client is None:
+        # client_id is invalid
+        error = "Invalid client_id"
+        return json_response({"error": error}, status=400)
+
+    request_validator = GMGRequestValidator(data)
+    rv = RequestTokenEndpoint(request_validator)
+    tokens = rv.create_request_token(request, {})
+
+    tokenized = {}
+    for t in tokens.split("&"):
+        key, value = t.split("=")
+        tokenized[key] = value
+
+    # check what encoding to return them in
+    return json_response(tokenized)
+    
+def authorize(request):
+    """ Displays a page for user to authorize """
+    _ = pass_to_ugettext
+    token = request.args.get("oauth_token", None)
+    if token is None:
+        # no token supplied, display a html 400 this time
+        err_msg = _("Must provide an oauth_token")
+        return render_400(request, err_msg=err_msg)
+
+    # AuthorizationEndpoint
+    
+
+@csrf_exempt
+def access_token(request):
+    """ Provides an access token based on a valid verifier and request token """ 
+    pass
index ee342eae0dfff478cd3baecb835f987680743ba6..ed903ce00ae7962643bf582e3238423b0f7defb8 100644 (file)
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+import json
 import logging
 from mediagoblin.db.models import User
 
 _log = logging.getLogger(__name__)
 
 
+# MIME-Types
+form_encoded = "application/x-www-form-urlencoded"
+json_encoded = "application/json"
+
 def setup_user_in_request(request):
     """
     Examine a request and tack on a request.user parameter if that's
@@ -36,3 +41,15 @@ def setup_user_in_request(request):
         # this session.
         _log.warn("Killing session for user id %r", request.session['user_id'])
         request.session.delete()
+
+def decode_request(request):
+    """ Decodes a request based on MIME-Type """
+    data = request.get_data()
+    
+    if request.content_type == json_encoded:
+        data = json.loads(data)
+    elif request.content_type == form_encoded:
+        data = request.form
+    else:
+        data = ""
+    return data
index 1fd242fb18a8f21abb2a6a5c81231768caf1427d..db8fc38828a0fad3517a0e20b06d293ee251e361 100644 (file)
@@ -45,6 +45,15 @@ def render_error(request, status=500, title=_('Oops!'),
         {'err_code': status, 'title': title, 'err_msg': err_msg}),
         status=status)
 
+def render_400(request, err_msg=None):
+    """ Render a standard 400 page"""
+    _ = pass_to_ugettext
+    title = _("Bad Request")
+    if err_msg is None:
+        err_msg = _("The request sent to the server is invalid, please double check it")
+
+    return render_error(request, 400, title, err_msg)
+
 def render_403(request):
     """Render a standard 403 page"""
     _ = pass_to_ugettext
index 6e026f30b6997257d931b9733ce8d0af4addde99..b16f8d560e12703cacc03e47e9635166a4c212c1 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -63,6 +63,8 @@ setup(
         'itsdangerous',
         'pytz',
         'six',
+        'oauthlib',
+        'pypump',
         ## This is optional!
         # 'translitcodec',
         ## For now we're expecting that users will install this from