##// END OF EJS Templates
caches: use individual namespaces per user to prevent beaker caching problems....
marcink -
r2572:5b07455a default
parent child Browse files
Show More
@@ -1,719 +1,726 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 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
24
25 import colander
25 import colander
26 import copy
26 import copy
27 import logging
27 import logging
28 import time
28 import time
29 import traceback
29 import traceback
30 import warnings
30 import warnings
31 import functools
31 import functools
32
32
33 from pyramid.threadlocal import get_current_registry
33 from pyramid.threadlocal import get_current_registry
34 from zope.cachedescriptors.property import Lazy as LazyProperty
34 from zope.cachedescriptors.property import Lazy as LazyProperty
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 caches
38 from rhodecode.lib import caches
39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
40 from rhodecode.lib.utils2 import safe_int
40 from rhodecode.lib.utils2 import safe_int
41 from rhodecode.lib.utils2 import safe_str
41 from rhodecode.lib.utils2 import safe_str
42 from rhodecode.model.db import User
42 from rhodecode.model.db import User
43 from rhodecode.model.meta import Session
43 from rhodecode.model.meta import Session
44 from rhodecode.model.settings import SettingsModel
44 from rhodecode.model.settings import SettingsModel
45 from rhodecode.model.user import UserModel
45 from rhodecode.model.user import UserModel
46 from rhodecode.model.user_group import UserGroupModel
46 from rhodecode.model.user_group import UserGroupModel
47
47
48
48
49 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
50
50
51 # auth types that authenticate() function can receive
51 # auth types that authenticate() function can receive
52 VCS_TYPE = 'vcs'
52 VCS_TYPE = 'vcs'
53 HTTP_TYPE = 'http'
53 HTTP_TYPE = 'http'
54
54
55
55
56 class hybrid_property(object):
56 class hybrid_property(object):
57 """
57 """
58 a property decorator that works both for instance and class
58 a property decorator that works both for instance and class
59 """
59 """
60 def __init__(self, fget, fset=None, fdel=None, expr=None):
60 def __init__(self, fget, fset=None, fdel=None, expr=None):
61 self.fget = fget
61 self.fget = fget
62 self.fset = fset
62 self.fset = fset
63 self.fdel = fdel
63 self.fdel = fdel
64 self.expr = expr or fget
64 self.expr = expr or fget
65 functools.update_wrapper(self, fget)
65 functools.update_wrapper(self, fget)
66
66
67 def __get__(self, instance, owner):
67 def __get__(self, instance, owner):
68 if instance is None:
68 if instance is None:
69 return self.expr(owner)
69 return self.expr(owner)
70 else:
70 else:
71 return self.fget(instance)
71 return self.fget(instance)
72
72
73 def __set__(self, instance, value):
73 def __set__(self, instance, value):
74 self.fset(instance, value)
74 self.fset(instance, value)
75
75
76 def __delete__(self, instance):
76 def __delete__(self, instance):
77 self.fdel(instance)
77 self.fdel(instance)
78
78
79
79
80 class LazyFormencode(object):
80 class LazyFormencode(object):
81 def __init__(self, formencode_obj, *args, **kwargs):
81 def __init__(self, formencode_obj, *args, **kwargs):
82 self.formencode_obj = formencode_obj
82 self.formencode_obj = formencode_obj
83 self.args = args
83 self.args = args
84 self.kwargs = kwargs
84 self.kwargs = kwargs
85
85
86 def __call__(self, *args, **kwargs):
86 def __call__(self, *args, **kwargs):
87 from inspect import isfunction
87 from inspect import isfunction
88 formencode_obj = self.formencode_obj
88 formencode_obj = self.formencode_obj
89 if isfunction(formencode_obj):
89 if isfunction(formencode_obj):
90 # case we wrap validators into functions
90 # case we wrap validators into functions
91 formencode_obj = self.formencode_obj(*args, **kwargs)
91 formencode_obj = self.formencode_obj(*args, **kwargs)
92 return formencode_obj(*self.args, **self.kwargs)
92 return formencode_obj(*self.args, **self.kwargs)
93
93
94
94
95 class RhodeCodeAuthPluginBase(object):
95 class RhodeCodeAuthPluginBase(object):
96 # cache the authentication request for N amount of seconds. Some kind
96 # cache the authentication request for N amount of seconds. Some kind
97 # of authentication methods are very heavy and it's very efficient to cache
97 # of authentication methods are very heavy and it's very efficient to cache
98 # the result of a call. If it's set to None (default) cache is off
98 # the result of a call. If it's set to None (default) cache is off
99 AUTH_CACHE_TTL = None
99 AUTH_CACHE_TTL = None
100 AUTH_CACHE = {}
100 AUTH_CACHE = {}
101
101
102 auth_func_attrs = {
102 auth_func_attrs = {
103 "username": "unique username",
103 "username": "unique username",
104 "firstname": "first name",
104 "firstname": "first name",
105 "lastname": "last name",
105 "lastname": "last name",
106 "email": "email address",
106 "email": "email address",
107 "groups": '["list", "of", "groups"]',
107 "groups": '["list", "of", "groups"]',
108 "user_group_sync":
108 "user_group_sync":
109 'True|False defines if returned user groups should be synced',
109 'True|False defines if returned user groups should be synced',
110 "extern_name": "name in external source of record",
110 "extern_name": "name in external source of record",
111 "extern_type": "type of external source of record",
111 "extern_type": "type of external source of record",
112 "admin": 'True|False defines if user should be RhodeCode super admin',
112 "admin": 'True|False defines if user should be RhodeCode super admin',
113 "active":
113 "active":
114 'True|False defines active state of user internally for RhodeCode',
114 'True|False defines active state of user internally for RhodeCode',
115 "active_from_extern":
115 "active_from_extern":
116 "True|False\None, active state from the external auth, "
116 "True|False\None, active state from the external auth, "
117 "None means use definition from RhodeCode extern_type active value"
117 "None means use definition from RhodeCode extern_type active value"
118
118
119 }
119 }
120 # set on authenticate() method and via set_auth_type func.
120 # set on authenticate() method and via set_auth_type func.
121 auth_type = None
121 auth_type = None
122
122
123 # set on authenticate() method and via set_calling_scope_repo, this is a
123 # set on authenticate() method and via set_calling_scope_repo, this is a
124 # calling scope repository when doing authentication most likely on VCS
124 # calling scope repository when doing authentication most likely on VCS
125 # operations
125 # operations
126 acl_repo_name = None
126 acl_repo_name = None
127
127
128 # List of setting names to store encrypted. Plugins may override this list
128 # List of setting names to store encrypted. Plugins may override this list
129 # to store settings encrypted.
129 # to store settings encrypted.
130 _settings_encrypted = []
130 _settings_encrypted = []
131
131
132 # Mapping of python to DB settings model types. Plugins may override or
132 # Mapping of python to DB settings model types. Plugins may override or
133 # extend this mapping.
133 # extend this mapping.
134 _settings_type_map = {
134 _settings_type_map = {
135 colander.String: 'unicode',
135 colander.String: 'unicode',
136 colander.Integer: 'int',
136 colander.Integer: 'int',
137 colander.Boolean: 'bool',
137 colander.Boolean: 'bool',
138 colander.List: 'list',
138 colander.List: 'list',
139 }
139 }
140
140
141 # list of keys in settings that are unsafe to be logged, should be passwords
141 # list of keys in settings that are unsafe to be logged, should be passwords
142 # or other crucial credentials
142 # or other crucial credentials
143 _settings_unsafe_keys = []
143 _settings_unsafe_keys = []
144
144
145 def __init__(self, plugin_id):
145 def __init__(self, plugin_id):
146 self._plugin_id = plugin_id
146 self._plugin_id = plugin_id
147
147
148 def __str__(self):
148 def __str__(self):
149 return self.get_id()
149 return self.get_id()
150
150
151 def _get_setting_full_name(self, name):
151 def _get_setting_full_name(self, name):
152 """
152 """
153 Return the full setting name used for storing values in the database.
153 Return the full setting name used for storing values in the database.
154 """
154 """
155 # TODO: johbo: Using the name here is problematic. It would be good to
155 # TODO: johbo: Using the name here is problematic. It would be good to
156 # introduce either new models in the database to hold Plugin and
156 # introduce either new models in the database to hold Plugin and
157 # PluginSetting or to use the plugin id here.
157 # PluginSetting or to use the plugin id here.
158 return 'auth_{}_{}'.format(self.name, name)
158 return 'auth_{}_{}'.format(self.name, name)
159
159
160 def _get_setting_type(self, name):
160 def _get_setting_type(self, name):
161 """
161 """
162 Return the type of a setting. This type is defined by the SettingsModel
162 Return the type of a setting. This type is defined by the SettingsModel
163 and determines how the setting is stored in DB. Optionally the suffix
163 and determines how the setting is stored in DB. Optionally the suffix
164 `.encrypted` is appended to instruct SettingsModel to store it
164 `.encrypted` is appended to instruct SettingsModel to store it
165 encrypted.
165 encrypted.
166 """
166 """
167 schema_node = self.get_settings_schema().get(name)
167 schema_node = self.get_settings_schema().get(name)
168 db_type = self._settings_type_map.get(
168 db_type = self._settings_type_map.get(
169 type(schema_node.typ), 'unicode')
169 type(schema_node.typ), 'unicode')
170 if name in self._settings_encrypted:
170 if name in self._settings_encrypted:
171 db_type = '{}.encrypted'.format(db_type)
171 db_type = '{}.encrypted'.format(db_type)
172 return db_type
172 return db_type
173
173
174 @LazyProperty
174 @LazyProperty
175 def plugin_settings(self):
175 def plugin_settings(self):
176 settings = SettingsModel().get_all_settings()
176 settings = SettingsModel().get_all_settings()
177 return settings
177 return settings
178
178
179 def is_enabled(self):
179 def is_enabled(self):
180 """
180 """
181 Returns true if this plugin is enabled. An enabled plugin can be
181 Returns true if this plugin is enabled. An enabled plugin can be
182 configured in the admin interface but it is not consulted during
182 configured in the admin interface but it is not consulted during
183 authentication.
183 authentication.
184 """
184 """
185 auth_plugins = SettingsModel().get_auth_plugins()
185 auth_plugins = SettingsModel().get_auth_plugins()
186 return self.get_id() in auth_plugins
186 return self.get_id() in auth_plugins
187
187
188 def is_active(self):
188 def is_active(self):
189 """
189 """
190 Returns true if the plugin is activated. An activated plugin is
190 Returns true if the plugin is activated. An activated plugin is
191 consulted during authentication, assumed it is also enabled.
191 consulted during authentication, assumed it is also enabled.
192 """
192 """
193 return self.get_setting_by_name('enabled')
193 return self.get_setting_by_name('enabled')
194
194
195 def get_id(self):
195 def get_id(self):
196 """
196 """
197 Returns the plugin id.
197 Returns the plugin id.
198 """
198 """
199 return self._plugin_id
199 return self._plugin_id
200
200
201 def get_display_name(self):
201 def get_display_name(self):
202 """
202 """
203 Returns a translation string for displaying purposes.
203 Returns a translation string for displaying purposes.
204 """
204 """
205 raise NotImplementedError('Not implemented in base class')
205 raise NotImplementedError('Not implemented in base class')
206
206
207 def get_settings_schema(self):
207 def get_settings_schema(self):
208 """
208 """
209 Returns a colander schema, representing the plugin settings.
209 Returns a colander schema, representing the plugin settings.
210 """
210 """
211 return AuthnPluginSettingsSchemaBase()
211 return AuthnPluginSettingsSchemaBase()
212
212
213 def get_setting_by_name(self, name, default=None, cache=True):
213 def get_setting_by_name(self, name, default=None, cache=True):
214 """
214 """
215 Returns a plugin setting by name.
215 Returns a plugin setting by name.
216 """
216 """
217 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
217 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
218 if cache:
218 if cache:
219 plugin_settings = self.plugin_settings
219 plugin_settings = self.plugin_settings
220 else:
220 else:
221 plugin_settings = SettingsModel().get_all_settings()
221 plugin_settings = SettingsModel().get_all_settings()
222
222
223 if full_name in plugin_settings:
223 if full_name in plugin_settings:
224 return plugin_settings[full_name]
224 return plugin_settings[full_name]
225 else:
225 else:
226 return default
226 return default
227
227
228 def create_or_update_setting(self, name, value):
228 def create_or_update_setting(self, name, value):
229 """
229 """
230 Create or update a setting for this plugin in the persistent storage.
230 Create or update a setting for this plugin in the persistent storage.
231 """
231 """
232 full_name = self._get_setting_full_name(name)
232 full_name = self._get_setting_full_name(name)
233 type_ = self._get_setting_type(name)
233 type_ = self._get_setting_type(name)
234 db_setting = SettingsModel().create_or_update_setting(
234 db_setting = SettingsModel().create_or_update_setting(
235 full_name, value, type_)
235 full_name, value, type_)
236 return db_setting.app_settings_value
236 return db_setting.app_settings_value
237
237
238 def get_settings(self):
238 def get_settings(self):
239 """
239 """
240 Returns the plugin settings as dictionary.
240 Returns the plugin settings as dictionary.
241 """
241 """
242 settings = {}
242 settings = {}
243 for node in self.get_settings_schema():
243 for node in self.get_settings_schema():
244 settings[node.name] = self.get_setting_by_name(node.name)
244 settings[node.name] = self.get_setting_by_name(node.name)
245 return settings
245 return settings
246
246
247 def log_safe_settings(self, settings):
247 def log_safe_settings(self, settings):
248 """
248 """
249 returns a log safe representation of settings, without any secrets
249 returns a log safe representation of settings, without any secrets
250 """
250 """
251 settings_copy = copy.deepcopy(settings)
251 settings_copy = copy.deepcopy(settings)
252 for k in self._settings_unsafe_keys:
252 for k in self._settings_unsafe_keys:
253 if k in settings_copy:
253 if k in settings_copy:
254 del settings_copy[k]
254 del settings_copy[k]
255 return settings_copy
255 return settings_copy
256
256
257 @hybrid_property
257 @hybrid_property
258 def name(self):
258 def name(self):
259 """
259 """
260 Returns the name of this authentication plugin.
260 Returns the name of this authentication plugin.
261
261
262 :returns: string
262 :returns: string
263 """
263 """
264 raise NotImplementedError("Not implemented in base class")
264 raise NotImplementedError("Not implemented in base class")
265
265
266 def get_url_slug(self):
266 def get_url_slug(self):
267 """
267 """
268 Returns a slug which should be used when constructing URLs which refer
268 Returns a slug which should be used when constructing URLs which refer
269 to this plugin. By default it returns the plugin name. If the name is
269 to this plugin. By default it returns the plugin name. If the name is
270 not suitable for using it in an URL the plugin should override this
270 not suitable for using it in an URL the plugin should override this
271 method.
271 method.
272 """
272 """
273 return self.name
273 return self.name
274
274
275 @property
275 @property
276 def is_headers_auth(self):
276 def is_headers_auth(self):
277 """
277 """
278 Returns True if this authentication plugin uses HTTP headers as
278 Returns True if this authentication plugin uses HTTP headers as
279 authentication method.
279 authentication method.
280 """
280 """
281 return False
281 return False
282
282
283 @hybrid_property
283 @hybrid_property
284 def is_container_auth(self):
284 def is_container_auth(self):
285 """
285 """
286 Deprecated method that indicates if this authentication plugin uses
286 Deprecated method that indicates if this authentication plugin uses
287 HTTP headers as authentication method.
287 HTTP headers as authentication method.
288 """
288 """
289 warnings.warn(
289 warnings.warn(
290 'Use is_headers_auth instead.', category=DeprecationWarning)
290 'Use is_headers_auth instead.', category=DeprecationWarning)
291 return self.is_headers_auth
291 return self.is_headers_auth
292
292
293 @hybrid_property
293 @hybrid_property
294 def allows_creating_users(self):
294 def allows_creating_users(self):
295 """
295 """
296 Defines if Plugin allows users to be created on-the-fly when
296 Defines if Plugin allows users to be created on-the-fly when
297 authentication is called. Controls how external plugins should behave
297 authentication is called. Controls how external plugins should behave
298 in terms if they are allowed to create new users, or not. Base plugins
298 in terms if they are allowed to create new users, or not. Base plugins
299 should not be allowed to, but External ones should be !
299 should not be allowed to, but External ones should be !
300
300
301 :return: bool
301 :return: bool
302 """
302 """
303 return False
303 return False
304
304
305 def set_auth_type(self, auth_type):
305 def set_auth_type(self, auth_type):
306 self.auth_type = auth_type
306 self.auth_type = auth_type
307
307
308 def set_calling_scope_repo(self, acl_repo_name):
308 def set_calling_scope_repo(self, acl_repo_name):
309 self.acl_repo_name = acl_repo_name
309 self.acl_repo_name = acl_repo_name
310
310
311 def allows_authentication_from(
311 def allows_authentication_from(
312 self, user, allows_non_existing_user=True,
312 self, user, allows_non_existing_user=True,
313 allowed_auth_plugins=None, allowed_auth_sources=None):
313 allowed_auth_plugins=None, allowed_auth_sources=None):
314 """
314 """
315 Checks if this authentication module should accept a request for
315 Checks if this authentication module should accept a request for
316 the current user.
316 the current user.
317
317
318 :param user: user object fetched using plugin's get_user() method.
318 :param user: user object fetched using plugin's get_user() method.
319 :param allows_non_existing_user: if True, don't allow the
319 :param allows_non_existing_user: if True, don't allow the
320 user to be empty, meaning not existing in our database
320 user to be empty, meaning not existing in our database
321 :param allowed_auth_plugins: if provided, users extern_type will be
321 :param allowed_auth_plugins: if provided, users extern_type will be
322 checked against a list of provided extern types, which are plugin
322 checked against a list of provided extern types, which are plugin
323 auth_names in the end
323 auth_names in the end
324 :param allowed_auth_sources: authentication type allowed,
324 :param allowed_auth_sources: authentication type allowed,
325 `http` or `vcs` default is both.
325 `http` or `vcs` default is both.
326 defines if plugin will accept only http authentication vcs
326 defines if plugin will accept only http authentication vcs
327 authentication(git/hg) or both
327 authentication(git/hg) or both
328 :returns: boolean
328 :returns: boolean
329 """
329 """
330 if not user and not allows_non_existing_user:
330 if not user and not allows_non_existing_user:
331 log.debug('User is empty but plugin does not allow empty users,'
331 log.debug('User is empty but plugin does not allow empty users,'
332 'not allowed to authenticate')
332 'not allowed to authenticate')
333 return False
333 return False
334
334
335 expected_auth_plugins = allowed_auth_plugins or [self.name]
335 expected_auth_plugins = allowed_auth_plugins or [self.name]
336 if user and (user.extern_type and
336 if user and (user.extern_type and
337 user.extern_type not in expected_auth_plugins):
337 user.extern_type not in expected_auth_plugins):
338 log.debug(
338 log.debug(
339 'User `%s` is bound to `%s` auth type. Plugin allows only '
339 'User `%s` is bound to `%s` auth type. Plugin allows only '
340 '%s, skipping', user, user.extern_type, expected_auth_plugins)
340 '%s, skipping', user, user.extern_type, expected_auth_plugins)
341
341
342 return False
342 return False
343
343
344 # by default accept both
344 # by default accept both
345 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
345 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
346 if self.auth_type not in expected_auth_from:
346 if self.auth_type not in expected_auth_from:
347 log.debug('Current auth source is %s but plugin only allows %s',
347 log.debug('Current auth source is %s but plugin only allows %s',
348 self.auth_type, expected_auth_from)
348 self.auth_type, expected_auth_from)
349 return False
349 return False
350
350
351 return True
351 return True
352
352
353 def get_user(self, username=None, **kwargs):
353 def get_user(self, username=None, **kwargs):
354 """
354 """
355 Helper method for user fetching in plugins, by default it's using
355 Helper method for user fetching in plugins, by default it's using
356 simple fetch by username, but this method can be custimized in plugins
356 simple fetch by username, but this method can be custimized in plugins
357 eg. headers auth plugin to fetch user by environ params
357 eg. headers auth plugin to fetch user by environ params
358
358
359 :param username: username if given to fetch from database
359 :param username: username if given to fetch from database
360 :param kwargs: extra arguments needed for user fetching.
360 :param kwargs: extra arguments needed for user fetching.
361 """
361 """
362 user = None
362 user = None
363 log.debug(
363 log.debug(
364 'Trying to fetch user `%s` from RhodeCode database', username)
364 'Trying to fetch user `%s` from RhodeCode database', username)
365 if username:
365 if username:
366 user = User.get_by_username(username)
366 user = User.get_by_username(username)
367 if not user:
367 if not user:
368 log.debug('User not found, fallback to fetch user in '
368 log.debug('User not found, fallback to fetch user in '
369 'case insensitive mode')
369 'case insensitive mode')
370 user = User.get_by_username(username, case_insensitive=True)
370 user = User.get_by_username(username, case_insensitive=True)
371 else:
371 else:
372 log.debug('provided username:`%s` is empty skipping...', username)
372 log.debug('provided username:`%s` is empty skipping...', username)
373 if not user:
373 if not user:
374 log.debug('User `%s` not found in database', username)
374 log.debug('User `%s` not found in database', username)
375 else:
375 else:
376 log.debug('Got DB user:%s', user)
376 log.debug('Got DB user:%s', user)
377 return user
377 return user
378
378
379 def user_activation_state(self):
379 def user_activation_state(self):
380 """
380 """
381 Defines user activation state when creating new users
381 Defines user activation state when creating new users
382
382
383 :returns: boolean
383 :returns: boolean
384 """
384 """
385 raise NotImplementedError("Not implemented in base class")
385 raise NotImplementedError("Not implemented in base class")
386
386
387 def auth(self, userobj, username, passwd, settings, **kwargs):
387 def auth(self, userobj, username, passwd, settings, **kwargs):
388 """
388 """
389 Given a user object (which may be null), username, a plaintext
389 Given a user object (which may be null), username, a plaintext
390 password, and a settings object (containing all the keys needed as
390 password, and a settings object (containing all the keys needed as
391 listed in settings()), authenticate this user's login attempt.
391 listed in settings()), authenticate this user's login attempt.
392
392
393 Return None on failure. On success, return a dictionary of the form:
393 Return None on failure. On success, return a dictionary of the form:
394
394
395 see: RhodeCodeAuthPluginBase.auth_func_attrs
395 see: RhodeCodeAuthPluginBase.auth_func_attrs
396 This is later validated for correctness
396 This is later validated for correctness
397 """
397 """
398 raise NotImplementedError("not implemented in base class")
398 raise NotImplementedError("not implemented in base class")
399
399
400 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
400 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
401 """
401 """
402 Wrapper to call self.auth() that validates call on it
402 Wrapper to call self.auth() that validates call on it
403
403
404 :param userobj: userobj
404 :param userobj: userobj
405 :param username: username
405 :param username: username
406 :param passwd: plaintext password
406 :param passwd: plaintext password
407 :param settings: plugin settings
407 :param settings: plugin settings
408 """
408 """
409 auth = self.auth(userobj, username, passwd, settings, **kwargs)
409 auth = self.auth(userobj, username, passwd, settings, **kwargs)
410 if auth:
410 if auth:
411 auth['_plugin'] = self.name
411 auth['_plugin'] = self.name
412 auth['_ttl_cache'] = self.get_ttl_cache(settings)
412 auth['_ttl_cache'] = self.get_ttl_cache(settings)
413 # check if hash should be migrated ?
413 # check if hash should be migrated ?
414 new_hash = auth.get('_hash_migrate')
414 new_hash = auth.get('_hash_migrate')
415 if new_hash:
415 if new_hash:
416 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
416 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
417 if 'user_group_sync' not in auth:
417 if 'user_group_sync' not in auth:
418 auth['user_group_sync'] = False
418 auth['user_group_sync'] = False
419 return self._validate_auth_return(auth)
419 return self._validate_auth_return(auth)
420 return auth
420 return auth
421
421
422 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
422 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
423 new_hash_cypher = _RhodeCodeCryptoBCrypt()
423 new_hash_cypher = _RhodeCodeCryptoBCrypt()
424 # extra checks, so make sure new hash is correct.
424 # extra checks, so make sure new hash is correct.
425 password_encoded = safe_str(password)
425 password_encoded = safe_str(password)
426 if new_hash and new_hash_cypher.hash_check(
426 if new_hash and new_hash_cypher.hash_check(
427 password_encoded, new_hash):
427 password_encoded, new_hash):
428 cur_user = User.get_by_username(username)
428 cur_user = User.get_by_username(username)
429 cur_user.password = new_hash
429 cur_user.password = new_hash
430 Session().add(cur_user)
430 Session().add(cur_user)
431 Session().flush()
431 Session().flush()
432 log.info('Migrated user %s hash to bcrypt', cur_user)
432 log.info('Migrated user %s hash to bcrypt', cur_user)
433
433
434 def _validate_auth_return(self, ret):
434 def _validate_auth_return(self, ret):
435 if not isinstance(ret, dict):
435 if not isinstance(ret, dict):
436 raise Exception('returned value from auth must be a dict')
436 raise Exception('returned value from auth must be a dict')
437 for k in self.auth_func_attrs:
437 for k in self.auth_func_attrs:
438 if k not in ret:
438 if k not in ret:
439 raise Exception('Missing %s attribute from returned data' % k)
439 raise Exception('Missing %s attribute from returned data' % k)
440 return ret
440 return ret
441
441
442 def get_ttl_cache(self, settings=None):
442 def get_ttl_cache(self, settings=None):
443 plugin_settings = settings or self.get_settings()
443 plugin_settings = settings or self.get_settings()
444 cache_ttl = 0
444 cache_ttl = 0
445
445
446 if isinstance(self.AUTH_CACHE_TTL, (int, long)):
446 if isinstance(self.AUTH_CACHE_TTL, (int, long)):
447 # plugin cache set inside is more important than the settings value
447 # plugin cache set inside is more important than the settings value
448 cache_ttl = self.AUTH_CACHE_TTL
448 cache_ttl = self.AUTH_CACHE_TTL
449 elif plugin_settings.get('cache_ttl'):
449 elif plugin_settings.get('cache_ttl'):
450 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
450 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
451
451
452 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
452 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
453 return plugin_cache_active, cache_ttl
453 return plugin_cache_active, cache_ttl
454
454
455
455
456 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
456 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
457
457
458 @hybrid_property
458 @hybrid_property
459 def allows_creating_users(self):
459 def allows_creating_users(self):
460 return True
460 return True
461
461
462 def use_fake_password(self):
462 def use_fake_password(self):
463 """
463 """
464 Return a boolean that indicates whether or not we should set the user's
464 Return a boolean that indicates whether or not we should set the user's
465 password to a random value when it is authenticated by this plugin.
465 password to a random value when it is authenticated by this plugin.
466 If your plugin provides authentication, then you will generally
466 If your plugin provides authentication, then you will generally
467 want this.
467 want this.
468
468
469 :returns: boolean
469 :returns: boolean
470 """
470 """
471 raise NotImplementedError("Not implemented in base class")
471 raise NotImplementedError("Not implemented in base class")
472
472
473 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
473 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
474 # at this point _authenticate calls plugin's `auth()` function
474 # at this point _authenticate calls plugin's `auth()` function
475 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
475 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
476 userobj, username, passwd, settings, **kwargs)
476 userobj, username, passwd, settings, **kwargs)
477
477
478 if auth:
478 if auth:
479 # maybe plugin will clean the username ?
479 # maybe plugin will clean the username ?
480 # we should use the return value
480 # we should use the return value
481 username = auth['username']
481 username = auth['username']
482
482
483 # if external source tells us that user is not active, we should
483 # if external source tells us that user is not active, we should
484 # skip rest of the process. This can prevent from creating users in
484 # skip rest of the process. This can prevent from creating users in
485 # RhodeCode when using external authentication, but if it's
485 # RhodeCode when using external authentication, but if it's
486 # inactive user we shouldn't create that user anyway
486 # inactive user we shouldn't create that user anyway
487 if auth['active_from_extern'] is False:
487 if auth['active_from_extern'] is False:
488 log.warning(
488 log.warning(
489 "User %s authenticated against %s, but is inactive",
489 "User %s authenticated against %s, but is inactive",
490 username, self.__module__)
490 username, self.__module__)
491 return None
491 return None
492
492
493 cur_user = User.get_by_username(username, case_insensitive=True)
493 cur_user = User.get_by_username(username, case_insensitive=True)
494 is_user_existing = cur_user is not None
494 is_user_existing = cur_user is not None
495
495
496 if is_user_existing:
496 if is_user_existing:
497 log.debug('Syncing user `%s` from '
497 log.debug('Syncing user `%s` from '
498 '`%s` plugin', username, self.name)
498 '`%s` plugin', username, self.name)
499 else:
499 else:
500 log.debug('Creating non existing user `%s` from '
500 log.debug('Creating non existing user `%s` from '
501 '`%s` plugin', username, self.name)
501 '`%s` plugin', username, self.name)
502
502
503 if self.allows_creating_users:
503 if self.allows_creating_users:
504 log.debug('Plugin `%s` allows to '
504 log.debug('Plugin `%s` allows to '
505 'create new users', self.name)
505 'create new users', self.name)
506 else:
506 else:
507 log.debug('Plugin `%s` does not allow to '
507 log.debug('Plugin `%s` does not allow to '
508 'create new users', self.name)
508 'create new users', self.name)
509
509
510 user_parameters = {
510 user_parameters = {
511 'username': username,
511 'username': username,
512 'email': auth["email"],
512 'email': auth["email"],
513 'firstname': auth["firstname"],
513 'firstname': auth["firstname"],
514 'lastname': auth["lastname"],
514 'lastname': auth["lastname"],
515 'active': auth["active"],
515 'active': auth["active"],
516 'admin': auth["admin"],
516 'admin': auth["admin"],
517 'extern_name': auth["extern_name"],
517 'extern_name': auth["extern_name"],
518 'extern_type': self.name,
518 'extern_type': self.name,
519 'plugin': self,
519 'plugin': self,
520 'allow_to_create_user': self.allows_creating_users,
520 'allow_to_create_user': self.allows_creating_users,
521 }
521 }
522
522
523 if not is_user_existing:
523 if not is_user_existing:
524 if self.use_fake_password():
524 if self.use_fake_password():
525 # Randomize the PW because we don't need it, but don't want
525 # Randomize the PW because we don't need it, but don't want
526 # them blank either
526 # them blank either
527 passwd = PasswordGenerator().gen_password(length=16)
527 passwd = PasswordGenerator().gen_password(length=16)
528 user_parameters['password'] = passwd
528 user_parameters['password'] = passwd
529 else:
529 else:
530 # Since the password is required by create_or_update method of
530 # Since the password is required by create_or_update method of
531 # UserModel, we need to set it explicitly.
531 # UserModel, we need to set it explicitly.
532 # The create_or_update method is smart and recognises the
532 # The create_or_update method is smart and recognises the
533 # password hashes as well.
533 # password hashes as well.
534 user_parameters['password'] = cur_user.password
534 user_parameters['password'] = cur_user.password
535
535
536 # we either create or update users, we also pass the flag
536 # we either create or update users, we also pass the flag
537 # that controls if this method can actually do that.
537 # that controls if this method can actually do that.
538 # raises NotAllowedToCreateUserError if it cannot, and we try to.
538 # raises NotAllowedToCreateUserError if it cannot, and we try to.
539 user = UserModel().create_or_update(**user_parameters)
539 user = UserModel().create_or_update(**user_parameters)
540 Session().flush()
540 Session().flush()
541 # enforce user is just in given groups, all of them has to be ones
541 # enforce user is just in given groups, all of them has to be ones
542 # created from plugins. We store this info in _group_data JSON
542 # created from plugins. We store this info in _group_data JSON
543 # field
543 # field
544
544
545 if auth['user_group_sync']:
545 if auth['user_group_sync']:
546 try:
546 try:
547 groups = auth['groups'] or []
547 groups = auth['groups'] or []
548 log.debug(
548 log.debug(
549 'Performing user_group sync based on set `%s` '
549 'Performing user_group sync based on set `%s` '
550 'returned by `%s` plugin', groups, self.name)
550 'returned by `%s` plugin', groups, self.name)
551 UserGroupModel().enforce_groups(user, groups, self.name)
551 UserGroupModel().enforce_groups(user, groups, self.name)
552 except Exception:
552 except Exception:
553 # for any reason group syncing fails, we should
553 # for any reason group syncing fails, we should
554 # proceed with login
554 # proceed with login
555 log.error(traceback.format_exc())
555 log.error(traceback.format_exc())
556
556
557 Session().commit()
557 Session().commit()
558 return auth
558 return auth
559
559
560
560
561 def loadplugin(plugin_id):
561 def loadplugin(plugin_id):
562 """
562 """
563 Loads and returns an instantiated authentication plugin.
563 Loads and returns an instantiated authentication plugin.
564 Returns the RhodeCodeAuthPluginBase subclass on success,
564 Returns the RhodeCodeAuthPluginBase subclass on success,
565 or None on failure.
565 or None on failure.
566 """
566 """
567 # TODO: Disusing pyramids thread locals to retrieve the registry.
567 # TODO: Disusing pyramids thread locals to retrieve the registry.
568 authn_registry = get_authn_registry()
568 authn_registry = get_authn_registry()
569 plugin = authn_registry.get_plugin(plugin_id)
569 plugin = authn_registry.get_plugin(plugin_id)
570 if plugin is None:
570 if plugin is None:
571 log.error('Authentication plugin not found: "%s"', plugin_id)
571 log.error('Authentication plugin not found: "%s"', plugin_id)
572 return plugin
572 return plugin
573
573
574
574
575 def get_authn_registry(registry=None):
575 def get_authn_registry(registry=None):
576 registry = registry or get_current_registry()
576 registry = registry or get_current_registry()
577 authn_registry = registry.getUtility(IAuthnPluginRegistry)
577 authn_registry = registry.getUtility(IAuthnPluginRegistry)
578 return authn_registry
578 return authn_registry
579
579
580
580
581 def get_auth_cache_manager(custom_ttl=None):
581 def get_auth_cache_manager(custom_ttl=None, suffix=None):
582 cache_name = 'rhodecode.authentication'
583 if suffix:
584 cache_name = 'rhodecode.authentication.{}'.format(suffix)
582 return caches.get_cache_manager(
585 return caches.get_cache_manager(
583 'auth_plugins', 'rhodecode.authentication', custom_ttl)
586 'auth_plugins', cache_name, custom_ttl)
584
587
585
588
586 def get_perms_cache_manager(custom_ttl=None):
589 def get_perms_cache_manager(custom_ttl=None, suffix=None):
590 cache_name = 'rhodecode.permissions'
591 if suffix:
592 cache_name = 'rhodecode.permissions.{}'.format(suffix)
587 return caches.get_cache_manager(
593 return caches.get_cache_manager(
588 'auth_plugins', 'rhodecode.permissions', custom_ttl)
594 'auth_plugins', cache_name, custom_ttl)
589
595
590
596
591 def authenticate(username, password, environ=None, auth_type=None,
597 def authenticate(username, password, environ=None, auth_type=None,
592 skip_missing=False, registry=None, acl_repo_name=None):
598 skip_missing=False, registry=None, acl_repo_name=None):
593 """
599 """
594 Authentication function used for access control,
600 Authentication function used for access control,
595 It tries to authenticate based on enabled authentication modules.
601 It tries to authenticate based on enabled authentication modules.
596
602
597 :param username: username can be empty for headers auth
603 :param username: username can be empty for headers auth
598 :param password: password can be empty for headers auth
604 :param password: password can be empty for headers auth
599 :param environ: environ headers passed for headers auth
605 :param environ: environ headers passed for headers auth
600 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
606 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
601 :param skip_missing: ignores plugins that are in db but not in environment
607 :param skip_missing: ignores plugins that are in db but not in environment
602 :returns: None if auth failed, plugin_user dict if auth is correct
608 :returns: None if auth failed, plugin_user dict if auth is correct
603 """
609 """
604 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
610 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
605 raise ValueError('auth type must be on of http, vcs got "%s" instead'
611 raise ValueError('auth type must be on of http, vcs got "%s" instead'
606 % auth_type)
612 % auth_type)
607 headers_only = environ and not (username and password)
613 headers_only = environ and not (username and password)
608
614
609 authn_registry = get_authn_registry(registry)
615 authn_registry = get_authn_registry(registry)
610 plugins_to_check = authn_registry.get_plugins_for_authentication()
616 plugins_to_check = authn_registry.get_plugins_for_authentication()
611 log.debug('Starting ordered authentication chain using %s plugins',
617 log.debug('Starting ordered authentication chain using %s plugins',
612 plugins_to_check)
618 plugins_to_check)
613 for plugin in plugins_to_check:
619 for plugin in plugins_to_check:
614 plugin.set_auth_type(auth_type)
620 plugin.set_auth_type(auth_type)
615 plugin.set_calling_scope_repo(acl_repo_name)
621 plugin.set_calling_scope_repo(acl_repo_name)
616
622
617 if headers_only and not plugin.is_headers_auth:
623 if headers_only and not plugin.is_headers_auth:
618 log.debug('Auth type is for headers only and plugin `%s` is not '
624 log.debug('Auth type is for headers only and plugin `%s` is not '
619 'headers plugin, skipping...', plugin.get_id())
625 'headers plugin, skipping...', plugin.get_id())
620 continue
626 continue
621
627
622 # load plugin settings from RhodeCode database
628 # load plugin settings from RhodeCode database
623 plugin_settings = plugin.get_settings()
629 plugin_settings = plugin.get_settings()
624 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
630 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
625 log.debug('Plugin settings:%s', plugin_sanitized_settings)
631 log.debug('Plugin settings:%s', plugin_sanitized_settings)
626
632
627 log.debug('Trying authentication using ** %s **', plugin.get_id())
633 log.debug('Trying authentication using ** %s **', plugin.get_id())
628 # use plugin's method of user extraction.
634 # use plugin's method of user extraction.
629 user = plugin.get_user(username, environ=environ,
635 user = plugin.get_user(username, environ=environ,
630 settings=plugin_settings)
636 settings=plugin_settings)
631 display_user = user.username if user else username
637 display_user = user.username if user else username
632 log.debug(
638 log.debug(
633 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
639 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
634
640
635 if not plugin.allows_authentication_from(user):
641 if not plugin.allows_authentication_from(user):
636 log.debug('Plugin %s does not accept user `%s` for authentication',
642 log.debug('Plugin %s does not accept user `%s` for authentication',
637 plugin.get_id(), display_user)
643 plugin.get_id(), display_user)
638 continue
644 continue
639 else:
645 else:
640 log.debug('Plugin %s accepted user `%s` for authentication',
646 log.debug('Plugin %s accepted user `%s` for authentication',
641 plugin.get_id(), display_user)
647 plugin.get_id(), display_user)
642
648
643 log.info('Authenticating user `%s` using %s plugin',
649 log.info('Authenticating user `%s` using %s plugin',
644 display_user, plugin.get_id())
650 display_user, plugin.get_id())
645
651
646 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
652 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
647
653
648 # get instance of cache manager configured for a namespace
654 # get instance of cache manager configured for a namespace
649 cache_manager = get_auth_cache_manager(custom_ttl=cache_ttl)
655 cache_manager = get_auth_cache_manager(
656 custom_ttl=cache_ttl, suffix=user.user_id)
650
657
651 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
658 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
652 plugin.get_id(), plugin_cache_active, cache_ttl)
659 plugin.get_id(), plugin_cache_active, cache_ttl)
653
660
654 # for environ based password can be empty, but then the validation is
661 # for environ based password can be empty, but then the validation is
655 # on the server that fills in the env data needed for authentication
662 # on the server that fills in the env data needed for authentication
656
663
657 _password_hash = caches.compute_key_from_params(
664 _password_hash = caches.compute_key_from_params(
658 plugin.name, username, (password or ''))
665 plugin.name, username, (password or ''))
659
666
660 # _authenticate is a wrapper for .auth() method of plugin.
667 # _authenticate is a wrapper for .auth() method of plugin.
661 # it checks if .auth() sends proper data.
668 # it checks if .auth() sends proper data.
662 # For RhodeCodeExternalAuthPlugin it also maps users to
669 # For RhodeCodeExternalAuthPlugin it also maps users to
663 # Database and maps the attributes returned from .auth()
670 # Database and maps the attributes returned from .auth()
664 # to RhodeCode database. If this function returns data
671 # to RhodeCode database. If this function returns data
665 # then auth is correct.
672 # then auth is correct.
666 start = time.time()
673 start = time.time()
667 log.debug('Running plugin `%s` _authenticate method', plugin.get_id())
674 log.debug('Running plugin `%s` _authenticate method', plugin.get_id())
668
675
669 def auth_func():
676 def auth_func():
670 """
677 """
671 This function is used internally in Cache of Beaker to calculate
678 This function is used internally in Cache of Beaker to calculate
672 Results
679 Results
673 """
680 """
674 log.debug('auth: calculating password access now...')
681 log.debug('auth: calculating password access now...')
675 return plugin._authenticate(
682 return plugin._authenticate(
676 user, username, password, plugin_settings,
683 user, username, password, plugin_settings,
677 environ=environ or {})
684 environ=environ or {})
678
685
679 if plugin_cache_active:
686 if plugin_cache_active:
680 log.debug('Trying to fetch cached auth by `...%s`', _password_hash[:6])
687 log.debug('Trying to fetch cached auth by `...%s`', _password_hash[:6])
681 plugin_user = cache_manager.get(
688 plugin_user = cache_manager.get(
682 _password_hash, createfunc=auth_func)
689 _password_hash, createfunc=auth_func)
683 else:
690 else:
684 plugin_user = auth_func()
691 plugin_user = auth_func()
685
692
686 auth_time = time.time() - start
693 auth_time = time.time() - start
687 log.debug('Authentication for plugin `%s` completed in %.3fs, '
694 log.debug('Authentication for plugin `%s` completed in %.3fs, '
688 'expiration time of fetched cache %.1fs.',
695 'expiration time of fetched cache %.1fs.',
689 plugin.get_id(), auth_time, cache_ttl)
696 plugin.get_id(), auth_time, cache_ttl)
690
697
691 log.debug('PLUGIN USER DATA: %s', plugin_user)
698 log.debug('PLUGIN USER DATA: %s', plugin_user)
692
699
693 if plugin_user:
700 if plugin_user:
694 log.debug('Plugin returned proper authentication data')
701 log.debug('Plugin returned proper authentication data')
695 return plugin_user
702 return plugin_user
696 # we failed to Auth because .auth() method didn't return proper user
703 # we failed to Auth because .auth() method didn't return proper user
697 log.debug("User `%s` failed to authenticate against %s",
704 log.debug("User `%s` failed to authenticate against %s",
698 display_user, plugin.get_id())
705 display_user, plugin.get_id())
699
706
700 # case when we failed to authenticate against all defined plugins
707 # case when we failed to authenticate against all defined plugins
701 return None
708 return None
702
709
703
710
704 def chop_at(s, sub, inclusive=False):
711 def chop_at(s, sub, inclusive=False):
705 """Truncate string ``s`` at the first occurrence of ``sub``.
712 """Truncate string ``s`` at the first occurrence of ``sub``.
706
713
707 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
714 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
708
715
709 >>> chop_at("plutocratic brats", "rat")
716 >>> chop_at("plutocratic brats", "rat")
710 'plutoc'
717 'plutoc'
711 >>> chop_at("plutocratic brats", "rat", True)
718 >>> chop_at("plutocratic brats", "rat", True)
712 'plutocrat'
719 'plutocrat'
713 """
720 """
714 pos = s.find(sub)
721 pos = s.find(sub)
715 if pos == -1:
722 if pos == -1:
716 return s
723 return s
717 if inclusive:
724 if inclusive:
718 return s[:pos+len(sub)]
725 return s[:pos+len(sub)]
719 return s[:pos]
726 return s[:pos]
@@ -1,186 +1,191 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2018 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 formencode.htmlfill
22 import formencode.htmlfill
23 import logging
23 import logging
24
24
25 from pyramid.httpexceptions import HTTPFound
25 from pyramid.httpexceptions import HTTPFound
26 from pyramid.renderers import render
26 from pyramid.renderers import render
27 from pyramid.response import Response
27 from pyramid.response import Response
28
28
29 from rhodecode.apps._base import BaseAppView
29 from rhodecode.apps._base import BaseAppView
30 from rhodecode.authentication.base import (
30 from rhodecode.authentication.base import (
31 get_auth_cache_manager, get_perms_cache_manager, get_authn_registry)
31 get_auth_cache_manager, get_perms_cache_manager, get_authn_registry)
32 from rhodecode.lib import helpers as h
32 from rhodecode.lib import helpers as h
33 from rhodecode.lib.auth import (
33 from rhodecode.lib.auth import (
34 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
34 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
35 from rhodecode.lib.caches import clear_cache_manager
35 from rhodecode.model.forms import AuthSettingsForm
36 from rhodecode.model.forms import AuthSettingsForm
36 from rhodecode.model.meta import Session
37 from rhodecode.model.meta import Session
37 from rhodecode.model.settings import SettingsModel
38 from rhodecode.model.settings import SettingsModel
38
39
39 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
40
41
41
42
42 class AuthnPluginViewBase(BaseAppView):
43 class AuthnPluginViewBase(BaseAppView):
43
44
44 def load_default_context(self):
45 def load_default_context(self):
45 c = self._get_local_tmpl_context()
46 c = self._get_local_tmpl_context()
46 self.plugin = self.context.plugin
47 self.plugin = self.context.plugin
47 return c
48 return c
48
49
49 @LoginRequired()
50 @LoginRequired()
50 @HasPermissionAllDecorator('hg.admin')
51 @HasPermissionAllDecorator('hg.admin')
51 def settings_get(self, defaults=None, errors=None):
52 def settings_get(self, defaults=None, errors=None):
52 """
53 """
53 View that displays the plugin settings as a form.
54 View that displays the plugin settings as a form.
54 """
55 """
55 c = self.load_default_context()
56 c = self.load_default_context()
56 defaults = defaults or {}
57 defaults = defaults or {}
57 errors = errors or {}
58 errors = errors or {}
58 schema = self.plugin.get_settings_schema()
59 schema = self.plugin.get_settings_schema()
59
60
60 # Compute default values for the form. Priority is:
61 # Compute default values for the form. Priority is:
61 # 1. Passed to this method 2. DB value 3. Schema default
62 # 1. Passed to this method 2. DB value 3. Schema default
62 for node in schema:
63 for node in schema:
63 if node.name not in defaults:
64 if node.name not in defaults:
64 defaults[node.name] = self.plugin.get_setting_by_name(
65 defaults[node.name] = self.plugin.get_setting_by_name(
65 node.name, node.default, cache=False)
66 node.name, node.default, cache=False)
66
67
67 template_context = {
68 template_context = {
68 'defaults': defaults,
69 'defaults': defaults,
69 'errors': errors,
70 'errors': errors,
70 'plugin': self.context.plugin,
71 'plugin': self.context.plugin,
71 'resource': self.context,
72 'resource': self.context,
72 }
73 }
73
74
74 return self._get_template_context(c, **template_context)
75 return self._get_template_context(c, **template_context)
75
76
76 @LoginRequired()
77 @LoginRequired()
77 @HasPermissionAllDecorator('hg.admin')
78 @HasPermissionAllDecorator('hg.admin')
78 @CSRFRequired()
79 @CSRFRequired()
79 def settings_post(self):
80 def settings_post(self):
80 """
81 """
81 View that validates and stores the plugin settings.
82 View that validates and stores the plugin settings.
82 """
83 """
83 _ = self.request.translate
84 _ = self.request.translate
84 self.load_default_context()
85 self.load_default_context()
85 schema = self.plugin.get_settings_schema()
86 schema = self.plugin.get_settings_schema()
86 data = self.request.params
87 data = self.request.params
87
88
88 try:
89 try:
89 valid_data = schema.deserialize(data)
90 valid_data = schema.deserialize(data)
90 except colander.Invalid as e:
91 except colander.Invalid as e:
91 # Display error message and display form again.
92 # Display error message and display form again.
92 h.flash(
93 h.flash(
93 _('Errors exist when saving plugin settings. '
94 _('Errors exist when saving plugin settings. '
94 'Please check the form inputs.'),
95 'Please check the form inputs.'),
95 category='error')
96 category='error')
96 defaults = {key: data[key] for key in data if key in schema}
97 defaults = {key: data[key] for key in data if key in schema}
97 return self.settings_get(errors=e.asdict(), defaults=defaults)
98 return self.settings_get(errors=e.asdict(), defaults=defaults)
98
99
99 # Store validated data.
100 # Store validated data.
100 for name, value in valid_data.items():
101 for name, value in valid_data.items():
101 self.plugin.create_or_update_setting(name, value)
102 self.plugin.create_or_update_setting(name, value)
102 Session().commit()
103 Session().commit()
103
104
105 # cleanup cache managers in case of change for plugin
106 # TODO(marcink): because we can register multiple namespaces
107 # we should at some point figure out how to retrieve ALL namespace
108 # cache managers and clear them...
109 cache_manager = get_auth_cache_manager()
110 clear_cache_manager(cache_manager)
111
112 cache_manager = get_perms_cache_manager()
113 clear_cache_manager(cache_manager)
114
104 # Display success message and redirect.
115 # Display success message and redirect.
105 h.flash(_('Auth settings updated successfully.'), category='success')
116 h.flash(_('Auth settings updated successfully.'), category='success')
106 redirect_to = self.request.resource_path(
117 redirect_to = self.request.resource_path(
107 self.context, route_name='auth_home')
118 self.context, route_name='auth_home')
108 return HTTPFound(redirect_to)
119 return HTTPFound(redirect_to)
109
120
110
121
111 class AuthSettingsView(BaseAppView):
122 class AuthSettingsView(BaseAppView):
112 def load_default_context(self):
123 def load_default_context(self):
113 c = self._get_local_tmpl_context()
124 c = self._get_local_tmpl_context()
114 return c
125 return c
115
126
116 @LoginRequired()
127 @LoginRequired()
117 @HasPermissionAllDecorator('hg.admin')
128 @HasPermissionAllDecorator('hg.admin')
118 def index(self, defaults=None, errors=None, prefix_error=False):
129 def index(self, defaults=None, errors=None, prefix_error=False):
119 c = self.load_default_context()
130 c = self.load_default_context()
120
131
121 defaults = defaults or {}
132 defaults = defaults or {}
122 authn_registry = get_authn_registry(self.request.registry)
133 authn_registry = get_authn_registry(self.request.registry)
123 enabled_plugins = SettingsModel().get_auth_plugins()
134 enabled_plugins = SettingsModel().get_auth_plugins()
124
135
125 # Create template context and render it.
136 # Create template context and render it.
126 template_context = {
137 template_context = {
127 'resource': self.context,
138 'resource': self.context,
128 'available_plugins': authn_registry.get_plugins(),
139 'available_plugins': authn_registry.get_plugins(),
129 'enabled_plugins': enabled_plugins,
140 'enabled_plugins': enabled_plugins,
130 }
141 }
131 html = render('rhodecode:templates/admin/auth/auth_settings.mako',
142 html = render('rhodecode:templates/admin/auth/auth_settings.mako',
132 self._get_template_context(c, **template_context),
143 self._get_template_context(c, **template_context),
133 self.request)
144 self.request)
134
145
135 # Create form default values and fill the form.
146 # Create form default values and fill the form.
136 form_defaults = {
147 form_defaults = {
137 'auth_plugins': ','.join(enabled_plugins)
148 'auth_plugins': ','.join(enabled_plugins)
138 }
149 }
139 form_defaults.update(defaults)
150 form_defaults.update(defaults)
140 html = formencode.htmlfill.render(
151 html = formencode.htmlfill.render(
141 html,
152 html,
142 defaults=form_defaults,
153 defaults=form_defaults,
143 errors=errors,
154 errors=errors,
144 prefix_error=prefix_error,
155 prefix_error=prefix_error,
145 encoding="UTF-8",
156 encoding="UTF-8",
146 force_defaults=False)
157 force_defaults=False)
147
158
148 return Response(html)
159 return Response(html)
149
160
150 @LoginRequired()
161 @LoginRequired()
151 @HasPermissionAllDecorator('hg.admin')
162 @HasPermissionAllDecorator('hg.admin')
152 @CSRFRequired()
163 @CSRFRequired()
153 def auth_settings(self):
164 def auth_settings(self):
154 _ = self.request.translate
165 _ = self.request.translate
155 try:
166 try:
156 form = AuthSettingsForm(self.request.translate)()
167 form = AuthSettingsForm(self.request.translate)()
157 form_result = form.to_python(self.request.POST)
168 form_result = form.to_python(self.request.POST)
158 plugins = ','.join(form_result['auth_plugins'])
169 plugins = ','.join(form_result['auth_plugins'])
159 setting = SettingsModel().create_or_update_setting(
170 setting = SettingsModel().create_or_update_setting(
160 'auth_plugins', plugins)
171 'auth_plugins', plugins)
161 Session().add(setting)
172 Session().add(setting)
162 Session().commit()
173 Session().commit()
163
174
164 cache_manager = get_auth_cache_manager()
165 cache_manager.clear()
166
167 cache_manager = get_perms_cache_manager()
168 cache_manager.clear()
169
170 h.flash(_('Auth settings updated successfully.'), category='success')
175 h.flash(_('Auth settings updated successfully.'), category='success')
171 except formencode.Invalid as errors:
176 except formencode.Invalid as errors:
172 e = errors.error_dict or {}
177 e = errors.error_dict or {}
173 h.flash(_('Errors exist when saving plugin setting. '
178 h.flash(_('Errors exist when saving plugin setting. '
174 'Please check the form inputs.'), category='error')
179 'Please check the form inputs.'), category='error')
175 return self.index(
180 return self.index(
176 defaults=errors.value,
181 defaults=errors.value,
177 errors=e,
182 errors=e,
178 prefix_error=False)
183 prefix_error=False)
179 except Exception:
184 except Exception:
180 log.exception('Exception in auth_settings')
185 log.exception('Exception in auth_settings')
181 h.flash(_('Error occurred during update of auth settings.'),
186 h.flash(_('Error occurred during update of auth settings.'),
182 category='error')
187 category='error')
183
188
184 redirect_to = self.request.resource_path(
189 redirect_to = self.request.resource_path(
185 self.context, route_name='auth_home')
190 self.context, route_name='auth_home')
186 return HTTPFound(redirect_to)
191 return HTTPFound(redirect_to)
@@ -1,644 +1,645 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2018 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 SimpleVCS middleware for handling protocol request (push/clone etc.)
22 SimpleVCS middleware for handling protocol request (push/clone etc.)
23 It's implemented with basic auth function
23 It's implemented with basic auth function
24 """
24 """
25
25
26 import os
26 import os
27 import re
27 import re
28 import logging
28 import logging
29 import importlib
29 import importlib
30 from functools import wraps
30 from functools import wraps
31
31
32 import time
32 import time
33 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
33 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
34 # TODO(marcink): check if we should use webob.exc here ?
34 # TODO(marcink): check if we should use webob.exc here ?
35 from pyramid.httpexceptions import (
35 from pyramid.httpexceptions import (
36 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
36 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
37 from zope.cachedescriptors.property import Lazy as LazyProperty
37 from zope.cachedescriptors.property import Lazy as LazyProperty
38
38
39 import rhodecode
39 import rhodecode
40 from rhodecode.authentication.base import (
40 from rhodecode.authentication.base import (
41 authenticate, get_perms_cache_manager, VCS_TYPE, loadplugin)
41 authenticate, get_perms_cache_manager, VCS_TYPE, loadplugin)
42 from rhodecode.lib import caches
42 from rhodecode.lib import caches
43 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
43 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
44 from rhodecode.lib.base import (
44 from rhodecode.lib.base import (
45 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
45 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
46 from rhodecode.lib.exceptions import (
46 from rhodecode.lib.exceptions import (
47 HTTPLockedRC, HTTPRequirementError, UserCreationError,
47 HTTPLockedRC, HTTPRequirementError, UserCreationError,
48 NotAllowedToCreateUserError)
48 NotAllowedToCreateUserError)
49 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
49 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
50 from rhodecode.lib.middleware import appenlight
50 from rhodecode.lib.middleware import appenlight
51 from rhodecode.lib.middleware.utils import scm_app_http
51 from rhodecode.lib.middleware.utils import scm_app_http
52 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
52 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
53 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
53 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
54 from rhodecode.lib.vcs.conf import settings as vcs_settings
54 from rhodecode.lib.vcs.conf import settings as vcs_settings
55 from rhodecode.lib.vcs.backends import base
55 from rhodecode.lib.vcs.backends import base
56 from rhodecode.model import meta
56 from rhodecode.model import meta
57 from rhodecode.model.db import User, Repository, PullRequest
57 from rhodecode.model.db import User, Repository, PullRequest
58 from rhodecode.model.scm import ScmModel
58 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.pull_request import PullRequestModel
59 from rhodecode.model.pull_request import PullRequestModel
60 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
60 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
61
61
62 log = logging.getLogger(__name__)
62 log = logging.getLogger(__name__)
63
63
64
64
65 def initialize_generator(factory):
65 def initialize_generator(factory):
66 """
66 """
67 Initializes the returned generator by draining its first element.
67 Initializes the returned generator by draining its first element.
68
68
69 This can be used to give a generator an initializer, which is the code
69 This can be used to give a generator an initializer, which is the code
70 up to the first yield statement. This decorator enforces that the first
70 up to the first yield statement. This decorator enforces that the first
71 produced element has the value ``"__init__"`` to make its special
71 produced element has the value ``"__init__"`` to make its special
72 purpose very explicit in the using code.
72 purpose very explicit in the using code.
73 """
73 """
74
74
75 @wraps(factory)
75 @wraps(factory)
76 def wrapper(*args, **kwargs):
76 def wrapper(*args, **kwargs):
77 gen = factory(*args, **kwargs)
77 gen = factory(*args, **kwargs)
78 try:
78 try:
79 init = gen.next()
79 init = gen.next()
80 except StopIteration:
80 except StopIteration:
81 raise ValueError('Generator must yield at least one element.')
81 raise ValueError('Generator must yield at least one element.')
82 if init != "__init__":
82 if init != "__init__":
83 raise ValueError('First yielded element must be "__init__".')
83 raise ValueError('First yielded element must be "__init__".')
84 return gen
84 return gen
85 return wrapper
85 return wrapper
86
86
87
87
88 class SimpleVCS(object):
88 class SimpleVCS(object):
89 """Common functionality for SCM HTTP handlers."""
89 """Common functionality for SCM HTTP handlers."""
90
90
91 SCM = 'unknown'
91 SCM = 'unknown'
92
92
93 acl_repo_name = None
93 acl_repo_name = None
94 url_repo_name = None
94 url_repo_name = None
95 vcs_repo_name = None
95 vcs_repo_name = None
96 rc_extras = {}
96 rc_extras = {}
97
97
98 # We have to handle requests to shadow repositories different than requests
98 # We have to handle requests to shadow repositories different than requests
99 # to normal repositories. Therefore we have to distinguish them. To do this
99 # to normal repositories. Therefore we have to distinguish them. To do this
100 # we use this regex which will match only on URLs pointing to shadow
100 # we use this regex which will match only on URLs pointing to shadow
101 # repositories.
101 # repositories.
102 shadow_repo_re = re.compile(
102 shadow_repo_re = re.compile(
103 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
103 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
104 '(?P<target>{slug_pat})/' # target repo
104 '(?P<target>{slug_pat})/' # target repo
105 'pull-request/(?P<pr_id>\d+)/' # pull request
105 'pull-request/(?P<pr_id>\d+)/' # pull request
106 'repository$' # shadow repo
106 'repository$' # shadow repo
107 .format(slug_pat=SLUG_RE.pattern))
107 .format(slug_pat=SLUG_RE.pattern))
108
108
109 def __init__(self, config, registry):
109 def __init__(self, config, registry):
110 self.registry = registry
110 self.registry = registry
111 self.config = config
111 self.config = config
112 # re-populated by specialized middleware
112 # re-populated by specialized middleware
113 self.repo_vcs_config = base.Config()
113 self.repo_vcs_config = base.Config()
114 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
114 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
115
115
116 registry.rhodecode_settings = self.rhodecode_settings
116 registry.rhodecode_settings = self.rhodecode_settings
117 # authenticate this VCS request using authfunc
117 # authenticate this VCS request using authfunc
118 auth_ret_code_detection = \
118 auth_ret_code_detection = \
119 str2bool(self.config.get('auth_ret_code_detection', False))
119 str2bool(self.config.get('auth_ret_code_detection', False))
120 self.authenticate = BasicAuth(
120 self.authenticate = BasicAuth(
121 '', authenticate, registry, config.get('auth_ret_code'),
121 '', authenticate, registry, config.get('auth_ret_code'),
122 auth_ret_code_detection)
122 auth_ret_code_detection)
123 self.ip_addr = '0.0.0.0'
123 self.ip_addr = '0.0.0.0'
124
124
125 @LazyProperty
125 @LazyProperty
126 def global_vcs_config(self):
126 def global_vcs_config(self):
127 try:
127 try:
128 return VcsSettingsModel().get_ui_settings_as_config_obj()
128 return VcsSettingsModel().get_ui_settings_as_config_obj()
129 except Exception:
129 except Exception:
130 return base.Config()
130 return base.Config()
131
131
132 @property
132 @property
133 def base_path(self):
133 def base_path(self):
134 settings_path = self.repo_vcs_config.get(
134 settings_path = self.repo_vcs_config.get(
135 *VcsSettingsModel.PATH_SETTING)
135 *VcsSettingsModel.PATH_SETTING)
136
136
137 if not settings_path:
137 if not settings_path:
138 settings_path = self.global_vcs_config.get(
138 settings_path = self.global_vcs_config.get(
139 *VcsSettingsModel.PATH_SETTING)
139 *VcsSettingsModel.PATH_SETTING)
140
140
141 if not settings_path:
141 if not settings_path:
142 # try, maybe we passed in explicitly as config option
142 # try, maybe we passed in explicitly as config option
143 settings_path = self.config.get('base_path')
143 settings_path = self.config.get('base_path')
144
144
145 if not settings_path:
145 if not settings_path:
146 raise ValueError('FATAL: base_path is empty')
146 raise ValueError('FATAL: base_path is empty')
147 return settings_path
147 return settings_path
148
148
149 def set_repo_names(self, environ):
149 def set_repo_names(self, environ):
150 """
150 """
151 This will populate the attributes acl_repo_name, url_repo_name,
151 This will populate the attributes acl_repo_name, url_repo_name,
152 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
152 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
153 shadow) repositories all names are equal. In case of requests to a
153 shadow) repositories all names are equal. In case of requests to a
154 shadow repository the acl-name points to the target repo of the pull
154 shadow repository the acl-name points to the target repo of the pull
155 request and the vcs-name points to the shadow repo file system path.
155 request and the vcs-name points to the shadow repo file system path.
156 The url-name is always the URL used by the vcs client program.
156 The url-name is always the URL used by the vcs client program.
157
157
158 Example in case of a shadow repo:
158 Example in case of a shadow repo:
159 acl_repo_name = RepoGroup/MyRepo
159 acl_repo_name = RepoGroup/MyRepo
160 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
160 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
161 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
161 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
162 """
162 """
163 # First we set the repo name from URL for all attributes. This is the
163 # First we set the repo name from URL for all attributes. This is the
164 # default if handling normal (non shadow) repo requests.
164 # default if handling normal (non shadow) repo requests.
165 self.url_repo_name = self._get_repository_name(environ)
165 self.url_repo_name = self._get_repository_name(environ)
166 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
166 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
167 self.is_shadow_repo = False
167 self.is_shadow_repo = False
168
168
169 # Check if this is a request to a shadow repository.
169 # Check if this is a request to a shadow repository.
170 match = self.shadow_repo_re.match(self.url_repo_name)
170 match = self.shadow_repo_re.match(self.url_repo_name)
171 if match:
171 if match:
172 match_dict = match.groupdict()
172 match_dict = match.groupdict()
173
173
174 # Build acl repo name from regex match.
174 # Build acl repo name from regex match.
175 acl_repo_name = safe_unicode('{groups}{target}'.format(
175 acl_repo_name = safe_unicode('{groups}{target}'.format(
176 groups=match_dict['groups'] or '',
176 groups=match_dict['groups'] or '',
177 target=match_dict['target']))
177 target=match_dict['target']))
178
178
179 # Retrieve pull request instance by ID from regex match.
179 # Retrieve pull request instance by ID from regex match.
180 pull_request = PullRequest.get(match_dict['pr_id'])
180 pull_request = PullRequest.get(match_dict['pr_id'])
181
181
182 # Only proceed if we got a pull request and if acl repo name from
182 # Only proceed if we got a pull request and if acl repo name from
183 # URL equals the target repo name of the pull request.
183 # URL equals the target repo name of the pull request.
184 if pull_request and (acl_repo_name ==
184 if pull_request and (acl_repo_name ==
185 pull_request.target_repo.repo_name):
185 pull_request.target_repo.repo_name):
186 # Get file system path to shadow repository.
186 # Get file system path to shadow repository.
187 workspace_id = PullRequestModel()._workspace_id(pull_request)
187 workspace_id = PullRequestModel()._workspace_id(pull_request)
188 target_vcs = pull_request.target_repo.scm_instance()
188 target_vcs = pull_request.target_repo.scm_instance()
189 vcs_repo_name = target_vcs._get_shadow_repository_path(
189 vcs_repo_name = target_vcs._get_shadow_repository_path(
190 workspace_id)
190 workspace_id)
191
191
192 # Store names for later usage.
192 # Store names for later usage.
193 self.vcs_repo_name = vcs_repo_name
193 self.vcs_repo_name = vcs_repo_name
194 self.acl_repo_name = acl_repo_name
194 self.acl_repo_name = acl_repo_name
195 self.is_shadow_repo = True
195 self.is_shadow_repo = True
196
196
197 log.debug('Setting all VCS repository names: %s', {
197 log.debug('Setting all VCS repository names: %s', {
198 'acl_repo_name': self.acl_repo_name,
198 'acl_repo_name': self.acl_repo_name,
199 'url_repo_name': self.url_repo_name,
199 'url_repo_name': self.url_repo_name,
200 'vcs_repo_name': self.vcs_repo_name,
200 'vcs_repo_name': self.vcs_repo_name,
201 })
201 })
202
202
203 @property
203 @property
204 def scm_app(self):
204 def scm_app(self):
205 custom_implementation = self.config['vcs.scm_app_implementation']
205 custom_implementation = self.config['vcs.scm_app_implementation']
206 if custom_implementation == 'http':
206 if custom_implementation == 'http':
207 log.info('Using HTTP implementation of scm app.')
207 log.info('Using HTTP implementation of scm app.')
208 scm_app_impl = scm_app_http
208 scm_app_impl = scm_app_http
209 else:
209 else:
210 log.info('Using custom implementation of scm_app: "{}"'.format(
210 log.info('Using custom implementation of scm_app: "{}"'.format(
211 custom_implementation))
211 custom_implementation))
212 scm_app_impl = importlib.import_module(custom_implementation)
212 scm_app_impl = importlib.import_module(custom_implementation)
213 return scm_app_impl
213 return scm_app_impl
214
214
215 def _get_by_id(self, repo_name):
215 def _get_by_id(self, repo_name):
216 """
216 """
217 Gets a special pattern _<ID> from clone url and tries to replace it
217 Gets a special pattern _<ID> from clone url and tries to replace it
218 with a repository_name for support of _<ID> non changeable urls
218 with a repository_name for support of _<ID> non changeable urls
219 """
219 """
220
220
221 data = repo_name.split('/')
221 data = repo_name.split('/')
222 if len(data) >= 2:
222 if len(data) >= 2:
223 from rhodecode.model.repo import RepoModel
223 from rhodecode.model.repo import RepoModel
224 by_id_match = RepoModel().get_repo_by_id(repo_name)
224 by_id_match = RepoModel().get_repo_by_id(repo_name)
225 if by_id_match:
225 if by_id_match:
226 data[1] = by_id_match.repo_name
226 data[1] = by_id_match.repo_name
227
227
228 return safe_str('/'.join(data))
228 return safe_str('/'.join(data))
229
229
230 def _invalidate_cache(self, repo_name):
230 def _invalidate_cache(self, repo_name):
231 """
231 """
232 Set's cache for this repository for invalidation on next access
232 Set's cache for this repository for invalidation on next access
233
233
234 :param repo_name: full repo name, also a cache key
234 :param repo_name: full repo name, also a cache key
235 """
235 """
236 ScmModel().mark_for_invalidation(repo_name)
236 ScmModel().mark_for_invalidation(repo_name)
237
237
238 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
238 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
239 db_repo = Repository.get_by_repo_name(repo_name)
239 db_repo = Repository.get_by_repo_name(repo_name)
240 if not db_repo:
240 if not db_repo:
241 log.debug('Repository `%s` not found inside the database.',
241 log.debug('Repository `%s` not found inside the database.',
242 repo_name)
242 repo_name)
243 return False
243 return False
244
244
245 if db_repo.repo_type != scm_type:
245 if db_repo.repo_type != scm_type:
246 log.warning(
246 log.warning(
247 'Repository `%s` have incorrect scm_type, expected %s got %s',
247 'Repository `%s` have incorrect scm_type, expected %s got %s',
248 repo_name, db_repo.repo_type, scm_type)
248 repo_name, db_repo.repo_type, scm_type)
249 return False
249 return False
250
250
251 config = db_repo._config
251 config = db_repo._config
252 config.set('extensions', 'largefiles', '')
252 config.set('extensions', 'largefiles', '')
253 return is_valid_repo(
253 return is_valid_repo(
254 repo_name, base_path,
254 repo_name, base_path,
255 explicit_scm=scm_type, expect_scm=scm_type, config=config)
255 explicit_scm=scm_type, expect_scm=scm_type, config=config)
256
256
257 def valid_and_active_user(self, user):
257 def valid_and_active_user(self, user):
258 """
258 """
259 Checks if that user is not empty, and if it's actually object it checks
259 Checks if that user is not empty, and if it's actually object it checks
260 if he's active.
260 if he's active.
261
261
262 :param user: user object or None
262 :param user: user object or None
263 :return: boolean
263 :return: boolean
264 """
264 """
265 if user is None:
265 if user is None:
266 return False
266 return False
267
267
268 elif user.active:
268 elif user.active:
269 return True
269 return True
270
270
271 return False
271 return False
272
272
273 @property
273 @property
274 def is_shadow_repo_dir(self):
274 def is_shadow_repo_dir(self):
275 return os.path.isdir(self.vcs_repo_name)
275 return os.path.isdir(self.vcs_repo_name)
276
276
277 def _check_permission(self, action, user, repo_name, ip_addr=None,
277 def _check_permission(self, action, user, repo_name, ip_addr=None,
278 plugin_id='', plugin_cache_active=False, cache_ttl=0):
278 plugin_id='', plugin_cache_active=False, cache_ttl=0):
279 """
279 """
280 Checks permissions using action (push/pull) user and repository
280 Checks permissions using action (push/pull) user and repository
281 name. If plugin_cache and ttl is set it will use the plugin which
281 name. If plugin_cache and ttl is set it will use the plugin which
282 authenticated the user to store the cached permissions result for N
282 authenticated the user to store the cached permissions result for N
283 amount of seconds as in cache_ttl
283 amount of seconds as in cache_ttl
284
284
285 :param action: push or pull action
285 :param action: push or pull action
286 :param user: user instance
286 :param user: user instance
287 :param repo_name: repository name
287 :param repo_name: repository name
288 """
288 """
289
289
290 # get instance of cache manager configured for a namespace
290 # get instance of cache manager configured for a namespace
291 cache_manager = get_perms_cache_manager(custom_ttl=cache_ttl)
291 cache_manager = get_perms_cache_manager(
292 custom_ttl=cache_ttl, suffix=user.user_id)
292 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
293 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
293 plugin_id, plugin_cache_active, cache_ttl)
294 plugin_id, plugin_cache_active, cache_ttl)
294
295
295 # for environ based password can be empty, but then the validation is
296 # for environ based password can be empty, but then the validation is
296 # on the server that fills in the env data needed for authentication
297 # on the server that fills in the env data needed for authentication
297 _perm_calc_hash = caches.compute_key_from_params(
298 _perm_calc_hash = caches.compute_key_from_params(
298 plugin_id, action, user.user_id, repo_name, ip_addr)
299 plugin_id, action, user.user_id, repo_name, ip_addr)
299
300
300 # _authenticate is a wrapper for .auth() method of plugin.
301 # _authenticate is a wrapper for .auth() method of plugin.
301 # it checks if .auth() sends proper data.
302 # it checks if .auth() sends proper data.
302 # For RhodeCodeExternalAuthPlugin it also maps users to
303 # For RhodeCodeExternalAuthPlugin it also maps users to
303 # Database and maps the attributes returned from .auth()
304 # Database and maps the attributes returned from .auth()
304 # to RhodeCode database. If this function returns data
305 # to RhodeCode database. If this function returns data
305 # then auth is correct.
306 # then auth is correct.
306 start = time.time()
307 start = time.time()
307 log.debug('Running plugin `%s` permissions check', plugin_id)
308 log.debug('Running plugin `%s` permissions check', plugin_id)
308
309
309 def perm_func():
310 def perm_func():
310 """
311 """
311 This function is used internally in Cache of Beaker to calculate
312 This function is used internally in Cache of Beaker to calculate
312 Results
313 Results
313 """
314 """
314 log.debug('auth: calculating permission access now...')
315 log.debug('auth: calculating permission access now...')
315 # check IP
316 # check IP
316 inherit = user.inherit_default_permissions
317 inherit = user.inherit_default_permissions
317 ip_allowed = AuthUser.check_ip_allowed(
318 ip_allowed = AuthUser.check_ip_allowed(
318 user.user_id, ip_addr, inherit_from_default=inherit)
319 user.user_id, ip_addr, inherit_from_default=inherit)
319 if ip_allowed:
320 if ip_allowed:
320 log.info('Access for IP:%s allowed', ip_addr)
321 log.info('Access for IP:%s allowed', ip_addr)
321 else:
322 else:
322 return False
323 return False
323
324
324 if action == 'push':
325 if action == 'push':
325 perms = ('repository.write', 'repository.admin')
326 perms = ('repository.write', 'repository.admin')
326 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
327 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
327 return False
328 return False
328
329
329 else:
330 else:
330 # any other action need at least read permission
331 # any other action need at least read permission
331 perms = (
332 perms = (
332 'repository.read', 'repository.write', 'repository.admin')
333 'repository.read', 'repository.write', 'repository.admin')
333 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
334 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
334 return False
335 return False
335
336
336 return True
337 return True
337
338
338 if plugin_cache_active:
339 if plugin_cache_active:
339 log.debug('Trying to fetch cached perms by %s', _perm_calc_hash[:6])
340 log.debug('Trying to fetch cached perms by %s', _perm_calc_hash[:6])
340 perm_result = cache_manager.get(
341 perm_result = cache_manager.get(
341 _perm_calc_hash, createfunc=perm_func)
342 _perm_calc_hash, createfunc=perm_func)
342 else:
343 else:
343 perm_result = perm_func()
344 perm_result = perm_func()
344
345
345 auth_time = time.time() - start
346 auth_time = time.time() - start
346 log.debug('Permissions for plugin `%s` completed in %.3fs, '
347 log.debug('Permissions for plugin `%s` completed in %.3fs, '
347 'expiration time of fetched cache %.1fs.',
348 'expiration time of fetched cache %.1fs.',
348 plugin_id, auth_time, cache_ttl)
349 plugin_id, auth_time, cache_ttl)
349
350
350 return perm_result
351 return perm_result
351
352
352 def _check_ssl(self, environ, start_response):
353 def _check_ssl(self, environ, start_response):
353 """
354 """
354 Checks the SSL check flag and returns False if SSL is not present
355 Checks the SSL check flag and returns False if SSL is not present
355 and required True otherwise
356 and required True otherwise
356 """
357 """
357 org_proto = environ['wsgi._org_proto']
358 org_proto = environ['wsgi._org_proto']
358 # check if we have SSL required ! if not it's a bad request !
359 # check if we have SSL required ! if not it's a bad request !
359 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
360 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
360 if require_ssl and org_proto == 'http':
361 if require_ssl and org_proto == 'http':
361 log.debug('proto is %s and SSL is required BAD REQUEST !',
362 log.debug('proto is %s and SSL is required BAD REQUEST !',
362 org_proto)
363 org_proto)
363 return False
364 return False
364 return True
365 return True
365
366
366 def _get_default_cache_ttl(self):
367 def _get_default_cache_ttl(self):
367 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
368 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
368 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
369 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
369 plugin_settings = plugin.get_settings()
370 plugin_settings = plugin.get_settings()
370 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
371 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
371 plugin_settings) or (False, 0)
372 plugin_settings) or (False, 0)
372 return plugin_cache_active, cache_ttl
373 return plugin_cache_active, cache_ttl
373
374
374 def __call__(self, environ, start_response):
375 def __call__(self, environ, start_response):
375 try:
376 try:
376 return self._handle_request(environ, start_response)
377 return self._handle_request(environ, start_response)
377 except Exception:
378 except Exception:
378 log.exception("Exception while handling request")
379 log.exception("Exception while handling request")
379 appenlight.track_exception(environ)
380 appenlight.track_exception(environ)
380 return HTTPInternalServerError()(environ, start_response)
381 return HTTPInternalServerError()(environ, start_response)
381 finally:
382 finally:
382 meta.Session.remove()
383 meta.Session.remove()
383
384
384 def _handle_request(self, environ, start_response):
385 def _handle_request(self, environ, start_response):
385
386
386 if not self._check_ssl(environ, start_response):
387 if not self._check_ssl(environ, start_response):
387 reason = ('SSL required, while RhodeCode was unable '
388 reason = ('SSL required, while RhodeCode was unable '
388 'to detect this as SSL request')
389 'to detect this as SSL request')
389 log.debug('User not allowed to proceed, %s', reason)
390 log.debug('User not allowed to proceed, %s', reason)
390 return HTTPNotAcceptable(reason)(environ, start_response)
391 return HTTPNotAcceptable(reason)(environ, start_response)
391
392
392 if not self.url_repo_name:
393 if not self.url_repo_name:
393 log.warning('Repository name is empty: %s', self.url_repo_name)
394 log.warning('Repository name is empty: %s', self.url_repo_name)
394 # failed to get repo name, we fail now
395 # failed to get repo name, we fail now
395 return HTTPNotFound()(environ, start_response)
396 return HTTPNotFound()(environ, start_response)
396 log.debug('Extracted repo name is %s', self.url_repo_name)
397 log.debug('Extracted repo name is %s', self.url_repo_name)
397
398
398 ip_addr = get_ip_addr(environ)
399 ip_addr = get_ip_addr(environ)
399 user_agent = get_user_agent(environ)
400 user_agent = get_user_agent(environ)
400 username = None
401 username = None
401
402
402 # skip passing error to error controller
403 # skip passing error to error controller
403 environ['pylons.status_code_redirect'] = True
404 environ['pylons.status_code_redirect'] = True
404
405
405 # ======================================================================
406 # ======================================================================
406 # GET ACTION PULL or PUSH
407 # GET ACTION PULL or PUSH
407 # ======================================================================
408 # ======================================================================
408 action = self._get_action(environ)
409 action = self._get_action(environ)
409
410
410 # ======================================================================
411 # ======================================================================
411 # Check if this is a request to a shadow repository of a pull request.
412 # Check if this is a request to a shadow repository of a pull request.
412 # In this case only pull action is allowed.
413 # In this case only pull action is allowed.
413 # ======================================================================
414 # ======================================================================
414 if self.is_shadow_repo and action != 'pull':
415 if self.is_shadow_repo and action != 'pull':
415 reason = 'Only pull action is allowed for shadow repositories.'
416 reason = 'Only pull action is allowed for shadow repositories.'
416 log.debug('User not allowed to proceed, %s', reason)
417 log.debug('User not allowed to proceed, %s', reason)
417 return HTTPNotAcceptable(reason)(environ, start_response)
418 return HTTPNotAcceptable(reason)(environ, start_response)
418
419
419 # Check if the shadow repo actually exists, in case someone refers
420 # Check if the shadow repo actually exists, in case someone refers
420 # to it, and it has been deleted because of successful merge.
421 # to it, and it has been deleted because of successful merge.
421 if self.is_shadow_repo and not self.is_shadow_repo_dir:
422 if self.is_shadow_repo and not self.is_shadow_repo_dir:
422 log.debug('Shadow repo detected, and shadow repo dir `%s` is missing',
423 log.debug('Shadow repo detected, and shadow repo dir `%s` is missing',
423 self.is_shadow_repo_dir)
424 self.is_shadow_repo_dir)
424 return HTTPNotFound()(environ, start_response)
425 return HTTPNotFound()(environ, start_response)
425
426
426 # ======================================================================
427 # ======================================================================
427 # CHECK ANONYMOUS PERMISSION
428 # CHECK ANONYMOUS PERMISSION
428 # ======================================================================
429 # ======================================================================
429 if action in ['pull', 'push']:
430 if action in ['pull', 'push']:
430 anonymous_user = User.get_default_user()
431 anonymous_user = User.get_default_user()
431 username = anonymous_user.username
432 username = anonymous_user.username
432 if anonymous_user.active:
433 if anonymous_user.active:
433 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
434 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
434 # ONLY check permissions if the user is activated
435 # ONLY check permissions if the user is activated
435 anonymous_perm = self._check_permission(
436 anonymous_perm = self._check_permission(
436 action, anonymous_user, self.acl_repo_name, ip_addr,
437 action, anonymous_user, self.acl_repo_name, ip_addr,
437 plugin_id='anonymous_access',
438 plugin_id='anonymous_access',
438 plugin_cache_active=plugin_cache_active, cache_ttl=cache_ttl,
439 plugin_cache_active=plugin_cache_active, cache_ttl=cache_ttl,
439 )
440 )
440 else:
441 else:
441 anonymous_perm = False
442 anonymous_perm = False
442
443
443 if not anonymous_user.active or not anonymous_perm:
444 if not anonymous_user.active or not anonymous_perm:
444 if not anonymous_user.active:
445 if not anonymous_user.active:
445 log.debug('Anonymous access is disabled, running '
446 log.debug('Anonymous access is disabled, running '
446 'authentication')
447 'authentication')
447
448
448 if not anonymous_perm:
449 if not anonymous_perm:
449 log.debug('Not enough credentials to access this '
450 log.debug('Not enough credentials to access this '
450 'repository as anonymous user')
451 'repository as anonymous user')
451
452
452 username = None
453 username = None
453 # ==============================================================
454 # ==============================================================
454 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
455 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
455 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
456 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
456 # ==============================================================
457 # ==============================================================
457
458
458 # try to auth based on environ, container auth methods
459 # try to auth based on environ, container auth methods
459 log.debug('Running PRE-AUTH for container based authentication')
460 log.debug('Running PRE-AUTH for container based authentication')
460 pre_auth = authenticate(
461 pre_auth = authenticate(
461 '', '', environ, VCS_TYPE, registry=self.registry,
462 '', '', environ, VCS_TYPE, registry=self.registry,
462 acl_repo_name=self.acl_repo_name)
463 acl_repo_name=self.acl_repo_name)
463 if pre_auth and pre_auth.get('username'):
464 if pre_auth and pre_auth.get('username'):
464 username = pre_auth['username']
465 username = pre_auth['username']
465 log.debug('PRE-AUTH got %s as username', username)
466 log.debug('PRE-AUTH got %s as username', username)
466 if pre_auth:
467 if pre_auth:
467 log.debug('PRE-AUTH successful from %s',
468 log.debug('PRE-AUTH successful from %s',
468 pre_auth.get('auth_data', {}).get('_plugin'))
469 pre_auth.get('auth_data', {}).get('_plugin'))
469
470
470 # If not authenticated by the container, running basic auth
471 # If not authenticated by the container, running basic auth
471 # before inject the calling repo_name for special scope checks
472 # before inject the calling repo_name for special scope checks
472 self.authenticate.acl_repo_name = self.acl_repo_name
473 self.authenticate.acl_repo_name = self.acl_repo_name
473
474
474 plugin_cache_active, cache_ttl = False, 0
475 plugin_cache_active, cache_ttl = False, 0
475 plugin = None
476 plugin = None
476 if not username:
477 if not username:
477 self.authenticate.realm = self.authenticate.get_rc_realm()
478 self.authenticate.realm = self.authenticate.get_rc_realm()
478
479
479 try:
480 try:
480 auth_result = self.authenticate(environ)
481 auth_result = self.authenticate(environ)
481 except (UserCreationError, NotAllowedToCreateUserError) as e:
482 except (UserCreationError, NotAllowedToCreateUserError) as e:
482 log.error(e)
483 log.error(e)
483 reason = safe_str(e)
484 reason = safe_str(e)
484 return HTTPNotAcceptable(reason)(environ, start_response)
485 return HTTPNotAcceptable(reason)(environ, start_response)
485
486
486 if isinstance(auth_result, dict):
487 if isinstance(auth_result, dict):
487 AUTH_TYPE.update(environ, 'basic')
488 AUTH_TYPE.update(environ, 'basic')
488 REMOTE_USER.update(environ, auth_result['username'])
489 REMOTE_USER.update(environ, auth_result['username'])
489 username = auth_result['username']
490 username = auth_result['username']
490 plugin = auth_result.get('auth_data', {}).get('_plugin')
491 plugin = auth_result.get('auth_data', {}).get('_plugin')
491 log.info(
492 log.info(
492 'MAIN-AUTH successful for user `%s` from %s plugin',
493 'MAIN-AUTH successful for user `%s` from %s plugin',
493 username, plugin)
494 username, plugin)
494
495
495 plugin_cache_active, cache_ttl = auth_result.get(
496 plugin_cache_active, cache_ttl = auth_result.get(
496 'auth_data', {}).get('_ttl_cache') or (False, 0)
497 'auth_data', {}).get('_ttl_cache') or (False, 0)
497 else:
498 else:
498 return auth_result.wsgi_application(
499 return auth_result.wsgi_application(
499 environ, start_response)
500 environ, start_response)
500
501
501
502
502 # ==============================================================
503 # ==============================================================
503 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
504 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
504 # ==============================================================
505 # ==============================================================
505 user = User.get_by_username(username)
506 user = User.get_by_username(username)
506 if not self.valid_and_active_user(user):
507 if not self.valid_and_active_user(user):
507 return HTTPForbidden()(environ, start_response)
508 return HTTPForbidden()(environ, start_response)
508 username = user.username
509 username = user.username
509 user.update_lastactivity()
510 user.update_lastactivity()
510 meta.Session().commit()
511 meta.Session().commit()
511
512
512 # check user attributes for password change flag
513 # check user attributes for password change flag
513 user_obj = user
514 user_obj = user
514 if user_obj and user_obj.username != User.DEFAULT_USER and \
515 if user_obj and user_obj.username != User.DEFAULT_USER and \
515 user_obj.user_data.get('force_password_change'):
516 user_obj.user_data.get('force_password_change'):
516 reason = 'password change required'
517 reason = 'password change required'
517 log.debug('User not allowed to authenticate, %s', reason)
518 log.debug('User not allowed to authenticate, %s', reason)
518 return HTTPNotAcceptable(reason)(environ, start_response)
519 return HTTPNotAcceptable(reason)(environ, start_response)
519
520
520 # check permissions for this repository
521 # check permissions for this repository
521 perm = self._check_permission(
522 perm = self._check_permission(
522 action, user, self.acl_repo_name, ip_addr,
523 action, user, self.acl_repo_name, ip_addr,
523 plugin, plugin_cache_active, cache_ttl)
524 plugin, plugin_cache_active, cache_ttl)
524 if not perm:
525 if not perm:
525 return HTTPForbidden()(environ, start_response)
526 return HTTPForbidden()(environ, start_response)
526
527
527 # extras are injected into UI object and later available
528 # extras are injected into UI object and later available
528 # in hooks executed by RhodeCode
529 # in hooks executed by RhodeCode
529 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
530 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
530 extras = vcs_operation_context(
531 extras = vcs_operation_context(
531 environ, repo_name=self.acl_repo_name, username=username,
532 environ, repo_name=self.acl_repo_name, username=username,
532 action=action, scm=self.SCM, check_locking=check_locking,
533 action=action, scm=self.SCM, check_locking=check_locking,
533 is_shadow_repo=self.is_shadow_repo
534 is_shadow_repo=self.is_shadow_repo
534 )
535 )
535
536
536 # ======================================================================
537 # ======================================================================
537 # REQUEST HANDLING
538 # REQUEST HANDLING
538 # ======================================================================
539 # ======================================================================
539 repo_path = os.path.join(
540 repo_path = os.path.join(
540 safe_str(self.base_path), safe_str(self.vcs_repo_name))
541 safe_str(self.base_path), safe_str(self.vcs_repo_name))
541 log.debug('Repository path is %s', repo_path)
542 log.debug('Repository path is %s', repo_path)
542
543
543 fix_PATH()
544 fix_PATH()
544
545
545 log.info(
546 log.info(
546 '%s action on %s repo "%s" by "%s" from %s %s',
547 '%s action on %s repo "%s" by "%s" from %s %s',
547 action, self.SCM, safe_str(self.url_repo_name),
548 action, self.SCM, safe_str(self.url_repo_name),
548 safe_str(username), ip_addr, user_agent)
549 safe_str(username), ip_addr, user_agent)
549
550
550 return self._generate_vcs_response(
551 return self._generate_vcs_response(
551 environ, start_response, repo_path, extras, action)
552 environ, start_response, repo_path, extras, action)
552
553
553 @initialize_generator
554 @initialize_generator
554 def _generate_vcs_response(
555 def _generate_vcs_response(
555 self, environ, start_response, repo_path, extras, action):
556 self, environ, start_response, repo_path, extras, action):
556 """
557 """
557 Returns a generator for the response content.
558 Returns a generator for the response content.
558
559
559 This method is implemented as a generator, so that it can trigger
560 This method is implemented as a generator, so that it can trigger
560 the cache validation after all content sent back to the client. It
561 the cache validation after all content sent back to the client. It
561 also handles the locking exceptions which will be triggered when
562 also handles the locking exceptions which will be triggered when
562 the first chunk is produced by the underlying WSGI application.
563 the first chunk is produced by the underlying WSGI application.
563 """
564 """
564 callback_daemon, extras = self._prepare_callback_daemon(extras)
565 callback_daemon, extras = self._prepare_callback_daemon(extras)
565 config = self._create_config(extras, self.acl_repo_name)
566 config = self._create_config(extras, self.acl_repo_name)
566 log.debug('HOOKS extras is %s', extras)
567 log.debug('HOOKS extras is %s', extras)
567 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
568 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
568 app.rc_extras = extras
569 app.rc_extras = extras
569
570
570 try:
571 try:
571 with callback_daemon:
572 with callback_daemon:
572 try:
573 try:
573 response = app(environ, start_response)
574 response = app(environ, start_response)
574 finally:
575 finally:
575 # This statement works together with the decorator
576 # This statement works together with the decorator
576 # "initialize_generator" above. The decorator ensures that
577 # "initialize_generator" above. The decorator ensures that
577 # we hit the first yield statement before the generator is
578 # we hit the first yield statement before the generator is
578 # returned back to the WSGI server. This is needed to
579 # returned back to the WSGI server. This is needed to
579 # ensure that the call to "app" above triggers the
580 # ensure that the call to "app" above triggers the
580 # needed callback to "start_response" before the
581 # needed callback to "start_response" before the
581 # generator is actually used.
582 # generator is actually used.
582 yield "__init__"
583 yield "__init__"
583
584
584 for chunk in response:
585 for chunk in response:
585 yield chunk
586 yield chunk
586 except Exception as exc:
587 except Exception as exc:
587 # TODO: martinb: Exceptions are only raised in case of the Pyro4
588 # TODO: martinb: Exceptions are only raised in case of the Pyro4
588 # backend. Refactor this except block after dropping Pyro4 support.
589 # backend. Refactor this except block after dropping Pyro4 support.
589 # TODO: johbo: Improve "translating" back the exception.
590 # TODO: johbo: Improve "translating" back the exception.
590 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
591 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
591 exc = HTTPLockedRC(*exc.args)
592 exc = HTTPLockedRC(*exc.args)
592 _code = rhodecode.CONFIG.get('lock_ret_code')
593 _code = rhodecode.CONFIG.get('lock_ret_code')
593 log.debug('Repository LOCKED ret code %s!', (_code,))
594 log.debug('Repository LOCKED ret code %s!', (_code,))
594 elif getattr(exc, '_vcs_kind', None) == 'requirement':
595 elif getattr(exc, '_vcs_kind', None) == 'requirement':
595 log.debug(
596 log.debug(
596 'Repository requires features unknown to this Mercurial')
597 'Repository requires features unknown to this Mercurial')
597 exc = HTTPRequirementError(*exc.args)
598 exc = HTTPRequirementError(*exc.args)
598 else:
599 else:
599 raise
600 raise
600
601
601 for chunk in exc(environ, start_response):
602 for chunk in exc(environ, start_response):
602 yield chunk
603 yield chunk
603 finally:
604 finally:
604 # invalidate cache on push
605 # invalidate cache on push
605 try:
606 try:
606 if action == 'push':
607 if action == 'push':
607 self._invalidate_cache(self.url_repo_name)
608 self._invalidate_cache(self.url_repo_name)
608 finally:
609 finally:
609 meta.Session.remove()
610 meta.Session.remove()
610
611
611 def _get_repository_name(self, environ):
612 def _get_repository_name(self, environ):
612 """Get repository name out of the environmnent
613 """Get repository name out of the environmnent
613
614
614 :param environ: WSGI environment
615 :param environ: WSGI environment
615 """
616 """
616 raise NotImplementedError()
617 raise NotImplementedError()
617
618
618 def _get_action(self, environ):
619 def _get_action(self, environ):
619 """Map request commands into a pull or push command.
620 """Map request commands into a pull or push command.
620
621
621 :param environ: WSGI environment
622 :param environ: WSGI environment
622 """
623 """
623 raise NotImplementedError()
624 raise NotImplementedError()
624
625
625 def _create_wsgi_app(self, repo_path, repo_name, config):
626 def _create_wsgi_app(self, repo_path, repo_name, config):
626 """Return the WSGI app that will finally handle the request."""
627 """Return the WSGI app that will finally handle the request."""
627 raise NotImplementedError()
628 raise NotImplementedError()
628
629
629 def _create_config(self, extras, repo_name):
630 def _create_config(self, extras, repo_name):
630 """Create a safe config representation."""
631 """Create a safe config representation."""
631 raise NotImplementedError()
632 raise NotImplementedError()
632
633
633 def _prepare_callback_daemon(self, extras):
634 def _prepare_callback_daemon(self, extras):
634 return prepare_callback_daemon(
635 return prepare_callback_daemon(
635 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
636 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
636 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
637 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
637
638
638
639
639 def _should_check_locking(query_string):
640 def _should_check_locking(query_string):
640 # this is kind of hacky, but due to how mercurial handles client-server
641 # this is kind of hacky, but due to how mercurial handles client-server
641 # server see all operation on commit; bookmarks, phases and
642 # server see all operation on commit; bookmarks, phases and
642 # obsolescence marker in different transaction, we don't want to check
643 # obsolescence marker in different transaction, we don't want to check
643 # locking on those
644 # locking on those
644 return query_string not in ['cmd=listkeys']
645 return query_string not in ['cmd=listkeys']
General Comments 0
You need to be logged in to leave comments. Login now