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