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