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