##// END OF EJS Templates
ldap: enable connection recycling on LDAP plugin.
marcink -
r2575:e09d5d1b default
parent child Browse files
Show More
@@ -1,481 +1,498 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 RhodeCode authentication plugin for LDAP
22 RhodeCode authentication plugin for LDAP
23 """
23 """
24
24
25
25 import re
26 import colander
26 import colander
27 import logging
27 import logging
28 import traceback
28 import traceback
29 import string
29
30
30 from rhodecode.translation import _
31 from rhodecode.translation import _
31 from rhodecode.authentication.base import (
32 from rhodecode.authentication.base import (
32 RhodeCodeExternalAuthPlugin, chop_at, hybrid_property)
33 RhodeCodeExternalAuthPlugin, chop_at, hybrid_property)
33 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
34 from rhodecode.authentication.routes import AuthnPluginResourceBase
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
35 from rhodecode.lib.colander_utils import strip_whitespace
36 from rhodecode.lib.colander_utils import strip_whitespace
36 from rhodecode.lib.exceptions import (
37 from rhodecode.lib.exceptions import (
37 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
38 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
38 )
39 )
39 from rhodecode.lib.utils2 import safe_unicode, safe_str
40 from rhodecode.lib.utils2 import safe_unicode, safe_str
40 from rhodecode.model.db import User
41 from rhodecode.model.db import User
41 from rhodecode.model.validators import Missing
42 from rhodecode.model.validators import Missing
42
43
43 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
44
45
45 try:
46 try:
46 import ldap
47 import ldap
47 except ImportError:
48 except ImportError:
48 # means that python-ldap is not installed, we use Missing object to mark
49 # means that python-ldap is not installed, we use Missing object to mark
49 # ldap lib is Missing
50 # ldap lib is Missing
50 ldap = Missing
51 ldap = Missing
51
52
52
53
54 class LdapError(Exception):
55 pass
56
53 def plugin_factory(plugin_id, *args, **kwds):
57 def plugin_factory(plugin_id, *args, **kwds):
54 """
58 """
55 Factory function that is called during plugin discovery.
59 Factory function that is called during plugin discovery.
56 It returns the plugin instance.
60 It returns the plugin instance.
57 """
61 """
58 plugin = RhodeCodeAuthPlugin(plugin_id)
62 plugin = RhodeCodeAuthPlugin(plugin_id)
59 return plugin
63 return plugin
60
64
61
65
62 class LdapAuthnResource(AuthnPluginResourceBase):
66 class LdapAuthnResource(AuthnPluginResourceBase):
63 pass
67 pass
64
68
65
69
66 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
70 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
67 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
71 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
68 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
72 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
69 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
73 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
70
74
71 host = colander.SchemaNode(
75 host = colander.SchemaNode(
72 colander.String(),
76 colander.String(),
73 default='',
77 default='',
74 description=_('Host[s] of the LDAP Server \n'
78 description=_('Host[s] of the LDAP Server \n'
75 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
79 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
76 'Multiple servers can be specified using commas'),
80 'Multiple servers can be specified using commas'),
77 preparer=strip_whitespace,
81 preparer=strip_whitespace,
78 title=_('LDAP Host'),
82 title=_('LDAP Host'),
79 widget='string')
83 widget='string')
80 port = colander.SchemaNode(
84 port = colander.SchemaNode(
81 colander.Int(),
85 colander.Int(),
82 default=389,
86 default=389,
83 description=_('Custom port that the LDAP server is listening on. '
87 description=_('Custom port that the LDAP server is listening on. '
84 'Default value is: 389'),
88 'Default value is: 389'),
85 preparer=strip_whitespace,
89 preparer=strip_whitespace,
86 title=_('Port'),
90 title=_('Port'),
87 validator=colander.Range(min=0, max=65536),
91 validator=colander.Range(min=0, max=65536),
88 widget='int')
92 widget='int')
89 dn_user = colander.SchemaNode(
93 dn_user = colander.SchemaNode(
90 colander.String(),
94 colander.String(),
91 default='',
95 default='',
92 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
96 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
93 'e.g., cn=admin,dc=mydomain,dc=com, or '
97 'e.g., cn=admin,dc=mydomain,dc=com, or '
94 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
98 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
95 missing='',
99 missing='',
96 preparer=strip_whitespace,
100 preparer=strip_whitespace,
97 title=_('Account'),
101 title=_('Account'),
98 widget='string')
102 widget='string')
99 dn_pass = colander.SchemaNode(
103 dn_pass = colander.SchemaNode(
100 colander.String(),
104 colander.String(),
101 default='',
105 default='',
102 description=_('Password to authenticate for given user DN.'),
106 description=_('Password to authenticate for given user DN.'),
103 missing='',
107 missing='',
104 preparer=strip_whitespace,
108 preparer=strip_whitespace,
105 title=_('Password'),
109 title=_('Password'),
106 widget='password')
110 widget='password')
107 tls_kind = colander.SchemaNode(
111 tls_kind = colander.SchemaNode(
108 colander.String(),
112 colander.String(),
109 default=tls_kind_choices[0],
113 default=tls_kind_choices[0],
110 description=_('TLS Type'),
114 description=_('TLS Type'),
111 title=_('Connection Security'),
115 title=_('Connection Security'),
112 validator=colander.OneOf(tls_kind_choices),
116 validator=colander.OneOf(tls_kind_choices),
113 widget='select')
117 widget='select')
114 tls_reqcert = colander.SchemaNode(
118 tls_reqcert = colander.SchemaNode(
115 colander.String(),
119 colander.String(),
116 default=tls_reqcert_choices[0],
120 default=tls_reqcert_choices[0],
117 description=_('Require Cert over TLS?. Self-signed and custom '
121 description=_('Require Cert over TLS?. Self-signed and custom '
118 'certificates can be used when\n `RhodeCode Certificate` '
122 'certificates can be used when\n `RhodeCode Certificate` '
119 'found in admin > settings > system info page is extended.'),
123 'found in admin > settings > system info page is extended.'),
120 title=_('Certificate Checks'),
124 title=_('Certificate Checks'),
121 validator=colander.OneOf(tls_reqcert_choices),
125 validator=colander.OneOf(tls_reqcert_choices),
122 widget='select')
126 widget='select')
123 base_dn = colander.SchemaNode(
127 base_dn = colander.SchemaNode(
124 colander.String(),
128 colander.String(),
125 default='',
129 default='',
126 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
130 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
127 'in it to be replaced with current user credentials \n'
131 'in it to be replaced with current user credentials \n'
128 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
132 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
129 missing='',
133 missing='',
130 preparer=strip_whitespace,
134 preparer=strip_whitespace,
131 title=_('Base DN'),
135 title=_('Base DN'),
132 widget='string')
136 widget='string')
133 filter = colander.SchemaNode(
137 filter = colander.SchemaNode(
134 colander.String(),
138 colander.String(),
135 default='',
139 default='',
136 description=_('Filter to narrow results \n'
140 description=_('Filter to narrow results \n'
137 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
141 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
138 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
142 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
139 missing='',
143 missing='',
140 preparer=strip_whitespace,
144 preparer=strip_whitespace,
141 title=_('LDAP Search Filter'),
145 title=_('LDAP Search Filter'),
142 widget='string')
146 widget='string')
143
147
144 search_scope = colander.SchemaNode(
148 search_scope = colander.SchemaNode(
145 colander.String(),
149 colander.String(),
146 default=search_scope_choices[2],
150 default=search_scope_choices[2],
147 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
151 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
148 title=_('LDAP Search Scope'),
152 title=_('LDAP Search Scope'),
149 validator=colander.OneOf(search_scope_choices),
153 validator=colander.OneOf(search_scope_choices),
150 widget='select')
154 widget='select')
151 attr_login = colander.SchemaNode(
155 attr_login = colander.SchemaNode(
152 colander.String(),
156 colander.String(),
153 default='uid',
157 default='uid',
154 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
158 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
155 preparer=strip_whitespace,
159 preparer=strip_whitespace,
156 title=_('Login Attribute'),
160 title=_('Login Attribute'),
157 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
161 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
158 widget='string')
162 widget='string')
159 attr_firstname = colander.SchemaNode(
163 attr_firstname = colander.SchemaNode(
160 colander.String(),
164 colander.String(),
161 default='',
165 default='',
162 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
166 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
163 missing='',
167 missing='',
164 preparer=strip_whitespace,
168 preparer=strip_whitespace,
165 title=_('First Name Attribute'),
169 title=_('First Name Attribute'),
166 widget='string')
170 widget='string')
167 attr_lastname = colander.SchemaNode(
171 attr_lastname = colander.SchemaNode(
168 colander.String(),
172 colander.String(),
169 default='',
173 default='',
170 description=_('LDAP Attribute to map to last name (e.g., sn)'),
174 description=_('LDAP Attribute to map to last name (e.g., sn)'),
171 missing='',
175 missing='',
172 preparer=strip_whitespace,
176 preparer=strip_whitespace,
173 title=_('Last Name Attribute'),
177 title=_('Last Name Attribute'),
174 widget='string')
178 widget='string')
175 attr_email = colander.SchemaNode(
179 attr_email = colander.SchemaNode(
176 colander.String(),
180 colander.String(),
177 default='',
181 default='',
178 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
182 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
179 'Emails are a crucial part of RhodeCode. \n'
183 'Emails are a crucial part of RhodeCode. \n'
180 'If possible add a valid email attribute to ldap users.'),
184 'If possible add a valid email attribute to ldap users.'),
181 missing='',
185 missing='',
182 preparer=strip_whitespace,
186 preparer=strip_whitespace,
183 title=_('Email Attribute'),
187 title=_('Email Attribute'),
184 widget='string')
188 widget='string')
185
189
186
190
187 class AuthLdap(object):
191 class AuthLdap(object):
188
192
189 def _build_servers(self):
193 def _build_servers(self):
190 return ', '.join(
194 return ', '.join(
191 ["{}://{}:{}".format(
195 ["{}://{}:{}".format(
192 self.ldap_server_type, host.strip(), self.LDAP_SERVER_PORT)
196 self.ldap_server_type, host.strip(), self.LDAP_SERVER_PORT)
193 for host in self.SERVER_ADDRESSES])
197 for host in self.SERVER_ADDRESSES])
194
198
195 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
199 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
196 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
200 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
197 search_scope='SUBTREE', attr_login='uid',
201 search_scope='SUBTREE', attr_login='uid',
198 ldap_filter=''):
202 ldap_filter=''):
199 if ldap == Missing:
203 if ldap == Missing:
200 raise LdapImportError("Missing or incompatible ldap library")
204 raise LdapImportError("Missing or incompatible ldap library")
201
205
202 self.debug = False
206 self.debug = False
203 self.ldap_version = ldap_version
207 self.ldap_version = ldap_version
204 self.ldap_server_type = 'ldap'
208 self.ldap_server_type = 'ldap'
205
209
206 self.TLS_KIND = tls_kind
210 self.TLS_KIND = tls_kind
207
211
208 if self.TLS_KIND == 'LDAPS':
212 if self.TLS_KIND == 'LDAPS':
209 port = port or 689
213 port = port or 689
210 self.ldap_server_type += 's'
214 self.ldap_server_type += 's'
211
215
212 OPT_X_TLS_DEMAND = 2
216 OPT_X_TLS_DEMAND = 2
213 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
217 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
214 OPT_X_TLS_DEMAND)
218 OPT_X_TLS_DEMAND)
215 # split server into list
219 # split server into list
216 self.SERVER_ADDRESSES = server.split(',')
220 self.SERVER_ADDRESSES = server.split(',')
217 self.LDAP_SERVER_PORT = port
221 self.LDAP_SERVER_PORT = port
218
222
219 # USE FOR READ ONLY BIND TO LDAP SERVER
223 # USE FOR READ ONLY BIND TO LDAP SERVER
220 self.attr_login = attr_login
224 self.attr_login = attr_login
221
225
222 self.LDAP_BIND_DN = safe_str(bind_dn)
226 self.LDAP_BIND_DN = safe_str(bind_dn)
223 self.LDAP_BIND_PASS = safe_str(bind_pass)
227 self.LDAP_BIND_PASS = safe_str(bind_pass)
224 self.LDAP_SERVER = self._build_servers()
228 self.LDAP_SERVER = self._build_servers()
225 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
229 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
226 self.BASE_DN = safe_str(base_dn)
230 self.BASE_DN = safe_str(base_dn)
227 self.LDAP_FILTER = safe_str(ldap_filter)
231 self.LDAP_FILTER = safe_str(ldap_filter)
228
232
229 def _get_ldap_server(self):
233 def _get_ldap_conn(self):
230 if self.debug:
234 if self.debug:
231 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
235 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
236
232 if hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
237 if hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
233 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR,
238 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR,
234 '/etc/openldap/cacerts')
239 '/etc/openldap/cacerts')
235 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
240 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
236 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
241 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
237 ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 60 * 10)
242 ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 60 * 10)
238 ldap.set_option(ldap.OPT_TIMEOUT, 60 * 10)
243 ldap.set_option(ldap.OPT_TIMEOUT, 60 * 10)
239
244
240 if self.TLS_KIND != 'PLAIN':
245 if self.TLS_KIND != 'PLAIN':
241 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
246 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
242 server = ldap.initialize(self.LDAP_SERVER)
247
248 log.debug('initializing LDAP connection to:%s', self.LDAP_SERVER)
249 ldap_conn = ldap.initialize(self.LDAP_SERVER)
243 if self.ldap_version == 2:
250 if self.ldap_version == 2:
244 server.protocol = ldap.VERSION2
251 ldap_conn.protocol = ldap.VERSION2
245 else:
252 else:
246 server.protocol = ldap.VERSION3
253 ldap_conn.protocol = ldap.VERSION3
247
254
248 if self.TLS_KIND == 'START_TLS':
255 if self.TLS_KIND == 'START_TLS':
249 server.start_tls_s()
256 ldap_conn.start_tls_s()
250
257
251 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
258 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
252 log.debug('Trying simple_bind with password and given login DN: %s',
259 log.debug('Trying simple_bind with password and given login DN: %s',
253 self.LDAP_BIND_DN)
260 self.LDAP_BIND_DN)
254 server.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
261 ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
255
262
256 return server
263 return ldap_conn
257
264
258 def get_uid(self, username):
265 def get_uid(self, username):
259 uid = username
266 uid = username
260 for server_addr in self.SERVER_ADDRESSES:
267 for server_addr in self.SERVER_ADDRESSES:
261 uid = chop_at(username, "@%s" % server_addr)
268 uid = chop_at(username, "@%s" % server_addr)
262 return uid
269 return uid
263
270
264 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
271 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
265 try:
272 try:
266 log.debug('Trying simple bind with %s', dn)
273 log.debug('Trying simple bind with %s', dn)
267 server.simple_bind_s(dn, safe_str(password))
274 server.simple_bind_s(dn, safe_str(password))
268 user = server.search_ext_s(
275 user = server.search_ext_s(
269 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
276 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
270 _, attrs = user
277 _, attrs = user
271 return attrs
278 return attrs
272
279
273 except ldap.INVALID_CREDENTIALS:
280 except ldap.INVALID_CREDENTIALS:
274 log.debug(
281 log.debug(
275 "LDAP rejected password for user '%s': %s, org_exc:",
282 "LDAP rejected password for user '%s': %s, org_exc:",
276 username, dn, exc_info=True)
283 username, dn, exc_info=True)
277
284
278 def authenticate_ldap(self, username, password):
285 def authenticate_ldap(self, username, password):
279 """
286 """
280 Authenticate a user via LDAP and return his/her LDAP properties.
287 Authenticate a user via LDAP and return his/her LDAP properties.
281
288
282 Raises AuthenticationError if the credentials are rejected, or
289 Raises AuthenticationError if the credentials are rejected, or
283 EnvironmentError if the LDAP server can't be reached.
290 EnvironmentError if the LDAP server can't be reached.
284
291
285 :param username: username
292 :param username: username
286 :param password: password
293 :param password: password
287 """
294 """
288
295
289 uid = self.get_uid(username)
296 uid = self.get_uid(username)
290
297
291 if not password:
298 if not password:
292 msg = "Authenticating user %s with blank password not allowed"
299 msg = "Authenticating user %s with blank password not allowed"
293 log.warning(msg, username)
300 log.warning(msg, username)
294 raise LdapPasswordError(msg)
301 raise LdapPasswordError(msg)
295 if "," in username:
302 if "," in username:
296 raise LdapUsernameError(
303 raise LdapUsernameError(
297 "invalid character `,` in username: `{}`".format(username))
304 "invalid character `,` in username: `{}`".format(username))
305 ldap_conn = None
298 try:
306 try:
299 server = self._get_ldap_server()
307 ldap_conn = self._get_ldap_conn()
300 filter_ = '(&%s(%s=%s))' % (
308 filter_ = '(&%s(%s=%s))' % (
301 self.LDAP_FILTER, self.attr_login, username)
309 self.LDAP_FILTER, self.attr_login, username)
302 log.debug("Authenticating %r filter %s at %s", self.BASE_DN,
310 log.debug("Authenticating %r filter %s at %s", self.BASE_DN,
303 filter_, self.LDAP_SERVER)
311 filter_, self.LDAP_SERVER)
304 lobjects = server.search_ext_s(
312 lobjects = ldap_conn.search_ext_s(
305 self.BASE_DN, self.SEARCH_SCOPE, filter_)
313 self.BASE_DN, self.SEARCH_SCOPE, filter_)
306
314
307 if not lobjects:
315 if not lobjects:
308 log.debug("No matching LDAP objects for authentication "
316 log.debug("No matching LDAP objects for authentication "
309 "of UID:'%s' username:(%s)", uid, username)
317 "of UID:'%s' username:(%s)", uid, username)
310 raise ldap.NO_SUCH_OBJECT()
318 raise ldap.NO_SUCH_OBJECT()
311
319
312 log.debug('Found matching ldap object, trying to authenticate')
320 log.debug('Found matching ldap object, trying to authenticate')
313 for (dn, _attrs) in lobjects:
321 for (dn, _attrs) in lobjects:
314 if dn is None:
322 if dn is None:
315 continue
323 continue
316
324
317 user_attrs = self.fetch_attrs_from_simple_bind(
325 user_attrs = self.fetch_attrs_from_simple_bind(
318 server, dn, username, password)
326 ldap_conn, dn, username, password)
319 if user_attrs:
327 if user_attrs:
320 break
328 break
321
329
322 else:
330 else:
323 raise LdapPasswordError(
331 raise LdapPasswordError(
324 'Failed to authenticate user `{}`'
332 'Failed to authenticate user `{}`'
325 'with given password'.format(username))
333 'with given password'.format(username))
326
334
327 except ldap.NO_SUCH_OBJECT:
335 except ldap.NO_SUCH_OBJECT:
328 log.debug("LDAP says no such user '%s' (%s), org_exc:",
336 log.debug("LDAP says no such user '%s' (%s), org_exc:",
329 uid, username, exc_info=True)
337 uid, username, exc_info=True)
330 raise LdapUsernameError('Unable to find user')
338 raise LdapUsernameError('Unable to find user')
331 except ldap.SERVER_DOWN:
339 except ldap.SERVER_DOWN:
332 org_exc = traceback.format_exc()
340 org_exc = traceback.format_exc()
333 raise LdapConnectionError(
341 raise LdapConnectionError(
334 "LDAP can't access authentication "
342 "LDAP can't access authentication "
335 "server, org_exc:%s" % org_exc)
343 "server, org_exc:%s" % org_exc)
344 finally:
345 if ldap_conn:
346 log.debug('ldap: connection release')
347 try:
348 ldap_conn.unbind_s()
349 except Exception:
350 # for any reason this can raise exception we must catch it
351 # to not crush the server
352 pass
336
353
337 return dn, user_attrs
354 return dn, user_attrs
338
355
339
356
340 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
357 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
341 # used to define dynamic binding in the
358 # used to define dynamic binding in the
342 DYNAMIC_BIND_VAR = '$login'
359 DYNAMIC_BIND_VAR = '$login'
343 _settings_unsafe_keys = ['dn_pass']
360 _settings_unsafe_keys = ['dn_pass']
344
361
345 def includeme(self, config):
362 def includeme(self, config):
346 config.add_authn_plugin(self)
363 config.add_authn_plugin(self)
347 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
364 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
348 config.add_view(
365 config.add_view(
349 'rhodecode.authentication.views.AuthnPluginViewBase',
366 'rhodecode.authentication.views.AuthnPluginViewBase',
350 attr='settings_get',
367 attr='settings_get',
351 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
368 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
352 request_method='GET',
369 request_method='GET',
353 route_name='auth_home',
370 route_name='auth_home',
354 context=LdapAuthnResource)
371 context=LdapAuthnResource)
355 config.add_view(
372 config.add_view(
356 'rhodecode.authentication.views.AuthnPluginViewBase',
373 'rhodecode.authentication.views.AuthnPluginViewBase',
357 attr='settings_post',
374 attr='settings_post',
358 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
375 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
359 request_method='POST',
376 request_method='POST',
360 route_name='auth_home',
377 route_name='auth_home',
361 context=LdapAuthnResource)
378 context=LdapAuthnResource)
362
379
363 def get_settings_schema(self):
380 def get_settings_schema(self):
364 return LdapSettingsSchema()
381 return LdapSettingsSchema()
365
382
366 def get_display_name(self):
383 def get_display_name(self):
367 return _('LDAP')
384 return _('LDAP')
368
385
369 @hybrid_property
386 @hybrid_property
370 def name(self):
387 def name(self):
371 return "ldap"
388 return "ldap"
372
389
373 def use_fake_password(self):
390 def use_fake_password(self):
374 return True
391 return True
375
392
376 def user_activation_state(self):
393 def user_activation_state(self):
377 def_user_perms = User.get_default_user().AuthUser().permissions['global']
394 def_user_perms = User.get_default_user().AuthUser().permissions['global']
378 return 'hg.extern_activate.auto' in def_user_perms
395 return 'hg.extern_activate.auto' in def_user_perms
379
396
380 def try_dynamic_binding(self, username, password, current_args):
397 def try_dynamic_binding(self, username, password, current_args):
381 """
398 """
382 Detects marker inside our original bind, and uses dynamic auth if
399 Detects marker inside our original bind, and uses dynamic auth if
383 present
400 present
384 """
401 """
385
402
386 org_bind = current_args['bind_dn']
403 org_bind = current_args['bind_dn']
387 passwd = current_args['bind_pass']
404 passwd = current_args['bind_pass']
388
405
389 def has_bind_marker(username):
406 def has_bind_marker(username):
390 if self.DYNAMIC_BIND_VAR in username:
407 if self.DYNAMIC_BIND_VAR in username:
391 return True
408 return True
392
409
393 # we only passed in user with "special" variable
410 # we only passed in user with "special" variable
394 if org_bind and has_bind_marker(org_bind) and not passwd:
411 if org_bind and has_bind_marker(org_bind) and not passwd:
395 log.debug('Using dynamic user/password binding for ldap '
412 log.debug('Using dynamic user/password binding for ldap '
396 'authentication. Replacing `%s` with username',
413 'authentication. Replacing `%s` with username',
397 self.DYNAMIC_BIND_VAR)
414 self.DYNAMIC_BIND_VAR)
398 current_args['bind_dn'] = org_bind.replace(
415 current_args['bind_dn'] = org_bind.replace(
399 self.DYNAMIC_BIND_VAR, username)
416 self.DYNAMIC_BIND_VAR, username)
400 current_args['bind_pass'] = password
417 current_args['bind_pass'] = password
401
418
402 return current_args
419 return current_args
403
420
404 def auth(self, userobj, username, password, settings, **kwargs):
421 def auth(self, userobj, username, password, settings, **kwargs):
405 """
422 """
406 Given a user object (which may be null), username, a plaintext password,
423 Given a user object (which may be null), username, a plaintext password,
407 and a settings object (containing all the keys needed as listed in
424 and a settings object (containing all the keys needed as listed in
408 settings()), authenticate this user's login attempt.
425 settings()), authenticate this user's login attempt.
409
426
410 Return None on failure. On success, return a dictionary of the form:
427 Return None on failure. On success, return a dictionary of the form:
411
428
412 see: RhodeCodeAuthPluginBase.auth_func_attrs
429 see: RhodeCodeAuthPluginBase.auth_func_attrs
413 This is later validated for correctness
430 This is later validated for correctness
414 """
431 """
415
432
416 if not username or not password:
433 if not username or not password:
417 log.debug('Empty username or password skipping...')
434 log.debug('Empty username or password skipping...')
418 return None
435 return None
419
436
420 ldap_args = {
437 ldap_args = {
421 'server': settings.get('host', ''),
438 'server': settings.get('host', ''),
422 'base_dn': settings.get('base_dn', ''),
439 'base_dn': settings.get('base_dn', ''),
423 'port': settings.get('port'),
440 'port': settings.get('port'),
424 'bind_dn': settings.get('dn_user'),
441 'bind_dn': settings.get('dn_user'),
425 'bind_pass': settings.get('dn_pass'),
442 'bind_pass': settings.get('dn_pass'),
426 'tls_kind': settings.get('tls_kind'),
443 'tls_kind': settings.get('tls_kind'),
427 'tls_reqcert': settings.get('tls_reqcert'),
444 'tls_reqcert': settings.get('tls_reqcert'),
428 'search_scope': settings.get('search_scope'),
445 'search_scope': settings.get('search_scope'),
429 'attr_login': settings.get('attr_login'),
446 'attr_login': settings.get('attr_login'),
430 'ldap_version': 3,
447 'ldap_version': 3,
431 'ldap_filter': settings.get('filter'),
448 'ldap_filter': settings.get('filter'),
432 }
449 }
433
450
434 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
451 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
435
452
436 log.debug('Checking for ldap authentication.')
453 log.debug('Checking for ldap authentication.')
437
454
438 try:
455 try:
439 aldap = AuthLdap(**ldap_args)
456 aldap = AuthLdap(**ldap_args)
440 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
457 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
441 log.debug('Got ldap DN response %s', user_dn)
458 log.debug('Got ldap DN response %s', user_dn)
442
459
443 def get_ldap_attr(k):
460 def get_ldap_attr(k):
444 return ldap_attrs.get(settings.get(k), [''])[0]
461 return ldap_attrs.get(settings.get(k), [''])[0]
445
462
446 # old attrs fetched from RhodeCode database
463 # old attrs fetched from RhodeCode database
447 admin = getattr(userobj, 'admin', False)
464 admin = getattr(userobj, 'admin', False)
448 active = getattr(userobj, 'active', True)
465 active = getattr(userobj, 'active', True)
449 email = getattr(userobj, 'email', '')
466 email = getattr(userobj, 'email', '')
450 username = getattr(userobj, 'username', username)
467 username = getattr(userobj, 'username', username)
451 firstname = getattr(userobj, 'firstname', '')
468 firstname = getattr(userobj, 'firstname', '')
452 lastname = getattr(userobj, 'lastname', '')
469 lastname = getattr(userobj, 'lastname', '')
453 extern_type = getattr(userobj, 'extern_type', '')
470 extern_type = getattr(userobj, 'extern_type', '')
454
471
455 groups = []
472 groups = []
456 user_attrs = {
473 user_attrs = {
457 'username': username,
474 'username': username,
458 'firstname': safe_unicode(
475 'firstname': safe_unicode(
459 get_ldap_attr('attr_firstname') or firstname),
476 get_ldap_attr('attr_firstname') or firstname),
460 'lastname': safe_unicode(
477 'lastname': safe_unicode(
461 get_ldap_attr('attr_lastname') or lastname),
478 get_ldap_attr('attr_lastname') or lastname),
462 'groups': groups,
479 'groups': groups,
463 'user_group_sync': False,
480 'user_group_sync': False,
464 'email': get_ldap_attr('attr_email') or email,
481 'email': get_ldap_attr('attr_email') or email,
465 'admin': admin,
482 'admin': admin,
466 'active': active,
483 'active': active,
467 'active_from_extern': None,
484 'active_from_extern': None,
468 'extern_name': user_dn,
485 'extern_name': user_dn,
469 'extern_type': extern_type,
486 'extern_type': extern_type,
470 }
487 }
471 log.debug('ldap user: %s', user_attrs)
488 log.debug('ldap user: %s', user_attrs)
472 log.info('user %s authenticated correctly', user_attrs['username'])
489 log.info('user %s authenticated correctly', user_attrs['username'])
473
490
474 return user_attrs
491 return user_attrs
475
492
476 except (LdapUsernameError, LdapPasswordError, LdapImportError):
493 except (LdapUsernameError, LdapPasswordError, LdapImportError):
477 log.exception("LDAP related exception")
494 log.exception("LDAP related exception")
478 return None
495 return None
479 except (Exception,):
496 except (Exception,):
480 log.exception("Other exception")
497 log.exception("Other exception")
481 return None
498 return None
General Comments 0
You need to be logged in to leave comments. Login now