##// END OF EJS Templates
logging: expose extra metadata to various important logs for loki
super-admin -
r4816:0163d6c9 default
parent child Browse files
Show More
@@ -1,816 +1,822 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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.statsd_client import StatsdClient
40 40 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
41 41 from rhodecode.lib.utils2 import safe_int, safe_str
42 42 from rhodecode.lib.exceptions import (LdapConnectionError, LdapUsernameError, 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 external_auth_session_key = 'rhodecode.external_auth'
57 57
58 58
59 59 class hybrid_property(object):
60 60 """
61 61 a property decorator that works both for instance and class
62 62 """
63 63 def __init__(self, fget, fset=None, fdel=None, expr=None):
64 64 self.fget = fget
65 65 self.fset = fset
66 66 self.fdel = fdel
67 67 self.expr = expr or fget
68 68 functools.update_wrapper(self, fget)
69 69
70 70 def __get__(self, instance, owner):
71 71 if instance is None:
72 72 return self.expr(owner)
73 73 else:
74 74 return self.fget(instance)
75 75
76 76 def __set__(self, instance, value):
77 77 self.fset(instance, value)
78 78
79 79 def __delete__(self, instance):
80 80 self.fdel(instance)
81 81
82 82
83 83 class LazyFormencode(object):
84 84 def __init__(self, formencode_obj, *args, **kwargs):
85 85 self.formencode_obj = formencode_obj
86 86 self.args = args
87 87 self.kwargs = kwargs
88 88
89 89 def __call__(self, *args, **kwargs):
90 90 from inspect import isfunction
91 91 formencode_obj = self.formencode_obj
92 92 if isfunction(formencode_obj):
93 93 # case we wrap validators into functions
94 94 formencode_obj = self.formencode_obj(*args, **kwargs)
95 95 return formencode_obj(*self.args, **self.kwargs)
96 96
97 97
98 98 class RhodeCodeAuthPluginBase(object):
99 99 # UID is used to register plugin to the registry
100 100 uid = None
101 101
102 102 # cache the authentication request for N amount of seconds. Some kind
103 103 # of authentication methods are very heavy and it's very efficient to cache
104 104 # the result of a call. If it's set to None (default) cache is off
105 105 AUTH_CACHE_TTL = None
106 106 AUTH_CACHE = {}
107 107
108 108 auth_func_attrs = {
109 109 "username": "unique username",
110 110 "firstname": "first name",
111 111 "lastname": "last name",
112 112 "email": "email address",
113 113 "groups": '["list", "of", "groups"]',
114 114 "user_group_sync":
115 115 'True|False defines if returned user groups should be synced',
116 116 "extern_name": "name in external source of record",
117 117 "extern_type": "type of external source of record",
118 118 "admin": 'True|False defines if user should be RhodeCode super admin',
119 119 "active":
120 120 'True|False defines active state of user internally for RhodeCode',
121 121 "active_from_extern":
122 122 "True|False|None, active state from the external auth, "
123 123 "None means use definition from RhodeCode extern_type active value"
124 124
125 125 }
126 126 # set on authenticate() method and via set_auth_type func.
127 127 auth_type = None
128 128
129 129 # set on authenticate() method and via set_calling_scope_repo, this is a
130 130 # calling scope repository when doing authentication most likely on VCS
131 131 # operations
132 132 acl_repo_name = None
133 133
134 134 # List of setting names to store encrypted. Plugins may override this list
135 135 # to store settings encrypted.
136 136 _settings_encrypted = []
137 137
138 138 # Mapping of python to DB settings model types. Plugins may override or
139 139 # extend this mapping.
140 140 _settings_type_map = {
141 141 colander.String: 'unicode',
142 142 colander.Integer: 'int',
143 143 colander.Boolean: 'bool',
144 144 colander.List: 'list',
145 145 }
146 146
147 147 # list of keys in settings that are unsafe to be logged, should be passwords
148 148 # or other crucial credentials
149 149 _settings_unsafe_keys = []
150 150
151 151 def __init__(self, plugin_id):
152 152 self._plugin_id = plugin_id
153 153 self._settings = {}
154 154
155 155 def __str__(self):
156 156 return self.get_id()
157 157
158 158 def _get_setting_full_name(self, name):
159 159 """
160 160 Return the full setting name used for storing values in the database.
161 161 """
162 162 # TODO: johbo: Using the name here is problematic. It would be good to
163 163 # introduce either new models in the database to hold Plugin and
164 164 # PluginSetting or to use the plugin id here.
165 165 return 'auth_{}_{}'.format(self.name, name)
166 166
167 167 def _get_setting_type(self, name):
168 168 """
169 169 Return the type of a setting. This type is defined by the SettingsModel
170 170 and determines how the setting is stored in DB. Optionally the suffix
171 171 `.encrypted` is appended to instruct SettingsModel to store it
172 172 encrypted.
173 173 """
174 174 schema_node = self.get_settings_schema().get(name)
175 175 db_type = self._settings_type_map.get(
176 176 type(schema_node.typ), 'unicode')
177 177 if name in self._settings_encrypted:
178 178 db_type = '{}.encrypted'.format(db_type)
179 179 return db_type
180 180
181 181 @classmethod
182 182 def docs(cls):
183 183 """
184 184 Defines documentation url which helps with plugin setup
185 185 """
186 186 return ''
187 187
188 188 @classmethod
189 189 def icon(cls):
190 190 """
191 191 Defines ICON in SVG format for authentication method
192 192 """
193 193 return ''
194 194
195 195 def is_enabled(self):
196 196 """
197 197 Returns true if this plugin is enabled. An enabled plugin can be
198 198 configured in the admin interface but it is not consulted during
199 199 authentication.
200 200 """
201 201 auth_plugins = SettingsModel().get_auth_plugins()
202 202 return self.get_id() in auth_plugins
203 203
204 204 def is_active(self, plugin_cached_settings=None):
205 205 """
206 206 Returns true if the plugin is activated. An activated plugin is
207 207 consulted during authentication, assumed it is also enabled.
208 208 """
209 209 return self.get_setting_by_name(
210 210 'enabled', plugin_cached_settings=plugin_cached_settings)
211 211
212 212 def get_id(self):
213 213 """
214 214 Returns the plugin id.
215 215 """
216 216 return self._plugin_id
217 217
218 218 def get_display_name(self, load_from_settings=False):
219 219 """
220 220 Returns a translation string for displaying purposes.
221 221 if load_from_settings is set, plugin settings can override the display name
222 222 """
223 223 raise NotImplementedError('Not implemented in base class')
224 224
225 225 def get_settings_schema(self):
226 226 """
227 227 Returns a colander schema, representing the plugin settings.
228 228 """
229 229 return AuthnPluginSettingsSchemaBase()
230 230
231 231 def _propagate_settings(self, raw_settings):
232 232 settings = {}
233 233 for node in self.get_settings_schema():
234 234 settings[node.name] = self.get_setting_by_name(
235 235 node.name, plugin_cached_settings=raw_settings)
236 236 return settings
237 237
238 238 def get_settings(self, use_cache=True):
239 239 """
240 240 Returns the plugin settings as dictionary.
241 241 """
242 242 if self._settings != {} and use_cache:
243 243 return self._settings
244 244
245 245 raw_settings = SettingsModel().get_all_settings()
246 246 settings = self._propagate_settings(raw_settings)
247 247
248 248 self._settings = settings
249 249 return self._settings
250 250
251 251 def get_setting_by_name(self, name, default=None, plugin_cached_settings=None):
252 252 """
253 253 Returns a plugin setting by name.
254 254 """
255 255 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
256 256 if plugin_cached_settings:
257 257 plugin_settings = plugin_cached_settings
258 258 else:
259 259 plugin_settings = SettingsModel().get_all_settings()
260 260
261 261 if full_name in plugin_settings:
262 262 return plugin_settings[full_name]
263 263 else:
264 264 return default
265 265
266 266 def create_or_update_setting(self, name, value):
267 267 """
268 268 Create or update a setting for this plugin in the persistent storage.
269 269 """
270 270 full_name = self._get_setting_full_name(name)
271 271 type_ = self._get_setting_type(name)
272 272 db_setting = SettingsModel().create_or_update_setting(
273 273 full_name, value, type_)
274 274 return db_setting.app_settings_value
275 275
276 276 def log_safe_settings(self, settings):
277 277 """
278 278 returns a log safe representation of settings, without any secrets
279 279 """
280 280 settings_copy = copy.deepcopy(settings)
281 281 for k in self._settings_unsafe_keys:
282 282 if k in settings_copy:
283 283 del settings_copy[k]
284 284 return settings_copy
285 285
286 286 @hybrid_property
287 287 def name(self):
288 288 """
289 289 Returns the name of this authentication plugin.
290 290
291 291 :returns: string
292 292 """
293 293 raise NotImplementedError("Not implemented in base class")
294 294
295 295 def get_url_slug(self):
296 296 """
297 297 Returns a slug which should be used when constructing URLs which refer
298 298 to this plugin. By default it returns the plugin name. If the name is
299 299 not suitable for using it in an URL the plugin should override this
300 300 method.
301 301 """
302 302 return self.name
303 303
304 304 @property
305 305 def is_headers_auth(self):
306 306 """
307 307 Returns True if this authentication plugin uses HTTP headers as
308 308 authentication method.
309 309 """
310 310 return False
311 311
312 312 @hybrid_property
313 313 def is_container_auth(self):
314 314 """
315 315 Deprecated method that indicates if this authentication plugin uses
316 316 HTTP headers as authentication method.
317 317 """
318 318 warnings.warn(
319 319 'Use is_headers_auth instead.', category=DeprecationWarning)
320 320 return self.is_headers_auth
321 321
322 322 @hybrid_property
323 323 def allows_creating_users(self):
324 324 """
325 325 Defines if Plugin allows users to be created on-the-fly when
326 326 authentication is called. Controls how external plugins should behave
327 327 in terms if they are allowed to create new users, or not. Base plugins
328 328 should not be allowed to, but External ones should be !
329 329
330 330 :return: bool
331 331 """
332 332 return False
333 333
334 334 def set_auth_type(self, auth_type):
335 335 self.auth_type = auth_type
336 336
337 337 def set_calling_scope_repo(self, acl_repo_name):
338 338 self.acl_repo_name = acl_repo_name
339 339
340 340 def allows_authentication_from(
341 341 self, user, allows_non_existing_user=True,
342 342 allowed_auth_plugins=None, allowed_auth_sources=None):
343 343 """
344 344 Checks if this authentication module should accept a request for
345 345 the current user.
346 346
347 347 :param user: user object fetched using plugin's get_user() method.
348 348 :param allows_non_existing_user: if True, don't allow the
349 349 user to be empty, meaning not existing in our database
350 350 :param allowed_auth_plugins: if provided, users extern_type will be
351 351 checked against a list of provided extern types, which are plugin
352 352 auth_names in the end
353 353 :param allowed_auth_sources: authentication type allowed,
354 354 `http` or `vcs` default is both.
355 355 defines if plugin will accept only http authentication vcs
356 356 authentication(git/hg) or both
357 357 :returns: boolean
358 358 """
359 359 if not user and not allows_non_existing_user:
360 360 log.debug('User is empty but plugin does not allow empty users,'
361 361 'not allowed to authenticate')
362 362 return False
363 363
364 364 expected_auth_plugins = allowed_auth_plugins or [self.name]
365 365 if user and (user.extern_type and
366 366 user.extern_type not in expected_auth_plugins):
367 367 log.debug(
368 368 'User `%s` is bound to `%s` auth type. Plugin allows only '
369 369 '%s, skipping', user, user.extern_type, expected_auth_plugins)
370 370
371 371 return False
372 372
373 373 # by default accept both
374 374 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
375 375 if self.auth_type not in expected_auth_from:
376 376 log.debug('Current auth source is %s but plugin only allows %s',
377 377 self.auth_type, expected_auth_from)
378 378 return False
379 379
380 380 return True
381 381
382 382 def get_user(self, username=None, **kwargs):
383 383 """
384 384 Helper method for user fetching in plugins, by default it's using
385 385 simple fetch by username, but this method can be custimized in plugins
386 386 eg. headers auth plugin to fetch user by environ params
387 387
388 388 :param username: username if given to fetch from database
389 389 :param kwargs: extra arguments needed for user fetching.
390 390 """
391 391 user = None
392 392 log.debug(
393 393 'Trying to fetch user `%s` from RhodeCode database', username)
394 394 if username:
395 395 user = User.get_by_username(username)
396 396 if not user:
397 397 log.debug('User not found, fallback to fetch user in '
398 398 'case insensitive mode')
399 399 user = User.get_by_username(username, case_insensitive=True)
400 400 else:
401 401 log.debug('provided username:`%s` is empty skipping...', username)
402 402 if not user:
403 403 log.debug('User `%s` not found in database', username)
404 404 else:
405 405 log.debug('Got DB user:%s', user)
406 406 return user
407 407
408 408 def user_activation_state(self):
409 409 """
410 410 Defines user activation state when creating new users
411 411
412 412 :returns: boolean
413 413 """
414 414 raise NotImplementedError("Not implemented in base class")
415 415
416 416 def auth(self, userobj, username, passwd, settings, **kwargs):
417 417 """
418 418 Given a user object (which may be null), username, a plaintext
419 419 password, and a settings object (containing all the keys needed as
420 420 listed in settings()), authenticate this user's login attempt.
421 421
422 422 Return None on failure. On success, return a dictionary of the form:
423 423
424 424 see: RhodeCodeAuthPluginBase.auth_func_attrs
425 425 This is later validated for correctness
426 426 """
427 427 raise NotImplementedError("not implemented in base class")
428 428
429 429 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
430 430 """
431 431 Wrapper to call self.auth() that validates call on it
432 432
433 433 :param userobj: userobj
434 434 :param username: username
435 435 :param passwd: plaintext password
436 436 :param settings: plugin settings
437 437 """
438 438 auth = self.auth(userobj, username, passwd, settings, **kwargs)
439 439 if auth:
440 440 auth['_plugin'] = self.name
441 441 auth['_ttl_cache'] = self.get_ttl_cache(settings)
442 442 # check if hash should be migrated ?
443 443 new_hash = auth.get('_hash_migrate')
444 444 if new_hash:
445 445 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
446 446 if 'user_group_sync' not in auth:
447 447 auth['user_group_sync'] = False
448 448 return self._validate_auth_return(auth)
449 449 return auth
450 450
451 451 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
452 452 new_hash_cypher = _RhodeCodeCryptoBCrypt()
453 453 # extra checks, so make sure new hash is correct.
454 454 password_encoded = safe_str(password)
455 455 if new_hash and new_hash_cypher.hash_check(
456 456 password_encoded, new_hash):
457 457 cur_user = User.get_by_username(username)
458 458 cur_user.password = new_hash
459 459 Session().add(cur_user)
460 460 Session().flush()
461 461 log.info('Migrated user %s hash to bcrypt', cur_user)
462 462
463 463 def _validate_auth_return(self, ret):
464 464 if not isinstance(ret, dict):
465 465 raise Exception('returned value from auth must be a dict')
466 466 for k in self.auth_func_attrs:
467 467 if k not in ret:
468 468 raise Exception('Missing %s attribute from returned data' % k)
469 469 return ret
470 470
471 471 def get_ttl_cache(self, settings=None):
472 472 plugin_settings = settings or self.get_settings()
473 473 # we set default to 30, we make a compromise here,
474 474 # performance > security, mostly due to LDAP/SVN, majority
475 475 # of users pick cache_ttl to be enabled
476 476 from rhodecode.authentication import plugin_default_auth_ttl
477 477 cache_ttl = plugin_default_auth_ttl
478 478
479 479 if isinstance(self.AUTH_CACHE_TTL, (int, long)):
480 480 # plugin cache set inside is more important than the settings value
481 481 cache_ttl = self.AUTH_CACHE_TTL
482 482 elif plugin_settings.get('cache_ttl'):
483 483 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
484 484
485 485 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
486 486 return plugin_cache_active, cache_ttl
487 487
488 488
489 489 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
490 490
491 491 @hybrid_property
492 492 def allows_creating_users(self):
493 493 return True
494 494
495 495 def use_fake_password(self):
496 496 """
497 497 Return a boolean that indicates whether or not we should set the user's
498 498 password to a random value when it is authenticated by this plugin.
499 499 If your plugin provides authentication, then you will generally
500 500 want this.
501 501
502 502 :returns: boolean
503 503 """
504 504 raise NotImplementedError("Not implemented in base class")
505 505
506 506 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
507 507 # at this point _authenticate calls plugin's `auth()` function
508 508 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
509 509 userobj, username, passwd, settings, **kwargs)
510 510
511 511 if auth:
512 512 # maybe plugin will clean the username ?
513 513 # we should use the return value
514 514 username = auth['username']
515 515
516 516 # if external source tells us that user is not active, we should
517 517 # skip rest of the process. This can prevent from creating users in
518 518 # RhodeCode when using external authentication, but if it's
519 519 # inactive user we shouldn't create that user anyway
520 520 if auth['active_from_extern'] is False:
521 521 log.warning(
522 522 "User %s authenticated against %s, but is inactive",
523 523 username, self.__module__)
524 524 return None
525 525
526 526 cur_user = User.get_by_username(username, case_insensitive=True)
527 527 is_user_existing = cur_user is not None
528 528
529 529 if is_user_existing:
530 530 log.debug('Syncing user `%s` from '
531 531 '`%s` plugin', username, self.name)
532 532 else:
533 533 log.debug('Creating non existing user `%s` from '
534 534 '`%s` plugin', username, self.name)
535 535
536 536 if self.allows_creating_users:
537 537 log.debug('Plugin `%s` allows to '
538 538 'create new users', self.name)
539 539 else:
540 540 log.debug('Plugin `%s` does not allow to '
541 541 'create new users', self.name)
542 542
543 543 user_parameters = {
544 544 'username': username,
545 545 'email': auth["email"],
546 546 'firstname': auth["firstname"],
547 547 'lastname': auth["lastname"],
548 548 'active': auth["active"],
549 549 'admin': auth["admin"],
550 550 'extern_name': auth["extern_name"],
551 551 'extern_type': self.name,
552 552 'plugin': self,
553 553 'allow_to_create_user': self.allows_creating_users,
554 554 }
555 555
556 556 if not is_user_existing:
557 557 if self.use_fake_password():
558 558 # Randomize the PW because we don't need it, but don't want
559 559 # them blank either
560 560 passwd = PasswordGenerator().gen_password(length=16)
561 561 user_parameters['password'] = passwd
562 562 else:
563 563 # Since the password is required by create_or_update method of
564 564 # UserModel, we need to set it explicitly.
565 565 # The create_or_update method is smart and recognises the
566 566 # password hashes as well.
567 567 user_parameters['password'] = cur_user.password
568 568
569 569 # we either create or update users, we also pass the flag
570 570 # that controls if this method can actually do that.
571 571 # raises NotAllowedToCreateUserError if it cannot, and we try to.
572 572 user = UserModel().create_or_update(**user_parameters)
573 573 Session().flush()
574 574 # enforce user is just in given groups, all of them has to be ones
575 575 # created from plugins. We store this info in _group_data JSON
576 576 # field
577 577
578 578 if auth['user_group_sync']:
579 579 try:
580 580 groups = auth['groups'] or []
581 581 log.debug(
582 582 'Performing user_group sync based on set `%s` '
583 583 'returned by `%s` plugin', groups, self.name)
584 584 UserGroupModel().enforce_groups(user, groups, self.name)
585 585 except Exception:
586 586 # for any reason group syncing fails, we should
587 587 # proceed with login
588 588 log.error(traceback.format_exc())
589 589
590 590 Session().commit()
591 591 return auth
592 592
593 593
594 594 class AuthLdapBase(object):
595 595
596 596 @classmethod
597 597 def _build_servers(cls, ldap_server_type, ldap_server, port, use_resolver=True):
598 598
599 599 def host_resolver(host, port, full_resolve=True):
600 600 """
601 601 Main work for this function is to prevent ldap connection issues,
602 602 and detect them early using a "greenified" sockets
603 603 """
604 604 host = host.strip()
605 605 if not full_resolve:
606 606 return '{}:{}'.format(host, port)
607 607
608 608 log.debug('LDAP: Resolving IP for LDAP host `%s`', host)
609 609 try:
610 610 ip = socket.gethostbyname(host)
611 611 log.debug('LDAP: Got LDAP host `%s` ip %s', host, ip)
612 612 except Exception:
613 613 raise LdapConnectionError('Failed to resolve host: `{}`'.format(host))
614 614
615 615 log.debug('LDAP: Checking if IP %s is accessible', ip)
616 616 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
617 617 try:
618 618 s.connect((ip, int(port)))
619 619 s.shutdown(socket.SHUT_RD)
620 620 log.debug('LDAP: connection to %s successful', ip)
621 621 except Exception:
622 622 raise LdapConnectionError(
623 623 'Failed to connect to host: `{}:{}`'.format(host, port))
624 624
625 625 return '{}:{}'.format(host, port)
626 626
627 627 if len(ldap_server) == 1:
628 628 # in case of single server use resolver to detect potential
629 629 # connection issues
630 630 full_resolve = True
631 631 else:
632 632 full_resolve = False
633 633
634 634 return ', '.join(
635 635 ["{}://{}".format(
636 636 ldap_server_type,
637 637 host_resolver(host, port, full_resolve=use_resolver and full_resolve))
638 638 for host in ldap_server])
639 639
640 640 @classmethod
641 641 def _get_server_list(cls, servers):
642 642 return map(string.strip, servers.split(','))
643 643
644 644 @classmethod
645 645 def get_uid(cls, username, server_addresses):
646 646 uid = username
647 647 for server_addr in server_addresses:
648 648 uid = chop_at(username, "@%s" % server_addr)
649 649 return uid
650 650
651 651 @classmethod
652 652 def validate_username(cls, username):
653 653 if "," in username:
654 654 raise LdapUsernameError(
655 655 "invalid character `,` in username: `{}`".format(username))
656 656
657 657 @classmethod
658 658 def validate_password(cls, username, password):
659 659 if not password:
660 660 msg = "Authenticating user %s with blank password not allowed"
661 661 log.warning(msg, username)
662 662 raise LdapPasswordError(msg)
663 663
664 664
665 665 def loadplugin(plugin_id):
666 666 """
667 667 Loads and returns an instantiated authentication plugin.
668 668 Returns the RhodeCodeAuthPluginBase subclass on success,
669 669 or None on failure.
670 670 """
671 671 # TODO: Disusing pyramids thread locals to retrieve the registry.
672 672 authn_registry = get_authn_registry()
673 673 plugin = authn_registry.get_plugin(plugin_id)
674 674 if plugin is None:
675 675 log.error('Authentication plugin not found: "%s"', plugin_id)
676 676 return plugin
677 677
678 678
679 679 def get_authn_registry(registry=None):
680 680 registry = registry or get_current_registry()
681 681 authn_registry = registry.queryUtility(IAuthnPluginRegistry)
682 682 return authn_registry
683 683
684 684
685 685 def authenticate(username, password, environ=None, auth_type=None,
686 686 skip_missing=False, registry=None, acl_repo_name=None):
687 687 """
688 688 Authentication function used for access control,
689 689 It tries to authenticate based on enabled authentication modules.
690 690
691 691 :param username: username can be empty for headers auth
692 692 :param password: password can be empty for headers auth
693 693 :param environ: environ headers passed for headers auth
694 694 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
695 695 :param skip_missing: ignores plugins that are in db but not in environment
696 696 :returns: None if auth failed, plugin_user dict if auth is correct
697 697 """
698 698 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
699 699 raise ValueError('auth type must be on of http, vcs got "%s" instead'
700 700 % auth_type)
701 701 headers_only = environ and not (username and password)
702 702
703 703 authn_registry = get_authn_registry(registry)
704 704
705 705 plugins_to_check = authn_registry.get_plugins_for_authentication()
706 706 log.debug('Starting ordered authentication chain using %s plugins',
707 707 [x.name for x in plugins_to_check])
708 708 for plugin in plugins_to_check:
709 709 plugin.set_auth_type(auth_type)
710 710 plugin.set_calling_scope_repo(acl_repo_name)
711 711
712 712 if headers_only and not plugin.is_headers_auth:
713 713 log.debug('Auth type is for headers only and plugin `%s` is not '
714 714 'headers plugin, skipping...', plugin.get_id())
715 715 continue
716 716
717 717 log.debug('Trying authentication using ** %s **', plugin.get_id())
718 718
719 719 # load plugin settings from RhodeCode database
720 720 plugin_settings = plugin.get_settings()
721 721 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
722 722 log.debug('Plugin `%s` settings:%s', plugin.get_id(), plugin_sanitized_settings)
723 723
724 724 # use plugin's method of user extraction.
725 725 user = plugin.get_user(username, environ=environ,
726 726 settings=plugin_settings)
727 727 display_user = user.username if user else username
728 728 log.debug(
729 729 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
730 730
731 731 if not plugin.allows_authentication_from(user):
732 732 log.debug('Plugin %s does not accept user `%s` for authentication',
733 733 plugin.get_id(), display_user)
734 734 continue
735 735 else:
736 736 log.debug('Plugin %s accepted user `%s` for authentication',
737 737 plugin.get_id(), display_user)
738 738
739 739 log.info('Authenticating user `%s` using %s plugin',
740 740 display_user, plugin.get_id())
741 741
742 742 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
743 743
744 744 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
745 745 plugin.get_id(), plugin_cache_active, cache_ttl)
746 746
747 747 user_id = user.user_id if user else 'no-user'
748 748 # don't cache for empty users
749 749 plugin_cache_active = plugin_cache_active and user_id
750 750 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
751 751 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
752 752
753 753 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
754 754 expiration_time=cache_ttl,
755 755 condition=plugin_cache_active)
756 756 def compute_auth(
757 757 cache_name, plugin_name, username, password):
758 758
759 759 # _authenticate is a wrapper for .auth() method of plugin.
760 760 # it checks if .auth() sends proper data.
761 761 # For RhodeCodeExternalAuthPlugin it also maps users to
762 762 # Database and maps the attributes returned from .auth()
763 763 # to RhodeCode database. If this function returns data
764 764 # then auth is correct.
765 765 log.debug('Running plugin `%s` _authenticate method '
766 766 'using username and password', plugin.get_id())
767 767 return plugin._authenticate(
768 768 user, username, password, plugin_settings,
769 769 environ=environ or {})
770 770
771 771 start = time.time()
772 772 # for environ based auth, password can be empty, but then the validation is
773 773 # on the server that fills in the env data needed for authentication
774 774 plugin_user = compute_auth('auth', plugin.name, username, (password or ''))
775 775
776 776 auth_time = time.time() - start
777 777 log.debug('Authentication for plugin `%s` completed in %.4fs, '
778 778 'expiration time of fetched cache %.1fs.',
779 plugin.get_id(), auth_time, cache_ttl)
779 plugin.get_id(), auth_time, cache_ttl,
780 extra={"plugin": plugin.get_id(), "time": auth_time})
780 781
781 782 log.debug('PLUGIN USER DATA: %s', plugin_user)
782 783
783 784 statsd = StatsdClient.statsd
784 785
785 786 if plugin_user:
786 787 log.debug('Plugin returned proper authentication data')
787 788 if statsd:
789 elapsed_time_ms = round(1000.0 * auth_time) # use ms only
788 790 statsd.incr('rhodecode_login_success_total')
791 statsd.timing("rhodecode_login_timing.histogram", elapsed_time_ms,
792 tags=["plugin:{}".format(plugin.get_id())],
793 use_decimals=False
794 )
789 795 return plugin_user
790 796
791 797 # we failed to Auth because .auth() method didn't return proper user
792 798 log.debug("User `%s` failed to authenticate against %s",
793 799 display_user, plugin.get_id())
794 800 if statsd:
795 801 statsd.incr('rhodecode_login_fail_total')
796 802
797 803 # case when we failed to authenticate against all defined plugins
798 804 return None
799 805
800 806
801 807 def chop_at(s, sub, inclusive=False):
802 808 """Truncate string ``s`` at the first occurrence of ``sub``.
803 809
804 810 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
805 811
806 812 >>> chop_at("plutocratic brats", "rat")
807 813 'plutoc'
808 814 >>> chop_at("plutocratic brats", "rat", True)
809 815 'plutocrat'
810 816 """
811 817 pos = s.find(sub)
812 818 if pos == -1:
813 819 return s
814 820 if inclusive:
815 821 return s[:pos+len(sub)]
816 822 return s[:pos]
@@ -1,231 +1,232 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2020 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, **kwargs):
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
80 80 def includeme(self, config):
81 81 config.add_authn_plugin(self)
82 82 config.add_authn_resource(self.get_id(), HeadersAuthnResource(self))
83 83 config.add_view(
84 84 'rhodecode.authentication.views.AuthnPluginViewBase',
85 85 attr='settings_get',
86 86 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
87 87 request_method='GET',
88 88 route_name='auth_home',
89 89 context=HeadersAuthnResource)
90 90 config.add_view(
91 91 'rhodecode.authentication.views.AuthnPluginViewBase',
92 92 attr='settings_post',
93 93 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
94 94 request_method='POST',
95 95 route_name='auth_home',
96 96 context=HeadersAuthnResource)
97 97
98 98 def get_display_name(self, load_from_settings=False):
99 99 return _('Headers')
100 100
101 101 def get_settings_schema(self):
102 102 return HeadersSettingsSchema()
103 103
104 104 @hybrid_property
105 105 def name(self):
106 106 return u"headers"
107 107
108 108 @property
109 109 def is_headers_auth(self):
110 110 return True
111 111
112 112 def use_fake_password(self):
113 113 return True
114 114
115 115 def user_activation_state(self):
116 116 def_user_perms = User.get_default_user().AuthUser().permissions['global']
117 117 return 'hg.extern_activate.auto' in def_user_perms
118 118
119 119 def _clean_username(self, username):
120 120 # Removing realm and domain from username
121 121 username = username.split('@')[0]
122 122 username = username.rsplit('\\')[-1]
123 123 return username
124 124
125 125 def _get_username(self, environ, settings):
126 126 username = None
127 127 environ = environ or {}
128 128 if not environ:
129 129 log.debug('got empty environ: %s', environ)
130 130
131 131 settings = settings or {}
132 132 if settings.get('header'):
133 133 header = settings.get('header')
134 134 username = environ.get(header)
135 135 log.debug('extracted %s:%s', header, username)
136 136
137 137 # fallback mode
138 138 if not username and settings.get('fallback_header'):
139 139 header = settings.get('fallback_header')
140 140 username = environ.get(header)
141 141 log.debug('extracted %s:%s', header, username)
142 142
143 143 if username and str2bool(settings.get('clean_username')):
144 144 log.debug('Received username `%s` from headers', username)
145 145 username = self._clean_username(username)
146 146 log.debug('New cleanup user is:%s', username)
147 147 return username
148 148
149 149 def get_user(self, username=None, **kwargs):
150 150 """
151 151 Helper method for user fetching in plugins, by default it's using
152 152 simple fetch by username, but this method can be custimized in plugins
153 153 eg. headers auth plugin to fetch user by environ params
154 154 :param username: username if given to fetch
155 155 :param kwargs: extra arguments needed for user fetching.
156 156 """
157 157 environ = kwargs.get('environ') or {}
158 158 settings = kwargs.get('settings') or {}
159 159 username = self._get_username(environ, settings)
160 160 # we got the username, so use default method now
161 161 return super(RhodeCodeAuthPlugin, self).get_user(username)
162 162
163 163 def auth(self, userobj, username, password, settings, **kwargs):
164 164 """
165 165 Get's the headers_auth username (or email). It tries to get username
166 166 from REMOTE_USER if this plugin is enabled, if that fails
167 167 it tries to get username from HTTP_X_FORWARDED_USER if fallback header
168 168 is set. clean_username extracts the username from this data if it's
169 169 having @ in it.
170 170 Return None on failure. On success, return a dictionary of the form:
171 171
172 172 see: RhodeCodeAuthPluginBase.auth_func_attrs
173 173
174 174 :param userobj:
175 175 :param username:
176 176 :param password:
177 177 :param settings:
178 178 :param kwargs:
179 179 """
180 180 environ = kwargs.get('environ')
181 181 if not environ:
182 182 log.debug('Empty environ data skipping...')
183 183 return None
184 184
185 185 if not userobj:
186 186 userobj = self.get_user('', environ=environ, settings=settings)
187 187
188 188 # we don't care passed username/password for headers auth plugins.
189 189 # only way to log in is using environ
190 190 username = None
191 191 if userobj:
192 192 username = getattr(userobj, 'username')
193 193
194 194 if not username:
195 195 # we don't have any objects in DB user doesn't exist extract
196 196 # username from environ based on the settings
197 197 username = self._get_username(environ, settings)
198 198
199 199 # if cannot fetch username, it's a no-go for this plugin to proceed
200 200 if not username:
201 201 return None
202 202
203 203 # old attrs fetched from RhodeCode database
204 204 admin = getattr(userobj, 'admin', False)
205 205 active = getattr(userobj, 'active', True)
206 206 email = getattr(userobj, 'email', '')
207 207 firstname = getattr(userobj, 'firstname', '')
208 208 lastname = getattr(userobj, 'lastname', '')
209 209 extern_type = getattr(userobj, 'extern_type', '')
210 210
211 211 user_attrs = {
212 212 'username': username,
213 213 'firstname': safe_unicode(firstname or username),
214 214 'lastname': safe_unicode(lastname or ''),
215 215 'groups': [],
216 216 'user_group_sync': False,
217 217 'email': email or '',
218 218 'admin': admin or False,
219 219 'active': active,
220 220 'active_from_extern': True,
221 221 'extern_name': username,
222 222 'extern_type': extern_type,
223 223 }
224 224
225 log.info('user `%s` authenticated correctly', user_attrs['username'])
225 log.info('user `%s` authenticated correctly', user_attrs['username'],
226 extra={"action": "user_auth_ok", "module": "auth_headers", "username": user_attrs["username"]})
226 227 return user_attrs
227 228
228 229
229 230 def includeme(config):
230 231 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
231 232 plugin_factory(plugin_id).includeme(config)
@@ -1,551 +1,552 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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, **kwargs):
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 scope_labels = {
73 73 ldap.SCOPE_BASE: 'SCOPE_BASE',
74 74 ldap.SCOPE_ONELEVEL: 'SCOPE_ONELEVEL',
75 75 ldap.SCOPE_SUBTREE: 'SCOPE_SUBTREE',
76 76 }
77 77
78 78 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
79 79 tls_kind='PLAIN', tls_reqcert='DEMAND', tls_cert_file=None,
80 80 tls_cert_dir=None, ldap_version=3,
81 81 search_scope='SUBTREE', attr_login='uid',
82 82 ldap_filter='', timeout=None):
83 83 if ldap == Missing:
84 84 raise LdapImportError("Missing or incompatible ldap library")
85 85
86 86 self.debug = False
87 87 self.timeout = timeout or 60 * 5
88 88 self.ldap_version = ldap_version
89 89 self.ldap_server_type = 'ldap'
90 90
91 91 self.TLS_KIND = tls_kind
92 92
93 93 if self.TLS_KIND == 'LDAPS':
94 94 port = port or 636
95 95 self.ldap_server_type += 's'
96 96
97 97 OPT_X_TLS_DEMAND = 2
98 98 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert, OPT_X_TLS_DEMAND)
99 99 self.TLS_CERT_FILE = tls_cert_file or ''
100 100 self.TLS_CERT_DIR = tls_cert_dir or self.default_tls_cert_dir
101 101
102 102 # split server into list
103 103 self.SERVER_ADDRESSES = self._get_server_list(server)
104 104 self.LDAP_SERVER_PORT = port
105 105
106 106 # USE FOR READ ONLY BIND TO LDAP SERVER
107 107 self.attr_login = attr_login
108 108
109 109 self.LDAP_BIND_DN = safe_str(bind_dn)
110 110 self.LDAP_BIND_PASS = safe_str(bind_pass)
111 111
112 112 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
113 113 self.BASE_DN = safe_str(base_dn)
114 114 self.LDAP_FILTER = safe_str(ldap_filter)
115 115
116 116 def _get_ldap_conn(self):
117 117
118 118 if self.debug:
119 119 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
120 120
121 121 if self.TLS_CERT_FILE and hasattr(ldap, 'OPT_X_TLS_CACERTFILE'):
122 122 ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, self.TLS_CERT_FILE)
123 123
124 124 elif hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
125 125 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, self.TLS_CERT_DIR)
126 126
127 127 if self.TLS_KIND != 'PLAIN':
128 128 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
129 129
130 130 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
131 131 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
132 132
133 133 # init connection now
134 134 ldap_servers = self._build_servers(
135 135 self.ldap_server_type, self.SERVER_ADDRESSES, self.LDAP_SERVER_PORT)
136 136 log.debug('initializing LDAP connection to:%s', ldap_servers)
137 137 ldap_conn = ldap.initialize(ldap_servers)
138 138 ldap_conn.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout)
139 139 ldap_conn.set_option(ldap.OPT_TIMEOUT, self.timeout)
140 140 ldap_conn.timeout = self.timeout
141 141
142 142 if self.ldap_version == 2:
143 143 ldap_conn.protocol = ldap.VERSION2
144 144 else:
145 145 ldap_conn.protocol = ldap.VERSION3
146 146
147 147 if self.TLS_KIND == 'START_TLS':
148 148 ldap_conn.start_tls_s()
149 149
150 150 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
151 151 log.debug('Trying simple_bind with password and given login DN: %r',
152 152 self.LDAP_BIND_DN)
153 153 ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
154 154 log.debug('simple_bind successful')
155 155 return ldap_conn
156 156
157 157 def fetch_attrs_from_simple_bind(self, ldap_conn, dn, username, password):
158 158 scope = ldap.SCOPE_BASE
159 159 scope_label = self.scope_labels.get(scope)
160 160 ldap_filter = '(objectClass=*)'
161 161
162 162 try:
163 163 log.debug('Trying authenticated search bind with dn: %r SCOPE: %s (and filter: %s)',
164 164 dn, scope_label, ldap_filter)
165 165 ldap_conn.simple_bind_s(dn, safe_str(password))
166 166 response = ldap_conn.search_ext_s(dn, scope, ldap_filter, attrlist=['*', '+'])
167 167
168 168 if not response:
169 169 log.error('search bind returned empty results: %r', response)
170 170 return {}
171 171 else:
172 172 _dn, attrs = response[0]
173 173 return attrs
174 174
175 175 except ldap.INVALID_CREDENTIALS:
176 176 log.debug("LDAP rejected password for user '%s': %s, org_exc:",
177 177 username, dn, exc_info=True)
178 178
179 179 def authenticate_ldap(self, username, password):
180 180 """
181 181 Authenticate a user via LDAP and return his/her LDAP properties.
182 182
183 183 Raises AuthenticationError if the credentials are rejected, or
184 184 EnvironmentError if the LDAP server can't be reached.
185 185
186 186 :param username: username
187 187 :param password: password
188 188 """
189 189
190 190 uid = self.get_uid(username, self.SERVER_ADDRESSES)
191 191 user_attrs = {}
192 192 dn = ''
193 193
194 194 self.validate_password(username, password)
195 195 self.validate_username(username)
196 196 scope_label = self.scope_labels.get(self.SEARCH_SCOPE)
197 197
198 198 ldap_conn = None
199 199 try:
200 200 ldap_conn = self._get_ldap_conn()
201 201 filter_ = '(&%s(%s=%s))' % (
202 202 self.LDAP_FILTER, self.attr_login, username)
203 203 log.debug("Authenticating %r filter %s and scope: %s",
204 204 self.BASE_DN, filter_, scope_label)
205 205
206 206 ldap_objects = ldap_conn.search_ext_s(
207 207 self.BASE_DN, self.SEARCH_SCOPE, filter_, attrlist=['*', '+'])
208 208
209 209 if not ldap_objects:
210 210 log.debug("No matching LDAP objects for authentication "
211 211 "of UID:'%s' username:(%s)", uid, username)
212 212 raise ldap.NO_SUCH_OBJECT()
213 213
214 214 log.debug('Found %s matching ldap object[s], trying to authenticate on each one now...', len(ldap_objects))
215 215 for (dn, _attrs) in ldap_objects:
216 216 if dn is None:
217 217 continue
218 218
219 219 user_attrs = self.fetch_attrs_from_simple_bind(
220 220 ldap_conn, dn, username, password)
221 221
222 222 if user_attrs:
223 223 log.debug('Got authenticated user attributes from DN:%s', dn)
224 224 break
225 225 else:
226 226 raise LdapPasswordError(
227 227 'Failed to authenticate user `{}` with given password'.format(username))
228 228
229 229 except ldap.NO_SUCH_OBJECT:
230 230 log.debug("LDAP says no such user '%s' (%s), org_exc:",
231 231 uid, username, exc_info=True)
232 232 raise LdapUsernameError('Unable to find user')
233 233 except ldap.SERVER_DOWN:
234 234 org_exc = traceback.format_exc()
235 235 raise LdapConnectionError(
236 236 "LDAP can't access authentication server, org_exc:%s" % org_exc)
237 237 finally:
238 238 if ldap_conn:
239 239 log.debug('ldap: connection release')
240 240 try:
241 241 ldap_conn.unbind_s()
242 242 except Exception:
243 243 # for any reason this can raise exception we must catch it
244 244 # to not crush the server
245 245 pass
246 246
247 247 return dn, user_attrs
248 248
249 249
250 250 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
251 251 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
252 252 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
253 253 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
254 254
255 255 host = colander.SchemaNode(
256 256 colander.String(),
257 257 default='',
258 258 description=_('Host[s] of the LDAP Server \n'
259 259 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
260 260 'Multiple servers can be specified using commas'),
261 261 preparer=strip_whitespace,
262 262 title=_('LDAP Host'),
263 263 widget='string')
264 264 port = colander.SchemaNode(
265 265 colander.Int(),
266 266 default=389,
267 267 description=_('Custom port that the LDAP server is listening on. '
268 268 'Default value is: 389, use 636 for LDAPS (SSL)'),
269 269 preparer=strip_whitespace,
270 270 title=_('Port'),
271 271 validator=colander.Range(min=0, max=65536),
272 272 widget='int')
273 273
274 274 timeout = colander.SchemaNode(
275 275 colander.Int(),
276 276 default=60 * 5,
277 277 description=_('Timeout for LDAP connection'),
278 278 preparer=strip_whitespace,
279 279 title=_('Connection timeout'),
280 280 validator=colander.Range(min=1),
281 281 widget='int')
282 282
283 283 dn_user = colander.SchemaNode(
284 284 colander.String(),
285 285 default='',
286 286 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
287 287 'e.g., cn=admin,dc=mydomain,dc=com, or '
288 288 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
289 289 missing='',
290 290 preparer=strip_whitespace,
291 291 title=_('Bind account'),
292 292 widget='string')
293 293 dn_pass = colander.SchemaNode(
294 294 colander.String(),
295 295 default='',
296 296 description=_('Password to authenticate for given user DN.'),
297 297 missing='',
298 298 preparer=strip_whitespace,
299 299 title=_('Bind account password'),
300 300 widget='password')
301 301 tls_kind = colander.SchemaNode(
302 302 colander.String(),
303 303 default=tls_kind_choices[0],
304 304 description=_('TLS Type'),
305 305 title=_('Connection Security'),
306 306 validator=colander.OneOf(tls_kind_choices),
307 307 widget='select')
308 308 tls_reqcert = colander.SchemaNode(
309 309 colander.String(),
310 310 default=tls_reqcert_choices[0],
311 311 description=_('Require Cert over TLS?. Self-signed and custom '
312 312 'certificates can be used when\n `RhodeCode Certificate` '
313 313 'found in admin > settings > system info page is extended.'),
314 314 title=_('Certificate Checks'),
315 315 validator=colander.OneOf(tls_reqcert_choices),
316 316 widget='select')
317 317 tls_cert_file = colander.SchemaNode(
318 318 colander.String(),
319 319 default='',
320 320 description=_('This specifies the PEM-format file path containing '
321 321 'certificates for use in TLS connection.\n'
322 322 'If not specified `TLS Cert dir` will be used'),
323 323 title=_('TLS Cert file'),
324 324 missing='',
325 325 widget='string')
326 326 tls_cert_dir = colander.SchemaNode(
327 327 colander.String(),
328 328 default=AuthLdap.default_tls_cert_dir,
329 329 description=_('This specifies the path of a directory that contains individual '
330 330 'CA certificates in separate files.'),
331 331 title=_('TLS Cert dir'),
332 332 widget='string')
333 333 base_dn = colander.SchemaNode(
334 334 colander.String(),
335 335 default='',
336 336 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
337 337 'in it to be replaced with current user username \n'
338 338 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
339 339 missing='',
340 340 preparer=strip_whitespace,
341 341 title=_('Base DN'),
342 342 widget='string')
343 343 filter = colander.SchemaNode(
344 344 colander.String(),
345 345 default='',
346 346 description=_('Filter to narrow results \n'
347 347 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
348 348 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
349 349 missing='',
350 350 preparer=strip_whitespace,
351 351 title=_('LDAP Search Filter'),
352 352 widget='string')
353 353
354 354 search_scope = colander.SchemaNode(
355 355 colander.String(),
356 356 default=search_scope_choices[2],
357 357 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
358 358 title=_('LDAP Search Scope'),
359 359 validator=colander.OneOf(search_scope_choices),
360 360 widget='select')
361 361 attr_login = colander.SchemaNode(
362 362 colander.String(),
363 363 default='uid',
364 364 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
365 365 preparer=strip_whitespace,
366 366 title=_('Login Attribute'),
367 367 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
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 attr_firstname = colander.SchemaNode(
380 380 colander.String(),
381 381 default='',
382 382 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
383 383 missing='',
384 384 preparer=strip_whitespace,
385 385 title=_('First Name Attribute'),
386 386 widget='string')
387 387 attr_lastname = colander.SchemaNode(
388 388 colander.String(),
389 389 default='',
390 390 description=_('LDAP Attribute to map to last name (e.g., sn)'),
391 391 missing='',
392 392 preparer=strip_whitespace,
393 393 title=_('Last Name Attribute'),
394 394 widget='string')
395 395
396 396
397 397 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
398 398 uid = 'ldap'
399 399 # used to define dynamic binding in the
400 400 DYNAMIC_BIND_VAR = '$login'
401 401 _settings_unsafe_keys = ['dn_pass']
402 402
403 403 def includeme(self, config):
404 404 config.add_authn_plugin(self)
405 405 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
406 406 config.add_view(
407 407 'rhodecode.authentication.views.AuthnPluginViewBase',
408 408 attr='settings_get',
409 409 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
410 410 request_method='GET',
411 411 route_name='auth_home',
412 412 context=LdapAuthnResource)
413 413 config.add_view(
414 414 'rhodecode.authentication.views.AuthnPluginViewBase',
415 415 attr='settings_post',
416 416 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
417 417 request_method='POST',
418 418 route_name='auth_home',
419 419 context=LdapAuthnResource)
420 420
421 421 def get_settings_schema(self):
422 422 return LdapSettingsSchema()
423 423
424 424 def get_display_name(self, load_from_settings=False):
425 425 return _('LDAP')
426 426
427 427 @classmethod
428 428 def docs(cls):
429 429 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-ldap.html"
430 430
431 431 @hybrid_property
432 432 def name(self):
433 433 return u"ldap"
434 434
435 435 def use_fake_password(self):
436 436 return True
437 437
438 438 def user_activation_state(self):
439 439 def_user_perms = User.get_default_user().AuthUser().permissions['global']
440 440 return 'hg.extern_activate.auto' in def_user_perms
441 441
442 442 def try_dynamic_binding(self, username, password, current_args):
443 443 """
444 444 Detects marker inside our original bind, and uses dynamic auth if
445 445 present
446 446 """
447 447
448 448 org_bind = current_args['bind_dn']
449 449 passwd = current_args['bind_pass']
450 450
451 451 def has_bind_marker(username):
452 452 if self.DYNAMIC_BIND_VAR in username:
453 453 return True
454 454
455 455 # we only passed in user with "special" variable
456 456 if org_bind and has_bind_marker(org_bind) and not passwd:
457 457 log.debug('Using dynamic user/password binding for ldap '
458 458 'authentication. Replacing `%s` with username',
459 459 self.DYNAMIC_BIND_VAR)
460 460 current_args['bind_dn'] = org_bind.replace(
461 461 self.DYNAMIC_BIND_VAR, username)
462 462 current_args['bind_pass'] = password
463 463
464 464 return current_args
465 465
466 466 def auth(self, userobj, username, password, settings, **kwargs):
467 467 """
468 468 Given a user object (which may be null), username, a plaintext password,
469 469 and a settings object (containing all the keys needed as listed in
470 470 settings()), authenticate this user's login attempt.
471 471
472 472 Return None on failure. On success, return a dictionary of the form:
473 473
474 474 see: RhodeCodeAuthPluginBase.auth_func_attrs
475 475 This is later validated for correctness
476 476 """
477 477
478 478 if not username or not password:
479 479 log.debug('Empty username or password skipping...')
480 480 return None
481 481
482 482 ldap_args = {
483 483 'server': settings.get('host', ''),
484 484 'base_dn': settings.get('base_dn', ''),
485 485 'port': settings.get('port'),
486 486 'bind_dn': settings.get('dn_user'),
487 487 'bind_pass': settings.get('dn_pass'),
488 488 'tls_kind': settings.get('tls_kind'),
489 489 'tls_reqcert': settings.get('tls_reqcert'),
490 490 'tls_cert_file': settings.get('tls_cert_file'),
491 491 'tls_cert_dir': settings.get('tls_cert_dir'),
492 492 'search_scope': settings.get('search_scope'),
493 493 'attr_login': settings.get('attr_login'),
494 494 'ldap_version': 3,
495 495 'ldap_filter': settings.get('filter'),
496 496 'timeout': settings.get('timeout')
497 497 }
498 498
499 499 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
500 500
501 501 log.debug('Checking for ldap authentication.')
502 502
503 503 try:
504 504 aldap = AuthLdap(**ldap_args)
505 505 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
506 506 log.debug('Got ldap DN response %s', user_dn)
507 507
508 508 def get_ldap_attr(k):
509 509 return ldap_attrs.get(settings.get(k), [''])[0]
510 510
511 511 # old attrs fetched from RhodeCode database
512 512 admin = getattr(userobj, 'admin', False)
513 513 active = getattr(userobj, 'active', True)
514 514 email = getattr(userobj, 'email', '')
515 515 username = getattr(userobj, 'username', username)
516 516 firstname = getattr(userobj, 'firstname', '')
517 517 lastname = getattr(userobj, 'lastname', '')
518 518 extern_type = getattr(userobj, 'extern_type', '')
519 519
520 520 groups = []
521 521
522 522 user_attrs = {
523 523 'username': username,
524 524 'firstname': safe_unicode(get_ldap_attr('attr_firstname') or firstname),
525 525 'lastname': safe_unicode(get_ldap_attr('attr_lastname') or lastname),
526 526 'groups': groups,
527 527 'user_group_sync': False,
528 528 'email': get_ldap_attr('attr_email') or email,
529 529 'admin': admin,
530 530 'active': active,
531 531 'active_from_extern': None,
532 532 'extern_name': user_dn,
533 533 'extern_type': extern_type,
534 534 }
535 535
536 536 log.debug('ldap user: %s', user_attrs)
537 log.info('user `%s` authenticated correctly', user_attrs['username'])
537 log.info('user `%s` authenticated correctly', user_attrs['username'],
538 extra={"action": "user_auth_ok", "module": "auth_ldap", "username": user_attrs["username"]})
538 539
539 540 return user_attrs
540 541
541 542 except (LdapUsernameError, LdapPasswordError, LdapImportError):
542 543 log.exception("LDAP related exception")
543 544 return None
544 545 except (Exception,):
545 546 log.exception("Other exception")
546 547 return None
547 548
548 549
549 550 def includeme(config):
550 551 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
551 552 plugin_factory(plugin_id).includeme(config)
@@ -1,171 +1,172 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2020 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, **kwargs):
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 75 uid = 'pam'
76 76 # PAM authentication can be slow. Repository operations involve a lot of
77 77 # auth calls. Little caching helps speedup push/pull operations significantly
78 78 AUTH_CACHE_TTL = 4
79 79
80 80 def includeme(self, config):
81 81 config.add_authn_plugin(self)
82 82 config.add_authn_resource(self.get_id(), PamAuthnResource(self))
83 83 config.add_view(
84 84 'rhodecode.authentication.views.AuthnPluginViewBase',
85 85 attr='settings_get',
86 86 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
87 87 request_method='GET',
88 88 route_name='auth_home',
89 89 context=PamAuthnResource)
90 90 config.add_view(
91 91 'rhodecode.authentication.views.AuthnPluginViewBase',
92 92 attr='settings_post',
93 93 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
94 94 request_method='POST',
95 95 route_name='auth_home',
96 96 context=PamAuthnResource)
97 97
98 98 def get_display_name(self, load_from_settings=False):
99 99 return _('PAM')
100 100
101 101 @classmethod
102 102 def docs(cls):
103 103 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-pam.html"
104 104
105 105 @hybrid_property
106 106 def name(self):
107 107 return u"pam"
108 108
109 109 def get_settings_schema(self):
110 110 return PamSettingsSchema()
111 111
112 112 def use_fake_password(self):
113 113 return True
114 114
115 115 def auth(self, userobj, username, password, settings, **kwargs):
116 116 if not username or not password:
117 117 log.debug('Empty username or password skipping...')
118 118 return None
119 119 _pam = pam.pam()
120 120 auth_result = _pam.authenticate(username, password, settings["service"])
121 121
122 122 if not auth_result:
123 123 log.error("PAM was unable to authenticate user: %s", username)
124 124 return None
125 125
126 126 log.debug('Got PAM response %s', auth_result)
127 127
128 128 # old attrs fetched from RhodeCode database
129 129 default_email = "%s@%s" % (username, socket.gethostname())
130 130 admin = getattr(userobj, 'admin', False)
131 131 active = getattr(userobj, 'active', True)
132 132 email = getattr(userobj, 'email', '') or default_email
133 133 username = getattr(userobj, 'username', username)
134 134 firstname = getattr(userobj, 'firstname', '')
135 135 lastname = getattr(userobj, 'lastname', '')
136 136 extern_type = getattr(userobj, 'extern_type', '')
137 137
138 138 user_attrs = {
139 139 'username': username,
140 140 'firstname': firstname,
141 141 'lastname': lastname,
142 142 'groups': [g.gr_name for g in grp.getgrall()
143 143 if username in g.gr_mem],
144 144 'user_group_sync': True,
145 145 'email': email,
146 146 'admin': admin,
147 147 'active': active,
148 148 'active_from_extern': None,
149 149 'extern_name': username,
150 150 'extern_type': extern_type,
151 151 }
152 152
153 153 try:
154 154 user_data = pwd.getpwnam(username)
155 155 regex = settings["gecos"]
156 156 match = re.search(regex, user_data.pw_gecos)
157 157 if match:
158 158 user_attrs["firstname"] = match.group('first_name')
159 159 user_attrs["lastname"] = match.group('last_name')
160 160 except Exception:
161 161 log.warning("Cannot extract additional info for PAM user")
162 162 pass
163 163
164 164 log.debug("pamuser: %s", user_attrs)
165 log.info('user `%s` authenticated correctly', user_attrs['username'])
165 log.info('user `%s` authenticated correctly', user_attrs['username'],
166 extra={"action": "user_auth_ok", "module": "auth_pam", "username": user_attrs["username"]})
166 167 return user_attrs
167 168
168 169
169 170 def includeme(config):
170 171 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
171 172 plugin_factory(plugin_id).includeme(config)
@@ -1,220 +1,222 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2020 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 import colander
28 28
29 29 from rhodecode.translation import _
30 30 from rhodecode.lib.utils2 import safe_str
31 31 from rhodecode.model.db import User
32 32 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
33 33 from rhodecode.authentication.base import (
34 34 RhodeCodeAuthPluginBase, hybrid_property, HTTP_TYPE, VCS_TYPE)
35 35 from rhodecode.authentication.routes import AuthnPluginResourceBase
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 def plugin_factory(plugin_id, *args, **kwargs):
41 41 plugin = RhodeCodeAuthPlugin(plugin_id)
42 42 return plugin
43 43
44 44
45 45 class RhodecodeAuthnResource(AuthnPluginResourceBase):
46 46 pass
47 47
48 48
49 49 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
50 50 uid = 'rhodecode'
51 51 AUTH_RESTRICTION_NONE = 'user_all'
52 52 AUTH_RESTRICTION_SUPER_ADMIN = 'user_super_admin'
53 53 AUTH_RESTRICTION_SCOPE_ALL = 'scope_all'
54 54 AUTH_RESTRICTION_SCOPE_HTTP = 'scope_http'
55 55 AUTH_RESTRICTION_SCOPE_VCS = 'scope_vcs'
56 56
57 57 def includeme(self, config):
58 58 config.add_authn_plugin(self)
59 59 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
60 60 config.add_view(
61 61 'rhodecode.authentication.views.AuthnPluginViewBase',
62 62 attr='settings_get',
63 63 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
64 64 request_method='GET',
65 65 route_name='auth_home',
66 66 context=RhodecodeAuthnResource)
67 67 config.add_view(
68 68 'rhodecode.authentication.views.AuthnPluginViewBase',
69 69 attr='settings_post',
70 70 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
71 71 request_method='POST',
72 72 route_name='auth_home',
73 73 context=RhodecodeAuthnResource)
74 74
75 75 def get_settings_schema(self):
76 76 return RhodeCodeSettingsSchema()
77 77
78 78 def get_display_name(self, load_from_settings=False):
79 79 return _('RhodeCode Internal')
80 80
81 81 @classmethod
82 82 def docs(cls):
83 83 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth.html"
84 84
85 85 @hybrid_property
86 86 def name(self):
87 87 return u"rhodecode"
88 88
89 89 def user_activation_state(self):
90 90 def_user_perms = User.get_default_user().AuthUser().permissions['global']
91 91 return 'hg.register.auto_activate' in def_user_perms
92 92
93 93 def allows_authentication_from(
94 94 self, user, allows_non_existing_user=True,
95 95 allowed_auth_plugins=None, allowed_auth_sources=None):
96 96 """
97 97 Custom method for this auth that doesn't accept non existing users.
98 98 We know that user exists in our database.
99 99 """
100 100 allows_non_existing_user = False
101 101 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
102 102 user, allows_non_existing_user=allows_non_existing_user)
103 103
104 104 def auth(self, userobj, username, password, settings, **kwargs):
105 105 if not userobj:
106 106 log.debug('userobj was:%s skipping', userobj)
107 107 return None
108 108
109 109 if userobj.extern_type != self.name:
110 110 log.warning("userobj:%s extern_type mismatch got:`%s` expected:`%s`",
111 111 userobj, userobj.extern_type, self.name)
112 112 return None
113 113
114 114 # check scope of auth
115 115 scope_restriction = settings.get('scope_restriction', '')
116 116
117 117 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_HTTP \
118 118 and self.auth_type != HTTP_TYPE:
119 119 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
120 120 userobj, self.auth_type, scope_restriction)
121 121 return None
122 122
123 123 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_VCS \
124 124 and self.auth_type != VCS_TYPE:
125 125 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
126 126 userobj, self.auth_type, scope_restriction)
127 127 return None
128 128
129 129 # check super-admin restriction
130 130 auth_restriction = settings.get('auth_restriction', '')
131 131
132 132 if auth_restriction == self.AUTH_RESTRICTION_SUPER_ADMIN \
133 133 and userobj.admin is False:
134 134 log.warning("userobj:%s is not super-admin and auth restriction is set to %s",
135 135 userobj, auth_restriction)
136 136 return None
137 137
138 138 user_attrs = {
139 139 "username": userobj.username,
140 140 "firstname": userobj.firstname,
141 141 "lastname": userobj.lastname,
142 142 "groups": [],
143 143 'user_group_sync': False,
144 144 "email": userobj.email,
145 145 "admin": userobj.admin,
146 146 "active": userobj.active,
147 147 "active_from_extern": userobj.active,
148 148 "extern_name": userobj.user_id,
149 149 "extern_type": userobj.extern_type,
150 150 }
151 151
152 152 log.debug("User attributes:%s", user_attrs)
153 153 if userobj.active:
154 154 from rhodecode.lib import auth
155 155 crypto_backend = auth.crypto_backend()
156 156 password_encoded = safe_str(password)
157 157 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
158 158 password_encoded, userobj.password or '')
159 159
160 160 if password_match and new_hash:
161 161 log.debug('user %s properly authenticated, but '
162 162 'requires hash change to bcrypt', userobj)
163 163 # if password match, and we use OLD deprecated hash,
164 164 # we should migrate this user hash password to the new hash
165 165 # we store the new returned by hash_check_with_upgrade function
166 166 user_attrs['_hash_migrate'] = new_hash
167 167
168 168 if userobj.username == User.DEFAULT_USER and userobj.active:
169 169 log.info('user `%s` authenticated correctly as anonymous user',
170 userobj.username)
170 userobj.username,
171 extra={"action": "user_auth_ok", "module": "auth_rhodecode_anon", "username": userobj.username})
171 172 return user_attrs
172 173
173 174 elif userobj.username == username and password_match:
174 log.info('user `%s` authenticated correctly', userobj.username)
175 log.info('user `%s` authenticated correctly', userobj.username,
176 extra={"action": "user_auth_ok", "module": "auth_rhodecode", "username": userobj.username})
175 177 return user_attrs
176 178 log.warning("user `%s` used a wrong password when "
177 179 "authenticating on this plugin", userobj.username)
178 180 return None
179 181 else:
180 182 log.warning('user `%s` failed to authenticate via %s, reason: account not '
181 183 'active.', username, self.name)
182 184 return None
183 185
184 186
185 187 class RhodeCodeSettingsSchema(AuthnPluginSettingsSchemaBase):
186 188
187 189 auth_restriction_choices = [
188 190 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_NONE, 'All users'),
189 191 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN, 'Super admins only'),
190 192 ]
191 193
192 194 auth_scope_choices = [
193 195 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_ALL, 'HTTP and VCS'),
194 196 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_HTTP, 'HTTP only'),
195 197 ]
196 198
197 199 auth_restriction = colander.SchemaNode(
198 200 colander.String(),
199 201 default=auth_restriction_choices[0],
200 202 description=_('Allowed user types for authentication using this plugin.'),
201 203 title=_('User restriction'),
202 204 validator=colander.OneOf([x[0] for x in auth_restriction_choices]),
203 205 widget='select_with_labels',
204 206 choices=auth_restriction_choices
205 207 )
206 208 scope_restriction = colander.SchemaNode(
207 209 colander.String(),
208 210 default=auth_scope_choices[0],
209 211 description=_('Allowed protocols for authentication using this plugin. '
210 212 'VCS means GIT/HG/SVN. HTTP is web based login.'),
211 213 title=_('Scope restriction'),
212 214 validator=colander.OneOf([x[0] for x in auth_scope_choices]),
213 215 widget='select_with_labels',
214 216 choices=auth_scope_choices
215 217 )
216 218
217 219
218 220 def includeme(config):
219 221 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
220 222 plugin_factory(plugin_id).includeme(config)
@@ -1,390 +1,390 b''
1 1 import sys
2 2 import threading
3 3 import weakref
4 4 from base64 import b64encode
5 5 from logging import getLogger
6 6 from os import urandom
7 7
8 8 from redis import StrictRedis
9 9
10 10 __version__ = '3.7.0'
11 11
12 12 loggers = {
13 13 k: getLogger("rhodecode." + ".".join((__name__, k)))
14 14 for k in [
15 15 "acquire",
16 16 "refresh.thread.start",
17 17 "refresh.thread.stop",
18 18 "refresh.thread.exit",
19 19 "refresh.start",
20 20 "refresh.shutdown",
21 21 "refresh.exit",
22 22 "release",
23 23 ]
24 24 }
25 25
26 26 PY3 = sys.version_info[0] == 3
27 27
28 28 if PY3:
29 29 text_type = str
30 30 binary_type = bytes
31 31 else:
32 32 text_type = unicode # noqa
33 33 binary_type = str
34 34
35 35
36 36 # Check if the id match. If not, return an error code.
37 37 UNLOCK_SCRIPT = b"""
38 38 if redis.call("get", KEYS[1]) ~= ARGV[1] then
39 39 return 1
40 40 else
41 41 redis.call("del", KEYS[2])
42 42 redis.call("lpush", KEYS[2], 1)
43 43 redis.call("pexpire", KEYS[2], ARGV[2])
44 44 redis.call("del", KEYS[1])
45 45 return 0
46 46 end
47 47 """
48 48
49 49 # Covers both cases when key doesn't exist and doesn't equal to lock's id
50 50 EXTEND_SCRIPT = b"""
51 51 if redis.call("get", KEYS[1]) ~= ARGV[1] then
52 52 return 1
53 53 elseif redis.call("ttl", KEYS[1]) < 0 then
54 54 return 2
55 55 else
56 56 redis.call("expire", KEYS[1], ARGV[2])
57 57 return 0
58 58 end
59 59 """
60 60
61 61 RESET_SCRIPT = b"""
62 62 redis.call('del', KEYS[2])
63 63 redis.call('lpush', KEYS[2], 1)
64 64 redis.call('pexpire', KEYS[2], ARGV[2])
65 65 return redis.call('del', KEYS[1])
66 66 """
67 67
68 68 RESET_ALL_SCRIPT = b"""
69 69 local locks = redis.call('keys', 'lock:*')
70 70 local signal
71 71 for _, lock in pairs(locks) do
72 72 signal = 'lock-signal:' .. string.sub(lock, 6)
73 73 redis.call('del', signal)
74 74 redis.call('lpush', signal, 1)
75 75 redis.call('expire', signal, 1)
76 76 redis.call('del', lock)
77 77 end
78 78 return #locks
79 79 """
80 80
81 81
82 82 class AlreadyAcquired(RuntimeError):
83 83 pass
84 84
85 85
86 86 class NotAcquired(RuntimeError):
87 87 pass
88 88
89 89
90 90 class AlreadyStarted(RuntimeError):
91 91 pass
92 92
93 93
94 94 class TimeoutNotUsable(RuntimeError):
95 95 pass
96 96
97 97
98 98 class InvalidTimeout(RuntimeError):
99 99 pass
100 100
101 101
102 102 class TimeoutTooLarge(RuntimeError):
103 103 pass
104 104
105 105
106 106 class NotExpirable(RuntimeError):
107 107 pass
108 108
109 109
110 110 class Lock(object):
111 111 """
112 112 A Lock context manager implemented via redis SETNX/BLPOP.
113 113 """
114 114 unlock_script = None
115 115 extend_script = None
116 116 reset_script = None
117 117 reset_all_script = None
118 118
119 119 def __init__(self, redis_client, name, expire=None, id=None, auto_renewal=False, strict=True, signal_expire=1000):
120 120 """
121 121 :param redis_client:
122 122 An instance of :class:`~StrictRedis`.
123 123 :param name:
124 124 The name (redis key) the lock should have.
125 125 :param expire:
126 126 The lock expiry time in seconds. If left at the default (None)
127 127 the lock will not expire.
128 128 :param id:
129 129 The ID (redis value) the lock should have. A random value is
130 130 generated when left at the default.
131 131
132 132 Note that if you specify this then the lock is marked as "held". Acquires
133 133 won't be possible.
134 134 :param auto_renewal:
135 135 If set to ``True``, Lock will automatically renew the lock so that it
136 136 doesn't expire for as long as the lock is held (acquire() called
137 137 or running in a context manager).
138 138
139 139 Implementation note: Renewal will happen using a daemon thread with
140 140 an interval of ``expire*2/3``. If wishing to use a different renewal
141 141 time, subclass Lock, call ``super().__init__()`` then set
142 142 ``self._lock_renewal_interval`` to your desired interval.
143 143 :param strict:
144 144 If set ``True`` then the ``redis_client`` needs to be an instance of ``redis.StrictRedis``.
145 145 :param signal_expire:
146 146 Advanced option to override signal list expiration in milliseconds. Increase it for very slow clients. Default: ``1000``.
147 147 """
148 148 if strict and not isinstance(redis_client, StrictRedis):
149 149 raise ValueError("redis_client must be instance of StrictRedis. "
150 150 "Use strict=False if you know what you're doing.")
151 151 if auto_renewal and expire is None:
152 152 raise ValueError("Expire may not be None when auto_renewal is set")
153 153
154 154 self._client = redis_client
155 155
156 156 if expire:
157 157 expire = int(expire)
158 158 if expire < 0:
159 159 raise ValueError("A negative expire is not acceptable.")
160 160 else:
161 161 expire = None
162 162 self._expire = expire
163 163
164 164 self._signal_expire = signal_expire
165 165 if id is None:
166 166 self._id = b64encode(urandom(18)).decode('ascii')
167 167 elif isinstance(id, binary_type):
168 168 try:
169 169 self._id = id.decode('ascii')
170 170 except UnicodeDecodeError:
171 171 self._id = b64encode(id).decode('ascii')
172 172 elif isinstance(id, text_type):
173 173 self._id = id
174 174 else:
175 175 raise TypeError("Incorrect type for `id`. Must be bytes/str not %s." % type(id))
176 176 self._name = 'lock:' + name
177 177 self._signal = 'lock-signal:' + name
178 178 self._lock_renewal_interval = (float(expire) * 2 / 3
179 179 if auto_renewal
180 180 else None)
181 181 self._lock_renewal_thread = None
182 182
183 183 self.register_scripts(redis_client)
184 184
185 185 @classmethod
186 186 def register_scripts(cls, redis_client):
187 187 global reset_all_script
188 188 if reset_all_script is None:
189 189 reset_all_script = redis_client.register_script(RESET_ALL_SCRIPT)
190 190 cls.unlock_script = redis_client.register_script(UNLOCK_SCRIPT)
191 191 cls.extend_script = redis_client.register_script(EXTEND_SCRIPT)
192 192 cls.reset_script = redis_client.register_script(RESET_SCRIPT)
193 193 cls.reset_all_script = redis_client.register_script(RESET_ALL_SCRIPT)
194 194
195 195 @property
196 196 def _held(self):
197 197 return self.id == self.get_owner_id()
198 198
199 199 def reset(self):
200 200 """
201 201 Forcibly deletes the lock. Use this with care.
202 202 """
203 203 self.reset_script(client=self._client, keys=(self._name, self._signal), args=(self.id, self._signal_expire))
204 204
205 205 @property
206 206 def id(self):
207 207 return self._id
208 208
209 209 def get_owner_id(self):
210 210 owner_id = self._client.get(self._name)
211 211 if isinstance(owner_id, binary_type):
212 212 owner_id = owner_id.decode('ascii', 'replace')
213 213 return owner_id
214 214
215 215 def acquire(self, blocking=True, timeout=None):
216 216 """
217 217 :param blocking:
218 218 Boolean value specifying whether lock should be blocking or not.
219 219 :param timeout:
220 220 An integer value specifying the maximum number of seconds to block.
221 221 """
222 222 logger = loggers["acquire"]
223 223
224 224 logger.debug("Getting blocking: %s acquire on %r ...", blocking, self._name)
225 225
226 226 if self._held:
227 227 owner_id = self.get_owner_id()
228 228 raise AlreadyAcquired("Already acquired from this Lock instance. Lock id: {}".format(owner_id))
229 229
230 230 if not blocking and timeout is not None:
231 231 raise TimeoutNotUsable("Timeout cannot be used if blocking=False")
232 232
233 233 if timeout:
234 234 timeout = int(timeout)
235 235 if timeout < 0:
236 236 raise InvalidTimeout("Timeout (%d) cannot be less than or equal to 0" % timeout)
237 237
238 238 if self._expire and not self._lock_renewal_interval and timeout > self._expire:
239 239 raise TimeoutTooLarge("Timeout (%d) cannot be greater than expire (%d)" % (timeout, self._expire))
240 240
241 241 busy = True
242 242 blpop_timeout = timeout or self._expire or 0
243 243 timed_out = False
244 244 while busy:
245 245 busy = not self._client.set(self._name, self._id, nx=True, ex=self._expire)
246 246 if busy:
247 247 if timed_out:
248 248 return False
249 249 elif blocking:
250 250 timed_out = not self._client.blpop(self._signal, blpop_timeout) and timeout
251 251 else:
252 252 logger.warning("Failed to get %r.", self._name)
253 253 return False
254 254
255 logger.info("Got lock for %r.", self._name)
255 logger.debug("Got lock for %r.", self._name)
256 256 if self._lock_renewal_interval is not None:
257 257 self._start_lock_renewer()
258 258 return True
259 259
260 260 def extend(self, expire=None):
261 261 """Extends expiration time of the lock.
262 262
263 263 :param expire:
264 264 New expiration time. If ``None`` - `expire` provided during
265 265 lock initialization will be taken.
266 266 """
267 267 if expire:
268 268 expire = int(expire)
269 269 if expire < 0:
270 270 raise ValueError("A negative expire is not acceptable.")
271 271 elif self._expire is not None:
272 272 expire = self._expire
273 273 else:
274 274 raise TypeError(
275 275 "To extend a lock 'expire' must be provided as an "
276 276 "argument to extend() method or at initialization time."
277 277 )
278 278
279 279 error = self.extend_script(client=self._client, keys=(self._name, self._signal), args=(self._id, expire))
280 280 if error == 1:
281 281 raise NotAcquired("Lock %s is not acquired or it already expired." % self._name)
282 282 elif error == 2:
283 283 raise NotExpirable("Lock %s has no assigned expiration time" % self._name)
284 284 elif error:
285 285 raise RuntimeError("Unsupported error code %s from EXTEND script" % error)
286 286
287 287 @staticmethod
288 288 def _lock_renewer(lockref, interval, stop):
289 289 """
290 290 Renew the lock key in redis every `interval` seconds for as long
291 291 as `self._lock_renewal_thread.should_exit` is False.
292 292 """
293 293 while not stop.wait(timeout=interval):
294 294 loggers["refresh.thread.start"].debug("Refreshing lock")
295 295 lock = lockref()
296 296 if lock is None:
297 297 loggers["refresh.thread.stop"].debug(
298 298 "The lock no longer exists, stopping lock refreshing"
299 299 )
300 300 break
301 301 lock.extend(expire=lock._expire)
302 302 del lock
303 303 loggers["refresh.thread.exit"].debug("Exit requested, stopping lock refreshing")
304 304
305 305 def _start_lock_renewer(self):
306 306 """
307 307 Starts the lock refresher thread.
308 308 """
309 309 if self._lock_renewal_thread is not None:
310 310 raise AlreadyStarted("Lock refresh thread already started")
311 311
312 312 loggers["refresh.start"].debug(
313 313 "Starting thread to refresh lock every %s seconds",
314 314 self._lock_renewal_interval
315 315 )
316 316 self._lock_renewal_stop = threading.Event()
317 317 self._lock_renewal_thread = threading.Thread(
318 318 group=None,
319 319 target=self._lock_renewer,
320 320 kwargs={'lockref': weakref.ref(self),
321 321 'interval': self._lock_renewal_interval,
322 322 'stop': self._lock_renewal_stop}
323 323 )
324 324 self._lock_renewal_thread.setDaemon(True)
325 325 self._lock_renewal_thread.start()
326 326
327 327 def _stop_lock_renewer(self):
328 328 """
329 329 Stop the lock renewer.
330 330
331 331 This signals the renewal thread and waits for its exit.
332 332 """
333 333 if self._lock_renewal_thread is None or not self._lock_renewal_thread.is_alive():
334 334 return
335 335 loggers["refresh.shutdown"].debug("Signalling the lock refresher to stop")
336 336 self._lock_renewal_stop.set()
337 337 self._lock_renewal_thread.join()
338 338 self._lock_renewal_thread = None
339 339 loggers["refresh.exit"].debug("Lock refresher has stopped")
340 340
341 341 def __enter__(self):
342 342 acquired = self.acquire(blocking=True)
343 343 assert acquired, "Lock wasn't acquired, but blocking=True"
344 344 return self
345 345
346 346 def __exit__(self, exc_type=None, exc_value=None, traceback=None):
347 347 self.release()
348 348
349 349 def release(self):
350 350 """Releases the lock, that was acquired with the same object.
351 351
352 352 .. note::
353 353
354 354 If you want to release a lock that you acquired in a different place you have two choices:
355 355
356 356 * Use ``Lock("name", id=id_from_other_place).release()``
357 357 * Use ``Lock("name").reset()``
358 358 """
359 359 if self._lock_renewal_thread is not None:
360 360 self._stop_lock_renewer()
361 361 loggers["release"].debug("Releasing %r.", self._name)
362 362 error = self.unlock_script(client=self._client, keys=(self._name, self._signal), args=(self._id, self._signal_expire))
363 363 if error == 1:
364 364 raise NotAcquired("Lock %s is not acquired or it already expired." % self._name)
365 365 elif error:
366 366 raise RuntimeError("Unsupported error code %s from EXTEND script." % error)
367 367
368 368 def locked(self):
369 369 """
370 370 Return true if the lock is acquired.
371 371
372 372 Checks that lock with same name already exists. This method returns true, even if
373 373 lock have another id.
374 374 """
375 375 return self._client.exists(self._name) == 1
376 376
377 377
378 378 reset_all_script = None
379 379
380 380
381 381 def reset_all(redis_client):
382 382 """
383 383 Forcibly deletes all locks if its remains (like a crash reason). Use this with care.
384 384
385 385 :param redis_client:
386 386 An instance of :class:`~StrictRedis`.
387 387 """
388 388 Lock.register_scripts(redis_client)
389 389
390 390 reset_all_script(client=redis_client) # noqa
@@ -1,303 +1,305 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2017-2020 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 logging
22 22 import datetime
23 23
24 24 from rhodecode.lib.jsonalchemy import JsonRaw
25 25 from rhodecode.model import meta
26 26 from rhodecode.model.db import User, UserLog, Repository
27 27
28 28
29 29 log = logging.getLogger(__name__)
30 30
31 31 # action as key, and expected action_data as value
32 32 ACTIONS_V1 = {
33 33 'user.login.success': {'user_agent': ''},
34 34 'user.login.failure': {'user_agent': ''},
35 35 'user.logout': {'user_agent': ''},
36 36 'user.register': {},
37 37 'user.password.reset_request': {},
38 38 'user.push': {'user_agent': '', 'commit_ids': []},
39 39 'user.pull': {'user_agent': ''},
40 40
41 41 'user.create': {'data': {}},
42 42 'user.delete': {'old_data': {}},
43 43 'user.edit': {'old_data': {}},
44 44 'user.edit.permissions': {},
45 45 'user.edit.ip.add': {'ip': {}, 'user': {}},
46 46 'user.edit.ip.delete': {'ip': {}, 'user': {}},
47 47 'user.edit.token.add': {'token': {}, 'user': {}},
48 48 'user.edit.token.delete': {'token': {}, 'user': {}},
49 49 'user.edit.email.add': {'email': ''},
50 50 'user.edit.email.delete': {'email': ''},
51 51 'user.edit.ssh_key.add': {'token': {}, 'user': {}},
52 52 'user.edit.ssh_key.delete': {'token': {}, 'user': {}},
53 53 'user.edit.password_reset.enabled': {},
54 54 'user.edit.password_reset.disabled': {},
55 55
56 56 'user_group.create': {'data': {}},
57 57 'user_group.delete': {'old_data': {}},
58 58 'user_group.edit': {'old_data': {}},
59 59 'user_group.edit.permissions': {},
60 60 'user_group.edit.member.add': {'user': {}},
61 61 'user_group.edit.member.delete': {'user': {}},
62 62
63 63 'repo.create': {'data': {}},
64 64 'repo.fork': {'data': {}},
65 65 'repo.edit': {'old_data': {}},
66 66 'repo.edit.permissions': {},
67 67 'repo.edit.permissions.branch': {},
68 68 'repo.archive': {'old_data': {}},
69 69 'repo.delete': {'old_data': {}},
70 70
71 71 'repo.archive.download': {'user_agent': '', 'archive_name': '',
72 72 'archive_spec': '', 'archive_cached': ''},
73 73
74 74 'repo.permissions.branch_rule.create': {},
75 75 'repo.permissions.branch_rule.edit': {},
76 76 'repo.permissions.branch_rule.delete': {},
77 77
78 78 'repo.pull_request.create': '',
79 79 'repo.pull_request.edit': '',
80 80 'repo.pull_request.delete': '',
81 81 'repo.pull_request.close': '',
82 82 'repo.pull_request.merge': '',
83 83 'repo.pull_request.vote': '',
84 84 'repo.pull_request.comment.create': '',
85 85 'repo.pull_request.comment.edit': '',
86 86 'repo.pull_request.comment.delete': '',
87 87
88 88 'repo.pull_request.reviewer.add': '',
89 89 'repo.pull_request.reviewer.delete': '',
90 90
91 91 'repo.pull_request.observer.add': '',
92 92 'repo.pull_request.observer.delete': '',
93 93
94 94 'repo.commit.strip': {'commit_id': ''},
95 95 'repo.commit.comment.create': {'data': {}},
96 96 'repo.commit.comment.delete': {'data': {}},
97 97 'repo.commit.comment.edit': {'data': {}},
98 98 'repo.commit.vote': '',
99 99
100 100 'repo.artifact.add': '',
101 101 'repo.artifact.delete': '',
102 102
103 103 'repo_group.create': {'data': {}},
104 104 'repo_group.edit': {'old_data': {}},
105 105 'repo_group.edit.permissions': {},
106 106 'repo_group.delete': {'old_data': {}},
107 107 }
108 108
109 109 ACTIONS = ACTIONS_V1
110 110
111 111 SOURCE_WEB = 'source_web'
112 112 SOURCE_API = 'source_api'
113 113
114 114
115 115 class UserWrap(object):
116 116 """
117 117 Fake object used to imitate AuthUser
118 118 """
119 119
120 120 def __init__(self, user_id=None, username=None, ip_addr=None):
121 121 self.user_id = user_id
122 122 self.username = username
123 123 self.ip_addr = ip_addr
124 124
125 125
126 126 class RepoWrap(object):
127 127 """
128 128 Fake object used to imitate RepoObject that audit logger requires
129 129 """
130 130
131 131 def __init__(self, repo_id=None, repo_name=None):
132 132 self.repo_id = repo_id
133 133 self.repo_name = repo_name
134 134
135 135
136 136 def _store_log(action_name, action_data, user_id, username, user_data,
137 137 ip_address, repository_id, repository_name):
138 138 user_log = UserLog()
139 139 user_log.version = UserLog.VERSION_2
140 140
141 141 user_log.action = action_name
142 142 user_log.action_data = action_data or JsonRaw(u'{}')
143 143
144 144 user_log.user_ip = ip_address
145 145
146 146 user_log.user_id = user_id
147 147 user_log.username = username
148 148 user_log.user_data = user_data or JsonRaw(u'{}')
149 149
150 150 user_log.repository_id = repository_id
151 151 user_log.repository_name = repository_name
152 152
153 153 user_log.action_date = datetime.datetime.now()
154 154
155 155 return user_log
156 156
157 157
158 158 def store_web(*args, **kwargs):
159 159 action_data = {}
160 160 org_action_data = kwargs.pop('action_data', {})
161 161 action_data.update(org_action_data)
162 162 action_data['source'] = SOURCE_WEB
163 163 kwargs['action_data'] = action_data
164 164
165 165 return store(*args, **kwargs)
166 166
167 167
168 168 def store_api(*args, **kwargs):
169 169 action_data = {}
170 170 org_action_data = kwargs.pop('action_data', {})
171 171 action_data.update(org_action_data)
172 172 action_data['source'] = SOURCE_API
173 173 kwargs['action_data'] = action_data
174 174
175 175 return store(*args, **kwargs)
176 176
177 177
178 178 def store(action, user, action_data=None, user_data=None, ip_addr=None,
179 179 repo=None, sa_session=None, commit=False):
180 180 """
181 181 Audit logger for various actions made by users, typically this
182 182 results in a call such::
183 183
184 184 from rhodecode.lib import audit_logger
185 185
186 186 audit_logger.store(
187 187 'repo.edit', user=self._rhodecode_user)
188 188 audit_logger.store(
189 189 'repo.delete', action_data={'data': repo_data},
190 190 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'))
191 191
192 192 # repo action
193 193 audit_logger.store(
194 194 'repo.delete',
195 195 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'),
196 196 repo=audit_logger.RepoWrap(repo_name='some-repo'))
197 197
198 198 # repo action, when we know and have the repository object already
199 199 audit_logger.store(
200 200 'repo.delete', action_data={'source': audit_logger.SOURCE_WEB, },
201 201 user=self._rhodecode_user,
202 202 repo=repo_object)
203 203
204 204 # alternative wrapper to the above
205 205 audit_logger.store_web(
206 206 'repo.delete', action_data={},
207 207 user=self._rhodecode_user,
208 208 repo=repo_object)
209 209
210 210 # without an user ?
211 211 audit_logger.store(
212 212 'user.login.failure',
213 213 user=audit_logger.UserWrap(
214 214 username=self.request.params.get('username'),
215 215 ip_addr=self.request.remote_addr))
216 216
217 217 """
218 218 from rhodecode.lib.utils2 import safe_unicode
219 219 from rhodecode.lib.auth import AuthUser
220 220
221 221 action_spec = ACTIONS.get(action, None)
222 222 if action_spec is None:
223 223 raise ValueError('Action `{}` is not supported'.format(action))
224 224
225 225 if not sa_session:
226 226 sa_session = meta.Session()
227 227
228 228 try:
229 229 username = getattr(user, 'username', None)
230 230 if not username:
231 231 pass
232 232
233 233 user_id = getattr(user, 'user_id', None)
234 234 if not user_id:
235 235 # maybe we have username ? Try to figure user_id from username
236 236 if username:
237 237 user_id = getattr(
238 238 User.get_by_username(username), 'user_id', None)
239 239
240 240 ip_addr = ip_addr or getattr(user, 'ip_addr', None)
241 241 if not ip_addr:
242 242 pass
243 243
244 244 if not user_data:
245 245 # try to get this from the auth user
246 246 if isinstance(user, AuthUser):
247 247 user_data = {
248 248 'username': user.username,
249 249 'email': user.email,
250 250 }
251 251
252 252 repository_name = getattr(repo, 'repo_name', None)
253 253 repository_id = getattr(repo, 'repo_id', None)
254 254 if not repository_id:
255 255 # maybe we have repo_name ? Try to figure repo_id from repo_name
256 256 if repository_name:
257 257 repository_id = getattr(
258 258 Repository.get_by_repo_name(repository_name), 'repo_id', None)
259 259
260 260 action_name = safe_unicode(action)
261 261 ip_address = safe_unicode(ip_addr)
262 262
263 263 with sa_session.no_autoflush:
264 264
265 265 user_log = _store_log(
266 266 action_name=action_name,
267 267 action_data=action_data or {},
268 268 user_id=user_id,
269 269 username=username,
270 270 user_data=user_data or {},
271 271 ip_address=ip_address,
272 272 repository_id=repository_id,
273 273 repository_name=repository_name
274 274 )
275 275
276 276 sa_session.add(user_log)
277 277 if commit:
278 278 sa_session.commit()
279 279 entry_id = user_log.entry_id or ''
280 280
281 281 update_user_last_activity(sa_session, user_id)
282 282
283 283 if commit:
284 284 sa_session.commit()
285 285
286 286 log.info('AUDIT[%s]: Logging action: `%s` by user:id:%s[%s] ip:%s',
287 entry_id, action_name, user_id, username, ip_address)
287 entry_id, action_name, user_id, username, ip_address,
288 extra={"entry_id": entry_id, "action": action_name,
289 "user_id": user_id, "ip": ip_address})
288 290
289 291 except Exception:
290 292 log.exception('AUDIT: failed to store audit log')
291 293
292 294
293 295 def update_user_last_activity(sa_session, user_id):
294 296 _last_activity = datetime.datetime.now()
295 297 try:
296 298 sa_session.query(User).filter(User.user_id == user_id).update(
297 299 {"last_activity": _last_activity})
298 300 log.debug(
299 301 'updated user `%s` last activity to:%s', user_id, _last_activity)
300 302 except Exception:
301 303 log.exception("Failed last activity update for user_id: %s", user_id)
302 304 sa_session.rollback()
303 305
@@ -1,2515 +1,2516 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 and permission libraries
23 23 """
24 24
25 25 import os
26 26
27 27 import colander
28 28 import time
29 29 import collections
30 30 import fnmatch
31 31 import hashlib
32 32 import itertools
33 33 import logging
34 34 import random
35 35 import traceback
36 36 from functools import wraps
37 37
38 38 import ipaddress
39 39
40 40 from pyramid.httpexceptions import HTTPForbidden, HTTPFound, HTTPNotFound
41 41 from sqlalchemy.orm.exc import ObjectDeletedError
42 42 from sqlalchemy.orm import joinedload
43 43 from zope.cachedescriptors.property import Lazy as LazyProperty
44 44
45 45 import rhodecode
46 46 from rhodecode.model import meta
47 47 from rhodecode.model.meta import Session
48 48 from rhodecode.model.user import UserModel
49 49 from rhodecode.model.db import (
50 50 false, User, Repository, Permission, UserToPerm, UserGroupToPerm, UserGroupMember,
51 51 UserIpMap, UserApiKeys, RepoGroup, UserGroup, UserNotice)
52 52 from rhodecode.lib import rc_cache
53 53 from rhodecode.lib.utils2 import safe_unicode, aslist, safe_str, md5, safe_int, sha1
54 54 from rhodecode.lib.utils import (
55 55 get_repo_slug, get_repo_group_slug, get_user_group_slug)
56 56 from rhodecode.lib.caching_query import FromCache
57 57
58 58 if rhodecode.is_unix:
59 59 import bcrypt
60 60
61 61 log = logging.getLogger(__name__)
62 62
63 63 csrf_token_key = "csrf_token"
64 64
65 65
66 66 class PasswordGenerator(object):
67 67 """
68 68 This is a simple class for generating password from different sets of
69 69 characters
70 70 usage::
71 71 passwd_gen = PasswordGenerator()
72 72 #print 8-letter password containing only big and small letters
73 73 of alphabet
74 74 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
75 75 """
76 76 ALPHABETS_NUM = r'''1234567890'''
77 77 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
78 78 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
79 79 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
80 80 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
81 81 + ALPHABETS_NUM + ALPHABETS_SPECIAL
82 82 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
83 83 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
84 84 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
85 85 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
86 86
87 87 def __init__(self, passwd=''):
88 88 self.passwd = passwd
89 89
90 90 def gen_password(self, length, type_=None):
91 91 if type_ is None:
92 92 type_ = self.ALPHABETS_FULL
93 93 self.passwd = ''.join([random.choice(type_) for _ in range(length)])
94 94 return self.passwd
95 95
96 96
97 97 class _RhodeCodeCryptoBase(object):
98 98 ENC_PREF = None
99 99
100 100 def hash_create(self, str_):
101 101 """
102 102 hash the string using
103 103
104 104 :param str_: password to hash
105 105 """
106 106 raise NotImplementedError
107 107
108 108 def hash_check_with_upgrade(self, password, hashed):
109 109 """
110 110 Returns tuple in which first element is boolean that states that
111 111 given password matches it's hashed version, and the second is new hash
112 112 of the password, in case this password should be migrated to new
113 113 cipher.
114 114 """
115 115 checked_hash = self.hash_check(password, hashed)
116 116 return checked_hash, None
117 117
118 118 def hash_check(self, password, hashed):
119 119 """
120 120 Checks matching password with it's hashed value.
121 121
122 122 :param password: password
123 123 :param hashed: password in hashed form
124 124 """
125 125 raise NotImplementedError
126 126
127 127 def _assert_bytes(self, value):
128 128 """
129 129 Passing in an `unicode` object can lead to hard to detect issues
130 130 if passwords contain non-ascii characters. Doing a type check
131 131 during runtime, so that such mistakes are detected early on.
132 132 """
133 133 if not isinstance(value, str):
134 134 raise TypeError(
135 135 "Bytestring required as input, got %r." % (value, ))
136 136
137 137
138 138 class _RhodeCodeCryptoBCrypt(_RhodeCodeCryptoBase):
139 139 ENC_PREF = ('$2a$10', '$2b$10')
140 140
141 141 def hash_create(self, str_):
142 142 self._assert_bytes(str_)
143 143 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
144 144
145 145 def hash_check_with_upgrade(self, password, hashed):
146 146 """
147 147 Returns tuple in which first element is boolean that states that
148 148 given password matches it's hashed version, and the second is new hash
149 149 of the password, in case this password should be migrated to new
150 150 cipher.
151 151
152 152 This implements special upgrade logic which works like that:
153 153 - check if the given password == bcrypted hash, if yes then we
154 154 properly used password and it was already in bcrypt. Proceed
155 155 without any changes
156 156 - if bcrypt hash check is not working try with sha256. If hash compare
157 157 is ok, it means we using correct but old hashed password. indicate
158 158 hash change and proceed
159 159 """
160 160
161 161 new_hash = None
162 162
163 163 # regular pw check
164 164 password_match_bcrypt = self.hash_check(password, hashed)
165 165
166 166 # now we want to know if the password was maybe from sha256
167 167 # basically calling _RhodeCodeCryptoSha256().hash_check()
168 168 if not password_match_bcrypt:
169 169 if _RhodeCodeCryptoSha256().hash_check(password, hashed):
170 170 new_hash = self.hash_create(password) # make new bcrypt hash
171 171 password_match_bcrypt = True
172 172
173 173 return password_match_bcrypt, new_hash
174 174
175 175 def hash_check(self, password, hashed):
176 176 """
177 177 Checks matching password with it's hashed value.
178 178
179 179 :param password: password
180 180 :param hashed: password in hashed form
181 181 """
182 182 self._assert_bytes(password)
183 183 try:
184 184 return bcrypt.hashpw(password, hashed) == hashed
185 185 except ValueError as e:
186 186 # we're having a invalid salt here probably, we should not crash
187 187 # just return with False as it would be a wrong password.
188 188 log.debug('Failed to check password hash using bcrypt %s',
189 189 safe_str(e))
190 190
191 191 return False
192 192
193 193
194 194 class _RhodeCodeCryptoSha256(_RhodeCodeCryptoBase):
195 195 ENC_PREF = '_'
196 196
197 197 def hash_create(self, str_):
198 198 self._assert_bytes(str_)
199 199 return hashlib.sha256(str_).hexdigest()
200 200
201 201 def hash_check(self, password, hashed):
202 202 """
203 203 Checks matching password with it's hashed value.
204 204
205 205 :param password: password
206 206 :param hashed: password in hashed form
207 207 """
208 208 self._assert_bytes(password)
209 209 return hashlib.sha256(password).hexdigest() == hashed
210 210
211 211
212 212 class _RhodeCodeCryptoTest(_RhodeCodeCryptoBase):
213 213 ENC_PREF = '_'
214 214
215 215 def hash_create(self, str_):
216 216 self._assert_bytes(str_)
217 217 return sha1(str_)
218 218
219 219 def hash_check(self, password, hashed):
220 220 """
221 221 Checks matching password with it's hashed value.
222 222
223 223 :param password: password
224 224 :param hashed: password in hashed form
225 225 """
226 226 self._assert_bytes(password)
227 227 return sha1(password) == hashed
228 228
229 229
230 230 def crypto_backend():
231 231 """
232 232 Return the matching crypto backend.
233 233
234 234 Selection is based on if we run tests or not, we pick sha1-test backend to run
235 235 tests faster since BCRYPT is expensive to calculate
236 236 """
237 237 if rhodecode.is_test:
238 238 RhodeCodeCrypto = _RhodeCodeCryptoTest()
239 239 else:
240 240 RhodeCodeCrypto = _RhodeCodeCryptoBCrypt()
241 241
242 242 return RhodeCodeCrypto
243 243
244 244
245 245 def get_crypt_password(password):
246 246 """
247 247 Create the hash of `password` with the active crypto backend.
248 248
249 249 :param password: The cleartext password.
250 250 :type password: unicode
251 251 """
252 252 password = safe_str(password)
253 253 return crypto_backend().hash_create(password)
254 254
255 255
256 256 def check_password(password, hashed):
257 257 """
258 258 Check if the value in `password` matches the hash in `hashed`.
259 259
260 260 :param password: The cleartext password.
261 261 :type password: unicode
262 262
263 263 :param hashed: The expected hashed version of the password.
264 264 :type hashed: The hash has to be passed in in text representation.
265 265 """
266 266 password = safe_str(password)
267 267 return crypto_backend().hash_check(password, hashed)
268 268
269 269
270 270 def generate_auth_token(data, salt=None):
271 271 """
272 272 Generates API KEY from given string
273 273 """
274 274
275 275 if salt is None:
276 276 salt = os.urandom(16)
277 277 return hashlib.sha1(safe_str(data) + salt).hexdigest()
278 278
279 279
280 280 def get_came_from(request):
281 281 """
282 282 get query_string+path from request sanitized after removing auth_token
283 283 """
284 284 _req = request
285 285
286 286 path = _req.path
287 287 if 'auth_token' in _req.GET:
288 288 # sanitize the request and remove auth_token for redirection
289 289 _req.GET.pop('auth_token')
290 290 qs = _req.query_string
291 291 if qs:
292 292 path += '?' + qs
293 293
294 294 return path
295 295
296 296
297 297 class CookieStoreWrapper(object):
298 298
299 299 def __init__(self, cookie_store):
300 300 self.cookie_store = cookie_store
301 301
302 302 def __repr__(self):
303 303 return 'CookieStore<%s>' % (self.cookie_store)
304 304
305 305 def get(self, key, other=None):
306 306 if isinstance(self.cookie_store, dict):
307 307 return self.cookie_store.get(key, other)
308 308 elif isinstance(self.cookie_store, AuthUser):
309 309 return self.cookie_store.__dict__.get(key, other)
310 310
311 311
312 312 def _cached_perms_data(user_id, scope, user_is_admin,
313 313 user_inherit_default_permissions, explicit, algo,
314 314 calculate_super_admin):
315 315
316 316 permissions = PermissionCalculator(
317 317 user_id, scope, user_is_admin, user_inherit_default_permissions,
318 318 explicit, algo, calculate_super_admin)
319 319 return permissions.calculate()
320 320
321 321
322 322 class PermOrigin(object):
323 323 SUPER_ADMIN = 'superadmin'
324 324 ARCHIVED = 'archived'
325 325
326 326 REPO_USER = 'user:%s'
327 327 REPO_USERGROUP = 'usergroup:%s'
328 328 REPO_OWNER = 'repo.owner'
329 329 REPO_DEFAULT = 'repo.default'
330 330 REPO_DEFAULT_NO_INHERIT = 'repo.default.no.inherit'
331 331 REPO_PRIVATE = 'repo.private'
332 332
333 333 REPOGROUP_USER = 'user:%s'
334 334 REPOGROUP_USERGROUP = 'usergroup:%s'
335 335 REPOGROUP_OWNER = 'group.owner'
336 336 REPOGROUP_DEFAULT = 'group.default'
337 337 REPOGROUP_DEFAULT_NO_INHERIT = 'group.default.no.inherit'
338 338
339 339 USERGROUP_USER = 'user:%s'
340 340 USERGROUP_USERGROUP = 'usergroup:%s'
341 341 USERGROUP_OWNER = 'usergroup.owner'
342 342 USERGROUP_DEFAULT = 'usergroup.default'
343 343 USERGROUP_DEFAULT_NO_INHERIT = 'usergroup.default.no.inherit'
344 344
345 345
346 346 class PermOriginDict(dict):
347 347 """
348 348 A special dict used for tracking permissions along with their origins.
349 349
350 350 `__setitem__` has been overridden to expect a tuple(perm, origin)
351 351 `__getitem__` will return only the perm
352 352 `.perm_origin_stack` will return the stack of (perm, origin) set per key
353 353
354 354 >>> perms = PermOriginDict()
355 355 >>> perms['resource'] = 'read', 'default', 1
356 356 >>> perms['resource']
357 357 'read'
358 358 >>> perms['resource'] = 'write', 'admin', 2
359 359 >>> perms['resource']
360 360 'write'
361 361 >>> perms.perm_origin_stack
362 362 {'resource': [('read', 'default', 1), ('write', 'admin', 2)]}
363 363 """
364 364
365 365 def __init__(self, *args, **kw):
366 366 dict.__init__(self, *args, **kw)
367 367 self.perm_origin_stack = collections.OrderedDict()
368 368
369 369 def __setitem__(self, key, perm_origin_obj_id):
370 370 (perm, origin, obj_id) = perm_origin_obj_id
371 371 self.perm_origin_stack.setdefault(key, []).append((perm, origin, obj_id))
372 372 dict.__setitem__(self, key, perm)
373 373
374 374
375 375 class BranchPermOriginDict(PermOriginDict):
376 376 """
377 377 Dedicated branch permissions dict, with tracking of patterns and origins.
378 378
379 379 >>> perms = BranchPermOriginDict()
380 380 >>> perms['resource'] = '*pattern', 'read', 'default'
381 381 >>> perms['resource']
382 382 {'*pattern': 'read'}
383 383 >>> perms['resource'] = '*pattern', 'write', 'admin'
384 384 >>> perms['resource']
385 385 {'*pattern': 'write'}
386 386 >>> perms.perm_origin_stack
387 387 {'resource': {'*pattern': [('read', 'default'), ('write', 'admin')]}}
388 388 """
389 389 def __setitem__(self, key, pattern_perm_origin):
390 390 (pattern, perm, origin) = pattern_perm_origin
391 391
392 392 self.perm_origin_stack.setdefault(key, {}) \
393 393 .setdefault(pattern, []).append((perm, origin))
394 394
395 395 if key in self:
396 396 self[key].__setitem__(pattern, perm)
397 397 else:
398 398 patterns = collections.OrderedDict()
399 399 patterns[pattern] = perm
400 400 dict.__setitem__(self, key, patterns)
401 401
402 402
403 403 class PermissionCalculator(object):
404 404
405 405 def __init__(
406 406 self, user_id, scope, user_is_admin,
407 407 user_inherit_default_permissions, explicit, algo,
408 408 calculate_super_admin_as_user=False):
409 409
410 410 self.user_id = user_id
411 411 self.user_is_admin = user_is_admin
412 412 self.inherit_default_permissions = user_inherit_default_permissions
413 413 self.explicit = explicit
414 414 self.algo = algo
415 415 self.calculate_super_admin_as_user = calculate_super_admin_as_user
416 416
417 417 scope = scope or {}
418 418 self.scope_repo_id = scope.get('repo_id')
419 419 self.scope_repo_group_id = scope.get('repo_group_id')
420 420 self.scope_user_group_id = scope.get('user_group_id')
421 421
422 422 self.default_user_id = User.get_default_user(cache=True).user_id
423 423
424 424 self.permissions_repositories = PermOriginDict()
425 425 self.permissions_repository_groups = PermOriginDict()
426 426 self.permissions_user_groups = PermOriginDict()
427 427 self.permissions_repository_branches = BranchPermOriginDict()
428 428 self.permissions_global = set()
429 429
430 430 self.default_repo_perms = Permission.get_default_repo_perms(
431 431 self.default_user_id, self.scope_repo_id)
432 432 self.default_repo_groups_perms = Permission.get_default_group_perms(
433 433 self.default_user_id, self.scope_repo_group_id)
434 434 self.default_user_group_perms = \
435 435 Permission.get_default_user_group_perms(
436 436 self.default_user_id, self.scope_user_group_id)
437 437
438 438 # default branch perms
439 439 self.default_branch_repo_perms = \
440 440 Permission.get_default_repo_branch_perms(
441 441 self.default_user_id, self.scope_repo_id)
442 442
443 443 def calculate(self):
444 444 if self.user_is_admin and not self.calculate_super_admin_as_user:
445 445 return self._calculate_super_admin_permissions()
446 446
447 447 self._calculate_global_default_permissions()
448 448 self._calculate_global_permissions()
449 449 self._calculate_default_permissions()
450 450 self._calculate_repository_permissions()
451 451 self._calculate_repository_branch_permissions()
452 452 self._calculate_repository_group_permissions()
453 453 self._calculate_user_group_permissions()
454 454 return self._permission_structure()
455 455
456 456 def _calculate_super_admin_permissions(self):
457 457 """
458 458 super-admin user have all default rights for repositories
459 459 and groups set to admin
460 460 """
461 461 self.permissions_global.add('hg.admin')
462 462 self.permissions_global.add('hg.create.write_on_repogroup.true')
463 463
464 464 # repositories
465 465 for perm in self.default_repo_perms:
466 466 r_k = perm.UserRepoToPerm.repository.repo_name
467 467 obj_id = perm.UserRepoToPerm.repository.repo_id
468 468 archived = perm.UserRepoToPerm.repository.archived
469 469 p = 'repository.admin'
470 470 self.permissions_repositories[r_k] = p, PermOrigin.SUPER_ADMIN, obj_id
471 471 # special case for archived repositories, which we block still even for
472 472 # super admins
473 473 if archived:
474 474 p = 'repository.read'
475 475 self.permissions_repositories[r_k] = p, PermOrigin.ARCHIVED, obj_id
476 476
477 477 # repository groups
478 478 for perm in self.default_repo_groups_perms:
479 479 rg_k = perm.UserRepoGroupToPerm.group.group_name
480 480 obj_id = perm.UserRepoGroupToPerm.group.group_id
481 481 p = 'group.admin'
482 482 self.permissions_repository_groups[rg_k] = p, PermOrigin.SUPER_ADMIN, obj_id
483 483
484 484 # user groups
485 485 for perm in self.default_user_group_perms:
486 486 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
487 487 obj_id = perm.UserUserGroupToPerm.user_group.users_group_id
488 488 p = 'usergroup.admin'
489 489 self.permissions_user_groups[u_k] = p, PermOrigin.SUPER_ADMIN, obj_id
490 490
491 491 # branch permissions
492 492 # since super-admin also can have custom rule permissions
493 493 # we *always* need to calculate those inherited from default, and also explicit
494 494 self._calculate_default_permissions_repository_branches(
495 495 user_inherit_object_permissions=False)
496 496 self._calculate_repository_branch_permissions()
497 497
498 498 return self._permission_structure()
499 499
500 500 def _calculate_global_default_permissions(self):
501 501 """
502 502 global permissions taken from the default user
503 503 """
504 504 default_global_perms = UserToPerm.query()\
505 505 .filter(UserToPerm.user_id == self.default_user_id)\
506 506 .options(joinedload(UserToPerm.permission))
507 507
508 508 for perm in default_global_perms:
509 509 self.permissions_global.add(perm.permission.permission_name)
510 510
511 511 if self.user_is_admin:
512 512 self.permissions_global.add('hg.admin')
513 513 self.permissions_global.add('hg.create.write_on_repogroup.true')
514 514
515 515 def _calculate_global_permissions(self):
516 516 """
517 517 Set global system permissions with user permissions or permissions
518 518 taken from the user groups of the current user.
519 519
520 520 The permissions include repo creating, repo group creating, forking
521 521 etc.
522 522 """
523 523
524 524 # now we read the defined permissions and overwrite what we have set
525 525 # before those can be configured from groups or users explicitly.
526 526
527 527 # In case we want to extend this list we should make sure
528 528 # this is in sync with User.DEFAULT_USER_PERMISSIONS definitions
529 529 from rhodecode.model.permission import PermissionModel
530 530
531 531 _configurable = frozenset([
532 532 PermissionModel.FORKING_DISABLED, PermissionModel.FORKING_ENABLED,
533 533 'hg.create.none', 'hg.create.repository',
534 534 'hg.usergroup.create.false', 'hg.usergroup.create.true',
535 535 'hg.repogroup.create.false', 'hg.repogroup.create.true',
536 536 'hg.create.write_on_repogroup.false', 'hg.create.write_on_repogroup.true',
537 537 'hg.inherit_default_perms.false', 'hg.inherit_default_perms.true'
538 538 ])
539 539
540 540 # USER GROUPS comes first user group global permissions
541 541 user_perms_from_users_groups = Session().query(UserGroupToPerm)\
542 542 .options(joinedload(UserGroupToPerm.permission))\
543 543 .join((UserGroupMember, UserGroupToPerm.users_group_id ==
544 544 UserGroupMember.users_group_id))\
545 545 .filter(UserGroupMember.user_id == self.user_id)\
546 546 .order_by(UserGroupToPerm.users_group_id)\
547 547 .all()
548 548
549 549 # need to group here by groups since user can be in more than
550 550 # one group, so we get all groups
551 551 _explicit_grouped_perms = [
552 552 [x, list(y)] for x, y in
553 553 itertools.groupby(user_perms_from_users_groups,
554 554 lambda _x: _x.users_group)]
555 555
556 556 for gr, perms in _explicit_grouped_perms:
557 557 # since user can be in multiple groups iterate over them and
558 558 # select the lowest permissions first (more explicit)
559 559 # TODO(marcink): do this^^
560 560
561 561 # group doesn't inherit default permissions so we actually set them
562 562 if not gr.inherit_default_permissions:
563 563 # NEED TO IGNORE all previously set configurable permissions
564 564 # and replace them with explicitly set from this user
565 565 # group permissions
566 566 self.permissions_global = self.permissions_global.difference(
567 567 _configurable)
568 568 for perm in perms:
569 569 self.permissions_global.add(perm.permission.permission_name)
570 570
571 571 # user explicit global permissions
572 572 user_perms = Session().query(UserToPerm)\
573 573 .options(joinedload(UserToPerm.permission))\
574 574 .filter(UserToPerm.user_id == self.user_id).all()
575 575
576 576 if not self.inherit_default_permissions:
577 577 # NEED TO IGNORE all configurable permissions and
578 578 # replace them with explicitly set from this user permissions
579 579 self.permissions_global = self.permissions_global.difference(
580 580 _configurable)
581 581 for perm in user_perms:
582 582 self.permissions_global.add(perm.permission.permission_name)
583 583
584 584 def _calculate_default_permissions_repositories(self, user_inherit_object_permissions):
585 585 for perm in self.default_repo_perms:
586 586 r_k = perm.UserRepoToPerm.repository.repo_name
587 587 obj_id = perm.UserRepoToPerm.repository.repo_id
588 588 archived = perm.UserRepoToPerm.repository.archived
589 589 p = perm.Permission.permission_name
590 590 o = PermOrigin.REPO_DEFAULT
591 591 self.permissions_repositories[r_k] = p, o, obj_id
592 592
593 593 # if we decide this user isn't inheriting permissions from
594 594 # default user we set him to .none so only explicit
595 595 # permissions work
596 596 if not user_inherit_object_permissions:
597 597 p = 'repository.none'
598 598 o = PermOrigin.REPO_DEFAULT_NO_INHERIT
599 599 self.permissions_repositories[r_k] = p, o, obj_id
600 600
601 601 if perm.Repository.private and not (
602 602 perm.Repository.user_id == self.user_id):
603 603 # disable defaults for private repos,
604 604 p = 'repository.none'
605 605 o = PermOrigin.REPO_PRIVATE
606 606 self.permissions_repositories[r_k] = p, o, obj_id
607 607
608 608 elif perm.Repository.user_id == self.user_id:
609 609 # set admin if owner
610 610 p = 'repository.admin'
611 611 o = PermOrigin.REPO_OWNER
612 612 self.permissions_repositories[r_k] = p, o, obj_id
613 613
614 614 if self.user_is_admin:
615 615 p = 'repository.admin'
616 616 o = PermOrigin.SUPER_ADMIN
617 617 self.permissions_repositories[r_k] = p, o, obj_id
618 618
619 619 # finally in case of archived repositories, we downgrade higher
620 620 # permissions to read
621 621 if archived:
622 622 current_perm = self.permissions_repositories[r_k]
623 623 if current_perm in ['repository.write', 'repository.admin']:
624 624 p = 'repository.read'
625 625 o = PermOrigin.ARCHIVED
626 626 self.permissions_repositories[r_k] = p, o, obj_id
627 627
628 628 def _calculate_default_permissions_repository_branches(self, user_inherit_object_permissions):
629 629 for perm in self.default_branch_repo_perms:
630 630
631 631 r_k = perm.UserRepoToPerm.repository.repo_name
632 632 p = perm.Permission.permission_name
633 633 pattern = perm.UserToRepoBranchPermission.branch_pattern
634 634 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
635 635
636 636 if not self.explicit:
637 637 cur_perm = self.permissions_repository_branches.get(r_k)
638 638 if cur_perm:
639 639 cur_perm = cur_perm[pattern]
640 640 cur_perm = cur_perm or 'branch.none'
641 641
642 642 p = self._choose_permission(p, cur_perm)
643 643
644 644 # NOTE(marcink): register all pattern/perm instances in this
645 645 # special dict that aggregates entries
646 646 self.permissions_repository_branches[r_k] = pattern, p, o
647 647
648 648 def _calculate_default_permissions_repository_groups(self, user_inherit_object_permissions):
649 649 for perm in self.default_repo_groups_perms:
650 650 rg_k = perm.UserRepoGroupToPerm.group.group_name
651 651 obj_id = perm.UserRepoGroupToPerm.group.group_id
652 652 p = perm.Permission.permission_name
653 653 o = PermOrigin.REPOGROUP_DEFAULT
654 654 self.permissions_repository_groups[rg_k] = p, o, obj_id
655 655
656 656 # if we decide this user isn't inheriting permissions from default
657 657 # user we set him to .none so only explicit permissions work
658 658 if not user_inherit_object_permissions:
659 659 p = 'group.none'
660 660 o = PermOrigin.REPOGROUP_DEFAULT_NO_INHERIT
661 661 self.permissions_repository_groups[rg_k] = p, o, obj_id
662 662
663 663 if perm.RepoGroup.user_id == self.user_id:
664 664 # set admin if owner
665 665 p = 'group.admin'
666 666 o = PermOrigin.REPOGROUP_OWNER
667 667 self.permissions_repository_groups[rg_k] = p, o, obj_id
668 668
669 669 if self.user_is_admin:
670 670 p = 'group.admin'
671 671 o = PermOrigin.SUPER_ADMIN
672 672 self.permissions_repository_groups[rg_k] = p, o, obj_id
673 673
674 674 def _calculate_default_permissions_user_groups(self, user_inherit_object_permissions):
675 675 for perm in self.default_user_group_perms:
676 676 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
677 677 obj_id = perm.UserUserGroupToPerm.user_group.users_group_id
678 678 p = perm.Permission.permission_name
679 679 o = PermOrigin.USERGROUP_DEFAULT
680 680 self.permissions_user_groups[u_k] = p, o, obj_id
681 681
682 682 # if we decide this user isn't inheriting permissions from default
683 683 # user we set him to .none so only explicit permissions work
684 684 if not user_inherit_object_permissions:
685 685 p = 'usergroup.none'
686 686 o = PermOrigin.USERGROUP_DEFAULT_NO_INHERIT
687 687 self.permissions_user_groups[u_k] = p, o, obj_id
688 688
689 689 if perm.UserGroup.user_id == self.user_id:
690 690 # set admin if owner
691 691 p = 'usergroup.admin'
692 692 o = PermOrigin.USERGROUP_OWNER
693 693 self.permissions_user_groups[u_k] = p, o, obj_id
694 694
695 695 if self.user_is_admin:
696 696 p = 'usergroup.admin'
697 697 o = PermOrigin.SUPER_ADMIN
698 698 self.permissions_user_groups[u_k] = p, o, obj_id
699 699
700 700 def _calculate_default_permissions(self):
701 701 """
702 702 Set default user permissions for repositories, repository branches,
703 703 repository groups, user groups taken from the default user.
704 704
705 705 Calculate inheritance of object permissions based on what we have now
706 706 in GLOBAL permissions. We check if .false is in GLOBAL since this is
707 707 explicitly set. Inherit is the opposite of .false being there.
708 708
709 709 .. note::
710 710
711 711 the syntax is little bit odd but what we need to check here is
712 712 the opposite of .false permission being in the list so even for
713 713 inconsistent state when both .true/.false is there
714 714 .false is more important
715 715
716 716 """
717 717 user_inherit_object_permissions = not ('hg.inherit_default_perms.false'
718 718 in self.permissions_global)
719 719
720 720 # default permissions inherited from `default` user permissions
721 721 self._calculate_default_permissions_repositories(
722 722 user_inherit_object_permissions)
723 723
724 724 self._calculate_default_permissions_repository_branches(
725 725 user_inherit_object_permissions)
726 726
727 727 self._calculate_default_permissions_repository_groups(
728 728 user_inherit_object_permissions)
729 729
730 730 self._calculate_default_permissions_user_groups(
731 731 user_inherit_object_permissions)
732 732
733 733 def _calculate_repository_permissions(self):
734 734 """
735 735 Repository access permissions for the current user.
736 736
737 737 Check if the user is part of user groups for this repository and
738 738 fill in the permission from it. `_choose_permission` decides of which
739 739 permission should be selected based on selected method.
740 740 """
741 741
742 742 # user group for repositories permissions
743 743 user_repo_perms_from_user_group = Permission\
744 744 .get_default_repo_perms_from_user_group(
745 745 self.user_id, self.scope_repo_id)
746 746
747 747 multiple_counter = collections.defaultdict(int)
748 748 for perm in user_repo_perms_from_user_group:
749 749 r_k = perm.UserGroupRepoToPerm.repository.repo_name
750 750 obj_id = perm.UserGroupRepoToPerm.repository.repo_id
751 751 multiple_counter[r_k] += 1
752 752 p = perm.Permission.permission_name
753 753 o = PermOrigin.REPO_USERGROUP % perm.UserGroupRepoToPerm\
754 754 .users_group.users_group_name
755 755
756 756 if multiple_counter[r_k] > 1:
757 757 cur_perm = self.permissions_repositories[r_k]
758 758 p = self._choose_permission(p, cur_perm)
759 759
760 760 self.permissions_repositories[r_k] = p, o, obj_id
761 761
762 762 if perm.Repository.user_id == self.user_id:
763 763 # set admin if owner
764 764 p = 'repository.admin'
765 765 o = PermOrigin.REPO_OWNER
766 766 self.permissions_repositories[r_k] = p, o, obj_id
767 767
768 768 if self.user_is_admin:
769 769 p = 'repository.admin'
770 770 o = PermOrigin.SUPER_ADMIN
771 771 self.permissions_repositories[r_k] = p, o, obj_id
772 772
773 773 # user explicit permissions for repositories, overrides any specified
774 774 # by the group permission
775 775 user_repo_perms = Permission.get_default_repo_perms(
776 776 self.user_id, self.scope_repo_id)
777 777 for perm in user_repo_perms:
778 778 r_k = perm.UserRepoToPerm.repository.repo_name
779 779 obj_id = perm.UserRepoToPerm.repository.repo_id
780 780 archived = perm.UserRepoToPerm.repository.archived
781 781 p = perm.Permission.permission_name
782 782 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
783 783
784 784 if not self.explicit:
785 785 cur_perm = self.permissions_repositories.get(
786 786 r_k, 'repository.none')
787 787 p = self._choose_permission(p, cur_perm)
788 788
789 789 self.permissions_repositories[r_k] = p, o, obj_id
790 790
791 791 if perm.Repository.user_id == self.user_id:
792 792 # set admin if owner
793 793 p = 'repository.admin'
794 794 o = PermOrigin.REPO_OWNER
795 795 self.permissions_repositories[r_k] = p, o, obj_id
796 796
797 797 if self.user_is_admin:
798 798 p = 'repository.admin'
799 799 o = PermOrigin.SUPER_ADMIN
800 800 self.permissions_repositories[r_k] = p, o, obj_id
801 801
802 802 # finally in case of archived repositories, we downgrade higher
803 803 # permissions to read
804 804 if archived:
805 805 current_perm = self.permissions_repositories[r_k]
806 806 if current_perm in ['repository.write', 'repository.admin']:
807 807 p = 'repository.read'
808 808 o = PermOrigin.ARCHIVED
809 809 self.permissions_repositories[r_k] = p, o, obj_id
810 810
811 811 def _calculate_repository_branch_permissions(self):
812 812 # user group for repositories permissions
813 813 user_repo_branch_perms_from_user_group = Permission\
814 814 .get_default_repo_branch_perms_from_user_group(
815 815 self.user_id, self.scope_repo_id)
816 816
817 817 multiple_counter = collections.defaultdict(int)
818 818 for perm in user_repo_branch_perms_from_user_group:
819 819 r_k = perm.UserGroupRepoToPerm.repository.repo_name
820 820 p = perm.Permission.permission_name
821 821 pattern = perm.UserGroupToRepoBranchPermission.branch_pattern
822 822 o = PermOrigin.REPO_USERGROUP % perm.UserGroupRepoToPerm\
823 823 .users_group.users_group_name
824 824
825 825 multiple_counter[r_k] += 1
826 826 if multiple_counter[r_k] > 1:
827 827 cur_perm = self.permissions_repository_branches[r_k][pattern]
828 828 p = self._choose_permission(p, cur_perm)
829 829
830 830 self.permissions_repository_branches[r_k] = pattern, p, o
831 831
832 832 # user explicit branch permissions for repositories, overrides
833 833 # any specified by the group permission
834 834 user_repo_branch_perms = Permission.get_default_repo_branch_perms(
835 835 self.user_id, self.scope_repo_id)
836 836
837 837 for perm in user_repo_branch_perms:
838 838
839 839 r_k = perm.UserRepoToPerm.repository.repo_name
840 840 p = perm.Permission.permission_name
841 841 pattern = perm.UserToRepoBranchPermission.branch_pattern
842 842 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
843 843
844 844 if not self.explicit:
845 845 cur_perm = self.permissions_repository_branches.get(r_k)
846 846 if cur_perm:
847 847 cur_perm = cur_perm[pattern]
848 848 cur_perm = cur_perm or 'branch.none'
849 849 p = self._choose_permission(p, cur_perm)
850 850
851 851 # NOTE(marcink): register all pattern/perm instances in this
852 852 # special dict that aggregates entries
853 853 self.permissions_repository_branches[r_k] = pattern, p, o
854 854
855 855 def _calculate_repository_group_permissions(self):
856 856 """
857 857 Repository group permissions for the current user.
858 858
859 859 Check if the user is part of user groups for repository groups and
860 860 fill in the permissions from it. `_choose_permission` decides of which
861 861 permission should be selected based on selected method.
862 862 """
863 863 # user group for repo groups permissions
864 864 user_repo_group_perms_from_user_group = Permission\
865 865 .get_default_group_perms_from_user_group(
866 866 self.user_id, self.scope_repo_group_id)
867 867
868 868 multiple_counter = collections.defaultdict(int)
869 869 for perm in user_repo_group_perms_from_user_group:
870 870 rg_k = perm.UserGroupRepoGroupToPerm.group.group_name
871 871 obj_id = perm.UserGroupRepoGroupToPerm.group.group_id
872 872 multiple_counter[rg_k] += 1
873 873 o = PermOrigin.REPOGROUP_USERGROUP % perm.UserGroupRepoGroupToPerm\
874 874 .users_group.users_group_name
875 875 p = perm.Permission.permission_name
876 876
877 877 if multiple_counter[rg_k] > 1:
878 878 cur_perm = self.permissions_repository_groups[rg_k]
879 879 p = self._choose_permission(p, cur_perm)
880 880 self.permissions_repository_groups[rg_k] = p, o, obj_id
881 881
882 882 if perm.RepoGroup.user_id == self.user_id:
883 883 # set admin if owner, even for member of other user group
884 884 p = 'group.admin'
885 885 o = PermOrigin.REPOGROUP_OWNER
886 886 self.permissions_repository_groups[rg_k] = p, o, obj_id
887 887
888 888 if self.user_is_admin:
889 889 p = 'group.admin'
890 890 o = PermOrigin.SUPER_ADMIN
891 891 self.permissions_repository_groups[rg_k] = p, o, obj_id
892 892
893 893 # user explicit permissions for repository groups
894 894 user_repo_groups_perms = Permission.get_default_group_perms(
895 895 self.user_id, self.scope_repo_group_id)
896 896 for perm in user_repo_groups_perms:
897 897 rg_k = perm.UserRepoGroupToPerm.group.group_name
898 898 obj_id = perm.UserRepoGroupToPerm.group.group_id
899 899 o = PermOrigin.REPOGROUP_USER % perm.UserRepoGroupToPerm\
900 900 .user.username
901 901 p = perm.Permission.permission_name
902 902
903 903 if not self.explicit:
904 904 cur_perm = self.permissions_repository_groups.get(rg_k, 'group.none')
905 905 p = self._choose_permission(p, cur_perm)
906 906
907 907 self.permissions_repository_groups[rg_k] = p, o, obj_id
908 908
909 909 if perm.RepoGroup.user_id == self.user_id:
910 910 # set admin if owner
911 911 p = 'group.admin'
912 912 o = PermOrigin.REPOGROUP_OWNER
913 913 self.permissions_repository_groups[rg_k] = p, o, obj_id
914 914
915 915 if self.user_is_admin:
916 916 p = 'group.admin'
917 917 o = PermOrigin.SUPER_ADMIN
918 918 self.permissions_repository_groups[rg_k] = p, o, obj_id
919 919
920 920 def _calculate_user_group_permissions(self):
921 921 """
922 922 User group permissions for the current user.
923 923 """
924 924 # user group for user group permissions
925 925 user_group_from_user_group = Permission\
926 926 .get_default_user_group_perms_from_user_group(
927 927 self.user_id, self.scope_user_group_id)
928 928
929 929 multiple_counter = collections.defaultdict(int)
930 930 for perm in user_group_from_user_group:
931 931 ug_k = perm.UserGroupUserGroupToPerm.target_user_group.users_group_name
932 932 obj_id = perm.UserGroupUserGroupToPerm.target_user_group.users_group_id
933 933 multiple_counter[ug_k] += 1
934 934 o = PermOrigin.USERGROUP_USERGROUP % perm.UserGroupUserGroupToPerm\
935 935 .user_group.users_group_name
936 936 p = perm.Permission.permission_name
937 937
938 938 if multiple_counter[ug_k] > 1:
939 939 cur_perm = self.permissions_user_groups[ug_k]
940 940 p = self._choose_permission(p, cur_perm)
941 941
942 942 self.permissions_user_groups[ug_k] = p, o, obj_id
943 943
944 944 if perm.UserGroup.user_id == self.user_id:
945 945 # set admin if owner, even for member of other user group
946 946 p = 'usergroup.admin'
947 947 o = PermOrigin.USERGROUP_OWNER
948 948 self.permissions_user_groups[ug_k] = p, o, obj_id
949 949
950 950 if self.user_is_admin:
951 951 p = 'usergroup.admin'
952 952 o = PermOrigin.SUPER_ADMIN
953 953 self.permissions_user_groups[ug_k] = p, o, obj_id
954 954
955 955 # user explicit permission for user groups
956 956 user_user_groups_perms = Permission.get_default_user_group_perms(
957 957 self.user_id, self.scope_user_group_id)
958 958 for perm in user_user_groups_perms:
959 959 ug_k = perm.UserUserGroupToPerm.user_group.users_group_name
960 960 obj_id = perm.UserUserGroupToPerm.user_group.users_group_id
961 961 o = PermOrigin.USERGROUP_USER % perm.UserUserGroupToPerm\
962 962 .user.username
963 963 p = perm.Permission.permission_name
964 964
965 965 if not self.explicit:
966 966 cur_perm = self.permissions_user_groups.get(ug_k, 'usergroup.none')
967 967 p = self._choose_permission(p, cur_perm)
968 968
969 969 self.permissions_user_groups[ug_k] = p, o, obj_id
970 970
971 971 if perm.UserGroup.user_id == self.user_id:
972 972 # set admin if owner
973 973 p = 'usergroup.admin'
974 974 o = PermOrigin.USERGROUP_OWNER
975 975 self.permissions_user_groups[ug_k] = p, o, obj_id
976 976
977 977 if self.user_is_admin:
978 978 p = 'usergroup.admin'
979 979 o = PermOrigin.SUPER_ADMIN
980 980 self.permissions_user_groups[ug_k] = p, o, obj_id
981 981
982 982 def _choose_permission(self, new_perm, cur_perm):
983 983 new_perm_val = Permission.PERM_WEIGHTS[new_perm]
984 984 cur_perm_val = Permission.PERM_WEIGHTS[cur_perm]
985 985 if self.algo == 'higherwin':
986 986 if new_perm_val > cur_perm_val:
987 987 return new_perm
988 988 return cur_perm
989 989 elif self.algo == 'lowerwin':
990 990 if new_perm_val < cur_perm_val:
991 991 return new_perm
992 992 return cur_perm
993 993
994 994 def _permission_structure(self):
995 995 return {
996 996 'global': self.permissions_global,
997 997 'repositories': self.permissions_repositories,
998 998 'repository_branches': self.permissions_repository_branches,
999 999 'repositories_groups': self.permissions_repository_groups,
1000 1000 'user_groups': self.permissions_user_groups,
1001 1001 }
1002 1002
1003 1003
1004 1004 def allowed_auth_token_access(view_name, auth_token, whitelist=None):
1005 1005 """
1006 1006 Check if given controller_name is in whitelist of auth token access
1007 1007 """
1008 1008 if not whitelist:
1009 1009 from rhodecode import CONFIG
1010 1010 whitelist = aslist(
1011 1011 CONFIG.get('api_access_controllers_whitelist'), sep=',')
1012 1012 # backward compat translation
1013 1013 compat = {
1014 1014 # old controller, new VIEW
1015 1015 'ChangesetController:*': 'RepoCommitsView:*',
1016 1016 'ChangesetController:changeset_patch': 'RepoCommitsView:repo_commit_patch',
1017 1017 'ChangesetController:changeset_raw': 'RepoCommitsView:repo_commit_raw',
1018 1018 'FilesController:raw': 'RepoCommitsView:repo_commit_raw',
1019 1019 'FilesController:archivefile': 'RepoFilesView:repo_archivefile',
1020 1020 'GistsController:*': 'GistView:*',
1021 1021 }
1022 1022
1023 1023 log.debug(
1024 1024 'Allowed views for AUTH TOKEN access: %s', whitelist)
1025 1025 auth_token_access_valid = False
1026 1026
1027 1027 for entry in whitelist:
1028 1028 token_match = True
1029 1029 if entry in compat:
1030 1030 # translate from old Controllers to Pyramid Views
1031 1031 entry = compat[entry]
1032 1032
1033 1033 if '@' in entry:
1034 1034 # specific AuthToken
1035 1035 entry, allowed_token = entry.split('@', 1)
1036 1036 token_match = auth_token == allowed_token
1037 1037
1038 1038 if fnmatch.fnmatch(view_name, entry) and token_match:
1039 1039 auth_token_access_valid = True
1040 1040 break
1041 1041
1042 1042 if auth_token_access_valid:
1043 1043 log.debug('view: `%s` matches entry in whitelist: %s',
1044 1044 view_name, whitelist)
1045 1045
1046 1046 else:
1047 1047 msg = ('view: `%s` does *NOT* match any entry in whitelist: %s'
1048 1048 % (view_name, whitelist))
1049 1049 if auth_token:
1050 1050 # if we use auth token key and don't have access it's a warning
1051 1051 log.warning(msg)
1052 1052 else:
1053 1053 log.debug(msg)
1054 1054
1055 1055 return auth_token_access_valid
1056 1056
1057 1057
1058 1058 class AuthUser(object):
1059 1059 """
1060 1060 A simple object that handles all attributes of user in RhodeCode
1061 1061
1062 1062 It does lookup based on API key,given user, or user present in session
1063 1063 Then it fills all required information for such user. It also checks if
1064 1064 anonymous access is enabled and if so, it returns default user as logged in
1065 1065 """
1066 1066 GLOBAL_PERMS = [x[0] for x in Permission.PERMS]
1067 1067 repo_read_perms = ['repository.read', 'repository.admin', 'repository.write']
1068 1068 repo_group_read_perms = ['group.read', 'group.write', 'group.admin']
1069 1069 user_group_read_perms = ['usergroup.read', 'usergroup.write', 'usergroup.admin']
1070 1070
1071 1071 def __init__(self, user_id=None, api_key=None, username=None, ip_addr=None):
1072 1072
1073 1073 self.user_id = user_id
1074 1074 self._api_key = api_key
1075 1075
1076 1076 self.api_key = None
1077 1077 self.username = username
1078 1078 self.ip_addr = ip_addr
1079 1079 self.name = ''
1080 1080 self.lastname = ''
1081 1081 self.first_name = ''
1082 1082 self.last_name = ''
1083 1083 self.email = ''
1084 1084 self.is_authenticated = False
1085 1085 self.admin = False
1086 1086 self.inherit_default_permissions = False
1087 1087 self.password = ''
1088 1088
1089 1089 self.anonymous_user = None # propagated on propagate_data
1090 1090 self.propagate_data()
1091 1091 self._instance = None
1092 1092 self._permissions_scoped_cache = {} # used to bind scoped calculation
1093 1093
1094 1094 @LazyProperty
1095 1095 def permissions(self):
1096 1096 return self.get_perms(user=self, cache=None)
1097 1097
1098 1098 @LazyProperty
1099 1099 def permissions_safe(self):
1100 1100 """
1101 1101 Filtered permissions excluding not allowed repositories
1102 1102 """
1103 1103 perms = self.get_perms(user=self, cache=None)
1104 1104
1105 1105 perms['repositories'] = {
1106 1106 k: v for k, v in perms['repositories'].items()
1107 1107 if v != 'repository.none'}
1108 1108 perms['repositories_groups'] = {
1109 1109 k: v for k, v in perms['repositories_groups'].items()
1110 1110 if v != 'group.none'}
1111 1111 perms['user_groups'] = {
1112 1112 k: v for k, v in perms['user_groups'].items()
1113 1113 if v != 'usergroup.none'}
1114 1114 perms['repository_branches'] = {
1115 1115 k: v for k, v in perms['repository_branches'].iteritems()
1116 1116 if v != 'branch.none'}
1117 1117 return perms
1118 1118
1119 1119 @LazyProperty
1120 1120 def permissions_full_details(self):
1121 1121 return self.get_perms(
1122 1122 user=self, cache=None, calculate_super_admin=True)
1123 1123
1124 1124 def permissions_with_scope(self, scope):
1125 1125 """
1126 1126 Call the get_perms function with scoped data. The scope in that function
1127 1127 narrows the SQL calls to the given ID of objects resulting in fetching
1128 1128 Just particular permission we want to obtain. If scope is an empty dict
1129 1129 then it basically narrows the scope to GLOBAL permissions only.
1130 1130
1131 1131 :param scope: dict
1132 1132 """
1133 1133 if 'repo_name' in scope:
1134 1134 obj = Repository.get_by_repo_name(scope['repo_name'])
1135 1135 if obj:
1136 1136 scope['repo_id'] = obj.repo_id
1137 1137 _scope = collections.OrderedDict()
1138 1138 _scope['repo_id'] = -1
1139 1139 _scope['user_group_id'] = -1
1140 1140 _scope['repo_group_id'] = -1
1141 1141
1142 1142 for k in sorted(scope.keys()):
1143 1143 _scope[k] = scope[k]
1144 1144
1145 1145 # store in cache to mimic how the @LazyProperty works,
1146 1146 # the difference here is that we use the unique key calculated
1147 1147 # from params and values
1148 1148 return self.get_perms(user=self, cache=None, scope=_scope)
1149 1149
1150 1150 def get_instance(self):
1151 1151 return User.get(self.user_id)
1152 1152
1153 1153 def propagate_data(self):
1154 1154 """
1155 1155 Fills in user data and propagates values to this instance. Maps fetched
1156 1156 user attributes to this class instance attributes
1157 1157 """
1158 1158 log.debug('AuthUser: starting data propagation for new potential user')
1159 1159 user_model = UserModel()
1160 1160 anon_user = self.anonymous_user = User.get_default_user(cache=True)
1161 1161 is_user_loaded = False
1162 1162
1163 1163 # lookup by userid
1164 1164 if self.user_id is not None and self.user_id != anon_user.user_id:
1165 1165 log.debug('Trying Auth User lookup by USER ID: `%s`', self.user_id)
1166 1166 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
1167 1167
1168 1168 # try go get user by api key
1169 1169 elif self._api_key and self._api_key != anon_user.api_key:
1170 1170 log.debug('Trying Auth User lookup by API KEY: `...%s`', self._api_key[-4:])
1171 1171 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
1172 1172
1173 1173 # lookup by username
1174 1174 elif self.username:
1175 1175 log.debug('Trying Auth User lookup by USER NAME: `%s`', self.username)
1176 1176 is_user_loaded = user_model.fill_data(self, username=self.username)
1177 1177 else:
1178 1178 log.debug('No data in %s that could been used to log in', self)
1179 1179
1180 1180 if not is_user_loaded:
1181 1181 log.debug(
1182 1182 'Failed to load user. Fallback to default user %s', anon_user)
1183 1183 # if we cannot authenticate user try anonymous
1184 1184 if anon_user.active:
1185 1185 log.debug('default user is active, using it as a session user')
1186 1186 user_model.fill_data(self, user_id=anon_user.user_id)
1187 1187 # then we set this user is logged in
1188 1188 self.is_authenticated = True
1189 1189 else:
1190 1190 log.debug('default user is NOT active')
1191 1191 # in case of disabled anonymous user we reset some of the
1192 1192 # parameters so such user is "corrupted", skipping the fill_data
1193 1193 for attr in ['user_id', 'username', 'admin', 'active']:
1194 1194 setattr(self, attr, None)
1195 1195 self.is_authenticated = False
1196 1196
1197 1197 if not self.username:
1198 1198 self.username = 'None'
1199 1199
1200 1200 log.debug('AuthUser: propagated user is now %s', self)
1201 1201
1202 1202 def get_perms(self, user, scope=None, explicit=True, algo='higherwin',
1203 1203 calculate_super_admin=False, cache=None):
1204 1204 """
1205 1205 Fills user permission attribute with permissions taken from database
1206 1206 works for permissions given for repositories, and for permissions that
1207 1207 are granted to groups
1208 1208
1209 1209 :param user: instance of User object from database
1210 1210 :param explicit: In case there are permissions both for user and a group
1211 1211 that user is part of, explicit flag will defiine if user will
1212 1212 explicitly override permissions from group, if it's False it will
1213 1213 make decision based on the algo
1214 1214 :param algo: algorithm to decide what permission should be choose if
1215 1215 it's multiple defined, eg user in two different groups. It also
1216 1216 decides if explicit flag is turned off how to specify the permission
1217 1217 for case when user is in a group + have defined separate permission
1218 1218 :param calculate_super_admin: calculate permissions for super-admin in the
1219 1219 same way as for regular user without speedups
1220 1220 :param cache: Use caching for calculation, None = let the cache backend decide
1221 1221 """
1222 1222 user_id = user.user_id
1223 1223 user_is_admin = user.is_admin
1224 1224
1225 1225 # inheritance of global permissions like create repo/fork repo etc
1226 1226 user_inherit_default_permissions = user.inherit_default_permissions
1227 1227
1228 1228 cache_seconds = safe_int(
1229 1229 rhodecode.CONFIG.get('rc_cache.cache_perms.expiration_time'))
1230 1230
1231 1231 if cache is None:
1232 1232 # let the backend cache decide
1233 1233 cache_on = cache_seconds > 0
1234 1234 else:
1235 1235 cache_on = cache
1236 1236
1237 1237 log.debug(
1238 1238 'Computing PERMISSION tree for user %s scope `%s` '
1239 1239 'with caching: %s[TTL: %ss]', user, scope, cache_on, cache_seconds or 0)
1240 1240
1241 1241 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
1242 1242 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1243 1243
1244 1244 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
1245 1245 condition=cache_on)
1246 1246 def compute_perm_tree(cache_name, cache_ver,
1247 1247 user_id, scope, user_is_admin,user_inherit_default_permissions,
1248 1248 explicit, algo, calculate_super_admin):
1249 1249 return _cached_perms_data(
1250 1250 user_id, scope, user_is_admin, user_inherit_default_permissions,
1251 1251 explicit, algo, calculate_super_admin)
1252 1252
1253 1253 start = time.time()
1254 1254 result = compute_perm_tree(
1255 1255 'permissions', 'v1', user_id, scope, user_is_admin,
1256 1256 user_inherit_default_permissions, explicit, algo,
1257 1257 calculate_super_admin)
1258 1258
1259 1259 result_repr = []
1260 1260 for k in result:
1261 1261 result_repr.append((k, len(result[k])))
1262 1262 total = time.time() - start
1263 1263 log.debug('PERMISSION tree for user %s computed in %.4fs: %s',
1264 1264 user, total, result_repr)
1265 1265
1266 1266 return result
1267 1267
1268 1268 @property
1269 1269 def is_default(self):
1270 1270 return self.username == User.DEFAULT_USER
1271 1271
1272 1272 @property
1273 1273 def is_admin(self):
1274 1274 return self.admin
1275 1275
1276 1276 @property
1277 1277 def is_user_object(self):
1278 1278 return self.user_id is not None
1279 1279
1280 1280 @property
1281 1281 def repositories_admin(self):
1282 1282 """
1283 1283 Returns list of repositories you're an admin of
1284 1284 """
1285 1285 return [
1286 1286 x[0] for x in self.permissions['repositories'].items()
1287 1287 if x[1] == 'repository.admin']
1288 1288
1289 1289 @property
1290 1290 def repository_groups_admin(self):
1291 1291 """
1292 1292 Returns list of repository groups you're an admin of
1293 1293 """
1294 1294 return [
1295 1295 x[0] for x in self.permissions['repositories_groups'].items()
1296 1296 if x[1] == 'group.admin']
1297 1297
1298 1298 @property
1299 1299 def user_groups_admin(self):
1300 1300 """
1301 1301 Returns list of user groups you're an admin of
1302 1302 """
1303 1303 return [
1304 1304 x[0] for x in self.permissions['user_groups'].items()
1305 1305 if x[1] == 'usergroup.admin']
1306 1306
1307 1307 def repo_acl_ids_from_stack(self, perms=None, prefix_filter=None, cache=False):
1308 1308 if not perms:
1309 1309 perms = AuthUser.repo_read_perms
1310 1310 allowed_ids = []
1311 1311 for k, stack_data in self.permissions['repositories'].perm_origin_stack.items():
1312 1312 perm, origin, obj_id = stack_data[-1] # last item is the current permission
1313 1313 if prefix_filter and not k.startswith(prefix_filter):
1314 1314 continue
1315 1315 if perm in perms:
1316 1316 allowed_ids.append(obj_id)
1317 1317 return allowed_ids
1318 1318
1319 1319 def repo_acl_ids(self, perms=None, name_filter=None, cache=False):
1320 1320 """
1321 1321 Returns list of repository ids that user have access to based on given
1322 1322 perms. The cache flag should be only used in cases that are used for
1323 1323 display purposes, NOT IN ANY CASE for permission checks.
1324 1324 """
1325 1325 from rhodecode.model.scm import RepoList
1326 1326 if not perms:
1327 1327 perms = AuthUser.repo_read_perms
1328 1328
1329 1329 if not isinstance(perms, list):
1330 1330 raise ValueError('perms parameter must be a list got {} instead'.format(perms))
1331 1331
1332 1332 def _cached_repo_acl(perm_def, _name_filter):
1333 1333 qry = Repository.query()
1334 1334 if _name_filter:
1335 1335 ilike_expression = u'%{}%'.format(safe_unicode(_name_filter))
1336 1336 qry = qry.filter(
1337 1337 Repository.repo_name.ilike(ilike_expression))
1338 1338
1339 1339 return [x.repo_id for x in
1340 1340 RepoList(qry, perm_set=perm_def, extra_kwargs={'user': self})]
1341 1341
1342 1342 log.debug('Computing REPO ACL IDS user %s', self)
1343 1343
1344 1344 cache_namespace_uid = 'cache_user_repo_acl_ids.{}'.format(self.user_id)
1345 1345 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1346 1346
1347 1347 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache)
1348 1348 def compute_repo_acl_ids(cache_ver, user_id, perm_def, _name_filter):
1349 1349 return _cached_repo_acl(perm_def, _name_filter)
1350 1350
1351 1351 start = time.time()
1352 1352 result = compute_repo_acl_ids('v1', self.user_id, perms, name_filter)
1353 1353 total = time.time() - start
1354 1354 log.debug('REPO ACL IDS for user %s computed in %.4fs', self, total)
1355 1355
1356 1356 return result
1357 1357
1358 1358 def repo_group_acl_ids_from_stack(self, perms=None, prefix_filter=None, cache=False):
1359 1359 if not perms:
1360 1360 perms = AuthUser.repo_group_read_perms
1361 1361 allowed_ids = []
1362 1362 for k, stack_data in self.permissions['repositories_groups'].perm_origin_stack.items():
1363 1363 perm, origin, obj_id = stack_data[-1] # last item is the current permission
1364 1364 if prefix_filter and not k.startswith(prefix_filter):
1365 1365 continue
1366 1366 if perm in perms:
1367 1367 allowed_ids.append(obj_id)
1368 1368 return allowed_ids
1369 1369
1370 1370 def repo_group_acl_ids(self, perms=None, name_filter=None, cache=False):
1371 1371 """
1372 1372 Returns list of repository group ids that user have access to based on given
1373 1373 perms. The cache flag should be only used in cases that are used for
1374 1374 display purposes, NOT IN ANY CASE for permission checks.
1375 1375 """
1376 1376 from rhodecode.model.scm import RepoGroupList
1377 1377 if not perms:
1378 1378 perms = AuthUser.repo_group_read_perms
1379 1379
1380 1380 if not isinstance(perms, list):
1381 1381 raise ValueError('perms parameter must be a list got {} instead'.format(perms))
1382 1382
1383 1383 def _cached_repo_group_acl(perm_def, _name_filter):
1384 1384 qry = RepoGroup.query()
1385 1385 if _name_filter:
1386 1386 ilike_expression = u'%{}%'.format(safe_unicode(_name_filter))
1387 1387 qry = qry.filter(
1388 1388 RepoGroup.group_name.ilike(ilike_expression))
1389 1389
1390 1390 return [x.group_id for x in
1391 1391 RepoGroupList(qry, perm_set=perm_def, extra_kwargs={'user': self})]
1392 1392
1393 1393 log.debug('Computing REPO GROUP ACL IDS user %s', self)
1394 1394
1395 1395 cache_namespace_uid = 'cache_user_repo_group_acl_ids.{}'.format(self.user_id)
1396 1396 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1397 1397
1398 1398 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache)
1399 1399 def compute_repo_group_acl_ids(cache_ver, user_id, perm_def, _name_filter):
1400 1400 return _cached_repo_group_acl(perm_def, _name_filter)
1401 1401
1402 1402 start = time.time()
1403 1403 result = compute_repo_group_acl_ids('v1', self.user_id, perms, name_filter)
1404 1404 total = time.time() - start
1405 1405 log.debug('REPO GROUP ACL IDS for user %s computed in %.4fs', self, total)
1406 1406
1407 1407 return result
1408 1408
1409 1409 def user_group_acl_ids_from_stack(self, perms=None, cache=False):
1410 1410 if not perms:
1411 1411 perms = AuthUser.user_group_read_perms
1412 1412 allowed_ids = []
1413 1413 for k, stack_data in self.permissions['user_groups'].perm_origin_stack.items():
1414 1414 perm, origin, obj_id = stack_data[-1] # last item is the current permission
1415 1415 if perm in perms:
1416 1416 allowed_ids.append(obj_id)
1417 1417 return allowed_ids
1418 1418
1419 1419 def user_group_acl_ids(self, perms=None, name_filter=None, cache=False):
1420 1420 """
1421 1421 Returns list of user group ids that user have access to based on given
1422 1422 perms. The cache flag should be only used in cases that are used for
1423 1423 display purposes, NOT IN ANY CASE for permission checks.
1424 1424 """
1425 1425 from rhodecode.model.scm import UserGroupList
1426 1426 if not perms:
1427 1427 perms = AuthUser.user_group_read_perms
1428 1428
1429 1429 if not isinstance(perms, list):
1430 1430 raise ValueError('perms parameter must be a list got {} instead'.format(perms))
1431 1431
1432 1432 def _cached_user_group_acl(perm_def, _name_filter):
1433 1433 qry = UserGroup.query()
1434 1434 if _name_filter:
1435 1435 ilike_expression = u'%{}%'.format(safe_unicode(_name_filter))
1436 1436 qry = qry.filter(
1437 1437 UserGroup.users_group_name.ilike(ilike_expression))
1438 1438
1439 1439 return [x.users_group_id for x in
1440 1440 UserGroupList(qry, perm_set=perm_def, extra_kwargs={'user': self})]
1441 1441
1442 1442 log.debug('Computing USER GROUP ACL IDS user %s', self)
1443 1443
1444 1444 cache_namespace_uid = 'cache_user_user_group_acl_ids.{}'.format(self.user_id)
1445 1445 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1446 1446
1447 1447 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache)
1448 1448 def compute_user_group_acl_ids(cache_ver, user_id, perm_def, _name_filter):
1449 1449 return _cached_user_group_acl(perm_def, _name_filter)
1450 1450
1451 1451 start = time.time()
1452 1452 result = compute_user_group_acl_ids('v1', self.user_id, perms, name_filter)
1453 1453 total = time.time() - start
1454 1454 log.debug('USER GROUP ACL IDS for user %s computed in %.4fs', self, total)
1455 1455
1456 1456 return result
1457 1457
1458 1458 @property
1459 1459 def ip_allowed(self):
1460 1460 """
1461 1461 Checks if ip_addr used in constructor is allowed from defined list of
1462 1462 allowed ip_addresses for user
1463 1463
1464 1464 :returns: boolean, True if ip is in allowed ip range
1465 1465 """
1466 1466 # check IP
1467 1467 inherit = self.inherit_default_permissions
1468 1468 return AuthUser.check_ip_allowed(self.user_id, self.ip_addr,
1469 1469 inherit_from_default=inherit)
1470 1470
1471 1471 @property
1472 1472 def personal_repo_group(self):
1473 1473 return RepoGroup.get_user_personal_repo_group(self.user_id)
1474 1474
1475 1475 @LazyProperty
1476 1476 def feed_token(self):
1477 1477 return self.get_instance().feed_token
1478 1478
1479 1479 @LazyProperty
1480 1480 def artifact_token(self):
1481 1481 return self.get_instance().artifact_token
1482 1482
1483 1483 @classmethod
1484 1484 def check_ip_allowed(cls, user_id, ip_addr, inherit_from_default):
1485 1485 allowed_ips = AuthUser.get_allowed_ips(
1486 1486 user_id, cache=True, inherit_from_default=inherit_from_default)
1487 1487 if check_ip_access(source_ip=ip_addr, allowed_ips=allowed_ips):
1488 1488 log.debug('IP:%s for user %s is in range of %s',
1489 1489 ip_addr, user_id, allowed_ips)
1490 1490 return True
1491 1491 else:
1492 1492 log.info('Access for IP:%s forbidden for user %s, '
1493 'not in %s', ip_addr, user_id, allowed_ips)
1493 'not in %s', ip_addr, user_id, allowed_ips,
1494 extra={"ip": ip_addr, "user_id": user_id})
1494 1495 return False
1495 1496
1496 1497 def get_branch_permissions(self, repo_name, perms=None):
1497 1498 perms = perms or self.permissions_with_scope({'repo_name': repo_name})
1498 1499 branch_perms = perms.get('repository_branches', {})
1499 1500 if not branch_perms:
1500 1501 return {}
1501 1502 repo_branch_perms = branch_perms.get(repo_name)
1502 1503 return repo_branch_perms or {}
1503 1504
1504 1505 def get_rule_and_branch_permission(self, repo_name, branch_name):
1505 1506 """
1506 1507 Check if this AuthUser has defined any permissions for branches. If any of
1507 1508 the rules match in order, we return the matching permissions
1508 1509 """
1509 1510
1510 1511 rule = default_perm = ''
1511 1512
1512 1513 repo_branch_perms = self.get_branch_permissions(repo_name=repo_name)
1513 1514 if not repo_branch_perms:
1514 1515 return rule, default_perm
1515 1516
1516 1517 # now calculate the permissions
1517 1518 for pattern, branch_perm in repo_branch_perms.items():
1518 1519 if fnmatch.fnmatch(branch_name, pattern):
1519 1520 rule = '`{}`=>{}'.format(pattern, branch_perm)
1520 1521 return rule, branch_perm
1521 1522
1522 1523 return rule, default_perm
1523 1524
1524 1525 def get_notice_messages(self):
1525 1526
1526 1527 notice_level = 'notice-error'
1527 1528 notice_messages = []
1528 1529 if self.is_default:
1529 1530 return [], notice_level
1530 1531
1531 1532 notices = UserNotice.query()\
1532 1533 .filter(UserNotice.user_id == self.user_id)\
1533 1534 .filter(UserNotice.notice_read == false())\
1534 1535 .all()
1535 1536
1536 1537 try:
1537 1538 for entry in notices:
1538 1539
1539 1540 msg = {
1540 1541 'msg_id': entry.user_notice_id,
1541 1542 'level': entry.notification_level,
1542 1543 'subject': entry.notice_subject,
1543 1544 'body': entry.notice_body,
1544 1545 }
1545 1546 notice_messages.append(msg)
1546 1547
1547 1548 log.debug('Got user %s %s messages', self, len(notice_messages))
1548 1549
1549 1550 levels = [x['level'] for x in notice_messages]
1550 1551 notice_level = 'notice-error' if 'error' in levels else 'notice-warning'
1551 1552 except Exception:
1552 1553 pass
1553 1554
1554 1555 return notice_messages, notice_level
1555 1556
1556 1557 def __repr__(self):
1557 1558 return self.repr_user(self.user_id, self.username, self.ip_addr, self.is_authenticated)
1558 1559
1559 1560 def set_authenticated(self, authenticated=True):
1560 1561 if self.user_id != self.anonymous_user.user_id:
1561 1562 self.is_authenticated = authenticated
1562 1563
1563 1564 def get_cookie_store(self):
1564 1565 return {
1565 1566 'username': self.username,
1566 1567 'password': md5(self.password or ''),
1567 1568 'user_id': self.user_id,
1568 1569 'is_authenticated': self.is_authenticated
1569 1570 }
1570 1571
1571 1572 @classmethod
1572 1573 def repr_user(cls, user_id=0, username='ANONYMOUS', ip='0.0.0.0', is_authenticated=False):
1573 1574 tmpl = "<AuthUser('id:{}[{}] ip:{} auth:{}')>"
1574 1575 return tmpl.format(user_id, username, ip, is_authenticated)
1575 1576
1576 1577 @classmethod
1577 1578 def from_cookie_store(cls, cookie_store):
1578 1579 """
1579 1580 Creates AuthUser from a cookie store
1580 1581
1581 1582 :param cls:
1582 1583 :param cookie_store:
1583 1584 """
1584 1585 user_id = cookie_store.get('user_id')
1585 1586 username = cookie_store.get('username')
1586 1587 api_key = cookie_store.get('api_key')
1587 1588 return AuthUser(user_id, api_key, username)
1588 1589
1589 1590 @classmethod
1590 1591 def get_allowed_ips(cls, user_id, cache=False, inherit_from_default=False):
1591 1592 _set = set()
1592 1593
1593 1594 if inherit_from_default:
1594 1595 def_user_id = User.get_default_user(cache=True).user_id
1595 1596 default_ips = UserIpMap.query().filter(UserIpMap.user_id == def_user_id)
1596 1597 if cache:
1597 1598 default_ips = default_ips.options(
1598 1599 FromCache("sql_cache_short", "get_user_ips_default"))
1599 1600
1600 1601 # populate from default user
1601 1602 for ip in default_ips:
1602 1603 try:
1603 1604 _set.add(ip.ip_addr)
1604 1605 except ObjectDeletedError:
1605 1606 # since we use heavy caching sometimes it happens that
1606 1607 # we get deleted objects here, we just skip them
1607 1608 pass
1608 1609
1609 1610 # NOTE:(marcink) we don't want to load any rules for empty
1610 1611 # user_id which is the case of access of non logged users when anonymous
1611 1612 # access is disabled
1612 1613 user_ips = []
1613 1614 if user_id:
1614 1615 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
1615 1616 if cache:
1616 1617 user_ips = user_ips.options(
1617 1618 FromCache("sql_cache_short", "get_user_ips_%s" % user_id))
1618 1619
1619 1620 for ip in user_ips:
1620 1621 try:
1621 1622 _set.add(ip.ip_addr)
1622 1623 except ObjectDeletedError:
1623 1624 # since we use heavy caching sometimes it happens that we get
1624 1625 # deleted objects here, we just skip them
1625 1626 pass
1626 1627 return _set or {ip for ip in ['0.0.0.0/0', '::/0']}
1627 1628
1628 1629
1629 1630 def set_available_permissions(settings):
1630 1631 """
1631 1632 This function will propagate pyramid settings with all available defined
1632 1633 permission given in db. We don't want to check each time from db for new
1633 1634 permissions since adding a new permission also requires application restart
1634 1635 ie. to decorate new views with the newly created permission
1635 1636
1636 1637 :param settings: current pyramid registry.settings
1637 1638
1638 1639 """
1639 1640 log.debug('auth: getting information about all available permissions')
1640 1641 try:
1641 1642 sa = meta.Session
1642 1643 all_perms = sa.query(Permission).all()
1643 1644 settings.setdefault('available_permissions',
1644 1645 [x.permission_name for x in all_perms])
1645 1646 log.debug('auth: set available permissions')
1646 1647 except Exception:
1647 1648 log.exception('Failed to fetch permissions from the database.')
1648 1649 raise
1649 1650
1650 1651
1651 1652 def get_csrf_token(session, force_new=False, save_if_missing=True):
1652 1653 """
1653 1654 Return the current authentication token, creating one if one doesn't
1654 1655 already exist and the save_if_missing flag is present.
1655 1656
1656 1657 :param session: pass in the pyramid session, else we use the global ones
1657 1658 :param force_new: force to re-generate the token and store it in session
1658 1659 :param save_if_missing: save the newly generated token if it's missing in
1659 1660 session
1660 1661 """
1661 1662 # NOTE(marcink): probably should be replaced with below one from pyramid 1.9
1662 1663 # from pyramid.csrf import get_csrf_token
1663 1664
1664 1665 if (csrf_token_key not in session and save_if_missing) or force_new:
1665 1666 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
1666 1667 session[csrf_token_key] = token
1667 1668 if hasattr(session, 'save'):
1668 1669 session.save()
1669 1670 return session.get(csrf_token_key)
1670 1671
1671 1672
1672 1673 def get_request(perm_class_instance):
1673 1674 from pyramid.threadlocal import get_current_request
1674 1675 pyramid_request = get_current_request()
1675 1676 return pyramid_request
1676 1677
1677 1678
1678 1679 # CHECK DECORATORS
1679 1680 class CSRFRequired(object):
1680 1681 """
1681 1682 Decorator for authenticating a form
1682 1683
1683 1684 This decorator uses an authorization token stored in the client's
1684 1685 session for prevention of certain Cross-site request forgery (CSRF)
1685 1686 attacks (See
1686 1687 http://en.wikipedia.org/wiki/Cross-site_request_forgery for more
1687 1688 information).
1688 1689
1689 1690 For use with the ``secure_form`` helper functions.
1690 1691
1691 1692 """
1692 1693 def __init__(self, token=csrf_token_key, header='X-CSRF-Token', except_methods=None):
1693 1694 self.token = token
1694 1695 self.header = header
1695 1696 self.except_methods = except_methods or []
1696 1697
1697 1698 def __call__(self, func):
1698 1699 return get_cython_compat_decorator(self.__wrapper, func)
1699 1700
1700 1701 def _get_csrf(self, _request):
1701 1702 return _request.POST.get(self.token, _request.headers.get(self.header))
1702 1703
1703 1704 def check_csrf(self, _request, cur_token):
1704 1705 supplied_token = self._get_csrf(_request)
1705 1706 return supplied_token and supplied_token == cur_token
1706 1707
1707 1708 def _get_request(self):
1708 1709 return get_request(self)
1709 1710
1710 1711 def __wrapper(self, func, *fargs, **fkwargs):
1711 1712 request = self._get_request()
1712 1713
1713 1714 if request.method in self.except_methods:
1714 1715 return func(*fargs, **fkwargs)
1715 1716
1716 1717 cur_token = get_csrf_token(request.session, save_if_missing=False)
1717 1718 if self.check_csrf(request, cur_token):
1718 1719 if request.POST.get(self.token):
1719 1720 del request.POST[self.token]
1720 1721 return func(*fargs, **fkwargs)
1721 1722 else:
1722 1723 reason = 'token-missing'
1723 1724 supplied_token = self._get_csrf(request)
1724 1725 if supplied_token and cur_token != supplied_token:
1725 1726 reason = 'token-mismatch [%s:%s]' % (
1726 1727 cur_token or ''[:6], supplied_token or ''[:6])
1727 1728
1728 1729 csrf_message = \
1729 1730 ("Cross-site request forgery detected, request denied. See "
1730 1731 "http://en.wikipedia.org/wiki/Cross-site_request_forgery for "
1731 1732 "more information.")
1732 1733 log.warn('Cross-site request forgery detected, request %r DENIED: %s '
1733 1734 'REMOTE_ADDR:%s, HEADERS:%s' % (
1734 1735 request, reason, request.remote_addr, request.headers))
1735 1736
1736 1737 raise HTTPForbidden(explanation=csrf_message)
1737 1738
1738 1739
1739 1740 class LoginRequired(object):
1740 1741 """
1741 1742 Must be logged in to execute this function else
1742 1743 redirect to login page
1743 1744
1744 1745 :param api_access: if enabled this checks only for valid auth token
1745 1746 and grants access based on valid token
1746 1747 """
1747 1748 def __init__(self, auth_token_access=None):
1748 1749 self.auth_token_access = auth_token_access
1749 1750 if self.auth_token_access:
1750 1751 valid_type = set(auth_token_access).intersection(set(UserApiKeys.ROLES))
1751 1752 if not valid_type:
1752 1753 raise ValueError('auth_token_access must be on of {}, got {}'.format(
1753 1754 UserApiKeys.ROLES, auth_token_access))
1754 1755
1755 1756 def __call__(self, func):
1756 1757 return get_cython_compat_decorator(self.__wrapper, func)
1757 1758
1758 1759 def _get_request(self):
1759 1760 return get_request(self)
1760 1761
1761 1762 def __wrapper(self, func, *fargs, **fkwargs):
1762 1763 from rhodecode.lib import helpers as h
1763 1764 cls = fargs[0]
1764 1765 user = cls._rhodecode_user
1765 1766 request = self._get_request()
1766 1767 _ = request.translate
1767 1768
1768 1769 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
1769 1770 log.debug('Starting login restriction checks for user: %s', user)
1770 1771 # check if our IP is allowed
1771 1772 ip_access_valid = True
1772 1773 if not user.ip_allowed:
1773 1774 h.flash(h.literal(_('IP {} not allowed'.format(user.ip_addr))),
1774 1775 category='warning')
1775 1776 ip_access_valid = False
1776 1777
1777 1778 # we used stored token that is extract from GET or URL param (if any)
1778 1779 _auth_token = request.user_auth_token
1779 1780
1780 1781 # check if we used an AUTH_TOKEN and it's a valid one
1781 1782 # defined white-list of controllers which API access will be enabled
1782 1783 whitelist = None
1783 1784 if self.auth_token_access:
1784 1785 # since this location is allowed by @LoginRequired decorator it's our
1785 1786 # only whitelist
1786 1787 whitelist = [loc]
1787 1788 auth_token_access_valid = allowed_auth_token_access(
1788 1789 loc, whitelist=whitelist, auth_token=_auth_token)
1789 1790
1790 1791 # explicit controller is enabled or API is in our whitelist
1791 1792 if auth_token_access_valid:
1792 1793 log.debug('Checking AUTH TOKEN access for %s', cls)
1793 1794 db_user = user.get_instance()
1794 1795
1795 1796 if db_user:
1796 1797 if self.auth_token_access:
1797 1798 roles = self.auth_token_access
1798 1799 else:
1799 1800 roles = [UserApiKeys.ROLE_HTTP]
1800 1801 log.debug('AUTH TOKEN: checking auth for user %s and roles %s',
1801 1802 db_user, roles)
1802 1803 token_match = db_user.authenticate_by_token(
1803 1804 _auth_token, roles=roles)
1804 1805 else:
1805 1806 log.debug('Unable to fetch db instance for auth user: %s', user)
1806 1807 token_match = False
1807 1808
1808 1809 if _auth_token and token_match:
1809 1810 auth_token_access_valid = True
1810 1811 log.debug('AUTH TOKEN ****%s is VALID', _auth_token[-4:])
1811 1812 else:
1812 1813 auth_token_access_valid = False
1813 1814 if not _auth_token:
1814 1815 log.debug("AUTH TOKEN *NOT* present in request")
1815 1816 else:
1816 1817 log.warning("AUTH TOKEN ****%s *NOT* valid", _auth_token[-4:])
1817 1818
1818 1819 log.debug('Checking if %s is authenticated @ %s', user.username, loc)
1819 1820 reason = 'RHODECODE_AUTH' if user.is_authenticated \
1820 1821 else 'AUTH_TOKEN_AUTH'
1821 1822
1822 1823 if ip_access_valid and (
1823 1824 user.is_authenticated or auth_token_access_valid):
1824 1825 log.info('user %s authenticating with:%s IS authenticated on func %s',
1825 1826 user, reason, loc)
1826 1827
1827 1828 return func(*fargs, **fkwargs)
1828 1829 else:
1829 1830 log.warning(
1830 1831 'user %s authenticating with:%s NOT authenticated on '
1831 1832 'func: %s: IP_ACCESS:%s AUTH_TOKEN_ACCESS:%s',
1832 1833 user, reason, loc, ip_access_valid, auth_token_access_valid)
1833 1834 # we preserve the get PARAM
1834 1835 came_from = get_came_from(request)
1835 1836
1836 1837 log.debug('redirecting to login page with %s', came_from)
1837 1838 raise HTTPFound(
1838 1839 h.route_path('login', _query={'came_from': came_from}))
1839 1840
1840 1841
1841 1842 class NotAnonymous(object):
1842 1843 """
1843 1844 Must be logged in to execute this function else
1844 1845 redirect to login page
1845 1846 """
1846 1847
1847 1848 def __call__(self, func):
1848 1849 return get_cython_compat_decorator(self.__wrapper, func)
1849 1850
1850 1851 def _get_request(self):
1851 1852 return get_request(self)
1852 1853
1853 1854 def __wrapper(self, func, *fargs, **fkwargs):
1854 1855 import rhodecode.lib.helpers as h
1855 1856 cls = fargs[0]
1856 1857 self.user = cls._rhodecode_user
1857 1858 request = self._get_request()
1858 1859 _ = request.translate
1859 1860 log.debug('Checking if user is not anonymous @%s', cls)
1860 1861
1861 1862 anonymous = self.user.username == User.DEFAULT_USER
1862 1863
1863 1864 if anonymous:
1864 1865 came_from = get_came_from(request)
1865 1866 h.flash(_('You need to be a registered user to '
1866 1867 'perform this action'),
1867 1868 category='warning')
1868 1869 raise HTTPFound(
1869 1870 h.route_path('login', _query={'came_from': came_from}))
1870 1871 else:
1871 1872 return func(*fargs, **fkwargs)
1872 1873
1873 1874
1874 1875 class PermsDecorator(object):
1875 1876 """
1876 1877 Base class for controller decorators, we extract the current user from
1877 1878 the class itself, which has it stored in base controllers
1878 1879 """
1879 1880
1880 1881 def __init__(self, *required_perms):
1881 1882 self.required_perms = set(required_perms)
1882 1883
1883 1884 def __call__(self, func):
1884 1885 return get_cython_compat_decorator(self.__wrapper, func)
1885 1886
1886 1887 def _get_request(self):
1887 1888 return get_request(self)
1888 1889
1889 1890 def __wrapper(self, func, *fargs, **fkwargs):
1890 1891 import rhodecode.lib.helpers as h
1891 1892 cls = fargs[0]
1892 1893 _user = cls._rhodecode_user
1893 1894 request = self._get_request()
1894 1895 _ = request.translate
1895 1896
1896 1897 log.debug('checking %s permissions %s for %s %s',
1897 1898 self.__class__.__name__, self.required_perms, cls, _user)
1898 1899
1899 1900 if self.check_permissions(_user):
1900 1901 log.debug('Permission granted for %s %s', cls, _user)
1901 1902 return func(*fargs, **fkwargs)
1902 1903
1903 1904 else:
1904 1905 log.debug('Permission denied for %s %s', cls, _user)
1905 1906 anonymous = _user.username == User.DEFAULT_USER
1906 1907
1907 1908 if anonymous:
1908 1909 came_from = get_came_from(self._get_request())
1909 1910 h.flash(_('You need to be signed in to view this page'),
1910 1911 category='warning')
1911 1912 raise HTTPFound(
1912 1913 h.route_path('login', _query={'came_from': came_from}))
1913 1914
1914 1915 else:
1915 1916 # redirect with 404 to prevent resource discovery
1916 1917 raise HTTPNotFound()
1917 1918
1918 1919 def check_permissions(self, user):
1919 1920 """Dummy function for overriding"""
1920 1921 raise NotImplementedError(
1921 1922 'You have to write this function in child class')
1922 1923
1923 1924
1924 1925 class HasPermissionAllDecorator(PermsDecorator):
1925 1926 """
1926 1927 Checks for access permission for all given predicates. All of them
1927 1928 have to be meet in order to fulfill the request
1928 1929 """
1929 1930
1930 1931 def check_permissions(self, user):
1931 1932 perms = user.permissions_with_scope({})
1932 1933 if self.required_perms.issubset(perms['global']):
1933 1934 return True
1934 1935 return False
1935 1936
1936 1937
1937 1938 class HasPermissionAnyDecorator(PermsDecorator):
1938 1939 """
1939 1940 Checks for access permission for any of given predicates. In order to
1940 1941 fulfill the request any of predicates must be meet
1941 1942 """
1942 1943
1943 1944 def check_permissions(self, user):
1944 1945 perms = user.permissions_with_scope({})
1945 1946 if self.required_perms.intersection(perms['global']):
1946 1947 return True
1947 1948 return False
1948 1949
1949 1950
1950 1951 class HasRepoPermissionAllDecorator(PermsDecorator):
1951 1952 """
1952 1953 Checks for access permission for all given predicates for specific
1953 1954 repository. All of them have to be meet in order to fulfill the request
1954 1955 """
1955 1956 def _get_repo_name(self):
1956 1957 _request = self._get_request()
1957 1958 return get_repo_slug(_request)
1958 1959
1959 1960 def check_permissions(self, user):
1960 1961 perms = user.permissions
1961 1962 repo_name = self._get_repo_name()
1962 1963
1963 1964 try:
1964 1965 user_perms = {perms['repositories'][repo_name]}
1965 1966 except KeyError:
1966 1967 log.debug('cannot locate repo with name: `%s` in permissions defs',
1967 1968 repo_name)
1968 1969 return False
1969 1970
1970 1971 log.debug('checking `%s` permissions for repo `%s`',
1971 1972 user_perms, repo_name)
1972 1973 if self.required_perms.issubset(user_perms):
1973 1974 return True
1974 1975 return False
1975 1976
1976 1977
1977 1978 class HasRepoPermissionAnyDecorator(PermsDecorator):
1978 1979 """
1979 1980 Checks for access permission for any of given predicates for specific
1980 1981 repository. In order to fulfill the request any of predicates must be meet
1981 1982 """
1982 1983 def _get_repo_name(self):
1983 1984 _request = self._get_request()
1984 1985 return get_repo_slug(_request)
1985 1986
1986 1987 def check_permissions(self, user):
1987 1988 perms = user.permissions
1988 1989 repo_name = self._get_repo_name()
1989 1990
1990 1991 try:
1991 1992 user_perms = {perms['repositories'][repo_name]}
1992 1993 except KeyError:
1993 1994 log.debug(
1994 1995 'cannot locate repo with name: `%s` in permissions defs',
1995 1996 repo_name)
1996 1997 return False
1997 1998
1998 1999 log.debug('checking `%s` permissions for repo `%s`',
1999 2000 user_perms, repo_name)
2000 2001 if self.required_perms.intersection(user_perms):
2001 2002 return True
2002 2003 return False
2003 2004
2004 2005
2005 2006 class HasRepoGroupPermissionAllDecorator(PermsDecorator):
2006 2007 """
2007 2008 Checks for access permission for all given predicates for specific
2008 2009 repository group. All of them have to be meet in order to
2009 2010 fulfill the request
2010 2011 """
2011 2012 def _get_repo_group_name(self):
2012 2013 _request = self._get_request()
2013 2014 return get_repo_group_slug(_request)
2014 2015
2015 2016 def check_permissions(self, user):
2016 2017 perms = user.permissions
2017 2018 group_name = self._get_repo_group_name()
2018 2019 try:
2019 2020 user_perms = {perms['repositories_groups'][group_name]}
2020 2021 except KeyError:
2021 2022 log.debug(
2022 2023 'cannot locate repo group with name: `%s` in permissions defs',
2023 2024 group_name)
2024 2025 return False
2025 2026
2026 2027 log.debug('checking `%s` permissions for repo group `%s`',
2027 2028 user_perms, group_name)
2028 2029 if self.required_perms.issubset(user_perms):
2029 2030 return True
2030 2031 return False
2031 2032
2032 2033
2033 2034 class HasRepoGroupPermissionAnyDecorator(PermsDecorator):
2034 2035 """
2035 2036 Checks for access permission for any of given predicates for specific
2036 2037 repository group. In order to fulfill the request any
2037 2038 of predicates must be met
2038 2039 """
2039 2040 def _get_repo_group_name(self):
2040 2041 _request = self._get_request()
2041 2042 return get_repo_group_slug(_request)
2042 2043
2043 2044 def check_permissions(self, user):
2044 2045 perms = user.permissions
2045 2046 group_name = self._get_repo_group_name()
2046 2047
2047 2048 try:
2048 2049 user_perms = {perms['repositories_groups'][group_name]}
2049 2050 except KeyError:
2050 2051 log.debug(
2051 2052 'cannot locate repo group with name: `%s` in permissions defs',
2052 2053 group_name)
2053 2054 return False
2054 2055
2055 2056 log.debug('checking `%s` permissions for repo group `%s`',
2056 2057 user_perms, group_name)
2057 2058 if self.required_perms.intersection(user_perms):
2058 2059 return True
2059 2060 return False
2060 2061
2061 2062
2062 2063 class HasUserGroupPermissionAllDecorator(PermsDecorator):
2063 2064 """
2064 2065 Checks for access permission for all given predicates for specific
2065 2066 user group. All of them have to be meet in order to fulfill the request
2066 2067 """
2067 2068 def _get_user_group_name(self):
2068 2069 _request = self._get_request()
2069 2070 return get_user_group_slug(_request)
2070 2071
2071 2072 def check_permissions(self, user):
2072 2073 perms = user.permissions
2073 2074 group_name = self._get_user_group_name()
2074 2075 try:
2075 2076 user_perms = {perms['user_groups'][group_name]}
2076 2077 except KeyError:
2077 2078 return False
2078 2079
2079 2080 if self.required_perms.issubset(user_perms):
2080 2081 return True
2081 2082 return False
2082 2083
2083 2084
2084 2085 class HasUserGroupPermissionAnyDecorator(PermsDecorator):
2085 2086 """
2086 2087 Checks for access permission for any of given predicates for specific
2087 2088 user group. In order to fulfill the request any of predicates must be meet
2088 2089 """
2089 2090 def _get_user_group_name(self):
2090 2091 _request = self._get_request()
2091 2092 return get_user_group_slug(_request)
2092 2093
2093 2094 def check_permissions(self, user):
2094 2095 perms = user.permissions
2095 2096 group_name = self._get_user_group_name()
2096 2097 try:
2097 2098 user_perms = {perms['user_groups'][group_name]}
2098 2099 except KeyError:
2099 2100 return False
2100 2101
2101 2102 if self.required_perms.intersection(user_perms):
2102 2103 return True
2103 2104 return False
2104 2105
2105 2106
2106 2107 # CHECK FUNCTIONS
2107 2108 class PermsFunction(object):
2108 2109 """Base function for other check functions"""
2109 2110
2110 2111 def __init__(self, *perms):
2111 2112 self.required_perms = set(perms)
2112 2113 self.repo_name = None
2113 2114 self.repo_group_name = None
2114 2115 self.user_group_name = None
2115 2116
2116 2117 def __bool__(self):
2117 2118 import inspect
2118 2119 frame = inspect.currentframe()
2119 2120 stack_trace = traceback.format_stack(frame)
2120 2121 log.error('Checking bool value on a class instance of perm '
2121 2122 'function is not allowed: %s', ''.join(stack_trace))
2122 2123 # rather than throwing errors, here we always return False so if by
2123 2124 # accident someone checks truth for just an instance it will always end
2124 2125 # up in returning False
2125 2126 return False
2126 2127 __nonzero__ = __bool__
2127 2128
2128 2129 def __call__(self, check_location='', user=None):
2129 2130 if not user:
2130 2131 log.debug('Using user attribute from global request')
2131 2132 request = self._get_request()
2132 2133 user = request.user
2133 2134
2134 2135 # init auth user if not already given
2135 2136 if not isinstance(user, AuthUser):
2136 2137 log.debug('Wrapping user %s into AuthUser', user)
2137 2138 user = AuthUser(user.user_id)
2138 2139
2139 2140 cls_name = self.__class__.__name__
2140 2141 check_scope = self._get_check_scope(cls_name)
2141 2142 check_location = check_location or 'unspecified location'
2142 2143
2143 2144 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
2144 2145 self.required_perms, user, check_scope, check_location)
2145 2146 if not user:
2146 2147 log.warning('Empty user given for permission check')
2147 2148 return False
2148 2149
2149 2150 if self.check_permissions(user):
2150 2151 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
2151 2152 check_scope, user, check_location)
2152 2153 return True
2153 2154
2154 2155 else:
2155 2156 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
2156 2157 check_scope, user, check_location)
2157 2158 return False
2158 2159
2159 2160 def _get_request(self):
2160 2161 return get_request(self)
2161 2162
2162 2163 def _get_check_scope(self, cls_name):
2163 2164 return {
2164 2165 'HasPermissionAll': 'GLOBAL',
2165 2166 'HasPermissionAny': 'GLOBAL',
2166 2167 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
2167 2168 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
2168 2169 'HasRepoGroupPermissionAll': 'repo_group:%s' % self.repo_group_name,
2169 2170 'HasRepoGroupPermissionAny': 'repo_group:%s' % self.repo_group_name,
2170 2171 'HasUserGroupPermissionAll': 'user_group:%s' % self.user_group_name,
2171 2172 'HasUserGroupPermissionAny': 'user_group:%s' % self.user_group_name,
2172 2173 }.get(cls_name, '?:%s' % cls_name)
2173 2174
2174 2175 def check_permissions(self, user):
2175 2176 """Dummy function for overriding"""
2176 2177 raise Exception('You have to write this function in child class')
2177 2178
2178 2179
2179 2180 class HasPermissionAll(PermsFunction):
2180 2181 def check_permissions(self, user):
2181 2182 perms = user.permissions_with_scope({})
2182 2183 if self.required_perms.issubset(perms.get('global')):
2183 2184 return True
2184 2185 return False
2185 2186
2186 2187
2187 2188 class HasPermissionAny(PermsFunction):
2188 2189 def check_permissions(self, user):
2189 2190 perms = user.permissions_with_scope({})
2190 2191 if self.required_perms.intersection(perms.get('global')):
2191 2192 return True
2192 2193 return False
2193 2194
2194 2195
2195 2196 class HasRepoPermissionAll(PermsFunction):
2196 2197 def __call__(self, repo_name=None, check_location='', user=None):
2197 2198 self.repo_name = repo_name
2198 2199 return super(HasRepoPermissionAll, self).__call__(check_location, user)
2199 2200
2200 2201 def _get_repo_name(self):
2201 2202 if not self.repo_name:
2202 2203 _request = self._get_request()
2203 2204 self.repo_name = get_repo_slug(_request)
2204 2205 return self.repo_name
2205 2206
2206 2207 def check_permissions(self, user):
2207 2208 self.repo_name = self._get_repo_name()
2208 2209 perms = user.permissions
2209 2210 try:
2210 2211 user_perms = {perms['repositories'][self.repo_name]}
2211 2212 except KeyError:
2212 2213 return False
2213 2214 if self.required_perms.issubset(user_perms):
2214 2215 return True
2215 2216 return False
2216 2217
2217 2218
2218 2219 class HasRepoPermissionAny(PermsFunction):
2219 2220 def __call__(self, repo_name=None, check_location='', user=None):
2220 2221 self.repo_name = repo_name
2221 2222 return super(HasRepoPermissionAny, self).__call__(check_location, user)
2222 2223
2223 2224 def _get_repo_name(self):
2224 2225 if not self.repo_name:
2225 2226 _request = self._get_request()
2226 2227 self.repo_name = get_repo_slug(_request)
2227 2228 return self.repo_name
2228 2229
2229 2230 def check_permissions(self, user):
2230 2231 self.repo_name = self._get_repo_name()
2231 2232 perms = user.permissions
2232 2233 try:
2233 2234 user_perms = {perms['repositories'][self.repo_name]}
2234 2235 except KeyError:
2235 2236 return False
2236 2237 if self.required_perms.intersection(user_perms):
2237 2238 return True
2238 2239 return False
2239 2240
2240 2241
2241 2242 class HasRepoGroupPermissionAny(PermsFunction):
2242 2243 def __call__(self, group_name=None, check_location='', user=None):
2243 2244 self.repo_group_name = group_name
2244 2245 return super(HasRepoGroupPermissionAny, self).__call__(check_location, user)
2245 2246
2246 2247 def check_permissions(self, user):
2247 2248 perms = user.permissions
2248 2249 try:
2249 2250 user_perms = {perms['repositories_groups'][self.repo_group_name]}
2250 2251 except KeyError:
2251 2252 return False
2252 2253 if self.required_perms.intersection(user_perms):
2253 2254 return True
2254 2255 return False
2255 2256
2256 2257
2257 2258 class HasRepoGroupPermissionAll(PermsFunction):
2258 2259 def __call__(self, group_name=None, check_location='', user=None):
2259 2260 self.repo_group_name = group_name
2260 2261 return super(HasRepoGroupPermissionAll, self).__call__(check_location, user)
2261 2262
2262 2263 def check_permissions(self, user):
2263 2264 perms = user.permissions
2264 2265 try:
2265 2266 user_perms = {perms['repositories_groups'][self.repo_group_name]}
2266 2267 except KeyError:
2267 2268 return False
2268 2269 if self.required_perms.issubset(user_perms):
2269 2270 return True
2270 2271 return False
2271 2272
2272 2273
2273 2274 class HasUserGroupPermissionAny(PermsFunction):
2274 2275 def __call__(self, user_group_name=None, check_location='', user=None):
2275 2276 self.user_group_name = user_group_name
2276 2277 return super(HasUserGroupPermissionAny, self).__call__(check_location, user)
2277 2278
2278 2279 def check_permissions(self, user):
2279 2280 perms = user.permissions
2280 2281 try:
2281 2282 user_perms = {perms['user_groups'][self.user_group_name]}
2282 2283 except KeyError:
2283 2284 return False
2284 2285 if self.required_perms.intersection(user_perms):
2285 2286 return True
2286 2287 return False
2287 2288
2288 2289
2289 2290 class HasUserGroupPermissionAll(PermsFunction):
2290 2291 def __call__(self, user_group_name=None, check_location='', user=None):
2291 2292 self.user_group_name = user_group_name
2292 2293 return super(HasUserGroupPermissionAll, self).__call__(check_location, user)
2293 2294
2294 2295 def check_permissions(self, user):
2295 2296 perms = user.permissions
2296 2297 try:
2297 2298 user_perms = {perms['user_groups'][self.user_group_name]}
2298 2299 except KeyError:
2299 2300 return False
2300 2301 if self.required_perms.issubset(user_perms):
2301 2302 return True
2302 2303 return False
2303 2304
2304 2305
2305 2306 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
2306 2307 class HasPermissionAnyMiddleware(object):
2307 2308 def __init__(self, *perms):
2308 2309 self.required_perms = set(perms)
2309 2310
2310 2311 def __call__(self, auth_user, repo_name):
2311 2312 # repo_name MUST be unicode, since we handle keys in permission
2312 2313 # dict by unicode
2313 2314 repo_name = safe_unicode(repo_name)
2314 2315 log.debug(
2315 2316 'Checking VCS protocol permissions %s for user:%s repo:`%s`',
2316 2317 self.required_perms, auth_user, repo_name)
2317 2318
2318 2319 if self.check_permissions(auth_user, repo_name):
2319 2320 log.debug('Permission to repo:`%s` GRANTED for user:%s @ %s',
2320 2321 repo_name, auth_user, 'PermissionMiddleware')
2321 2322 return True
2322 2323
2323 2324 else:
2324 2325 log.debug('Permission to repo:`%s` DENIED for user:%s @ %s',
2325 2326 repo_name, auth_user, 'PermissionMiddleware')
2326 2327 return False
2327 2328
2328 2329 def check_permissions(self, user, repo_name):
2329 2330 perms = user.permissions_with_scope({'repo_name': repo_name})
2330 2331
2331 2332 try:
2332 2333 user_perms = {perms['repositories'][repo_name]}
2333 2334 except Exception:
2334 2335 log.exception('Error while accessing user permissions')
2335 2336 return False
2336 2337
2337 2338 if self.required_perms.intersection(user_perms):
2338 2339 return True
2339 2340 return False
2340 2341
2341 2342
2342 2343 # SPECIAL VERSION TO HANDLE API AUTH
2343 2344 class _BaseApiPerm(object):
2344 2345 def __init__(self, *perms):
2345 2346 self.required_perms = set(perms)
2346 2347
2347 2348 def __call__(self, check_location=None, user=None, repo_name=None,
2348 2349 group_name=None, user_group_name=None):
2349 2350 cls_name = self.__class__.__name__
2350 2351 check_scope = 'global:%s' % (self.required_perms,)
2351 2352 if repo_name:
2352 2353 check_scope += ', repo_name:%s' % (repo_name,)
2353 2354
2354 2355 if group_name:
2355 2356 check_scope += ', repo_group_name:%s' % (group_name,)
2356 2357
2357 2358 if user_group_name:
2358 2359 check_scope += ', user_group_name:%s' % (user_group_name,)
2359 2360
2360 2361 log.debug('checking cls:%s %s %s @ %s',
2361 2362 cls_name, self.required_perms, check_scope, check_location)
2362 2363 if not user:
2363 2364 log.debug('Empty User passed into arguments')
2364 2365 return False
2365 2366
2366 2367 # process user
2367 2368 if not isinstance(user, AuthUser):
2368 2369 user = AuthUser(user.user_id)
2369 2370 if not check_location:
2370 2371 check_location = 'unspecified'
2371 2372 if self.check_permissions(user.permissions, repo_name, group_name,
2372 2373 user_group_name):
2373 2374 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
2374 2375 check_scope, user, check_location)
2375 2376 return True
2376 2377
2377 2378 else:
2378 2379 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
2379 2380 check_scope, user, check_location)
2380 2381 return False
2381 2382
2382 2383 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2383 2384 user_group_name=None):
2384 2385 """
2385 2386 implement in child class should return True if permissions are ok,
2386 2387 False otherwise
2387 2388
2388 2389 :param perm_defs: dict with permission definitions
2389 2390 :param repo_name: repo name
2390 2391 """
2391 2392 raise NotImplementedError()
2392 2393
2393 2394
2394 2395 class HasPermissionAllApi(_BaseApiPerm):
2395 2396 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2396 2397 user_group_name=None):
2397 2398 if self.required_perms.issubset(perm_defs.get('global')):
2398 2399 return True
2399 2400 return False
2400 2401
2401 2402
2402 2403 class HasPermissionAnyApi(_BaseApiPerm):
2403 2404 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2404 2405 user_group_name=None):
2405 2406 if self.required_perms.intersection(perm_defs.get('global')):
2406 2407 return True
2407 2408 return False
2408 2409
2409 2410
2410 2411 class HasRepoPermissionAllApi(_BaseApiPerm):
2411 2412 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2412 2413 user_group_name=None):
2413 2414 try:
2414 2415 _user_perms = {perm_defs['repositories'][repo_name]}
2415 2416 except KeyError:
2416 2417 log.warning(traceback.format_exc())
2417 2418 return False
2418 2419 if self.required_perms.issubset(_user_perms):
2419 2420 return True
2420 2421 return False
2421 2422
2422 2423
2423 2424 class HasRepoPermissionAnyApi(_BaseApiPerm):
2424 2425 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2425 2426 user_group_name=None):
2426 2427 try:
2427 2428 _user_perms = {perm_defs['repositories'][repo_name]}
2428 2429 except KeyError:
2429 2430 log.warning(traceback.format_exc())
2430 2431 return False
2431 2432 if self.required_perms.intersection(_user_perms):
2432 2433 return True
2433 2434 return False
2434 2435
2435 2436
2436 2437 class HasRepoGroupPermissionAnyApi(_BaseApiPerm):
2437 2438 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2438 2439 user_group_name=None):
2439 2440 try:
2440 2441 _user_perms = {perm_defs['repositories_groups'][group_name]}
2441 2442 except KeyError:
2442 2443 log.warning(traceback.format_exc())
2443 2444 return False
2444 2445 if self.required_perms.intersection(_user_perms):
2445 2446 return True
2446 2447 return False
2447 2448
2448 2449
2449 2450 class HasRepoGroupPermissionAllApi(_BaseApiPerm):
2450 2451 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2451 2452 user_group_name=None):
2452 2453 try:
2453 2454 _user_perms = {perm_defs['repositories_groups'][group_name]}
2454 2455 except KeyError:
2455 2456 log.warning(traceback.format_exc())
2456 2457 return False
2457 2458 if self.required_perms.issubset(_user_perms):
2458 2459 return True
2459 2460 return False
2460 2461
2461 2462
2462 2463 class HasUserGroupPermissionAnyApi(_BaseApiPerm):
2463 2464 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2464 2465 user_group_name=None):
2465 2466 try:
2466 2467 _user_perms = {perm_defs['user_groups'][user_group_name]}
2467 2468 except KeyError:
2468 2469 log.warning(traceback.format_exc())
2469 2470 return False
2470 2471 if self.required_perms.intersection(_user_perms):
2471 2472 return True
2472 2473 return False
2473 2474
2474 2475
2475 2476 def check_ip_access(source_ip, allowed_ips=None):
2476 2477 """
2477 2478 Checks if source_ip is a subnet of any of allowed_ips.
2478 2479
2479 2480 :param source_ip:
2480 2481 :param allowed_ips: list of allowed ips together with mask
2481 2482 """
2482 2483 log.debug('checking if ip:%s is subnet of %s', source_ip, allowed_ips)
2483 2484 source_ip_address = ipaddress.ip_address(safe_unicode(source_ip))
2484 2485 if isinstance(allowed_ips, (tuple, list, set)):
2485 2486 for ip in allowed_ips:
2486 2487 ip = safe_unicode(ip)
2487 2488 try:
2488 2489 network_address = ipaddress.ip_network(ip, strict=False)
2489 2490 if source_ip_address in network_address:
2490 2491 log.debug('IP %s is network %s', source_ip_address, network_address)
2491 2492 return True
2492 2493 # for any case we cannot determine the IP, don't crash just
2493 2494 # skip it and log as error, we want to say forbidden still when
2494 2495 # sending bad IP
2495 2496 except Exception:
2496 2497 log.error(traceback.format_exc())
2497 2498 continue
2498 2499 return False
2499 2500
2500 2501
2501 2502 def get_cython_compat_decorator(wrapper, func):
2502 2503 """
2503 2504 Creates a cython compatible decorator. The previously used
2504 2505 decorator.decorator() function seems to be incompatible with cython.
2505 2506
2506 2507 :param wrapper: __wrapper method of the decorator class
2507 2508 :param func: decorated function
2508 2509 """
2509 2510 @wraps(func)
2510 2511 def local_wrapper(*args, **kwds):
2511 2512 return wrapper(func, *args, **kwds)
2512 2513 local_wrapper.__wrapped__ = func
2513 2514 return local_wrapper
2514 2515
2515 2516
@@ -1,680 +1,680 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 Database creation, and setup module for RhodeCode Enterprise. Used for creation
23 23 of database as well as for migration operations
24 24 """
25 25
26 26 import os
27 27 import sys
28 28 import time
29 29 import uuid
30 30 import logging
31 31 import getpass
32 32 from os.path import dirname as dn, join as jn
33 33
34 34 from sqlalchemy.engine import create_engine
35 35
36 36 from rhodecode import __dbversion__
37 37 from rhodecode.model import init_model
38 38 from rhodecode.model.user import UserModel
39 39 from rhodecode.model.db import (
40 40 User, Permission, RhodeCodeUi, RhodeCodeSetting, UserToPerm,
41 41 DbMigrateVersion, RepoGroup, UserRepoGroupToPerm, CacheKey, Repository)
42 42 from rhodecode.model.meta import Session, Base
43 43 from rhodecode.model.permission import PermissionModel
44 44 from rhodecode.model.repo import RepoModel
45 45 from rhodecode.model.repo_group import RepoGroupModel
46 46 from rhodecode.model.settings import SettingsModel
47 47
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51
52 52 def notify(msg):
53 53 """
54 54 Notification for migrations messages
55 55 """
56 56 ml = len(msg) + (4 * 2)
57 57 print(('\n%s\n*** %s ***\n%s' % ('*' * ml, msg, '*' * ml)).upper())
58 58
59 59
60 60 class DbManage(object):
61 61
62 62 def __init__(self, log_sql, dbconf, root, tests=False,
63 63 SESSION=None, cli_args=None):
64 64 self.dbname = dbconf.split('/')[-1]
65 65 self.tests = tests
66 66 self.root = root
67 67 self.dburi = dbconf
68 68 self.log_sql = log_sql
69 69 self.cli_args = cli_args or {}
70 70 self.init_db(SESSION=SESSION)
71 71 self.ask_ok = self.get_ask_ok_func(self.cli_args.get('force_ask'))
72 72
73 73 def db_exists(self):
74 74 if not self.sa:
75 75 self.init_db()
76 76 try:
77 77 self.sa.query(RhodeCodeUi)\
78 78 .filter(RhodeCodeUi.ui_key == '/')\
79 79 .scalar()
80 80 return True
81 81 except Exception:
82 82 return False
83 83 finally:
84 84 self.sa.rollback()
85 85
86 86 def get_ask_ok_func(self, param):
87 87 if param not in [None]:
88 88 # return a function lambda that has a default set to param
89 89 return lambda *args, **kwargs: param
90 90 else:
91 91 from rhodecode.lib.utils import ask_ok
92 92 return ask_ok
93 93
94 94 def init_db(self, SESSION=None):
95 95 if SESSION:
96 96 self.sa = SESSION
97 97 else:
98 98 # init new sessions
99 99 engine = create_engine(self.dburi, echo=self.log_sql)
100 100 init_model(engine)
101 101 self.sa = Session()
102 102
103 103 def create_tables(self, override=False):
104 104 """
105 105 Create a auth database
106 106 """
107 107
108 108 log.info("Existing database with the same name is going to be destroyed.")
109 109 log.info("Setup command will run DROP ALL command on that database.")
110 110 if self.tests:
111 111 destroy = True
112 112 else:
113 113 destroy = self.ask_ok('Are you sure that you want to destroy the old database? [y/n]')
114 114 if not destroy:
115 log.info('Nothing done.')
115 log.info('db tables bootstrap: Nothing done.')
116 116 sys.exit(0)
117 117 if destroy:
118 118 Base.metadata.drop_all()
119 119
120 120 checkfirst = not override
121 121 Base.metadata.create_all(checkfirst=checkfirst)
122 122 log.info('Created tables for %s', self.dbname)
123 123
124 124 def set_db_version(self):
125 125 ver = DbMigrateVersion()
126 126 ver.version = __dbversion__
127 127 ver.repository_id = 'rhodecode_db_migrations'
128 128 ver.repository_path = 'versions'
129 129 self.sa.add(ver)
130 130 log.info('db version set to: %s', __dbversion__)
131 131
132 132 def run_post_migration_tasks(self):
133 133 """
134 134 Run various tasks before actually doing migrations
135 135 """
136 136 # delete cache keys on each upgrade
137 137 total = CacheKey.query().count()
138 138 log.info("Deleting (%s) cache keys now...", total)
139 139 CacheKey.delete_all_cache()
140 140
141 141 def upgrade(self, version=None):
142 142 """
143 143 Upgrades given database schema to given revision following
144 144 all needed steps, to perform the upgrade
145 145
146 146 """
147 147
148 148 from rhodecode.lib.dbmigrate.migrate.versioning import api
149 149 from rhodecode.lib.dbmigrate.migrate.exceptions import \
150 150 DatabaseNotControlledError
151 151
152 152 if 'sqlite' in self.dburi:
153 153 print(
154 154 '********************** WARNING **********************\n'
155 155 'Make sure your version of sqlite is at least 3.7.X. \n'
156 156 'Earlier versions are known to fail on some migrations\n'
157 157 '*****************************************************\n')
158 158
159 159 upgrade = self.ask_ok(
160 160 'You are about to perform a database upgrade. Make '
161 161 'sure you have backed up your database. '
162 162 'Continue ? [y/n]')
163 163 if not upgrade:
164 164 log.info('No upgrade performed')
165 165 sys.exit(0)
166 166
167 167 repository_path = jn(dn(dn(dn(os.path.realpath(__file__)))),
168 168 'rhodecode/lib/dbmigrate')
169 169 db_uri = self.dburi
170 170
171 171 if version:
172 172 DbMigrateVersion.set_version(version)
173 173
174 174 try:
175 175 curr_version = api.db_version(db_uri, repository_path)
176 176 msg = ('Found current database db_uri under version '
177 177 'control with version {}'.format(curr_version))
178 178
179 179 except (RuntimeError, DatabaseNotControlledError):
180 180 curr_version = 1
181 181 msg = ('Current database is not under version control. Setting '
182 182 'as version %s' % curr_version)
183 183 api.version_control(db_uri, repository_path, curr_version)
184 184
185 185 notify(msg)
186 186
187 187
188 188 if curr_version == __dbversion__:
189 189 log.info('This database is already at the newest version')
190 190 sys.exit(0)
191 191
192 192 upgrade_steps = range(curr_version + 1, __dbversion__ + 1)
193 193 notify('attempting to upgrade database from '
194 194 'version %s to version %s' % (curr_version, __dbversion__))
195 195
196 196 # CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE
197 197 _step = None
198 198 for step in upgrade_steps:
199 199 notify('performing upgrade step %s' % step)
200 200 time.sleep(0.5)
201 201
202 202 api.upgrade(db_uri, repository_path, step)
203 203 self.sa.rollback()
204 204 notify('schema upgrade for step %s completed' % (step,))
205 205
206 206 _step = step
207 207
208 208 self.run_post_migration_tasks()
209 209 notify('upgrade to version %s successful' % _step)
210 210
211 211 def fix_repo_paths(self):
212 212 """
213 213 Fixes an old RhodeCode version path into new one without a '*'
214 214 """
215 215
216 216 paths = self.sa.query(RhodeCodeUi)\
217 217 .filter(RhodeCodeUi.ui_key == '/')\
218 218 .scalar()
219 219
220 220 paths.ui_value = paths.ui_value.replace('*', '')
221 221
222 222 try:
223 223 self.sa.add(paths)
224 224 self.sa.commit()
225 225 except Exception:
226 226 self.sa.rollback()
227 227 raise
228 228
229 229 def fix_default_user(self):
230 230 """
231 231 Fixes an old default user with some 'nicer' default values,
232 232 used mostly for anonymous access
233 233 """
234 234 def_user = self.sa.query(User)\
235 235 .filter(User.username == User.DEFAULT_USER)\
236 236 .one()
237 237
238 238 def_user.name = 'Anonymous'
239 239 def_user.lastname = 'User'
240 240 def_user.email = User.DEFAULT_USER_EMAIL
241 241
242 242 try:
243 243 self.sa.add(def_user)
244 244 self.sa.commit()
245 245 except Exception:
246 246 self.sa.rollback()
247 247 raise
248 248
249 249 def fix_settings(self):
250 250 """
251 251 Fixes rhodecode settings and adds ga_code key for google analytics
252 252 """
253 253
254 254 hgsettings3 = RhodeCodeSetting('ga_code', '')
255 255
256 256 try:
257 257 self.sa.add(hgsettings3)
258 258 self.sa.commit()
259 259 except Exception:
260 260 self.sa.rollback()
261 261 raise
262 262
263 263 def create_admin_and_prompt(self):
264 264
265 265 # defaults
266 266 defaults = self.cli_args
267 267 username = defaults.get('username')
268 268 password = defaults.get('password')
269 269 email = defaults.get('email')
270 270
271 271 if username is None:
272 272 username = raw_input('Specify admin username:')
273 273 if password is None:
274 274 password = self._get_admin_password()
275 275 if not password:
276 276 # second try
277 277 password = self._get_admin_password()
278 278 if not password:
279 279 sys.exit()
280 280 if email is None:
281 281 email = raw_input('Specify admin email:')
282 282 api_key = self.cli_args.get('api_key')
283 283 self.create_user(username, password, email, True,
284 284 strict_creation_check=False,
285 285 api_key=api_key)
286 286
287 287 def _get_admin_password(self):
288 288 password = getpass.getpass('Specify admin password '
289 289 '(min 6 chars):')
290 290 confirm = getpass.getpass('Confirm password:')
291 291
292 292 if password != confirm:
293 293 log.error('passwords mismatch')
294 294 return False
295 295 if len(password) < 6:
296 296 log.error('password is too short - use at least 6 characters')
297 297 return False
298 298
299 299 return password
300 300
301 301 def create_test_admin_and_users(self):
302 302 log.info('creating admin and regular test users')
303 303 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, \
304 304 TEST_USER_ADMIN_PASS, TEST_USER_ADMIN_EMAIL, \
305 305 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, \
306 306 TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR2_LOGIN, \
307 307 TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL
308 308
309 309 self.create_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
310 310 TEST_USER_ADMIN_EMAIL, True, api_key=True)
311 311
312 312 self.create_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
313 313 TEST_USER_REGULAR_EMAIL, False, api_key=True)
314 314
315 315 self.create_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS,
316 316 TEST_USER_REGULAR2_EMAIL, False, api_key=True)
317 317
318 318 def create_ui_settings(self, repo_store_path):
319 319 """
320 320 Creates ui settings, fills out hooks
321 321 and disables dotencode
322 322 """
323 323 settings_model = SettingsModel(sa=self.sa)
324 324 from rhodecode.lib.vcs.backends.hg import largefiles_store
325 325 from rhodecode.lib.vcs.backends.git import lfs_store
326 326
327 327 # Build HOOKS
328 328 hooks = [
329 329 (RhodeCodeUi.HOOK_REPO_SIZE, 'python:vcsserver.hooks.repo_size'),
330 330
331 331 # HG
332 332 (RhodeCodeUi.HOOK_PRE_PULL, 'python:vcsserver.hooks.pre_pull'),
333 333 (RhodeCodeUi.HOOK_PULL, 'python:vcsserver.hooks.log_pull_action'),
334 334 (RhodeCodeUi.HOOK_PRE_PUSH, 'python:vcsserver.hooks.pre_push'),
335 335 (RhodeCodeUi.HOOK_PRETX_PUSH, 'python:vcsserver.hooks.pre_push'),
336 336 (RhodeCodeUi.HOOK_PUSH, 'python:vcsserver.hooks.log_push_action'),
337 337 (RhodeCodeUi.HOOK_PUSH_KEY, 'python:vcsserver.hooks.key_push'),
338 338
339 339 ]
340 340
341 341 for key, value in hooks:
342 342 hook_obj = settings_model.get_ui_by_key(key)
343 343 hooks2 = hook_obj if hook_obj else RhodeCodeUi()
344 344 hooks2.ui_section = 'hooks'
345 345 hooks2.ui_key = key
346 346 hooks2.ui_value = value
347 347 self.sa.add(hooks2)
348 348
349 349 # enable largefiles
350 350 largefiles = RhodeCodeUi()
351 351 largefiles.ui_section = 'extensions'
352 352 largefiles.ui_key = 'largefiles'
353 353 largefiles.ui_value = ''
354 354 self.sa.add(largefiles)
355 355
356 356 # set default largefiles cache dir, defaults to
357 357 # /repo_store_location/.cache/largefiles
358 358 largefiles = RhodeCodeUi()
359 359 largefiles.ui_section = 'largefiles'
360 360 largefiles.ui_key = 'usercache'
361 361 largefiles.ui_value = largefiles_store(repo_store_path)
362 362
363 363 self.sa.add(largefiles)
364 364
365 365 # set default lfs cache dir, defaults to
366 366 # /repo_store_location/.cache/lfs_store
367 367 lfsstore = RhodeCodeUi()
368 368 lfsstore.ui_section = 'vcs_git_lfs'
369 369 lfsstore.ui_key = 'store_location'
370 370 lfsstore.ui_value = lfs_store(repo_store_path)
371 371
372 372 self.sa.add(lfsstore)
373 373
374 374 # enable hgsubversion disabled by default
375 375 hgsubversion = RhodeCodeUi()
376 376 hgsubversion.ui_section = 'extensions'
377 377 hgsubversion.ui_key = 'hgsubversion'
378 378 hgsubversion.ui_value = ''
379 379 hgsubversion.ui_active = False
380 380 self.sa.add(hgsubversion)
381 381
382 382 # enable hgevolve disabled by default
383 383 hgevolve = RhodeCodeUi()
384 384 hgevolve.ui_section = 'extensions'
385 385 hgevolve.ui_key = 'evolve'
386 386 hgevolve.ui_value = ''
387 387 hgevolve.ui_active = False
388 388 self.sa.add(hgevolve)
389 389
390 390 hgevolve = RhodeCodeUi()
391 391 hgevolve.ui_section = 'experimental'
392 392 hgevolve.ui_key = 'evolution'
393 393 hgevolve.ui_value = ''
394 394 hgevolve.ui_active = False
395 395 self.sa.add(hgevolve)
396 396
397 397 hgevolve = RhodeCodeUi()
398 398 hgevolve.ui_section = 'experimental'
399 399 hgevolve.ui_key = 'evolution.exchange'
400 400 hgevolve.ui_value = ''
401 401 hgevolve.ui_active = False
402 402 self.sa.add(hgevolve)
403 403
404 404 hgevolve = RhodeCodeUi()
405 405 hgevolve.ui_section = 'extensions'
406 406 hgevolve.ui_key = 'topic'
407 407 hgevolve.ui_value = ''
408 408 hgevolve.ui_active = False
409 409 self.sa.add(hgevolve)
410 410
411 411 # enable hggit disabled by default
412 412 hggit = RhodeCodeUi()
413 413 hggit.ui_section = 'extensions'
414 414 hggit.ui_key = 'hggit'
415 415 hggit.ui_value = ''
416 416 hggit.ui_active = False
417 417 self.sa.add(hggit)
418 418
419 419 # set svn branch defaults
420 420 branches = ["/branches/*", "/trunk"]
421 421 tags = ["/tags/*"]
422 422
423 423 for branch in branches:
424 424 settings_model.create_ui_section_value(
425 425 RhodeCodeUi.SVN_BRANCH_ID, branch)
426 426
427 427 for tag in tags:
428 428 settings_model.create_ui_section_value(RhodeCodeUi.SVN_TAG_ID, tag)
429 429
430 430 def create_auth_plugin_options(self, skip_existing=False):
431 431 """
432 432 Create default auth plugin settings, and make it active
433 433
434 434 :param skip_existing:
435 435 """
436 436 defaults = [
437 437 ('auth_plugins',
438 438 'egg:rhodecode-enterprise-ce#token,egg:rhodecode-enterprise-ce#rhodecode',
439 439 'list'),
440 440
441 441 ('auth_authtoken_enabled',
442 442 'True',
443 443 'bool'),
444 444
445 445 ('auth_rhodecode_enabled',
446 446 'True',
447 447 'bool'),
448 448 ]
449 449 for k, v, t in defaults:
450 450 if (skip_existing and
451 451 SettingsModel().get_setting_by_name(k) is not None):
452 452 log.debug('Skipping option %s', k)
453 453 continue
454 454 setting = RhodeCodeSetting(k, v, t)
455 455 self.sa.add(setting)
456 456
457 457 def create_default_options(self, skip_existing=False):
458 458 """Creates default settings"""
459 459
460 460 for k, v, t in [
461 461 ('default_repo_enable_locking', False, 'bool'),
462 462 ('default_repo_enable_downloads', False, 'bool'),
463 463 ('default_repo_enable_statistics', False, 'bool'),
464 464 ('default_repo_private', False, 'bool'),
465 465 ('default_repo_type', 'hg', 'unicode')]:
466 466
467 467 if (skip_existing and
468 468 SettingsModel().get_setting_by_name(k) is not None):
469 469 log.debug('Skipping option %s', k)
470 470 continue
471 471 setting = RhodeCodeSetting(k, v, t)
472 472 self.sa.add(setting)
473 473
474 474 def fixup_groups(self):
475 475 def_usr = User.get_default_user()
476 476 for g in RepoGroup.query().all():
477 477 g.group_name = g.get_new_name(g.name)
478 478 self.sa.add(g)
479 479 # get default perm
480 480 default = UserRepoGroupToPerm.query()\
481 481 .filter(UserRepoGroupToPerm.group == g)\
482 482 .filter(UserRepoGroupToPerm.user == def_usr)\
483 483 .scalar()
484 484
485 485 if default is None:
486 486 log.debug('missing default permission for group %s adding', g)
487 487 perm_obj = RepoGroupModel()._create_default_perms(g)
488 488 self.sa.add(perm_obj)
489 489
490 490 def reset_permissions(self, username):
491 491 """
492 492 Resets permissions to default state, useful when old systems had
493 493 bad permissions, we must clean them up
494 494
495 495 :param username:
496 496 """
497 497 default_user = User.get_by_username(username)
498 498 if not default_user:
499 499 return
500 500
501 501 u2p = UserToPerm.query()\
502 502 .filter(UserToPerm.user == default_user).all()
503 503 fixed = False
504 504 if len(u2p) != len(Permission.DEFAULT_USER_PERMISSIONS):
505 505 for p in u2p:
506 506 Session().delete(p)
507 507 fixed = True
508 508 self.populate_default_permissions()
509 509 return fixed
510 510
511 511 def config_prompt(self, test_repo_path='', retries=3):
512 512 defaults = self.cli_args
513 513 _path = defaults.get('repos_location')
514 514 if retries == 3:
515 515 log.info('Setting up repositories config')
516 516
517 517 if _path is not None:
518 518 path = _path
519 519 elif not self.tests and not test_repo_path:
520 520 path = raw_input(
521 521 'Enter a valid absolute path to store repositories. '
522 522 'All repositories in that path will be added automatically:'
523 523 )
524 524 else:
525 525 path = test_repo_path
526 526 path_ok = True
527 527
528 528 # check proper dir
529 529 if not os.path.isdir(path):
530 530 path_ok = False
531 531 log.error('Given path %s is not a valid directory', path)
532 532
533 533 elif not os.path.isabs(path):
534 534 path_ok = False
535 535 log.error('Given path %s is not an absolute path', path)
536 536
537 537 # check if path is at least readable.
538 538 if not os.access(path, os.R_OK):
539 539 path_ok = False
540 540 log.error('Given path %s is not readable', path)
541 541
542 542 # check write access, warn user about non writeable paths
543 543 elif not os.access(path, os.W_OK) and path_ok:
544 544 log.warning('No write permission to given path %s', path)
545 545
546 546 q = ('Given path %s is not writeable, do you want to '
547 547 'continue with read only mode ? [y/n]' % (path,))
548 548 if not self.ask_ok(q):
549 549 log.error('Canceled by user')
550 550 sys.exit(-1)
551 551
552 552 if retries == 0:
553 553 sys.exit('max retries reached')
554 554 if not path_ok:
555 555 retries -= 1
556 556 return self.config_prompt(test_repo_path, retries)
557 557
558 558 real_path = os.path.normpath(os.path.realpath(path))
559 559
560 560 if real_path != os.path.normpath(path):
561 561 q = ('Path looks like a symlink, RhodeCode Enterprise will store '
562 562 'given path as %s ? [y/n]') % (real_path,)
563 563 if not self.ask_ok(q):
564 564 log.error('Canceled by user')
565 565 sys.exit(-1)
566 566
567 567 return real_path
568 568
569 569 def create_settings(self, path):
570 570
571 571 self.create_ui_settings(path)
572 572
573 573 ui_config = [
574 574 ('web', 'push_ssl', 'False'),
575 575 ('web', 'allow_archive', 'gz zip bz2'),
576 576 ('web', 'allow_push', '*'),
577 577 ('web', 'baseurl', '/'),
578 578 ('paths', '/', path),
579 579 ('phases', 'publish', 'True')
580 580 ]
581 581 for section, key, value in ui_config:
582 582 ui_conf = RhodeCodeUi()
583 583 setattr(ui_conf, 'ui_section', section)
584 584 setattr(ui_conf, 'ui_key', key)
585 585 setattr(ui_conf, 'ui_value', value)
586 586 self.sa.add(ui_conf)
587 587
588 588 # rhodecode app settings
589 589 settings = [
590 590 ('realm', 'RhodeCode', 'unicode'),
591 591 ('title', '', 'unicode'),
592 592 ('pre_code', '', 'unicode'),
593 593 ('post_code', '', 'unicode'),
594 594
595 595 # Visual
596 596 ('show_public_icon', True, 'bool'),
597 597 ('show_private_icon', True, 'bool'),
598 598 ('stylify_metatags', True, 'bool'),
599 599 ('dashboard_items', 100, 'int'),
600 600 ('admin_grid_items', 25, 'int'),
601 601
602 602 ('markup_renderer', 'markdown', 'unicode'),
603 603
604 604 ('repository_fields', True, 'bool'),
605 605 ('show_version', True, 'bool'),
606 606 ('show_revision_number', True, 'bool'),
607 607 ('show_sha_length', 12, 'int'),
608 608
609 609 ('use_gravatar', False, 'bool'),
610 610 ('gravatar_url', User.DEFAULT_GRAVATAR_URL, 'unicode'),
611 611
612 612 ('clone_uri_tmpl', Repository.DEFAULT_CLONE_URI, 'unicode'),
613 613 ('clone_uri_id_tmpl', Repository.DEFAULT_CLONE_URI_ID, 'unicode'),
614 614 ('clone_uri_ssh_tmpl', Repository.DEFAULT_CLONE_URI_SSH, 'unicode'),
615 615 ('support_url', '', 'unicode'),
616 616 ('update_url', RhodeCodeSetting.DEFAULT_UPDATE_URL, 'unicode'),
617 617
618 618 # VCS Settings
619 619 ('pr_merge_enabled', True, 'bool'),
620 620 ('use_outdated_comments', True, 'bool'),
621 621 ('diff_cache', True, 'bool'),
622 622 ]
623 623
624 624 for key, val, type_ in settings:
625 625 sett = RhodeCodeSetting(key, val, type_)
626 626 self.sa.add(sett)
627 627
628 628 self.create_auth_plugin_options()
629 629 self.create_default_options()
630 630
631 631 log.info('created ui config')
632 632
633 633 def create_user(self, username, password, email='', admin=False,
634 634 strict_creation_check=True, api_key=None):
635 635 log.info('creating user `%s`', username)
636 636 user = UserModel().create_or_update(
637 637 username, password, email, firstname=u'RhodeCode', lastname=u'Admin',
638 638 active=True, admin=admin, extern_type="rhodecode",
639 639 strict_creation_check=strict_creation_check)
640 640
641 641 if api_key:
642 642 log.info('setting a new default auth token for user `%s`', username)
643 643 UserModel().add_auth_token(
644 644 user=user, lifetime_minutes=-1,
645 645 role=UserModel.auth_token_role.ROLE_ALL,
646 646 description=u'BUILTIN TOKEN')
647 647
648 648 def create_default_user(self):
649 649 log.info('creating default user')
650 650 # create default user for handling default permissions.
651 651 user = UserModel().create_or_update(username=User.DEFAULT_USER,
652 652 password=str(uuid.uuid1())[:20],
653 653 email=User.DEFAULT_USER_EMAIL,
654 654 firstname=u'Anonymous',
655 655 lastname=u'User',
656 656 strict_creation_check=False)
657 657 # based on configuration options activate/de-activate this user which
658 658 # controlls anonymous access
659 659 if self.cli_args.get('public_access') is False:
660 660 log.info('Public access disabled')
661 661 user.active = False
662 662 Session().add(user)
663 663 Session().commit()
664 664
665 665 def create_permissions(self):
666 666 """
667 667 Creates all permissions defined in the system
668 668 """
669 669 # module.(access|create|change|delete)_[name]
670 670 # module.(none|read|write|admin)
671 671 log.info('creating permissions')
672 672 PermissionModel(self.sa).create_permissions()
673 673
674 674 def populate_default_permissions(self):
675 675 """
676 676 Populate default permissions. It will create only the default
677 677 permissions that are missing, and not alter already defined ones
678 678 """
679 679 log.info('creating default user permissions')
680 680 PermissionModel(self.sa).create_default_user_permissions(user=User.DEFAULT_USER)
@@ -1,90 +1,93 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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 time
22 22 import logging
23 23
24 24 import rhodecode
25 25 from rhodecode.lib.auth import AuthUser
26 26 from rhodecode.lib.base import get_ip_addr, get_access_path, get_user_agent
27 27 from rhodecode.lib.utils2 import safe_str, get_current_rhodecode_user
28 28
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32
33 33 class RequestWrapperTween(object):
34 34 def __init__(self, handler, registry):
35 35 self.handler = handler
36 36 self.registry = registry
37 37
38 38 # one-time configuration code goes here
39 39
40 40 def _get_user_info(self, request):
41 41 user = get_current_rhodecode_user(request)
42 42 if not user:
43 43 user = AuthUser.repr_user(ip=get_ip_addr(request.environ))
44 44 return user
45 45
46 46 def __call__(self, request):
47 47 start = time.time()
48 48 log.debug('Starting request time measurement')
49 49 response = None
50 50 try:
51 51 response = self.handler(request)
52 52 finally:
53 53 count = request.request_count()
54 54 _ver_ = rhodecode.__version__
55 55 _path = safe_str(get_access_path(request.environ))
56 56 _auth_user = self._get_user_info(request)
57 ip = get_ip_addr(request.environ)
58 match_route = request.matched_route.name if request.matched_route else "NOT_FOUND"
57 59
58 60 total = time.time() - start
59 61 log.info(
60 62 'Req[%4s] %s %s Request to %s time: %.4fs [%s], RhodeCode %s',
61 63 count, _auth_user, request.environ.get('REQUEST_METHOD'),
62 _path, total, get_user_agent(request. environ), _ver_
64 _path, total, get_user_agent(request. environ), _ver_,
65 extra={"time": total, "ver": _ver_, "ip": ip,
66 "path": _path, "view_name": match_route}
63 67 )
64 68
65 69 statsd = request.registry.statsd
66 70 if statsd:
67 match_route = request.matched_route.name if request.matched_route else "NOT_FOUND"
68 71 resp_code = getattr(response, 'status_code', 'UNDEFINED')
69 72 elapsed_time_ms = round(1000.0 * total) # use ms only
70 73 statsd.timing(
71 74 "rhodecode_req_timing.histogram", elapsed_time_ms,
72 75 tags=[
73 76 "view_name:{}".format(match_route),
74 77 "code:{}".format(resp_code)
75 78 ],
76 79 use_decimals=False
77 80 )
78 81 statsd.incr(
79 82 'rhodecode_req_total', tags=[
80 83 "view_name:{}".format(match_route),
81 84 "code:{}".format(resp_code)
82 85 ])
83 86
84 87 return response
85 88
86 89
87 90 def includeme(config):
88 91 config.add_tween(
89 92 'rhodecode.lib.middleware.request_wrapper.RequestWrapperTween',
90 93 )
@@ -1,422 +1,422 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2015-2020 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 import os
21 21 import time
22 22 import logging
23 23 import functools
24 24 import threading
25 25
26 26 from dogpile.cache import CacheRegion
27 27 from dogpile.cache.util import compat
28 28
29 29 import rhodecode
30 30 from rhodecode.lib.utils import safe_str, sha1
31 31 from rhodecode.lib.utils2 import safe_unicode, str2bool
32 32 from rhodecode.model.db import Session, CacheKey, IntegrityError
33 33
34 34 from rhodecode.lib.rc_cache import cache_key_meta
35 35 from rhodecode.lib.rc_cache import region_meta
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 def isCython(func):
41 41 """
42 42 Private helper that checks if a function is a cython function.
43 43 """
44 44 return func.__class__.__name__ == 'cython_function_or_method'
45 45
46 46
47 47 class RhodeCodeCacheRegion(CacheRegion):
48 48
49 49 def conditional_cache_on_arguments(
50 50 self, namespace=None,
51 51 expiration_time=None,
52 52 should_cache_fn=None,
53 53 to_str=compat.string_type,
54 54 function_key_generator=None,
55 55 condition=True):
56 56 """
57 57 Custom conditional decorator, that will not touch any dogpile internals if
58 58 condition isn't meet. This works a bit different than should_cache_fn
59 59 And it's faster in cases we don't ever want to compute cached values
60 60 """
61 61 expiration_time_is_callable = compat.callable(expiration_time)
62 62
63 63 if function_key_generator is None:
64 64 function_key_generator = self.function_key_generator
65 65
66 66 # workaround for py2 and cython problems, this block should be removed
67 67 # once we've migrated to py3
68 68 if 'cython' == 'cython':
69 69 def decorator(fn):
70 70 if to_str is compat.string_type:
71 71 # backwards compatible
72 72 key_generator = function_key_generator(namespace, fn)
73 73 else:
74 74 key_generator = function_key_generator(namespace, fn, to_str=to_str)
75 75
76 76 @functools.wraps(fn)
77 77 def decorate(*arg, **kw):
78 78 key = key_generator(*arg, **kw)
79 79
80 80 @functools.wraps(fn)
81 81 def creator():
82 82 return fn(*arg, **kw)
83 83
84 84 if not condition:
85 85 return creator()
86 86
87 87 timeout = expiration_time() if expiration_time_is_callable \
88 88 else expiration_time
89 89
90 90 return self.get_or_create(key, creator, timeout, should_cache_fn)
91 91
92 92 def invalidate(*arg, **kw):
93 93 key = key_generator(*arg, **kw)
94 94 self.delete(key)
95 95
96 96 def set_(value, *arg, **kw):
97 97 key = key_generator(*arg, **kw)
98 98 self.set(key, value)
99 99
100 100 def get(*arg, **kw):
101 101 key = key_generator(*arg, **kw)
102 102 return self.get(key)
103 103
104 104 def refresh(*arg, **kw):
105 105 key = key_generator(*arg, **kw)
106 106 value = fn(*arg, **kw)
107 107 self.set(key, value)
108 108 return value
109 109
110 110 decorate.set = set_
111 111 decorate.invalidate = invalidate
112 112 decorate.refresh = refresh
113 113 decorate.get = get
114 114 decorate.original = fn
115 115 decorate.key_generator = key_generator
116 116 decorate.__wrapped__ = fn
117 117
118 118 return decorate
119 119 return decorator
120 120
121 121 def get_or_create_for_user_func(key_generator, user_func, *arg, **kw):
122 122
123 123 if not condition:
124 log.debug('Calling un-cached func:%s', user_func.func_name)
124 log.debug('Calling un-cached method:%s', user_func.func_name)
125 125 start = time.time()
126 126 result = user_func(*arg, **kw)
127 127 total = time.time() - start
128 log.debug('un-cached func:%s took %.4fs', user_func.func_name, total)
128 log.debug('un-cached method:%s took %.4fs', user_func.func_name, total)
129 129 return result
130 130
131 131 key = key_generator(*arg, **kw)
132 132
133 133 timeout = expiration_time() if expiration_time_is_callable \
134 134 else expiration_time
135 135
136 log.debug('Calling cached fn:%s', user_func.func_name)
136 log.debug('Calling cached method:`%s`', user_func.func_name)
137 137 return self.get_or_create(key, user_func, timeout, should_cache_fn, (arg, kw))
138 138
139 139 def cache_decorator(user_func):
140 140 if to_str is compat.string_type:
141 141 # backwards compatible
142 142 key_generator = function_key_generator(namespace, user_func)
143 143 else:
144 144 key_generator = function_key_generator(namespace, user_func, to_str=to_str)
145 145
146 146 def refresh(*arg, **kw):
147 147 """
148 148 Like invalidate, but regenerates the value instead
149 149 """
150 150 key = key_generator(*arg, **kw)
151 151 value = user_func(*arg, **kw)
152 152 self.set(key, value)
153 153 return value
154 154
155 155 def invalidate(*arg, **kw):
156 156 key = key_generator(*arg, **kw)
157 157 self.delete(key)
158 158
159 159 def set_(value, *arg, **kw):
160 160 key = key_generator(*arg, **kw)
161 161 self.set(key, value)
162 162
163 163 def get(*arg, **kw):
164 164 key = key_generator(*arg, **kw)
165 165 return self.get(key)
166 166
167 167 user_func.set = set_
168 168 user_func.invalidate = invalidate
169 169 user_func.get = get
170 170 user_func.refresh = refresh
171 171 user_func.key_generator = key_generator
172 172 user_func.original = user_func
173 173
174 174 # Use `decorate` to preserve the signature of :param:`user_func`.
175 175 return decorator.decorate(user_func, functools.partial(
176 176 get_or_create_for_user_func, key_generator))
177 177
178 178 return cache_decorator
179 179
180 180
181 181 def make_region(*arg, **kw):
182 182 return RhodeCodeCacheRegion(*arg, **kw)
183 183
184 184
185 185 def get_default_cache_settings(settings, prefixes=None):
186 186 prefixes = prefixes or []
187 187 cache_settings = {}
188 188 for key in settings.keys():
189 189 for prefix in prefixes:
190 190 if key.startswith(prefix):
191 191 name = key.split(prefix)[1].strip()
192 192 val = settings[key]
193 193 if isinstance(val, compat.string_types):
194 194 val = val.strip()
195 195 cache_settings[name] = val
196 196 return cache_settings
197 197
198 198
199 199 def compute_key_from_params(*args):
200 200 """
201 201 Helper to compute key from given params to be used in cache manager
202 202 """
203 203 return sha1("_".join(map(safe_str, args)))
204 204
205 205
206 206 def backend_key_generator(backend):
207 207 """
208 208 Special wrapper that also sends over the backend to the key generator
209 209 """
210 210 def wrapper(namespace, fn):
211 211 return key_generator(backend, namespace, fn)
212 212 return wrapper
213 213
214 214
215 215 def key_generator(backend, namespace, fn):
216 216 fname = fn.__name__
217 217
218 218 def generate_key(*args):
219 219 backend_prefix = getattr(backend, 'key_prefix', None) or 'backend_prefix'
220 220 namespace_pref = namespace or 'default_namespace'
221 221 arg_key = compute_key_from_params(*args)
222 222 final_key = "{}:{}:{}_{}".format(backend_prefix, namespace_pref, fname, arg_key)
223 223
224 224 return final_key
225 225
226 226 return generate_key
227 227
228 228
229 229 def get_or_create_region(region_name, region_namespace=None):
230 230 from rhodecode.lib.rc_cache.backends import FileNamespaceBackend
231 231 region_obj = region_meta.dogpile_cache_regions.get(region_name)
232 232 if not region_obj:
233 233 raise EnvironmentError(
234 234 'Region `{}` not in configured: {}.'.format(
235 235 region_name, region_meta.dogpile_cache_regions.keys()))
236 236
237 237 region_uid_name = '{}:{}'.format(region_name, region_namespace)
238 238 if isinstance(region_obj.actual_backend, FileNamespaceBackend):
239 239 region_exist = region_meta.dogpile_cache_regions.get(region_namespace)
240 240 if region_exist:
241 241 log.debug('Using already configured region: %s', region_namespace)
242 242 return region_exist
243 243 cache_dir = region_meta.dogpile_config_defaults['cache_dir']
244 244 expiration_time = region_obj.expiration_time
245 245
246 246 if not os.path.isdir(cache_dir):
247 247 os.makedirs(cache_dir)
248 248 new_region = make_region(
249 249 name=region_uid_name,
250 250 function_key_generator=backend_key_generator(region_obj.actual_backend)
251 251 )
252 252 namespace_filename = os.path.join(
253 253 cache_dir, "{}.cache.dbm".format(region_namespace))
254 254 # special type that allows 1db per namespace
255 255 new_region.configure(
256 256 backend='dogpile.cache.rc.file_namespace',
257 257 expiration_time=expiration_time,
258 258 arguments={"filename": namespace_filename}
259 259 )
260 260
261 261 # create and save in region caches
262 262 log.debug('configuring new region: %s', region_uid_name)
263 263 region_obj = region_meta.dogpile_cache_regions[region_namespace] = new_region
264 264
265 265 return region_obj
266 266
267 267
268 268 def clear_cache_namespace(cache_region, cache_namespace_uid, invalidate=False):
269 269 region = get_or_create_region(cache_region, cache_namespace_uid)
270 270 cache_keys = region.backend.list_keys(prefix=cache_namespace_uid)
271 271 num_delete_keys = len(cache_keys)
272 272 if invalidate:
273 273 region.invalidate(hard=False)
274 274 else:
275 275 if num_delete_keys:
276 276 region.delete_multi(cache_keys)
277 277 return num_delete_keys
278 278
279 279
280 280 class ActiveRegionCache(object):
281 281 def __init__(self, context, cache_data):
282 282 self.context = context
283 283 self.cache_data = cache_data
284 284
285 285 def should_invalidate(self):
286 286 return False
287 287
288 288
289 289 class FreshRegionCache(object):
290 290 def __init__(self, context, cache_data):
291 291 self.context = context
292 292 self.cache_data = cache_data
293 293
294 294 def should_invalidate(self):
295 295 return True
296 296
297 297
298 298 class InvalidationContext(object):
299 299 """
300 300 usage::
301 301
302 302 from rhodecode.lib import rc_cache
303 303
304 304 cache_namespace_uid = CacheKey.SOME_NAMESPACE.format(1)
305 305 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
306 306
307 307 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=True)
308 308 def heavy_compute(cache_name, param1, param2):
309 309 print('COMPUTE {}, {}, {}'.format(cache_name, param1, param2))
310 310
311 311 # invalidation namespace is shared namespace key for all process caches
312 312 # we use it to send a global signal
313 313 invalidation_namespace = 'repo_cache:1'
314 314
315 315 inv_context_manager = rc_cache.InvalidationContext(
316 316 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace)
317 317 with inv_context_manager as invalidation_context:
318 318 args = ('one', 'two')
319 319 # re-compute and store cache if we get invalidate signal
320 320 if invalidation_context.should_invalidate():
321 321 result = heavy_compute.refresh(*args)
322 322 else:
323 323 result = heavy_compute(*args)
324 324
325 325 compute_time = inv_context_manager.compute_time
326 326 log.debug('result computed in %.4fs', compute_time)
327 327
328 328 # To send global invalidation signal, simply run
329 329 CacheKey.set_invalidate(invalidation_namespace)
330 330
331 331 """
332 332
333 333 def __repr__(self):
334 334 return '<InvalidationContext:{}[{}]>'.format(
335 335 safe_str(self.cache_key), safe_str(self.uid))
336 336
337 337 def __init__(self, uid, invalidation_namespace='',
338 338 raise_exception=False, thread_scoped=None):
339 339 self.uid = uid
340 340 self.invalidation_namespace = invalidation_namespace
341 341 self.raise_exception = raise_exception
342 342 self.proc_id = safe_unicode(rhodecode.CONFIG.get('instance_id') or 'DEFAULT')
343 343 self.thread_id = 'global'
344 344
345 345 if thread_scoped is None:
346 346 # if we set "default" we can override this via .ini settings
347 347 thread_scoped = str2bool(rhodecode.CONFIG.get('cache_thread_scoped'))
348 348
349 349 # Append the thread id to the cache key if this invalidation context
350 350 # should be scoped to the current thread.
351 351 if thread_scoped is True:
352 352 self.thread_id = threading.current_thread().ident
353 353
354 354 self.cache_key = compute_key_from_params(uid)
355 355 self.cache_key = 'proc:{}|thread:{}|params:{}'.format(
356 356 self.proc_id, self.thread_id, self.cache_key)
357 357 self.compute_time = 0
358 358
359 359 def get_or_create_cache_obj(self, cache_type, invalidation_namespace=''):
360 360 invalidation_namespace = invalidation_namespace or self.invalidation_namespace
361 361 # fetch all cache keys for this namespace and convert them to a map to find if we
362 362 # have specific cache_key object registered. We do this because we want to have
363 363 # all consistent cache_state_uid for newly registered objects
364 364 cache_obj_map = CacheKey.get_namespace_map(invalidation_namespace)
365 365 cache_obj = cache_obj_map.get(self.cache_key)
366 366 log.debug('Fetched cache obj %s using %s cache key.', cache_obj, self.cache_key)
367 367 if not cache_obj:
368 368 new_cache_args = invalidation_namespace
369 369 first_cache_obj = next(cache_obj_map.itervalues()) if cache_obj_map else None
370 370 cache_state_uid = None
371 371 if first_cache_obj:
372 372 cache_state_uid = first_cache_obj.cache_state_uid
373 373 cache_obj = CacheKey(self.cache_key, cache_args=new_cache_args,
374 374 cache_state_uid=cache_state_uid)
375 375 cache_key_meta.cache_keys_by_pid.append(self.cache_key)
376 376
377 377 return cache_obj
378 378
379 379 def __enter__(self):
380 380 """
381 381 Test if current object is valid, and return CacheRegion function
382 382 that does invalidation and calculation
383 383 """
384 384 log.debug('Entering cache invalidation check context: %s', self.invalidation_namespace)
385 385 # register or get a new key based on uid
386 386 self.cache_obj = self.get_or_create_cache_obj(cache_type=self.uid)
387 387 cache_data = self.cache_obj.get_dict()
388 388 self._start_time = time.time()
389 389 if self.cache_obj.cache_active:
390 390 # means our cache obj is existing and marked as it's
391 391 # cache is not outdated, we return ActiveRegionCache
392 392 self.skip_cache_active_change = True
393 393
394 394 return ActiveRegionCache(context=self, cache_data=cache_data)
395 395
396 396 # the key is either not existing or set to False, we return
397 397 # the real invalidator which re-computes value. We additionally set
398 398 # the flag to actually update the Database objects
399 399 self.skip_cache_active_change = False
400 400 return FreshRegionCache(context=self, cache_data=cache_data)
401 401
402 402 def __exit__(self, exc_type, exc_val, exc_tb):
403 403 # save compute time
404 404 self.compute_time = time.time() - self._start_time
405 405
406 406 if self.skip_cache_active_change:
407 407 return
408 408
409 409 try:
410 410 self.cache_obj.cache_active = True
411 411 Session().add(self.cache_obj)
412 412 Session().commit()
413 413 except IntegrityError:
414 414 # if we catch integrity error, it means we inserted this object
415 415 # assumption is that's really an edge race-condition case and
416 416 # it's safe is to skip it
417 417 Session().rollback()
418 418 except Exception:
419 419 log.exception('Failed to commit on cache key update')
420 420 Session().rollback()
421 421 if self.raise_exception:
422 422 raise
General Comments 0
You need to be logged in to leave comments. Login now