##// END OF EJS Templates
auth: use cache_ttl from a plugin to also cache permissions....
marcink -
r2154:574d07a8 default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,711 +1,730 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 md5_safe, 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
80
81 class LazyFormencode(object):
81 class LazyFormencode(object):
82 def __init__(self, formencode_obj, *args, **kwargs):
82 def __init__(self, formencode_obj, *args, **kwargs):
83 self.formencode_obj = formencode_obj
83 self.formencode_obj = formencode_obj
84 self.args = args
84 self.args = args
85 self.kwargs = kwargs
85 self.kwargs = kwargs
86
86
87 def __call__(self, *args, **kwargs):
87 def __call__(self, *args, **kwargs):
88 from inspect import isfunction
88 from inspect import isfunction
89 formencode_obj = self.formencode_obj
89 formencode_obj = self.formencode_obj
90 if isfunction(formencode_obj):
90 if isfunction(formencode_obj):
91 # case we wrap validators into functions
91 # case we wrap validators into functions
92 formencode_obj = self.formencode_obj(*args, **kwargs)
92 formencode_obj = self.formencode_obj(*args, **kwargs)
93 return formencode_obj(*self.args, **self.kwargs)
93 return formencode_obj(*self.args, **self.kwargs)
94
94
95
95
96 class RhodeCodeAuthPluginBase(object):
96 class RhodeCodeAuthPluginBase(object):
97 # cache the authentication request for N amount of seconds. Some kind
97 # cache the authentication request for N amount of seconds. Some kind
98 # of authentication methods are very heavy and it's very efficient to cache
98 # of authentication methods are very heavy and it's very efficient to cache
99 # the result of a call. If it's set to None (default) cache is off
99 # the result of a call. If it's set to None (default) cache is off
100 AUTH_CACHE_TTL = None
100 AUTH_CACHE_TTL = None
101 AUTH_CACHE = {}
101 AUTH_CACHE = {}
102
102
103 auth_func_attrs = {
103 auth_func_attrs = {
104 "username": "unique username",
104 "username": "unique username",
105 "firstname": "first name",
105 "firstname": "first name",
106 "lastname": "last name",
106 "lastname": "last name",
107 "email": "email address",
107 "email": "email address",
108 "groups": '["list", "of", "groups"]',
108 "groups": '["list", "of", "groups"]',
109 "extern_name": "name in external source of record",
109 "extern_name": "name in external source of record",
110 "extern_type": "type of external source of record",
110 "extern_type": "type of external source of record",
111 "admin": 'True|False defines if user should be RhodeCode super admin',
111 "admin": 'True|False defines if user should be RhodeCode super admin',
112 "active":
112 "active":
113 'True|False defines active state of user internally for RhodeCode',
113 'True|False defines active state of user internally for RhodeCode',
114 "active_from_extern":
114 "active_from_extern":
115 "True|False\None, active state from the external auth, "
115 "True|False\None, active state from the external auth, "
116 "None means use definition from RhodeCode extern_type active value"
116 "None means use definition from RhodeCode extern_type active value"
117 }
117 }
118 # set on authenticate() method and via set_auth_type func.
118 # set on authenticate() method and via set_auth_type func.
119 auth_type = None
119 auth_type = None
120
120
121 # set on authenticate() method and via set_calling_scope_repo, this is a
121 # set on authenticate() method and via set_calling_scope_repo, this is a
122 # calling scope repository when doing authentication most likely on VCS
122 # calling scope repository when doing authentication most likely on VCS
123 # operations
123 # operations
124 acl_repo_name = None
124 acl_repo_name = None
125
125
126 # List of setting names to store encrypted. Plugins may override this list
126 # List of setting names to store encrypted. Plugins may override this list
127 # to store settings encrypted.
127 # to store settings encrypted.
128 _settings_encrypted = []
128 _settings_encrypted = []
129
129
130 # Mapping of python to DB settings model types. Plugins may override or
130 # Mapping of python to DB settings model types. Plugins may override or
131 # extend this mapping.
131 # extend this mapping.
132 _settings_type_map = {
132 _settings_type_map = {
133 colander.String: 'unicode',
133 colander.String: 'unicode',
134 colander.Integer: 'int',
134 colander.Integer: 'int',
135 colander.Boolean: 'bool',
135 colander.Boolean: 'bool',
136 colander.List: 'list',
136 colander.List: 'list',
137 }
137 }
138
138
139 # list of keys in settings that are unsafe to be logged, should be passwords
139 # list of keys in settings that are unsafe to be logged, should be passwords
140 # or other crucial credentials
140 # or other crucial credentials
141 _settings_unsafe_keys = []
141 _settings_unsafe_keys = []
142
142
143 def __init__(self, plugin_id):
143 def __init__(self, plugin_id):
144 self._plugin_id = plugin_id
144 self._plugin_id = plugin_id
145
145
146 def __str__(self):
146 def __str__(self):
147 return self.get_id()
147 return self.get_id()
148
148
149 def _get_setting_full_name(self, name):
149 def _get_setting_full_name(self, name):
150 """
150 """
151 Return the full setting name used for storing values in the database.
151 Return the full setting name used for storing values in the database.
152 """
152 """
153 # TODO: johbo: Using the name here is problematic. It would be good to
153 # TODO: johbo: Using the name here is problematic. It would be good to
154 # introduce either new models in the database to hold Plugin and
154 # introduce either new models in the database to hold Plugin and
155 # PluginSetting or to use the plugin id here.
155 # PluginSetting or to use the plugin id here.
156 return 'auth_{}_{}'.format(self.name, name)
156 return 'auth_{}_{}'.format(self.name, name)
157
157
158 def _get_setting_type(self, name):
158 def _get_setting_type(self, name):
159 """
159 """
160 Return the type of a setting. This type is defined by the SettingsModel
160 Return the type of a setting. This type is defined by the SettingsModel
161 and determines how the setting is stored in DB. Optionally the suffix
161 and determines how the setting is stored in DB. Optionally the suffix
162 `.encrypted` is appended to instruct SettingsModel to store it
162 `.encrypted` is appended to instruct SettingsModel to store it
163 encrypted.
163 encrypted.
164 """
164 """
165 schema_node = self.get_settings_schema().get(name)
165 schema_node = self.get_settings_schema().get(name)
166 db_type = self._settings_type_map.get(
166 db_type = self._settings_type_map.get(
167 type(schema_node.typ), 'unicode')
167 type(schema_node.typ), 'unicode')
168 if name in self._settings_encrypted:
168 if name in self._settings_encrypted:
169 db_type = '{}.encrypted'.format(db_type)
169 db_type = '{}.encrypted'.format(db_type)
170 return db_type
170 return db_type
171
171
172 @LazyProperty
172 @LazyProperty
173 def plugin_settings(self):
173 def plugin_settings(self):
174 settings = SettingsModel().get_all_settings()
174 settings = SettingsModel().get_all_settings()
175 return settings
175 return settings
176
176
177 def is_enabled(self):
177 def is_enabled(self):
178 """
178 """
179 Returns true if this plugin is enabled. An enabled plugin can be
179 Returns true if this plugin is enabled. An enabled plugin can be
180 configured in the admin interface but it is not consulted during
180 configured in the admin interface but it is not consulted during
181 authentication.
181 authentication.
182 """
182 """
183 auth_plugins = SettingsModel().get_auth_plugins()
183 auth_plugins = SettingsModel().get_auth_plugins()
184 return self.get_id() in auth_plugins
184 return self.get_id() in auth_plugins
185
185
186 def is_active(self):
186 def is_active(self):
187 """
187 """
188 Returns true if the plugin is activated. An activated plugin is
188 Returns true if the plugin is activated. An activated plugin is
189 consulted during authentication, assumed it is also enabled.
189 consulted during authentication, assumed it is also enabled.
190 """
190 """
191 return self.get_setting_by_name('enabled')
191 return self.get_setting_by_name('enabled')
192
192
193 def get_id(self):
193 def get_id(self):
194 """
194 """
195 Returns the plugin id.
195 Returns the plugin id.
196 """
196 """
197 return self._plugin_id
197 return self._plugin_id
198
198
199 def get_display_name(self):
199 def get_display_name(self):
200 """
200 """
201 Returns a translation string for displaying purposes.
201 Returns a translation string for displaying purposes.
202 """
202 """
203 raise NotImplementedError('Not implemented in base class')
203 raise NotImplementedError('Not implemented in base class')
204
204
205 def get_settings_schema(self):
205 def get_settings_schema(self):
206 """
206 """
207 Returns a colander schema, representing the plugin settings.
207 Returns a colander schema, representing the plugin settings.
208 """
208 """
209 return AuthnPluginSettingsSchemaBase()
209 return AuthnPluginSettingsSchemaBase()
210
210
211 def get_setting_by_name(self, name, default=None):
211 def get_setting_by_name(self, name, default=None):
212 """
212 """
213 Returns a plugin setting by name.
213 Returns a plugin setting by name.
214 """
214 """
215 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
215 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
216 plugin_settings = self.plugin_settings
216 plugin_settings = self.plugin_settings
217
217
218 return plugin_settings.get(full_name) or default
218 return plugin_settings.get(full_name) or default
219
219
220 def create_or_update_setting(self, name, value):
220 def create_or_update_setting(self, name, value):
221 """
221 """
222 Create or update a setting for this plugin in the persistent storage.
222 Create or update a setting for this plugin in the persistent storage.
223 """
223 """
224 full_name = self._get_setting_full_name(name)
224 full_name = self._get_setting_full_name(name)
225 type_ = self._get_setting_type(name)
225 type_ = self._get_setting_type(name)
226 db_setting = SettingsModel().create_or_update_setting(
226 db_setting = SettingsModel().create_or_update_setting(
227 full_name, value, type_)
227 full_name, value, type_)
228 return db_setting.app_settings_value
228 return db_setting.app_settings_value
229
229
230 def get_settings(self):
230 def get_settings(self):
231 """
231 """
232 Returns the plugin settings as dictionary.
232 Returns the plugin settings as dictionary.
233 """
233 """
234 settings = {}
234 settings = {}
235 for node in self.get_settings_schema():
235 for node in self.get_settings_schema():
236 settings[node.name] = self.get_setting_by_name(node.name)
236 settings[node.name] = self.get_setting_by_name(node.name)
237 return settings
237 return settings
238
238
239 def log_safe_settings(self, settings):
239 def log_safe_settings(self, settings):
240 """
240 """
241 returns a log safe representation of settings, without any secrets
241 returns a log safe representation of settings, without any secrets
242 """
242 """
243 settings_copy = copy.deepcopy(settings)
243 settings_copy = copy.deepcopy(settings)
244 for k in self._settings_unsafe_keys:
244 for k in self._settings_unsafe_keys:
245 if k in settings_copy:
245 if k in settings_copy:
246 del settings_copy[k]
246 del settings_copy[k]
247 return settings_copy
247 return settings_copy
248
248
249 @property
249 @property
250 def validators(self):
250 def validators(self):
251 """
251 """
252 Exposes RhodeCode validators modules
252 Exposes RhodeCode validators modules
253 """
253 """
254 # this is a hack to overcome issues with pylons threadlocals and
254 # this is a hack to overcome issues with pylons threadlocals and
255 # translator object _() not being registered properly.
255 # translator object _() not being registered properly.
256 class LazyCaller(object):
256 class LazyCaller(object):
257 def __init__(self, name):
257 def __init__(self, name):
258 self.validator_name = name
258 self.validator_name = name
259
259
260 def __call__(self, *args, **kwargs):
260 def __call__(self, *args, **kwargs):
261 from rhodecode.model import validators as v
261 from rhodecode.model import validators as v
262 obj = getattr(v, self.validator_name)
262 obj = getattr(v, self.validator_name)
263 # log.debug('Initializing lazy formencode object: %s', obj)
263 # log.debug('Initializing lazy formencode object: %s', obj)
264 return LazyFormencode(obj, *args, **kwargs)
264 return LazyFormencode(obj, *args, **kwargs)
265
265
266 class ProxyGet(object):
266 class ProxyGet(object):
267 def __getattribute__(self, name):
267 def __getattribute__(self, name):
268 return LazyCaller(name)
268 return LazyCaller(name)
269
269
270 return ProxyGet()
270 return ProxyGet()
271
271
272 @hybrid_property
272 @hybrid_property
273 def name(self):
273 def name(self):
274 """
274 """
275 Returns the name of this authentication plugin.
275 Returns the name of this authentication plugin.
276
276
277 :returns: string
277 :returns: string
278 """
278 """
279 raise NotImplementedError("Not implemented in base class")
279 raise NotImplementedError("Not implemented in base class")
280
280
281 def get_url_slug(self):
281 def get_url_slug(self):
282 """
282 """
283 Returns a slug which should be used when constructing URLs which refer
283 Returns a slug which should be used when constructing URLs which refer
284 to this plugin. By default it returns the plugin name. If the name is
284 to this plugin. By default it returns the plugin name. If the name is
285 not suitable for using it in an URL the plugin should override this
285 not suitable for using it in an URL the plugin should override this
286 method.
286 method.
287 """
287 """
288 return self.name
288 return self.name
289
289
290 @property
290 @property
291 def is_headers_auth(self):
291 def is_headers_auth(self):
292 """
292 """
293 Returns True if this authentication plugin uses HTTP headers as
293 Returns True if this authentication plugin uses HTTP headers as
294 authentication method.
294 authentication method.
295 """
295 """
296 return False
296 return False
297
297
298 @hybrid_property
298 @hybrid_property
299 def is_container_auth(self):
299 def is_container_auth(self):
300 """
300 """
301 Deprecated method that indicates if this authentication plugin uses
301 Deprecated method that indicates if this authentication plugin uses
302 HTTP headers as authentication method.
302 HTTP headers as authentication method.
303 """
303 """
304 warnings.warn(
304 warnings.warn(
305 'Use is_headers_auth instead.', category=DeprecationWarning)
305 'Use is_headers_auth instead.', category=DeprecationWarning)
306 return self.is_headers_auth
306 return self.is_headers_auth
307
307
308 @hybrid_property
308 @hybrid_property
309 def allows_creating_users(self):
309 def allows_creating_users(self):
310 """
310 """
311 Defines if Plugin allows users to be created on-the-fly when
311 Defines if Plugin allows users to be created on-the-fly when
312 authentication is called. Controls how external plugins should behave
312 authentication is called. Controls how external plugins should behave
313 in terms if they are allowed to create new users, or not. Base plugins
313 in terms if they are allowed to create new users, or not. Base plugins
314 should not be allowed to, but External ones should be !
314 should not be allowed to, but External ones should be !
315
315
316 :return: bool
316 :return: bool
317 """
317 """
318 return False
318 return False
319
319
320 def set_auth_type(self, auth_type):
320 def set_auth_type(self, auth_type):
321 self.auth_type = auth_type
321 self.auth_type = auth_type
322
322
323 def set_calling_scope_repo(self, acl_repo_name):
323 def set_calling_scope_repo(self, acl_repo_name):
324 self.acl_repo_name = acl_repo_name
324 self.acl_repo_name = acl_repo_name
325
325
326 def allows_authentication_from(
326 def allows_authentication_from(
327 self, user, allows_non_existing_user=True,
327 self, user, allows_non_existing_user=True,
328 allowed_auth_plugins=None, allowed_auth_sources=None):
328 allowed_auth_plugins=None, allowed_auth_sources=None):
329 """
329 """
330 Checks if this authentication module should accept a request for
330 Checks if this authentication module should accept a request for
331 the current user.
331 the current user.
332
332
333 :param user: user object fetched using plugin's get_user() method.
333 :param user: user object fetched using plugin's get_user() method.
334 :param allows_non_existing_user: if True, don't allow the
334 :param allows_non_existing_user: if True, don't allow the
335 user to be empty, meaning not existing in our database
335 user to be empty, meaning not existing in our database
336 :param allowed_auth_plugins: if provided, users extern_type will be
336 :param allowed_auth_plugins: if provided, users extern_type will be
337 checked against a list of provided extern types, which are plugin
337 checked against a list of provided extern types, which are plugin
338 auth_names in the end
338 auth_names in the end
339 :param allowed_auth_sources: authentication type allowed,
339 :param allowed_auth_sources: authentication type allowed,
340 `http` or `vcs` default is both.
340 `http` or `vcs` default is both.
341 defines if plugin will accept only http authentication vcs
341 defines if plugin will accept only http authentication vcs
342 authentication(git/hg) or both
342 authentication(git/hg) or both
343 :returns: boolean
343 :returns: boolean
344 """
344 """
345 if not user and not allows_non_existing_user:
345 if not user and not allows_non_existing_user:
346 log.debug('User is empty but plugin does not allow empty users,'
346 log.debug('User is empty but plugin does not allow empty users,'
347 'not allowed to authenticate')
347 'not allowed to authenticate')
348 return False
348 return False
349
349
350 expected_auth_plugins = allowed_auth_plugins or [self.name]
350 expected_auth_plugins = allowed_auth_plugins or [self.name]
351 if user and (user.extern_type and
351 if user and (user.extern_type and
352 user.extern_type not in expected_auth_plugins):
352 user.extern_type not in expected_auth_plugins):
353 log.debug(
353 log.debug(
354 'User `%s` is bound to `%s` auth type. Plugin allows only '
354 'User `%s` is bound to `%s` auth type. Plugin allows only '
355 '%s, skipping', user, user.extern_type, expected_auth_plugins)
355 '%s, skipping', user, user.extern_type, expected_auth_plugins)
356
356
357 return False
357 return False
358
358
359 # by default accept both
359 # by default accept both
360 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
360 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
361 if self.auth_type not in expected_auth_from:
361 if self.auth_type not in expected_auth_from:
362 log.debug('Current auth source is %s but plugin only allows %s',
362 log.debug('Current auth source is %s but plugin only allows %s',
363 self.auth_type, expected_auth_from)
363 self.auth_type, expected_auth_from)
364 return False
364 return False
365
365
366 return True
366 return True
367
367
368 def get_user(self, username=None, **kwargs):
368 def get_user(self, username=None, **kwargs):
369 """
369 """
370 Helper method for user fetching in plugins, by default it's using
370 Helper method for user fetching in plugins, by default it's using
371 simple fetch by username, but this method can be custimized in plugins
371 simple fetch by username, but this method can be custimized in plugins
372 eg. headers auth plugin to fetch user by environ params
372 eg. headers auth plugin to fetch user by environ params
373
373
374 :param username: username if given to fetch from database
374 :param username: username if given to fetch from database
375 :param kwargs: extra arguments needed for user fetching.
375 :param kwargs: extra arguments needed for user fetching.
376 """
376 """
377 user = None
377 user = None
378 log.debug(
378 log.debug(
379 'Trying to fetch user `%s` from RhodeCode database', username)
379 'Trying to fetch user `%s` from RhodeCode database', username)
380 if username:
380 if username:
381 user = User.get_by_username(username)
381 user = User.get_by_username(username)
382 if not user:
382 if not user:
383 log.debug('User not found, fallback to fetch user in '
383 log.debug('User not found, fallback to fetch user in '
384 'case insensitive mode')
384 'case insensitive mode')
385 user = User.get_by_username(username, case_insensitive=True)
385 user = User.get_by_username(username, case_insensitive=True)
386 else:
386 else:
387 log.debug('provided username:`%s` is empty skipping...', username)
387 log.debug('provided username:`%s` is empty skipping...', username)
388 if not user:
388 if not user:
389 log.debug('User `%s` not found in database', username)
389 log.debug('User `%s` not found in database', username)
390 else:
390 else:
391 log.debug('Got DB user:%s', user)
391 log.debug('Got DB user:%s', user)
392 return user
392 return user
393
393
394 def user_activation_state(self):
394 def user_activation_state(self):
395 """
395 """
396 Defines user activation state when creating new users
396 Defines user activation state when creating new users
397
397
398 :returns: boolean
398 :returns: boolean
399 """
399 """
400 raise NotImplementedError("Not implemented in base class")
400 raise NotImplementedError("Not implemented in base class")
401
401
402 def auth(self, userobj, username, passwd, settings, **kwargs):
402 def auth(self, userobj, username, passwd, settings, **kwargs):
403 """
403 """
404 Given a user object (which may be null), username, a plaintext
404 Given a user object (which may be null), username, a plaintext
405 password, and a settings object (containing all the keys needed as
405 password, and a settings object (containing all the keys needed as
406 listed in settings()), authenticate this user's login attempt.
406 listed in settings()), authenticate this user's login attempt.
407
407
408 Return None on failure. On success, return a dictionary of the form:
408 Return None on failure. On success, return a dictionary of the form:
409
409
410 see: RhodeCodeAuthPluginBase.auth_func_attrs
410 see: RhodeCodeAuthPluginBase.auth_func_attrs
411 This is later validated for correctness
411 This is later validated for correctness
412 """
412 """
413 raise NotImplementedError("not implemented in base class")
413 raise NotImplementedError("not implemented in base class")
414
414
415 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
415 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
416 """
416 """
417 Wrapper to call self.auth() that validates call on it
417 Wrapper to call self.auth() that validates call on it
418
418
419 :param userobj: userobj
419 :param userobj: userobj
420 :param username: username
420 :param username: username
421 :param passwd: plaintext password
421 :param passwd: plaintext password
422 :param settings: plugin settings
422 :param settings: plugin settings
423 """
423 """
424 auth = self.auth(userobj, username, passwd, settings, **kwargs)
424 auth = self.auth(userobj, username, passwd, settings, **kwargs)
425 if auth:
425 if auth:
426 auth['_plugin'] = self.name
427 auth['_ttl_cache'] = self.get_ttl_cache(settings)
426 # check if hash should be migrated ?
428 # check if hash should be migrated ?
427 new_hash = auth.get('_hash_migrate')
429 new_hash = auth.get('_hash_migrate')
428 if new_hash:
430 if new_hash:
429 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
431 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
430 return self._validate_auth_return(auth)
432 return self._validate_auth_return(auth)
433
431 return auth
434 return auth
432
435
433 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
436 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
434 new_hash_cypher = _RhodeCodeCryptoBCrypt()
437 new_hash_cypher = _RhodeCodeCryptoBCrypt()
435 # extra checks, so make sure new hash is correct.
438 # extra checks, so make sure new hash is correct.
436 password_encoded = safe_str(password)
439 password_encoded = safe_str(password)
437 if new_hash and new_hash_cypher.hash_check(
440 if new_hash and new_hash_cypher.hash_check(
438 password_encoded, new_hash):
441 password_encoded, new_hash):
439 cur_user = User.get_by_username(username)
442 cur_user = User.get_by_username(username)
440 cur_user.password = new_hash
443 cur_user.password = new_hash
441 Session().add(cur_user)
444 Session().add(cur_user)
442 Session().flush()
445 Session().flush()
443 log.info('Migrated user %s hash to bcrypt', cur_user)
446 log.info('Migrated user %s hash to bcrypt', cur_user)
444
447
445 def _validate_auth_return(self, ret):
448 def _validate_auth_return(self, ret):
446 if not isinstance(ret, dict):
449 if not isinstance(ret, dict):
447 raise Exception('returned value from auth must be a dict')
450 raise Exception('returned value from auth must be a dict')
448 for k in self.auth_func_attrs:
451 for k in self.auth_func_attrs:
449 if k not in ret:
452 if k not in ret:
450 raise Exception('Missing %s attribute from returned data' % k)
453 raise Exception('Missing %s attribute from returned data' % k)
451 return ret
454 return ret
452
455
456 def get_ttl_cache(self, settings=None):
457 plugin_settings = settings or self.get_settings()
458 cache_ttl = 0
459
460 if isinstance(self.AUTH_CACHE_TTL, (int, long)):
461 # plugin cache set inside is more important than the settings value
462 cache_ttl = self.AUTH_CACHE_TTL
463 elif plugin_settings.get('cache_ttl'):
464 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
465
466 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
467 return plugin_cache_active, cache_ttl
468
453
469
454 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
470 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
455
471
456 @hybrid_property
472 @hybrid_property
457 def allows_creating_users(self):
473 def allows_creating_users(self):
458 return True
474 return True
459
475
460 def use_fake_password(self):
476 def use_fake_password(self):
461 """
477 """
462 Return a boolean that indicates whether or not we should set the user's
478 Return a boolean that indicates whether or not we should set the user's
463 password to a random value when it is authenticated by this plugin.
479 password to a random value when it is authenticated by this plugin.
464 If your plugin provides authentication, then you will generally
480 If your plugin provides authentication, then you will generally
465 want this.
481 want this.
466
482
467 :returns: boolean
483 :returns: boolean
468 """
484 """
469 raise NotImplementedError("Not implemented in base class")
485 raise NotImplementedError("Not implemented in base class")
470
486
471 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
487 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
472 # at this point _authenticate calls plugin's `auth()` function
488 # at this point _authenticate calls plugin's `auth()` function
473 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
489 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
474 userobj, username, passwd, settings, **kwargs)
490 userobj, username, passwd, settings, **kwargs)
475
491
476 if auth:
492 if auth:
477 # maybe plugin will clean the username ?
493 # maybe plugin will clean the username ?
478 # we should use the return value
494 # we should use the return value
479 username = auth['username']
495 username = auth['username']
480
496
481 # if external source tells us that user is not active, we should
497 # if external source tells us that user is not active, we should
482 # skip rest of the process. This can prevent from creating users in
498 # skip rest of the process. This can prevent from creating users in
483 # RhodeCode when using external authentication, but if it's
499 # RhodeCode when using external authentication, but if it's
484 # inactive user we shouldn't create that user anyway
500 # inactive user we shouldn't create that user anyway
485 if auth['active_from_extern'] is False:
501 if auth['active_from_extern'] is False:
486 log.warning(
502 log.warning(
487 "User %s authenticated against %s, but is inactive",
503 "User %s authenticated against %s, but is inactive",
488 username, self.__module__)
504 username, self.__module__)
489 return None
505 return None
490
506
491 cur_user = User.get_by_username(username, case_insensitive=True)
507 cur_user = User.get_by_username(username, case_insensitive=True)
492 is_user_existing = cur_user is not None
508 is_user_existing = cur_user is not None
493
509
494 if is_user_existing:
510 if is_user_existing:
495 log.debug('Syncing user `%s` from '
511 log.debug('Syncing user `%s` from '
496 '`%s` plugin', username, self.name)
512 '`%s` plugin', username, self.name)
497 else:
513 else:
498 log.debug('Creating non existing user `%s` from '
514 log.debug('Creating non existing user `%s` from '
499 '`%s` plugin', username, self.name)
515 '`%s` plugin', username, self.name)
500
516
501 if self.allows_creating_users:
517 if self.allows_creating_users:
502 log.debug('Plugin `%s` allows to '
518 log.debug('Plugin `%s` allows to '
503 'create new users', self.name)
519 'create new users', self.name)
504 else:
520 else:
505 log.debug('Plugin `%s` does not allow to '
521 log.debug('Plugin `%s` does not allow to '
506 'create new users', self.name)
522 'create new users', self.name)
507
523
508 user_parameters = {
524 user_parameters = {
509 'username': username,
525 'username': username,
510 'email': auth["email"],
526 'email': auth["email"],
511 'firstname': auth["firstname"],
527 'firstname': auth["firstname"],
512 'lastname': auth["lastname"],
528 'lastname': auth["lastname"],
513 'active': auth["active"],
529 'active': auth["active"],
514 'admin': auth["admin"],
530 'admin': auth["admin"],
515 'extern_name': auth["extern_name"],
531 'extern_name': auth["extern_name"],
516 'extern_type': self.name,
532 'extern_type': self.name,
517 'plugin': self,
533 'plugin': self,
518 'allow_to_create_user': self.allows_creating_users,
534 'allow_to_create_user': self.allows_creating_users,
519 }
535 }
520
536
521 if not is_user_existing:
537 if not is_user_existing:
522 if self.use_fake_password():
538 if self.use_fake_password():
523 # Randomize the PW because we don't need it, but don't want
539 # Randomize the PW because we don't need it, but don't want
524 # them blank either
540 # them blank either
525 passwd = PasswordGenerator().gen_password(length=16)
541 passwd = PasswordGenerator().gen_password(length=16)
526 user_parameters['password'] = passwd
542 user_parameters['password'] = passwd
527 else:
543 else:
528 # Since the password is required by create_or_update method of
544 # Since the password is required by create_or_update method of
529 # UserModel, we need to set it explicitly.
545 # UserModel, we need to set it explicitly.
530 # The create_or_update method is smart and recognises the
546 # The create_or_update method is smart and recognises the
531 # password hashes as well.
547 # password hashes as well.
532 user_parameters['password'] = cur_user.password
548 user_parameters['password'] = cur_user.password
533
549
534 # we either create or update users, we also pass the flag
550 # we either create or update users, we also pass the flag
535 # that controls if this method can actually do that.
551 # that controls if this method can actually do that.
536 # raises NotAllowedToCreateUserError if it cannot, and we try to.
552 # raises NotAllowedToCreateUserError if it cannot, and we try to.
537 user = UserModel().create_or_update(**user_parameters)
553 user = UserModel().create_or_update(**user_parameters)
538 Session().flush()
554 Session().flush()
539 # enforce user is just in given groups, all of them has to be ones
555 # enforce user is just in given groups, all of them has to be ones
540 # created from plugins. We store this info in _group_data JSON
556 # created from plugins. We store this info in _group_data JSON
541 # field
557 # field
542 try:
558 try:
543 groups = auth['groups'] or []
559 groups = auth['groups'] or []
544 log.debug(
560 log.debug(
545 'Performing user_group sync based on set `%s` '
561 'Performing user_group sync based on set `%s` '
546 'returned by this plugin', groups)
562 'returned by this plugin', groups)
547 UserGroupModel().enforce_groups(user, groups, self.name)
563 UserGroupModel().enforce_groups(user, groups, self.name)
548 except Exception:
564 except Exception:
549 # for any reason group syncing fails, we should
565 # for any reason group syncing fails, we should
550 # proceed with login
566 # proceed with login
551 log.error(traceback.format_exc())
567 log.error(traceback.format_exc())
552 Session().commit()
568 Session().commit()
553 return auth
569 return auth
554
570
555
571
556 def loadplugin(plugin_id):
572 def loadplugin(plugin_id):
557 """
573 """
558 Loads and returns an instantiated authentication plugin.
574 Loads and returns an instantiated authentication plugin.
559 Returns the RhodeCodeAuthPluginBase subclass on success,
575 Returns the RhodeCodeAuthPluginBase subclass on success,
560 or None on failure.
576 or None on failure.
561 """
577 """
562 # TODO: Disusing pyramids thread locals to retrieve the registry.
578 # TODO: Disusing pyramids thread locals to retrieve the registry.
563 authn_registry = get_authn_registry()
579 authn_registry = get_authn_registry()
564 plugin = authn_registry.get_plugin(plugin_id)
580 plugin = authn_registry.get_plugin(plugin_id)
565 if plugin is None:
581 if plugin is None:
566 log.error('Authentication plugin not found: "%s"', plugin_id)
582 log.error('Authentication plugin not found: "%s"', plugin_id)
567 return plugin
583 return plugin
568
584
569
585
570 def get_authn_registry(registry=None):
586 def get_authn_registry(registry=None):
571 registry = registry or get_current_registry()
587 registry = registry or get_current_registry()
572 authn_registry = registry.getUtility(IAuthnPluginRegistry)
588 authn_registry = registry.getUtility(IAuthnPluginRegistry)
573 return authn_registry
589 return authn_registry
574
590
575
591
576 def get_auth_cache_manager(custom_ttl=None):
592 def get_auth_cache_manager(custom_ttl=None):
577 return caches.get_cache_manager(
593 return caches.get_cache_manager(
578 'auth_plugins', 'rhodecode.authentication', custom_ttl)
594 'auth_plugins', 'rhodecode.authentication', custom_ttl)
579
595
580
596
597 def get_perms_cache_manager(custom_ttl=None):
598 return caches.get_cache_manager(
599 'auth_plugins', 'rhodecode.permissions', custom_ttl)
600
601
581 def authenticate(username, password, environ=None, auth_type=None,
602 def authenticate(username, password, environ=None, auth_type=None,
582 skip_missing=False, registry=None, acl_repo_name=None):
603 skip_missing=False, registry=None, acl_repo_name=None):
583 """
604 """
584 Authentication function used for access control,
605 Authentication function used for access control,
585 It tries to authenticate based on enabled authentication modules.
606 It tries to authenticate based on enabled authentication modules.
586
607
587 :param username: username can be empty for headers auth
608 :param username: username can be empty for headers auth
588 :param password: password can be empty for headers auth
609 :param password: password can be empty for headers auth
589 :param environ: environ headers passed for headers auth
610 :param environ: environ headers passed for headers auth
590 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
611 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
591 :param skip_missing: ignores plugins that are in db but not in environment
612 :param skip_missing: ignores plugins that are in db but not in environment
592 :returns: None if auth failed, plugin_user dict if auth is correct
613 :returns: None if auth failed, plugin_user dict if auth is correct
593 """
614 """
594 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
615 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
595 raise ValueError('auth type must be on of http, vcs got "%s" instead'
616 raise ValueError('auth type must be on of http, vcs got "%s" instead'
596 % auth_type)
617 % auth_type)
597 headers_only = environ and not (username and password)
618 headers_only = environ and not (username and password)
598
619
599 authn_registry = get_authn_registry(registry)
620 authn_registry = get_authn_registry(registry)
600 plugins_to_check = authn_registry.get_plugins_for_authentication()
621 plugins_to_check = authn_registry.get_plugins_for_authentication()
601 log.debug('Starting ordered authentication chain using %s plugins',
622 log.debug('Starting ordered authentication chain using %s plugins',
602 plugins_to_check)
623 plugins_to_check)
603 for plugin in plugins_to_check:
624 for plugin in plugins_to_check:
604 plugin.set_auth_type(auth_type)
625 plugin.set_auth_type(auth_type)
605 plugin.set_calling_scope_repo(acl_repo_name)
626 plugin.set_calling_scope_repo(acl_repo_name)
606
627
607 if headers_only and not plugin.is_headers_auth:
628 if headers_only and not plugin.is_headers_auth:
608 log.debug('Auth type is for headers only and plugin `%s` is not '
629 log.debug('Auth type is for headers only and plugin `%s` is not '
609 'headers plugin, skipping...', plugin.get_id())
630 'headers plugin, skipping...', plugin.get_id())
610 continue
631 continue
611
632
612 # load plugin settings from RhodeCode database
633 # load plugin settings from RhodeCode database
613 plugin_settings = plugin.get_settings()
634 plugin_settings = plugin.get_settings()
614 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
635 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
615 log.debug('Plugin settings:%s', plugin_sanitized_settings)
636 log.debug('Plugin settings:%s', plugin_sanitized_settings)
616
637
617 log.debug('Trying authentication using ** %s **', plugin.get_id())
638 log.debug('Trying authentication using ** %s **', plugin.get_id())
618 # use plugin's method of user extraction.
639 # use plugin's method of user extraction.
619 user = plugin.get_user(username, environ=environ,
640 user = plugin.get_user(username, environ=environ,
620 settings=plugin_settings)
641 settings=plugin_settings)
621 display_user = user.username if user else username
642 display_user = user.username if user else username
622 log.debug(
643 log.debug(
623 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
644 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
624
645
625 if not plugin.allows_authentication_from(user):
646 if not plugin.allows_authentication_from(user):
626 log.debug('Plugin %s does not accept user `%s` for authentication',
647 log.debug('Plugin %s does not accept user `%s` for authentication',
627 plugin.get_id(), display_user)
648 plugin.get_id(), display_user)
628 continue
649 continue
629 else:
650 else:
630 log.debug('Plugin %s accepted user `%s` for authentication',
651 log.debug('Plugin %s accepted user `%s` for authentication',
631 plugin.get_id(), display_user)
652 plugin.get_id(), display_user)
632
653
633 log.info('Authenticating user `%s` using %s plugin',
654 log.info('Authenticating user `%s` using %s plugin',
634 display_user, plugin.get_id())
655 display_user, plugin.get_id())
635
656
636 _cache_ttl = 0
657 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
637
638 if isinstance(plugin.AUTH_CACHE_TTL, (int, long)):
639 # plugin cache set inside is more important than the settings value
640 _cache_ttl = plugin.AUTH_CACHE_TTL
641 elif plugin_settings.get('cache_ttl'):
642 _cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
643
644 plugin_cache_active = bool(_cache_ttl and _cache_ttl > 0)
645
658
646 # get instance of cache manager configured for a namespace
659 # get instance of cache manager configured for a namespace
647 cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl)
660 cache_manager = get_auth_cache_manager(custom_ttl=cache_ttl)
648
661
649 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
662 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
650 plugin.get_id(), plugin_cache_active, _cache_ttl)
663 plugin.get_id(), plugin_cache_active, cache_ttl)
651
664
652 # for environ based password can be empty, but then the validation is
665 # for environ based password can be empty, but then the validation is
653 # on the server that fills in the env data needed for authentication
666 # on the server that fills in the env data needed for authentication
654 _password_hash = md5_safe(plugin.name + username + (password or ''))
667
668 _password_hash = caches.compute_key_from_params(
669 plugin.name, username, (password or ''))
655
670
656 # _authenticate is a wrapper for .auth() method of plugin.
671 # _authenticate is a wrapper for .auth() method of plugin.
657 # it checks if .auth() sends proper data.
672 # it checks if .auth() sends proper data.
658 # For RhodeCodeExternalAuthPlugin it also maps users to
673 # For RhodeCodeExternalAuthPlugin it also maps users to
659 # Database and maps the attributes returned from .auth()
674 # Database and maps the attributes returned from .auth()
660 # to RhodeCode database. If this function returns data
675 # to RhodeCode database. If this function returns data
661 # then auth is correct.
676 # then auth is correct.
662 start = time.time()
677 start = time.time()
663 log.debug('Running plugin `%s` _authenticate method', plugin.get_id())
678 log.debug('Running plugin `%s` _authenticate method', plugin.get_id())
664
679
665 def auth_func():
680 def auth_func():
666 """
681 """
667 This function is used internally in Cache of Beaker to calculate
682 This function is used internally in Cache of Beaker to calculate
668 Results
683 Results
669 """
684 """
685 log.debug('auth: calculating password access now...')
670 return plugin._authenticate(
686 return plugin._authenticate(
671 user, username, password, plugin_settings,
687 user, username, password, plugin_settings,
672 environ=environ or {})
688 environ=environ or {})
673
689
674 if plugin_cache_active:
690 if plugin_cache_active:
691 log.debug('Trying to fetch cached auth by %s', _password_hash[:6])
675 plugin_user = cache_manager.get(
692 plugin_user = cache_manager.get(
676 _password_hash, createfunc=auth_func)
693 _password_hash, createfunc=auth_func)
677 else:
694 else:
678 plugin_user = auth_func()
695 plugin_user = auth_func()
679
696
680 auth_time = time.time() - start
697 auth_time = time.time() - start
681 log.debug('Authentication for plugin `%s` completed in %.3fs, '
698 log.debug('Authentication for plugin `%s` completed in %.3fs, '
682 'expiration time of fetched cache %.1fs.',
699 'expiration time of fetched cache %.1fs.',
683 plugin.get_id(), auth_time, _cache_ttl)
700 plugin.get_id(), auth_time, cache_ttl)
684
701
685 log.debug('PLUGIN USER DATA: %s', plugin_user)
702 log.debug('PLUGIN USER DATA: %s', plugin_user)
686
703
687 if plugin_user:
704 if plugin_user:
688 log.debug('Plugin returned proper authentication data')
705 log.debug('Plugin returned proper authentication data')
689 return plugin_user
706 return plugin_user
690 # we failed to Auth because .auth() method didn't return proper user
707 # we failed to Auth because .auth() method didn't return proper user
691 log.debug("User `%s` failed to authenticate against %s",
708 log.debug("User `%s` failed to authenticate against %s",
692 display_user, plugin.get_id())
709 display_user, plugin.get_id())
710
711 # case when we failed to authenticate against all defined plugins
693 return None
712 return None
694
713
695
714
696 def chop_at(s, sub, inclusive=False):
715 def chop_at(s, sub, inclusive=False):
697 """Truncate string ``s`` at the first occurrence of ``sub``.
716 """Truncate string ``s`` at the first occurrence of ``sub``.
698
717
699 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
718 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
700
719
701 >>> chop_at("plutocratic brats", "rat")
720 >>> chop_at("plutocratic brats", "rat")
702 'plutoc'
721 'plutoc'
703 >>> chop_at("plutocratic brats", "rat", True)
722 >>> chop_at("plutocratic brats", "rat", True)
704 'plutocrat'
723 'plutocrat'
705 """
724 """
706 pos = s.find(sub)
725 pos = s.find(sub)
707 if pos == -1:
726 if pos == -1:
708 return s
727 return s
709 if inclusive:
728 if inclusive:
710 return s[:pos+len(sub)]
729 return s[:pos+len(sub)]
711 return s[:pos]
730 return s[:pos]
@@ -1,52 +1,51 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 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
22
23 from rhodecode.translation import _
23 from rhodecode.translation import _
24
24
25
25
26 class AuthnPluginSettingsSchemaBase(colander.MappingSchema):
26 class AuthnPluginSettingsSchemaBase(colander.MappingSchema):
27 """
27 """
28 This base schema is intended for use in authentication plugins.
28 This base schema is intended for use in authentication plugins.
29 It adds a few default settings (e.g., "enabled"), so that plugin
29 It adds a few default settings (e.g., "enabled"), so that plugin
30 authors don't have to maintain a bunch of boilerplate.
30 authors don't have to maintain a bunch of boilerplate.
31 """
31 """
32 enabled = colander.SchemaNode(
32 enabled = colander.SchemaNode(
33 colander.Bool(),
33 colander.Bool(),
34 default=False,
34 default=False,
35 description=_('Enable or disable this authentication plugin.'),
35 description=_('Enable or disable this authentication plugin.'),
36 missing=False,
36 missing=False,
37 title=_('Enabled'),
37 title=_('Enabled'),
38 widget='bool',
38 widget='bool',
39 )
39 )
40 cache_ttl = colander.SchemaNode(
40 cache_ttl = colander.SchemaNode(
41 colander.Int(),
41 colander.Int(),
42 default=0,
42 default=0,
43 description=_('Amount of seconds to cache the authentication response'
43 description=_('Amount of seconds to cache the authentication and '
44 'call for this plugin. \n'
44 'permissions check response call for this plugin. \n'
45 'Useful for long calls like LDAP to improve the '
45 'Useful for expensive calls like LDAP to improve the '
46 'performance of the authentication system '
46 'performance of the system (0 means disabled).'),
47 '(0 means disabled).'),
48 missing=0,
47 missing=0,
49 title=_('Auth Cache TTL'),
48 title=_('Auth Cache TTL'),
50 validator=colander.Range(min=0, max=None),
49 validator=colander.Range(min=0, max=None),
51 widget='int',
50 widget='int',
52 )
51 )
@@ -1,630 +1,631 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 The base Controller API
22 The base Controller API
23 Provides the BaseController class for subclassing. And usage in different
23 Provides the BaseController class for subclassing. And usage in different
24 controllers
24 controllers
25 """
25 """
26
26
27 import logging
27 import logging
28 import socket
28 import socket
29
29
30 import markupsafe
30 import markupsafe
31 import ipaddress
31 import ipaddress
32 import pyramid.threadlocal
32 import pyramid.threadlocal
33
33
34 from paste.auth.basic import AuthBasicAuthenticator
34 from paste.auth.basic import AuthBasicAuthenticator
35 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
36 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
37
37
38 import rhodecode
38 import rhodecode
39 from rhodecode.authentication.base import VCS_TYPE
39 from rhodecode.authentication.base import VCS_TYPE
40 from rhodecode.lib import auth, utils2
40 from rhodecode.lib import auth, utils2
41 from rhodecode.lib import helpers as h
41 from rhodecode.lib import helpers as h
42 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
42 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
43 from rhodecode.lib.exceptions import UserCreationError
43 from rhodecode.lib.exceptions import UserCreationError
44 from rhodecode.lib.utils import (
44 from rhodecode.lib.utils import (
45 get_repo_slug, set_rhodecode_config, password_changed,
45 get_repo_slug, set_rhodecode_config, password_changed,
46 get_enabled_hook_classes)
46 get_enabled_hook_classes)
47 from rhodecode.lib.utils2 import (
47 from rhodecode.lib.utils2 import (
48 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist, safe_str)
48 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist, safe_str)
49 from rhodecode.model import meta
49 from rhodecode.model import meta
50 from rhodecode.model.db import Repository, User, ChangesetComment
50 from rhodecode.model.db import Repository, User, ChangesetComment
51 from rhodecode.model.notification import NotificationModel
51 from rhodecode.model.notification import NotificationModel
52 from rhodecode.model.scm import ScmModel
52 from rhodecode.model.scm import ScmModel
53 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
53 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
54
54
55 # NOTE(marcink): remove after base controller is no longer required
55 # NOTE(marcink): remove after base controller is no longer required
56 from pylons.controllers import WSGIController
56 from pylons.controllers import WSGIController
57 from pylons.i18n import translation
57 from pylons.i18n import translation
58
58
59 log = logging.getLogger(__name__)
59 log = logging.getLogger(__name__)
60
60
61
61
62 # hack to make the migration to pyramid easier
62 # hack to make the migration to pyramid easier
63 def render(template_name, extra_vars=None, cache_key=None,
63 def render(template_name, extra_vars=None, cache_key=None,
64 cache_type=None, cache_expire=None):
64 cache_type=None, cache_expire=None):
65 """Render a template with Mako
65 """Render a template with Mako
66
66
67 Accepts the cache options ``cache_key``, ``cache_type``, and
67 Accepts the cache options ``cache_key``, ``cache_type``, and
68 ``cache_expire``.
68 ``cache_expire``.
69
69
70 """
70 """
71 from pylons.templating import literal
71 from pylons.templating import literal
72 from pylons.templating import cached_template, pylons_globals
72 from pylons.templating import cached_template, pylons_globals
73
73
74 # Create a render callable for the cache function
74 # Create a render callable for the cache function
75 def render_template():
75 def render_template():
76 # Pull in extra vars if needed
76 # Pull in extra vars if needed
77 globs = extra_vars or {}
77 globs = extra_vars or {}
78
78
79 # Second, get the globals
79 # Second, get the globals
80 globs.update(pylons_globals())
80 globs.update(pylons_globals())
81
81
82 globs['_ungettext'] = globs['ungettext']
82 globs['_ungettext'] = globs['ungettext']
83 # Grab a template reference
83 # Grab a template reference
84 template = globs['app_globals'].mako_lookup.get_template(template_name)
84 template = globs['app_globals'].mako_lookup.get_template(template_name)
85
85
86 return literal(template.render_unicode(**globs))
86 return literal(template.render_unicode(**globs))
87
87
88 return cached_template(template_name, render_template, cache_key=cache_key,
88 return cached_template(template_name, render_template, cache_key=cache_key,
89 cache_type=cache_type, cache_expire=cache_expire)
89 cache_type=cache_type, cache_expire=cache_expire)
90
90
91 def _filter_proxy(ip):
91 def _filter_proxy(ip):
92 """
92 """
93 Passed in IP addresses in HEADERS can be in a special format of multiple
93 Passed in IP addresses in HEADERS can be in a special format of multiple
94 ips. Those comma separated IPs are passed from various proxies in the
94 ips. Those comma separated IPs are passed from various proxies in the
95 chain of request processing. The left-most being the original client.
95 chain of request processing. The left-most being the original client.
96 We only care about the first IP which came from the org. client.
96 We only care about the first IP which came from the org. client.
97
97
98 :param ip: ip string from headers
98 :param ip: ip string from headers
99 """
99 """
100 if ',' in ip:
100 if ',' in ip:
101 _ips = ip.split(',')
101 _ips = ip.split(',')
102 _first_ip = _ips[0].strip()
102 _first_ip = _ips[0].strip()
103 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
103 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
104 return _first_ip
104 return _first_ip
105 return ip
105 return ip
106
106
107
107
108 def _filter_port(ip):
108 def _filter_port(ip):
109 """
109 """
110 Removes a port from ip, there are 4 main cases to handle here.
110 Removes a port from ip, there are 4 main cases to handle here.
111 - ipv4 eg. 127.0.0.1
111 - ipv4 eg. 127.0.0.1
112 - ipv6 eg. ::1
112 - ipv6 eg. ::1
113 - ipv4+port eg. 127.0.0.1:8080
113 - ipv4+port eg. 127.0.0.1:8080
114 - ipv6+port eg. [::1]:8080
114 - ipv6+port eg. [::1]:8080
115
115
116 :param ip:
116 :param ip:
117 """
117 """
118 def is_ipv6(ip_addr):
118 def is_ipv6(ip_addr):
119 if hasattr(socket, 'inet_pton'):
119 if hasattr(socket, 'inet_pton'):
120 try:
120 try:
121 socket.inet_pton(socket.AF_INET6, ip_addr)
121 socket.inet_pton(socket.AF_INET6, ip_addr)
122 except socket.error:
122 except socket.error:
123 return False
123 return False
124 else:
124 else:
125 # fallback to ipaddress
125 # fallback to ipaddress
126 try:
126 try:
127 ipaddress.IPv6Address(safe_unicode(ip_addr))
127 ipaddress.IPv6Address(safe_unicode(ip_addr))
128 except Exception:
128 except Exception:
129 return False
129 return False
130 return True
130 return True
131
131
132 if ':' not in ip: # must be ipv4 pure ip
132 if ':' not in ip: # must be ipv4 pure ip
133 return ip
133 return ip
134
134
135 if '[' in ip and ']' in ip: # ipv6 with port
135 if '[' in ip and ']' in ip: # ipv6 with port
136 return ip.split(']')[0][1:].lower()
136 return ip.split(']')[0][1:].lower()
137
137
138 # must be ipv6 or ipv4 with port
138 # must be ipv6 or ipv4 with port
139 if is_ipv6(ip):
139 if is_ipv6(ip):
140 return ip
140 return ip
141 else:
141 else:
142 ip, _port = ip.split(':')[:2] # means ipv4+port
142 ip, _port = ip.split(':')[:2] # means ipv4+port
143 return ip
143 return ip
144
144
145
145
146 def get_ip_addr(environ):
146 def get_ip_addr(environ):
147 proxy_key = 'HTTP_X_REAL_IP'
147 proxy_key = 'HTTP_X_REAL_IP'
148 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
148 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
149 def_key = 'REMOTE_ADDR'
149 def_key = 'REMOTE_ADDR'
150 _filters = lambda x: _filter_port(_filter_proxy(x))
150 _filters = lambda x: _filter_port(_filter_proxy(x))
151
151
152 ip = environ.get(proxy_key)
152 ip = environ.get(proxy_key)
153 if ip:
153 if ip:
154 return _filters(ip)
154 return _filters(ip)
155
155
156 ip = environ.get(proxy_key2)
156 ip = environ.get(proxy_key2)
157 if ip:
157 if ip:
158 return _filters(ip)
158 return _filters(ip)
159
159
160 ip = environ.get(def_key, '0.0.0.0')
160 ip = environ.get(def_key, '0.0.0.0')
161 return _filters(ip)
161 return _filters(ip)
162
162
163
163
164 def get_server_ip_addr(environ, log_errors=True):
164 def get_server_ip_addr(environ, log_errors=True):
165 hostname = environ.get('SERVER_NAME')
165 hostname = environ.get('SERVER_NAME')
166 try:
166 try:
167 return socket.gethostbyname(hostname)
167 return socket.gethostbyname(hostname)
168 except Exception as e:
168 except Exception as e:
169 if log_errors:
169 if log_errors:
170 # in some cases this lookup is not possible, and we don't want to
170 # in some cases this lookup is not possible, and we don't want to
171 # make it an exception in logs
171 # make it an exception in logs
172 log.exception('Could not retrieve server ip address: %s', e)
172 log.exception('Could not retrieve server ip address: %s', e)
173 return hostname
173 return hostname
174
174
175
175
176 def get_server_port(environ):
176 def get_server_port(environ):
177 return environ.get('SERVER_PORT')
177 return environ.get('SERVER_PORT')
178
178
179
179
180 def get_access_path(environ):
180 def get_access_path(environ):
181 path = environ.get('PATH_INFO')
181 path = environ.get('PATH_INFO')
182 org_req = environ.get('pylons.original_request')
182 org_req = environ.get('pylons.original_request')
183 if org_req:
183 if org_req:
184 path = org_req.environ.get('PATH_INFO')
184 path = org_req.environ.get('PATH_INFO')
185 return path
185 return path
186
186
187
187
188 def get_user_agent(environ):
188 def get_user_agent(environ):
189 return environ.get('HTTP_USER_AGENT')
189 return environ.get('HTTP_USER_AGENT')
190
190
191
191
192 def vcs_operation_context(
192 def vcs_operation_context(
193 environ, repo_name, username, action, scm, check_locking=True,
193 environ, repo_name, username, action, scm, check_locking=True,
194 is_shadow_repo=False):
194 is_shadow_repo=False):
195 """
195 """
196 Generate the context for a vcs operation, e.g. push or pull.
196 Generate the context for a vcs operation, e.g. push or pull.
197
197
198 This context is passed over the layers so that hooks triggered by the
198 This context is passed over the layers so that hooks triggered by the
199 vcs operation know details like the user, the user's IP address etc.
199 vcs operation know details like the user, the user's IP address etc.
200
200
201 :param check_locking: Allows to switch of the computation of the locking
201 :param check_locking: Allows to switch of the computation of the locking
202 data. This serves mainly the need of the simplevcs middleware to be
202 data. This serves mainly the need of the simplevcs middleware to be
203 able to disable this for certain operations.
203 able to disable this for certain operations.
204
204
205 """
205 """
206 # Tri-state value: False: unlock, None: nothing, True: lock
206 # Tri-state value: False: unlock, None: nothing, True: lock
207 make_lock = None
207 make_lock = None
208 locked_by = [None, None, None]
208 locked_by = [None, None, None]
209 is_anonymous = username == User.DEFAULT_USER
209 is_anonymous = username == User.DEFAULT_USER
210 if not is_anonymous and check_locking:
210 if not is_anonymous and check_locking:
211 log.debug('Checking locking on repository "%s"', repo_name)
211 log.debug('Checking locking on repository "%s"', repo_name)
212 user = User.get_by_username(username)
212 user = User.get_by_username(username)
213 repo = Repository.get_by_repo_name(repo_name)
213 repo = Repository.get_by_repo_name(repo_name)
214 make_lock, __, locked_by = repo.get_locking_state(
214 make_lock, __, locked_by = repo.get_locking_state(
215 action, user.user_id)
215 action, user.user_id)
216
216
217 settings_model = VcsSettingsModel(repo=repo_name)
217 settings_model = VcsSettingsModel(repo=repo_name)
218 ui_settings = settings_model.get_ui_settings()
218 ui_settings = settings_model.get_ui_settings()
219
219
220 extras = {
220 extras = {
221 'ip': get_ip_addr(environ),
221 'ip': get_ip_addr(environ),
222 'username': username,
222 'username': username,
223 'action': action,
223 'action': action,
224 'repository': repo_name,
224 'repository': repo_name,
225 'scm': scm,
225 'scm': scm,
226 'config': rhodecode.CONFIG['__file__'],
226 'config': rhodecode.CONFIG['__file__'],
227 'make_lock': make_lock,
227 'make_lock': make_lock,
228 'locked_by': locked_by,
228 'locked_by': locked_by,
229 'server_url': utils2.get_server_url(environ),
229 'server_url': utils2.get_server_url(environ),
230 'user_agent': get_user_agent(environ),
230 'user_agent': get_user_agent(environ),
231 'hooks': get_enabled_hook_classes(ui_settings),
231 'hooks': get_enabled_hook_classes(ui_settings),
232 'is_shadow_repo': is_shadow_repo,
232 'is_shadow_repo': is_shadow_repo,
233 }
233 }
234 return extras
234 return extras
235
235
236
236
237 class BasicAuth(AuthBasicAuthenticator):
237 class BasicAuth(AuthBasicAuthenticator):
238
238
239 def __init__(self, realm, authfunc, registry, auth_http_code=None,
239 def __init__(self, realm, authfunc, registry, auth_http_code=None,
240 initial_call_detection=False, acl_repo_name=None):
240 initial_call_detection=False, acl_repo_name=None):
241 self.realm = realm
241 self.realm = realm
242 self.initial_call = initial_call_detection
242 self.initial_call = initial_call_detection
243 self.authfunc = authfunc
243 self.authfunc = authfunc
244 self.registry = registry
244 self.registry = registry
245 self.acl_repo_name = acl_repo_name
245 self.acl_repo_name = acl_repo_name
246 self._rc_auth_http_code = auth_http_code
246 self._rc_auth_http_code = auth_http_code
247
247
248 def _get_response_from_code(self, http_code):
248 def _get_response_from_code(self, http_code):
249 try:
249 try:
250 return get_exception(safe_int(http_code))
250 return get_exception(safe_int(http_code))
251 except Exception:
251 except Exception:
252 log.exception('Failed to fetch response for code %s' % http_code)
252 log.exception('Failed to fetch response for code %s' % http_code)
253 return HTTPForbidden
253 return HTTPForbidden
254
254
255 def get_rc_realm(self):
255 def get_rc_realm(self):
256 return safe_str(self.registry.rhodecode_settings.get('rhodecode_realm'))
256 return safe_str(self.registry.rhodecode_settings.get('rhodecode_realm'))
257
257
258 def build_authentication(self):
258 def build_authentication(self):
259 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
259 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
260 if self._rc_auth_http_code and not self.initial_call:
260 if self._rc_auth_http_code and not self.initial_call:
261 # return alternative HTTP code if alternative http return code
261 # return alternative HTTP code if alternative http return code
262 # is specified in RhodeCode config, but ONLY if it's not the
262 # is specified in RhodeCode config, but ONLY if it's not the
263 # FIRST call
263 # FIRST call
264 custom_response_klass = self._get_response_from_code(
264 custom_response_klass = self._get_response_from_code(
265 self._rc_auth_http_code)
265 self._rc_auth_http_code)
266 return custom_response_klass(headers=head)
266 return custom_response_klass(headers=head)
267 return HTTPUnauthorized(headers=head)
267 return HTTPUnauthorized(headers=head)
268
268
269 def authenticate(self, environ):
269 def authenticate(self, environ):
270 authorization = AUTHORIZATION(environ)
270 authorization = AUTHORIZATION(environ)
271 if not authorization:
271 if not authorization:
272 return self.build_authentication()
272 return self.build_authentication()
273 (authmeth, auth) = authorization.split(' ', 1)
273 (authmeth, auth) = authorization.split(' ', 1)
274 if 'basic' != authmeth.lower():
274 if 'basic' != authmeth.lower():
275 return self.build_authentication()
275 return self.build_authentication()
276 auth = auth.strip().decode('base64')
276 auth = auth.strip().decode('base64')
277 _parts = auth.split(':', 1)
277 _parts = auth.split(':', 1)
278 if len(_parts) == 2:
278 if len(_parts) == 2:
279 username, password = _parts
279 username, password = _parts
280 if self.authfunc(
280 auth_data = self.authfunc(
281 username, password, environ, VCS_TYPE,
281 username, password, environ, VCS_TYPE,
282 registry=self.registry, acl_repo_name=self.acl_repo_name):
282 registry=self.registry, acl_repo_name=self.acl_repo_name)
283 return username
283 if auth_data:
284 return {'username': username, 'auth_data': auth_data}
284 if username and password:
285 if username and password:
285 # we mark that we actually executed authentication once, at
286 # we mark that we actually executed authentication once, at
286 # that point we can use the alternative auth code
287 # that point we can use the alternative auth code
287 self.initial_call = False
288 self.initial_call = False
288
289
289 return self.build_authentication()
290 return self.build_authentication()
290
291
291 __call__ = authenticate
292 __call__ = authenticate
292
293
293
294
294 def calculate_version_hash(config):
295 def calculate_version_hash(config):
295 return md5(
296 return md5(
296 config.get('beaker.session.secret', '') +
297 config.get('beaker.session.secret', '') +
297 rhodecode.__version__)[:8]
298 rhodecode.__version__)[:8]
298
299
299
300
300 def get_current_lang(request):
301 def get_current_lang(request):
301 # NOTE(marcink): remove after pyramid move
302 # NOTE(marcink): remove after pyramid move
302 try:
303 try:
303 return translation.get_lang()[0]
304 return translation.get_lang()[0]
304 except:
305 except:
305 pass
306 pass
306
307
307 return getattr(request, '_LOCALE_', request.locale_name)
308 return getattr(request, '_LOCALE_', request.locale_name)
308
309
309
310
310 def attach_context_attributes(context, request, user_id):
311 def attach_context_attributes(context, request, user_id):
311 """
312 """
312 Attach variables into template context called `c`, please note that
313 Attach variables into template context called `c`, please note that
313 request could be pylons or pyramid request in here.
314 request could be pylons or pyramid request in here.
314 """
315 """
315 # NOTE(marcink): remove check after pyramid migration
316 # NOTE(marcink): remove check after pyramid migration
316 if hasattr(request, 'registry'):
317 if hasattr(request, 'registry'):
317 config = request.registry.settings
318 config = request.registry.settings
318 else:
319 else:
319 from pylons import config
320 from pylons import config
320
321
321 rc_config = SettingsModel().get_all_settings(cache=True)
322 rc_config = SettingsModel().get_all_settings(cache=True)
322
323
323 context.rhodecode_version = rhodecode.__version__
324 context.rhodecode_version = rhodecode.__version__
324 context.rhodecode_edition = config.get('rhodecode.edition')
325 context.rhodecode_edition = config.get('rhodecode.edition')
325 # unique secret + version does not leak the version but keep consistency
326 # unique secret + version does not leak the version but keep consistency
326 context.rhodecode_version_hash = calculate_version_hash(config)
327 context.rhodecode_version_hash = calculate_version_hash(config)
327
328
328 # Default language set for the incoming request
329 # Default language set for the incoming request
329 context.language = get_current_lang(request)
330 context.language = get_current_lang(request)
330
331
331 # Visual options
332 # Visual options
332 context.visual = AttributeDict({})
333 context.visual = AttributeDict({})
333
334
334 # DB stored Visual Items
335 # DB stored Visual Items
335 context.visual.show_public_icon = str2bool(
336 context.visual.show_public_icon = str2bool(
336 rc_config.get('rhodecode_show_public_icon'))
337 rc_config.get('rhodecode_show_public_icon'))
337 context.visual.show_private_icon = str2bool(
338 context.visual.show_private_icon = str2bool(
338 rc_config.get('rhodecode_show_private_icon'))
339 rc_config.get('rhodecode_show_private_icon'))
339 context.visual.stylify_metatags = str2bool(
340 context.visual.stylify_metatags = str2bool(
340 rc_config.get('rhodecode_stylify_metatags'))
341 rc_config.get('rhodecode_stylify_metatags'))
341 context.visual.dashboard_items = safe_int(
342 context.visual.dashboard_items = safe_int(
342 rc_config.get('rhodecode_dashboard_items', 100))
343 rc_config.get('rhodecode_dashboard_items', 100))
343 context.visual.admin_grid_items = safe_int(
344 context.visual.admin_grid_items = safe_int(
344 rc_config.get('rhodecode_admin_grid_items', 100))
345 rc_config.get('rhodecode_admin_grid_items', 100))
345 context.visual.repository_fields = str2bool(
346 context.visual.repository_fields = str2bool(
346 rc_config.get('rhodecode_repository_fields'))
347 rc_config.get('rhodecode_repository_fields'))
347 context.visual.show_version = str2bool(
348 context.visual.show_version = str2bool(
348 rc_config.get('rhodecode_show_version'))
349 rc_config.get('rhodecode_show_version'))
349 context.visual.use_gravatar = str2bool(
350 context.visual.use_gravatar = str2bool(
350 rc_config.get('rhodecode_use_gravatar'))
351 rc_config.get('rhodecode_use_gravatar'))
351 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
352 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
352 context.visual.default_renderer = rc_config.get(
353 context.visual.default_renderer = rc_config.get(
353 'rhodecode_markup_renderer', 'rst')
354 'rhodecode_markup_renderer', 'rst')
354 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
355 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
355 context.visual.rhodecode_support_url = \
356 context.visual.rhodecode_support_url = \
356 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
357 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
357
358
358 context.visual.affected_files_cut_off = 60
359 context.visual.affected_files_cut_off = 60
359
360
360 context.pre_code = rc_config.get('rhodecode_pre_code')
361 context.pre_code = rc_config.get('rhodecode_pre_code')
361 context.post_code = rc_config.get('rhodecode_post_code')
362 context.post_code = rc_config.get('rhodecode_post_code')
362 context.rhodecode_name = rc_config.get('rhodecode_title')
363 context.rhodecode_name = rc_config.get('rhodecode_title')
363 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
364 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
364 # if we have specified default_encoding in the request, it has more
365 # if we have specified default_encoding in the request, it has more
365 # priority
366 # priority
366 if request.GET.get('default_encoding'):
367 if request.GET.get('default_encoding'):
367 context.default_encodings.insert(0, request.GET.get('default_encoding'))
368 context.default_encodings.insert(0, request.GET.get('default_encoding'))
368 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
369 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
369
370
370 # INI stored
371 # INI stored
371 context.labs_active = str2bool(
372 context.labs_active = str2bool(
372 config.get('labs_settings_active', 'false'))
373 config.get('labs_settings_active', 'false'))
373 context.visual.allow_repo_location_change = str2bool(
374 context.visual.allow_repo_location_change = str2bool(
374 config.get('allow_repo_location_change', True))
375 config.get('allow_repo_location_change', True))
375 context.visual.allow_custom_hooks_settings = str2bool(
376 context.visual.allow_custom_hooks_settings = str2bool(
376 config.get('allow_custom_hooks_settings', True))
377 config.get('allow_custom_hooks_settings', True))
377 context.debug_style = str2bool(config.get('debug_style', False))
378 context.debug_style = str2bool(config.get('debug_style', False))
378
379
379 context.rhodecode_instanceid = config.get('instance_id')
380 context.rhodecode_instanceid = config.get('instance_id')
380
381
381 context.visual.cut_off_limit_diff = safe_int(
382 context.visual.cut_off_limit_diff = safe_int(
382 config.get('cut_off_limit_diff'))
383 config.get('cut_off_limit_diff'))
383 context.visual.cut_off_limit_file = safe_int(
384 context.visual.cut_off_limit_file = safe_int(
384 config.get('cut_off_limit_file'))
385 config.get('cut_off_limit_file'))
385
386
386 # AppEnlight
387 # AppEnlight
387 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
388 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
388 context.appenlight_api_public_key = config.get(
389 context.appenlight_api_public_key = config.get(
389 'appenlight.api_public_key', '')
390 'appenlight.api_public_key', '')
390 context.appenlight_server_url = config.get('appenlight.server_url', '')
391 context.appenlight_server_url = config.get('appenlight.server_url', '')
391
392
392 # JS template context
393 # JS template context
393 context.template_context = {
394 context.template_context = {
394 'repo_name': None,
395 'repo_name': None,
395 'repo_type': None,
396 'repo_type': None,
396 'repo_landing_commit': None,
397 'repo_landing_commit': None,
397 'rhodecode_user': {
398 'rhodecode_user': {
398 'username': None,
399 'username': None,
399 'email': None,
400 'email': None,
400 'notification_status': False
401 'notification_status': False
401 },
402 },
402 'visual': {
403 'visual': {
403 'default_renderer': None
404 'default_renderer': None
404 },
405 },
405 'commit_data': {
406 'commit_data': {
406 'commit_id': None
407 'commit_id': None
407 },
408 },
408 'pull_request_data': {'pull_request_id': None},
409 'pull_request_data': {'pull_request_id': None},
409 'timeago': {
410 'timeago': {
410 'refresh_time': 120 * 1000,
411 'refresh_time': 120 * 1000,
411 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
412 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
412 },
413 },
413 'pyramid_dispatch': {
414 'pyramid_dispatch': {
414
415
415 },
416 },
416 'extra': {'plugins': {}}
417 'extra': {'plugins': {}}
417 }
418 }
418 # END CONFIG VARS
419 # END CONFIG VARS
419
420
420 # TODO: This dosn't work when called from pylons compatibility tween.
421 # TODO: This dosn't work when called from pylons compatibility tween.
421 # Fix this and remove it from base controller.
422 # Fix this and remove it from base controller.
422 # context.repo_name = get_repo_slug(request) # can be empty
423 # context.repo_name = get_repo_slug(request) # can be empty
423
424
424 diffmode = 'sideside'
425 diffmode = 'sideside'
425 if request.GET.get('diffmode'):
426 if request.GET.get('diffmode'):
426 if request.GET['diffmode'] == 'unified':
427 if request.GET['diffmode'] == 'unified':
427 diffmode = 'unified'
428 diffmode = 'unified'
428 elif request.session.get('diffmode'):
429 elif request.session.get('diffmode'):
429 diffmode = request.session['diffmode']
430 diffmode = request.session['diffmode']
430
431
431 context.diffmode = diffmode
432 context.diffmode = diffmode
432
433
433 if request.session.get('diffmode') != diffmode:
434 if request.session.get('diffmode') != diffmode:
434 request.session['diffmode'] = diffmode
435 request.session['diffmode'] = diffmode
435
436
436 context.csrf_token = auth.get_csrf_token(session=request.session)
437 context.csrf_token = auth.get_csrf_token(session=request.session)
437 context.backends = rhodecode.BACKENDS.keys()
438 context.backends = rhodecode.BACKENDS.keys()
438 context.backends.sort()
439 context.backends.sort()
439 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
440 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
440
441
441 # NOTE(marcink): when migrated to pyramid we don't need to set this anymore,
442 # NOTE(marcink): when migrated to pyramid we don't need to set this anymore,
442 # given request will ALWAYS be pyramid one
443 # given request will ALWAYS be pyramid one
443 pyramid_request = pyramid.threadlocal.get_current_request()
444 pyramid_request = pyramid.threadlocal.get_current_request()
444 context.pyramid_request = pyramid_request
445 context.pyramid_request = pyramid_request
445
446
446 # web case
447 # web case
447 if hasattr(pyramid_request, 'user'):
448 if hasattr(pyramid_request, 'user'):
448 context.auth_user = pyramid_request.user
449 context.auth_user = pyramid_request.user
449 context.rhodecode_user = pyramid_request.user
450 context.rhodecode_user = pyramid_request.user
450
451
451 # api case
452 # api case
452 if hasattr(pyramid_request, 'rpc_user'):
453 if hasattr(pyramid_request, 'rpc_user'):
453 context.auth_user = pyramid_request.rpc_user
454 context.auth_user = pyramid_request.rpc_user
454 context.rhodecode_user = pyramid_request.rpc_user
455 context.rhodecode_user = pyramid_request.rpc_user
455
456
456 # attach the whole call context to the request
457 # attach the whole call context to the request
457 request.call_context = context
458 request.call_context = context
458
459
459
460
460 def get_auth_user(request):
461 def get_auth_user(request):
461 environ = request.environ
462 environ = request.environ
462 session = request.session
463 session = request.session
463
464
464 ip_addr = get_ip_addr(environ)
465 ip_addr = get_ip_addr(environ)
465 # make sure that we update permissions each time we call controller
466 # make sure that we update permissions each time we call controller
466 _auth_token = (request.GET.get('auth_token', '') or
467 _auth_token = (request.GET.get('auth_token', '') or
467 request.GET.get('api_key', ''))
468 request.GET.get('api_key', ''))
468
469
469 if _auth_token:
470 if _auth_token:
470 # when using API_KEY we assume user exists, and
471 # when using API_KEY we assume user exists, and
471 # doesn't need auth based on cookies.
472 # doesn't need auth based on cookies.
472 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
473 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
473 authenticated = False
474 authenticated = False
474 else:
475 else:
475 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
476 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
476 try:
477 try:
477 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
478 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
478 ip_addr=ip_addr)
479 ip_addr=ip_addr)
479 except UserCreationError as e:
480 except UserCreationError as e:
480 h.flash(e, 'error')
481 h.flash(e, 'error')
481 # container auth or other auth functions that create users
482 # container auth or other auth functions that create users
482 # on the fly can throw this exception signaling that there's
483 # on the fly can throw this exception signaling that there's
483 # issue with user creation, explanation should be provided
484 # issue with user creation, explanation should be provided
484 # in Exception itself. We then create a simple blank
485 # in Exception itself. We then create a simple blank
485 # AuthUser
486 # AuthUser
486 auth_user = AuthUser(ip_addr=ip_addr)
487 auth_user = AuthUser(ip_addr=ip_addr)
487
488
488 if password_changed(auth_user, session):
489 if password_changed(auth_user, session):
489 session.invalidate()
490 session.invalidate()
490 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
491 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
491 auth_user = AuthUser(ip_addr=ip_addr)
492 auth_user = AuthUser(ip_addr=ip_addr)
492
493
493 authenticated = cookie_store.get('is_authenticated')
494 authenticated = cookie_store.get('is_authenticated')
494
495
495 if not auth_user.is_authenticated and auth_user.is_user_object:
496 if not auth_user.is_authenticated and auth_user.is_user_object:
496 # user is not authenticated and not empty
497 # user is not authenticated and not empty
497 auth_user.set_authenticated(authenticated)
498 auth_user.set_authenticated(authenticated)
498
499
499 return auth_user
500 return auth_user
500
501
501
502
502 class BaseController(WSGIController):
503 class BaseController(WSGIController):
503
504
504 def __before__(self):
505 def __before__(self):
505 """
506 """
506 __before__ is called before controller methods and after __call__
507 __before__ is called before controller methods and after __call__
507 """
508 """
508 # on each call propagate settings calls into global settings.
509 # on each call propagate settings calls into global settings.
509 from pylons import config
510 from pylons import config
510 from pylons import tmpl_context as c, request, url
511 from pylons import tmpl_context as c, request, url
511 set_rhodecode_config(config)
512 set_rhodecode_config(config)
512 attach_context_attributes(c, request, self._rhodecode_user.user_id)
513 attach_context_attributes(c, request, self._rhodecode_user.user_id)
513
514
514 # TODO: Remove this when fixed in attach_context_attributes()
515 # TODO: Remove this when fixed in attach_context_attributes()
515 c.repo_name = get_repo_slug(request) # can be empty
516 c.repo_name = get_repo_slug(request) # can be empty
516
517
517 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
518 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
518 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
519 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
519 self.sa = meta.Session
520 self.sa = meta.Session
520 self.scm_model = ScmModel(self.sa)
521 self.scm_model = ScmModel(self.sa)
521
522
522 # set user language
523 # set user language
523 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
524 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
524 if user_lang:
525 if user_lang:
525 translation.set_lang(user_lang)
526 translation.set_lang(user_lang)
526 log.debug('set language to %s for user %s',
527 log.debug('set language to %s for user %s',
527 user_lang, self._rhodecode_user)
528 user_lang, self._rhodecode_user)
528
529
529 def _dispatch_redirect(self, with_url, environ, start_response):
530 def _dispatch_redirect(self, with_url, environ, start_response):
530 from webob.exc import HTTPFound
531 from webob.exc import HTTPFound
531 resp = HTTPFound(with_url)
532 resp = HTTPFound(with_url)
532 environ['SCRIPT_NAME'] = '' # handle prefix middleware
533 environ['SCRIPT_NAME'] = '' # handle prefix middleware
533 environ['PATH_INFO'] = with_url
534 environ['PATH_INFO'] = with_url
534 return resp(environ, start_response)
535 return resp(environ, start_response)
535
536
536 def __call__(self, environ, start_response):
537 def __call__(self, environ, start_response):
537 """Invoke the Controller"""
538 """Invoke the Controller"""
538 # WSGIController.__call__ dispatches to the Controller method
539 # WSGIController.__call__ dispatches to the Controller method
539 # the request is routed to. This routing information is
540 # the request is routed to. This routing information is
540 # available in environ['pylons.routes_dict']
541 # available in environ['pylons.routes_dict']
541 from rhodecode.lib import helpers as h
542 from rhodecode.lib import helpers as h
542 from pylons import tmpl_context as c, request, url
543 from pylons import tmpl_context as c, request, url
543
544
544 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
545 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
545 if environ.get('debugtoolbar.wants_pylons_context', False):
546 if environ.get('debugtoolbar.wants_pylons_context', False):
546 environ['debugtoolbar.pylons_context'] = c._current_obj()
547 environ['debugtoolbar.pylons_context'] = c._current_obj()
547
548
548 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
549 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
549 environ['pylons.routes_dict']['action']])
550 environ['pylons.routes_dict']['action']])
550
551
551 self.rc_config = SettingsModel().get_all_settings(cache=True)
552 self.rc_config = SettingsModel().get_all_settings(cache=True)
552 self.ip_addr = get_ip_addr(environ)
553 self.ip_addr = get_ip_addr(environ)
553
554
554 # The rhodecode auth user is looked up and passed through the
555 # The rhodecode auth user is looked up and passed through the
555 # environ by the pylons compatibility tween in pyramid.
556 # environ by the pylons compatibility tween in pyramid.
556 # So we can just grab it from there.
557 # So we can just grab it from there.
557 auth_user = environ['rc_auth_user']
558 auth_user = environ['rc_auth_user']
558
559
559 # set globals for auth user
560 # set globals for auth user
560 request.user = auth_user
561 request.user = auth_user
561 self._rhodecode_user = auth_user
562 self._rhodecode_user = auth_user
562
563
563 log.info('IP: %s User: %s accessed %s [%s]' % (
564 log.info('IP: %s User: %s accessed %s [%s]' % (
564 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
565 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
565 _route_name)
566 _route_name)
566 )
567 )
567
568
568 user_obj = auth_user.get_instance()
569 user_obj = auth_user.get_instance()
569 if user_obj and user_obj.user_data.get('force_password_change'):
570 if user_obj and user_obj.user_data.get('force_password_change'):
570 h.flash('You are required to change your password', 'warning',
571 h.flash('You are required to change your password', 'warning',
571 ignore_duplicate=True)
572 ignore_duplicate=True)
572 return self._dispatch_redirect(
573 return self._dispatch_redirect(
573 url('my_account_password'), environ, start_response)
574 url('my_account_password'), environ, start_response)
574
575
575 return WSGIController.__call__(self, environ, start_response)
576 return WSGIController.__call__(self, environ, start_response)
576
577
577
578
578 def h_filter(s):
579 def h_filter(s):
579 """
580 """
580 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
581 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
581 we wrap this with additional functionality that converts None to empty
582 we wrap this with additional functionality that converts None to empty
582 strings
583 strings
583 """
584 """
584 if s is None:
585 if s is None:
585 return markupsafe.Markup()
586 return markupsafe.Markup()
586 return markupsafe.escape(s)
587 return markupsafe.escape(s)
587
588
588
589
589 def add_events_routes(config):
590 def add_events_routes(config):
590 """
591 """
591 Adds routing that can be used in events. Because some events are triggered
592 Adds routing that can be used in events. Because some events are triggered
592 outside of pyramid context, we need to bootstrap request with some
593 outside of pyramid context, we need to bootstrap request with some
593 routing registered
594 routing registered
594 """
595 """
595 config.add_route(name='home', pattern='/')
596 config.add_route(name='home', pattern='/')
596
597
597 config.add_route(name='repo_summary', pattern='/{repo_name}')
598 config.add_route(name='repo_summary', pattern='/{repo_name}')
598 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
599 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
599 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
600 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
600
601
601 config.add_route(name='pullrequest_show',
602 config.add_route(name='pullrequest_show',
602 pattern='/{repo_name}/pull-request/{pull_request_id}')
603 pattern='/{repo_name}/pull-request/{pull_request_id}')
603 config.add_route(name='pull_requests_global',
604 config.add_route(name='pull_requests_global',
604 pattern='/pull-request/{pull_request_id}')
605 pattern='/pull-request/{pull_request_id}')
605
606
606 config.add_route(name='repo_commit',
607 config.add_route(name='repo_commit',
607 pattern='/{repo_name}/changeset/{commit_id}')
608 pattern='/{repo_name}/changeset/{commit_id}')
608 config.add_route(name='repo_files',
609 config.add_route(name='repo_files',
609 pattern='/{repo_name}/files/{commit_id}/{f_path}')
610 pattern='/{repo_name}/files/{commit_id}/{f_path}')
610
611
611
612
612 def bootstrap_request(**kwargs):
613 def bootstrap_request(**kwargs):
613 import pyramid.testing
614 import pyramid.testing
614
615
615 class TestRequest(pyramid.testing.DummyRequest):
616 class TestRequest(pyramid.testing.DummyRequest):
616 application_url = kwargs.pop('application_url', 'http://example.com')
617 application_url = kwargs.pop('application_url', 'http://example.com')
617 host = kwargs.pop('host', 'example.com:80')
618 host = kwargs.pop('host', 'example.com:80')
618 domain = kwargs.pop('domain', 'example.com')
619 domain = kwargs.pop('domain', 'example.com')
619
620
620 class TestDummySession(pyramid.testing.DummySession):
621 class TestDummySession(pyramid.testing.DummySession):
621 def save(*arg, **kw):
622 def save(*arg, **kw):
622 pass
623 pass
623
624
624 request = TestRequest(**kwargs)
625 request = TestRequest(**kwargs)
625 request.session = TestDummySession()
626 request.session = TestDummySession()
626
627
627 config = pyramid.testing.setUp(request=request)
628 config = pyramid.testing.setUp(request=request)
628 add_events_routes(config)
629 add_events_routes(config)
629 return request
630 return request
630
631
@@ -1,540 +1,599 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2017 RhodeCode GmbH
3 # Copyright (C) 2014-2017 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 logging
28 import logging
28 import importlib
29 import importlib
29 import re
30 from functools import wraps
30 from functools import wraps
31
31
32 import time
32 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
33 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
33 from webob.exc import (
34 from webob.exc import (
34 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
35 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
35
36
36 import rhodecode
37 import rhodecode
37 from rhodecode.authentication.base import authenticate, VCS_TYPE
38 from rhodecode.authentication.base import (
39 authenticate, get_perms_cache_manager, VCS_TYPE)
40 from rhodecode.lib import caches
38 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
41 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
39 from rhodecode.lib.base import (
42 from rhodecode.lib.base import (
40 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
43 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
41 from rhodecode.lib.exceptions import (
44 from rhodecode.lib.exceptions import (
42 HTTPLockedRC, HTTPRequirementError, UserCreationError,
45 HTTPLockedRC, HTTPRequirementError, UserCreationError,
43 NotAllowedToCreateUserError)
46 NotAllowedToCreateUserError)
44 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
47 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
45 from rhodecode.lib.middleware import appenlight
48 from rhodecode.lib.middleware import appenlight
46 from rhodecode.lib.middleware.utils import scm_app_http
49 from rhodecode.lib.middleware.utils import scm_app_http
47 from rhodecode.lib.utils import (
50 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
48 is_valid_repo, get_rhodecode_base_path, SLUG_RE)
49 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
51 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
50 from rhodecode.lib.vcs.conf import settings as vcs_settings
52 from rhodecode.lib.vcs.conf import settings as vcs_settings
51 from rhodecode.lib.vcs.backends import base
53 from rhodecode.lib.vcs.backends import base
52 from rhodecode.model import meta
54 from rhodecode.model import meta
53 from rhodecode.model.db import User, Repository, PullRequest
55 from rhodecode.model.db import User, Repository, PullRequest
54 from rhodecode.model.scm import ScmModel
56 from rhodecode.model.scm import ScmModel
55 from rhodecode.model.pull_request import PullRequestModel
57 from rhodecode.model.pull_request import PullRequestModel
56 from rhodecode.model.settings import SettingsModel
58 from rhodecode.model.settings import SettingsModel
57
59
58 log = logging.getLogger(__name__)
60 log = logging.getLogger(__name__)
59
61
60
62
61 def initialize_generator(factory):
63 def initialize_generator(factory):
62 """
64 """
63 Initializes the returned generator by draining its first element.
65 Initializes the returned generator by draining its first element.
64
66
65 This can be used to give a generator an initializer, which is the code
67 This can be used to give a generator an initializer, which is the code
66 up to the first yield statement. This decorator enforces that the first
68 up to the first yield statement. This decorator enforces that the first
67 produced element has the value ``"__init__"`` to make its special
69 produced element has the value ``"__init__"`` to make its special
68 purpose very explicit in the using code.
70 purpose very explicit in the using code.
69 """
71 """
70
72
71 @wraps(factory)
73 @wraps(factory)
72 def wrapper(*args, **kwargs):
74 def wrapper(*args, **kwargs):
73 gen = factory(*args, **kwargs)
75 gen = factory(*args, **kwargs)
74 try:
76 try:
75 init = gen.next()
77 init = gen.next()
76 except StopIteration:
78 except StopIteration:
77 raise ValueError('Generator must yield at least one element.')
79 raise ValueError('Generator must yield at least one element.')
78 if init != "__init__":
80 if init != "__init__":
79 raise ValueError('First yielded element must be "__init__".')
81 raise ValueError('First yielded element must be "__init__".')
80 return gen
82 return gen
81 return wrapper
83 return wrapper
82
84
83
85
84 class SimpleVCS(object):
86 class SimpleVCS(object):
85 """Common functionality for SCM HTTP handlers."""
87 """Common functionality for SCM HTTP handlers."""
86
88
87 SCM = 'unknown'
89 SCM = 'unknown'
88
90
89 acl_repo_name = None
91 acl_repo_name = None
90 url_repo_name = None
92 url_repo_name = None
91 vcs_repo_name = None
93 vcs_repo_name = None
92
94
93 # We have to handle requests to shadow repositories different than requests
95 # We have to handle requests to shadow repositories different than requests
94 # to normal repositories. Therefore we have to distinguish them. To do this
96 # to normal repositories. Therefore we have to distinguish them. To do this
95 # we use this regex which will match only on URLs pointing to shadow
97 # we use this regex which will match only on URLs pointing to shadow
96 # repositories.
98 # repositories.
97 shadow_repo_re = re.compile(
99 shadow_repo_re = re.compile(
98 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
100 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
99 '(?P<target>{slug_pat})/' # target repo
101 '(?P<target>{slug_pat})/' # target repo
100 'pull-request/(?P<pr_id>\d+)/' # pull request
102 'pull-request/(?P<pr_id>\d+)/' # pull request
101 'repository$' # shadow repo
103 'repository$' # shadow repo
102 .format(slug_pat=SLUG_RE.pattern))
104 .format(slug_pat=SLUG_RE.pattern))
103
105
104 def __init__(self, application, config, registry):
106 def __init__(self, application, config, registry):
105 self.registry = registry
107 self.registry = registry
106 self.application = application
108 self.application = application
107 self.config = config
109 self.config = config
108 # re-populated by specialized middleware
110 # re-populated by specialized middleware
109 self.repo_vcs_config = base.Config()
111 self.repo_vcs_config = base.Config()
110 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
112 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
111 self.basepath = rhodecode.CONFIG['base_path']
113 self.basepath = rhodecode.CONFIG['base_path']
112 registry.rhodecode_settings = self.rhodecode_settings
114 registry.rhodecode_settings = self.rhodecode_settings
113 # authenticate this VCS request using authfunc
115 # authenticate this VCS request using authfunc
114 auth_ret_code_detection = \
116 auth_ret_code_detection = \
115 str2bool(self.config.get('auth_ret_code_detection', False))
117 str2bool(self.config.get('auth_ret_code_detection', False))
116 self.authenticate = BasicAuth(
118 self.authenticate = BasicAuth(
117 '', authenticate, registry, config.get('auth_ret_code'),
119 '', authenticate, registry, config.get('auth_ret_code'),
118 auth_ret_code_detection)
120 auth_ret_code_detection)
119 self.ip_addr = '0.0.0.0'
121 self.ip_addr = '0.0.0.0'
120
122
121 def set_repo_names(self, environ):
123 def set_repo_names(self, environ):
122 """
124 """
123 This will populate the attributes acl_repo_name, url_repo_name,
125 This will populate the attributes acl_repo_name, url_repo_name,
124 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
126 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
125 shadow) repositories all names are equal. In case of requests to a
127 shadow) repositories all names are equal. In case of requests to a
126 shadow repository the acl-name points to the target repo of the pull
128 shadow repository the acl-name points to the target repo of the pull
127 request and the vcs-name points to the shadow repo file system path.
129 request and the vcs-name points to the shadow repo file system path.
128 The url-name is always the URL used by the vcs client program.
130 The url-name is always the URL used by the vcs client program.
129
131
130 Example in case of a shadow repo:
132 Example in case of a shadow repo:
131 acl_repo_name = RepoGroup/MyRepo
133 acl_repo_name = RepoGroup/MyRepo
132 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
134 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
133 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
135 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
134 """
136 """
135 # First we set the repo name from URL for all attributes. This is the
137 # First we set the repo name from URL for all attributes. This is the
136 # default if handling normal (non shadow) repo requests.
138 # default if handling normal (non shadow) repo requests.
137 self.url_repo_name = self._get_repository_name(environ)
139 self.url_repo_name = self._get_repository_name(environ)
138 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
140 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
139 self.is_shadow_repo = False
141 self.is_shadow_repo = False
140
142
141 # Check if this is a request to a shadow repository.
143 # Check if this is a request to a shadow repository.
142 match = self.shadow_repo_re.match(self.url_repo_name)
144 match = self.shadow_repo_re.match(self.url_repo_name)
143 if match:
145 if match:
144 match_dict = match.groupdict()
146 match_dict = match.groupdict()
145
147
146 # Build acl repo name from regex match.
148 # Build acl repo name from regex match.
147 acl_repo_name = safe_unicode('{groups}{target}'.format(
149 acl_repo_name = safe_unicode('{groups}{target}'.format(
148 groups=match_dict['groups'] or '',
150 groups=match_dict['groups'] or '',
149 target=match_dict['target']))
151 target=match_dict['target']))
150
152
151 # Retrieve pull request instance by ID from regex match.
153 # Retrieve pull request instance by ID from regex match.
152 pull_request = PullRequest.get(match_dict['pr_id'])
154 pull_request = PullRequest.get(match_dict['pr_id'])
153
155
154 # Only proceed if we got a pull request and if acl repo name from
156 # Only proceed if we got a pull request and if acl repo name from
155 # URL equals the target repo name of the pull request.
157 # URL equals the target repo name of the pull request.
156 if pull_request and (acl_repo_name ==
158 if pull_request and (acl_repo_name ==
157 pull_request.target_repo.repo_name):
159 pull_request.target_repo.repo_name):
158 # Get file system path to shadow repository.
160 # Get file system path to shadow repository.
159 workspace_id = PullRequestModel()._workspace_id(pull_request)
161 workspace_id = PullRequestModel()._workspace_id(pull_request)
160 target_vcs = pull_request.target_repo.scm_instance()
162 target_vcs = pull_request.target_repo.scm_instance()
161 vcs_repo_name = target_vcs._get_shadow_repository_path(
163 vcs_repo_name = target_vcs._get_shadow_repository_path(
162 workspace_id)
164 workspace_id)
163
165
164 # Store names for later usage.
166 # Store names for later usage.
165 self.vcs_repo_name = vcs_repo_name
167 self.vcs_repo_name = vcs_repo_name
166 self.acl_repo_name = acl_repo_name
168 self.acl_repo_name = acl_repo_name
167 self.is_shadow_repo = True
169 self.is_shadow_repo = True
168
170
169 log.debug('Setting all VCS repository names: %s', {
171 log.debug('Setting all VCS repository names: %s', {
170 'acl_repo_name': self.acl_repo_name,
172 'acl_repo_name': self.acl_repo_name,
171 'url_repo_name': self.url_repo_name,
173 'url_repo_name': self.url_repo_name,
172 'vcs_repo_name': self.vcs_repo_name,
174 'vcs_repo_name': self.vcs_repo_name,
173 })
175 })
174
176
175 @property
177 @property
176 def scm_app(self):
178 def scm_app(self):
177 custom_implementation = self.config['vcs.scm_app_implementation']
179 custom_implementation = self.config['vcs.scm_app_implementation']
178 if custom_implementation == 'http':
180 if custom_implementation == 'http':
179 log.info('Using HTTP implementation of scm app.')
181 log.info('Using HTTP implementation of scm app.')
180 scm_app_impl = scm_app_http
182 scm_app_impl = scm_app_http
181 else:
183 else:
182 log.info('Using custom implementation of scm_app: "{}"'.format(
184 log.info('Using custom implementation of scm_app: "{}"'.format(
183 custom_implementation))
185 custom_implementation))
184 scm_app_impl = importlib.import_module(custom_implementation)
186 scm_app_impl = importlib.import_module(custom_implementation)
185 return scm_app_impl
187 return scm_app_impl
186
188
187 def _get_by_id(self, repo_name):
189 def _get_by_id(self, repo_name):
188 """
190 """
189 Gets a special pattern _<ID> from clone url and tries to replace it
191 Gets a special pattern _<ID> from clone url and tries to replace it
190 with a repository_name for support of _<ID> non changeable urls
192 with a repository_name for support of _<ID> non changeable urls
191 """
193 """
192
194
193 data = repo_name.split('/')
195 data = repo_name.split('/')
194 if len(data) >= 2:
196 if len(data) >= 2:
195 from rhodecode.model.repo import RepoModel
197 from rhodecode.model.repo import RepoModel
196 by_id_match = RepoModel().get_repo_by_id(repo_name)
198 by_id_match = RepoModel().get_repo_by_id(repo_name)
197 if by_id_match:
199 if by_id_match:
198 data[1] = by_id_match.repo_name
200 data[1] = by_id_match.repo_name
199
201
200 return safe_str('/'.join(data))
202 return safe_str('/'.join(data))
201
203
202 def _invalidate_cache(self, repo_name):
204 def _invalidate_cache(self, repo_name):
203 """
205 """
204 Set's cache for this repository for invalidation on next access
206 Set's cache for this repository for invalidation on next access
205
207
206 :param repo_name: full repo name, also a cache key
208 :param repo_name: full repo name, also a cache key
207 """
209 """
208 ScmModel().mark_for_invalidation(repo_name)
210 ScmModel().mark_for_invalidation(repo_name)
209
211
210 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
212 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
211 db_repo = Repository.get_by_repo_name(repo_name)
213 db_repo = Repository.get_by_repo_name(repo_name)
212 if not db_repo:
214 if not db_repo:
213 log.debug('Repository `%s` not found inside the database.',
215 log.debug('Repository `%s` not found inside the database.',
214 repo_name)
216 repo_name)
215 return False
217 return False
216
218
217 if db_repo.repo_type != scm_type:
219 if db_repo.repo_type != scm_type:
218 log.warning(
220 log.warning(
219 'Repository `%s` have incorrect scm_type, expected %s got %s',
221 'Repository `%s` have incorrect scm_type, expected %s got %s',
220 repo_name, db_repo.repo_type, scm_type)
222 repo_name, db_repo.repo_type, scm_type)
221 return False
223 return False
222
224
223 return is_valid_repo(repo_name, base_path, explicit_scm=scm_type)
225 return is_valid_repo(repo_name, base_path, explicit_scm=scm_type)
224
226
225 def valid_and_active_user(self, user):
227 def valid_and_active_user(self, user):
226 """
228 """
227 Checks if that user is not empty, and if it's actually object it checks
229 Checks if that user is not empty, and if it's actually object it checks
228 if he's active.
230 if he's active.
229
231
230 :param user: user object or None
232 :param user: user object or None
231 :return: boolean
233 :return: boolean
232 """
234 """
233 if user is None:
235 if user is None:
234 return False
236 return False
235
237
236 elif user.active:
238 elif user.active:
237 return True
239 return True
238
240
239 return False
241 return False
240
242
241 @property
243 @property
242 def is_shadow_repo_dir(self):
244 def is_shadow_repo_dir(self):
243 return os.path.isdir(self.vcs_repo_name)
245 return os.path.isdir(self.vcs_repo_name)
244
246
245 def _check_permission(self, action, user, repo_name, ip_addr=None):
247 def _check_permission(self, action, user, repo_name, ip_addr=None,
248 plugin_id='', plugin_cache_active=False, cache_ttl=0):
246 """
249 """
247 Checks permissions using action (push/pull) user and repository
250 Checks permissions using action (push/pull) user and repository
248 name
251 name. If plugin_cache and ttl is set it will use the plugin which
252 authenticated the user to store the cached permissions result for N
253 amount of seconds as in cache_ttl
249
254
250 :param action: push or pull action
255 :param action: push or pull action
251 :param user: user instance
256 :param user: user instance
252 :param repo_name: repository name
257 :param repo_name: repository name
253 """
258 """
254 # check IP
259
255 inherit = user.inherit_default_permissions
260 # get instance of cache manager configured for a namespace
256 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
261 cache_manager = get_perms_cache_manager(custom_ttl=cache_ttl)
257 inherit_from_default=inherit)
262 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
258 if ip_allowed:
263 plugin_id, plugin_cache_active, cache_ttl)
259 log.info('Access for IP:%s allowed', ip_addr)
264
260 else:
265 # for environ based password can be empty, but then the validation is
261 return False
266 # on the server that fills in the env data needed for authentication
267 _perm_calc_hash = caches.compute_key_from_params(
268 plugin_id, action, user.user_id, repo_name, ip_addr)
262
269
263 if action == 'push':
270 # _authenticate is a wrapper for .auth() method of plugin.
264 if not HasPermissionAnyMiddleware('repository.write',
271 # it checks if .auth() sends proper data.
265 'repository.admin')(user,
272 # For RhodeCodeExternalAuthPlugin it also maps users to
266 repo_name):
273 # Database and maps the attributes returned from .auth()
274 # to RhodeCode database. If this function returns data
275 # then auth is correct.
276 start = time.time()
277 log.debug('Running plugin `%s` permissions check', plugin_id)
278
279 def perm_func():
280 """
281 This function is used internally in Cache of Beaker to calculate
282 Results
283 """
284 log.debug('auth: calculating permission access now...')
285 # check IP
286 inherit = user.inherit_default_permissions
287 ip_allowed = AuthUser.check_ip_allowed(
288 user.user_id, ip_addr, inherit_from_default=inherit)
289 if ip_allowed:
290 log.info('Access for IP:%s allowed', ip_addr)
291 else:
267 return False
292 return False
268
293
294 if action == 'push':
295 perms = ('repository.write', 'repository.admin')
296 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
297 return False
298
299 else:
300 # any other action need at least read permission
301 perms = (
302 'repository.read', 'repository.write', 'repository.admin')
303 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
304 return False
305
306 return True
307
308 if plugin_cache_active:
309 log.debug('Trying to fetch cached perms by %s', _perm_calc_hash[:6])
310 perm_result = cache_manager.get(
311 _perm_calc_hash, createfunc=perm_func)
269 else:
312 else:
270 # any other action need at least read permission
313 perm_result = perm_func()
271 if not HasPermissionAnyMiddleware('repository.read',
272 'repository.write',
273 'repository.admin')(user,
274 repo_name):
275 return False
276
314
277 return True
315 auth_time = time.time() - start
316 log.debug('Permissions for plugin `%s` completed in %.3fs, '
317 'expiration time of fetched cache %.1fs.',
318 plugin_id, auth_time, cache_ttl)
319
320 return perm_result
278
321
279 def _check_ssl(self, environ, start_response):
322 def _check_ssl(self, environ, start_response):
280 """
323 """
281 Checks the SSL check flag and returns False if SSL is not present
324 Checks the SSL check flag and returns False if SSL is not present
282 and required True otherwise
325 and required True otherwise
283 """
326 """
284 org_proto = environ['wsgi._org_proto']
327 org_proto = environ['wsgi._org_proto']
285 # check if we have SSL required ! if not it's a bad request !
328 # check if we have SSL required ! if not it's a bad request !
286 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
329 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
287 if require_ssl and org_proto == 'http':
330 if require_ssl and org_proto == 'http':
288 log.debug('proto is %s and SSL is required BAD REQUEST !',
331 log.debug('proto is %s and SSL is required BAD REQUEST !',
289 org_proto)
332 org_proto)
290 return False
333 return False
291 return True
334 return True
292
335
293 def __call__(self, environ, start_response):
336 def __call__(self, environ, start_response):
294 try:
337 try:
295 return self._handle_request(environ, start_response)
338 return self._handle_request(environ, start_response)
296 except Exception:
339 except Exception:
297 log.exception("Exception while handling request")
340 log.exception("Exception while handling request")
298 appenlight.track_exception(environ)
341 appenlight.track_exception(environ)
299 return HTTPInternalServerError()(environ, start_response)
342 return HTTPInternalServerError()(environ, start_response)
300 finally:
343 finally:
301 meta.Session.remove()
344 meta.Session.remove()
302
345
303 def _handle_request(self, environ, start_response):
346 def _handle_request(self, environ, start_response):
304
347
305 if not self._check_ssl(environ, start_response):
348 if not self._check_ssl(environ, start_response):
306 reason = ('SSL required, while RhodeCode was unable '
349 reason = ('SSL required, while RhodeCode was unable '
307 'to detect this as SSL request')
350 'to detect this as SSL request')
308 log.debug('User not allowed to proceed, %s', reason)
351 log.debug('User not allowed to proceed, %s', reason)
309 return HTTPNotAcceptable(reason)(environ, start_response)
352 return HTTPNotAcceptable(reason)(environ, start_response)
310
353
311 if not self.url_repo_name:
354 if not self.url_repo_name:
312 log.warning('Repository name is empty: %s', self.url_repo_name)
355 log.warning('Repository name is empty: %s', self.url_repo_name)
313 # failed to get repo name, we fail now
356 # failed to get repo name, we fail now
314 return HTTPNotFound()(environ, start_response)
357 return HTTPNotFound()(environ, start_response)
315 log.debug('Extracted repo name is %s', self.url_repo_name)
358 log.debug('Extracted repo name is %s', self.url_repo_name)
316
359
317 ip_addr = get_ip_addr(environ)
360 ip_addr = get_ip_addr(environ)
318 user_agent = get_user_agent(environ)
361 user_agent = get_user_agent(environ)
319 username = None
362 username = None
320
363
321 # skip passing error to error controller
364 # skip passing error to error controller
322 environ['pylons.status_code_redirect'] = True
365 environ['pylons.status_code_redirect'] = True
323
366
324 # ======================================================================
367 # ======================================================================
325 # GET ACTION PULL or PUSH
368 # GET ACTION PULL or PUSH
326 # ======================================================================
369 # ======================================================================
327 action = self._get_action(environ)
370 action = self._get_action(environ)
328
371
329 # ======================================================================
372 # ======================================================================
330 # Check if this is a request to a shadow repository of a pull request.
373 # Check if this is a request to a shadow repository of a pull request.
331 # In this case only pull action is allowed.
374 # In this case only pull action is allowed.
332 # ======================================================================
375 # ======================================================================
333 if self.is_shadow_repo and action != 'pull':
376 if self.is_shadow_repo and action != 'pull':
334 reason = 'Only pull action is allowed for shadow repositories.'
377 reason = 'Only pull action is allowed for shadow repositories.'
335 log.debug('User not allowed to proceed, %s', reason)
378 log.debug('User not allowed to proceed, %s', reason)
336 return HTTPNotAcceptable(reason)(environ, start_response)
379 return HTTPNotAcceptable(reason)(environ, start_response)
337
380
338 # Check if the shadow repo actually exists, in case someone refers
381 # Check if the shadow repo actually exists, in case someone refers
339 # to it, and it has been deleted because of successful merge.
382 # to it, and it has been deleted because of successful merge.
340 if self.is_shadow_repo and not self.is_shadow_repo_dir:
383 if self.is_shadow_repo and not self.is_shadow_repo_dir:
341 return HTTPNotFound()(environ, start_response)
384 return HTTPNotFound()(environ, start_response)
342
385
343 # ======================================================================
386 # ======================================================================
344 # CHECK ANONYMOUS PERMISSION
387 # CHECK ANONYMOUS PERMISSION
345 # ======================================================================
388 # ======================================================================
346 if action in ['pull', 'push']:
389 if action in ['pull', 'push']:
347 anonymous_user = User.get_default_user()
390 anonymous_user = User.get_default_user()
348 username = anonymous_user.username
391 username = anonymous_user.username
349 if anonymous_user.active:
392 if anonymous_user.active:
350 # ONLY check permissions if the user is activated
393 # ONLY check permissions if the user is activated
351 anonymous_perm = self._check_permission(
394 anonymous_perm = self._check_permission(
352 action, anonymous_user, self.acl_repo_name, ip_addr)
395 action, anonymous_user, self.acl_repo_name, ip_addr)
353 else:
396 else:
354 anonymous_perm = False
397 anonymous_perm = False
355
398
356 if not anonymous_user.active or not anonymous_perm:
399 if not anonymous_user.active or not anonymous_perm:
357 if not anonymous_user.active:
400 if not anonymous_user.active:
358 log.debug('Anonymous access is disabled, running '
401 log.debug('Anonymous access is disabled, running '
359 'authentication')
402 'authentication')
360
403
361 if not anonymous_perm:
404 if not anonymous_perm:
362 log.debug('Not enough credentials to access this '
405 log.debug('Not enough credentials to access this '
363 'repository as anonymous user')
406 'repository as anonymous user')
364
407
365 username = None
408 username = None
366 # ==============================================================
409 # ==============================================================
367 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
410 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
368 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
411 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
369 # ==============================================================
412 # ==============================================================
370
413
371 # try to auth based on environ, container auth methods
414 # try to auth based on environ, container auth methods
372 log.debug('Running PRE-AUTH for container based authentication')
415 log.debug('Running PRE-AUTH for container based authentication')
373 pre_auth = authenticate(
416 pre_auth = authenticate(
374 '', '', environ, VCS_TYPE, registry=self.registry,
417 '', '', environ, VCS_TYPE, registry=self.registry,
375 acl_repo_name=self.acl_repo_name)
418 acl_repo_name=self.acl_repo_name)
376 if pre_auth and pre_auth.get('username'):
419 if pre_auth and pre_auth.get('username'):
377 username = pre_auth['username']
420 username = pre_auth['username']
378 log.debug('PRE-AUTH got %s as username', username)
421 log.debug('PRE-AUTH got %s as username', username)
422 if pre_auth:
423 log.debug('PRE-AUTH successful from %s',
424 pre_auth.get('auth_data', {}).get('_plugin'))
379
425
380 # If not authenticated by the container, running basic auth
426 # If not authenticated by the container, running basic auth
381 # before inject the calling repo_name for special scope checks
427 # before inject the calling repo_name for special scope checks
382 self.authenticate.acl_repo_name = self.acl_repo_name
428 self.authenticate.acl_repo_name = self.acl_repo_name
429
430 plugin_cache_active, cache_ttl = False, 0
431 plugin = None
383 if not username:
432 if not username:
384 self.authenticate.realm = self.authenticate.get_rc_realm()
433 self.authenticate.realm = self.authenticate.get_rc_realm()
385
434
386 try:
435 try:
387 result = self.authenticate(environ)
436 auth_result = self.authenticate(environ)
388 except (UserCreationError, NotAllowedToCreateUserError) as e:
437 except (UserCreationError, NotAllowedToCreateUserError) as e:
389 log.error(e)
438 log.error(e)
390 reason = safe_str(e)
439 reason = safe_str(e)
391 return HTTPNotAcceptable(reason)(environ, start_response)
440 return HTTPNotAcceptable(reason)(environ, start_response)
392
441
393 if isinstance(result, str):
442 if isinstance(auth_result, dict):
394 AUTH_TYPE.update(environ, 'basic')
443 AUTH_TYPE.update(environ, 'basic')
395 REMOTE_USER.update(environ, result)
444 REMOTE_USER.update(environ, auth_result['username'])
396 username = result
445 username = auth_result['username']
446 plugin = auth_result.get('auth_data', {}).get('_plugin')
447 log.info(
448 'MAIN-AUTH successful for user `%s` from %s plugin',
449 username, plugin)
450
451 plugin_cache_active, cache_ttl = auth_result.get(
452 'auth_data', {}).get('_ttl_cache') or (False, 0)
397 else:
453 else:
398 return result.wsgi_application(environ, start_response)
454 return auth_result.wsgi_application(
455 environ, start_response)
456
399
457
400 # ==============================================================
458 # ==============================================================
401 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
459 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
402 # ==============================================================
460 # ==============================================================
403 user = User.get_by_username(username)
461 user = User.get_by_username(username)
404 if not self.valid_and_active_user(user):
462 if not self.valid_and_active_user(user):
405 return HTTPForbidden()(environ, start_response)
463 return HTTPForbidden()(environ, start_response)
406 username = user.username
464 username = user.username
407 user.update_lastactivity()
465 user.update_lastactivity()
408 meta.Session().commit()
466 meta.Session().commit()
409
467
410 # check user attributes for password change flag
468 # check user attributes for password change flag
411 user_obj = user
469 user_obj = user
412 if user_obj and user_obj.username != User.DEFAULT_USER and \
470 if user_obj and user_obj.username != User.DEFAULT_USER and \
413 user_obj.user_data.get('force_password_change'):
471 user_obj.user_data.get('force_password_change'):
414 reason = 'password change required'
472 reason = 'password change required'
415 log.debug('User not allowed to authenticate, %s', reason)
473 log.debug('User not allowed to authenticate, %s', reason)
416 return HTTPNotAcceptable(reason)(environ, start_response)
474 return HTTPNotAcceptable(reason)(environ, start_response)
417
475
418 # check permissions for this repository
476 # check permissions for this repository
419 perm = self._check_permission(
477 perm = self._check_permission(
420 action, user, self.acl_repo_name, ip_addr)
478 action, user, self.acl_repo_name, ip_addr,
479 plugin, plugin_cache_active, cache_ttl)
421 if not perm:
480 if not perm:
422 return HTTPForbidden()(environ, start_response)
481 return HTTPForbidden()(environ, start_response)
423
482
424 # extras are injected into UI object and later available
483 # extras are injected into UI object and later available
425 # in hooks executed by rhodecode
484 # in hooks executed by RhodeCode
426 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
485 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
427 extras = vcs_operation_context(
486 extras = vcs_operation_context(
428 environ, repo_name=self.acl_repo_name, username=username,
487 environ, repo_name=self.acl_repo_name, username=username,
429 action=action, scm=self.SCM, check_locking=check_locking,
488 action=action, scm=self.SCM, check_locking=check_locking,
430 is_shadow_repo=self.is_shadow_repo
489 is_shadow_repo=self.is_shadow_repo
431 )
490 )
432
491
433 # ======================================================================
492 # ======================================================================
434 # REQUEST HANDLING
493 # REQUEST HANDLING
435 # ======================================================================
494 # ======================================================================
436 repo_path = os.path.join(
495 repo_path = os.path.join(
437 safe_str(self.basepath), safe_str(self.vcs_repo_name))
496 safe_str(self.basepath), safe_str(self.vcs_repo_name))
438 log.debug('Repository path is %s', repo_path)
497 log.debug('Repository path is %s', repo_path)
439
498
440 fix_PATH()
499 fix_PATH()
441
500
442 log.info(
501 log.info(
443 '%s action on %s repo "%s" by "%s" from %s %s',
502 '%s action on %s repo "%s" by "%s" from %s %s',
444 action, self.SCM, safe_str(self.url_repo_name),
503 action, self.SCM, safe_str(self.url_repo_name),
445 safe_str(username), ip_addr, user_agent)
504 safe_str(username), ip_addr, user_agent)
446
505
447 return self._generate_vcs_response(
506 return self._generate_vcs_response(
448 environ, start_response, repo_path, extras, action)
507 environ, start_response, repo_path, extras, action)
449
508
450 @initialize_generator
509 @initialize_generator
451 def _generate_vcs_response(
510 def _generate_vcs_response(
452 self, environ, start_response, repo_path, extras, action):
511 self, environ, start_response, repo_path, extras, action):
453 """
512 """
454 Returns a generator for the response content.
513 Returns a generator for the response content.
455
514
456 This method is implemented as a generator, so that it can trigger
515 This method is implemented as a generator, so that it can trigger
457 the cache validation after all content sent back to the client. It
516 the cache validation after all content sent back to the client. It
458 also handles the locking exceptions which will be triggered when
517 also handles the locking exceptions which will be triggered when
459 the first chunk is produced by the underlying WSGI application.
518 the first chunk is produced by the underlying WSGI application.
460 """
519 """
461 callback_daemon, extras = self._prepare_callback_daemon(extras)
520 callback_daemon, extras = self._prepare_callback_daemon(extras)
462 config = self._create_config(extras, self.acl_repo_name)
521 config = self._create_config(extras, self.acl_repo_name)
463 log.debug('HOOKS extras is %s', extras)
522 log.debug('HOOKS extras is %s', extras)
464 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
523 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
465
524
466 try:
525 try:
467 with callback_daemon:
526 with callback_daemon:
468 try:
527 try:
469 response = app(environ, start_response)
528 response = app(environ, start_response)
470 finally:
529 finally:
471 # This statement works together with the decorator
530 # This statement works together with the decorator
472 # "initialize_generator" above. The decorator ensures that
531 # "initialize_generator" above. The decorator ensures that
473 # we hit the first yield statement before the generator is
532 # we hit the first yield statement before the generator is
474 # returned back to the WSGI server. This is needed to
533 # returned back to the WSGI server. This is needed to
475 # ensure that the call to "app" above triggers the
534 # ensure that the call to "app" above triggers the
476 # needed callback to "start_response" before the
535 # needed callback to "start_response" before the
477 # generator is actually used.
536 # generator is actually used.
478 yield "__init__"
537 yield "__init__"
479
538
480 for chunk in response:
539 for chunk in response:
481 yield chunk
540 yield chunk
482 except Exception as exc:
541 except Exception as exc:
483 # TODO: martinb: Exceptions are only raised in case of the Pyro4
542 # TODO: martinb: Exceptions are only raised in case of the Pyro4
484 # backend. Refactor this except block after dropping Pyro4 support.
543 # backend. Refactor this except block after dropping Pyro4 support.
485 # TODO: johbo: Improve "translating" back the exception.
544 # TODO: johbo: Improve "translating" back the exception.
486 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
545 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
487 exc = HTTPLockedRC(*exc.args)
546 exc = HTTPLockedRC(*exc.args)
488 _code = rhodecode.CONFIG.get('lock_ret_code')
547 _code = rhodecode.CONFIG.get('lock_ret_code')
489 log.debug('Repository LOCKED ret code %s!', (_code,))
548 log.debug('Repository LOCKED ret code %s!', (_code,))
490 elif getattr(exc, '_vcs_kind', None) == 'requirement':
549 elif getattr(exc, '_vcs_kind', None) == 'requirement':
491 log.debug(
550 log.debug(
492 'Repository requires features unknown to this Mercurial')
551 'Repository requires features unknown to this Mercurial')
493 exc = HTTPRequirementError(*exc.args)
552 exc = HTTPRequirementError(*exc.args)
494 else:
553 else:
495 raise
554 raise
496
555
497 for chunk in exc(environ, start_response):
556 for chunk in exc(environ, start_response):
498 yield chunk
557 yield chunk
499 finally:
558 finally:
500 # invalidate cache on push
559 # invalidate cache on push
501 try:
560 try:
502 if action == 'push':
561 if action == 'push':
503 self._invalidate_cache(self.url_repo_name)
562 self._invalidate_cache(self.url_repo_name)
504 finally:
563 finally:
505 meta.Session.remove()
564 meta.Session.remove()
506
565
507 def _get_repository_name(self, environ):
566 def _get_repository_name(self, environ):
508 """Get repository name out of the environmnent
567 """Get repository name out of the environmnent
509
568
510 :param environ: WSGI environment
569 :param environ: WSGI environment
511 """
570 """
512 raise NotImplementedError()
571 raise NotImplementedError()
513
572
514 def _get_action(self, environ):
573 def _get_action(self, environ):
515 """Map request commands into a pull or push command.
574 """Map request commands into a pull or push command.
516
575
517 :param environ: WSGI environment
576 :param environ: WSGI environment
518 """
577 """
519 raise NotImplementedError()
578 raise NotImplementedError()
520
579
521 def _create_wsgi_app(self, repo_path, repo_name, config):
580 def _create_wsgi_app(self, repo_path, repo_name, config):
522 """Return the WSGI app that will finally handle the request."""
581 """Return the WSGI app that will finally handle the request."""
523 raise NotImplementedError()
582 raise NotImplementedError()
524
583
525 def _create_config(self, extras, repo_name):
584 def _create_config(self, extras, repo_name):
526 """Create a safe config representation."""
585 """Create a safe config representation."""
527 raise NotImplementedError()
586 raise NotImplementedError()
528
587
529 def _prepare_callback_daemon(self, extras):
588 def _prepare_callback_daemon(self, extras):
530 return prepare_callback_daemon(
589 return prepare_callback_daemon(
531 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
590 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
532 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
591 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
533
592
534
593
535 def _should_check_locking(query_string):
594 def _should_check_locking(query_string):
536 # this is kind of hacky, but due to how mercurial handles client-server
595 # this is kind of hacky, but due to how mercurial handles client-server
537 # server see all operation on commit; bookmarks, phases and
596 # server see all operation on commit; bookmarks, phases and
538 # obsolescence marker in different transaction, we don't want to check
597 # obsolescence marker in different transaction, we don't want to check
539 # locking on those
598 # locking on those
540 return query_string not in ['cmd=listkeys']
599 return query_string not in ['cmd=listkeys']
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,183 +1,188 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 mock
21 import mock
22 import pytest
22 import pytest
23
23
24 from rhodecode.lib.auth import _RhodeCodeCryptoBCrypt
24 from rhodecode.lib.auth import _RhodeCodeCryptoBCrypt
25 from rhodecode.authentication.base import RhodeCodeAuthPluginBase
25 from rhodecode.authentication.base import RhodeCodeAuthPluginBase
26 from rhodecode.authentication.plugins.auth_ldap import RhodeCodeAuthPlugin
26 from rhodecode.authentication.plugins.auth_ldap import RhodeCodeAuthPlugin
27 from rhodecode.model import db
27 from rhodecode.model import db
28
28
29
29
30 class TestAuthPlugin(RhodeCodeAuthPluginBase):
31
32 def name(self):
33 return 'stub_auth'
34
30 def test_authenticate_returns_from_auth(stub_auth_data):
35 def test_authenticate_returns_from_auth(stub_auth_data):
31 plugin = RhodeCodeAuthPluginBase('stub_id')
36 plugin = TestAuthPlugin('stub_id')
32 with mock.patch.object(plugin, 'auth') as auth_mock:
37 with mock.patch.object(plugin, 'auth') as auth_mock:
33 auth_mock.return_value = stub_auth_data
38 auth_mock.return_value = stub_auth_data
34 result = plugin._authenticate(mock.Mock(), 'test', 'password', {})
39 result = plugin._authenticate(mock.Mock(), 'test', 'password', {})
35 assert stub_auth_data == result
40 assert stub_auth_data == result
36
41
37
42
38 def test_authenticate_returns_empty_auth_data():
43 def test_authenticate_returns_empty_auth_data():
39 auth_data = {}
44 auth_data = {}
40 plugin = RhodeCodeAuthPluginBase('stub_id')
45 plugin = TestAuthPlugin('stub_id')
41 with mock.patch.object(plugin, 'auth') as auth_mock:
46 with mock.patch.object(plugin, 'auth') as auth_mock:
42 auth_mock.return_value = auth_data
47 auth_mock.return_value = auth_data
43 result = plugin._authenticate(mock.Mock(), 'test', 'password', {})
48 result = plugin._authenticate(mock.Mock(), 'test', 'password', {})
44 assert auth_data == result
49 assert auth_data == result
45
50
46
51
47 def test_authenticate_skips_hash_migration_if_mismatch(stub_auth_data):
52 def test_authenticate_skips_hash_migration_if_mismatch(stub_auth_data):
48 stub_auth_data['_hash_migrate'] = 'new-hash'
53 stub_auth_data['_hash_migrate'] = 'new-hash'
49 plugin = RhodeCodeAuthPluginBase('stub_id')
54 plugin = TestAuthPlugin('stub_id')
50 with mock.patch.object(plugin, 'auth') as auth_mock:
55 with mock.patch.object(plugin, 'auth') as auth_mock:
51 auth_mock.return_value = stub_auth_data
56 auth_mock.return_value = stub_auth_data
52 result = plugin._authenticate(mock.Mock(), 'test', 'password', {})
57 result = plugin._authenticate(mock.Mock(), 'test', 'password', {})
53
58
54 user = db.User.get_by_username(stub_auth_data['username'])
59 user = db.User.get_by_username(stub_auth_data['username'])
55 assert user.password != 'new-hash'
60 assert user.password != 'new-hash'
56 assert result == stub_auth_data
61 assert result == stub_auth_data
57
62
58
63
59 def test_authenticate_migrates_to_new_hash(stub_auth_data):
64 def test_authenticate_migrates_to_new_hash(stub_auth_data):
60 new_password = b'new-password'
65 new_password = b'new-password'
61 new_hash = _RhodeCodeCryptoBCrypt().hash_create(new_password)
66 new_hash = _RhodeCodeCryptoBCrypt().hash_create(new_password)
62 stub_auth_data['_hash_migrate'] = new_hash
67 stub_auth_data['_hash_migrate'] = new_hash
63 plugin = RhodeCodeAuthPluginBase('stub_id')
68 plugin = TestAuthPlugin('stub_id')
64 with mock.patch.object(plugin, 'auth') as auth_mock:
69 with mock.patch.object(plugin, 'auth') as auth_mock:
65 auth_mock.return_value = stub_auth_data
70 auth_mock.return_value = stub_auth_data
66 result = plugin._authenticate(
71 result = plugin._authenticate(
67 mock.Mock(), stub_auth_data['username'], new_password, {})
72 mock.Mock(), stub_auth_data['username'], new_password, {})
68
73
69 user = db.User.get_by_username(stub_auth_data['username'])
74 user = db.User.get_by_username(stub_auth_data['username'])
70 assert user.password == new_hash
75 assert user.password == new_hash
71 assert result == stub_auth_data
76 assert result == stub_auth_data
72
77
73
78
74 @pytest.fixture
79 @pytest.fixture
75 def stub_auth_data(user_util):
80 def stub_auth_data(user_util):
76 user = user_util.create_user()
81 user = user_util.create_user()
77 data = {
82 data = {
78 'username': user.username,
83 'username': user.username,
79 'password': 'password',
84 'password': 'password',
80 'email': 'test@example.org',
85 'email': 'test@example.org',
81 'firstname': 'John',
86 'firstname': 'John',
82 'lastname': 'Smith',
87 'lastname': 'Smith',
83 'groups': [],
88 'groups': [],
84 'active': True,
89 'active': True,
85 'admin': False,
90 'admin': False,
86 'extern_name': 'test',
91 'extern_name': 'test',
87 'extern_type': 'ldap',
92 'extern_type': 'ldap',
88 'active_from_extern': True
93 'active_from_extern': True
89 }
94 }
90 return data
95 return data
91
96
92
97
93 class TestRhodeCodeAuthPlugin(object):
98 class TestRhodeCodeAuthPlugin(object):
94 def setup_method(self, method):
99 def setup_method(self, method):
95 self.finalizers = []
100 self.finalizers = []
96 self.user = mock.Mock()
101 self.user = mock.Mock()
97 self.user.username = 'test'
102 self.user.username = 'test'
98 self.user.password = 'old-password'
103 self.user.password = 'old-password'
99 self.fake_auth = {
104 self.fake_auth = {
100 'username': 'test',
105 'username': 'test',
101 'password': 'test',
106 'password': 'test',
102 'email': 'test@example.org',
107 'email': 'test@example.org',
103 'firstname': 'John',
108 'firstname': 'John',
104 'lastname': 'Smith',
109 'lastname': 'Smith',
105 'groups': [],
110 'groups': [],
106 'active': True,
111 'active': True,
107 'admin': False,
112 'admin': False,
108 'extern_name': 'test',
113 'extern_name': 'test',
109 'extern_type': 'ldap',
114 'extern_type': 'ldap',
110 'active_from_extern': True
115 'active_from_extern': True
111 }
116 }
112
117
113 def teardown_method(self, method):
118 def teardown_method(self, method):
114 if self.finalizers:
119 if self.finalizers:
115 for finalizer in self.finalizers:
120 for finalizer in self.finalizers:
116 finalizer()
121 finalizer()
117 self.finalizers = []
122 self.finalizers = []
118
123
119 def test_fake_password_is_created_for_the_new_user(self):
124 def test_fake_password_is_created_for_the_new_user(self):
120 self._patch()
125 self._patch()
121 auth_plugin = RhodeCodeAuthPlugin('stub_id')
126 auth_plugin = RhodeCodeAuthPlugin('stub_id')
122 auth_plugin._authenticate(self.user, 'test', 'test', [])
127 auth_plugin._authenticate(self.user, 'test', 'test', [])
123 self.password_generator_mock.assert_called_once_with(length=16)
128 self.password_generator_mock.assert_called_once_with(length=16)
124 create_user_kwargs = self.create_user_mock.call_args[1]
129 create_user_kwargs = self.create_user_mock.call_args[1]
125 assert create_user_kwargs['password'] == 'new-password'
130 assert create_user_kwargs['password'] == 'new-password'
126
131
127 def test_fake_password_is_not_created_for_the_existing_user(self):
132 def test_fake_password_is_not_created_for_the_existing_user(self):
128 self._patch()
133 self._patch()
129 self.get_user_mock.return_value = self.user
134 self.get_user_mock.return_value = self.user
130 auth_plugin = RhodeCodeAuthPlugin('stub_id')
135 auth_plugin = RhodeCodeAuthPlugin('stub_id')
131 auth_plugin._authenticate(self.user, 'test', 'test', [])
136 auth_plugin._authenticate(self.user, 'test', 'test', [])
132 assert self.password_generator_mock.called is False
137 assert self.password_generator_mock.called is False
133 create_user_kwargs = self.create_user_mock.call_args[1]
138 create_user_kwargs = self.create_user_mock.call_args[1]
134 assert create_user_kwargs['password'] == self.user.password
139 assert create_user_kwargs['password'] == self.user.password
135
140
136 def _patch(self):
141 def _patch(self):
137 get_user_patch = mock.patch('rhodecode.model.db.User.get_by_username')
142 get_user_patch = mock.patch('rhodecode.model.db.User.get_by_username')
138 self.get_user_mock = get_user_patch.start()
143 self.get_user_mock = get_user_patch.start()
139 self.get_user_mock.return_value = None
144 self.get_user_mock.return_value = None
140 self.finalizers.append(get_user_patch.stop)
145 self.finalizers.append(get_user_patch.stop)
141
146
142 create_user_patch = mock.patch(
147 create_user_patch = mock.patch(
143 'rhodecode.model.user.UserModel.create_or_update')
148 'rhodecode.model.user.UserModel.create_or_update')
144 self.create_user_mock = create_user_patch.start()
149 self.create_user_mock = create_user_patch.start()
145 self.create_user_mock.return_value = None
150 self.create_user_mock.return_value = None
146 self.finalizers.append(create_user_patch.stop)
151 self.finalizers.append(create_user_patch.stop)
147
152
148 auth_patch = mock.patch.object(RhodeCodeAuthPlugin, 'auth')
153 auth_patch = mock.patch.object(RhodeCodeAuthPlugin, 'auth')
149 self.auth_mock = auth_patch.start()
154 self.auth_mock = auth_patch.start()
150 self.auth_mock.return_value = self.fake_auth
155 self.auth_mock.return_value = self.fake_auth
151 self.finalizers.append(auth_patch.stop)
156 self.finalizers.append(auth_patch.stop)
152
157
153 password_generator_patch = mock.patch(
158 password_generator_patch = mock.patch(
154 'rhodecode.lib.auth.PasswordGenerator.gen_password')
159 'rhodecode.lib.auth.PasswordGenerator.gen_password')
155 self.password_generator_mock = password_generator_patch.start()
160 self.password_generator_mock = password_generator_patch.start()
156 self.password_generator_mock.return_value = 'new-password'
161 self.password_generator_mock.return_value = 'new-password'
157 self.finalizers.append(password_generator_patch.stop)
162 self.finalizers.append(password_generator_patch.stop)
158
163
159
164
160 def test_missing_ldap():
165 def test_missing_ldap():
161 from rhodecode.model.validators import Missing
166 from rhodecode.model.validators import Missing
162
167
163 try:
168 try:
164 import ldap_not_existing
169 import ldap_not_existing
165 except ImportError:
170 except ImportError:
166 # means that python-ldap is not installed
171 # means that python-ldap is not installed
167 ldap_not_existing = Missing
172 ldap_not_existing = Missing
168
173
169 # missing is singleton
174 # missing is singleton
170 assert ldap_not_existing == Missing
175 assert ldap_not_existing == Missing
171
176
172
177
173 def test_import_ldap():
178 def test_import_ldap():
174 from rhodecode.model.validators import Missing
179 from rhodecode.model.validators import Missing
175
180
176 try:
181 try:
177 import ldap
182 import ldap
178 except ImportError:
183 except ImportError:
179 # means that python-ldap is not installed
184 # means that python-ldap is not installed
180 ldap = Missing
185 ldap = Missing
181
186
182 # missing is singleton
187 # missing is singleton
183 assert False is (ldap == Missing)
188 assert False is (ldap == Missing)
General Comments 0
You need to be logged in to leave comments. Login now