##// 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 # -*- 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 import time
21 import logging
22 import logging
22 from pylons import tmpl_context as c
23 from pylons import tmpl_context as c
23 from pyramid.httpexceptions import HTTPFound
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 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
28
31
29
32
30 ADMIN_PREFIX = '/_admin'
33 ADMIN_PREFIX = '/_admin'
31 STATIC_FILE_PREFIX = '/_static'
34 STATIC_FILE_PREFIX = '/_static'
32
35
33
36
34 class TemplateArgs(StrictAttributeDict):
37 class TemplateArgs(StrictAttributeDict):
35 pass
38 pass
36
39
37
40
38 class BaseAppView(object):
41 class BaseAppView(object):
39
42
40 def __init__(self, context, request):
43 def __init__(self, context, request):
41 self.request = request
44 self.request = request
42 self.context = context
45 self.context = context
43 self.session = request.session
46 self.session = request.session
44 self._rhodecode_user = request.user # auth user
47 self._rhodecode_user = request.user # auth user
45 self._rhodecode_db_user = self._rhodecode_user.get_instance()
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 def _get_local_tmpl_context(self):
78 def _get_local_tmpl_context(self):
48 c = TemplateArgs()
79 c = TemplateArgs()
49 c.auth_user = self.request.user
80 c.auth_user = self.request.user
50 return c
81 return c
51
82
52 def _register_global_c(self, tmpl_args):
83 def _register_global_c(self, tmpl_args):
53 """
84 """
54 Registers attributes to pylons global `c`
85 Registers attributes to pylons global `c`
55 """
86 """
56 # TODO(marcink): remove once pyramid migration is finished
87 # TODO(marcink): remove once pyramid migration is finished
57 for k, v in tmpl_args.items():
88 for k, v in tmpl_args.items():
58 setattr(c, k, v)
89 setattr(c, k, v)
59
90
60 def _get_template_context(self, tmpl_args):
91 def _get_template_context(self, tmpl_args):
61 self._register_global_c(tmpl_args)
92 self._register_global_c(tmpl_args)
62
93
63 local_tmpl_args = {
94 local_tmpl_args = {
64 'defaults': {},
95 'defaults': {},
65 'errors': {},
96 'errors': {},
66 }
97 }
67 local_tmpl_args.update(tmpl_args)
98 local_tmpl_args.update(tmpl_args)
68 return local_tmpl_args
99 return local_tmpl_args
69
100
70 def load_default_context(self):
101 def load_default_context(self):
71 """
102 """
72 example:
103 example:
73
104
74 def load_default_context(self):
105 def load_default_context(self):
75 c = self._get_local_tmpl_context()
106 c = self._get_local_tmpl_context()
76 c.custom_var = 'foobar'
107 c.custom_var = 'foobar'
77 self._register_global_c(c)
108 self._register_global_c(c)
78 return c
109 return c
79 """
110 """
80 raise NotImplementedError('Needs implementation in view class')
111 raise NotImplementedError('Needs implementation in view class')
81
112
@@ -1,128 +1,133 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 import mock
22 import mock
23 import pytest
23 import pytest
24
24
25 from rhodecode.apps.login.views import LoginView, CaptchaData
25 from rhodecode.apps.login.views import LoginView, CaptchaData
26 from rhodecode.config.routing import ADMIN_PREFIX
26 from rhodecode.config.routing import ADMIN_PREFIX
27 from rhodecode.lib.utils2 import AttributeDict
27 from rhodecode.model.settings import SettingsModel
28 from rhodecode.model.settings import SettingsModel
28 from rhodecode.tests.utils import AssertResponse
29 from rhodecode.tests.utils import AssertResponse
29
30
30
31
31 class RhodeCodeSetting(object):
32 class RhodeCodeSetting(object):
32 def __init__(self, name, value):
33 def __init__(self, name, value):
33 self.name = name
34 self.name = name
34 self.value = value
35 self.value = value
35
36
36 def __enter__(self):
37 def __enter__(self):
37 from rhodecode.model.settings import SettingsModel
38 from rhodecode.model.settings import SettingsModel
38 model = SettingsModel()
39 model = SettingsModel()
39 self.old_setting = model.get_setting_by_name(self.name)
40 self.old_setting = model.get_setting_by_name(self.name)
40 model.create_or_update_setting(name=self.name, val=self.value)
41 model.create_or_update_setting(name=self.name, val=self.value)
41 return self
42 return self
42
43
43 def __exit__(self, type, value, traceback):
44 def __exit__(self, exc_type, exc_val, exc_tb):
44 model = SettingsModel()
45 model = SettingsModel()
45 if self.old_setting:
46 if self.old_setting:
46 model.create_or_update_setting(
47 model.create_or_update_setting(
47 name=self.name, val=self.old_setting.app_settings_value)
48 name=self.name, val=self.old_setting.app_settings_value)
48 else:
49 else:
49 model.create_or_update_setting(name=self.name)
50 model.create_or_update_setting(name=self.name)
50
51
51
52
52 class TestRegisterCaptcha(object):
53 class TestRegisterCaptcha(object):
53
54
54 @pytest.mark.parametrize('private_key, public_key, expected', [
55 @pytest.mark.parametrize('private_key, public_key, expected', [
55 ('', '', CaptchaData(False, '', '')),
56 ('', '', CaptchaData(False, '', '')),
56 ('', 'pubkey', CaptchaData(False, '', 'pubkey')),
57 ('', 'pubkey', CaptchaData(False, '', 'pubkey')),
57 ('privkey', '', CaptchaData(True, 'privkey', '')),
58 ('privkey', '', CaptchaData(True, 'privkey', '')),
58 ('privkey', 'pubkey', CaptchaData(True, 'privkey', 'pubkey')),
59 ('privkey', 'pubkey', CaptchaData(True, 'privkey', 'pubkey')),
59 ])
60 ])
60 def test_get_captcha_data(self, private_key, public_key, expected, db):
61 def test_get_captcha_data(self, private_key, public_key, expected, db,
61 login_view = LoginView(mock.Mock(), mock.Mock())
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 with RhodeCodeSetting('captcha_private_key', private_key):
67 with RhodeCodeSetting('captcha_private_key', private_key):
63 with RhodeCodeSetting('captcha_public_key', public_key):
68 with RhodeCodeSetting('captcha_public_key', public_key):
64 captcha = login_view._get_captcha_data()
69 captcha = login_view._get_captcha_data()
65 assert captcha == expected
70 assert captcha == expected
66
71
67 @pytest.mark.parametrize('active', [False, True])
72 @pytest.mark.parametrize('active', [False, True])
68 @mock.patch.object(LoginView, '_get_captcha_data')
73 @mock.patch.object(LoginView, '_get_captcha_data')
69 def test_private_key_does_not_leak_to_html(
74 def test_private_key_does_not_leak_to_html(
70 self, m_get_captcha_data, active, app):
75 self, m_get_captcha_data, active, app):
71 captcha = CaptchaData(
76 captcha = CaptchaData(
72 active=active, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
77 active=active, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
73 m_get_captcha_data.return_value = captcha
78 m_get_captcha_data.return_value = captcha
74
79
75 response = app.get(ADMIN_PREFIX + '/register')
80 response = app.get(ADMIN_PREFIX + '/register')
76 assert 'PRIVATE_KEY' not in response
81 assert 'PRIVATE_KEY' not in response
77
82
78 @pytest.mark.parametrize('active', [False, True])
83 @pytest.mark.parametrize('active', [False, True])
79 @mock.patch.object(LoginView, '_get_captcha_data')
84 @mock.patch.object(LoginView, '_get_captcha_data')
80 def test_register_view_renders_captcha(
85 def test_register_view_renders_captcha(
81 self, m_get_captcha_data, active, app):
86 self, m_get_captcha_data, active, app):
82 captcha = CaptchaData(
87 captcha = CaptchaData(
83 active=active, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
88 active=active, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
84 m_get_captcha_data.return_value = captcha
89 m_get_captcha_data.return_value = captcha
85
90
86 response = app.get(ADMIN_PREFIX + '/register')
91 response = app.get(ADMIN_PREFIX + '/register')
87
92
88 assertr = AssertResponse(response)
93 assertr = AssertResponse(response)
89 if active:
94 if active:
90 assertr.one_element_exists('#recaptcha_field')
95 assertr.one_element_exists('#recaptcha_field')
91 else:
96 else:
92 assertr.no_element_exists('#recaptcha_field')
97 assertr.no_element_exists('#recaptcha_field')
93
98
94 @pytest.mark.parametrize('valid', [False, True])
99 @pytest.mark.parametrize('valid', [False, True])
95 @mock.patch('rhodecode.apps.login.views.submit')
100 @mock.patch('rhodecode.apps.login.views.submit')
96 @mock.patch.object(LoginView, '_get_captcha_data')
101 @mock.patch.object(LoginView, '_get_captcha_data')
97 def test_register_with_active_captcha(
102 def test_register_with_active_captcha(
98 self, m_get_captcha_data, m_submit, valid, app, csrf_token):
103 self, m_get_captcha_data, m_submit, valid, app, csrf_token):
99 captcha = CaptchaData(
104 captcha = CaptchaData(
100 active=True, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
105 active=True, private_key='PRIVATE_KEY', public_key='PUBLIC_KEY')
101 m_get_captcha_data.return_value = captcha
106 m_get_captcha_data.return_value = captcha
102 m_response = mock.Mock()
107 m_response = mock.Mock()
103 m_response.is_valid = valid
108 m_response.is_valid = valid
104 m_submit.return_value = m_response
109 m_submit.return_value = m_response
105
110
106 params = {
111 params = {
107 'csrf_token': csrf_token,
112 'csrf_token': csrf_token,
108 'email': 'pytest@example.com',
113 'email': 'pytest@example.com',
109 'firstname': 'pytest-firstname',
114 'firstname': 'pytest-firstname',
110 'lastname': 'pytest-lastname',
115 'lastname': 'pytest-lastname',
111 'password': 'secret',
116 'password': 'secret',
112 'password_confirmation': 'secret',
117 'password_confirmation': 'secret',
113 'username': 'pytest',
118 'username': 'pytest',
114 }
119 }
115 response = app.post(ADMIN_PREFIX + '/register', params=params)
120 response = app.post(ADMIN_PREFIX + '/register', params=params)
116
121
117 if valid:
122 if valid:
118 # If we provided a valid captcha input we expect a successful
123 # If we provided a valid captcha input we expect a successful
119 # registration and redirect to the login page.
124 # registration and redirect to the login page.
120 assert response.status_int == 302
125 assert response.status_int == 302
121 assert 'location' in response.headers
126 assert 'location' in response.headers
122 assert ADMIN_PREFIX + '/login' in response.headers['location']
127 assert ADMIN_PREFIX + '/login' in response.headers['location']
123 else:
128 else:
124 # If captche input is invalid we expect to stay on the registration
129 # If captche input is invalid we expect to stay on the registration
125 # page with an error message displayed.
130 # page with an error message displayed.
126 assertr = AssertResponse(response)
131 assertr = AssertResponse(response)
127 assert response.status_int == 200
132 assert response.status_int == 200
128 assertr.one_element_exists('#recaptcha_field ~ span.error-message')
133 assertr.one_element_exists('#recaptcha_field ~ span.error-message')
@@ -1,598 +1,589 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, acl_repo_name=None):
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.acl_repo_name = acl_repo_name
218 self._rc_auth_http_code = auth_http_code
218 self._rc_auth_http_code = auth_http_code
219
219
220 def _get_response_from_code(self, http_code):
220 def _get_response_from_code(self, http_code):
221 try:
221 try:
222 return get_exception(safe_int(http_code))
222 return get_exception(safe_int(http_code))
223 except Exception:
223 except Exception:
224 log.exception('Failed to fetch response for code %s' % http_code)
224 log.exception('Failed to fetch response for code %s' % http_code)
225 return HTTPForbidden
225 return HTTPForbidden
226
226
227 def build_authentication(self):
227 def build_authentication(self):
228 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
228 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
229 if self._rc_auth_http_code and not self.initial_call:
229 if self._rc_auth_http_code and not self.initial_call:
230 # return alternative HTTP code if alternative http return code
230 # return alternative HTTP code if alternative http return code
231 # 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
232 # FIRST call
232 # FIRST call
233 custom_response_klass = self._get_response_from_code(
233 custom_response_klass = self._get_response_from_code(
234 self._rc_auth_http_code)
234 self._rc_auth_http_code)
235 return custom_response_klass(headers=head)
235 return custom_response_klass(headers=head)
236 return HTTPUnauthorized(headers=head)
236 return HTTPUnauthorized(headers=head)
237
237
238 def authenticate(self, environ):
238 def authenticate(self, environ):
239 authorization = AUTHORIZATION(environ)
239 authorization = AUTHORIZATION(environ)
240 if not authorization:
240 if not authorization:
241 return self.build_authentication()
241 return self.build_authentication()
242 (authmeth, auth) = authorization.split(' ', 1)
242 (authmeth, auth) = authorization.split(' ', 1)
243 if 'basic' != authmeth.lower():
243 if 'basic' != authmeth.lower():
244 return self.build_authentication()
244 return self.build_authentication()
245 auth = auth.strip().decode('base64')
245 auth = auth.strip().decode('base64')
246 _parts = auth.split(':', 1)
246 _parts = auth.split(':', 1)
247 if len(_parts) == 2:
247 if len(_parts) == 2:
248 username, password = _parts
248 username, password = _parts
249 if self.authfunc(
249 if self.authfunc(
250 username, password, environ, VCS_TYPE,
250 username, password, environ, VCS_TYPE,
251 registry=self.registry, acl_repo_name=self.acl_repo_name):
251 registry=self.registry, acl_repo_name=self.acl_repo_name):
252 return username
252 return username
253 if username and password:
253 if username and password:
254 # we mark that we actually executed authentication once, at
254 # we mark that we actually executed authentication once, at
255 # that point we can use the alternative auth code
255 # that point we can use the alternative auth code
256 self.initial_call = False
256 self.initial_call = False
257
257
258 return self.build_authentication()
258 return self.build_authentication()
259
259
260 __call__ = authenticate
260 __call__ = authenticate
261
261
262
262
263 def attach_context_attributes(context, request):
263 def attach_context_attributes(context, request):
264 """
264 """
265 Attach variables into template context called `c`, please note that
265 Attach variables into template context called `c`, please note that
266 request could be pylons or pyramid request in here.
266 request could be pylons or pyramid request in here.
267 """
267 """
268 rc_config = SettingsModel().get_all_settings(cache=True)
268 rc_config = SettingsModel().get_all_settings(cache=True)
269
269
270 context.rhodecode_version = rhodecode.__version__
270 context.rhodecode_version = rhodecode.__version__
271 context.rhodecode_edition = config.get('rhodecode.edition')
271 context.rhodecode_edition = config.get('rhodecode.edition')
272 # unique secret + version does not leak the version but keep consistency
272 # unique secret + version does not leak the version but keep consistency
273 context.rhodecode_version_hash = md5(
273 context.rhodecode_version_hash = md5(
274 config.get('beaker.session.secret', '') +
274 config.get('beaker.session.secret', '') +
275 rhodecode.__version__)[:8]
275 rhodecode.__version__)[:8]
276
276
277 # Default language set for the incoming request
277 # Default language set for the incoming request
278 context.language = translation.get_lang()[0]
278 context.language = translation.get_lang()[0]
279
279
280 # Visual options
280 # Visual options
281 context.visual = AttributeDict({})
281 context.visual = AttributeDict({})
282
282
283 # DB stored Visual Items
283 # DB stored Visual Items
284 context.visual.show_public_icon = str2bool(
284 context.visual.show_public_icon = str2bool(
285 rc_config.get('rhodecode_show_public_icon'))
285 rc_config.get('rhodecode_show_public_icon'))
286 context.visual.show_private_icon = str2bool(
286 context.visual.show_private_icon = str2bool(
287 rc_config.get('rhodecode_show_private_icon'))
287 rc_config.get('rhodecode_show_private_icon'))
288 context.visual.stylify_metatags = str2bool(
288 context.visual.stylify_metatags = str2bool(
289 rc_config.get('rhodecode_stylify_metatags'))
289 rc_config.get('rhodecode_stylify_metatags'))
290 context.visual.dashboard_items = safe_int(
290 context.visual.dashboard_items = safe_int(
291 rc_config.get('rhodecode_dashboard_items', 100))
291 rc_config.get('rhodecode_dashboard_items', 100))
292 context.visual.admin_grid_items = safe_int(
292 context.visual.admin_grid_items = safe_int(
293 rc_config.get('rhodecode_admin_grid_items', 100))
293 rc_config.get('rhodecode_admin_grid_items', 100))
294 context.visual.repository_fields = str2bool(
294 context.visual.repository_fields = str2bool(
295 rc_config.get('rhodecode_repository_fields'))
295 rc_config.get('rhodecode_repository_fields'))
296 context.visual.show_version = str2bool(
296 context.visual.show_version = str2bool(
297 rc_config.get('rhodecode_show_version'))
297 rc_config.get('rhodecode_show_version'))
298 context.visual.use_gravatar = str2bool(
298 context.visual.use_gravatar = str2bool(
299 rc_config.get('rhodecode_use_gravatar'))
299 rc_config.get('rhodecode_use_gravatar'))
300 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
300 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
301 context.visual.default_renderer = rc_config.get(
301 context.visual.default_renderer = rc_config.get(
302 'rhodecode_markup_renderer', 'rst')
302 'rhodecode_markup_renderer', 'rst')
303 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
303 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
304 context.visual.rhodecode_support_url = \
304 context.visual.rhodecode_support_url = \
305 rc_config.get('rhodecode_support_url') or url('rhodecode_support')
305 rc_config.get('rhodecode_support_url') or url('rhodecode_support')
306
306
307 context.pre_code = rc_config.get('rhodecode_pre_code')
307 context.pre_code = rc_config.get('rhodecode_pre_code')
308 context.post_code = rc_config.get('rhodecode_post_code')
308 context.post_code = rc_config.get('rhodecode_post_code')
309 context.rhodecode_name = rc_config.get('rhodecode_title')
309 context.rhodecode_name = rc_config.get('rhodecode_title')
310 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
310 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
311 # 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
312 # priority
312 # priority
313 if request.GET.get('default_encoding'):
313 if request.GET.get('default_encoding'):
314 context.default_encodings.insert(0, request.GET.get('default_encoding'))
314 context.default_encodings.insert(0, request.GET.get('default_encoding'))
315 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
315 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
316
316
317 # INI stored
317 # INI stored
318 context.labs_active = str2bool(
318 context.labs_active = str2bool(
319 config.get('labs_settings_active', 'false'))
319 config.get('labs_settings_active', 'false'))
320 context.visual.allow_repo_location_change = str2bool(
320 context.visual.allow_repo_location_change = str2bool(
321 config.get('allow_repo_location_change', True))
321 config.get('allow_repo_location_change', True))
322 context.visual.allow_custom_hooks_settings = str2bool(
322 context.visual.allow_custom_hooks_settings = str2bool(
323 config.get('allow_custom_hooks_settings', True))
323 config.get('allow_custom_hooks_settings', True))
324 context.debug_style = str2bool(config.get('debug_style', False))
324 context.debug_style = str2bool(config.get('debug_style', False))
325
325
326 context.rhodecode_instanceid = config.get('instance_id')
326 context.rhodecode_instanceid = config.get('instance_id')
327
327
328 # AppEnlight
328 # AppEnlight
329 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
329 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
330 context.appenlight_api_public_key = config.get(
330 context.appenlight_api_public_key = config.get(
331 'appenlight.api_public_key', '')
331 'appenlight.api_public_key', '')
332 context.appenlight_server_url = config.get('appenlight.server_url', '')
332 context.appenlight_server_url = config.get('appenlight.server_url', '')
333
333
334 # JS template context
334 # JS template context
335 context.template_context = {
335 context.template_context = {
336 'repo_name': None,
336 'repo_name': None,
337 'repo_type': None,
337 'repo_type': None,
338 'repo_landing_commit': None,
338 'repo_landing_commit': None,
339 'rhodecode_user': {
339 'rhodecode_user': {
340 'username': None,
340 'username': None,
341 'email': None,
341 'email': None,
342 'notification_status': False
342 'notification_status': False
343 },
343 },
344 'visual': {
344 'visual': {
345 'default_renderer': None
345 'default_renderer': None
346 },
346 },
347 'commit_data': {
347 'commit_data': {
348 'commit_id': None
348 'commit_id': None
349 },
349 },
350 'pull_request_data': {'pull_request_id': None},
350 'pull_request_data': {'pull_request_id': None},
351 'timeago': {
351 'timeago': {
352 'refresh_time': 120 * 1000,
352 'refresh_time': 120 * 1000,
353 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
353 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
354 },
354 },
355 'pylons_dispatch': {
355 'pylons_dispatch': {
356 # 'controller': request.environ['pylons.routes_dict']['controller'],
356 # 'controller': request.environ['pylons.routes_dict']['controller'],
357 # 'action': request.environ['pylons.routes_dict']['action'],
357 # 'action': request.environ['pylons.routes_dict']['action'],
358 },
358 },
359 'pyramid_dispatch': {
359 'pyramid_dispatch': {
360
360
361 },
361 },
362 'extra': {'plugins': {}}
362 'extra': {'plugins': {}}
363 }
363 }
364 # END CONFIG VARS
364 # END CONFIG VARS
365
365
366 # TODO: This dosn't work when called from pylons compatibility tween.
366 # TODO: This dosn't work when called from pylons compatibility tween.
367 # Fix this and remove it from base controller.
367 # Fix this and remove it from base controller.
368 # context.repo_name = get_repo_slug(request) # can be empty
368 # context.repo_name = get_repo_slug(request) # can be empty
369
369
370 diffmode = 'sideside'
370 diffmode = 'sideside'
371 if request.GET.get('diffmode'):
371 if request.GET.get('diffmode'):
372 if request.GET['diffmode'] == 'unified':
372 if request.GET['diffmode'] == 'unified':
373 diffmode = 'unified'
373 diffmode = 'unified'
374 elif request.session.get('diffmode'):
374 elif request.session.get('diffmode'):
375 diffmode = request.session['diffmode']
375 diffmode = request.session['diffmode']
376
376
377 context.diffmode = diffmode
377 context.diffmode = diffmode
378
378
379 if request.session.get('diffmode') != diffmode:
379 if request.session.get('diffmode') != diffmode:
380 request.session['diffmode'] = diffmode
380 request.session['diffmode'] = diffmode
381
381
382 context.csrf_token = auth.get_csrf_token()
382 context.csrf_token = auth.get_csrf_token()
383 context.backends = rhodecode.BACKENDS.keys()
383 context.backends = rhodecode.BACKENDS.keys()
384 context.backends.sort()
384 context.backends.sort()
385 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(
385 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(
386 context.rhodecode_user.user_id)
386 context.rhodecode_user.user_id)
387
387
388 context.pyramid_request = pyramid.threadlocal.get_current_request()
388 context.pyramid_request = pyramid.threadlocal.get_current_request()
389
389
390
390
391 def get_auth_user(environ):
391 def get_auth_user(environ):
392 ip_addr = get_ip_addr(environ)
392 ip_addr = get_ip_addr(environ)
393 # make sure that we update permissions each time we call controller
393 # make sure that we update permissions each time we call controller
394 _auth_token = (request.GET.get('auth_token', '') or
394 _auth_token = (request.GET.get('auth_token', '') or
395 request.GET.get('api_key', ''))
395 request.GET.get('api_key', ''))
396
396
397 if _auth_token:
397 if _auth_token:
398 # when using API_KEY we assume user exists, and
398 # when using API_KEY we assume user exists, and
399 # doesn't need auth based on cookies.
399 # doesn't need auth based on cookies.
400 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
400 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
401 authenticated = False
401 authenticated = False
402 else:
402 else:
403 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
403 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
404 try:
404 try:
405 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
405 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
406 ip_addr=ip_addr)
406 ip_addr=ip_addr)
407 except UserCreationError as e:
407 except UserCreationError as e:
408 h.flash(e, 'error')
408 h.flash(e, 'error')
409 # container auth or other auth functions that create users
409 # container auth or other auth functions that create users
410 # on the fly can throw this exception signaling that there's
410 # on the fly can throw this exception signaling that there's
411 # issue with user creation, explanation should be provided
411 # issue with user creation, explanation should be provided
412 # in Exception itself. We then create a simple blank
412 # in Exception itself. We then create a simple blank
413 # AuthUser
413 # AuthUser
414 auth_user = AuthUser(ip_addr=ip_addr)
414 auth_user = AuthUser(ip_addr=ip_addr)
415
415
416 if password_changed(auth_user, session):
416 if password_changed(auth_user, session):
417 session.invalidate()
417 session.invalidate()
418 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
418 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
419 auth_user = AuthUser(ip_addr=ip_addr)
419 auth_user = AuthUser(ip_addr=ip_addr)
420
420
421 authenticated = cookie_store.get('is_authenticated')
421 authenticated = cookie_store.get('is_authenticated')
422
422
423 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:
424 # user is not authenticated and not empty
424 # user is not authenticated and not empty
425 auth_user.set_authenticated(authenticated)
425 auth_user.set_authenticated(authenticated)
426
426
427 return auth_user
427 return auth_user
428
428
429
429
430 class BaseController(WSGIController):
430 class BaseController(WSGIController):
431
431
432 def __before__(self):
432 def __before__(self):
433 """
433 """
434 __before__ is called before controller methods and after __call__
434 __before__ is called before controller methods and after __call__
435 """
435 """
436 # on each call propagate settings calls into global settings.
436 # on each call propagate settings calls into global settings.
437 set_rhodecode_config(config)
437 set_rhodecode_config(config)
438 attach_context_attributes(c, request)
438 attach_context_attributes(c, request)
439
439
440 # TODO: Remove this when fixed in attach_context_attributes()
440 # TODO: Remove this when fixed in attach_context_attributes()
441 c.repo_name = get_repo_slug(request) # can be empty
441 c.repo_name = get_repo_slug(request) # can be empty
442
442
443 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'))
444 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'))
445 self.sa = meta.Session
445 self.sa = meta.Session
446 self.scm_model = ScmModel(self.sa)
446 self.scm_model = ScmModel(self.sa)
447
447
448 # set user language
448 # set user language
449 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
449 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
450 if user_lang:
450 if user_lang:
451 translation.set_lang(user_lang)
451 translation.set_lang(user_lang)
452 log.debug('set language to %s for user %s',
452 log.debug('set language to %s for user %s',
453 user_lang, self._rhodecode_user)
453 user_lang, self._rhodecode_user)
454
454
455 def _dispatch_redirect(self, with_url, environ, start_response):
455 def _dispatch_redirect(self, with_url, environ, start_response):
456 resp = HTTPFound(with_url)
456 resp = HTTPFound(with_url)
457 environ['SCRIPT_NAME'] = '' # handle prefix middleware
457 environ['SCRIPT_NAME'] = '' # handle prefix middleware
458 environ['PATH_INFO'] = with_url
458 environ['PATH_INFO'] = with_url
459 return resp(environ, start_response)
459 return resp(environ, start_response)
460
460
461 def __call__(self, environ, start_response):
461 def __call__(self, environ, start_response):
462 """Invoke the Controller"""
462 """Invoke the Controller"""
463 # WSGIController.__call__ dispatches to the Controller method
463 # WSGIController.__call__ dispatches to the Controller method
464 # the request is routed to. This routing information is
464 # the request is routed to. This routing information is
465 # available in environ['pylons.routes_dict']
465 # available in environ['pylons.routes_dict']
466 from rhodecode.lib import helpers as h
466 from rhodecode.lib import helpers as h
467
467
468 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
468 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
469 if environ.get('debugtoolbar.wants_pylons_context', False):
469 if environ.get('debugtoolbar.wants_pylons_context', False):
470 environ['debugtoolbar.pylons_context'] = c._current_obj()
470 environ['debugtoolbar.pylons_context'] = c._current_obj()
471
471
472 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
472 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
473 environ['pylons.routes_dict']['action']])
473 environ['pylons.routes_dict']['action']])
474
474
475 self.rc_config = SettingsModel().get_all_settings(cache=True)
475 self.rc_config = SettingsModel().get_all_settings(cache=True)
476 self.ip_addr = get_ip_addr(environ)
476 self.ip_addr = get_ip_addr(environ)
477
477
478 # The rhodecode auth user is looked up and passed through the
478 # The rhodecode auth user is looked up and passed through the
479 # environ by the pylons compatibility tween in pyramid.
479 # environ by the pylons compatibility tween in pyramid.
480 # So we can just grab it from there.
480 # So we can just grab it from there.
481 auth_user = environ['rc_auth_user']
481 auth_user = environ['rc_auth_user']
482
482
483 # set globals for auth user
483 # set globals for auth user
484 request.user = auth_user
484 request.user = auth_user
485 c.rhodecode_user = self._rhodecode_user = auth_user
485 c.rhodecode_user = self._rhodecode_user = auth_user
486
486
487 log.info('IP: %s User: %s accessed %s [%s]' % (
487 log.info('IP: %s User: %s accessed %s [%s]' % (
488 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
488 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
489 _route_name)
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 user_obj = auth_user.get_instance()
492 user_obj = auth_user.get_instance()
495 if user_obj and user_obj.user_data.get('force_password_change'):
493 if user_obj and user_obj.user_data.get('force_password_change'):
496 h.flash('You are required to change your password', 'warning',
494 h.flash('You are required to change your password', 'warning',
497 ignore_duplicate=True)
495 ignore_duplicate=True)
498
496 return self._dispatch_redirect(
499 skip_user_check_urls = [
497 url('my_account_password'), environ, start_response)
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)
507
498
508 return WSGIController.__call__(self, environ, start_response)
499 return WSGIController.__call__(self, environ, start_response)
509
500
510
501
511 class BaseRepoController(BaseController):
502 class BaseRepoController(BaseController):
512 """
503 """
513 Base class for controllers responsible for loading all needed data for
504 Base class for controllers responsible for loading all needed data for
514 repository loaded items are
505 repository loaded items are
515
506
516 c.rhodecode_repo: instance of scm repository
507 c.rhodecode_repo: instance of scm repository
517 c.rhodecode_db_repo: instance of db
508 c.rhodecode_db_repo: instance of db
518 c.repository_requirements_missing: shows that repository specific data
509 c.repository_requirements_missing: shows that repository specific data
519 could not be displayed due to the missing requirements
510 could not be displayed due to the missing requirements
520 c.repository_pull_requests: show number of open pull requests
511 c.repository_pull_requests: show number of open pull requests
521 """
512 """
522
513
523 def __before__(self):
514 def __before__(self):
524 super(BaseRepoController, self).__before__()
515 super(BaseRepoController, self).__before__()
525 if c.repo_name: # extracted from routes
516 if c.repo_name: # extracted from routes
526 db_repo = Repository.get_by_repo_name(c.repo_name)
517 db_repo = Repository.get_by_repo_name(c.repo_name)
527 if not db_repo:
518 if not db_repo:
528 return
519 return
529
520
530 log.debug(
521 log.debug(
531 'Found repository in database %s with state `%s`',
522 'Found repository in database %s with state `%s`',
532 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
523 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
533 route = getattr(request.environ.get('routes.route'), 'name', '')
524 route = getattr(request.environ.get('routes.route'), 'name', '')
534
525
535 # allow to delete repos that are somehow damages in filesystem
526 # allow to delete repos that are somehow damages in filesystem
536 if route in ['delete_repo']:
527 if route in ['delete_repo']:
537 return
528 return
538
529
539 if db_repo.repo_state in [Repository.STATE_PENDING]:
530 if db_repo.repo_state in [Repository.STATE_PENDING]:
540 if route in ['repo_creating_home']:
531 if route in ['repo_creating_home']:
541 return
532 return
542 check_url = url('repo_creating_home', repo_name=c.repo_name)
533 check_url = url('repo_creating_home', repo_name=c.repo_name)
543 return redirect(check_url)
534 return redirect(check_url)
544
535
545 self.rhodecode_db_repo = db_repo
536 self.rhodecode_db_repo = db_repo
546
537
547 missing_requirements = False
538 missing_requirements = False
548 try:
539 try:
549 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
540 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
550 except RepositoryRequirementError as e:
541 except RepositoryRequirementError as e:
551 missing_requirements = True
542 missing_requirements = True
552 self._handle_missing_requirements(e)
543 self._handle_missing_requirements(e)
553
544
554 if self.rhodecode_repo is None and not missing_requirements:
545 if self.rhodecode_repo is None and not missing_requirements:
555 log.error('%s this repository is present in database but it '
546 log.error('%s this repository is present in database but it '
556 'cannot be created as an scm instance', c.repo_name)
547 'cannot be created as an scm instance', c.repo_name)
557
548
558 h.flash(_(
549 h.flash(_(
559 "The repository at %(repo_name)s cannot be located.") %
550 "The repository at %(repo_name)s cannot be located.") %
560 {'repo_name': c.repo_name},
551 {'repo_name': c.repo_name},
561 category='error', ignore_duplicate=True)
552 category='error', ignore_duplicate=True)
562 redirect(url('home'))
553 redirect(url('home'))
563
554
564 # update last change according to VCS data
555 # update last change according to VCS data
565 if not missing_requirements:
556 if not missing_requirements:
566 commit = db_repo.get_commit(
557 commit = db_repo.get_commit(
567 pre_load=["author", "date", "message", "parents"])
558 pre_load=["author", "date", "message", "parents"])
568 db_repo.update_commit_cache(commit)
559 db_repo.update_commit_cache(commit)
569
560
570 # Prepare context
561 # Prepare context
571 c.rhodecode_db_repo = db_repo
562 c.rhodecode_db_repo = db_repo
572 c.rhodecode_repo = self.rhodecode_repo
563 c.rhodecode_repo = self.rhodecode_repo
573 c.repository_requirements_missing = missing_requirements
564 c.repository_requirements_missing = missing_requirements
574
565
575 self._update_global_counters(self.scm_model, db_repo)
566 self._update_global_counters(self.scm_model, db_repo)
576
567
577 def _update_global_counters(self, scm_model, db_repo):
568 def _update_global_counters(self, scm_model, db_repo):
578 """
569 """
579 Base variables that are exposed to every page of repository
570 Base variables that are exposed to every page of repository
580 """
571 """
581 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
572 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
582
573
583 def _handle_missing_requirements(self, error):
574 def _handle_missing_requirements(self, error):
584 self.rhodecode_repo = None
575 self.rhodecode_repo = None
585 log.error(
576 log.error(
586 'Requirements are missing for repository %s: %s',
577 'Requirements are missing for repository %s: %s',
587 c.repo_name, error.message)
578 c.repo_name, error.message)
588
579
589 summary_url = url('summary_home', repo_name=c.repo_name)
580 summary_url = url('summary_home', repo_name=c.repo_name)
590 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
581 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
591 settings_update_url = url('repo', repo_name=c.repo_name)
582 settings_update_url = url('repo', repo_name=c.repo_name)
592 path = request.path
583 path = request.path
593 should_redirect = (
584 should_redirect = (
594 path not in (summary_url, settings_update_url)
585 path not in (summary_url, settings_update_url)
595 and '/settings' not in path or path == statistics_url
586 and '/settings' not in path or path == statistics_url
596 )
587 )
597 if should_redirect:
588 if should_redirect:
598 redirect(summary_url)
589 redirect(summary_url)
General Comments 0
You need to be logged in to leave comments. Login now