From daf29c011a7224eef95fe3eb0e5f45f385abc869 Mon Sep 17 00:00:00 2001 From: Rodney Ewing Date: Mon, 1 Jul 2013 11:46:57 -0700 Subject: [PATCH 01/13] starting ldap plugin --- mediagoblin/plugins/ldap/__init__.py | 62 ++++++++++++++++++++++++++++ mediagoblin/plugins/ldap/tools.py | 60 +++++++++++++++++++++++++++ mediagoblin/plugins/ldap/views.py | 44 ++++++++++++++++++++ 3 files changed, 166 insertions(+) create mode 100644 mediagoblin/plugins/ldap/__init__.py create mode 100644 mediagoblin/plugins/ldap/tools.py create mode 100644 mediagoblin/plugins/ldap/views.py diff --git a/mediagoblin/plugins/ldap/__init__.py b/mediagoblin/plugins/ldap/__init__.py new file mode 100644 index 00000000..a46a0ed3 --- /dev/null +++ b/mediagoblin/plugins/ldap/__init__.py @@ -0,0 +1,62 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from mediagoblin.auth.tools import create_basic_user +from mediagoblin.plugins.ldap.tools import LDAP +from mediagoblin.plugins.ldap import forms +from mediagoblin.tools import pluginapi + + +def setup_plugin(): + config = pluginapi.get_config('mediagoblin.plugins.ldap') + + routes = [ + ('mediagoblin.plugins.ldap.register', + '/auth/ldap/register/', + 'mediagoblin.plugins.ldap.views:register')] + pluginapi.register_routes(routes) + + +def check_login_simple(username, password, request): + l = LDAP(request) + return l.login(username, password) + + +def create_user(register_form): + user = create_basic_user(register_form) + return user + + +def get_login_form(request): + return forms.LoginForm(request.form) + + +def auth(): + return True + + +def append_to_global_context(context): + context['pass_auth'] = True + return context + +hooks = { + 'setup': setup_plugin, + 'authentication': auth, + 'auth_check_login_simple': check_login_simple, + 'auth_create_user': create_user, + 'template_global_context': append_to_global_context, + 'auth_get_login_form': get_login_form, +} diff --git a/mediagoblin/plugins/ldap/tools.py b/mediagoblin/plugins/ldap/tools.py new file mode 100644 index 00000000..6134aaba --- /dev/null +++ b/mediagoblin/plugins/ldap/tools.py @@ -0,0 +1,60 @@ +# 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 ldap +import logging + +from mediagoblin import mg_globals +from mediagoblin.db.models import User +from mediagoblin.tools.response import redirect + +_log = logging.getLogger(__name__) + + +class LDAP(object): + def __init__(self, request): + self.ldap_settings = mg_globals.global_config['plugins']['mediagoblin.plugins.ldap'] + self.request = request + + def _connect(self, server): + _log.info('Connecting to {0}.'.format(server['LDAP_HOST'])) + self.conn = ldap.initialize('ldap://{0}:{1}/'.format( + server['LDAP_HOST'], server['LDAP_PORT'])) + + def login(self, username, password): + for k, v in self.ldap_settings.iteritems(): + try: + import ipdb + ipdb.set_trace() + self._connect(v) + user_dn = v['USER_DN_TEMPLATE'].format(username=username) + self.conn.simple_bind_s(user_dn, password.encode('utf8')) + return self._get_or_create_user(username) + + except ldap.LDAPError, e: + _log.info(e) + + return None + + def _get_or_create_user(self, username): + user = User.query.filter_by( + username=username).first() + + if user: + return user + + self.request.session['username'] = username + redirect( + self.request, 'mediagoblin.plugins.ldap.register') diff --git a/mediagoblin/plugins/ldap/views.py b/mediagoblin/plugins/ldap/views.py new file mode 100644 index 00000000..95132f96 --- /dev/null +++ b/mediagoblin/plugins/ldap/views.py @@ -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 . +from mediagoblin.auth.tools import register_user +from mediagoblin.plugins.ldap import forms +from mediagoblin.tools.response import redirect, render_to_response + + +def register(request): + username = request.session.pop('username') + if 'email' in request.session: + email = request.session.pop('email') + else: + email = None + register_form = forms.RegisterForm(request.form, username=username, + email=email) + + if request.method == 'POST' and 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')}) From c4513740bff20a0807b160c4bebf9a9a8955c03f Mon Sep 17 00:00:00 2001 From: Rodney Ewing Date: Mon, 8 Jul 2013 16:36:38 -0700 Subject: [PATCH 02/13] ldap uses it own views --- mediagoblin/plugins/ldap/__init__.py | 27 ++----- mediagoblin/plugins/ldap/forms.py | 40 ++++++++++ mediagoblin/plugins/ldap/tools.py | 22 +----- mediagoblin/plugins/ldap/views.py | 77 ++++++++++++++++--- .../templates/mediagoblin/auth/login.html | 2 +- 5 files changed, 120 insertions(+), 48 deletions(-) create mode 100644 mediagoblin/plugins/ldap/forms.py diff --git a/mediagoblin/plugins/ldap/__init__.py b/mediagoblin/plugins/ldap/__init__.py index a46a0ed3..18203c92 100644 --- a/mediagoblin/plugins/ldap/__init__.py +++ b/mediagoblin/plugins/ldap/__init__.py @@ -15,7 +15,6 @@ # along with this program. If not, see . from mediagoblin.auth.tools import create_basic_user -from mediagoblin.plugins.ldap.tools import LDAP from mediagoblin.plugins.ldap import forms from mediagoblin.tools import pluginapi @@ -26,37 +25,27 @@ def setup_plugin(): routes = [ ('mediagoblin.plugins.ldap.register', '/auth/ldap/register/', - 'mediagoblin.plugins.ldap.views:register')] + 'mediagoblin.plugins.ldap.views:register'), + ('mediagoblin.plugins.ldap.login', + '/auth/ldap/login/', + 'mediagoblin.plugins.ldap.views:login')] pluginapi.register_routes(routes) -def check_login_simple(username, password, request): - l = LDAP(request) - return l.login(username, password) - - def create_user(register_form): - user = create_basic_user(register_form) - return user + return create_basic_user(register_form) -def get_login_form(request): - return forms.LoginForm(request.form) +def no_pass_redirect(): + return 'ldap' def auth(): return True - -def append_to_global_context(context): - context['pass_auth'] = True - return context - hooks = { 'setup': setup_plugin, 'authentication': auth, - 'auth_check_login_simple': check_login_simple, + 'auth_no_pass_redirect': no_pass_redirect, 'auth_create_user': create_user, - 'template_global_context': append_to_global_context, - 'auth_get_login_form': get_login_form, } 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 . +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/tools.py b/mediagoblin/plugins/ldap/tools.py index 6134aaba..05cff5f9 100644 --- a/mediagoblin/plugins/ldap/tools.py +++ b/mediagoblin/plugins/ldap/tools.py @@ -17,16 +17,13 @@ import ldap import logging from mediagoblin import mg_globals -from mediagoblin.db.models import User -from mediagoblin.tools.response import redirect _log = logging.getLogger(__name__) class LDAP(object): - def __init__(self, request): + def __init__(self): self.ldap_settings = mg_globals.global_config['plugins']['mediagoblin.plugins.ldap'] - self.request = request def _connect(self, server): _log.info('Connecting to {0}.'.format(server['LDAP_HOST'])) @@ -36,25 +33,12 @@ class LDAP(object): def login(self, username, password): for k, v in self.ldap_settings.iteritems(): try: - import ipdb - ipdb.set_trace() self._connect(v) user_dn = v['USER_DN_TEMPLATE'].format(username=username) self.conn.simple_bind_s(user_dn, password.encode('utf8')) - return self._get_or_create_user(username) + return username except ldap.LDAPError, e: _log.info(e) - return None - - def _get_or_create_user(self, username): - user = User.query.filter_by( - username=username).first() - - if user: - return user - - self.request.session['username'] = username - redirect( - self.request, 'mediagoblin.plugins.ldap.register') + return False diff --git a/mediagoblin/plugins/ldap/views.py b/mediagoblin/plugins/ldap/views.py index 95132f96..217c6d8c 100644 --- a/mediagoblin/plugins/ldap/views.py +++ b/mediagoblin/plugins/ldap/views.py @@ -13,21 +13,80 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +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 -def register(request): - username = request.session.pop('username') - if 'email' in request.session: - email = request.session.pop('email') - else: - email = None - register_form = forms.RegisterForm(request.form, username=username, - email=email) +@auth_enabled +def login(request): + login_form = forms.LoginForm(request.form) - if request.method == 'POST' and register_form.validate(): + login_failed = False + + if request.method == 'POST' and login_form.validate(): + l = LDAP() + username = 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(request.form, + username=username) + + 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: diff --git a/mediagoblin/templates/mediagoblin/auth/login.html b/mediagoblin/templates/mediagoblin/auth/login.html index 3329b5d0..49e906db 100644 --- a/mediagoblin/templates/mediagoblin/auth/login.html +++ b/mediagoblin/templates/mediagoblin/auth/login.html @@ -48,7 +48,7 @@ {% endif %} {% template_hook("login_link") %} {{ wtforms_util.render_divs(login_form, True) }} - {% if pass_auth %} + {% if pass_auth is defined %}

{% trans %}Forgot your password?{% endtrans %} From 11782c0061c4c386fc5d8315b33a6d8464e83013 Mon Sep 17 00:00:00 2001 From: Rodney Ewing Date: Tue, 9 Jul 2013 09:37:23 -0700 Subject: [PATCH 03/13] - changed host and port to just a server uri - added an option to connect with TLS - unbind after when done --- mediagoblin/plugins/ldap/tools.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/mediagoblin/plugins/ldap/tools.py b/mediagoblin/plugins/ldap/tools.py index 05cff5f9..fd13cfe4 100644 --- a/mediagoblin/plugins/ldap/tools.py +++ b/mediagoblin/plugins/ldap/tools.py @@ -26,19 +26,26 @@ class LDAP(object): self.ldap_settings = mg_globals.global_config['plugins']['mediagoblin.plugins.ldap'] def _connect(self, server): - _log.info('Connecting to {0}.'.format(server['LDAP_HOST'])) - self.conn = ldap.initialize('ldap://{0}:{1}/'.format( - server['LDAP_HOST'], server['LDAP_PORT'])) + _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 login(self, username, password): for k, v in self.ldap_settings.iteritems(): try: self._connect(v) - user_dn = v['USER_DN_TEMPLATE'].format(username=username) + user_dn = v['LDAP_USER_DN_TEMPLATE'].format(username=username) self.conn.simple_bind_s(user_dn, password.encode('utf8')) return username except ldap.LDAPError, e: _log.info(e) + finally: + _log.info('Unbinding {0}.').format(v['LDAP_SERVER_URI']) + self.conn.unbind() + return False From 517eb8b4433888a3ac11f0ed9efeb30dca68838b Mon Sep 17 00:00:00 2001 From: Rodney Ewing Date: Tue, 9 Jul 2013 10:44:44 -0700 Subject: [PATCH 04/13] - fixed typo with unbinding code - added the ability to get the user's email from the ldap server upon registration --- mediagoblin/plugins/ldap/tools.py | 20 +++++++++++++++++--- mediagoblin/plugins/ldap/views.py | 7 ++++--- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/mediagoblin/plugins/ldap/tools.py b/mediagoblin/plugins/ldap/tools.py index fd13cfe4..3f15c07a 100644 --- a/mediagoblin/plugins/ldap/tools.py +++ b/mediagoblin/plugins/ldap/tools.py @@ -33,19 +33,33 @@ class LDAP(object): _log.info('Initiating TLS') self.conn.start_tls_s() + def _get_email(self, server, username): + results = self.conn.search_s(server['LDAP_SEARCH_BASE'], + ldap.SCOPE_SUBTREE, 'uid={0}' + .format(username), + [server['EMAIL_SEARCH_FIELD']]) + + try: + 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')) - return username + 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']) + _log.info('Unbinding {0}.'.format(v['LDAP_SERVER_URI'])) self.conn.unbind() - return False + return False, None diff --git a/mediagoblin/plugins/ldap/views.py b/mediagoblin/plugins/ldap/views.py index 217c6d8c..aef1bf56 100644 --- a/mediagoblin/plugins/ldap/views.py +++ b/mediagoblin/plugins/ldap/views.py @@ -31,7 +31,8 @@ def login(request): if request.method == 'POST' and login_form.validate(): l = LDAP() - username = l.login(login_form.username.data, login_form.password.data) + username, email = l.login(login_form.username.data, + login_form.password.data) if username: user = User.query.filter_by( @@ -55,8 +56,8 @@ def login(request): 'instance.')) return redirect(request, 'index') - register_form = forms.RegisterForm(request.form, - username=username) + register_form = forms.RegisterForm(username=username, + email=email) return render_to_response( request, From bcc12142aef5e5f47de3e99fcaad9c61610a4752 Mon Sep 17 00:00:00 2001 From: Rodney Ewing Date: Tue, 9 Jul 2013 10:49:00 -0700 Subject: [PATCH 05/13] only create a user if the register_form is from the ldap plugin --- mediagoblin/plugins/ldap/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mediagoblin/plugins/ldap/__init__.py b/mediagoblin/plugins/ldap/__init__.py index 18203c92..64474cce 100644 --- a/mediagoblin/plugins/ldap/__init__.py +++ b/mediagoblin/plugins/ldap/__init__.py @@ -33,7 +33,8 @@ def setup_plugin(): def create_user(register_form): - return create_basic_user(register_form) + if 'username' in register_form and 'password' not in register_form: + return create_basic_user(register_form) def no_pass_redirect(): From 547ab1d9d039e5292f266fa36f92cbac718e541f Mon Sep 17 00:00:00 2001 From: Rodney Ewing Date: Tue, 9 Jul 2013 11:20:57 -0700 Subject: [PATCH 06/13] added a create_account hook --- mediagoblin/plugins/ldap/__init__.py | 8 ++++++ .../plugins/ldap/create_account_link.html | 25 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 mediagoblin/plugins/ldap/templates/mediagoblin/plugins/ldap/create_account_link.html diff --git a/mediagoblin/plugins/ldap/__init__.py b/mediagoblin/plugins/ldap/__init__.py index 64474cce..51574a3a 100644 --- a/mediagoblin/plugins/ldap/__init__.py +++ b/mediagoblin/plugins/ldap/__init__.py @@ -13,11 +13,14 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import os from mediagoblin.auth.tools import create_basic_user from mediagoblin.plugins.ldap import forms from mediagoblin.tools import pluginapi +PLUGIN_DIR = os.path.dirname(__file__) + def setup_plugin(): config = pluginapi.get_config('mediagoblin.plugins.ldap') @@ -29,7 +32,12 @@ def setup_plugin(): ('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): 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 . +#} + +{% block create_account %} + {% if allow_registration %} +

+ {% trans %}Sign in to create an account!{% endtrans %} +

+ {% endif %} +{% endblock %} From 8e7f78933e223d1147cee6eb892c0f60bc973279 Mon Sep 17 00:00:00 2001 From: Rodney Ewing Date: Tue, 9 Jul 2013 11:24:51 -0700 Subject: [PATCH 07/13] removed unused import --- mediagoblin/plugins/ldap/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mediagoblin/plugins/ldap/__init__.py b/mediagoblin/plugins/ldap/__init__.py index 51574a3a..4673acee 100644 --- a/mediagoblin/plugins/ldap/__init__.py +++ b/mediagoblin/plugins/ldap/__init__.py @@ -16,7 +16,6 @@ import os from mediagoblin.auth.tools import create_basic_user -from mediagoblin.plugins.ldap import forms from mediagoblin.tools import pluginapi PLUGIN_DIR = os.path.dirname(__file__) From 2b55a0f8654d45ca109160459952a7e8bfd1f8f0 Mon Sep 17 00:00:00 2001 From: Rodney Ewing Date: Tue, 9 Jul 2013 11:48:36 -0700 Subject: [PATCH 08/13] use pluginapi.get_config --- mediagoblin/plugins/ldap/tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mediagoblin/plugins/ldap/tools.py b/mediagoblin/plugins/ldap/tools.py index 3f15c07a..35a681ec 100644 --- a/mediagoblin/plugins/ldap/tools.py +++ b/mediagoblin/plugins/ldap/tools.py @@ -16,14 +16,14 @@ import ldap import logging -from mediagoblin import mg_globals +from mediagoblin.tools import pluginapi _log = logging.getLogger(__name__) class LDAP(object): def __init__(self): - self.ldap_settings = mg_globals.global_config['plugins']['mediagoblin.plugins.ldap'] + self.ldap_settings = pluginapi.get_config('mediagoblin.plugins.ldap') def _connect(self, server): _log.info('Connecting to {0}.'.format(server['LDAP_SERVER_URI'])) From 994e70e85eba526a0847e618702966bcf28f65e9 Mon Sep 17 00:00:00 2001 From: Rodney Ewing Date: Tue, 9 Jul 2013 13:23:13 -0700 Subject: [PATCH 09/13] added docs for ldap plugin --- docs/source/index.rst | 1 + docs/source/plugindocs/ldap.rst | 2 ++ mediagoblin/plugins/ldap/README.rst | 49 +++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 docs/source/plugindocs/ldap.rst create mode 100644 mediagoblin/plugins/ldap/README.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index de6c9c0d..cad3c033 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -59,6 +59,7 @@ Part 2: Core plugin documentation plugindocs/oauth plugindocs/trim_whitespace plugindocs/raven + 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..2539eb18 --- /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. From f92018b649c98fb6d5b6f7c067e56fb7587e4707 Mon Sep 17 00:00:00 2001 From: Rodney Ewing Date: Tue, 9 Jul 2013 13:40:42 -0700 Subject: [PATCH 10/13] typo in docs --- mediagoblin/plugins/ldap/README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mediagoblin/plugins/ldap/README.rst b/mediagoblin/plugins/ldap/README.rst index 2539eb18..ae03b31e 100644 --- a/mediagoblin/plugins/ldap/README.rst +++ b/mediagoblin/plugins/ldap/README.rst @@ -2,7 +2,7 @@ ldap plugin ============= -.. Warning: +.. Warning:: This plugin is not compatible with the other authentication plugins. This plugin allow your GNU Mediagoblin instance to authenticate against an @@ -40,10 +40,10 @@ 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: +.. 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 + 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. From 1bc5b9dfb12fc521d805115545db6a080b8f392e Mon Sep 17 00:00:00 2001 From: Rodney Ewing Date: Tue, 9 Jul 2013 13:44:26 -0700 Subject: [PATCH 11/13] catch a keyerror --- mediagoblin/plugins/ldap/tools.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mediagoblin/plugins/ldap/tools.py b/mediagoblin/plugins/ldap/tools.py index 35a681ec..1c436792 100644 --- a/mediagoblin/plugins/ldap/tools.py +++ b/mediagoblin/plugins/ldap/tools.py @@ -34,12 +34,12 @@ class LDAP(object): self.conn.start_tls_s() def _get_email(self, server, username): - results = self.conn.search_s(server['LDAP_SEARCH_BASE'], - ldap.SCOPE_SUBTREE, 'uid={0}' - .format(username), - [server['EMAIL_SEARCH_FIELD']]) - 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 From d68ada283b7ac3e1b85823ff6e09c9b8007c56dd Mon Sep 17 00:00:00 2001 From: Rodney Ewing Date: Tue, 9 Jul 2013 16:33:38 -0700 Subject: [PATCH 12/13] added tests for ldap plugin --- .../tests/auth_configs/ldap_appconfig.ini | 41 ++++++ mediagoblin/tests/test_ldap.py | 123 ++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 mediagoblin/tests/auth_configs/ldap_appconfig.ini create mode 100644 mediagoblin/tests/test_ldap.py 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 . +[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..ec6da467 --- /dev/null +++ b/mediagoblin/tests/test_ldap.py @@ -0,0 +1,123 @@ +# 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 + + +@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() From b1ac6795922bded7e8dbe8dcf769e57b410a4915 Mon Sep 17 00:00:00 2001 From: Rodney Ewing Date: Wed, 10 Jul 2013 14:10:48 -0700 Subject: [PATCH 13/13] skip test if python-ldap is not installed --- mediagoblin/tests/test_ldap.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mediagoblin/tests/test_ldap.py b/mediagoblin/tests/test_ldap.py index ec6da467..2f9a1372 100644 --- a/mediagoblin/tests/test_ldap.py +++ b/mediagoblin/tests/test_ldap.py @@ -23,6 +23,8 @@ 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):