##// END OF EJS Templates
ldap: adde common ldap-ce na ldap-ee structure, and extend options...
marcink -
r3235:db5132ef default
parent child Browse files
Show More
@@ -89,7 +89,7 b' class TestAuthSettingsView(object):'
89 'timeout': 3600,
89 'timeout': 3600,
90 'tls_kind': 'PLAIN',
90 'tls_kind': 'PLAIN',
91 'tls_reqcert': 'NEVER',
91 'tls_reqcert': 'NEVER',
92
92 'tls_cert_dir':'/etc/openldap/cacerts',
93 'dn_user': 'test_user',
93 'dn_user': 'test_user',
94 'dn_pass': 'test_pass',
94 'dn_pass': 'test_pass',
95 'base_dn': 'test_base_dn',
95 'base_dn': 'test_base_dn',
@@ -38,7 +38,8 b' from rhodecode.authentication.schema imp'
38 from rhodecode.lib import rc_cache
38 from rhodecode.lib import rc_cache
39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
40 from rhodecode.lib.utils2 import safe_int, safe_str
40 from rhodecode.lib.utils2 import safe_int, safe_str
41 from rhodecode.lib.exceptions import LdapConnectionError
41 from rhodecode.lib.exceptions import LdapConnectionError, LdapUsernameError, \
42 LdapPasswordError
42 from rhodecode.model.db import User
43 from rhodecode.model.db import User
43 from rhodecode.model.meta import Session
44 from rhodecode.model.meta import Session
44 from rhodecode.model.settings import SettingsModel
45 from rhodecode.model.settings import SettingsModel
@@ -577,7 +578,8 b' class RhodeCodeExternalAuthPlugin(RhodeC'
577 class AuthLdapBase(object):
578 class AuthLdapBase(object):
578
579
579 @classmethod
580 @classmethod
580 def _build_servers(cls, ldap_server_type, ldap_server, port):
581 def _build_servers(cls, ldap_server_type, ldap_server, port, use_resolver=True):
582
581 def host_resolver(host, port, full_resolve=True):
583 def host_resolver(host, port, full_resolve=True):
582 """
584 """
583 Main work for this function is to prevent ldap connection issues,
585 Main work for this function is to prevent ldap connection issues,
@@ -616,7 +618,7 b' class AuthLdapBase(object):'
616 return ', '.join(
618 return ', '.join(
617 ["{}://{}".format(
619 ["{}://{}".format(
618 ldap_server_type,
620 ldap_server_type,
619 host_resolver(host, port, full_resolve=full_resolve))
621 host_resolver(host, port, full_resolve=use_resolver and full_resolve))
620 for host in ldap_server])
622 for host in ldap_server])
621
623
622 @classmethod
624 @classmethod
@@ -630,6 +632,19 b' class AuthLdapBase(object):'
630 uid = chop_at(username, "@%s" % server_addr)
632 uid = chop_at(username, "@%s" % server_addr)
631 return uid
633 return uid
632
634
635 @classmethod
636 def validate_username(cls, username):
637 if "," in username:
638 raise LdapUsernameError(
639 "invalid character `,` in username: `{}`".format(username))
640
641 @classmethod
642 def validate_password(cls, username, password):
643 if not password:
644 msg = "Authenticating user %s with blank password not allowed"
645 log.warning(msg, username)
646 raise LdapPasswordError(msg)
647
633
648
634 def loadplugin(plugin_id):
649 def loadplugin(plugin_id):
635 """
650 """
@@ -22,7 +22,6 b''
22 RhodeCode authentication plugin for LDAP
22 RhodeCode authentication plugin for LDAP
23 """
23 """
24
24
25 import os
26 import logging
25 import logging
27 import traceback
26 import traceback
28
27
@@ -67,6 +66,171 b' class LdapAuthnResource(AuthnPluginResou'
67 pass
66 pass
68
67
69
68
69 class AuthLdap(AuthLdapBase):
70 default_tls_cert_dir = '/etc/openldap/cacerts'
71
72 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
73 tls_kind='PLAIN', tls_reqcert='DEMAND', tls_cert_file=None,
74 tls_cert_dir=None, ldap_version=3,
75 search_scope='SUBTREE', attr_login='uid',
76 ldap_filter='', timeout=None):
77 if ldap == Missing:
78 raise LdapImportError("Missing or incompatible ldap library")
79
80 self.debug = False
81 self.timeout = timeout or 60 * 5
82 self.ldap_version = ldap_version
83 self.ldap_server_type = 'ldap'
84
85 self.TLS_KIND = tls_kind
86
87 if self.TLS_KIND == 'LDAPS':
88 port = port or 689
89 self.ldap_server_type += 's'
90
91 OPT_X_TLS_DEMAND = 2
92 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert, OPT_X_TLS_DEMAND)
93 self.TLS_CERT_FILE = tls_cert_file or ''
94 self.TLS_CERT_DIR = tls_cert_dir or self.default_tls_cert_dir
95
96 # split server into list
97 self.SERVER_ADDRESSES = self._get_server_list(server)
98 self.LDAP_SERVER_PORT = port
99
100 # USE FOR READ ONLY BIND TO LDAP SERVER
101 self.attr_login = attr_login
102
103 self.LDAP_BIND_DN = safe_str(bind_dn)
104 self.LDAP_BIND_PASS = safe_str(bind_pass)
105
106 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
107 self.BASE_DN = safe_str(base_dn)
108 self.LDAP_FILTER = safe_str(ldap_filter)
109
110 def _get_ldap_conn(self):
111
112 if self.debug:
113 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
114
115 if self.TLS_CERT_FILE and hasattr(ldap, 'OPT_X_TLS_CACERTFILE'):
116 ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, self.TLS_CERT_FILE)
117
118 elif hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
119 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, self.TLS_CERT_DIR)
120
121 if self.TLS_KIND != 'PLAIN':
122 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
123
124 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
125 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
126
127 # init connection now
128 ldap_servers = self._build_servers(
129 self.ldap_server_type, self.SERVER_ADDRESSES, self.LDAP_SERVER_PORT)
130 log.debug('initializing LDAP connection to:%s', ldap_servers)
131 ldap_conn = ldap.initialize(ldap_servers)
132 ldap_conn.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout)
133 ldap_conn.set_option(ldap.OPT_TIMEOUT, self.timeout)
134 ldap_conn.timeout = self.timeout
135
136 if self.ldap_version == 2:
137 ldap_conn.protocol = ldap.VERSION2
138 else:
139 ldap_conn.protocol = ldap.VERSION3
140
141 if self.TLS_KIND == 'START_TLS':
142 ldap_conn.start_tls_s()
143
144 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
145 log.debug('Trying simple_bind with password and given login DN: %s',
146 self.LDAP_BIND_DN)
147 ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
148
149 return ldap_conn
150
151 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
152 try:
153 log.debug('Trying simple bind with %s', dn)
154 server.simple_bind_s(dn, safe_str(password))
155 user = server.search_ext_s(
156 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
157 _, attrs = user
158 return attrs
159
160 except ldap.INVALID_CREDENTIALS:
161 log.debug(
162 "LDAP rejected password for user '%s': %s, org_exc:",
163 username, dn, exc_info=True)
164
165 def authenticate_ldap(self, username, password):
166 """
167 Authenticate a user via LDAP and return his/her LDAP properties.
168
169 Raises AuthenticationError if the credentials are rejected, or
170 EnvironmentError if the LDAP server can't be reached.
171
172 :param username: username
173 :param password: password
174 """
175
176 uid = self.get_uid(username, self.SERVER_ADDRESSES)
177 user_attrs = {}
178 dn = ''
179
180 self.validate_password(username, password)
181 self.validate_username(username)
182
183 ldap_conn = None
184 try:
185 ldap_conn = self._get_ldap_conn()
186 filter_ = '(&%s(%s=%s))' % (
187 self.LDAP_FILTER, self.attr_login, username)
188 log.debug("Authenticating %r filter %s", self.BASE_DN, filter_)
189
190 lobjects = ldap_conn.search_ext_s(
191 self.BASE_DN, self.SEARCH_SCOPE, filter_)
192
193 if not lobjects:
194 log.debug("No matching LDAP objects for authentication "
195 "of UID:'%s' username:(%s)", uid, username)
196 raise ldap.NO_SUCH_OBJECT()
197
198 log.debug('Found matching ldap object, trying to authenticate')
199 for (dn, _attrs) in lobjects:
200 if dn is None:
201 continue
202
203 user_attrs = self.fetch_attrs_from_simple_bind(
204 ldap_conn, dn, username, password)
205 if user_attrs:
206 break
207 else:
208 raise LdapPasswordError(
209 'Failed to authenticate user `{}`'
210 'with given password'.format(username))
211
212 except ldap.NO_SUCH_OBJECT:
213 log.debug("LDAP says no such user '%s' (%s), org_exc:",
214 uid, username, exc_info=True)
215 raise LdapUsernameError('Unable to find user')
216 except ldap.SERVER_DOWN:
217 org_exc = traceback.format_exc()
218 raise LdapConnectionError(
219 "LDAP can't access authentication "
220 "server, org_exc:%s" % org_exc)
221 finally:
222 if ldap_conn:
223 log.debug('ldap: connection release')
224 try:
225 ldap_conn.unbind_s()
226 except Exception:
227 # for any reason this can raise exception we must catch it
228 # to not crush the server
229 pass
230
231 return dn, user_attrs
232
233
70 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
234 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
71 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
235 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
72 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
236 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
@@ -134,6 +298,22 b' class LdapSettingsSchema(AuthnPluginSett'
134 title=_('Certificate Checks'),
298 title=_('Certificate Checks'),
135 validator=colander.OneOf(tls_reqcert_choices),
299 validator=colander.OneOf(tls_reqcert_choices),
136 widget='select')
300 widget='select')
301 tls_cert_file = colander.SchemaNode(
302 colander.String(),
303 default='',
304 description=_('This specifies the PEM-format file path containing '
305 'certificates for use in TLS connection.\n'
306 'If not specified `TLS Cert dir` will be used'),
307 title=_('TLS Cert file'),
308 missing='',
309 widget='string')
310 tls_cert_dir = colander.SchemaNode(
311 colander.String(),
312 default=AuthLdap.default_tls_cert_dir,
313 description=_('This specifies the path of a directory that contains individual '
314 'CA certificates in separate files.'),
315 title=_('TLS Cert dir'),
316 widget='string')
137 base_dn = colander.SchemaNode(
317 base_dn = colander.SchemaNode(
138 colander.String(),
318 colander.String(),
139 default='',
319 default='',
@@ -198,175 +378,6 b' class LdapSettingsSchema(AuthnPluginSett'
198 widget='string')
378 widget='string')
199
379
200
380
201 class AuthLdap(AuthLdapBase):
202
203 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
204 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
205 search_scope='SUBTREE', attr_login='uid',
206 ldap_filter='', timeout=None):
207 if ldap == Missing:
208 raise LdapImportError("Missing or incompatible ldap library")
209
210 self.debug = False
211 self.timeout = timeout or 60 * 5
212 self.ldap_version = ldap_version
213 self.ldap_server_type = 'ldap'
214
215 self.TLS_KIND = tls_kind
216
217 if self.TLS_KIND == 'LDAPS':
218 port = port or 689
219 self.ldap_server_type += 's'
220
221 OPT_X_TLS_DEMAND = 2
222 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
223 OPT_X_TLS_DEMAND)
224 self.LDAP_SERVER = server
225 # split server into list
226 self.SERVER_ADDRESSES = self._get_server_list(server)
227 self.LDAP_SERVER_PORT = port
228
229 # USE FOR READ ONLY BIND TO LDAP SERVER
230 self.attr_login = attr_login
231
232 self.LDAP_BIND_DN = safe_str(bind_dn)
233 self.LDAP_BIND_PASS = safe_str(bind_pass)
234
235 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
236 self.BASE_DN = safe_str(base_dn)
237 self.LDAP_FILTER = safe_str(ldap_filter)
238
239 def _get_ldap_conn(self):
240
241 if self.debug:
242 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
243
244 default_cert_path = os.environ.get('SSL_CERT_FILE')
245 default_cert_dir = os.environ.get('SSL_CERT_DIR', '/etc/openldap/cacerts')
246 if default_cert_path and hasattr(ldap, 'OPT_X_TLS_CACERTFILE'):
247 ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, default_cert_path)
248
249 elif hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
250 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, default_cert_dir)
251
252 if self.TLS_KIND != 'PLAIN':
253 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
254
255 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
256 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
257
258 # init connection now
259 ldap_servers = self._build_servers(
260 self.ldap_server_type, self.SERVER_ADDRESSES, self.LDAP_SERVER_PORT)
261 log.debug('initializing LDAP connection to:%s', ldap_servers)
262 ldap_conn = ldap.initialize(ldap_servers)
263 ldap_conn.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout)
264 ldap_conn.set_option(ldap.OPT_TIMEOUT, self.timeout)
265 ldap_conn.timeout = self.timeout
266
267 if self.ldap_version == 2:
268 ldap_conn.protocol = ldap.VERSION2
269 else:
270 ldap_conn.protocol = ldap.VERSION3
271
272 if self.TLS_KIND == 'START_TLS':
273 ldap_conn.start_tls_s()
274
275 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
276 log.debug('Trying simple_bind with password and given login DN: %s',
277 self.LDAP_BIND_DN)
278 ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
279
280 return ldap_conn
281
282 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
283 try:
284 log.debug('Trying simple bind with %s', dn)
285 server.simple_bind_s(dn, safe_str(password))
286 user = server.search_ext_s(
287 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
288 _, attrs = user
289 return attrs
290
291 except ldap.INVALID_CREDENTIALS:
292 log.debug(
293 "LDAP rejected password for user '%s': %s, org_exc:",
294 username, dn, exc_info=True)
295
296 def authenticate_ldap(self, username, password):
297 """
298 Authenticate a user via LDAP and return his/her LDAP properties.
299
300 Raises AuthenticationError if the credentials are rejected, or
301 EnvironmentError if the LDAP server can't be reached.
302
303 :param username: username
304 :param password: password
305 """
306
307 uid = self.get_uid(username, self.SERVER_ADDRESSES)
308 user_attrs = {}
309 dn = ''
310
311 if not password:
312 msg = "Authenticating user %s with blank password not allowed"
313 log.warning(msg, username)
314 raise LdapPasswordError(msg)
315 if "," in username:
316 raise LdapUsernameError(
317 "invalid character `,` in username: `{}`".format(username))
318 ldap_conn = None
319 try:
320 ldap_conn = self._get_ldap_conn()
321 filter_ = '(&%s(%s=%s))' % (
322 self.LDAP_FILTER, self.attr_login, username)
323 log.debug(
324 "Authenticating %r filter %s", self.BASE_DN, filter_)
325 lobjects = ldap_conn.search_ext_s(
326 self.BASE_DN, self.SEARCH_SCOPE, filter_)
327
328 if not lobjects:
329 log.debug("No matching LDAP objects for authentication "
330 "of UID:'%s' username:(%s)", uid, username)
331 raise ldap.NO_SUCH_OBJECT()
332
333 log.debug('Found matching ldap object, trying to authenticate')
334 for (dn, _attrs) in lobjects:
335 if dn is None:
336 continue
337
338 user_attrs = self.fetch_attrs_from_simple_bind(
339 ldap_conn, dn, username, password)
340 if user_attrs:
341 break
342
343 else:
344 raise LdapPasswordError(
345 'Failed to authenticate user `{}`'
346 'with given password'.format(username))
347
348 except ldap.NO_SUCH_OBJECT:
349 log.debug("LDAP says no such user '%s' (%s), org_exc:",
350 uid, username, exc_info=True)
351 raise LdapUsernameError('Unable to find user')
352 except ldap.SERVER_DOWN:
353 org_exc = traceback.format_exc()
354 raise LdapConnectionError(
355 "LDAP can't access authentication "
356 "server, org_exc:%s" % org_exc)
357 finally:
358 if ldap_conn:
359 log.debug('ldap: connection release')
360 try:
361 ldap_conn.unbind_s()
362 except Exception:
363 # for any reason this can raise exception we must catch it
364 # to not crush the server
365 pass
366
367 return dn, user_attrs
368
369
370 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
381 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
371 # used to define dynamic binding in the
382 # used to define dynamic binding in the
372 DYNAMIC_BIND_VAR = '$login'
383 DYNAMIC_BIND_VAR = '$login'
@@ -459,6 +470,8 b' class RhodeCodeAuthPlugin(RhodeCodeExter'
459 'bind_pass': settings.get('dn_pass'),
470 'bind_pass': settings.get('dn_pass'),
460 'tls_kind': settings.get('tls_kind'),
471 'tls_kind': settings.get('tls_kind'),
461 'tls_reqcert': settings.get('tls_reqcert'),
472 'tls_reqcert': settings.get('tls_reqcert'),
473 'tls_cert_file': settings.get('tls_cert_file'),
474 'tls_cert_dir': settings.get('tls_cert_dir'),
462 'search_scope': settings.get('search_scope'),
475 'search_scope': settings.get('search_scope'),
463 'attr_login': settings.get('attr_login'),
476 'attr_login': settings.get('attr_login'),
464 'ldap_version': 3,
477 'ldap_version': 3,
@@ -490,10 +503,8 b' class RhodeCodeAuthPlugin(RhodeCodeExter'
490 groups = []
503 groups = []
491 user_attrs = {
504 user_attrs = {
492 'username': username,
505 'username': username,
493 'firstname': safe_unicode(
506 'firstname': safe_unicode(get_ldap_attr('attr_firstname') or firstname),
494 get_ldap_attr('attr_firstname') or firstname),
507 'lastname': safe_unicode(get_ldap_attr('attr_lastname') or lastname),
495 'lastname': safe_unicode(
496 get_ldap_attr('attr_lastname') or lastname),
497 'groups': groups,
508 'groups': groups,
498 'user_group_sync': False,
509 'user_group_sync': False,
499 'email': get_ldap_attr('attr_email') or email,
510 'email': get_ldap_attr('attr_email') or email,
@@ -503,6 +514,7 b' class RhodeCodeAuthPlugin(RhodeCodeExter'
503 'extern_name': user_dn,
514 'extern_name': user_dn,
504 'extern_type': extern_type,
515 'extern_type': extern_type,
505 }
516 }
517
506 log.debug('ldap user: %s', user_attrs)
518 log.debug('ldap user: %s', user_attrs)
507 log.info('user `%s` authenticated correctly', user_attrs['username'])
519 log.info('user `%s` authenticated correctly', user_attrs['username'])
508
520
@@ -514,4 +526,3 b' class RhodeCodeAuthPlugin(RhodeCodeExter'
514 except (Exception,):
526 except (Exception,):
515 log.exception("Other exception")
527 log.exception("Other exception")
516 return None
528 return None
517
General Comments 0
You need to be logged in to leave comments. Login now