From a062149e90731cfd730d8a539a32354065a8c9e8 Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Thu, 13 Sep 2012 20:59:00 +0200 Subject: Created API plugin, moved api_auth to the API plugin --- mediagoblin/plugins/api/tools.py | 53 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 mediagoblin/plugins/api/tools.py (limited to 'mediagoblin/plugins/api/tools.py') diff --git a/mediagoblin/plugins/api/tools.py b/mediagoblin/plugins/api/tools.py new file mode 100644 index 00000000..a8873b06 --- /dev/null +++ b/mediagoblin/plugins/api/tools.py @@ -0,0 +1,53 @@ +# 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 logging + +from functools import wraps +from webob import exc + +from mediagoblin.tools.pluginapi import PluginManager + +_log = logging.getLogger(__name__) + + +def api_auth(controller): + @wraps(controller) + def wrapper(request, *args, **kw): + auth_candidates = [] + + for auth in PluginManager().get_hook_callables('auth'): + _log.debug('Plugin auth: {0}'.format(auth)) + if auth.trigger(request): + auth_candidates.append(auth) + + # If we can't find any authentication methods, we should not let them + # pass. + if not auth_candidates: + return exc.HTTPForbidden() + + # For now, just select the first one in the list + auth = auth_candidates[0] + + _log.debug('Using {0} to authorize request {1}'.format( + auth, request.url)) + + if not auth(request, *args, **kw): + return exc.HTTPForbidden() + + return controller(request, *args, **kw) + + return wrapper -- cgit v1.2.3 From 42c837523e5ac70a03fb310dbb15bec03d4108cd Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Sat, 15 Sep 2012 15:25:26 +0200 Subject: Added /api/entries view --- mediagoblin/plugins/api/tools.py | 61 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) (limited to 'mediagoblin/plugins/api/tools.py') diff --git a/mediagoblin/plugins/api/tools.py b/mediagoblin/plugins/api/tools.py index a8873b06..4d306274 100644 --- a/mediagoblin/plugins/api/tools.py +++ b/mediagoblin/plugins/api/tools.py @@ -15,15 +15,74 @@ # along with this program. If not, see . import logging +import json from functools import wraps -from webob import exc +from webob import exc, Response +from mediagoblin import mg_globals from mediagoblin.tools.pluginapi import PluginManager +from mediagoblin.storage.filestorage import BasicFileStorage _log = logging.getLogger(__name__) +class Auth(object): + ''' + An object with two significant methods, 'trigger' and 'run'. + + Using a similar object to this, plugins can register specific + authentication logic, for example the GET param 'access_token' for OAuth. + + - trigger: Analyze the 'request' argument, return True if you think you + can handle the request, otherwise return False + - run: The authentication logic, set the request.user object to the user + you intend to authenticate and return True, otherwise return False. + + If run() returns False, an HTTP 403 Forbidden error will be shown. + + You may also display custom errors, just raise them within the run() + method. + ''' + def trigger(self, request): + raise NotImplemented() + + def __call__(self, request, *args, **kw): + raise NotImplemented() + + +def json_response(serializable): + response = Response(json.dumps(serializable)) + response.headers['Content-Type'] = 'application/json' + return response + + +def get_entry_serializable(entry): + return { + 'user': entry.get_uploader.username, + 'user_id': entry.get_uploader.id, + 'id': entry.id, + 'created': entry.created.isoformat(), + 'title': entry.title, + 'license': entry.license, + 'description': entry.description, + 'description_html': entry.description_html, + 'media_type': entry.media_type, + 'media_files': get_media_file_paths(entry.media_files)} + + +def get_media_file_paths(media_files): + if isinstance(mg_globals.public_store, BasicFileStorage): + pass # TODO + + media_urls = {} + + for key, val in media_files.items(): + media_urls[key] = mg_globals.public_store.file_url(val) + + return media_urls + + def api_auth(controller): @wraps(controller) def wrapper(request, *args, **kw): -- cgit v1.2.3 From 85726f7360e2a7dbc74764c26501ac633bc10049 Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Sat, 15 Sep 2012 15:54:22 +0200 Subject: Added fields to /api/entries, wrote docstrings for api.tools --- mediagoblin/plugins/api/tools.py | 44 +++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) (limited to 'mediagoblin/plugins/api/tools.py') diff --git a/mediagoblin/plugins/api/tools.py b/mediagoblin/plugins/api/tools.py index 4d306274..e3b15b23 100644 --- a/mediagoblin/plugins/api/tools.py +++ b/mediagoblin/plugins/api/tools.py @@ -51,16 +51,37 @@ class Auth(object): raise NotImplemented() -def json_response(serializable): - response = Response(json.dumps(serializable)) +def json_response(serializable, *args, **kw): + ''' + Serializes a json objects and returns a webob.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 + webob.Response.__init__ method. + ''' + response = Response(json.dumps(serializable), *args, **kw) response.headers['Content-Type'] = 'application/json' return response -def get_entry_serializable(entry): +def get_entry_serializable(entry, urlgen): + ''' + Returns a serializable dict() of a MediaEntry instance. + + :param entry: A MediaEntry instance + :param urlgen: An urlgen instance, can be found on the request object passed + to views. + ''' return { 'user': entry.get_uploader.username, 'user_id': entry.get_uploader.id, + 'user_bio': entry.get_uploader.bio, + 'user_bio_html': entry.get_uploader.bio_html, + 'user_permalink': urlgen('mediagoblin.user_pages.user_home', + user=entry.get_uploader.username, + qualified=True), 'id': entry.id, 'created': entry.created.isoformat(), 'title': entry.title, @@ -68,10 +89,18 @@ def get_entry_serializable(entry): 'description': entry.description, 'description_html': entry.description_html, 'media_type': entry.media_type, - 'media_files': get_media_file_paths(entry.media_files)} + 'permalink': entry.url_for_self(urlgen, qualified=True), + 'media_files': get_media_file_paths(entry.media_files, urlgen)} + +def get_media_file_paths(media_files, urlgen): + ''' + Returns a dictionary of media files with `file_handle` => `qualified URL` -def get_media_file_paths(media_files): + :param media_files: dict-like object consisting of `file_handle => `listy + filepath` pairs. + :param urlgen: An urlgen object, usually found on request.urlgen. + ''' if isinstance(mg_globals.public_store, BasicFileStorage): pass # TODO @@ -84,6 +113,11 @@ def get_media_file_paths(media_files): def api_auth(controller): + ''' + Decorator, allows plugins to register auth methods that will then be + evaluated against the request, finally a worthy authenticator object is + chosen and used to decide whether to grant or deny access. + ''' @wraps(controller) def wrapper(request, *args, **kw): auth_candidates = [] -- cgit v1.2.3 From 965b39a84fc240a24ae1bd7215bb06361b6df98f Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Sat, 15 Sep 2012 22:18:49 +0200 Subject: Added CORS headers to API json_response --- mediagoblin/plugins/api/tools.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'mediagoblin/plugins/api/tools.py') diff --git a/mediagoblin/plugins/api/tools.py b/mediagoblin/plugins/api/tools.py index e3b15b23..5488e515 100644 --- a/mediagoblin/plugins/api/tools.py +++ b/mediagoblin/plugins/api/tools.py @@ -63,6 +63,11 @@ def json_response(serializable, *args, **kw): ''' response = Response(json.dumps(serializable), *args, **kw) response.headers['Content-Type'] = 'application/json' + cors_headers = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With'} + response.headers.update(cors_headers) return response -- cgit v1.2.3 From c92aa0d0b21e01223db7eeaa2fcea4d961b512d9 Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Sat, 15 Sep 2012 22:34:34 +0200 Subject: API: Fixed media file URLs, limits - Added default limit and limit arg to get_entries - Fixed URL generation for BasicFileStorage files in API --- mediagoblin/plugins/api/tools.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) (limited to 'mediagoblin/plugins/api/tools.py') diff --git a/mediagoblin/plugins/api/tools.py b/mediagoblin/plugins/api/tools.py index 5488e515..e5aca29b 100644 --- a/mediagoblin/plugins/api/tools.py +++ b/mediagoblin/plugins/api/tools.py @@ -19,6 +19,7 @@ import json from functools import wraps from webob import exc, Response +from urlparse import urljoin from mediagoblin import mg_globals from mediagoblin.tools.pluginapi import PluginManager @@ -106,13 +107,16 @@ def get_media_file_paths(media_files, urlgen): filepath` pairs. :param urlgen: An urlgen object, usually found on request.urlgen. ''' - if isinstance(mg_globals.public_store, BasicFileStorage): - pass # TODO - media_urls = {} for key, val in media_files.items(): - media_urls[key] = mg_globals.public_store.file_url(val) + if isinstance(mg_globals.public_store, BasicFileStorage): + # BasicFileStorage does not provide a qualified URI + media_urls[key] = urljoin( + urlgen('index', qualified=True), + mg_globals.public_store.file_url(val)) + else: + media_urls[key] = mg_globals.public_store.file_url(val) return media_urls -- cgit v1.2.3 From 09e528acbb4d1321fce5cec8b22fd7fd153bf68a Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Mon, 17 Sep 2012 23:54:27 +0200 Subject: Fixed validation in API post_entry. Added state to API get_entry_serializable --- mediagoblin/plugins/api/tools.py | 1 + 1 file changed, 1 insertion(+) (limited to 'mediagoblin/plugins/api/tools.py') diff --git a/mediagoblin/plugins/api/tools.py b/mediagoblin/plugins/api/tools.py index e5aca29b..c4630ba7 100644 --- a/mediagoblin/plugins/api/tools.py +++ b/mediagoblin/plugins/api/tools.py @@ -95,6 +95,7 @@ def get_entry_serializable(entry, urlgen): 'description': entry.description, 'description_html': entry.description_html, 'media_type': entry.media_type, + 'state': entry.state, 'permalink': entry.url_for_self(urlgen, qualified=True), 'media_files': get_media_file_paths(entry.media_files, urlgen)} -- cgit v1.2.3 From 88a9662be4f97da5b04a3842c8d0caa2652be355 Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Fri, 21 Sep 2012 13:02:35 +0200 Subject: Added client registration caps to OAuth plugin THE MIGRATIONS SUPPLIED WITH THIS COMMIT WILL DROP AND RE-CREATE YOUR oauth__tokens AND oauth__codes TABLES. ALL YOUR OAUTH CODES AND TOKENS WILL BE LOST. - Fixed pylint issues in db/sql/migrations. - Added __repr__ to the User model. - Added _disable_cors option to json_response. - Added crude error handling to the api.tools.api_auth decorator - Updated the OAuth README. - Added client registration, client overview, connection overview, client authorization views and templates. - Added error handling to the OAuthAuth Auth object. - Added AuthorizationForm, ClientRegistrationForm in oauth/forms. - Added migrations for OAuth, added client registration migration. - Added OAuthClient, OAuthUserClient models. - Added oauth/tools with require_client_auth decorator method. --- mediagoblin/plugins/api/tools.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) (limited to 'mediagoblin/plugins/api/tools.py') diff --git a/mediagoblin/plugins/api/tools.py b/mediagoblin/plugins/api/tools.py index c4630ba7..ecc50364 100644 --- a/mediagoblin/plugins/api/tools.py +++ b/mediagoblin/plugins/api/tools.py @@ -52,7 +52,7 @@ class Auth(object): raise NotImplemented() -def json_response(serializable, *args, **kw): +def json_response(serializable, _disable_cors=False, *args, **kw): ''' Serializes a json objects and returns a webob.Response object with the serialized value as the response body and Content-Type: application/json. @@ -64,11 +64,14 @@ def json_response(serializable, *args, **kw): ''' response = Response(json.dumps(serializable), *args, **kw) response.headers['Content-Type'] = 'application/json' - cors_headers = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With'} - response.headers.update(cors_headers) + + 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'} + response.headers.update(cors_headers) + return response @@ -149,6 +152,11 @@ def api_auth(controller): auth, request.url)) if not auth(request, *args, **kw): + if getattr(auth, 'errors', []): + return json_response({ + 'status': 403, + 'errors': auth.errors}) + return exc.HTTPForbidden() return controller(request, *args, **kw) -- cgit v1.2.3 From 62d14bf50baf45ac15fe5276be74b073de880f77 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Thu, 15 Nov 2012 16:55:15 +0100 Subject: Transition webob.HttpForbidden to webob's exceptions Forbidden Also the BadRequest exception. --- mediagoblin/plugins/api/tools.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'mediagoblin/plugins/api/tools.py') diff --git a/mediagoblin/plugins/api/tools.py b/mediagoblin/plugins/api/tools.py index ecc50364..c4073d23 100644 --- a/mediagoblin/plugins/api/tools.py +++ b/mediagoblin/plugins/api/tools.py @@ -18,8 +18,9 @@ import logging import json from functools import wraps -from webob import exc, Response +from webob import Response from urlparse import urljoin +from werkzeug.exceptions import Forbidden from mediagoblin import mg_globals from mediagoblin.tools.pluginapi import PluginManager @@ -143,7 +144,7 @@ def api_auth(controller): # If we can't find any authentication methods, we should not let them # pass. if not auth_candidates: - return exc.HTTPForbidden() + return Forbidden() # For now, just select the first one in the list auth = auth_candidates[0] @@ -157,7 +158,7 @@ def api_auth(controller): 'status': 403, 'errors': auth.errors}) - return exc.HTTPForbidden() + return Forbidden() return controller(request, *args, **kw) -- cgit v1.2.3 From 7c552c0bd76a4bb0292bbdf694d0ce4ba65de0a7 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Wed, 12 Dec 2012 11:38:51 +0100 Subject: plugins/api: use headers.set(), not headers.update() The werkzeug.Response().headers do not offer an update() method as the same key can be twice in the header 'dict'. Thus, iterate over the header keys and use header.set(key, value) which replaces an existing header key. Signed-off-by: Sebastian Spaeth --- mediagoblin/plugins/api/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'mediagoblin/plugins/api/tools.py') diff --git a/mediagoblin/plugins/api/tools.py b/mediagoblin/plugins/api/tools.py index c4073d23..333a5682 100644 --- a/mediagoblin/plugins/api/tools.py +++ b/mediagoblin/plugins/api/tools.py @@ -71,7 +71,7 @@ def json_response(serializable, _disable_cors=False, *args, **kw): 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With'} - response.headers.update(cors_headers) + (response.headers.set(key, value) for key, value in cors_headers) return response -- cgit v1.2.3 From cc195d5b821319732038fbd5d13bcc8b5a951ffc Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Fri, 16 Nov 2012 11:31:16 +0100 Subject: plugins/api: webob.Response -> werkzeug.Response --- mediagoblin/plugins/api/tools.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) (limited to 'mediagoblin/plugins/api/tools.py') diff --git a/mediagoblin/plugins/api/tools.py b/mediagoblin/plugins/api/tools.py index 333a5682..0ef91127 100644 --- a/mediagoblin/plugins/api/tools.py +++ b/mediagoblin/plugins/api/tools.py @@ -18,10 +18,9 @@ import logging import json from functools import wraps -from webob import Response from urlparse import urljoin from werkzeug.exceptions import Forbidden - +from werkzeug.wrappers import Response from mediagoblin import mg_globals from mediagoblin.tools.pluginapi import PluginManager from mediagoblin.storage.filestorage import BasicFileStorage @@ -55,16 +54,15 @@ class Auth(object): def json_response(serializable, _disable_cors=False, *args, **kw): ''' - Serializes a json objects and returns a webob.Response object with the + 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 - webob.Response.__init__ method. + Response.__init__ method. ''' - response = Response(json.dumps(serializable), *args, **kw) - response.headers['Content-Type'] = 'application/json' + response = Response(json.dumps(serializable), *args, content_type='application/json', **kw) if not _disable_cors: cors_headers = { -- cgit v1.2.3 From cfa922295e5ddfaab336a3c2c0403422f77758b6 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Sun, 23 Dec 2012 11:58:51 +0100 Subject: Convert return HttpException to raise HttpException controllers (view function) raise HttpException's and do not return them. Signed-off-by: Sebastian Spaeth --- mediagoblin/plugins/api/tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'mediagoblin/plugins/api/tools.py') diff --git a/mediagoblin/plugins/api/tools.py b/mediagoblin/plugins/api/tools.py index 0ef91127..03f528ce 100644 --- a/mediagoblin/plugins/api/tools.py +++ b/mediagoblin/plugins/api/tools.py @@ -142,7 +142,7 @@ def api_auth(controller): # If we can't find any authentication methods, we should not let them # pass. if not auth_candidates: - return Forbidden() + raise Forbidden() # For now, just select the first one in the list auth = auth_candidates[0] @@ -156,7 +156,7 @@ def api_auth(controller): 'status': 403, 'errors': auth.errors}) - return Forbidden() + raise Forbidden() return controller(request, *args, **kw) -- cgit v1.2.3 From 57c6473aa2146f3337a42cb4b9c54dc164b7b76a Mon Sep 17 00:00:00 2001 From: Joar Wandborg Date: Sun, 23 Dec 2012 00:34:27 +0100 Subject: Added API tests --- mediagoblin/plugins/api/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'mediagoblin/plugins/api/tools.py') diff --git a/mediagoblin/plugins/api/tools.py b/mediagoblin/plugins/api/tools.py index 03f528ce..e5878258 100644 --- a/mediagoblin/plugins/api/tools.py +++ b/mediagoblin/plugins/api/tools.py @@ -135,8 +135,8 @@ def api_auth(controller): auth_candidates = [] for auth in PluginManager().get_hook_callables('auth'): - _log.debug('Plugin auth: {0}'.format(auth)) if auth.trigger(request): + _log.debug('{0} believes it is capable of authenticating this request.'.format(auth)) auth_candidates.append(auth) # If we can't find any authentication methods, we should not let them -- cgit v1.2.3 From 9b2cd962af78ce8fbe2bce88d7b9d3a9d01e4aa9 Mon Sep 17 00:00:00 2001 From: Runar Petursson Date: Fri, 15 Feb 2013 10:17:24 +0800 Subject: plugins/api: fix for cross origin requests The response headers were never getting set because of a bug in the 7c552c0 commit. This expands the loop into a more readable form and results in the headers getting set. --- mediagoblin/plugins/api/tools.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'mediagoblin/plugins/api/tools.py') diff --git a/mediagoblin/plugins/api/tools.py b/mediagoblin/plugins/api/tools.py index e5878258..92411f4b 100644 --- a/mediagoblin/plugins/api/tools.py +++ b/mediagoblin/plugins/api/tools.py @@ -69,7 +69,8 @@ def json_response(serializable, _disable_cors=False, *args, **kw): 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With'} - (response.headers.set(key, value) for key, value in cors_headers) + for key, value in cors_headers.iteritems(): + response.headers.set(key, value) return response -- cgit v1.2.3