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