Ticket #5568: 0001-Fix-LDAP-for-Active-Directory.patch

File 0001-Fix-LDAP-for-Active-Directory.patch, 14.2 KB (added by Meaulnes, 5 years ago)

LDAP Active Directory patch

  • .gitignore

    From 56efa725ed49e22e9376d445f957f6ca09932e5b Mon Sep 17 00:00:00 2001
    From: Kirk Gleason <kgleason@bloominsuranceagency.com>
    Date: Wed, 11 Apr 2018 09:44:13 -0400
    Subject: [PATCH] Added in Active Directory config flags that are required.
     Adjusted email search so that it returns the entire user object. Made the
     search field variable.
    Updated docs to add in AD specific bits
    Converted the LDAP_START_TLS option from string to bool
    Added some explanation to all of the LDAP configuration options.
    Converted LDAP_ACTIVE_DIRECTORY from bool to string
    Converted bool config options to string
    Ignore PyCharm artifacts, package-lock, and pytest cache files.
    Added in checks for all of the possible LDAP config variables, and tested for them. It is not yet properly handling required values, but it's no different than when I started looking at it.
    Updated the docs to reflect the optional config values.
    More details on how authenitcation works are in the documentation. The logic of EMAIL_SEARCH_FIELD is more clear.
    EMAIL_SEARCH_FIELD defaults to None, and is used to trigger or skip the email lookup
    Active Direcotry configuration option is more clearly named.
    Can now restrcit ability to LDAP auth based on group membership.
     .gitignore                          |   5 +
     mediagoblin/plugins/ldap/README.rst | 147 +++++++++++++++++++++++++++-
     mediagoblin/plugins/ldap/tools.py   | 109 +++++++++++++++++++--
     3 files changed, 251 insertions(+), 10 deletions(-)
    diff --git a/.gitignore b/.gitignore
    index c4a1497f..17733abf 100644
    a b  
    3537# pyconfigure/automake generated files
     69# Pycharm artifacts
  • mediagoblin/plugins/ldap/README.rst

    diff --git a/mediagoblin/plugins/ldap/README.rst b/mediagoblin/plugins/ldap/README.rst
    index ea9a34b3..8233aff9 100644
    a b  
    2020.. Warning::
    21    This plugin is not compatible with the other authentication plugins.
     21    This plugin is not compatible with the other authentication plugins.
     22    All other authentication plugins will need to be disabled in order
     23    for this plugin to work.
    2325This plugin allow your GNU Mediagoblin instance to authenticate against an
    2426LDAP server.
    under the ldap plugin::  
    4446    [[[server1]]]
    4547    LDAP_SERVER_URI = 'ldap://ldap.testathon.net:389'
    4648    LDAP_USER_DN_TEMPLATE = 'cn={username},ou=users,dc=testathon,dc=net'
     49    LDAP_SEARCH_BASE = 'ou=users,dc=testathon,dc=net'
    4750    [[[server2]]]
    4851    ...
    5053Make any necessary changes to the above to work with your sever. Make sure
    5154``{username}`` is where the username should be in LDAP_USER_DN_TEMPLATE.
    5356If you would like to fetch the users email from the ldap server upon account
    5457registration, add ``LDAP_SEARCH_BASE = 'ou=users,dc=testathon,dc=net'`` and
    5558``EMAIL_SEARCH_FIELD = 'mail'`` under you server configuration in your
    5659MediaGoblin .ini file.
     61If you are using Microsoft's Active Directory for your LDAP provider, you will
     62want to specify the following::
     64    [[mediagoblin.plugins.ldap]]
     65    [[[server1]]]
     66    LDAP_SERVER_URI = 'ldap://ldap.testathon.net'
     67    LDAP_USER_DN_TEMPLATE = '{username}@testathon.net'
     68    LDAP_SEARCH_BASE = 'ou=users,dc=testathon,dc=net'
     69    LDAP_IS_ACTIVE_DIRECTORY = 'true'
     70    UID_SEARCH_FIELD = 'sAMAccountName'
     71    [[[server2]]]
     72    ...
    5874.. Warning::
    5975   By default, this plugin provides no encryption when communicating with the
    6076   ldap servers. If you would like to use an SSL connection, change
    MediaGoblin .ini file.  
    6278   port for SSL connections is 636. If you would like to use a TLS connection,
    6379   add ``LDAP_START_TLS = 'true'`` under your server configuration in your
    6480   MediaGoblin .ini file.
     82   If you are able, start with SSL & TLS disabled, until you have things working,
     83   then enable the security pieces one at a time to help eliminate issues as you
     84   are getting started.
     86How LDAP Authentication works
     89When the LDAP plugin is enabled and all other authentication plugins are
     90disabled, attempting to Register or Login will result in the LDAP login form
     91being presented to the end user.
     93The end user will enter their LDAP credentials. A lookup is made against the
     94local users table in the GNU Mediagoblin database. If a user with the specified
     95username already exists, then the user will be authenticated against the
     98If a user with the specified username does not already exist in the GNU
     99Mediagoblin database, then the LDAP authenitcation will be performed. If the
     100user does not have permissions in LDAP to authenticate to GNU Mediagoblin,
     101they will be presented with a login error.
     103If they are allowed to authenticate, and ``EMAIL_SEARCH_FIELD`` is not
     104specified, the user will be prompted to enter their email address. Upon
     105submission, they will be successfully registered and authenticated.
     107If they are allowed to authenticate and ``EMAIL_SEARCH_FIELD`` is specified,
     108an email address lookup will be performed against the directory. The user will
     109be prompted to confirm or change their email address. Upon submission, they
     110will be successfully registered and authenticated.
     113LDAP Configuration Options
     119This required option is to specify the DNS name or IP address of the LDAP
     120server to which your GNU Mediagoblin instance will attempt to bind. In the
     121examples, the ports are specified but they are not required.
     123For plain LDAP, the default port is 387.
     124For LDAPS, the default port is 636.
     126If your instance is using a non-standard port, the port should be indicated.
     131This is the required template to use when LDAP searches for a user. It is
     132imperative that the value have ``{username}`` in it somewhere, as the string is
     133interpolated with the username at the time of login.
     135The value of this will vary depending up the LDAP schema in the domain. It is
     136possible to use either a full path
     137( ``cn={username},ou=users,dc=testathon,dc=net`` ) or a UPN
     138( ``{username}@testathon.net`` ). Some Active Directory users have reported
     139that the second form of the LDAP_USER_DN_TEMPLATE works better.
     144This is required and represents the root of the domain where GNU Mediagoblin
     145will search for users' email addresses. If your users should all exist under
     146a certain OU, then it is possible to restrict the scope of the search by
     147specifying an OU, as in the example. If users are scattered across all of the
     148domain, the it is also possible to specify just the domain itself:
     149``LDAP_SEARCH_BASE = 'dc=testathon,dc=net'``
     154If this optional field is specified in the LDAP configuration, then GNU
     155Mediagoblin will lookup the user's email address in LDAP as soon as the user
     156authenticates, and the field named in the configuration will used as the search
     159If this field is not specified, the user will be asked to input
     160their email address when registering.
     162The default value is None.
     167This optional value is used to specify the name of the field that holds the UID.
     168For example, imagine that your username in LDAP is ``media.goblin``. For most
     169LDAP the search string will need to be ``uid = media.goblin``. In this case,
     170the value of UID_SEARCH_FIELD should be set to ``uid``.
     172However, Active Directory uses a different field for this, and the value should
     173be adjusted to be ``sAMAccountName``.
     175The default value is ``'uid'``.
     180This optional value is used to specify if you are using Active Directory. If that is the
     181case, this value should be set to ``'true'``, otherwise it should be left at
     184The default value is ``'false'``.
     189This optional value will enable TLS for LDAP communications. If your LDAP
     190server has a TLS certificate that your GNU Mediagoblin will trust, then enable
     191this by setting the value to ``'true'``.
     193The default value is ``'false'``.
     197This optional value will be used to restrict the LDAP authentication to users
     198who match the filter criteria. This string is built using LDAP filtering syntax.
     200For example, to restrict authentication to members of the MediaGoblinGroup
     201container that is located in the Groups OU, a filter such as this could be used:
     203``LDAP_FILTER = '(&(objectClass=person)(memberOf=cn=MediaGoblinGroup,ou=Groups,dc=testathon,dc=net))'``
     205Any user who is not a member of the MediaGoblinGroup container will be denied authentication.
     207The default value of this filter is ``(objectClass=person)``
  • mediagoblin/plugins/ldap/tools.py

    diff --git a/mediagoblin/plugins/ldap/tools.py b/mediagoblin/plugins/ldap/tools.py
    index 2be2dcd7..4c527208 100644
    a b class LDAP(object):  
    2727    def __init__(self):
    2828        self.ldap_settings = pluginapi.get_config('mediagoblin.plugins.ldap')
     30        for k, v in six.iteritems(self.ldap_settings):
     31            try:
     32                v['LDAP_SERVER_URI']
     33            except KeyError:
     34                _log.error('LDAP_SERVER_URI was not defined in the config.')
     35                # Do something here to raise a fatal error
     37            try:
     38                v['LDAP_START_TLS']
     39            except KeyError:
     40                _log.info('LDAP_START_TLS is not defined. Assuming false')
     41                self.ldap_settings[k]['LDAP_START_TLS'] = 'false'
     43            try:
     44                v['LDAP_USER_DN_TEMPLATE']
     45            except KeyError:
     46                _log.error('LDAP_USER_DN_TEMPLATE '
     47                           'was not defined in the config')
     48                # Do something here to raise a fatal error
     50            try:
     51                v['LDAP_IS_ACTIVE_DIRECTORY']
     52            except KeyError:
     53                _log.info('Active Directory flag was not set. Assuming false')
     54                self.ldap_settings[k]['LDAP_IS_ACTIVE_DIRECTORY'] = 'false'
     56            try:
     57                v['LDAP_SEARCH_BASE']
     58            except KeyError:
     59                _log.error('LDAP_SEARCH_BASE was not defined in the config')
     60                # Do something here to raise a fatal error
     62            try:
     63                v['UID_SEARCH_FIELD']
     64            except KeyError:
     65                _log.info('UID_SEARCH_FIELD was not defined in the config. '
     66                          'Assuming ''uid''.')
     67                self.ldap_settings[k]['UID_SEARCH_FIELD'] = 'uid'
     69            try:
     70                v['EMAIL_SEARCH_FIELD']
     71            except KeyError:
     72                _log.info('EMAIL_SEARCH_FIELD was not defined in the config. '
     73                          'Assuming mail lookup is not wanted.')
     74                self.ldap_settings[k]['EMAIL_SEARCH_FIELD'] = None
     76            try:
     77                v['LDAP_FILTER']
     78            except KeyError:
     79                _log.info('LDAP_FILTER was not defined in the config. '
     80                          'Assuming ''(objectClass=person)''')
     81                self.ldap_settings[k]['LDAP_FILTER'] = '(objectClass=person)'
     83        _log.info(self.ldap_settings)
    3085    def _connect(self, server):
    3186        _log.info('Connecting to {0}.'.format(server['LDAP_SERVER_URI']))
    3287        self.conn = ldap.initialize(server['LDAP_SERVER_URI'])
    34         if server['LDAP_START_TLS'] == 'true':
     89        if server['LDAP_START_TLS'].lower() == 'true':
    3590            _log.info('Initiating TLS')
    3691            self.conn.start_tls_s()
    3893    def _get_email(self, server, username):
     94        if server['EMAIL_SEARCH_FIELD']:
     95            try:
     96                filter = '{0}={1}'.format(server['UID_SEARCH_FIELD'], username)
     97                attrs = [server['EMAIL_SEARCH_FIELD']]
     98                results = self.conn.search_s(server['LDAP_SEARCH_BASE'],
     99                                            ldap.SCOPE_SUBTREE, filter, attrs)
     101                email = results[0][1][server['EMAIL_SEARCH_FIELD']][0]
     102            except KeyError:
     103                email = None
     104        else:
     105            email = None
     107        return email
     109    def _validate_account(self, server, username):
    39110        try:
     111            filter = server['LDAP_FILTER']
     112            attrs = [server['UID_SEARCH_FIELD']]
    40114            results = self.conn.search_s(server['LDAP_SEARCH_BASE'],
    41                                         ldap.SCOPE_SUBTREE, 'uid={0}'
    42                                         .format(username),
    43                                         [server['EMAIL_SEARCH_FIELD']])
     115                                         ldap.SCOPE_SUBTREE, filter, attrs)
     117            valid_account = False
     118            for res in results:
     119                if res[1][server['UID_SEARCH_FIELD']][0] == username:
     120                    valid_account = True
     121                    break
    45             email = results[0][1][server['EMAIL_SEARCH_FIELD']][0]
    46123        except KeyError:
    47             email = None
     124            valid_account = False
     125        except TypeError:
     126            valid_account = False
    49         return email
     128        return valid_account
    51130    def login(self, username, password):
    52131        for k, v in six.iteritems(self.ldap_settings):
    53132            try:
    54133                self._connect(v)
    55134                user_dn = v['LDAP_USER_DN_TEMPLATE'].format(username=username)
     136                if v['LDAP_IS_ACTIVE_DIRECTORY'].lower() == 'true':
     137                    self.conn.protocol_version = ldap.VERSION3
     138                    self.conn.set_option(ldap.OPT_REFERRALS, 0)
     140                _log.info('Attempting to bind to {0} as {1}'.format(
     141                    v['LDAP_SERVER_URI'], user_dn))
    56142                self.conn.simple_bind_s(user_dn, password.encode('utf8'))
    57                 email = self._get_email(v, username)
     144                if self._validate_account(v, username):
     145                    email = self._get_email(v, username)
     146                else:
     147                    return False, None
    58149                return username, email
     151            except ValueError, e:
     152                _log.info(e)
    60153            except ldap.LDAPError, e:
    61154                _log.info(e)