##// END OF EJS Templates
ldap: fixed email extraction typo. An empty...
marcink -
r886:8170c0f9 default
parent child Browse files
Show More
@@ -1,464 +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 preparer=strip_whitespace,
144 preparer=strip_whitespace,
145 title=_('Login Attribute'),
145 title=_('Login Attribute'),
146 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
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.debug = False
190 self.ldap_version = ldap_version
190 self.ldap_version = ldap_version
191 self.ldap_server_type = 'ldap'
191 self.ldap_server_type = 'ldap'
192
192
193 self.TLS_KIND = tls_kind
193 self.TLS_KIND = tls_kind
194
194
195 if self.TLS_KIND == 'LDAPS':
195 if self.TLS_KIND == 'LDAPS':
196 port = port or 689
196 port = port or 689
197 self.ldap_server_type += 's'
197 self.ldap_server_type += 's'
198
198
199 OPT_X_TLS_DEMAND = 2
199 OPT_X_TLS_DEMAND = 2
200 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
200 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
201 OPT_X_TLS_DEMAND)
201 OPT_X_TLS_DEMAND)
202 # split server into list
202 # split server into list
203 self.SERVER_ADDRESSES = server.split(',')
203 self.SERVER_ADDRESSES = server.split(',')
204 self.LDAP_SERVER_PORT = port
204 self.LDAP_SERVER_PORT = port
205
205
206 # USE FOR READ ONLY BIND TO LDAP SERVER
206 # USE FOR READ ONLY BIND TO LDAP SERVER
207 self.attr_login = attr_login
207 self.attr_login = attr_login
208
208
209 self.LDAP_BIND_DN = safe_str(bind_dn)
209 self.LDAP_BIND_DN = safe_str(bind_dn)
210 self.LDAP_BIND_PASS = safe_str(bind_pass)
210 self.LDAP_BIND_PASS = safe_str(bind_pass)
211 self.LDAP_SERVER = self._build_servers()
211 self.LDAP_SERVER = self._build_servers()
212 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
212 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
213 self.BASE_DN = safe_str(base_dn)
213 self.BASE_DN = safe_str(base_dn)
214 self.LDAP_FILTER = safe_str(ldap_filter)
214 self.LDAP_FILTER = safe_str(ldap_filter)
215
215
216 def _get_ldap_server(self):
216 def _get_ldap_server(self):
217 if self.debug:
217 if self.debug:
218 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
218 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
219 if hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
219 if hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
220 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR,
220 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR,
221 '/etc/openldap/cacerts')
221 '/etc/openldap/cacerts')
222 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
222 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
223 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
223 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
224 ldap.set_option(ldap.OPT_TIMEOUT, 20)
224 ldap.set_option(ldap.OPT_TIMEOUT, 20)
225 ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 10)
225 ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 10)
226 ldap.set_option(ldap.OPT_TIMELIMIT, 15)
226 ldap.set_option(ldap.OPT_TIMELIMIT, 15)
227 if self.TLS_KIND != 'PLAIN':
227 if self.TLS_KIND != 'PLAIN':
228 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)
229 server = ldap.initialize(self.LDAP_SERVER)
229 server = ldap.initialize(self.LDAP_SERVER)
230 if self.ldap_version == 2:
230 if self.ldap_version == 2:
231 server.protocol = ldap.VERSION2
231 server.protocol = ldap.VERSION2
232 else:
232 else:
233 server.protocol = ldap.VERSION3
233 server.protocol = ldap.VERSION3
234
234
235 if self.TLS_KIND == 'START_TLS':
235 if self.TLS_KIND == 'START_TLS':
236 server.start_tls_s()
236 server.start_tls_s()
237
237
238 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
238 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
239 log.debug('Trying simple_bind with password and given DN: %s',
239 log.debug('Trying simple_bind with password and given DN: %s',
240 self.LDAP_BIND_DN)
240 self.LDAP_BIND_DN)
241 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)
242
242
243 return server
243 return server
244
244
245 def get_uid(self, username):
245 def get_uid(self, username):
246 from rhodecode.lib.helpers import chop_at
246 from rhodecode.lib.helpers import chop_at
247 uid = username
247 uid = username
248 for server_addr in self.SERVER_ADDRESSES:
248 for server_addr in self.SERVER_ADDRESSES:
249 uid = chop_at(username, "@%s" % server_addr)
249 uid = chop_at(username, "@%s" % server_addr)
250 return uid
250 return uid
251
251
252 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
252 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
253 try:
253 try:
254 log.debug('Trying simple bind with %s', dn)
254 log.debug('Trying simple bind with %s', dn)
255 server.simple_bind_s(dn, safe_str(password))
255 server.simple_bind_s(dn, safe_str(password))
256 user = server.search_ext_s(
256 user = server.search_ext_s(
257 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
257 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
258 _, attrs = user
258 _, attrs = user
259 return attrs
259 return attrs
260
260
261 except ldap.INVALID_CREDENTIALS:
261 except ldap.INVALID_CREDENTIALS:
262 log.debug(
262 log.debug(
263 "LDAP rejected password for user '%s': %s, org_exc:",
263 "LDAP rejected password for user '%s': %s, org_exc:",
264 username, dn, exc_info=True)
264 username, dn, exc_info=True)
265
265
266 def authenticate_ldap(self, username, password):
266 def authenticate_ldap(self, username, password):
267 """
267 """
268 Authenticate a user via LDAP and return his/her LDAP properties.
268 Authenticate a user via LDAP and return his/her LDAP properties.
269
269
270 Raises AuthenticationError if the credentials are rejected, or
270 Raises AuthenticationError if the credentials are rejected, or
271 EnvironmentError if the LDAP server can't be reached.
271 EnvironmentError if the LDAP server can't be reached.
272
272
273 :param username: username
273 :param username: username
274 :param password: password
274 :param password: password
275 """
275 """
276
276
277 uid = self.get_uid(username)
277 uid = self.get_uid(username)
278
278
279 if not password:
279 if not password:
280 msg = "Authenticating user %s with blank password not allowed"
280 msg = "Authenticating user %s with blank password not allowed"
281 log.warning(msg, username)
281 log.warning(msg, username)
282 raise LdapPasswordError(msg)
282 raise LdapPasswordError(msg)
283 if "," in username:
283 if "," in username:
284 raise LdapUsernameError("invalid character in username: ,")
284 raise LdapUsernameError("invalid character in username: ,")
285 try:
285 try:
286 server = self._get_ldap_server()
286 server = self._get_ldap_server()
287 filter_ = '(&%s(%s=%s))' % (
287 filter_ = '(&%s(%s=%s))' % (
288 self.LDAP_FILTER, self.attr_login, username)
288 self.LDAP_FILTER, self.attr_login, username)
289 log.debug("Authenticating %r filter %s at %s", self.BASE_DN,
289 log.debug("Authenticating %r filter %s at %s", self.BASE_DN,
290 filter_, self.LDAP_SERVER)
290 filter_, self.LDAP_SERVER)
291 lobjects = server.search_ext_s(
291 lobjects = server.search_ext_s(
292 self.BASE_DN, self.SEARCH_SCOPE, filter_)
292 self.BASE_DN, self.SEARCH_SCOPE, filter_)
293
293
294 if not lobjects:
294 if not lobjects:
295 raise ldap.NO_SUCH_OBJECT()
295 raise ldap.NO_SUCH_OBJECT()
296
296
297 for (dn, _attrs) in lobjects:
297 for (dn, _attrs) in lobjects:
298 if dn is None:
298 if dn is None:
299 continue
299 continue
300
300
301 user_attrs = self.fetch_attrs_from_simple_bind(
301 user_attrs = self.fetch_attrs_from_simple_bind(
302 server, dn, username, password)
302 server, dn, username, password)
303 if user_attrs:
303 if user_attrs:
304 break
304 break
305
305
306 else:
306 else:
307 log.debug("No matching LDAP objects for authentication "
307 log.debug("No matching LDAP objects for authentication "
308 "of '%s' (%s)", uid, username)
308 "of '%s' (%s)", uid, username)
309 raise LdapPasswordError('Failed to authenticate user '
309 raise LdapPasswordError('Failed to authenticate user '
310 'with given password')
310 'with given password')
311
311
312 except ldap.NO_SUCH_OBJECT:
312 except ldap.NO_SUCH_OBJECT:
313 log.debug("LDAP says no such user '%s' (%s), org_exc:",
313 log.debug("LDAP says no such user '%s' (%s), org_exc:",
314 uid, username, exc_info=True)
314 uid, username, exc_info=True)
315 raise LdapUsernameError()
315 raise LdapUsernameError()
316 except ldap.SERVER_DOWN:
316 except ldap.SERVER_DOWN:
317 org_exc = traceback.format_exc()
317 org_exc = traceback.format_exc()
318 raise LdapConnectionError(
318 raise LdapConnectionError(
319 "LDAP can't access authentication "
319 "LDAP can't access authentication "
320 "server, org_exc:%s" % org_exc)
320 "server, org_exc:%s" % org_exc)
321
321
322 return dn, user_attrs
322 return dn, user_attrs
323
323
324
324
325 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
325 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
326 # used to define dynamic binding in the
326 # used to define dynamic binding in the
327 DYNAMIC_BIND_VAR = '$login'
327 DYNAMIC_BIND_VAR = '$login'
328
328
329 def includeme(self, config):
329 def includeme(self, config):
330 config.add_authn_plugin(self)
330 config.add_authn_plugin(self)
331 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
331 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
332 config.add_view(
332 config.add_view(
333 'rhodecode.authentication.views.AuthnPluginViewBase',
333 'rhodecode.authentication.views.AuthnPluginViewBase',
334 attr='settings_get',
334 attr='settings_get',
335 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
335 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
336 request_method='GET',
336 request_method='GET',
337 route_name='auth_home',
337 route_name='auth_home',
338 context=LdapAuthnResource)
338 context=LdapAuthnResource)
339 config.add_view(
339 config.add_view(
340 'rhodecode.authentication.views.AuthnPluginViewBase',
340 'rhodecode.authentication.views.AuthnPluginViewBase',
341 attr='settings_post',
341 attr='settings_post',
342 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
342 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
343 request_method='POST',
343 request_method='POST',
344 route_name='auth_home',
344 route_name='auth_home',
345 context=LdapAuthnResource)
345 context=LdapAuthnResource)
346
346
347 def get_settings_schema(self):
347 def get_settings_schema(self):
348 return LdapSettingsSchema()
348 return LdapSettingsSchema()
349
349
350 def get_display_name(self):
350 def get_display_name(self):
351 return _('LDAP')
351 return _('LDAP')
352
352
353 @hybrid_property
353 @hybrid_property
354 def name(self):
354 def name(self):
355 return "ldap"
355 return "ldap"
356
356
357 def use_fake_password(self):
357 def use_fake_password(self):
358 return True
358 return True
359
359
360 def user_activation_state(self):
360 def user_activation_state(self):
361 def_user_perms = User.get_default_user().AuthUser.permissions['global']
361 def_user_perms = User.get_default_user().AuthUser.permissions['global']
362 return 'hg.extern_activate.auto' in def_user_perms
362 return 'hg.extern_activate.auto' in def_user_perms
363
363
364 def try_dynamic_binding(self, username, password, current_args):
364 def try_dynamic_binding(self, username, password, current_args):
365 """
365 """
366 Detects marker inside our original bind, and uses dynamic auth if
366 Detects marker inside our original bind, and uses dynamic auth if
367 present
367 present
368 """
368 """
369
369
370 org_bind = current_args['bind_dn']
370 org_bind = current_args['bind_dn']
371 passwd = current_args['bind_pass']
371 passwd = current_args['bind_pass']
372
372
373 def has_bind_marker(username):
373 def has_bind_marker(username):
374 if self.DYNAMIC_BIND_VAR in username:
374 if self.DYNAMIC_BIND_VAR in username:
375 return True
375 return True
376
376
377 # we only passed in user with "special" variable
377 # we only passed in user with "special" variable
378 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:
379 log.debug('Using dynamic user/password binding for ldap '
379 log.debug('Using dynamic user/password binding for ldap '
380 'authentication. Replacing `%s` with username',
380 'authentication. Replacing `%s` with username',
381 self.DYNAMIC_BIND_VAR)
381 self.DYNAMIC_BIND_VAR)
382 current_args['bind_dn'] = org_bind.replace(
382 current_args['bind_dn'] = org_bind.replace(
383 self.DYNAMIC_BIND_VAR, username)
383 self.DYNAMIC_BIND_VAR, username)
384 current_args['bind_pass'] = password
384 current_args['bind_pass'] = password
385
385
386 return current_args
386 return current_args
387
387
388 def auth(self, userobj, username, password, settings, **kwargs):
388 def auth(self, userobj, username, password, settings, **kwargs):
389 """
389 """
390 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,
391 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
392 settings()), authenticate this user's login attempt.
392 settings()), authenticate this user's login attempt.
393
393
394 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:
395
395
396 see: RhodeCodeAuthPluginBase.auth_func_attrs
396 see: RhodeCodeAuthPluginBase.auth_func_attrs
397 This is later validated for correctness
397 This is later validated for correctness
398 """
398 """
399
399
400 if not username or not password:
400 if not username or not password:
401 log.debug('Empty username or password skipping...')
401 log.debug('Empty username or password skipping...')
402 return None
402 return None
403
403
404 ldap_args = {
404 ldap_args = {
405 'server': settings.get('host', ''),
405 'server': settings.get('host', ''),
406 'base_dn': settings.get('base_dn', ''),
406 'base_dn': settings.get('base_dn', ''),
407 'port': settings.get('port'),
407 'port': settings.get('port'),
408 'bind_dn': settings.get('dn_user'),
408 'bind_dn': settings.get('dn_user'),
409 'bind_pass': settings.get('dn_pass'),
409 'bind_pass': settings.get('dn_pass'),
410 'tls_kind': settings.get('tls_kind'),
410 'tls_kind': settings.get('tls_kind'),
411 'tls_reqcert': settings.get('tls_reqcert'),
411 'tls_reqcert': settings.get('tls_reqcert'),
412 'search_scope': settings.get('search_scope'),
412 'search_scope': settings.get('search_scope'),
413 'attr_login': settings.get('attr_login'),
413 'attr_login': settings.get('attr_login'),
414 'ldap_version': 3,
414 'ldap_version': 3,
415 'ldap_filter': settings.get('filter'),
415 'ldap_filter': settings.get('filter'),
416 }
416 }
417
417
418 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
418 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
419
419
420 log.debug('Checking for ldap authentication.')
420 log.debug('Checking for ldap authentication.')
421
421
422 try:
422 try:
423 aldap = AuthLdap(**ldap_args)
423 aldap = AuthLdap(**ldap_args)
424 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
424 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
425 log.debug('Got ldap DN response %s', user_dn)
425 log.debug('Got ldap DN response %s', user_dn)
426
426
427 def get_ldap_attr(k):
427 def get_ldap_attr(k):
428 return ldap_attrs.get(settings.get(k), [''])[0]
428 return ldap_attrs.get(settings.get(k), [''])[0]
429
429
430 # old attrs fetched from RhodeCode database
430 # old attrs fetched from RhodeCode database
431 admin = getattr(userobj, 'admin', False)
431 admin = getattr(userobj, 'admin', False)
432 active = getattr(userobj, 'active', True)
432 active = getattr(userobj, 'active', True)
433 email = getattr(userobj, 'email', '')
433 email = getattr(userobj, 'email', '')
434 username = getattr(userobj, 'username', username)
434 username = getattr(userobj, 'username', username)
435 firstname = getattr(userobj, 'firstname', '')
435 firstname = getattr(userobj, 'firstname', '')
436 lastname = getattr(userobj, 'lastname', '')
436 lastname = getattr(userobj, 'lastname', '')
437 extern_type = getattr(userobj, 'extern_type', '')
437 extern_type = getattr(userobj, 'extern_type', '')
438
438
439 groups = []
439 groups = []
440 user_attrs = {
440 user_attrs = {
441 'username': username,
441 'username': username,
442 'firstname': safe_unicode(
442 'firstname': safe_unicode(
443 get_ldap_attr('attr_firstname') or firstname),
443 get_ldap_attr('attr_firstname') or firstname),
444 'lastname': safe_unicode(
444 'lastname': safe_unicode(
445 get_ldap_attr('attr_lastname') or lastname),
445 get_ldap_attr('attr_lastname') or lastname),
446 'groups': groups,
446 'groups': groups,
447 'email': get_ldap_attr('attr_email' or email),
447 'email': get_ldap_attr('attr_email') or email,
448 'admin': admin,
448 'admin': admin,
449 'active': active,
449 'active': active,
450 "active_from_extern": None,
450 "active_from_extern": None,
451 'extern_name': user_dn,
451 'extern_name': user_dn,
452 'extern_type': extern_type,
452 'extern_type': extern_type,
453 }
453 }
454 log.debug('ldap user: %s', user_attrs)
454 log.debug('ldap user: %s', user_attrs)
455 log.info('user %s authenticated correctly', user_attrs['username'])
455 log.info('user %s authenticated correctly', user_attrs['username'])
456
456
457 return user_attrs
457 return user_attrs
458
458
459 except (LdapUsernameError, LdapPasswordError, LdapImportError):
459 except (LdapUsernameError, LdapPasswordError, LdapImportError):
460 log.exception("LDAP related exception")
460 log.exception("LDAP related exception")
461 return None
461 return None
462 except (Exception,):
462 except (Exception,):
463 log.exception("Other exception")
463 log.exception("Other exception")
464 return None
464 return None
General Comments 0
You need to be logged in to leave comments. Login now