aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--mediagoblin/auth/views.py14
-rw-r--r--mediagoblin/plugins/openid/__init__.py2
-rw-r--r--mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html1
-rw-r--r--mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login_link.html2
-rw-r--r--mediagoblin/plugins/persona/__init__.py116
-rw-r--r--mediagoblin/plugins/persona/forms.py41
-rw-r--r--mediagoblin/plugins/persona/models.py36
-rw-r--r--mediagoblin/plugins/persona/static/js/persona.js49
-rw-r--r--mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/edit.html43
-rw-r--r--mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/edit_link.html25
-rw-r--r--mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/login_link.html25
-rw-r--r--mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona.html30
-rw-r--r--mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/persona_js_head.html21
-rw-r--r--mediagoblin/plugins/persona/templates/mediagoblin/plugins/persona/register_link.html25
-rw-r--r--mediagoblin/plugins/persona/views.py191
-rw-r--r--mediagoblin/templates/mediagoblin/base.html31
-rw-r--r--mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html10
-rw-r--r--mediagoblin/tests/auth_configs/persona_appconfig.ini42
-rw-r--r--mediagoblin/tests/test_persona.py210
19 files changed, 904 insertions, 10 deletions
diff --git a/mediagoblin/auth/views.py b/mediagoblin/auth/views.py
index d54762b0..dd71d5c1 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/__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 33df7200..8d74c2b9 100644
--- a/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html
+++ b/mediagoblin/plugins/openid/templates/mediagoblin/plugins/openid/login.html
@@ -44,6 +44,7 @@
{% trans %}Log in to create an account!{% endtrans %}
</p>
{% endif %}
+ {% template_hook('login_link') %}
{% if pass_auth is defined %}
<p>
<a href="{{ request.urlgen('mediagoblin.auth.login') }}?{{ request.query_string }}">
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 %}
<p>
<a href="{{ request.urlgen('mediagoblin.plugins.openid.login') }}?{{ request.query_string }}">
{%- trans %}Or login with OpenID!{% endtrans %}
</a>
</p>
+ {% endif %}
{% endblock %}
diff --git a/mediagoblin/plugins/persona/__init__.py b/mediagoblin/plugins/persona/__init__.py
new file mode 100644
index 00000000..d74ba0d7
--- /dev/null
+++ b/mediagoblin/plugins/persona/__init__.py
@@ -0,0 +1,116 @@
+# 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 <http://www.gnu.org/licenses/>.
+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',
+ '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):
+ 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 <http://www.gnu.org/licenses/>.
+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 <http://www.gnu.org/licenses/>.
+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..a1d0172f
--- /dev/null
+++ b/mediagoblin/plugins/persona/static/js/persona.js
@@ -0,0 +1,49 @@
+/**
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+$(document).ready(function () {
+ var signinLink = document.getElementById('persona_login');
+ if (signinLink) {
+ 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(); };
+ }
+
+ 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 <http://www.gnu.org/licenses/>.
+#}
+{% extends "mediagoblin/base.html" %}
+
+{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
+
+{% block title -%}
+ {% trans %}Add an OpenID{% endtrans %} &mdash; {{ super() }}
+{%- endblock %}
+
+{% block mediagoblin_content %}
+ <form action="{{ request.urlgen('mediagoblin.plugins.persona.edit') }}"
+ method="POST" enctype="multipart/form-data">
+ {{ csrf_token }}
+ <div class="form_box">
+ <h1>{% trans %}Delete a Persona email address{% endtrans %}</h1>
+ <p>
+ <a href="javascript:;" id="persona_login">
+ {% trans %}Add a Persona email address{% endtrans %}
+ </a>
+ </p>
+ {{ wtforms_util.render_divs(form, True) }}
+ <div class="form_submit_buttons">
+ <input type="submit" value="{% trans %}Delete{% endtrans %}" class="button_form"/>
+ </div>
+ </div>
+ </form>
+{% endblock %}
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 <http://www.gnu.org/licenses/>.
+#}
+
+{% block persona_edit_link %}
+ <p>
+ <a href="{{ request.urlgen('mediagoblin.plugins.persona.edit') }}">
+ {% trans %}Edit your Persona email addresses{% endtrans %}
+ </a>
+ </p>
+{% 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 <http://www.gnu.org/licenses/>.
+#}
+
+{% block person_login_link %}
+ <p>
+ <a href="javascript:;" id="persona_login">
+ {% trans %}Or login with Persona!{% endtrans %}
+ </a>
+ </p>
+{% 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..ec0e1875
--- /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 <http://www.gnu.org/licenses/>.
+#}
+{% block persona %}
+ <form id="_persona_login"
+ action=
+ {%- if edit_persona is defined -%}
+ "{{ request.urlgen('mediagoblin.plugins.persona.add') }}"
+ {%- else -%}
+ "{{ request.urlgen('mediagoblin.plugins.persona.login') }}"
+ {%- endif %}
+ method="POST">
+ {{ csrf_token }}
+ <input type="hidden" name="assertion" type="text" id="_assertion"/>
+ </form>
+{% 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 <http://www.gnu.org/licenses/>.
+#}
+
+<script src="https://login.persona.org/include.js"></script>
+<script type="text/javascript"
+ src="{{ request.staticdirect('/js/persona.js', 'coreplugin_persona') }}"></script>
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 <http://www.gnu.org/licenses/>.
+#}
+
+{% block persona_register_link %}
+ <p>
+ <a href="javascript:;" id="persona_login">
+ {% trans %}Or register with Persona!{% endtrans %}
+ </a>
+ </p>
+{% endblock %}
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 <http://www.gnu.org/licenses/>.
+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/base.html b/mediagoblin/templates/mediagoblin/base.html
index 65dbebc0..483b6dfa 100644
--- a/mediagoblin/templates/mediagoblin/base.html
+++ b/mediagoblin/templates/mediagoblin/base.html
@@ -23,6 +23,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="IE=Edge">
<title>{% block title %}{{ app_config['html_title'] }}{% endblock %}</title>
<link rel="stylesheet" type="text/css"
href="{{ request.staticdirect('/css/extlib/reset.css') }}"/>
@@ -46,6 +47,8 @@
{% include "mediagoblin/extra_head.html" %}
{% template_hook("head") %}
+ {% template_hook("persona_head") %}
+
{% block mediagoblin_head %}
{% endblock mediagoblin_head %}
</head>
@@ -73,11 +76,22 @@
user=request.user.username) }}"
class="button_action_highlight">
{% trans %}Verify your email!{% endtrans %}</a>
- or <a href="{{ request.urlgen('mediagoblin.auth.logout') }}">{% trans %}log out{% endtrans %}</a>
+ or <a id="logout" href=
+ {% if persona is not defined %}
+ "{{ request.urlgen('mediagoblin.auth.logout') }}"
+ {% else %}
+ "javascript:;"
+ {% endif %}
+ >{% trans %}log out{% endtrans %}</a>
{% endif %}
{%- elif auth %}
- <a href="{{ request.urlgen('mediagoblin.auth.login') }}?next={{
- request.base_url|urlencode }}">
+ <a href=
+ {% if persona_auth is defined %}
+ "javascript:;" id="persona_login"
+ {% else %}
+ "{{ request.urlgen('mediagoblin.auth.login') }}"
+ {% endif %}
+ >
{%- trans %}Log in{% endtrans -%}
</a>
{%- endif %}
@@ -101,7 +115,13 @@
{%- trans %}Media processing panel{% endtrans -%}
</a>
&middot;
- <a href="{{ request.urlgen('mediagoblin.auth.logout') }}">{% trans %}Log out{% endtrans %}</a>
+ <a id="logout" href=
+ {% if persona is not defined %}
+ "{{ request.urlgen('mediagoblin.auth.logout') }}"
+ {% else %}
+ "javascript:;"
+ {% endif %}
+ >{% trans %}Log out{% endtrans %}</a>
</p>
<a class="button_action" href="{{ request.urlgen('mediagoblin.submit.start') }}">
{%- trans %}Add media{% endtrans -%}
@@ -128,6 +148,9 @@
{% include "mediagoblin/utils/messages.html" %}
{% block mediagoblin_content %}
{% endblock mediagoblin_content %}
+ {% if csrf_token is defined %}
+ {% template_hook("persona_form") %}
+ {% endif %}
</div>
{%- include "mediagoblin/bits/base_footer.html" %}
</div>
diff --git a/mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html b/mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html
index e2463328..4e55e618 100644
--- a/mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html
+++ b/mediagoblin/templates/mediagoblin/bits/frontpage_welcome.html
@@ -26,8 +26,14 @@
<p>{% trans %}To add your own media, place comments, and more, you can log in with your MediaGoblin account.{% endtrans %}</p>
{% if allow_registration %}
<p>{% trans %}Don't have one yet? It's easy!{% endtrans %}</p>
- {% trans register_url=request.urlgen('mediagoblin.auth.register') -%}
- <a class="button_action_highlight" href="{{ register_url }}">Create an account at this site</a>
+ <a class="button_action_highlight" href=
+ {% if persona_auth is defined %}
+ "javascript:;" id="persona_login1"
+ {% else %}
+ "{{ request.urlgen('mediagoblin.auth.register') }}"
+ {% endif %}
+ {% trans %}
+ >Create an account at this site</a>
or
{%- endtrans %}
{% endif %}
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 <http://www.gnu.org/licenses/>.
+[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 <http://www.gnu.org/licenses/>.
+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()