##// END OF EJS Templates
authentication: use registerd UID for plugin definition for more consistent loading of auth plugins.
marcink -
r3246:ef7f5bf1 default
parent child Browse files
Show More
@@ -1,792 +1,795 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Authentication modules
23 23 """
24 24 import socket
25 25 import string
26 26 import colander
27 27 import copy
28 28 import logging
29 29 import time
30 30 import traceback
31 31 import warnings
32 32 import functools
33 33
34 34 from pyramid.threadlocal import get_current_registry
35 35
36 36 from rhodecode.authentication.interface import IAuthnPluginRegistry
37 37 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
38 38 from rhodecode.lib import rc_cache
39 39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
40 40 from rhodecode.lib.utils2 import safe_int, safe_str
41 41 from rhodecode.lib.exceptions import LdapConnectionError, LdapUsernameError, \
42 42 LdapPasswordError
43 43 from rhodecode.model.db import User
44 44 from rhodecode.model.meta import Session
45 45 from rhodecode.model.settings import SettingsModel
46 46 from rhodecode.model.user import UserModel
47 47 from rhodecode.model.user_group import UserGroupModel
48 48
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52 # auth types that authenticate() function can receive
53 53 VCS_TYPE = 'vcs'
54 54 HTTP_TYPE = 'http'
55 55
56 56
57 57 class hybrid_property(object):
58 58 """
59 59 a property decorator that works both for instance and class
60 60 """
61 61 def __init__(self, fget, fset=None, fdel=None, expr=None):
62 62 self.fget = fget
63 63 self.fset = fset
64 64 self.fdel = fdel
65 65 self.expr = expr or fget
66 66 functools.update_wrapper(self, fget)
67 67
68 68 def __get__(self, instance, owner):
69 69 if instance is None:
70 70 return self.expr(owner)
71 71 else:
72 72 return self.fget(instance)
73 73
74 74 def __set__(self, instance, value):
75 75 self.fset(instance, value)
76 76
77 77 def __delete__(self, instance):
78 78 self.fdel(instance)
79 79
80 80
81 81 class LazyFormencode(object):
82 82 def __init__(self, formencode_obj, *args, **kwargs):
83 83 self.formencode_obj = formencode_obj
84 84 self.args = args
85 85 self.kwargs = kwargs
86 86
87 87 def __call__(self, *args, **kwargs):
88 88 from inspect import isfunction
89 89 formencode_obj = self.formencode_obj
90 90 if isfunction(formencode_obj):
91 91 # case we wrap validators into functions
92 92 formencode_obj = self.formencode_obj(*args, **kwargs)
93 93 return formencode_obj(*self.args, **self.kwargs)
94 94
95 95
96 96 class RhodeCodeAuthPluginBase(object):
97 # UID is used to register plugin to the registry
98 uid = None
99
97 100 # cache the authentication request for N amount of seconds. Some kind
98 101 # of authentication methods are very heavy and it's very efficient to cache
99 102 # the result of a call. If it's set to None (default) cache is off
100 103 AUTH_CACHE_TTL = None
101 104 AUTH_CACHE = {}
102 105
103 106 auth_func_attrs = {
104 107 "username": "unique username",
105 108 "firstname": "first name",
106 109 "lastname": "last name",
107 110 "email": "email address",
108 111 "groups": '["list", "of", "groups"]',
109 112 "user_group_sync":
110 113 'True|False defines if returned user groups should be synced',
111 114 "extern_name": "name in external source of record",
112 115 "extern_type": "type of external source of record",
113 116 "admin": 'True|False defines if user should be RhodeCode super admin',
114 117 "active":
115 118 'True|False defines active state of user internally for RhodeCode',
116 119 "active_from_extern":
117 120 "True|False\None, active state from the external auth, "
118 121 "None means use definition from RhodeCode extern_type active value"
119 122
120 123 }
121 124 # set on authenticate() method and via set_auth_type func.
122 125 auth_type = None
123 126
124 127 # set on authenticate() method and via set_calling_scope_repo, this is a
125 128 # calling scope repository when doing authentication most likely on VCS
126 129 # operations
127 130 acl_repo_name = None
128 131
129 132 # List of setting names to store encrypted. Plugins may override this list
130 133 # to store settings encrypted.
131 134 _settings_encrypted = []
132 135
133 136 # Mapping of python to DB settings model types. Plugins may override or
134 137 # extend this mapping.
135 138 _settings_type_map = {
136 139 colander.String: 'unicode',
137 140 colander.Integer: 'int',
138 141 colander.Boolean: 'bool',
139 142 colander.List: 'list',
140 143 }
141 144
142 145 # list of keys in settings that are unsafe to be logged, should be passwords
143 146 # or other crucial credentials
144 147 _settings_unsafe_keys = []
145 148
146 149 def __init__(self, plugin_id):
147 150 self._plugin_id = plugin_id
148 151
149 152 def __str__(self):
150 153 return self.get_id()
151 154
152 155 def _get_setting_full_name(self, name):
153 156 """
154 157 Return the full setting name used for storing values in the database.
155 158 """
156 159 # TODO: johbo: Using the name here is problematic. It would be good to
157 160 # introduce either new models in the database to hold Plugin and
158 161 # PluginSetting or to use the plugin id here.
159 162 return 'auth_{}_{}'.format(self.name, name)
160 163
161 164 def _get_setting_type(self, name):
162 165 """
163 166 Return the type of a setting. This type is defined by the SettingsModel
164 167 and determines how the setting is stored in DB. Optionally the suffix
165 168 `.encrypted` is appended to instruct SettingsModel to store it
166 169 encrypted.
167 170 """
168 171 schema_node = self.get_settings_schema().get(name)
169 172 db_type = self._settings_type_map.get(
170 173 type(schema_node.typ), 'unicode')
171 174 if name in self._settings_encrypted:
172 175 db_type = '{}.encrypted'.format(db_type)
173 176 return db_type
174 177
175 178 @classmethod
176 179 def docs(cls):
177 180 """
178 181 Defines documentation url which helps with plugin setup
179 182 """
180 183 return ''
181 184
182 185 @classmethod
183 186 def icon(cls):
184 187 """
185 188 Defines ICON in SVG format for authentication method
186 189 """
187 190 return ''
188 191
189 192 def is_enabled(self):
190 193 """
191 194 Returns true if this plugin is enabled. An enabled plugin can be
192 195 configured in the admin interface but it is not consulted during
193 196 authentication.
194 197 """
195 198 auth_plugins = SettingsModel().get_auth_plugins()
196 199 return self.get_id() in auth_plugins
197 200
198 201 def is_active(self, plugin_cached_settings=None):
199 202 """
200 203 Returns true if the plugin is activated. An activated plugin is
201 204 consulted during authentication, assumed it is also enabled.
202 205 """
203 206 return self.get_setting_by_name(
204 207 'enabled', plugin_cached_settings=plugin_cached_settings)
205 208
206 209 def get_id(self):
207 210 """
208 211 Returns the plugin id.
209 212 """
210 213 return self._plugin_id
211 214
212 215 def get_display_name(self):
213 216 """
214 217 Returns a translation string for displaying purposes.
215 218 """
216 219 raise NotImplementedError('Not implemented in base class')
217 220
218 221 def get_settings_schema(self):
219 222 """
220 223 Returns a colander schema, representing the plugin settings.
221 224 """
222 225 return AuthnPluginSettingsSchemaBase()
223 226
224 227 def get_settings(self):
225 228 """
226 229 Returns the plugin settings as dictionary.
227 230 """
228 231 settings = {}
229 232 raw_settings = SettingsModel().get_all_settings()
230 233 for node in self.get_settings_schema():
231 234 settings[node.name] = self.get_setting_by_name(
232 235 node.name, plugin_cached_settings=raw_settings)
233 236 return settings
234 237
235 238 def get_setting_by_name(self, name, default=None, plugin_cached_settings=None):
236 239 """
237 240 Returns a plugin setting by name.
238 241 """
239 242 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
240 243 if plugin_cached_settings:
241 244 plugin_settings = plugin_cached_settings
242 245 else:
243 246 plugin_settings = SettingsModel().get_all_settings()
244 247
245 248 if full_name in plugin_settings:
246 249 return plugin_settings[full_name]
247 250 else:
248 251 return default
249 252
250 253 def create_or_update_setting(self, name, value):
251 254 """
252 255 Create or update a setting for this plugin in the persistent storage.
253 256 """
254 257 full_name = self._get_setting_full_name(name)
255 258 type_ = self._get_setting_type(name)
256 259 db_setting = SettingsModel().create_or_update_setting(
257 260 full_name, value, type_)
258 261 return db_setting.app_settings_value
259 262
260 263 def log_safe_settings(self, settings):
261 264 """
262 265 returns a log safe representation of settings, without any secrets
263 266 """
264 267 settings_copy = copy.deepcopy(settings)
265 268 for k in self._settings_unsafe_keys:
266 269 if k in settings_copy:
267 270 del settings_copy[k]
268 271 return settings_copy
269 272
270 273 @hybrid_property
271 274 def name(self):
272 275 """
273 276 Returns the name of this authentication plugin.
274 277
275 278 :returns: string
276 279 """
277 280 raise NotImplementedError("Not implemented in base class")
278 281
279 282 def get_url_slug(self):
280 283 """
281 284 Returns a slug which should be used when constructing URLs which refer
282 285 to this plugin. By default it returns the plugin name. If the name is
283 286 not suitable for using it in an URL the plugin should override this
284 287 method.
285 288 """
286 289 return self.name
287 290
288 291 @property
289 292 def is_headers_auth(self):
290 293 """
291 294 Returns True if this authentication plugin uses HTTP headers as
292 295 authentication method.
293 296 """
294 297 return False
295 298
296 299 @hybrid_property
297 300 def is_container_auth(self):
298 301 """
299 302 Deprecated method that indicates if this authentication plugin uses
300 303 HTTP headers as authentication method.
301 304 """
302 305 warnings.warn(
303 306 'Use is_headers_auth instead.', category=DeprecationWarning)
304 307 return self.is_headers_auth
305 308
306 309 @hybrid_property
307 310 def allows_creating_users(self):
308 311 """
309 312 Defines if Plugin allows users to be created on-the-fly when
310 313 authentication is called. Controls how external plugins should behave
311 314 in terms if they are allowed to create new users, or not. Base plugins
312 315 should not be allowed to, but External ones should be !
313 316
314 317 :return: bool
315 318 """
316 319 return False
317 320
318 321 def set_auth_type(self, auth_type):
319 322 self.auth_type = auth_type
320 323
321 324 def set_calling_scope_repo(self, acl_repo_name):
322 325 self.acl_repo_name = acl_repo_name
323 326
324 327 def allows_authentication_from(
325 328 self, user, allows_non_existing_user=True,
326 329 allowed_auth_plugins=None, allowed_auth_sources=None):
327 330 """
328 331 Checks if this authentication module should accept a request for
329 332 the current user.
330 333
331 334 :param user: user object fetched using plugin's get_user() method.
332 335 :param allows_non_existing_user: if True, don't allow the
333 336 user to be empty, meaning not existing in our database
334 337 :param allowed_auth_plugins: if provided, users extern_type will be
335 338 checked against a list of provided extern types, which are plugin
336 339 auth_names in the end
337 340 :param allowed_auth_sources: authentication type allowed,
338 341 `http` or `vcs` default is both.
339 342 defines if plugin will accept only http authentication vcs
340 343 authentication(git/hg) or both
341 344 :returns: boolean
342 345 """
343 346 if not user and not allows_non_existing_user:
344 347 log.debug('User is empty but plugin does not allow empty users,'
345 348 'not allowed to authenticate')
346 349 return False
347 350
348 351 expected_auth_plugins = allowed_auth_plugins or [self.name]
349 352 if user and (user.extern_type and
350 353 user.extern_type not in expected_auth_plugins):
351 354 log.debug(
352 355 'User `%s` is bound to `%s` auth type. Plugin allows only '
353 356 '%s, skipping', user, user.extern_type, expected_auth_plugins)
354 357
355 358 return False
356 359
357 360 # by default accept both
358 361 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
359 362 if self.auth_type not in expected_auth_from:
360 363 log.debug('Current auth source is %s but plugin only allows %s',
361 364 self.auth_type, expected_auth_from)
362 365 return False
363 366
364 367 return True
365 368
366 369 def get_user(self, username=None, **kwargs):
367 370 """
368 371 Helper method for user fetching in plugins, by default it's using
369 372 simple fetch by username, but this method can be custimized in plugins
370 373 eg. headers auth plugin to fetch user by environ params
371 374
372 375 :param username: username if given to fetch from database
373 376 :param kwargs: extra arguments needed for user fetching.
374 377 """
375 378 user = None
376 379 log.debug(
377 380 'Trying to fetch user `%s` from RhodeCode database', username)
378 381 if username:
379 382 user = User.get_by_username(username)
380 383 if not user:
381 384 log.debug('User not found, fallback to fetch user in '
382 385 'case insensitive mode')
383 386 user = User.get_by_username(username, case_insensitive=True)
384 387 else:
385 388 log.debug('provided username:`%s` is empty skipping...', username)
386 389 if not user:
387 390 log.debug('User `%s` not found in database', username)
388 391 else:
389 392 log.debug('Got DB user:%s', user)
390 393 return user
391 394
392 395 def user_activation_state(self):
393 396 """
394 397 Defines user activation state when creating new users
395 398
396 399 :returns: boolean
397 400 """
398 401 raise NotImplementedError("Not implemented in base class")
399 402
400 403 def auth(self, userobj, username, passwd, settings, **kwargs):
401 404 """
402 405 Given a user object (which may be null), username, a plaintext
403 406 password, and a settings object (containing all the keys needed as
404 407 listed in settings()), authenticate this user's login attempt.
405 408
406 409 Return None on failure. On success, return a dictionary of the form:
407 410
408 411 see: RhodeCodeAuthPluginBase.auth_func_attrs
409 412 This is later validated for correctness
410 413 """
411 414 raise NotImplementedError("not implemented in base class")
412 415
413 416 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
414 417 """
415 418 Wrapper to call self.auth() that validates call on it
416 419
417 420 :param userobj: userobj
418 421 :param username: username
419 422 :param passwd: plaintext password
420 423 :param settings: plugin settings
421 424 """
422 425 auth = self.auth(userobj, username, passwd, settings, **kwargs)
423 426 if auth:
424 427 auth['_plugin'] = self.name
425 428 auth['_ttl_cache'] = self.get_ttl_cache(settings)
426 429 # check if hash should be migrated ?
427 430 new_hash = auth.get('_hash_migrate')
428 431 if new_hash:
429 432 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
430 433 if 'user_group_sync' not in auth:
431 434 auth['user_group_sync'] = False
432 435 return self._validate_auth_return(auth)
433 436 return auth
434 437
435 438 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
436 439 new_hash_cypher = _RhodeCodeCryptoBCrypt()
437 440 # extra checks, so make sure new hash is correct.
438 441 password_encoded = safe_str(password)
439 442 if new_hash and new_hash_cypher.hash_check(
440 443 password_encoded, new_hash):
441 444 cur_user = User.get_by_username(username)
442 445 cur_user.password = new_hash
443 446 Session().add(cur_user)
444 447 Session().flush()
445 448 log.info('Migrated user %s hash to bcrypt', cur_user)
446 449
447 450 def _validate_auth_return(self, ret):
448 451 if not isinstance(ret, dict):
449 452 raise Exception('returned value from auth must be a dict')
450 453 for k in self.auth_func_attrs:
451 454 if k not in ret:
452 455 raise Exception('Missing %s attribute from returned data' % k)
453 456 return ret
454 457
455 458 def get_ttl_cache(self, settings=None):
456 459 plugin_settings = settings or self.get_settings()
457 460 # we set default to 30, we make a compromise here,
458 461 # performance > security, mostly due to LDAP/SVN, majority
459 462 # of users pick cache_ttl to be enabled
460 463 from rhodecode.authentication import plugin_default_auth_ttl
461 464 cache_ttl = plugin_default_auth_ttl
462 465
463 466 if isinstance(self.AUTH_CACHE_TTL, (int, long)):
464 467 # plugin cache set inside is more important than the settings value
465 468 cache_ttl = self.AUTH_CACHE_TTL
466 469 elif plugin_settings.get('cache_ttl'):
467 470 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
468 471
469 472 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
470 473 return plugin_cache_active, cache_ttl
471 474
472 475
473 476 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
474 477
475 478 @hybrid_property
476 479 def allows_creating_users(self):
477 480 return True
478 481
479 482 def use_fake_password(self):
480 483 """
481 484 Return a boolean that indicates whether or not we should set the user's
482 485 password to a random value when it is authenticated by this plugin.
483 486 If your plugin provides authentication, then you will generally
484 487 want this.
485 488
486 489 :returns: boolean
487 490 """
488 491 raise NotImplementedError("Not implemented in base class")
489 492
490 493 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
491 494 # at this point _authenticate calls plugin's `auth()` function
492 495 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
493 496 userobj, username, passwd, settings, **kwargs)
494 497
495 498 if auth:
496 499 # maybe plugin will clean the username ?
497 500 # we should use the return value
498 501 username = auth['username']
499 502
500 503 # if external source tells us that user is not active, we should
501 504 # skip rest of the process. This can prevent from creating users in
502 505 # RhodeCode when using external authentication, but if it's
503 506 # inactive user we shouldn't create that user anyway
504 507 if auth['active_from_extern'] is False:
505 508 log.warning(
506 509 "User %s authenticated against %s, but is inactive",
507 510 username, self.__module__)
508 511 return None
509 512
510 513 cur_user = User.get_by_username(username, case_insensitive=True)
511 514 is_user_existing = cur_user is not None
512 515
513 516 if is_user_existing:
514 517 log.debug('Syncing user `%s` from '
515 518 '`%s` plugin', username, self.name)
516 519 else:
517 520 log.debug('Creating non existing user `%s` from '
518 521 '`%s` plugin', username, self.name)
519 522
520 523 if self.allows_creating_users:
521 524 log.debug('Plugin `%s` allows to '
522 525 'create new users', self.name)
523 526 else:
524 527 log.debug('Plugin `%s` does not allow to '
525 528 'create new users', self.name)
526 529
527 530 user_parameters = {
528 531 'username': username,
529 532 'email': auth["email"],
530 533 'firstname': auth["firstname"],
531 534 'lastname': auth["lastname"],
532 535 'active': auth["active"],
533 536 'admin': auth["admin"],
534 537 'extern_name': auth["extern_name"],
535 538 'extern_type': self.name,
536 539 'plugin': self,
537 540 'allow_to_create_user': self.allows_creating_users,
538 541 }
539 542
540 543 if not is_user_existing:
541 544 if self.use_fake_password():
542 545 # Randomize the PW because we don't need it, but don't want
543 546 # them blank either
544 547 passwd = PasswordGenerator().gen_password(length=16)
545 548 user_parameters['password'] = passwd
546 549 else:
547 550 # Since the password is required by create_or_update method of
548 551 # UserModel, we need to set it explicitly.
549 552 # The create_or_update method is smart and recognises the
550 553 # password hashes as well.
551 554 user_parameters['password'] = cur_user.password
552 555
553 556 # we either create or update users, we also pass the flag
554 557 # that controls if this method can actually do that.
555 558 # raises NotAllowedToCreateUserError if it cannot, and we try to.
556 559 user = UserModel().create_or_update(**user_parameters)
557 560 Session().flush()
558 561 # enforce user is just in given groups, all of them has to be ones
559 562 # created from plugins. We store this info in _group_data JSON
560 563 # field
561 564
562 565 if auth['user_group_sync']:
563 566 try:
564 567 groups = auth['groups'] or []
565 568 log.debug(
566 569 'Performing user_group sync based on set `%s` '
567 570 'returned by `%s` plugin', groups, self.name)
568 571 UserGroupModel().enforce_groups(user, groups, self.name)
569 572 except Exception:
570 573 # for any reason group syncing fails, we should
571 574 # proceed with login
572 575 log.error(traceback.format_exc())
573 576
574 577 Session().commit()
575 578 return auth
576 579
577 580
578 581 class AuthLdapBase(object):
579 582
580 583 @classmethod
581 584 def _build_servers(cls, ldap_server_type, ldap_server, port, use_resolver=True):
582 585
583 586 def host_resolver(host, port, full_resolve=True):
584 587 """
585 588 Main work for this function is to prevent ldap connection issues,
586 589 and detect them early using a "greenified" sockets
587 590 """
588 591 host = host.strip()
589 592 if not full_resolve:
590 593 return '{}:{}'.format(host, port)
591 594
592 595 log.debug('LDAP: Resolving IP for LDAP host %s', host)
593 596 try:
594 597 ip = socket.gethostbyname(host)
595 598 log.debug('Got LDAP server %s ip %s', host, ip)
596 599 except Exception:
597 600 raise LdapConnectionError(
598 601 'Failed to resolve host: `{}`'.format(host))
599 602
600 603 log.debug('LDAP: Checking if IP %s is accessible', ip)
601 604 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
602 605 try:
603 606 s.connect((ip, int(port)))
604 607 s.shutdown(socket.SHUT_RD)
605 608 except Exception:
606 609 raise LdapConnectionError(
607 610 'Failed to connect to host: `{}:{}`'.format(host, port))
608 611
609 612 return '{}:{}'.format(host, port)
610 613
611 614 if len(ldap_server) == 1:
612 615 # in case of single server use resolver to detect potential
613 616 # connection issues
614 617 full_resolve = True
615 618 else:
616 619 full_resolve = False
617 620
618 621 return ', '.join(
619 622 ["{}://{}".format(
620 623 ldap_server_type,
621 624 host_resolver(host, port, full_resolve=use_resolver and full_resolve))
622 625 for host in ldap_server])
623 626
624 627 @classmethod
625 628 def _get_server_list(cls, servers):
626 629 return map(string.strip, servers.split(','))
627 630
628 631 @classmethod
629 632 def get_uid(cls, username, server_addresses):
630 633 uid = username
631 634 for server_addr in server_addresses:
632 635 uid = chop_at(username, "@%s" % server_addr)
633 636 return uid
634 637
635 638 @classmethod
636 639 def validate_username(cls, username):
637 640 if "," in username:
638 641 raise LdapUsernameError(
639 642 "invalid character `,` in username: `{}`".format(username))
640 643
641 644 @classmethod
642 645 def validate_password(cls, username, password):
643 646 if not password:
644 647 msg = "Authenticating user %s with blank password not allowed"
645 648 log.warning(msg, username)
646 649 raise LdapPasswordError(msg)
647 650
648 651
649 652 def loadplugin(plugin_id):
650 653 """
651 654 Loads and returns an instantiated authentication plugin.
652 655 Returns the RhodeCodeAuthPluginBase subclass on success,
653 656 or None on failure.
654 657 """
655 658 # TODO: Disusing pyramids thread locals to retrieve the registry.
656 659 authn_registry = get_authn_registry()
657 660 plugin = authn_registry.get_plugin(plugin_id)
658 661 if plugin is None:
659 662 log.error('Authentication plugin not found: "%s"', plugin_id)
660 663 return plugin
661 664
662 665
663 666 def get_authn_registry(registry=None):
664 667 registry = registry or get_current_registry()
665 668 authn_registry = registry.getUtility(IAuthnPluginRegistry)
666 669 return authn_registry
667 670
668 671
669 672 def authenticate(username, password, environ=None, auth_type=None,
670 673 skip_missing=False, registry=None, acl_repo_name=None):
671 674 """
672 675 Authentication function used for access control,
673 676 It tries to authenticate based on enabled authentication modules.
674 677
675 678 :param username: username can be empty for headers auth
676 679 :param password: password can be empty for headers auth
677 680 :param environ: environ headers passed for headers auth
678 681 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
679 682 :param skip_missing: ignores plugins that are in db but not in environment
680 683 :returns: None if auth failed, plugin_user dict if auth is correct
681 684 """
682 685 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
683 686 raise ValueError('auth type must be on of http, vcs got "%s" instead'
684 687 % auth_type)
685 688 headers_only = environ and not (username and password)
686 689
687 690 authn_registry = get_authn_registry(registry)
688 691 plugins_to_check = authn_registry.get_plugins_for_authentication()
689 692 log.debug('Starting ordered authentication chain using %s plugins',
690 693 [x.name for x in plugins_to_check])
691 694 for plugin in plugins_to_check:
692 695 plugin.set_auth_type(auth_type)
693 696 plugin.set_calling_scope_repo(acl_repo_name)
694 697
695 698 if headers_only and not plugin.is_headers_auth:
696 699 log.debug('Auth type is for headers only and plugin `%s` is not '
697 700 'headers plugin, skipping...', plugin.get_id())
698 701 continue
699 702
700 703 log.debug('Trying authentication using ** %s **', plugin.get_id())
701 704
702 705 # load plugin settings from RhodeCode database
703 706 plugin_settings = plugin.get_settings()
704 707 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
705 708 log.debug('Plugin `%s` settings:%s', plugin.get_id(), plugin_sanitized_settings)
706 709
707 710 # use plugin's method of user extraction.
708 711 user = plugin.get_user(username, environ=environ,
709 712 settings=plugin_settings)
710 713 display_user = user.username if user else username
711 714 log.debug(
712 715 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
713 716
714 717 if not plugin.allows_authentication_from(user):
715 718 log.debug('Plugin %s does not accept user `%s` for authentication',
716 719 plugin.get_id(), display_user)
717 720 continue
718 721 else:
719 722 log.debug('Plugin %s accepted user `%s` for authentication',
720 723 plugin.get_id(), display_user)
721 724
722 725 log.info('Authenticating user `%s` using %s plugin',
723 726 display_user, plugin.get_id())
724 727
725 728 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
726 729
727 730 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
728 731 plugin.get_id(), plugin_cache_active, cache_ttl)
729 732
730 733 user_id = user.user_id if user else None
731 734 # don't cache for empty users
732 735 plugin_cache_active = plugin_cache_active and user_id
733 736 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
734 737 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
735 738
736 739 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
737 740 expiration_time=cache_ttl,
738 741 condition=plugin_cache_active)
739 742 def compute_auth(
740 743 cache_name, plugin_name, username, password):
741 744
742 745 # _authenticate is a wrapper for .auth() method of plugin.
743 746 # it checks if .auth() sends proper data.
744 747 # For RhodeCodeExternalAuthPlugin it also maps users to
745 748 # Database and maps the attributes returned from .auth()
746 749 # to RhodeCode database. If this function returns data
747 750 # then auth is correct.
748 751 log.debug('Running plugin `%s` _authenticate method '
749 752 'using username and password', plugin.get_id())
750 753 return plugin._authenticate(
751 754 user, username, password, plugin_settings,
752 755 environ=environ or {})
753 756
754 757 start = time.time()
755 758 # for environ based auth, password can be empty, but then the validation is
756 759 # on the server that fills in the env data needed for authentication
757 760 plugin_user = compute_auth('auth', plugin.name, username, (password or ''))
758 761
759 762 auth_time = time.time() - start
760 763 log.debug('Authentication for plugin `%s` completed in %.3fs, '
761 764 'expiration time of fetched cache %.1fs.',
762 765 plugin.get_id(), auth_time, cache_ttl)
763 766
764 767 log.debug('PLUGIN USER DATA: %s', plugin_user)
765 768
766 769 if plugin_user:
767 770 log.debug('Plugin returned proper authentication data')
768 771 return plugin_user
769 772 # we failed to Auth because .auth() method didn't return proper user
770 773 log.debug("User `%s` failed to authenticate against %s",
771 774 display_user, plugin.get_id())
772 775
773 776 # case when we failed to authenticate against all defined plugins
774 777 return None
775 778
776 779
777 780 def chop_at(s, sub, inclusive=False):
778 781 """Truncate string ``s`` at the first occurrence of ``sub``.
779 782
780 783 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
781 784
782 785 >>> chop_at("plutocratic brats", "rat")
783 786 'plutoc'
784 787 >>> chop_at("plutocratic brats", "rat", True)
785 788 'plutocrat'
786 789 """
787 790 pos = s.find(sub)
788 791 if pos == -1:
789 792 return s
790 793 if inclusive:
791 794 return s[:pos+len(sub)]
792 795 return s[:pos]
@@ -1,294 +1,295 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 RhodeCode authentication plugin for Atlassian CROWD
23 23 """
24 24
25 25
26 26 import colander
27 27 import base64
28 28 import logging
29 29 import urllib2
30 30
31 31 from rhodecode.translation import _
32 32 from rhodecode.authentication.base import (
33 33 RhodeCodeExternalAuthPlugin, hybrid_property)
34 34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
35 35 from rhodecode.authentication.routes import AuthnPluginResourceBase
36 36 from rhodecode.lib.colander_utils import strip_whitespace
37 37 from rhodecode.lib.ext_json import json, formatted_json
38 38 from rhodecode.model.db import User
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 def plugin_factory(plugin_id, *args, **kwds):
44 44 """
45 45 Factory function that is called during plugin discovery.
46 46 It returns the plugin instance.
47 47 """
48 48 plugin = RhodeCodeAuthPlugin(plugin_id)
49 49 return plugin
50 50
51 51
52 52 class CrowdAuthnResource(AuthnPluginResourceBase):
53 53 pass
54 54
55 55
56 56 class CrowdSettingsSchema(AuthnPluginSettingsSchemaBase):
57 57 host = colander.SchemaNode(
58 58 colander.String(),
59 59 default='127.0.0.1',
60 60 description=_('The FQDN or IP of the Atlassian CROWD Server'),
61 61 preparer=strip_whitespace,
62 62 title=_('Host'),
63 63 widget='string')
64 64 port = colander.SchemaNode(
65 65 colander.Int(),
66 66 default=8095,
67 67 description=_('The Port in use by the Atlassian CROWD Server'),
68 68 preparer=strip_whitespace,
69 69 title=_('Port'),
70 70 validator=colander.Range(min=0, max=65536),
71 71 widget='int')
72 72 app_name = colander.SchemaNode(
73 73 colander.String(),
74 74 default='',
75 75 description=_('The Application Name to authenticate to CROWD'),
76 76 preparer=strip_whitespace,
77 77 title=_('Application Name'),
78 78 widget='string')
79 79 app_password = colander.SchemaNode(
80 80 colander.String(),
81 81 default='',
82 82 description=_('The password to authenticate to CROWD'),
83 83 preparer=strip_whitespace,
84 84 title=_('Application Password'),
85 85 widget='password')
86 86 admin_groups = colander.SchemaNode(
87 87 colander.String(),
88 88 default='',
89 89 description=_('A comma separated list of group names that identify '
90 90 'users as RhodeCode Administrators'),
91 91 missing='',
92 92 preparer=strip_whitespace,
93 93 title=_('Admin Groups'),
94 94 widget='string')
95 95
96 96
97 97 class CrowdServer(object):
98 98 def __init__(self, *args, **kwargs):
99 99 """
100 100 Create a new CrowdServer object that points to IP/Address 'host',
101 101 on the given port, and using the given method (https/http). user and
102 102 passwd can be set here or with set_credentials. If unspecified,
103 103 "version" defaults to "latest".
104 104
105 105 example::
106 106
107 107 cserver = CrowdServer(host="127.0.0.1",
108 108 port="8095",
109 109 user="some_app",
110 110 passwd="some_passwd",
111 111 version="1")
112 112 """
113 113 if not "port" in kwargs:
114 114 kwargs["port"] = "8095"
115 115 self._logger = kwargs.get("logger", logging.getLogger(__name__))
116 116 self._uri = "%s://%s:%s/crowd" % (kwargs.get("method", "http"),
117 117 kwargs.get("host", "127.0.0.1"),
118 118 kwargs.get("port", "8095"))
119 119 self.set_credentials(kwargs.get("user", ""),
120 120 kwargs.get("passwd", ""))
121 121 self._version = kwargs.get("version", "latest")
122 122 self._url_list = None
123 123 self._appname = "crowd"
124 124
125 125 def set_credentials(self, user, passwd):
126 126 self.user = user
127 127 self.passwd = passwd
128 128 self._make_opener()
129 129
130 130 def _make_opener(self):
131 131 mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
132 132 mgr.add_password(None, self._uri, self.user, self.passwd)
133 133 handler = urllib2.HTTPBasicAuthHandler(mgr)
134 134 self.opener = urllib2.build_opener(handler)
135 135
136 136 def _request(self, url, body=None, headers=None,
137 137 method=None, noformat=False,
138 138 empty_response_ok=False):
139 139 _headers = {"Content-type": "application/json",
140 140 "Accept": "application/json"}
141 141 if self.user and self.passwd:
142 142 authstring = base64.b64encode("%s:%s" % (self.user, self.passwd))
143 143 _headers["Authorization"] = "Basic %s" % authstring
144 144 if headers:
145 145 _headers.update(headers)
146 146 log.debug("Sent crowd: \n%s"
147 147 % (formatted_json({"url": url, "body": body,
148 148 "headers": _headers})))
149 149 request = urllib2.Request(url, body, _headers)
150 150 if method:
151 151 request.get_method = lambda: method
152 152
153 153 global msg
154 154 msg = ""
155 155 try:
156 156 rdoc = self.opener.open(request)
157 157 msg = "".join(rdoc.readlines())
158 158 if not msg and empty_response_ok:
159 159 rval = {}
160 160 rval["status"] = True
161 161 rval["error"] = "Response body was empty"
162 162 elif not noformat:
163 163 rval = json.loads(msg)
164 164 rval["status"] = True
165 165 else:
166 166 rval = "".join(rdoc.readlines())
167 167 except Exception as e:
168 168 if not noformat:
169 169 rval = {"status": False,
170 170 "body": body,
171 171 "error": str(e) + "\n" + msg}
172 172 else:
173 173 rval = None
174 174 return rval
175 175
176 176 def user_auth(self, username, password):
177 177 """Authenticate a user against crowd. Returns brief information about
178 178 the user."""
179 179 url = ("%s/rest/usermanagement/%s/authentication?username=%s"
180 180 % (self._uri, self._version, username))
181 181 body = json.dumps({"value": password})
182 182 return self._request(url, body)
183 183
184 184 def user_groups(self, username):
185 185 """Retrieve a list of groups to which this user belongs."""
186 186 url = ("%s/rest/usermanagement/%s/user/group/nested?username=%s"
187 187 % (self._uri, self._version, username))
188 188 return self._request(url)
189 189
190 190
191 191 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
192 uid = 'crowd'
192 193 _settings_unsafe_keys = ['app_password']
193 194
194 195 def includeme(self, config):
195 196 config.add_authn_plugin(self)
196 197 config.add_authn_resource(self.get_id(), CrowdAuthnResource(self))
197 198 config.add_view(
198 199 'rhodecode.authentication.views.AuthnPluginViewBase',
199 200 attr='settings_get',
200 201 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
201 202 request_method='GET',
202 203 route_name='auth_home',
203 204 context=CrowdAuthnResource)
204 205 config.add_view(
205 206 'rhodecode.authentication.views.AuthnPluginViewBase',
206 207 attr='settings_post',
207 208 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
208 209 request_method='POST',
209 210 route_name='auth_home',
210 211 context=CrowdAuthnResource)
211 212
212 213 def get_settings_schema(self):
213 214 return CrowdSettingsSchema()
214 215
215 216 def get_display_name(self):
216 217 return _('CROWD')
217 218
218 219 @classmethod
219 220 def docs(cls):
220 221 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-crowd.html"
221 222
222 223 @hybrid_property
223 224 def name(self):
224 225 return "crowd"
225 226
226 227 def use_fake_password(self):
227 228 return True
228 229
229 230 def user_activation_state(self):
230 231 def_user_perms = User.get_default_user().AuthUser().permissions['global']
231 232 return 'hg.extern_activate.auto' in def_user_perms
232 233
233 234 def auth(self, userobj, username, password, settings, **kwargs):
234 235 """
235 236 Given a user object (which may be null), username, a plaintext password,
236 237 and a settings object (containing all the keys needed as listed in settings()),
237 238 authenticate this user's login attempt.
238 239
239 240 Return None on failure. On success, return a dictionary of the form:
240 241
241 242 see: RhodeCodeAuthPluginBase.auth_func_attrs
242 243 This is later validated for correctness
243 244 """
244 245 if not username or not password:
245 246 log.debug('Empty username or password skipping...')
246 247 return None
247 248
248 249 log.debug("Crowd settings: \n%s", formatted_json(settings))
249 250 server = CrowdServer(**settings)
250 251 server.set_credentials(settings["app_name"], settings["app_password"])
251 252 crowd_user = server.user_auth(username, password)
252 253 log.debug("Crowd returned: \n%s", formatted_json(crowd_user))
253 254 if not crowd_user["status"]:
254 255 return None
255 256
256 257 res = server.user_groups(crowd_user["name"])
257 258 log.debug("Crowd groups: \n%s", formatted_json(res))
258 259 crowd_user["groups"] = [x["name"] for x in res["groups"]]
259 260
260 261 # old attrs fetched from RhodeCode database
261 262 admin = getattr(userobj, 'admin', False)
262 263 active = getattr(userobj, 'active', True)
263 264 email = getattr(userobj, 'email', '')
264 265 username = getattr(userobj, 'username', username)
265 266 firstname = getattr(userobj, 'firstname', '')
266 267 lastname = getattr(userobj, 'lastname', '')
267 268 extern_type = getattr(userobj, 'extern_type', '')
268 269
269 270 user_attrs = {
270 271 'username': username,
271 272 'firstname': crowd_user["first-name"] or firstname,
272 273 'lastname': crowd_user["last-name"] or lastname,
273 274 'groups': crowd_user["groups"],
274 275 'user_group_sync': True,
275 276 'email': crowd_user["email"] or email,
276 277 'admin': admin,
277 278 'active': active,
278 279 'active_from_extern': crowd_user.get('active'),
279 280 'extern_name': crowd_user["name"],
280 281 'extern_type': extern_type,
281 282 }
282 283
283 284 # set an admin if we're in admin_groups of crowd
284 285 for group in settings["admin_groups"]:
285 286 if group in user_attrs["groups"]:
286 287 user_attrs["admin"] = True
287 288 log.debug("Final crowd user object: \n%s", formatted_json(user_attrs))
288 289 log.info('user `%s` authenticated correctly', user_attrs['username'])
289 290 return user_attrs
290 291
291 292
292 293 def includeme(config):
293 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format('crowd')
294 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
294 295 plugin_factory(plugin_id).includeme(config)
@@ -1,230 +1,230 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import colander
22 22 import logging
23 23
24 24 from rhodecode.translation import _
25 25 from rhodecode.authentication.base import (
26 26 RhodeCodeExternalAuthPlugin, hybrid_property)
27 27 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
28 28 from rhodecode.authentication.routes import AuthnPluginResourceBase
29 29 from rhodecode.lib.colander_utils import strip_whitespace
30 30 from rhodecode.lib.utils2 import str2bool, safe_unicode
31 31 from rhodecode.model.db import User
32 32
33 33
34 34 log = logging.getLogger(__name__)
35 35
36 36
37 37 def plugin_factory(plugin_id, *args, **kwds):
38 38 """
39 39 Factory function that is called during plugin discovery.
40 40 It returns the plugin instance.
41 41 """
42 42 plugin = RhodeCodeAuthPlugin(plugin_id)
43 43 return plugin
44 44
45 45
46 46 class HeadersAuthnResource(AuthnPluginResourceBase):
47 47 pass
48 48
49 49
50 50 class HeadersSettingsSchema(AuthnPluginSettingsSchemaBase):
51 51 header = colander.SchemaNode(
52 52 colander.String(),
53 53 default='REMOTE_USER',
54 54 description=_('Header to extract the user from'),
55 55 preparer=strip_whitespace,
56 56 title=_('Header'),
57 57 widget='string')
58 58 fallback_header = colander.SchemaNode(
59 59 colander.String(),
60 60 default='HTTP_X_FORWARDED_USER',
61 61 description=_('Header to extract the user from when main one fails'),
62 62 preparer=strip_whitespace,
63 63 title=_('Fallback header'),
64 64 widget='string')
65 65 clean_username = colander.SchemaNode(
66 66 colander.Boolean(),
67 67 default=True,
68 68 description=_('Perform cleaning of user, if passed user has @ in '
69 69 'username then first part before @ is taken. '
70 70 'If there\'s \\ in the username only the part after '
71 71 ' \\ is taken'),
72 72 missing=False,
73 73 title=_('Clean username'),
74 74 widget='bool')
75 75
76 76
77 77 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
78
78 uid = 'headers'
79 79 def includeme(self, config):
80 80 config.add_authn_plugin(self)
81 81 config.add_authn_resource(self.get_id(), HeadersAuthnResource(self))
82 82 config.add_view(
83 83 'rhodecode.authentication.views.AuthnPluginViewBase',
84 84 attr='settings_get',
85 85 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
86 86 request_method='GET',
87 87 route_name='auth_home',
88 88 context=HeadersAuthnResource)
89 89 config.add_view(
90 90 'rhodecode.authentication.views.AuthnPluginViewBase',
91 91 attr='settings_post',
92 92 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
93 93 request_method='POST',
94 94 route_name='auth_home',
95 95 context=HeadersAuthnResource)
96 96
97 97 def get_display_name(self):
98 98 return _('Headers')
99 99
100 100 def get_settings_schema(self):
101 101 return HeadersSettingsSchema()
102 102
103 103 @hybrid_property
104 104 def name(self):
105 105 return 'headers'
106 106
107 107 @property
108 108 def is_headers_auth(self):
109 109 return True
110 110
111 111 def use_fake_password(self):
112 112 return True
113 113
114 114 def user_activation_state(self):
115 115 def_user_perms = User.get_default_user().AuthUser().permissions['global']
116 116 return 'hg.extern_activate.auto' in def_user_perms
117 117
118 118 def _clean_username(self, username):
119 119 # Removing realm and domain from username
120 120 username = username.split('@')[0]
121 121 username = username.rsplit('\\')[-1]
122 122 return username
123 123
124 124 def _get_username(self, environ, settings):
125 125 username = None
126 126 environ = environ or {}
127 127 if not environ:
128 128 log.debug('got empty environ: %s', environ)
129 129
130 130 settings = settings or {}
131 131 if settings.get('header'):
132 132 header = settings.get('header')
133 133 username = environ.get(header)
134 134 log.debug('extracted %s:%s', header, username)
135 135
136 136 # fallback mode
137 137 if not username and settings.get('fallback_header'):
138 138 header = settings.get('fallback_header')
139 139 username = environ.get(header)
140 140 log.debug('extracted %s:%s', header, username)
141 141
142 142 if username and str2bool(settings.get('clean_username')):
143 143 log.debug('Received username `%s` from headers', username)
144 144 username = self._clean_username(username)
145 145 log.debug('New cleanup user is:%s', username)
146 146 return username
147 147
148 148 def get_user(self, username=None, **kwargs):
149 149 """
150 150 Helper method for user fetching in plugins, by default it's using
151 151 simple fetch by username, but this method can be custimized in plugins
152 152 eg. headers auth plugin to fetch user by environ params
153 153 :param username: username if given to fetch
154 154 :param kwargs: extra arguments needed for user fetching.
155 155 """
156 156 environ = kwargs.get('environ') or {}
157 157 settings = kwargs.get('settings') or {}
158 158 username = self._get_username(environ, settings)
159 159 # we got the username, so use default method now
160 160 return super(RhodeCodeAuthPlugin, self).get_user(username)
161 161
162 162 def auth(self, userobj, username, password, settings, **kwargs):
163 163 """
164 164 Get's the headers_auth username (or email). It tries to get username
165 165 from REMOTE_USER if this plugin is enabled, if that fails
166 166 it tries to get username from HTTP_X_FORWARDED_USER if fallback header
167 167 is set. clean_username extracts the username from this data if it's
168 168 having @ in it.
169 169 Return None on failure. On success, return a dictionary of the form:
170 170
171 171 see: RhodeCodeAuthPluginBase.auth_func_attrs
172 172
173 173 :param userobj:
174 174 :param username:
175 175 :param password:
176 176 :param settings:
177 177 :param kwargs:
178 178 """
179 179 environ = kwargs.get('environ')
180 180 if not environ:
181 181 log.debug('Empty environ data skipping...')
182 182 return None
183 183
184 184 if not userobj:
185 185 userobj = self.get_user('', environ=environ, settings=settings)
186 186
187 187 # we don't care passed username/password for headers auth plugins.
188 188 # only way to log in is using environ
189 189 username = None
190 190 if userobj:
191 191 username = getattr(userobj, 'username')
192 192
193 193 if not username:
194 194 # we don't have any objects in DB user doesn't exist extract
195 195 # username from environ based on the settings
196 196 username = self._get_username(environ, settings)
197 197
198 198 # if cannot fetch username, it's a no-go for this plugin to proceed
199 199 if not username:
200 200 return None
201 201
202 202 # old attrs fetched from RhodeCode database
203 203 admin = getattr(userobj, 'admin', False)
204 204 active = getattr(userobj, 'active', True)
205 205 email = getattr(userobj, 'email', '')
206 206 firstname = getattr(userobj, 'firstname', '')
207 207 lastname = getattr(userobj, 'lastname', '')
208 208 extern_type = getattr(userobj, 'extern_type', '')
209 209
210 210 user_attrs = {
211 211 'username': username,
212 212 'firstname': safe_unicode(firstname or username),
213 213 'lastname': safe_unicode(lastname or ''),
214 214 'groups': [],
215 215 'user_group_sync': False,
216 216 'email': email or '',
217 217 'admin': admin or False,
218 218 'active': active,
219 219 'active_from_extern': True,
220 220 'extern_name': username,
221 221 'extern_type': extern_type,
222 222 }
223 223
224 224 log.info('user `%s` authenticated correctly', user_attrs['username'])
225 225 return user_attrs
226 226
227 227
228 228 def includeme(config):
229 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format('headers')
229 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
230 230 plugin_factory(plugin_id).includeme(config)
@@ -1,172 +1,173 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 RhodeCode authentication plugin for 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 rhodecode.translation import _
34 34 from rhodecode.authentication.base import (
35 35 RhodeCodeExternalAuthPlugin, hybrid_property)
36 36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 37 from rhodecode.authentication.routes import AuthnPluginResourceBase
38 38 from rhodecode.lib.colander_utils import strip_whitespace
39 39 from rhodecode.lib.utils2 import safe_unicode
40 40 from rhodecode.model.db import User
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 def plugin_factory(plugin_id, *args, **kwds):
46 46 """
47 47 Factory function that is called during plugin discovery.
48 48 It returns the plugin instance.
49 49 """
50 50 plugin = RhodeCodeAuthPlugin(plugin_id)
51 51 return plugin
52 52
53 53
54 54 class JasigCasAuthnResource(AuthnPluginResourceBase):
55 55 pass
56 56
57 57
58 58 class JasigCasSettingsSchema(AuthnPluginSettingsSchemaBase):
59 59 service_url = colander.SchemaNode(
60 60 colander.String(),
61 61 default='https://domain.com/cas/v1/tickets',
62 62 description=_('The url of the Jasig CAS REST service'),
63 63 preparer=strip_whitespace,
64 64 title=_('URL'),
65 65 widget='string')
66 66
67 67
68 68 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
69 uid = 'jasig_cas'
69 70
70 71 def includeme(self, config):
71 72 config.add_authn_plugin(self)
72 73 config.add_authn_resource(self.get_id(), JasigCasAuthnResource(self))
73 74 config.add_view(
74 75 'rhodecode.authentication.views.AuthnPluginViewBase',
75 76 attr='settings_get',
76 77 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
77 78 request_method='GET',
78 79 route_name='auth_home',
79 80 context=JasigCasAuthnResource)
80 81 config.add_view(
81 82 'rhodecode.authentication.views.AuthnPluginViewBase',
82 83 attr='settings_post',
83 84 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
84 85 request_method='POST',
85 86 route_name='auth_home',
86 87 context=JasigCasAuthnResource)
87 88
88 89 def get_settings_schema(self):
89 90 return JasigCasSettingsSchema()
90 91
91 92 def get_display_name(self):
92 93 return _('Jasig-CAS')
93 94
94 95 @hybrid_property
95 96 def name(self):
96 97 return "jasig-cas"
97 98
98 99 @property
99 100 def is_headers_auth(self):
100 101 return True
101 102
102 103 def use_fake_password(self):
103 104 return True
104 105
105 106 def user_activation_state(self):
106 107 def_user_perms = User.get_default_user().AuthUser().permissions['global']
107 108 return 'hg.extern_activate.auto' in def_user_perms
108 109
109 110 def auth(self, userobj, username, password, settings, **kwargs):
110 111 """
111 112 Given a user object (which may be null), username, a plaintext password,
112 113 and a settings object (containing all the keys needed as listed in settings()),
113 114 authenticate this user's login attempt.
114 115
115 116 Return None on failure. On success, return a dictionary of the form:
116 117
117 118 see: RhodeCodeAuthPluginBase.auth_func_attrs
118 119 This is later validated for correctness
119 120 """
120 121 if not username or not password:
121 122 log.debug('Empty username or password skipping...')
122 123 return None
123 124
124 125 log.debug("Jasig CAS settings: %s", settings)
125 126 params = urllib.urlencode({'username': username, 'password': password})
126 127 headers = {"Content-type": "application/x-www-form-urlencoded",
127 128 "Accept": "text/plain",
128 129 "User-Agent": "RhodeCode-auth-%s" % rhodecode.__version__}
129 130 url = settings["service_url"]
130 131
131 132 log.debug("Sent Jasig CAS: \n%s",
132 133 {"url": url, "body": params, "headers": headers})
133 134 request = urllib2.Request(url, params, headers)
134 135 try:
135 136 response = urllib2.urlopen(request)
136 137 except urllib2.HTTPError as e:
137 138 log.debug("HTTPError when requesting Jasig CAS (status code: %d)", e.code)
138 139 return None
139 140 except urllib2.URLError as e:
140 141 log.debug("URLError when requesting Jasig CAS url: %s ", url)
141 142 return None
142 143
143 144 # old attrs fetched from RhodeCode database
144 145 admin = getattr(userobj, 'admin', False)
145 146 active = getattr(userobj, 'active', True)
146 147 email = getattr(userobj, 'email', '')
147 148 username = getattr(userobj, 'username', username)
148 149 firstname = getattr(userobj, 'firstname', '')
149 150 lastname = getattr(userobj, 'lastname', '')
150 151 extern_type = getattr(userobj, 'extern_type', '')
151 152
152 153 user_attrs = {
153 154 'username': username,
154 155 'firstname': safe_unicode(firstname or username),
155 156 'lastname': safe_unicode(lastname or ''),
156 157 'groups': [],
157 158 'user_group_sync': False,
158 159 'email': email or '',
159 160 'admin': admin or False,
160 161 'active': active,
161 162 'active_from_extern': True,
162 163 'extern_name': username,
163 164 'extern_type': extern_type,
164 165 }
165 166
166 167 log.info('user `%s` authenticated correctly', user_attrs['username'])
167 168 return user_attrs
168 169
169 170
170 171 def includeme(config):
171 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format('jasig_cas')
172 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
172 173 plugin_factory(plugin_id).includeme(config)
@@ -1,533 +1,534 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 RhodeCode authentication plugin for LDAP
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27
28 28 import colander
29 29 from rhodecode.translation import _
30 30 from rhodecode.authentication.base import (
31 31 RhodeCodeExternalAuthPlugin, AuthLdapBase, hybrid_property)
32 32 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
33 33 from rhodecode.authentication.routes import AuthnPluginResourceBase
34 34 from rhodecode.lib.colander_utils import strip_whitespace
35 35 from rhodecode.lib.exceptions import (
36 36 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
37 37 )
38 38 from rhodecode.lib.utils2 import safe_unicode, safe_str
39 39 from rhodecode.model.db import User
40 40 from rhodecode.model.validators import Missing
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44 try:
45 45 import ldap
46 46 except ImportError:
47 47 # means that python-ldap is not installed, we use Missing object to mark
48 48 # ldap lib is Missing
49 49 ldap = Missing
50 50
51 51
52 52 class LdapError(Exception):
53 53 pass
54 54
55 55
56 56 def plugin_factory(plugin_id, *args, **kwds):
57 57 """
58 58 Factory function that is called during plugin discovery.
59 59 It returns the plugin instance.
60 60 """
61 61 plugin = RhodeCodeAuthPlugin(plugin_id)
62 62 return plugin
63 63
64 64
65 65 class LdapAuthnResource(AuthnPluginResourceBase):
66 66 pass
67 67
68 68
69 69 class AuthLdap(AuthLdapBase):
70 70 default_tls_cert_dir = '/etc/openldap/cacerts'
71 71
72 72 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
73 73 tls_kind='PLAIN', tls_reqcert='DEMAND', tls_cert_file=None,
74 74 tls_cert_dir=None, ldap_version=3,
75 75 search_scope='SUBTREE', attr_login='uid',
76 76 ldap_filter='', timeout=None):
77 77 if ldap == Missing:
78 78 raise LdapImportError("Missing or incompatible ldap library")
79 79
80 80 self.debug = False
81 81 self.timeout = timeout or 60 * 5
82 82 self.ldap_version = ldap_version
83 83 self.ldap_server_type = 'ldap'
84 84
85 85 self.TLS_KIND = tls_kind
86 86
87 87 if self.TLS_KIND == 'LDAPS':
88 88 port = port or 689
89 89 self.ldap_server_type += 's'
90 90
91 91 OPT_X_TLS_DEMAND = 2
92 92 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert, OPT_X_TLS_DEMAND)
93 93 self.TLS_CERT_FILE = tls_cert_file or ''
94 94 self.TLS_CERT_DIR = tls_cert_dir or self.default_tls_cert_dir
95 95
96 96 # split server into list
97 97 self.SERVER_ADDRESSES = self._get_server_list(server)
98 98 self.LDAP_SERVER_PORT = port
99 99
100 100 # USE FOR READ ONLY BIND TO LDAP SERVER
101 101 self.attr_login = attr_login
102 102
103 103 self.LDAP_BIND_DN = safe_str(bind_dn)
104 104 self.LDAP_BIND_PASS = safe_str(bind_pass)
105 105
106 106 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
107 107 self.BASE_DN = safe_str(base_dn)
108 108 self.LDAP_FILTER = safe_str(ldap_filter)
109 109
110 110 def _get_ldap_conn(self):
111 111
112 112 if self.debug:
113 113 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
114 114
115 115 if self.TLS_CERT_FILE and hasattr(ldap, 'OPT_X_TLS_CACERTFILE'):
116 116 ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, self.TLS_CERT_FILE)
117 117
118 118 elif hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
119 119 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, self.TLS_CERT_DIR)
120 120
121 121 if self.TLS_KIND != 'PLAIN':
122 122 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
123 123
124 124 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
125 125 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
126 126
127 127 # init connection now
128 128 ldap_servers = self._build_servers(
129 129 self.ldap_server_type, self.SERVER_ADDRESSES, self.LDAP_SERVER_PORT)
130 130 log.debug('initializing LDAP connection to:%s', ldap_servers)
131 131 ldap_conn = ldap.initialize(ldap_servers)
132 132 ldap_conn.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout)
133 133 ldap_conn.set_option(ldap.OPT_TIMEOUT, self.timeout)
134 134 ldap_conn.timeout = self.timeout
135 135
136 136 if self.ldap_version == 2:
137 137 ldap_conn.protocol = ldap.VERSION2
138 138 else:
139 139 ldap_conn.protocol = ldap.VERSION3
140 140
141 141 if self.TLS_KIND == 'START_TLS':
142 142 ldap_conn.start_tls_s()
143 143
144 144 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
145 145 log.debug('Trying simple_bind with password and given login DN: %s',
146 146 self.LDAP_BIND_DN)
147 147 ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
148 148
149 149 return ldap_conn
150 150
151 151 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
152 152 try:
153 153 log.debug('Trying simple bind with %s', dn)
154 154 server.simple_bind_s(dn, safe_str(password))
155 155 user = server.search_ext_s(
156 156 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
157 157 _, attrs = user
158 158 return attrs
159 159
160 160 except ldap.INVALID_CREDENTIALS:
161 161 log.debug(
162 162 "LDAP rejected password for user '%s': %s, org_exc:",
163 163 username, dn, exc_info=True)
164 164
165 165 def authenticate_ldap(self, username, password):
166 166 """
167 167 Authenticate a user via LDAP and return his/her LDAP properties.
168 168
169 169 Raises AuthenticationError if the credentials are rejected, or
170 170 EnvironmentError if the LDAP server can't be reached.
171 171
172 172 :param username: username
173 173 :param password: password
174 174 """
175 175
176 176 uid = self.get_uid(username, self.SERVER_ADDRESSES)
177 177 user_attrs = {}
178 178 dn = ''
179 179
180 180 self.validate_password(username, password)
181 181 self.validate_username(username)
182 182
183 183 ldap_conn = None
184 184 try:
185 185 ldap_conn = self._get_ldap_conn()
186 186 filter_ = '(&%s(%s=%s))' % (
187 187 self.LDAP_FILTER, self.attr_login, username)
188 188 log.debug("Authenticating %r filter %s", self.BASE_DN, filter_)
189 189
190 190 lobjects = ldap_conn.search_ext_s(
191 191 self.BASE_DN, self.SEARCH_SCOPE, filter_)
192 192
193 193 if not lobjects:
194 194 log.debug("No matching LDAP objects for authentication "
195 195 "of UID:'%s' username:(%s)", uid, username)
196 196 raise ldap.NO_SUCH_OBJECT()
197 197
198 198 log.debug('Found matching ldap object, trying to authenticate')
199 199 for (dn, _attrs) in lobjects:
200 200 if dn is None:
201 201 continue
202 202
203 203 user_attrs = self.fetch_attrs_from_simple_bind(
204 204 ldap_conn, dn, username, password)
205 205 if user_attrs:
206 206 break
207 207 else:
208 208 raise LdapPasswordError(
209 209 'Failed to authenticate user `{}`'
210 210 'with given password'.format(username))
211 211
212 212 except ldap.NO_SUCH_OBJECT:
213 213 log.debug("LDAP says no such user '%s' (%s), org_exc:",
214 214 uid, username, exc_info=True)
215 215 raise LdapUsernameError('Unable to find user')
216 216 except ldap.SERVER_DOWN:
217 217 org_exc = traceback.format_exc()
218 218 raise LdapConnectionError(
219 219 "LDAP can't access authentication "
220 220 "server, org_exc:%s" % org_exc)
221 221 finally:
222 222 if ldap_conn:
223 223 log.debug('ldap: connection release')
224 224 try:
225 225 ldap_conn.unbind_s()
226 226 except Exception:
227 227 # for any reason this can raise exception we must catch it
228 228 # to not crush the server
229 229 pass
230 230
231 231 return dn, user_attrs
232 232
233 233
234 234 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
235 235 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
236 236 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
237 237 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
238 238
239 239 host = colander.SchemaNode(
240 240 colander.String(),
241 241 default='',
242 242 description=_('Host[s] of the LDAP Server \n'
243 243 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
244 244 'Multiple servers can be specified using commas'),
245 245 preparer=strip_whitespace,
246 246 title=_('LDAP Host'),
247 247 widget='string')
248 248 port = colander.SchemaNode(
249 249 colander.Int(),
250 250 default=389,
251 251 description=_('Custom port that the LDAP server is listening on. '
252 252 'Default value is: 389'),
253 253 preparer=strip_whitespace,
254 254 title=_('Port'),
255 255 validator=colander.Range(min=0, max=65536),
256 256 widget='int')
257 257
258 258 timeout = colander.SchemaNode(
259 259 colander.Int(),
260 260 default=60 * 5,
261 261 description=_('Timeout for LDAP connection'),
262 262 preparer=strip_whitespace,
263 263 title=_('Connection timeout'),
264 264 validator=colander.Range(min=1),
265 265 widget='int')
266 266
267 267 dn_user = colander.SchemaNode(
268 268 colander.String(),
269 269 default='',
270 270 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
271 271 'e.g., cn=admin,dc=mydomain,dc=com, or '
272 272 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
273 273 missing='',
274 274 preparer=strip_whitespace,
275 275 title=_('Account'),
276 276 widget='string')
277 277 dn_pass = colander.SchemaNode(
278 278 colander.String(),
279 279 default='',
280 280 description=_('Password to authenticate for given user DN.'),
281 281 missing='',
282 282 preparer=strip_whitespace,
283 283 title=_('Password'),
284 284 widget='password')
285 285 tls_kind = colander.SchemaNode(
286 286 colander.String(),
287 287 default=tls_kind_choices[0],
288 288 description=_('TLS Type'),
289 289 title=_('Connection Security'),
290 290 validator=colander.OneOf(tls_kind_choices),
291 291 widget='select')
292 292 tls_reqcert = colander.SchemaNode(
293 293 colander.String(),
294 294 default=tls_reqcert_choices[0],
295 295 description=_('Require Cert over TLS?. Self-signed and custom '
296 296 'certificates can be used when\n `RhodeCode Certificate` '
297 297 'found in admin > settings > system info page is extended.'),
298 298 title=_('Certificate Checks'),
299 299 validator=colander.OneOf(tls_reqcert_choices),
300 300 widget='select')
301 301 tls_cert_file = colander.SchemaNode(
302 302 colander.String(),
303 303 default='',
304 304 description=_('This specifies the PEM-format file path containing '
305 305 'certificates for use in TLS connection.\n'
306 306 'If not specified `TLS Cert dir` will be used'),
307 307 title=_('TLS Cert file'),
308 308 missing='',
309 309 widget='string')
310 310 tls_cert_dir = colander.SchemaNode(
311 311 colander.String(),
312 312 default=AuthLdap.default_tls_cert_dir,
313 313 description=_('This specifies the path of a directory that contains individual '
314 314 'CA certificates in separate files.'),
315 315 title=_('TLS Cert dir'),
316 316 widget='string')
317 317 base_dn = colander.SchemaNode(
318 318 colander.String(),
319 319 default='',
320 320 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
321 321 'in it to be replaced with current user credentials \n'
322 322 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
323 323 missing='',
324 324 preparer=strip_whitespace,
325 325 title=_('Base DN'),
326 326 widget='string')
327 327 filter = colander.SchemaNode(
328 328 colander.String(),
329 329 default='',
330 330 description=_('Filter to narrow results \n'
331 331 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
332 332 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
333 333 missing='',
334 334 preparer=strip_whitespace,
335 335 title=_('LDAP Search Filter'),
336 336 widget='string')
337 337
338 338 search_scope = colander.SchemaNode(
339 339 colander.String(),
340 340 default=search_scope_choices[2],
341 341 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
342 342 title=_('LDAP Search Scope'),
343 343 validator=colander.OneOf(search_scope_choices),
344 344 widget='select')
345 345 attr_login = colander.SchemaNode(
346 346 colander.String(),
347 347 default='uid',
348 348 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
349 349 preparer=strip_whitespace,
350 350 title=_('Login Attribute'),
351 351 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
352 352 widget='string')
353 353 attr_firstname = colander.SchemaNode(
354 354 colander.String(),
355 355 default='',
356 356 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
357 357 missing='',
358 358 preparer=strip_whitespace,
359 359 title=_('First Name Attribute'),
360 360 widget='string')
361 361 attr_lastname = colander.SchemaNode(
362 362 colander.String(),
363 363 default='',
364 364 description=_('LDAP Attribute to map to last name (e.g., sn)'),
365 365 missing='',
366 366 preparer=strip_whitespace,
367 367 title=_('Last Name Attribute'),
368 368 widget='string')
369 369 attr_email = colander.SchemaNode(
370 370 colander.String(),
371 371 default='',
372 372 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
373 373 'Emails are a crucial part of RhodeCode. \n'
374 374 'If possible add a valid email attribute to ldap users.'),
375 375 missing='',
376 376 preparer=strip_whitespace,
377 377 title=_('Email Attribute'),
378 378 widget='string')
379 379
380 380
381 381 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
382 uid = 'ldap'
382 383 # used to define dynamic binding in the
383 384 DYNAMIC_BIND_VAR = '$login'
384 385 _settings_unsafe_keys = ['dn_pass']
385 386
386 387 def includeme(self, config):
387 388 config.add_authn_plugin(self)
388 389 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
389 390 config.add_view(
390 391 'rhodecode.authentication.views.AuthnPluginViewBase',
391 392 attr='settings_get',
392 393 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
393 394 request_method='GET',
394 395 route_name='auth_home',
395 396 context=LdapAuthnResource)
396 397 config.add_view(
397 398 'rhodecode.authentication.views.AuthnPluginViewBase',
398 399 attr='settings_post',
399 400 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
400 401 request_method='POST',
401 402 route_name='auth_home',
402 403 context=LdapAuthnResource)
403 404
404 405 def get_settings_schema(self):
405 406 return LdapSettingsSchema()
406 407
407 408 def get_display_name(self):
408 409 return _('LDAP')
409 410
410 411 @classmethod
411 412 def docs(cls):
412 413 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-ldap.html"
413 414
414 415 @hybrid_property
415 416 def name(self):
416 417 return "ldap"
417 418
418 419 def use_fake_password(self):
419 420 return True
420 421
421 422 def user_activation_state(self):
422 423 def_user_perms = User.get_default_user().AuthUser().permissions['global']
423 424 return 'hg.extern_activate.auto' in def_user_perms
424 425
425 426 def try_dynamic_binding(self, username, password, current_args):
426 427 """
427 428 Detects marker inside our original bind, and uses dynamic auth if
428 429 present
429 430 """
430 431
431 432 org_bind = current_args['bind_dn']
432 433 passwd = current_args['bind_pass']
433 434
434 435 def has_bind_marker(username):
435 436 if self.DYNAMIC_BIND_VAR in username:
436 437 return True
437 438
438 439 # we only passed in user with "special" variable
439 440 if org_bind and has_bind_marker(org_bind) and not passwd:
440 441 log.debug('Using dynamic user/password binding for ldap '
441 442 'authentication. Replacing `%s` with username',
442 443 self.DYNAMIC_BIND_VAR)
443 444 current_args['bind_dn'] = org_bind.replace(
444 445 self.DYNAMIC_BIND_VAR, username)
445 446 current_args['bind_pass'] = password
446 447
447 448 return current_args
448 449
449 450 def auth(self, userobj, username, password, settings, **kwargs):
450 451 """
451 452 Given a user object (which may be null), username, a plaintext password,
452 453 and a settings object (containing all the keys needed as listed in
453 454 settings()), authenticate this user's login attempt.
454 455
455 456 Return None on failure. On success, return a dictionary of the form:
456 457
457 458 see: RhodeCodeAuthPluginBase.auth_func_attrs
458 459 This is later validated for correctness
459 460 """
460 461
461 462 if not username or not password:
462 463 log.debug('Empty username or password skipping...')
463 464 return None
464 465
465 466 ldap_args = {
466 467 'server': settings.get('host', ''),
467 468 'base_dn': settings.get('base_dn', ''),
468 469 'port': settings.get('port'),
469 470 'bind_dn': settings.get('dn_user'),
470 471 'bind_pass': settings.get('dn_pass'),
471 472 'tls_kind': settings.get('tls_kind'),
472 473 'tls_reqcert': settings.get('tls_reqcert'),
473 474 'tls_cert_file': settings.get('tls_cert_file'),
474 475 'tls_cert_dir': settings.get('tls_cert_dir'),
475 476 'search_scope': settings.get('search_scope'),
476 477 'attr_login': settings.get('attr_login'),
477 478 'ldap_version': 3,
478 479 'ldap_filter': settings.get('filter'),
479 480 'timeout': settings.get('timeout')
480 481 }
481 482
482 483 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
483 484
484 485 log.debug('Checking for ldap authentication.')
485 486
486 487 try:
487 488 aldap = AuthLdap(**ldap_args)
488 489 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
489 490 log.debug('Got ldap DN response %s', user_dn)
490 491
491 492 def get_ldap_attr(k):
492 493 return ldap_attrs.get(settings.get(k), [''])[0]
493 494
494 495 # old attrs fetched from RhodeCode database
495 496 admin = getattr(userobj, 'admin', False)
496 497 active = getattr(userobj, 'active', True)
497 498 email = getattr(userobj, 'email', '')
498 499 username = getattr(userobj, 'username', username)
499 500 firstname = getattr(userobj, 'firstname', '')
500 501 lastname = getattr(userobj, 'lastname', '')
501 502 extern_type = getattr(userobj, 'extern_type', '')
502 503
503 504 groups = []
504 505 user_attrs = {
505 506 'username': username,
506 507 'firstname': safe_unicode(get_ldap_attr('attr_firstname') or firstname),
507 508 'lastname': safe_unicode(get_ldap_attr('attr_lastname') or lastname),
508 509 'groups': groups,
509 510 'user_group_sync': False,
510 511 'email': get_ldap_attr('attr_email') or email,
511 512 'admin': admin,
512 513 'active': active,
513 514 'active_from_extern': None,
514 515 'extern_name': user_dn,
515 516 'extern_type': extern_type,
516 517 }
517 518
518 519 log.debug('ldap user: %s', user_attrs)
519 520 log.info('user `%s` authenticated correctly', user_attrs['username'])
520 521
521 522 return user_attrs
522 523
523 524 except (LdapUsernameError, LdapPasswordError, LdapImportError):
524 525 log.exception("LDAP related exception")
525 526 return None
526 527 except (Exception,):
527 528 log.exception("Other exception")
528 529 return None
529 530
530 531
531 532 def includeme(config):
532 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format('ldap')
533 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
533 534 plugin_factory(plugin_id).includeme(config)
@@ -1,170 +1,171 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 RhodeCode authentication library for PAM
23 23 """
24 24
25 25 import colander
26 26 import grp
27 27 import logging
28 28 import pam
29 29 import pwd
30 30 import re
31 31 import socket
32 32
33 33 from rhodecode.translation import _
34 34 from rhodecode.authentication.base import (
35 35 RhodeCodeExternalAuthPlugin, hybrid_property)
36 36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 37 from rhodecode.authentication.routes import AuthnPluginResourceBase
38 38 from rhodecode.lib.colander_utils import strip_whitespace
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 def plugin_factory(plugin_id, *args, **kwds):
44 44 """
45 45 Factory function that is called during plugin discovery.
46 46 It returns the plugin instance.
47 47 """
48 48 plugin = RhodeCodeAuthPlugin(plugin_id)
49 49 return plugin
50 50
51 51
52 52 class PamAuthnResource(AuthnPluginResourceBase):
53 53 pass
54 54
55 55
56 56 class PamSettingsSchema(AuthnPluginSettingsSchemaBase):
57 57 service = colander.SchemaNode(
58 58 colander.String(),
59 59 default='login',
60 60 description=_('PAM service name to use for authentication.'),
61 61 preparer=strip_whitespace,
62 62 title=_('PAM service name'),
63 63 widget='string')
64 64 gecos = colander.SchemaNode(
65 65 colander.String(),
66 66 default='(?P<last_name>.+),\s*(?P<first_name>\w+)',
67 67 description=_('Regular expression for extracting user name/email etc. '
68 68 'from Unix userinfo.'),
69 69 preparer=strip_whitespace,
70 70 title=_('Gecos Regex'),
71 71 widget='string')
72 72
73 73
74 74 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
75 uid = 'pam'
75 76 # PAM authentication can be slow. Repository operations involve a lot of
76 77 # auth calls. Little caching helps speedup push/pull operations significantly
77 78 AUTH_CACHE_TTL = 4
78 79
79 80 def includeme(self, config):
80 81 config.add_authn_plugin(self)
81 82 config.add_authn_resource(self.get_id(), PamAuthnResource(self))
82 83 config.add_view(
83 84 'rhodecode.authentication.views.AuthnPluginViewBase',
84 85 attr='settings_get',
85 86 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
86 87 request_method='GET',
87 88 route_name='auth_home',
88 89 context=PamAuthnResource)
89 90 config.add_view(
90 91 'rhodecode.authentication.views.AuthnPluginViewBase',
91 92 attr='settings_post',
92 93 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
93 94 request_method='POST',
94 95 route_name='auth_home',
95 96 context=PamAuthnResource)
96 97
97 98 def get_display_name(self):
98 99 return _('PAM')
99 100
100 101 @classmethod
101 102 def docs(cls):
102 103 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-pam.html"
103 104
104 105 @hybrid_property
105 106 def name(self):
106 107 return "pam"
107 108
108 109 def get_settings_schema(self):
109 110 return PamSettingsSchema()
110 111
111 112 def use_fake_password(self):
112 113 return True
113 114
114 115 def auth(self, userobj, username, password, settings, **kwargs):
115 116 if not username or not password:
116 117 log.debug('Empty username or password skipping...')
117 118 return None
118 119 _pam = pam.pam()
119 120 auth_result = _pam.authenticate(username, password, settings["service"])
120 121
121 122 if not auth_result:
122 123 log.error("PAM was unable to authenticate user: %s", username)
123 124 return None
124 125
125 126 log.debug('Got PAM response %s', auth_result)
126 127
127 128 # old attrs fetched from RhodeCode database
128 129 default_email = "%s@%s" % (username, socket.gethostname())
129 130 admin = getattr(userobj, 'admin', False)
130 131 active = getattr(userobj, 'active', True)
131 132 email = getattr(userobj, 'email', '') or default_email
132 133 username = getattr(userobj, 'username', username)
133 134 firstname = getattr(userobj, 'firstname', '')
134 135 lastname = getattr(userobj, 'lastname', '')
135 136 extern_type = getattr(userobj, 'extern_type', '')
136 137
137 138 user_attrs = {
138 139 'username': username,
139 140 'firstname': firstname,
140 141 'lastname': lastname,
141 142 'groups': [g.gr_name for g in grp.getgrall()
142 143 if username in g.gr_mem],
143 144 'user_group_sync': True,
144 145 'email': email,
145 146 'admin': admin,
146 147 'active': active,
147 148 'active_from_extern': None,
148 149 'extern_name': username,
149 150 'extern_type': extern_type,
150 151 }
151 152
152 153 try:
153 154 user_data = pwd.getpwnam(username)
154 155 regex = settings["gecos"]
155 156 match = re.search(regex, user_data.pw_gecos)
156 157 if match:
157 158 user_attrs["firstname"] = match.group('first_name')
158 159 user_attrs["lastname"] = match.group('last_name')
159 160 except Exception:
160 161 log.warning("Cannot extract additional info for PAM user")
161 162 pass
162 163
163 164 log.debug("pamuser: %s", user_attrs)
164 165 log.info('user `%s` authenticated correctly', user_attrs['username'])
165 166 return user_attrs
166 167
167 168
168 169 def includeme(config):
169 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format('pam')
170 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
170 171 plugin_factory(plugin_id).includeme(config)
@@ -1,148 +1,149 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 RhodeCode authentication plugin for built in internal auth
23 23 """
24 24
25 25 import logging
26 26
27 27 from rhodecode.translation import _
28 28
29 29 from rhodecode.authentication.base import RhodeCodeAuthPluginBase, hybrid_property
30 30 from rhodecode.authentication.routes import AuthnPluginResourceBase
31 31 from rhodecode.lib.utils2 import safe_str
32 32 from rhodecode.model.db import User
33 33
34 34 log = logging.getLogger(__name__)
35 35
36 36
37 37 def plugin_factory(plugin_id, *args, **kwds):
38 38 plugin = RhodeCodeAuthPlugin(plugin_id)
39 39 return plugin
40 40
41 41
42 42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
43 43 pass
44 44
45 45
46 46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
47 uid = 'rhodecode'
47 48
48 49 def includeme(self, config):
49 50 config.add_authn_plugin(self)
50 51 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
51 52 config.add_view(
52 53 'rhodecode.authentication.views.AuthnPluginViewBase',
53 54 attr='settings_get',
54 55 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
55 56 request_method='GET',
56 57 route_name='auth_home',
57 58 context=RhodecodeAuthnResource)
58 59 config.add_view(
59 60 'rhodecode.authentication.views.AuthnPluginViewBase',
60 61 attr='settings_post',
61 62 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
62 63 request_method='POST',
63 64 route_name='auth_home',
64 65 context=RhodecodeAuthnResource)
65 66
66 67 def get_display_name(self):
67 68 return _('RhodeCode Internal')
68 69
69 70 @hybrid_property
70 71 def name(self):
71 72 return "rhodecode"
72 73
73 74 def user_activation_state(self):
74 75 def_user_perms = User.get_default_user().AuthUser().permissions['global']
75 76 return 'hg.register.auto_activate' in def_user_perms
76 77
77 78 def allows_authentication_from(
78 79 self, user, allows_non_existing_user=True,
79 80 allowed_auth_plugins=None, allowed_auth_sources=None):
80 81 """
81 82 Custom method for this auth that doesn't accept non existing users.
82 83 We know that user exists in our database.
83 84 """
84 85 allows_non_existing_user = False
85 86 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
86 87 user, allows_non_existing_user=allows_non_existing_user)
87 88
88 89 def auth(self, userobj, username, password, settings, **kwargs):
89 90 if not userobj:
90 91 log.debug('userobj was:%s skipping', userobj)
91 92 return None
92 93 if userobj.extern_type != self.name:
93 94 log.warning(
94 95 "userobj:%s extern_type mismatch got:`%s` expected:`%s`",
95 96 userobj, userobj.extern_type, self.name)
96 97 return None
97 98
98 99 user_attrs = {
99 100 "username": userobj.username,
100 101 "firstname": userobj.firstname,
101 102 "lastname": userobj.lastname,
102 103 "groups": [],
103 104 'user_group_sync': False,
104 105 "email": userobj.email,
105 106 "admin": userobj.admin,
106 107 "active": userobj.active,
107 108 "active_from_extern": userobj.active,
108 109 "extern_name": userobj.user_id,
109 110 "extern_type": userobj.extern_type,
110 111 }
111 112
112 113 log.debug("User attributes:%s", user_attrs)
113 114 if userobj.active:
114 115 from rhodecode.lib import auth
115 116 crypto_backend = auth.crypto_backend()
116 117 password_encoded = safe_str(password)
117 118 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
118 119 password_encoded, userobj.password or '')
119 120
120 121 if password_match and new_hash:
121 122 log.debug('user %s properly authenticated, but '
122 123 'requires hash change to bcrypt', userobj)
123 124 # if password match, and we use OLD deprecated hash,
124 125 # we should migrate this user hash password to the new hash
125 126 # we store the new returned by hash_check_with_upgrade function
126 127 user_attrs['_hash_migrate'] = new_hash
127 128
128 129 if userobj.username == User.DEFAULT_USER and userobj.active:
129 130 log.info(
130 131 'user `%s` authenticated correctly as anonymous user', userobj.username)
131 132 return user_attrs
132 133
133 134 elif userobj.username == username and password_match:
134 135 log.info('user `%s` authenticated correctly', userobj.username)
135 136 return user_attrs
136 137 log.warn("user `%s` used a wrong password when "
137 138 "authenticating on this plugin", userobj.username)
138 139 return None
139 140 else:
140 141 log.warning(
141 142 'user `%s` failed to authenticate via %s, reason: account not '
142 143 'active.', username, self.name)
143 144 return None
144 145
145 146
146 147 def includeme(config):
147 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format('rhodecode')
148 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
148 149 plugin_factory(plugin_id).includeme(config)
@@ -1,156 +1,157 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 RhodeCode authentication token plugin for built in internal auth
23 23 """
24 24
25 25 import logging
26 26
27 27 from rhodecode.translation import _
28 28 from rhodecode.authentication.base import (
29 29 RhodeCodeAuthPluginBase, VCS_TYPE, hybrid_property)
30 30 from rhodecode.authentication.routes import AuthnPluginResourceBase
31 31 from rhodecode.model.db import User, UserApiKeys, Repository
32 32
33 33
34 34 log = logging.getLogger(__name__)
35 35
36 36
37 37 def plugin_factory(plugin_id, *args, **kwds):
38 38 plugin = RhodeCodeAuthPlugin(plugin_id)
39 39 return plugin
40 40
41 41
42 42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
43 43 pass
44 44
45 45
46 46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
47 47 """
48 48 Enables usage of authentication tokens for vcs operations.
49 49 """
50 uid = 'token'
50 51
51 52 def includeme(self, config):
52 53 config.add_authn_plugin(self)
53 54 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
54 55 config.add_view(
55 56 'rhodecode.authentication.views.AuthnPluginViewBase',
56 57 attr='settings_get',
57 58 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
58 59 request_method='GET',
59 60 route_name='auth_home',
60 61 context=RhodecodeAuthnResource)
61 62 config.add_view(
62 63 'rhodecode.authentication.views.AuthnPluginViewBase',
63 64 attr='settings_post',
64 65 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
65 66 request_method='POST',
66 67 route_name='auth_home',
67 68 context=RhodecodeAuthnResource)
68 69
69 70 def get_display_name(self):
70 71 return _('Rhodecode Token')
71 72
72 73 @classmethod
73 74 def docs(cls):
74 75 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-token.html"
75 76
76 77 @hybrid_property
77 78 def name(self):
78 79 return "authtoken"
79 80
80 81 def user_activation_state(self):
81 82 def_user_perms = User.get_default_user().AuthUser().permissions['global']
82 83 return 'hg.register.auto_activate' in def_user_perms
83 84
84 85 def allows_authentication_from(
85 86 self, user, allows_non_existing_user=True,
86 87 allowed_auth_plugins=None, allowed_auth_sources=None):
87 88 """
88 89 Custom method for this auth that doesn't accept empty users. And also
89 90 allows users from all other active plugins to use it and also
90 91 authenticate against it. But only via vcs mode
91 92 """
92 93 from rhodecode.authentication.base import get_authn_registry
93 94 authn_registry = get_authn_registry()
94 95
95 96 active_plugins = set(
96 97 [x.name for x in authn_registry.get_plugins_for_authentication()])
97 98 active_plugins.discard(self.name)
98 99
99 100 allowed_auth_plugins = [self.name] + list(active_plugins)
100 101 # only for vcs operations
101 102 allowed_auth_sources = [VCS_TYPE]
102 103
103 104 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
104 105 user, allows_non_existing_user=False,
105 106 allowed_auth_plugins=allowed_auth_plugins,
106 107 allowed_auth_sources=allowed_auth_sources)
107 108
108 109 def auth(self, userobj, username, password, settings, **kwargs):
109 110 if not userobj:
110 111 log.debug('userobj was:%s skipping', userobj)
111 112 return None
112 113
113 114 user_attrs = {
114 115 "username": userobj.username,
115 116 "firstname": userobj.firstname,
116 117 "lastname": userobj.lastname,
117 118 "groups": [],
118 119 'user_group_sync': False,
119 120 "email": userobj.email,
120 121 "admin": userobj.admin,
121 122 "active": userobj.active,
122 123 "active_from_extern": userobj.active,
123 124 "extern_name": userobj.user_id,
124 125 "extern_type": userobj.extern_type,
125 126 }
126 127
127 128 log.debug('Authenticating user with args %s', user_attrs)
128 129 if userobj.active:
129 130 # calling context repo for token scopes
130 131 scope_repo_id = None
131 132 if self.acl_repo_name:
132 133 repo = Repository.get_by_repo_name(self.acl_repo_name)
133 134 scope_repo_id = repo.repo_id if repo else None
134 135
135 136 token_match = userobj.authenticate_by_token(
136 137 password, roles=[UserApiKeys.ROLE_VCS],
137 138 scope_repo_id=scope_repo_id)
138 139
139 140 if userobj.username == username and token_match:
140 141 log.info(
141 142 'user `%s` successfully authenticated via %s',
142 143 user_attrs['username'], self.name)
143 144 return user_attrs
144 145 log.warn(
145 146 'user `%s` failed to authenticate via %s, reason: bad or '
146 147 'inactive token.', username, self.name)
147 148 else:
148 149 log.warning(
149 150 'user `%s` failed to authenticate via %s, reason: account not '
150 151 'active.', username, self.name)
151 152 return None
152 153
153 154
154 155 def includeme(config):
155 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format('token')
156 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
156 157 plugin_factory(plugin_id).includeme(config)
General Comments 0
You need to be logged in to leave comments. Login now