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