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