##// END OF EJS Templates
ldap: changed plugin to help with debugging and error handling....
marcink -
r4303:5bc0438b default
parent child Browse files
Show More
@@ -1,535 +1,551 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 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 import logging
25 import logging
26 import traceback
26 import traceback
27
27
28 import colander
28 import colander
29 from rhodecode.translation import _
29 from rhodecode.translation import _
30 from rhodecode.authentication.base import (
30 from rhodecode.authentication.base import (
31 RhodeCodeExternalAuthPlugin, AuthLdapBase, hybrid_property)
31 RhodeCodeExternalAuthPlugin, AuthLdapBase, hybrid_property)
32 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
32 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
33 from rhodecode.authentication.routes import AuthnPluginResourceBase
33 from rhodecode.authentication.routes import AuthnPluginResourceBase
34 from rhodecode.lib.colander_utils import strip_whitespace
34 from rhodecode.lib.colander_utils import strip_whitespace
35 from rhodecode.lib.exceptions import (
35 from rhodecode.lib.exceptions import (
36 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
36 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
37 )
37 )
38 from rhodecode.lib.utils2 import safe_unicode, safe_str
38 from rhodecode.lib.utils2 import safe_unicode, safe_str
39 from rhodecode.model.db import User
39 from rhodecode.model.db import User
40 from rhodecode.model.validators import Missing
40 from rhodecode.model.validators import Missing
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44 try:
44 try:
45 import ldap
45 import ldap
46 except ImportError:
46 except ImportError:
47 # means that python-ldap is not installed, we use Missing object to mark
47 # means that python-ldap is not installed, we use Missing object to mark
48 # ldap lib is Missing
48 # ldap lib is Missing
49 ldap = Missing
49 ldap = Missing
50
50
51
51
52 class LdapError(Exception):
52 class LdapError(Exception):
53 pass
53 pass
54
54
55
55
56 def plugin_factory(plugin_id, *args, **kwargs):
56 def plugin_factory(plugin_id, *args, **kwargs):
57 """
57 """
58 Factory function that is called during plugin discovery.
58 Factory function that is called during plugin discovery.
59 It returns the plugin instance.
59 It returns the plugin instance.
60 """
60 """
61 plugin = RhodeCodeAuthPlugin(plugin_id)
61 plugin = RhodeCodeAuthPlugin(plugin_id)
62 return plugin
62 return plugin
63
63
64
64
65 class LdapAuthnResource(AuthnPluginResourceBase):
65 class LdapAuthnResource(AuthnPluginResourceBase):
66 pass
66 pass
67
67
68
68
69 class AuthLdap(AuthLdapBase):
69 class AuthLdap(AuthLdapBase):
70 default_tls_cert_dir = '/etc/openldap/cacerts'
70 default_tls_cert_dir = '/etc/openldap/cacerts'
71
71
72 scope_labels = {
73 ldap.SCOPE_BASE: 'SCOPE_BASE',
74 ldap.SCOPE_ONELEVEL: 'SCOPE_ONELEVEL',
75 ldap.SCOPE_SUBTREE: 'SCOPE_SUBTREE',
76 }
77
72 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
78 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
73 tls_kind='PLAIN', tls_reqcert='DEMAND', tls_cert_file=None,
79 tls_kind='PLAIN', tls_reqcert='DEMAND', tls_cert_file=None,
74 tls_cert_dir=None, ldap_version=3,
80 tls_cert_dir=None, ldap_version=3,
75 search_scope='SUBTREE', attr_login='uid',
81 search_scope='SUBTREE', attr_login='uid',
76 ldap_filter='', timeout=None):
82 ldap_filter='', timeout=None):
77 if ldap == Missing:
83 if ldap == Missing:
78 raise LdapImportError("Missing or incompatible ldap library")
84 raise LdapImportError("Missing or incompatible ldap library")
79
85
80 self.debug = False
86 self.debug = False
81 self.timeout = timeout or 60 * 5
87 self.timeout = timeout or 60 * 5
82 self.ldap_version = ldap_version
88 self.ldap_version = ldap_version
83 self.ldap_server_type = 'ldap'
89 self.ldap_server_type = 'ldap'
84
90
85 self.TLS_KIND = tls_kind
91 self.TLS_KIND = tls_kind
86
92
87 if self.TLS_KIND == 'LDAPS':
93 if self.TLS_KIND == 'LDAPS':
88 port = port or 636
94 port = port or 636
89 self.ldap_server_type += 's'
95 self.ldap_server_type += 's'
90
96
91 OPT_X_TLS_DEMAND = 2
97 OPT_X_TLS_DEMAND = 2
92 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert, OPT_X_TLS_DEMAND)
98 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert, OPT_X_TLS_DEMAND)
93 self.TLS_CERT_FILE = tls_cert_file or ''
99 self.TLS_CERT_FILE = tls_cert_file or ''
94 self.TLS_CERT_DIR = tls_cert_dir or self.default_tls_cert_dir
100 self.TLS_CERT_DIR = tls_cert_dir or self.default_tls_cert_dir
95
101
96 # split server into list
102 # split server into list
97 self.SERVER_ADDRESSES = self._get_server_list(server)
103 self.SERVER_ADDRESSES = self._get_server_list(server)
98 self.LDAP_SERVER_PORT = port
104 self.LDAP_SERVER_PORT = port
99
105
100 # USE FOR READ ONLY BIND TO LDAP SERVER
106 # USE FOR READ ONLY BIND TO LDAP SERVER
101 self.attr_login = attr_login
107 self.attr_login = attr_login
102
108
103 self.LDAP_BIND_DN = safe_str(bind_dn)
109 self.LDAP_BIND_DN = safe_str(bind_dn)
104 self.LDAP_BIND_PASS = safe_str(bind_pass)
110 self.LDAP_BIND_PASS = safe_str(bind_pass)
105
111
106 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
112 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
107 self.BASE_DN = safe_str(base_dn)
113 self.BASE_DN = safe_str(base_dn)
108 self.LDAP_FILTER = safe_str(ldap_filter)
114 self.LDAP_FILTER = safe_str(ldap_filter)
109
115
110 def _get_ldap_conn(self):
116 def _get_ldap_conn(self):
111
117
112 if self.debug:
118 if self.debug:
113 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
119 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
114
120
115 if self.TLS_CERT_FILE and hasattr(ldap, 'OPT_X_TLS_CACERTFILE'):
121 if self.TLS_CERT_FILE and hasattr(ldap, 'OPT_X_TLS_CACERTFILE'):
116 ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, self.TLS_CERT_FILE)
122 ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, self.TLS_CERT_FILE)
117
123
118 elif hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
124 elif hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
119 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, self.TLS_CERT_DIR)
125 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, self.TLS_CERT_DIR)
120
126
121 if self.TLS_KIND != 'PLAIN':
127 if self.TLS_KIND != 'PLAIN':
122 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
128 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
123
129
124 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
130 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
125 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
131 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
126
132
127 # init connection now
133 # init connection now
128 ldap_servers = self._build_servers(
134 ldap_servers = self._build_servers(
129 self.ldap_server_type, self.SERVER_ADDRESSES, self.LDAP_SERVER_PORT)
135 self.ldap_server_type, self.SERVER_ADDRESSES, self.LDAP_SERVER_PORT)
130 log.debug('initializing LDAP connection to:%s', ldap_servers)
136 log.debug('initializing LDAP connection to:%s', ldap_servers)
131 ldap_conn = ldap.initialize(ldap_servers)
137 ldap_conn = ldap.initialize(ldap_servers)
132 ldap_conn.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout)
138 ldap_conn.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout)
133 ldap_conn.set_option(ldap.OPT_TIMEOUT, self.timeout)
139 ldap_conn.set_option(ldap.OPT_TIMEOUT, self.timeout)
134 ldap_conn.timeout = self.timeout
140 ldap_conn.timeout = self.timeout
135
141
136 if self.ldap_version == 2:
142 if self.ldap_version == 2:
137 ldap_conn.protocol = ldap.VERSION2
143 ldap_conn.protocol = ldap.VERSION2
138 else:
144 else:
139 ldap_conn.protocol = ldap.VERSION3
145 ldap_conn.protocol = ldap.VERSION3
140
146
141 if self.TLS_KIND == 'START_TLS':
147 if self.TLS_KIND == 'START_TLS':
142 ldap_conn.start_tls_s()
148 ldap_conn.start_tls_s()
143
149
144 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
150 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
145 log.debug('Trying simple_bind with password and given login DN: %r',
151 log.debug('Trying simple_bind with password and given login DN: %r',
146 self.LDAP_BIND_DN)
152 self.LDAP_BIND_DN)
147 ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
153 ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
148 log.debug('simple_bind successful')
154 log.debug('simple_bind successful')
149 return ldap_conn
155 return ldap_conn
150
156
151 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
157 def fetch_attrs_from_simple_bind(self, ldap_conn, dn, username, password):
158 scope = ldap.SCOPE_BASE
159 scope_label = self.scope_labels.get(scope)
160 ldap_filter = '(objectClass=*)'
161
152 try:
162 try:
153 log.debug('Trying simple bind with %r', dn)
163 log.debug('Trying authenticated search bind with dn: %r SCOPE: %s (and filter: %s)',
154 server.simple_bind_s(dn, safe_str(password))
164 dn, scope_label, ldap_filter)
155 _dn, attrs = server.search_ext_s(
165 ldap_conn.simple_bind_s(dn, safe_str(password))
156 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
166 response = ldap_conn.search_ext_s(dn, scope, ldap_filter, attrlist=['*', '+'])
157
167
158 return attrs
168 if not response:
169 log.error('search bind returned empty results: %r', response)
170 return {}
171 else:
172 _dn, attrs = response[0]
173 return attrs
159
174
160 except ldap.INVALID_CREDENTIALS:
175 except ldap.INVALID_CREDENTIALS:
161 log.debug(
176 log.debug("LDAP rejected password for user '%s': %s, org_exc:",
162 "LDAP rejected password for user '%s': %s, org_exc:",
177 username, dn, exc_info=True)
163 username, dn, exc_info=True)
164
178
165 def authenticate_ldap(self, username, password):
179 def authenticate_ldap(self, username, password):
166 """
180 """
167 Authenticate a user via LDAP and return his/her LDAP properties.
181 Authenticate a user via LDAP and return his/her LDAP properties.
168
182
169 Raises AuthenticationError if the credentials are rejected, or
183 Raises AuthenticationError if the credentials are rejected, or
170 EnvironmentError if the LDAP server can't be reached.
184 EnvironmentError if the LDAP server can't be reached.
171
185
172 :param username: username
186 :param username: username
173 :param password: password
187 :param password: password
174 """
188 """
175
189
176 uid = self.get_uid(username, self.SERVER_ADDRESSES)
190 uid = self.get_uid(username, self.SERVER_ADDRESSES)
177 user_attrs = {}
191 user_attrs = {}
178 dn = ''
192 dn = ''
179
193
180 self.validate_password(username, password)
194 self.validate_password(username, password)
181 self.validate_username(username)
195 self.validate_username(username)
196 scope_label = self.scope_labels.get(self.SEARCH_SCOPE)
182
197
183 ldap_conn = None
198 ldap_conn = None
184 try:
199 try:
185 ldap_conn = self._get_ldap_conn()
200 ldap_conn = self._get_ldap_conn()
186 filter_ = '(&%s(%s=%s))' % (
201 filter_ = '(&%s(%s=%s))' % (
187 self.LDAP_FILTER, self.attr_login, username)
202 self.LDAP_FILTER, self.attr_login, username)
188 log.debug("Authenticating %r filter %s", self.BASE_DN, filter_)
203 log.debug("Authenticating %r filter %s and scope: %s",
204 self.BASE_DN, filter_, scope_label)
189
205
190 lobjects = ldap_conn.search_ext_s(
206 ldap_objects = ldap_conn.search_ext_s(
191 self.BASE_DN, self.SEARCH_SCOPE, filter_)
207 self.BASE_DN, self.SEARCH_SCOPE, filter_, attrlist=['*', '+'])
192
208
193 if not lobjects:
209 if not ldap_objects:
194 log.debug("No matching LDAP objects for authentication "
210 log.debug("No matching LDAP objects for authentication "
195 "of UID:'%s' username:(%s)", uid, username)
211 "of UID:'%s' username:(%s)", uid, username)
196 raise ldap.NO_SUCH_OBJECT()
212 raise ldap.NO_SUCH_OBJECT()
197
213
198 log.debug('Found matching ldap object, trying to authenticate')
214 log.debug('Found %s matching ldap object[s], trying to authenticate on each one now...', len(ldap_objects))
199 for (dn, _attrs) in lobjects:
215 for (dn, _attrs) in ldap_objects:
200 if dn is None:
216 if dn is None:
201 continue
217 continue
202
218
203 user_attrs = self.fetch_attrs_from_simple_bind(
219 user_attrs = self.fetch_attrs_from_simple_bind(
204 ldap_conn, dn, username, password)
220 ldap_conn, dn, username, password)
221
205 if user_attrs:
222 if user_attrs:
223 log.debug('Got authenticated user attributes from DN:%s', dn)
206 break
224 break
207 else:
225 else:
208 raise LdapPasswordError(
226 raise LdapPasswordError(
209 'Failed to authenticate user `{}` '
227 'Failed to authenticate user `{}` with given password'.format(username))
210 'with given password'.format(username))
211
228
212 except ldap.NO_SUCH_OBJECT:
229 except ldap.NO_SUCH_OBJECT:
213 log.debug("LDAP says no such user '%s' (%s), org_exc:",
230 log.debug("LDAP says no such user '%s' (%s), org_exc:",
214 uid, username, exc_info=True)
231 uid, username, exc_info=True)
215 raise LdapUsernameError('Unable to find user')
232 raise LdapUsernameError('Unable to find user')
216 except ldap.SERVER_DOWN:
233 except ldap.SERVER_DOWN:
217 org_exc = traceback.format_exc()
234 org_exc = traceback.format_exc()
218 raise LdapConnectionError(
235 raise LdapConnectionError(
219 "LDAP can't access authentication "
236 "LDAP can't access authentication server, org_exc:%s" % org_exc)
220 "server, org_exc:%s" % org_exc)
221 finally:
237 finally:
222 if ldap_conn:
238 if ldap_conn:
223 log.debug('ldap: connection release')
239 log.debug('ldap: connection release')
224 try:
240 try:
225 ldap_conn.unbind_s()
241 ldap_conn.unbind_s()
226 except Exception:
242 except Exception:
227 # for any reason this can raise exception we must catch it
243 # for any reason this can raise exception we must catch it
228 # to not crush the server
244 # to not crush the server
229 pass
245 pass
230
246
231 return dn, user_attrs
247 return dn, user_attrs
232
248
233
249
234 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
250 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
235 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
251 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
236 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
252 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
237 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
253 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
238
254
239 host = colander.SchemaNode(
255 host = colander.SchemaNode(
240 colander.String(),
256 colander.String(),
241 default='',
257 default='',
242 description=_('Host[s] of the LDAP Server \n'
258 description=_('Host[s] of the LDAP Server \n'
243 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
259 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
244 'Multiple servers can be specified using commas'),
260 'Multiple servers can be specified using commas'),
245 preparer=strip_whitespace,
261 preparer=strip_whitespace,
246 title=_('LDAP Host'),
262 title=_('LDAP Host'),
247 widget='string')
263 widget='string')
248 port = colander.SchemaNode(
264 port = colander.SchemaNode(
249 colander.Int(),
265 colander.Int(),
250 default=389,
266 default=389,
251 description=_('Custom port that the LDAP server is listening on. '
267 description=_('Custom port that the LDAP server is listening on. '
252 'Default value is: 389, use 636 for LDAPS (SSL)'),
268 'Default value is: 389, use 636 for LDAPS (SSL)'),
253 preparer=strip_whitespace,
269 preparer=strip_whitespace,
254 title=_('Port'),
270 title=_('Port'),
255 validator=colander.Range(min=0, max=65536),
271 validator=colander.Range(min=0, max=65536),
256 widget='int')
272 widget='int')
257
273
258 timeout = colander.SchemaNode(
274 timeout = colander.SchemaNode(
259 colander.Int(),
275 colander.Int(),
260 default=60 * 5,
276 default=60 * 5,
261 description=_('Timeout for LDAP connection'),
277 description=_('Timeout for LDAP connection'),
262 preparer=strip_whitespace,
278 preparer=strip_whitespace,
263 title=_('Connection timeout'),
279 title=_('Connection timeout'),
264 validator=colander.Range(min=1),
280 validator=colander.Range(min=1),
265 widget='int')
281 widget='int')
266
282
267 dn_user = colander.SchemaNode(
283 dn_user = colander.SchemaNode(
268 colander.String(),
284 colander.String(),
269 default='',
285 default='',
270 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
286 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
271 'e.g., cn=admin,dc=mydomain,dc=com, or '
287 'e.g., cn=admin,dc=mydomain,dc=com, or '
272 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
288 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
273 missing='',
289 missing='',
274 preparer=strip_whitespace,
290 preparer=strip_whitespace,
275 title=_('Bind account'),
291 title=_('Bind account'),
276 widget='string')
292 widget='string')
277 dn_pass = colander.SchemaNode(
293 dn_pass = colander.SchemaNode(
278 colander.String(),
294 colander.String(),
279 default='',
295 default='',
280 description=_('Password to authenticate for given user DN.'),
296 description=_('Password to authenticate for given user DN.'),
281 missing='',
297 missing='',
282 preparer=strip_whitespace,
298 preparer=strip_whitespace,
283 title=_('Bind account password'),
299 title=_('Bind account password'),
284 widget='password')
300 widget='password')
285 tls_kind = colander.SchemaNode(
301 tls_kind = colander.SchemaNode(
286 colander.String(),
302 colander.String(),
287 default=tls_kind_choices[0],
303 default=tls_kind_choices[0],
288 description=_('TLS Type'),
304 description=_('TLS Type'),
289 title=_('Connection Security'),
305 title=_('Connection Security'),
290 validator=colander.OneOf(tls_kind_choices),
306 validator=colander.OneOf(tls_kind_choices),
291 widget='select')
307 widget='select')
292 tls_reqcert = colander.SchemaNode(
308 tls_reqcert = colander.SchemaNode(
293 colander.String(),
309 colander.String(),
294 default=tls_reqcert_choices[0],
310 default=tls_reqcert_choices[0],
295 description=_('Require Cert over TLS?. Self-signed and custom '
311 description=_('Require Cert over TLS?. Self-signed and custom '
296 'certificates can be used when\n `RhodeCode Certificate` '
312 'certificates can be used when\n `RhodeCode Certificate` '
297 'found in admin > settings > system info page is extended.'),
313 'found in admin > settings > system info page is extended.'),
298 title=_('Certificate Checks'),
314 title=_('Certificate Checks'),
299 validator=colander.OneOf(tls_reqcert_choices),
315 validator=colander.OneOf(tls_reqcert_choices),
300 widget='select')
316 widget='select')
301 tls_cert_file = colander.SchemaNode(
317 tls_cert_file = colander.SchemaNode(
302 colander.String(),
318 colander.String(),
303 default='',
319 default='',
304 description=_('This specifies the PEM-format file path containing '
320 description=_('This specifies the PEM-format file path containing '
305 'certificates for use in TLS connection.\n'
321 'certificates for use in TLS connection.\n'
306 'If not specified `TLS Cert dir` will be used'),
322 'If not specified `TLS Cert dir` will be used'),
307 title=_('TLS Cert file'),
323 title=_('TLS Cert file'),
308 missing='',
324 missing='',
309 widget='string')
325 widget='string')
310 tls_cert_dir = colander.SchemaNode(
326 tls_cert_dir = colander.SchemaNode(
311 colander.String(),
327 colander.String(),
312 default=AuthLdap.default_tls_cert_dir,
328 default=AuthLdap.default_tls_cert_dir,
313 description=_('This specifies the path of a directory that contains individual '
329 description=_('This specifies the path of a directory that contains individual '
314 'CA certificates in separate files.'),
330 'CA certificates in separate files.'),
315 title=_('TLS Cert dir'),
331 title=_('TLS Cert dir'),
316 widget='string')
332 widget='string')
317 base_dn = colander.SchemaNode(
333 base_dn = colander.SchemaNode(
318 colander.String(),
334 colander.String(),
319 default='',
335 default='',
320 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
336 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
321 'in it to be replaced with current user username \n'
337 'in it to be replaced with current user username \n'
322 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
338 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
323 missing='',
339 missing='',
324 preparer=strip_whitespace,
340 preparer=strip_whitespace,
325 title=_('Base DN'),
341 title=_('Base DN'),
326 widget='string')
342 widget='string')
327 filter = colander.SchemaNode(
343 filter = colander.SchemaNode(
328 colander.String(),
344 colander.String(),
329 default='',
345 default='',
330 description=_('Filter to narrow results \n'
346 description=_('Filter to narrow results \n'
331 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
347 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
332 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
348 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
333 missing='',
349 missing='',
334 preparer=strip_whitespace,
350 preparer=strip_whitespace,
335 title=_('LDAP Search Filter'),
351 title=_('LDAP Search Filter'),
336 widget='string')
352 widget='string')
337
353
338 search_scope = colander.SchemaNode(
354 search_scope = colander.SchemaNode(
339 colander.String(),
355 colander.String(),
340 default=search_scope_choices[2],
356 default=search_scope_choices[2],
341 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
357 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
342 title=_('LDAP Search Scope'),
358 title=_('LDAP Search Scope'),
343 validator=colander.OneOf(search_scope_choices),
359 validator=colander.OneOf(search_scope_choices),
344 widget='select')
360 widget='select')
345 attr_login = colander.SchemaNode(
361 attr_login = colander.SchemaNode(
346 colander.String(),
362 colander.String(),
347 default='uid',
363 default='uid',
348 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
364 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
349 preparer=strip_whitespace,
365 preparer=strip_whitespace,
350 title=_('Login Attribute'),
366 title=_('Login Attribute'),
351 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
367 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
352 widget='string')
368 widget='string')
353 attr_email = colander.SchemaNode(
369 attr_email = colander.SchemaNode(
354 colander.String(),
370 colander.String(),
355 default='',
371 default='',
356 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
372 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
357 'Emails are a crucial part of RhodeCode. \n'
373 'Emails are a crucial part of RhodeCode. \n'
358 'If possible add a valid email attribute to ldap users.'),
374 'If possible add a valid email attribute to ldap users.'),
359 missing='',
375 missing='',
360 preparer=strip_whitespace,
376 preparer=strip_whitespace,
361 title=_('Email Attribute'),
377 title=_('Email Attribute'),
362 widget='string')
378 widget='string')
363 attr_firstname = colander.SchemaNode(
379 attr_firstname = colander.SchemaNode(
364 colander.String(),
380 colander.String(),
365 default='',
381 default='',
366 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
382 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
367 missing='',
383 missing='',
368 preparer=strip_whitespace,
384 preparer=strip_whitespace,
369 title=_('First Name Attribute'),
385 title=_('First Name Attribute'),
370 widget='string')
386 widget='string')
371 attr_lastname = colander.SchemaNode(
387 attr_lastname = colander.SchemaNode(
372 colander.String(),
388 colander.String(),
373 default='',
389 default='',
374 description=_('LDAP Attribute to map to last name (e.g., sn)'),
390 description=_('LDAP Attribute to map to last name (e.g., sn)'),
375 missing='',
391 missing='',
376 preparer=strip_whitespace,
392 preparer=strip_whitespace,
377 title=_('Last Name Attribute'),
393 title=_('Last Name Attribute'),
378 widget='string')
394 widget='string')
379
395
380
396
381 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
397 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
382 uid = 'ldap'
398 uid = 'ldap'
383 # used to define dynamic binding in the
399 # used to define dynamic binding in the
384 DYNAMIC_BIND_VAR = '$login'
400 DYNAMIC_BIND_VAR = '$login'
385 _settings_unsafe_keys = ['dn_pass']
401 _settings_unsafe_keys = ['dn_pass']
386
402
387 def includeme(self, config):
403 def includeme(self, config):
388 config.add_authn_plugin(self)
404 config.add_authn_plugin(self)
389 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
405 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
390 config.add_view(
406 config.add_view(
391 'rhodecode.authentication.views.AuthnPluginViewBase',
407 'rhodecode.authentication.views.AuthnPluginViewBase',
392 attr='settings_get',
408 attr='settings_get',
393 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
409 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
394 request_method='GET',
410 request_method='GET',
395 route_name='auth_home',
411 route_name='auth_home',
396 context=LdapAuthnResource)
412 context=LdapAuthnResource)
397 config.add_view(
413 config.add_view(
398 'rhodecode.authentication.views.AuthnPluginViewBase',
414 'rhodecode.authentication.views.AuthnPluginViewBase',
399 attr='settings_post',
415 attr='settings_post',
400 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
416 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
401 request_method='POST',
417 request_method='POST',
402 route_name='auth_home',
418 route_name='auth_home',
403 context=LdapAuthnResource)
419 context=LdapAuthnResource)
404
420
405 def get_settings_schema(self):
421 def get_settings_schema(self):
406 return LdapSettingsSchema()
422 return LdapSettingsSchema()
407
423
408 def get_display_name(self):
424 def get_display_name(self):
409 return _('LDAP')
425 return _('LDAP')
410
426
411 @classmethod
427 @classmethod
412 def docs(cls):
428 def docs(cls):
413 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-ldap.html"
429 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-ldap.html"
414
430
415 @hybrid_property
431 @hybrid_property
416 def name(self):
432 def name(self):
417 return u"ldap"
433 return u"ldap"
418
434
419 def use_fake_password(self):
435 def use_fake_password(self):
420 return True
436 return True
421
437
422 def user_activation_state(self):
438 def user_activation_state(self):
423 def_user_perms = User.get_default_user().AuthUser().permissions['global']
439 def_user_perms = User.get_default_user().AuthUser().permissions['global']
424 return 'hg.extern_activate.auto' in def_user_perms
440 return 'hg.extern_activate.auto' in def_user_perms
425
441
426 def try_dynamic_binding(self, username, password, current_args):
442 def try_dynamic_binding(self, username, password, current_args):
427 """
443 """
428 Detects marker inside our original bind, and uses dynamic auth if
444 Detects marker inside our original bind, and uses dynamic auth if
429 present
445 present
430 """
446 """
431
447
432 org_bind = current_args['bind_dn']
448 org_bind = current_args['bind_dn']
433 passwd = current_args['bind_pass']
449 passwd = current_args['bind_pass']
434
450
435 def has_bind_marker(username):
451 def has_bind_marker(username):
436 if self.DYNAMIC_BIND_VAR in username:
452 if self.DYNAMIC_BIND_VAR in username:
437 return True
453 return True
438
454
439 # we only passed in user with "special" variable
455 # we only passed in user with "special" variable
440 if org_bind and has_bind_marker(org_bind) and not passwd:
456 if org_bind and has_bind_marker(org_bind) and not passwd:
441 log.debug('Using dynamic user/password binding for ldap '
457 log.debug('Using dynamic user/password binding for ldap '
442 'authentication. Replacing `%s` with username',
458 'authentication. Replacing `%s` with username',
443 self.DYNAMIC_BIND_VAR)
459 self.DYNAMIC_BIND_VAR)
444 current_args['bind_dn'] = org_bind.replace(
460 current_args['bind_dn'] = org_bind.replace(
445 self.DYNAMIC_BIND_VAR, username)
461 self.DYNAMIC_BIND_VAR, username)
446 current_args['bind_pass'] = password
462 current_args['bind_pass'] = password
447
463
448 return current_args
464 return current_args
449
465
450 def auth(self, userobj, username, password, settings, **kwargs):
466 def auth(self, userobj, username, password, settings, **kwargs):
451 """
467 """
452 Given a user object (which may be null), username, a plaintext password,
468 Given a user object (which may be null), username, a plaintext password,
453 and a settings object (containing all the keys needed as listed in
469 and a settings object (containing all the keys needed as listed in
454 settings()), authenticate this user's login attempt.
470 settings()), authenticate this user's login attempt.
455
471
456 Return None on failure. On success, return a dictionary of the form:
472 Return None on failure. On success, return a dictionary of the form:
457
473
458 see: RhodeCodeAuthPluginBase.auth_func_attrs
474 see: RhodeCodeAuthPluginBase.auth_func_attrs
459 This is later validated for correctness
475 This is later validated for correctness
460 """
476 """
461
477
462 if not username or not password:
478 if not username or not password:
463 log.debug('Empty username or password skipping...')
479 log.debug('Empty username or password skipping...')
464 return None
480 return None
465
481
466 ldap_args = {
482 ldap_args = {
467 'server': settings.get('host', ''),
483 'server': settings.get('host', ''),
468 'base_dn': settings.get('base_dn', ''),
484 'base_dn': settings.get('base_dn', ''),
469 'port': settings.get('port'),
485 'port': settings.get('port'),
470 'bind_dn': settings.get('dn_user'),
486 'bind_dn': settings.get('dn_user'),
471 'bind_pass': settings.get('dn_pass'),
487 'bind_pass': settings.get('dn_pass'),
472 'tls_kind': settings.get('tls_kind'),
488 'tls_kind': settings.get('tls_kind'),
473 'tls_reqcert': settings.get('tls_reqcert'),
489 'tls_reqcert': settings.get('tls_reqcert'),
474 'tls_cert_file': settings.get('tls_cert_file'),
490 'tls_cert_file': settings.get('tls_cert_file'),
475 'tls_cert_dir': settings.get('tls_cert_dir'),
491 'tls_cert_dir': settings.get('tls_cert_dir'),
476 'search_scope': settings.get('search_scope'),
492 'search_scope': settings.get('search_scope'),
477 'attr_login': settings.get('attr_login'),
493 'attr_login': settings.get('attr_login'),
478 'ldap_version': 3,
494 'ldap_version': 3,
479 'ldap_filter': settings.get('filter'),
495 'ldap_filter': settings.get('filter'),
480 'timeout': settings.get('timeout')
496 'timeout': settings.get('timeout')
481 }
497 }
482
498
483 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
499 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
484
500
485 log.debug('Checking for ldap authentication.')
501 log.debug('Checking for ldap authentication.')
486
502
487 try:
503 try:
488 aldap = AuthLdap(**ldap_args)
504 aldap = AuthLdap(**ldap_args)
489 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
505 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
490 log.debug('Got ldap DN response %s', user_dn)
506 log.debug('Got ldap DN response %s', user_dn)
491
507
492 def get_ldap_attr(k):
508 def get_ldap_attr(k):
493 return ldap_attrs.get(settings.get(k), [''])[0]
509 return ldap_attrs.get(settings.get(k), [''])[0]
494
510
495 # old attrs fetched from RhodeCode database
511 # old attrs fetched from RhodeCode database
496 admin = getattr(userobj, 'admin', False)
512 admin = getattr(userobj, 'admin', False)
497 active = getattr(userobj, 'active', True)
513 active = getattr(userobj, 'active', True)
498 email = getattr(userobj, 'email', '')
514 email = getattr(userobj, 'email', '')
499 username = getattr(userobj, 'username', username)
515 username = getattr(userobj, 'username', username)
500 firstname = getattr(userobj, 'firstname', '')
516 firstname = getattr(userobj, 'firstname', '')
501 lastname = getattr(userobj, 'lastname', '')
517 lastname = getattr(userobj, 'lastname', '')
502 extern_type = getattr(userobj, 'extern_type', '')
518 extern_type = getattr(userobj, 'extern_type', '')
503
519
504 groups = []
520 groups = []
505
521
506 user_attrs = {
522 user_attrs = {
507 'username': username,
523 'username': username,
508 'firstname': safe_unicode(get_ldap_attr('attr_firstname') or firstname),
524 'firstname': safe_unicode(get_ldap_attr('attr_firstname') or firstname),
509 'lastname': safe_unicode(get_ldap_attr('attr_lastname') or lastname),
525 'lastname': safe_unicode(get_ldap_attr('attr_lastname') or lastname),
510 'groups': groups,
526 'groups': groups,
511 'user_group_sync': False,
527 'user_group_sync': False,
512 'email': get_ldap_attr('attr_email') or email,
528 'email': get_ldap_attr('attr_email') or email,
513 'admin': admin,
529 'admin': admin,
514 'active': active,
530 'active': active,
515 'active_from_extern': None,
531 'active_from_extern': None,
516 'extern_name': user_dn,
532 'extern_name': user_dn,
517 'extern_type': extern_type,
533 'extern_type': extern_type,
518 }
534 }
519
535
520 log.debug('ldap user: %s', user_attrs)
536 log.debug('ldap user: %s', user_attrs)
521 log.info('user `%s` authenticated correctly', user_attrs['username'])
537 log.info('user `%s` authenticated correctly', user_attrs['username'])
522
538
523 return user_attrs
539 return user_attrs
524
540
525 except (LdapUsernameError, LdapPasswordError, LdapImportError):
541 except (LdapUsernameError, LdapPasswordError, LdapImportError):
526 log.exception("LDAP related exception")
542 log.exception("LDAP related exception")
527 return None
543 return None
528 except (Exception,):
544 except (Exception,):
529 log.exception("Other exception")
545 log.exception("Other exception")
530 return None
546 return None
531
547
532
548
533 def includeme(config):
549 def includeme(config):
534 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
550 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
535 plugin_factory(plugin_id).includeme(config)
551 plugin_factory(plugin_id).includeme(config)
General Comments 0
You need to be logged in to leave comments. Login now