Ticket #5568: 0001-Fix-LDAP-for-Active-Directory.patch
File 0001-Fix-LDAP-for-Active-Directory.patch, 14.2 KB (added by , 5 years ago) |
---|
-
.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 31 31 /mediagoblin.ini 32 32 /node_modules/ 33 33 /pip-selfcheck.json 34 /mediagoblin/tests/.pytest_cache/ 35 package-lock.json 34 36 35 37 # pyconfigure/automake generated files 36 38 /Makefile … … venv* 63 65 /extlib/leaflet/ 64 66 /extlib/tinymce/ 65 67 /extlib/video.js/ 68 69 # Pycharm artifacts 70 .idea/ -
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 18 18 ============= 19 19 20 20 .. 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. 22 24 23 25 This plugin allow your GNU Mediagoblin instance to authenticate against an 24 26 LDAP server. … … under the ldap plugin:: 44 46 [[[server1]]] 45 47 LDAP_SERVER_URI = 'ldap://ldap.testathon.net:389' 46 48 LDAP_USER_DN_TEMPLATE = 'cn={username},ou=users,dc=testathon,dc=net' 49 LDAP_SEARCH_BASE = 'ou=users,dc=testathon,dc=net' 47 50 [[[server2]]] 48 51 ... 49 52 50 53 Make any necessary changes to the above to work with your sever. Make sure 51 54 ``{username}`` is where the username should be in LDAP_USER_DN_TEMPLATE. 52 55 53 56 If you would like to fetch the users email from the ldap server upon account 54 57 registration, add ``LDAP_SEARCH_BASE = 'ou=users,dc=testathon,dc=net'`` and 55 58 ``EMAIL_SEARCH_FIELD = 'mail'`` under you server configuration in your 56 59 MediaGoblin .ini file. 57 60 61 If you are using Microsoft's Active Directory for your LDAP provider, you will 62 want to specify the following:: 63 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 ... 73 58 74 .. Warning:: 59 75 By default, this plugin provides no encryption when communicating with the 60 76 ldap servers. If you would like to use an SSL connection, change … … MediaGoblin .ini file. 62 78 port for SSL connections is 636. If you would like to use a TLS connection, 63 79 add ``LDAP_START_TLS = 'true'`` under your server configuration in your 64 80 MediaGoblin .ini file. 81 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. 85 86 How LDAP Authentication works 87 ============================= 88 89 When the LDAP plugin is enabled and all other authentication plugins are 90 disabled, attempting to Register or Login will result in the LDAP login form 91 being presented to the end user. 92 93 The end user will enter their LDAP credentials. A lookup is made against the 94 local users table in the GNU Mediagoblin database. If a user with the specified 95 username already exists, then the user will be authenticated against the 96 directory. 97 98 If a user with the specified username does not already exist in the GNU 99 Mediagoblin database, then the LDAP authenitcation will be performed. If the 100 user does not have permissions in LDAP to authenticate to GNU Mediagoblin, 101 they will be presented with a login error. 102 103 If they are allowed to authenticate, and ``EMAIL_SEARCH_FIELD`` is not 104 specified, the user will be prompted to enter their email address. Upon 105 submission, they will be successfully registered and authenticated. 106 107 If they are allowed to authenticate and ``EMAIL_SEARCH_FIELD`` is specified, 108 an email address lookup will be performed against the directory. The user will 109 be prompted to confirm or change their email address. Upon submission, they 110 will be successfully registered and authenticated. 111 112 113 LDAP Configuration Options 114 ========================== 115 116 LDAP_SERVER_URI 117 --------------- 118 119 This required option is to specify the DNS name or IP address of the LDAP 120 server to which your GNU Mediagoblin instance will attempt to bind. In the 121 examples, the ports are specified but they are not required. 122 123 For plain LDAP, the default port is 387. 124 For LDAPS, the default port is 636. 125 126 If your instance is using a non-standard port, the port should be indicated. 127 128 LDAP_USER_DN_TEMPLATE 129 --------------------- 130 131 This is the required template to use when LDAP searches for a user. It is 132 imperative that the value have ``{username}`` in it somewhere, as the string is 133 interpolated with the username at the time of login. 134 135 The value of this will vary depending up the LDAP schema in the domain. It is 136 possible 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 139 that the second form of the LDAP_USER_DN_TEMPLATE works better. 140 141 LDAP_SEARCH_BASE 142 ---------------- 143 144 This is required and represents the root of the domain where GNU Mediagoblin 145 will search for users' email addresses. If your users should all exist under 146 a certain OU, then it is possible to restrict the scope of the search by 147 specifying an OU, as in the example. If users are scattered across all of the 148 domain, the it is also possible to specify just the domain itself: 149 ``LDAP_SEARCH_BASE = 'dc=testathon,dc=net'`` 150 151 EMAIL_SEARCH_FIELD 152 ------------------ 153 154 If this optional field is specified in the LDAP configuration, then GNU 155 Mediagoblin will lookup the user's email address in LDAP as soon as the user 156 authenticates, and the field named in the configuration will used as the search 157 field. 158 159 If this field is not specified, the user will be asked to input 160 their email address when registering. 161 162 The default value is None. 163 164 UID_SEARCH_FIELD 165 ---------------- 166 167 This optional value is used to specify the name of the field that holds the UID. 168 For example, imagine that your username in LDAP is ``media.goblin``. For most 169 LDAP the search string will need to be ``uid = media.goblin``. In this case, 170 the value of UID_SEARCH_FIELD should be set to ``uid``. 171 172 However, Active Directory uses a different field for this, and the value should 173 be adjusted to be ``sAMAccountName``. 174 175 The default value is ``'uid'``. 176 177 LDAP_IS_ACTIVE_DIRECTORY 178 --------------------- 179 180 This optional value is used to specify if you are using Active Directory. If that is the 181 case, this value should be set to ``'true'``, otherwise it should be left at 182 ``'false'`` 183 184 The default value is ``'false'``. 185 186 LDAP_START_TLS 187 -------------- 188 189 This optional value will enable TLS for LDAP communications. If your LDAP 190 server has a TLS certificate that your GNU Mediagoblin will trust, then enable 191 this by setting the value to ``'true'``. 192 193 The default value is ``'false'``. 194 195 LDAP_FILTER 196 ----------- 197 This optional value will be used to restrict the LDAP authentication to users 198 who match the filter criteria. This string is built using LDAP filtering syntax. 199 200 For example, to restrict authentication to members of the MediaGoblinGroup 201 container that is located in the Groups OU, a filter such as this could be used: 202 203 ``LDAP_FILTER = '(&(objectClass=person)(memberOf=cn=MediaGoblinGroup,ou=Groups,dc=testathon,dc=net))'`` 204 205 Any user who is not a member of the MediaGoblinGroup container will be denied authentication. 206 207 The 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): 27 27 def __init__(self): 28 28 self.ldap_settings = pluginapi.get_config('mediagoblin.plugins.ldap') 29 29 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 36 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' 42 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 49 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' 55 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 61 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' 68 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 75 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)' 82 83 _log.info(self.ldap_settings) 84 30 85 def _connect(self, server): 31 86 _log.info('Connecting to {0}.'.format(server['LDAP_SERVER_URI'])) 32 87 self.conn = ldap.initialize(server['LDAP_SERVER_URI']) 33 88 34 if server['LDAP_START_TLS'] == 'true':89 if server['LDAP_START_TLS'].lower() == 'true': 35 90 _log.info('Initiating TLS') 36 91 self.conn.start_tls_s() 37 92 38 93 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) 100 101 email = results[0][1][server['EMAIL_SEARCH_FIELD']][0] 102 except KeyError: 103 email = None 104 else: 105 email = None 106 107 return email 108 109 def _validate_account(self, server, username): 39 110 try: 111 filter = server['LDAP_FILTER'] 112 attrs = [server['UID_SEARCH_FIELD']] 113 40 114 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) 116 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 44 122 45 email = results[0][1][server['EMAIL_SEARCH_FIELD']][0]46 123 except KeyError: 47 email = None 124 valid_account = False 125 except TypeError: 126 valid_account = False 48 127 49 return email128 return valid_account 50 129 51 130 def login(self, username, password): 52 131 for k, v in six.iteritems(self.ldap_settings): 53 132 try: 54 133 self._connect(v) 55 134 user_dn = v['LDAP_USER_DN_TEMPLATE'].format(username=username) 135 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) 139 140 _log.info('Attempting to bind to {0} as {1}'.format( 141 v['LDAP_SERVER_URI'], user_dn)) 56 142 self.conn.simple_bind_s(user_dn, password.encode('utf8')) 57 email = self._get_email(v, username) 143 144 if self._validate_account(v, username): 145 email = self._get_email(v, username) 146 else: 147 return False, None 148 58 149 return username, email 59 150 151 except ValueError, e: 152 _log.info(e) 60 153 except ldap.LDAPError, e: 61 154 _log.info(e) 62 155