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