##// END OF EJS Templates
login: simplify came_from validation...
Søren Løvborg -
r5510:a0a9ae75 stable
parent child Browse files
Show More
@@ -1,270 +1,257 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.login
15 kallithea.controllers.login
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Login controller for Kallithea
18 Login controller for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 22, 2010
22 :created_on: Apr 22, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28
28
29 import logging
29 import logging
30 import formencode
30 import formencode
31 import urlparse
31 import urlparse
32
32
33 from formencode import htmlfill
33 from formencode import htmlfill
34 from webob.exc import HTTPFound, HTTPBadRequest
34 from webob.exc import HTTPFound, HTTPBadRequest
35 from pylons.i18n.translation import _
35 from pylons.i18n.translation import _
36 from pylons.controllers.util import redirect
36 from pylons.controllers.util import redirect
37 from pylons import request, session, tmpl_context as c, url
37 from pylons import request, session, tmpl_context as c, url
38
38
39 import kallithea.lib.helpers as h
39 import kallithea.lib.helpers as h
40 from kallithea.lib.auth import AuthUser, HasPermissionAnyDecorator
40 from kallithea.lib.auth import AuthUser, HasPermissionAnyDecorator
41 from kallithea.lib.base import BaseController, log_in_user, render
41 from kallithea.lib.base import BaseController, log_in_user, render
42 from kallithea.lib.exceptions import UserCreationError
42 from kallithea.lib.exceptions import UserCreationError
43 from kallithea.lib.utils2 import safe_str
43 from kallithea.lib.utils2 import safe_str
44 from kallithea.model.db import User, Setting
44 from kallithea.model.db import User, Setting
45 from kallithea.model.forms import \
45 from kallithea.model.forms import \
46 LoginForm, RegisterForm, PasswordResetRequestForm, PasswordResetConfirmationForm
46 LoginForm, RegisterForm, PasswordResetRequestForm, PasswordResetConfirmationForm
47 from kallithea.model.user import UserModel
47 from kallithea.model.user import UserModel
48 from kallithea.model.meta import Session
48 from kallithea.model.meta import Session
49
49
50
50
51 log = logging.getLogger(__name__)
51 log = logging.getLogger(__name__)
52
52
53
53
54 class LoginController(BaseController):
54 class LoginController(BaseController):
55
55
56 def __before__(self):
56 def __before__(self):
57 super(LoginController, self).__before__()
57 super(LoginController, self).__before__()
58
58
59 def _validate_came_from(self, came_from):
59 def _validate_came_from(self, came_from):
60 """Return True if came_from is valid and can and should be used"""
60 """Return True if came_from is valid and can and should be used"""
61 if not came_from:
61 url = urlparse.urlsplit(came_from)
62 return False
62 return not url.scheme and not url.netloc
63
64 parsed = urlparse.urlparse(came_from)
65 server_parsed = urlparse.urlparse(url.current())
66 allowed_schemes = ['http', 'https']
67 if parsed.scheme and parsed.scheme not in allowed_schemes:
68 log.error('Suspicious URL scheme detected %s for url %s',
69 parsed.scheme, parsed)
70 return False
71 if server_parsed.netloc != parsed.netloc:
72 log.error('Suspicious NETLOC detected %s for url %s server url '
73 'is: %s' % (parsed.netloc, parsed, server_parsed))
74 return False
75 return True
76
63
77 def index(self):
64 def index(self):
78 c.came_from = safe_str(request.GET.pop('came_from', ''))
65 c.came_from = safe_str(request.GET.pop('came_from', ''))
79 if c.came_from:
66 if c.came_from:
80 if not self._validate_came_from(c.came_from):
67 if not self._validate_came_from(c.came_from):
81 log.error('Invalid came_from (not server-relative): %r', c.came_from)
68 log.error('Invalid came_from (not server-relative): %r', c.came_from)
82 raise HTTPBadRequest()
69 raise HTTPBadRequest()
83 came_from = url(c.came_from, **request.GET)
70 came_from = url(c.came_from, **request.GET)
84 else:
71 else:
85 c.came_from = came_from = url('home')
72 c.came_from = came_from = url('home')
86
73
87 not_default = self.authuser.username != User.DEFAULT_USER
74 not_default = self.authuser.username != User.DEFAULT_USER
88 ip_allowed = AuthUser.check_ip_allowed(self.authuser, self.ip_addr)
75 ip_allowed = AuthUser.check_ip_allowed(self.authuser, self.ip_addr)
89
76
90 # redirect if already logged in
77 # redirect if already logged in
91 if self.authuser.is_authenticated and not_default and ip_allowed:
78 if self.authuser.is_authenticated and not_default and ip_allowed:
92 raise HTTPFound(location=came_from)
79 raise HTTPFound(location=came_from)
93
80
94 if request.POST:
81 if request.POST:
95 # import Login Form validator class
82 # import Login Form validator class
96 login_form = LoginForm()
83 login_form = LoginForm()
97 try:
84 try:
98 c.form_result = login_form.to_python(dict(request.POST))
85 c.form_result = login_form.to_python(dict(request.POST))
99 # form checks for username/password, now we're authenticated
86 # form checks for username/password, now we're authenticated
100 username = c.form_result['username']
87 username = c.form_result['username']
101 user = User.get_by_username(username, case_insensitive=True)
88 user = User.get_by_username(username, case_insensitive=True)
102 except formencode.Invalid as errors:
89 except formencode.Invalid as errors:
103 defaults = errors.value
90 defaults = errors.value
104 # remove password from filling in form again
91 # remove password from filling in form again
105 del defaults['password']
92 del defaults['password']
106 return htmlfill.render(
93 return htmlfill.render(
107 render('/login.html'),
94 render('/login.html'),
108 defaults=errors.value,
95 defaults=errors.value,
109 errors=errors.error_dict or {},
96 errors=errors.error_dict or {},
110 prefix_error=False,
97 prefix_error=False,
111 encoding="UTF-8",
98 encoding="UTF-8",
112 force_defaults=False)
99 force_defaults=False)
113 except UserCreationError as e:
100 except UserCreationError as e:
114 # container auth or other auth functions that create users on
101 # container auth or other auth functions that create users on
115 # the fly can throw this exception signaling that there's issue
102 # the fly can throw this exception signaling that there's issue
116 # with user creation, explanation should be provided in
103 # with user creation, explanation should be provided in
117 # Exception itself
104 # Exception itself
118 h.flash(e, 'error')
105 h.flash(e, 'error')
119 else:
106 else:
120 log_in_user(user, c.form_result['remember'],
107 log_in_user(user, c.form_result['remember'],
121 is_external_auth=False)
108 is_external_auth=False)
122 raise HTTPFound(location=came_from)
109 raise HTTPFound(location=came_from)
123
110
124 return render('/login.html')
111 return render('/login.html')
125
112
126 @HasPermissionAnyDecorator('hg.admin', 'hg.register.auto_activate',
113 @HasPermissionAnyDecorator('hg.admin', 'hg.register.auto_activate',
127 'hg.register.manual_activate')
114 'hg.register.manual_activate')
128 def register(self):
115 def register(self):
129 c.auto_active = 'hg.register.auto_activate' in User.get_default_user()\
116 c.auto_active = 'hg.register.auto_activate' in User.get_default_user()\
130 .AuthUser.permissions['global']
117 .AuthUser.permissions['global']
131
118
132 settings = Setting.get_app_settings()
119 settings = Setting.get_app_settings()
133 captcha_private_key = settings.get('captcha_private_key')
120 captcha_private_key = settings.get('captcha_private_key')
134 c.captcha_active = bool(captcha_private_key)
121 c.captcha_active = bool(captcha_private_key)
135 c.captcha_public_key = settings.get('captcha_public_key')
122 c.captcha_public_key = settings.get('captcha_public_key')
136
123
137 if request.POST:
124 if request.POST:
138 register_form = RegisterForm()()
125 register_form = RegisterForm()()
139 try:
126 try:
140 form_result = register_form.to_python(dict(request.POST))
127 form_result = register_form.to_python(dict(request.POST))
141 form_result['active'] = c.auto_active
128 form_result['active'] = c.auto_active
142
129
143 if c.captcha_active:
130 if c.captcha_active:
144 from kallithea.lib.recaptcha import submit
131 from kallithea.lib.recaptcha import submit
145 response = submit(request.POST.get('recaptcha_challenge_field'),
132 response = submit(request.POST.get('recaptcha_challenge_field'),
146 request.POST.get('recaptcha_response_field'),
133 request.POST.get('recaptcha_response_field'),
147 private_key=captcha_private_key,
134 private_key=captcha_private_key,
148 remoteip=self.ip_addr)
135 remoteip=self.ip_addr)
149 if c.captcha_active and not response.is_valid:
136 if c.captcha_active and not response.is_valid:
150 _value = form_result
137 _value = form_result
151 _msg = _('Bad captcha')
138 _msg = _('Bad captcha')
152 error_dict = {'recaptcha_field': _msg}
139 error_dict = {'recaptcha_field': _msg}
153 raise formencode.Invalid(_msg, _value, None,
140 raise formencode.Invalid(_msg, _value, None,
154 error_dict=error_dict)
141 error_dict=error_dict)
155
142
156 UserModel().create_registration(form_result)
143 UserModel().create_registration(form_result)
157 h.flash(_('You have successfully registered into Kallithea'),
144 h.flash(_('You have successfully registered into Kallithea'),
158 category='success')
145 category='success')
159 Session().commit()
146 Session().commit()
160 return redirect(url('login_home'))
147 return redirect(url('login_home'))
161
148
162 except formencode.Invalid as errors:
149 except formencode.Invalid as errors:
163 return htmlfill.render(
150 return htmlfill.render(
164 render('/register.html'),
151 render('/register.html'),
165 defaults=errors.value,
152 defaults=errors.value,
166 errors=errors.error_dict or {},
153 errors=errors.error_dict or {},
167 prefix_error=False,
154 prefix_error=False,
168 encoding="UTF-8",
155 encoding="UTF-8",
169 force_defaults=False)
156 force_defaults=False)
170 except UserCreationError as e:
157 except UserCreationError as e:
171 # container auth or other auth functions that create users on
158 # container auth or other auth functions that create users on
172 # the fly can throw this exception signaling that there's issue
159 # the fly can throw this exception signaling that there's issue
173 # with user creation, explanation should be provided in
160 # with user creation, explanation should be provided in
174 # Exception itself
161 # Exception itself
175 h.flash(e, 'error')
162 h.flash(e, 'error')
176
163
177 return render('/register.html')
164 return render('/register.html')
178
165
179 def password_reset(self):
166 def password_reset(self):
180 settings = Setting.get_app_settings()
167 settings = Setting.get_app_settings()
181 captcha_private_key = settings.get('captcha_private_key')
168 captcha_private_key = settings.get('captcha_private_key')
182 c.captcha_active = bool(captcha_private_key)
169 c.captcha_active = bool(captcha_private_key)
183 c.captcha_public_key = settings.get('captcha_public_key')
170 c.captcha_public_key = settings.get('captcha_public_key')
184
171
185 if request.POST:
172 if request.POST:
186 password_reset_form = PasswordResetRequestForm()()
173 password_reset_form = PasswordResetRequestForm()()
187 try:
174 try:
188 form_result = password_reset_form.to_python(dict(request.POST))
175 form_result = password_reset_form.to_python(dict(request.POST))
189 if c.captcha_active:
176 if c.captcha_active:
190 from kallithea.lib.recaptcha import submit
177 from kallithea.lib.recaptcha import submit
191 response = submit(request.POST.get('recaptcha_challenge_field'),
178 response = submit(request.POST.get('recaptcha_challenge_field'),
192 request.POST.get('recaptcha_response_field'),
179 request.POST.get('recaptcha_response_field'),
193 private_key=captcha_private_key,
180 private_key=captcha_private_key,
194 remoteip=self.ip_addr)
181 remoteip=self.ip_addr)
195 if c.captcha_active and not response.is_valid:
182 if c.captcha_active and not response.is_valid:
196 _value = form_result
183 _value = form_result
197 _msg = _('Bad captcha')
184 _msg = _('Bad captcha')
198 error_dict = {'recaptcha_field': _msg}
185 error_dict = {'recaptcha_field': _msg}
199 raise formencode.Invalid(_msg, _value, None,
186 raise formencode.Invalid(_msg, _value, None,
200 error_dict=error_dict)
187 error_dict=error_dict)
201 redirect_link = UserModel().send_reset_password_email(form_result)
188 redirect_link = UserModel().send_reset_password_email(form_result)
202 h.flash(_('A password reset confirmation code has been sent'),
189 h.flash(_('A password reset confirmation code has been sent'),
203 category='success')
190 category='success')
204 return redirect(redirect_link)
191 return redirect(redirect_link)
205
192
206 except formencode.Invalid as errors:
193 except formencode.Invalid as errors:
207 return htmlfill.render(
194 return htmlfill.render(
208 render('/password_reset.html'),
195 render('/password_reset.html'),
209 defaults=errors.value,
196 defaults=errors.value,
210 errors=errors.error_dict or {},
197 errors=errors.error_dict or {},
211 prefix_error=False,
198 prefix_error=False,
212 encoding="UTF-8",
199 encoding="UTF-8",
213 force_defaults=False)
200 force_defaults=False)
214
201
215 return render('/password_reset.html')
202 return render('/password_reset.html')
216
203
217 def password_reset_confirmation(self):
204 def password_reset_confirmation(self):
218 # This controller handles both GET and POST requests, though we
205 # This controller handles both GET and POST requests, though we
219 # only ever perform the actual password change on POST (since
206 # only ever perform the actual password change on POST (since
220 # GET requests are not allowed to have side effects, and do not
207 # GET requests are not allowed to have side effects, and do not
221 # receive automatic CSRF protection).
208 # receive automatic CSRF protection).
222
209
223 # The template needs the email address outside of the form.
210 # The template needs the email address outside of the form.
224 c.email = request.params.get('email')
211 c.email = request.params.get('email')
225
212
226 if not request.POST:
213 if not request.POST:
227 return htmlfill.render(
214 return htmlfill.render(
228 render('/password_reset_confirmation.html'),
215 render('/password_reset_confirmation.html'),
229 defaults=dict(request.params),
216 defaults=dict(request.params),
230 encoding='UTF-8')
217 encoding='UTF-8')
231
218
232 form = PasswordResetConfirmationForm()()
219 form = PasswordResetConfirmationForm()()
233 try:
220 try:
234 form_result = form.to_python(dict(request.POST))
221 form_result = form.to_python(dict(request.POST))
235 except formencode.Invalid as errors:
222 except formencode.Invalid as errors:
236 return htmlfill.render(
223 return htmlfill.render(
237 render('/password_reset_confirmation.html'),
224 render('/password_reset_confirmation.html'),
238 defaults=errors.value,
225 defaults=errors.value,
239 errors=errors.error_dict or {},
226 errors=errors.error_dict or {},
240 prefix_error=False,
227 prefix_error=False,
241 encoding='UTF-8')
228 encoding='UTF-8')
242
229
243 if not UserModel().verify_reset_password_token(
230 if not UserModel().verify_reset_password_token(
244 form_result['email'],
231 form_result['email'],
245 form_result['timestamp'],
232 form_result['timestamp'],
246 form_result['token'],
233 form_result['token'],
247 ):
234 ):
248 return htmlfill.render(
235 return htmlfill.render(
249 render('/password_reset_confirmation.html'),
236 render('/password_reset_confirmation.html'),
250 defaults=form_result,
237 defaults=form_result,
251 errors={'token': _('Invalid password reset token')},
238 errors={'token': _('Invalid password reset token')},
252 prefix_error=False,
239 prefix_error=False,
253 encoding='UTF-8')
240 encoding='UTF-8')
254
241
255 UserModel().reset_password(form_result['email'], form_result['password'])
242 UserModel().reset_password(form_result['email'], form_result['password'])
256 h.flash(_('Successfully updated password'), category='success')
243 h.flash(_('Successfully updated password'), category='success')
257 return redirect(url('login_home'))
244 return redirect(url('login_home'))
258
245
259 def logout(self):
246 def logout(self):
260 session.delete()
247 session.delete()
261 log.info('Logging out and deleting session for user')
248 log.info('Logging out and deleting session for user')
262 redirect(url('home'))
249 redirect(url('home'))
263
250
264 def authentication_token(self):
251 def authentication_token(self):
265 """Return the CSRF protection token for the session - just like it
252 """Return the CSRF protection token for the session - just like it
266 could have been screen scraped from a page with a form.
253 could have been screen scraped from a page with a form.
267 Only intended for testing but might also be useful for other kinds
254 Only intended for testing but might also be useful for other kinds
268 of automation.
255 of automation.
269 """
256 """
270 return h.authentication_token()
257 return h.authentication_token()
@@ -1,500 +1,496 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 import re
2 import re
3 import time
3 import time
4
4
5 import mock
5 import mock
6
6
7 from kallithea.tests import *
7 from kallithea.tests import *
8 from kallithea.tests.fixture import Fixture
8 from kallithea.tests.fixture import Fixture
9 from kallithea.lib.utils2 import generate_api_key
9 from kallithea.lib.utils2 import generate_api_key
10 from kallithea.lib.auth import check_password
10 from kallithea.lib.auth import check_password
11 from kallithea.lib import helpers as h
11 from kallithea.lib import helpers as h
12 from kallithea.model.api_key import ApiKeyModel
12 from kallithea.model.api_key import ApiKeyModel
13 from kallithea.model import validators
13 from kallithea.model import validators
14 from kallithea.model.db import User, Notification
14 from kallithea.model.db import User, Notification
15 from kallithea.model.meta import Session
15 from kallithea.model.meta import Session
16 from kallithea.model.user import UserModel
16 from kallithea.model.user import UserModel
17
17
18 fixture = Fixture()
18 fixture = Fixture()
19
19
20
20
21 class TestLoginController(TestController):
21 class TestLoginController(TestController):
22 def setUp(self):
22 def setUp(self):
23 self.remove_all_notifications()
23 self.remove_all_notifications()
24 self.assertEqual(Notification.query().all(), [])
24 self.assertEqual(Notification.query().all(), [])
25
25
26 def test_index(self):
26 def test_index(self):
27 response = self.app.get(url(controller='login', action='index'))
27 response = self.app.get(url(controller='login', action='index'))
28 self.assertEqual(response.status, '200 OK')
28 self.assertEqual(response.status, '200 OK')
29 # Test response...
29 # Test response...
30
30
31 def test_login_admin_ok(self):
31 def test_login_admin_ok(self):
32 response = self.app.post(url(controller='login', action='index'),
32 response = self.app.post(url(controller='login', action='index'),
33 {'username': TEST_USER_ADMIN_LOGIN,
33 {'username': TEST_USER_ADMIN_LOGIN,
34 'password': TEST_USER_ADMIN_PASS})
34 'password': TEST_USER_ADMIN_PASS})
35 self.assertEqual(response.status, '302 Found')
35 self.assertEqual(response.status, '302 Found')
36 self.assert_authenticated_user(response, TEST_USER_ADMIN_LOGIN)
36 self.assert_authenticated_user(response, TEST_USER_ADMIN_LOGIN)
37
37
38 response = response.follow()
38 response = response.follow()
39 response.mustcontain('/%s' % HG_REPO)
39 response.mustcontain('/%s' % HG_REPO)
40
40
41 def test_login_regular_ok(self):
41 def test_login_regular_ok(self):
42 response = self.app.post(url(controller='login', action='index'),
42 response = self.app.post(url(controller='login', action='index'),
43 {'username': TEST_USER_REGULAR_LOGIN,
43 {'username': TEST_USER_REGULAR_LOGIN,
44 'password': TEST_USER_REGULAR_PASS})
44 'password': TEST_USER_REGULAR_PASS})
45
45
46 self.assertEqual(response.status, '302 Found')
46 self.assertEqual(response.status, '302 Found')
47 self.assert_authenticated_user(response, TEST_USER_REGULAR_LOGIN)
47 self.assert_authenticated_user(response, TEST_USER_REGULAR_LOGIN)
48
48
49 response = response.follow()
49 response = response.follow()
50 response.mustcontain('/%s' % HG_REPO)
50 response.mustcontain('/%s' % HG_REPO)
51
51
52 def test_login_ok_came_from(self):
52 def test_login_ok_came_from(self):
53 test_came_from = '/_admin/users'
53 test_came_from = '/_admin/users'
54 response = self.app.post(url(controller='login', action='index',
54 response = self.app.post(url(controller='login', action='index',
55 came_from=test_came_from),
55 came_from=test_came_from),
56 {'username': TEST_USER_ADMIN_LOGIN,
56 {'username': TEST_USER_ADMIN_LOGIN,
57 'password': TEST_USER_ADMIN_PASS})
57 'password': TEST_USER_ADMIN_PASS})
58 self.assertEqual(response.status, '302 Found')
58 self.assertEqual(response.status, '302 Found')
59 response = response.follow()
59 response = response.follow()
60
60
61 self.assertEqual(response.status, '200 OK')
61 self.assertEqual(response.status, '200 OK')
62 response.mustcontain('Users Administration')
62 response.mustcontain('Users Administration')
63
63
64 def test_login_do_not_remember(self):
64 def test_login_do_not_remember(self):
65 response = self.app.post(url(controller='login', action='index'),
65 response = self.app.post(url(controller='login', action='index'),
66 {'username': TEST_USER_REGULAR_LOGIN,
66 {'username': TEST_USER_REGULAR_LOGIN,
67 'password': TEST_USER_REGULAR_PASS,
67 'password': TEST_USER_REGULAR_PASS,
68 'remember': False})
68 'remember': False})
69
69
70 self.assertIn('Set-Cookie', response.headers)
70 self.assertIn('Set-Cookie', response.headers)
71 for cookie in response.headers.getall('Set-Cookie'):
71 for cookie in response.headers.getall('Set-Cookie'):
72 self.assertFalse(re.search(r';\s+(Max-Age|Expires)=', cookie, re.IGNORECASE),
72 self.assertFalse(re.search(r';\s+(Max-Age|Expires)=', cookie, re.IGNORECASE),
73 'Cookie %r has expiration date, but should be a session cookie' % cookie)
73 'Cookie %r has expiration date, but should be a session cookie' % cookie)
74
74
75 def test_login_remember(self):
75 def test_login_remember(self):
76 response = self.app.post(url(controller='login', action='index'),
76 response = self.app.post(url(controller='login', action='index'),
77 {'username': TEST_USER_REGULAR_LOGIN,
77 {'username': TEST_USER_REGULAR_LOGIN,
78 'password': TEST_USER_REGULAR_PASS,
78 'password': TEST_USER_REGULAR_PASS,
79 'remember': True})
79 'remember': True})
80
80
81 self.assertIn('Set-Cookie', response.headers)
81 self.assertIn('Set-Cookie', response.headers)
82 for cookie in response.headers.getall('Set-Cookie'):
82 for cookie in response.headers.getall('Set-Cookie'):
83 self.assertTrue(re.search(r';\s+(Max-Age|Expires)=', cookie, re.IGNORECASE),
83 self.assertTrue(re.search(r';\s+(Max-Age|Expires)=', cookie, re.IGNORECASE),
84 'Cookie %r should have expiration date, but is a session cookie' % cookie)
84 'Cookie %r should have expiration date, but is a session cookie' % cookie)
85
85
86 def test_logout(self):
86 def test_logout(self):
87 response = self.app.post(url(controller='login', action='index'),
87 response = self.app.post(url(controller='login', action='index'),
88 {'username': TEST_USER_REGULAR_LOGIN,
88 {'username': TEST_USER_REGULAR_LOGIN,
89 'password': TEST_USER_REGULAR_PASS})
89 'password': TEST_USER_REGULAR_PASS})
90
90
91 # Verify that a login session has been established.
91 # Verify that a login session has been established.
92 response = self.app.get(url(controller='login', action='index'))
92 response = self.app.get(url(controller='login', action='index'))
93 response = response.follow()
93 response = response.follow()
94 self.assertIn('authuser', response.session)
94 self.assertIn('authuser', response.session)
95
95
96 response.click('Log Out')
96 response.click('Log Out')
97
97
98 # Verify that the login session has been terminated.
98 # Verify that the login session has been terminated.
99 response = self.app.get(url(controller='login', action='index'))
99 response = self.app.get(url(controller='login', action='index'))
100 self.assertNotIn('authuser', response.session)
100 self.assertNotIn('authuser', response.session)
101
101
102 @parameterized.expand([
102 @parameterized.expand([
103 ('data:text/html,<script>window.alert("xss")</script>',),
103 ('data:text/html,<script>window.alert("xss")</script>',),
104 ('mailto:test@example.com',),
104 ('mailto:test@example.com',),
105 ('file:///etc/passwd',),
105 ('file:///etc/passwd',),
106 ('ftp://ftp.example.com',),
106 ('ftp://ftp.example.com',),
107 ('http://other.example.com/bl%C3%A5b%C3%A6rgr%C3%B8d',),
107 ('http://other.example.com/bl%C3%A5b%C3%A6rgr%C3%B8d',),
108 ('//evil.example.com/',),
108 ])
109 ])
109 def test_login_bad_came_froms(self, url_came_from):
110 def test_login_bad_came_froms(self, url_came_from):
110 response = self.app.post(url(controller='login', action='index',
111 response = self.app.post(url(controller='login', action='index',
111 came_from=url_came_from),
112 came_from=url_came_from),
112 {'username': TEST_USER_ADMIN_LOGIN,
113 {'username': TEST_USER_ADMIN_LOGIN,
113 'password': TEST_USER_ADMIN_PASS})
114 'password': TEST_USER_ADMIN_PASS},
114 self.assertEqual(response.status, '302 Found')
115 status=400)
115 self.assertEqual(response._environ['paste.testing_variables']
116 ['tmpl_context'].came_from, '/')
117 response = response.follow()
118
119 self.assertEqual(response.status, '200 OK')
120
116
121 def test_login_short_password(self):
117 def test_login_short_password(self):
122 response = self.app.post(url(controller='login', action='index'),
118 response = self.app.post(url(controller='login', action='index'),
123 {'username': TEST_USER_ADMIN_LOGIN,
119 {'username': TEST_USER_ADMIN_LOGIN,
124 'password': 'as'})
120 'password': 'as'})
125 self.assertEqual(response.status, '200 OK')
121 self.assertEqual(response.status, '200 OK')
126
122
127 response.mustcontain('Enter 3 characters or more')
123 response.mustcontain('Enter 3 characters or more')
128
124
129 def test_login_wrong_username_password(self):
125 def test_login_wrong_username_password(self):
130 response = self.app.post(url(controller='login', action='index'),
126 response = self.app.post(url(controller='login', action='index'),
131 {'username': 'error',
127 {'username': 'error',
132 'password': 'test12'})
128 'password': 'test12'})
133
129
134 response.mustcontain('Invalid username or password')
130 response.mustcontain('Invalid username or password')
135
131
136 # verify that get arguments are correctly passed along login redirection
132 # verify that get arguments are correctly passed along login redirection
137
133
138 @parameterized.expand([
134 @parameterized.expand([
139 ({'foo':'one', 'bar':'two'}, ('foo=one', 'bar=two')),
135 ({'foo':'one', 'bar':'two'}, ('foo=one', 'bar=two')),
140 ({'blue': u'blå'.encode('utf-8'), 'green':u'grøn'},
136 ({'blue': u'blå'.encode('utf-8'), 'green':u'grøn'},
141 ('blue=bl%C3%A5', 'green=gr%C3%B8n')),
137 ('blue=bl%C3%A5', 'green=gr%C3%B8n')),
142 ])
138 ])
143 def test_redirection_to_login_form_preserves_get_args(self, args, args_encoded):
139 def test_redirection_to_login_form_preserves_get_args(self, args, args_encoded):
144 with fixture.anon_access(False):
140 with fixture.anon_access(False):
145 response = self.app.get(url(controller='summary', action='index',
141 response = self.app.get(url(controller='summary', action='index',
146 repo_name=HG_REPO,
142 repo_name=HG_REPO,
147 **args))
143 **args))
148 self.assertEqual(response.status, '302 Found')
144 self.assertEqual(response.status, '302 Found')
149 for encoded in args_encoded:
145 for encoded in args_encoded:
150 self.assertIn(encoded, response.location)
146 self.assertIn(encoded, response.location)
151
147
152 @parameterized.expand([
148 @parameterized.expand([
153 ({'foo':'one', 'bar':'two'}, ('foo=one', 'bar=two')),
149 ({'foo':'one', 'bar':'two'}, ('foo=one', 'bar=two')),
154 ({'blue': u'blå'.encode('utf-8'), 'green':u'grøn'},
150 ({'blue': u'blå'.encode('utf-8'), 'green':u'grøn'},
155 ('blue=bl%C3%A5', 'green=gr%C3%B8n')),
151 ('blue=bl%C3%A5', 'green=gr%C3%B8n')),
156 ])
152 ])
157 def test_login_form_preserves_get_args(self, args, args_encoded):
153 def test_login_form_preserves_get_args(self, args, args_encoded):
158 response = self.app.get(url(controller='login', action='index',
154 response = self.app.get(url(controller='login', action='index',
159 came_from = '/_admin/users',
155 came_from = '/_admin/users',
160 **args))
156 **args))
161 for encoded in args_encoded:
157 for encoded in args_encoded:
162 self.assertIn(encoded, response.form.action)
158 self.assertIn(encoded, response.form.action)
163
159
164 @parameterized.expand([
160 @parameterized.expand([
165 ({'foo':'one', 'bar':'two'}, ('foo=one', 'bar=two')),
161 ({'foo':'one', 'bar':'two'}, ('foo=one', 'bar=two')),
166 ({'blue': u'blå'.encode('utf-8'), 'green':u'grøn'},
162 ({'blue': u'blå'.encode('utf-8'), 'green':u'grøn'},
167 ('blue=bl%C3%A5', 'green=gr%C3%B8n')),
163 ('blue=bl%C3%A5', 'green=gr%C3%B8n')),
168 ])
164 ])
169 def test_redirection_after_successful_login_preserves_get_args(self, args, args_encoded):
165 def test_redirection_after_successful_login_preserves_get_args(self, args, args_encoded):
170 response = self.app.post(url(controller='login', action='index',
166 response = self.app.post(url(controller='login', action='index',
171 came_from = '/_admin/users',
167 came_from = '/_admin/users',
172 **args),
168 **args),
173 {'username': TEST_USER_ADMIN_LOGIN,
169 {'username': TEST_USER_ADMIN_LOGIN,
174 'password': TEST_USER_ADMIN_PASS})
170 'password': TEST_USER_ADMIN_PASS})
175 self.assertEqual(response.status, '302 Found')
171 self.assertEqual(response.status, '302 Found')
176 for encoded in args_encoded:
172 for encoded in args_encoded:
177 self.assertIn(encoded, response.location)
173 self.assertIn(encoded, response.location)
178
174
179 @parameterized.expand([
175 @parameterized.expand([
180 ({'foo':'one', 'bar':'two'}, ('foo=one', 'bar=two')),
176 ({'foo':'one', 'bar':'two'}, ('foo=one', 'bar=two')),
181 ({'blue': u'blå'.encode('utf-8'), 'green':u'grøn'},
177 ({'blue': u'blå'.encode('utf-8'), 'green':u'grøn'},
182 ('blue=bl%C3%A5', 'green=gr%C3%B8n')),
178 ('blue=bl%C3%A5', 'green=gr%C3%B8n')),
183 ])
179 ])
184 def test_login_form_after_incorrect_login_preserves_get_args(self, args, args_encoded):
180 def test_login_form_after_incorrect_login_preserves_get_args(self, args, args_encoded):
185 response = self.app.post(url(controller='login', action='index',
181 response = self.app.post(url(controller='login', action='index',
186 came_from = '/_admin/users',
182 came_from = '/_admin/users',
187 **args),
183 **args),
188 {'username': 'error',
184 {'username': 'error',
189 'password': 'test12'})
185 'password': 'test12'})
190
186
191 response.mustcontain('Invalid username or password')
187 response.mustcontain('Invalid username or password')
192 for encoded in args_encoded:
188 for encoded in args_encoded:
193 self.assertIn(encoded, response.form.action)
189 self.assertIn(encoded, response.form.action)
194
190
195 #==========================================================================
191 #==========================================================================
196 # REGISTRATIONS
192 # REGISTRATIONS
197 #==========================================================================
193 #==========================================================================
198 def test_register(self):
194 def test_register(self):
199 response = self.app.get(url(controller='login', action='register'))
195 response = self.app.get(url(controller='login', action='register'))
200 response.mustcontain('Sign Up')
196 response.mustcontain('Sign Up')
201
197
202 def test_register_err_same_username(self):
198 def test_register_err_same_username(self):
203 uname = TEST_USER_ADMIN_LOGIN
199 uname = TEST_USER_ADMIN_LOGIN
204 response = self.app.post(url(controller='login', action='register'),
200 response = self.app.post(url(controller='login', action='register'),
205 {'username': uname,
201 {'username': uname,
206 'password': 'test12',
202 'password': 'test12',
207 'password_confirmation': 'test12',
203 'password_confirmation': 'test12',
208 'email': 'goodmail@example.com',
204 'email': 'goodmail@example.com',
209 'firstname': 'test',
205 'firstname': 'test',
210 'lastname': 'test'})
206 'lastname': 'test'})
211
207
212 msg = validators.ValidUsername()._messages['username_exists']
208 msg = validators.ValidUsername()._messages['username_exists']
213 msg = h.html_escape(msg % {'username': uname})
209 msg = h.html_escape(msg % {'username': uname})
214 response.mustcontain(msg)
210 response.mustcontain(msg)
215
211
216 def test_register_err_same_email(self):
212 def test_register_err_same_email(self):
217 response = self.app.post(url(controller='login', action='register'),
213 response = self.app.post(url(controller='login', action='register'),
218 {'username': 'test_admin_0',
214 {'username': 'test_admin_0',
219 'password': 'test12',
215 'password': 'test12',
220 'password_confirmation': 'test12',
216 'password_confirmation': 'test12',
221 'email': TEST_USER_ADMIN_EMAIL,
217 'email': TEST_USER_ADMIN_EMAIL,
222 'firstname': 'test',
218 'firstname': 'test',
223 'lastname': 'test'})
219 'lastname': 'test'})
224
220
225 msg = validators.UniqSystemEmail()()._messages['email_taken']
221 msg = validators.UniqSystemEmail()()._messages['email_taken']
226 response.mustcontain(msg)
222 response.mustcontain(msg)
227
223
228 def test_register_err_same_email_case_sensitive(self):
224 def test_register_err_same_email_case_sensitive(self):
229 response = self.app.post(url(controller='login', action='register'),
225 response = self.app.post(url(controller='login', action='register'),
230 {'username': 'test_admin_1',
226 {'username': 'test_admin_1',
231 'password': 'test12',
227 'password': 'test12',
232 'password_confirmation': 'test12',
228 'password_confirmation': 'test12',
233 'email': TEST_USER_ADMIN_EMAIL.title(),
229 'email': TEST_USER_ADMIN_EMAIL.title(),
234 'firstname': 'test',
230 'firstname': 'test',
235 'lastname': 'test'})
231 'lastname': 'test'})
236 msg = validators.UniqSystemEmail()()._messages['email_taken']
232 msg = validators.UniqSystemEmail()()._messages['email_taken']
237 response.mustcontain(msg)
233 response.mustcontain(msg)
238
234
239 def test_register_err_wrong_data(self):
235 def test_register_err_wrong_data(self):
240 response = self.app.post(url(controller='login', action='register'),
236 response = self.app.post(url(controller='login', action='register'),
241 {'username': 'xs',
237 {'username': 'xs',
242 'password': 'test',
238 'password': 'test',
243 'password_confirmation': 'test',
239 'password_confirmation': 'test',
244 'email': 'goodmailm',
240 'email': 'goodmailm',
245 'firstname': 'test',
241 'firstname': 'test',
246 'lastname': 'test'})
242 'lastname': 'test'})
247 self.assertEqual(response.status, '200 OK')
243 self.assertEqual(response.status, '200 OK')
248 response.mustcontain('An email address must contain a single @')
244 response.mustcontain('An email address must contain a single @')
249 response.mustcontain('Enter a value 6 characters long or more')
245 response.mustcontain('Enter a value 6 characters long or more')
250
246
251 def test_register_err_username(self):
247 def test_register_err_username(self):
252 response = self.app.post(url(controller='login', action='register'),
248 response = self.app.post(url(controller='login', action='register'),
253 {'username': 'error user',
249 {'username': 'error user',
254 'password': 'test12',
250 'password': 'test12',
255 'password_confirmation': 'test12',
251 'password_confirmation': 'test12',
256 'email': 'goodmailm',
252 'email': 'goodmailm',
257 'firstname': 'test',
253 'firstname': 'test',
258 'lastname': 'test'})
254 'lastname': 'test'})
259
255
260 response.mustcontain('An email address must contain a single @')
256 response.mustcontain('An email address must contain a single @')
261 response.mustcontain('Username may only contain '
257 response.mustcontain('Username may only contain '
262 'alphanumeric characters underscores, '
258 'alphanumeric characters underscores, '
263 'periods or dashes and must begin with an '
259 'periods or dashes and must begin with an '
264 'alphanumeric character')
260 'alphanumeric character')
265
261
266 def test_register_err_case_sensitive(self):
262 def test_register_err_case_sensitive(self):
267 usr = TEST_USER_ADMIN_LOGIN.title()
263 usr = TEST_USER_ADMIN_LOGIN.title()
268 response = self.app.post(url(controller='login', action='register'),
264 response = self.app.post(url(controller='login', action='register'),
269 {'username': usr,
265 {'username': usr,
270 'password': 'test12',
266 'password': 'test12',
271 'password_confirmation': 'test12',
267 'password_confirmation': 'test12',
272 'email': 'goodmailm',
268 'email': 'goodmailm',
273 'firstname': 'test',
269 'firstname': 'test',
274 'lastname': 'test'})
270 'lastname': 'test'})
275
271
276 response.mustcontain('An email address must contain a single @')
272 response.mustcontain('An email address must contain a single @')
277 msg = validators.ValidUsername()._messages['username_exists']
273 msg = validators.ValidUsername()._messages['username_exists']
278 msg = h.html_escape(msg % {'username': usr})
274 msg = h.html_escape(msg % {'username': usr})
279 response.mustcontain(msg)
275 response.mustcontain(msg)
280
276
281 def test_register_special_chars(self):
277 def test_register_special_chars(self):
282 response = self.app.post(url(controller='login', action='register'),
278 response = self.app.post(url(controller='login', action='register'),
283 {'username': 'xxxaxn',
279 {'username': 'xxxaxn',
284 'password': 'ąćźżąśśśś',
280 'password': 'ąćźżąśśśś',
285 'password_confirmation': 'ąćźżąśśśś',
281 'password_confirmation': 'ąćźżąśśśś',
286 'email': 'goodmailm@test.plx',
282 'email': 'goodmailm@test.plx',
287 'firstname': 'test',
283 'firstname': 'test',
288 'lastname': 'test'})
284 'lastname': 'test'})
289
285
290 msg = validators.ValidPassword()._messages['invalid_password']
286 msg = validators.ValidPassword()._messages['invalid_password']
291 response.mustcontain(msg)
287 response.mustcontain(msg)
292
288
293 def test_register_password_mismatch(self):
289 def test_register_password_mismatch(self):
294 response = self.app.post(url(controller='login', action='register'),
290 response = self.app.post(url(controller='login', action='register'),
295 {'username': 'xs',
291 {'username': 'xs',
296 'password': '123qwe',
292 'password': '123qwe',
297 'password_confirmation': 'qwe123',
293 'password_confirmation': 'qwe123',
298 'email': 'goodmailm@test.plxa',
294 'email': 'goodmailm@test.plxa',
299 'firstname': 'test',
295 'firstname': 'test',
300 'lastname': 'test'})
296 'lastname': 'test'})
301 msg = validators.ValidPasswordsMatch('password', 'password_confirmation')._messages['password_mismatch']
297 msg = validators.ValidPasswordsMatch('password', 'password_confirmation')._messages['password_mismatch']
302 response.mustcontain(msg)
298 response.mustcontain(msg)
303
299
304 def test_register_ok(self):
300 def test_register_ok(self):
305 username = 'test_regular4'
301 username = 'test_regular4'
306 password = 'qweqwe'
302 password = 'qweqwe'
307 email = 'user4@example.com'
303 email = 'user4@example.com'
308 name = 'testname'
304 name = 'testname'
309 lastname = 'testlastname'
305 lastname = 'testlastname'
310
306
311 response = self.app.post(url(controller='login', action='register'),
307 response = self.app.post(url(controller='login', action='register'),
312 {'username': username,
308 {'username': username,
313 'password': password,
309 'password': password,
314 'password_confirmation': password,
310 'password_confirmation': password,
315 'email': email,
311 'email': email,
316 'firstname': name,
312 'firstname': name,
317 'lastname': lastname,
313 'lastname': lastname,
318 'admin': True}) # This should be overriden
314 'admin': True}) # This should be overriden
319 self.assertEqual(response.status, '302 Found')
315 self.assertEqual(response.status, '302 Found')
320 self.checkSessionFlash(response, 'You have successfully registered into Kallithea')
316 self.checkSessionFlash(response, 'You have successfully registered into Kallithea')
321
317
322 ret = Session().query(User).filter(User.username == 'test_regular4').one()
318 ret = Session().query(User).filter(User.username == 'test_regular4').one()
323 self.assertEqual(ret.username, username)
319 self.assertEqual(ret.username, username)
324 self.assertEqual(check_password(password, ret.password), True)
320 self.assertEqual(check_password(password, ret.password), True)
325 self.assertEqual(ret.email, email)
321 self.assertEqual(ret.email, email)
326 self.assertEqual(ret.name, name)
322 self.assertEqual(ret.name, name)
327 self.assertEqual(ret.lastname, lastname)
323 self.assertEqual(ret.lastname, lastname)
328 self.assertNotEqual(ret.api_key, None)
324 self.assertNotEqual(ret.api_key, None)
329 self.assertEqual(ret.admin, False)
325 self.assertEqual(ret.admin, False)
330
326
331 #==========================================================================
327 #==========================================================================
332 # PASSWORD RESET
328 # PASSWORD RESET
333 #==========================================================================
329 #==========================================================================
334
330
335 def test_forgot_password_wrong_mail(self):
331 def test_forgot_password_wrong_mail(self):
336 bad_email = 'username%wrongmail.org'
332 bad_email = 'username%wrongmail.org'
337 response = self.app.post(
333 response = self.app.post(
338 url(controller='login', action='password_reset'),
334 url(controller='login', action='password_reset'),
339 {'email': bad_email, }
335 {'email': bad_email, }
340 )
336 )
341
337
342 response.mustcontain('An email address must contain a single @')
338 response.mustcontain('An email address must contain a single @')
343
339
344 def test_forgot_password(self):
340 def test_forgot_password(self):
345 response = self.app.get(url(controller='login',
341 response = self.app.get(url(controller='login',
346 action='password_reset'))
342 action='password_reset'))
347 self.assertEqual(response.status, '200 OK')
343 self.assertEqual(response.status, '200 OK')
348
344
349 username = 'test_password_reset_1'
345 username = 'test_password_reset_1'
350 password = 'qweqwe'
346 password = 'qweqwe'
351 email = 'username@example.com'
347 email = 'username@example.com'
352 name = 'passwd'
348 name = 'passwd'
353 lastname = 'reset'
349 lastname = 'reset'
354 timestamp = int(time.time())
350 timestamp = int(time.time())
355
351
356 new = User()
352 new = User()
357 new.username = username
353 new.username = username
358 new.password = password
354 new.password = password
359 new.email = email
355 new.email = email
360 new.name = name
356 new.name = name
361 new.lastname = lastname
357 new.lastname = lastname
362 new.api_key = generate_api_key()
358 new.api_key = generate_api_key()
363 Session().add(new)
359 Session().add(new)
364 Session().commit()
360 Session().commit()
365
361
366 response = self.app.post(url(controller='login',
362 response = self.app.post(url(controller='login',
367 action='password_reset'),
363 action='password_reset'),
368 {'email': email, })
364 {'email': email, })
369
365
370 self.checkSessionFlash(response, 'A password reset confirmation code has been sent')
366 self.checkSessionFlash(response, 'A password reset confirmation code has been sent')
371
367
372 response = response.follow()
368 response = response.follow()
373
369
374 # BAD TOKEN
370 # BAD TOKEN
375
371
376 token = "bad"
372 token = "bad"
377
373
378 response = self.app.post(url(controller='login',
374 response = self.app.post(url(controller='login',
379 action='password_reset_confirmation'),
375 action='password_reset_confirmation'),
380 {'email': email,
376 {'email': email,
381 'timestamp': timestamp,
377 'timestamp': timestamp,
382 'password': "p@ssw0rd",
378 'password': "p@ssw0rd",
383 'password_confirm': "p@ssw0rd",
379 'password_confirm': "p@ssw0rd",
384 'token': token,
380 'token': token,
385 })
381 })
386 self.assertEqual(response.status, '200 OK')
382 self.assertEqual(response.status, '200 OK')
387 response.mustcontain('Invalid password reset token')
383 response.mustcontain('Invalid password reset token')
388
384
389 # GOOD TOKEN
385 # GOOD TOKEN
390
386
391 # TODO: The token should ideally be taken from the mail sent
387 # TODO: The token should ideally be taken from the mail sent
392 # above, instead of being recalculated.
388 # above, instead of being recalculated.
393
389
394 token = UserModel().get_reset_password_token(
390 token = UserModel().get_reset_password_token(
395 User.get_by_username(username), timestamp, self.authentication_token())
391 User.get_by_username(username), timestamp, self.authentication_token())
396
392
397 response = self.app.get(url(controller='login',
393 response = self.app.get(url(controller='login',
398 action='password_reset_confirmation',
394 action='password_reset_confirmation',
399 email=email,
395 email=email,
400 timestamp=timestamp,
396 timestamp=timestamp,
401 token=token))
397 token=token))
402 self.assertEqual(response.status, '200 OK')
398 self.assertEqual(response.status, '200 OK')
403 response.mustcontain("You are about to set a new password for the email address %s" % email)
399 response.mustcontain("You are about to set a new password for the email address %s" % email)
404
400
405 response = self.app.post(url(controller='login',
401 response = self.app.post(url(controller='login',
406 action='password_reset_confirmation'),
402 action='password_reset_confirmation'),
407 {'email': email,
403 {'email': email,
408 'timestamp': timestamp,
404 'timestamp': timestamp,
409 'password': "p@ssw0rd",
405 'password': "p@ssw0rd",
410 'password_confirm': "p@ssw0rd",
406 'password_confirm': "p@ssw0rd",
411 'token': token,
407 'token': token,
412 })
408 })
413 self.assertEqual(response.status, '302 Found')
409 self.assertEqual(response.status, '302 Found')
414 self.checkSessionFlash(response, 'Successfully updated password')
410 self.checkSessionFlash(response, 'Successfully updated password')
415
411
416 response = response.follow()
412 response = response.follow()
417
413
418 #==========================================================================
414 #==========================================================================
419 # API
415 # API
420 #==========================================================================
416 #==========================================================================
421
417
422 def _get_api_whitelist(self, values=None):
418 def _get_api_whitelist(self, values=None):
423 config = {'api_access_controllers_whitelist': values or []}
419 config = {'api_access_controllers_whitelist': values or []}
424 return config
420 return config
425
421
426 @parameterized.expand([
422 @parameterized.expand([
427 ('none', None),
423 ('none', None),
428 ('empty_string', ''),
424 ('empty_string', ''),
429 ('fake_number', '123456'),
425 ('fake_number', '123456'),
430 ('proper_api_key', None)
426 ('proper_api_key', None)
431 ])
427 ])
432 def test_access_not_whitelisted_page_via_api_key(self, test_name, api_key):
428 def test_access_not_whitelisted_page_via_api_key(self, test_name, api_key):
433 whitelist = self._get_api_whitelist([])
429 whitelist = self._get_api_whitelist([])
434 with mock.patch('kallithea.CONFIG', whitelist):
430 with mock.patch('kallithea.CONFIG', whitelist):
435 self.assertEqual([],
431 self.assertEqual([],
436 whitelist['api_access_controllers_whitelist'])
432 whitelist['api_access_controllers_whitelist'])
437 if test_name == 'proper_api_key':
433 if test_name == 'proper_api_key':
438 #use builtin if api_key is None
434 #use builtin if api_key is None
439 api_key = User.get_first_admin().api_key
435 api_key = User.get_first_admin().api_key
440
436
441 with fixture.anon_access(False):
437 with fixture.anon_access(False):
442 self.app.get(url(controller='changeset',
438 self.app.get(url(controller='changeset',
443 action='changeset_raw',
439 action='changeset_raw',
444 repo_name=HG_REPO, revision='tip', api_key=api_key),
440 repo_name=HG_REPO, revision='tip', api_key=api_key),
445 status=403)
441 status=403)
446
442
447 @parameterized.expand([
443 @parameterized.expand([
448 ('none', None, 302),
444 ('none', None, 302),
449 ('empty_string', '', 302),
445 ('empty_string', '', 302),
450 ('fake_number', '123456', 302),
446 ('fake_number', '123456', 302),
451 ('fake_not_alnum', 'a-z', 302),
447 ('fake_not_alnum', 'a-z', 302),
452 ('fake_api_key', '0123456789abcdef0123456789ABCDEF01234567', 302),
448 ('fake_api_key', '0123456789abcdef0123456789ABCDEF01234567', 302),
453 ('proper_api_key', None, 200)
449 ('proper_api_key', None, 200)
454 ])
450 ])
455 def test_access_whitelisted_page_via_api_key(self, test_name, api_key, code):
451 def test_access_whitelisted_page_via_api_key(self, test_name, api_key, code):
456 whitelist = self._get_api_whitelist(['ChangesetController:changeset_raw'])
452 whitelist = self._get_api_whitelist(['ChangesetController:changeset_raw'])
457 with mock.patch('kallithea.CONFIG', whitelist):
453 with mock.patch('kallithea.CONFIG', whitelist):
458 self.assertEqual(['ChangesetController:changeset_raw'],
454 self.assertEqual(['ChangesetController:changeset_raw'],
459 whitelist['api_access_controllers_whitelist'])
455 whitelist['api_access_controllers_whitelist'])
460 if test_name == 'proper_api_key':
456 if test_name == 'proper_api_key':
461 api_key = User.get_first_admin().api_key
457 api_key = User.get_first_admin().api_key
462
458
463 with fixture.anon_access(False):
459 with fixture.anon_access(False):
464 self.app.get(url(controller='changeset',
460 self.app.get(url(controller='changeset',
465 action='changeset_raw',
461 action='changeset_raw',
466 repo_name=HG_REPO, revision='tip', api_key=api_key),
462 repo_name=HG_REPO, revision='tip', api_key=api_key),
467 status=code)
463 status=code)
468
464
469 def test_access_page_via_extra_api_key(self):
465 def test_access_page_via_extra_api_key(self):
470 whitelist = self._get_api_whitelist(['ChangesetController:changeset_raw'])
466 whitelist = self._get_api_whitelist(['ChangesetController:changeset_raw'])
471 with mock.patch('kallithea.CONFIG', whitelist):
467 with mock.patch('kallithea.CONFIG', whitelist):
472 self.assertEqual(['ChangesetController:changeset_raw'],
468 self.assertEqual(['ChangesetController:changeset_raw'],
473 whitelist['api_access_controllers_whitelist'])
469 whitelist['api_access_controllers_whitelist'])
474
470
475 new_api_key = ApiKeyModel().create(TEST_USER_ADMIN_LOGIN, u'test')
471 new_api_key = ApiKeyModel().create(TEST_USER_ADMIN_LOGIN, u'test')
476 Session().commit()
472 Session().commit()
477 with fixture.anon_access(False):
473 with fixture.anon_access(False):
478 self.app.get(url(controller='changeset',
474 self.app.get(url(controller='changeset',
479 action='changeset_raw',
475 action='changeset_raw',
480 repo_name=HG_REPO, revision='tip', api_key=new_api_key.api_key),
476 repo_name=HG_REPO, revision='tip', api_key=new_api_key.api_key),
481 status=200)
477 status=200)
482
478
483 def test_access_page_via_expired_api_key(self):
479 def test_access_page_via_expired_api_key(self):
484 whitelist = self._get_api_whitelist(['ChangesetController:changeset_raw'])
480 whitelist = self._get_api_whitelist(['ChangesetController:changeset_raw'])
485 with mock.patch('kallithea.CONFIG', whitelist):
481 with mock.patch('kallithea.CONFIG', whitelist):
486 self.assertEqual(['ChangesetController:changeset_raw'],
482 self.assertEqual(['ChangesetController:changeset_raw'],
487 whitelist['api_access_controllers_whitelist'])
483 whitelist['api_access_controllers_whitelist'])
488
484
489 new_api_key = ApiKeyModel().create(TEST_USER_ADMIN_LOGIN, u'test')
485 new_api_key = ApiKeyModel().create(TEST_USER_ADMIN_LOGIN, u'test')
490 Session().commit()
486 Session().commit()
491 #patch the API key and make it expired
487 #patch the API key and make it expired
492 new_api_key.expires = 0
488 new_api_key.expires = 0
493 Session().add(new_api_key)
489 Session().add(new_api_key)
494 Session().commit()
490 Session().commit()
495 with fixture.anon_access(False):
491 with fixture.anon_access(False):
496 self.app.get(url(controller='changeset',
492 self.app.get(url(controller='changeset',
497 action='changeset_raw',
493 action='changeset_raw',
498 repo_name=HG_REPO, revision='tip',
494 repo_name=HG_REPO, revision='tip',
499 api_key=new_api_key.api_key),
495 api_key=new_api_key.api_key),
500 status=302)
496 status=302)
General Comments 0
You need to be logged in to leave comments. Login now