From 4f8f0a4e1f2d5a1eb64f47e62958d77bd12379f3 Mon Sep 17 00:00:00 2001 From: Rodney Ewing Date: Wed, 26 Jun 2013 12:04:45 -0700 Subject: [PATCH] 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() -- 2.25.1