##// END OF EJS Templates
pyramid: added checks for password change for authenticated users.
marcink -
r1539:7998d3c5 default
parent child Browse files
Show More
@@ -1,81 +1,112 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 import time
21 22 import logging
22 23 from pylons import tmpl_context as c
23 24 from pyramid.httpexceptions import HTTPFound
24 25
25 from rhodecode.lib.utils2 import StrictAttributeDict
26 from rhodecode.lib import helpers as h
27 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int
28 from rhodecode.model.db import User
26 29
27 30 log = logging.getLogger(__name__)
28 31
29 32
30 33 ADMIN_PREFIX = '/_admin'
31 34 STATIC_FILE_PREFIX = '/_static'
32 35
33 36
34 37 class TemplateArgs(StrictAttributeDict):
35 38 pass
36 39
37 40
38 41 class BaseAppView(object):
39 42
40 43 def __init__(self, context, request):
41 44 self.request = request
42 45 self.context = context
43 46 self.session = request.session
44 47 self._rhodecode_user = request.user # auth user
45 48 self._rhodecode_db_user = self._rhodecode_user.get_instance()
49 self._maybe_needs_password_change(
50 request.matched_route.name, self._rhodecode_db_user)
51
52 def _maybe_needs_password_change(self, view_name, user_obj):
53 log.debug('Checking if user %s needs password change on view %s',
54 user_obj, view_name)
55 skip_user_views = [
56 'logout', 'login',
57 'my_account_password', 'my_account_password_update'
58 ]
59
60 if not user_obj:
61 return
62
63 if user_obj.username == User.DEFAULT_USER:
64 return
65
66 now = time.time()
67 should_change = user_obj.user_data.get('force_password_change')
68 change_after = safe_int(should_change) or 0
69 if should_change and now > change_after:
70 log.debug('User %s requires password change', user_obj)
71 h.flash('You are required to change your password', 'warning',
72 ignore_duplicate=True)
73
74 if view_name not in skip_user_views:
75 raise HTTPFound(
76 self.request.route_path('my_account_password'))
46 77
47 78 def _get_local_tmpl_context(self):
48 79 c = TemplateArgs()
49 80 c.auth_user = self.request.user
50 81 return c
51 82
52 83 def _register_global_c(self, tmpl_args):
53 84 """
54 85 Registers attributes to pylons global `c`
55 86 """
56 87 # TODO(marcink): remove once pyramid migration is finished
57 88 for k, v in tmpl_args.items():
58 89 setattr(c, k, v)
59 90
60 91 def _get_template_context(self, tmpl_args):
61 92 self._register_global_c(tmpl_args)
62 93
63 94 local_tmpl_args = {
64 95 'defaults': {},
65 96 'errors': {},
66 97 }
67 98 local_tmpl_args.update(tmpl_args)
68 99 return local_tmpl_args
69 100
70 101 def load_default_context(self):
71 102 """
72 103 example:
73 104
74 105 def load_default_context(self):
75 106 c = self._get_local_tmpl_context()
76 107 c.custom_var = 'foobar'
77 108 self._register_global_c(c)
78 109 return c
79 110 """
80 111 raise NotImplementedError('Needs implementation in view class')
81 112
@@ -1,128 +1,133 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import mock
23 23 import pytest
24 24
25 25 from rhodecode.apps.login.views import LoginView, CaptchaData
26 26 from rhodecode.config.routing import ADMIN_PREFIX
27 from rhodecode.lib.utils2 import AttributeDict
27 28 from rhodecode.model.settings import SettingsModel
28 29 from rhodecode.tests.utils import AssertResponse
29 30
30 31
31 32 class RhodeCodeSetting(object):
32 33 def __init__(self, name, value):
33 34 self.name = name
34 35 self.value = value
35 36
36 37 def __enter__(self):
37 38 from rhodecode.model.settings import SettingsModel
38 39 model = SettingsModel()
39 40 self.old_setting = model.get_setting_by_name(self.name)
40 41 model.create_or_update_setting(name=self.name, val=self.value)
41 42 return self
42 43
43 def __exit__(self, type, value, traceback):
44 def __exit__(self, exc_type, exc_val, exc_tb):
44 45 model = SettingsModel()
45 46 if self.old_setting:
46 47 model.create_or_update_setting(
47 48 name=self.name, val=self.old_setting.app_settings_value)
48 49 else:
49 50 model.create_or_update_setting(name=self.name)
50 51
51 52
52 53 class TestRegisterCaptcha(object):
53 54
54 55 @pytest.mark.parametrize('private_key, public_key, expected', [
55 56 ('', '', CaptchaData(False, '', '')),
56 57 ('', 'pubkey', CaptchaData(False, '', 'pubkey')),
57 58 ('privkey', '', CaptchaData(True, 'privkey', '')),
58 59 ('privkey', 'pubkey', CaptchaData(True, 'privkey', 'pubkey')),
59 60 ])
60 def test_get_captcha_data(self, private_key, public_key, expected, db):
61 login_view = LoginView(mock.Mock(), mock.Mock())
61 def test_get_captcha_data(self, private_key, public_key, expected, db,
62 request_stub, user_util):
63 request_stub.user = user_util.create_user().AuthUser
64 request_stub.matched_route = AttributeDict({'name': 'login'})
65 login_view = LoginView(mock.Mock(), request_stub)
66
62 67 with RhodeCodeSetting('captcha_private_key', private_key):
63 68 with RhodeCodeSetting('captcha_public_key', public_key):
64 69 captcha = login_view._get_captcha_data()
65 70 assert captcha == expected
66 71
67 72 @pytest.mark.parametrize('active', [False, True])
68 73 @mock.patch.object(LoginView, '_get_captcha_data')
69 74 def test_private_key_does_not_leak_to_html(
70 75 self, m_get_captcha_data, active, app):
71 76 captcha = CaptchaData(
72 77 active=active, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
73 78 m_get_captcha_data.return_value = captcha
74 79
75 80 response = app.get(ADMIN_PREFIX + '/register')
76 81 assert 'PRIVATE_KEY' not in response
77 82
78 83 @pytest.mark.parametrize('active', [False, True])
79 84 @mock.patch.object(LoginView, '_get_captcha_data')
80 85 def test_register_view_renders_captcha(
81 86 self, m_get_captcha_data, active, app):
82 87 captcha = CaptchaData(
83 88 active=active, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
84 89 m_get_captcha_data.return_value = captcha
85 90
86 91 response = app.get(ADMIN_PREFIX + '/register')
87 92
88 93 assertr = AssertResponse(response)
89 94 if active:
90 95 assertr.one_element_exists('#recaptcha_field')
91 96 else:
92 97 assertr.no_element_exists('#recaptcha_field')
93 98
94 99 @pytest.mark.parametrize('valid', [False, True])
95 100 @mock.patch('rhodecode.apps.login.views.submit')
96 101 @mock.patch.object(LoginView, '_get_captcha_data')
97 102 def test_register_with_active_captcha(
98 103 self, m_get_captcha_data, m_submit, valid, app, csrf_token):
99 104 captcha = CaptchaData(
100 105 active=True, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
101 106 m_get_captcha_data.return_value = captcha
102 107 m_response = mock.Mock()
103 108 m_response.is_valid = valid
104 109 m_submit.return_value = m_response
105 110
106 111 params = {
107 112 'csrf_token': csrf_token,
108 113 'email': 'pytest@example.com',
109 114 'firstname': 'pytest-firstname',
110 115 'lastname': 'pytest-lastname',
111 116 'password': 'secret',
112 117 'password_confirmation': 'secret',
113 118 'username': 'pytest',
114 119 }
115 120 response = app.post(ADMIN_PREFIX + '/register', params=params)
116 121
117 122 if valid:
118 123 # If we provided a valid captcha input we expect a successful
119 124 # registration and redirect to the login page.
120 125 assert response.status_int == 302
121 126 assert 'location' in response.headers
122 127 assert ADMIN_PREFIX + '/login' in response.headers['location']
123 128 else:
124 129 # If captche input is invalid we expect to stay on the registration
125 130 # page with an error message displayed.
126 131 assertr = AssertResponse(response)
127 132 assert response.status_int == 200
128 133 assertr.one_element_exists('#recaptcha_field ~ span.error-message')
@@ -1,598 +1,589 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 The base Controller API
23 23 Provides the BaseController class for subclassing. And usage in different
24 24 controllers
25 25 """
26 26
27 27 import logging
28 28 import socket
29 29
30 30 import ipaddress
31 31 import pyramid.threadlocal
32 32
33 33 from paste.auth.basic import AuthBasicAuthenticator
34 34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 36 from pylons import config, tmpl_context as c, request, session, url
37 37 from pylons.controllers import WSGIController
38 38 from pylons.controllers.util import redirect
39 39 from pylons.i18n import translation
40 40 # marcink: don't remove this import
41 41 from pylons.templating import render_mako as render # noqa
42 42 from pylons.i18n.translation import _
43 43 from webob.exc import HTTPFound
44 44
45 45
46 46 import rhodecode
47 47 from rhodecode.authentication.base import VCS_TYPE
48 48 from rhodecode.lib import auth, utils2
49 49 from rhodecode.lib import helpers as h
50 50 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
51 51 from rhodecode.lib.exceptions import UserCreationError
52 52 from rhodecode.lib.utils import (
53 53 get_repo_slug, set_rhodecode_config, password_changed,
54 54 get_enabled_hook_classes)
55 55 from rhodecode.lib.utils2 import (
56 56 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
57 57 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
58 58 from rhodecode.model import meta
59 59 from rhodecode.model.db import Repository, User, ChangesetComment
60 60 from rhodecode.model.notification import NotificationModel
61 61 from rhodecode.model.scm import ScmModel
62 62 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
63 63
64 64
65 65 log = logging.getLogger(__name__)
66 66
67 67
68 68 def _filter_proxy(ip):
69 69 """
70 70 Passed in IP addresses in HEADERS can be in a special format of multiple
71 71 ips. Those comma separated IPs are passed from various proxies in the
72 72 chain of request processing. The left-most being the original client.
73 73 We only care about the first IP which came from the org. client.
74 74
75 75 :param ip: ip string from headers
76 76 """
77 77 if ',' in ip:
78 78 _ips = ip.split(',')
79 79 _first_ip = _ips[0].strip()
80 80 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
81 81 return _first_ip
82 82 return ip
83 83
84 84
85 85 def _filter_port(ip):
86 86 """
87 87 Removes a port from ip, there are 4 main cases to handle here.
88 88 - ipv4 eg. 127.0.0.1
89 89 - ipv6 eg. ::1
90 90 - ipv4+port eg. 127.0.0.1:8080
91 91 - ipv6+port eg. [::1]:8080
92 92
93 93 :param ip:
94 94 """
95 95 def is_ipv6(ip_addr):
96 96 if hasattr(socket, 'inet_pton'):
97 97 try:
98 98 socket.inet_pton(socket.AF_INET6, ip_addr)
99 99 except socket.error:
100 100 return False
101 101 else:
102 102 # fallback to ipaddress
103 103 try:
104 104 ipaddress.IPv6Address(ip_addr)
105 105 except Exception:
106 106 return False
107 107 return True
108 108
109 109 if ':' not in ip: # must be ipv4 pure ip
110 110 return ip
111 111
112 112 if '[' in ip and ']' in ip: # ipv6 with port
113 113 return ip.split(']')[0][1:].lower()
114 114
115 115 # must be ipv6 or ipv4 with port
116 116 if is_ipv6(ip):
117 117 return ip
118 118 else:
119 119 ip, _port = ip.split(':')[:2] # means ipv4+port
120 120 return ip
121 121
122 122
123 123 def get_ip_addr(environ):
124 124 proxy_key = 'HTTP_X_REAL_IP'
125 125 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
126 126 def_key = 'REMOTE_ADDR'
127 127 _filters = lambda x: _filter_port(_filter_proxy(x))
128 128
129 129 ip = environ.get(proxy_key)
130 130 if ip:
131 131 return _filters(ip)
132 132
133 133 ip = environ.get(proxy_key2)
134 134 if ip:
135 135 return _filters(ip)
136 136
137 137 ip = environ.get(def_key, '0.0.0.0')
138 138 return _filters(ip)
139 139
140 140
141 141 def get_server_ip_addr(environ, log_errors=True):
142 142 hostname = environ.get('SERVER_NAME')
143 143 try:
144 144 return socket.gethostbyname(hostname)
145 145 except Exception as e:
146 146 if log_errors:
147 147 # in some cases this lookup is not possible, and we don't want to
148 148 # make it an exception in logs
149 149 log.exception('Could not retrieve server ip address: %s', e)
150 150 return hostname
151 151
152 152
153 153 def get_server_port(environ):
154 154 return environ.get('SERVER_PORT')
155 155
156 156
157 157 def get_access_path(environ):
158 158 path = environ.get('PATH_INFO')
159 159 org_req = environ.get('pylons.original_request')
160 160 if org_req:
161 161 path = org_req.environ.get('PATH_INFO')
162 162 return path
163 163
164 164
165 165 def vcs_operation_context(
166 166 environ, repo_name, username, action, scm, check_locking=True,
167 167 is_shadow_repo=False):
168 168 """
169 169 Generate the context for a vcs operation, e.g. push or pull.
170 170
171 171 This context is passed over the layers so that hooks triggered by the
172 172 vcs operation know details like the user, the user's IP address etc.
173 173
174 174 :param check_locking: Allows to switch of the computation of the locking
175 175 data. This serves mainly the need of the simplevcs middleware to be
176 176 able to disable this for certain operations.
177 177
178 178 """
179 179 # Tri-state value: False: unlock, None: nothing, True: lock
180 180 make_lock = None
181 181 locked_by = [None, None, None]
182 182 is_anonymous = username == User.DEFAULT_USER
183 183 if not is_anonymous and check_locking:
184 184 log.debug('Checking locking on repository "%s"', repo_name)
185 185 user = User.get_by_username(username)
186 186 repo = Repository.get_by_repo_name(repo_name)
187 187 make_lock, __, locked_by = repo.get_locking_state(
188 188 action, user.user_id)
189 189
190 190 settings_model = VcsSettingsModel(repo=repo_name)
191 191 ui_settings = settings_model.get_ui_settings()
192 192
193 193 extras = {
194 194 'ip': get_ip_addr(environ),
195 195 'username': username,
196 196 'action': action,
197 197 'repository': repo_name,
198 198 'scm': scm,
199 199 'config': rhodecode.CONFIG['__file__'],
200 200 'make_lock': make_lock,
201 201 'locked_by': locked_by,
202 202 'server_url': utils2.get_server_url(environ),
203 203 'hooks': get_enabled_hook_classes(ui_settings),
204 204 'is_shadow_repo': is_shadow_repo,
205 205 }
206 206 return extras
207 207
208 208
209 209 class BasicAuth(AuthBasicAuthenticator):
210 210
211 211 def __init__(self, realm, authfunc, registry, auth_http_code=None,
212 212 initial_call_detection=False, acl_repo_name=None):
213 213 self.realm = realm
214 214 self.initial_call = initial_call_detection
215 215 self.authfunc = authfunc
216 216 self.registry = registry
217 217 self.acl_repo_name = acl_repo_name
218 218 self._rc_auth_http_code = auth_http_code
219 219
220 220 def _get_response_from_code(self, http_code):
221 221 try:
222 222 return get_exception(safe_int(http_code))
223 223 except Exception:
224 224 log.exception('Failed to fetch response for code %s' % http_code)
225 225 return HTTPForbidden
226 226
227 227 def build_authentication(self):
228 228 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
229 229 if self._rc_auth_http_code and not self.initial_call:
230 230 # return alternative HTTP code if alternative http return code
231 231 # is specified in RhodeCode config, but ONLY if it's not the
232 232 # FIRST call
233 233 custom_response_klass = self._get_response_from_code(
234 234 self._rc_auth_http_code)
235 235 return custom_response_klass(headers=head)
236 236 return HTTPUnauthorized(headers=head)
237 237
238 238 def authenticate(self, environ):
239 239 authorization = AUTHORIZATION(environ)
240 240 if not authorization:
241 241 return self.build_authentication()
242 242 (authmeth, auth) = authorization.split(' ', 1)
243 243 if 'basic' != authmeth.lower():
244 244 return self.build_authentication()
245 245 auth = auth.strip().decode('base64')
246 246 _parts = auth.split(':', 1)
247 247 if len(_parts) == 2:
248 248 username, password = _parts
249 249 if self.authfunc(
250 250 username, password, environ, VCS_TYPE,
251 251 registry=self.registry, acl_repo_name=self.acl_repo_name):
252 252 return username
253 253 if username and password:
254 254 # we mark that we actually executed authentication once, at
255 255 # that point we can use the alternative auth code
256 256 self.initial_call = False
257 257
258 258 return self.build_authentication()
259 259
260 260 __call__ = authenticate
261 261
262 262
263 263 def attach_context_attributes(context, request):
264 264 """
265 265 Attach variables into template context called `c`, please note that
266 266 request could be pylons or pyramid request in here.
267 267 """
268 268 rc_config = SettingsModel().get_all_settings(cache=True)
269 269
270 270 context.rhodecode_version = rhodecode.__version__
271 271 context.rhodecode_edition = config.get('rhodecode.edition')
272 272 # unique secret + version does not leak the version but keep consistency
273 273 context.rhodecode_version_hash = md5(
274 274 config.get('beaker.session.secret', '') +
275 275 rhodecode.__version__)[:8]
276 276
277 277 # Default language set for the incoming request
278 278 context.language = translation.get_lang()[0]
279 279
280 280 # Visual options
281 281 context.visual = AttributeDict({})
282 282
283 283 # DB stored Visual Items
284 284 context.visual.show_public_icon = str2bool(
285 285 rc_config.get('rhodecode_show_public_icon'))
286 286 context.visual.show_private_icon = str2bool(
287 287 rc_config.get('rhodecode_show_private_icon'))
288 288 context.visual.stylify_metatags = str2bool(
289 289 rc_config.get('rhodecode_stylify_metatags'))
290 290 context.visual.dashboard_items = safe_int(
291 291 rc_config.get('rhodecode_dashboard_items', 100))
292 292 context.visual.admin_grid_items = safe_int(
293 293 rc_config.get('rhodecode_admin_grid_items', 100))
294 294 context.visual.repository_fields = str2bool(
295 295 rc_config.get('rhodecode_repository_fields'))
296 296 context.visual.show_version = str2bool(
297 297 rc_config.get('rhodecode_show_version'))
298 298 context.visual.use_gravatar = str2bool(
299 299 rc_config.get('rhodecode_use_gravatar'))
300 300 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
301 301 context.visual.default_renderer = rc_config.get(
302 302 'rhodecode_markup_renderer', 'rst')
303 303 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
304 304 context.visual.rhodecode_support_url = \
305 305 rc_config.get('rhodecode_support_url') or url('rhodecode_support')
306 306
307 307 context.pre_code = rc_config.get('rhodecode_pre_code')
308 308 context.post_code = rc_config.get('rhodecode_post_code')
309 309 context.rhodecode_name = rc_config.get('rhodecode_title')
310 310 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
311 311 # if we have specified default_encoding in the request, it has more
312 312 # priority
313 313 if request.GET.get('default_encoding'):
314 314 context.default_encodings.insert(0, request.GET.get('default_encoding'))
315 315 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
316 316
317 317 # INI stored
318 318 context.labs_active = str2bool(
319 319 config.get('labs_settings_active', 'false'))
320 320 context.visual.allow_repo_location_change = str2bool(
321 321 config.get('allow_repo_location_change', True))
322 322 context.visual.allow_custom_hooks_settings = str2bool(
323 323 config.get('allow_custom_hooks_settings', True))
324 324 context.debug_style = str2bool(config.get('debug_style', False))
325 325
326 326 context.rhodecode_instanceid = config.get('instance_id')
327 327
328 328 # AppEnlight
329 329 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
330 330 context.appenlight_api_public_key = config.get(
331 331 'appenlight.api_public_key', '')
332 332 context.appenlight_server_url = config.get('appenlight.server_url', '')
333 333
334 334 # JS template context
335 335 context.template_context = {
336 336 'repo_name': None,
337 337 'repo_type': None,
338 338 'repo_landing_commit': None,
339 339 'rhodecode_user': {
340 340 'username': None,
341 341 'email': None,
342 342 'notification_status': False
343 343 },
344 344 'visual': {
345 345 'default_renderer': None
346 346 },
347 347 'commit_data': {
348 348 'commit_id': None
349 349 },
350 350 'pull_request_data': {'pull_request_id': None},
351 351 'timeago': {
352 352 'refresh_time': 120 * 1000,
353 353 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
354 354 },
355 355 'pylons_dispatch': {
356 356 # 'controller': request.environ['pylons.routes_dict']['controller'],
357 357 # 'action': request.environ['pylons.routes_dict']['action'],
358 358 },
359 359 'pyramid_dispatch': {
360 360
361 361 },
362 362 'extra': {'plugins': {}}
363 363 }
364 364 # END CONFIG VARS
365 365
366 366 # TODO: This dosn't work when called from pylons compatibility tween.
367 367 # Fix this and remove it from base controller.
368 368 # context.repo_name = get_repo_slug(request) # can be empty
369 369
370 370 diffmode = 'sideside'
371 371 if request.GET.get('diffmode'):
372 372 if request.GET['diffmode'] == 'unified':
373 373 diffmode = 'unified'
374 374 elif request.session.get('diffmode'):
375 375 diffmode = request.session['diffmode']
376 376
377 377 context.diffmode = diffmode
378 378
379 379 if request.session.get('diffmode') != diffmode:
380 380 request.session['diffmode'] = diffmode
381 381
382 382 context.csrf_token = auth.get_csrf_token()
383 383 context.backends = rhodecode.BACKENDS.keys()
384 384 context.backends.sort()
385 385 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(
386 386 context.rhodecode_user.user_id)
387 387
388 388 context.pyramid_request = pyramid.threadlocal.get_current_request()
389 389
390 390
391 391 def get_auth_user(environ):
392 392 ip_addr = get_ip_addr(environ)
393 393 # make sure that we update permissions each time we call controller
394 394 _auth_token = (request.GET.get('auth_token', '') or
395 395 request.GET.get('api_key', ''))
396 396
397 397 if _auth_token:
398 398 # when using API_KEY we assume user exists, and
399 399 # doesn't need auth based on cookies.
400 400 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
401 401 authenticated = False
402 402 else:
403 403 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
404 404 try:
405 405 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
406 406 ip_addr=ip_addr)
407 407 except UserCreationError as e:
408 408 h.flash(e, 'error')
409 409 # container auth or other auth functions that create users
410 410 # on the fly can throw this exception signaling that there's
411 411 # issue with user creation, explanation should be provided
412 412 # in Exception itself. We then create a simple blank
413 413 # AuthUser
414 414 auth_user = AuthUser(ip_addr=ip_addr)
415 415
416 416 if password_changed(auth_user, session):
417 417 session.invalidate()
418 418 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
419 419 auth_user = AuthUser(ip_addr=ip_addr)
420 420
421 421 authenticated = cookie_store.get('is_authenticated')
422 422
423 423 if not auth_user.is_authenticated and auth_user.is_user_object:
424 424 # user is not authenticated and not empty
425 425 auth_user.set_authenticated(authenticated)
426 426
427 427 return auth_user
428 428
429 429
430 430 class BaseController(WSGIController):
431 431
432 432 def __before__(self):
433 433 """
434 434 __before__ is called before controller methods and after __call__
435 435 """
436 436 # on each call propagate settings calls into global settings.
437 437 set_rhodecode_config(config)
438 438 attach_context_attributes(c, request)
439 439
440 440 # TODO: Remove this when fixed in attach_context_attributes()
441 441 c.repo_name = get_repo_slug(request) # can be empty
442 442
443 443 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
444 444 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
445 445 self.sa = meta.Session
446 446 self.scm_model = ScmModel(self.sa)
447 447
448 448 # set user language
449 449 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
450 450 if user_lang:
451 451 translation.set_lang(user_lang)
452 452 log.debug('set language to %s for user %s',
453 453 user_lang, self._rhodecode_user)
454 454
455 455 def _dispatch_redirect(self, with_url, environ, start_response):
456 456 resp = HTTPFound(with_url)
457 457 environ['SCRIPT_NAME'] = '' # handle prefix middleware
458 458 environ['PATH_INFO'] = with_url
459 459 return resp(environ, start_response)
460 460
461 461 def __call__(self, environ, start_response):
462 462 """Invoke the Controller"""
463 463 # WSGIController.__call__ dispatches to the Controller method
464 464 # the request is routed to. This routing information is
465 465 # available in environ['pylons.routes_dict']
466 466 from rhodecode.lib import helpers as h
467 467
468 468 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
469 469 if environ.get('debugtoolbar.wants_pylons_context', False):
470 470 environ['debugtoolbar.pylons_context'] = c._current_obj()
471 471
472 472 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
473 473 environ['pylons.routes_dict']['action']])
474 474
475 475 self.rc_config = SettingsModel().get_all_settings(cache=True)
476 476 self.ip_addr = get_ip_addr(environ)
477 477
478 478 # The rhodecode auth user is looked up and passed through the
479 479 # environ by the pylons compatibility tween in pyramid.
480 480 # So we can just grab it from there.
481 481 auth_user = environ['rc_auth_user']
482 482
483 483 # set globals for auth user
484 484 request.user = auth_user
485 485 c.rhodecode_user = self._rhodecode_user = auth_user
486 486
487 487 log.info('IP: %s User: %s accessed %s [%s]' % (
488 488 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
489 489 _route_name)
490 490 )
491 491
492 # TODO: Maybe this should be move to pyramid to cover all views.
493 # check user attributes for password change flag
494 492 user_obj = auth_user.get_instance()
495 493 if user_obj and user_obj.user_data.get('force_password_change'):
496 494 h.flash('You are required to change your password', 'warning',
497 495 ignore_duplicate=True)
498
499 skip_user_check_urls = [
500 'error.document', 'login.logout', 'login.index',
501 'admin/my_account.my_account_password',
502 'admin/my_account.my_account_password_update'
503 ]
504 if _route_name not in skip_user_check_urls:
505 return self._dispatch_redirect(
506 url('my_account_password'), environ, start_response)
496 return self._dispatch_redirect(
497 url('my_account_password'), environ, start_response)
507 498
508 499 return WSGIController.__call__(self, environ, start_response)
509 500
510 501
511 502 class BaseRepoController(BaseController):
512 503 """
513 504 Base class for controllers responsible for loading all needed data for
514 505 repository loaded items are
515 506
516 507 c.rhodecode_repo: instance of scm repository
517 508 c.rhodecode_db_repo: instance of db
518 509 c.repository_requirements_missing: shows that repository specific data
519 510 could not be displayed due to the missing requirements
520 511 c.repository_pull_requests: show number of open pull requests
521 512 """
522 513
523 514 def __before__(self):
524 515 super(BaseRepoController, self).__before__()
525 516 if c.repo_name: # extracted from routes
526 517 db_repo = Repository.get_by_repo_name(c.repo_name)
527 518 if not db_repo:
528 519 return
529 520
530 521 log.debug(
531 522 'Found repository in database %s with state `%s`',
532 523 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
533 524 route = getattr(request.environ.get('routes.route'), 'name', '')
534 525
535 526 # allow to delete repos that are somehow damages in filesystem
536 527 if route in ['delete_repo']:
537 528 return
538 529
539 530 if db_repo.repo_state in [Repository.STATE_PENDING]:
540 531 if route in ['repo_creating_home']:
541 532 return
542 533 check_url = url('repo_creating_home', repo_name=c.repo_name)
543 534 return redirect(check_url)
544 535
545 536 self.rhodecode_db_repo = db_repo
546 537
547 538 missing_requirements = False
548 539 try:
549 540 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
550 541 except RepositoryRequirementError as e:
551 542 missing_requirements = True
552 543 self._handle_missing_requirements(e)
553 544
554 545 if self.rhodecode_repo is None and not missing_requirements:
555 546 log.error('%s this repository is present in database but it '
556 547 'cannot be created as an scm instance', c.repo_name)
557 548
558 549 h.flash(_(
559 550 "The repository at %(repo_name)s cannot be located.") %
560 551 {'repo_name': c.repo_name},
561 552 category='error', ignore_duplicate=True)
562 553 redirect(url('home'))
563 554
564 555 # update last change according to VCS data
565 556 if not missing_requirements:
566 557 commit = db_repo.get_commit(
567 558 pre_load=["author", "date", "message", "parents"])
568 559 db_repo.update_commit_cache(commit)
569 560
570 561 # Prepare context
571 562 c.rhodecode_db_repo = db_repo
572 563 c.rhodecode_repo = self.rhodecode_repo
573 564 c.repository_requirements_missing = missing_requirements
574 565
575 566 self._update_global_counters(self.scm_model, db_repo)
576 567
577 568 def _update_global_counters(self, scm_model, db_repo):
578 569 """
579 570 Base variables that are exposed to every page of repository
580 571 """
581 572 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
582 573
583 574 def _handle_missing_requirements(self, error):
584 575 self.rhodecode_repo = None
585 576 log.error(
586 577 'Requirements are missing for repository %s: %s',
587 578 c.repo_name, error.message)
588 579
589 580 summary_url = url('summary_home', repo_name=c.repo_name)
590 581 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
591 582 settings_update_url = url('repo', repo_name=c.repo_name)
592 583 path = request.path
593 584 should_redirect = (
594 585 path not in (summary_url, settings_update_url)
595 586 and '/settings' not in path or path == statistics_url
596 587 )
597 588 if should_redirect:
598 589 redirect(summary_url)
General Comments 0
You need to be logged in to leave comments. Login now