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