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