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