From 04e08d422ad179de881e4394b99f2231d1b65a90 Mon Sep 17 00:00:00 2001
From: xray7224
Date: Thu, 27 Jun 2013 19:34:21 +0100
Subject: [PATCH 01/27] Moves json_response into tools/json.py
---
mediagoblin/plugins/api/tools.py | 24 -----------------
mediagoblin/plugins/api/views.py | 4 +--
mediagoblin/plugins/oauth/tools.py | 2 +-
mediagoblin/plugins/oauth/views.py | 2 +-
mediagoblin/tools/json.py | 41 ++++++++++++++++++++++++++++++
5 files changed, 45 insertions(+), 28 deletions(-)
create mode 100644 mediagoblin/tools/json.py
diff --git a/mediagoblin/plugins/api/tools.py b/mediagoblin/plugins/api/tools.py
index 92411f4b..d1b3ebb1 100644
--- a/mediagoblin/plugins/api/tools.py
+++ b/mediagoblin/plugins/api/tools.py
@@ -51,30 +51,6 @@ class Auth(object):
def __call__(self, request, *args, **kw):
raise NotImplemented()
-
-def json_response(serializable, _disable_cors=False, *args, **kw):
- '''
- Serializes a json objects and returns a werkzeug Response object with the
- serialized value as the response body and Content-Type: application/json.
-
- :param serializable: A json-serializable object
-
- Any extra arguments and keyword arguments are passed to the
- Response.__init__ method.
- '''
- response = Response(json.dumps(serializable), *args, content_type='application/json', **kw)
-
- if not _disable_cors:
- cors_headers = {
- 'Access-Control-Allow-Origin': '*',
- 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
- 'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With'}
- for key, value in cors_headers.iteritems():
- response.headers.set(key, value)
-
- return response
-
-
def get_entry_serializable(entry, urlgen):
'''
Returns a serializable dict() of a MediaEntry instance.
diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py
index 9159fe65..738ea25f 100644
--- a/mediagoblin/plugins/api/views.py
+++ b/mediagoblin/plugins/api/views.py
@@ -21,11 +21,11 @@ from os.path import splitext
from werkzeug.exceptions import BadRequest, Forbidden
from werkzeug.wrappers import Response
+from mediagoblin.tools.json import json_response
from mediagoblin.decorators import require_active_login
from mediagoblin.meddleware.csrf import csrf_exempt
from mediagoblin.media_types import sniff_media
-from mediagoblin.plugins.api.tools import api_auth, get_entry_serializable, \
- json_response
+from mediagoblin.plugins.api.tools import api_auth, get_entry_serializable
from mediagoblin.submit.lib import check_file_field, prepare_queue_task, \
run_process_media, new_upload_entry
diff --git a/mediagoblin/plugins/oauth/tools.py b/mediagoblin/plugins/oauth/tools.py
index 27ff32b4..1e0fc6ef 100644
--- a/mediagoblin/plugins/oauth/tools.py
+++ b/mediagoblin/plugins/oauth/tools.py
@@ -23,7 +23,7 @@ from datetime import datetime
from functools import wraps
-from mediagoblin.plugins.api.tools import json_response
+from mediagoblin.tools.json import json_response
def require_client_auth(controller):
diff --git a/mediagoblin/plugins/oauth/views.py b/mediagoblin/plugins/oauth/views.py
index d6fd314f..a5d66111 100644
--- a/mediagoblin/plugins/oauth/views.py
+++ b/mediagoblin/plugins/oauth/views.py
@@ -22,6 +22,7 @@ from urllib import urlencode
from werkzeug.exceptions import BadRequest
from mediagoblin.tools.response import render_to_response, redirect
+from mediagoblin.tools.json import json_response
from mediagoblin.decorators import require_active_login
from mediagoblin.messages import add_message, SUCCESS
from mediagoblin.tools.translate import pass_to_ugettext as _
@@ -31,7 +32,6 @@ from mediagoblin.plugins.oauth.forms import ClientRegistrationForm, \
AuthorizationForm
from mediagoblin.plugins.oauth.tools import require_client_auth, \
create_token
-from mediagoblin.plugins.api.tools import json_response
_log = logging.getLogger(__name__)
diff --git a/mediagoblin/tools/json.py b/mediagoblin/tools/json.py
new file mode 100644
index 00000000..a8437b82
--- /dev/null
+++ b/mediagoblin/tools/json.py
@@ -0,0 +1,41 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import json
+
+from werkzeug.wrappers import Response
+
+def json_response(serializable, _disable_cors=False, *args, **kw):
+ '''
+ Serializes a json objects and returns a werkzeug Response object with the
+ serialized value as the response body and Content-Type: application/json.
+
+ :param serializable: A json-serializable object
+
+ Any extra arguments and keyword arguments are passed to the
+ Response.__init__ method.
+ '''
+ response = Response(json.dumps(serializable), *args, content_type='application/json', **kw)
+
+ if not _disable_cors:
+ cors_headers = {
+ 'Access-Control-Allow-Origin': '*',
+ 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
+ 'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With'}
+ for key, value in cors_headers.iteritems():
+ response.headers.set(key, value)
+
+ return response
From c840cb66180a77e630c261c21967a6afc87411e9 Mon Sep 17 00:00:00 2001
From: xray7224
Date: Thu, 27 Jun 2013 19:34:21 +0100
Subject: [PATCH 02/27] Moves json_response into tools/json.py
---
mediagoblin/federation/__init__.py | 16 ++++++++++++++
mediagoblin/federation/routing.py | 19 +++++++++++++++++
mediagoblin/federation/views.py | 34 ++++++++++++++++++++++++++++++
3 files changed, 69 insertions(+)
create mode 100644 mediagoblin/federation/__init__.py
create mode 100644 mediagoblin/federation/routing.py
create mode 100644 mediagoblin/federation/views.py
diff --git a/mediagoblin/federation/__init__.py b/mediagoblin/federation/__init__.py
new file mode 100644
index 00000000..719b56e7
--- /dev/null
+++ b/mediagoblin/federation/__init__.py
@@ -0,0 +1,16 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
diff --git a/mediagoblin/federation/routing.py b/mediagoblin/federation/routing.py
new file mode 100644
index 00000000..6a75628e
--- /dev/null
+++ b/mediagoblin/federation/routing.py
@@ -0,0 +1,19 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from mediagoblin.tools.routing import add_route
+
+add_route("mediagoblin.federation", "/api/client/register", "mediagoblin.federation.views:client_register")
diff --git a/mediagoblin/federation/views.py b/mediagoblin/federation/views.py
new file mode 100644
index 00000000..097dc625
--- /dev/null
+++ b/mediagoblin/federation/views.py
@@ -0,0 +1,34 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from mediagoblin.tools.json import json_response
+
+# possible client types
+client_types = ["web", "native"] # currently what pump supports
+
+def client_register(request):
+ """ Endpoint for client registration """
+ if request.method == "POST":
+ # new client registration
+
+ return json_response({"dir":dir(request)})
+
+ # check they haven't given us client_id or client_type, they're only used for updating
+ pass
+
+ elif request.method == "PUT":
+ # updating client
+ pass
From 4990b47ce401dc86353a261825771a6811be4a8c Mon Sep 17 00:00:00 2001
From: xray7224
Date: Fri, 28 Jun 2013 17:59:32 +0100
Subject: [PATCH 03/27] Working client registration
---
mediagoblin/db/models.py | 25 +++++++++++++++++-
mediagoblin/federation/views.py | 46 ++++++++++++++++++++++++++-------
mediagoblin/routing.py | 1 +
mediagoblin/tools/crypto.py | 15 +++++++++++
mediagoblin/tools/json.py | 41 -----------------------------
mediagoblin/tools/response.py | 27 +++++++++++++++++--
6 files changed, 101 insertions(+), 54 deletions(-)
delete mode 100644 mediagoblin/tools/json.py
diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py
index 826d47ba..4c39c025 100644
--- a/mediagoblin/db/models.py
+++ b/mediagoblin/db/models.py
@@ -105,6 +105,29 @@ class User(Base, UserMixin):
_log.info('Deleted user "{0}" account'.format(self.username))
+class Client(Base):
+ """
+ Model representing a client - Used for API Auth
+ """
+ __tablename__ = "core__clients"
+
+ id = Column(Unicode, nullable=True, primary_key=True)
+ secret = Column(Unicode, nullable=False)
+ expirey = Column(DateTime, nullable=True)
+ application_type = Column(Unicode, nullable=False)
+ created = Column(DateTime, nullable=False, default=datetime.datetime.now)
+ updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
+
+ # optional stuff
+ redirect_uri = Column(Unicode, nullable=True)
+ logo_uri = Column(Unicode, nullable=True)
+ application_name = Column(Unicode, nullable=True)
+
+ def __repr__(self):
+ return "".format(self.id)
+
+
+
class MediaEntry(Base, MediaEntryMixin):
"""
TODO: Consider fetching the media_files using join
@@ -580,7 +603,7 @@ with_polymorphic(
[ProcessingNotification, CommentNotification])
MODELS = [
- User, MediaEntry, Tag, MediaTag, MediaComment, Collection, CollectionItem,
+ User, Client, MediaEntry, Tag, MediaTag, MediaComment, Collection, CollectionItem,
MediaFile, FileKeynames, MediaAttachmentFile, ProcessingMetaData,
Notification, CommentNotification, ProcessingNotification,
CommentSubscription]
diff --git a/mediagoblin/federation/views.py b/mediagoblin/federation/views.py
index 097dc625..bfd58d27 100644
--- a/mediagoblin/federation/views.py
+++ b/mediagoblin/federation/views.py
@@ -14,21 +14,47 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from mediagoblin.tools.json import json_response
+import json
+
+from mediagoblin.meddleware.csrf import csrf_exempt
+from mediagoblin.tools.response import json_response
+from mediagoblin.tools.crypto import random_string
+from mediagoblin.db.models import Client
# possible client types
client_types = ["web", "native"] # currently what pump supports
+@csrf_exempt
def client_register(request):
""" Endpoint for client registration """
- if request.method == "POST":
- # new 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"})
+ else:
+ return json_response({"error":"Unknown Content-Type"}, status=400)
- return json_response({"dir":dir(request)})
+ if "type" not in data:
+ return json_response({"error":"No registration type provided"}, status=400)
+
+ # generate the client_id and client_secret
+ client_id = random_string(22) # seems to be what pump uses
+ client_secret = random_string(43) # again, seems to be what pump uses
+ expirey = 0 # for now, lets not have it expire
+ expirey_db = None if expirey == 0 else expirey
+ client = Client(
+ id=client_id,
+ secret=client_secret,
+ expirey=expirey_db,
+ application_type=data["type"]
+ )
+ client.save()
- # check they haven't given us client_id or client_type, they're only used for updating
- pass
-
- elif request.method == "PUT":
- # updating client
- pass
+ return json_response(
+ {
+ "client_id":client_id,
+ "client_secret":client_secret,
+ "expires_at":expirey,
+ })
diff --git a/mediagoblin/routing.py b/mediagoblin/routing.py
index 986eb2ed..3a54aaa0 100644
--- a/mediagoblin/routing.py
+++ b/mediagoblin/routing.py
@@ -36,6 +36,7 @@ def get_url_map():
import mediagoblin.webfinger.routing
import mediagoblin.listings.routing
import mediagoblin.notifications.routing
+ import mediagoblin.federation.routing
for route in PluginManager().get_routes():
add_route(*route)
diff --git a/mediagoblin/tools/crypto.py b/mediagoblin/tools/crypto.py
index 1379d21b..917e674c 100644
--- a/mediagoblin/tools/crypto.py
+++ b/mediagoblin/tools/crypto.py
@@ -14,6 +14,8 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+import base64
+import string
import errno
import itsdangerous
import logging
@@ -24,6 +26,9 @@ from mediagoblin import mg_globals
_log = logging.getLogger(__name__)
+# produces base64 alphabet
+alphabet = string.ascii_letters + "-_"
+base = len(alphabet)
# Use the system (hardware-based) random number generator if it exists.
# -- this optimization is lifted from Django
@@ -111,3 +116,13 @@ def get_timed_signer_url(namespace):
assert __itsda_secret is not None
return itsdangerous.URLSafeTimedSerializer(__itsda_secret,
salt=namespace)
+
+def random_string(length):
+ """ Returns a URL safe base64 encoded crypographically strong string """
+ rstring = ""
+ for i in range(length):
+ n = getrandbits(6) # 6 bytes = 2^6 = 64
+ n = divmod(n, base)[1]
+ rstring += alphabet[n]
+
+ return rstring
diff --git a/mediagoblin/tools/json.py b/mediagoblin/tools/json.py
deleted file mode 100644
index a8437b82..00000000
--- a/mediagoblin/tools/json.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# GNU MediaGoblin -- federated, autonomous media hosting
-# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
-
-import json
-
-from werkzeug.wrappers import Response
-
-def json_response(serializable, _disable_cors=False, *args, **kw):
- '''
- Serializes a json objects and returns a werkzeug Response object with the
- serialized value as the response body and Content-Type: application/json.
-
- :param serializable: A json-serializable object
-
- Any extra arguments and keyword arguments are passed to the
- Response.__init__ method.
- '''
- response = Response(json.dumps(serializable), *args, content_type='application/json', **kw)
-
- if not _disable_cors:
- cors_headers = {
- 'Access-Control-Allow-Origin': '*',
- 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
- 'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With'}
- for key, value in cors_headers.iteritems():
- response.headers.set(key, value)
-
- return response
diff --git a/mediagoblin/tools/response.py b/mediagoblin/tools/response.py
index 0be1f835..1fd242fb 100644
--- a/mediagoblin/tools/response.py
+++ b/mediagoblin/tools/response.py
@@ -14,6 +14,8 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+import json
+
import werkzeug.utils
from werkzeug.wrappers import Response as wz_Response
from mediagoblin.tools.template import render_template
@@ -31,7 +33,6 @@ def render_to_response(request, template, context, status=200):
render_template(request, template, context),
status=status)
-
def render_error(request, status=500, title=_('Oops!'),
err_msg=_('An error occured')):
"""Render any error page with a given error code, title and text body
@@ -44,7 +45,6 @@ def render_error(request, status=500, title=_('Oops!'),
{'err_code': status, 'title': title, 'err_msg': err_msg}),
status=status)
-
def render_403(request):
"""Render a standard 403 page"""
_ = pass_to_ugettext
@@ -106,3 +106,26 @@ def redirect_obj(request, obj):
Requires obj to have a .url_for_self method."""
return redirect(request, location=obj.url_for_self(request.urlgen))
+
+def json_response(serializable, _disable_cors=False, *args, **kw):
+ '''
+ Serializes a json objects and returns a werkzeug Response object with the
+ serialized value as the response body and Content-Type: application/json.
+
+ :param serializable: A json-serializable object
+
+ Any extra arguments and keyword arguments are passed to the
+ Response.__init__ method.
+ '''
+
+ response = wz_Response(json.dumps(serializable), *args, content_type='application/json', **kw)
+
+ if not _disable_cors:
+ cors_headers = {
+ 'Access-Control-Allow-Origin': '*',
+ 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
+ 'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With'}
+ for key, value in cors_headers.iteritems():
+ response.headers.set(key, value)
+
+ return response
From 54fbbf092310a3f1b29817dba90105732132e19b Mon Sep 17 00:00:00 2001
From: xray7224
Date: Fri, 28 Jun 2013 19:34:56 +0100
Subject: [PATCH 04/27] Adds more support to begin to deal with updates
---
mediagoblin/federation/views.py | 34 +++++++++++++++++++++++++++++++--
1 file changed, 32 insertions(+), 2 deletions(-)
diff --git a/mediagoblin/federation/views.py b/mediagoblin/federation/views.py
index bfd58d27..f16ae1df 100644
--- a/mediagoblin/federation/views.py
+++ b/mediagoblin/federation/views.py
@@ -38,17 +38,47 @@ def client_register(request):
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)
+
+ 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)
+ elif "client_secret" not in data:
+ return json_response({"error":"client_secret is required to update."}, status=400)
+
+ client = Client.query.filter_by(id=data["client_id"], secret=data["client_secret"]).all()
+
+ if not client:
+ return json_response({"error":"Unauthorized.", status=403)
+
+ elif client_type == "client_associate":
+ # registering
+ if "client_id" in data:
+ return json_response({"error":"Only set client_id for update."}, status=400)
+ elif "access_token" in data:
+ return json_response({"error":"access_token not needed for registration."}, status=400)
+ elif "client_secret" in data:
+ return json_response({"error":"Only set client_secret for update."}, status=400)
+
# generate the client_id and client_secret
client_id = random_string(22) # seems to be what pump uses
client_secret = random_string(43) # again, seems to be what pump uses
expirey = 0 # for now, lets not have it expire
expirey_db = None if expirey == 0 else expirey
+
+ # save it
client = Client(
id=client_id,
secret=client_secret,
expirey=expirey_db,
- application_type=data["type"]
+ application_type=data["type"],
+ logo_url=data.get("logo_url", None),
+ redirect_uri=data.get("redirect_uri", None)
)
client.save()
From 763e300d7c6d798056c629e24b22298691ccc02e Mon Sep 17 00:00:00 2001
From: xray7224
Date: Sun, 30 Jun 2013 15:26:49 +0100
Subject: [PATCH 05/27] Adds update ability
---
mediagoblin/federation/views.py | 20 +++++++++++++++++++-
1 file changed, 19 insertions(+), 1 deletion(-)
diff --git a/mediagoblin/federation/views.py b/mediagoblin/federation/views.py
index f16ae1df..56bacbb1 100644
--- a/mediagoblin/federation/views.py
+++ b/mediagoblin/federation/views.py
@@ -56,6 +56,23 @@ def client_register(request):
if not client:
return json_response({"error":"Unauthorized.", status=403)
+ client.logo_url = data.get("logo_url", client.logo_url)
+ client.application_name = data.get("application_name", client.application_name)
+ app_name = ("application_type", client.application_name)
+ if app_name in client_types:
+ client.application_name = app_name
+
+ client.save()
+
+ expirey = 0 if client.expirey is None else client.expirey
+
+ return json_response(
+ {
+ "client_id":client.id,
+ "client_secret":client.secret,
+ "expires":expirey,
+ })
+
elif client_type == "client_associate":
# registering
if "client_id" in data:
@@ -78,7 +95,8 @@ def client_register(request):
expirey=expirey_db,
application_type=data["type"],
logo_url=data.get("logo_url", None),
- redirect_uri=data.get("redirect_uri", None)
+ redirect_uri=data.get("redirect_uri", None),
+ application_type=data["application_type"]
)
client.save()
From c33a34d45964a7e49a5eeeabde0ef4a8132ac591 Mon Sep 17 00:00:00 2001
From: xray7224
Date: Mon, 1 Jul 2013 17:50:39 +0100
Subject: [PATCH 06/27] Client registration now supports
application/x-www-form-urlencoded now
---
mediagoblin/db/models.py | 12 ++--
mediagoblin/federation/views.py | 98 +++++++++++++++++++++------------
mediagoblin/tools/validator.py | 46 ++++++++++++++++
3 files changed, 118 insertions(+), 38 deletions(-)
create mode 100644 mediagoblin/tools/validator.py
diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py
index 4c39c025..daee9295 100644
--- a/mediagoblin/db/models.py
+++ b/mediagoblin/db/models.py
@@ -119,12 +119,16 @@ class Client(Base):
updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
# optional stuff
- redirect_uri = Column(Unicode, nullable=True)
- logo_uri = Column(Unicode, nullable=True)
+ redirect_uri = Column(JSONEncoded, nullable=True)
+ logo_url = Column(Unicode, nullable=True)
application_name = Column(Unicode, nullable=True)
-
+ contacts = Column(JSONEncoded, nullable=True)
+
def __repr__(self):
- return "".format(self.id)
+ if self.application_name:
+ return "".format(self.application_name, self.id)
+ else:
+ return "".format(self.id)
diff --git a/mediagoblin/federation/views.py b/mediagoblin/federation/views.py
index 56bacbb1..743fd142 100644
--- a/mediagoblin/federation/views.py
+++ b/mediagoblin/federation/views.py
@@ -19,6 +19,7 @@ import json
from mediagoblin.meddleware.csrf import csrf_exempt
from mediagoblin.tools.response import json_response
from mediagoblin.tools.crypto import random_string
+from mediagoblin.tools.validator import validate_email, validate_url
from mediagoblin.db.models import Client
# possible client types
@@ -33,12 +34,13 @@ def client_register(request):
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)
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)
@@ -51,27 +53,16 @@ def client_register(request):
elif "client_secret" not in data:
return json_response({"error":"client_secret is required to update."}, status=400)
- client = Client.query.filter_by(id=data["client_id"], secret=data["client_secret"]).all()
+ client = Client.query.filter_by(id=data["client_id"], secret=data["client_secret"]).first()
- if not client:
- return json_response({"error":"Unauthorized.", status=403)
+ if client is None:
+ return json_response({"error":"Unauthorized."}, status=403)
- client.logo_url = data.get("logo_url", client.logo_url)
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
-
- client.save()
-
- expirey = 0 if client.expirey is None else client.expirey
-
- return json_response(
- {
- "client_id":client.id,
- "client_secret":client.secret,
- "expires":expirey,
- })
elif client_type == "client_associate":
# registering
@@ -82,27 +73,66 @@ def client_register(request):
elif "client_secret" in data:
return json_response({"error":"Only set client_secret for update."}, status=400)
- # generate the client_id and client_secret
- client_id = random_string(22) # seems to be what pump uses
- client_secret = random_string(43) # again, seems to be what pump uses
- expirey = 0 # for now, lets not have it expire
- expirey_db = None if expirey == 0 else expirey
-
- # save it
- client = Client(
- id=client_id,
- secret=client_secret,
- expirey=expirey_db,
- application_type=data["type"],
- logo_url=data.get("logo_url", None),
- redirect_uri=data.get("redirect_uri", None),
- application_type=data["application_type"]
- )
+ # generate the client_id and client_secret
+ client_id = random_string(22) # seems to be what pump uses
+ client_secret = random_string(43) # again, seems to be what pump uses
+ expirey = 0 # for now, lets not have it expire
+ expirey_db = None if expirey == 0 else expirey
+
+ # save it
+ client = Client(
+ id=client_id,
+ secret=client_secret,
+ expirey=expirey_db,
+ application_type=data["application_type"],
+ )
+
+ else:
+ return json_response({"error":"Invalid registration type"}, 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)
+ else:
+ client.logo_url = logo_url
+ application_name=data.get("application_name", None)
+
+ 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)
+
+ 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)
+
+
+ client.contacts = contacts
+
+ 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)
+
+ 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)
+
+ client.request_uri = request_uri
+
+
client.save()
+ expirey = 0 if client.expirey is None else client.expirey
+
return json_response(
{
- "client_id":client_id,
- "client_secret":client_secret,
+ "client_id":client.id,
+ "client_secret":client.secret,
"expires_at":expirey,
})
diff --git a/mediagoblin/tools/validator.py b/mediagoblin/tools/validator.py
new file mode 100644
index 00000000..03598f9c
--- /dev/null
+++ b/mediagoblin/tools/validator.py
@@ -0,0 +1,46 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from wtforms.validators import Email, URL
+
+def validate_email(email):
+ """
+ Validates an email
+
+ Returns True if valid and False if invalid
+ """
+
+ email_re = Email().regex
+ result = email_re.match(email)
+ if result is None:
+ return False
+ else:
+ return result.string
+
+def validate_url(url):
+ """
+ Validates a url
+
+ Returns True if valid and False if invalid
+ """
+
+ url_re = URL().regex
+ result = url_re.match(url)
+ if result is None:
+ return False
+ else:
+ return result.string
+
From be7f90b3f537190d199989625f75d334dbca7080 Mon Sep 17 00:00:00 2001
From: xray7224
Date: Mon, 1 Jul 2013 19:13:07 +0100
Subject: [PATCH 07/27] Adds the docs for client registration
---
docs/source/api/client_register.rst | 158 ++++++++++++++++++++++++++++
1 file changed, 158 insertions(+)
create mode 100644 docs/source/api/client_register.rst
diff --git a/docs/source/api/client_register.rst b/docs/source/api/client_register.rst
new file mode 100644
index 00000000..088eb51d
--- /dev/null
+++ b/docs/source/api/client_register.rst
@@ -0,0 +1,158 @@
+.. MediaGoblin Documentation
+
+ Written in 2011, 2012 by MediaGoblin contributors
+
+ To the extent possible under law, the author(s) have dedicated all
+ copyright and related and neighboring rights to this software to
+ the public domain worldwide. This software is distributed without
+ any warranty.
+
+ You should have received a copy of the CC0 Public Domain
+ Dedication along with this software. If not, see
+ .
+
+====================
+Registering a Client
+====================
+
+To use the GNU MediaGoblin API you need to use the dynamic client registration. This has been adapted from the `OpenID specification `_, this is the only part of OpenID that is being used to serve the purpose to provide the client registration which is used in OAuth.
+
+The endpoint is ``/api/client/register``
+
+The parameters are:
+
+type
+ **required** - This must be either *client_associate* (for new registration) or *client_update*
+
+client_id
+ **update only** - This should only be used updating client information, this is the client_id given when you register
+
+client_secret
+ **update only** - This should only be used updating client information, this is the client_secret given when you register
+
+contacts
+ **optional** - This a space seporated list of email addresses to contact of people responsible for the client
+
+application_type
+ **required** - This is the type of client you are making, this must be either *web* or *native*
+
+application_name
+ **optional** - This is the name of your client
+
+logo_url
+ **optional** - This is a URL of the logo image for your client
+
+redirect_uri
+ **optional** - This is a space seporated list of pre-registered URLs for use at the Authorization Server
+
+
+Response
+--------
+
+You will get back a response::
+
+client_id
+ This identifies a client
+
+client_secret
+ This is the secret.
+
+expires_at
+ This is time that the client credentials expire. If this is 0 the client registration does not expire.
+
+=======
+Example
+=======
+
+Register Client
+---------------
+
+To register a client for the first time, this is the minimum you must supply::
+
+ {
+ "type": "client_associate",
+ "application_type": "native"
+ }
+
+A Response will look like::
+
+ {
+ "client_secret": "hJtfhaQzgKerlLVdaeRAgmbcstSOBLRfgOinMxBCHcb",
+ "expires_at": 0,
+ "client_id": "vwljdhUMhhNbdKizpjZlxv"
+ }
+
+
+Updating Client
+---------------
+
+Using the response we got above we can update the information and add new information we may have opted not to supply::
+
+ {
+ "type": "client_update",
+ "client_id": "vwljdhUMhhNbdKizpjZlxv",
+ "client_secret": "hJtfhaQzgKerlLVdaeRAgmbcstSOBLRfgOinMxBCHcb",
+ "application_type": "web",
+ "application_name": "MyClient!",
+ "logo_url": "https://myclient.org/images/my_logo.png",
+ "contacts": "myemail@someprovider.com another_developer@provider.net",
+ }
+
+The response will just return back the client_id and client_secret you sent::
+
+ {
+ "client_id": "vwljdhUMhhNbdKizpjZlxv",
+ "client_secret": "hJtfhaQzgKerlLVdaeRAgmbcstSOBLRfgOinMxBCHcb",
+ "expires_at": 0
+ }
+
+
+======
+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.
+
+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.
+
+No registration type provided
+ This is when you leave out the ``type``. This should either be client_update or client_associate
+
+Unknown application_type.
+ This is when you have provided a ``type`` however this isn't one of the known types.
+
+client_id is required to update.
+ When you try and update you need to specify the client_id, this will be what you were given when you initially registered the client.
+
+client_secret is required to update.
+ When you try to update you need to specify the client_secrer, this will be what you were given when you initially register the client.
+
+Unauthorized.
+ This is when you are trying to update however the client_id and/or client_secret you have submitted are incorrect.
+
+Only set client_id for update.
+ This should only be given when you update.
+
+Only set client_secret for update.
+ This should only be given when you update.
+
+Logo URL is not a valid URL
+ This is when the URL specified did not meet the validation.
+
+contacts must be a string of space-separated email addresses.
+ ``contacts`` should be a string (not a list), ensure each email is seporated by a space
+
+Email is not a valid email
+ This is when you have submitted an invalid email address
+
+redirect_uris must be space-separated URLs.
+ ``redirect_uris`` should be a string (not a list), ensure each URL is seporated by a space
+
+URI is not a valid URI
+ This is when your URI is invalid.
+
+
From d41c6a5349db0ac573e8f0d29d239febc705f7c9 Mon Sep 17 00:00:00 2001
From: xray7224
Date: Mon, 8 Jul 2013 20:35:03 +0100
Subject: [PATCH 08/27] Adds oauth support up until authorization
---
docs/source/api/client_register.rst | 4 +-
mediagoblin/db/models.py | 37 +++++-
mediagoblin/federation/routing.py | 26 +++-
mediagoblin/federation/views.py | 177 ++++++++++++++++++++++------
mediagoblin/tools/request.py | 17 +++
mediagoblin/tools/response.py | 9 ++
setup.py | 2 +
7 files changed, 230 insertions(+), 42 deletions(-)
diff --git a/docs/source/api/client_register.rst b/docs/source/api/client_register.rst
index 088eb51d..4ad7908e 100644
--- a/docs/source/api/client_register.rst
+++ b/docs/source/api/client_register.rst
@@ -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.
diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py
index daee9295..8a71aa09 100644
--- a/mediagoblin/db/models.py
+++ b/mediagoblin/db/models.py
@@ -130,7 +130,36 @@ class Client(Base):
else:
return "".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]
######################################################
diff --git a/mediagoblin/federation/routing.py b/mediagoblin/federation/routing.py
index 6a75628e..f7e6f72c 100644
--- a/mediagoblin/federation/routing.py
+++ b/mediagoblin/federation/routing.py
@@ -16,4 +16,28 @@
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"
+ )
diff --git a/mediagoblin/federation/views.py b/mediagoblin/federation/views.py
index 743fd142..6c000855 100644
--- a/mediagoblin/federation/views.py
+++ b/mediagoblin/federation/views.py
@@ -14,13 +14,15 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-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
diff --git a/mediagoblin/tools/request.py b/mediagoblin/tools/request.py
index ee342eae..ed903ce0 100644
--- a/mediagoblin/tools/request.py
+++ b/mediagoblin/tools/request.py
@@ -14,12 +14,17 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+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
diff --git a/mediagoblin/tools/response.py b/mediagoblin/tools/response.py
index 1fd242fb..db8fc388 100644
--- a/mediagoblin/tools/response.py
+++ b/mediagoblin/tools/response.py
@@ -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
diff --git a/setup.py b/setup.py
index 6e026f30..b16f8d56 100644
--- 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
From 405aa45adc14d3c67a120618ecc0ae792f5881de Mon Sep 17 00:00:00 2001
From: xray7224
Date: Wed, 10 Jul 2013 15:49:59 +0100
Subject: [PATCH 09/27] Adds more support for oauth - access_token & decorators
still to do
---
mediagoblin/db/models.py | 2 +-
mediagoblin/federation/forms.py | 8 +
mediagoblin/federation/views.py | 165 ++++++++++++++++--
mediagoblin/static/css/base.css | 7 +
.../templates/mediagoblin/api/authorize.html | 56 ++++++
.../templates/mediagoblin/api/oob.html | 33 ++++
mediagoblin/tools/request.py | 2 +-
7 files changed, 260 insertions(+), 13 deletions(-)
create mode 100644 mediagoblin/federation/forms.py
create mode 100644 mediagoblin/templates/mediagoblin/api/authorize.html
create mode 100644 mediagoblin/templates/mediagoblin/api/oob.html
diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py
index 8a71aa09..b6ae533e 100644
--- a/mediagoblin/db/models.py
+++ b/mediagoblin/db/models.py
@@ -143,7 +143,7 @@ class RequestToken(Base):
used = Column(Boolean, default=False)
authenticated = Column(Boolean, default=False)
verifier = Column(Unicode, nullable=True)
- callback = Column(Unicode, nullable=True)
+ callback = Column(Unicode, nullable=False, default=u"oob")
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
diff --git a/mediagoblin/federation/forms.py b/mediagoblin/federation/forms.py
new file mode 100644
index 00000000..39d6fc27
--- /dev/null
+++ b/mediagoblin/federation/forms.py
@@ -0,0 +1,8 @@
+import wtforms
+from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
+
+class AuthorizeForm(wtforms.Form):
+ """ Form used to authorize the request token """
+
+ oauth_token = wtforms.HiddenField("oauth_token")
+ oauth_verifier = wtforms.HiddenField("oauth_verifier")
diff --git a/mediagoblin/federation/views.py b/mediagoblin/federation/views.py
index 6c000855..9559df10 100644
--- a/mediagoblin/federation/views.py
+++ b/mediagoblin/federation/views.py
@@ -14,15 +14,22 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from oauthlib.oauth1 import RequestValidator, RequestTokenEndpoint
+import datetime
+import oauthlib.common
+from oauthlib.oauth1 import (AuthorizationEndpoint, RequestValidator,
+ RequestTokenEndpoint)
+
+from mediagoblin.decorators import require_active_login
from mediagoblin.tools.translate import pass_to_ugettext
from mediagoblin.meddleware.csrf import csrf_exempt
from mediagoblin.tools.request import decode_request
-from mediagoblin.tools.response import json_response, render_400
+from mediagoblin.tools.response import (render_to_response, redirect,
+ 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, RequestToken, AccessToken
+from mediagoblin.db.models import User, Client, RequestToken, AccessToken
+from mediagoblin.federation.forms import AuthorizeForm
# possible client types
client_types = ["web", "native"] # currently what pump supports
@@ -120,7 +127,8 @@ def client_register(request):
)
else:
client.logo_url = logo_url
- application_name=data.get("application_name", None)
+
+ client.application_name = data.get("application_name", None)
contacts = data.get("contact", None)
if contacts is not None:
@@ -171,7 +179,7 @@ class ValidationException(Exception):
class GMGRequestValidator(RequestValidator):
- def __init__(self, data):
+ def __init__(self, data=None):
self.POST = data
def save_request_token(self, token, request):
@@ -183,8 +191,25 @@ class GMGRequestValidator(RequestValidator):
secret=token["oauth_token_secret"],
)
request_token.client = client_id
+ request_token.callback = token.get("oauth_callback", None)
request_token.save()
+ def save_verifier(self, token, verifier, request):
+ """ Saves the oauth request verifier """
+ request_token = RequestToken.query.filter_by(token=token).first()
+ request_token.verifier = verifier["oauth_verifier"]
+ request_token.save()
+
+
+ def save_access_token(self, token, request):
+ """ Saves access token in db """
+ access_token = AccessToken(
+ token=token["oauth_token"],
+ secret=token["oauth_secret"],
+ )
+ access_token.request_token = request.body["oauth_token"]
+ access_token.user = token["user"].id
+ access_token.save()
@csrf_exempt
def request_token(request):
@@ -195,10 +220,16 @@ def request_token(request):
error = "Could not decode data."
return json_response({"error": error}, status=400)
- if data is "":
+ if data == "":
error = "Unknown Content-Type"
return json_response({"error": error}, status=400)
+ print data
+
+ if "Authorization" not in data:
+ error = "Missing required parameter."
+ return json_response({"error": error}, status=400)
+
# Convert 'Authorization' to a dictionary
authorization = {}
@@ -207,6 +238,10 @@ def request_token(request):
authorization[key] = value
data[u"Authorization"] = authorization
+ if "oauth_consumer_key" not in data[u"Authorization"]:
+ error = "Missing required parameter."
+ return json_respinse({"error": error}, status=400)
+
# check the client_id
client_id = data[u"Authorization"][u"oauth_consumer_key"]
client = Client.query.filter_by(id=client_id).first()
@@ -217,29 +252,137 @@ def request_token(request):
request_validator = GMGRequestValidator(data)
rv = RequestTokenEndpoint(request_validator)
- tokens = rv.create_request_token(request, {})
+ tokens = rv.create_request_token(request, authorization)
tokenized = {}
for t in tokens.split("&"):
key, value = t.split("=")
tokenized[key] = value
+ print "[DEBUG] %s" % tokenized
+
# check what encoding to return them in
return json_response(tokenized)
-
+
+class WTFormData(dict):
+ """
+ Provides a WTForm usable dictionary
+ """
+ def getlist(self, key):
+ v = self[key]
+ if not isinstance(v, (list, tuple)):
+ v = [v]
+ return v
+
+@require_active_login
def authorize(request):
""" Displays a page for user to authorize """
+ if request.method == "POST":
+ return authorize_finish(request)
+
_ = 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")
+ err_msg = _("Must provide an oauth_token.")
return render_400(request, err_msg=err_msg)
- # AuthorizationEndpoint
+ oauth_request = RequestToken.query.filter_by(token=token).first()
+ if oauth_request is None:
+ err_msg = _("No request token found.")
+ return render_400(request, err_msg)
+ if oauth_request.used:
+ return authorize_finish(request)
+
+ if oauth_request.verifier is None:
+ orequest = oauthlib.common.Request(
+ uri=request.url,
+ http_method=request.method,
+ body=request.get_data(),
+ headers=request.headers
+ )
+ request_validator = GMGRequestValidator()
+ auth_endpoint = AuthorizationEndpoint(request_validator)
+ verifier = auth_endpoint.create_verifier(orequest, {})
+ oauth_request.verifier = verifier["oauth_verifier"]
+
+ oauth_request.user = request.user.id
+ oauth_request.save()
+
+ # find client & build context
+ client = Client.query.filter_by(id=oauth_request.client).first()
+
+ authorize_form = AuthorizeForm(WTFormData({
+ "oauth_token": oauth_request.token,
+ "oauth_verifier": oauth_request.verifier
+ }))
+
+ context = {
+ "user": request.user,
+ "oauth_request": oauth_request,
+ "client": client,
+ "authorize_form": authorize_form,
+ }
+
+
+ # AuthorizationEndpoint
+ return render_to_response(
+ request,
+ "mediagoblin/api/authorize.html",
+ context
+ )
+
+
+def authorize_finish(request):
+ """ Finishes the authorize """
+ _ = pass_to_ugettext
+ token = request.form["oauth_token"]
+ verifier = request.form["oauth_verifier"]
+ oauth_request = RequestToken.query.filter_by(token=token, verifier=verifier)
+ oauth_request = oauth_request.first()
+
+ if oauth_request is None:
+ # invalid token or verifier
+ err_msg = _("No request token found.")
+ return render_400(request, err_msg)
+
+ oauth_request.used = True
+ oauth_request.updated = datetime.datetime.now()
+ oauth_request.save()
+
+ if oauth_request.callback == "oob":
+ # out of bounds
+ context = {"oauth_request": oauth_request}
+ return render_to_response(
+ request,
+ "mediagoblin/api/oob.html",
+ context
+ )
+
+ # okay we need to redirect them then!
+ querystring = "?oauth_token={0}&oauth_verifier={1}".format(
+ oauth_request.token,
+ oauth_request.verifier
+ )
+
+ return redirect(
+ request,
+ querystring=querystring,
+ location=oauth_request.callback
+ )
@csrf_exempt
def access_token(request):
""" Provides an access token based on a valid verifier and request token """
- pass
+ try:
+ data = decode_request(request)
+ except ValueError:
+ error = "Could not decode data."
+ return json_response({"error": error}, status=400)
+
+ if data == "":
+ error = "Unknown Content-Type"
+ return json_response({"error": error}, status=400)
+
+ print "debug: %s" % data
diff --git a/mediagoblin/static/css/base.css b/mediagoblin/static/css/base.css
index 8b57584d..0d813bf5 100644
--- a/mediagoblin/static/css/base.css
+++ b/mediagoblin/static/css/base.css
@@ -753,3 +753,10 @@ pre {
#exif_additional_info table tr {
margin-bottom: 10px;
}
+
+p.verifier {
+ text-align:center;
+ font-size:50px;
+ none repeat scroll 0% 0% rgb(221, 221, 221);
+ padding: 1em 0px;
+}
diff --git a/mediagoblin/templates/mediagoblin/api/authorize.html b/mediagoblin/templates/mediagoblin/api/authorize.html
new file mode 100644
index 00000000..d0ec2616
--- /dev/null
+++ b/mediagoblin/templates/mediagoblin/api/authorize.html
@@ -0,0 +1,56 @@
+{#
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#}
+{% extends "mediagoblin/base.html" %}
+
+{% block title -%}
+ {% trans %}Authorization{% endtrans %} — {{ super() }}
+{%- endblock %}
+
+{% block mediagoblin_content %}
+
+{% trans %}Authorize{% endtrans %}
+
+
+ {% trans %}You are logged in as{% endtrans %}
+ {{user.username}}
+
+
+ {% trans %}Do you want to authorize {% endtrans %}
+ {% if client.application_name -%}
+ {{ client.application_name }}
+ {%- else -%}
+ {% trans %}an unknown application{% endtrans %}
+ {%- endif %}
+ {% trans %} to access your account? {% endtrans %}
+
+ {% trans %}Applications with access to your account can: {% endtrans %}
+
+ - {% trans %}Post new media as you{% endtrans %}
+ - {% trans %}See your information (e.g profile, meida, etc...){% endtrans %}
+ - {% trans %}Change your information{% endtrans %}
+
+
+
+
+
+{% endblock %}
diff --git a/mediagoblin/templates/mediagoblin/api/oob.html b/mediagoblin/templates/mediagoblin/api/oob.html
new file mode 100644
index 00000000..d290472a
--- /dev/null
+++ b/mediagoblin/templates/mediagoblin/api/oob.html
@@ -0,0 +1,33 @@
+{#
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#}
+{% extends "mediagoblin/base.html" %}
+
+{% block title -%}
+ {% trans %}Authorization Finished{% endtrans %} — {{ super() }}
+{%- endblock %}
+
+{% block mediagoblin_content %}
+
+{% trans %}Authorization Complete{% endtrans %}
+
+{% trans %}Copy and paste this into your client:{% endtrans %}
+
+
+ {{ oauth_request.verifier }}
+
+{% endblock %}
diff --git a/mediagoblin/tools/request.py b/mediagoblin/tools/request.py
index ed903ce0..2c9e609d 100644
--- a/mediagoblin/tools/request.py
+++ b/mediagoblin/tools/request.py
@@ -48,7 +48,7 @@ def decode_request(request):
if request.content_type == json_encoded:
data = json.loads(data)
- elif request.content_type == form_encoded:
+ elif request.content_type == form_encoded or request.content_type == "":
data = request.form
else:
data = ""
From 2b60a56cbec44f789ee2efe71294979d7784515c Mon Sep 17 00:00:00 2001
From: xray7224
Date: Thu, 11 Jul 2013 17:58:58 +0100
Subject: [PATCH 10/27] Finishes most of oauth, just decorator to complete
---
mediagoblin/decorators.py | 16 ++++++-
mediagoblin/federation/views.py | 78 ++++++++++++++++-----------------
mediagoblin/tools/request.py | 10 +++++
mediagoblin/tools/response.py | 19 ++++++++
4 files changed, 81 insertions(+), 42 deletions(-)
diff --git a/mediagoblin/decorators.py b/mediagoblin/decorators.py
index ece222f5..ce26e46c 100644
--- a/mediagoblin/decorators.py
+++ b/mediagoblin/decorators.py
@@ -22,7 +22,8 @@ from werkzeug.exceptions import Forbidden, NotFound
from mediagoblin import mg_globals as mgg
from mediagoblin import messages
from mediagoblin.db.models import MediaEntry, User
-from mediagoblin.tools.response import redirect, render_404
+from mediagoblin.tools.request import decode_authorization_header
+from mediagoblin.tools.response import json_response, redirect, render_404
from mediagoblin.tools.translate import pass_to_ugettext as _
@@ -268,3 +269,16 @@ def auth_enabled(controller):
return controller(request, *args, **kwargs)
return wrapper
+
+def oauth_requeired(controller):
+ """ Used to wrap API endpoints where oauth is required """
+ @wraps(controller)
+ def wrapper(request, *args, **kwargs):
+ data = request.headers
+ authorization = decode_authorization_header(data)
+
+ if authorization == dict():
+ error = "Missing required parameter."
+ return json_response({"error": error}, status=400)
+
+
diff --git a/mediagoblin/federation/views.py b/mediagoblin/federation/views.py
index 9559df10..a6dcc79b 100644
--- a/mediagoblin/federation/views.py
+++ b/mediagoblin/federation/views.py
@@ -18,14 +18,15 @@ import datetime
import oauthlib.common
from oauthlib.oauth1 import (AuthorizationEndpoint, RequestValidator,
- RequestTokenEndpoint)
+ RequestTokenEndpoint, AccessTokenEndpoint)
from mediagoblin.decorators import require_active_login
from mediagoblin.tools.translate import pass_to_ugettext
from mediagoblin.meddleware.csrf import csrf_exempt
-from mediagoblin.tools.request import decode_request
+from mediagoblin.tools.request import decode_request, decode_authorization_header
from mediagoblin.tools.response import (render_to_response, redirect,
- json_response, render_400)
+ json_response, render_400,
+ form_response)
from mediagoblin.tools.crypto import random_string
from mediagoblin.tools.validator import validate_email, validate_url
from mediagoblin.db.models import User, Client, RequestToken, AccessToken
@@ -184,7 +185,7 @@ class GMGRequestValidator(RequestValidator):
def save_request_token(self, token, request):
""" Saves request token in db """
- client_id = self.POST[u"Authorization"][u"oauth_consumer_key"]
+ client_id = self.POST[u"oauth_consumer_key"]
request_token = RequestToken(
token=token["oauth_token"],
@@ -200,17 +201,21 @@ class GMGRequestValidator(RequestValidator):
request_token.verifier = verifier["oauth_verifier"]
request_token.save()
-
def save_access_token(self, token, request):
""" Saves access token in db """
access_token = AccessToken(
token=token["oauth_token"],
- secret=token["oauth_secret"],
+ secret=token["oauth_token_secret"],
)
- access_token.request_token = request.body["oauth_token"]
- access_token.user = token["user"].id
+ access_token.request_token = request.oauth_token
+ request_token = RequestToken.query.filter_by(token=request.oauth_token).first()
+ access_token.user = request_token.user
access_token.save()
+ def get_realms(*args, **kwargs):
+ """ Currently a stub - called when making AccessTokens """
+ return list()
+
@csrf_exempt
def request_token(request):
""" Returns request token """
@@ -224,45 +229,32 @@ def request_token(request):
error = "Unknown Content-Type"
return json_response({"error": error}, status=400)
- print data
+ if not data and request.headers:
+ data = request.headers
+
+ data = dict(data) # mutableifying
- if "Authorization" not in data:
+ authorization = decode_authorization_header(data)
+
+
+ if authorization == dict() or u"oauth_consumer_key" not in authorization:
error = "Missing required parameter."
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
-
- if "oauth_consumer_key" not in data[u"Authorization"]:
- error = "Missing required parameter."
- return json_respinse({"error": error}, status=400)
-
# check the client_id
- client_id = data[u"Authorization"][u"oauth_consumer_key"]
+ client_id = 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)
+ # make request token and return to client
+ request_validator = GMGRequestValidator(authorization)
rv = RequestTokenEndpoint(request_validator)
tokens = rv.create_request_token(request, authorization)
- tokenized = {}
- for t in tokens.split("&"):
- key, value = t.split("=")
- tokenized[key] = value
-
- print "[DEBUG] %s" % tokenized
-
- # check what encoding to return them in
- return json_response(tokenized)
+ return form_response(tokens)
class WTFormData(dict):
"""
@@ -375,14 +367,18 @@ def authorize_finish(request):
@csrf_exempt
def access_token(request):
""" Provides an access token based on a valid verifier and request token """
- try:
- data = decode_request(request)
- except ValueError:
- error = "Could not decode data."
+ data = request.headers
+
+ parsed_tokens = decode_authorization_header(data)
+
+ if parsed_tokens == dict() or "oauth_token" not in parsed_tokens:
+ error = "Missing required parameter."
return json_response({"error": error}, status=400)
- if data == "":
- error = "Unknown Content-Type"
- return json_response({"error": error}, status=400)
- print "debug: %s" % data
+ request.oauth_token = parsed_tokens["oauth_token"]
+ request_validator = GMGRequestValidator(data)
+ av = AccessTokenEndpoint(request_validator)
+ tokens = av.create_access_token(request, {})
+ return form_response(tokens)
+
diff --git a/mediagoblin/tools/request.py b/mediagoblin/tools/request.py
index 2c9e609d..0c0fc557 100644
--- a/mediagoblin/tools/request.py
+++ b/mediagoblin/tools/request.py
@@ -14,6 +14,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+import re
import json
import logging
from mediagoblin.db.models import User
@@ -25,6 +26,9 @@ _log = logging.getLogger(__name__)
form_encoded = "application/x-www-form-urlencoded"
json_encoded = "application/json"
+# Regex for Authorization header
+auth_header_re = re.compile('(\w+)[:=] ?"?(\w+)"?')
+
def setup_user_in_request(request):
"""
Examine a request and tack on a request.user parameter if that's
@@ -53,3 +57,9 @@ def decode_request(request):
else:
data = ""
return data
+
+def decode_authorization_header(header):
+ """ Decodes a HTTP Authorization Header to python dictionary """
+ authorization = header.get("Authorization", "")
+ tokens = dict(auth_header_re.findall(authorization))
+ return tokens
diff --git a/mediagoblin/tools/response.py b/mediagoblin/tools/response.py
index db8fc388..b0401e08 100644
--- a/mediagoblin/tools/response.py
+++ b/mediagoblin/tools/response.py
@@ -138,3 +138,22 @@ def json_response(serializable, _disable_cors=False, *args, **kw):
response.headers.set(key, value)
return response
+
+def form_response(data, *args, **kwargs):
+ """
+ Responds using application/x-www-form-urlencoded and returns a werkzeug
+ Response object with the data argument as the body
+ and 'application/x-www-form-urlencoded' as the Content-Type.
+
+ Any extra arguments and keyword arguments are passed to the
+ Response.__init__ method.
+ """
+
+ response = wz_Response(
+ data,
+ content_type="application/x-www-form-urlencoded",
+ *args,
+ **kwargs
+ )
+
+ return response
From 786bbd79e8d77c06a9d86aee00edc4dd3e89d651 Mon Sep 17 00:00:00 2001
From: xray7224
Date: Thu, 11 Jul 2013 19:43:00 +0100
Subject: [PATCH 11/27] Cleans up some of the OAuth code
---
mediagoblin/decorators.py | 4 +-
mediagoblin/federation/exceptions.py | 18 ++++++
mediagoblin/federation/oauth.py | 80 ++++++++++++++++++++++++
mediagoblin/federation/tools/__init__.py | 0
mediagoblin/federation/tools/request.py | 27 ++++++++
mediagoblin/federation/views.py | 56 ++---------------
mediagoblin/tools/request.py | 9 ---
7 files changed, 134 insertions(+), 60 deletions(-)
create mode 100644 mediagoblin/federation/exceptions.py
create mode 100644 mediagoblin/federation/oauth.py
create mode 100644 mediagoblin/federation/tools/__init__.py
create mode 100644 mediagoblin/federation/tools/request.py
diff --git a/mediagoblin/decorators.py b/mediagoblin/decorators.py
index ce26e46c..1fdb78d7 100644
--- a/mediagoblin/decorators.py
+++ b/mediagoblin/decorators.py
@@ -22,10 +22,11 @@ from werkzeug.exceptions import Forbidden, NotFound
from mediagoblin import mg_globals as mgg
from mediagoblin import messages
from mediagoblin.db.models import MediaEntry, User
-from mediagoblin.tools.request import decode_authorization_header
from mediagoblin.tools.response import json_response, redirect, render_404
from mediagoblin.tools.translate import pass_to_ugettext as _
+from mediagoblin.federation.tools.request import decode_authorization_header
+from mediagoblin.federation.oauth import GMGRequestValidator
def require_active_login(controller):
"""
@@ -282,3 +283,4 @@ def oauth_requeired(controller):
return json_response({"error": error}, status=400)
+
diff --git a/mediagoblin/federation/exceptions.py b/mediagoblin/federation/exceptions.py
new file mode 100644
index 00000000..5eccba34
--- /dev/null
+++ b/mediagoblin/federation/exceptions.py
@@ -0,0 +1,18 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+class ValidationException(Exception):
+ pass
diff --git a/mediagoblin/federation/oauth.py b/mediagoblin/federation/oauth.py
new file mode 100644
index 00000000..c94b0a9d
--- /dev/null
+++ b/mediagoblin/federation/oauth.py
@@ -0,0 +1,80 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from oauthlib.common import Request
+from oauthlib.oauth1 import (AuthorizationEndpoint, RequestValidator,
+ RequestTokenEndpoint, AccessTokenEndpoint)
+
+from mediagoblin.db.models import Client, RequestToken, AccessToken
+
+
+
+class GMGRequestValidator(RequestValidator):
+
+ def __init__(self, data=None):
+ self.POST = data
+
+ def save_request_token(self, token, request):
+ """ Saves request token in db """
+ client_id = self.POST[u"oauth_consumer_key"]
+
+ request_token = RequestToken(
+ token=token["oauth_token"],
+ secret=token["oauth_token_secret"],
+ )
+ request_token.client = client_id
+ request_token.callback = token.get("oauth_callback", None)
+ request_token.save()
+
+ def save_verifier(self, token, verifier, request):
+ """ Saves the oauth request verifier """
+ request_token = RequestToken.query.filter_by(token=token).first()
+ request_token.verifier = verifier["oauth_verifier"]
+ request_token.save()
+
+ def save_access_token(self, token, request):
+ """ Saves access token in db """
+ access_token = AccessToken(
+ token=token["oauth_token"],
+ secret=token["oauth_token_secret"],
+ )
+ access_token.request_token = request.oauth_token
+ request_token = RequestToken.query.filter_by(token=request.oauth_token).first()
+ access_token.user = request_token.user
+ access_token.save()
+
+ def get_realms(*args, **kwargs):
+ """ Currently a stub - called when making AccessTokens """
+ return list()
+
+class GMGRequest(Request):
+ """
+ Fills in data to produce a oauth.common.Request object from a
+ werkzeug Request object
+ """
+
+ def __init__(self, request, *args, **kwargs):
+ """
+ :param request: werkzeug request object
+
+ any extra params are passed to oauthlib.common.Request object
+ """
+ kwargs["uri"] = kwargs.get("uri", request.url)
+ kwargs["http_method"] = kwargs.get("http_method", request.method)
+ kwargs["body"] = kwargs.get("body", request.get_data())
+ kwargs["headers"] = kwargs.get("headers", dict(request.headers))
+
+ super(GMGRequest, self).__init__(*args, **kwargs)
diff --git a/mediagoblin/federation/tools/__init__.py b/mediagoblin/federation/tools/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/mediagoblin/federation/tools/request.py b/mediagoblin/federation/tools/request.py
new file mode 100644
index 00000000..4f5be277
--- /dev/null
+++ b/mediagoblin/federation/tools/request.py
@@ -0,0 +1,27 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import re
+
+# Regex for parsing Authorization string
+auth_header_re = re.compile('(\w+)[:=] ?"?(\w+)"?')
+
+def decode_authorization_header(header):
+ """ Decodes a HTTP Authorization Header to python dictionary """
+ authorization = header.get("Authorization", "")
+ tokens = dict(auth_header_re.findall(authorization))
+ return tokens
+
diff --git a/mediagoblin/federation/views.py b/mediagoblin/federation/views.py
index a6dcc79b..29b5647e 100644
--- a/mediagoblin/federation/views.py
+++ b/mediagoblin/federation/views.py
@@ -16,21 +16,23 @@
import datetime
-import oauthlib.common
from oauthlib.oauth1 import (AuthorizationEndpoint, RequestValidator,
RequestTokenEndpoint, AccessTokenEndpoint)
from mediagoblin.decorators import require_active_login
from mediagoblin.tools.translate import pass_to_ugettext
from mediagoblin.meddleware.csrf import csrf_exempt
-from mediagoblin.tools.request import decode_request, decode_authorization_header
+from mediagoblin.tools.request import decode_request
from mediagoblin.tools.response import (render_to_response, redirect,
json_response, render_400,
form_response)
from mediagoblin.tools.crypto import random_string
from mediagoblin.tools.validator import validate_email, validate_url
-from mediagoblin.db.models import User, Client, RequestToken, AccessToken
from mediagoblin.federation.forms import AuthorizeForm
+from mediagoblin.federation.exceptions import ValidationException
+from mediagoblin.federation.oauth import GMGRequestValidator, GMGRequest
+from mediagoblin.federation.tools.request import decode_authorization_header
+from mediagoblin.db.models import Client, RequestToken, AccessToken
# possible client types
client_types = ["web", "native"] # currently what pump supports
@@ -175,47 +177,6 @@ def client_register(request):
"expires_at": expirey,
})
-class ValidationException(Exception):
- pass
-
-class GMGRequestValidator(RequestValidator):
-
- def __init__(self, data=None):
- self.POST = data
-
- def save_request_token(self, token, request):
- """ Saves request token in db """
- client_id = self.POST[u"oauth_consumer_key"]
-
- request_token = RequestToken(
- token=token["oauth_token"],
- secret=token["oauth_token_secret"],
- )
- request_token.client = client_id
- request_token.callback = token.get("oauth_callback", None)
- request_token.save()
-
- def save_verifier(self, token, verifier, request):
- """ Saves the oauth request verifier """
- request_token = RequestToken.query.filter_by(token=token).first()
- request_token.verifier = verifier["oauth_verifier"]
- request_token.save()
-
- def save_access_token(self, token, request):
- """ Saves access token in db """
- access_token = AccessToken(
- token=token["oauth_token"],
- secret=token["oauth_token_secret"],
- )
- access_token.request_token = request.oauth_token
- request_token = RequestToken.query.filter_by(token=request.oauth_token).first()
- access_token.user = request_token.user
- access_token.save()
-
- def get_realms(*args, **kwargs):
- """ Currently a stub - called when making AccessTokens """
- return list()
-
@csrf_exempt
def request_token(request):
""" Returns request token """
@@ -288,12 +249,7 @@ def authorize(request):
return authorize_finish(request)
if oauth_request.verifier is None:
- orequest = oauthlib.common.Request(
- uri=request.url,
- http_method=request.method,
- body=request.get_data(),
- headers=request.headers
- )
+ orequest = GMGRequest(request)
request_validator = GMGRequestValidator()
auth_endpoint = AuthorizationEndpoint(request_validator)
verifier = auth_endpoint.create_verifier(orequest, {})
diff --git a/mediagoblin/tools/request.py b/mediagoblin/tools/request.py
index 0c0fc557..d4739039 100644
--- a/mediagoblin/tools/request.py
+++ b/mediagoblin/tools/request.py
@@ -14,7 +14,6 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import re
import json
import logging
from mediagoblin.db.models import User
@@ -26,8 +25,6 @@ _log = logging.getLogger(__name__)
form_encoded = "application/x-www-form-urlencoded"
json_encoded = "application/json"
-# Regex for Authorization header
-auth_header_re = re.compile('(\w+)[:=] ?"?(\w+)"?')
def setup_user_in_request(request):
"""
@@ -57,9 +54,3 @@ def decode_request(request):
else:
data = ""
return data
-
-def decode_authorization_header(header):
- """ Decodes a HTTP Authorization Header to python dictionary """
- authorization = header.get("Authorization", "")
- tokens = dict(auth_header_re.findall(authorization))
- return tokens
From 1e2675b0c0ee2bf35705b538ec94978fe4f005d4 Mon Sep 17 00:00:00 2001
From: xray7224
Date: Thu, 11 Jul 2013 20:24:20 +0100
Subject: [PATCH 12/27] Adds the decorator
---
mediagoblin/decorators.py | 20 +++++++++++++++++---
mediagoblin/federation/oauth.py | 2 ++
mediagoblin/federation/routing.py | 6 ++++++
mediagoblin/federation/views.py | 8 ++++++--
4 files changed, 31 insertions(+), 5 deletions(-)
diff --git a/mediagoblin/decorators.py b/mediagoblin/decorators.py
index 1fdb78d7..ad36f376 100644
--- a/mediagoblin/decorators.py
+++ b/mediagoblin/decorators.py
@@ -18,6 +18,7 @@ from functools import wraps
from urlparse import urljoin
from werkzeug.exceptions import Forbidden, NotFound
+from oauthlib.oauth1 import ResourceEndpoint
from mediagoblin import mg_globals as mgg
from mediagoblin import messages
@@ -271,7 +272,7 @@ def auth_enabled(controller):
return wrapper
-def oauth_requeired(controller):
+def oauth_required(controller):
""" Used to wrap API endpoints where oauth is required """
@wraps(controller)
def wrapper(request, *args, **kwargs):
@@ -282,5 +283,18 @@ def oauth_requeired(controller):
error = "Missing required parameter."
return json_response({"error": error}, status=400)
-
-
+
+ request_validator = GMGRequestValidator()
+ resource_endpoint = ResourceEndpoint(request_validator)
+ valid, request = resource_endpoint.validate_protected_resource_request(
+ uri=request.url,
+ http_method=request.method,
+ body=request.get_data(),
+ headers=dict(request.headers),
+ )
+ #print "[VALID] %s" % valid
+ #print "[REQUEST] %s" % request
+
+ return controller(request, *args, **kwargs)
+
+ return wrapper
diff --git a/mediagoblin/federation/oauth.py b/mediagoblin/federation/oauth.py
index c94b0a9d..ff45882d 100644
--- a/mediagoblin/federation/oauth.py
+++ b/mediagoblin/federation/oauth.py
@@ -24,6 +24,8 @@ from mediagoblin.db.models import Client, RequestToken, AccessToken
class GMGRequestValidator(RequestValidator):
+ enforce_ssl = False
+
def __init__(self, data=None):
self.POST = data
diff --git a/mediagoblin/federation/routing.py b/mediagoblin/federation/routing.py
index f7e6f72c..5dc71456 100644
--- a/mediagoblin/federation/routing.py
+++ b/mediagoblin/federation/routing.py
@@ -41,3 +41,9 @@ add_route(
"/oauth/access_token",
"mediagoblin.federation.views:access_token"
)
+
+add_route(
+ "mediagoblin.federation",
+ "/api/test",
+ "mediagoblin.federation.views:test"
+ )
diff --git a/mediagoblin/federation/views.py b/mediagoblin/federation/views.py
index 29b5647e..c538f4cb 100644
--- a/mediagoblin/federation/views.py
+++ b/mediagoblin/federation/views.py
@@ -19,7 +19,7 @@ import datetime
from oauthlib.oauth1 import (AuthorizationEndpoint, RequestValidator,
RequestTokenEndpoint, AccessTokenEndpoint)
-from mediagoblin.decorators import require_active_login
+from mediagoblin.decorators import require_active_login, oauth_required
from mediagoblin.tools.translate import pass_to_ugettext
from mediagoblin.meddleware.csrf import csrf_exempt
from mediagoblin.tools.request import decode_request
@@ -337,4 +337,8 @@ def access_token(request):
av = AccessTokenEndpoint(request_validator)
tokens = av.create_access_token(request, {})
return form_response(tokens)
-
+
+@csrf_exempt
+@oauth_required
+def test(request):
+ return json_response({"check":"OK"})
From 49a47ec991152a5dd25a7460e1d3d11afb73d32d Mon Sep 17 00:00:00 2001
From: xray7224
Date: Thu, 11 Jul 2013 20:55:08 +0100
Subject: [PATCH 13/27] Ensures endpoint queries with @oauth_required are
validated
---
mediagoblin/decorators.py | 6 +++--
mediagoblin/federation/oauth.py | 45 +++++++++++++++++++++++++++++++++
2 files changed, 49 insertions(+), 2 deletions(-)
diff --git a/mediagoblin/decorators.py b/mediagoblin/decorators.py
index ad36f376..bb2ba7a5 100644
--- a/mediagoblin/decorators.py
+++ b/mediagoblin/decorators.py
@@ -292,8 +292,10 @@ def oauth_required(controller):
body=request.get_data(),
headers=dict(request.headers),
)
- #print "[VALID] %s" % valid
- #print "[REQUEST] %s" % request
+
+ if not valid:
+ error = "Invalid oauth prarameter."
+ return json_response({"error": error}, status=400)
return controller(request, *args, **kwargs)
diff --git a/mediagoblin/federation/oauth.py b/mediagoblin/federation/oauth.py
index ff45882d..846b0794 100644
--- a/mediagoblin/federation/oauth.py
+++ b/mediagoblin/federation/oauth.py
@@ -62,6 +62,51 @@ class GMGRequestValidator(RequestValidator):
""" Currently a stub - called when making AccessTokens """
return list()
+ def validate_timestamp_and_nonce(self, client_key, timestamp,
+ nonce, request, request_token=None,
+ access_token=None):
+ return True # TODO!!! - SECURITY RISK IF NOT DONE
+
+ def validate_client_key(self, client_key, request):
+ """ Verifies client exists with id of client_key """
+ client = Client.query.filter_by(id=client_key).first()
+ if client is None:
+ return False
+
+ return True
+
+ def validate_access_token(self, client_key, token, request):
+ """ Verifies token exists for client with id of client_key """
+ client = Client.query.filter_by(id=client_key).first()
+ token = AccessToken.query.filter_by(token=token)
+ token = token.first()
+
+ if token is None:
+ return False
+
+ request_token = RequestToken.query.filter_by(token=token.request_token)
+ request_token = request_token.first()
+
+ if client.id != request_token.client:
+ return False
+
+ return True
+
+ def validate_realms(self, *args, **kwargs):
+ """ Would validate reals however not using these yet. """
+ return True # implement when realms are implemented
+
+
+ def get_client_secret(self, client_key, request):
+ """ Retrives a client secret with from a client with an id of client_key """
+ client = Client.query.filter_by(id=client_key).first()
+ return client.secret
+
+ def get_access_token_secret(self, client_key, token, request):
+ client = Client.query.filter_by(id=client_key).first()
+ access_token = AccessToken.query.filter_by(token=token).first()
+ return access_token.secret
+
class GMGRequest(Request):
"""
Fills in data to produce a oauth.common.Request object from a
From cfe7054c13880657fdcb95068a734554ff847cea Mon Sep 17 00:00:00 2001
From: xray7224
Date: Sun, 14 Jul 2013 16:24:04 +0100
Subject: [PATCH 14/27] Using nonce now, preventing OAuth replay attacks
---
mediagoblin/db/models.py | 14 ++++++++++++--
mediagoblin/federation/oauth.py | 9 +++++++--
mediagoblin/federation/views.py | 10 +++++++++-
3 files changed, 28 insertions(+), 5 deletions(-)
diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py
index b6ae533e..74dea44e 100644
--- a/mediagoblin/db/models.py
+++ b/mediagoblin/db/models.py
@@ -161,6 +161,16 @@ class AccessToken(Base):
updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
+class NonceTimestamp(Base):
+ """
+ A place the timestamp and nonce can be stored - this is for OAuth1
+ """
+ __tablename__ = "core__nonce_timestamps"
+
+ nonce = Column(Unicode, nullable=False, primary_key=True)
+ timestamp = Column(DateTime, nullable=False, primary_key=True)
+
+
class MediaEntry(Base, MediaEntryMixin):
"""
TODO: Consider fetching the media_files using join
@@ -636,8 +646,8 @@ with_polymorphic(
[ProcessingNotification, CommentNotification])
MODELS = [
- User, Client, RequestToken, AccessToken, MediaEntry, Tag, MediaTag,
- MediaComment, Collection, CollectionItem, MediaFile, FileKeynames,
+ User, Client, RequestToken, AccessToken, NonceTimestamp, MediaEntry, Tag,
+ MediaTag, MediaComment, Collection, CollectionItem, MediaFile, FileKeynames,
MediaAttachmentFile, ProcessingMetaData, Notification, CommentNotification,
ProcessingNotification, CommentSubscription]
diff --git a/mediagoblin/federation/oauth.py b/mediagoblin/federation/oauth.py
index 846b0794..ea0fea2c 100644
--- a/mediagoblin/federation/oauth.py
+++ b/mediagoblin/federation/oauth.py
@@ -18,7 +18,7 @@ from oauthlib.common import Request
from oauthlib.oauth1 import (AuthorizationEndpoint, RequestValidator,
RequestTokenEndpoint, AccessTokenEndpoint)
-from mediagoblin.db.models import Client, RequestToken, AccessToken
+from mediagoblin.db.models import NonceTimestamp, Client, RequestToken, AccessToken
@@ -65,7 +65,12 @@ class GMGRequestValidator(RequestValidator):
def validate_timestamp_and_nonce(self, client_key, timestamp,
nonce, request, request_token=None,
access_token=None):
- return True # TODO!!! - SECURITY RISK IF NOT DONE
+ nc = NonceTimestamp.query.filter_by(timestamp=timestamp, nonce=nonce)
+ nc = nc.first()
+ if nc is None:
+ return True
+
+ return False
def validate_client_key(self, client_key, request):
""" Verifies client exists with id of client_key """
diff --git a/mediagoblin/federation/views.py b/mediagoblin/federation/views.py
index c538f4cb..aae9d55a 100644
--- a/mediagoblin/federation/views.py
+++ b/mediagoblin/federation/views.py
@@ -32,7 +32,7 @@ from mediagoblin.federation.forms import AuthorizeForm
from mediagoblin.federation.exceptions import ValidationException
from mediagoblin.federation.oauth import GMGRequestValidator, GMGRequest
from mediagoblin.federation.tools.request import decode_authorization_header
-from mediagoblin.db.models import Client, RequestToken, AccessToken
+from mediagoblin.db.models import NonceTimestamp, Client, RequestToken, AccessToken
# possible client types
client_types = ["web", "native"] # currently what pump supports
@@ -215,6 +215,14 @@ def request_token(request):
rv = RequestTokenEndpoint(request_validator)
tokens = rv.create_request_token(request, authorization)
+ # store the nonce & timestamp before we return back
+ nonce = authorization[u"oauth_nonce"]
+ timestamp = authorization[u"oauth_timestamp"]
+ timestamp = datetime.datetime.fromtimestamp(int(timestamp))
+
+ nc = NonceTimestamp(nonce=nonce, timestamp=timestamp)
+ nc.save()
+
return form_response(tokens)
class WTFormData(dict):
From 1c694fbec5daf567a8ec49baf4df2abfa408442a Mon Sep 17 00:00:00 2001
From: xray7224
Date: Sun, 14 Jul 2013 19:00:52 +0100
Subject: [PATCH 15/27] Fixes tests
---
mediagoblin/federation/routing.py | 6 ------
mediagoblin/federation/views.py | 4 ----
mediagoblin/plugins/api/views.py | 2 +-
mediagoblin/plugins/oauth/__init__.py | 12 ++++++------
mediagoblin/plugins/oauth/tools.py | 2 +-
mediagoblin/plugins/oauth/views.py | 3 +--
mediagoblin/tests/test_http_callback.py | 4 ++--
mediagoblin/tests/{test_oauth.py => test_oauth2.py} | 12 ++++++------
8 files changed, 17 insertions(+), 28 deletions(-)
rename mediagoblin/tests/{test_oauth.py => test_oauth2.py} (95%)
diff --git a/mediagoblin/federation/routing.py b/mediagoblin/federation/routing.py
index 5dc71456..bc3a7a7e 100644
--- a/mediagoblin/federation/routing.py
+++ b/mediagoblin/federation/routing.py
@@ -23,7 +23,6 @@ add_route(
"mediagoblin.federation.views:client_register"
)
-
add_route(
"mediagoblin.federation",
"/oauth/request_token",
@@ -42,8 +41,3 @@ add_route(
"mediagoblin.federation.views:access_token"
)
-add_route(
- "mediagoblin.federation",
- "/api/test",
- "mediagoblin.federation.views:test"
- )
diff --git a/mediagoblin/federation/views.py b/mediagoblin/federation/views.py
index aae9d55a..94eb9886 100644
--- a/mediagoblin/federation/views.py
+++ b/mediagoblin/federation/views.py
@@ -346,7 +346,3 @@ def access_token(request):
tokens = av.create_access_token(request, {})
return form_response(tokens)
-@csrf_exempt
-@oauth_required
-def test(request):
- return json_response({"check":"OK"})
diff --git a/mediagoblin/plugins/api/views.py b/mediagoblin/plugins/api/views.py
index 738ea25f..b7e74799 100644
--- a/mediagoblin/plugins/api/views.py
+++ b/mediagoblin/plugins/api/views.py
@@ -21,7 +21,7 @@ from os.path import splitext
from werkzeug.exceptions import BadRequest, Forbidden
from werkzeug.wrappers import Response
-from mediagoblin.tools.json import json_response
+from mediagoblin.tools.response import json_response
from mediagoblin.decorators import require_active_login
from mediagoblin.meddleware.csrf import csrf_exempt
from mediagoblin.media_types import sniff_media
diff --git a/mediagoblin/plugins/oauth/__init__.py b/mediagoblin/plugins/oauth/__init__.py
index 5762379d..82c1f380 100644
--- a/mediagoblin/plugins/oauth/__init__.py
+++ b/mediagoblin/plugins/oauth/__init__.py
@@ -35,22 +35,22 @@ def setup_plugin():
routes = [
('mediagoblin.plugins.oauth.authorize',
- '/oauth/authorize',
+ '/oauth-2/authorize',
'mediagoblin.plugins.oauth.views:authorize'),
('mediagoblin.plugins.oauth.authorize_client',
- '/oauth/client/authorize',
+ '/oauth-2/client/authorize',
'mediagoblin.plugins.oauth.views:authorize_client'),
('mediagoblin.plugins.oauth.access_token',
- '/oauth/access_token',
+ '/oauth-2/access_token',
'mediagoblin.plugins.oauth.views:access_token'),
('mediagoblin.plugins.oauth.list_connections',
- '/oauth/client/connections',
+ '/oauth-2/client/connections',
'mediagoblin.plugins.oauth.views:list_connections'),
('mediagoblin.plugins.oauth.register_client',
- '/oauth/client/register',
+ '/oauth-2/client/register',
'mediagoblin.plugins.oauth.views:register_client'),
('mediagoblin.plugins.oauth.list_clients',
- '/oauth/client/list',
+ '/oauth-2/client/list',
'mediagoblin.plugins.oauth.views:list_clients')]
pluginapi.register_routes(routes)
diff --git a/mediagoblin/plugins/oauth/tools.py b/mediagoblin/plugins/oauth/tools.py
index 1e0fc6ef..af0a3305 100644
--- a/mediagoblin/plugins/oauth/tools.py
+++ b/mediagoblin/plugins/oauth/tools.py
@@ -23,7 +23,7 @@ from datetime import datetime
from functools import wraps
-from mediagoblin.tools.json import json_response
+from mediagoblin.tools.response import json_response
def require_client_auth(controller):
diff --git a/mediagoblin/plugins/oauth/views.py b/mediagoblin/plugins/oauth/views.py
index a5d66111..de637d6b 100644
--- a/mediagoblin/plugins/oauth/views.py
+++ b/mediagoblin/plugins/oauth/views.py
@@ -21,8 +21,7 @@ from urllib import urlencode
from werkzeug.exceptions import BadRequest
-from mediagoblin.tools.response import render_to_response, redirect
-from mediagoblin.tools.json import json_response
+from mediagoblin.tools.response import render_to_response, redirect, json_response
from mediagoblin.decorators import require_active_login
from mediagoblin.messages import add_message, SUCCESS
from mediagoblin.tools.translate import pass_to_ugettext as _
diff --git a/mediagoblin/tests/test_http_callback.py b/mediagoblin/tests/test_http_callback.py
index a0511af7..64b7ee8f 100644
--- a/mediagoblin/tests/test_http_callback.py
+++ b/mediagoblin/tests/test_http_callback.py
@@ -23,7 +23,7 @@ from mediagoblin import mg_globals
from mediagoblin.tools import processing
from mediagoblin.tests.tools import fixture_add_user
from mediagoblin.tests.test_submission import GOOD_PNG
-from mediagoblin.tests import test_oauth as oauth
+from mediagoblin.tests import test_oauth2 as oauth
class TestHTTPCallback(object):
@@ -44,7 +44,7 @@ class TestHTTPCallback(object):
'password': self.user_password})
def get_access_token(self, client_id, client_secret, code):
- response = self.test_app.get('/oauth/access_token', {
+ response = self.test_app.get('/oauth-2/access_token', {
'code': code,
'client_id': client_id,
'client_secret': client_secret})
diff --git a/mediagoblin/tests/test_oauth.py b/mediagoblin/tests/test_oauth2.py
similarity index 95%
rename from mediagoblin/tests/test_oauth.py
rename to mediagoblin/tests/test_oauth2.py
index ea3bd798..86f9e8cc 100644
--- a/mediagoblin/tests/test_oauth.py
+++ b/mediagoblin/tests/test_oauth2.py
@@ -51,7 +51,7 @@ class TestOAuth(object):
def register_client(self, name, client_type, description=None,
redirect_uri=''):
return self.test_app.post(
- '/oauth/client/register', {
+ '/oauth-2/client/register', {
'name': name,
'description': description,
'type': client_type,
@@ -115,7 +115,7 @@ class TestOAuth(object):
client_identifier = client.identifier
redirect_uri = 'https://foo.example'
- response = self.test_app.get('/oauth/authorize', {
+ response = self.test_app.get('/oauth-2/authorize', {
'client_id': client.identifier,
'scope': 'all',
'redirect_uri': redirect_uri})
@@ -129,7 +129,7 @@ class TestOAuth(object):
# Short for client authorization post reponse
capr = self.test_app.post(
- '/oauth/client/authorize', {
+ '/oauth-2/client/authorize', {
'client_id': form.client_id.data,
'allow': 'Allow',
'next': form.next.data})
@@ -155,7 +155,7 @@ class TestOAuth(object):
client = self.db.OAuthClient.query.filter(
self.db.OAuthClient.identifier == unicode(client_id)).first()
- token_res = self.test_app.get('/oauth/access_token?client_id={0}&\
+ token_res = self.test_app.get('/oauth-2/access_token?client_id={0}&\
code={1}&client_secret={2}'.format(client_id, code, client.secret))
assert token_res.status_int == 200
@@ -183,7 +183,7 @@ code={1}&client_secret={2}'.format(client_id, code, client.secret))
client = self.db.OAuthClient.query.filter(
self.db.OAuthClient.identifier == unicode(client_id)).first()
- token_res = self.test_app.get('/oauth/access_token?\
+ token_res = self.test_app.get('/oauth-2/access_token?\
code={0}&client_secret={1}'.format(code, client.secret))
assert token_res.status_int == 200
@@ -204,7 +204,7 @@ code={0}&client_secret={1}'.format(code, client.secret))
client = self.db.OAuthClient.query.filter(
self.db.OAuthClient.identifier == client_id).first()
- token_res = self.test_app.get('/oauth/access_token',
+ token_res = self.test_app.get('/oauth-2/access_token',
{'refresh_token': token_data['refresh_token'],
'client_id': client_id,
'client_secret': client.secret
From 86ba41688332e3f71779f76c486889a7a099fa91 Mon Sep 17 00:00:00 2001
From: xray7224
Date: Tue, 16 Jul 2013 19:19:49 +0100
Subject: [PATCH 16/27] Adds some tests for the OAuth and some docs
---
docs/source/api/oauth.rst | 36 +++++++++
mediagoblin/federation/views.py | 19 ++---
mediagoblin/tests/test_oauth1.py | 122 +++++++++++++++++++++++++++++++
3 files changed, 168 insertions(+), 9 deletions(-)
create mode 100644 docs/source/api/oauth.rst
create mode 100644 mediagoblin/tests/test_oauth1.py
diff --git a/docs/source/api/oauth.rst b/docs/source/api/oauth.rst
new file mode 100644
index 00000000..003ad492
--- /dev/null
+++ b/docs/source/api/oauth.rst
@@ -0,0 +1,36 @@
+.. MediaGoblin Documentation
+
+ Written in 2011, 2012 by MediaGoblin contributors
+
+ To the extent possible under law, the author(s) have dedicated all
+ copyright and related and neighboring rights to this software to
+ the public domain worldwide. This software is distributed without
+ any warranty.
+
+ You should have received a copy of the CC0 Public Domain
+ Dedication along with this software. If not, see
+ .
+
+==============
+Authentication
+==============
+
+GNU MediaGoblin uses OAuth1 to authenticate requests to the API. There are many
+libraries out there for OAuth1, you're likely not going to have to do much. There
+is a library for the GNU MediaGoblin called `PyPump `_.
+We are not using OAuth2 as we want to stay completely compatable with GNU MediaGoblin.
+
+
+We use :doc:`client_register` to get the client ID and secret.
+
+Endpoints
+---------
+
+These are the endpoints you need to use for the oauth requests:
+
+`/oauth/request_token` is for getting the request token.
+
+`/oauth/authorize` is to send the user to to authorize your application.
+
+`/oauth/access_token` is for getting the access token to use in requests.
+
diff --git a/mediagoblin/federation/views.py b/mediagoblin/federation/views.py
index 94eb9886..7eb9f148 100644
--- a/mediagoblin/federation/views.py
+++ b/mediagoblin/federation/views.py
@@ -108,13 +108,14 @@ def client_register(request):
client_secret = random_string(43) # again, seems to be what pump uses
expirey = 0 # for now, lets not have it expire
expirey_db = None if expirey == 0 else expirey
-
+ application_type = data["application_type"]
+
# save it
client = Client(
id=client_id,
secret=client_secret,
expirey=expirey_db,
- application_type=data["application_type"],
+ application_type=application_type,
)
else:
@@ -133,7 +134,7 @@ def client_register(request):
client.application_name = data.get("application_name", None)
- contacts = data.get("contact", None)
+ contacts = data.get("contacts", None)
if contacts is not None:
if type(contacts) is not unicode:
error = "Contacts must be a string of space-seporated email addresses."
@@ -149,21 +150,21 @@ def client_register(request):
client.contacts = contacts
- request_uri = data.get("request_uris", None)
- if request_uri is not None:
- if type(request_uri) is not unicode:
+ redirect_uris = data.get("redirect_uris", None)
+ if redirect_uris is not None:
+ if type(redirect_uris) is not unicode:
error = "redirect_uris must be space-seporated URLs."
return json_respinse({"error": error}, status=400)
- request_uri = request_uri.split()
+ redirect_uris = redirect_uris.split()
- for uri in request_uri:
+ for uri in redirect_uris:
if not validate_url(uri):
# not a valid uri
error = "URI {0} is not a valid URI".format(uri)
return json_response({"error": error}, status=400)
- client.request_uri = request_uri
+ client.redirect_uri = redirect_uris
client.save()
diff --git a/mediagoblin/tests/test_oauth1.py b/mediagoblin/tests/test_oauth1.py
new file mode 100644
index 00000000..f3b44850
--- /dev/null
+++ b/mediagoblin/tests/test_oauth1.py
@@ -0,0 +1,122 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import json
+
+import pytest
+from urlparse import parse_qs, urlparse
+
+from mediagoblin import mg_globals
+from mediagoblin.tools import template, pluginapi
+from mediagoblin.tests.tools import fixture_add_user
+
+
+class TestOAuth(object):
+ @pytest.fixture(autouse=True)
+ def setup(self, test_app):
+ self.test_app = test_app
+
+ self.db = mg_globals.database
+
+ self.pman = pluginapi.PluginManager()
+
+ self.user_password = "AUserPassword123"
+ self.user = fixture_add_user("OAuthy", self.user_password)
+
+ self.login()
+
+ def login(self):
+ self.test_app.post(
+ "/auth/login/", {
+ "username": self.user.username,
+ "password": self.user_password})
+
+ def register_client(self, **kwargs):
+ """ Regiters a client with the API """
+
+ kwargs["type"] = "client_associate"
+ kwargs["application_type"] = kwargs.get("application_type", "native")
+ return self.test_app.post("/api/client/register", kwargs)
+
+ def test_client_client_register_limited_info(self):
+ """ Tests that a client can be registered with limited information """
+ response = self.register_client()
+ client_info = json.loads(response.body)
+
+ client = self.db.Client.query.filter_by(id=client_info["client_id"]).first()
+
+ assert response.status_int == 200
+ assert client is not None
+
+ def test_client_register_full_info(self):
+ """ Provides every piece of information possible to register client """
+ query = {
+ "application_name": "Testificate MD",
+ "application_type": "web",
+ "contacts": "someone@someplace.com tuteo@tsengeo.lu",
+ "logo_url": "http://ayrel.com/utral.png",
+ "redirect_uris": "http://navi-kosman.lu http://gmg-yawne-oeru.lu",
+ }
+
+ response = self.register_client(**query)
+ client_info = json.loads(response.body)
+
+ client = self.db.Client.query.filter_by(id=client_info["client_id"]).first()
+
+ assert client is not None
+ assert client.secret == client_info["client_secret"]
+ assert client.application_type == query["application_type"]
+ assert client.redirect_uri == query["redirect_uris"].split()
+ assert client.logo_url == query["logo_url"]
+ assert client.contacts == query["contacts"].split()
+
+
+ def test_client_update(self):
+ """ Tests that you can update a client """
+ # first we need to register a client
+ response = self.register_client()
+
+ client_info = json.loads(response.body)
+ client = self.db.Client.query.filter_by(id=client_info["client_id"]).first()
+
+ # Now update
+ update_query = {
+ "type": "client_update",
+ "application_name": "neytiri",
+ "contacts": "someone@someplace.com abc@cba.com",
+ "logo_url": "http://place.com/picture.png",
+ "application_type": "web",
+ "redirect_uris": "http://blah.gmg/whatever https://inboxen.org/",
+ }
+
+ update_response = self.register_client(**update_query)
+
+ assert update_response.status_int == 200
+ client_info = json.loads(update_response.body)
+ client = self.Client.query.filter_by(id=client_info["client_id"]).first()
+
+ assert client.secret == client_info["client_secret"]
+ assert client.application_type == update_query["application_type"]
+ assert client.application_name == update_query["application_name"]
+ assert client.contacts == update_query["contacts"].split()
+ assert client.logo_url == update_query["logo_url"]
+ assert client.redirect_uri == update_query["redirect_uris"].split()
+
+ def request_token(self):
+ """ Test a request for a request token """
+ response = self.register_client()
+
+
From 89d5b44e0aee5845f816a89a9f8b3364940daea3 Mon Sep 17 00:00:00 2001
From: xray7224
Date: Thu, 18 Jul 2013 19:15:05 +0100
Subject: [PATCH 17/27] Adds test for request_tokens
---
mediagoblin/federation/oauth.py | 6 ++-
mediagoblin/federation/tools/request.py | 19 +++++---
mediagoblin/federation/views.py | 8 ++--
mediagoblin/tests/test_oauth1.py | 58 ++++++++++++++++++++++---
4 files changed, 71 insertions(+), 20 deletions(-)
diff --git a/mediagoblin/federation/oauth.py b/mediagoblin/federation/oauth.py
index ea0fea2c..764b8535 100644
--- a/mediagoblin/federation/oauth.py
+++ b/mediagoblin/federation/oauth.py
@@ -26,8 +26,9 @@ class GMGRequestValidator(RequestValidator):
enforce_ssl = False
- def __init__(self, data=None):
+ def __init__(self, data=None, *args, **kwargs):
self.POST = data
+ super(GMGRequestValidator, self).__init__(*args, **kwargs)
def save_request_token(self, token, request):
""" Saves request token in db """
@@ -38,7 +39,8 @@ class GMGRequestValidator(RequestValidator):
secret=token["oauth_token_secret"],
)
request_token.client = client_id
- request_token.callback = token.get("oauth_callback", None)
+ if u"oauth_callback" in self.POST:
+ request_token.callback = self.POST[u"oauth_callback"]
request_token.save()
def save_verifier(self, token, verifier, request):
diff --git a/mediagoblin/federation/tools/request.py b/mediagoblin/federation/tools/request.py
index 4f5be277..6e484bb6 100644
--- a/mediagoblin/federation/tools/request.py
+++ b/mediagoblin/federation/tools/request.py
@@ -14,14 +14,19 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import re
-
-# Regex for parsing Authorization string
-auth_header_re = re.compile('(\w+)[:=] ?"?(\w+)"?')
-
def decode_authorization_header(header):
""" Decodes a HTTP Authorization Header to python dictionary """
- authorization = header.get("Authorization", "")
- tokens = dict(auth_header_re.findall(authorization))
+ authorization = header.get("Authorization", "").lstrip(" ").lstrip("OAuth")
+ tokens = {}
+
+ for param in authorization.split(","):
+ key, value = param.split("=")
+
+ key = key.lstrip(" ")
+ value = value.lstrip(" ").lstrip('"')
+ value = value.rstrip(" ").rstrip('"')
+
+ tokens[key] = value
+
return tokens
diff --git a/mediagoblin/federation/views.py b/mediagoblin/federation/views.py
index 7eb9f148..633a19d4 100644
--- a/mediagoblin/federation/views.py
+++ b/mediagoblin/federation/views.py
@@ -198,7 +198,6 @@ def request_token(request):
authorization = decode_authorization_header(data)
-
if authorization == dict() or u"oauth_consumer_key" not in authorization:
error = "Missing required parameter."
return json_response({"error": error}, status=400)
@@ -206,12 +205,13 @@ def request_token(request):
# check the client_id
client_id = authorization[u"oauth_consumer_key"]
client = Client.query.filter_by(id=client_id).first()
- if client is None:
+
+ if client == None:
# client_id is invalid
error = "Invalid client_id"
return json_response({"error": error}, status=400)
- # make request token and return to client
+ # make request token and return to client
request_validator = GMGRequestValidator(authorization)
rv = RequestTokenEndpoint(request_validator)
tokens = rv.create_request_token(request, authorization)
@@ -219,7 +219,7 @@ def request_token(request):
# store the nonce & timestamp before we return back
nonce = authorization[u"oauth_nonce"]
timestamp = authorization[u"oauth_timestamp"]
- timestamp = datetime.datetime.fromtimestamp(int(timestamp))
+ timestamp = datetime.datetime.fromtimestamp(float(timestamp))
nc = NonceTimestamp(nonce=nonce, timestamp=timestamp)
nc.save()
diff --git a/mediagoblin/tests/test_oauth1.py b/mediagoblin/tests/test_oauth1.py
index f3b44850..073c2884 100644
--- a/mediagoblin/tests/test_oauth1.py
+++ b/mediagoblin/tests/test_oauth1.py
@@ -14,17 +14,23 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import json
+import cgi
import pytest
from urlparse import parse_qs, urlparse
+from oauthlib.oauth1 import Client
+
from mediagoblin import mg_globals
from mediagoblin.tools import template, pluginapi
from mediagoblin.tests.tools import fixture_add_user
class TestOAuth(object):
+
+ MIME_FORM = "application/x-www-form-urlencoded"
+ MIME_JSON = "application/json"
+
@pytest.fixture(autouse=True)
def setup(self, test_app):
self.test_app = test_app
@@ -54,7 +60,7 @@ class TestOAuth(object):
def test_client_client_register_limited_info(self):
""" Tests that a client can be registered with limited information """
response = self.register_client()
- client_info = json.loads(response.body)
+ client_info = response.json
client = self.db.Client.query.filter_by(id=client_info["client_id"]).first()
@@ -72,7 +78,7 @@ class TestOAuth(object):
}
response = self.register_client(**query)
- client_info = json.loads(response.body)
+ client_info = response.json
client = self.db.Client.query.filter_by(id=client_info["client_id"]).first()
@@ -89,7 +95,7 @@ class TestOAuth(object):
# first we need to register a client
response = self.register_client()
- client_info = json.loads(response.body)
+ client_info = response.json
client = self.db.Client.query.filter_by(id=client_info["client_id"]).first()
# Now update
@@ -105,8 +111,8 @@ class TestOAuth(object):
update_response = self.register_client(**update_query)
assert update_response.status_int == 200
- client_info = json.loads(update_response.body)
- client = self.Client.query.filter_by(id=client_info["client_id"]).first()
+ client_info = update_response.json
+ client = self.db.Client.query.filter_by(id=client_info["client_id"]).first()
assert client.secret == client_info["client_secret"]
assert client.application_type == update_query["application_type"]
@@ -115,8 +121,46 @@ class TestOAuth(object):
assert client.logo_url == update_query["logo_url"]
assert client.redirect_uri == update_query["redirect_uris"].split()
- def request_token(self):
+ def to_authorize_headers(self, data):
+ headers = ""
+ for key, value in data.items():
+ headers += '{0}="{1}",'.format(key, value)
+ return {"Authorization": "OAuth " + headers[:-1]}
+
+ def test_request_token(self):
""" Test a request for a request token """
response = self.register_client()
+ client_id = response.json["client_id"]
+
+ endpoint = "/oauth/request_token"
+ request_query = {
+ "oauth_consumer_key": client_id,
+ "oauth_nonce": "abcdefghij",
+ "oauth_timestamp": 123456789.0,
+ "oauth_callback": "https://some.url/callback",
+ }
+
+ headers = self.to_authorize_headers(request_query)
+
+ headers["Content-Type"] = self.MIME_FORM
+
+ response = self.test_app.post(endpoint, headers=headers)
+ response = cgi.parse_qs(response.body)
+
+ # each element is a list, reduce it to a string
+ for key, value in response.items():
+ response[key] = value[0]
+
+ request_token = self.db.RequestToken.query.filter_by(
+ token=response["oauth_token"]
+ ).first()
+
+ client = self.db.Client.query.filter_by(id=client_id).first()
+
+ assert request_token is not None
+ assert request_token.secret == response["oauth_token_secret"]
+ assert request_token.client == client.id
+ assert request_token.used == False
+ assert request_token.callback == request_query["oauth_callback"]
From 8ddd7769de7a90f71d8dd3e0cc2c491e51d76d47 Mon Sep 17 00:00:00 2001
From: xray7224
Date: Thu, 18 Jul 2013 20:21:35 +0100
Subject: [PATCH 18/27] Adds migration for OAuth1 tables
---
mediagoblin/db/migrations.py | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/migrations.py
index fe4ffb3e..4673e0ce 100644
--- a/mediagoblin/db/migrations.py
+++ b/mediagoblin/db/migrations.py
@@ -26,7 +26,9 @@ from sqlalchemy.sql import and_
from migrate.changeset.constraint import UniqueConstraint
from mediagoblin.db.migration_tools import RegisterMigration, inspect_table
-from mediagoblin.db.models import MediaEntry, Collection, User, MediaComment
+from mediagoblin.db.models import (MediaEntry, Collection, User, MediaComment,
+ Client, RequestToken, AccessToken,
+ NonceTimestamp)
MIGRATIONS = {}
@@ -379,3 +381,15 @@ def pw_hash_nullable(db):
constraint.create()
db.commit()
+
+
+@RegisterMigration(14, MIGRATIONS)
+def create_oauth1_tables(db):
+ """ Creates the OAuth1 tables """
+
+ Client.__table__.create(db.bind)
+ RequestToken.__table__.create(db.bind)
+ AccessToken.__table__.create(db.bind)
+ NonceTimestamp.__table__.create(db.bind)
+
+ db.commit()
From 7271b062821ab012a774e813e61a35401f3ed7d7 Mon Sep 17 00:00:00 2001
From: xray7224
Date: Thu, 18 Jul 2013 20:39:15 +0100
Subject: [PATCH 19/27] Moves first versions of the the models to migrations
---
mediagoblin/db/migrations.py | 79 ++++++++++++++++++++++++++++++++----
1 file changed, 72 insertions(+), 7 deletions(-)
diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/migrations.py
index 4673e0ce..015dbff0 100644
--- a/mediagoblin/db/migrations.py
+++ b/mediagoblin/db/migrations.py
@@ -26,9 +26,7 @@ from sqlalchemy.sql import and_
from migrate.changeset.constraint import UniqueConstraint
from mediagoblin.db.migration_tools import RegisterMigration, inspect_table
-from mediagoblin.db.models import (MediaEntry, Collection, User, MediaComment,
- Client, RequestToken, AccessToken,
- NonceTimestamp)
+from mediagoblin.db.models import MediaEntry, Collection, User, MediaComment
MIGRATIONS = {}
@@ -383,13 +381,80 @@ def pw_hash_nullable(db):
db.commit()
+# oauth1 migrations
+class Client_v0(Base):
+ """
+ Model representing a client - Used for API Auth
+ """
+ __tablename__ = "core__clients"
+
+ id = Column(Unicode, nullable=True, primary_key=True)
+ secret = Column(Unicode, nullable=False)
+ expirey = Column(DateTime, nullable=True)
+ application_type = Column(Unicode, nullable=False)
+ created = Column(DateTime, nullable=False, default=datetime.datetime.now)
+ updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
+
+ # optional stuff
+ redirect_uri = Column(JSONEncoded, nullable=True)
+ logo_url = Column(Unicode, nullable=True)
+ application_name = Column(Unicode, nullable=True)
+ contacts = Column(JSONEncoded, nullable=True)
+
+ def __repr__(self):
+ if self.application_name:
+ return "".format(self.application_name, self.id)
+ else:
+ return "".format(self.id)
+
+class RequestToken_v0(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=False, default=u"oob")
+ created = Column(DateTime, nullable=False, default=datetime.datetime.now)
+ updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
+
+class AccessToken_v0(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 NonceTimestamp_v0(Base):
+ """
+ A place the timestamp and nonce can be stored - this is for OAuth1
+ """
+ __tablename__ = "core__nonce_timestamps"
+
+ nonce = Column(Unicode, nullable=False, primary_key=True)
+ timestamp = Column(DateTime, nullable=False, primary_key=True)
+
+
@RegisterMigration(14, MIGRATIONS)
def create_oauth1_tables(db):
""" Creates the OAuth1 tables """
- Client.__table__.create(db.bind)
- RequestToken.__table__.create(db.bind)
- AccessToken.__table__.create(db.bind)
- NonceTimestamp.__table__.create(db.bind)
+ Client_v0.__table__.create(db.bind)
+ RequestToken_v0.__table__.create(db.bind)
+ AccessToken_v0.__table__.create(db.bind)
+ NonceTimestamp_v0.__table__.create(db.bind)
db.commit()
From 617bff18301e5b51612ae9fca4022593b6ec9413 Mon Sep 17 00:00:00 2001
From: Jessica Tallon
Date: Sat, 20 Jul 2013 19:08:02 +0100
Subject: [PATCH 20/27] Fixes some typo's and removes unused imports
---
mediagoblin/federation/views.py | 11 +++++------
1 file changed, 5 insertions(+), 6 deletions(-)
diff --git a/mediagoblin/federation/views.py b/mediagoblin/federation/views.py
index 633a19d4..8c26799f 100644
--- a/mediagoblin/federation/views.py
+++ b/mediagoblin/federation/views.py
@@ -16,10 +16,10 @@
import datetime
-from oauthlib.oauth1 import (AuthorizationEndpoint, RequestValidator,
- RequestTokenEndpoint, AccessTokenEndpoint)
+from oauthlib.oauth1 import (RequestTokenEndpoint, AuthorizationEndpoint,
+ AccessTokenEndpoint)
-from mediagoblin.decorators import require_active_login, oauth_required
+from mediagoblin.decorators import require_active_login
from mediagoblin.tools.translate import pass_to_ugettext
from mediagoblin.meddleware.csrf import csrf_exempt
from mediagoblin.tools.request import decode_request
@@ -29,10 +29,9 @@ from mediagoblin.tools.response import (render_to_response, redirect,
from mediagoblin.tools.crypto import random_string
from mediagoblin.tools.validator import validate_email, validate_url
from mediagoblin.federation.forms import AuthorizeForm
-from mediagoblin.federation.exceptions import ValidationException
from mediagoblin.federation.oauth import GMGRequestValidator, GMGRequest
from mediagoblin.federation.tools.request import decode_authorization_header
-from mediagoblin.db.models import NonceTimestamp, Client, RequestToken, AccessToken
+from mediagoblin.db.models import NonceTimestamp, Client, RequestToken
# possible client types
client_types = ["web", "native"] # currently what pump supports
@@ -154,7 +153,7 @@ def client_register(request):
if redirect_uris is not None:
if type(redirect_uris) is not unicode:
error = "redirect_uris must be space-seporated URLs."
- return json_respinse({"error": error}, status=400)
+ return json_response({"error": error}, status=400)
redirect_uris = redirect_uris.split()
From 8e3bf97821b7057920286aca16c649e48f3275a1 Mon Sep 17 00:00:00 2001
From: Jessica Tallon
Date: Mon, 22 Jul 2013 17:17:01 +0100
Subject: [PATCH 21/27] Fix problem with migration - OAuth
---
mediagoblin/db/migrations.py | 14 ++++++++------
mediagoblin/federation/forms.py | 1 -
mediagoblin/federation/oauth.py | 4 +---
3 files changed, 9 insertions(+), 10 deletions(-)
diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/migrations.py
index 015dbff0..374ab4c8 100644
--- a/mediagoblin/db/migrations.py
+++ b/mediagoblin/db/migrations.py
@@ -25,6 +25,8 @@ from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql import and_
from migrate.changeset.constraint import UniqueConstraint
+
+from mediagoblin.db.extratypes import JSONEncoded
from mediagoblin.db.migration_tools import RegisterMigration, inspect_table
from mediagoblin.db.models import MediaEntry, Collection, User, MediaComment
@@ -382,7 +384,7 @@ def pw_hash_nullable(db):
# oauth1 migrations
-class Client_v0(Base):
+class Client_v0(declarative_base()):
"""
Model representing a client - Used for API Auth
"""
@@ -407,7 +409,7 @@ class Client_v0(Base):
else:
return "".format(self.id)
-class RequestToken_v0(Base):
+class RequestToken_v0(declarative_base()):
"""
Model for representing the request tokens
"""
@@ -415,7 +417,7 @@ class RequestToken_v0(Base):
token = Column(Unicode, primary_key=True)
secret = Column(Unicode, nullable=False)
- client = Column(Unicode, ForeignKey(Client.id))
+ client = Column(Unicode, ForeignKey(Client_v0.id))
user = Column(Integer, ForeignKey(User.id), nullable=True)
used = Column(Boolean, default=False)
authenticated = Column(Boolean, default=False)
@@ -424,7 +426,7 @@ class RequestToken_v0(Base):
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
-class AccessToken_v0(Base):
+class AccessToken_v0(declarative_base()):
"""
Model for representing the access tokens
"""
@@ -433,12 +435,12 @@ class AccessToken_v0(Base):
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))
+ request_token = Column(Unicode, ForeignKey(RequestToken_v0.token))
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
-class NonceTimestamp_v0(Base):
+class NonceTimestamp_v0(declarative_base()):
"""
A place the timestamp and nonce can be stored - this is for OAuth1
"""
diff --git a/mediagoblin/federation/forms.py b/mediagoblin/federation/forms.py
index 39d6fc27..94c7cb52 100644
--- a/mediagoblin/federation/forms.py
+++ b/mediagoblin/federation/forms.py
@@ -1,5 +1,4 @@
import wtforms
-from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
class AuthorizeForm(wtforms.Form):
""" Form used to authorize the request token """
diff --git a/mediagoblin/federation/oauth.py b/mediagoblin/federation/oauth.py
index 764b8535..8229c47d 100644
--- a/mediagoblin/federation/oauth.py
+++ b/mediagoblin/federation/oauth.py
@@ -15,8 +15,7 @@
# along with this program. If not, see .
from oauthlib.common import Request
-from oauthlib.oauth1 import (AuthorizationEndpoint, RequestValidator,
- RequestTokenEndpoint, AccessTokenEndpoint)
+from oauthlib.oauth1 import RequestValidator
from mediagoblin.db.models import NonceTimestamp, Client, RequestToken, AccessToken
@@ -110,7 +109,6 @@ class GMGRequestValidator(RequestValidator):
return client.secret
def get_access_token_secret(self, client_key, token, request):
- client = Client.query.filter_by(id=client_key).first()
access_token = AccessToken.query.filter_by(token=token).first()
return access_token.secret
From 657263abdf9b47f1598a7633c1e0039d0eb7b043 Mon Sep 17 00:00:00 2001
From: xray7224
Date: Mon, 22 Jul 2013 16:56:22 +0100
Subject: [PATCH 22/27] Refactor WTFormData
---
mediagoblin/federation/tools/forms.py | 25 +++++++++++++++++++++++++
mediagoblin/federation/views.py | 11 +----------
2 files changed, 26 insertions(+), 10 deletions(-)
create mode 100644 mediagoblin/federation/tools/forms.py
diff --git a/mediagoblin/federation/tools/forms.py b/mediagoblin/federation/tools/forms.py
new file mode 100644
index 00000000..e3eb3298
--- /dev/null
+++ b/mediagoblin/federation/tools/forms.py
@@ -0,0 +1,25 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+class WTFormData(dict):
+ """
+ Provides a WTForm usable dictionary
+ """
+ def getlist(self, key):
+ v = self[key]
+ if not isinstance(v, (list, tuple)):
+ v = [v]
+ return v
diff --git a/mediagoblin/federation/views.py b/mediagoblin/federation/views.py
index 8c26799f..5bb93a68 100644
--- a/mediagoblin/federation/views.py
+++ b/mediagoblin/federation/views.py
@@ -31,6 +31,7 @@ from mediagoblin.tools.validator import validate_email, validate_url
from mediagoblin.federation.forms import AuthorizeForm
from mediagoblin.federation.oauth import GMGRequestValidator, GMGRequest
from mediagoblin.federation.tools.request import decode_authorization_header
+from mediagoblin.federation.tools.forms import WTFormData
from mediagoblin.db.models import NonceTimestamp, Client, RequestToken
# possible client types
@@ -225,16 +226,6 @@ def request_token(request):
return form_response(tokens)
-class WTFormData(dict):
- """
- Provides a WTForm usable dictionary
- """
- def getlist(self, key):
- v = self[key]
- if not isinstance(v, (list, tuple)):
- v = [v]
- return v
-
@require_active_login
def authorize(request):
""" Displays a page for user to authorize """
From 005181b1663a05e55faa56a474cf6f5d81a255c9 Mon Sep 17 00:00:00 2001
From: xray7224
Date: Mon, 22 Jul 2013 17:06:00 +0100
Subject: [PATCH 23/27] Renames OAuth1 code to federation
---
mediagoblin/{federation => oauth}/__init__.py | 0
mediagoblin/{federation => oauth}/exceptions.py | 0
mediagoblin/{federation => oauth}/forms.py | 0
mediagoblin/{federation => oauth}/oauth.py | 0
mediagoblin/{federation => oauth}/routing.py | 16 ++++++++--------
.../{federation => oauth}/tools/__init__.py | 0
mediagoblin/{federation => oauth}/tools/forms.py | 0
.../{federation => oauth}/tools/request.py | 0
mediagoblin/{federation => oauth}/views.py | 10 +++++-----
9 files changed, 13 insertions(+), 13 deletions(-)
rename mediagoblin/{federation => oauth}/__init__.py (100%)
rename mediagoblin/{federation => oauth}/exceptions.py (100%)
rename mediagoblin/{federation => oauth}/forms.py (100%)
rename mediagoblin/{federation => oauth}/oauth.py (100%)
rename mediagoblin/{federation => oauth}/routing.py (75%)
rename mediagoblin/{federation => oauth}/tools/__init__.py (100%)
rename mediagoblin/{federation => oauth}/tools/forms.py (100%)
rename mediagoblin/{federation => oauth}/tools/request.py (100%)
rename mediagoblin/{federation => oauth}/views.py (97%)
diff --git a/mediagoblin/federation/__init__.py b/mediagoblin/oauth/__init__.py
similarity index 100%
rename from mediagoblin/federation/__init__.py
rename to mediagoblin/oauth/__init__.py
diff --git a/mediagoblin/federation/exceptions.py b/mediagoblin/oauth/exceptions.py
similarity index 100%
rename from mediagoblin/federation/exceptions.py
rename to mediagoblin/oauth/exceptions.py
diff --git a/mediagoblin/federation/forms.py b/mediagoblin/oauth/forms.py
similarity index 100%
rename from mediagoblin/federation/forms.py
rename to mediagoblin/oauth/forms.py
diff --git a/mediagoblin/federation/oauth.py b/mediagoblin/oauth/oauth.py
similarity index 100%
rename from mediagoblin/federation/oauth.py
rename to mediagoblin/oauth/oauth.py
diff --git a/mediagoblin/federation/routing.py b/mediagoblin/oauth/routing.py
similarity index 75%
rename from mediagoblin/federation/routing.py
rename to mediagoblin/oauth/routing.py
index bc3a7a7e..e45077bb 100644
--- a/mediagoblin/federation/routing.py
+++ b/mediagoblin/oauth/routing.py
@@ -18,26 +18,26 @@ from mediagoblin.tools.routing import add_route
# client registration & oauth
add_route(
- "mediagoblin.federation",
+ "mediagoblin.oauth",
"/api/client/register",
- "mediagoblin.federation.views:client_register"
+ "mediagoblin.oauth.views:client_register"
)
add_route(
- "mediagoblin.federation",
+ "mediagoblin.oauth",
"/oauth/request_token",
- "mediagoblin.federation.views:request_token"
+ "mediagoblin.oauth.views:request_token"
)
add_route(
- "mediagoblin.federation",
+ "mediagoblin.oauth",
"/oauth/authorize",
- "mediagoblin.federation.views:authorize",
+ "mediagoblin.oauth.views:authorize",
)
add_route(
- "mediagoblin.federation",
+ "mediagoblin.oauth",
"/oauth/access_token",
- "mediagoblin.federation.views:access_token"
+ "mediagoblin.oauth.views:access_token"
)
diff --git a/mediagoblin/federation/tools/__init__.py b/mediagoblin/oauth/tools/__init__.py
similarity index 100%
rename from mediagoblin/federation/tools/__init__.py
rename to mediagoblin/oauth/tools/__init__.py
diff --git a/mediagoblin/federation/tools/forms.py b/mediagoblin/oauth/tools/forms.py
similarity index 100%
rename from mediagoblin/federation/tools/forms.py
rename to mediagoblin/oauth/tools/forms.py
diff --git a/mediagoblin/federation/tools/request.py b/mediagoblin/oauth/tools/request.py
similarity index 100%
rename from mediagoblin/federation/tools/request.py
rename to mediagoblin/oauth/tools/request.py
diff --git a/mediagoblin/federation/views.py b/mediagoblin/oauth/views.py
similarity index 97%
rename from mediagoblin/federation/views.py
rename to mediagoblin/oauth/views.py
index 5bb93a68..116eb023 100644
--- a/mediagoblin/federation/views.py
+++ b/mediagoblin/oauth/views.py
@@ -18,7 +18,7 @@ import datetime
from oauthlib.oauth1 import (RequestTokenEndpoint, AuthorizationEndpoint,
AccessTokenEndpoint)
-
+
from mediagoblin.decorators import require_active_login
from mediagoblin.tools.translate import pass_to_ugettext
from mediagoblin.meddleware.csrf import csrf_exempt
@@ -28,10 +28,10 @@ from mediagoblin.tools.response import (render_to_response, redirect,
form_response)
from mediagoblin.tools.crypto import random_string
from mediagoblin.tools.validator import validate_email, validate_url
-from mediagoblin.federation.forms import AuthorizeForm
-from mediagoblin.federation.oauth import GMGRequestValidator, GMGRequest
-from mediagoblin.federation.tools.request import decode_authorization_header
-from mediagoblin.federation.tools.forms import WTFormData
+from mediagoblin.oauth.forms import AuthorizeForm
+from mediagoblin.oauth.oauth import GMGRequestValidator, GMGRequest
+from mediagoblin.oauth.tools.request import decode_authorization_header
+from mediagoblin.oauth.tools.forms import WTFormData
from mediagoblin.db.models import NonceTimestamp, Client, RequestToken
# possible client types
From 0ec89cb29fbd4b1b31534e5bc66c914c381837c5 Mon Sep 17 00:00:00 2001
From: xray7224
Date: Mon, 29 Jul 2013 17:25:10 +0100
Subject: [PATCH 24/27] Fixes problem with headers pointing to old federation
dir
---
mediagoblin/decorators.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/mediagoblin/decorators.py b/mediagoblin/decorators.py
index 302ab247..685d0d98 100644
--- a/mediagoblin/decorators.py
+++ b/mediagoblin/decorators.py
@@ -26,8 +26,8 @@ from mediagoblin.db.models import MediaEntry, User
from mediagoblin.tools.response import json_response, redirect, render_404
from mediagoblin.tools.translate import pass_to_ugettext as _
-from mediagoblin.federation.tools.request import decode_authorization_header
-from mediagoblin.federation.oauth import GMGRequestValidator
+from mediagoblin.oauth.tools.request import decode_authorization_header
+from mediagoblin.oauth.oauth import GMGRequestValidator
def require_active_login(controller):
"""
From 4554d6e0140fab80a3b6e0d3ac5fc015d769e152 Mon Sep 17 00:00:00 2001
From: xray7224
Date: Mon, 29 Jul 2013 17:28:50 +0100
Subject: [PATCH 25/27] Fix problem with routing to oauth
---
mediagoblin/routing.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/mediagoblin/routing.py b/mediagoblin/routing.py
index 3a54aaa0..c2b2304d 100644
--- a/mediagoblin/routing.py
+++ b/mediagoblin/routing.py
@@ -36,6 +36,7 @@ def get_url_map():
import mediagoblin.webfinger.routing
import mediagoblin.listings.routing
import mediagoblin.notifications.routing
+ import mediagoblin.oauth.routing
import mediagoblin.federation.routing
for route in PluginManager().get_routes():
From cae55705b154c9c78380678ca340a124220c2774 Mon Sep 17 00:00:00 2001
From: xray7224
Date: Mon, 29 Jul 2013 17:48:53 +0100
Subject: [PATCH 26/27] Fix problem causing exception when invalid
Authentication header provided
---
mediagoblin/oauth/tools/request.py | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/mediagoblin/oauth/tools/request.py b/mediagoblin/oauth/tools/request.py
index 6e484bb6..5ce2da77 100644
--- a/mediagoblin/oauth/tools/request.py
+++ b/mediagoblin/oauth/tools/request.py
@@ -20,8 +20,11 @@ def decode_authorization_header(header):
tokens = {}
for param in authorization.split(","):
- key, value = param.split("=")
-
+ try:
+ key, value = param.split("=")
+ except ValueError:
+ continue
+
key = key.lstrip(" ")
value = value.lstrip(" ").lstrip('"')
value = value.rstrip(" ").rstrip('"')
From 89909bd6921efb4ec3296c908c5d57eba35ebd21 Mon Sep 17 00:00:00 2001
From: Jessica Tallon
Date: Tue, 13 Aug 2013 10:45:09 +0100
Subject: [PATCH 27/27] Fix import errors when running tests
---
mediagoblin/routing.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/mediagoblin/routing.py b/mediagoblin/routing.py
index c2b2304d..5961f33b 100644
--- a/mediagoblin/routing.py
+++ b/mediagoblin/routing.py
@@ -37,8 +37,7 @@ def get_url_map():
import mediagoblin.listings.routing
import mediagoblin.notifications.routing
import mediagoblin.oauth.routing
- import mediagoblin.federation.routing
-
+
for route in PluginManager().get_routes():
add_route(*route)