##// END OF EJS Templates
authentication: enabled authentication with auth_token and repository scope....
marcink -
r1510:77606b4c default
parent child Browse files
Show More

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

@@ -1,649 +1,658 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 logging
26 import logging
27 import time
27 import time
28 import traceback
28 import traceback
29 import warnings
29 import warnings
30
30
31 from pyramid.threadlocal import get_current_registry
31 from pyramid.threadlocal import get_current_registry
32 from sqlalchemy.ext.hybrid import hybrid_property
32 from sqlalchemy.ext.hybrid import hybrid_property
33
33
34 from rhodecode.authentication.interface import IAuthnPluginRegistry
34 from rhodecode.authentication.interface import IAuthnPluginRegistry
35 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
35 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
36 from rhodecode.lib import caches
36 from rhodecode.lib import caches
37 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
37 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
38 from rhodecode.lib.utils2 import md5_safe, safe_int
38 from rhodecode.lib.utils2 import md5_safe, safe_int
39 from rhodecode.lib.utils2 import safe_str
39 from rhodecode.lib.utils2 import safe_str
40 from rhodecode.model.db import User
40 from rhodecode.model.db import User
41 from rhodecode.model.meta import Session
41 from rhodecode.model.meta import Session
42 from rhodecode.model.settings import SettingsModel
42 from rhodecode.model.settings import SettingsModel
43 from rhodecode.model.user import UserModel
43 from rhodecode.model.user import UserModel
44 from rhodecode.model.user_group import UserGroupModel
44 from rhodecode.model.user_group import UserGroupModel
45
45
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49 # auth types that authenticate() function can receive
49 # auth types that authenticate() function can receive
50 VCS_TYPE = 'vcs'
50 VCS_TYPE = 'vcs'
51 HTTP_TYPE = 'http'
51 HTTP_TYPE = 'http'
52
52
53
53
54 class LazyFormencode(object):
54 class LazyFormencode(object):
55 def __init__(self, formencode_obj, *args, **kwargs):
55 def __init__(self, formencode_obj, *args, **kwargs):
56 self.formencode_obj = formencode_obj
56 self.formencode_obj = formencode_obj
57 self.args = args
57 self.args = args
58 self.kwargs = kwargs
58 self.kwargs = kwargs
59
59
60 def __call__(self, *args, **kwargs):
60 def __call__(self, *args, **kwargs):
61 from inspect import isfunction
61 from inspect import isfunction
62 formencode_obj = self.formencode_obj
62 formencode_obj = self.formencode_obj
63 if isfunction(formencode_obj):
63 if isfunction(formencode_obj):
64 # case we wrap validators into functions
64 # case we wrap validators into functions
65 formencode_obj = self.formencode_obj(*args, **kwargs)
65 formencode_obj = self.formencode_obj(*args, **kwargs)
66 return formencode_obj(*self.args, **self.kwargs)
66 return formencode_obj(*self.args, **self.kwargs)
67
67
68
68
69 class RhodeCodeAuthPluginBase(object):
69 class RhodeCodeAuthPluginBase(object):
70 # cache the authentication request for N amount of seconds. Some kind
70 # cache the authentication request for N amount of seconds. Some kind
71 # of authentication methods are very heavy and it's very efficient to cache
71 # of authentication methods are very heavy and it's very efficient to cache
72 # the result of a call. If it's set to None (default) cache is off
72 # the result of a call. If it's set to None (default) cache is off
73 AUTH_CACHE_TTL = None
73 AUTH_CACHE_TTL = None
74 AUTH_CACHE = {}
74 AUTH_CACHE = {}
75
75
76 auth_func_attrs = {
76 auth_func_attrs = {
77 "username": "unique username",
77 "username": "unique username",
78 "firstname": "first name",
78 "firstname": "first name",
79 "lastname": "last name",
79 "lastname": "last name",
80 "email": "email address",
80 "email": "email address",
81 "groups": '["list", "of", "groups"]',
81 "groups": '["list", "of", "groups"]',
82 "extern_name": "name in external source of record",
82 "extern_name": "name in external source of record",
83 "extern_type": "type of external source of record",
83 "extern_type": "type of external source of record",
84 "admin": 'True|False defines if user should be RhodeCode super admin',
84 "admin": 'True|False defines if user should be RhodeCode super admin',
85 "active":
85 "active":
86 'True|False defines active state of user internally for RhodeCode',
86 'True|False defines active state of user internally for RhodeCode',
87 "active_from_extern":
87 "active_from_extern":
88 "True|False\None, active state from the external auth, "
88 "True|False\None, active state from the external auth, "
89 "None means use definition from RhodeCode extern_type active value"
89 "None means use definition from RhodeCode extern_type active value"
90 }
90 }
91 # set on authenticate() method and via set_auth_type func.
91 # set on authenticate() method and via set_auth_type func.
92 auth_type = None
92 auth_type = None
93
93
94 # set on authenticate() method and via set_calling_scope_repo, this is a
95 # calling scope repository when doing authentication most likely on VCS
96 # operations
97 acl_repo_name = None
98
94 # List of setting names to store encrypted. Plugins may override this list
99 # List of setting names to store encrypted. Plugins may override this list
95 # to store settings encrypted.
100 # to store settings encrypted.
96 _settings_encrypted = []
101 _settings_encrypted = []
97
102
98 # Mapping of python to DB settings model types. Plugins may override or
103 # Mapping of python to DB settings model types. Plugins may override or
99 # extend this mapping.
104 # extend this mapping.
100 _settings_type_map = {
105 _settings_type_map = {
101 colander.String: 'unicode',
106 colander.String: 'unicode',
102 colander.Integer: 'int',
107 colander.Integer: 'int',
103 colander.Boolean: 'bool',
108 colander.Boolean: 'bool',
104 colander.List: 'list',
109 colander.List: 'list',
105 }
110 }
106
111
107 def __init__(self, plugin_id):
112 def __init__(self, plugin_id):
108 self._plugin_id = plugin_id
113 self._plugin_id = plugin_id
109
114
110 def __str__(self):
115 def __str__(self):
111 return self.get_id()
116 return self.get_id()
112
117
113 def _get_setting_full_name(self, name):
118 def _get_setting_full_name(self, name):
114 """
119 """
115 Return the full setting name used for storing values in the database.
120 Return the full setting name used for storing values in the database.
116 """
121 """
117 # TODO: johbo: Using the name here is problematic. It would be good to
122 # TODO: johbo: Using the name here is problematic. It would be good to
118 # introduce either new models in the database to hold Plugin and
123 # introduce either new models in the database to hold Plugin and
119 # PluginSetting or to use the plugin id here.
124 # PluginSetting or to use the plugin id here.
120 return 'auth_{}_{}'.format(self.name, name)
125 return 'auth_{}_{}'.format(self.name, name)
121
126
122 def _get_setting_type(self, name):
127 def _get_setting_type(self, name):
123 """
128 """
124 Return the type of a setting. This type is defined by the SettingsModel
129 Return the type of a setting. This type is defined by the SettingsModel
125 and determines how the setting is stored in DB. Optionally the suffix
130 and determines how the setting is stored in DB. Optionally the suffix
126 `.encrypted` is appended to instruct SettingsModel to store it
131 `.encrypted` is appended to instruct SettingsModel to store it
127 encrypted.
132 encrypted.
128 """
133 """
129 schema_node = self.get_settings_schema().get(name)
134 schema_node = self.get_settings_schema().get(name)
130 db_type = self._settings_type_map.get(
135 db_type = self._settings_type_map.get(
131 type(schema_node.typ), 'unicode')
136 type(schema_node.typ), 'unicode')
132 if name in self._settings_encrypted:
137 if name in self._settings_encrypted:
133 db_type = '{}.encrypted'.format(db_type)
138 db_type = '{}.encrypted'.format(db_type)
134 return db_type
139 return db_type
135
140
136 def is_enabled(self):
141 def is_enabled(self):
137 """
142 """
138 Returns true if this plugin is enabled. An enabled plugin can be
143 Returns true if this plugin is enabled. An enabled plugin can be
139 configured in the admin interface but it is not consulted during
144 configured in the admin interface but it is not consulted during
140 authentication.
145 authentication.
141 """
146 """
142 auth_plugins = SettingsModel().get_auth_plugins()
147 auth_plugins = SettingsModel().get_auth_plugins()
143 return self.get_id() in auth_plugins
148 return self.get_id() in auth_plugins
144
149
145 def is_active(self):
150 def is_active(self):
146 """
151 """
147 Returns true if the plugin is activated. An activated plugin is
152 Returns true if the plugin is activated. An activated plugin is
148 consulted during authentication, assumed it is also enabled.
153 consulted during authentication, assumed it is also enabled.
149 """
154 """
150 return self.get_setting_by_name('enabled')
155 return self.get_setting_by_name('enabled')
151
156
152 def get_id(self):
157 def get_id(self):
153 """
158 """
154 Returns the plugin id.
159 Returns the plugin id.
155 """
160 """
156 return self._plugin_id
161 return self._plugin_id
157
162
158 def get_display_name(self):
163 def get_display_name(self):
159 """
164 """
160 Returns a translation string for displaying purposes.
165 Returns a translation string for displaying purposes.
161 """
166 """
162 raise NotImplementedError('Not implemented in base class')
167 raise NotImplementedError('Not implemented in base class')
163
168
164 def get_settings_schema(self):
169 def get_settings_schema(self):
165 """
170 """
166 Returns a colander schema, representing the plugin settings.
171 Returns a colander schema, representing the plugin settings.
167 """
172 """
168 return AuthnPluginSettingsSchemaBase()
173 return AuthnPluginSettingsSchemaBase()
169
174
170 def get_setting_by_name(self, name, default=None):
175 def get_setting_by_name(self, name, default=None):
171 """
176 """
172 Returns a plugin setting by name.
177 Returns a plugin setting by name.
173 """
178 """
174 full_name = self._get_setting_full_name(name)
179 full_name = self._get_setting_full_name(name)
175 db_setting = SettingsModel().get_setting_by_name(full_name)
180 db_setting = SettingsModel().get_setting_by_name(full_name)
176 return db_setting.app_settings_value if db_setting else default
181 return db_setting.app_settings_value if db_setting else default
177
182
178 def create_or_update_setting(self, name, value):
183 def create_or_update_setting(self, name, value):
179 """
184 """
180 Create or update a setting for this plugin in the persistent storage.
185 Create or update a setting for this plugin in the persistent storage.
181 """
186 """
182 full_name = self._get_setting_full_name(name)
187 full_name = self._get_setting_full_name(name)
183 type_ = self._get_setting_type(name)
188 type_ = self._get_setting_type(name)
184 db_setting = SettingsModel().create_or_update_setting(
189 db_setting = SettingsModel().create_or_update_setting(
185 full_name, value, type_)
190 full_name, value, type_)
186 return db_setting.app_settings_value
191 return db_setting.app_settings_value
187
192
188 def get_settings(self):
193 def get_settings(self):
189 """
194 """
190 Returns the plugin settings as dictionary.
195 Returns the plugin settings as dictionary.
191 """
196 """
192 settings = {}
197 settings = {}
193 for node in self.get_settings_schema():
198 for node in self.get_settings_schema():
194 settings[node.name] = self.get_setting_by_name(node.name)
199 settings[node.name] = self.get_setting_by_name(node.name)
195 return settings
200 return settings
196
201
197 @property
202 @property
198 def validators(self):
203 def validators(self):
199 """
204 """
200 Exposes RhodeCode validators modules
205 Exposes RhodeCode validators modules
201 """
206 """
202 # this is a hack to overcome issues with pylons threadlocals and
207 # this is a hack to overcome issues with pylons threadlocals and
203 # translator object _() not beein registered properly.
208 # translator object _() not beein registered properly.
204 class LazyCaller(object):
209 class LazyCaller(object):
205 def __init__(self, name):
210 def __init__(self, name):
206 self.validator_name = name
211 self.validator_name = name
207
212
208 def __call__(self, *args, **kwargs):
213 def __call__(self, *args, **kwargs):
209 from rhodecode.model import validators as v
214 from rhodecode.model import validators as v
210 obj = getattr(v, self.validator_name)
215 obj = getattr(v, self.validator_name)
211 # log.debug('Initializing lazy formencode object: %s', obj)
216 # log.debug('Initializing lazy formencode object: %s', obj)
212 return LazyFormencode(obj, *args, **kwargs)
217 return LazyFormencode(obj, *args, **kwargs)
213
218
214 class ProxyGet(object):
219 class ProxyGet(object):
215 def __getattribute__(self, name):
220 def __getattribute__(self, name):
216 return LazyCaller(name)
221 return LazyCaller(name)
217
222
218 return ProxyGet()
223 return ProxyGet()
219
224
220 @hybrid_property
225 @hybrid_property
221 def name(self):
226 def name(self):
222 """
227 """
223 Returns the name of this authentication plugin.
228 Returns the name of this authentication plugin.
224
229
225 :returns: string
230 :returns: string
226 """
231 """
227 raise NotImplementedError("Not implemented in base class")
232 raise NotImplementedError("Not implemented in base class")
228
233
229 def get_url_slug(self):
234 def get_url_slug(self):
230 """
235 """
231 Returns a slug which should be used when constructing URLs which refer
236 Returns a slug which should be used when constructing URLs which refer
232 to this plugin. By default it returns the plugin name. If the name is
237 to this plugin. By default it returns the plugin name. If the name is
233 not suitable for using it in an URL the plugin should override this
238 not suitable for using it in an URL the plugin should override this
234 method.
239 method.
235 """
240 """
236 return self.name
241 return self.name
237
242
238 @property
243 @property
239 def is_headers_auth(self):
244 def is_headers_auth(self):
240 """
245 """
241 Returns True if this authentication plugin uses HTTP headers as
246 Returns True if this authentication plugin uses HTTP headers as
242 authentication method.
247 authentication method.
243 """
248 """
244 return False
249 return False
245
250
246 @hybrid_property
251 @hybrid_property
247 def is_container_auth(self):
252 def is_container_auth(self):
248 """
253 """
249 Deprecated method that indicates if this authentication plugin uses
254 Deprecated method that indicates if this authentication plugin uses
250 HTTP headers as authentication method.
255 HTTP headers as authentication method.
251 """
256 """
252 warnings.warn(
257 warnings.warn(
253 'Use is_headers_auth instead.', category=DeprecationWarning)
258 'Use is_headers_auth instead.', category=DeprecationWarning)
254 return self.is_headers_auth
259 return self.is_headers_auth
255
260
256 @hybrid_property
261 @hybrid_property
257 def allows_creating_users(self):
262 def allows_creating_users(self):
258 """
263 """
259 Defines if Plugin allows users to be created on-the-fly when
264 Defines if Plugin allows users to be created on-the-fly when
260 authentication is called. Controls how external plugins should behave
265 authentication is called. Controls how external plugins should behave
261 in terms if they are allowed to create new users, or not. Base plugins
266 in terms if they are allowed to create new users, or not. Base plugins
262 should not be allowed to, but External ones should be !
267 should not be allowed to, but External ones should be !
263
268
264 :return: bool
269 :return: bool
265 """
270 """
266 return False
271 return False
267
272
268 def set_auth_type(self, auth_type):
273 def set_auth_type(self, auth_type):
269 self.auth_type = auth_type
274 self.auth_type = auth_type
270
275
276 def set_calling_scope_repo(self, acl_repo_name):
277 self.acl_repo_name = acl_repo_name
278
271 def allows_authentication_from(
279 def allows_authentication_from(
272 self, user, allows_non_existing_user=True,
280 self, user, allows_non_existing_user=True,
273 allowed_auth_plugins=None, allowed_auth_sources=None):
281 allowed_auth_plugins=None, allowed_auth_sources=None):
274 """
282 """
275 Checks if this authentication module should accept a request for
283 Checks if this authentication module should accept a request for
276 the current user.
284 the current user.
277
285
278 :param user: user object fetched using plugin's get_user() method.
286 :param user: user object fetched using plugin's get_user() method.
279 :param allows_non_existing_user: if True, don't allow the
287 :param allows_non_existing_user: if True, don't allow the
280 user to be empty, meaning not existing in our database
288 user to be empty, meaning not existing in our database
281 :param allowed_auth_plugins: if provided, users extern_type will be
289 :param allowed_auth_plugins: if provided, users extern_type will be
282 checked against a list of provided extern types, which are plugin
290 checked against a list of provided extern types, which are plugin
283 auth_names in the end
291 auth_names in the end
284 :param allowed_auth_sources: authentication type allowed,
292 :param allowed_auth_sources: authentication type allowed,
285 `http` or `vcs` default is both.
293 `http` or `vcs` default is both.
286 defines if plugin will accept only http authentication vcs
294 defines if plugin will accept only http authentication vcs
287 authentication(git/hg) or both
295 authentication(git/hg) or both
288 :returns: boolean
296 :returns: boolean
289 """
297 """
290 if not user and not allows_non_existing_user:
298 if not user and not allows_non_existing_user:
291 log.debug('User is empty but plugin does not allow empty users,'
299 log.debug('User is empty but plugin does not allow empty users,'
292 'not allowed to authenticate')
300 'not allowed to authenticate')
293 return False
301 return False
294
302
295 expected_auth_plugins = allowed_auth_plugins or [self.name]
303 expected_auth_plugins = allowed_auth_plugins or [self.name]
296 if user and (user.extern_type and
304 if user and (user.extern_type and
297 user.extern_type not in expected_auth_plugins):
305 user.extern_type not in expected_auth_plugins):
298 log.debug(
306 log.debug(
299 'User `%s` is bound to `%s` auth type. Plugin allows only '
307 'User `%s` is bound to `%s` auth type. Plugin allows only '
300 '%s, skipping', user, user.extern_type, expected_auth_plugins)
308 '%s, skipping', user, user.extern_type, expected_auth_plugins)
301
309
302 return False
310 return False
303
311
304 # by default accept both
312 # by default accept both
305 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
313 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
306 if self.auth_type not in expected_auth_from:
314 if self.auth_type not in expected_auth_from:
307 log.debug('Current auth source is %s but plugin only allows %s',
315 log.debug('Current auth source is %s but plugin only allows %s',
308 self.auth_type, expected_auth_from)
316 self.auth_type, expected_auth_from)
309 return False
317 return False
310
318
311 return True
319 return True
312
320
313 def get_user(self, username=None, **kwargs):
321 def get_user(self, username=None, **kwargs):
314 """
322 """
315 Helper method for user fetching in plugins, by default it's using
323 Helper method for user fetching in plugins, by default it's using
316 simple fetch by username, but this method can be custimized in plugins
324 simple fetch by username, but this method can be custimized in plugins
317 eg. headers auth plugin to fetch user by environ params
325 eg. headers auth plugin to fetch user by environ params
318
326
319 :param username: username if given to fetch from database
327 :param username: username if given to fetch from database
320 :param kwargs: extra arguments needed for user fetching.
328 :param kwargs: extra arguments needed for user fetching.
321 """
329 """
322 user = None
330 user = None
323 log.debug(
331 log.debug(
324 'Trying to fetch user `%s` from RhodeCode database', username)
332 'Trying to fetch user `%s` from RhodeCode database', username)
325 if username:
333 if username:
326 user = User.get_by_username(username)
334 user = User.get_by_username(username)
327 if not user:
335 if not user:
328 log.debug('User not found, fallback to fetch user in '
336 log.debug('User not found, fallback to fetch user in '
329 'case insensitive mode')
337 'case insensitive mode')
330 user = User.get_by_username(username, case_insensitive=True)
338 user = User.get_by_username(username, case_insensitive=True)
331 else:
339 else:
332 log.debug('provided username:`%s` is empty skipping...', username)
340 log.debug('provided username:`%s` is empty skipping...', username)
333 if not user:
341 if not user:
334 log.debug('User `%s` not found in database', username)
342 log.debug('User `%s` not found in database', username)
335 else:
343 else:
336 log.debug('Got DB user:%s', user)
344 log.debug('Got DB user:%s', user)
337 return user
345 return user
338
346
339 def user_activation_state(self):
347 def user_activation_state(self):
340 """
348 """
341 Defines user activation state when creating new users
349 Defines user activation state when creating new users
342
350
343 :returns: boolean
351 :returns: boolean
344 """
352 """
345 raise NotImplementedError("Not implemented in base class")
353 raise NotImplementedError("Not implemented in base class")
346
354
347 def auth(self, userobj, username, passwd, settings, **kwargs):
355 def auth(self, userobj, username, passwd, settings, **kwargs):
348 """
356 """
349 Given a user object (which may be null), username, a plaintext
357 Given a user object (which may be null), username, a plaintext
350 password, and a settings object (containing all the keys needed as
358 password, and a settings object (containing all the keys needed as
351 listed in settings()), authenticate this user's login attempt.
359 listed in settings()), authenticate this user's login attempt.
352
360
353 Return None on failure. On success, return a dictionary of the form:
361 Return None on failure. On success, return a dictionary of the form:
354
362
355 see: RhodeCodeAuthPluginBase.auth_func_attrs
363 see: RhodeCodeAuthPluginBase.auth_func_attrs
356 This is later validated for correctness
364 This is later validated for correctness
357 """
365 """
358 raise NotImplementedError("not implemented in base class")
366 raise NotImplementedError("not implemented in base class")
359
367
360 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
368 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
361 """
369 """
362 Wrapper to call self.auth() that validates call on it
370 Wrapper to call self.auth() that validates call on it
363
371
364 :param userobj: userobj
372 :param userobj: userobj
365 :param username: username
373 :param username: username
366 :param passwd: plaintext password
374 :param passwd: plaintext password
367 :param settings: plugin settings
375 :param settings: plugin settings
368 """
376 """
369 auth = self.auth(userobj, username, passwd, settings, **kwargs)
377 auth = self.auth(userobj, username, passwd, settings, **kwargs)
370 if auth:
378 if auth:
371 # check if hash should be migrated ?
379 # check if hash should be migrated ?
372 new_hash = auth.get('_hash_migrate')
380 new_hash = auth.get('_hash_migrate')
373 if new_hash:
381 if new_hash:
374 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
382 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
375 return self._validate_auth_return(auth)
383 return self._validate_auth_return(auth)
376 return auth
384 return auth
377
385
378 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
386 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
379 new_hash_cypher = _RhodeCodeCryptoBCrypt()
387 new_hash_cypher = _RhodeCodeCryptoBCrypt()
380 # extra checks, so make sure new hash is correct.
388 # extra checks, so make sure new hash is correct.
381 password_encoded = safe_str(password)
389 password_encoded = safe_str(password)
382 if new_hash and new_hash_cypher.hash_check(
390 if new_hash and new_hash_cypher.hash_check(
383 password_encoded, new_hash):
391 password_encoded, new_hash):
384 cur_user = User.get_by_username(username)
392 cur_user = User.get_by_username(username)
385 cur_user.password = new_hash
393 cur_user.password = new_hash
386 Session().add(cur_user)
394 Session().add(cur_user)
387 Session().flush()
395 Session().flush()
388 log.info('Migrated user %s hash to bcrypt', cur_user)
396 log.info('Migrated user %s hash to bcrypt', cur_user)
389
397
390 def _validate_auth_return(self, ret):
398 def _validate_auth_return(self, ret):
391 if not isinstance(ret, dict):
399 if not isinstance(ret, dict):
392 raise Exception('returned value from auth must be a dict')
400 raise Exception('returned value from auth must be a dict')
393 for k in self.auth_func_attrs:
401 for k in self.auth_func_attrs:
394 if k not in ret:
402 if k not in ret:
395 raise Exception('Missing %s attribute from returned data' % k)
403 raise Exception('Missing %s attribute from returned data' % k)
396 return ret
404 return ret
397
405
398
406
399 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
407 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
400
408
401 @hybrid_property
409 @hybrid_property
402 def allows_creating_users(self):
410 def allows_creating_users(self):
403 return True
411 return True
404
412
405 def use_fake_password(self):
413 def use_fake_password(self):
406 """
414 """
407 Return a boolean that indicates whether or not we should set the user's
415 Return a boolean that indicates whether or not we should set the user's
408 password to a random value when it is authenticated by this plugin.
416 password to a random value when it is authenticated by this plugin.
409 If your plugin provides authentication, then you will generally
417 If your plugin provides authentication, then you will generally
410 want this.
418 want this.
411
419
412 :returns: boolean
420 :returns: boolean
413 """
421 """
414 raise NotImplementedError("Not implemented in base class")
422 raise NotImplementedError("Not implemented in base class")
415
423
416 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
424 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
417 # at this point _authenticate calls plugin's `auth()` function
425 # at this point _authenticate calls plugin's `auth()` function
418 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
426 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
419 userobj, username, passwd, settings, **kwargs)
427 userobj, username, passwd, settings, **kwargs)
420 if auth:
428 if auth:
421 # maybe plugin will clean the username ?
429 # maybe plugin will clean the username ?
422 # we should use the return value
430 # we should use the return value
423 username = auth['username']
431 username = auth['username']
424
432
425 # if external source tells us that user is not active, we should
433 # if external source tells us that user is not active, we should
426 # skip rest of the process. This can prevent from creating users in
434 # skip rest of the process. This can prevent from creating users in
427 # RhodeCode when using external authentication, but if it's
435 # RhodeCode when using external authentication, but if it's
428 # inactive user we shouldn't create that user anyway
436 # inactive user we shouldn't create that user anyway
429 if auth['active_from_extern'] is False:
437 if auth['active_from_extern'] is False:
430 log.warning(
438 log.warning(
431 "User %s authenticated against %s, but is inactive",
439 "User %s authenticated against %s, but is inactive",
432 username, self.__module__)
440 username, self.__module__)
433 return None
441 return None
434
442
435 cur_user = User.get_by_username(username, case_insensitive=True)
443 cur_user = User.get_by_username(username, case_insensitive=True)
436 is_user_existing = cur_user is not None
444 is_user_existing = cur_user is not None
437
445
438 if is_user_existing:
446 if is_user_existing:
439 log.debug('Syncing user `%s` from '
447 log.debug('Syncing user `%s` from '
440 '`%s` plugin', username, self.name)
448 '`%s` plugin', username, self.name)
441 else:
449 else:
442 log.debug('Creating non existing user `%s` from '
450 log.debug('Creating non existing user `%s` from '
443 '`%s` plugin', username, self.name)
451 '`%s` plugin', username, self.name)
444
452
445 if self.allows_creating_users:
453 if self.allows_creating_users:
446 log.debug('Plugin `%s` allows to '
454 log.debug('Plugin `%s` allows to '
447 'create new users', self.name)
455 'create new users', self.name)
448 else:
456 else:
449 log.debug('Plugin `%s` does not allow to '
457 log.debug('Plugin `%s` does not allow to '
450 'create new users', self.name)
458 'create new users', self.name)
451
459
452 user_parameters = {
460 user_parameters = {
453 'username': username,
461 'username': username,
454 'email': auth["email"],
462 'email': auth["email"],
455 'firstname': auth["firstname"],
463 'firstname': auth["firstname"],
456 'lastname': auth["lastname"],
464 'lastname': auth["lastname"],
457 'active': auth["active"],
465 'active': auth["active"],
458 'admin': auth["admin"],
466 'admin': auth["admin"],
459 'extern_name': auth["extern_name"],
467 'extern_name': auth["extern_name"],
460 'extern_type': self.name,
468 'extern_type': self.name,
461 'plugin': self,
469 'plugin': self,
462 'allow_to_create_user': self.allows_creating_users,
470 'allow_to_create_user': self.allows_creating_users,
463 }
471 }
464
472
465 if not is_user_existing:
473 if not is_user_existing:
466 if self.use_fake_password():
474 if self.use_fake_password():
467 # Randomize the PW because we don't need it, but don't want
475 # Randomize the PW because we don't need it, but don't want
468 # them blank either
476 # them blank either
469 passwd = PasswordGenerator().gen_password(length=16)
477 passwd = PasswordGenerator().gen_password(length=16)
470 user_parameters['password'] = passwd
478 user_parameters['password'] = passwd
471 else:
479 else:
472 # Since the password is required by create_or_update method of
480 # Since the password is required by create_or_update method of
473 # UserModel, we need to set it explicitly.
481 # UserModel, we need to set it explicitly.
474 # The create_or_update method is smart and recognises the
482 # The create_or_update method is smart and recognises the
475 # password hashes as well.
483 # password hashes as well.
476 user_parameters['password'] = cur_user.password
484 user_parameters['password'] = cur_user.password
477
485
478 # we either create or update users, we also pass the flag
486 # we either create or update users, we also pass the flag
479 # that controls if this method can actually do that.
487 # that controls if this method can actually do that.
480 # raises NotAllowedToCreateUserError if it cannot, and we try to.
488 # raises NotAllowedToCreateUserError if it cannot, and we try to.
481 user = UserModel().create_or_update(**user_parameters)
489 user = UserModel().create_or_update(**user_parameters)
482 Session().flush()
490 Session().flush()
483 # enforce user is just in given groups, all of them has to be ones
491 # enforce user is just in given groups, all of them has to be ones
484 # created from plugins. We store this info in _group_data JSON
492 # created from plugins. We store this info in _group_data JSON
485 # field
493 # field
486 try:
494 try:
487 groups = auth['groups'] or []
495 groups = auth['groups'] or []
488 UserGroupModel().enforce_groups(user, groups, self.name)
496 UserGroupModel().enforce_groups(user, groups, self.name)
489 except Exception:
497 except Exception:
490 # for any reason group syncing fails, we should
498 # for any reason group syncing fails, we should
491 # proceed with login
499 # proceed with login
492 log.error(traceback.format_exc())
500 log.error(traceback.format_exc())
493 Session().commit()
501 Session().commit()
494 return auth
502 return auth
495
503
496
504
497 def loadplugin(plugin_id):
505 def loadplugin(plugin_id):
498 """
506 """
499 Loads and returns an instantiated authentication plugin.
507 Loads and returns an instantiated authentication plugin.
500 Returns the RhodeCodeAuthPluginBase subclass on success,
508 Returns the RhodeCodeAuthPluginBase subclass on success,
501 or None on failure.
509 or None on failure.
502 """
510 """
503 # TODO: Disusing pyramids thread locals to retrieve the registry.
511 # TODO: Disusing pyramids thread locals to retrieve the registry.
504 authn_registry = get_authn_registry()
512 authn_registry = get_authn_registry()
505 plugin = authn_registry.get_plugin(plugin_id)
513 plugin = authn_registry.get_plugin(plugin_id)
506 if plugin is None:
514 if plugin is None:
507 log.error('Authentication plugin not found: "%s"', plugin_id)
515 log.error('Authentication plugin not found: "%s"', plugin_id)
508 return plugin
516 return plugin
509
517
510
518
511 def get_authn_registry(registry=None):
519 def get_authn_registry(registry=None):
512 registry = registry or get_current_registry()
520 registry = registry or get_current_registry()
513 authn_registry = registry.getUtility(IAuthnPluginRegistry)
521 authn_registry = registry.getUtility(IAuthnPluginRegistry)
514 return authn_registry
522 return authn_registry
515
523
516
524
517 def get_auth_cache_manager(custom_ttl=None):
525 def get_auth_cache_manager(custom_ttl=None):
518 return caches.get_cache_manager(
526 return caches.get_cache_manager(
519 'auth_plugins', 'rhodecode.authentication', custom_ttl)
527 'auth_plugins', 'rhodecode.authentication', custom_ttl)
520
528
521
529
522 def authenticate(username, password, environ=None, auth_type=None,
530 def authenticate(username, password, environ=None, auth_type=None,
523 skip_missing=False, registry=None):
531 skip_missing=False, registry=None, acl_repo_name=None):
524 """
532 """
525 Authentication function used for access control,
533 Authentication function used for access control,
526 It tries to authenticate based on enabled authentication modules.
534 It tries to authenticate based on enabled authentication modules.
527
535
528 :param username: username can be empty for headers auth
536 :param username: username can be empty for headers auth
529 :param password: password can be empty for headers auth
537 :param password: password can be empty for headers auth
530 :param environ: environ headers passed for headers auth
538 :param environ: environ headers passed for headers auth
531 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
539 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
532 :param skip_missing: ignores plugins that are in db but not in environment
540 :param skip_missing: ignores plugins that are in db but not in environment
533 :returns: None if auth failed, plugin_user dict if auth is correct
541 :returns: None if auth failed, plugin_user dict if auth is correct
534 """
542 """
535 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
543 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
536 raise ValueError('auth type must be on of http, vcs got "%s" instead'
544 raise ValueError('auth type must be on of http, vcs got "%s" instead'
537 % auth_type)
545 % auth_type)
538 headers_only = environ and not (username and password)
546 headers_only = environ and not (username and password)
539
547
540 authn_registry = get_authn_registry(registry)
548 authn_registry = get_authn_registry(registry)
541 for plugin in authn_registry.get_plugins_for_authentication():
549 for plugin in authn_registry.get_plugins_for_authentication():
542 plugin.set_auth_type(auth_type)
550 plugin.set_auth_type(auth_type)
551 plugin.set_calling_scope_repo(acl_repo_name)
543 user = plugin.get_user(username)
552 user = plugin.get_user(username)
544 display_user = user.username if user else username
553 display_user = user.username if user else username
545
554
546 if headers_only and not plugin.is_headers_auth:
555 if headers_only and not plugin.is_headers_auth:
547 log.debug('Auth type is for headers only and plugin `%s` is not '
556 log.debug('Auth type is for headers only and plugin `%s` is not '
548 'headers plugin, skipping...', plugin.get_id())
557 'headers plugin, skipping...', plugin.get_id())
549 continue
558 continue
550
559
551 # load plugin settings from RhodeCode database
560 # load plugin settings from RhodeCode database
552 plugin_settings = plugin.get_settings()
561 plugin_settings = plugin.get_settings()
553 log.debug('Plugin settings:%s', plugin_settings)
562 log.debug('Plugin settings:%s', plugin_settings)
554
563
555 log.debug('Trying authentication using ** %s **', plugin.get_id())
564 log.debug('Trying authentication using ** %s **', plugin.get_id())
556 # use plugin's method of user extraction.
565 # use plugin's method of user extraction.
557 user = plugin.get_user(username, environ=environ,
566 user = plugin.get_user(username, environ=environ,
558 settings=plugin_settings)
567 settings=plugin_settings)
559 display_user = user.username if user else username
568 display_user = user.username if user else username
560 log.debug(
569 log.debug(
561 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
570 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
562
571
563 if not plugin.allows_authentication_from(user):
572 if not plugin.allows_authentication_from(user):
564 log.debug('Plugin %s does not accept user `%s` for authentication',
573 log.debug('Plugin %s does not accept user `%s` for authentication',
565 plugin.get_id(), display_user)
574 plugin.get_id(), display_user)
566 continue
575 continue
567 else:
576 else:
568 log.debug('Plugin %s accepted user `%s` for authentication',
577 log.debug('Plugin %s accepted user `%s` for authentication',
569 plugin.get_id(), display_user)
578 plugin.get_id(), display_user)
570
579
571 log.info('Authenticating user `%s` using %s plugin',
580 log.info('Authenticating user `%s` using %s plugin',
572 display_user, plugin.get_id())
581 display_user, plugin.get_id())
573
582
574 _cache_ttl = 0
583 _cache_ttl = 0
575
584
576 if isinstance(plugin.AUTH_CACHE_TTL, (int, long)):
585 if isinstance(plugin.AUTH_CACHE_TTL, (int, long)):
577 # plugin cache set inside is more important than the settings value
586 # plugin cache set inside is more important than the settings value
578 _cache_ttl = plugin.AUTH_CACHE_TTL
587 _cache_ttl = plugin.AUTH_CACHE_TTL
579 elif plugin_settings.get('cache_ttl'):
588 elif plugin_settings.get('cache_ttl'):
580 _cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
589 _cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
581
590
582 plugin_cache_active = bool(_cache_ttl and _cache_ttl > 0)
591 plugin_cache_active = bool(_cache_ttl and _cache_ttl > 0)
583
592
584 # get instance of cache manager configured for a namespace
593 # get instance of cache manager configured for a namespace
585 cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl)
594 cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl)
586
595
587 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
596 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
588 plugin.get_id(), plugin_cache_active, _cache_ttl)
597 plugin.get_id(), plugin_cache_active, _cache_ttl)
589
598
590 # for environ based password can be empty, but then the validation is
599 # for environ based password can be empty, but then the validation is
591 # on the server that fills in the env data needed for authentication
600 # on the server that fills in the env data needed for authentication
592 _password_hash = md5_safe(plugin.name + username + (password or ''))
601 _password_hash = md5_safe(plugin.name + username + (password or ''))
593
602
594 # _authenticate is a wrapper for .auth() method of plugin.
603 # _authenticate is a wrapper for .auth() method of plugin.
595 # it checks if .auth() sends proper data.
604 # it checks if .auth() sends proper data.
596 # For RhodeCodeExternalAuthPlugin it also maps users to
605 # For RhodeCodeExternalAuthPlugin it also maps users to
597 # Database and maps the attributes returned from .auth()
606 # Database and maps the attributes returned from .auth()
598 # to RhodeCode database. If this function returns data
607 # to RhodeCode database. If this function returns data
599 # then auth is correct.
608 # then auth is correct.
600 start = time.time()
609 start = time.time()
601 log.debug('Running plugin `%s` _authenticate method', plugin.get_id())
610 log.debug('Running plugin `%s` _authenticate method', plugin.get_id())
602
611
603 def auth_func():
612 def auth_func():
604 """
613 """
605 This function is used internally in Cache of Beaker to calculate
614 This function is used internally in Cache of Beaker to calculate
606 Results
615 Results
607 """
616 """
608 return plugin._authenticate(
617 return plugin._authenticate(
609 user, username, password, plugin_settings,
618 user, username, password, plugin_settings,
610 environ=environ or {})
619 environ=environ or {})
611
620
612 if plugin_cache_active:
621 if plugin_cache_active:
613 plugin_user = cache_manager.get(
622 plugin_user = cache_manager.get(
614 _password_hash, createfunc=auth_func)
623 _password_hash, createfunc=auth_func)
615 else:
624 else:
616 plugin_user = auth_func()
625 plugin_user = auth_func()
617
626
618 auth_time = time.time() - start
627 auth_time = time.time() - start
619 log.debug('Authentication for plugin `%s` completed in %.3fs, '
628 log.debug('Authentication for plugin `%s` completed in %.3fs, '
620 'expiration time of fetched cache %.1fs.',
629 'expiration time of fetched cache %.1fs.',
621 plugin.get_id(), auth_time, _cache_ttl)
630 plugin.get_id(), auth_time, _cache_ttl)
622
631
623 log.debug('PLUGIN USER DATA: %s', plugin_user)
632 log.debug('PLUGIN USER DATA: %s', plugin_user)
624
633
625 if plugin_user:
634 if plugin_user:
626 log.debug('Plugin returned proper authentication data')
635 log.debug('Plugin returned proper authentication data')
627 return plugin_user
636 return plugin_user
628 # we failed to Auth because .auth() method didn't return proper user
637 # we failed to Auth because .auth() method didn't return proper user
629 log.debug("User `%s` failed to authenticate against %s",
638 log.debug("User `%s` failed to authenticate against %s",
630 display_user, plugin.get_id())
639 display_user, plugin.get_id())
631 return None
640 return None
632
641
633
642
634 def chop_at(s, sub, inclusive=False):
643 def chop_at(s, sub, inclusive=False):
635 """Truncate string ``s`` at the first occurrence of ``sub``.
644 """Truncate string ``s`` at the first occurrence of ``sub``.
636
645
637 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
646 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
638
647
639 >>> chop_at("plutocratic brats", "rat")
648 >>> chop_at("plutocratic brats", "rat")
640 'plutoc'
649 'plutoc'
641 >>> chop_at("plutocratic brats", "rat", True)
650 >>> chop_at("plutocratic brats", "rat", True)
642 'plutocrat'
651 'plutocrat'
643 """
652 """
644 pos = s.find(sub)
653 pos = s.find(sub)
645 if pos == -1:
654 if pos == -1:
646 return s
655 return s
647 if inclusive:
656 if inclusive:
648 return s[:pos+len(sub)]
657 return s[:pos+len(sub)]
649 return s[:pos]
658 return s[:pos]
@@ -1,139 +1,146 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-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 RhodeCode authentication token plugin for built in internal auth
22 RhodeCode authentication token plugin for built in internal auth
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 from rhodecode.translation import _
27 from rhodecode.translation import _
28 from rhodecode.authentication.base import (
28 from rhodecode.authentication.base import (
29 RhodeCodeAuthPluginBase, VCS_TYPE, hybrid_property)
29 RhodeCodeAuthPluginBase, VCS_TYPE, hybrid_property)
30 from rhodecode.authentication.routes import AuthnPluginResourceBase
30 from rhodecode.authentication.routes import AuthnPluginResourceBase
31 from rhodecode.model.db import User, UserApiKeys
31 from rhodecode.model.db import User, UserApiKeys, Repository
32
32
33
33
34 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
35
35
36
36
37 def plugin_factory(plugin_id, *args, **kwds):
37 def plugin_factory(plugin_id, *args, **kwds):
38 plugin = RhodeCodeAuthPlugin(plugin_id)
38 plugin = RhodeCodeAuthPlugin(plugin_id)
39 return plugin
39 return plugin
40
40
41
41
42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
43 pass
43 pass
44
44
45
45
46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
47 """
47 """
48 Enables usage of authentication tokens for vcs operations.
48 Enables usage of authentication tokens for vcs operations.
49 """
49 """
50
50
51 def includeme(self, config):
51 def includeme(self, config):
52 config.add_authn_plugin(self)
52 config.add_authn_plugin(self)
53 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
53 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
54 config.add_view(
54 config.add_view(
55 'rhodecode.authentication.views.AuthnPluginViewBase',
55 'rhodecode.authentication.views.AuthnPluginViewBase',
56 attr='settings_get',
56 attr='settings_get',
57 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
57 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
58 request_method='GET',
58 request_method='GET',
59 route_name='auth_home',
59 route_name='auth_home',
60 context=RhodecodeAuthnResource)
60 context=RhodecodeAuthnResource)
61 config.add_view(
61 config.add_view(
62 'rhodecode.authentication.views.AuthnPluginViewBase',
62 'rhodecode.authentication.views.AuthnPluginViewBase',
63 attr='settings_post',
63 attr='settings_post',
64 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
64 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
65 request_method='POST',
65 request_method='POST',
66 route_name='auth_home',
66 route_name='auth_home',
67 context=RhodecodeAuthnResource)
67 context=RhodecodeAuthnResource)
68
68
69 def get_display_name(self):
69 def get_display_name(self):
70 return _('Rhodecode Token Auth')
70 return _('Rhodecode Token Auth')
71
71
72 @hybrid_property
72 @hybrid_property
73 def name(self):
73 def name(self):
74 return "authtoken"
74 return "authtoken"
75
75
76 def user_activation_state(self):
76 def user_activation_state(self):
77 def_user_perms = User.get_default_user().AuthUser.permissions['global']
77 def_user_perms = User.get_default_user().AuthUser.permissions['global']
78 return 'hg.register.auto_activate' in def_user_perms
78 return 'hg.register.auto_activate' in def_user_perms
79
79
80 def allows_authentication_from(
80 def allows_authentication_from(
81 self, user, allows_non_existing_user=True,
81 self, user, allows_non_existing_user=True,
82 allowed_auth_plugins=None, allowed_auth_sources=None):
82 allowed_auth_plugins=None, allowed_auth_sources=None):
83 """
83 """
84 Custom method for this auth that doesn't accept empty users. And also
84 Custom method for this auth that doesn't accept empty users. And also
85 allows users from all other active plugins to use it and also
85 allows users from all other active plugins to use it and also
86 authenticate against it. But only via vcs mode
86 authenticate against it. But only via vcs mode
87 """
87 """
88 from rhodecode.authentication.base import get_authn_registry
88 from rhodecode.authentication.base import get_authn_registry
89 authn_registry = get_authn_registry()
89 authn_registry = get_authn_registry()
90
90
91 active_plugins = set(
91 active_plugins = set(
92 [x.name for x in authn_registry.get_plugins_for_authentication()])
92 [x.name for x in authn_registry.get_plugins_for_authentication()])
93 active_plugins.discard(self.name)
93 active_plugins.discard(self.name)
94
94
95 allowed_auth_plugins = [self.name] + list(active_plugins)
95 allowed_auth_plugins = [self.name] + list(active_plugins)
96 # only for vcs operations
96 # only for vcs operations
97 allowed_auth_sources = [VCS_TYPE]
97 allowed_auth_sources = [VCS_TYPE]
98
98
99 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
99 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
100 user, allows_non_existing_user=False,
100 user, allows_non_existing_user=False,
101 allowed_auth_plugins=allowed_auth_plugins,
101 allowed_auth_plugins=allowed_auth_plugins,
102 allowed_auth_sources=allowed_auth_sources)
102 allowed_auth_sources=allowed_auth_sources)
103
103
104 def auth(self, userobj, username, password, settings, **kwargs):
104 def auth(self, userobj, username, password, settings, **kwargs):
105 if not userobj:
105 if not userobj:
106 log.debug('userobj was:%s skipping' % (userobj, ))
106 log.debug('userobj was:%s skipping' % (userobj, ))
107 return None
107 return None
108
108
109 user_attrs = {
109 user_attrs = {
110 "username": userobj.username,
110 "username": userobj.username,
111 "firstname": userobj.firstname,
111 "firstname": userobj.firstname,
112 "lastname": userobj.lastname,
112 "lastname": userobj.lastname,
113 "groups": [],
113 "groups": [],
114 "email": userobj.email,
114 "email": userobj.email,
115 "admin": userobj.admin,
115 "admin": userobj.admin,
116 "active": userobj.active,
116 "active": userobj.active,
117 "active_from_extern": userobj.active,
117 "active_from_extern": userobj.active,
118 "extern_name": userobj.user_id,
118 "extern_name": userobj.user_id,
119 "extern_type": userobj.extern_type,
119 "extern_type": userobj.extern_type,
120 }
120 }
121
121
122 log.debug('Authenticating user with args %s', user_attrs)
122 log.debug('Authenticating user with args %s', user_attrs)
123 if userobj.active:
123 if userobj.active:
124 # calling context repo for token scopes
125 scope_repo_id = None
126 if self.acl_repo_name:
127 repo = Repository.get_by_repo_name(self.acl_repo_name)
128 scope_repo_id = repo.repo_id if repo else None
129
124 token_match = userobj.authenticate_by_token(
130 token_match = userobj.authenticate_by_token(
125 password, roles=[UserApiKeys.ROLE_VCS])
131 password, roles=[UserApiKeys.ROLE_VCS],
132 scope_repo_id=scope_repo_id)
126
133
127 if userobj.username == username and token_match:
134 if userobj.username == username and token_match:
128 log.info(
135 log.info(
129 'user `%s` successfully authenticated via %s',
136 'user `%s` successfully authenticated via %s',
130 user_attrs['username'], self.name)
137 user_attrs['username'], self.name)
131 return user_attrs
138 return user_attrs
132 log.error(
139 log.error(
133 'user `%s` failed to authenticate via %s, reason: bad or '
140 'user `%s` failed to authenticate via %s, reason: bad or '
134 'inactive token.', username, self.name)
141 'inactive token.', username, self.name)
135 else:
142 else:
136 log.warning(
143 log.warning(
137 'user `%s` failed to authenticate via %s, reason: account not '
144 'user `%s` failed to authenticate via %s, reason: account not '
138 'active.', username, self.name)
145 'active.', username, self.name)
139 return None
146 return None
@@ -1,597 +1,598 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 ipaddress
30 import ipaddress
31 import pyramid.threadlocal
31 import pyramid.threadlocal
32
32
33 from paste.auth.basic import AuthBasicAuthenticator
33 from paste.auth.basic import AuthBasicAuthenticator
34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 from pylons import config, tmpl_context as c, request, session, url
36 from pylons import config, tmpl_context as c, request, session, url
37 from pylons.controllers import WSGIController
37 from pylons.controllers import WSGIController
38 from pylons.controllers.util import redirect
38 from pylons.controllers.util import redirect
39 from pylons.i18n import translation
39 from pylons.i18n import translation
40 # marcink: don't remove this import
40 # marcink: don't remove this import
41 from pylons.templating import render_mako as render # noqa
41 from pylons.templating import render_mako as render # noqa
42 from pylons.i18n.translation import _
42 from pylons.i18n.translation import _
43 from webob.exc import HTTPFound
43 from webob.exc import HTTPFound
44
44
45
45
46 import rhodecode
46 import rhodecode
47 from rhodecode.authentication.base import VCS_TYPE
47 from rhodecode.authentication.base import VCS_TYPE
48 from rhodecode.lib import auth, utils2
48 from rhodecode.lib import auth, utils2
49 from rhodecode.lib import helpers as h
49 from rhodecode.lib import helpers as h
50 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
50 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
51 from rhodecode.lib.exceptions import UserCreationError
51 from rhodecode.lib.exceptions import UserCreationError
52 from rhodecode.lib.utils import (
52 from rhodecode.lib.utils import (
53 get_repo_slug, set_rhodecode_config, password_changed,
53 get_repo_slug, set_rhodecode_config, password_changed,
54 get_enabled_hook_classes)
54 get_enabled_hook_classes)
55 from rhodecode.lib.utils2 import (
55 from rhodecode.lib.utils2 import (
56 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
56 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
57 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
57 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
58 from rhodecode.model import meta
58 from rhodecode.model import meta
59 from rhodecode.model.db import Repository, User, ChangesetComment
59 from rhodecode.model.db import Repository, User, ChangesetComment
60 from rhodecode.model.notification import NotificationModel
60 from rhodecode.model.notification import NotificationModel
61 from rhodecode.model.scm import ScmModel
61 from rhodecode.model.scm import ScmModel
62 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
62 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
63
63
64
64
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67
67
68 def _filter_proxy(ip):
68 def _filter_proxy(ip):
69 """
69 """
70 Passed in IP addresses in HEADERS can be in a special format of multiple
70 Passed in IP addresses in HEADERS can be in a special format of multiple
71 ips. Those comma separated IPs are passed from various proxies in the
71 ips. Those comma separated IPs are passed from various proxies in the
72 chain of request processing. The left-most being the original client.
72 chain of request processing. The left-most being the original client.
73 We only care about the first IP which came from the org. client.
73 We only care about the first IP which came from the org. client.
74
74
75 :param ip: ip string from headers
75 :param ip: ip string from headers
76 """
76 """
77 if ',' in ip:
77 if ',' in ip:
78 _ips = ip.split(',')
78 _ips = ip.split(',')
79 _first_ip = _ips[0].strip()
79 _first_ip = _ips[0].strip()
80 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
80 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
81 return _first_ip
81 return _first_ip
82 return ip
82 return ip
83
83
84
84
85 def _filter_port(ip):
85 def _filter_port(ip):
86 """
86 """
87 Removes a port from ip, there are 4 main cases to handle here.
87 Removes a port from ip, there are 4 main cases to handle here.
88 - ipv4 eg. 127.0.0.1
88 - ipv4 eg. 127.0.0.1
89 - ipv6 eg. ::1
89 - ipv6 eg. ::1
90 - ipv4+port eg. 127.0.0.1:8080
90 - ipv4+port eg. 127.0.0.1:8080
91 - ipv6+port eg. [::1]:8080
91 - ipv6+port eg. [::1]:8080
92
92
93 :param ip:
93 :param ip:
94 """
94 """
95 def is_ipv6(ip_addr):
95 def is_ipv6(ip_addr):
96 if hasattr(socket, 'inet_pton'):
96 if hasattr(socket, 'inet_pton'):
97 try:
97 try:
98 socket.inet_pton(socket.AF_INET6, ip_addr)
98 socket.inet_pton(socket.AF_INET6, ip_addr)
99 except socket.error:
99 except socket.error:
100 return False
100 return False
101 else:
101 else:
102 # fallback to ipaddress
102 # fallback to ipaddress
103 try:
103 try:
104 ipaddress.IPv6Address(ip_addr)
104 ipaddress.IPv6Address(ip_addr)
105 except Exception:
105 except Exception:
106 return False
106 return False
107 return True
107 return True
108
108
109 if ':' not in ip: # must be ipv4 pure ip
109 if ':' not in ip: # must be ipv4 pure ip
110 return ip
110 return ip
111
111
112 if '[' in ip and ']' in ip: # ipv6 with port
112 if '[' in ip and ']' in ip: # ipv6 with port
113 return ip.split(']')[0][1:].lower()
113 return ip.split(']')[0][1:].lower()
114
114
115 # must be ipv6 or ipv4 with port
115 # must be ipv6 or ipv4 with port
116 if is_ipv6(ip):
116 if is_ipv6(ip):
117 return ip
117 return ip
118 else:
118 else:
119 ip, _port = ip.split(':')[:2] # means ipv4+port
119 ip, _port = ip.split(':')[:2] # means ipv4+port
120 return ip
120 return ip
121
121
122
122
123 def get_ip_addr(environ):
123 def get_ip_addr(environ):
124 proxy_key = 'HTTP_X_REAL_IP'
124 proxy_key = 'HTTP_X_REAL_IP'
125 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
125 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
126 def_key = 'REMOTE_ADDR'
126 def_key = 'REMOTE_ADDR'
127 _filters = lambda x: _filter_port(_filter_proxy(x))
127 _filters = lambda x: _filter_port(_filter_proxy(x))
128
128
129 ip = environ.get(proxy_key)
129 ip = environ.get(proxy_key)
130 if ip:
130 if ip:
131 return _filters(ip)
131 return _filters(ip)
132
132
133 ip = environ.get(proxy_key2)
133 ip = environ.get(proxy_key2)
134 if ip:
134 if ip:
135 return _filters(ip)
135 return _filters(ip)
136
136
137 ip = environ.get(def_key, '0.0.0.0')
137 ip = environ.get(def_key, '0.0.0.0')
138 return _filters(ip)
138 return _filters(ip)
139
139
140
140
141 def get_server_ip_addr(environ, log_errors=True):
141 def get_server_ip_addr(environ, log_errors=True):
142 hostname = environ.get('SERVER_NAME')
142 hostname = environ.get('SERVER_NAME')
143 try:
143 try:
144 return socket.gethostbyname(hostname)
144 return socket.gethostbyname(hostname)
145 except Exception as e:
145 except Exception as e:
146 if log_errors:
146 if log_errors:
147 # in some cases this lookup is not possible, and we don't want to
147 # in some cases this lookup is not possible, and we don't want to
148 # make it an exception in logs
148 # make it an exception in logs
149 log.exception('Could not retrieve server ip address: %s', e)
149 log.exception('Could not retrieve server ip address: %s', e)
150 return hostname
150 return hostname
151
151
152
152
153 def get_server_port(environ):
153 def get_server_port(environ):
154 return environ.get('SERVER_PORT')
154 return environ.get('SERVER_PORT')
155
155
156
156
157 def get_access_path(environ):
157 def get_access_path(environ):
158 path = environ.get('PATH_INFO')
158 path = environ.get('PATH_INFO')
159 org_req = environ.get('pylons.original_request')
159 org_req = environ.get('pylons.original_request')
160 if org_req:
160 if org_req:
161 path = org_req.environ.get('PATH_INFO')
161 path = org_req.environ.get('PATH_INFO')
162 return path
162 return path
163
163
164
164
165 def vcs_operation_context(
165 def vcs_operation_context(
166 environ, repo_name, username, action, scm, check_locking=True,
166 environ, repo_name, username, action, scm, check_locking=True,
167 is_shadow_repo=False):
167 is_shadow_repo=False):
168 """
168 """
169 Generate the context for a vcs operation, e.g. push or pull.
169 Generate the context for a vcs operation, e.g. push or pull.
170
170
171 This context is passed over the layers so that hooks triggered by the
171 This context is passed over the layers so that hooks triggered by the
172 vcs operation know details like the user, the user's IP address etc.
172 vcs operation know details like the user, the user's IP address etc.
173
173
174 :param check_locking: Allows to switch of the computation of the locking
174 :param check_locking: Allows to switch of the computation of the locking
175 data. This serves mainly the need of the simplevcs middleware to be
175 data. This serves mainly the need of the simplevcs middleware to be
176 able to disable this for certain operations.
176 able to disable this for certain operations.
177
177
178 """
178 """
179 # Tri-state value: False: unlock, None: nothing, True: lock
179 # Tri-state value: False: unlock, None: nothing, True: lock
180 make_lock = None
180 make_lock = None
181 locked_by = [None, None, None]
181 locked_by = [None, None, None]
182 is_anonymous = username == User.DEFAULT_USER
182 is_anonymous = username == User.DEFAULT_USER
183 if not is_anonymous and check_locking:
183 if not is_anonymous and check_locking:
184 log.debug('Checking locking on repository "%s"', repo_name)
184 log.debug('Checking locking on repository "%s"', repo_name)
185 user = User.get_by_username(username)
185 user = User.get_by_username(username)
186 repo = Repository.get_by_repo_name(repo_name)
186 repo = Repository.get_by_repo_name(repo_name)
187 make_lock, __, locked_by = repo.get_locking_state(
187 make_lock, __, locked_by = repo.get_locking_state(
188 action, user.user_id)
188 action, user.user_id)
189
189
190 settings_model = VcsSettingsModel(repo=repo_name)
190 settings_model = VcsSettingsModel(repo=repo_name)
191 ui_settings = settings_model.get_ui_settings()
191 ui_settings = settings_model.get_ui_settings()
192
192
193 extras = {
193 extras = {
194 'ip': get_ip_addr(environ),
194 'ip': get_ip_addr(environ),
195 'username': username,
195 'username': username,
196 'action': action,
196 'action': action,
197 'repository': repo_name,
197 'repository': repo_name,
198 'scm': scm,
198 'scm': scm,
199 'config': rhodecode.CONFIG['__file__'],
199 'config': rhodecode.CONFIG['__file__'],
200 'make_lock': make_lock,
200 'make_lock': make_lock,
201 'locked_by': locked_by,
201 'locked_by': locked_by,
202 'server_url': utils2.get_server_url(environ),
202 'server_url': utils2.get_server_url(environ),
203 'hooks': get_enabled_hook_classes(ui_settings),
203 'hooks': get_enabled_hook_classes(ui_settings),
204 'is_shadow_repo': is_shadow_repo,
204 'is_shadow_repo': is_shadow_repo,
205 }
205 }
206 return extras
206 return extras
207
207
208
208
209 class BasicAuth(AuthBasicAuthenticator):
209 class BasicAuth(AuthBasicAuthenticator):
210
210
211 def __init__(self, realm, authfunc, registry, auth_http_code=None,
211 def __init__(self, realm, authfunc, registry, auth_http_code=None,
212 initial_call_detection=False):
212 initial_call_detection=False, acl_repo_name=None):
213 self.realm = realm
213 self.realm = realm
214 self.initial_call = initial_call_detection
214 self.initial_call = initial_call_detection
215 self.authfunc = authfunc
215 self.authfunc = authfunc
216 self.registry = registry
216 self.registry = registry
217 self.acl_repo_name = acl_repo_name
217 self._rc_auth_http_code = auth_http_code
218 self._rc_auth_http_code = auth_http_code
218
219
219 def _get_response_from_code(self, http_code):
220 def _get_response_from_code(self, http_code):
220 try:
221 try:
221 return get_exception(safe_int(http_code))
222 return get_exception(safe_int(http_code))
222 except Exception:
223 except Exception:
223 log.exception('Failed to fetch response for code %s' % http_code)
224 log.exception('Failed to fetch response for code %s' % http_code)
224 return HTTPForbidden
225 return HTTPForbidden
225
226
226 def build_authentication(self):
227 def build_authentication(self):
227 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
228 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
228 if self._rc_auth_http_code and not self.initial_call:
229 if self._rc_auth_http_code and not self.initial_call:
229 # return alternative HTTP code if alternative http return code
230 # return alternative HTTP code if alternative http return code
230 # is specified in RhodeCode config, but ONLY if it's not the
231 # is specified in RhodeCode config, but ONLY if it's not the
231 # FIRST call
232 # FIRST call
232 custom_response_klass = self._get_response_from_code(
233 custom_response_klass = self._get_response_from_code(
233 self._rc_auth_http_code)
234 self._rc_auth_http_code)
234 return custom_response_klass(headers=head)
235 return custom_response_klass(headers=head)
235 return HTTPUnauthorized(headers=head)
236 return HTTPUnauthorized(headers=head)
236
237
237 def authenticate(self, environ):
238 def authenticate(self, environ):
238 authorization = AUTHORIZATION(environ)
239 authorization = AUTHORIZATION(environ)
239 if not authorization:
240 if not authorization:
240 return self.build_authentication()
241 return self.build_authentication()
241 (authmeth, auth) = authorization.split(' ', 1)
242 (authmeth, auth) = authorization.split(' ', 1)
242 if 'basic' != authmeth.lower():
243 if 'basic' != authmeth.lower():
243 return self.build_authentication()
244 return self.build_authentication()
244 auth = auth.strip().decode('base64')
245 auth = auth.strip().decode('base64')
245 _parts = auth.split(':', 1)
246 _parts = auth.split(':', 1)
246 if len(_parts) == 2:
247 if len(_parts) == 2:
247 username, password = _parts
248 username, password = _parts
248 if self.authfunc(
249 if self.authfunc(
249 username, password, environ, VCS_TYPE,
250 username, password, environ, VCS_TYPE,
250 registry=self.registry):
251 registry=self.registry, acl_repo_name=self.acl_repo_name):
251 return username
252 return username
252 if username and password:
253 if username and password:
253 # we mark that we actually executed authentication once, at
254 # we mark that we actually executed authentication once, at
254 # that point we can use the alternative auth code
255 # that point we can use the alternative auth code
255 self.initial_call = False
256 self.initial_call = False
256
257
257 return self.build_authentication()
258 return self.build_authentication()
258
259
259 __call__ = authenticate
260 __call__ = authenticate
260
261
261
262
262 def attach_context_attributes(context, request):
263 def attach_context_attributes(context, request):
263 """
264 """
264 Attach variables into template context called `c`, please note that
265 Attach variables into template context called `c`, please note that
265 request could be pylons or pyramid request in here.
266 request could be pylons or pyramid request in here.
266 """
267 """
267 rc_config = SettingsModel().get_all_settings(cache=True)
268 rc_config = SettingsModel().get_all_settings(cache=True)
268
269
269 context.rhodecode_version = rhodecode.__version__
270 context.rhodecode_version = rhodecode.__version__
270 context.rhodecode_edition = config.get('rhodecode.edition')
271 context.rhodecode_edition = config.get('rhodecode.edition')
271 # unique secret + version does not leak the version but keep consistency
272 # unique secret + version does not leak the version but keep consistency
272 context.rhodecode_version_hash = md5(
273 context.rhodecode_version_hash = md5(
273 config.get('beaker.session.secret', '') +
274 config.get('beaker.session.secret', '') +
274 rhodecode.__version__)[:8]
275 rhodecode.__version__)[:8]
275
276
276 # Default language set for the incoming request
277 # Default language set for the incoming request
277 context.language = translation.get_lang()[0]
278 context.language = translation.get_lang()[0]
278
279
279 # Visual options
280 # Visual options
280 context.visual = AttributeDict({})
281 context.visual = AttributeDict({})
281
282
282 # DB stored Visual Items
283 # DB stored Visual Items
283 context.visual.show_public_icon = str2bool(
284 context.visual.show_public_icon = str2bool(
284 rc_config.get('rhodecode_show_public_icon'))
285 rc_config.get('rhodecode_show_public_icon'))
285 context.visual.show_private_icon = str2bool(
286 context.visual.show_private_icon = str2bool(
286 rc_config.get('rhodecode_show_private_icon'))
287 rc_config.get('rhodecode_show_private_icon'))
287 context.visual.stylify_metatags = str2bool(
288 context.visual.stylify_metatags = str2bool(
288 rc_config.get('rhodecode_stylify_metatags'))
289 rc_config.get('rhodecode_stylify_metatags'))
289 context.visual.dashboard_items = safe_int(
290 context.visual.dashboard_items = safe_int(
290 rc_config.get('rhodecode_dashboard_items', 100))
291 rc_config.get('rhodecode_dashboard_items', 100))
291 context.visual.admin_grid_items = safe_int(
292 context.visual.admin_grid_items = safe_int(
292 rc_config.get('rhodecode_admin_grid_items', 100))
293 rc_config.get('rhodecode_admin_grid_items', 100))
293 context.visual.repository_fields = str2bool(
294 context.visual.repository_fields = str2bool(
294 rc_config.get('rhodecode_repository_fields'))
295 rc_config.get('rhodecode_repository_fields'))
295 context.visual.show_version = str2bool(
296 context.visual.show_version = str2bool(
296 rc_config.get('rhodecode_show_version'))
297 rc_config.get('rhodecode_show_version'))
297 context.visual.use_gravatar = str2bool(
298 context.visual.use_gravatar = str2bool(
298 rc_config.get('rhodecode_use_gravatar'))
299 rc_config.get('rhodecode_use_gravatar'))
299 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
300 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
300 context.visual.default_renderer = rc_config.get(
301 context.visual.default_renderer = rc_config.get(
301 'rhodecode_markup_renderer', 'rst')
302 'rhodecode_markup_renderer', 'rst')
302 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
303 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
303 context.visual.rhodecode_support_url = \
304 context.visual.rhodecode_support_url = \
304 rc_config.get('rhodecode_support_url') or url('rhodecode_support')
305 rc_config.get('rhodecode_support_url') or url('rhodecode_support')
305
306
306 context.pre_code = rc_config.get('rhodecode_pre_code')
307 context.pre_code = rc_config.get('rhodecode_pre_code')
307 context.post_code = rc_config.get('rhodecode_post_code')
308 context.post_code = rc_config.get('rhodecode_post_code')
308 context.rhodecode_name = rc_config.get('rhodecode_title')
309 context.rhodecode_name = rc_config.get('rhodecode_title')
309 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
310 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
310 # if we have specified default_encoding in the request, it has more
311 # if we have specified default_encoding in the request, it has more
311 # priority
312 # priority
312 if request.GET.get('default_encoding'):
313 if request.GET.get('default_encoding'):
313 context.default_encodings.insert(0, request.GET.get('default_encoding'))
314 context.default_encodings.insert(0, request.GET.get('default_encoding'))
314 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
315 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
315
316
316 # INI stored
317 # INI stored
317 context.labs_active = str2bool(
318 context.labs_active = str2bool(
318 config.get('labs_settings_active', 'false'))
319 config.get('labs_settings_active', 'false'))
319 context.visual.allow_repo_location_change = str2bool(
320 context.visual.allow_repo_location_change = str2bool(
320 config.get('allow_repo_location_change', True))
321 config.get('allow_repo_location_change', True))
321 context.visual.allow_custom_hooks_settings = str2bool(
322 context.visual.allow_custom_hooks_settings = str2bool(
322 config.get('allow_custom_hooks_settings', True))
323 config.get('allow_custom_hooks_settings', True))
323 context.debug_style = str2bool(config.get('debug_style', False))
324 context.debug_style = str2bool(config.get('debug_style', False))
324
325
325 context.rhodecode_instanceid = config.get('instance_id')
326 context.rhodecode_instanceid = config.get('instance_id')
326
327
327 # AppEnlight
328 # AppEnlight
328 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
329 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
329 context.appenlight_api_public_key = config.get(
330 context.appenlight_api_public_key = config.get(
330 'appenlight.api_public_key', '')
331 'appenlight.api_public_key', '')
331 context.appenlight_server_url = config.get('appenlight.server_url', '')
332 context.appenlight_server_url = config.get('appenlight.server_url', '')
332
333
333 # JS template context
334 # JS template context
334 context.template_context = {
335 context.template_context = {
335 'repo_name': None,
336 'repo_name': None,
336 'repo_type': None,
337 'repo_type': None,
337 'repo_landing_commit': None,
338 'repo_landing_commit': None,
338 'rhodecode_user': {
339 'rhodecode_user': {
339 'username': None,
340 'username': None,
340 'email': None,
341 'email': None,
341 'notification_status': False
342 'notification_status': False
342 },
343 },
343 'visual': {
344 'visual': {
344 'default_renderer': None
345 'default_renderer': None
345 },
346 },
346 'commit_data': {
347 'commit_data': {
347 'commit_id': None
348 'commit_id': None
348 },
349 },
349 'pull_request_data': {'pull_request_id': None},
350 'pull_request_data': {'pull_request_id': None},
350 'timeago': {
351 'timeago': {
351 'refresh_time': 120 * 1000,
352 'refresh_time': 120 * 1000,
352 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
353 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
353 },
354 },
354 'pylons_dispatch': {
355 'pylons_dispatch': {
355 # 'controller': request.environ['pylons.routes_dict']['controller'],
356 # 'controller': request.environ['pylons.routes_dict']['controller'],
356 # 'action': request.environ['pylons.routes_dict']['action'],
357 # 'action': request.environ['pylons.routes_dict']['action'],
357 },
358 },
358 'pyramid_dispatch': {
359 'pyramid_dispatch': {
359
360
360 },
361 },
361 'extra': {'plugins': {}}
362 'extra': {'plugins': {}}
362 }
363 }
363 # END CONFIG VARS
364 # END CONFIG VARS
364
365
365 # TODO: This dosn't work when called from pylons compatibility tween.
366 # TODO: This dosn't work when called from pylons compatibility tween.
366 # Fix this and remove it from base controller.
367 # Fix this and remove it from base controller.
367 # context.repo_name = get_repo_slug(request) # can be empty
368 # context.repo_name = get_repo_slug(request) # can be empty
368
369
369 diffmode = 'sideside'
370 diffmode = 'sideside'
370 if request.GET.get('diffmode'):
371 if request.GET.get('diffmode'):
371 if request.GET['diffmode'] == 'unified':
372 if request.GET['diffmode'] == 'unified':
372 diffmode = 'unified'
373 diffmode = 'unified'
373 elif request.session.get('diffmode'):
374 elif request.session.get('diffmode'):
374 diffmode = request.session['diffmode']
375 diffmode = request.session['diffmode']
375
376
376 context.diffmode = diffmode
377 context.diffmode = diffmode
377
378
378 if request.session.get('diffmode') != diffmode:
379 if request.session.get('diffmode') != diffmode:
379 request.session['diffmode'] = diffmode
380 request.session['diffmode'] = diffmode
380
381
381 context.csrf_token = auth.get_csrf_token()
382 context.csrf_token = auth.get_csrf_token()
382 context.backends = rhodecode.BACKENDS.keys()
383 context.backends = rhodecode.BACKENDS.keys()
383 context.backends.sort()
384 context.backends.sort()
384 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(
385 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(
385 context.rhodecode_user.user_id)
386 context.rhodecode_user.user_id)
386
387
387 context.pyramid_request = pyramid.threadlocal.get_current_request()
388 context.pyramid_request = pyramid.threadlocal.get_current_request()
388
389
389
390
390 def get_auth_user(environ):
391 def get_auth_user(environ):
391 ip_addr = get_ip_addr(environ)
392 ip_addr = get_ip_addr(environ)
392 # make sure that we update permissions each time we call controller
393 # make sure that we update permissions each time we call controller
393 _auth_token = (request.GET.get('auth_token', '') or
394 _auth_token = (request.GET.get('auth_token', '') or
394 request.GET.get('api_key', ''))
395 request.GET.get('api_key', ''))
395
396
396 if _auth_token:
397 if _auth_token:
397 # when using API_KEY we assume user exists, and
398 # when using API_KEY we assume user exists, and
398 # doesn't need auth based on cookies.
399 # doesn't need auth based on cookies.
399 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
400 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
400 authenticated = False
401 authenticated = False
401 else:
402 else:
402 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
403 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
403 try:
404 try:
404 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
405 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
405 ip_addr=ip_addr)
406 ip_addr=ip_addr)
406 except UserCreationError as e:
407 except UserCreationError as e:
407 h.flash(e, 'error')
408 h.flash(e, 'error')
408 # container auth or other auth functions that create users
409 # container auth or other auth functions that create users
409 # on the fly can throw this exception signaling that there's
410 # on the fly can throw this exception signaling that there's
410 # issue with user creation, explanation should be provided
411 # issue with user creation, explanation should be provided
411 # in Exception itself. We then create a simple blank
412 # in Exception itself. We then create a simple blank
412 # AuthUser
413 # AuthUser
413 auth_user = AuthUser(ip_addr=ip_addr)
414 auth_user = AuthUser(ip_addr=ip_addr)
414
415
415 if password_changed(auth_user, session):
416 if password_changed(auth_user, session):
416 session.invalidate()
417 session.invalidate()
417 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
418 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
418 auth_user = AuthUser(ip_addr=ip_addr)
419 auth_user = AuthUser(ip_addr=ip_addr)
419
420
420 authenticated = cookie_store.get('is_authenticated')
421 authenticated = cookie_store.get('is_authenticated')
421
422
422 if not auth_user.is_authenticated and auth_user.is_user_object:
423 if not auth_user.is_authenticated and auth_user.is_user_object:
423 # user is not authenticated and not empty
424 # user is not authenticated and not empty
424 auth_user.set_authenticated(authenticated)
425 auth_user.set_authenticated(authenticated)
425
426
426 return auth_user
427 return auth_user
427
428
428
429
429 class BaseController(WSGIController):
430 class BaseController(WSGIController):
430
431
431 def __before__(self):
432 def __before__(self):
432 """
433 """
433 __before__ is called before controller methods and after __call__
434 __before__ is called before controller methods and after __call__
434 """
435 """
435 # on each call propagate settings calls into global settings.
436 # on each call propagate settings calls into global settings.
436 set_rhodecode_config(config)
437 set_rhodecode_config(config)
437 attach_context_attributes(c, request)
438 attach_context_attributes(c, request)
438
439
439 # TODO: Remove this when fixed in attach_context_attributes()
440 # TODO: Remove this when fixed in attach_context_attributes()
440 c.repo_name = get_repo_slug(request) # can be empty
441 c.repo_name = get_repo_slug(request) # can be empty
441
442
442 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
443 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
443 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
444 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
444 self.sa = meta.Session
445 self.sa = meta.Session
445 self.scm_model = ScmModel(self.sa)
446 self.scm_model = ScmModel(self.sa)
446
447
447 # set user language
448 # set user language
448 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
449 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
449 if user_lang:
450 if user_lang:
450 translation.set_lang(user_lang)
451 translation.set_lang(user_lang)
451 log.debug('set language to %s for user %s',
452 log.debug('set language to %s for user %s',
452 user_lang, self._rhodecode_user)
453 user_lang, self._rhodecode_user)
453
454
454 def _dispatch_redirect(self, with_url, environ, start_response):
455 def _dispatch_redirect(self, with_url, environ, start_response):
455 resp = HTTPFound(with_url)
456 resp = HTTPFound(with_url)
456 environ['SCRIPT_NAME'] = '' # handle prefix middleware
457 environ['SCRIPT_NAME'] = '' # handle prefix middleware
457 environ['PATH_INFO'] = with_url
458 environ['PATH_INFO'] = with_url
458 return resp(environ, start_response)
459 return resp(environ, start_response)
459
460
460 def __call__(self, environ, start_response):
461 def __call__(self, environ, start_response):
461 """Invoke the Controller"""
462 """Invoke the Controller"""
462 # WSGIController.__call__ dispatches to the Controller method
463 # WSGIController.__call__ dispatches to the Controller method
463 # the request is routed to. This routing information is
464 # the request is routed to. This routing information is
464 # available in environ['pylons.routes_dict']
465 # available in environ['pylons.routes_dict']
465 from rhodecode.lib import helpers as h
466 from rhodecode.lib import helpers as h
466
467
467 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
468 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
468 if environ.get('debugtoolbar.wants_pylons_context', False):
469 if environ.get('debugtoolbar.wants_pylons_context', False):
469 environ['debugtoolbar.pylons_context'] = c._current_obj()
470 environ['debugtoolbar.pylons_context'] = c._current_obj()
470
471
471 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
472 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
472 environ['pylons.routes_dict']['action']])
473 environ['pylons.routes_dict']['action']])
473
474
474 self.rc_config = SettingsModel().get_all_settings(cache=True)
475 self.rc_config = SettingsModel().get_all_settings(cache=True)
475 self.ip_addr = get_ip_addr(environ)
476 self.ip_addr = get_ip_addr(environ)
476
477
477 # The rhodecode auth user is looked up and passed through the
478 # The rhodecode auth user is looked up and passed through the
478 # environ by the pylons compatibility tween in pyramid.
479 # environ by the pylons compatibility tween in pyramid.
479 # So we can just grab it from there.
480 # So we can just grab it from there.
480 auth_user = environ['rc_auth_user']
481 auth_user = environ['rc_auth_user']
481
482
482 # set globals for auth user
483 # set globals for auth user
483 request.user = auth_user
484 request.user = auth_user
484 c.rhodecode_user = self._rhodecode_user = auth_user
485 c.rhodecode_user = self._rhodecode_user = auth_user
485
486
486 log.info('IP: %s User: %s accessed %s [%s]' % (
487 log.info('IP: %s User: %s accessed %s [%s]' % (
487 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
488 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
488 _route_name)
489 _route_name)
489 )
490 )
490
491
491 # TODO: Maybe this should be move to pyramid to cover all views.
492 # TODO: Maybe this should be move to pyramid to cover all views.
492 # check user attributes for password change flag
493 # check user attributes for password change flag
493 user_obj = auth_user.get_instance()
494 user_obj = auth_user.get_instance()
494 if user_obj and user_obj.user_data.get('force_password_change'):
495 if user_obj and user_obj.user_data.get('force_password_change'):
495 h.flash('You are required to change your password', 'warning',
496 h.flash('You are required to change your password', 'warning',
496 ignore_duplicate=True)
497 ignore_duplicate=True)
497
498
498 skip_user_check_urls = [
499 skip_user_check_urls = [
499 'error.document', 'login.logout', 'login.index',
500 'error.document', 'login.logout', 'login.index',
500 'admin/my_account.my_account_password',
501 'admin/my_account.my_account_password',
501 'admin/my_account.my_account_password_update'
502 'admin/my_account.my_account_password_update'
502 ]
503 ]
503 if _route_name not in skip_user_check_urls:
504 if _route_name not in skip_user_check_urls:
504 return self._dispatch_redirect(
505 return self._dispatch_redirect(
505 url('my_account_password'), environ, start_response)
506 url('my_account_password'), environ, start_response)
506
507
507 return WSGIController.__call__(self, environ, start_response)
508 return WSGIController.__call__(self, environ, start_response)
508
509
509
510
510 class BaseRepoController(BaseController):
511 class BaseRepoController(BaseController):
511 """
512 """
512 Base class for controllers responsible for loading all needed data for
513 Base class for controllers responsible for loading all needed data for
513 repository loaded items are
514 repository loaded items are
514
515
515 c.rhodecode_repo: instance of scm repository
516 c.rhodecode_repo: instance of scm repository
516 c.rhodecode_db_repo: instance of db
517 c.rhodecode_db_repo: instance of db
517 c.repository_requirements_missing: shows that repository specific data
518 c.repository_requirements_missing: shows that repository specific data
518 could not be displayed due to the missing requirements
519 could not be displayed due to the missing requirements
519 c.repository_pull_requests: show number of open pull requests
520 c.repository_pull_requests: show number of open pull requests
520 """
521 """
521
522
522 def __before__(self):
523 def __before__(self):
523 super(BaseRepoController, self).__before__()
524 super(BaseRepoController, self).__before__()
524 if c.repo_name: # extracted from routes
525 if c.repo_name: # extracted from routes
525 db_repo = Repository.get_by_repo_name(c.repo_name)
526 db_repo = Repository.get_by_repo_name(c.repo_name)
526 if not db_repo:
527 if not db_repo:
527 return
528 return
528
529
529 log.debug(
530 log.debug(
530 'Found repository in database %s with state `%s`',
531 'Found repository in database %s with state `%s`',
531 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
532 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
532 route = getattr(request.environ.get('routes.route'), 'name', '')
533 route = getattr(request.environ.get('routes.route'), 'name', '')
533
534
534 # allow to delete repos that are somehow damages in filesystem
535 # allow to delete repos that are somehow damages in filesystem
535 if route in ['delete_repo']:
536 if route in ['delete_repo']:
536 return
537 return
537
538
538 if db_repo.repo_state in [Repository.STATE_PENDING]:
539 if db_repo.repo_state in [Repository.STATE_PENDING]:
539 if route in ['repo_creating_home']:
540 if route in ['repo_creating_home']:
540 return
541 return
541 check_url = url('repo_creating_home', repo_name=c.repo_name)
542 check_url = url('repo_creating_home', repo_name=c.repo_name)
542 return redirect(check_url)
543 return redirect(check_url)
543
544
544 self.rhodecode_db_repo = db_repo
545 self.rhodecode_db_repo = db_repo
545
546
546 missing_requirements = False
547 missing_requirements = False
547 try:
548 try:
548 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
549 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
549 except RepositoryRequirementError as e:
550 except RepositoryRequirementError as e:
550 missing_requirements = True
551 missing_requirements = True
551 self._handle_missing_requirements(e)
552 self._handle_missing_requirements(e)
552
553
553 if self.rhodecode_repo is None and not missing_requirements:
554 if self.rhodecode_repo is None and not missing_requirements:
554 log.error('%s this repository is present in database but it '
555 log.error('%s this repository is present in database but it '
555 'cannot be created as an scm instance', c.repo_name)
556 'cannot be created as an scm instance', c.repo_name)
556
557
557 h.flash(_(
558 h.flash(_(
558 "The repository at %(repo_name)s cannot be located.") %
559 "The repository at %(repo_name)s cannot be located.") %
559 {'repo_name': c.repo_name},
560 {'repo_name': c.repo_name},
560 category='error', ignore_duplicate=True)
561 category='error', ignore_duplicate=True)
561 redirect(url('home'))
562 redirect(url('home'))
562
563
563 # update last change according to VCS data
564 # update last change according to VCS data
564 if not missing_requirements:
565 if not missing_requirements:
565 commit = db_repo.get_commit(
566 commit = db_repo.get_commit(
566 pre_load=["author", "date", "message", "parents"])
567 pre_load=["author", "date", "message", "parents"])
567 db_repo.update_commit_cache(commit)
568 db_repo.update_commit_cache(commit)
568
569
569 # Prepare context
570 # Prepare context
570 c.rhodecode_db_repo = db_repo
571 c.rhodecode_db_repo = db_repo
571 c.rhodecode_repo = self.rhodecode_repo
572 c.rhodecode_repo = self.rhodecode_repo
572 c.repository_requirements_missing = missing_requirements
573 c.repository_requirements_missing = missing_requirements
573
574
574 self._update_global_counters(self.scm_model, db_repo)
575 self._update_global_counters(self.scm_model, db_repo)
575
576
576 def _update_global_counters(self, scm_model, db_repo):
577 def _update_global_counters(self, scm_model, db_repo):
577 """
578 """
578 Base variables that are exposed to every page of repository
579 Base variables that are exposed to every page of repository
579 """
580 """
580 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
581 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
581
582
582 def _handle_missing_requirements(self, error):
583 def _handle_missing_requirements(self, error):
583 self.rhodecode_repo = None
584 self.rhodecode_repo = None
584 log.error(
585 log.error(
585 'Requirements are missing for repository %s: %s',
586 'Requirements are missing for repository %s: %s',
586 c.repo_name, error.message)
587 c.repo_name, error.message)
587
588
588 summary_url = url('summary_home', repo_name=c.repo_name)
589 summary_url = url('summary_home', repo_name=c.repo_name)
589 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
590 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
590 settings_update_url = url('repo', repo_name=c.repo_name)
591 settings_update_url = url('repo', repo_name=c.repo_name)
591 path = request.path
592 path = request.path
592 should_redirect = (
593 should_redirect = (
593 path not in (summary_url, settings_update_url)
594 path not in (summary_url, settings_update_url)
594 and '/settings' not in path or path == statistics_url
595 and '/settings' not in path or path == statistics_url
595 )
596 )
596 if should_redirect:
597 if should_redirect:
597 redirect(summary_url)
598 redirect(summary_url)
@@ -1,526 +1,529 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 logging
27 import logging
28 import importlib
28 import importlib
29 import re
29 import re
30 from functools import wraps
30 from functools import wraps
31
31
32 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
32 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
33 from webob.exc import (
33 from webob.exc import (
34 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
34 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
35
35
36 import rhodecode
36 import rhodecode
37 from rhodecode.authentication.base import authenticate, VCS_TYPE
37 from rhodecode.authentication.base import authenticate, VCS_TYPE
38 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
38 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
39 from rhodecode.lib.base import BasicAuth, get_ip_addr, vcs_operation_context
39 from rhodecode.lib.base import BasicAuth, get_ip_addr, vcs_operation_context
40 from rhodecode.lib.exceptions import (
40 from rhodecode.lib.exceptions import (
41 HTTPLockedRC, HTTPRequirementError, UserCreationError,
41 HTTPLockedRC, HTTPRequirementError, UserCreationError,
42 NotAllowedToCreateUserError)
42 NotAllowedToCreateUserError)
43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
44 from rhodecode.lib.middleware import appenlight
44 from rhodecode.lib.middleware import appenlight
45 from rhodecode.lib.middleware.utils import scm_app_http
45 from rhodecode.lib.middleware.utils import scm_app_http
46 from rhodecode.lib.utils import (
46 from rhodecode.lib.utils import (
47 is_valid_repo, get_rhodecode_realm, get_rhodecode_base_path, SLUG_RE)
47 is_valid_repo, get_rhodecode_realm, get_rhodecode_base_path, SLUG_RE)
48 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
48 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
49 from rhodecode.lib.vcs.conf import settings as vcs_settings
49 from rhodecode.lib.vcs.conf import settings as vcs_settings
50 from rhodecode.lib.vcs.backends import base
50 from rhodecode.lib.vcs.backends import base
51 from rhodecode.model import meta
51 from rhodecode.model import meta
52 from rhodecode.model.db import User, Repository, PullRequest
52 from rhodecode.model.db import User, Repository, PullRequest
53 from rhodecode.model.scm import ScmModel
53 from rhodecode.model.scm import ScmModel
54 from rhodecode.model.pull_request import PullRequestModel
54 from rhodecode.model.pull_request import PullRequestModel
55
55
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 def initialize_generator(factory):
60 def initialize_generator(factory):
61 """
61 """
62 Initializes the returned generator by draining its first element.
62 Initializes the returned generator by draining its first element.
63
63
64 This can be used to give a generator an initializer, which is the code
64 This can be used to give a generator an initializer, which is the code
65 up to the first yield statement. This decorator enforces that the first
65 up to the first yield statement. This decorator enforces that the first
66 produced element has the value ``"__init__"`` to make its special
66 produced element has the value ``"__init__"`` to make its special
67 purpose very explicit in the using code.
67 purpose very explicit in the using code.
68 """
68 """
69
69
70 @wraps(factory)
70 @wraps(factory)
71 def wrapper(*args, **kwargs):
71 def wrapper(*args, **kwargs):
72 gen = factory(*args, **kwargs)
72 gen = factory(*args, **kwargs)
73 try:
73 try:
74 init = gen.next()
74 init = gen.next()
75 except StopIteration:
75 except StopIteration:
76 raise ValueError('Generator must yield at least one element.')
76 raise ValueError('Generator must yield at least one element.')
77 if init != "__init__":
77 if init != "__init__":
78 raise ValueError('First yielded element must be "__init__".')
78 raise ValueError('First yielded element must be "__init__".')
79 return gen
79 return gen
80 return wrapper
80 return wrapper
81
81
82
82
83 class SimpleVCS(object):
83 class SimpleVCS(object):
84 """Common functionality for SCM HTTP handlers."""
84 """Common functionality for SCM HTTP handlers."""
85
85
86 SCM = 'unknown'
86 SCM = 'unknown'
87
87
88 acl_repo_name = None
88 acl_repo_name = None
89 url_repo_name = None
89 url_repo_name = None
90 vcs_repo_name = None
90 vcs_repo_name = None
91
91
92 # We have to handle requests to shadow repositories different than requests
92 # We have to handle requests to shadow repositories different than requests
93 # to normal repositories. Therefore we have to distinguish them. To do this
93 # to normal repositories. Therefore we have to distinguish them. To do this
94 # we use this regex which will match only on URLs pointing to shadow
94 # we use this regex which will match only on URLs pointing to shadow
95 # repositories.
95 # repositories.
96 shadow_repo_re = re.compile(
96 shadow_repo_re = re.compile(
97 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
97 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
98 '(?P<target>{slug_pat})/' # target repo
98 '(?P<target>{slug_pat})/' # target repo
99 'pull-request/(?P<pr_id>\d+)/' # pull request
99 'pull-request/(?P<pr_id>\d+)/' # pull request
100 'repository$' # shadow repo
100 'repository$' # shadow repo
101 .format(slug_pat=SLUG_RE.pattern))
101 .format(slug_pat=SLUG_RE.pattern))
102
102
103 def __init__(self, application, config, registry):
103 def __init__(self, application, config, registry):
104 self.registry = registry
104 self.registry = registry
105 self.application = application
105 self.application = application
106 self.config = config
106 self.config = config
107 # re-populated by specialized middleware
107 # re-populated by specialized middleware
108 self.repo_vcs_config = base.Config()
108 self.repo_vcs_config = base.Config()
109
109
110 # base path of repo locations
110 # base path of repo locations
111 self.basepath = get_rhodecode_base_path()
111 self.basepath = get_rhodecode_base_path()
112 # authenticate this VCS request using authfunc
112 # authenticate this VCS request using authfunc
113 auth_ret_code_detection = \
113 auth_ret_code_detection = \
114 str2bool(self.config.get('auth_ret_code_detection', False))
114 str2bool(self.config.get('auth_ret_code_detection', False))
115 self.authenticate = BasicAuth(
115 self.authenticate = BasicAuth(
116 '', authenticate, registry, config.get('auth_ret_code'),
116 '', authenticate, registry, config.get('auth_ret_code'),
117 auth_ret_code_detection)
117 auth_ret_code_detection)
118 self.ip_addr = '0.0.0.0'
118 self.ip_addr = '0.0.0.0'
119
119
120 def set_repo_names(self, environ):
120 def set_repo_names(self, environ):
121 """
121 """
122 This will populate the attributes acl_repo_name, url_repo_name,
122 This will populate the attributes acl_repo_name, url_repo_name,
123 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
123 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
124 shadow) repositories all names are equal. In case of requests to a
124 shadow) repositories all names are equal. In case of requests to a
125 shadow repository the acl-name points to the target repo of the pull
125 shadow repository the acl-name points to the target repo of the pull
126 request and the vcs-name points to the shadow repo file system path.
126 request and the vcs-name points to the shadow repo file system path.
127 The url-name is always the URL used by the vcs client program.
127 The url-name is always the URL used by the vcs client program.
128
128
129 Example in case of a shadow repo:
129 Example in case of a shadow repo:
130 acl_repo_name = RepoGroup/MyRepo
130 acl_repo_name = RepoGroup/MyRepo
131 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
131 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
132 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
132 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
133 """
133 """
134 # First we set the repo name from URL for all attributes. This is the
134 # First we set the repo name from URL for all attributes. This is the
135 # default if handling normal (non shadow) repo requests.
135 # default if handling normal (non shadow) repo requests.
136 self.url_repo_name = self._get_repository_name(environ)
136 self.url_repo_name = self._get_repository_name(environ)
137 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
137 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
138 self.is_shadow_repo = False
138 self.is_shadow_repo = False
139
139
140 # Check if this is a request to a shadow repository.
140 # Check if this is a request to a shadow repository.
141 match = self.shadow_repo_re.match(self.url_repo_name)
141 match = self.shadow_repo_re.match(self.url_repo_name)
142 if match:
142 if match:
143 match_dict = match.groupdict()
143 match_dict = match.groupdict()
144
144
145 # Build acl repo name from regex match.
145 # Build acl repo name from regex match.
146 acl_repo_name = safe_unicode('{groups}{target}'.format(
146 acl_repo_name = safe_unicode('{groups}{target}'.format(
147 groups=match_dict['groups'] or '',
147 groups=match_dict['groups'] or '',
148 target=match_dict['target']))
148 target=match_dict['target']))
149
149
150 # Retrieve pull request instance by ID from regex match.
150 # Retrieve pull request instance by ID from regex match.
151 pull_request = PullRequest.get(match_dict['pr_id'])
151 pull_request = PullRequest.get(match_dict['pr_id'])
152
152
153 # Only proceed if we got a pull request and if acl repo name from
153 # Only proceed if we got a pull request and if acl repo name from
154 # URL equals the target repo name of the pull request.
154 # URL equals the target repo name of the pull request.
155 if pull_request and (acl_repo_name ==
155 if pull_request and (acl_repo_name ==
156 pull_request.target_repo.repo_name):
156 pull_request.target_repo.repo_name):
157 # Get file system path to shadow repository.
157 # Get file system path to shadow repository.
158 workspace_id = PullRequestModel()._workspace_id(pull_request)
158 workspace_id = PullRequestModel()._workspace_id(pull_request)
159 target_vcs = pull_request.target_repo.scm_instance()
159 target_vcs = pull_request.target_repo.scm_instance()
160 vcs_repo_name = target_vcs._get_shadow_repository_path(
160 vcs_repo_name = target_vcs._get_shadow_repository_path(
161 workspace_id)
161 workspace_id)
162
162
163 # Store names for later usage.
163 # Store names for later usage.
164 self.vcs_repo_name = vcs_repo_name
164 self.vcs_repo_name = vcs_repo_name
165 self.acl_repo_name = acl_repo_name
165 self.acl_repo_name = acl_repo_name
166 self.is_shadow_repo = True
166 self.is_shadow_repo = True
167
167
168 log.debug('Setting all VCS repository names: %s', {
168 log.debug('Setting all VCS repository names: %s', {
169 'acl_repo_name': self.acl_repo_name,
169 'acl_repo_name': self.acl_repo_name,
170 'url_repo_name': self.url_repo_name,
170 'url_repo_name': self.url_repo_name,
171 'vcs_repo_name': self.vcs_repo_name,
171 'vcs_repo_name': self.vcs_repo_name,
172 })
172 })
173
173
174 @property
174 @property
175 def scm_app(self):
175 def scm_app(self):
176 custom_implementation = self.config['vcs.scm_app_implementation']
176 custom_implementation = self.config['vcs.scm_app_implementation']
177 if custom_implementation == 'http':
177 if custom_implementation == 'http':
178 log.info('Using HTTP implementation of scm app.')
178 log.info('Using HTTP implementation of scm app.')
179 scm_app_impl = scm_app_http
179 scm_app_impl = scm_app_http
180 else:
180 else:
181 log.info('Using custom implementation of scm_app: "{}"'.format(
181 log.info('Using custom implementation of scm_app: "{}"'.format(
182 custom_implementation))
182 custom_implementation))
183 scm_app_impl = importlib.import_module(custom_implementation)
183 scm_app_impl = importlib.import_module(custom_implementation)
184 return scm_app_impl
184 return scm_app_impl
185
185
186 def _get_by_id(self, repo_name):
186 def _get_by_id(self, repo_name):
187 """
187 """
188 Gets a special pattern _<ID> from clone url and tries to replace it
188 Gets a special pattern _<ID> from clone url and tries to replace it
189 with a repository_name for support of _<ID> non changeable urls
189 with a repository_name for support of _<ID> non changeable urls
190 """
190 """
191
191
192 data = repo_name.split('/')
192 data = repo_name.split('/')
193 if len(data) >= 2:
193 if len(data) >= 2:
194 from rhodecode.model.repo import RepoModel
194 from rhodecode.model.repo import RepoModel
195 by_id_match = RepoModel().get_repo_by_id(repo_name)
195 by_id_match = RepoModel().get_repo_by_id(repo_name)
196 if by_id_match:
196 if by_id_match:
197 data[1] = by_id_match.repo_name
197 data[1] = by_id_match.repo_name
198
198
199 return safe_str('/'.join(data))
199 return safe_str('/'.join(data))
200
200
201 def _invalidate_cache(self, repo_name):
201 def _invalidate_cache(self, repo_name):
202 """
202 """
203 Set's cache for this repository for invalidation on next access
203 Set's cache for this repository for invalidation on next access
204
204
205 :param repo_name: full repo name, also a cache key
205 :param repo_name: full repo name, also a cache key
206 """
206 """
207 ScmModel().mark_for_invalidation(repo_name)
207 ScmModel().mark_for_invalidation(repo_name)
208
208
209 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
209 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
210 db_repo = Repository.get_by_repo_name(repo_name)
210 db_repo = Repository.get_by_repo_name(repo_name)
211 if not db_repo:
211 if not db_repo:
212 log.debug('Repository `%s` not found inside the database.',
212 log.debug('Repository `%s` not found inside the database.',
213 repo_name)
213 repo_name)
214 return False
214 return False
215
215
216 if db_repo.repo_type != scm_type:
216 if db_repo.repo_type != scm_type:
217 log.warning(
217 log.warning(
218 'Repository `%s` have incorrect scm_type, expected %s got %s',
218 'Repository `%s` have incorrect scm_type, expected %s got %s',
219 repo_name, db_repo.repo_type, scm_type)
219 repo_name, db_repo.repo_type, scm_type)
220 return False
220 return False
221
221
222 return is_valid_repo(repo_name, base_path, explicit_scm=scm_type)
222 return is_valid_repo(repo_name, base_path, explicit_scm=scm_type)
223
223
224 def valid_and_active_user(self, user):
224 def valid_and_active_user(self, user):
225 """
225 """
226 Checks if that user is not empty, and if it's actually object it checks
226 Checks if that user is not empty, and if it's actually object it checks
227 if he's active.
227 if he's active.
228
228
229 :param user: user object or None
229 :param user: user object or None
230 :return: boolean
230 :return: boolean
231 """
231 """
232 if user is None:
232 if user is None:
233 return False
233 return False
234
234
235 elif user.active:
235 elif user.active:
236 return True
236 return True
237
237
238 return False
238 return False
239
239
240 def _check_permission(self, action, user, repo_name, ip_addr=None):
240 def _check_permission(self, action, user, repo_name, ip_addr=None):
241 """
241 """
242 Checks permissions using action (push/pull) user and repository
242 Checks permissions using action (push/pull) user and repository
243 name
243 name
244
244
245 :param action: push or pull action
245 :param action: push or pull action
246 :param user: user instance
246 :param user: user instance
247 :param repo_name: repository name
247 :param repo_name: repository name
248 """
248 """
249 # check IP
249 # check IP
250 inherit = user.inherit_default_permissions
250 inherit = user.inherit_default_permissions
251 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
251 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
252 inherit_from_default=inherit)
252 inherit_from_default=inherit)
253 if ip_allowed:
253 if ip_allowed:
254 log.info('Access for IP:%s allowed', ip_addr)
254 log.info('Access for IP:%s allowed', ip_addr)
255 else:
255 else:
256 return False
256 return False
257
257
258 if action == 'push':
258 if action == 'push':
259 if not HasPermissionAnyMiddleware('repository.write',
259 if not HasPermissionAnyMiddleware('repository.write',
260 'repository.admin')(user,
260 'repository.admin')(user,
261 repo_name):
261 repo_name):
262 return False
262 return False
263
263
264 else:
264 else:
265 # any other action need at least read permission
265 # any other action need at least read permission
266 if not HasPermissionAnyMiddleware('repository.read',
266 if not HasPermissionAnyMiddleware('repository.read',
267 'repository.write',
267 'repository.write',
268 'repository.admin')(user,
268 'repository.admin')(user,
269 repo_name):
269 repo_name):
270 return False
270 return False
271
271
272 return True
272 return True
273
273
274 def _check_ssl(self, environ, start_response):
274 def _check_ssl(self, environ, start_response):
275 """
275 """
276 Checks the SSL check flag and returns False if SSL is not present
276 Checks the SSL check flag and returns False if SSL is not present
277 and required True otherwise
277 and required True otherwise
278 """
278 """
279 org_proto = environ['wsgi._org_proto']
279 org_proto = environ['wsgi._org_proto']
280 # check if we have SSL required ! if not it's a bad request !
280 # check if we have SSL required ! if not it's a bad request !
281 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
281 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
282 if require_ssl and org_proto == 'http':
282 if require_ssl and org_proto == 'http':
283 log.debug('proto is %s and SSL is required BAD REQUEST !',
283 log.debug('proto is %s and SSL is required BAD REQUEST !',
284 org_proto)
284 org_proto)
285 return False
285 return False
286 return True
286 return True
287
287
288 def __call__(self, environ, start_response):
288 def __call__(self, environ, start_response):
289 try:
289 try:
290 return self._handle_request(environ, start_response)
290 return self._handle_request(environ, start_response)
291 except Exception:
291 except Exception:
292 log.exception("Exception while handling request")
292 log.exception("Exception while handling request")
293 appenlight.track_exception(environ)
293 appenlight.track_exception(environ)
294 return HTTPInternalServerError()(environ, start_response)
294 return HTTPInternalServerError()(environ, start_response)
295 finally:
295 finally:
296 meta.Session.remove()
296 meta.Session.remove()
297
297
298 def _handle_request(self, environ, start_response):
298 def _handle_request(self, environ, start_response):
299
299
300 if not self._check_ssl(environ, start_response):
300 if not self._check_ssl(environ, start_response):
301 reason = ('SSL required, while RhodeCode was unable '
301 reason = ('SSL required, while RhodeCode was unable '
302 'to detect this as SSL request')
302 'to detect this as SSL request')
303 log.debug('User not allowed to proceed, %s', reason)
303 log.debug('User not allowed to proceed, %s', reason)
304 return HTTPNotAcceptable(reason)(environ, start_response)
304 return HTTPNotAcceptable(reason)(environ, start_response)
305
305
306 if not self.url_repo_name:
306 if not self.url_repo_name:
307 log.warning('Repository name is empty: %s', self.url_repo_name)
307 log.warning('Repository name is empty: %s', self.url_repo_name)
308 # failed to get repo name, we fail now
308 # failed to get repo name, we fail now
309 return HTTPNotFound()(environ, start_response)
309 return HTTPNotFound()(environ, start_response)
310 log.debug('Extracted repo name is %s', self.url_repo_name)
310 log.debug('Extracted repo name is %s', self.url_repo_name)
311
311
312 ip_addr = get_ip_addr(environ)
312 ip_addr = get_ip_addr(environ)
313 username = None
313 username = None
314
314
315 # skip passing error to error controller
315 # skip passing error to error controller
316 environ['pylons.status_code_redirect'] = True
316 environ['pylons.status_code_redirect'] = True
317
317
318 # ======================================================================
318 # ======================================================================
319 # GET ACTION PULL or PUSH
319 # GET ACTION PULL or PUSH
320 # ======================================================================
320 # ======================================================================
321 action = self._get_action(environ)
321 action = self._get_action(environ)
322
322
323 # ======================================================================
323 # ======================================================================
324 # Check if this is a request to a shadow repository of a pull request.
324 # Check if this is a request to a shadow repository of a pull request.
325 # In this case only pull action is allowed.
325 # In this case only pull action is allowed.
326 # ======================================================================
326 # ======================================================================
327 if self.is_shadow_repo and action != 'pull':
327 if self.is_shadow_repo and action != 'pull':
328 reason = 'Only pull action is allowed for shadow repositories.'
328 reason = 'Only pull action is allowed for shadow repositories.'
329 log.debug('User not allowed to proceed, %s', reason)
329 log.debug('User not allowed to proceed, %s', reason)
330 return HTTPNotAcceptable(reason)(environ, start_response)
330 return HTTPNotAcceptable(reason)(environ, start_response)
331
331
332 # ======================================================================
332 # ======================================================================
333 # CHECK ANONYMOUS PERMISSION
333 # CHECK ANONYMOUS PERMISSION
334 # ======================================================================
334 # ======================================================================
335 if action in ['pull', 'push']:
335 if action in ['pull', 'push']:
336 anonymous_user = User.get_default_user()
336 anonymous_user = User.get_default_user()
337 username = anonymous_user.username
337 username = anonymous_user.username
338 if anonymous_user.active:
338 if anonymous_user.active:
339 # ONLY check permissions if the user is activated
339 # ONLY check permissions if the user is activated
340 anonymous_perm = self._check_permission(
340 anonymous_perm = self._check_permission(
341 action, anonymous_user, self.acl_repo_name, ip_addr)
341 action, anonymous_user, self.acl_repo_name, ip_addr)
342 else:
342 else:
343 anonymous_perm = False
343 anonymous_perm = False
344
344
345 if not anonymous_user.active or not anonymous_perm:
345 if not anonymous_user.active or not anonymous_perm:
346 if not anonymous_user.active:
346 if not anonymous_user.active:
347 log.debug('Anonymous access is disabled, running '
347 log.debug('Anonymous access is disabled, running '
348 'authentication')
348 'authentication')
349
349
350 if not anonymous_perm:
350 if not anonymous_perm:
351 log.debug('Not enough credentials to access this '
351 log.debug('Not enough credentials to access this '
352 'repository as anonymous user')
352 'repository as anonymous user')
353
353
354 username = None
354 username = None
355 # ==============================================================
355 # ==============================================================
356 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
356 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
357 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
357 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
358 # ==============================================================
358 # ==============================================================
359
359
360 # try to auth based on environ, container auth methods
360 # try to auth based on environ, container auth methods
361 log.debug('Running PRE-AUTH for container based authentication')
361 log.debug('Running PRE-AUTH for container based authentication')
362 pre_auth = authenticate(
362 pre_auth = authenticate(
363 '', '', environ, VCS_TYPE, registry=self.registry)
363 '', '', environ, VCS_TYPE, registry=self.registry,
364 acl_repo_name=self.acl_repo_name)
364 if pre_auth and pre_auth.get('username'):
365 if pre_auth and pre_auth.get('username'):
365 username = pre_auth['username']
366 username = pre_auth['username']
366 log.debug('PRE-AUTH got %s as username', username)
367 log.debug('PRE-AUTH got %s as username', username)
367
368
368 # If not authenticated by the container, running basic auth
369 # If not authenticated by the container, running basic auth
370 # before inject the calling repo_name for special scope checks
371 self.authenticate.acl_repo_name = self.acl_repo_name
369 if not username:
372 if not username:
370 self.authenticate.realm = get_rhodecode_realm()
373 self.authenticate.realm = get_rhodecode_realm()
371
374
372 try:
375 try:
373 result = self.authenticate(environ)
376 result = self.authenticate(environ)
374 except (UserCreationError, NotAllowedToCreateUserError) as e:
377 except (UserCreationError, NotAllowedToCreateUserError) as e:
375 log.error(e)
378 log.error(e)
376 reason = safe_str(e)
379 reason = safe_str(e)
377 return HTTPNotAcceptable(reason)(environ, start_response)
380 return HTTPNotAcceptable(reason)(environ, start_response)
378
381
379 if isinstance(result, str):
382 if isinstance(result, str):
380 AUTH_TYPE.update(environ, 'basic')
383 AUTH_TYPE.update(environ, 'basic')
381 REMOTE_USER.update(environ, result)
384 REMOTE_USER.update(environ, result)
382 username = result
385 username = result
383 else:
386 else:
384 return result.wsgi_application(environ, start_response)
387 return result.wsgi_application(environ, start_response)
385
388
386 # ==============================================================
389 # ==============================================================
387 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
390 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
388 # ==============================================================
391 # ==============================================================
389 user = User.get_by_username(username)
392 user = User.get_by_username(username)
390 if not self.valid_and_active_user(user):
393 if not self.valid_and_active_user(user):
391 return HTTPForbidden()(environ, start_response)
394 return HTTPForbidden()(environ, start_response)
392 username = user.username
395 username = user.username
393 user.update_lastactivity()
396 user.update_lastactivity()
394 meta.Session().commit()
397 meta.Session().commit()
395
398
396 # check user attributes for password change flag
399 # check user attributes for password change flag
397 user_obj = user
400 user_obj = user
398 if user_obj and user_obj.username != User.DEFAULT_USER and \
401 if user_obj and user_obj.username != User.DEFAULT_USER and \
399 user_obj.user_data.get('force_password_change'):
402 user_obj.user_data.get('force_password_change'):
400 reason = 'password change required'
403 reason = 'password change required'
401 log.debug('User not allowed to authenticate, %s', reason)
404 log.debug('User not allowed to authenticate, %s', reason)
402 return HTTPNotAcceptable(reason)(environ, start_response)
405 return HTTPNotAcceptable(reason)(environ, start_response)
403
406
404 # check permissions for this repository
407 # check permissions for this repository
405 perm = self._check_permission(
408 perm = self._check_permission(
406 action, user, self.acl_repo_name, ip_addr)
409 action, user, self.acl_repo_name, ip_addr)
407 if not perm:
410 if not perm:
408 return HTTPForbidden()(environ, start_response)
411 return HTTPForbidden()(environ, start_response)
409
412
410 # extras are injected into UI object and later available
413 # extras are injected into UI object and later available
411 # in hooks executed by rhodecode
414 # in hooks executed by rhodecode
412 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
415 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
413 extras = vcs_operation_context(
416 extras = vcs_operation_context(
414 environ, repo_name=self.acl_repo_name, username=username,
417 environ, repo_name=self.acl_repo_name, username=username,
415 action=action, scm=self.SCM, check_locking=check_locking,
418 action=action, scm=self.SCM, check_locking=check_locking,
416 is_shadow_repo=self.is_shadow_repo
419 is_shadow_repo=self.is_shadow_repo
417 )
420 )
418
421
419 # ======================================================================
422 # ======================================================================
420 # REQUEST HANDLING
423 # REQUEST HANDLING
421 # ======================================================================
424 # ======================================================================
422 repo_path = os.path.join(
425 repo_path = os.path.join(
423 safe_str(self.basepath), safe_str(self.vcs_repo_name))
426 safe_str(self.basepath), safe_str(self.vcs_repo_name))
424 log.debug('Repository path is %s', repo_path)
427 log.debug('Repository path is %s', repo_path)
425
428
426 fix_PATH()
429 fix_PATH()
427
430
428 log.info(
431 log.info(
429 '%s action on %s repo "%s" by "%s" from %s',
432 '%s action on %s repo "%s" by "%s" from %s',
430 action, self.SCM, safe_str(self.url_repo_name),
433 action, self.SCM, safe_str(self.url_repo_name),
431 safe_str(username), ip_addr)
434 safe_str(username), ip_addr)
432
435
433 return self._generate_vcs_response(
436 return self._generate_vcs_response(
434 environ, start_response, repo_path, extras, action)
437 environ, start_response, repo_path, extras, action)
435
438
436 @initialize_generator
439 @initialize_generator
437 def _generate_vcs_response(
440 def _generate_vcs_response(
438 self, environ, start_response, repo_path, extras, action):
441 self, environ, start_response, repo_path, extras, action):
439 """
442 """
440 Returns a generator for the response content.
443 Returns a generator for the response content.
441
444
442 This method is implemented as a generator, so that it can trigger
445 This method is implemented as a generator, so that it can trigger
443 the cache validation after all content sent back to the client. It
446 the cache validation after all content sent back to the client. It
444 also handles the locking exceptions which will be triggered when
447 also handles the locking exceptions which will be triggered when
445 the first chunk is produced by the underlying WSGI application.
448 the first chunk is produced by the underlying WSGI application.
446 """
449 """
447 callback_daemon, extras = self._prepare_callback_daemon(extras)
450 callback_daemon, extras = self._prepare_callback_daemon(extras)
448 config = self._create_config(extras, self.acl_repo_name)
451 config = self._create_config(extras, self.acl_repo_name)
449 log.debug('HOOKS extras is %s', extras)
452 log.debug('HOOKS extras is %s', extras)
450 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
453 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
451
454
452 try:
455 try:
453 with callback_daemon:
456 with callback_daemon:
454 try:
457 try:
455 response = app(environ, start_response)
458 response = app(environ, start_response)
456 finally:
459 finally:
457 # This statement works together with the decorator
460 # This statement works together with the decorator
458 # "initialize_generator" above. The decorator ensures that
461 # "initialize_generator" above. The decorator ensures that
459 # we hit the first yield statement before the generator is
462 # we hit the first yield statement before the generator is
460 # returned back to the WSGI server. This is needed to
463 # returned back to the WSGI server. This is needed to
461 # ensure that the call to "app" above triggers the
464 # ensure that the call to "app" above triggers the
462 # needed callback to "start_response" before the
465 # needed callback to "start_response" before the
463 # generator is actually used.
466 # generator is actually used.
464 yield "__init__"
467 yield "__init__"
465
468
466 for chunk in response:
469 for chunk in response:
467 yield chunk
470 yield chunk
468 except Exception as exc:
471 except Exception as exc:
469 # TODO: martinb: Exceptions are only raised in case of the Pyro4
472 # TODO: martinb: Exceptions are only raised in case of the Pyro4
470 # backend. Refactor this except block after dropping Pyro4 support.
473 # backend. Refactor this except block after dropping Pyro4 support.
471 # TODO: johbo: Improve "translating" back the exception.
474 # TODO: johbo: Improve "translating" back the exception.
472 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
475 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
473 exc = HTTPLockedRC(*exc.args)
476 exc = HTTPLockedRC(*exc.args)
474 _code = rhodecode.CONFIG.get('lock_ret_code')
477 _code = rhodecode.CONFIG.get('lock_ret_code')
475 log.debug('Repository LOCKED ret code %s!', (_code,))
478 log.debug('Repository LOCKED ret code %s!', (_code,))
476 elif getattr(exc, '_vcs_kind', None) == 'requirement':
479 elif getattr(exc, '_vcs_kind', None) == 'requirement':
477 log.debug(
480 log.debug(
478 'Repository requires features unknown to this Mercurial')
481 'Repository requires features unknown to this Mercurial')
479 exc = HTTPRequirementError(*exc.args)
482 exc = HTTPRequirementError(*exc.args)
480 else:
483 else:
481 raise
484 raise
482
485
483 for chunk in exc(environ, start_response):
486 for chunk in exc(environ, start_response):
484 yield chunk
487 yield chunk
485 finally:
488 finally:
486 # invalidate cache on push
489 # invalidate cache on push
487 try:
490 try:
488 if action == 'push':
491 if action == 'push':
489 self._invalidate_cache(self.url_repo_name)
492 self._invalidate_cache(self.url_repo_name)
490 finally:
493 finally:
491 meta.Session.remove()
494 meta.Session.remove()
492
495
493 def _get_repository_name(self, environ):
496 def _get_repository_name(self, environ):
494 """Get repository name out of the environmnent
497 """Get repository name out of the environmnent
495
498
496 :param environ: WSGI environment
499 :param environ: WSGI environment
497 """
500 """
498 raise NotImplementedError()
501 raise NotImplementedError()
499
502
500 def _get_action(self, environ):
503 def _get_action(self, environ):
501 """Map request commands into a pull or push command.
504 """Map request commands into a pull or push command.
502
505
503 :param environ: WSGI environment
506 :param environ: WSGI environment
504 """
507 """
505 raise NotImplementedError()
508 raise NotImplementedError()
506
509
507 def _create_wsgi_app(self, repo_path, repo_name, config):
510 def _create_wsgi_app(self, repo_path, repo_name, config):
508 """Return the WSGI app that will finally handle the request."""
511 """Return the WSGI app that will finally handle the request."""
509 raise NotImplementedError()
512 raise NotImplementedError()
510
513
511 def _create_config(self, extras, repo_name):
514 def _create_config(self, extras, repo_name):
512 """Create a safe config representation."""
515 """Create a safe config representation."""
513 raise NotImplementedError()
516 raise NotImplementedError()
514
517
515 def _prepare_callback_daemon(self, extras):
518 def _prepare_callback_daemon(self, extras):
516 return prepare_callback_daemon(
519 return prepare_callback_daemon(
517 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
520 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
518 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
521 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
519
522
520
523
521 def _should_check_locking(query_string):
524 def _should_check_locking(query_string):
522 # this is kind of hacky, but due to how mercurial handles client-server
525 # this is kind of hacky, but due to how mercurial handles client-server
523 # server see all operation on commit; bookmarks, phases and
526 # server see all operation on commit; bookmarks, phases and
524 # obsolescence marker in different transaction, we don't want to check
527 # obsolescence marker in different transaction, we don't want to check
525 # locking on those
528 # locking on those
526 return query_string not in ['cmd=listkeys']
529 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,257 +1,267 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 py.test config for test suite for making push/pull operations.
22 py.test config for test suite for making push/pull operations.
23
23
24 .. important::
24 .. important::
25
25
26 You must have git >= 1.8.5 for tests to work fine. With 68b939b git started
26 You must have git >= 1.8.5 for tests to work fine. With 68b939b git started
27 to redirect things to stderr instead of stdout.
27 to redirect things to stderr instead of stdout.
28 """
28 """
29
29
30 import ConfigParser
30 import ConfigParser
31 import os
31 import os
32 import subprocess32
32 import subprocess32
33 import tempfile
33 import tempfile
34 import textwrap
34 import textwrap
35 import pytest
35 import pytest
36
36
37 import rhodecode
37 import rhodecode
38 from rhodecode.model.db import Repository
38 from rhodecode.model.db import Repository
39 from rhodecode.model.meta import Session
39 from rhodecode.model.meta import Session
40 from rhodecode.model.settings import SettingsModel
40 from rhodecode.model.settings import SettingsModel
41 from rhodecode.tests import (
41 from rhodecode.tests import (
42 GIT_REPO, HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,)
42 GIT_REPO, HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,)
43 from rhodecode.tests.fixture import Fixture
43 from rhodecode.tests.fixture import Fixture
44 from rhodecode.tests.utils import (
44 from rhodecode.tests.utils import (
45 set_anonymous_access, is_url_reachable, wait_for_url)
45 set_anonymous_access, is_url_reachable, wait_for_url)
46
46
47 RC_LOG = os.path.join(tempfile.gettempdir(), 'rc.log')
47 RC_LOG = os.path.join(tempfile.gettempdir(), 'rc.log')
48 REPO_GROUP = 'a_repo_group'
48 REPO_GROUP = 'a_repo_group'
49 HG_REPO_WITH_GROUP = '%s/%s' % (REPO_GROUP, HG_REPO)
49 HG_REPO_WITH_GROUP = '%s/%s' % (REPO_GROUP, HG_REPO)
50 GIT_REPO_WITH_GROUP = '%s/%s' % (REPO_GROUP, GIT_REPO)
50 GIT_REPO_WITH_GROUP = '%s/%s' % (REPO_GROUP, GIT_REPO)
51
51
52
52
53 def assert_no_running_instance(url):
53 def assert_no_running_instance(url):
54 if is_url_reachable(url):
54 if is_url_reachable(url):
55 print("Hint: Usually this means another instance of Enterprise "
55 print("Hint: Usually this means another instance of Enterprise "
56 "is running in the background.")
56 "is running in the background.")
57 pytest.fail(
57 pytest.fail(
58 "Port is not free at %s, cannot start web interface" % url)
58 "Port is not free at %s, cannot start web interface" % url)
59
59
60
60
61 def get_host_url(pylons_config):
61 def get_host_url(pylons_config):
62 """Construct the host url using the port in the test configuration."""
62 """Construct the host url using the port in the test configuration."""
63 config = ConfigParser.ConfigParser()
63 config = ConfigParser.ConfigParser()
64 config.read(pylons_config)
64 config.read(pylons_config)
65
65
66 return '127.0.0.1:%s' % config.get('server:main', 'port')
66 return '127.0.0.1:%s' % config.get('server:main', 'port')
67
67
68
68
69 class RcWebServer(object):
69 class RcWebServer(object):
70 """
70 """
71 Represents a running RCE web server used as a test fixture.
71 Represents a running RCE web server used as a test fixture.
72 """
72 """
73 def __init__(self, pylons_config):
73 def __init__(self, pylons_config):
74 self.pylons_config = pylons_config
74 self.pylons_config = pylons_config
75
75
76 def repo_clone_url(self, repo_name, **kwargs):
76 def repo_clone_url(self, repo_name, **kwargs):
77 params = {
77 params = {
78 'user': TEST_USER_ADMIN_LOGIN,
78 'user': TEST_USER_ADMIN_LOGIN,
79 'passwd': TEST_USER_ADMIN_PASS,
79 'passwd': TEST_USER_ADMIN_PASS,
80 'host': get_host_url(self.pylons_config),
80 'host': get_host_url(self.pylons_config),
81 'cloned_repo': repo_name,
81 'cloned_repo': repo_name,
82 }
82 }
83 params.update(**kwargs)
83 params.update(**kwargs)
84 _url = 'http://%(user)s:%(passwd)s@%(host)s/%(cloned_repo)s' % params
84 _url = 'http://%(user)s:%(passwd)s@%(host)s/%(cloned_repo)s' % params
85 return _url
85 return _url
86
86
87
87
88 @pytest.fixture(scope="module")
88 @pytest.fixture(scope="module")
89 def rcextensions(request, pylonsapp, tmpdir_factory):
89 def rcextensions(request, pylonsapp, tmpdir_factory):
90 """
90 """
91 Installs a testing rcextensions pack to ensure they work as expected.
91 Installs a testing rcextensions pack to ensure they work as expected.
92 """
92 """
93 init_content = textwrap.dedent("""
93 init_content = textwrap.dedent("""
94 # Forward import the example rcextensions to make it
94 # Forward import the example rcextensions to make it
95 # active for our tests.
95 # active for our tests.
96 from rhodecode.tests.other.example_rcextensions import *
96 from rhodecode.tests.other.example_rcextensions import *
97 """)
97 """)
98
98
99 # Note: rcextensions are looked up based on the path of the ini file
99 # Note: rcextensions are looked up based on the path of the ini file
100 root_path = tmpdir_factory.getbasetemp()
100 root_path = tmpdir_factory.getbasetemp()
101 rcextensions_path = root_path.join('rcextensions')
101 rcextensions_path = root_path.join('rcextensions')
102 init_path = rcextensions_path.join('__init__.py')
102 init_path = rcextensions_path.join('__init__.py')
103
103
104 if rcextensions_path.check():
104 if rcextensions_path.check():
105 pytest.fail(
105 pytest.fail(
106 "Path for rcextensions already exists, please clean up before "
106 "Path for rcextensions already exists, please clean up before "
107 "test run this path: %s" % (rcextensions_path, ))
107 "test run this path: %s" % (rcextensions_path, ))
108 return
108 return
109
109
110 request.addfinalizer(rcextensions_path.remove)
110 request.addfinalizer(rcextensions_path.remove)
111 init_path.write_binary(init_content, ensure=True)
111 init_path.write_binary(init_content, ensure=True)
112
112
113
113
114 @pytest.fixture(scope="module")
114 @pytest.fixture(scope="module")
115 def repos(request, pylonsapp):
115 def repos(request, pylonsapp):
116 """Create a copy of each test repo in a repo group."""
116 """Create a copy of each test repo in a repo group."""
117 fixture = Fixture()
117 fixture = Fixture()
118 repo_group = fixture.create_repo_group(REPO_GROUP)
118 repo_group = fixture.create_repo_group(REPO_GROUP)
119 repo_group_id = repo_group.group_id
119 repo_group_id = repo_group.group_id
120 fixture.create_fork(HG_REPO, HG_REPO,
120 fixture.create_fork(HG_REPO, HG_REPO,
121 repo_name_full=HG_REPO_WITH_GROUP,
121 repo_name_full=HG_REPO_WITH_GROUP,
122 repo_group=repo_group_id)
122 repo_group=repo_group_id)
123 fixture.create_fork(GIT_REPO, GIT_REPO,
123 fixture.create_fork(GIT_REPO, GIT_REPO,
124 repo_name_full=GIT_REPO_WITH_GROUP,
124 repo_name_full=GIT_REPO_WITH_GROUP,
125 repo_group=repo_group_id)
125 repo_group=repo_group_id)
126
126
127 @request.addfinalizer
127 @request.addfinalizer
128 def cleanup():
128 def cleanup():
129 fixture.destroy_repo(HG_REPO_WITH_GROUP)
129 fixture.destroy_repo(HG_REPO_WITH_GROUP)
130 fixture.destroy_repo(GIT_REPO_WITH_GROUP)
130 fixture.destroy_repo(GIT_REPO_WITH_GROUP)
131 fixture.destroy_repo_group(repo_group_id)
131 fixture.destroy_repo_group(repo_group_id)
132
132
133
133
134 @pytest.fixture(scope="module")
134 @pytest.fixture(scope="module")
135 def rc_web_server_config(pylons_config):
135 def rc_web_server_config(testini_factory):
136 """
136 """
137 Configuration file used for the fixture `rc_web_server`.
137 Configuration file used for the fixture `rc_web_server`.
138 """
138 """
139 return pylons_config
139 CUSTOM_PARAMS = [
140 {'handler_console': {'level': 'DEBUG'}},
141 ]
142 return testini_factory(CUSTOM_PARAMS)
140
143
141
144
142 @pytest.fixture(scope="module")
145 @pytest.fixture(scope="module")
143 def rc_web_server(
146 def rc_web_server(
144 request, pylonsapp, rc_web_server_config, repos, rcextensions):
147 request, pylonsapp, rc_web_server_config, repos, rcextensions):
145 """
148 """
146 Run the web server as a subprocess.
149 Run the web server as a subprocess.
147
150
148 Since we have already a running vcsserver, this is not spawned again.
151 Since we have already a running vcsserver, this is not spawned again.
149 """
152 """
150 env = os.environ.copy()
153 env = os.environ.copy()
151 env['RC_NO_TMP_PATH'] = '1'
154 env['RC_NO_TMP_PATH'] = '1'
152
155
153 server_out = open(RC_LOG, 'w')
156 rc_log = RC_LOG
157 server_out = open(rc_log, 'w')
154
158
155 # TODO: Would be great to capture the output and err of the subprocess
159 # TODO: Would be great to capture the output and err of the subprocess
156 # and make it available in a section of the py.test report in case of an
160 # and make it available in a section of the py.test report in case of an
157 # error.
161 # error.
158
162
159 host_url = 'http://' + get_host_url(rc_web_server_config)
163 host_url = 'http://' + get_host_url(rc_web_server_config)
160 assert_no_running_instance(host_url)
164 assert_no_running_instance(host_url)
161 command = ['rcserver', rc_web_server_config]
165 command = ['pserve', rc_web_server_config]
162
166
163 print('Starting rcserver: {}'.format(host_url))
167 print('Starting rcserver: {}'.format(host_url))
164 print('Command: {}'.format(command))
168 print('Command: {}'.format(command))
165 print('Logfile: {}'.format(RC_LOG))
169 print('Logfile: {}'.format(rc_log))
166
170
167 proc = subprocess32.Popen(
171 proc = subprocess32.Popen(
168 command, bufsize=0, env=env, stdout=server_out, stderr=server_out)
172 command, bufsize=0, env=env, stdout=server_out, stderr=server_out)
169
173
170 wait_for_url(host_url, timeout=30)
174 wait_for_url(host_url, timeout=30)
171
175
172 @request.addfinalizer
176 @request.addfinalizer
173 def stop_web_server():
177 def stop_web_server():
174 # TODO: Find out how to integrate with the reporting of py.test to
178 # TODO: Find out how to integrate with the reporting of py.test to
175 # make this information available.
179 # make this information available.
176 print "\nServer log file written to %s" % (RC_LOG, )
180 print("\nServer log file written to %s" % (rc_log, ))
177 proc.kill()
181 proc.kill()
182 server_out.flush()
178 server_out.close()
183 server_out.close()
179
184
180 return RcWebServer(rc_web_server_config)
185 return RcWebServer(rc_web_server_config)
181
186
182
187
183 @pytest.fixture(scope='class', autouse=True)
188 @pytest.fixture(scope='class', autouse=True)
184 def disable_anonymous_user_access(pylonsapp):
189 def disable_anonymous_user_access(pylonsapp):
185 set_anonymous_access(False)
190 set_anonymous_access(False)
186
191
187
192
188 @pytest.fixture
193 @pytest.fixture
189 def disable_locking(pylonsapp):
194 def disable_locking(pylonsapp):
190 r = Repository.get_by_repo_name(GIT_REPO)
195 r = Repository.get_by_repo_name(GIT_REPO)
191 Repository.unlock(r)
196 Repository.unlock(r)
192 r.enable_locking = False
197 r.enable_locking = False
193 Session().add(r)
198 Session().add(r)
194 Session().commit()
199 Session().commit()
195
200
196 r = Repository.get_by_repo_name(HG_REPO)
201 r = Repository.get_by_repo_name(HG_REPO)
197 Repository.unlock(r)
202 Repository.unlock(r)
198 r.enable_locking = False
203 r.enable_locking = False
199 Session().add(r)
204 Session().add(r)
200 Session().commit()
205 Session().commit()
201
206
202
207
203 @pytest.fixture
208 @pytest.fixture
204 def enable_auth_plugins(request, pylonsapp, csrf_token):
209 def enable_auth_plugins(request, pylonsapp, csrf_token):
205 """
210 """
206 Return a factory object that when called, allows to control which
211 Return a factory object that when called, allows to control which
207 authentication plugins are enabled.
212 authentication plugins are enabled.
208 """
213 """
209 def _enable_plugins(plugins_list, override=None):
214 def _enable_plugins(plugins_list, override=None):
210 override = override or {}
215 override = override or {}
211 params = {
216 params = {
212 'auth_plugins': ','.join(plugins_list),
217 'auth_plugins': ','.join(plugins_list),
213 'csrf_token': csrf_token,
218 }
219
220 # helper translate some names to others
221 name_map = {
222 'token': 'authtoken'
214 }
223 }
215
224
216 for module in plugins_list:
225 for module in plugins_list:
217 plugin = rhodecode.authentication.base.loadplugin(module)
226 plugin_name = module.partition('#')[-1]
218 plugin_name = plugin.name
227 if plugin_name in name_map:
228 plugin_name = name_map[plugin_name]
219 enabled_plugin = 'auth_%s_enabled' % plugin_name
229 enabled_plugin = 'auth_%s_enabled' % plugin_name
220 cache_ttl = 'auth_%s_cache_ttl' % plugin_name
230 cache_ttl = 'auth_%s_cache_ttl' % plugin_name
221
231
222 # default params that are needed for each plugin,
232 # default params that are needed for each plugin,
223 # `enabled` and `cache_ttl`
233 # `enabled` and `cache_ttl`
224 params.update({
234 params.update({
225 enabled_plugin: True,
235 enabled_plugin: True,
226 cache_ttl: 0
236 cache_ttl: 0
227 })
237 })
228 if override.get:
238 if override.get:
229 params.update(override.get(module, {}))
239 params.update(override.get(module, {}))
230
240
231 validated_params = params
241 validated_params = params
232 for k, v in validated_params.items():
242 for k, v in validated_params.items():
233 setting = SettingsModel().create_or_update_setting(k, v)
243 setting = SettingsModel().create_or_update_setting(k, v)
234 Session().add(setting)
244 Session().add(setting)
235 Session().commit()
245 Session().commit()
236
246
237 def cleanup():
247 def cleanup():
238 _enable_plugins(['egg:rhodecode-enterprise-ce#rhodecode'])
248 _enable_plugins(['egg:rhodecode-enterprise-ce#rhodecode'])
239
249
240 request.addfinalizer(cleanup)
250 request.addfinalizer(cleanup)
241
251
242 return _enable_plugins
252 return _enable_plugins
243
253
244
254
245 @pytest.fixture
255 @pytest.fixture
246 def fs_repo_only(request, rhodecode_fixtures):
256 def fs_repo_only(request, rhodecode_fixtures):
247 def fs_repo_fabric(repo_name, repo_type):
257 def fs_repo_fabric(repo_name, repo_type):
248 rhodecode_fixtures.create_repo(repo_name, repo_type=repo_type)
258 rhodecode_fixtures.create_repo(repo_name, repo_type=repo_type)
249 rhodecode_fixtures.destroy_repo(repo_name, fs_remove=False)
259 rhodecode_fixtures.destroy_repo(repo_name, fs_remove=False)
250
260
251 def cleanup():
261 def cleanup():
252 rhodecode_fixtures.destroy_repo(repo_name, fs_remove=True)
262 rhodecode_fixtures.destroy_repo(repo_name, fs_remove=True)
253 rhodecode_fixtures.destroy_repo_on_filesystem(repo_name)
263 rhodecode_fixtures.destroy_repo_on_filesystem(repo_name)
254
264
255 request.addfinalizer(cleanup)
265 request.addfinalizer(cleanup)
256
266
257 return fs_repo_fabric
267 return fs_repo_fabric
@@ -1,364 +1,474 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 Test suite for making push/pull operations, on specially modified INI files
22 Test suite for making push/pull operations, on specially modified INI files
23
23
24 .. important::
24 .. important::
25
25
26 You must have git >= 1.8.5 for tests to work fine. With 68b939b git started
26 You must have git >= 1.8.5 for tests to work fine. With 68b939b git started
27 to redirect things to stderr instead of stdout.
27 to redirect things to stderr instead of stdout.
28 """
28 """
29
29
30
30
31 import os
31 import os
32 import time
32 import time
33
33
34 import pytest
34 import pytest
35
35
36 from rhodecode.lib.vcs.backends.git.repository import GitRepository
36 from rhodecode.lib.vcs.backends.git.repository import GitRepository
37 from rhodecode.lib.vcs.nodes import FileNode
37 from rhodecode.lib.vcs.nodes import FileNode
38 from rhodecode.model.auth_token import AuthTokenModel
38 from rhodecode.model.db import Repository, UserIpMap, CacheKey
39 from rhodecode.model.db import Repository, UserIpMap, CacheKey
39 from rhodecode.model.meta import Session
40 from rhodecode.model.meta import Session
40 from rhodecode.model.user import UserModel
41 from rhodecode.model.user import UserModel
41 from rhodecode.tests import (GIT_REPO, HG_REPO, TEST_USER_ADMIN_LOGIN)
42 from rhodecode.tests import (GIT_REPO, HG_REPO, TEST_USER_ADMIN_LOGIN)
42
43
43 from rhodecode.tests.other.vcs_operations import (
44 from rhodecode.tests.other.vcs_operations import (
44 Command, _check_proper_clone, _check_proper_git_push, _add_files_and_push,
45 Command, _check_proper_clone, _check_proper_git_push, _add_files_and_push,
45 HG_REPO_WITH_GROUP, GIT_REPO_WITH_GROUP)
46 HG_REPO_WITH_GROUP, GIT_REPO_WITH_GROUP)
46
47
47
48
48 @pytest.mark.usefixtures("disable_locking")
49 @pytest.mark.usefixtures("disable_locking")
49 class TestVCSOperations:
50 class TestVCSOperations(object):
50
51
51 def test_clone_hg_repo_by_admin(self, rc_web_server, tmpdir):
52 def test_clone_hg_repo_by_admin(self, rc_web_server, tmpdir):
52 clone_url = rc_web_server.repo_clone_url(HG_REPO)
53 clone_url = rc_web_server.repo_clone_url(HG_REPO)
53 stdout, stderr = Command('/tmp').execute(
54 stdout, stderr = Command('/tmp').execute(
54 'hg clone', clone_url, tmpdir.strpath)
55 'hg clone', clone_url, tmpdir.strpath)
55 _check_proper_clone(stdout, stderr, 'hg')
56 _check_proper_clone(stdout, stderr, 'hg')
56
57
57 def test_clone_git_repo_by_admin(self, rc_web_server, tmpdir):
58 def test_clone_git_repo_by_admin(self, rc_web_server, tmpdir):
58 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
59 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
59 cmd = Command('/tmp')
60 cmd = Command('/tmp')
60 stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath)
61 stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath)
61 _check_proper_clone(stdout, stderr, 'git')
62 _check_proper_clone(stdout, stderr, 'git')
62 cmd.assert_returncode_success()
63 cmd.assert_returncode_success()
63
64
64 def test_clone_hg_repo_by_id_by_admin(self, rc_web_server, tmpdir):
65 def test_clone_hg_repo_by_id_by_admin(self, rc_web_server, tmpdir):
65 repo_id = Repository.get_by_repo_name(HG_REPO).repo_id
66 repo_id = Repository.get_by_repo_name(HG_REPO).repo_id
66 clone_url = rc_web_server.repo_clone_url('_%s' % repo_id)
67 clone_url = rc_web_server.repo_clone_url('_%s' % repo_id)
67 stdout, stderr = Command('/tmp').execute(
68 stdout, stderr = Command('/tmp').execute(
68 'hg clone', clone_url, tmpdir.strpath)
69 'hg clone', clone_url, tmpdir.strpath)
69 _check_proper_clone(stdout, stderr, 'hg')
70 _check_proper_clone(stdout, stderr, 'hg')
70
71
71 def test_clone_git_repo_by_id_by_admin(self, rc_web_server, tmpdir):
72 def test_clone_git_repo_by_id_by_admin(self, rc_web_server, tmpdir):
72 repo_id = Repository.get_by_repo_name(GIT_REPO).repo_id
73 repo_id = Repository.get_by_repo_name(GIT_REPO).repo_id
73 clone_url = rc_web_server.repo_clone_url('_%s' % repo_id)
74 clone_url = rc_web_server.repo_clone_url('_%s' % repo_id)
74 cmd = Command('/tmp')
75 cmd = Command('/tmp')
75 stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath)
76 stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath)
76 _check_proper_clone(stdout, stderr, 'git')
77 _check_proper_clone(stdout, stderr, 'git')
77 cmd.assert_returncode_success()
78 cmd.assert_returncode_success()
78
79
79 def test_clone_hg_repo_with_group_by_admin(self, rc_web_server, tmpdir):
80 def test_clone_hg_repo_with_group_by_admin(self, rc_web_server, tmpdir):
80 clone_url = rc_web_server.repo_clone_url(HG_REPO_WITH_GROUP)
81 clone_url = rc_web_server.repo_clone_url(HG_REPO_WITH_GROUP)
81 stdout, stderr = Command('/tmp').execute(
82 stdout, stderr = Command('/tmp').execute(
82 'hg clone', clone_url, tmpdir.strpath)
83 'hg clone', clone_url, tmpdir.strpath)
83 _check_proper_clone(stdout, stderr, 'hg')
84 _check_proper_clone(stdout, stderr, 'hg')
84
85
85 def test_clone_git_repo_with_group_by_admin(self, rc_web_server, tmpdir):
86 def test_clone_git_repo_with_group_by_admin(self, rc_web_server, tmpdir):
86 clone_url = rc_web_server.repo_clone_url(GIT_REPO_WITH_GROUP)
87 clone_url = rc_web_server.repo_clone_url(GIT_REPO_WITH_GROUP)
87 cmd = Command('/tmp')
88 cmd = Command('/tmp')
88 stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath)
89 stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath)
89 _check_proper_clone(stdout, stderr, 'git')
90 _check_proper_clone(stdout, stderr, 'git')
90 cmd.assert_returncode_success()
91 cmd.assert_returncode_success()
91
92
92 def test_clone_git_repo_shallow_by_admin(self, rc_web_server, tmpdir):
93 def test_clone_git_repo_shallow_by_admin(self, rc_web_server, tmpdir):
93 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
94 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
94 cmd = Command('/tmp')
95 cmd = Command('/tmp')
95 stdout, stderr = cmd.execute(
96 stdout, stderr = cmd.execute(
96 'git clone --depth=1', clone_url, tmpdir.strpath)
97 'git clone --depth=1', clone_url, tmpdir.strpath)
97
98
98 assert '' == stdout
99 assert '' == stdout
99 assert 'Cloning into' in stderr
100 assert 'Cloning into' in stderr
100 cmd.assert_returncode_success()
101 cmd.assert_returncode_success()
101
102
102 def test_clone_wrong_credentials_hg(self, rc_web_server, tmpdir):
103 def test_clone_wrong_credentials_hg(self, rc_web_server, tmpdir):
103 clone_url = rc_web_server.repo_clone_url(HG_REPO, passwd='bad!')
104 clone_url = rc_web_server.repo_clone_url(HG_REPO, passwd='bad!')
104 stdout, stderr = Command('/tmp').execute(
105 stdout, stderr = Command('/tmp').execute(
105 'hg clone', clone_url, tmpdir.strpath)
106 'hg clone', clone_url, tmpdir.strpath)
106 assert 'abort: authorization failed' in stderr
107 assert 'abort: authorization failed' in stderr
107
108
108 def test_clone_wrong_credentials_git(self, rc_web_server, tmpdir):
109 def test_clone_wrong_credentials_git(self, rc_web_server, tmpdir):
109 clone_url = rc_web_server.repo_clone_url(GIT_REPO, passwd='bad!')
110 clone_url = rc_web_server.repo_clone_url(GIT_REPO, passwd='bad!')
110 stdout, stderr = Command('/tmp').execute(
111 stdout, stderr = Command('/tmp').execute(
111 'git clone', clone_url, tmpdir.strpath)
112 'git clone', clone_url, tmpdir.strpath)
112 assert 'fatal: Authentication failed' in stderr
113 assert 'fatal: Authentication failed' in stderr
113
114
114 def test_clone_git_dir_as_hg(self, rc_web_server, tmpdir):
115 def test_clone_git_dir_as_hg(self, rc_web_server, tmpdir):
115 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
116 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
116 stdout, stderr = Command('/tmp').execute(
117 stdout, stderr = Command('/tmp').execute(
117 'hg clone', clone_url, tmpdir.strpath)
118 'hg clone', clone_url, tmpdir.strpath)
118 assert 'HTTP Error 404: Not Found' in stderr
119 assert 'HTTP Error 404: Not Found' in stderr
119
120
120 def test_clone_hg_repo_as_git(self, rc_web_server, tmpdir):
121 def test_clone_hg_repo_as_git(self, rc_web_server, tmpdir):
121 clone_url = rc_web_server.repo_clone_url(HG_REPO)
122 clone_url = rc_web_server.repo_clone_url(HG_REPO)
122 stdout, stderr = Command('/tmp').execute(
123 stdout, stderr = Command('/tmp').execute(
123 'git clone', clone_url, tmpdir.strpath)
124 'git clone', clone_url, tmpdir.strpath)
124 assert 'not found' in stderr
125 assert 'not found' in stderr
125
126
126 def test_clone_non_existing_path_hg(self, rc_web_server, tmpdir):
127 def test_clone_non_existing_path_hg(self, rc_web_server, tmpdir):
127 clone_url = rc_web_server.repo_clone_url('trololo')
128 clone_url = rc_web_server.repo_clone_url('trololo')
128 stdout, stderr = Command('/tmp').execute(
129 stdout, stderr = Command('/tmp').execute(
129 'hg clone', clone_url, tmpdir.strpath)
130 'hg clone', clone_url, tmpdir.strpath)
130 assert 'HTTP Error 404: Not Found' in stderr
131 assert 'HTTP Error 404: Not Found' in stderr
131
132
132 def test_clone_non_existing_path_git(self, rc_web_server, tmpdir):
133 def test_clone_non_existing_path_git(self, rc_web_server, tmpdir):
133 clone_url = rc_web_server.repo_clone_url('trololo')
134 clone_url = rc_web_server.repo_clone_url('trololo')
134 stdout, stderr = Command('/tmp').execute('git clone', clone_url)
135 stdout, stderr = Command('/tmp').execute('git clone', clone_url)
135 assert 'not found' in stderr
136 assert 'not found' in stderr
136
137
137 def test_clone_existing_path_hg_not_in_database(
138 def test_clone_existing_path_hg_not_in_database(
138 self, rc_web_server, tmpdir, fs_repo_only):
139 self, rc_web_server, tmpdir, fs_repo_only):
139
140
140 db_name = fs_repo_only('not-in-db-hg', repo_type='hg')
141 db_name = fs_repo_only('not-in-db-hg', repo_type='hg')
141 clone_url = rc_web_server.repo_clone_url(db_name)
142 clone_url = rc_web_server.repo_clone_url(db_name)
142 stdout, stderr = Command('/tmp').execute(
143 stdout, stderr = Command('/tmp').execute(
143 'hg clone', clone_url, tmpdir.strpath)
144 'hg clone', clone_url, tmpdir.strpath)
144 assert 'HTTP Error 404: Not Found' in stderr
145 assert 'HTTP Error 404: Not Found' in stderr
145
146
146 def test_clone_existing_path_git_not_in_database(
147 def test_clone_existing_path_git_not_in_database(
147 self, rc_web_server, tmpdir, fs_repo_only):
148 self, rc_web_server, tmpdir, fs_repo_only):
148 db_name = fs_repo_only('not-in-db-git', repo_type='git')
149 db_name = fs_repo_only('not-in-db-git', repo_type='git')
149 clone_url = rc_web_server.repo_clone_url(db_name)
150 clone_url = rc_web_server.repo_clone_url(db_name)
150 stdout, stderr = Command('/tmp').execute(
151 stdout, stderr = Command('/tmp').execute(
151 'git clone', clone_url, tmpdir.strpath)
152 'git clone', clone_url, tmpdir.strpath)
152 assert 'not found' in stderr
153 assert 'not found' in stderr
153
154
154 def test_clone_existing_path_hg_not_in_database_different_scm(
155 def test_clone_existing_path_hg_not_in_database_different_scm(
155 self, rc_web_server, tmpdir, fs_repo_only):
156 self, rc_web_server, tmpdir, fs_repo_only):
156 db_name = fs_repo_only('not-in-db-git', repo_type='git')
157 db_name = fs_repo_only('not-in-db-git', repo_type='git')
157 clone_url = rc_web_server.repo_clone_url(db_name)
158 clone_url = rc_web_server.repo_clone_url(db_name)
158 stdout, stderr = Command('/tmp').execute(
159 stdout, stderr = Command('/tmp').execute(
159 'hg clone', clone_url, tmpdir.strpath)
160 'hg clone', clone_url, tmpdir.strpath)
160 assert 'HTTP Error 404: Not Found' in stderr
161 assert 'HTTP Error 404: Not Found' in stderr
161
162
162 def test_clone_existing_path_git_not_in_database_different_scm(
163 def test_clone_existing_path_git_not_in_database_different_scm(
163 self, rc_web_server, tmpdir, fs_repo_only):
164 self, rc_web_server, tmpdir, fs_repo_only):
164 db_name = fs_repo_only('not-in-db-hg', repo_type='hg')
165 db_name = fs_repo_only('not-in-db-hg', repo_type='hg')
165 clone_url = rc_web_server.repo_clone_url(db_name)
166 clone_url = rc_web_server.repo_clone_url(db_name)
166 stdout, stderr = Command('/tmp').execute(
167 stdout, stderr = Command('/tmp').execute(
167 'git clone', clone_url, tmpdir.strpath)
168 'git clone', clone_url, tmpdir.strpath)
168 assert 'not found' in stderr
169 assert 'not found' in stderr
169
170
170 def test_push_new_file_hg(self, rc_web_server, tmpdir):
171 def test_push_new_file_hg(self, rc_web_server, tmpdir):
171 clone_url = rc_web_server.repo_clone_url(HG_REPO)
172 clone_url = rc_web_server.repo_clone_url(HG_REPO)
172 stdout, stderr = Command('/tmp').execute(
173 stdout, stderr = Command('/tmp').execute(
173 'hg clone', clone_url, tmpdir.strpath)
174 'hg clone', clone_url, tmpdir.strpath)
174
175
175 stdout, stderr = _add_files_and_push(
176 stdout, stderr = _add_files_and_push(
176 'hg', tmpdir.strpath, clone_url=clone_url)
177 'hg', tmpdir.strpath, clone_url=clone_url)
177
178
178 assert 'pushing to' in stdout
179 assert 'pushing to' in stdout
179 assert 'size summary' in stdout
180 assert 'size summary' in stdout
180
181
181 def test_push_new_file_git(self, rc_web_server, tmpdir):
182 def test_push_new_file_git(self, rc_web_server, tmpdir):
182 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
183 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
183 stdout, stderr = Command('/tmp').execute(
184 stdout, stderr = Command('/tmp').execute(
184 'git clone', clone_url, tmpdir.strpath)
185 'git clone', clone_url, tmpdir.strpath)
185
186
186 # commit some stuff into this repo
187 # commit some stuff into this repo
187 stdout, stderr = _add_files_and_push(
188 stdout, stderr = _add_files_and_push(
188 'git', tmpdir.strpath, clone_url=clone_url)
189 'git', tmpdir.strpath, clone_url=clone_url)
189
190
190 _check_proper_git_push(stdout, stderr)
191 _check_proper_git_push(stdout, stderr)
191
192
192 def test_push_invalidates_cache_hg(self, rc_web_server, tmpdir):
193 def test_push_invalidates_cache_hg(self, rc_web_server, tmpdir):
193 key = CacheKey.query().filter(CacheKey.cache_key == HG_REPO).scalar()
194 key = CacheKey.query().filter(CacheKey.cache_key == HG_REPO).scalar()
194 if not key:
195 if not key:
195 key = CacheKey(HG_REPO, HG_REPO)
196 key = CacheKey(HG_REPO, HG_REPO)
196
197
197 key.cache_active = True
198 key.cache_active = True
198 Session().add(key)
199 Session().add(key)
199 Session().commit()
200 Session().commit()
200
201
201 clone_url = rc_web_server.repo_clone_url(HG_REPO)
202 clone_url = rc_web_server.repo_clone_url(HG_REPO)
202 stdout, stderr = Command('/tmp').execute(
203 stdout, stderr = Command('/tmp').execute(
203 'hg clone', clone_url, tmpdir.strpath)
204 'hg clone', clone_url, tmpdir.strpath)
204
205
205 stdout, stderr = _add_files_and_push(
206 stdout, stderr = _add_files_and_push(
206 'hg', tmpdir.strpath, clone_url=clone_url, files_no=1)
207 'hg', tmpdir.strpath, clone_url=clone_url, files_no=1)
207
208
208 key = CacheKey.query().filter(CacheKey.cache_key == HG_REPO).one()
209 key = CacheKey.query().filter(CacheKey.cache_key == HG_REPO).one()
209 assert key.cache_active is False
210 assert key.cache_active is False
210
211
211 def test_push_invalidates_cache_git(self, rc_web_server, tmpdir):
212 def test_push_invalidates_cache_git(self, rc_web_server, tmpdir):
212 key = CacheKey.query().filter(CacheKey.cache_key == GIT_REPO).scalar()
213 key = CacheKey.query().filter(CacheKey.cache_key == GIT_REPO).scalar()
213 if not key:
214 if not key:
214 key = CacheKey(GIT_REPO, GIT_REPO)
215 key = CacheKey(GIT_REPO, GIT_REPO)
215
216
216 key.cache_active = True
217 key.cache_active = True
217 Session().add(key)
218 Session().add(key)
218 Session().commit()
219 Session().commit()
219
220
220 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
221 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
221 stdout, stderr = Command('/tmp').execute(
222 stdout, stderr = Command('/tmp').execute(
222 'git clone', clone_url, tmpdir.strpath)
223 'git clone', clone_url, tmpdir.strpath)
223
224
224 # commit some stuff into this repo
225 # commit some stuff into this repo
225 stdout, stderr = _add_files_and_push(
226 stdout, stderr = _add_files_and_push(
226 'git', tmpdir.strpath, clone_url=clone_url, files_no=1)
227 'git', tmpdir.strpath, clone_url=clone_url, files_no=1)
227 _check_proper_git_push(stdout, stderr)
228 _check_proper_git_push(stdout, stderr)
228
229
229 key = CacheKey.query().filter(CacheKey.cache_key == GIT_REPO).one()
230 key = CacheKey.query().filter(CacheKey.cache_key == GIT_REPO).one()
230
231
231 assert key.cache_active is False
232 assert key.cache_active is False
232
233
233 def test_push_wrong_credentials_hg(self, rc_web_server, tmpdir):
234 def test_push_wrong_credentials_hg(self, rc_web_server, tmpdir):
234 clone_url = rc_web_server.repo_clone_url(HG_REPO)
235 clone_url = rc_web_server.repo_clone_url(HG_REPO)
235 stdout, stderr = Command('/tmp').execute(
236 stdout, stderr = Command('/tmp').execute(
236 'hg clone', clone_url, tmpdir.strpath)
237 'hg clone', clone_url, tmpdir.strpath)
237
238
238 push_url = rc_web_server.repo_clone_url(
239 push_url = rc_web_server.repo_clone_url(
239 HG_REPO, user='bad', passwd='name')
240 HG_REPO, user='bad', passwd='name')
240 stdout, stderr = _add_files_and_push(
241 stdout, stderr = _add_files_and_push(
241 'hg', tmpdir.strpath, clone_url=push_url)
242 'hg', tmpdir.strpath, clone_url=push_url)
242
243
243 assert 'abort: authorization failed' in stderr
244 assert 'abort: authorization failed' in stderr
244
245
245 def test_push_wrong_credentials_git(self, rc_web_server, tmpdir):
246 def test_push_wrong_credentials_git(self, rc_web_server, tmpdir):
246 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
247 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
247 stdout, stderr = Command('/tmp').execute(
248 stdout, stderr = Command('/tmp').execute(
248 'git clone', clone_url, tmpdir.strpath)
249 'git clone', clone_url, tmpdir.strpath)
249
250
250 push_url = rc_web_server.repo_clone_url(
251 push_url = rc_web_server.repo_clone_url(
251 GIT_REPO, user='bad', passwd='name')
252 GIT_REPO, user='bad', passwd='name')
252 stdout, stderr = _add_files_and_push(
253 stdout, stderr = _add_files_and_push(
253 'git', tmpdir.strpath, clone_url=push_url)
254 'git', tmpdir.strpath, clone_url=push_url)
254
255
255 assert 'fatal: Authentication failed' in stderr
256 assert 'fatal: Authentication failed' in stderr
256
257
257 def test_push_back_to_wrong_url_hg(self, rc_web_server, tmpdir):
258 def test_push_back_to_wrong_url_hg(self, rc_web_server, tmpdir):
258 clone_url = rc_web_server.repo_clone_url(HG_REPO)
259 clone_url = rc_web_server.repo_clone_url(HG_REPO)
259 stdout, stderr = Command('/tmp').execute(
260 stdout, stderr = Command('/tmp').execute(
260 'hg clone', clone_url, tmpdir.strpath)
261 'hg clone', clone_url, tmpdir.strpath)
261
262
262 stdout, stderr = _add_files_and_push(
263 stdout, stderr = _add_files_and_push(
263 'hg', tmpdir.strpath,
264 'hg', tmpdir.strpath,
264 clone_url=rc_web_server.repo_clone_url('not-existing'))
265 clone_url=rc_web_server.repo_clone_url('not-existing'))
265
266
266 assert 'HTTP Error 404: Not Found' in stderr
267 assert 'HTTP Error 404: Not Found' in stderr
267
268
268 def test_push_back_to_wrong_url_git(self, rc_web_server, tmpdir):
269 def test_push_back_to_wrong_url_git(self, rc_web_server, tmpdir):
269 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
270 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
270 stdout, stderr = Command('/tmp').execute(
271 stdout, stderr = Command('/tmp').execute(
271 'git clone', clone_url, tmpdir.strpath)
272 'git clone', clone_url, tmpdir.strpath)
272
273
273 stdout, stderr = _add_files_and_push(
274 stdout, stderr = _add_files_and_push(
274 'git', tmpdir.strpath,
275 'git', tmpdir.strpath,
275 clone_url=rc_web_server.repo_clone_url('not-existing'))
276 clone_url=rc_web_server.repo_clone_url('not-existing'))
276
277
277 assert 'not found' in stderr
278 assert 'not found' in stderr
278
279
279 def test_ip_restriction_hg(self, rc_web_server, tmpdir):
280 def test_ip_restriction_hg(self, rc_web_server, tmpdir):
280 user_model = UserModel()
281 user_model = UserModel()
281 try:
282 try:
282 user_model.add_extra_ip(TEST_USER_ADMIN_LOGIN, '10.10.10.10/32')
283 user_model.add_extra_ip(TEST_USER_ADMIN_LOGIN, '10.10.10.10/32')
283 Session().commit()
284 Session().commit()
284 time.sleep(2)
285 time.sleep(2)
285 clone_url = rc_web_server.repo_clone_url(HG_REPO)
286 clone_url = rc_web_server.repo_clone_url(HG_REPO)
286 stdout, stderr = Command('/tmp').execute(
287 stdout, stderr = Command('/tmp').execute(
287 'hg clone', clone_url, tmpdir.strpath)
288 'hg clone', clone_url, tmpdir.strpath)
288 assert 'abort: HTTP Error 403: Forbidden' in stderr
289 assert 'abort: HTTP Error 403: Forbidden' in stderr
289 finally:
290 finally:
290 # release IP restrictions
291 # release IP restrictions
291 for ip in UserIpMap.getAll():
292 for ip in UserIpMap.getAll():
292 UserIpMap.delete(ip.ip_id)
293 UserIpMap.delete(ip.ip_id)
293 Session().commit()
294 Session().commit()
294
295
295 time.sleep(2)
296 time.sleep(2)
296
297
297 stdout, stderr = Command('/tmp').execute(
298 stdout, stderr = Command('/tmp').execute(
298 'hg clone', clone_url, tmpdir.strpath)
299 'hg clone', clone_url, tmpdir.strpath)
299 _check_proper_clone(stdout, stderr, 'hg')
300 _check_proper_clone(stdout, stderr, 'hg')
300
301
301 def test_ip_restriction_git(self, rc_web_server, tmpdir):
302 def test_ip_restriction_git(self, rc_web_server, tmpdir):
302 user_model = UserModel()
303 user_model = UserModel()
303 try:
304 try:
304 user_model.add_extra_ip(TEST_USER_ADMIN_LOGIN, '10.10.10.10/32')
305 user_model.add_extra_ip(TEST_USER_ADMIN_LOGIN, '10.10.10.10/32')
305 Session().commit()
306 Session().commit()
306 time.sleep(2)
307 time.sleep(2)
307 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
308 clone_url = rc_web_server.repo_clone_url(GIT_REPO)
308 stdout, stderr = Command('/tmp').execute(
309 stdout, stderr = Command('/tmp').execute(
309 'git clone', clone_url, tmpdir.strpath)
310 'git clone', clone_url, tmpdir.strpath)
310 msg = "The requested URL returned error: 403"
311 msg = "The requested URL returned error: 403"
311 assert msg in stderr
312 assert msg in stderr
312 finally:
313 finally:
313 # release IP restrictions
314 # release IP restrictions
314 for ip in UserIpMap.getAll():
315 for ip in UserIpMap.getAll():
315 UserIpMap.delete(ip.ip_id)
316 UserIpMap.delete(ip.ip_id)
316 Session().commit()
317 Session().commit()
317
318
318 time.sleep(2)
319 time.sleep(2)
319
320
320 cmd = Command('/tmp')
321 cmd = Command('/tmp')
321 stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath)
322 stdout, stderr = cmd.execute('git clone', clone_url, tmpdir.strpath)
322 cmd.assert_returncode_success()
323 cmd.assert_returncode_success()
323 _check_proper_clone(stdout, stderr, 'git')
324 _check_proper_clone(stdout, stderr, 'git')
324
325
326 def test_clone_by_auth_token(
327 self, rc_web_server, tmpdir, user_util, enable_auth_plugins):
328 enable_auth_plugins(['egg:rhodecode-enterprise-ce#token',
329 'egg:rhodecode-enterprise-ce#rhodecode'])
330
331 user = user_util.create_user()
332 token = user.auth_tokens[1]
333
334 clone_url = rc_web_server.repo_clone_url(
335 HG_REPO, user=user.username, passwd=token)
336
337 stdout, stderr = Command('/tmp').execute(
338 'hg clone', clone_url, tmpdir.strpath)
339 _check_proper_clone(stdout, stderr, 'hg')
340
341 def test_clone_by_auth_token_expired(
342 self, rc_web_server, tmpdir, user_util, enable_auth_plugins):
343 enable_auth_plugins(['egg:rhodecode-enterprise-ce#token',
344 'egg:rhodecode-enterprise-ce#rhodecode'])
345
346 user = user_util.create_user()
347 auth_token = AuthTokenModel().create(
348 user.user_id, 'test-token', -10, AuthTokenModel.cls.ROLE_VCS)
349 token = auth_token.api_key
350
351 clone_url = rc_web_server.repo_clone_url(
352 HG_REPO, user=user.username, passwd=token)
353
354 stdout, stderr = Command('/tmp').execute(
355 'hg clone', clone_url, tmpdir.strpath)
356 assert 'abort: authorization failed' in stderr
357
358 def test_clone_by_auth_token_bad_role(
359 self, rc_web_server, tmpdir, user_util, enable_auth_plugins):
360 enable_auth_plugins(['egg:rhodecode-enterprise-ce#token',
361 'egg:rhodecode-enterprise-ce#rhodecode'])
362
363 user = user_util.create_user()
364 auth_token = AuthTokenModel().create(
365 user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_API)
366 token = auth_token.api_key
367
368 clone_url = rc_web_server.repo_clone_url(
369 HG_REPO, user=user.username, passwd=token)
370
371 stdout, stderr = Command('/tmp').execute(
372 'hg clone', clone_url, tmpdir.strpath)
373 assert 'abort: authorization failed' in stderr
374
375 def test_clone_by_auth_token_user_disabled(
376 self, rc_web_server, tmpdir, user_util, enable_auth_plugins):
377 enable_auth_plugins(['egg:rhodecode-enterprise-ce#token',
378 'egg:rhodecode-enterprise-ce#rhodecode'])
379 user = user_util.create_user()
380 user.active = False
381 Session().add(user)
382 Session().commit()
383 token = user.auth_tokens[1]
384
385 clone_url = rc_web_server.repo_clone_url(
386 HG_REPO, user=user.username, passwd=token)
387
388 stdout, stderr = Command('/tmp').execute(
389 'hg clone', clone_url, tmpdir.strpath)
390 assert 'abort: authorization failed' in stderr
391
392
393 def test_clone_by_auth_token_with_scope(
394 self, rc_web_server, tmpdir, user_util, enable_auth_plugins):
395 enable_auth_plugins(['egg:rhodecode-enterprise-ce#token',
396 'egg:rhodecode-enterprise-ce#rhodecode'])
397 user = user_util.create_user()
398 auth_token = AuthTokenModel().create(
399 user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_VCS)
400 token = auth_token.api_key
401
402 # manually set scope
403 auth_token.repo = Repository.get_by_repo_name(HG_REPO)
404 Session().add(auth_token)
405 Session().commit()
406
407 clone_url = rc_web_server.repo_clone_url(
408 HG_REPO, user=user.username, passwd=token)
409
410 stdout, stderr = Command('/tmp').execute(
411 'hg clone', clone_url, tmpdir.strpath)
412 _check_proper_clone(stdout, stderr, 'hg')
413
414 def test_clone_by_auth_token_with_wrong_scope(
415 self, rc_web_server, tmpdir, user_util, enable_auth_plugins):
416 enable_auth_plugins(['egg:rhodecode-enterprise-ce#token',
417 'egg:rhodecode-enterprise-ce#rhodecode'])
418 user = user_util.create_user()
419 auth_token = AuthTokenModel().create(
420 user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_VCS)
421 token = auth_token.api_key
422
423 # manually set scope
424 auth_token.repo = Repository.get_by_repo_name(GIT_REPO)
425 Session().add(auth_token)
426 Session().commit()
427
428 clone_url = rc_web_server.repo_clone_url(
429 HG_REPO, user=user.username, passwd=token)
430
431 stdout, stderr = Command('/tmp').execute(
432 'hg clone', clone_url, tmpdir.strpath)
433 assert 'abort: authorization failed' in stderr
434
325
435
326 def test_git_sets_default_branch_if_not_master(
436 def test_git_sets_default_branch_if_not_master(
327 backend_git, tmpdir, disable_locking, rc_web_server):
437 backend_git, tmpdir, disable_locking, rc_web_server):
328 empty_repo = backend_git.create_repo()
438 empty_repo = backend_git.create_repo()
329 clone_url = rc_web_server.repo_clone_url(empty_repo.repo_name)
439 clone_url = rc_web_server.repo_clone_url(empty_repo.repo_name)
330
440
331 cmd = Command(tmpdir.strpath)
441 cmd = Command(tmpdir.strpath)
332 cmd.execute('git clone', clone_url)
442 cmd.execute('git clone', clone_url)
333
443
334 repo = GitRepository(os.path.join(tmpdir.strpath, empty_repo.repo_name))
444 repo = GitRepository(os.path.join(tmpdir.strpath, empty_repo.repo_name))
335 repo.in_memory_commit.add(FileNode('file', content=''))
445 repo.in_memory_commit.add(FileNode('file', content=''))
336 repo.in_memory_commit.commit(
446 repo.in_memory_commit.commit(
337 message='Commit on branch test',
447 message='Commit on branch test',
338 author='Automatic test',
448 author='Automatic test',
339 branch='test')
449 branch='test')
340
450
341 repo_cmd = Command(repo.path)
451 repo_cmd = Command(repo.path)
342 stdout, stderr = repo_cmd.execute('git push --verbose origin test')
452 stdout, stderr = repo_cmd.execute('git push --verbose origin test')
343 _check_proper_git_push(
453 _check_proper_git_push(
344 stdout, stderr, branch='test', should_set_default_branch=True)
454 stdout, stderr, branch='test', should_set_default_branch=True)
345
455
346 stdout, stderr = cmd.execute(
456 stdout, stderr = cmd.execute(
347 'git clone', clone_url, empty_repo.repo_name + '-clone')
457 'git clone', clone_url, empty_repo.repo_name + '-clone')
348 _check_proper_clone(stdout, stderr, 'git')
458 _check_proper_clone(stdout, stderr, 'git')
349
459
350 # Doing an explicit commit in order to get latest user logs on MySQL
460 # Doing an explicit commit in order to get latest user logs on MySQL
351 Session().commit()
461 Session().commit()
352
462
353
463
354 def test_git_fetches_from_remote_repository_with_annotated_tags(
464 def test_git_fetches_from_remote_repository_with_annotated_tags(
355 backend_git, disable_locking, rc_web_server):
465 backend_git, disable_locking, rc_web_server):
356 # Note: This is a test specific to the git backend. It checks the
466 # Note: This is a test specific to the git backend. It checks the
357 # integration of fetching from a remote repository which contains
467 # integration of fetching from a remote repository which contains
358 # annotated tags.
468 # annotated tags.
359
469
360 # Dulwich shows this specific behavior only when
470 # Dulwich shows this specific behavior only when
361 # operating against a remote repository.
471 # operating against a remote repository.
362 source_repo = backend_git['annotated-tag']
472 source_repo = backend_git['annotated-tag']
363 target_vcs_repo = backend_git.create_repo().scm_instance()
473 target_vcs_repo = backend_git.create_repo().scm_instance()
364 target_vcs_repo.fetch(rc_web_server.repo_clone_url(source_repo.repo_name))
474 target_vcs_repo.fetch(rc_web_server.repo_clone_url(source_repo.repo_name))
General Comments 0
You need to be logged in to leave comments. Login now