From 4f8f0a4e1f2d5a1eb64f47e62958d77bd12379f3 Mon Sep 17 00:00:00 2001 From: Rodney Ewing Date: Wed, 26 Jun 2013 12:04:45 -0700 Subject: [PATCH 01/45] merge --squash persona branch to take care of a false merge commit in the basic_auth branch that persona is forked from Conflicts: mediagoblin/templates/mediagoblin/auth/login.html mediagoblin/templates/mediagoblin/auth/register.html mediagoblin/templates/mediagoblin/edit/edit_account.html These are commit messages from the squashed persona stuff: - added tests and fixed minor errors - fixed a redirect loop when only persona is enabled and accessing /auth/login - moved persona.js to plugin's static dir - fixes for add/remove persona emails - add and remove personas - working with multiple plugins - working version - switched to hidden form instead of ajax - beginings --- mediagoblin/auth/views.py | 14 +- .../mediagoblin/plugins/openid/login.html | 7 + mediagoblin/plugins/persona/__init__.py | 113 ++++++++++ mediagoblin/plugins/persona/forms.py | 41 ++++ mediagoblin/plugins/persona/models.py | 36 +++ .../plugins/persona/static/js/persona.js | 44 ++++ .../mediagoblin/plugins/persona/edit.html | 43 ++++ .../mediagoblin/plugins/persona/persona.html | 30 +++ .../plugins/persona/persona_js_head.html | 21 ++ mediagoblin/plugins/persona/views.py | 191 ++++++++++++++++ .../templates/mediagoblin/auth/login.html | 7 + .../templates/mediagoblin/auth/register.html | 8 + mediagoblin/templates/mediagoblin/base.html | 33 ++- .../mediagoblin/bits/frontpage_welcome.html | 10 +- .../mediagoblin/edit/edit_account.html | 8 + .../tests/auth_configs/persona_appconfig.ini | 42 ++++ mediagoblin/tests/test_persona.py | 210 ++++++++++++++++++ 17 files changed, 848 insertions(+), 10 deletions(-) create mode 100644 mediagoblin/plugins/persona/__init__.py create mode 100644 mediagoblin/plugins/persona/forms.py create mode 100644 mediagoblin/plugins/persona/models.py create mode 100644 mediagoblin/plugins/persona/static/js/persona.js create mode 100644 mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/edit.html create mode 100644 mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona.html create mode 100644 mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona_js_head.html create mode 100644 mediagoblin/plugins/persona/views.py create mode 100644 mediagoblin/tests/auth_configs/persona_appconfig.ini create mode 100644 mediagoblin/tests/test_persona.py diff --git a/mediagoblin/auth/views.py b/mediagoblin/auth/views.py index 1cff8dcc..560080e4 100644 --- a/mediagoblin/auth/views.py +++ b/mediagoblin/auth/views.py @@ -41,8 +41,11 @@ def register(request): """ if 'pass_auth' not in request.template_env.globals: redirect_name = hook_handle('auth_no_pass_redirect') - return redirect(request, 'mediagoblin.plugins.{0}.register'.format( - redirect_name)) + if redirect_name: + return redirect(request, 'mediagoblin.plugins.{0}.register'.format( + redirect_name)) + else: + return redirect(request, 'index') register_form = hook_handle("auth_get_registration_form", request) @@ -73,8 +76,11 @@ def login(request): """ if 'pass_auth' not in request.template_env.globals: redirect_name = hook_handle('auth_no_pass_redirect') - return redirect(request, 'mediagoblin.plugins.{0}.login'.format( - redirect_name)) + if redirect_name: + return redirect(request, 'mediagoblin.plugins.{0}.login'.format( + redirect_name)) + else: + return redirect(request, 'index') login_form = hook_handle("auth_get_login_form", request) diff --git a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html index 33df7200..604c4bec 100644 --- a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html +++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html @@ -44,6 +44,13 @@ {% trans %}Log in to create an account!{% endtrans %}

{% endif %} + {% if persona is defined %} +

+ + {% trans %}Or login with Persona!{% endtrans %} + +

+ {% endif %} {% if pass_auth is defined %}

diff --git a/mediagoblin/plugins/persona/__init__.py b/mediagoblin/plugins/persona/__init__.py new file mode 100644 index 00000000..20cbbbf9 --- /dev/null +++ b/mediagoblin/plugins/persona/__init__.py @@ -0,0 +1,113 @@ +# 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 pkg_resources import resource_filename +import os + +from sqlalchemy import or_ + +from mediagoblin.auth.tools import create_basic_user +from mediagoblin.db.models import User +from mediagoblin.plugins.persona.models import PersonaUserEmails +from mediagoblin.tools import pluginapi +from mediagoblin.tools.staticdirect import PluginStatic +from mediagoblin.tools.translate import pass_to_ugettext as _ + +PLUGIN_DIR = os.path.dirname(__file__) + + +def setup_plugin(): + config = pluginapi.get_config('mediagoblin.plugins.persona') + + routes = [ + ('mediagoblin.plugins.persona.login', + '/auth/persona/login/', + 'mediagoblin.plugins.persona.views:login'), + ('mediagoblin.plugins.persona.register', + '/auth/persona/register/', + 'mediagoblin.plugins.persona.views:register'), + ('mediagoblin.plugins.persona.edit', + '/edit/persona/', + 'mediagoblin.plugins.persona.views:edit'), + ('mediagoblin.plugins.persona.add', + '/edit/persona/add/', + 'mediagoblin.plugins.persona.views:add')] + + pluginapi.register_routes(routes) + pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates')) + pluginapi.register_template_hooks( + {'persona_head': 'mediagoblin/plugins/persona/persona_js_head.html', + 'persona_form': 'mediagoblin/plugins/persona/persona.html'}) + + +def create_user(register_form): + if 'persona_email' in register_form: + username = register_form.username.data + user = User.query.filter( + or_( + User.username == username, + User.email == username, + )).first() + + if not user: + user = create_basic_user(register_form) + + new_entry = PersonaUserEmails() + new_entry.persona_email = register_form.persona_email.data + new_entry.user_id = user.id + new_entry.save() + + return user + + +def extra_validation(register_form): + persona_email = register_form.persona_email.data if 'persona_email' in \ + register_form else None + if persona_email: + persona_email_exists = PersonaUserEmails.query.filter_by( + persona_email=persona_email + ).count() + + extra_validation_passes = True + + if persona_email_exists: + register_form.persona_email.errors.append( + _('Sorry, an account is already registered to that Persona' + ' email.')) + extra_validation_passes = False + + return extra_validation_passes + + +def Auth(): + return True + + +def add_to_global_context(context): + if len(pluginapi.hook_runall('authentication')) == 1: + context['persona_auth'] = True + context['persona'] = True + return context + +hooks = { + 'setup': setup_plugin, + 'authentication': Auth, + 'auth_extra_validation': extra_validation, + 'auth_create_user': create_user, + 'template_global_context': add_to_global_context, + 'static_setup': lambda: PluginStatic( + 'coreplugin_persona', + resource_filename('mediagoblin.plugins.persona', 'static')) +} diff --git a/mediagoblin/plugins/persona/forms.py b/mediagoblin/plugins/persona/forms.py new file mode 100644 index 00000000..608be0c7 --- /dev/null +++ b/mediagoblin/plugins/persona/forms.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 wtforms + +from mediagoblin.tools.translate import lazy_pass_to_ugettext as _ +from mediagoblin.auth.tools import normalize_user_or_email_field + + +class RegistrationForm(wtforms.Form): + username = wtforms.TextField( + _('Username'), + [wtforms.validators.Required(), + normalize_user_or_email_field(allow_email=False)]) + email = wtforms.TextField( + _('Email address'), + [wtforms.validators.Required(), + normalize_user_or_email_field(allow_user=False)]) + persona_email = wtforms.HiddenField( + '', + [wtforms.validators.Required(), + normalize_user_or_email_field(allow_user=False)]) + + +class EditForm(wtforms.Form): + email = wtforms.TextField( + _('Email address'), + [wtforms.validators.Required(), + normalize_user_or_email_field(allow_user=False)]) diff --git a/mediagoblin/plugins/persona/models.py b/mediagoblin/plugins/persona/models.py new file mode 100644 index 00000000..ff3c525a --- /dev/null +++ b/mediagoblin/plugins/persona/models.py @@ -0,0 +1,36 @@ +# 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 sqlalchemy import Column, Integer, Unicode, ForeignKey +from sqlalchemy.orm import relationship, backref + +from mediagoblin.db.models import User +from mediagoblin.db.base import Base + + +class PersonaUserEmails(Base): + __tablename__ = "persona__user_emails" + + id = Column(Integer, primary_key=True) + persona_email = Column(Unicode, nullable=False) + user_id = Column(Integer, ForeignKey(User.id), nullable=False) + + # Persona's are owned by their user, so do the full thing. + user = relationship(User, backref=backref('persona_emails', + cascade='all, delete-orphan')) + +MODELS = [ + PersonaUserEmails +] diff --git a/mediagoblin/plugins/persona/static/js/persona.js b/mediagoblin/plugins/persona/static/js/persona.js new file mode 100644 index 00000000..034f2574 --- /dev/null +++ b/mediagoblin/plugins/persona/static/js/persona.js @@ -0,0 +1,44 @@ +/** + * 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 . + */ + +$(document).ready(function () { + var signinLink = document.getElementById('persona_login'); + if (signinLink) { + signinLink.onclick = function() { navigator.id.request(); }; + } + + var signoutLink = document.getElementById('logout'); + if (signoutLink) { + signoutLink.onclick = function() { navigator.id.logout(); }; + } + + navigator.id.watch({ + onlogin: function(assertion) { + document.getElementById('_assertion').value = assertion; + document.getElementById('_persona_login').submit() + }, + onlogout: function() { + $.ajax({ + type: 'POST', + url: '/auth/logout', + success: function(res, status, xhr) { window.location.reload(); }, + error: function(xhr, status, err) { alert("Logout failure: " + err); } + }); + } + }); +}); diff --git a/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/edit.html b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/edit.html new file mode 100644 index 00000000..be62b8cc --- /dev/null +++ b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/edit.html @@ -0,0 +1,43 @@ +{# +# 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" %} + +{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} + +{% block title -%} + {% trans %}Add an OpenID{% endtrans %} — {{ super() }} +{%- endblock %} + +{% block mediagoblin_content %} +

+ {{ csrf_token }} + +
+{% endblock %} diff --git a/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona.html b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona.html new file mode 100644 index 00000000..c953f393 --- /dev/null +++ b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona.html @@ -0,0 +1,30 @@ +{# +# 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 . +#} +{% block persona %} +
+ {{ csrf_token }} + +
+{% endblock %} diff --git a/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona_js_head.html b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona_js_head.html new file mode 100644 index 00000000..8c0d72d5 --- /dev/null +++ b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona_js_head.html @@ -0,0 +1,21 @@ +{# +# 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/plugins/persona/views.py b/mediagoblin/plugins/persona/views.py new file mode 100644 index 00000000..f3aff38d --- /dev/null +++ b/mediagoblin/plugins/persona/views.py @@ -0,0 +1,191 @@ +# 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 logging +import requests + +from werkzeug.exceptions import BadRequest + +from mediagoblin import messages, mg_globals +from mediagoblin.auth.tools import register_user +from mediagoblin.decorators import (auth_enabled, allow_registration, + require_active_login) +from mediagoblin.tools.response import render_to_response, redirect +from mediagoblin.tools.translate import pass_to_ugettext as _ +from mediagoblin.plugins.persona import forms +from mediagoblin.plugins.persona.models import PersonaUserEmails + +_log = logging.getLogger(__name__) + + +def _get_response(request): + if 'assertion' not in request.form: + _log.debug('assertion not in request.form') + raise BadRequest() + + data = {'assertion': request.form['assertion'], + 'audience': request.urlgen('index', qualified=True)} + resp = requests.post('https://verifier.login.persona.org/verify', + data=data, verify=True) + + if resp.ok: + verification_data = json.loads(resp.content) + + if verification_data['status'] == 'okay': + return verification_data['email'] + + return None + + +@auth_enabled +def login(request): + if request.method == 'GET': + return redirect(request, 'mediagoblin.auth.login') + + email = _get_response(request) + if email: + query = PersonaUserEmails.query.filter_by( + persona_email=email + ).first() + user = query.user if query else None + + if user: + request.session['user_id'] = unicode(user.id) + request.session.save() + + return redirect(request, "index") + + else: + if not mg_globals.app.auth: + messages.add_message( + request, + messages.WARNING, + _('Sorry, authentication is disabled on this instance.')) + + return redirect(request, 'index') + + register_form = forms.RegistrationForm(email=email, + persona_email=email) + return render_to_response( + request, + 'mediagoblin/auth/register.html', + {'register_form': register_form, + 'post_url': request.urlgen( + 'mediagoblin.plugins.persona.register')}) + + return redirect(request, 'mediagoblin.auth.login') + + +@allow_registration +@auth_enabled +def register(request): + if request.method == 'GET': + # Need to connect to persona before registering a user. If method is + # 'GET', then this page was acessed without logging in first. + return redirect(request, 'mediagoblin.auth.login') + register_form = forms.RegistrationForm(request.form) + + if register_form.validate(): + user = register_user(request, register_form) + + if user: + # redirect the user to their homepage... there will be a + # message waiting for them to verify their email + return redirect( + request, 'mediagoblin.user_pages.user_home', + user=user.username) + + return render_to_response( + request, + 'mediagoblin/auth/register.html', + {'register_form': register_form, + 'post_url': request.urlgen('mediagoblin.plugins.persona.register')}) + + +@require_active_login +def edit(request): + form = forms.EditForm(request.form) + + if request.method == 'POST' and form.validate(): + query = PersonaUserEmails.query.filter_by( + persona_email=form.email.data) + user = query.first().user if query.first() else None + + if user and user.id == int(request.user.id): + count = len(user.persona_emails) + + if count > 1 or user.pw_hash: + # User has more then one Persona email or also has a password. + query.first().delete() + + messages.add_message( + request, + messages.SUCCESS, + _('The Persona email address was successfully removed.')) + + return redirect(request, 'mediagoblin.edit.account') + + elif not count > 1: + form.email.errors.append( + _("You can't delete your only Persona email address unless" + " you have a password set.")) + + else: + form.email.errors.append( + _('That Persona email address is not registered to this' + ' account.')) + + return render_to_response( + request, + 'mediagoblin/plugins/persona/edit.html', + {'form': form, + 'edit_persona': True}) + + +@require_active_login +def add(request): + if request.method == 'GET': + return redirect(request, 'mediagoblin.plugins.persona.edit') + + email = _get_response(request) + + if email: + query = PersonaUserEmails.query.filter_by( + persona_email=email + ).first() + user_exists = query.user if query else None + + if user_exists: + messages.add_message( + request, + messages.WARNING, + _('Sorry, an account is already registered with that Persona' + ' email address.')) + return redirect(request, 'mediagoblin.plugins.persona.edit') + + else: + # Save the Persona Email to the user + new_entry = PersonaUserEmails() + new_entry.persona_email = email + new_entry.user_id = request.user.id + new_entry.save() + + messages.add_message( + request, + messages.SUCCESS, + _('Your Person email address was saved successfully.')) + + return redirect(request, 'mediagoblin.edit.account') diff --git a/mediagoblin/templates/mediagoblin/auth/login.html b/mediagoblin/templates/mediagoblin/auth/login.html index 3329b5d0..af8c61b5 100644 --- a/mediagoblin/templates/mediagoblin/auth/login.html +++ b/mediagoblin/templates/mediagoblin/auth/login.html @@ -47,6 +47,13 @@

{% endif %} {% template_hook("login_link") %} + {% if persona is defined %} +

+ + {% trans %}Or login with Persona!{% endtrans %} + +

+ {% endif %} {{ wtforms_util.render_divs(login_form, True) }} {% if pass_auth %}

diff --git a/mediagoblin/templates/mediagoblin/auth/register.html b/mediagoblin/templates/mediagoblin/auth/register.html index a7b8033f..8ed389a0 100644 --- a/mediagoblin/templates/mediagoblin/auth/register.html +++ b/mediagoblin/templates/mediagoblin/auth/register.html @@ -35,6 +35,14 @@

{% trans %}Create an account!{% endtrans %}

{% template_hook("register_link") %} + {% template_hook("openid_register_link") %} + {% if persona is defined %} +

+ + {% trans %}Or register with Persona!{% endtrans %} + +

+ {% endif %} {{ wtforms_util.render_divs(register_form, True) }} {{ csrf_token }}
diff --git a/mediagoblin/templates/mediagoblin/base.html b/mediagoblin/templates/mediagoblin/base.html index 1fc4467c..f2f03382 100644 --- a/mediagoblin/templates/mediagoblin/base.html +++ b/mediagoblin/templates/mediagoblin/base.html @@ -23,6 +23,7 @@ + {% block title %}{{ app_config['html_title'] }}{% endblock %} @@ -46,6 +47,10 @@ {% include "mediagoblin/extra_head.html" %} {% template_hook("head") %} + {% if persona is defined %} + {% template_hook("persona_head") %} + {% endif %} + {% block mediagoblin_head %} {% endblock mediagoblin_head %} @@ -73,11 +78,22 @@ user=request.user.username) }}" class="button_action_highlight"> {% trans %}Verify your email!{% endtrans %} - or {% trans %}log out{% endtrans %} + or {% trans %}log out{% endtrans %} {% endif %} {%- elif auth %} - + {%- trans %}Log in{% endtrans -%} {%- endif %} @@ -101,7 +117,13 @@ {%- trans %}Media processing panel{% endtrans -%} · - {% trans %}Log out{% endtrans %} + {% trans %}Log out{% endtrans %}

{%- trans %}Add media{% endtrans -%} @@ -128,6 +150,9 @@ {% include "mediagoblin/utils/messages.html" %} {% block mediagoblin_content %} {% endblock mediagoblin_content %} + {% if persona is defined and csrf_token is defined %} + {% template_hook("persona_form") %} + {% endif %}
{%- include "mediagoblin/bits/base_footer.html" %}
diff --git a/mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html b/mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html index 9ef28a4d..dbc23a1f 100644 --- a/mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html +++ b/mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html @@ -26,8 +26,14 @@

{% trans %}To add your own media, place comments, and more, you can log in with your MediaGoblin account.{% endtrans %}

{% if allow_registration %}

{% trans %}Don't have one yet? It's easy!{% endtrans %}

- {% trans register_url=request.urlgen('mediagoblin.auth.register') -%} -
Create an account at this site + Create an account at this site or {%- endtrans %} {% endif %} diff --git a/mediagoblin/templates/mediagoblin/edit/edit_account.html b/mediagoblin/templates/mediagoblin/edit/edit_account.html index 51293acb..9bd488d7 100644 --- a/mediagoblin/templates/mediagoblin/edit/edit_account.html +++ b/mediagoblin/templates/mediagoblin/edit/edit_account.html @@ -49,6 +49,14 @@

{% endif %} {% template_hook("edit_link") %} + {% template_hook("openid_edit_link") %} + {% if persona is defined %} +

+ + {% trans %}Edit your Persona email addresses{% endtrans %} + +

+ {% endif %} {{ wtforms_util.render_divs(form, True) }}
diff --git a/mediagoblin/tests/auth_configs/persona_appconfig.ini b/mediagoblin/tests/auth_configs/persona_appconfig.ini new file mode 100644 index 00000000..0bd5d634 --- /dev/null +++ b/mediagoblin/tests/auth_configs/persona_appconfig.ini @@ -0,0 +1,42 @@ +# 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 . +[mediagoblin] +direct_remote_path = /test_static/ +email_sender_address = "notice@mediagoblin.example.org" +email_debug_mode = true + +# TODO: Switch to using an in-memory database +sql_engine = "sqlite:///%(here)s/user_dev/mediagoblin.db" + +# Celery shouldn't be set up by the application as it's setup via +# mediagoblin.init.celery.from_celery +celery_setup_elsewhere = true + +[storage:publicstore] +base_dir = %(here)s/user_dev/media/public +base_url = /mgoblin_media/ + +[storage:queuestore] +base_dir = %(here)s/user_dev/media/queue + +[celery] +CELERY_ALWAYS_EAGER = true +CELERY_RESULT_DBURI = "sqlite:///%(here)s/user_dev/celery.db" +BROKER_HOST = "sqlite:///%(here)s/user_dev/kombu.db" + +[plugins] +[[mediagoblin.plugins.persona]] + diff --git a/mediagoblin/tests/test_persona.py b/mediagoblin/tests/test_persona.py new file mode 100644 index 00000000..1d03ea7f --- /dev/null +++ b/mediagoblin/tests/test_persona.py @@ -0,0 +1,210 @@ +# 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 urlparse +import pkg_resources +import pytest +import mock + +from mediagoblin import mg_globals +from mediagoblin.db.base import Session +from mediagoblin.tests.tools import get_app +from mediagoblin.tools import template + + +# App with plugin enabled +@pytest.fixture() +def persona_plugin_app(request): + return get_app( + request, + mgoblin_config=pkg_resources.resource_filename( + 'mediagoblin.tests.auth_configs', + 'persona_appconfig.ini')) + + +class TestPersonaPlugin(object): + def test_authentication_views(self, persona_plugin_app): + res = persona_plugin_app.get('/auth/login/') + + assert urlparse.urlsplit(res.location)[2] == '/' + + res = persona_plugin_app.get('/auth/register/') + + assert urlparse.urlsplit(res.location)[2] == '/' + + res = persona_plugin_app.get('/auth/persona/login/') + + assert urlparse.urlsplit(res.location)[2] == '/auth/login/' + + res = persona_plugin_app.get('/auth/persona/register/') + + assert urlparse.urlsplit(res.location)[2] == '/auth/login/' + + @mock.patch('mediagoblin.plugins.persona.views._get_response', mock.Mock(return_value=u'test@example.com')) + def _test_registration(): + # No register users + template.clear_test_template_context() + res = persona_plugin_app.post( + '/auth/persona/login/', {}) + + assert 'mediagoblin/auth/register.html' in template.TEMPLATE_TEST_CONTEXT + context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html'] + register_form = context['register_form'] + + assert register_form.email.data == u'test@example.com' + assert register_form.persona_email.data == u'test@example.com' + + template.clear_test_template_context() + res = persona_plugin_app.post( + '/auth/persona/register/', {}) + + assert 'mediagoblin/auth/register.html' in template.TEMPLATE_TEST_CONTEXT + context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html'] + register_form = context['register_form'] + + assert register_form.username.errors == [u'This field is required.'] + assert register_form.email.errors == [u'This field is required.'] + assert register_form.persona_email.errors == [u'This field is required.'] + + # Successful register + template.clear_test_template_context() + res = persona_plugin_app.post( + '/auth/persona/register/', + {'username': 'chris', + 'email': 'chris@example.com', + 'persona_email': 'test@example.com'}) + res.follow() + + assert urlparse.urlsplit(res.location)[2] == '/u/chris/' + assert 'mediagoblin/user_pages/user.html' in template.TEMPLATE_TEST_CONTEXT + + # Try to register same Persona email address + template.clear_test_template_context() + res = persona_plugin_app.post( + '/auth/persona/register/', + {'username': 'chris1', + 'email': 'chris1@example.com', + 'persona_email': 'test@example.com'}) + + assert 'mediagoblin/auth/register.html' in template.TEMPLATE_TEST_CONTEXT + context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html'] + register_form = context['register_form'] + + assert register_form.persona_email.errors == [u'Sorry, an account is already registered to that Persona email.'] + + # Logout + persona_plugin_app.get('/auth/logout/') + + # Get user and detach from session + test_user = mg_globals.database.User.find_one({ + 'username': u'chris'}) + test_user.email_verified = True + test_user.status = u'active' + test_user.save() + test_user = mg_globals.database.User.find_one({ + 'username': u'chris'}) + Session.expunge(test_user) + + # Add another user for _test_edit_persona + persona_plugin_app.post( + '/auth/persona/register/', + {'username': 'chris1', + 'email': 'chris1@example.com', + 'persona_email': 'test1@example.com'}) + + # Log back in + template.clear_test_template_context() + res = persona_plugin_app.post( + '/auth/persona/login/') + res.follow() + + assert urlparse.urlsplit(res.location)[2] == '/' + assert 'mediagoblin/root.html' in template.TEMPLATE_TEST_CONTEXT + + # Make sure user is in the session + context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/root.html'] + session = context['request'].session + assert session['user_id'] == unicode(test_user.id) + + _test_registration() + + @mock.patch('mediagoblin.plugins.persona.views._get_response', mock.Mock(return_value=u'new@example.com')) + def _test_edit_persona(): + # Try and delete only Persona email address + template.clear_test_template_context() + res = persona_plugin_app.post( + '/edit/persona/', + {'email': 'test@example.com'}) + + assert 'mediagoblin/plugins/persona/edit.html' in template.TEMPLATE_TEST_CONTEXT + context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/plugins/persona/edit.html'] + form = context['form'] + + assert form.email.errors == [u"You can't delete your only Persona email address unless you have a password set."] + + template.clear_test_template_context() + res = persona_plugin_app.post( + '/edit/persona/', {}) + + assert 'mediagoblin/plugins/persona/edit.html' in template.TEMPLATE_TEST_CONTEXT + context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/plugins/persona/edit.html'] + form = context['form'] + + assert form.email.errors == [u'This field is required.'] + + # Try and delete Persona not owned by the user + template.clear_test_template_context() + res = persona_plugin_app.post( + '/edit/persona/', + {'email': 'test1@example.com'}) + + assert 'mediagoblin/plugins/persona/edit.html' in template.TEMPLATE_TEST_CONTEXT + context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/plugins/persona/edit.html'] + form = context['form'] + + assert form.email.errors == [u'That Persona email address is not registered to this account.'] + + res = persona_plugin_app.get('/edit/persona/add/') + + assert urlparse.urlsplit(res.location)[2] == '/edit/persona/' + + # Add Persona email address + template.clear_test_template_context() + res = persona_plugin_app.post( + '/edit/persona/add/') + res.follow() + + assert urlparse.urlsplit(res.location)[2] == '/edit/account/' + + # Delete a Persona + res = persona_plugin_app.post( + '/edit/persona/', + {'email': 'test@example.com'}) + res.follow() + + assert urlparse.urlsplit(res.location)[2] == '/edit/account/' + + _test_edit_persona() + + @mock.patch('mediagoblin.plugins.persona.views._get_response', mock.Mock(return_value=u'test1@example.com')) + def _test_add_existing(): + template.clear_test_template_context() + res = persona_plugin_app.post( + '/edit/persona/add/') + res.follow() + + assert urlparse.urlsplit(res.location)[2] == '/edit/persona/' + + _test_add_existing() From e39b9cc60f6ccd8853ee19633d09750be541f767 Mon Sep 17 00:00:00 2001 From: Rodney Ewing Date: Wed, 3 Jul 2013 15:04:25 -0500 Subject: [PATCH 02/45] moved persona template stuff to use template_hooks --- mediagoblin/plugins/openid/__init__.py | 2 ++ .../mediagoblin/plugins/openid/login.html | 8 +----- .../plugins/openid/login_link.html | 2 ++ mediagoblin/plugins/persona/__init__.py | 5 +++- .../plugins/persona/edit_link.html | 25 +++++++++++++++++++ .../plugins/persona/login_link.html | 25 +++++++++++++++++++ .../plugins/persona/register_link.html | 25 +++++++++++++++++++ .../templates/mediagoblin/auth/login.html | 7 ------ .../templates/mediagoblin/auth/register.html | 8 ------ mediagoblin/templates/mediagoblin/base.html | 6 ++--- .../mediagoblin/edit/edit_account.html | 8 ------ 11 files changed, 86 insertions(+), 35 deletions(-) create mode 100644 mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/edit_link.html create mode 100644 mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/login_link.html create mode 100644 mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/register_link.html diff --git a/mediagoblin/plugins/openid/__init__.py b/mediagoblin/plugins/openid/__init__.py index ee88808c..ca17a7e8 100644 --- a/mediagoblin/plugins/openid/__init__.py +++ b/mediagoblin/plugins/openid/__init__.py @@ -120,4 +120,6 @@ hooks = { 'auth_no_pass_redirect': no_pass_redirect, ('mediagoblin.auth.register', 'mediagoblin/auth/register.html'): add_to_form_context, + ('mediagoblin.auth.login', + 'mediagoblin/auth/login.html'): add_to_form_context } diff --git a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html index 604c4bec..8d74c2b9 100644 --- a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html +++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html @@ -44,13 +44,7 @@ {% trans %}Log in to create an account!{% endtrans %}

{% endif %} - {% if persona is defined %} -

- - {% trans %}Or login with Persona!{% endtrans %} - -

- {% endif %} + {% template_hook('login_link') %} {% if pass_auth is defined %}

diff --git a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login_link.html b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login_link.html index e5e77d01..fa4d5e85 100644 --- a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login_link.html +++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login_link.html @@ -17,9 +17,11 @@ #} {% block openid_login_link %} + {% if openid_link is defined %}

{%- trans %}Or login with OpenID!{% endtrans %}

+ {% endif %} {% endblock %} diff --git a/mediagoblin/plugins/persona/__init__.py b/mediagoblin/plugins/persona/__init__.py index 20cbbbf9..d74ba0d7 100644 --- a/mediagoblin/plugins/persona/__init__.py +++ b/mediagoblin/plugins/persona/__init__.py @@ -49,7 +49,10 @@ def setup_plugin(): pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates')) pluginapi.register_template_hooks( {'persona_head': 'mediagoblin/plugins/persona/persona_js_head.html', - 'persona_form': 'mediagoblin/plugins/persona/persona.html'}) + 'persona_form': 'mediagoblin/plugins/persona/persona.html', + 'edit_link': 'mediagoblin/plugins/persona/edit_link.html', + 'login_link': 'mediagoblin/plugins/persona/login_link.html', + 'register_link': 'mediagoblin/plugins/persona/register_link.html'}) def create_user(register_form): diff --git a/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/edit_link.html b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/edit_link.html new file mode 100644 index 00000000..08879da5 --- /dev/null +++ b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/edit_link.html @@ -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 . +#} + +{% block persona_edit_link %} +

+ + {% trans %}Edit your Persona email addresses{% endtrans %} + +

+{% endblock %} diff --git a/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/login_link.html b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/login_link.html new file mode 100644 index 00000000..975683da --- /dev/null +++ b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/login_link.html @@ -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 . +#} + +{% block person_login_link %} +

+ + {% trans %}Or login with Persona!{% endtrans %} + +

+{% endblock %} diff --git a/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/register_link.html b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/register_link.html new file mode 100644 index 00000000..bcd9ae2b --- /dev/null +++ b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/register_link.html @@ -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 . +#} + +{% block persona_register_link %} +

+ + {% trans %}Or register with Persona!{% endtrans %} + +

+{% endblock %} diff --git a/mediagoblin/templates/mediagoblin/auth/login.html b/mediagoblin/templates/mediagoblin/auth/login.html index af8c61b5..3329b5d0 100644 --- a/mediagoblin/templates/mediagoblin/auth/login.html +++ b/mediagoblin/templates/mediagoblin/auth/login.html @@ -47,13 +47,6 @@

{% endif %} {% template_hook("login_link") %} - {% if persona is defined %} -

- - {% trans %}Or login with Persona!{% endtrans %} - -

- {% endif %} {{ wtforms_util.render_divs(login_form, True) }} {% if pass_auth %}

diff --git a/mediagoblin/templates/mediagoblin/auth/register.html b/mediagoblin/templates/mediagoblin/auth/register.html index 8ed389a0..a7b8033f 100644 --- a/mediagoblin/templates/mediagoblin/auth/register.html +++ b/mediagoblin/templates/mediagoblin/auth/register.html @@ -35,14 +35,6 @@

{% trans %}Create an account!{% endtrans %}

{% template_hook("register_link") %} - {% template_hook("openid_register_link") %} - {% if persona is defined %} -

- - {% trans %}Or register with Persona!{% endtrans %} - -

- {% endif %} {{ wtforms_util.render_divs(register_form, True) }} {{ csrf_token }}
diff --git a/mediagoblin/templates/mediagoblin/base.html b/mediagoblin/templates/mediagoblin/base.html index f2f03382..77cecd93 100644 --- a/mediagoblin/templates/mediagoblin/base.html +++ b/mediagoblin/templates/mediagoblin/base.html @@ -47,9 +47,7 @@ {% include "mediagoblin/extra_head.html" %} {% template_hook("head") %} - {% if persona is defined %} - {% template_hook("persona_head") %} - {% endif %} + {% template_hook("persona_head") %} {% block mediagoblin_head %} {% endblock mediagoblin_head %} @@ -150,7 +148,7 @@ {% include "mediagoblin/utils/messages.html" %} {% block mediagoblin_content %} {% endblock mediagoblin_content %} - {% if persona is defined and csrf_token is defined %} + {% if csrf_token is defined %} {% template_hook("persona_form") %} {% endif %}
diff --git a/mediagoblin/templates/mediagoblin/edit/edit_account.html b/mediagoblin/templates/mediagoblin/edit/edit_account.html index 9bd488d7..51293acb 100644 --- a/mediagoblin/templates/mediagoblin/edit/edit_account.html +++ b/mediagoblin/templates/mediagoblin/edit/edit_account.html @@ -49,14 +49,6 @@

{% endif %} {% template_hook("edit_link") %} - {% template_hook("openid_edit_link") %} - {% if persona is defined %} -

- - {% trans %}Edit your Persona email addresses{% endtrans %} - -

- {% endif %} {{ wtforms_util.render_divs(form, True) }}
From 20d4995d810ce7c7dc0d9453508507cbaa6fdce6 Mon Sep 17 00:00:00 2001 From: Rodney Ewing Date: Wed, 3 Jul 2013 08:35:56 -0700 Subject: [PATCH 03/45] made it so that the create account button works with only persona enabled --- mediagoblin/plugins/persona/static/js/persona.js | 5 +++++ .../templates/mediagoblin/bits/frontpage_welcome.html | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/mediagoblin/plugins/persona/static/js/persona.js b/mediagoblin/plugins/persona/static/js/persona.js index 034f2574..a1d0172f 100644 --- a/mediagoblin/plugins/persona/static/js/persona.js +++ b/mediagoblin/plugins/persona/static/js/persona.js @@ -22,6 +22,11 @@ $(document).ready(function () { signinLink.onclick = function() { navigator.id.request(); }; } + var signinLink1 = document.getElementById('persona_login1'); + if (signinLink1) { + signinLink1.onclick = function() { navigator.id.request(); }; + } + var signoutLink = document.getElementById('logout'); if (signoutLink) { signoutLink.onclick = function() { navigator.id.logout(); }; diff --git a/mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html b/mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html index dbc23a1f..9ac63a56 100644 --- a/mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html +++ b/mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html @@ -28,7 +28,7 @@

{% trans %}Don't have one yet? It's easy!{% endtrans %}

Date: Tue, 9 Jul 2013 12:31:01 -0700 Subject: [PATCH 04/45] basic_auth documentation --- docs/source/plugindocs/basic_auth.rst | 2 ++ mediagoblin/plugins/basic_auth/README.rst | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 docs/source/plugindocs/basic_auth.rst create mode 100644 mediagoblin/plugins/basic_auth/README.rst diff --git a/docs/source/plugindocs/basic_auth.rst b/docs/source/plugindocs/basic_auth.rst new file mode 100644 index 00000000..83492ac2 --- /dev/null +++ b/docs/source/plugindocs/basic_auth.rst @@ -0,0 +1,2 @@ +.. include:: ../../../mediagoblin/plugins/basic_auth/README.rst + diff --git a/mediagoblin/plugins/basic_auth/README.rst b/mediagoblin/plugins/basic_auth/README.rst new file mode 100644 index 00000000..65a7345e --- /dev/null +++ b/mediagoblin/plugins/basic_auth/README.rst @@ -0,0 +1,22 @@ +=================== + Basic_auth plugin +=================== + +The basic_auth plugin is enabled by default in mediagoblin.ini. This plugin +provides basic username and password authentication for GNU Mediagoblin. + +This plugin can be enabled alongside :ref:`openid-chapter` and +:ref:`persona-chapter`. + +Set up the Basic_auth plugin +============================ + +1. Add the following to your MediaGoblin .ini file in the ``[plugins]`` section:: + + [[mediagoblin.plugins.basic_auth]] + +2. Run:: + + gmg assetlink + + in order to link basic_auth's static assets From 2c4cdd096f77772ae1f07a163e68c033c0fe1b2c Mon Sep 17 00:00:00 2001 From: Rodney Ewing Date: Tue, 9 Jul 2013 12:34:13 -0700 Subject: [PATCH 05/45] added openid docs --- docs/source/plugindocs/openid.rst | 2 ++ mediagoblin/plugins/openid/README.rst | 32 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 docs/source/plugindocs/openid.rst create mode 100644 mediagoblin/plugins/openid/README.rst diff --git a/docs/source/plugindocs/openid.rst b/docs/source/plugindocs/openid.rst new file mode 100644 index 00000000..045bf9d0 --- /dev/null +++ b/docs/source/plugindocs/openid.rst @@ -0,0 +1,2 @@ +.. include:: ../../../mediagoblin/plugins/openid/README.rst + diff --git a/mediagoblin/plugins/openid/README.rst b/mediagoblin/plugins/openid/README.rst new file mode 100644 index 00000000..e753b0f9 --- /dev/null +++ b/mediagoblin/plugins/openid/README.rst @@ -0,0 +1,32 @@ +=================== + Openid plugin +=================== + +The Openid plugin allows user to login to your GNU Mediagoblin instance using +their openid url. + +This plugin can be enabled alongside :ref:`basic_auth-chapter` and +:ref:`persona-chapter`. + +.. note:: + When :reg:`basic_auth-chapter` is enabled alongside this Openid plugin, and + a user creates an account using their Openid. If they would like to add a + password to their account, they can use the forgot password feature to do + so. + + +Set up the Openid plugin +============================ + +1. Install the ``python-openid`` package. + +2. Add the following to your MediaGoblin .ini file in the ``[plugins]`` section:: + + [[mediagoblin.plugins.openid]] + +3. Run:: + + gmg dbupdate + + in order to create and apply migrations to any database tables that the + plugin requires. From bd0ece0557d7ac4cbbf77617b125060b8c083892 Mon Sep 17 00:00:00 2001 From: Rodney Ewing Date: Tue, 9 Jul 2013 12:57:57 -0700 Subject: [PATCH 06/45] added basic_auth and openid docs to index and link them together --- docs/source/index.rst | 2 ++ mediagoblin/plugins/basic_auth/README.rst | 6 ++++-- mediagoblin/plugins/openid/README.rst | 12 +++++++----- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index c8a3f040..f6bc5561 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -59,6 +59,8 @@ Part 2: Core plugin documentation plugindocs/oauth plugindocs/trim_whitespace plugindocs/raven + plugindocs/basic_auth + plugindocs/openid Part 3: Plugin Writer's Guide diff --git a/mediagoblin/plugins/basic_auth/README.rst b/mediagoblin/plugins/basic_auth/README.rst index 65a7345e..82f247ed 100644 --- a/mediagoblin/plugins/basic_auth/README.rst +++ b/mediagoblin/plugins/basic_auth/README.rst @@ -1,5 +1,7 @@ +.. _basic_auth-chapter: + =================== - Basic_auth plugin + basic_auth plugin =================== The basic_auth plugin is enabled by default in mediagoblin.ini. This plugin @@ -8,7 +10,7 @@ provides basic username and password authentication for GNU Mediagoblin. This plugin can be enabled alongside :ref:`openid-chapter` and :ref:`persona-chapter`. -Set up the Basic_auth plugin +Set up the basic_auth plugin ============================ 1. Add the following to your MediaGoblin .ini file in the ``[plugins]`` section:: diff --git a/mediagoblin/plugins/openid/README.rst b/mediagoblin/plugins/openid/README.rst index e753b0f9..870a2b58 100644 --- a/mediagoblin/plugins/openid/README.rst +++ b/mediagoblin/plugins/openid/README.rst @@ -1,21 +1,23 @@ +.. _openid-chapter: + =================== - Openid plugin + openid plugin =================== -The Openid plugin allows user to login to your GNU Mediagoblin instance using +The openid plugin allows user to login to your GNU Mediagoblin instance using their openid url. This plugin can be enabled alongside :ref:`basic_auth-chapter` and :ref:`persona-chapter`. .. note:: - When :reg:`basic_auth-chapter` is enabled alongside this Openid plugin, and - a user creates an account using their Openid. If they would like to add a + When :ref:`basic_auth-chapter` is enabled alongside this openid plugin, and + a user creates an account using their openid. If they would like to add a password to their account, they can use the forgot password feature to do so. -Set up the Openid plugin +Set up the openid plugin ============================ 1. Install the ``python-openid`` package. From 26d2cce85ef629a58d80def85fcda8e324d5d182 Mon Sep 17 00:00:00 2001 From: Rodney Ewing Date: Tue, 9 Jul 2013 13:31:32 -0700 Subject: [PATCH 07/45] added docs for the persona plugin --- docs/source/index.rst | 1 + docs/source/plugindocs/persona.rst | 2 ++ mediagoblin/plugins/persona/README.rst | 41 ++++++++++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 docs/source/plugindocs/persona.rst create mode 100644 mediagoblin/plugins/persona/README.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index f6bc5561..0e7d0c2b 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -61,6 +61,7 @@ Part 2: Core plugin documentation plugindocs/raven plugindocs/basic_auth plugindocs/openid + plugindocs/persona Part 3: Plugin Writer's Guide diff --git a/docs/source/plugindocs/persona.rst b/docs/source/plugindocs/persona.rst new file mode 100644 index 00000000..2524127d --- /dev/null +++ b/docs/source/plugindocs/persona.rst @@ -0,0 +1,2 @@ +.. include:: ../../../mediagoblin/plugins/persona/README.rst + diff --git a/mediagoblin/plugins/persona/README.rst b/mediagoblin/plugins/persona/README.rst new file mode 100644 index 00000000..fd086129 --- /dev/null +++ b/mediagoblin/plugins/persona/README.rst @@ -0,0 +1,41 @@ +.. _persona-chapter: + +================ + persona plugin +================ + +The persona plugin allows users to login to you GNU MediaGoblin instance using +`Mozilla's Persona`_. + +This plugin can be enabled alongside :ref:`openid-chapter` and +:ref:`basic_auth-chapter`. + +.. note:: + When :ref:`basic_auth-chapter` is enabled alongside this persona plugin, and + a user creates an account using their persona. If they would like to add a + password to their account, they can use the forgot password feature to do + so. + +.. _Mozilla's Persona: https://www.mozilla.org/en-US/persona/ + +Set up the persona plugin +========================= + +1. Install the ``requests`` package. + +2. Add the following to your MediaGoblin .ini file in the ``[plugins]`` section:: + + [[mediagoblin.plugins.persona]] + +3. Run:: + + gmg dbupdate + + in order to create and apply migrations to any database tables that the + plugin requires. + +4. Run:: + + gmg assetlink + + in order to persona's static assets. From 94a566e60e79c39d74fdcbe668bca1d1d41d4aa3 Mon Sep 17 00:00:00 2001 From: Rodney Ewing Date: Tue, 9 Jul 2013 13:41:40 -0700 Subject: [PATCH 08/45] typo --- mediagoblin/plugins/persona/README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mediagoblin/plugins/persona/README.rst b/mediagoblin/plugins/persona/README.rst index fd086129..ef19ac5d 100644 --- a/mediagoblin/plugins/persona/README.rst +++ b/mediagoblin/plugins/persona/README.rst @@ -5,7 +5,7 @@ ================ The persona plugin allows users to login to you GNU MediaGoblin instance using -`Mozilla's Persona`_. +`Mozilla Persona`_. This plugin can be enabled alongside :ref:`openid-chapter` and :ref:`basic_auth-chapter`. @@ -16,7 +16,7 @@ This plugin can be enabled alongside :ref:`openid-chapter` and password to their account, they can use the forgot password feature to do so. -.. _Mozilla's Persona: https://www.mozilla.org/en-US/persona/ +.. _Mozilla Persona: https://www.mozilla.org/en-US/persona/ Set up the persona plugin ========================= From d194770dd24c70cf1306d1287ec2cf82f07e2107 Mon Sep 17 00:00:00 2001 From: Rodney Ewing Date: Wed, 10 Jul 2013 13:29:58 -0700 Subject: [PATCH 09/45] added docs explaining the authentication hooks --- docs/source/index.rst | 1 + docs/source/pluginwriter/authhooks.rst | 86 ++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 docs/source/pluginwriter/authhooks.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index 0e7d0c2b..723dfaf8 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -77,6 +77,7 @@ This guide covers writing new GNU MediaGoblin plugins. pluginwriter/database pluginwriter/api pluginwriter/tests + pluginwriter/authhooks Part 4: Developer's Zone diff --git a/docs/source/pluginwriter/authhooks.rst b/docs/source/pluginwriter/authhooks.rst new file mode 100644 index 00000000..9721d729 --- /dev/null +++ b/docs/source/pluginwriter/authhooks.rst @@ -0,0 +1,86 @@ +====================== + Authentication Hooks +====================== + +This documents the hooks that are currently available for authentication +plugins. If you need new hooks for your plugin, go ahead a submit a patch. + +What hooks are available? +========================= + +'authentication' +---------------- + +This hook just needs to return ``True`` as this is how +the MediaGoblin app knows that an authentication plugin is enabled. + + +'auth_extra_validation' +----------------------- + +This hook is used to provide any additional validation of the registration +form when using ``mediagoblin.auth.tools.register_user()``. This hook runs +through all enabled auth plugins. + + +'auth_create_user' +------------------ + +This hook is used by ``mediagoblin.auth.tools.register_user()`` so plugins can +store the necessary information when creating a user. This hook runs through +all enabled auth plugins. + +'auth_get_user' +--------------- + +This hook is used by ``mediagoblin.auth.tools.check_login_simple()``. Your +plugin should return a ``User`` object given a username. + +'auth_no_pass_redirect' +----------------------- + +This hook is called in ``mediagoblin.auth.views`` in both the ``login`` and +``register`` views. This hook should return the name of your plugin, so that +if :ref:`basic_auth-chapter` is not enabled, the user will be redirected to the +correct login and registration views for your plugin. + +The code assumes that it can generate a valid url given +``mediagoblin.plugins.{{ your_plugin_here }}.login`` and +``mediagoblin.plugins.{{ your_plugin_here }}.register``. This is only needed if +you will not be using the ``login`` and ``register`` views in +``mediagoblin.auth.views``. + +'auth_get_login_form' +--------------------- + +This hook is called in ``mediagoblin.auth.views.login()``. If you are not using +that view, then you do not need this hook. This hook should take a ``request`` +object and return the ``LoginForm`` for your plugin. + +'auth_get_registration_form' +---------------------------- + +This hook is called in ``mediagoblin.auth.views.register()``. If you are not +using that view, then you do not need this hook. This hook should take a +``request`` object and return the ``RegisterForm`` for your plugin. + +'auth_gen_password_hash' +------------------------ + +This hook should accept a ``raw_pass`` and an ``extra_salt`` and return a +hashed password to be stored in ``User.pw_hash``. + +'auth_check_password' +--------------------- + +This hook should accept a ``raw_pass``, a ``stored_hash``, and an ``extra_salt``. +Your plugin should then check that the ``raw_pass`` hashes to the same thing as +the ``stored_hash`` and return either ``True`` or ``False``. + +'auth_fake_login_attempt' +------------------------- + +This hook is called in ``mediagoblin.auth.tools.check_login_simple``. It is +called if a user is not found and should do something that takes the same amount +of time as your ``check_password`` function. This is to help prevent timining +attacks. From 04e08d422ad179de881e4394b99f2231d1b65a90 Mon Sep 17 00:00:00 2001 From: xray7224 Date: Thu, 27 Jun 2013 19:34:21 +0100 Subject: [PATCH 10/45] 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 11/45] 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 12/45] 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 13/45] 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 14/45] 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 15/45] 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 16/45] 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 17/45] 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 18/45] 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 %}
  • +
+
+ +
+ {{ csrf_token }} + {{ authorize_form.oauth_token }} + {{ authorize_form.oauth_verifier }} + +
+

+{% 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 19/45] 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 20/45] 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 21/45] 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 22/45] 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 23/45] 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 24/45] 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 25/45] 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 26/45] 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 27/45] 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 28/45] 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 29/45] 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 30/45] 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 31/45] 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 32/45] 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 33/45] 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 34/45] 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 35/45] 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 135dd5296b16a8612a787e8beca28ea805e32583 Mon Sep 17 00:00:00 2001 From: Rodney Ewing Date: Fri, 9 Aug 2013 11:38:55 -0700 Subject: [PATCH 36/45] change dropdown button to
from
so that it is tabbable --- mediagoblin/templates/mediagoblin/base.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mediagoblin/templates/mediagoblin/base.html b/mediagoblin/templates/mediagoblin/base.html index 1fc4467c..bbc77351 100644 --- a/mediagoblin/templates/mediagoblin/base.html +++ b/mediagoblin/templates/mediagoblin/base.html @@ -65,8 +65,8 @@ {{ notification_count }} {% endif %} -
-
+ + {% elif request.user and request.user.status == "needs_email_verification" %} {# the following link should only appear when verification is needed #} - {{ notification_count }} + + {{ notification_count }} {% endif %} diff --git a/mediagoblin/templates/mediagoblin/fragments/header_notifications.html b/mediagoblin/templates/mediagoblin/fragments/header_notifications.html index 613100aa..70d7935a 100644 --- a/mediagoblin/templates/mediagoblin/fragments/header_notifications.html +++ b/mediagoblin/templates/mediagoblin/fragments/header_notifications.html @@ -1,4 +1,4 @@ -{% set notifications = request.notifications.get_notifications(request.user.id) %} +{% set notifications = get_notifications(request.user.id) %} {% if notifications %}

{% trans %}New comments{% endtrans %}

diff --git a/mediagoblin/templates/mediagoblin/utils/comment-subscription.html b/mediagoblin/templates/mediagoblin/utils/comment-subscription.html index bd367e80..75da5e89 100644 --- a/mediagoblin/templates/mediagoblin/utils/comment-subscription.html +++ b/mediagoblin/templates/mediagoblin/utils/comment-subscription.html @@ -16,8 +16,7 @@ # along with this program. If not, see . #} {%- if request.user %} - {% set subscription = request.notifications.get_comment_subscription( - request.user.id, media.id) %} + {% set subscription = get_comment_subscription(request.user.id, media.id) %} {% if not subscription or not subscription.notify %} {{ csrf_token }} From fc714df00d933db2cab20f4b3a8f2b9a0ddb098d Mon Sep 17 00:00:00 2001 From: Christopher Allan Webber Date: Thu, 15 Aug 2013 18:29:35 -0500 Subject: [PATCH 41/45] Removing PyPump as we aren't using it yet and there's a dependency conflict ;| --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 0155b8ce..b485a2ff 100644 --- a/setup.py +++ b/setup.py @@ -64,7 +64,6 @@ setup( 'pytz', 'six', 'oauthlib', - 'pypump', ## This is optional! # 'translitcodec', ## For now we're expecting that users will install this from From 71b2bee6228164993394f74202e5acd82535c34d Mon Sep 17 00:00:00 2001 From: Rodney Ewing Date: Thu, 15 Aug 2013 17:36:56 -0700 Subject: [PATCH 42/45] fix persona tests by using a correct query --- mediagoblin/tests/test_persona.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mediagoblin/tests/test_persona.py b/mediagoblin/tests/test_persona.py index 1d03ea7f..ce795258 100644 --- a/mediagoblin/tests/test_persona.py +++ b/mediagoblin/tests/test_persona.py @@ -108,13 +108,13 @@ class TestPersonaPlugin(object): persona_plugin_app.get('/auth/logout/') # Get user and detach from session - test_user = mg_globals.database.User.find_one({ - 'username': u'chris'}) + test_user = mg_globals.database.User.query.filter_by( + username=u'chris').first() test_user.email_verified = True test_user.status = u'active' test_user.save() - test_user = mg_globals.database.User.find_one({ - 'username': u'chris'}) + test_user = mg_globals.database.User.query.filter_by( + username=u'chris').first() Session.expunge(test_user) # Add another user for _test_edit_persona From fb2f2bece2b088bb88d3f92b1cc6d71ad2d41a2d Mon Sep 17 00:00:00 2001 From: Rodney Ewing Date: Fri, 16 Aug 2013 11:48:03 -0700 Subject: [PATCH 43/45] use urlgen --- mediagoblin/plugins/persona/static/js/persona.js | 6 ++++-- .../templates/mediagoblin/plugins/persona/persona.html | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/mediagoblin/plugins/persona/static/js/persona.js b/mediagoblin/plugins/persona/static/js/persona.js index a1d0172f..a6def398 100644 --- a/mediagoblin/plugins/persona/static/js/persona.js +++ b/mediagoblin/plugins/persona/static/js/persona.js @@ -32,6 +32,8 @@ $(document).ready(function () { signoutLink.onclick = function() { navigator.id.logout(); }; } + var logout_url = document.getElementById('_logout_url').value; + navigator.id.watch({ onlogin: function(assertion) { document.getElementById('_assertion').value = assertion; @@ -39,8 +41,8 @@ $(document).ready(function () { }, onlogout: function() { $.ajax({ - type: 'POST', - url: '/auth/logout', + type: 'GET', + url: logout_url, success: function(res, status, xhr) { window.location.reload(); }, error: function(xhr, status, err) { alert("Logout failure: " + err); } }); diff --git a/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona.html b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona.html index ec0e1875..372bd246 100644 --- a/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona.html +++ b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona.html @@ -26,5 +26,7 @@ method="POST"> {{ csrf_token }} + {% endblock %} From e7c08e3550d19691d989d48d8d499eb7b2b9f80a Mon Sep 17 00:00:00 2001 From: Rodney Ewing Date: Fri, 16 Aug 2013 11:49:33 -0700 Subject: [PATCH 44/45] load js at the end of the page --- mediagoblin/plugins/persona/__init__.py | 2 +- .../persona/{persona_js_head.html => persona_js_end.html} | 0 mediagoblin/templates/mediagoblin/base.html | 2 -- mediagoblin/templates/mediagoblin/bits/body_end.html | 2 ++ 4 files changed, 3 insertions(+), 3 deletions(-) rename mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/{persona_js_head.html => persona_js_end.html} (100%) diff --git a/mediagoblin/plugins/persona/__init__.py b/mediagoblin/plugins/persona/__init__.py index d74ba0d7..700c18e2 100644 --- a/mediagoblin/plugins/persona/__init__.py +++ b/mediagoblin/plugins/persona/__init__.py @@ -48,7 +48,7 @@ def setup_plugin(): pluginapi.register_routes(routes) pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates')) pluginapi.register_template_hooks( - {'persona_head': 'mediagoblin/plugins/persona/persona_js_head.html', + {'persona_end': 'mediagoblin/plugins/persona/persona_js_end.html', 'persona_form': 'mediagoblin/plugins/persona/persona.html', 'edit_link': 'mediagoblin/plugins/persona/edit_link.html', 'login_link': 'mediagoblin/plugins/persona/login_link.html', diff --git a/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona_js_head.html b/mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona_js_end.html similarity index 100% rename from mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona_js_head.html rename to mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona_js_end.html diff --git a/mediagoblin/templates/mediagoblin/base.html b/mediagoblin/templates/mediagoblin/base.html index 483b6dfa..88027472 100644 --- a/mediagoblin/templates/mediagoblin/base.html +++ b/mediagoblin/templates/mediagoblin/base.html @@ -47,8 +47,6 @@ {% include "mediagoblin/extra_head.html" %} {% template_hook("head") %} - {% template_hook("persona_head") %} - {% block mediagoblin_head %} {% endblock mediagoblin_head %} diff --git a/mediagoblin/templates/mediagoblin/bits/body_end.html b/mediagoblin/templates/mediagoblin/bits/body_end.html index bb7b9762..c8f5e2d6 100644 --- a/mediagoblin/templates/mediagoblin/bits/body_end.html +++ b/mediagoblin/templates/mediagoblin/bits/body_end.html @@ -15,3 +15,5 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -#} + +{% template_hook("persona_end") %} From e7b8059f17c98ee88d933af52b0c4d858e882e8e Mon Sep 17 00:00:00 2001 From: Alon Levy Date: Fri, 16 Aug 2013 11:23:22 +0300 Subject: [PATCH 45/45] media_confirm_delete: redirect to next, then prev, then user home Instead of redirecting directly to user's home. Makes the flow for mass or just a few deletions easier. For really large deletions it would still make sense to have a dedicated view, but this is still way better then losing context by jumping back to the home view. Signed-off-by: Alon Levy --- mediagoblin/user_pages/views.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py index 91ea04b8..9d3cb08b 100644 --- a/mediagoblin/user_pages/views.py +++ b/mediagoblin/user_pages/views.py @@ -301,8 +301,12 @@ def media_confirm_delete(request, media): messages.add_message( request, messages.SUCCESS, _('You deleted the media.')) - return redirect(request, "mediagoblin.user_pages.user_home", - user=username) + location = media.url_to_next(request.urlgen) + if not location: + location=media.url_to_prev(request.urlgen) + if not location: + location="mediagoblin.user_pages.user_home" + return redirect(request, location=location, user=username) else: messages.add_message( request, messages.ERROR,