##// 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 89 'timeout': 3600,
90 90 'tls_kind': 'PLAIN',
91 91 'tls_reqcert': 'NEVER',
92
92 'tls_cert_dir':'/etc/openldap/cacerts',
93 93 'dn_user': 'test_user',
94 94 'dn_pass': 'test_pass',
95 95 'base_dn': 'test_base_dn',
@@ -38,7 +38,8 b' from rhodecode.authentication.schema imp'
38 38 from rhodecode.lib import rc_cache
39 39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
40 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 43 from rhodecode.model.db import User
43 44 from rhodecode.model.meta import Session
44 45 from rhodecode.model.settings import SettingsModel
@@ -577,7 +578,8 b' class RhodeCodeExternalAuthPlugin(RhodeC'
577 578 class AuthLdapBase(object):
578 579
579 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 583 def host_resolver(host, port, full_resolve=True):
582 584 """
583 585 Main work for this function is to prevent ldap connection issues,
@@ -616,7 +618,7 b' class AuthLdapBase(object):'
616 618 return ', '.join(
617 619 ["{}://{}".format(
618 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 622 for host in ldap_server])
621 623
622 624 @classmethod
@@ -630,6 +632,19 b' class AuthLdapBase(object):'
630 632 uid = chop_at(username, "@%s" % server_addr)
631 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 649 def loadplugin(plugin_id):
635 650 """
@@ -22,7 +22,6 b''
22 22 RhodeCode authentication plugin for LDAP
23 23 """
24 24
25 import os
26 25 import logging
27 26 import traceback
28 27
@@ -67,6 +66,171 b' class LdapAuthnResource(AuthnPluginResou'
67 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 234 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
71 235 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
72 236 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
@@ -134,6 +298,22 b' class LdapSettingsSchema(AuthnPluginSett'
134 298 title=_('Certificate Checks'),
135 299 validator=colander.OneOf(tls_reqcert_choices),
136 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 317 base_dn = colander.SchemaNode(
138 318 colander.String(),
139 319 default='',
@@ -198,175 +378,6 b' class LdapSettingsSchema(AuthnPluginSett'
198 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 381 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
371 382 # used to define dynamic binding in the
372 383 DYNAMIC_BIND_VAR = '$login'
@@ -459,6 +470,8 b' class RhodeCodeAuthPlugin(RhodeCodeExter'
459 470 'bind_pass': settings.get('dn_pass'),
460 471 'tls_kind': settings.get('tls_kind'),
461 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 475 'search_scope': settings.get('search_scope'),
463 476 'attr_login': settings.get('attr_login'),
464 477 'ldap_version': 3,
@@ -490,10 +503,8 b' class RhodeCodeAuthPlugin(RhodeCodeExter'
490 503 groups = []
491 504 user_attrs = {
492 505 'username': username,
493 'firstname': safe_unicode(
494 get_ldap_attr('attr_firstname') or firstname),
495 'lastname': safe_unicode(
496 get_ldap_attr('attr_lastname') or lastname),
506 'firstname': safe_unicode(get_ldap_attr('attr_firstname') or firstname),
507 'lastname': safe_unicode(get_ldap_attr('attr_lastname') or lastname),
497 508 'groups': groups,
498 509 'user_group_sync': False,
499 510 'email': get_ldap_attr('attr_email') or email,
@@ -503,6 +514,7 b' class RhodeCodeAuthPlugin(RhodeCodeExter'
503 514 'extern_name': user_dn,
504 515 'extern_type': extern_type,
505 516 }
517
506 518 log.debug('ldap user: %s', user_attrs)
507 519 log.info('user `%s` authenticated correctly', user_attrs['username'])
508 520
@@ -514,4 +526,3 b' class RhodeCodeAuthPlugin(RhodeCodeExter'
514 526 except (Exception,):
515 527 log.exception("Other exception")
516 528 return None
517
General Comments 0
You need to be logged in to leave comments. Login now