##// END OF EJS Templates
auth: add scope and login restrictions to rhodecode plugin, and scope restriction to token plugin....
marcink -
r3392:5cc5c872 default
parent child Browse files
Show More
@@ -1,569 +1,579 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 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 urlparse
21 import urlparse
22
22
23 import mock
23 import mock
24 import pytest
24 import pytest
25
25
26 from rhodecode.tests import (
26 from rhodecode.tests import (
27 assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN,
27 assert_session_flash, HG_REPO, TEST_USER_ADMIN_LOGIN,
28 no_newline_id_generator)
28 no_newline_id_generator)
29 from rhodecode.tests.fixture import Fixture
29 from rhodecode.tests.fixture import Fixture
30 from rhodecode.lib.auth import check_password
30 from rhodecode.lib.auth import check_password
31 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
32 from rhodecode.model.auth_token import AuthTokenModel
32 from rhodecode.model.auth_token import AuthTokenModel
33 from rhodecode.model.db import User, Notification, UserApiKeys
33 from rhodecode.model.db import User, Notification, UserApiKeys
34 from rhodecode.model.meta import Session
34 from rhodecode.model.meta import Session
35
35
36 fixture = Fixture()
36 fixture = Fixture()
37
37
38 whitelist_view = ['RepoCommitsView:repo_commit_raw']
38 whitelist_view = ['RepoCommitsView:repo_commit_raw']
39
39
40
40
41 def route_path(name, params=None, **kwargs):
41 def route_path(name, params=None, **kwargs):
42 import urllib
42 import urllib
43 from rhodecode.apps._base import ADMIN_PREFIX
43 from rhodecode.apps._base import ADMIN_PREFIX
44
44
45 base_url = {
45 base_url = {
46 'login': ADMIN_PREFIX + '/login',
46 'login': ADMIN_PREFIX + '/login',
47 'logout': ADMIN_PREFIX + '/logout',
47 'logout': ADMIN_PREFIX + '/logout',
48 'register': ADMIN_PREFIX + '/register',
48 'register': ADMIN_PREFIX + '/register',
49 'reset_password':
49 'reset_password':
50 ADMIN_PREFIX + '/password_reset',
50 ADMIN_PREFIX + '/password_reset',
51 'reset_password_confirmation':
51 'reset_password_confirmation':
52 ADMIN_PREFIX + '/password_reset_confirmation',
52 ADMIN_PREFIX + '/password_reset_confirmation',
53
53
54 'admin_permissions_application':
54 'admin_permissions_application':
55 ADMIN_PREFIX + '/permissions/application',
55 ADMIN_PREFIX + '/permissions/application',
56 'admin_permissions_application_update':
56 'admin_permissions_application_update':
57 ADMIN_PREFIX + '/permissions/application/update',
57 ADMIN_PREFIX + '/permissions/application/update',
58
58
59 'repo_commit_raw': '/{repo_name}/raw-changeset/{commit_id}'
59 'repo_commit_raw': '/{repo_name}/raw-changeset/{commit_id}'
60
60
61 }[name].format(**kwargs)
61 }[name].format(**kwargs)
62
62
63 if params:
63 if params:
64 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
64 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
65 return base_url
65 return base_url
66
66
67
67
68 @pytest.mark.usefixtures('app')
68 @pytest.mark.usefixtures('app')
69 class TestLoginController(object):
69 class TestLoginController(object):
70 destroy_users = set()
70 destroy_users = set()
71
71
72 @classmethod
72 @classmethod
73 def teardown_class(cls):
73 def teardown_class(cls):
74 fixture.destroy_users(cls.destroy_users)
74 fixture.destroy_users(cls.destroy_users)
75
75
76 def teardown_method(self, method):
76 def teardown_method(self, method):
77 for n in Notification.query().all():
77 for n in Notification.query().all():
78 Session().delete(n)
78 Session().delete(n)
79
79
80 Session().commit()
80 Session().commit()
81 assert Notification.query().all() == []
81 assert Notification.query().all() == []
82
82
83 def test_index(self):
83 def test_index(self):
84 response = self.app.get(route_path('login'))
84 response = self.app.get(route_path('login'))
85 assert response.status == '200 OK'
85 assert response.status == '200 OK'
86 # Test response...
86 # Test response...
87
87
88 def test_login_admin_ok(self):
88 def test_login_admin_ok(self):
89 response = self.app.post(route_path('login'),
89 response = self.app.post(route_path('login'),
90 {'username': 'test_admin',
90 {'username': 'test_admin',
91 'password': 'test12'}, status=302)
91 'password': 'test12'}, status=302)
92 response = response.follow()
92 response = response.follow()
93 session = response.get_session_from_response()
93 session = response.get_session_from_response()
94 username = session['rhodecode_user'].get('username')
94 username = session['rhodecode_user'].get('username')
95 assert username == 'test_admin'
95 assert username == 'test_admin'
96 response.mustcontain('/%s' % HG_REPO)
96 response.mustcontain('/%s' % HG_REPO)
97
97
98 def test_login_regular_ok(self):
98 def test_login_regular_ok(self):
99 response = self.app.post(route_path('login'),
99 response = self.app.post(route_path('login'),
100 {'username': 'test_regular',
100 {'username': 'test_regular',
101 'password': 'test12'}, status=302)
101 'password': 'test12'}, status=302)
102
102
103 response = response.follow()
103 response = response.follow()
104 session = response.get_session_from_response()
104 session = response.get_session_from_response()
105 username = session['rhodecode_user'].get('username')
105 username = session['rhodecode_user'].get('username')
106 assert username == 'test_regular'
106 assert username == 'test_regular'
107
107
108 response.mustcontain('/%s' % HG_REPO)
108 response.mustcontain('/%s' % HG_REPO)
109
109
110 def test_login_regular_forbidden_when_super_admin_restriction(self):
110 def test_login_regular_forbidden_when_super_admin_restriction(self):
111 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
111 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
112 with fixture.login_restriction(RhodeCodeAuthPlugin.LOGIN_RESTRICTION_SUPER_ADMIN):
112 with fixture.auth_restriction(RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN):
113 response = self.app.post(route_path('login'),
114 {'username': 'test_regular',
115 'password': 'test12'})
116
117 response.mustcontain('invalid user name')
118 response.mustcontain('invalid password')
119
120 def test_login_regular_forbidden_when_scope_restriction(self):
121 from rhodecode.authentication.plugins.auth_rhodecode import RhodeCodeAuthPlugin
122 with fixture.scope_restriction(RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_VCS):
113 response = self.app.post(route_path('login'),
123 response = self.app.post(route_path('login'),
114 {'username': 'test_regular',
124 {'username': 'test_regular',
115 'password': 'test12'})
125 'password': 'test12'})
116
126
117 response.mustcontain('invalid user name')
127 response.mustcontain('invalid user name')
118 response.mustcontain('invalid password')
128 response.mustcontain('invalid password')
119
129
120 def test_login_ok_came_from(self):
130 def test_login_ok_came_from(self):
121 test_came_from = '/_admin/users?branch=stable'
131 test_came_from = '/_admin/users?branch=stable'
122 _url = '{}?came_from={}'.format(route_path('login'), test_came_from)
132 _url = '{}?came_from={}'.format(route_path('login'), test_came_from)
123 response = self.app.post(
133 response = self.app.post(
124 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
134 _url, {'username': 'test_admin', 'password': 'test12'}, status=302)
125
135
126 assert 'branch=stable' in response.location
136 assert 'branch=stable' in response.location
127 response = response.follow()
137 response = response.follow()
128
138
129 assert response.status == '200 OK'
139 assert response.status == '200 OK'
130 response.mustcontain('Users administration')
140 response.mustcontain('Users administration')
131
141
132 def test_redirect_to_login_with_get_args(self):
142 def test_redirect_to_login_with_get_args(self):
133 with fixture.anon_access(False):
143 with fixture.anon_access(False):
134 kwargs = {'branch': 'stable'}
144 kwargs = {'branch': 'stable'}
135 response = self.app.get(
145 response = self.app.get(
136 h.route_path('repo_summary', repo_name=HG_REPO, _query=kwargs),
146 h.route_path('repo_summary', repo_name=HG_REPO, _query=kwargs),
137 status=302)
147 status=302)
138
148
139 response_query = urlparse.parse_qsl(response.location)
149 response_query = urlparse.parse_qsl(response.location)
140 assert 'branch=stable' in response_query[0][1]
150 assert 'branch=stable' in response_query[0][1]
141
151
142 def test_login_form_with_get_args(self):
152 def test_login_form_with_get_args(self):
143 _url = '{}?came_from=/_admin/users,branch=stable'.format(route_path('login'))
153 _url = '{}?came_from=/_admin/users,branch=stable'.format(route_path('login'))
144 response = self.app.get(_url)
154 response = self.app.get(_url)
145 assert 'branch%3Dstable' in response.form.action
155 assert 'branch%3Dstable' in response.form.action
146
156
147 @pytest.mark.parametrize("url_came_from", [
157 @pytest.mark.parametrize("url_came_from", [
148 'data:text/html,<script>window.alert("xss")</script>',
158 'data:text/html,<script>window.alert("xss")</script>',
149 'mailto:test@rhodecode.org',
159 'mailto:test@rhodecode.org',
150 'file:///etc/passwd',
160 'file:///etc/passwd',
151 'ftp://some.ftp.server',
161 'ftp://some.ftp.server',
152 'http://other.domain',
162 'http://other.domain',
153 '/\r\nX-Forwarded-Host: http://example.org',
163 '/\r\nX-Forwarded-Host: http://example.org',
154 ], ids=no_newline_id_generator)
164 ], ids=no_newline_id_generator)
155 def test_login_bad_came_froms(self, url_came_from):
165 def test_login_bad_came_froms(self, url_came_from):
156 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
166 _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
157 response = self.app.post(
167 response = self.app.post(
158 _url,
168 _url,
159 {'username': 'test_admin', 'password': 'test12'})
169 {'username': 'test_admin', 'password': 'test12'})
160 assert response.status == '302 Found'
170 assert response.status == '302 Found'
161 response = response.follow()
171 response = response.follow()
162 assert response.status == '200 OK'
172 assert response.status == '200 OK'
163 assert response.request.path == '/'
173 assert response.request.path == '/'
164
174
165 def test_login_short_password(self):
175 def test_login_short_password(self):
166 response = self.app.post(route_path('login'),
176 response = self.app.post(route_path('login'),
167 {'username': 'test_admin',
177 {'username': 'test_admin',
168 'password': 'as'})
178 'password': 'as'})
169 assert response.status == '200 OK'
179 assert response.status == '200 OK'
170
180
171 response.mustcontain('Enter 3 characters or more')
181 response.mustcontain('Enter 3 characters or more')
172
182
173 def test_login_wrong_non_ascii_password(self, user_regular):
183 def test_login_wrong_non_ascii_password(self, user_regular):
174 response = self.app.post(
184 response = self.app.post(
175 route_path('login'),
185 route_path('login'),
176 {'username': user_regular.username,
186 {'username': user_regular.username,
177 'password': u'invalid-non-asci\xe4'.encode('utf8')})
187 'password': u'invalid-non-asci\xe4'.encode('utf8')})
178
188
179 response.mustcontain('invalid user name')
189 response.mustcontain('invalid user name')
180 response.mustcontain('invalid password')
190 response.mustcontain('invalid password')
181
191
182 def test_login_with_non_ascii_password(self, user_util):
192 def test_login_with_non_ascii_password(self, user_util):
183 password = u'valid-non-ascii\xe4'
193 password = u'valid-non-ascii\xe4'
184 user = user_util.create_user(password=password)
194 user = user_util.create_user(password=password)
185 response = self.app.post(
195 response = self.app.post(
186 route_path('login'),
196 route_path('login'),
187 {'username': user.username,
197 {'username': user.username,
188 'password': password.encode('utf-8')})
198 'password': password.encode('utf-8')})
189 assert response.status_code == 302
199 assert response.status_code == 302
190
200
191 def test_login_wrong_username_password(self):
201 def test_login_wrong_username_password(self):
192 response = self.app.post(route_path('login'),
202 response = self.app.post(route_path('login'),
193 {'username': 'error',
203 {'username': 'error',
194 'password': 'test12'})
204 'password': 'test12'})
195
205
196 response.mustcontain('invalid user name')
206 response.mustcontain('invalid user name')
197 response.mustcontain('invalid password')
207 response.mustcontain('invalid password')
198
208
199 def test_login_admin_ok_password_migration(self, real_crypto_backend):
209 def test_login_admin_ok_password_migration(self, real_crypto_backend):
200 from rhodecode.lib import auth
210 from rhodecode.lib import auth
201
211
202 # create new user, with sha256 password
212 # create new user, with sha256 password
203 temp_user = 'test_admin_sha256'
213 temp_user = 'test_admin_sha256'
204 user = fixture.create_user(temp_user)
214 user = fixture.create_user(temp_user)
205 user.password = auth._RhodeCodeCryptoSha256().hash_create(
215 user.password = auth._RhodeCodeCryptoSha256().hash_create(
206 b'test123')
216 b'test123')
207 Session().add(user)
217 Session().add(user)
208 Session().commit()
218 Session().commit()
209 self.destroy_users.add(temp_user)
219 self.destroy_users.add(temp_user)
210 response = self.app.post(route_path('login'),
220 response = self.app.post(route_path('login'),
211 {'username': temp_user,
221 {'username': temp_user,
212 'password': 'test123'}, status=302)
222 'password': 'test123'}, status=302)
213
223
214 response = response.follow()
224 response = response.follow()
215 session = response.get_session_from_response()
225 session = response.get_session_from_response()
216 username = session['rhodecode_user'].get('username')
226 username = session['rhodecode_user'].get('username')
217 assert username == temp_user
227 assert username == temp_user
218 response.mustcontain('/%s' % HG_REPO)
228 response.mustcontain('/%s' % HG_REPO)
219
229
220 # new password should be bcrypted, after log-in and transfer
230 # new password should be bcrypted, after log-in and transfer
221 user = User.get_by_username(temp_user)
231 user = User.get_by_username(temp_user)
222 assert user.password.startswith('$')
232 assert user.password.startswith('$')
223
233
224 # REGISTRATIONS
234 # REGISTRATIONS
225 def test_register(self):
235 def test_register(self):
226 response = self.app.get(route_path('register'))
236 response = self.app.get(route_path('register'))
227 response.mustcontain('Create an Account')
237 response.mustcontain('Create an Account')
228
238
229 def test_register_err_same_username(self):
239 def test_register_err_same_username(self):
230 uname = 'test_admin'
240 uname = 'test_admin'
231 response = self.app.post(
241 response = self.app.post(
232 route_path('register'),
242 route_path('register'),
233 {
243 {
234 'username': uname,
244 'username': uname,
235 'password': 'test12',
245 'password': 'test12',
236 'password_confirmation': 'test12',
246 'password_confirmation': 'test12',
237 'email': 'goodmail@domain.com',
247 'email': 'goodmail@domain.com',
238 'firstname': 'test',
248 'firstname': 'test',
239 'lastname': 'test'
249 'lastname': 'test'
240 }
250 }
241 )
251 )
242
252
243 assertr = response.assert_response()
253 assertr = response.assert_response()
244 msg = 'Username "%(username)s" already exists'
254 msg = 'Username "%(username)s" already exists'
245 msg = msg % {'username': uname}
255 msg = msg % {'username': uname}
246 assertr.element_contains('#username+.error-message', msg)
256 assertr.element_contains('#username+.error-message', msg)
247
257
248 def test_register_err_same_email(self):
258 def test_register_err_same_email(self):
249 response = self.app.post(
259 response = self.app.post(
250 route_path('register'),
260 route_path('register'),
251 {
261 {
252 'username': 'test_admin_0',
262 'username': 'test_admin_0',
253 'password': 'test12',
263 'password': 'test12',
254 'password_confirmation': 'test12',
264 'password_confirmation': 'test12',
255 'email': 'test_admin@mail.com',
265 'email': 'test_admin@mail.com',
256 'firstname': 'test',
266 'firstname': 'test',
257 'lastname': 'test'
267 'lastname': 'test'
258 }
268 }
259 )
269 )
260
270
261 assertr = response.assert_response()
271 assertr = response.assert_response()
262 msg = u'This e-mail address is already taken'
272 msg = u'This e-mail address is already taken'
263 assertr.element_contains('#email+.error-message', msg)
273 assertr.element_contains('#email+.error-message', msg)
264
274
265 def test_register_err_same_email_case_sensitive(self):
275 def test_register_err_same_email_case_sensitive(self):
266 response = self.app.post(
276 response = self.app.post(
267 route_path('register'),
277 route_path('register'),
268 {
278 {
269 'username': 'test_admin_1',
279 'username': 'test_admin_1',
270 'password': 'test12',
280 'password': 'test12',
271 'password_confirmation': 'test12',
281 'password_confirmation': 'test12',
272 'email': 'TesT_Admin@mail.COM',
282 'email': 'TesT_Admin@mail.COM',
273 'firstname': 'test',
283 'firstname': 'test',
274 'lastname': 'test'
284 'lastname': 'test'
275 }
285 }
276 )
286 )
277 assertr = response.assert_response()
287 assertr = response.assert_response()
278 msg = u'This e-mail address is already taken'
288 msg = u'This e-mail address is already taken'
279 assertr.element_contains('#email+.error-message', msg)
289 assertr.element_contains('#email+.error-message', msg)
280
290
281 def test_register_err_wrong_data(self):
291 def test_register_err_wrong_data(self):
282 response = self.app.post(
292 response = self.app.post(
283 route_path('register'),
293 route_path('register'),
284 {
294 {
285 'username': 'xs',
295 'username': 'xs',
286 'password': 'test',
296 'password': 'test',
287 'password_confirmation': 'test',
297 'password_confirmation': 'test',
288 'email': 'goodmailm',
298 'email': 'goodmailm',
289 'firstname': 'test',
299 'firstname': 'test',
290 'lastname': 'test'
300 'lastname': 'test'
291 }
301 }
292 )
302 )
293 assert response.status == '200 OK'
303 assert response.status == '200 OK'
294 response.mustcontain('An email address must contain a single @')
304 response.mustcontain('An email address must contain a single @')
295 response.mustcontain('Enter a value 6 characters long or more')
305 response.mustcontain('Enter a value 6 characters long or more')
296
306
297 def test_register_err_username(self):
307 def test_register_err_username(self):
298 response = self.app.post(
308 response = self.app.post(
299 route_path('register'),
309 route_path('register'),
300 {
310 {
301 'username': 'error user',
311 'username': 'error user',
302 'password': 'test12',
312 'password': 'test12',
303 'password_confirmation': 'test12',
313 'password_confirmation': 'test12',
304 'email': 'goodmailm',
314 'email': 'goodmailm',
305 'firstname': 'test',
315 'firstname': 'test',
306 'lastname': 'test'
316 'lastname': 'test'
307 }
317 }
308 )
318 )
309
319
310 response.mustcontain('An email address must contain a single @')
320 response.mustcontain('An email address must contain a single @')
311 response.mustcontain(
321 response.mustcontain(
312 'Username may only contain '
322 'Username may only contain '
313 'alphanumeric characters underscores, '
323 'alphanumeric characters underscores, '
314 'periods or dashes and must begin with '
324 'periods or dashes and must begin with '
315 'alphanumeric character')
325 'alphanumeric character')
316
326
317 def test_register_err_case_sensitive(self):
327 def test_register_err_case_sensitive(self):
318 usr = 'Test_Admin'
328 usr = 'Test_Admin'
319 response = self.app.post(
329 response = self.app.post(
320 route_path('register'),
330 route_path('register'),
321 {
331 {
322 'username': usr,
332 'username': usr,
323 'password': 'test12',
333 'password': 'test12',
324 'password_confirmation': 'test12',
334 'password_confirmation': 'test12',
325 'email': 'goodmailm',
335 'email': 'goodmailm',
326 'firstname': 'test',
336 'firstname': 'test',
327 'lastname': 'test'
337 'lastname': 'test'
328 }
338 }
329 )
339 )
330
340
331 assertr = response.assert_response()
341 assertr = response.assert_response()
332 msg = u'Username "%(username)s" already exists'
342 msg = u'Username "%(username)s" already exists'
333 msg = msg % {'username': usr}
343 msg = msg % {'username': usr}
334 assertr.element_contains('#username+.error-message', msg)
344 assertr.element_contains('#username+.error-message', msg)
335
345
336 def test_register_special_chars(self):
346 def test_register_special_chars(self):
337 response = self.app.post(
347 response = self.app.post(
338 route_path('register'),
348 route_path('register'),
339 {
349 {
340 'username': 'xxxaxn',
350 'username': 'xxxaxn',
341 'password': 'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
351 'password': 'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
342 'password_confirmation': 'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
352 'password_confirmation': 'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
343 'email': 'goodmailm@test.plx',
353 'email': 'goodmailm@test.plx',
344 'firstname': 'test',
354 'firstname': 'test',
345 'lastname': 'test'
355 'lastname': 'test'
346 }
356 }
347 )
357 )
348
358
349 msg = u'Invalid characters (non-ascii) in password'
359 msg = u'Invalid characters (non-ascii) in password'
350 response.mustcontain(msg)
360 response.mustcontain(msg)
351
361
352 def test_register_password_mismatch(self):
362 def test_register_password_mismatch(self):
353 response = self.app.post(
363 response = self.app.post(
354 route_path('register'),
364 route_path('register'),
355 {
365 {
356 'username': 'xs',
366 'username': 'xs',
357 'password': '123qwe',
367 'password': '123qwe',
358 'password_confirmation': 'qwe123',
368 'password_confirmation': 'qwe123',
359 'email': 'goodmailm@test.plxa',
369 'email': 'goodmailm@test.plxa',
360 'firstname': 'test',
370 'firstname': 'test',
361 'lastname': 'test'
371 'lastname': 'test'
362 }
372 }
363 )
373 )
364 msg = u'Passwords do not match'
374 msg = u'Passwords do not match'
365 response.mustcontain(msg)
375 response.mustcontain(msg)
366
376
367 def test_register_ok(self):
377 def test_register_ok(self):
368 username = 'test_regular4'
378 username = 'test_regular4'
369 password = 'qweqwe'
379 password = 'qweqwe'
370 email = 'marcin@test.com'
380 email = 'marcin@test.com'
371 name = 'testname'
381 name = 'testname'
372 lastname = 'testlastname'
382 lastname = 'testlastname'
373
383
374 # this initializes a session
384 # this initializes a session
375 response = self.app.get(route_path('register'))
385 response = self.app.get(route_path('register'))
376 response.mustcontain('Create an Account')
386 response.mustcontain('Create an Account')
377
387
378
388
379 response = self.app.post(
389 response = self.app.post(
380 route_path('register'),
390 route_path('register'),
381 {
391 {
382 'username': username,
392 'username': username,
383 'password': password,
393 'password': password,
384 'password_confirmation': password,
394 'password_confirmation': password,
385 'email': email,
395 'email': email,
386 'firstname': name,
396 'firstname': name,
387 'lastname': lastname,
397 'lastname': lastname,
388 'admin': True
398 'admin': True
389 },
399 },
390 status=302
400 status=302
391 ) # This should be overridden
401 ) # This should be overridden
392
402
393 assert_session_flash(
403 assert_session_flash(
394 response, 'You have successfully registered with RhodeCode')
404 response, 'You have successfully registered with RhodeCode')
395
405
396 ret = Session().query(User).filter(
406 ret = Session().query(User).filter(
397 User.username == 'test_regular4').one()
407 User.username == 'test_regular4').one()
398 assert ret.username == username
408 assert ret.username == username
399 assert check_password(password, ret.password)
409 assert check_password(password, ret.password)
400 assert ret.email == email
410 assert ret.email == email
401 assert ret.name == name
411 assert ret.name == name
402 assert ret.lastname == lastname
412 assert ret.lastname == lastname
403 assert ret.auth_tokens is not None
413 assert ret.auth_tokens is not None
404 assert not ret.admin
414 assert not ret.admin
405
415
406 def test_forgot_password_wrong_mail(self):
416 def test_forgot_password_wrong_mail(self):
407 bad_email = 'marcin@wrongmail.org'
417 bad_email = 'marcin@wrongmail.org'
408 # this initializes a session
418 # this initializes a session
409 self.app.get(route_path('reset_password'))
419 self.app.get(route_path('reset_password'))
410
420
411 response = self.app.post(
421 response = self.app.post(
412 route_path('reset_password'), {'email': bad_email, }
422 route_path('reset_password'), {'email': bad_email, }
413 )
423 )
414 assert_session_flash(response,
424 assert_session_flash(response,
415 'If such email exists, a password reset link was sent to it.')
425 'If such email exists, a password reset link was sent to it.')
416
426
417 def test_forgot_password(self, user_util):
427 def test_forgot_password(self, user_util):
418 # this initializes a session
428 # this initializes a session
419 self.app.get(route_path('reset_password'))
429 self.app.get(route_path('reset_password'))
420
430
421 user = user_util.create_user()
431 user = user_util.create_user()
422 user_id = user.user_id
432 user_id = user.user_id
423 email = user.email
433 email = user.email
424
434
425 response = self.app.post(route_path('reset_password'), {'email': email, })
435 response = self.app.post(route_path('reset_password'), {'email': email, })
426
436
427 assert_session_flash(response,
437 assert_session_flash(response,
428 'If such email exists, a password reset link was sent to it.')
438 'If such email exists, a password reset link was sent to it.')
429
439
430 # BAD KEY
440 # BAD KEY
431 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey')
441 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey')
432 response = self.app.get(confirm_url, status=302)
442 response = self.app.get(confirm_url, status=302)
433 assert response.location.endswith(route_path('reset_password'))
443 assert response.location.endswith(route_path('reset_password'))
434 assert_session_flash(response, 'Given reset token is invalid')
444 assert_session_flash(response, 'Given reset token is invalid')
435
445
436 response.follow() # cleanup flash
446 response.follow() # cleanup flash
437
447
438 # GOOD KEY
448 # GOOD KEY
439 key = UserApiKeys.query()\
449 key = UserApiKeys.query()\
440 .filter(UserApiKeys.user_id == user_id)\
450 .filter(UserApiKeys.user_id == user_id)\
441 .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\
451 .filter(UserApiKeys.role == UserApiKeys.ROLE_PASSWORD_RESET)\
442 .first()
452 .first()
443
453
444 assert key
454 assert key
445
455
446 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), key.api_key)
456 confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), key.api_key)
447 response = self.app.get(confirm_url)
457 response = self.app.get(confirm_url)
448 assert response.status == '302 Found'
458 assert response.status == '302 Found'
449 assert response.location.endswith(route_path('login'))
459 assert response.location.endswith(route_path('login'))
450
460
451 assert_session_flash(
461 assert_session_flash(
452 response,
462 response,
453 'Your password reset was successful, '
463 'Your password reset was successful, '
454 'a new password has been sent to your email')
464 'a new password has been sent to your email')
455
465
456 response.follow()
466 response.follow()
457
467
458 def _get_api_whitelist(self, values=None):
468 def _get_api_whitelist(self, values=None):
459 config = {'api_access_controllers_whitelist': values or []}
469 config = {'api_access_controllers_whitelist': values or []}
460 return config
470 return config
461
471
462 @pytest.mark.parametrize("test_name, auth_token", [
472 @pytest.mark.parametrize("test_name, auth_token", [
463 ('none', None),
473 ('none', None),
464 ('empty_string', ''),
474 ('empty_string', ''),
465 ('fake_number', '123456'),
475 ('fake_number', '123456'),
466 ('proper_auth_token', None)
476 ('proper_auth_token', None)
467 ])
477 ])
468 def test_access_not_whitelisted_page_via_auth_token(
478 def test_access_not_whitelisted_page_via_auth_token(
469 self, test_name, auth_token, user_admin):
479 self, test_name, auth_token, user_admin):
470
480
471 whitelist = self._get_api_whitelist([])
481 whitelist = self._get_api_whitelist([])
472 with mock.patch.dict('rhodecode.CONFIG', whitelist):
482 with mock.patch.dict('rhodecode.CONFIG', whitelist):
473 assert [] == whitelist['api_access_controllers_whitelist']
483 assert [] == whitelist['api_access_controllers_whitelist']
474 if test_name == 'proper_auth_token':
484 if test_name == 'proper_auth_token':
475 # use builtin if api_key is None
485 # use builtin if api_key is None
476 auth_token = user_admin.api_key
486 auth_token = user_admin.api_key
477
487
478 with fixture.anon_access(False):
488 with fixture.anon_access(False):
479 self.app.get(
489 self.app.get(
480 route_path('repo_commit_raw',
490 route_path('repo_commit_raw',
481 repo_name=HG_REPO, commit_id='tip',
491 repo_name=HG_REPO, commit_id='tip',
482 params=dict(api_key=auth_token)),
492 params=dict(api_key=auth_token)),
483 status=302)
493 status=302)
484
494
485 @pytest.mark.parametrize("test_name, auth_token, code", [
495 @pytest.mark.parametrize("test_name, auth_token, code", [
486 ('none', None, 302),
496 ('none', None, 302),
487 ('empty_string', '', 302),
497 ('empty_string', '', 302),
488 ('fake_number', '123456', 302),
498 ('fake_number', '123456', 302),
489 ('proper_auth_token', None, 200)
499 ('proper_auth_token', None, 200)
490 ])
500 ])
491 def test_access_whitelisted_page_via_auth_token(
501 def test_access_whitelisted_page_via_auth_token(
492 self, test_name, auth_token, code, user_admin):
502 self, test_name, auth_token, code, user_admin):
493
503
494 whitelist = self._get_api_whitelist(whitelist_view)
504 whitelist = self._get_api_whitelist(whitelist_view)
495
505
496 with mock.patch.dict('rhodecode.CONFIG', whitelist):
506 with mock.patch.dict('rhodecode.CONFIG', whitelist):
497 assert whitelist_view == whitelist['api_access_controllers_whitelist']
507 assert whitelist_view == whitelist['api_access_controllers_whitelist']
498
508
499 if test_name == 'proper_auth_token':
509 if test_name == 'proper_auth_token':
500 auth_token = user_admin.api_key
510 auth_token = user_admin.api_key
501 assert auth_token
511 assert auth_token
502
512
503 with fixture.anon_access(False):
513 with fixture.anon_access(False):
504 self.app.get(
514 self.app.get(
505 route_path('repo_commit_raw',
515 route_path('repo_commit_raw',
506 repo_name=HG_REPO, commit_id='tip',
516 repo_name=HG_REPO, commit_id='tip',
507 params=dict(api_key=auth_token)),
517 params=dict(api_key=auth_token)),
508 status=code)
518 status=code)
509
519
510 @pytest.mark.parametrize("test_name, auth_token, code", [
520 @pytest.mark.parametrize("test_name, auth_token, code", [
511 ('proper_auth_token', None, 200),
521 ('proper_auth_token', None, 200),
512 ('wrong_auth_token', '123456', 302),
522 ('wrong_auth_token', '123456', 302),
513 ])
523 ])
514 def test_access_whitelisted_page_via_auth_token_bound_to_token(
524 def test_access_whitelisted_page_via_auth_token_bound_to_token(
515 self, test_name, auth_token, code, user_admin):
525 self, test_name, auth_token, code, user_admin):
516
526
517 expected_token = auth_token
527 expected_token = auth_token
518 if test_name == 'proper_auth_token':
528 if test_name == 'proper_auth_token':
519 auth_token = user_admin.api_key
529 auth_token = user_admin.api_key
520 expected_token = auth_token
530 expected_token = auth_token
521 assert auth_token
531 assert auth_token
522
532
523 whitelist = self._get_api_whitelist([
533 whitelist = self._get_api_whitelist([
524 'RepoCommitsView:repo_commit_raw@{}'.format(expected_token)])
534 'RepoCommitsView:repo_commit_raw@{}'.format(expected_token)])
525
535
526 with mock.patch.dict('rhodecode.CONFIG', whitelist):
536 with mock.patch.dict('rhodecode.CONFIG', whitelist):
527
537
528 with fixture.anon_access(False):
538 with fixture.anon_access(False):
529 self.app.get(
539 self.app.get(
530 route_path('repo_commit_raw',
540 route_path('repo_commit_raw',
531 repo_name=HG_REPO, commit_id='tip',
541 repo_name=HG_REPO, commit_id='tip',
532 params=dict(api_key=auth_token)),
542 params=dict(api_key=auth_token)),
533 status=code)
543 status=code)
534
544
535 def test_access_page_via_extra_auth_token(self):
545 def test_access_page_via_extra_auth_token(self):
536 whitelist = self._get_api_whitelist(whitelist_view)
546 whitelist = self._get_api_whitelist(whitelist_view)
537 with mock.patch.dict('rhodecode.CONFIG', whitelist):
547 with mock.patch.dict('rhodecode.CONFIG', whitelist):
538 assert whitelist_view == \
548 assert whitelist_view == \
539 whitelist['api_access_controllers_whitelist']
549 whitelist['api_access_controllers_whitelist']
540
550
541 new_auth_token = AuthTokenModel().create(
551 new_auth_token = AuthTokenModel().create(
542 TEST_USER_ADMIN_LOGIN, 'test')
552 TEST_USER_ADMIN_LOGIN, 'test')
543 Session().commit()
553 Session().commit()
544 with fixture.anon_access(False):
554 with fixture.anon_access(False):
545 self.app.get(
555 self.app.get(
546 route_path('repo_commit_raw',
556 route_path('repo_commit_raw',
547 repo_name=HG_REPO, commit_id='tip',
557 repo_name=HG_REPO, commit_id='tip',
548 params=dict(api_key=new_auth_token.api_key)),
558 params=dict(api_key=new_auth_token.api_key)),
549 status=200)
559 status=200)
550
560
551 def test_access_page_via_expired_auth_token(self):
561 def test_access_page_via_expired_auth_token(self):
552 whitelist = self._get_api_whitelist(whitelist_view)
562 whitelist = self._get_api_whitelist(whitelist_view)
553 with mock.patch.dict('rhodecode.CONFIG', whitelist):
563 with mock.patch.dict('rhodecode.CONFIG', whitelist):
554 assert whitelist_view == \
564 assert whitelist_view == \
555 whitelist['api_access_controllers_whitelist']
565 whitelist['api_access_controllers_whitelist']
556
566
557 new_auth_token = AuthTokenModel().create(
567 new_auth_token = AuthTokenModel().create(
558 TEST_USER_ADMIN_LOGIN, 'test')
568 TEST_USER_ADMIN_LOGIN, 'test')
559 Session().commit()
569 Session().commit()
560 # patch the api key and make it expired
570 # patch the api key and make it expired
561 new_auth_token.expires = 0
571 new_auth_token.expires = 0
562 Session().add(new_auth_token)
572 Session().add(new_auth_token)
563 Session().commit()
573 Session().commit()
564 with fixture.anon_access(False):
574 with fixture.anon_access(False):
565 self.app.get(
575 self.app.get(
566 route_path('repo_commit_raw',
576 route_path('repo_commit_raw',
567 repo_name=HG_REPO, commit_id='tip',
577 repo_name=HG_REPO, commit_id='tip',
568 params=dict(api_key=new_auth_token.api_key)),
578 params=dict(api_key=new_auth_token.api_key)),
569 status=302)
579 status=302)
@@ -1,186 +1,220 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2019 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 RhodeCode authentication plugin for built in internal auth
22 RhodeCode authentication plugin for built in internal auth
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 import colander
27 import colander
28
28
29 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
30 from rhodecode.translation import _
29 from rhodecode.translation import _
31
32 from rhodecode.authentication.base import RhodeCodeAuthPluginBase, hybrid_property
33 from rhodecode.authentication.routes import AuthnPluginResourceBase
34 from rhodecode.lib.utils2 import safe_str
30 from rhodecode.lib.utils2 import safe_str
35 from rhodecode.model.db import User
31 from rhodecode.model.db import User
32 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
33 from rhodecode.authentication.base import (
34 RhodeCodeAuthPluginBase, hybrid_property, HTTP_TYPE, VCS_TYPE)
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
36
36
37 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
38
38
39
39
40 def plugin_factory(plugin_id, *args, **kwargs):
40 def plugin_factory(plugin_id, *args, **kwargs):
41 plugin = RhodeCodeAuthPlugin(plugin_id)
41 plugin = RhodeCodeAuthPlugin(plugin_id)
42 return plugin
42 return plugin
43
43
44
44
45 class RhodecodeAuthnResource(AuthnPluginResourceBase):
45 class RhodecodeAuthnResource(AuthnPluginResourceBase):
46 pass
46 pass
47
47
48
48
49 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
49 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
50 uid = 'rhodecode'
50 uid = 'rhodecode'
51 LOGIN_RESTRICTION_NONE = 'none'
51 AUTH_RESTRICTION_NONE = 'user_all'
52 LOGIN_RESTRICTION_SUPER_ADMIN = 'super_admin'
52 AUTH_RESTRICTION_SUPER_ADMIN = 'user_super_admin'
53 AUTH_RESTRICTION_SCOPE_ALL = 'scope_all'
54 AUTH_RESTRICTION_SCOPE_HTTP = 'scope_http'
55 AUTH_RESTRICTION_SCOPE_VCS = 'scope_vcs'
53
56
54 def includeme(self, config):
57 def includeme(self, config):
55 config.add_authn_plugin(self)
58 config.add_authn_plugin(self)
56 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
59 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
57 config.add_view(
60 config.add_view(
58 'rhodecode.authentication.views.AuthnPluginViewBase',
61 'rhodecode.authentication.views.AuthnPluginViewBase',
59 attr='settings_get',
62 attr='settings_get',
60 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
63 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
61 request_method='GET',
64 request_method='GET',
62 route_name='auth_home',
65 route_name='auth_home',
63 context=RhodecodeAuthnResource)
66 context=RhodecodeAuthnResource)
64 config.add_view(
67 config.add_view(
65 'rhodecode.authentication.views.AuthnPluginViewBase',
68 'rhodecode.authentication.views.AuthnPluginViewBase',
66 attr='settings_post',
69 attr='settings_post',
67 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
70 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
68 request_method='POST',
71 request_method='POST',
69 route_name='auth_home',
72 route_name='auth_home',
70 context=RhodecodeAuthnResource)
73 context=RhodecodeAuthnResource)
71
74
72 def get_settings_schema(self):
75 def get_settings_schema(self):
73 return RhodeCodeSettingsSchema()
76 return RhodeCodeSettingsSchema()
74
77
75 def get_display_name(self):
78 def get_display_name(self):
76 return _('RhodeCode Internal')
79 return _('RhodeCode Internal')
77
80
78 @classmethod
81 @classmethod
79 def docs(cls):
82 def docs(cls):
80 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth.html"
83 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth.html"
81
84
82 @hybrid_property
85 @hybrid_property
83 def name(self):
86 def name(self):
84 return u"rhodecode"
87 return u"rhodecode"
85
88
86 def user_activation_state(self):
89 def user_activation_state(self):
87 def_user_perms = User.get_default_user().AuthUser().permissions['global']
90 def_user_perms = User.get_default_user().AuthUser().permissions['global']
88 return 'hg.register.auto_activate' in def_user_perms
91 return 'hg.register.auto_activate' in def_user_perms
89
92
90 def allows_authentication_from(
93 def allows_authentication_from(
91 self, user, allows_non_existing_user=True,
94 self, user, allows_non_existing_user=True,
92 allowed_auth_plugins=None, allowed_auth_sources=None):
95 allowed_auth_plugins=None, allowed_auth_sources=None):
93 """
96 """
94 Custom method for this auth that doesn't accept non existing users.
97 Custom method for this auth that doesn't accept non existing users.
95 We know that user exists in our database.
98 We know that user exists in our database.
96 """
99 """
97 allows_non_existing_user = False
100 allows_non_existing_user = False
98 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
101 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
99 user, allows_non_existing_user=allows_non_existing_user)
102 user, allows_non_existing_user=allows_non_existing_user)
100
103
101 def auth(self, userobj, username, password, settings, **kwargs):
104 def auth(self, userobj, username, password, settings, **kwargs):
102 if not userobj:
105 if not userobj:
103 log.debug('userobj was:%s skipping', userobj)
106 log.debug('userobj was:%s skipping', userobj)
104 return None
107 return None
105
108
106 if userobj.extern_type != self.name:
109 if userobj.extern_type != self.name:
107 log.warning(
110 log.warning("userobj:%s extern_type mismatch got:`%s` expected:`%s`",
108 "userobj:%s extern_type mismatch got:`%s` expected:`%s`",
109 userobj, userobj.extern_type, self.name)
111 userobj, userobj.extern_type, self.name)
110 return None
112 return None
111
113
112 login_restriction = settings.get('login_restriction', '')
114 # check scope of auth
113 if login_restriction == self.LOGIN_RESTRICTION_SUPER_ADMIN and userobj.admin is False:
115 scope_restriction = settings.get('scope_restriction', '')
114 log.info(
116
115 "userobj:%s is not super-admin and login restriction is set to %s",
117 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_HTTP \
116 userobj, login_restriction)
118 and self.auth_type != HTTP_TYPE:
119 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
120 userobj, self.auth_type, scope_restriction)
121 return None
122
123 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_VCS \
124 and self.auth_type != VCS_TYPE:
125 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
126 userobj, self.auth_type, scope_restriction)
127 return None
128
129 # check super-admin restriction
130 auth_restriction = settings.get('auth_restriction', '')
131
132 if auth_restriction == self.AUTH_RESTRICTION_SUPER_ADMIN \
133 and userobj.admin is False:
134 log.warning("userobj:%s is not super-admin and auth restriction is set to %s",
135 userobj, auth_restriction)
117 return None
136 return None
118
137
119 user_attrs = {
138 user_attrs = {
120 "username": userobj.username,
139 "username": userobj.username,
121 "firstname": userobj.firstname,
140 "firstname": userobj.firstname,
122 "lastname": userobj.lastname,
141 "lastname": userobj.lastname,
123 "groups": [],
142 "groups": [],
124 'user_group_sync': False,
143 'user_group_sync': False,
125 "email": userobj.email,
144 "email": userobj.email,
126 "admin": userobj.admin,
145 "admin": userobj.admin,
127 "active": userobj.active,
146 "active": userobj.active,
128 "active_from_extern": userobj.active,
147 "active_from_extern": userobj.active,
129 "extern_name": userobj.user_id,
148 "extern_name": userobj.user_id,
130 "extern_type": userobj.extern_type,
149 "extern_type": userobj.extern_type,
131 }
150 }
132
151
133 log.debug("User attributes:%s", user_attrs)
152 log.debug("User attributes:%s", user_attrs)
134 if userobj.active:
153 if userobj.active:
135 from rhodecode.lib import auth
154 from rhodecode.lib import auth
136 crypto_backend = auth.crypto_backend()
155 crypto_backend = auth.crypto_backend()
137 password_encoded = safe_str(password)
156 password_encoded = safe_str(password)
138 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
157 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
139 password_encoded, userobj.password or '')
158 password_encoded, userobj.password or '')
140
159
141 if password_match and new_hash:
160 if password_match and new_hash:
142 log.debug('user %s properly authenticated, but '
161 log.debug('user %s properly authenticated, but '
143 'requires hash change to bcrypt', userobj)
162 'requires hash change to bcrypt', userobj)
144 # if password match, and we use OLD deprecated hash,
163 # if password match, and we use OLD deprecated hash,
145 # we should migrate this user hash password to the new hash
164 # we should migrate this user hash password to the new hash
146 # we store the new returned by hash_check_with_upgrade function
165 # we store the new returned by hash_check_with_upgrade function
147 user_attrs['_hash_migrate'] = new_hash
166 user_attrs['_hash_migrate'] = new_hash
148
167
149 if userobj.username == User.DEFAULT_USER and userobj.active:
168 if userobj.username == User.DEFAULT_USER and userobj.active:
150 log.info('user `%s` authenticated correctly as anonymous user',
169 log.info('user `%s` authenticated correctly as anonymous user',
151 userobj.username)
170 userobj.username)
152 return user_attrs
171 return user_attrs
153
172
154 elif userobj.username == username and password_match:
173 elif userobj.username == username and password_match:
155 log.info('user `%s` authenticated correctly', userobj.username)
174 log.info('user `%s` authenticated correctly', userobj.username)
156 return user_attrs
175 return user_attrs
157 log.warn("user `%s` used a wrong password when "
176 log.warning("user `%s` used a wrong password when "
158 "authenticating on this plugin", userobj.username)
177 "authenticating on this plugin", userobj.username)
159 return None
178 return None
160 else:
179 else:
161 log.warning(
180 log.warning('user `%s` failed to authenticate via %s, reason: account not '
162 'user `%s` failed to authenticate via %s, reason: account not '
163 'active.', username, self.name)
181 'active.', username, self.name)
164 return None
182 return None
165
183
166
184
167 class RhodeCodeSettingsSchema(AuthnPluginSettingsSchemaBase):
185 class RhodeCodeSettingsSchema(AuthnPluginSettingsSchemaBase):
168 login_restriction_choices = [
186
169 (RhodeCodeAuthPlugin.LOGIN_RESTRICTION_NONE, 'All users'),
187 auth_restriction_choices = [
170 (RhodeCodeAuthPlugin.LOGIN_RESTRICTION_SUPER_ADMIN, 'Super admins only')
188 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_NONE, 'All users'),
189 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN, 'Super admins only'),
190 ]
191
192 auth_scope_choices = [
193 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_ALL, 'HTTP and VCS'),
194 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_HTTP, 'HTTP only'),
171 ]
195 ]
172
196
173 login_restriction = colander.SchemaNode(
197 auth_restriction = colander.SchemaNode(
174 colander.String(),
198 colander.String(),
175 default=login_restriction_choices[0],
199 default=auth_restriction_choices[0],
176 description=_('Choose login restrition for users.'),
200 description=_('Allowed user types for authentication using this plugin.'),
177 title=_('Login restriction'),
201 title=_('User restriction'),
178 validator=colander.OneOf([x[0] for x in login_restriction_choices]),
202 validator=colander.OneOf([x[0] for x in auth_restriction_choices]),
179 widget='select_with_labels',
203 widget='select_with_labels',
180 choices=login_restriction_choices
204 choices=auth_restriction_choices
205 )
206 scope_restriction = colander.SchemaNode(
207 colander.String(),
208 default=auth_scope_choices[0],
209 description=_('Allowed protocols for authentication using this plugin. '
210 'VCS means GIT/HG/SVN. HTTP is web based login.'),
211 title=_('Scope restriction'),
212 validator=colander.OneOf([x[0] for x in auth_scope_choices]),
213 widget='select_with_labels',
214 choices=auth_scope_choices
181 )
215 )
182
216
183
217
184 def includeme(config):
218 def includeme(config):
185 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
219 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
186 plugin_factory(plugin_id).includeme(config)
220 plugin_factory(plugin_id).includeme(config)
@@ -1,157 +1,177 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 RhodeCode authentication token plugin for built in internal auth
22 RhodeCode authentication token plugin for built in internal auth
23 """
23 """
24
24
25 import logging
25 import logging
26 import colander
26
27
28 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
27 from rhodecode.translation import _
29 from rhodecode.translation import _
28 from rhodecode.authentication.base import (
30 from rhodecode.authentication.base import (
29 RhodeCodeAuthPluginBase, VCS_TYPE, hybrid_property)
31 RhodeCodeAuthPluginBase, VCS_TYPE, hybrid_property)
30 from rhodecode.authentication.routes import AuthnPluginResourceBase
32 from rhodecode.authentication.routes import AuthnPluginResourceBase
31 from rhodecode.model.db import User, UserApiKeys, Repository
33 from rhodecode.model.db import User, UserApiKeys, Repository
32
34
33
35
34 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
35
37
36
38
37 def plugin_factory(plugin_id, *args, **kwargs):
39 def plugin_factory(plugin_id, *args, **kwargs):
38 plugin = RhodeCodeAuthPlugin(plugin_id)
40 plugin = RhodeCodeAuthPlugin(plugin_id)
39 return plugin
41 return plugin
40
42
41
43
42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
44 class RhodecodeAuthnResource(AuthnPluginResourceBase):
43 pass
45 pass
44
46
45
47
46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
48 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
47 """
49 """
48 Enables usage of authentication tokens for vcs operations.
50 Enables usage of authentication tokens for vcs operations.
49 """
51 """
50 uid = 'token'
52 uid = 'token'
53 AUTH_RESTRICTION_SCOPE_VCS = 'scope_vcs'
51
54
52 def includeme(self, config):
55 def includeme(self, config):
53 config.add_authn_plugin(self)
56 config.add_authn_plugin(self)
54 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
57 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
55 config.add_view(
58 config.add_view(
56 'rhodecode.authentication.views.AuthnPluginViewBase',
59 'rhodecode.authentication.views.AuthnPluginViewBase',
57 attr='settings_get',
60 attr='settings_get',
58 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
61 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
59 request_method='GET',
62 request_method='GET',
60 route_name='auth_home',
63 route_name='auth_home',
61 context=RhodecodeAuthnResource)
64 context=RhodecodeAuthnResource)
62 config.add_view(
65 config.add_view(
63 'rhodecode.authentication.views.AuthnPluginViewBase',
66 'rhodecode.authentication.views.AuthnPluginViewBase',
64 attr='settings_post',
67 attr='settings_post',
65 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
68 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
66 request_method='POST',
69 request_method='POST',
67 route_name='auth_home',
70 route_name='auth_home',
68 context=RhodecodeAuthnResource)
71 context=RhodecodeAuthnResource)
69
72
73 def get_settings_schema(self):
74 return RhodeCodeSettingsSchema()
75
70 def get_display_name(self):
76 def get_display_name(self):
71 return _('Rhodecode Token')
77 return _('Rhodecode Token')
72
78
73 @classmethod
79 @classmethod
74 def docs(cls):
80 def docs(cls):
75 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-token.html"
81 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-token.html"
76
82
77 @hybrid_property
83 @hybrid_property
78 def name(self):
84 def name(self):
79 return u"authtoken"
85 return u"authtoken"
80
86
81 def user_activation_state(self):
87 def user_activation_state(self):
82 def_user_perms = User.get_default_user().AuthUser().permissions['global']
88 def_user_perms = User.get_default_user().AuthUser().permissions['global']
83 return 'hg.register.auto_activate' in def_user_perms
89 return 'hg.register.auto_activate' in def_user_perms
84
90
85 def allows_authentication_from(
91 def allows_authentication_from(
86 self, user, allows_non_existing_user=True,
92 self, user, allows_non_existing_user=True,
87 allowed_auth_plugins=None, allowed_auth_sources=None):
93 allowed_auth_plugins=None, allowed_auth_sources=None):
88 """
94 """
89 Custom method for this auth that doesn't accept empty users. And also
95 Custom method for this auth that doesn't accept empty users. And also
90 allows users from all other active plugins to use it and also
96 allows users from all other active plugins to use it and also
91 authenticate against it. But only via vcs mode
97 authenticate against it. But only via vcs mode
92 """
98 """
93 from rhodecode.authentication.base import get_authn_registry
99 from rhodecode.authentication.base import get_authn_registry
94 authn_registry = get_authn_registry()
100 authn_registry = get_authn_registry()
95
101
96 active_plugins = set(
102 active_plugins = set(
97 [x.name for x in authn_registry.get_plugins_for_authentication()])
103 [x.name for x in authn_registry.get_plugins_for_authentication()])
98 active_plugins.discard(self.name)
104 active_plugins.discard(self.name)
99
105
100 allowed_auth_plugins = [self.name] + list(active_plugins)
106 allowed_auth_plugins = [self.name] + list(active_plugins)
101 # only for vcs operations
107 # only for vcs operations
102 allowed_auth_sources = [VCS_TYPE]
108 allowed_auth_sources = [VCS_TYPE]
103
109
104 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
110 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
105 user, allows_non_existing_user=False,
111 user, allows_non_existing_user=False,
106 allowed_auth_plugins=allowed_auth_plugins,
112 allowed_auth_plugins=allowed_auth_plugins,
107 allowed_auth_sources=allowed_auth_sources)
113 allowed_auth_sources=allowed_auth_sources)
108
114
109 def auth(self, userobj, username, password, settings, **kwargs):
115 def auth(self, userobj, username, password, settings, **kwargs):
110 if not userobj:
116 if not userobj:
111 log.debug('userobj was:%s skipping', userobj)
117 log.debug('userobj was:%s skipping', userobj)
112 return None
118 return None
113
119
114 user_attrs = {
120 user_attrs = {
115 "username": userobj.username,
121 "username": userobj.username,
116 "firstname": userobj.firstname,
122 "firstname": userobj.firstname,
117 "lastname": userobj.lastname,
123 "lastname": userobj.lastname,
118 "groups": [],
124 "groups": [],
119 'user_group_sync': False,
125 'user_group_sync': False,
120 "email": userobj.email,
126 "email": userobj.email,
121 "admin": userobj.admin,
127 "admin": userobj.admin,
122 "active": userobj.active,
128 "active": userobj.active,
123 "active_from_extern": userobj.active,
129 "active_from_extern": userobj.active,
124 "extern_name": userobj.user_id,
130 "extern_name": userobj.user_id,
125 "extern_type": userobj.extern_type,
131 "extern_type": userobj.extern_type,
126 }
132 }
127
133
128 log.debug('Authenticating user with args %s', user_attrs)
134 log.debug('Authenticating user with args %s', user_attrs)
129 if userobj.active:
135 if userobj.active:
130 # calling context repo for token scopes
136 # calling context repo for token scopes
131 scope_repo_id = None
137 scope_repo_id = None
132 if self.acl_repo_name:
138 if self.acl_repo_name:
133 repo = Repository.get_by_repo_name(self.acl_repo_name)
139 repo = Repository.get_by_repo_name(self.acl_repo_name)
134 scope_repo_id = repo.repo_id if repo else None
140 scope_repo_id = repo.repo_id if repo else None
135
141
136 token_match = userobj.authenticate_by_token(
142 token_match = userobj.authenticate_by_token(
137 password, roles=[UserApiKeys.ROLE_VCS],
143 password, roles=[UserApiKeys.ROLE_VCS],
138 scope_repo_id=scope_repo_id)
144 scope_repo_id=scope_repo_id)
139
145
140 if userobj.username == username and token_match:
146 if userobj.username == username and token_match:
141 log.info(
147 log.info(
142 'user `%s` successfully authenticated via %s',
148 'user `%s` successfully authenticated via %s',
143 user_attrs['username'], self.name)
149 user_attrs['username'], self.name)
144 return user_attrs
150 return user_attrs
145 log.warn(
151 log.warning('user `%s` failed to authenticate via %s, reason: bad or '
146 'user `%s` failed to authenticate via %s, reason: bad or '
147 'inactive token.', username, self.name)
152 'inactive token.', username, self.name)
148 else:
153 else:
149 log.warning(
154 log.warning('user `%s` failed to authenticate via %s, reason: account not '
150 'user `%s` failed to authenticate via %s, reason: account not '
151 'active.', username, self.name)
155 'active.', username, self.name)
152 return None
156 return None
153
157
154
158
155 def includeme(config):
159 def includeme(config):
156 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
160 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
157 plugin_factory(plugin_id).includeme(config)
161 plugin_factory(plugin_id).includeme(config)
162
163
164 class RhodeCodeSettingsSchema(AuthnPluginSettingsSchemaBase):
165 auth_scope_choices = [
166 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_VCS, 'VCS only'),
167 ]
168
169 scope_restriction = colander.SchemaNode(
170 colander.String(),
171 default=auth_scope_choices[0],
172 description=_('Choose operation scope restriction when authenticating.'),
173 title=_('Scope restriction'),
174 validator=colander.OneOf([x[0] for x in auth_scope_choices]),
175 widget='select_with_labels',
176 choices=auth_scope_choices
177 )
@@ -1,383 +1,415 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 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 Helpers for fixture generation
22 Helpers for fixture generation
23 """
23 """
24
24
25 import os
25 import os
26 import time
26 import time
27 import tempfile
27 import tempfile
28 import shutil
28 import shutil
29
29
30 import configobj
30 import configobj
31
31
32 from rhodecode.tests import *
32 from rhodecode.tests import *
33 from rhodecode.model.db import Repository, User, RepoGroup, UserGroup, Gist, UserEmailMap
33 from rhodecode.model.db import Repository, User, RepoGroup, UserGroup, Gist, UserEmailMap
34 from rhodecode.model.meta import Session
34 from rhodecode.model.meta import Session
35 from rhodecode.model.repo import RepoModel
35 from rhodecode.model.repo import RepoModel
36 from rhodecode.model.user import UserModel
36 from rhodecode.model.user import UserModel
37 from rhodecode.model.repo_group import RepoGroupModel
37 from rhodecode.model.repo_group import RepoGroupModel
38 from rhodecode.model.user_group import UserGroupModel
38 from rhodecode.model.user_group import UserGroupModel
39 from rhodecode.model.gist import GistModel
39 from rhodecode.model.gist import GistModel
40 from rhodecode.model.auth_token import AuthTokenModel
40 from rhodecode.model.auth_token import AuthTokenModel
41 from rhodecode.authentication.plugins.auth_rhodecode import \
41 from rhodecode.authentication.plugins.auth_rhodecode import \
42 RhodeCodeAuthPlugin
42 RhodeCodeAuthPlugin
43
43
44 dn = os.path.dirname
44 dn = os.path.dirname
45 FIXTURES = os.path.join(dn(dn(os.path.abspath(__file__))), 'tests', 'fixtures')
45 FIXTURES = os.path.join(dn(dn(os.path.abspath(__file__))), 'tests', 'fixtures')
46
46
47
47
48 def error_function(*args, **kwargs):
48 def error_function(*args, **kwargs):
49 raise Exception('Total Crash !')
49 raise Exception('Total Crash !')
50
50
51
51
52 class TestINI(object):
52 class TestINI(object):
53 """
53 """
54 Allows to create a new test.ini file as a copy of existing one with edited
54 Allows to create a new test.ini file as a copy of existing one with edited
55 data. Example usage::
55 data. Example usage::
56
56
57 with TestINI('test.ini', [{'section':{'key':val'}]) as new_test_ini_path:
57 with TestINI('test.ini', [{'section':{'key':val'}]) as new_test_ini_path:
58 print('paster server %s' % new_test_ini)
58 print('paster server %s' % new_test_ini)
59 """
59 """
60
60
61 def __init__(self, ini_file_path, ini_params, new_file_prefix='DEFAULT',
61 def __init__(self, ini_file_path, ini_params, new_file_prefix='DEFAULT',
62 destroy=True, dir=None):
62 destroy=True, dir=None):
63 self.ini_file_path = ini_file_path
63 self.ini_file_path = ini_file_path
64 self.ini_params = ini_params
64 self.ini_params = ini_params
65 self.new_path = None
65 self.new_path = None
66 self.new_path_prefix = new_file_prefix
66 self.new_path_prefix = new_file_prefix
67 self._destroy = destroy
67 self._destroy = destroy
68 self._dir = dir
68 self._dir = dir
69
69
70 def __enter__(self):
70 def __enter__(self):
71 return self.create()
71 return self.create()
72
72
73 def __exit__(self, exc_type, exc_val, exc_tb):
73 def __exit__(self, exc_type, exc_val, exc_tb):
74 self.destroy()
74 self.destroy()
75
75
76 def create(self):
76 def create(self):
77 config = configobj.ConfigObj(
77 config = configobj.ConfigObj(
78 self.ini_file_path, file_error=True, write_empty_values=True)
78 self.ini_file_path, file_error=True, write_empty_values=True)
79
79
80 for data in self.ini_params:
80 for data in self.ini_params:
81 section, ini_params = data.items()[0]
81 section, ini_params = data.items()[0]
82 for key, val in ini_params.items():
82 for key, val in ini_params.items():
83 config[section][key] = val
83 config[section][key] = val
84 with tempfile.NamedTemporaryFile(
84 with tempfile.NamedTemporaryFile(
85 prefix=self.new_path_prefix, suffix='.ini', dir=self._dir,
85 prefix=self.new_path_prefix, suffix='.ini', dir=self._dir,
86 delete=False) as new_ini_file:
86 delete=False) as new_ini_file:
87 config.write(new_ini_file)
87 config.write(new_ini_file)
88 self.new_path = new_ini_file.name
88 self.new_path = new_ini_file.name
89
89
90 return self.new_path
90 return self.new_path
91
91
92 def destroy(self):
92 def destroy(self):
93 if self._destroy:
93 if self._destroy:
94 os.remove(self.new_path)
94 os.remove(self.new_path)
95
95
96
96
97 class Fixture(object):
97 class Fixture(object):
98
98
99 def anon_access(self, status):
99 def anon_access(self, status):
100 """
100 """
101 Context process for disabling anonymous access. use like:
101 Context process for disabling anonymous access. use like:
102 fixture = Fixture()
102 fixture = Fixture()
103 with fixture.anon_access(False):
103 with fixture.anon_access(False):
104 #tests
104 #tests
105
105
106 after this block anon access will be set to `not status`
106 after this block anon access will be set to `not status`
107 """
107 """
108
108
109 class context(object):
109 class context(object):
110 def __enter__(self):
110 def __enter__(self):
111 anon = User.get_default_user()
111 anon = User.get_default_user()
112 anon.active = status
112 anon.active = status
113 Session().add(anon)
113 Session().add(anon)
114 Session().commit()
114 Session().commit()
115 time.sleep(1.5) # must sleep for cache (1s to expire)
115 time.sleep(1.5) # must sleep for cache (1s to expire)
116
116
117 def __exit__(self, exc_type, exc_val, exc_tb):
117 def __exit__(self, exc_type, exc_val, exc_tb):
118 anon = User.get_default_user()
118 anon = User.get_default_user()
119 anon.active = not status
119 anon.active = not status
120 Session().add(anon)
120 Session().add(anon)
121 Session().commit()
121 Session().commit()
122
122
123 return context()
123 return context()
124
124
125 def login_restriction(self, login_restriction):
125 def auth_restriction(self, auth_restriction):
126 """
126 """
127 Context process for changing the builtin rhodecode plugin login restrictions.
127 Context process for changing the builtin rhodecode plugin auth restrictions.
128 Use like:
128 Use like:
129 fixture = Fixture()
129 fixture = Fixture()
130 with fixture.login_restriction('super_admin'):
130 with fixture.auth_restriction('super_admin'):
131 #tests
131 #tests
132
132
133 after this block login restriction will be taken off
133 after this block auth restriction will be taken off
134 """
134 """
135
135
136 class context(object):
136 class context(object):
137 def _get_pluing(self):
137 def _get_pluing(self):
138 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(
138 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(
139 RhodeCodeAuthPlugin.uid)
139 RhodeCodeAuthPlugin.uid)
140 plugin = RhodeCodeAuthPlugin(plugin_id)
140 plugin = RhodeCodeAuthPlugin(plugin_id)
141 return plugin
141 return plugin
142
142
143 def __enter__(self):
143 def __enter__(self):
144 plugin = self._get_pluing()
144 plugin = self._get_pluing()
145 plugin.create_or_update_setting(
145 plugin.create_or_update_setting(
146 'login_restriction', login_restriction)
146 'auth_restriction', auth_restriction)
147 Session().commit()
147 Session().commit()
148
148
149 def __exit__(self, exc_type, exc_val, exc_tb):
149 def __exit__(self, exc_type, exc_val, exc_tb):
150 plugin = self._get_pluing()
150 plugin = self._get_pluing()
151 plugin.create_or_update_setting(
151 plugin.create_or_update_setting(
152 'login_restriction', RhodeCodeAuthPlugin.LOGIN_RESTRICTION_NONE)
152 'auth_restriction', RhodeCodeAuthPlugin.AUTH_RESTRICTION_NONE)
153 Session().commit()
154
155 return context()
156
157 def scope_restriction(self, scope_restriction):
158 """
159 Context process for changing the builtin rhodecode plugin scope restrictions.
160 Use like:
161 fixture = Fixture()
162 with fixture.scope_restriction('scope_http'):
163 #tests
164
165 after this block scope restriction will be taken off
166 """
167
168 class context(object):
169 def _get_pluing(self):
170 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(
171 RhodeCodeAuthPlugin.uid)
172 plugin = RhodeCodeAuthPlugin(plugin_id)
173 return plugin
174
175 def __enter__(self):
176 plugin = self._get_pluing()
177 plugin.create_or_update_setting(
178 'scope_restriction', scope_restriction)
179 Session().commit()
180
181 def __exit__(self, exc_type, exc_val, exc_tb):
182 plugin = self._get_pluing()
183 plugin.create_or_update_setting(
184 'scope_restriction', RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_ALL)
153 Session().commit()
185 Session().commit()
154
186
155 return context()
187 return context()
156
188
157 def _get_repo_create_params(self, **custom):
189 def _get_repo_create_params(self, **custom):
158 defs = {
190 defs = {
159 'repo_name': None,
191 'repo_name': None,
160 'repo_type': 'hg',
192 'repo_type': 'hg',
161 'clone_uri': '',
193 'clone_uri': '',
162 'push_uri': '',
194 'push_uri': '',
163 'repo_group': '-1',
195 'repo_group': '-1',
164 'repo_description': 'DESC',
196 'repo_description': 'DESC',
165 'repo_private': False,
197 'repo_private': False,
166 'repo_landing_rev': 'rev:tip',
198 'repo_landing_rev': 'rev:tip',
167 'repo_copy_permissions': False,
199 'repo_copy_permissions': False,
168 'repo_state': Repository.STATE_CREATED,
200 'repo_state': Repository.STATE_CREATED,
169 }
201 }
170 defs.update(custom)
202 defs.update(custom)
171 if 'repo_name_full' not in custom:
203 if 'repo_name_full' not in custom:
172 defs.update({'repo_name_full': defs['repo_name']})
204 defs.update({'repo_name_full': defs['repo_name']})
173
205
174 # fix the repo name if passed as repo_name_full
206 # fix the repo name if passed as repo_name_full
175 if defs['repo_name']:
207 if defs['repo_name']:
176 defs['repo_name'] = defs['repo_name'].split('/')[-1]
208 defs['repo_name'] = defs['repo_name'].split('/')[-1]
177
209
178 return defs
210 return defs
179
211
180 def _get_group_create_params(self, **custom):
212 def _get_group_create_params(self, **custom):
181 defs = {
213 defs = {
182 'group_name': None,
214 'group_name': None,
183 'group_description': 'DESC',
215 'group_description': 'DESC',
184 'perm_updates': [],
216 'perm_updates': [],
185 'perm_additions': [],
217 'perm_additions': [],
186 'perm_deletions': [],
218 'perm_deletions': [],
187 'group_parent_id': -1,
219 'group_parent_id': -1,
188 'enable_locking': False,
220 'enable_locking': False,
189 'recursive': False,
221 'recursive': False,
190 }
222 }
191 defs.update(custom)
223 defs.update(custom)
192
224
193 return defs
225 return defs
194
226
195 def _get_user_create_params(self, name, **custom):
227 def _get_user_create_params(self, name, **custom):
196 defs = {
228 defs = {
197 'username': name,
229 'username': name,
198 'password': 'qweqwe',
230 'password': 'qweqwe',
199 'email': '%s+test@rhodecode.org' % name,
231 'email': '%s+test@rhodecode.org' % name,
200 'firstname': 'TestUser',
232 'firstname': 'TestUser',
201 'lastname': 'Test',
233 'lastname': 'Test',
202 'active': True,
234 'active': True,
203 'admin': False,
235 'admin': False,
204 'extern_type': 'rhodecode',
236 'extern_type': 'rhodecode',
205 'extern_name': None,
237 'extern_name': None,
206 }
238 }
207 defs.update(custom)
239 defs.update(custom)
208
240
209 return defs
241 return defs
210
242
211 def _get_user_group_create_params(self, name, **custom):
243 def _get_user_group_create_params(self, name, **custom):
212 defs = {
244 defs = {
213 'users_group_name': name,
245 'users_group_name': name,
214 'user_group_description': 'DESC',
246 'user_group_description': 'DESC',
215 'users_group_active': True,
247 'users_group_active': True,
216 'user_group_data': {},
248 'user_group_data': {},
217 }
249 }
218 defs.update(custom)
250 defs.update(custom)
219
251
220 return defs
252 return defs
221
253
222 def create_repo(self, name, **kwargs):
254 def create_repo(self, name, **kwargs):
223 repo_group = kwargs.get('repo_group')
255 repo_group = kwargs.get('repo_group')
224 if isinstance(repo_group, RepoGroup):
256 if isinstance(repo_group, RepoGroup):
225 kwargs['repo_group'] = repo_group.group_id
257 kwargs['repo_group'] = repo_group.group_id
226 name = name.split(Repository.NAME_SEP)[-1]
258 name = name.split(Repository.NAME_SEP)[-1]
227 name = Repository.NAME_SEP.join((repo_group.group_name, name))
259 name = Repository.NAME_SEP.join((repo_group.group_name, name))
228
260
229 if 'skip_if_exists' in kwargs:
261 if 'skip_if_exists' in kwargs:
230 del kwargs['skip_if_exists']
262 del kwargs['skip_if_exists']
231 r = Repository.get_by_repo_name(name)
263 r = Repository.get_by_repo_name(name)
232 if r:
264 if r:
233 return r
265 return r
234
266
235 form_data = self._get_repo_create_params(repo_name=name, **kwargs)
267 form_data = self._get_repo_create_params(repo_name=name, **kwargs)
236 cur_user = kwargs.get('cur_user', TEST_USER_ADMIN_LOGIN)
268 cur_user = kwargs.get('cur_user', TEST_USER_ADMIN_LOGIN)
237 RepoModel().create(form_data, cur_user)
269 RepoModel().create(form_data, cur_user)
238 Session().commit()
270 Session().commit()
239 repo = Repository.get_by_repo_name(name)
271 repo = Repository.get_by_repo_name(name)
240 assert repo
272 assert repo
241 return repo
273 return repo
242
274
243 def create_fork(self, repo_to_fork, fork_name, **kwargs):
275 def create_fork(self, repo_to_fork, fork_name, **kwargs):
244 repo_to_fork = Repository.get_by_repo_name(repo_to_fork)
276 repo_to_fork = Repository.get_by_repo_name(repo_to_fork)
245
277
246 form_data = self._get_repo_create_params(repo_name=fork_name,
278 form_data = self._get_repo_create_params(repo_name=fork_name,
247 fork_parent_id=repo_to_fork.repo_id,
279 fork_parent_id=repo_to_fork.repo_id,
248 repo_type=repo_to_fork.repo_type,
280 repo_type=repo_to_fork.repo_type,
249 **kwargs)
281 **kwargs)
250 #TODO: fix it !!
282 #TODO: fix it !!
251 form_data['description'] = form_data['repo_description']
283 form_data['description'] = form_data['repo_description']
252 form_data['private'] = form_data['repo_private']
284 form_data['private'] = form_data['repo_private']
253 form_data['landing_rev'] = form_data['repo_landing_rev']
285 form_data['landing_rev'] = form_data['repo_landing_rev']
254
286
255 owner = kwargs.get('cur_user', TEST_USER_ADMIN_LOGIN)
287 owner = kwargs.get('cur_user', TEST_USER_ADMIN_LOGIN)
256 RepoModel().create_fork(form_data, cur_user=owner)
288 RepoModel().create_fork(form_data, cur_user=owner)
257 Session().commit()
289 Session().commit()
258 r = Repository.get_by_repo_name(fork_name)
290 r = Repository.get_by_repo_name(fork_name)
259 assert r
291 assert r
260 return r
292 return r
261
293
262 def destroy_repo(self, repo_name, **kwargs):
294 def destroy_repo(self, repo_name, **kwargs):
263 RepoModel().delete(repo_name, pull_requests='delete', **kwargs)
295 RepoModel().delete(repo_name, pull_requests='delete', **kwargs)
264 Session().commit()
296 Session().commit()
265
297
266 def destroy_repo_on_filesystem(self, repo_name):
298 def destroy_repo_on_filesystem(self, repo_name):
267 rm_path = os.path.join(RepoModel().repos_path, repo_name)
299 rm_path = os.path.join(RepoModel().repos_path, repo_name)
268 if os.path.isdir(rm_path):
300 if os.path.isdir(rm_path):
269 shutil.rmtree(rm_path)
301 shutil.rmtree(rm_path)
270
302
271 def create_repo_group(self, name, **kwargs):
303 def create_repo_group(self, name, **kwargs):
272 if 'skip_if_exists' in kwargs:
304 if 'skip_if_exists' in kwargs:
273 del kwargs['skip_if_exists']
305 del kwargs['skip_if_exists']
274 gr = RepoGroup.get_by_group_name(group_name=name)
306 gr = RepoGroup.get_by_group_name(group_name=name)
275 if gr:
307 if gr:
276 return gr
308 return gr
277 form_data = self._get_group_create_params(group_name=name, **kwargs)
309 form_data = self._get_group_create_params(group_name=name, **kwargs)
278 owner = kwargs.get('cur_user', TEST_USER_ADMIN_LOGIN)
310 owner = kwargs.get('cur_user', TEST_USER_ADMIN_LOGIN)
279 gr = RepoGroupModel().create(
311 gr = RepoGroupModel().create(
280 group_name=form_data['group_name'],
312 group_name=form_data['group_name'],
281 group_description=form_data['group_name'],
313 group_description=form_data['group_name'],
282 owner=owner)
314 owner=owner)
283 Session().commit()
315 Session().commit()
284 gr = RepoGroup.get_by_group_name(gr.group_name)
316 gr = RepoGroup.get_by_group_name(gr.group_name)
285 return gr
317 return gr
286
318
287 def destroy_repo_group(self, repogroupid):
319 def destroy_repo_group(self, repogroupid):
288 RepoGroupModel().delete(repogroupid)
320 RepoGroupModel().delete(repogroupid)
289 Session().commit()
321 Session().commit()
290
322
291 def create_user(self, name, **kwargs):
323 def create_user(self, name, **kwargs):
292 if 'skip_if_exists' in kwargs:
324 if 'skip_if_exists' in kwargs:
293 del kwargs['skip_if_exists']
325 del kwargs['skip_if_exists']
294 user = User.get_by_username(name)
326 user = User.get_by_username(name)
295 if user:
327 if user:
296 return user
328 return user
297 form_data = self._get_user_create_params(name, **kwargs)
329 form_data = self._get_user_create_params(name, **kwargs)
298 user = UserModel().create(form_data)
330 user = UserModel().create(form_data)
299
331
300 # create token for user
332 # create token for user
301 AuthTokenModel().create(
333 AuthTokenModel().create(
302 user=user, description=u'TEST_USER_TOKEN')
334 user=user, description=u'TEST_USER_TOKEN')
303
335
304 Session().commit()
336 Session().commit()
305 user = User.get_by_username(user.username)
337 user = User.get_by_username(user.username)
306 return user
338 return user
307
339
308 def destroy_user(self, userid):
340 def destroy_user(self, userid):
309 UserModel().delete(userid)
341 UserModel().delete(userid)
310 Session().commit()
342 Session().commit()
311
343
312 def create_additional_user_email(self, user, email):
344 def create_additional_user_email(self, user, email):
313 uem = UserEmailMap()
345 uem = UserEmailMap()
314 uem.user = user
346 uem.user = user
315 uem.email = email
347 uem.email = email
316 Session().add(uem)
348 Session().add(uem)
317 return uem
349 return uem
318
350
319 def destroy_users(self, userid_iter):
351 def destroy_users(self, userid_iter):
320 for user_id in userid_iter:
352 for user_id in userid_iter:
321 if User.get_by_username(user_id):
353 if User.get_by_username(user_id):
322 UserModel().delete(user_id)
354 UserModel().delete(user_id)
323 Session().commit()
355 Session().commit()
324
356
325 def create_user_group(self, name, **kwargs):
357 def create_user_group(self, name, **kwargs):
326 if 'skip_if_exists' in kwargs:
358 if 'skip_if_exists' in kwargs:
327 del kwargs['skip_if_exists']
359 del kwargs['skip_if_exists']
328 gr = UserGroup.get_by_group_name(group_name=name)
360 gr = UserGroup.get_by_group_name(group_name=name)
329 if gr:
361 if gr:
330 return gr
362 return gr
331 # map active flag to the real attribute. For API consistency of fixtures
363 # map active flag to the real attribute. For API consistency of fixtures
332 if 'active' in kwargs:
364 if 'active' in kwargs:
333 kwargs['users_group_active'] = kwargs['active']
365 kwargs['users_group_active'] = kwargs['active']
334 del kwargs['active']
366 del kwargs['active']
335 form_data = self._get_user_group_create_params(name, **kwargs)
367 form_data = self._get_user_group_create_params(name, **kwargs)
336 owner = kwargs.get('cur_user', TEST_USER_ADMIN_LOGIN)
368 owner = kwargs.get('cur_user', TEST_USER_ADMIN_LOGIN)
337 user_group = UserGroupModel().create(
369 user_group = UserGroupModel().create(
338 name=form_data['users_group_name'],
370 name=form_data['users_group_name'],
339 description=form_data['user_group_description'],
371 description=form_data['user_group_description'],
340 owner=owner, active=form_data['users_group_active'],
372 owner=owner, active=form_data['users_group_active'],
341 group_data=form_data['user_group_data'])
373 group_data=form_data['user_group_data'])
342 Session().commit()
374 Session().commit()
343 user_group = UserGroup.get_by_group_name(user_group.users_group_name)
375 user_group = UserGroup.get_by_group_name(user_group.users_group_name)
344 return user_group
376 return user_group
345
377
346 def destroy_user_group(self, usergroupid):
378 def destroy_user_group(self, usergroupid):
347 UserGroupModel().delete(user_group=usergroupid, force=True)
379 UserGroupModel().delete(user_group=usergroupid, force=True)
348 Session().commit()
380 Session().commit()
349
381
350 def create_gist(self, **kwargs):
382 def create_gist(self, **kwargs):
351 form_data = {
383 form_data = {
352 'description': 'new-gist',
384 'description': 'new-gist',
353 'owner': TEST_USER_ADMIN_LOGIN,
385 'owner': TEST_USER_ADMIN_LOGIN,
354 'gist_type': GistModel.cls.GIST_PUBLIC,
386 'gist_type': GistModel.cls.GIST_PUBLIC,
355 'lifetime': -1,
387 'lifetime': -1,
356 'acl_level': Gist.ACL_LEVEL_PUBLIC,
388 'acl_level': Gist.ACL_LEVEL_PUBLIC,
357 'gist_mapping': {'filename1.txt': {'content': 'hello world'},}
389 'gist_mapping': {'filename1.txt': {'content': 'hello world'},}
358 }
390 }
359 form_data.update(kwargs)
391 form_data.update(kwargs)
360 gist = GistModel().create(
392 gist = GistModel().create(
361 description=form_data['description'], owner=form_data['owner'],
393 description=form_data['description'], owner=form_data['owner'],
362 gist_mapping=form_data['gist_mapping'], gist_type=form_data['gist_type'],
394 gist_mapping=form_data['gist_mapping'], gist_type=form_data['gist_type'],
363 lifetime=form_data['lifetime'], gist_acl_level=form_data['acl_level']
395 lifetime=form_data['lifetime'], gist_acl_level=form_data['acl_level']
364 )
396 )
365 Session().commit()
397 Session().commit()
366 return gist
398 return gist
367
399
368 def destroy_gists(self, gistid=None):
400 def destroy_gists(self, gistid=None):
369 for g in GistModel.cls.get_all():
401 for g in GistModel.cls.get_all():
370 if gistid:
402 if gistid:
371 if gistid == g.gist_access_id:
403 if gistid == g.gist_access_id:
372 GistModel().delete(g)
404 GistModel().delete(g)
373 else:
405 else:
374 GistModel().delete(g)
406 GistModel().delete(g)
375 Session().commit()
407 Session().commit()
376
408
377 def load_resource(self, resource_name, strip=False):
409 def load_resource(self, resource_name, strip=False):
378 with open(os.path.join(FIXTURES, resource_name)) as f:
410 with open(os.path.join(FIXTURES, resource_name)) as f:
379 source = f.read()
411 source = f.read()
380 if strip:
412 if strip:
381 source = source.strip()
413 source = source.strip()
382
414
383 return source
415 return source
General Comments 0
You need to be logged in to leave comments. Login now