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