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