diff options
-rw-r--r-- | docs/source/index.rst | 1 | ||||
-rw-r--r-- | docs/source/plugindocs/ldap.rst | 2 | ||||
-rw-r--r-- | mediagoblin/plugins/ldap/README.rst | 49 | ||||
-rw-r--r-- | mediagoblin/plugins/ldap/__init__.py | 59 | ||||
-rw-r--r-- | mediagoblin/plugins/ldap/forms.py | 40 | ||||
-rw-r--r-- | mediagoblin/plugins/ldap/templates/mediagoblin/plugins/ldap/create_account_link.html | 25 | ||||
-rw-r--r-- | mediagoblin/plugins/ldap/tools.py | 65 | ||||
-rw-r--r-- | mediagoblin/plugins/ldap/views.py | 104 | ||||
-rw-r--r-- | mediagoblin/tests/auth_configs/ldap_appconfig.ini | 41 | ||||
-rw-r--r-- | mediagoblin/tests/test_ldap.py | 125 |
10 files changed, 511 insertions, 0 deletions
diff --git a/docs/source/index.rst b/docs/source/index.rst index e7fda4ad..75a5b66e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -61,6 +61,7 @@ Part 2: Core plugin documentation plugindocs/basic_auth plugindocs/openid plugindocs/persona + plugindocs/ldap Part 3: Plugin Writer's Guide diff --git a/docs/source/plugindocs/ldap.rst b/docs/source/plugindocs/ldap.rst new file mode 100644 index 00000000..3938c0c7 --- /dev/null +++ b/docs/source/plugindocs/ldap.rst @@ -0,0 +1,2 @@ +.. include:: ../../../mediagoblin/plugins/ldap/README.rst + diff --git a/mediagoblin/plugins/ldap/README.rst b/mediagoblin/plugins/ldap/README.rst new file mode 100644 index 00000000..ae03b31e --- /dev/null +++ b/mediagoblin/plugins/ldap/README.rst @@ -0,0 +1,49 @@ +============= + ldap plugin +============= + +.. Warning:: + This plugin is not compatible with the other authentication plugins. + +This plugin allow your GNU Mediagoblin instance to authenticate against an +LDAP server. + +Set up the ldap plugin +====================== + +1. Install the ``python-ldap`` package. + +2. Add the following to your MediaGoblin .ini file in the ``[plugins]`` section:: + + [[mediagoblin.plugins.ldap]] + +Configuring the ldap plugin +=========================== + +This plugin allows you to use multiple ldap servers for authentication. + +In order to configure a server, add the following to you MediaGoblin .ini file +under the ldap plugin:: + + [[mediagoblin.plugins.ldap]] + [[[server1]]] + LDAP_SERVER_URI = 'ldap://ldap.testathon.net:389' + LDAP_USER_DN_TEMPLATE = 'cn={username},ou=users,dc=testathon,dc=net' + [[[server2]]] + ... + +Make any necessary changes to the above to work with your sever. Make sure +``{username}`` is where the username should be in LDAP_USER_DN_TEMPLATE. + +If you would like to fetch the users email from the ldap server upon account +registration, add ``LDAP_SEARCH_BASE = 'ou=users,dc=testathon,dc=net'`` and +``EMAIL_SEARCH_FIELD = 'mail'`` under you server configuration in your +MediaGoblin .ini file. + +.. Warning:: + By default, this plugin provides no encryption when communicating with the + ldap servers. If you would like to use an SSL connection, change + LDAP_SERVER_URI to use ``ldaps://`` and whichever port you use. Default ldap + port for SSL connections is 636. If you would like to use a TLS connection, + add ``LDAP_START_TLS = 'true'`` under your server configuration in your + MediaGoblin .ini file. diff --git a/mediagoblin/plugins/ldap/__init__.py b/mediagoblin/plugins/ldap/__init__.py new file mode 100644 index 00000000..4673acee --- /dev/null +++ b/mediagoblin/plugins/ldap/__init__.py @@ -0,0 +1,59 @@ +# 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 os + +from mediagoblin.auth.tools import create_basic_user +from mediagoblin.tools import pluginapi + +PLUGIN_DIR = os.path.dirname(__file__) + + +def setup_plugin(): + config = pluginapi.get_config('mediagoblin.plugins.ldap') + + routes = [ + ('mediagoblin.plugins.ldap.register', + '/auth/ldap/register/', + 'mediagoblin.plugins.ldap.views:register'), + ('mediagoblin.plugins.ldap.login', + '/auth/ldap/login/', + 'mediagoblin.plugins.ldap.views:login')] + + pluginapi.register_routes(routes) + pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates')) + + pluginapi.register_template_hooks( + {'create_account': 'mediagoblin/plugins/ldap/create_account_link.html'}) + + +def create_user(register_form): + if 'username' in register_form and 'password' not in register_form: + return create_basic_user(register_form) + + +def no_pass_redirect(): + return 'ldap' + + +def auth(): + return True + +hooks = { + 'setup': setup_plugin, + 'authentication': auth, + 'auth_no_pass_redirect': no_pass_redirect, + 'auth_create_user': create_user, +} diff --git a/mediagoblin/plugins/ldap/forms.py b/mediagoblin/plugins/ldap/forms.py new file mode 100644 index 00000000..7ec1479e --- /dev/null +++ b/mediagoblin/plugins/ldap/forms.py @@ -0,0 +1,40 @@ +# 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 RegisterForm(wtforms.Form): + username = wtforms.HiddenField( + '', + [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)]) + + +class LoginForm(wtforms.Form): + username = wtforms.TextField( + _('Username'), + [wtforms.validators.Required(), + normalize_user_or_email_field()]) + password = wtforms.PasswordField( + _('Password'), + [wtforms.validators.Required()]) diff --git a/mediagoblin/plugins/ldap/templates/mediagoblin/plugins/ldap/create_account_link.html b/mediagoblin/plugins/ldap/templates/mediagoblin/plugins/ldap/create_account_link.html new file mode 100644 index 00000000..947e4ae2 --- /dev/null +++ b/mediagoblin/plugins/ldap/templates/mediagoblin/plugins/ldap/create_account_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 create_account %} + {% if allow_registration %} + <p> + {% trans %}Sign in to create an account!{% endtrans %} + </p> + {% endif %} +{% endblock %} diff --git a/mediagoblin/plugins/ldap/tools.py b/mediagoblin/plugins/ldap/tools.py new file mode 100644 index 00000000..1c436792 --- /dev/null +++ b/mediagoblin/plugins/ldap/tools.py @@ -0,0 +1,65 @@ +# 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 ldap +import logging + +from mediagoblin.tools import pluginapi + +_log = logging.getLogger(__name__) + + +class LDAP(object): + def __init__(self): + self.ldap_settings = pluginapi.get_config('mediagoblin.plugins.ldap') + + def _connect(self, server): + _log.info('Connecting to {0}.'.format(server['LDAP_SERVER_URI'])) + self.conn = ldap.initialize(server['LDAP_SERVER_URI']) + + if server['LDAP_START_TLS'] == 'true': + _log.info('Initiating TLS') + self.conn.start_tls_s() + + def _get_email(self, server, username): + try: + results = self.conn.search_s(server['LDAP_SEARCH_BASE'], + ldap.SCOPE_SUBTREE, 'uid={0}' + .format(username), + [server['EMAIL_SEARCH_FIELD']]) + + email = results[0][1][server['EMAIL_SEARCH_FIELD']][0] + except KeyError: + email = None + + return email + + def login(self, username, password): + for k, v in self.ldap_settings.iteritems(): + try: + self._connect(v) + user_dn = v['LDAP_USER_DN_TEMPLATE'].format(username=username) + self.conn.simple_bind_s(user_dn, password.encode('utf8')) + email = self._get_email(v, username) + return username, email + + except ldap.LDAPError, e: + _log.info(e) + + finally: + _log.info('Unbinding {0}.'.format(v['LDAP_SERVER_URI'])) + self.conn.unbind() + + return False, None diff --git a/mediagoblin/plugins/ldap/views.py b/mediagoblin/plugins/ldap/views.py new file mode 100644 index 00000000..aef1bf56 --- /dev/null +++ b/mediagoblin/plugins/ldap/views.py @@ -0,0 +1,104 @@ +# 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 mediagoblin import mg_globals, messages +from mediagoblin.auth.tools import register_user +from mediagoblin.db.models import User +from mediagoblin.decorators import allow_registration, auth_enabled +from mediagoblin.plugins.ldap import forms +from mediagoblin.plugins.ldap.tools import LDAP +from mediagoblin.tools.translate import pass_to_ugettext as _ +from mediagoblin.tools.response import redirect, render_to_response + + +@auth_enabled +def login(request): + login_form = forms.LoginForm(request.form) + + login_failed = False + + if request.method == 'POST' and login_form.validate(): + l = LDAP() + username, email = l.login(login_form.username.data, + login_form.password.data) + + if username: + user = User.query.filter_by( + username=username).first() + + if user: + # set up login in session + request.session['user_id'] = unicode(user.id) + request.session.save() + + if request.form.get('next'): + return redirect(request, location=request.form['next']) + else: + 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.RegisterForm(username=username, + email=email) + + return render_to_response( + request, + 'mediagoblin/auth/register.html', + {'register_form': register_form, + 'post_url': request.urlgen('mediagoblin.plugins.ldap.register')}) + + login_failed = True + + return render_to_response( + request, + 'mediagoblin/auth/login.html', + {'login_form': login_form, + 'next': request.GET.get('next') or request.form.get('next'), + 'login_failed': login_failed, + 'post_url': request.urlgen('mediagoblin.plugins.ldap.login'), + 'allow_registration': mg_globals.app_config["allow_registration"]}) + + +@allow_registration +@auth_enabled +def register(request): + if request.method == 'GET': + return redirect( + request, + 'mediagoblin.plugins.ldap.login') + + register_form = forms.RegisterForm(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.ldap.register')}) diff --git a/mediagoblin/tests/auth_configs/ldap_appconfig.ini b/mediagoblin/tests/auth_configs/ldap_appconfig.ini new file mode 100644 index 00000000..9be37e17 --- /dev/null +++ b/mediagoblin/tests/auth_configs/ldap_appconfig.ini @@ -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/>. +[mediagoblin] +direct_remote_path = /test_static/ +email_sender_address = "notice@mediagoblin.example.org" +email_debug_mode = true + +sql_engine = "sqlite://" +run_migrations = true + +# 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.ldap]] diff --git a/mediagoblin/tests/test_ldap.py b/mediagoblin/tests/test_ldap.py new file mode 100644 index 00000000..2f9a1372 --- /dev/null +++ b/mediagoblin/tests/test_ldap.py @@ -0,0 +1,125 @@ +# 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 + +pytest.importorskip("ldap") + + +@pytest.fixture() +def ldap_plugin_app(request): + return get_app( + request, + mgoblin_config=pkg_resources.resource_filename( + 'mediagoblin.tests.auth_configs', + 'ldap_appconfig.ini')) + + +def return_value(): + return u'chris', u'chris@example.com' + + +def test_ldap_plugin(ldap_plugin_app): + res = ldap_plugin_app.get('/auth/login/') + + assert urlparse.urlsplit(res.location)[2] == '/auth/ldap/login/' + + res = ldap_plugin_app.get('/auth/register/') + + assert urlparse.urlsplit(res.location)[2] == '/auth/ldap/register/' + + res = ldap_plugin_app.get('/auth/ldap/register/') + + assert urlparse.urlsplit(res.location)[2] == '/auth/ldap/login/' + + template.clear_test_template_context() + res = ldap_plugin_app.post( + '/auth/ldap/login/', {}) + + context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/login.html'] + form = context['login_form'] + assert form.username.errors == [u'This field is required.'] + assert form.password.errors == [u'This field is required.'] + + @mock.patch('mediagoblin.plugins.ldap.tools.LDAP.login', mock.Mock(return_value=return_value())) + def _test_authentication(): + template.clear_test_template_context() + res = ldap_plugin_app.post( + '/auth/ldap/login/', + {'username': u'chris', + 'password': u'toast'}) + + context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html'] + register_form = context['register_form'] + + assert register_form.username.data == u'chris' + assert register_form.email.data == u'chris@example.com' + + template.clear_test_template_context() + res = ldap_plugin_app.post( + '/auth/ldap/register/', + {'username': u'chris', + 'email': u'chris@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 with same email and username + template.clear_test_template_context() + res = ldap_plugin_app.post( + '/auth/ldap/register/', + {'username': u'chris', + 'email': u'chris@example.com'}) + + context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html'] + register_form = context['register_form'] + + assert register_form.email.errors == [u'Sorry, a user with that email address already exists.'] + assert register_form.username.errors == [u'Sorry, a user with that name already exists.'] + + # Log out + ldap_plugin_app.get('/auth/logout/') + + # Get user and detach from session + test_user = mg_globals.database.User.find_one({ + 'username': u'chris'}) + Session.expunge(test_user) + + # Log back in + template.clear_test_template_context() + res = ldap_plugin_app.post( + '/auth/ldap/login/', + {'username': u'chris', + 'password': u'toast'}) + 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_authentication() |