Merge remote-tracking branch 'refs/remotes/rodney757/new_ldap'

Conflicts:
	docs/source/index.rst
	mediagoblin/templates/mediagoblin/auth/login.html
This commit is contained in:
Christopher Allan Webber 2013-09-20 07:32:52 -05:00
commit d33a954cad
10 changed files with 511 additions and 0 deletions

View File

@ -61,6 +61,7 @@ Part 2: Core plugin documentation
plugindocs/basic_auth plugindocs/basic_auth
plugindocs/openid plugindocs/openid
plugindocs/persona plugindocs/persona
plugindocs/ldap
Part 3: Plugin Writer's Guide Part 3: Plugin Writer's Guide

View File

@ -0,0 +1,2 @@
.. include:: ../../../mediagoblin/plugins/ldap/README.rst

View File

@ -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.

View File

@ -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,
}

View File

@ -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()])

View File

@ -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 %}

View File

@ -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

View File

@ -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')})

View File

@ -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]]

View File

@ -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()