##// END OF EJS Templates
artifacts: handle detach/delete of artifacts for users who own them and are to be deleted....
marcink -
r4011:e2f9b772 default
parent child Browse files
Show More
@@ -1,790 +1,790 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 pytest
22 22 from sqlalchemy.orm.exc import NoResultFound
23 23
24 24 from rhodecode.lib import auth
25 25 from rhodecode.lib import helpers as h
26 26 from rhodecode.model.db import User, UserApiKeys, UserEmailMap, Repository
27 27 from rhodecode.model.meta import Session
28 28 from rhodecode.model.user import UserModel
29 29
30 30 from rhodecode.tests import (
31 31 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
32 32 from rhodecode.tests.fixture import Fixture
33 33
34 34 fixture = Fixture()
35 35
36 36
37 37 def route_path(name, params=None, **kwargs):
38 38 import urllib
39 39 from rhodecode.apps._base import ADMIN_PREFIX
40 40
41 41 base_url = {
42 42 'users':
43 43 ADMIN_PREFIX + '/users',
44 44 'users_data':
45 45 ADMIN_PREFIX + '/users_data',
46 46 'users_create':
47 47 ADMIN_PREFIX + '/users/create',
48 48 'users_new':
49 49 ADMIN_PREFIX + '/users/new',
50 50 'user_edit':
51 51 ADMIN_PREFIX + '/users/{user_id}/edit',
52 52 'user_edit_advanced':
53 53 ADMIN_PREFIX + '/users/{user_id}/edit/advanced',
54 54 'user_edit_global_perms':
55 55 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions',
56 56 'user_edit_global_perms_update':
57 57 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions/update',
58 58 'user_update':
59 59 ADMIN_PREFIX + '/users/{user_id}/update',
60 60 'user_delete':
61 61 ADMIN_PREFIX + '/users/{user_id}/delete',
62 62 'user_create_personal_repo_group':
63 63 ADMIN_PREFIX + '/users/{user_id}/create_repo_group',
64 64
65 65 'edit_user_auth_tokens':
66 66 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens',
67 67 'edit_user_auth_tokens_add':
68 68 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/new',
69 69 'edit_user_auth_tokens_delete':
70 70 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/delete',
71 71
72 72 'edit_user_emails':
73 73 ADMIN_PREFIX + '/users/{user_id}/edit/emails',
74 74 'edit_user_emails_add':
75 75 ADMIN_PREFIX + '/users/{user_id}/edit/emails/new',
76 76 'edit_user_emails_delete':
77 77 ADMIN_PREFIX + '/users/{user_id}/edit/emails/delete',
78 78
79 79 'edit_user_ips':
80 80 ADMIN_PREFIX + '/users/{user_id}/edit/ips',
81 81 'edit_user_ips_add':
82 82 ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
83 83 'edit_user_ips_delete':
84 84 ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
85 85
86 86 'edit_user_perms_summary':
87 87 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary',
88 88 'edit_user_perms_summary_json':
89 89 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary/json',
90 90
91 91 'edit_user_audit_logs':
92 92 ADMIN_PREFIX + '/users/{user_id}/edit/audit',
93 93
94 94 'edit_user_audit_logs_download':
95 95 ADMIN_PREFIX + '/users/{user_id}/edit/audit/download',
96 96
97 97 }[name].format(**kwargs)
98 98
99 99 if params:
100 100 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
101 101 return base_url
102 102
103 103
104 104 class TestAdminUsersView(TestController):
105 105
106 106 def test_show_users(self):
107 107 self.log_user()
108 108 self.app.get(route_path('users'))
109 109
110 110 def test_show_users_data(self, xhr_header):
111 111 self.log_user()
112 112 response = self.app.get(route_path(
113 113 'users_data'), extra_environ=xhr_header)
114 114
115 115 all_users = User.query().filter(
116 116 User.username != User.DEFAULT_USER).count()
117 117 assert response.json['recordsTotal'] == all_users
118 118
119 119 def test_show_users_data_filtered(self, xhr_header):
120 120 self.log_user()
121 121 response = self.app.get(route_path(
122 122 'users_data', params={'search[value]': 'empty_search'}),
123 123 extra_environ=xhr_header)
124 124
125 125 all_users = User.query().filter(
126 126 User.username != User.DEFAULT_USER).count()
127 127 assert response.json['recordsTotal'] == all_users
128 128 assert response.json['recordsFiltered'] == 0
129 129
130 130 def test_auth_tokens_default_user(self):
131 131 self.log_user()
132 132 user = User.get_default_user()
133 133 response = self.app.get(
134 134 route_path('edit_user_auth_tokens', user_id=user.user_id),
135 135 status=302)
136 136
137 137 def test_auth_tokens(self):
138 138 self.log_user()
139 139
140 140 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
141 141 user_id = user.user_id
142 142 auth_tokens = user.auth_tokens
143 143 response = self.app.get(
144 144 route_path('edit_user_auth_tokens', user_id=user_id))
145 145 for token in auth_tokens:
146 146 response.mustcontain(token)
147 147 response.mustcontain('never')
148 148
149 149 @pytest.mark.parametrize("desc, lifetime", [
150 150 ('forever', -1),
151 151 ('5mins', 60*5),
152 152 ('30days', 60*60*24*30),
153 153 ])
154 154 def test_add_auth_token(self, desc, lifetime, user_util):
155 155 self.log_user()
156 156 user = user_util.create_user()
157 157 user_id = user.user_id
158 158
159 159 response = self.app.post(
160 160 route_path('edit_user_auth_tokens_add', user_id=user_id),
161 161 {'description': desc, 'lifetime': lifetime,
162 162 'csrf_token': self.csrf_token})
163 163 assert_session_flash(response, 'Auth token successfully created')
164 164
165 165 response = response.follow()
166 166 user = User.get(user_id)
167 167 for auth_token in user.auth_tokens:
168 168 response.mustcontain(auth_token)
169 169
170 170 def test_delete_auth_token(self, user_util):
171 171 self.log_user()
172 172 user = user_util.create_user()
173 173 user_id = user.user_id
174 174 keys = user.auth_tokens
175 175 assert 2 == len(keys)
176 176
177 177 response = self.app.post(
178 178 route_path('edit_user_auth_tokens_add', user_id=user_id),
179 179 {'description': 'desc', 'lifetime': -1,
180 180 'csrf_token': self.csrf_token})
181 181 assert_session_flash(response, 'Auth token successfully created')
182 182 response.follow()
183 183
184 184 # now delete our key
185 185 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
186 186 assert 3 == len(keys)
187 187
188 188 response = self.app.post(
189 189 route_path('edit_user_auth_tokens_delete', user_id=user_id),
190 190 {'del_auth_token': keys[0].user_api_key_id,
191 191 'csrf_token': self.csrf_token})
192 192
193 193 assert_session_flash(response, 'Auth token successfully deleted')
194 194 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
195 195 assert 2 == len(keys)
196 196
197 197 def test_ips(self):
198 198 self.log_user()
199 199 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
200 200 response = self.app.get(route_path('edit_user_ips', user_id=user.user_id))
201 201 response.mustcontain('All IP addresses are allowed')
202 202
203 203 @pytest.mark.parametrize("test_name, ip, ip_range, failure", [
204 204 ('127/24', '127.0.0.1/24', '127.0.0.0 - 127.0.0.255', False),
205 205 ('10/32', '10.0.0.10/32', '10.0.0.10 - 10.0.0.10', False),
206 206 ('0/16', '0.0.0.0/16', '0.0.0.0 - 0.0.255.255', False),
207 207 ('0/8', '0.0.0.0/8', '0.0.0.0 - 0.255.255.255', False),
208 208 ('127_bad_mask', '127.0.0.1/99', '127.0.0.1 - 127.0.0.1', True),
209 209 ('127_bad_ip', 'foobar', 'foobar', True),
210 210 ])
211 211 def test_ips_add(self, user_util, test_name, ip, ip_range, failure):
212 212 self.log_user()
213 213 user = user_util.create_user(username=test_name)
214 214 user_id = user.user_id
215 215
216 216 response = self.app.post(
217 217 route_path('edit_user_ips_add', user_id=user_id),
218 218 params={'new_ip': ip, 'csrf_token': self.csrf_token})
219 219
220 220 if failure:
221 221 assert_session_flash(
222 222 response, 'Please enter a valid IPv4 or IpV6 address')
223 223 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
224 224
225 225 response.mustcontain(no=[ip])
226 226 response.mustcontain(no=[ip_range])
227 227
228 228 else:
229 229 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
230 230 response.mustcontain(ip)
231 231 response.mustcontain(ip_range)
232 232
233 233 def test_ips_delete(self, user_util):
234 234 self.log_user()
235 235 user = user_util.create_user()
236 236 user_id = user.user_id
237 237 ip = '127.0.0.1/32'
238 238 ip_range = '127.0.0.1 - 127.0.0.1'
239 239 new_ip = UserModel().add_extra_ip(user_id, ip)
240 240 Session().commit()
241 241 new_ip_id = new_ip.ip_id
242 242
243 243 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
244 244 response.mustcontain(ip)
245 245 response.mustcontain(ip_range)
246 246
247 247 self.app.post(
248 248 route_path('edit_user_ips_delete', user_id=user_id),
249 249 params={'del_ip_id': new_ip_id, 'csrf_token': self.csrf_token})
250 250
251 251 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
252 252 response.mustcontain('All IP addresses are allowed')
253 253 response.mustcontain(no=[ip])
254 254 response.mustcontain(no=[ip_range])
255 255
256 256 def test_emails(self):
257 257 self.log_user()
258 258 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
259 259 response = self.app.get(
260 260 route_path('edit_user_emails', user_id=user.user_id))
261 261 response.mustcontain('No additional emails specified')
262 262
263 263 def test_emails_add(self, user_util):
264 264 self.log_user()
265 265 user = user_util.create_user()
266 266 user_id = user.user_id
267 267
268 268 self.app.post(
269 269 route_path('edit_user_emails_add', user_id=user_id),
270 270 params={'new_email': 'example@rhodecode.com',
271 271 'csrf_token': self.csrf_token})
272 272
273 273 response = self.app.get(
274 274 route_path('edit_user_emails', user_id=user_id))
275 275 response.mustcontain('example@rhodecode.com')
276 276
277 277 def test_emails_add_existing_email(self, user_util, user_regular):
278 278 existing_email = user_regular.email
279 279
280 280 self.log_user()
281 281 user = user_util.create_user()
282 282 user_id = user.user_id
283 283
284 284 response = self.app.post(
285 285 route_path('edit_user_emails_add', user_id=user_id),
286 286 params={'new_email': existing_email,
287 287 'csrf_token': self.csrf_token})
288 288 assert_session_flash(
289 289 response, 'This e-mail address is already taken')
290 290
291 291 response = self.app.get(
292 292 route_path('edit_user_emails', user_id=user_id))
293 293 response.mustcontain(no=[existing_email])
294 294
295 295 def test_emails_delete(self, user_util):
296 296 self.log_user()
297 297 user = user_util.create_user()
298 298 user_id = user.user_id
299 299
300 300 self.app.post(
301 301 route_path('edit_user_emails_add', user_id=user_id),
302 302 params={'new_email': 'example@rhodecode.com',
303 303 'csrf_token': self.csrf_token})
304 304
305 305 response = self.app.get(
306 306 route_path('edit_user_emails', user_id=user_id))
307 307 response.mustcontain('example@rhodecode.com')
308 308
309 309 user_email = UserEmailMap.query()\
310 310 .filter(UserEmailMap.email == 'example@rhodecode.com') \
311 311 .filter(UserEmailMap.user_id == user_id)\
312 312 .one()
313 313
314 314 del_email_id = user_email.email_id
315 315 self.app.post(
316 316 route_path('edit_user_emails_delete', user_id=user_id),
317 317 params={'del_email_id': del_email_id,
318 318 'csrf_token': self.csrf_token})
319 319
320 320 response = self.app.get(
321 321 route_path('edit_user_emails', user_id=user_id))
322 322 response.mustcontain(no=['example@rhodecode.com'])
323 323
324 324 def test_create(self, request, xhr_header):
325 325 self.log_user()
326 326 username = 'newtestuser'
327 327 password = 'test12'
328 328 password_confirmation = password
329 329 name = 'name'
330 330 lastname = 'lastname'
331 331 email = 'mail@mail.com'
332 332
333 333 self.app.get(route_path('users_new'))
334 334
335 335 response = self.app.post(route_path('users_create'), params={
336 336 'username': username,
337 337 'password': password,
338 338 'password_confirmation': password_confirmation,
339 339 'firstname': name,
340 340 'active': True,
341 341 'lastname': lastname,
342 342 'extern_name': 'rhodecode',
343 343 'extern_type': 'rhodecode',
344 344 'email': email,
345 345 'csrf_token': self.csrf_token,
346 346 })
347 347 user_link = h.link_to(
348 348 username,
349 349 route_path(
350 350 'user_edit', user_id=User.get_by_username(username).user_id))
351 351 assert_session_flash(response, 'Created user %s' % (user_link,))
352 352
353 353 @request.addfinalizer
354 354 def cleanup():
355 355 fixture.destroy_user(username)
356 356 Session().commit()
357 357
358 358 new_user = User.query().filter(User.username == username).one()
359 359
360 360 assert new_user.username == username
361 361 assert auth.check_password(password, new_user.password)
362 362 assert new_user.name == name
363 363 assert new_user.lastname == lastname
364 364 assert new_user.email == email
365 365
366 366 response = self.app.get(route_path('users_data'),
367 367 extra_environ=xhr_header)
368 368 response.mustcontain(username)
369 369
370 370 def test_create_err(self):
371 371 self.log_user()
372 372 username = 'new_user'
373 373 password = ''
374 374 name = 'name'
375 375 lastname = 'lastname'
376 376 email = 'errmail.com'
377 377
378 378 self.app.get(route_path('users_new'))
379 379
380 380 response = self.app.post(route_path('users_create'), params={
381 381 'username': username,
382 382 'password': password,
383 383 'name': name,
384 384 'active': False,
385 385 'lastname': lastname,
386 386 'email': email,
387 387 'csrf_token': self.csrf_token,
388 388 })
389 389
390 390 msg = u'Username "%(username)s" is forbidden'
391 391 msg = h.html_escape(msg % {'username': 'new_user'})
392 392 response.mustcontain('<span class="error-message">%s</span>' % msg)
393 393 response.mustcontain(
394 394 '<span class="error-message">Please enter a value</span>')
395 395 response.mustcontain(
396 396 '<span class="error-message">An email address must contain a'
397 397 ' single @</span>')
398 398
399 399 def get_user():
400 400 Session().query(User).filter(User.username == username).one()
401 401
402 402 with pytest.raises(NoResultFound):
403 403 get_user()
404 404
405 405 def test_new(self):
406 406 self.log_user()
407 407 self.app.get(route_path('users_new'))
408 408
409 409 @pytest.mark.parametrize("name, attrs", [
410 410 ('firstname', {'firstname': 'new_username'}),
411 411 ('lastname', {'lastname': 'new_username'}),
412 412 ('admin', {'admin': True}),
413 413 ('admin', {'admin': False}),
414 414 ('extern_type', {'extern_type': 'ldap'}),
415 415 ('extern_type', {'extern_type': None}),
416 416 ('extern_name', {'extern_name': 'test'}),
417 417 ('extern_name', {'extern_name': None}),
418 418 ('active', {'active': False}),
419 419 ('active', {'active': True}),
420 420 ('email', {'email': 'some@email.com'}),
421 421 ('language', {'language': 'de'}),
422 422 ('language', {'language': 'en'}),
423 423 # ('new_password', {'new_password': 'foobar123',
424 424 # 'password_confirmation': 'foobar123'})
425 425 ])
426 426 def test_update(self, name, attrs, user_util):
427 427 self.log_user()
428 428 usr = user_util.create_user(
429 429 password='qweqwe',
430 430 email='testme@rhodecode.org',
431 431 extern_type='rhodecode',
432 432 extern_name='xxx',
433 433 )
434 434 user_id = usr.user_id
435 435 Session().commit()
436 436
437 437 params = usr.get_api_data()
438 438 cur_lang = params['language'] or 'en'
439 439 params.update({
440 440 'password_confirmation': '',
441 441 'new_password': '',
442 442 'language': cur_lang,
443 443 'csrf_token': self.csrf_token,
444 444 })
445 445 params.update({'new_password': ''})
446 446 params.update(attrs)
447 447 if name == 'email':
448 448 params['emails'] = [attrs['email']]
449 449 elif name == 'extern_type':
450 450 # cannot update this via form, expected value is original one
451 451 params['extern_type'] = "rhodecode"
452 452 elif name == 'extern_name':
453 453 # cannot update this via form, expected value is original one
454 454 params['extern_name'] = 'xxx'
455 455 # special case since this user is not
456 456 # logged in yet his data is not filled
457 457 # so we use creation data
458 458
459 459 response = self.app.post(
460 460 route_path('user_update', user_id=usr.user_id), params)
461 461 assert response.status_int == 302
462 462 assert_session_flash(response, 'User updated successfully')
463 463
464 464 updated_user = User.get(user_id)
465 465 updated_params = updated_user.get_api_data()
466 466 updated_params.update({'password_confirmation': ''})
467 467 updated_params.update({'new_password': ''})
468 468
469 469 del params['csrf_token']
470 470 assert params == updated_params
471 471
472 472 def test_update_and_migrate_password(
473 473 self, autologin_user, real_crypto_backend, user_util):
474 474
475 475 user = user_util.create_user()
476 476 temp_user = user.username
477 477 user.password = auth._RhodeCodeCryptoSha256().hash_create(
478 478 b'test123')
479 479 Session().add(user)
480 480 Session().commit()
481 481
482 482 params = user.get_api_data()
483 483
484 484 params.update({
485 485 'password_confirmation': 'qweqwe123',
486 486 'new_password': 'qweqwe123',
487 487 'language': 'en',
488 488 'csrf_token': autologin_user.csrf_token,
489 489 })
490 490
491 491 response = self.app.post(
492 492 route_path('user_update', user_id=user.user_id), params)
493 493 assert response.status_int == 302
494 494 assert_session_flash(response, 'User updated successfully')
495 495
496 496 # new password should be bcrypted, after log-in and transfer
497 497 user = User.get_by_username(temp_user)
498 498 assert user.password.startswith('$')
499 499
500 500 updated_user = User.get_by_username(temp_user)
501 501 updated_params = updated_user.get_api_data()
502 502 updated_params.update({'password_confirmation': 'qweqwe123'})
503 503 updated_params.update({'new_password': 'qweqwe123'})
504 504
505 505 del params['csrf_token']
506 506 assert params == updated_params
507 507
508 508 def test_delete(self):
509 509 self.log_user()
510 510 username = 'newtestuserdeleteme'
511 511
512 512 fixture.create_user(name=username)
513 513
514 514 new_user = Session().query(User)\
515 515 .filter(User.username == username).one()
516 516 response = self.app.post(
517 517 route_path('user_delete', user_id=new_user.user_id),
518 518 params={'csrf_token': self.csrf_token})
519 519
520 assert_session_flash(response, 'Successfully deleted user')
520 assert_session_flash(response, 'Successfully deleted user `{}`'.format(username))
521 521
522 522 def test_delete_owner_of_repository(self, request, user_util):
523 523 self.log_user()
524 524 obj_name = 'test_repo'
525 525 usr = user_util.create_user()
526 526 username = usr.username
527 527 fixture.create_repo(obj_name, cur_user=usr.username)
528 528
529 529 new_user = Session().query(User)\
530 530 .filter(User.username == username).one()
531 531 response = self.app.post(
532 532 route_path('user_delete', user_id=new_user.user_id),
533 533 params={'csrf_token': self.csrf_token})
534 534
535 535 msg = 'user "%s" still owns 1 repositories and cannot be removed. ' \
536 536 'Switch owners or remove those repositories:%s' % (username, obj_name)
537 537 assert_session_flash(response, msg)
538 538 fixture.destroy_repo(obj_name)
539 539
540 540 def test_delete_owner_of_repository_detaching(self, request, user_util):
541 541 self.log_user()
542 542 obj_name = 'test_repo'
543 543 usr = user_util.create_user(auto_cleanup=False)
544 544 username = usr.username
545 545 fixture.create_repo(obj_name, cur_user=usr.username)
546 546
547 547 new_user = Session().query(User)\
548 548 .filter(User.username == username).one()
549 549 response = self.app.post(
550 550 route_path('user_delete', user_id=new_user.user_id),
551 551 params={'user_repos': 'detach', 'csrf_token': self.csrf_token})
552 552
553 553 msg = 'Detached 1 repositories'
554 554 assert_session_flash(response, msg)
555 555 fixture.destroy_repo(obj_name)
556 556
557 557 def test_delete_owner_of_repository_deleting(self, request, user_util):
558 558 self.log_user()
559 559 obj_name = 'test_repo'
560 560 usr = user_util.create_user(auto_cleanup=False)
561 561 username = usr.username
562 562 fixture.create_repo(obj_name, cur_user=usr.username)
563 563
564 564 new_user = Session().query(User)\
565 565 .filter(User.username == username).one()
566 566 response = self.app.post(
567 567 route_path('user_delete', user_id=new_user.user_id),
568 568 params={'user_repos': 'delete', 'csrf_token': self.csrf_token})
569 569
570 570 msg = 'Deleted 1 repositories'
571 571 assert_session_flash(response, msg)
572 572
573 573 def test_delete_owner_of_repository_group(self, request, user_util):
574 574 self.log_user()
575 575 obj_name = 'test_group'
576 576 usr = user_util.create_user()
577 577 username = usr.username
578 578 fixture.create_repo_group(obj_name, cur_user=usr.username)
579 579
580 580 new_user = Session().query(User)\
581 581 .filter(User.username == username).one()
582 582 response = self.app.post(
583 583 route_path('user_delete', user_id=new_user.user_id),
584 584 params={'csrf_token': self.csrf_token})
585 585
586 586 msg = 'user "%s" still owns 1 repository groups and cannot be removed. ' \
587 587 'Switch owners or remove those repository groups:%s' % (username, obj_name)
588 588 assert_session_flash(response, msg)
589 589 fixture.destroy_repo_group(obj_name)
590 590
591 591 def test_delete_owner_of_repository_group_detaching(self, request, user_util):
592 592 self.log_user()
593 593 obj_name = 'test_group'
594 594 usr = user_util.create_user(auto_cleanup=False)
595 595 username = usr.username
596 596 fixture.create_repo_group(obj_name, cur_user=usr.username)
597 597
598 598 new_user = Session().query(User)\
599 599 .filter(User.username == username).one()
600 600 response = self.app.post(
601 601 route_path('user_delete', user_id=new_user.user_id),
602 602 params={'user_repo_groups': 'delete', 'csrf_token': self.csrf_token})
603 603
604 604 msg = 'Deleted 1 repository groups'
605 605 assert_session_flash(response, msg)
606 606
607 607 def test_delete_owner_of_repository_group_deleting(self, request, user_util):
608 608 self.log_user()
609 609 obj_name = 'test_group'
610 610 usr = user_util.create_user(auto_cleanup=False)
611 611 username = usr.username
612 612 fixture.create_repo_group(obj_name, cur_user=usr.username)
613 613
614 614 new_user = Session().query(User)\
615 615 .filter(User.username == username).one()
616 616 response = self.app.post(
617 617 route_path('user_delete', user_id=new_user.user_id),
618 618 params={'user_repo_groups': 'detach', 'csrf_token': self.csrf_token})
619 619
620 620 msg = 'Detached 1 repository groups'
621 621 assert_session_flash(response, msg)
622 622 fixture.destroy_repo_group(obj_name)
623 623
624 624 def test_delete_owner_of_user_group(self, request, user_util):
625 625 self.log_user()
626 626 obj_name = 'test_user_group'
627 627 usr = user_util.create_user()
628 628 username = usr.username
629 629 fixture.create_user_group(obj_name, cur_user=usr.username)
630 630
631 631 new_user = Session().query(User)\
632 632 .filter(User.username == username).one()
633 633 response = self.app.post(
634 634 route_path('user_delete', user_id=new_user.user_id),
635 635 params={'csrf_token': self.csrf_token})
636 636
637 637 msg = 'user "%s" still owns 1 user groups and cannot be removed. ' \
638 638 'Switch owners or remove those user groups:%s' % (username, obj_name)
639 639 assert_session_flash(response, msg)
640 640 fixture.destroy_user_group(obj_name)
641 641
642 642 def test_delete_owner_of_user_group_detaching(self, request, user_util):
643 643 self.log_user()
644 644 obj_name = 'test_user_group'
645 645 usr = user_util.create_user(auto_cleanup=False)
646 646 username = usr.username
647 647 fixture.create_user_group(obj_name, cur_user=usr.username)
648 648
649 649 new_user = Session().query(User)\
650 650 .filter(User.username == username).one()
651 651 try:
652 652 response = self.app.post(
653 653 route_path('user_delete', user_id=new_user.user_id),
654 654 params={'user_user_groups': 'detach',
655 655 'csrf_token': self.csrf_token})
656 656
657 657 msg = 'Detached 1 user groups'
658 658 assert_session_flash(response, msg)
659 659 finally:
660 660 fixture.destroy_user_group(obj_name)
661 661
662 662 def test_delete_owner_of_user_group_deleting(self, request, user_util):
663 663 self.log_user()
664 664 obj_name = 'test_user_group'
665 665 usr = user_util.create_user(auto_cleanup=False)
666 666 username = usr.username
667 667 fixture.create_user_group(obj_name, cur_user=usr.username)
668 668
669 669 new_user = Session().query(User)\
670 670 .filter(User.username == username).one()
671 671 response = self.app.post(
672 672 route_path('user_delete', user_id=new_user.user_id),
673 673 params={'user_user_groups': 'delete', 'csrf_token': self.csrf_token})
674 674
675 675 msg = 'Deleted 1 user groups'
676 676 assert_session_flash(response, msg)
677 677
678 678 def test_edit(self, user_util):
679 679 self.log_user()
680 680 user = user_util.create_user()
681 681 self.app.get(route_path('user_edit', user_id=user.user_id))
682 682
683 683 def test_edit_default_user_redirect(self):
684 684 self.log_user()
685 685 user = User.get_default_user()
686 686 self.app.get(route_path('user_edit', user_id=user.user_id), status=302)
687 687
688 688 @pytest.mark.parametrize(
689 689 'repo_create, repo_create_write, user_group_create, repo_group_create,'
690 690 'fork_create, inherit_default_permissions, expect_error,'
691 691 'expect_form_error', [
692 692 ('hg.create.none', 'hg.create.write_on_repogroup.false',
693 693 'hg.usergroup.create.false', 'hg.repogroup.create.false',
694 694 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
695 695 ('hg.create.repository', 'hg.create.write_on_repogroup.false',
696 696 'hg.usergroup.create.false', 'hg.repogroup.create.false',
697 697 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
698 698 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
699 699 'hg.usergroup.create.true', 'hg.repogroup.create.true',
700 700 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
701 701 False),
702 702 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
703 703 'hg.usergroup.create.true', 'hg.repogroup.create.true',
704 704 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
705 705 True),
706 706 ('', '', '', '', '', '', True, False),
707 707 ])
708 708 def test_global_perms_on_user(
709 709 self, repo_create, repo_create_write, user_group_create,
710 710 repo_group_create, fork_create, expect_error, expect_form_error,
711 711 inherit_default_permissions, user_util):
712 712 self.log_user()
713 713 user = user_util.create_user()
714 714 uid = user.user_id
715 715
716 716 # ENABLE REPO CREATE ON A GROUP
717 717 perm_params = {
718 718 'inherit_default_permissions': False,
719 719 'default_repo_create': repo_create,
720 720 'default_repo_create_on_write': repo_create_write,
721 721 'default_user_group_create': user_group_create,
722 722 'default_repo_group_create': repo_group_create,
723 723 'default_fork_create': fork_create,
724 724 'default_inherit_default_permissions': inherit_default_permissions,
725 725 'csrf_token': self.csrf_token,
726 726 }
727 727 response = self.app.post(
728 728 route_path('user_edit_global_perms_update', user_id=uid),
729 729 params=perm_params)
730 730
731 731 if expect_form_error:
732 732 assert response.status_int == 200
733 733 response.mustcontain('Value must be one of')
734 734 else:
735 735 if expect_error:
736 736 msg = 'An error occurred during permissions saving'
737 737 else:
738 738 msg = 'User global permissions updated successfully'
739 739 ug = User.get(uid)
740 740 del perm_params['inherit_default_permissions']
741 741 del perm_params['csrf_token']
742 742 assert perm_params == ug.get_default_perms()
743 743 assert_session_flash(response, msg)
744 744
745 745 def test_global_permissions_initial_values(self, user_util):
746 746 self.log_user()
747 747 user = user_util.create_user()
748 748 uid = user.user_id
749 749 response = self.app.get(
750 750 route_path('user_edit_global_perms', user_id=uid))
751 751 default_user = User.get_default_user()
752 752 default_permissions = default_user.get_default_perms()
753 753 assert_response = response.assert_response()
754 754 expected_permissions = (
755 755 'default_repo_create', 'default_repo_create_on_write',
756 756 'default_fork_create', 'default_repo_group_create',
757 757 'default_user_group_create', 'default_inherit_default_permissions')
758 758 for permission in expected_permissions:
759 759 css_selector = '[name={}][checked=checked]'.format(permission)
760 760 element = assert_response.get_element(css_selector)
761 761 assert element.value == default_permissions[permission]
762 762
763 763 def test_perms_summary_page(self):
764 764 user = self.log_user()
765 765 response = self.app.get(
766 766 route_path('edit_user_perms_summary', user_id=user['user_id']))
767 767 for repo in Repository.query().all():
768 768 response.mustcontain(repo.repo_name)
769 769
770 770 def test_perms_summary_page_json(self):
771 771 user = self.log_user()
772 772 response = self.app.get(
773 773 route_path('edit_user_perms_summary_json', user_id=user['user_id']))
774 774 for repo in Repository.query().all():
775 775 response.mustcontain(repo.repo_name)
776 776
777 777 def test_audit_log_page(self):
778 778 user = self.log_user()
779 779 self.app.get(
780 780 route_path('edit_user_audit_logs', user_id=user['user_id']))
781 781
782 782 def test_audit_log_page_download(self):
783 783 user = self.log_user()
784 784 user_id = user['user_id']
785 785 response = self.app.get(
786 786 route_path('edit_user_audit_logs_download', user_id=user_id))
787 787
788 788 assert response.content_disposition == \
789 789 'attachment; filename=user_{}_audit_logs.json'.format(user_id)
790 790 assert response.content_type == "application/json"
@@ -1,1317 +1,1329 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-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 logging
22 22 import datetime
23 23 import formencode
24 24 import formencode.htmlfill
25 25
26 26 from pyramid.httpexceptions import HTTPFound
27 27 from pyramid.view import view_config
28 28 from pyramid.renderers import render
29 29 from pyramid.response import Response
30 30
31 31 from rhodecode import events
32 32 from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView
33 33 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
34 34 from rhodecode.authentication.base import get_authn_registry, RhodeCodeExternalAuthPlugin
35 35 from rhodecode.authentication.plugins import auth_rhodecode
36 36 from rhodecode.events import trigger
37 37 from rhodecode.model.db import true
38 38
39 39 from rhodecode.lib import audit_logger, rc_cache
40 40 from rhodecode.lib.exceptions import (
41 41 UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
42 42 UserOwnsUserGroupsException, DefaultUserException)
43 43 from rhodecode.lib.ext_json import json
44 44 from rhodecode.lib.auth import (
45 45 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
46 46 from rhodecode.lib import helpers as h
47 47 from rhodecode.lib.utils2 import safe_int, safe_unicode, AttributeDict
48 48 from rhodecode.model.auth_token import AuthTokenModel
49 49 from rhodecode.model.forms import (
50 50 UserForm, UserIndividualPermissionsForm, UserPermissionsForm,
51 51 UserExtraEmailForm, UserExtraIpForm)
52 52 from rhodecode.model.permission import PermissionModel
53 53 from rhodecode.model.repo_group import RepoGroupModel
54 54 from rhodecode.model.ssh_key import SshKeyModel
55 55 from rhodecode.model.user import UserModel
56 56 from rhodecode.model.user_group import UserGroupModel
57 57 from rhodecode.model.db import (
58 58 or_, coalesce,IntegrityError, User, UserGroup, UserIpMap, UserEmailMap,
59 59 UserApiKeys, UserSshKeys, RepoGroup)
60 60 from rhodecode.model.meta import Session
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64
65 65 class AdminUsersView(BaseAppView, DataGridAppView):
66 66
67 67 def load_default_context(self):
68 68 c = self._get_local_tmpl_context()
69 69 return c
70 70
71 71 @LoginRequired()
72 72 @HasPermissionAllDecorator('hg.admin')
73 73 @view_config(
74 74 route_name='users', request_method='GET',
75 75 renderer='rhodecode:templates/admin/users/users.mako')
76 76 def users_list(self):
77 77 c = self.load_default_context()
78 78 return self._get_template_context(c)
79 79
80 80 @LoginRequired()
81 81 @HasPermissionAllDecorator('hg.admin')
82 82 @view_config(
83 83 # renderer defined below
84 84 route_name='users_data', request_method='GET',
85 85 renderer='json_ext', xhr=True)
86 86 def users_list_data(self):
87 87 self.load_default_context()
88 88 column_map = {
89 89 'first_name': 'name',
90 90 'last_name': 'lastname',
91 91 }
92 92 draw, start, limit = self._extract_chunk(self.request)
93 93 search_q, order_by, order_dir = self._extract_ordering(
94 94 self.request, column_map=column_map)
95 95 _render = self.request.get_partial_renderer(
96 96 'rhodecode:templates/data_table/_dt_elements.mako')
97 97
98 98 def user_actions(user_id, username):
99 99 return _render("user_actions", user_id, username)
100 100
101 101 users_data_total_count = User.query()\
102 102 .filter(User.username != User.DEFAULT_USER) \
103 103 .count()
104 104
105 105 users_data_total_inactive_count = User.query()\
106 106 .filter(User.username != User.DEFAULT_USER) \
107 107 .filter(User.active != true())\
108 108 .count()
109 109
110 110 # json generate
111 111 base_q = User.query().filter(User.username != User.DEFAULT_USER)
112 112 base_inactive_q = base_q.filter(User.active != true())
113 113
114 114 if search_q:
115 115 like_expression = u'%{}%'.format(safe_unicode(search_q))
116 116 base_q = base_q.filter(or_(
117 117 User.username.ilike(like_expression),
118 118 User._email.ilike(like_expression),
119 119 User.name.ilike(like_expression),
120 120 User.lastname.ilike(like_expression),
121 121 ))
122 122 base_inactive_q = base_q.filter(User.active != true())
123 123
124 124 users_data_total_filtered_count = base_q.count()
125 125 users_data_total_filtered_inactive_count = base_inactive_q.count()
126 126
127 127 sort_col = getattr(User, order_by, None)
128 128 if sort_col:
129 129 if order_dir == 'asc':
130 130 # handle null values properly to order by NULL last
131 131 if order_by in ['last_activity']:
132 132 sort_col = coalesce(sort_col, datetime.date.max)
133 133 sort_col = sort_col.asc()
134 134 else:
135 135 # handle null values properly to order by NULL last
136 136 if order_by in ['last_activity']:
137 137 sort_col = coalesce(sort_col, datetime.date.min)
138 138 sort_col = sort_col.desc()
139 139
140 140 base_q = base_q.order_by(sort_col)
141 141 base_q = base_q.offset(start).limit(limit)
142 142
143 143 users_list = base_q.all()
144 144
145 145 users_data = []
146 146 for user in users_list:
147 147 users_data.append({
148 148 "username": h.gravatar_with_user(self.request, user.username),
149 149 "email": user.email,
150 150 "first_name": user.first_name,
151 151 "last_name": user.last_name,
152 152 "last_login": h.format_date(user.last_login),
153 153 "last_activity": h.format_date(user.last_activity),
154 154 "active": h.bool2icon(user.active),
155 155 "active_raw": user.active,
156 156 "admin": h.bool2icon(user.admin),
157 157 "extern_type": user.extern_type,
158 158 "extern_name": user.extern_name,
159 159 "action": user_actions(user.user_id, user.username),
160 160 })
161 161 data = ({
162 162 'draw': draw,
163 163 'data': users_data,
164 164 'recordsTotal': users_data_total_count,
165 165 'recordsFiltered': users_data_total_filtered_count,
166 166 'recordsTotalInactive': users_data_total_inactive_count,
167 167 'recordsFilteredInactive': users_data_total_filtered_inactive_count
168 168 })
169 169
170 170 return data
171 171
172 172 def _set_personal_repo_group_template_vars(self, c_obj):
173 173 DummyUser = AttributeDict({
174 174 'username': '${username}',
175 175 'user_id': '${user_id}',
176 176 })
177 177 c_obj.default_create_repo_group = RepoGroupModel() \
178 178 .get_default_create_personal_repo_group()
179 179 c_obj.personal_repo_group_name = RepoGroupModel() \
180 180 .get_personal_group_name(DummyUser)
181 181
182 182 @LoginRequired()
183 183 @HasPermissionAllDecorator('hg.admin')
184 184 @view_config(
185 185 route_name='users_new', request_method='GET',
186 186 renderer='rhodecode:templates/admin/users/user_add.mako')
187 187 def users_new(self):
188 188 _ = self.request.translate
189 189 c = self.load_default_context()
190 190 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
191 191 self._set_personal_repo_group_template_vars(c)
192 192 return self._get_template_context(c)
193 193
194 194 @LoginRequired()
195 195 @HasPermissionAllDecorator('hg.admin')
196 196 @CSRFRequired()
197 197 @view_config(
198 198 route_name='users_create', request_method='POST',
199 199 renderer='rhodecode:templates/admin/users/user_add.mako')
200 200 def users_create(self):
201 201 _ = self.request.translate
202 202 c = self.load_default_context()
203 203 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
204 204 user_model = UserModel()
205 205 user_form = UserForm(self.request.translate)()
206 206 try:
207 207 form_result = user_form.to_python(dict(self.request.POST))
208 208 user = user_model.create(form_result)
209 209 Session().flush()
210 210 creation_data = user.get_api_data()
211 211 username = form_result['username']
212 212
213 213 audit_logger.store_web(
214 214 'user.create', action_data={'data': creation_data},
215 215 user=c.rhodecode_user)
216 216
217 217 user_link = h.link_to(
218 218 h.escape(username),
219 219 h.route_path('user_edit', user_id=user.user_id))
220 220 h.flash(h.literal(_('Created user %(user_link)s')
221 221 % {'user_link': user_link}), category='success')
222 222 Session().commit()
223 223 except formencode.Invalid as errors:
224 224 self._set_personal_repo_group_template_vars(c)
225 225 data = render(
226 226 'rhodecode:templates/admin/users/user_add.mako',
227 227 self._get_template_context(c), self.request)
228 228 html = formencode.htmlfill.render(
229 229 data,
230 230 defaults=errors.value,
231 231 errors=errors.error_dict or {},
232 232 prefix_error=False,
233 233 encoding="UTF-8",
234 234 force_defaults=False
235 235 )
236 236 return Response(html)
237 237 except UserCreationError as e:
238 238 h.flash(e, 'error')
239 239 except Exception:
240 240 log.exception("Exception creation of user")
241 241 h.flash(_('Error occurred during creation of user %s')
242 242 % self.request.POST.get('username'), category='error')
243 243 raise HTTPFound(h.route_path('users'))
244 244
245 245
246 246 class UsersView(UserAppView):
247 247 ALLOW_SCOPED_TOKENS = False
248 248 """
249 249 This view has alternative version inside EE, if modified please take a look
250 250 in there as well.
251 251 """
252 252
253 253 def get_auth_plugins(self):
254 254 valid_plugins = []
255 255 authn_registry = get_authn_registry(self.request.registry)
256 256 for plugin in authn_registry.get_plugins_for_authentication():
257 257 if isinstance(plugin, RhodeCodeExternalAuthPlugin):
258 258 valid_plugins.append(plugin)
259 259 elif plugin.name == 'rhodecode':
260 260 valid_plugins.append(plugin)
261 261
262 262 # extend our choices if user has set a bound plugin which isn't enabled at the
263 263 # moment
264 264 extern_type = self.db_user.extern_type
265 265 if extern_type not in [x.uid for x in valid_plugins]:
266 266 try:
267 267 plugin = authn_registry.get_plugin_by_uid(extern_type)
268 268 if plugin:
269 269 valid_plugins.append(plugin)
270 270
271 271 except Exception:
272 272 log.exception(
273 273 'Could not extend user plugins with `{}`'.format(extern_type))
274 274 return valid_plugins
275 275
276 276 def load_default_context(self):
277 277 req = self.request
278 278
279 279 c = self._get_local_tmpl_context()
280 280 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
281 281 c.allowed_languages = [
282 282 ('en', 'English (en)'),
283 283 ('de', 'German (de)'),
284 284 ('fr', 'French (fr)'),
285 285 ('it', 'Italian (it)'),
286 286 ('ja', 'Japanese (ja)'),
287 287 ('pl', 'Polish (pl)'),
288 288 ('pt', 'Portuguese (pt)'),
289 289 ('ru', 'Russian (ru)'),
290 290 ('zh', 'Chinese (zh)'),
291 291 ]
292 292
293 293 c.allowed_extern_types = [
294 294 (x.uid, x.get_display_name()) for x in self.get_auth_plugins()
295 295 ]
296 296
297 297 c.available_permissions = req.registry.settings['available_permissions']
298 298 PermissionModel().set_global_permission_choices(
299 299 c, gettext_translator=req.translate)
300 300
301 301 return c
302 302
303 303 @LoginRequired()
304 304 @HasPermissionAllDecorator('hg.admin')
305 305 @CSRFRequired()
306 306 @view_config(
307 307 route_name='user_update', request_method='POST',
308 308 renderer='rhodecode:templates/admin/users/user_edit.mako')
309 309 def user_update(self):
310 310 _ = self.request.translate
311 311 c = self.load_default_context()
312 312
313 313 user_id = self.db_user_id
314 314 c.user = self.db_user
315 315
316 316 c.active = 'profile'
317 317 c.extern_type = c.user.extern_type
318 318 c.extern_name = c.user.extern_name
319 319 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
320 320 available_languages = [x[0] for x in c.allowed_languages]
321 321 _form = UserForm(self.request.translate, edit=True,
322 322 available_languages=available_languages,
323 323 old_data={'user_id': user_id,
324 324 'email': c.user.email})()
325 325 form_result = {}
326 326 old_values = c.user.get_api_data()
327 327 try:
328 328 form_result = _form.to_python(dict(self.request.POST))
329 329 skip_attrs = ['extern_name']
330 330 # TODO: plugin should define if username can be updated
331 331 if c.extern_type != "rhodecode":
332 332 # forbid updating username for external accounts
333 333 skip_attrs.append('username')
334 334
335 335 UserModel().update_user(
336 336 user_id, skip_attrs=skip_attrs, **form_result)
337 337
338 338 audit_logger.store_web(
339 339 'user.edit', action_data={'old_data': old_values},
340 340 user=c.rhodecode_user)
341 341
342 342 Session().commit()
343 343 h.flash(_('User updated successfully'), category='success')
344 344 except formencode.Invalid as errors:
345 345 data = render(
346 346 'rhodecode:templates/admin/users/user_edit.mako',
347 347 self._get_template_context(c), self.request)
348 348 html = formencode.htmlfill.render(
349 349 data,
350 350 defaults=errors.value,
351 351 errors=errors.error_dict or {},
352 352 prefix_error=False,
353 353 encoding="UTF-8",
354 354 force_defaults=False
355 355 )
356 356 return Response(html)
357 357 except UserCreationError as e:
358 358 h.flash(e, 'error')
359 359 except Exception:
360 360 log.exception("Exception updating user")
361 361 h.flash(_('Error occurred during update of user %s')
362 362 % form_result.get('username'), category='error')
363 363 raise HTTPFound(h.route_path('user_edit', user_id=user_id))
364 364
365 365 @LoginRequired()
366 366 @HasPermissionAllDecorator('hg.admin')
367 367 @CSRFRequired()
368 368 @view_config(
369 369 route_name='user_delete', request_method='POST',
370 370 renderer='rhodecode:templates/admin/users/user_edit.mako')
371 371 def user_delete(self):
372 372 _ = self.request.translate
373 373 c = self.load_default_context()
374 374 c.user = self.db_user
375 375
376 376 _repos = c.user.repositories
377 377 _repo_groups = c.user.repository_groups
378 378 _user_groups = c.user.user_groups
379 _artifacts = c.user.artifacts
379 380
380 381 handle_repos = None
381 382 handle_repo_groups = None
382 383 handle_user_groups = None
383 # dummy call for flash of handle
384 set_handle_flash_repos = lambda: None
385 set_handle_flash_repo_groups = lambda: None
386 set_handle_flash_user_groups = lambda: None
384 handle_artifacts = None
385
386 # calls for flash of handle based on handle case detach or delete
387 def set_handle_flash_repos():
388 handle = handle_repos
389 if handle == 'detach':
390 h.flash(_('Detached %s repositories') % len(_repos),
391 category='success')
392 elif handle == 'delete':
393 h.flash(_('Deleted %s repositories') % len(_repos),
394 category='success')
395
396 def set_handle_flash_repo_groups():
397 handle = handle_repo_groups
398 if handle == 'detach':
399 h.flash(_('Detached %s repository groups') % len(_repo_groups),
400 category='success')
401 elif handle == 'delete':
402 h.flash(_('Deleted %s repository groups') % len(_repo_groups),
403 category='success')
404
405 def set_handle_flash_user_groups():
406 handle = handle_user_groups
407 if handle == 'detach':
408 h.flash(_('Detached %s user groups') % len(_user_groups),
409 category='success')
410 elif handle == 'delete':
411 h.flash(_('Deleted %s user groups') % len(_user_groups),
412 category='success')
413
414 def set_handle_flash_artifacts():
415 handle = handle_artifacts
416 if handle == 'detach':
417 h.flash(_('Detached %s artifacts') % len(_artifacts),
418 category='success')
419 elif handle == 'delete':
420 h.flash(_('Deleted %s artifacts') % len(_artifacts),
421 category='success')
387 422
388 423 if _repos and self.request.POST.get('user_repos'):
389 do = self.request.POST['user_repos']
390 if do == 'detach':
391 handle_repos = 'detach'
392 set_handle_flash_repos = lambda: h.flash(
393 _('Detached %s repositories') % len(_repos),
394 category='success')
395 elif do == 'delete':
396 handle_repos = 'delete'
397 set_handle_flash_repos = lambda: h.flash(
398 _('Deleted %s repositories') % len(_repos),
399 category='success')
424 handle_repos = self.request.POST['user_repos']
400 425
401 426 if _repo_groups and self.request.POST.get('user_repo_groups'):
402 do = self.request.POST['user_repo_groups']
403 if do == 'detach':
404 handle_repo_groups = 'detach'
405 set_handle_flash_repo_groups = lambda: h.flash(
406 _('Detached %s repository groups') % len(_repo_groups),
407 category='success')
408 elif do == 'delete':
409 handle_repo_groups = 'delete'
410 set_handle_flash_repo_groups = lambda: h.flash(
411 _('Deleted %s repository groups') % len(_repo_groups),
412 category='success')
427 handle_repo_groups = self.request.POST['user_repo_groups']
413 428
414 429 if _user_groups and self.request.POST.get('user_user_groups'):
415 do = self.request.POST['user_user_groups']
416 if do == 'detach':
417 handle_user_groups = 'detach'
418 set_handle_flash_user_groups = lambda: h.flash(
419 _('Detached %s user groups') % len(_user_groups),
420 category='success')
421 elif do == 'delete':
422 handle_user_groups = 'delete'
423 set_handle_flash_user_groups = lambda: h.flash(
424 _('Deleted %s user groups') % len(_user_groups),
425 category='success')
430 handle_user_groups = self.request.POST['user_user_groups']
431
432 if _artifacts and self.request.POST.get('user_artifacts'):
433 handle_artifacts = self.request.POST['user_artifacts']
426 434
427 435 old_values = c.user.get_api_data()
436
428 437 try:
429 438 UserModel().delete(c.user, handle_repos=handle_repos,
430 439 handle_repo_groups=handle_repo_groups,
431 handle_user_groups=handle_user_groups)
440 handle_user_groups=handle_user_groups,
441 handle_artifacts=handle_artifacts)
432 442
433 443 audit_logger.store_web(
434 444 'user.delete', action_data={'old_data': old_values},
435 445 user=c.rhodecode_user)
436 446
437 447 Session().commit()
438 448 set_handle_flash_repos()
439 449 set_handle_flash_repo_groups()
440 450 set_handle_flash_user_groups()
441 h.flash(_('Successfully deleted user'), category='success')
451 set_handle_flash_artifacts()
452 username = h.escape(old_values['username'])
453 h.flash(_('Successfully deleted user `{}`').format(username), category='success')
442 454 except (UserOwnsReposException, UserOwnsRepoGroupsException,
443 455 UserOwnsUserGroupsException, DefaultUserException) as e:
444 456 h.flash(e, category='warning')
445 457 except Exception:
446 458 log.exception("Exception during deletion of user")
447 459 h.flash(_('An error occurred during deletion of user'),
448 460 category='error')
449 461 raise HTTPFound(h.route_path('users'))
450 462
451 463 @LoginRequired()
452 464 @HasPermissionAllDecorator('hg.admin')
453 465 @view_config(
454 466 route_name='user_edit', request_method='GET',
455 467 renderer='rhodecode:templates/admin/users/user_edit.mako')
456 468 def user_edit(self):
457 469 _ = self.request.translate
458 470 c = self.load_default_context()
459 471 c.user = self.db_user
460 472
461 473 c.active = 'profile'
462 474 c.extern_type = c.user.extern_type
463 475 c.extern_name = c.user.extern_name
464 476 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
465 477
466 478 defaults = c.user.get_dict()
467 479 defaults.update({'language': c.user.user_data.get('language')})
468 480
469 481 data = render(
470 482 'rhodecode:templates/admin/users/user_edit.mako',
471 483 self._get_template_context(c), self.request)
472 484 html = formencode.htmlfill.render(
473 485 data,
474 486 defaults=defaults,
475 487 encoding="UTF-8",
476 488 force_defaults=False
477 489 )
478 490 return Response(html)
479 491
480 492 @LoginRequired()
481 493 @HasPermissionAllDecorator('hg.admin')
482 494 @view_config(
483 495 route_name='user_edit_advanced', request_method='GET',
484 496 renderer='rhodecode:templates/admin/users/user_edit.mako')
485 497 def user_edit_advanced(self):
486 498 _ = self.request.translate
487 499 c = self.load_default_context()
488 500
489 501 user_id = self.db_user_id
490 502 c.user = self.db_user
491 503
492 504 c.active = 'advanced'
493 505 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
494 506 c.personal_repo_group_name = RepoGroupModel()\
495 507 .get_personal_group_name(c.user)
496 508
497 509 c.user_to_review_rules = sorted(
498 510 (x.user for x in c.user.user_review_rules),
499 511 key=lambda u: u.username.lower())
500 512
501 513 c.first_admin = User.get_first_super_admin()
502 514 defaults = c.user.get_dict()
503 515
504 516 # Interim workaround if the user participated on any pull requests as a
505 517 # reviewer.
506 518 has_review = len(c.user.reviewer_pull_requests)
507 519 c.can_delete_user = not has_review
508 520 c.can_delete_user_message = ''
509 521 inactive_link = h.link_to(
510 522 'inactive', h.route_path('user_edit', user_id=user_id, _anchor='active'))
511 523 if has_review == 1:
512 524 c.can_delete_user_message = h.literal(_(
513 525 'The user participates as reviewer in {} pull request and '
514 526 'cannot be deleted. \nYou can set the user to '
515 527 '"{}" instead of deleting it.').format(
516 528 has_review, inactive_link))
517 529 elif has_review:
518 530 c.can_delete_user_message = h.literal(_(
519 531 'The user participates as reviewer in {} pull requests and '
520 532 'cannot be deleted. \nYou can set the user to '
521 533 '"{}" instead of deleting it.').format(
522 534 has_review, inactive_link))
523 535
524 536 data = render(
525 537 'rhodecode:templates/admin/users/user_edit.mako',
526 538 self._get_template_context(c), self.request)
527 539 html = formencode.htmlfill.render(
528 540 data,
529 541 defaults=defaults,
530 542 encoding="UTF-8",
531 543 force_defaults=False
532 544 )
533 545 return Response(html)
534 546
535 547 @LoginRequired()
536 548 @HasPermissionAllDecorator('hg.admin')
537 549 @view_config(
538 550 route_name='user_edit_global_perms', request_method='GET',
539 551 renderer='rhodecode:templates/admin/users/user_edit.mako')
540 552 def user_edit_global_perms(self):
541 553 _ = self.request.translate
542 554 c = self.load_default_context()
543 555 c.user = self.db_user
544 556
545 557 c.active = 'global_perms'
546 558
547 559 c.default_user = User.get_default_user()
548 560 defaults = c.user.get_dict()
549 561 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
550 562 defaults.update(c.default_user.get_default_perms())
551 563 defaults.update(c.user.get_default_perms())
552 564
553 565 data = render(
554 566 'rhodecode:templates/admin/users/user_edit.mako',
555 567 self._get_template_context(c), self.request)
556 568 html = formencode.htmlfill.render(
557 569 data,
558 570 defaults=defaults,
559 571 encoding="UTF-8",
560 572 force_defaults=False
561 573 )
562 574 return Response(html)
563 575
564 576 @LoginRequired()
565 577 @HasPermissionAllDecorator('hg.admin')
566 578 @CSRFRequired()
567 579 @view_config(
568 580 route_name='user_edit_global_perms_update', request_method='POST',
569 581 renderer='rhodecode:templates/admin/users/user_edit.mako')
570 582 def user_edit_global_perms_update(self):
571 583 _ = self.request.translate
572 584 c = self.load_default_context()
573 585
574 586 user_id = self.db_user_id
575 587 c.user = self.db_user
576 588
577 589 c.active = 'global_perms'
578 590 try:
579 591 # first stage that verifies the checkbox
580 592 _form = UserIndividualPermissionsForm(self.request.translate)
581 593 form_result = _form.to_python(dict(self.request.POST))
582 594 inherit_perms = form_result['inherit_default_permissions']
583 595 c.user.inherit_default_permissions = inherit_perms
584 596 Session().add(c.user)
585 597
586 598 if not inherit_perms:
587 599 # only update the individual ones if we un check the flag
588 600 _form = UserPermissionsForm(
589 601 self.request.translate,
590 602 [x[0] for x in c.repo_create_choices],
591 603 [x[0] for x in c.repo_create_on_write_choices],
592 604 [x[0] for x in c.repo_group_create_choices],
593 605 [x[0] for x in c.user_group_create_choices],
594 606 [x[0] for x in c.fork_choices],
595 607 [x[0] for x in c.inherit_default_permission_choices])()
596 608
597 609 form_result = _form.to_python(dict(self.request.POST))
598 610 form_result.update({'perm_user_id': c.user.user_id})
599 611
600 612 PermissionModel().update_user_permissions(form_result)
601 613
602 614 # TODO(marcink): implement global permissions
603 615 # audit_log.store_web('user.edit.permissions')
604 616
605 617 Session().commit()
606 618
607 619 h.flash(_('User global permissions updated successfully'),
608 620 category='success')
609 621
610 622 except formencode.Invalid as errors:
611 623 data = render(
612 624 'rhodecode:templates/admin/users/user_edit.mako',
613 625 self._get_template_context(c), self.request)
614 626 html = formencode.htmlfill.render(
615 627 data,
616 628 defaults=errors.value,
617 629 errors=errors.error_dict or {},
618 630 prefix_error=False,
619 631 encoding="UTF-8",
620 632 force_defaults=False
621 633 )
622 634 return Response(html)
623 635 except Exception:
624 636 log.exception("Exception during permissions saving")
625 637 h.flash(_('An error occurred during permissions saving'),
626 638 category='error')
627 639
628 640 affected_user_ids = [user_id]
629 641 PermissionModel().trigger_permission_flush(affected_user_ids)
630 642 raise HTTPFound(h.route_path('user_edit_global_perms', user_id=user_id))
631 643
632 644 @LoginRequired()
633 645 @HasPermissionAllDecorator('hg.admin')
634 646 @CSRFRequired()
635 647 @view_config(
636 648 route_name='user_enable_force_password_reset', request_method='POST',
637 649 renderer='rhodecode:templates/admin/users/user_edit.mako')
638 650 def user_enable_force_password_reset(self):
639 651 _ = self.request.translate
640 652 c = self.load_default_context()
641 653
642 654 user_id = self.db_user_id
643 655 c.user = self.db_user
644 656
645 657 try:
646 658 c.user.update_userdata(force_password_change=True)
647 659
648 660 msg = _('Force password change enabled for user')
649 661 audit_logger.store_web('user.edit.password_reset.enabled',
650 662 user=c.rhodecode_user)
651 663
652 664 Session().commit()
653 665 h.flash(msg, category='success')
654 666 except Exception:
655 667 log.exception("Exception during password reset for user")
656 668 h.flash(_('An error occurred during password reset for user'),
657 669 category='error')
658 670
659 671 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
660 672
661 673 @LoginRequired()
662 674 @HasPermissionAllDecorator('hg.admin')
663 675 @CSRFRequired()
664 676 @view_config(
665 677 route_name='user_disable_force_password_reset', request_method='POST',
666 678 renderer='rhodecode:templates/admin/users/user_edit.mako')
667 679 def user_disable_force_password_reset(self):
668 680 _ = self.request.translate
669 681 c = self.load_default_context()
670 682
671 683 user_id = self.db_user_id
672 684 c.user = self.db_user
673 685
674 686 try:
675 687 c.user.update_userdata(force_password_change=False)
676 688
677 689 msg = _('Force password change disabled for user')
678 690 audit_logger.store_web(
679 691 'user.edit.password_reset.disabled',
680 692 user=c.rhodecode_user)
681 693
682 694 Session().commit()
683 695 h.flash(msg, category='success')
684 696 except Exception:
685 697 log.exception("Exception during password reset for user")
686 698 h.flash(_('An error occurred during password reset for user'),
687 699 category='error')
688 700
689 701 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
690 702
691 703 @LoginRequired()
692 704 @HasPermissionAllDecorator('hg.admin')
693 705 @CSRFRequired()
694 706 @view_config(
695 707 route_name='user_create_personal_repo_group', request_method='POST',
696 708 renderer='rhodecode:templates/admin/users/user_edit.mako')
697 709 def user_create_personal_repo_group(self):
698 710 """
699 711 Create personal repository group for this user
700 712 """
701 713 from rhodecode.model.repo_group import RepoGroupModel
702 714
703 715 _ = self.request.translate
704 716 c = self.load_default_context()
705 717
706 718 user_id = self.db_user_id
707 719 c.user = self.db_user
708 720
709 721 personal_repo_group = RepoGroup.get_user_personal_repo_group(
710 722 c.user.user_id)
711 723 if personal_repo_group:
712 724 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
713 725
714 726 personal_repo_group_name = RepoGroupModel().get_personal_group_name(c.user)
715 727 named_personal_group = RepoGroup.get_by_group_name(
716 728 personal_repo_group_name)
717 729 try:
718 730
719 731 if named_personal_group and named_personal_group.user_id == c.user.user_id:
720 732 # migrate the same named group, and mark it as personal
721 733 named_personal_group.personal = True
722 734 Session().add(named_personal_group)
723 735 Session().commit()
724 736 msg = _('Linked repository group `%s` as personal' % (
725 737 personal_repo_group_name,))
726 738 h.flash(msg, category='success')
727 739 elif not named_personal_group:
728 740 RepoGroupModel().create_personal_repo_group(c.user)
729 741
730 742 msg = _('Created repository group `%s`' % (
731 743 personal_repo_group_name,))
732 744 h.flash(msg, category='success')
733 745 else:
734 746 msg = _('Repository group `%s` is already taken' % (
735 747 personal_repo_group_name,))
736 748 h.flash(msg, category='warning')
737 749 except Exception:
738 750 log.exception("Exception during repository group creation")
739 751 msg = _(
740 752 'An error occurred during repository group creation for user')
741 753 h.flash(msg, category='error')
742 754 Session().rollback()
743 755
744 756 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
745 757
746 758 @LoginRequired()
747 759 @HasPermissionAllDecorator('hg.admin')
748 760 @view_config(
749 761 route_name='edit_user_auth_tokens', request_method='GET',
750 762 renderer='rhodecode:templates/admin/users/user_edit.mako')
751 763 def auth_tokens(self):
752 764 _ = self.request.translate
753 765 c = self.load_default_context()
754 766 c.user = self.db_user
755 767
756 768 c.active = 'auth_tokens'
757 769
758 770 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
759 771 c.role_values = [
760 772 (x, AuthTokenModel.cls._get_role_name(x))
761 773 for x in AuthTokenModel.cls.ROLES]
762 774 c.role_options = [(c.role_values, _("Role"))]
763 775 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
764 776 c.user.user_id, show_expired=True)
765 777 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
766 778 return self._get_template_context(c)
767 779
768 780 def maybe_attach_token_scope(self, token):
769 781 # implemented in EE edition
770 782 pass
771 783
772 784 @LoginRequired()
773 785 @HasPermissionAllDecorator('hg.admin')
774 786 @CSRFRequired()
775 787 @view_config(
776 788 route_name='edit_user_auth_tokens_add', request_method='POST')
777 789 def auth_tokens_add(self):
778 790 _ = self.request.translate
779 791 c = self.load_default_context()
780 792
781 793 user_id = self.db_user_id
782 794 c.user = self.db_user
783 795
784 796 user_data = c.user.get_api_data()
785 797 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
786 798 description = self.request.POST.get('description')
787 799 role = self.request.POST.get('role')
788 800
789 801 token = UserModel().add_auth_token(
790 802 user=c.user.user_id,
791 803 lifetime_minutes=lifetime, role=role, description=description,
792 804 scope_callback=self.maybe_attach_token_scope)
793 805 token_data = token.get_api_data()
794 806
795 807 audit_logger.store_web(
796 808 'user.edit.token.add', action_data={
797 809 'data': {'token': token_data, 'user': user_data}},
798 810 user=self._rhodecode_user, )
799 811 Session().commit()
800 812
801 813 h.flash(_("Auth token successfully created"), category='success')
802 814 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
803 815
804 816 @LoginRequired()
805 817 @HasPermissionAllDecorator('hg.admin')
806 818 @CSRFRequired()
807 819 @view_config(
808 820 route_name='edit_user_auth_tokens_delete', request_method='POST')
809 821 def auth_tokens_delete(self):
810 822 _ = self.request.translate
811 823 c = self.load_default_context()
812 824
813 825 user_id = self.db_user_id
814 826 c.user = self.db_user
815 827
816 828 user_data = c.user.get_api_data()
817 829
818 830 del_auth_token = self.request.POST.get('del_auth_token')
819 831
820 832 if del_auth_token:
821 833 token = UserApiKeys.get_or_404(del_auth_token)
822 834 token_data = token.get_api_data()
823 835
824 836 AuthTokenModel().delete(del_auth_token, c.user.user_id)
825 837 audit_logger.store_web(
826 838 'user.edit.token.delete', action_data={
827 839 'data': {'token': token_data, 'user': user_data}},
828 840 user=self._rhodecode_user,)
829 841 Session().commit()
830 842 h.flash(_("Auth token successfully deleted"), category='success')
831 843
832 844 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
833 845
834 846 @LoginRequired()
835 847 @HasPermissionAllDecorator('hg.admin')
836 848 @view_config(
837 849 route_name='edit_user_ssh_keys', request_method='GET',
838 850 renderer='rhodecode:templates/admin/users/user_edit.mako')
839 851 def ssh_keys(self):
840 852 _ = self.request.translate
841 853 c = self.load_default_context()
842 854 c.user = self.db_user
843 855
844 856 c.active = 'ssh_keys'
845 857 c.default_key = self.request.GET.get('default_key')
846 858 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
847 859 return self._get_template_context(c)
848 860
849 861 @LoginRequired()
850 862 @HasPermissionAllDecorator('hg.admin')
851 863 @view_config(
852 864 route_name='edit_user_ssh_keys_generate_keypair', request_method='GET',
853 865 renderer='rhodecode:templates/admin/users/user_edit.mako')
854 866 def ssh_keys_generate_keypair(self):
855 867 _ = self.request.translate
856 868 c = self.load_default_context()
857 869
858 870 c.user = self.db_user
859 871
860 872 c.active = 'ssh_keys_generate'
861 873 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
862 874 c.private, c.public = SshKeyModel().generate_keypair(comment=comment)
863 875
864 876 return self._get_template_context(c)
865 877
866 878 @LoginRequired()
867 879 @HasPermissionAllDecorator('hg.admin')
868 880 @CSRFRequired()
869 881 @view_config(
870 882 route_name='edit_user_ssh_keys_add', request_method='POST')
871 883 def ssh_keys_add(self):
872 884 _ = self.request.translate
873 885 c = self.load_default_context()
874 886
875 887 user_id = self.db_user_id
876 888 c.user = self.db_user
877 889
878 890 user_data = c.user.get_api_data()
879 891 key_data = self.request.POST.get('key_data')
880 892 description = self.request.POST.get('description')
881 893
882 894 fingerprint = 'unknown'
883 895 try:
884 896 if not key_data:
885 897 raise ValueError('Please add a valid public key')
886 898
887 899 key = SshKeyModel().parse_key(key_data.strip())
888 900 fingerprint = key.hash_md5()
889 901
890 902 ssh_key = SshKeyModel().create(
891 903 c.user.user_id, fingerprint, key.keydata, description)
892 904 ssh_key_data = ssh_key.get_api_data()
893 905
894 906 audit_logger.store_web(
895 907 'user.edit.ssh_key.add', action_data={
896 908 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
897 909 user=self._rhodecode_user, )
898 910 Session().commit()
899 911
900 912 # Trigger an event on change of keys.
901 913 trigger(SshKeyFileChangeEvent(), self.request.registry)
902 914
903 915 h.flash(_("Ssh Key successfully created"), category='success')
904 916
905 917 except IntegrityError:
906 918 log.exception("Exception during ssh key saving")
907 919 err = 'Such key with fingerprint `{}` already exists, ' \
908 920 'please use a different one'.format(fingerprint)
909 921 h.flash(_('An error occurred during ssh key saving: {}').format(err),
910 922 category='error')
911 923 except Exception as e:
912 924 log.exception("Exception during ssh key saving")
913 925 h.flash(_('An error occurred during ssh key saving: {}').format(e),
914 926 category='error')
915 927
916 928 return HTTPFound(
917 929 h.route_path('edit_user_ssh_keys', user_id=user_id))
918 930
919 931 @LoginRequired()
920 932 @HasPermissionAllDecorator('hg.admin')
921 933 @CSRFRequired()
922 934 @view_config(
923 935 route_name='edit_user_ssh_keys_delete', request_method='POST')
924 936 def ssh_keys_delete(self):
925 937 _ = self.request.translate
926 938 c = self.load_default_context()
927 939
928 940 user_id = self.db_user_id
929 941 c.user = self.db_user
930 942
931 943 user_data = c.user.get_api_data()
932 944
933 945 del_ssh_key = self.request.POST.get('del_ssh_key')
934 946
935 947 if del_ssh_key:
936 948 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
937 949 ssh_key_data = ssh_key.get_api_data()
938 950
939 951 SshKeyModel().delete(del_ssh_key, c.user.user_id)
940 952 audit_logger.store_web(
941 953 'user.edit.ssh_key.delete', action_data={
942 954 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
943 955 user=self._rhodecode_user,)
944 956 Session().commit()
945 957 # Trigger an event on change of keys.
946 958 trigger(SshKeyFileChangeEvent(), self.request.registry)
947 959 h.flash(_("Ssh key successfully deleted"), category='success')
948 960
949 961 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
950 962
951 963 @LoginRequired()
952 964 @HasPermissionAllDecorator('hg.admin')
953 965 @view_config(
954 966 route_name='edit_user_emails', request_method='GET',
955 967 renderer='rhodecode:templates/admin/users/user_edit.mako')
956 968 def emails(self):
957 969 _ = self.request.translate
958 970 c = self.load_default_context()
959 971 c.user = self.db_user
960 972
961 973 c.active = 'emails'
962 974 c.user_email_map = UserEmailMap.query() \
963 975 .filter(UserEmailMap.user == c.user).all()
964 976
965 977 return self._get_template_context(c)
966 978
967 979 @LoginRequired()
968 980 @HasPermissionAllDecorator('hg.admin')
969 981 @CSRFRequired()
970 982 @view_config(
971 983 route_name='edit_user_emails_add', request_method='POST')
972 984 def emails_add(self):
973 985 _ = self.request.translate
974 986 c = self.load_default_context()
975 987
976 988 user_id = self.db_user_id
977 989 c.user = self.db_user
978 990
979 991 email = self.request.POST.get('new_email')
980 992 user_data = c.user.get_api_data()
981 993 try:
982 994
983 995 form = UserExtraEmailForm(self.request.translate)()
984 996 data = form.to_python({'email': email})
985 997 email = data['email']
986 998
987 999 UserModel().add_extra_email(c.user.user_id, email)
988 1000 audit_logger.store_web(
989 1001 'user.edit.email.add',
990 1002 action_data={'email': email, 'user': user_data},
991 1003 user=self._rhodecode_user)
992 1004 Session().commit()
993 1005 h.flash(_("Added new email address `%s` for user account") % email,
994 1006 category='success')
995 1007 except formencode.Invalid as error:
996 1008 h.flash(h.escape(error.error_dict['email']), category='error')
997 1009 except IntegrityError:
998 1010 log.warning("Email %s already exists", email)
999 1011 h.flash(_('Email `{}` is already registered for another user.').format(email),
1000 1012 category='error')
1001 1013 except Exception:
1002 1014 log.exception("Exception during email saving")
1003 1015 h.flash(_('An error occurred during email saving'),
1004 1016 category='error')
1005 1017 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1006 1018
1007 1019 @LoginRequired()
1008 1020 @HasPermissionAllDecorator('hg.admin')
1009 1021 @CSRFRequired()
1010 1022 @view_config(
1011 1023 route_name='edit_user_emails_delete', request_method='POST')
1012 1024 def emails_delete(self):
1013 1025 _ = self.request.translate
1014 1026 c = self.load_default_context()
1015 1027
1016 1028 user_id = self.db_user_id
1017 1029 c.user = self.db_user
1018 1030
1019 1031 email_id = self.request.POST.get('del_email_id')
1020 1032 user_model = UserModel()
1021 1033
1022 1034 email = UserEmailMap.query().get(email_id).email
1023 1035 user_data = c.user.get_api_data()
1024 1036 user_model.delete_extra_email(c.user.user_id, email_id)
1025 1037 audit_logger.store_web(
1026 1038 'user.edit.email.delete',
1027 1039 action_data={'email': email, 'user': user_data},
1028 1040 user=self._rhodecode_user)
1029 1041 Session().commit()
1030 1042 h.flash(_("Removed email address from user account"),
1031 1043 category='success')
1032 1044 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1033 1045
1034 1046 @LoginRequired()
1035 1047 @HasPermissionAllDecorator('hg.admin')
1036 1048 @view_config(
1037 1049 route_name='edit_user_ips', request_method='GET',
1038 1050 renderer='rhodecode:templates/admin/users/user_edit.mako')
1039 1051 def ips(self):
1040 1052 _ = self.request.translate
1041 1053 c = self.load_default_context()
1042 1054 c.user = self.db_user
1043 1055
1044 1056 c.active = 'ips'
1045 1057 c.user_ip_map = UserIpMap.query() \
1046 1058 .filter(UserIpMap.user == c.user).all()
1047 1059
1048 1060 c.inherit_default_ips = c.user.inherit_default_permissions
1049 1061 c.default_user_ip_map = UserIpMap.query() \
1050 1062 .filter(UserIpMap.user == User.get_default_user()).all()
1051 1063
1052 1064 return self._get_template_context(c)
1053 1065
1054 1066 @LoginRequired()
1055 1067 @HasPermissionAllDecorator('hg.admin')
1056 1068 @CSRFRequired()
1057 1069 @view_config(
1058 1070 route_name='edit_user_ips_add', request_method='POST')
1059 1071 # NOTE(marcink): this view is allowed for default users, as we can
1060 1072 # edit their IP white list
1061 1073 def ips_add(self):
1062 1074 _ = self.request.translate
1063 1075 c = self.load_default_context()
1064 1076
1065 1077 user_id = self.db_user_id
1066 1078 c.user = self.db_user
1067 1079
1068 1080 user_model = UserModel()
1069 1081 desc = self.request.POST.get('description')
1070 1082 try:
1071 1083 ip_list = user_model.parse_ip_range(
1072 1084 self.request.POST.get('new_ip'))
1073 1085 except Exception as e:
1074 1086 ip_list = []
1075 1087 log.exception("Exception during ip saving")
1076 1088 h.flash(_('An error occurred during ip saving:%s' % (e,)),
1077 1089 category='error')
1078 1090 added = []
1079 1091 user_data = c.user.get_api_data()
1080 1092 for ip in ip_list:
1081 1093 try:
1082 1094 form = UserExtraIpForm(self.request.translate)()
1083 1095 data = form.to_python({'ip': ip})
1084 1096 ip = data['ip']
1085 1097
1086 1098 user_model.add_extra_ip(c.user.user_id, ip, desc)
1087 1099 audit_logger.store_web(
1088 1100 'user.edit.ip.add',
1089 1101 action_data={'ip': ip, 'user': user_data},
1090 1102 user=self._rhodecode_user)
1091 1103 Session().commit()
1092 1104 added.append(ip)
1093 1105 except formencode.Invalid as error:
1094 1106 msg = error.error_dict['ip']
1095 1107 h.flash(msg, category='error')
1096 1108 except Exception:
1097 1109 log.exception("Exception during ip saving")
1098 1110 h.flash(_('An error occurred during ip saving'),
1099 1111 category='error')
1100 1112 if added:
1101 1113 h.flash(
1102 1114 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
1103 1115 category='success')
1104 1116 if 'default_user' in self.request.POST:
1105 1117 # case for editing global IP list we do it for 'DEFAULT' user
1106 1118 raise HTTPFound(h.route_path('admin_permissions_ips'))
1107 1119 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1108 1120
1109 1121 @LoginRequired()
1110 1122 @HasPermissionAllDecorator('hg.admin')
1111 1123 @CSRFRequired()
1112 1124 @view_config(
1113 1125 route_name='edit_user_ips_delete', request_method='POST')
1114 1126 # NOTE(marcink): this view is allowed for default users, as we can
1115 1127 # edit their IP white list
1116 1128 def ips_delete(self):
1117 1129 _ = self.request.translate
1118 1130 c = self.load_default_context()
1119 1131
1120 1132 user_id = self.db_user_id
1121 1133 c.user = self.db_user
1122 1134
1123 1135 ip_id = self.request.POST.get('del_ip_id')
1124 1136 user_model = UserModel()
1125 1137 user_data = c.user.get_api_data()
1126 1138 ip = UserIpMap.query().get(ip_id).ip_addr
1127 1139 user_model.delete_extra_ip(c.user.user_id, ip_id)
1128 1140 audit_logger.store_web(
1129 1141 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
1130 1142 user=self._rhodecode_user)
1131 1143 Session().commit()
1132 1144 h.flash(_("Removed ip address from user whitelist"), category='success')
1133 1145
1134 1146 if 'default_user' in self.request.POST:
1135 1147 # case for editing global IP list we do it for 'DEFAULT' user
1136 1148 raise HTTPFound(h.route_path('admin_permissions_ips'))
1137 1149 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1138 1150
1139 1151 @LoginRequired()
1140 1152 @HasPermissionAllDecorator('hg.admin')
1141 1153 @view_config(
1142 1154 route_name='edit_user_groups_management', request_method='GET',
1143 1155 renderer='rhodecode:templates/admin/users/user_edit.mako')
1144 1156 def groups_management(self):
1145 1157 c = self.load_default_context()
1146 1158 c.user = self.db_user
1147 1159 c.data = c.user.group_member
1148 1160
1149 1161 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
1150 1162 for group in c.user.group_member]
1151 1163 c.groups = json.dumps(groups)
1152 1164 c.active = 'groups'
1153 1165
1154 1166 return self._get_template_context(c)
1155 1167
1156 1168 @LoginRequired()
1157 1169 @HasPermissionAllDecorator('hg.admin')
1158 1170 @CSRFRequired()
1159 1171 @view_config(
1160 1172 route_name='edit_user_groups_management_updates', request_method='POST')
1161 1173 def groups_management_updates(self):
1162 1174 _ = self.request.translate
1163 1175 c = self.load_default_context()
1164 1176
1165 1177 user_id = self.db_user_id
1166 1178 c.user = self.db_user
1167 1179
1168 1180 user_groups = set(self.request.POST.getall('users_group_id'))
1169 1181 user_groups_objects = []
1170 1182
1171 1183 for ugid in user_groups:
1172 1184 user_groups_objects.append(
1173 1185 UserGroupModel().get_group(safe_int(ugid)))
1174 1186 user_group_model = UserGroupModel()
1175 1187 added_to_groups, removed_from_groups = \
1176 1188 user_group_model.change_groups(c.user, user_groups_objects)
1177 1189
1178 1190 user_data = c.user.get_api_data()
1179 1191 for user_group_id in added_to_groups:
1180 1192 user_group = UserGroup.get(user_group_id)
1181 1193 old_values = user_group.get_api_data()
1182 1194 audit_logger.store_web(
1183 1195 'user_group.edit.member.add',
1184 1196 action_data={'user': user_data, 'old_data': old_values},
1185 1197 user=self._rhodecode_user)
1186 1198
1187 1199 for user_group_id in removed_from_groups:
1188 1200 user_group = UserGroup.get(user_group_id)
1189 1201 old_values = user_group.get_api_data()
1190 1202 audit_logger.store_web(
1191 1203 'user_group.edit.member.delete',
1192 1204 action_data={'user': user_data, 'old_data': old_values},
1193 1205 user=self._rhodecode_user)
1194 1206
1195 1207 Session().commit()
1196 1208 c.active = 'user_groups_management'
1197 1209 h.flash(_("Groups successfully changed"), category='success')
1198 1210
1199 1211 return HTTPFound(h.route_path(
1200 1212 'edit_user_groups_management', user_id=user_id))
1201 1213
1202 1214 @LoginRequired()
1203 1215 @HasPermissionAllDecorator('hg.admin')
1204 1216 @view_config(
1205 1217 route_name='edit_user_audit_logs', request_method='GET',
1206 1218 renderer='rhodecode:templates/admin/users/user_edit.mako')
1207 1219 def user_audit_logs(self):
1208 1220 _ = self.request.translate
1209 1221 c = self.load_default_context()
1210 1222 c.user = self.db_user
1211 1223
1212 1224 c.active = 'audit'
1213 1225
1214 1226 p = safe_int(self.request.GET.get('page', 1), 1)
1215 1227
1216 1228 filter_term = self.request.GET.get('filter')
1217 1229 user_log = UserModel().get_user_log(c.user, filter_term)
1218 1230
1219 1231 def url_generator(**kw):
1220 1232 if filter_term:
1221 1233 kw['filter'] = filter_term
1222 1234 return self.request.current_route_path(_query=kw)
1223 1235
1224 1236 c.audit_logs = h.Page(
1225 1237 user_log, page=p, items_per_page=10, url=url_generator)
1226 1238 c.filter_term = filter_term
1227 1239 return self._get_template_context(c)
1228 1240
1229 1241 @LoginRequired()
1230 1242 @HasPermissionAllDecorator('hg.admin')
1231 1243 @view_config(
1232 1244 route_name='edit_user_audit_logs_download', request_method='GET',
1233 1245 renderer='string')
1234 1246 def user_audit_logs_download(self):
1235 1247 _ = self.request.translate
1236 1248 c = self.load_default_context()
1237 1249 c.user = self.db_user
1238 1250
1239 1251 user_log = UserModel().get_user_log(c.user, filter_term=None)
1240 1252
1241 1253 audit_log_data = {}
1242 1254 for entry in user_log:
1243 1255 audit_log_data[entry.user_log_id] = entry.get_dict()
1244 1256
1245 1257 response = Response(json.dumps(audit_log_data, indent=4))
1246 1258 response.content_disposition = str(
1247 1259 'attachment; filename=%s' % 'user_{}_audit_logs.json'.format(c.user.user_id))
1248 1260 response.content_type = 'application/json'
1249 1261
1250 1262 return response
1251 1263
1252 1264 @LoginRequired()
1253 1265 @HasPermissionAllDecorator('hg.admin')
1254 1266 @view_config(
1255 1267 route_name='edit_user_perms_summary', request_method='GET',
1256 1268 renderer='rhodecode:templates/admin/users/user_edit.mako')
1257 1269 def user_perms_summary(self):
1258 1270 _ = self.request.translate
1259 1271 c = self.load_default_context()
1260 1272 c.user = self.db_user
1261 1273
1262 1274 c.active = 'perms_summary'
1263 1275 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1264 1276
1265 1277 return self._get_template_context(c)
1266 1278
1267 1279 @LoginRequired()
1268 1280 @HasPermissionAllDecorator('hg.admin')
1269 1281 @view_config(
1270 1282 route_name='edit_user_perms_summary_json', request_method='GET',
1271 1283 renderer='json_ext')
1272 1284 def user_perms_summary_json(self):
1273 1285 self.load_default_context()
1274 1286 perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
1275 1287
1276 1288 return perm_user.permissions
1277 1289
1278 1290 @LoginRequired()
1279 1291 @HasPermissionAllDecorator('hg.admin')
1280 1292 @view_config(
1281 1293 route_name='edit_user_caches', request_method='GET',
1282 1294 renderer='rhodecode:templates/admin/users/user_edit.mako')
1283 1295 def user_caches(self):
1284 1296 _ = self.request.translate
1285 1297 c = self.load_default_context()
1286 1298 c.user = self.db_user
1287 1299
1288 1300 c.active = 'caches'
1289 1301 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1290 1302
1291 1303 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1292 1304 c.region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1293 1305 c.backend = c.region.backend
1294 1306 c.user_keys = sorted(c.region.backend.list_keys(prefix=cache_namespace_uid))
1295 1307
1296 1308 return self._get_template_context(c)
1297 1309
1298 1310 @LoginRequired()
1299 1311 @HasPermissionAllDecorator('hg.admin')
1300 1312 @CSRFRequired()
1301 1313 @view_config(
1302 1314 route_name='edit_user_caches_update', request_method='POST')
1303 1315 def user_caches_update(self):
1304 1316 _ = self.request.translate
1305 1317 c = self.load_default_context()
1306 1318 c.user = self.db_user
1307 1319
1308 1320 c.active = 'caches'
1309 1321 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1310 1322
1311 1323 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1312 1324 del_keys = rc_cache.clear_cache_namespace('cache_perms', cache_namespace_uid)
1313 1325
1314 1326 h.flash(_("Deleted {} cache keys").format(del_keys), category='success')
1315 1327
1316 1328 return HTTPFound(h.route_path(
1317 1329 'edit_user_caches', user_id=c.user.user_id))
@@ -1,171 +1,175 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 Set of custom exceptions used in RhodeCode
23 23 """
24 24
25 25 from webob.exc import HTTPClientError
26 26 from pyramid.httpexceptions import HTTPBadGateway
27 27
28 28
29 29 class LdapUsernameError(Exception):
30 30 pass
31 31
32 32
33 33 class LdapPasswordError(Exception):
34 34 pass
35 35
36 36
37 37 class LdapConnectionError(Exception):
38 38 pass
39 39
40 40
41 41 class LdapImportError(Exception):
42 42 pass
43 43
44 44
45 45 class DefaultUserException(Exception):
46 46 pass
47 47
48 48
49 49 class UserOwnsReposException(Exception):
50 50 pass
51 51
52 52
53 53 class UserOwnsRepoGroupsException(Exception):
54 54 pass
55 55
56 56
57 57 class UserOwnsUserGroupsException(Exception):
58 58 pass
59 59
60 60
61 class UserOwnsArtifactsException(Exception):
62 pass
63
64
61 65 class UserGroupAssignedException(Exception):
62 66 pass
63 67
64 68
65 69 class StatusChangeOnClosedPullRequestError(Exception):
66 70 pass
67 71
68 72
69 73 class AttachedForksError(Exception):
70 74 pass
71 75
72 76
73 77 class AttachedPullRequestsError(Exception):
74 78 pass
75 79
76 80
77 81 class RepoGroupAssignmentError(Exception):
78 82 pass
79 83
80 84
81 85 class NonRelativePathError(Exception):
82 86 pass
83 87
84 88
85 89 class HTTPRequirementError(HTTPClientError):
86 90 title = explanation = 'Repository Requirement Missing'
87 91 reason = None
88 92
89 93 def __init__(self, message, *args, **kwargs):
90 94 self.title = self.explanation = message
91 95 super(HTTPRequirementError, self).__init__(*args, **kwargs)
92 96 self.args = (message, )
93 97
94 98
95 99 class HTTPLockedRC(HTTPClientError):
96 100 """
97 101 Special Exception For locked Repos in RhodeCode, the return code can
98 102 be overwritten by _code keyword argument passed into constructors
99 103 """
100 104 code = 423
101 105 title = explanation = 'Repository Locked'
102 106 reason = None
103 107
104 108 def __init__(self, message, *args, **kwargs):
105 109 from rhodecode import CONFIG
106 110 from rhodecode.lib.utils2 import safe_int
107 111 _code = CONFIG.get('lock_ret_code')
108 112 self.code = safe_int(_code, self.code)
109 113 self.title = self.explanation = message
110 114 super(HTTPLockedRC, self).__init__(*args, **kwargs)
111 115 self.args = (message, )
112 116
113 117
114 118 class HTTPBranchProtected(HTTPClientError):
115 119 """
116 120 Special Exception For Indicating that branch is protected in RhodeCode, the
117 121 return code can be overwritten by _code keyword argument passed into constructors
118 122 """
119 123 code = 403
120 124 title = explanation = 'Branch Protected'
121 125 reason = None
122 126
123 127 def __init__(self, message, *args, **kwargs):
124 128 self.title = self.explanation = message
125 129 super(HTTPBranchProtected, self).__init__(*args, **kwargs)
126 130 self.args = (message, )
127 131
128 132
129 133 class IMCCommitError(Exception):
130 134 pass
131 135
132 136
133 137 class UserCreationError(Exception):
134 138 pass
135 139
136 140
137 141 class NotAllowedToCreateUserError(Exception):
138 142 pass
139 143
140 144
141 145 class RepositoryCreationError(Exception):
142 146 pass
143 147
144 148
145 149 class VCSServerUnavailable(HTTPBadGateway):
146 150 """ HTTP Exception class for VCS Server errors """
147 151 code = 502
148 152 title = 'VCS Server Error'
149 153 causes = [
150 154 'VCS Server is not running',
151 155 'Incorrect vcs.server=host:port',
152 156 'Incorrect vcs.server.protocol',
153 157 ]
154 158
155 159 def __init__(self, message=''):
156 160 self.explanation = 'Could not connect to VCS Server'
157 161 if message:
158 162 self.explanation += ': ' + message
159 163 super(VCSServerUnavailable, self).__init__()
160 164
161 165
162 166 class ArtifactMetadataDuplicate(ValueError):
163 167
164 168 def __init__(self, *args, **kwargs):
165 169 self.err_section = kwargs.pop('err_section', None)
166 170 self.err_key = kwargs.pop('err_key', None)
167 171 super(ArtifactMetadataDuplicate, self).__init__(*args, **kwargs)
168 172
169 173
170 174 class ArtifactMetadataBadValueType(ValueError):
171 175 pass
@@ -1,5422 +1,5429 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 Database Models for RhodeCode Enterprise
23 23 """
24 24
25 25 import re
26 26 import os
27 27 import time
28 28 import string
29 29 import hashlib
30 30 import logging
31 31 import datetime
32 32 import uuid
33 33 import warnings
34 34 import ipaddress
35 35 import functools
36 36 import traceback
37 37 import collections
38 38
39 39 from sqlalchemy import (
40 40 or_, and_, not_, func, TypeDecorator, event,
41 41 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
42 42 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
43 43 Text, Float, PickleType, BigInteger)
44 44 from sqlalchemy.sql.expression import true, false, case
45 45 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
46 46 from sqlalchemy.orm import (
47 47 relationship, joinedload, class_mapper, validates, aliased)
48 48 from sqlalchemy.ext.declarative import declared_attr
49 49 from sqlalchemy.ext.hybrid import hybrid_property
50 50 from sqlalchemy.exc import IntegrityError # pragma: no cover
51 51 from sqlalchemy.dialects.mysql import LONGTEXT
52 52 from zope.cachedescriptors.property import Lazy as LazyProperty
53 53 from pyramid import compat
54 54 from pyramid.threadlocal import get_current_request
55 55 from webhelpers.text import collapse, remove_formatting
56 56
57 57 from rhodecode.translation import _
58 58 from rhodecode.lib.vcs import get_vcs_instance
59 59 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
60 60 from rhodecode.lib.utils2 import (
61 61 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
62 62 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
63 63 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time, OrderedDefaultDict)
64 64 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
65 65 JsonRaw
66 66 from rhodecode.lib.ext_json import json
67 67 from rhodecode.lib.caching_query import FromCache
68 68 from rhodecode.lib.encrypt import AESCipher, validate_and_get_enc_data
69 69 from rhodecode.lib.encrypt2 import Encryptor
70 70 from rhodecode.lib.exceptions import (
71 71 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
72 72 from rhodecode.model.meta import Base, Session
73 73
74 74 URL_SEP = '/'
75 75 log = logging.getLogger(__name__)
76 76
77 77 # =============================================================================
78 78 # BASE CLASSES
79 79 # =============================================================================
80 80
81 81 # this is propagated from .ini file rhodecode.encrypted_values.secret or
82 82 # beaker.session.secret if first is not set.
83 83 # and initialized at environment.py
84 84 ENCRYPTION_KEY = None
85 85
86 86 # used to sort permissions by types, '#' used here is not allowed to be in
87 87 # usernames, and it's very early in sorted string.printable table.
88 88 PERMISSION_TYPE_SORT = {
89 89 'admin': '####',
90 90 'write': '###',
91 91 'read': '##',
92 92 'none': '#',
93 93 }
94 94
95 95
96 96 def display_user_sort(obj):
97 97 """
98 98 Sort function used to sort permissions in .permissions() function of
99 99 Repository, RepoGroup, UserGroup. Also it put the default user in front
100 100 of all other resources
101 101 """
102 102
103 103 if obj.username == User.DEFAULT_USER:
104 104 return '#####'
105 105 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
106 106 return prefix + obj.username
107 107
108 108
109 109 def display_user_group_sort(obj):
110 110 """
111 111 Sort function used to sort permissions in .permissions() function of
112 112 Repository, RepoGroup, UserGroup. Also it put the default user in front
113 113 of all other resources
114 114 """
115 115
116 116 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
117 117 return prefix + obj.users_group_name
118 118
119 119
120 120 def _hash_key(k):
121 121 return sha1_safe(k)
122 122
123 123
124 124 def in_filter_generator(qry, items, limit=500):
125 125 """
126 126 Splits IN() into multiple with OR
127 127 e.g.::
128 128 cnt = Repository.query().filter(
129 129 or_(
130 130 *in_filter_generator(Repository.repo_id, range(100000))
131 131 )).count()
132 132 """
133 133 if not items:
134 134 # empty list will cause empty query which might cause security issues
135 135 # this can lead to hidden unpleasant results
136 136 items = [-1]
137 137
138 138 parts = []
139 139 for chunk in xrange(0, len(items), limit):
140 140 parts.append(
141 141 qry.in_(items[chunk: chunk + limit])
142 142 )
143 143
144 144 return parts
145 145
146 146
147 147 base_table_args = {
148 148 'extend_existing': True,
149 149 'mysql_engine': 'InnoDB',
150 150 'mysql_charset': 'utf8',
151 151 'sqlite_autoincrement': True
152 152 }
153 153
154 154
155 155 class EncryptedTextValue(TypeDecorator):
156 156 """
157 157 Special column for encrypted long text data, use like::
158 158
159 159 value = Column("encrypted_value", EncryptedValue(), nullable=False)
160 160
161 161 This column is intelligent so if value is in unencrypted form it return
162 162 unencrypted form, but on save it always encrypts
163 163 """
164 164 impl = Text
165 165
166 166 def process_bind_param(self, value, dialect):
167 167 """
168 168 Setter for storing value
169 169 """
170 170 import rhodecode
171 171 if not value:
172 172 return value
173 173
174 174 # protect against double encrypting if values is already encrypted
175 175 if value.startswith('enc$aes$') \
176 176 or value.startswith('enc$aes_hmac$') \
177 177 or value.startswith('enc2$'):
178 178 raise ValueError('value needs to be in unencrypted format, '
179 179 'ie. not starting with enc$ or enc2$')
180 180
181 181 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
182 182 if algo == 'aes':
183 183 return 'enc$aes_hmac$%s' % AESCipher(ENCRYPTION_KEY, hmac=True).encrypt(value)
184 184 elif algo == 'fernet':
185 185 return Encryptor(ENCRYPTION_KEY).encrypt(value)
186 186 else:
187 187 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
188 188
189 189 def process_result_value(self, value, dialect):
190 190 """
191 191 Getter for retrieving value
192 192 """
193 193
194 194 import rhodecode
195 195 if not value:
196 196 return value
197 197
198 198 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
199 199 enc_strict_mode = str2bool(rhodecode.CONFIG.get('rhodecode.encrypted_values.strict') or True)
200 200 if algo == 'aes':
201 201 decrypted_data = validate_and_get_enc_data(value, ENCRYPTION_KEY, enc_strict_mode)
202 202 elif algo == 'fernet':
203 203 return Encryptor(ENCRYPTION_KEY).decrypt(value)
204 204 else:
205 205 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
206 206 return decrypted_data
207 207
208 208
209 209 class BaseModel(object):
210 210 """
211 211 Base Model for all classes
212 212 """
213 213
214 214 @classmethod
215 215 def _get_keys(cls):
216 216 """return column names for this model """
217 217 return class_mapper(cls).c.keys()
218 218
219 219 def get_dict(self):
220 220 """
221 221 return dict with keys and values corresponding
222 222 to this model data """
223 223
224 224 d = {}
225 225 for k in self._get_keys():
226 226 d[k] = getattr(self, k)
227 227
228 228 # also use __json__() if present to get additional fields
229 229 _json_attr = getattr(self, '__json__', None)
230 230 if _json_attr:
231 231 # update with attributes from __json__
232 232 if callable(_json_attr):
233 233 _json_attr = _json_attr()
234 234 for k, val in _json_attr.iteritems():
235 235 d[k] = val
236 236 return d
237 237
238 238 def get_appstruct(self):
239 239 """return list with keys and values tuples corresponding
240 240 to this model data """
241 241
242 242 lst = []
243 243 for k in self._get_keys():
244 244 lst.append((k, getattr(self, k),))
245 245 return lst
246 246
247 247 def populate_obj(self, populate_dict):
248 248 """populate model with data from given populate_dict"""
249 249
250 250 for k in self._get_keys():
251 251 if k in populate_dict:
252 252 setattr(self, k, populate_dict[k])
253 253
254 254 @classmethod
255 255 def query(cls):
256 256 return Session().query(cls)
257 257
258 258 @classmethod
259 259 def get(cls, id_):
260 260 if id_:
261 261 return cls.query().get(id_)
262 262
263 263 @classmethod
264 264 def get_or_404(cls, id_):
265 265 from pyramid.httpexceptions import HTTPNotFound
266 266
267 267 try:
268 268 id_ = int(id_)
269 269 except (TypeError, ValueError):
270 270 raise HTTPNotFound()
271 271
272 272 res = cls.query().get(id_)
273 273 if not res:
274 274 raise HTTPNotFound()
275 275 return res
276 276
277 277 @classmethod
278 278 def getAll(cls):
279 279 # deprecated and left for backward compatibility
280 280 return cls.get_all()
281 281
282 282 @classmethod
283 283 def get_all(cls):
284 284 return cls.query().all()
285 285
286 286 @classmethod
287 287 def delete(cls, id_):
288 288 obj = cls.query().get(id_)
289 289 Session().delete(obj)
290 290
291 291 @classmethod
292 292 def identity_cache(cls, session, attr_name, value):
293 293 exist_in_session = []
294 294 for (item_cls, pkey), instance in session.identity_map.items():
295 295 if cls == item_cls and getattr(instance, attr_name) == value:
296 296 exist_in_session.append(instance)
297 297 if exist_in_session:
298 298 if len(exist_in_session) == 1:
299 299 return exist_in_session[0]
300 300 log.exception(
301 301 'multiple objects with attr %s and '
302 302 'value %s found with same name: %r',
303 303 attr_name, value, exist_in_session)
304 304
305 305 def __repr__(self):
306 306 if hasattr(self, '__unicode__'):
307 307 # python repr needs to return str
308 308 try:
309 309 return safe_str(self.__unicode__())
310 310 except UnicodeDecodeError:
311 311 pass
312 312 return '<DB:%s>' % (self.__class__.__name__)
313 313
314 314
315 315 class RhodeCodeSetting(Base, BaseModel):
316 316 __tablename__ = 'rhodecode_settings'
317 317 __table_args__ = (
318 318 UniqueConstraint('app_settings_name'),
319 319 base_table_args
320 320 )
321 321
322 322 SETTINGS_TYPES = {
323 323 'str': safe_str,
324 324 'int': safe_int,
325 325 'unicode': safe_unicode,
326 326 'bool': str2bool,
327 327 'list': functools.partial(aslist, sep=',')
328 328 }
329 329 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
330 330 GLOBAL_CONF_KEY = 'app_settings'
331 331
332 332 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
333 333 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
334 334 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
335 335 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
336 336
337 337 def __init__(self, key='', val='', type='unicode'):
338 338 self.app_settings_name = key
339 339 self.app_settings_type = type
340 340 self.app_settings_value = val
341 341
342 342 @validates('_app_settings_value')
343 343 def validate_settings_value(self, key, val):
344 344 assert type(val) == unicode
345 345 return val
346 346
347 347 @hybrid_property
348 348 def app_settings_value(self):
349 349 v = self._app_settings_value
350 350 _type = self.app_settings_type
351 351 if _type:
352 352 _type = self.app_settings_type.split('.')[0]
353 353 # decode the encrypted value
354 354 if 'encrypted' in self.app_settings_type:
355 355 cipher = EncryptedTextValue()
356 356 v = safe_unicode(cipher.process_result_value(v, None))
357 357
358 358 converter = self.SETTINGS_TYPES.get(_type) or \
359 359 self.SETTINGS_TYPES['unicode']
360 360 return converter(v)
361 361
362 362 @app_settings_value.setter
363 363 def app_settings_value(self, val):
364 364 """
365 365 Setter that will always make sure we use unicode in app_settings_value
366 366
367 367 :param val:
368 368 """
369 369 val = safe_unicode(val)
370 370 # encode the encrypted value
371 371 if 'encrypted' in self.app_settings_type:
372 372 cipher = EncryptedTextValue()
373 373 val = safe_unicode(cipher.process_bind_param(val, None))
374 374 self._app_settings_value = val
375 375
376 376 @hybrid_property
377 377 def app_settings_type(self):
378 378 return self._app_settings_type
379 379
380 380 @app_settings_type.setter
381 381 def app_settings_type(self, val):
382 382 if val.split('.')[0] not in self.SETTINGS_TYPES:
383 383 raise Exception('type must be one of %s got %s'
384 384 % (self.SETTINGS_TYPES.keys(), val))
385 385 self._app_settings_type = val
386 386
387 387 @classmethod
388 388 def get_by_prefix(cls, prefix):
389 389 return RhodeCodeSetting.query()\
390 390 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
391 391 .all()
392 392
393 393 def __unicode__(self):
394 394 return u"<%s('%s:%s[%s]')>" % (
395 395 self.__class__.__name__,
396 396 self.app_settings_name, self.app_settings_value,
397 397 self.app_settings_type
398 398 )
399 399
400 400
401 401 class RhodeCodeUi(Base, BaseModel):
402 402 __tablename__ = 'rhodecode_ui'
403 403 __table_args__ = (
404 404 UniqueConstraint('ui_key'),
405 405 base_table_args
406 406 )
407 407
408 408 HOOK_REPO_SIZE = 'changegroup.repo_size'
409 409 # HG
410 410 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
411 411 HOOK_PULL = 'outgoing.pull_logger'
412 412 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
413 413 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
414 414 HOOK_PUSH = 'changegroup.push_logger'
415 415 HOOK_PUSH_KEY = 'pushkey.key_push'
416 416
417 417 HOOKS_BUILTIN = [
418 418 HOOK_PRE_PULL,
419 419 HOOK_PULL,
420 420 HOOK_PRE_PUSH,
421 421 HOOK_PRETX_PUSH,
422 422 HOOK_PUSH,
423 423 HOOK_PUSH_KEY,
424 424 ]
425 425
426 426 # TODO: johbo: Unify way how hooks are configured for git and hg,
427 427 # git part is currently hardcoded.
428 428
429 429 # SVN PATTERNS
430 430 SVN_BRANCH_ID = 'vcs_svn_branch'
431 431 SVN_TAG_ID = 'vcs_svn_tag'
432 432
433 433 ui_id = Column(
434 434 "ui_id", Integer(), nullable=False, unique=True, default=None,
435 435 primary_key=True)
436 436 ui_section = Column(
437 437 "ui_section", String(255), nullable=True, unique=None, default=None)
438 438 ui_key = Column(
439 439 "ui_key", String(255), nullable=True, unique=None, default=None)
440 440 ui_value = Column(
441 441 "ui_value", String(255), nullable=True, unique=None, default=None)
442 442 ui_active = Column(
443 443 "ui_active", Boolean(), nullable=True, unique=None, default=True)
444 444
445 445 def __repr__(self):
446 446 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
447 447 self.ui_key, self.ui_value)
448 448
449 449
450 450 class RepoRhodeCodeSetting(Base, BaseModel):
451 451 __tablename__ = 'repo_rhodecode_settings'
452 452 __table_args__ = (
453 453 UniqueConstraint(
454 454 'app_settings_name', 'repository_id',
455 455 name='uq_repo_rhodecode_setting_name_repo_id'),
456 456 base_table_args
457 457 )
458 458
459 459 repository_id = Column(
460 460 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
461 461 nullable=False)
462 462 app_settings_id = Column(
463 463 "app_settings_id", Integer(), nullable=False, unique=True,
464 464 default=None, primary_key=True)
465 465 app_settings_name = Column(
466 466 "app_settings_name", String(255), nullable=True, unique=None,
467 467 default=None)
468 468 _app_settings_value = Column(
469 469 "app_settings_value", String(4096), nullable=True, unique=None,
470 470 default=None)
471 471 _app_settings_type = Column(
472 472 "app_settings_type", String(255), nullable=True, unique=None,
473 473 default=None)
474 474
475 475 repository = relationship('Repository')
476 476
477 477 def __init__(self, repository_id, key='', val='', type='unicode'):
478 478 self.repository_id = repository_id
479 479 self.app_settings_name = key
480 480 self.app_settings_type = type
481 481 self.app_settings_value = val
482 482
483 483 @validates('_app_settings_value')
484 484 def validate_settings_value(self, key, val):
485 485 assert type(val) == unicode
486 486 return val
487 487
488 488 @hybrid_property
489 489 def app_settings_value(self):
490 490 v = self._app_settings_value
491 491 type_ = self.app_settings_type
492 492 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
493 493 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
494 494 return converter(v)
495 495
496 496 @app_settings_value.setter
497 497 def app_settings_value(self, val):
498 498 """
499 499 Setter that will always make sure we use unicode in app_settings_value
500 500
501 501 :param val:
502 502 """
503 503 self._app_settings_value = safe_unicode(val)
504 504
505 505 @hybrid_property
506 506 def app_settings_type(self):
507 507 return self._app_settings_type
508 508
509 509 @app_settings_type.setter
510 510 def app_settings_type(self, val):
511 511 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
512 512 if val not in SETTINGS_TYPES:
513 513 raise Exception('type must be one of %s got %s'
514 514 % (SETTINGS_TYPES.keys(), val))
515 515 self._app_settings_type = val
516 516
517 517 def __unicode__(self):
518 518 return u"<%s('%s:%s:%s[%s]')>" % (
519 519 self.__class__.__name__, self.repository.repo_name,
520 520 self.app_settings_name, self.app_settings_value,
521 521 self.app_settings_type
522 522 )
523 523
524 524
525 525 class RepoRhodeCodeUi(Base, BaseModel):
526 526 __tablename__ = 'repo_rhodecode_ui'
527 527 __table_args__ = (
528 528 UniqueConstraint(
529 529 'repository_id', 'ui_section', 'ui_key',
530 530 name='uq_repo_rhodecode_ui_repository_id_section_key'),
531 531 base_table_args
532 532 )
533 533
534 534 repository_id = Column(
535 535 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
536 536 nullable=False)
537 537 ui_id = Column(
538 538 "ui_id", Integer(), nullable=False, unique=True, default=None,
539 539 primary_key=True)
540 540 ui_section = Column(
541 541 "ui_section", String(255), nullable=True, unique=None, default=None)
542 542 ui_key = Column(
543 543 "ui_key", String(255), nullable=True, unique=None, default=None)
544 544 ui_value = Column(
545 545 "ui_value", String(255), nullable=True, unique=None, default=None)
546 546 ui_active = Column(
547 547 "ui_active", Boolean(), nullable=True, unique=None, default=True)
548 548
549 549 repository = relationship('Repository')
550 550
551 551 def __repr__(self):
552 552 return '<%s[%s:%s]%s=>%s]>' % (
553 553 self.__class__.__name__, self.repository.repo_name,
554 554 self.ui_section, self.ui_key, self.ui_value)
555 555
556 556
557 557 class User(Base, BaseModel):
558 558 __tablename__ = 'users'
559 559 __table_args__ = (
560 560 UniqueConstraint('username'), UniqueConstraint('email'),
561 561 Index('u_username_idx', 'username'),
562 562 Index('u_email_idx', 'email'),
563 563 base_table_args
564 564 )
565 565
566 566 DEFAULT_USER = 'default'
567 567 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
568 568 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
569 569
570 570 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
571 571 username = Column("username", String(255), nullable=True, unique=None, default=None)
572 572 password = Column("password", String(255), nullable=True, unique=None, default=None)
573 573 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
574 574 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
575 575 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
576 576 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
577 577 _email = Column("email", String(255), nullable=True, unique=None, default=None)
578 578 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
579 579 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
580 580
581 581 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
582 582 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
583 583 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
584 584 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
585 585 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
586 586 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
587 587
588 588 user_log = relationship('UserLog')
589 589 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
590 590
591 591 repositories = relationship('Repository')
592 592 repository_groups = relationship('RepoGroup')
593 593 user_groups = relationship('UserGroup')
594 594
595 595 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
596 596 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
597 597
598 598 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
599 599 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
600 600 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
601 601
602 602 group_member = relationship('UserGroupMember', cascade='all')
603 603
604 604 notifications = relationship('UserNotification', cascade='all')
605 605 # notifications assigned to this user
606 606 user_created_notifications = relationship('Notification', cascade='all')
607 607 # comments created by this user
608 608 user_comments = relationship('ChangesetComment', cascade='all')
609 609 # user profile extra info
610 610 user_emails = relationship('UserEmailMap', cascade='all')
611 611 user_ip_map = relationship('UserIpMap', cascade='all')
612 612 user_auth_tokens = relationship('UserApiKeys', cascade='all')
613 613 user_ssh_keys = relationship('UserSshKeys', cascade='all')
614 614
615 615 # gists
616 616 user_gists = relationship('Gist', cascade='all')
617 617 # user pull requests
618 618 user_pull_requests = relationship('PullRequest', cascade='all')
619 619 # external identities
620 extenal_identities = relationship(
620 external_identities = relationship(
621 621 'ExternalIdentity',
622 622 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
623 623 cascade='all')
624 624 # review rules
625 625 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
626 626
627 # artifacts owned
628 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id')
629
630 # no cascade, set NULL
631 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id')
632
627 633 def __unicode__(self):
628 634 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
629 635 self.user_id, self.username)
630 636
631 637 @hybrid_property
632 638 def email(self):
633 639 return self._email
634 640
635 641 @email.setter
636 642 def email(self, val):
637 643 self._email = val.lower() if val else None
638 644
639 645 @hybrid_property
640 646 def first_name(self):
641 647 from rhodecode.lib import helpers as h
642 648 if self.name:
643 649 return h.escape(self.name)
644 650 return self.name
645 651
646 652 @hybrid_property
647 653 def last_name(self):
648 654 from rhodecode.lib import helpers as h
649 655 if self.lastname:
650 656 return h.escape(self.lastname)
651 657 return self.lastname
652 658
653 659 @hybrid_property
654 660 def api_key(self):
655 661 """
656 662 Fetch if exist an auth-token with role ALL connected to this user
657 663 """
658 664 user_auth_token = UserApiKeys.query()\
659 665 .filter(UserApiKeys.user_id == self.user_id)\
660 666 .filter(or_(UserApiKeys.expires == -1,
661 667 UserApiKeys.expires >= time.time()))\
662 668 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
663 669 if user_auth_token:
664 670 user_auth_token = user_auth_token.api_key
665 671
666 672 return user_auth_token
667 673
668 674 @api_key.setter
669 675 def api_key(self, val):
670 676 # don't allow to set API key this is deprecated for now
671 677 self._api_key = None
672 678
673 679 @property
674 680 def reviewer_pull_requests(self):
675 681 return PullRequestReviewers.query() \
676 682 .options(joinedload(PullRequestReviewers.pull_request)) \
677 683 .filter(PullRequestReviewers.user_id == self.user_id) \
678 684 .all()
679 685
680 686 @property
681 687 def firstname(self):
682 688 # alias for future
683 689 return self.name
684 690
685 691 @property
686 692 def emails(self):
687 693 other = UserEmailMap.query()\
688 694 .filter(UserEmailMap.user == self) \
689 695 .order_by(UserEmailMap.email_id.asc()) \
690 696 .all()
691 697 return [self.email] + [x.email for x in other]
692 698
693 699 @property
694 700 def auth_tokens(self):
695 701 auth_tokens = self.get_auth_tokens()
696 702 return [x.api_key for x in auth_tokens]
697 703
698 704 def get_auth_tokens(self):
699 705 return UserApiKeys.query()\
700 706 .filter(UserApiKeys.user == self)\
701 707 .order_by(UserApiKeys.user_api_key_id.asc())\
702 708 .all()
703 709
704 710 @LazyProperty
705 711 def feed_token(self):
706 712 return self.get_feed_token()
707 713
708 714 def get_feed_token(self, cache=True):
709 715 feed_tokens = UserApiKeys.query()\
710 716 .filter(UserApiKeys.user == self)\
711 717 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
712 718 if cache:
713 719 feed_tokens = feed_tokens.options(
714 720 FromCache("sql_cache_short", "get_user_feed_token_%s" % self.user_id))
715 721
716 722 feed_tokens = feed_tokens.all()
717 723 if feed_tokens:
718 724 return feed_tokens[0].api_key
719 725 return 'NO_FEED_TOKEN_AVAILABLE'
720 726
721 727 @LazyProperty
722 728 def artifact_token(self):
723 729 return self.get_artifact_token()
724 730
725 731 def get_artifact_token(self, cache=True):
726 732 artifacts_tokens = UserApiKeys.query()\
727 733 .filter(UserApiKeys.user == self)\
728 734 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
729 735 if cache:
730 736 artifacts_tokens = artifacts_tokens.options(
731 737 FromCache("sql_cache_short", "get_user_artifact_token_%s" % self.user_id))
732 738
733 739 artifacts_tokens = artifacts_tokens.all()
734 740 if artifacts_tokens:
735 741 return artifacts_tokens[0].api_key
736 742 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
737 743
738 744 @classmethod
739 745 def get(cls, user_id, cache=False):
740 746 if not user_id:
741 747 return
742 748
743 749 user = cls.query()
744 750 if cache:
745 751 user = user.options(
746 752 FromCache("sql_cache_short", "get_users_%s" % user_id))
747 753 return user.get(user_id)
748 754
749 755 @classmethod
750 756 def extra_valid_auth_tokens(cls, user, role=None):
751 757 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
752 758 .filter(or_(UserApiKeys.expires == -1,
753 759 UserApiKeys.expires >= time.time()))
754 760 if role:
755 761 tokens = tokens.filter(or_(UserApiKeys.role == role,
756 762 UserApiKeys.role == UserApiKeys.ROLE_ALL))
757 763 return tokens.all()
758 764
759 765 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
760 766 from rhodecode.lib import auth
761 767
762 768 log.debug('Trying to authenticate user: %s via auth-token, '
763 769 'and roles: %s', self, roles)
764 770
765 771 if not auth_token:
766 772 return False
767 773
768 774 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
769 775 tokens_q = UserApiKeys.query()\
770 776 .filter(UserApiKeys.user_id == self.user_id)\
771 777 .filter(or_(UserApiKeys.expires == -1,
772 778 UserApiKeys.expires >= time.time()))
773 779
774 780 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
775 781
776 782 crypto_backend = auth.crypto_backend()
777 783 enc_token_map = {}
778 784 plain_token_map = {}
779 785 for token in tokens_q:
780 786 if token.api_key.startswith(crypto_backend.ENC_PREF):
781 787 enc_token_map[token.api_key] = token
782 788 else:
783 789 plain_token_map[token.api_key] = token
784 790 log.debug(
785 791 'Found %s plain and %s encrypted tokens to check for authentication for this user',
786 792 len(plain_token_map), len(enc_token_map))
787 793
788 794 # plain token match comes first
789 795 match = plain_token_map.get(auth_token)
790 796
791 797 # check encrypted tokens now
792 798 if not match:
793 799 for token_hash, token in enc_token_map.items():
794 800 # NOTE(marcink): this is expensive to calculate, but most secure
795 801 if crypto_backend.hash_check(auth_token, token_hash):
796 802 match = token
797 803 break
798 804
799 805 if match:
800 806 log.debug('Found matching token %s', match)
801 807 if match.repo_id:
802 808 log.debug('Found scope, checking for scope match of token %s', match)
803 809 if match.repo_id == scope_repo_id:
804 810 return True
805 811 else:
806 812 log.debug(
807 813 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
808 814 'and calling scope is:%s, skipping further checks',
809 815 match.repo, scope_repo_id)
810 816 return False
811 817 else:
812 818 return True
813 819
814 820 return False
815 821
816 822 @property
817 823 def ip_addresses(self):
818 824 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
819 825 return [x.ip_addr for x in ret]
820 826
821 827 @property
822 828 def username_and_name(self):
823 829 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
824 830
825 831 @property
826 832 def username_or_name_or_email(self):
827 833 full_name = self.full_name if self.full_name is not ' ' else None
828 834 return self.username or full_name or self.email
829 835
830 836 @property
831 837 def full_name(self):
832 838 return '%s %s' % (self.first_name, self.last_name)
833 839
834 840 @property
835 841 def full_name_or_username(self):
836 842 return ('%s %s' % (self.first_name, self.last_name)
837 843 if (self.first_name and self.last_name) else self.username)
838 844
839 845 @property
840 846 def full_contact(self):
841 847 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
842 848
843 849 @property
844 850 def short_contact(self):
845 851 return '%s %s' % (self.first_name, self.last_name)
846 852
847 853 @property
848 854 def is_admin(self):
849 855 return self.admin
850 856
851 857 def AuthUser(self, **kwargs):
852 858 """
853 859 Returns instance of AuthUser for this user
854 860 """
855 861 from rhodecode.lib.auth import AuthUser
856 862 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
857 863
858 864 @hybrid_property
859 865 def user_data(self):
860 866 if not self._user_data:
861 867 return {}
862 868
863 869 try:
864 870 return json.loads(self._user_data)
865 871 except TypeError:
866 872 return {}
867 873
868 874 @user_data.setter
869 875 def user_data(self, val):
870 876 if not isinstance(val, dict):
871 877 raise Exception('user_data must be dict, got %s' % type(val))
872 878 try:
873 879 self._user_data = json.dumps(val)
874 880 except Exception:
875 881 log.error(traceback.format_exc())
876 882
877 883 @classmethod
878 884 def get_by_username(cls, username, case_insensitive=False,
879 885 cache=False, identity_cache=False):
880 886 session = Session()
881 887
882 888 if case_insensitive:
883 889 q = cls.query().filter(
884 890 func.lower(cls.username) == func.lower(username))
885 891 else:
886 892 q = cls.query().filter(cls.username == username)
887 893
888 894 if cache:
889 895 if identity_cache:
890 896 val = cls.identity_cache(session, 'username', username)
891 897 if val:
892 898 return val
893 899 else:
894 900 cache_key = "get_user_by_name_%s" % _hash_key(username)
895 901 q = q.options(
896 902 FromCache("sql_cache_short", cache_key))
897 903
898 904 return q.scalar()
899 905
900 906 @classmethod
901 907 def get_by_auth_token(cls, auth_token, cache=False):
902 908 q = UserApiKeys.query()\
903 909 .filter(UserApiKeys.api_key == auth_token)\
904 910 .filter(or_(UserApiKeys.expires == -1,
905 911 UserApiKeys.expires >= time.time()))
906 912 if cache:
907 913 q = q.options(
908 914 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
909 915
910 916 match = q.first()
911 917 if match:
912 918 return match.user
913 919
914 920 @classmethod
915 921 def get_by_email(cls, email, case_insensitive=False, cache=False):
916 922
917 923 if case_insensitive:
918 924 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
919 925
920 926 else:
921 927 q = cls.query().filter(cls.email == email)
922 928
923 929 email_key = _hash_key(email)
924 930 if cache:
925 931 q = q.options(
926 932 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
927 933
928 934 ret = q.scalar()
929 935 if ret is None:
930 936 q = UserEmailMap.query()
931 937 # try fetching in alternate email map
932 938 if case_insensitive:
933 939 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
934 940 else:
935 941 q = q.filter(UserEmailMap.email == email)
936 942 q = q.options(joinedload(UserEmailMap.user))
937 943 if cache:
938 944 q = q.options(
939 945 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
940 946 ret = getattr(q.scalar(), 'user', None)
941 947
942 948 return ret
943 949
944 950 @classmethod
945 951 def get_from_cs_author(cls, author):
946 952 """
947 953 Tries to get User objects out of commit author string
948 954
949 955 :param author:
950 956 """
951 957 from rhodecode.lib.helpers import email, author_name
952 958 # Valid email in the attribute passed, see if they're in the system
953 959 _email = email(author)
954 960 if _email:
955 961 user = cls.get_by_email(_email, case_insensitive=True)
956 962 if user:
957 963 return user
958 964 # Maybe we can match by username?
959 965 _author = author_name(author)
960 966 user = cls.get_by_username(_author, case_insensitive=True)
961 967 if user:
962 968 return user
963 969
964 970 def update_userdata(self, **kwargs):
965 971 usr = self
966 972 old = usr.user_data
967 973 old.update(**kwargs)
968 974 usr.user_data = old
969 975 Session().add(usr)
970 976 log.debug('updated userdata with %s', kwargs)
971 977
972 978 def update_lastlogin(self):
973 979 """Update user lastlogin"""
974 980 self.last_login = datetime.datetime.now()
975 981 Session().add(self)
976 982 log.debug('updated user %s lastlogin', self.username)
977 983
978 984 def update_password(self, new_password):
979 985 from rhodecode.lib.auth import get_crypt_password
980 986
981 987 self.password = get_crypt_password(new_password)
982 988 Session().add(self)
983 989
984 990 @classmethod
985 991 def get_first_super_admin(cls):
986 992 user = User.query()\
987 993 .filter(User.admin == true()) \
988 994 .order_by(User.user_id.asc()) \
989 995 .first()
990 996
991 997 if user is None:
992 998 raise Exception('FATAL: Missing administrative account!')
993 999 return user
994 1000
995 1001 @classmethod
996 1002 def get_all_super_admins(cls, only_active=False):
997 1003 """
998 1004 Returns all admin accounts sorted by username
999 1005 """
1000 1006 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1001 1007 if only_active:
1002 1008 qry = qry.filter(User.active == true())
1003 1009 return qry.all()
1004 1010
1005 1011 @classmethod
1006 1012 def get_default_user(cls, cache=False, refresh=False):
1007 1013 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1008 1014 if user is None:
1009 1015 raise Exception('FATAL: Missing default account!')
1010 1016 if refresh:
1011 1017 # The default user might be based on outdated state which
1012 1018 # has been loaded from the cache.
1013 1019 # A call to refresh() ensures that the
1014 1020 # latest state from the database is used.
1015 1021 Session().refresh(user)
1016 1022 return user
1017 1023
1018 1024 def _get_default_perms(self, user, suffix=''):
1019 1025 from rhodecode.model.permission import PermissionModel
1020 1026 return PermissionModel().get_default_perms(user.user_perms, suffix)
1021 1027
1022 1028 def get_default_perms(self, suffix=''):
1023 1029 return self._get_default_perms(self, suffix)
1024 1030
1025 1031 def get_api_data(self, include_secrets=False, details='full'):
1026 1032 """
1027 1033 Common function for generating user related data for API
1028 1034
1029 1035 :param include_secrets: By default secrets in the API data will be replaced
1030 1036 by a placeholder value to prevent exposing this data by accident. In case
1031 1037 this data shall be exposed, set this flag to ``True``.
1032 1038
1033 1039 :param details: details can be 'basic|full' basic gives only a subset of
1034 1040 the available user information that includes user_id, name and emails.
1035 1041 """
1036 1042 user = self
1037 1043 user_data = self.user_data
1038 1044 data = {
1039 1045 'user_id': user.user_id,
1040 1046 'username': user.username,
1041 1047 'firstname': user.name,
1042 1048 'lastname': user.lastname,
1043 1049 'email': user.email,
1044 1050 'emails': user.emails,
1045 1051 }
1046 1052 if details == 'basic':
1047 1053 return data
1048 1054
1049 1055 auth_token_length = 40
1050 1056 auth_token_replacement = '*' * auth_token_length
1051 1057
1052 1058 extras = {
1053 1059 'auth_tokens': [auth_token_replacement],
1054 1060 'active': user.active,
1055 1061 'admin': user.admin,
1056 1062 'extern_type': user.extern_type,
1057 1063 'extern_name': user.extern_name,
1058 1064 'last_login': user.last_login,
1059 1065 'last_activity': user.last_activity,
1060 1066 'ip_addresses': user.ip_addresses,
1061 1067 'language': user_data.get('language')
1062 1068 }
1063 1069 data.update(extras)
1064 1070
1065 1071 if include_secrets:
1066 1072 data['auth_tokens'] = user.auth_tokens
1067 1073 return data
1068 1074
1069 1075 def __json__(self):
1070 1076 data = {
1071 1077 'full_name': self.full_name,
1072 1078 'full_name_or_username': self.full_name_or_username,
1073 1079 'short_contact': self.short_contact,
1074 1080 'full_contact': self.full_contact,
1075 1081 }
1076 1082 data.update(self.get_api_data())
1077 1083 return data
1078 1084
1079 1085
1080 1086 class UserApiKeys(Base, BaseModel):
1081 1087 __tablename__ = 'user_api_keys'
1082 1088 __table_args__ = (
1083 1089 Index('uak_api_key_idx', 'api_key'),
1084 1090 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1085 1091 base_table_args
1086 1092 )
1087 1093 __mapper_args__ = {}
1088 1094
1089 1095 # ApiKey role
1090 1096 ROLE_ALL = 'token_role_all'
1091 1097 ROLE_HTTP = 'token_role_http'
1092 1098 ROLE_VCS = 'token_role_vcs'
1093 1099 ROLE_API = 'token_role_api'
1094 1100 ROLE_FEED = 'token_role_feed'
1095 1101 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1096 1102 ROLE_PASSWORD_RESET = 'token_password_reset'
1097 1103
1098 1104 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1099 1105
1100 1106 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1101 1107 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1102 1108 api_key = Column("api_key", String(255), nullable=False, unique=True)
1103 1109 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1104 1110 expires = Column('expires', Float(53), nullable=False)
1105 1111 role = Column('role', String(255), nullable=True)
1106 1112 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1107 1113
1108 1114 # scope columns
1109 1115 repo_id = Column(
1110 1116 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1111 1117 nullable=True, unique=None, default=None)
1112 1118 repo = relationship('Repository', lazy='joined')
1113 1119
1114 1120 repo_group_id = Column(
1115 1121 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1116 1122 nullable=True, unique=None, default=None)
1117 1123 repo_group = relationship('RepoGroup', lazy='joined')
1118 1124
1119 1125 user = relationship('User', lazy='joined')
1120 1126
1121 1127 def __unicode__(self):
1122 1128 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1123 1129
1124 1130 def __json__(self):
1125 1131 data = {
1126 1132 'auth_token': self.api_key,
1127 1133 'role': self.role,
1128 1134 'scope': self.scope_humanized,
1129 1135 'expired': self.expired
1130 1136 }
1131 1137 return data
1132 1138
1133 1139 def get_api_data(self, include_secrets=False):
1134 1140 data = self.__json__()
1135 1141 if include_secrets:
1136 1142 return data
1137 1143 else:
1138 1144 data['auth_token'] = self.token_obfuscated
1139 1145 return data
1140 1146
1141 1147 @hybrid_property
1142 1148 def description_safe(self):
1143 1149 from rhodecode.lib import helpers as h
1144 1150 return h.escape(self.description)
1145 1151
1146 1152 @property
1147 1153 def expired(self):
1148 1154 if self.expires == -1:
1149 1155 return False
1150 1156 return time.time() > self.expires
1151 1157
1152 1158 @classmethod
1153 1159 def _get_role_name(cls, role):
1154 1160 return {
1155 1161 cls.ROLE_ALL: _('all'),
1156 1162 cls.ROLE_HTTP: _('http/web interface'),
1157 1163 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1158 1164 cls.ROLE_API: _('api calls'),
1159 1165 cls.ROLE_FEED: _('feed access'),
1160 1166 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1161 1167 }.get(role, role)
1162 1168
1163 1169 @property
1164 1170 def role_humanized(self):
1165 1171 return self._get_role_name(self.role)
1166 1172
1167 1173 def _get_scope(self):
1168 1174 if self.repo:
1169 1175 return 'Repository: {}'.format(self.repo.repo_name)
1170 1176 if self.repo_group:
1171 1177 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1172 1178 return 'Global'
1173 1179
1174 1180 @property
1175 1181 def scope_humanized(self):
1176 1182 return self._get_scope()
1177 1183
1178 1184 @property
1179 1185 def token_obfuscated(self):
1180 1186 if self.api_key:
1181 1187 return self.api_key[:4] + "****"
1182 1188
1183 1189
1184 1190 class UserEmailMap(Base, BaseModel):
1185 1191 __tablename__ = 'user_email_map'
1186 1192 __table_args__ = (
1187 1193 Index('uem_email_idx', 'email'),
1188 1194 UniqueConstraint('email'),
1189 1195 base_table_args
1190 1196 )
1191 1197 __mapper_args__ = {}
1192 1198
1193 1199 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1194 1200 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1195 1201 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1196 1202 user = relationship('User', lazy='joined')
1197 1203
1198 1204 @validates('_email')
1199 1205 def validate_email(self, key, email):
1200 1206 # check if this email is not main one
1201 1207 main_email = Session().query(User).filter(User.email == email).scalar()
1202 1208 if main_email is not None:
1203 1209 raise AttributeError('email %s is present is user table' % email)
1204 1210 return email
1205 1211
1206 1212 @hybrid_property
1207 1213 def email(self):
1208 1214 return self._email
1209 1215
1210 1216 @email.setter
1211 1217 def email(self, val):
1212 1218 self._email = val.lower() if val else None
1213 1219
1214 1220
1215 1221 class UserIpMap(Base, BaseModel):
1216 1222 __tablename__ = 'user_ip_map'
1217 1223 __table_args__ = (
1218 1224 UniqueConstraint('user_id', 'ip_addr'),
1219 1225 base_table_args
1220 1226 )
1221 1227 __mapper_args__ = {}
1222 1228
1223 1229 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1224 1230 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1225 1231 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1226 1232 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1227 1233 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1228 1234 user = relationship('User', lazy='joined')
1229 1235
1230 1236 @hybrid_property
1231 1237 def description_safe(self):
1232 1238 from rhodecode.lib import helpers as h
1233 1239 return h.escape(self.description)
1234 1240
1235 1241 @classmethod
1236 1242 def _get_ip_range(cls, ip_addr):
1237 1243 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1238 1244 return [str(net.network_address), str(net.broadcast_address)]
1239 1245
1240 1246 def __json__(self):
1241 1247 return {
1242 1248 'ip_addr': self.ip_addr,
1243 1249 'ip_range': self._get_ip_range(self.ip_addr),
1244 1250 }
1245 1251
1246 1252 def __unicode__(self):
1247 1253 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1248 1254 self.user_id, self.ip_addr)
1249 1255
1250 1256
1251 1257 class UserSshKeys(Base, BaseModel):
1252 1258 __tablename__ = 'user_ssh_keys'
1253 1259 __table_args__ = (
1254 1260 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1255 1261
1256 1262 UniqueConstraint('ssh_key_fingerprint'),
1257 1263
1258 1264 base_table_args
1259 1265 )
1260 1266 __mapper_args__ = {}
1261 1267
1262 1268 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1263 1269 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1264 1270 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1265 1271
1266 1272 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1267 1273
1268 1274 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1269 1275 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1270 1276 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1271 1277
1272 1278 user = relationship('User', lazy='joined')
1273 1279
1274 1280 def __json__(self):
1275 1281 data = {
1276 1282 'ssh_fingerprint': self.ssh_key_fingerprint,
1277 1283 'description': self.description,
1278 1284 'created_on': self.created_on
1279 1285 }
1280 1286 return data
1281 1287
1282 1288 def get_api_data(self):
1283 1289 data = self.__json__()
1284 1290 return data
1285 1291
1286 1292
1287 1293 class UserLog(Base, BaseModel):
1288 1294 __tablename__ = 'user_logs'
1289 1295 __table_args__ = (
1290 1296 base_table_args,
1291 1297 )
1292 1298
1293 1299 VERSION_1 = 'v1'
1294 1300 VERSION_2 = 'v2'
1295 1301 VERSIONS = [VERSION_1, VERSION_2]
1296 1302
1297 1303 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1298 1304 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1299 1305 username = Column("username", String(255), nullable=True, unique=None, default=None)
1300 1306 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1301 1307 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1302 1308 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1303 1309 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1304 1310 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1305 1311
1306 1312 version = Column("version", String(255), nullable=True, default=VERSION_1)
1307 1313 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1308 1314 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1309 1315
1310 1316 def __unicode__(self):
1311 1317 return u"<%s('id:%s:%s')>" % (
1312 1318 self.__class__.__name__, self.repository_name, self.action)
1313 1319
1314 1320 def __json__(self):
1315 1321 return {
1316 1322 'user_id': self.user_id,
1317 1323 'username': self.username,
1318 1324 'repository_id': self.repository_id,
1319 1325 'repository_name': self.repository_name,
1320 1326 'user_ip': self.user_ip,
1321 1327 'action_date': self.action_date,
1322 1328 'action': self.action,
1323 1329 }
1324 1330
1325 1331 @hybrid_property
1326 1332 def entry_id(self):
1327 1333 return self.user_log_id
1328 1334
1329 1335 @property
1330 1336 def action_as_day(self):
1331 1337 return datetime.date(*self.action_date.timetuple()[:3])
1332 1338
1333 1339 user = relationship('User')
1334 1340 repository = relationship('Repository', cascade='')
1335 1341
1336 1342
1337 1343 class UserGroup(Base, BaseModel):
1338 1344 __tablename__ = 'users_groups'
1339 1345 __table_args__ = (
1340 1346 base_table_args,
1341 1347 )
1342 1348
1343 1349 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1344 1350 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1345 1351 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1346 1352 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1347 1353 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1348 1354 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1349 1355 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1350 1356 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1351 1357
1352 1358 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined")
1353 1359 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1354 1360 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1355 1361 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1356 1362 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1357 1363 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1358 1364
1359 1365 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1360 1366 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1361 1367
1362 1368 @classmethod
1363 1369 def _load_group_data(cls, column):
1364 1370 if not column:
1365 1371 return {}
1366 1372
1367 1373 try:
1368 1374 return json.loads(column) or {}
1369 1375 except TypeError:
1370 1376 return {}
1371 1377
1372 1378 @hybrid_property
1373 1379 def description_safe(self):
1374 1380 from rhodecode.lib import helpers as h
1375 1381 return h.escape(self.user_group_description)
1376 1382
1377 1383 @hybrid_property
1378 1384 def group_data(self):
1379 1385 return self._load_group_data(self._group_data)
1380 1386
1381 1387 @group_data.expression
1382 1388 def group_data(self, **kwargs):
1383 1389 return self._group_data
1384 1390
1385 1391 @group_data.setter
1386 1392 def group_data(self, val):
1387 1393 try:
1388 1394 self._group_data = json.dumps(val)
1389 1395 except Exception:
1390 1396 log.error(traceback.format_exc())
1391 1397
1392 1398 @classmethod
1393 1399 def _load_sync(cls, group_data):
1394 1400 if group_data:
1395 1401 return group_data.get('extern_type')
1396 1402
1397 1403 @property
1398 1404 def sync(self):
1399 1405 return self._load_sync(self.group_data)
1400 1406
1401 1407 def __unicode__(self):
1402 1408 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1403 1409 self.users_group_id,
1404 1410 self.users_group_name)
1405 1411
1406 1412 @classmethod
1407 1413 def get_by_group_name(cls, group_name, cache=False,
1408 1414 case_insensitive=False):
1409 1415 if case_insensitive:
1410 1416 q = cls.query().filter(func.lower(cls.users_group_name) ==
1411 1417 func.lower(group_name))
1412 1418
1413 1419 else:
1414 1420 q = cls.query().filter(cls.users_group_name == group_name)
1415 1421 if cache:
1416 1422 q = q.options(
1417 1423 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1418 1424 return q.scalar()
1419 1425
1420 1426 @classmethod
1421 1427 def get(cls, user_group_id, cache=False):
1422 1428 if not user_group_id:
1423 1429 return
1424 1430
1425 1431 user_group = cls.query()
1426 1432 if cache:
1427 1433 user_group = user_group.options(
1428 1434 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1429 1435 return user_group.get(user_group_id)
1430 1436
1431 1437 def permissions(self, with_admins=True, with_owner=True,
1432 1438 expand_from_user_groups=False):
1433 1439 """
1434 1440 Permissions for user groups
1435 1441 """
1436 1442 _admin_perm = 'usergroup.admin'
1437 1443
1438 1444 owner_row = []
1439 1445 if with_owner:
1440 1446 usr = AttributeDict(self.user.get_dict())
1441 1447 usr.owner_row = True
1442 1448 usr.permission = _admin_perm
1443 1449 owner_row.append(usr)
1444 1450
1445 1451 super_admin_ids = []
1446 1452 super_admin_rows = []
1447 1453 if with_admins:
1448 1454 for usr in User.get_all_super_admins():
1449 1455 super_admin_ids.append(usr.user_id)
1450 1456 # if this admin is also owner, don't double the record
1451 1457 if usr.user_id == owner_row[0].user_id:
1452 1458 owner_row[0].admin_row = True
1453 1459 else:
1454 1460 usr = AttributeDict(usr.get_dict())
1455 1461 usr.admin_row = True
1456 1462 usr.permission = _admin_perm
1457 1463 super_admin_rows.append(usr)
1458 1464
1459 1465 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1460 1466 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1461 1467 joinedload(UserUserGroupToPerm.user),
1462 1468 joinedload(UserUserGroupToPerm.permission),)
1463 1469
1464 1470 # get owners and admins and permissions. We do a trick of re-writing
1465 1471 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1466 1472 # has a global reference and changing one object propagates to all
1467 1473 # others. This means if admin is also an owner admin_row that change
1468 1474 # would propagate to both objects
1469 1475 perm_rows = []
1470 1476 for _usr in q.all():
1471 1477 usr = AttributeDict(_usr.user.get_dict())
1472 1478 # if this user is also owner/admin, mark as duplicate record
1473 1479 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1474 1480 usr.duplicate_perm = True
1475 1481 usr.permission = _usr.permission.permission_name
1476 1482 perm_rows.append(usr)
1477 1483
1478 1484 # filter the perm rows by 'default' first and then sort them by
1479 1485 # admin,write,read,none permissions sorted again alphabetically in
1480 1486 # each group
1481 1487 perm_rows = sorted(perm_rows, key=display_user_sort)
1482 1488
1483 1489 user_groups_rows = []
1484 1490 if expand_from_user_groups:
1485 1491 for ug in self.permission_user_groups(with_members=True):
1486 1492 for user_data in ug.members:
1487 1493 user_groups_rows.append(user_data)
1488 1494
1489 1495 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1490 1496
1491 1497 def permission_user_groups(self, with_members=False):
1492 1498 q = UserGroupUserGroupToPerm.query()\
1493 1499 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1494 1500 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1495 1501 joinedload(UserGroupUserGroupToPerm.target_user_group),
1496 1502 joinedload(UserGroupUserGroupToPerm.permission),)
1497 1503
1498 1504 perm_rows = []
1499 1505 for _user_group in q.all():
1500 1506 entry = AttributeDict(_user_group.user_group.get_dict())
1501 1507 entry.permission = _user_group.permission.permission_name
1502 1508 if with_members:
1503 1509 entry.members = [x.user.get_dict()
1504 1510 for x in _user_group.user_group.members]
1505 1511 perm_rows.append(entry)
1506 1512
1507 1513 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1508 1514 return perm_rows
1509 1515
1510 1516 def _get_default_perms(self, user_group, suffix=''):
1511 1517 from rhodecode.model.permission import PermissionModel
1512 1518 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1513 1519
1514 1520 def get_default_perms(self, suffix=''):
1515 1521 return self._get_default_perms(self, suffix)
1516 1522
1517 1523 def get_api_data(self, with_group_members=True, include_secrets=False):
1518 1524 """
1519 1525 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1520 1526 basically forwarded.
1521 1527
1522 1528 """
1523 1529 user_group = self
1524 1530 data = {
1525 1531 'users_group_id': user_group.users_group_id,
1526 1532 'group_name': user_group.users_group_name,
1527 1533 'group_description': user_group.user_group_description,
1528 1534 'active': user_group.users_group_active,
1529 1535 'owner': user_group.user.username,
1530 1536 'sync': user_group.sync,
1531 1537 'owner_email': user_group.user.email,
1532 1538 }
1533 1539
1534 1540 if with_group_members:
1535 1541 users = []
1536 1542 for user in user_group.members:
1537 1543 user = user.user
1538 1544 users.append(user.get_api_data(include_secrets=include_secrets))
1539 1545 data['users'] = users
1540 1546
1541 1547 return data
1542 1548
1543 1549
1544 1550 class UserGroupMember(Base, BaseModel):
1545 1551 __tablename__ = 'users_groups_members'
1546 1552 __table_args__ = (
1547 1553 base_table_args,
1548 1554 )
1549 1555
1550 1556 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1551 1557 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1552 1558 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1553 1559
1554 1560 user = relationship('User', lazy='joined')
1555 1561 users_group = relationship('UserGroup')
1556 1562
1557 1563 def __init__(self, gr_id='', u_id=''):
1558 1564 self.users_group_id = gr_id
1559 1565 self.user_id = u_id
1560 1566
1561 1567
1562 1568 class RepositoryField(Base, BaseModel):
1563 1569 __tablename__ = 'repositories_fields'
1564 1570 __table_args__ = (
1565 1571 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1566 1572 base_table_args,
1567 1573 )
1568 1574
1569 1575 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1570 1576
1571 1577 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1572 1578 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1573 1579 field_key = Column("field_key", String(250))
1574 1580 field_label = Column("field_label", String(1024), nullable=False)
1575 1581 field_value = Column("field_value", String(10000), nullable=False)
1576 1582 field_desc = Column("field_desc", String(1024), nullable=False)
1577 1583 field_type = Column("field_type", String(255), nullable=False, unique=None)
1578 1584 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1579 1585
1580 1586 repository = relationship('Repository')
1581 1587
1582 1588 @property
1583 1589 def field_key_prefixed(self):
1584 1590 return 'ex_%s' % self.field_key
1585 1591
1586 1592 @classmethod
1587 1593 def un_prefix_key(cls, key):
1588 1594 if key.startswith(cls.PREFIX):
1589 1595 return key[len(cls.PREFIX):]
1590 1596 return key
1591 1597
1592 1598 @classmethod
1593 1599 def get_by_key_name(cls, key, repo):
1594 1600 row = cls.query()\
1595 1601 .filter(cls.repository == repo)\
1596 1602 .filter(cls.field_key == key).scalar()
1597 1603 return row
1598 1604
1599 1605
1600 1606 class Repository(Base, BaseModel):
1601 1607 __tablename__ = 'repositories'
1602 1608 __table_args__ = (
1603 1609 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1604 1610 base_table_args,
1605 1611 )
1606 1612 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1607 1613 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1608 1614 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1609 1615
1610 1616 STATE_CREATED = 'repo_state_created'
1611 1617 STATE_PENDING = 'repo_state_pending'
1612 1618 STATE_ERROR = 'repo_state_error'
1613 1619
1614 1620 LOCK_AUTOMATIC = 'lock_auto'
1615 1621 LOCK_API = 'lock_api'
1616 1622 LOCK_WEB = 'lock_web'
1617 1623 LOCK_PULL = 'lock_pull'
1618 1624
1619 1625 NAME_SEP = URL_SEP
1620 1626
1621 1627 repo_id = Column(
1622 1628 "repo_id", Integer(), nullable=False, unique=True, default=None,
1623 1629 primary_key=True)
1624 1630 _repo_name = Column(
1625 1631 "repo_name", Text(), nullable=False, default=None)
1626 1632 _repo_name_hash = Column(
1627 1633 "repo_name_hash", String(255), nullable=False, unique=True)
1628 1634 repo_state = Column("repo_state", String(255), nullable=True)
1629 1635
1630 1636 clone_uri = Column(
1631 1637 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1632 1638 default=None)
1633 1639 push_uri = Column(
1634 1640 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1635 1641 default=None)
1636 1642 repo_type = Column(
1637 1643 "repo_type", String(255), nullable=False, unique=False, default=None)
1638 1644 user_id = Column(
1639 1645 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1640 1646 unique=False, default=None)
1641 1647 private = Column(
1642 1648 "private", Boolean(), nullable=True, unique=None, default=None)
1643 1649 archived = Column(
1644 1650 "archived", Boolean(), nullable=True, unique=None, default=None)
1645 1651 enable_statistics = Column(
1646 1652 "statistics", Boolean(), nullable=True, unique=None, default=True)
1647 1653 enable_downloads = Column(
1648 1654 "downloads", Boolean(), nullable=True, unique=None, default=True)
1649 1655 description = Column(
1650 1656 "description", String(10000), nullable=True, unique=None, default=None)
1651 1657 created_on = Column(
1652 1658 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1653 1659 default=datetime.datetime.now)
1654 1660 updated_on = Column(
1655 1661 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1656 1662 default=datetime.datetime.now)
1657 1663 _landing_revision = Column(
1658 1664 "landing_revision", String(255), nullable=False, unique=False,
1659 1665 default=None)
1660 1666 enable_locking = Column(
1661 1667 "enable_locking", Boolean(), nullable=False, unique=None,
1662 1668 default=False)
1663 1669 _locked = Column(
1664 1670 "locked", String(255), nullable=True, unique=False, default=None)
1665 1671 _changeset_cache = Column(
1666 1672 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1667 1673
1668 1674 fork_id = Column(
1669 1675 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1670 1676 nullable=True, unique=False, default=None)
1671 1677 group_id = Column(
1672 1678 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1673 1679 unique=False, default=None)
1674 1680
1675 1681 user = relationship('User', lazy='joined')
1676 1682 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1677 1683 group = relationship('RepoGroup', lazy='joined')
1678 1684 repo_to_perm = relationship(
1679 1685 'UserRepoToPerm', cascade='all',
1680 1686 order_by='UserRepoToPerm.repo_to_perm_id')
1681 1687 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1682 1688 stats = relationship('Statistics', cascade='all', uselist=False)
1683 1689
1684 1690 followers = relationship(
1685 1691 'UserFollowing',
1686 1692 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1687 1693 cascade='all')
1688 1694 extra_fields = relationship(
1689 1695 'RepositoryField', cascade="all, delete-orphan")
1690 1696 logs = relationship('UserLog')
1691 1697 comments = relationship(
1692 1698 'ChangesetComment', cascade="all, delete-orphan")
1693 1699 pull_requests_source = relationship(
1694 1700 'PullRequest',
1695 1701 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1696 1702 cascade="all, delete-orphan")
1697 1703 pull_requests_target = relationship(
1698 1704 'PullRequest',
1699 1705 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1700 1706 cascade="all, delete-orphan")
1701 1707 ui = relationship('RepoRhodeCodeUi', cascade="all")
1702 1708 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1703 1709 integrations = relationship('Integration', cascade="all, delete-orphan")
1704 1710
1705 1711 scoped_tokens = relationship('UserApiKeys', cascade="all")
1706 1712
1707 artifacts = relationship('FileStore', cascade="all")
1713 # no cascade, set NULL
1714 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id')
1708 1715
1709 1716 def __unicode__(self):
1710 1717 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1711 1718 safe_unicode(self.repo_name))
1712 1719
1713 1720 @hybrid_property
1714 1721 def description_safe(self):
1715 1722 from rhodecode.lib import helpers as h
1716 1723 return h.escape(self.description)
1717 1724
1718 1725 @hybrid_property
1719 1726 def landing_rev(self):
1720 1727 # always should return [rev_type, rev]
1721 1728 if self._landing_revision:
1722 1729 _rev_info = self._landing_revision.split(':')
1723 1730 if len(_rev_info) < 2:
1724 1731 _rev_info.insert(0, 'rev')
1725 1732 return [_rev_info[0], _rev_info[1]]
1726 1733 return [None, None]
1727 1734
1728 1735 @landing_rev.setter
1729 1736 def landing_rev(self, val):
1730 1737 if ':' not in val:
1731 1738 raise ValueError('value must be delimited with `:` and consist '
1732 1739 'of <rev_type>:<rev>, got %s instead' % val)
1733 1740 self._landing_revision = val
1734 1741
1735 1742 @hybrid_property
1736 1743 def locked(self):
1737 1744 if self._locked:
1738 1745 user_id, timelocked, reason = self._locked.split(':')
1739 1746 lock_values = int(user_id), timelocked, reason
1740 1747 else:
1741 1748 lock_values = [None, None, None]
1742 1749 return lock_values
1743 1750
1744 1751 @locked.setter
1745 1752 def locked(self, val):
1746 1753 if val and isinstance(val, (list, tuple)):
1747 1754 self._locked = ':'.join(map(str, val))
1748 1755 else:
1749 1756 self._locked = None
1750 1757
1751 1758 @hybrid_property
1752 1759 def changeset_cache(self):
1753 1760 from rhodecode.lib.vcs.backends.base import EmptyCommit
1754 1761 dummy = EmptyCommit().__json__()
1755 1762 if not self._changeset_cache:
1756 1763 dummy['source_repo_id'] = self.repo_id
1757 1764 return json.loads(json.dumps(dummy))
1758 1765
1759 1766 try:
1760 1767 return json.loads(self._changeset_cache)
1761 1768 except TypeError:
1762 1769 return dummy
1763 1770 except Exception:
1764 1771 log.error(traceback.format_exc())
1765 1772 return dummy
1766 1773
1767 1774 @changeset_cache.setter
1768 1775 def changeset_cache(self, val):
1769 1776 try:
1770 1777 self._changeset_cache = json.dumps(val)
1771 1778 except Exception:
1772 1779 log.error(traceback.format_exc())
1773 1780
1774 1781 @hybrid_property
1775 1782 def repo_name(self):
1776 1783 return self._repo_name
1777 1784
1778 1785 @repo_name.setter
1779 1786 def repo_name(self, value):
1780 1787 self._repo_name = value
1781 1788 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1782 1789
1783 1790 @classmethod
1784 1791 def normalize_repo_name(cls, repo_name):
1785 1792 """
1786 1793 Normalizes os specific repo_name to the format internally stored inside
1787 1794 database using URL_SEP
1788 1795
1789 1796 :param cls:
1790 1797 :param repo_name:
1791 1798 """
1792 1799 return cls.NAME_SEP.join(repo_name.split(os.sep))
1793 1800
1794 1801 @classmethod
1795 1802 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1796 1803 session = Session()
1797 1804 q = session.query(cls).filter(cls.repo_name == repo_name)
1798 1805
1799 1806 if cache:
1800 1807 if identity_cache:
1801 1808 val = cls.identity_cache(session, 'repo_name', repo_name)
1802 1809 if val:
1803 1810 return val
1804 1811 else:
1805 1812 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1806 1813 q = q.options(
1807 1814 FromCache("sql_cache_short", cache_key))
1808 1815
1809 1816 return q.scalar()
1810 1817
1811 1818 @classmethod
1812 1819 def get_by_id_or_repo_name(cls, repoid):
1813 1820 if isinstance(repoid, (int, long)):
1814 1821 try:
1815 1822 repo = cls.get(repoid)
1816 1823 except ValueError:
1817 1824 repo = None
1818 1825 else:
1819 1826 repo = cls.get_by_repo_name(repoid)
1820 1827 return repo
1821 1828
1822 1829 @classmethod
1823 1830 def get_by_full_path(cls, repo_full_path):
1824 1831 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1825 1832 repo_name = cls.normalize_repo_name(repo_name)
1826 1833 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1827 1834
1828 1835 @classmethod
1829 1836 def get_repo_forks(cls, repo_id):
1830 1837 return cls.query().filter(Repository.fork_id == repo_id)
1831 1838
1832 1839 @classmethod
1833 1840 def base_path(cls):
1834 1841 """
1835 1842 Returns base path when all repos are stored
1836 1843
1837 1844 :param cls:
1838 1845 """
1839 1846 q = Session().query(RhodeCodeUi)\
1840 1847 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1841 1848 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1842 1849 return q.one().ui_value
1843 1850
1844 1851 @classmethod
1845 1852 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1846 1853 case_insensitive=True, archived=False):
1847 1854 q = Repository.query()
1848 1855
1849 1856 if not archived:
1850 1857 q = q.filter(Repository.archived.isnot(true()))
1851 1858
1852 1859 if not isinstance(user_id, Optional):
1853 1860 q = q.filter(Repository.user_id == user_id)
1854 1861
1855 1862 if not isinstance(group_id, Optional):
1856 1863 q = q.filter(Repository.group_id == group_id)
1857 1864
1858 1865 if case_insensitive:
1859 1866 q = q.order_by(func.lower(Repository.repo_name))
1860 1867 else:
1861 1868 q = q.order_by(Repository.repo_name)
1862 1869
1863 1870 return q.all()
1864 1871
1865 1872 @property
1866 1873 def repo_uid(self):
1867 1874 return '_{}'.format(self.repo_id)
1868 1875
1869 1876 @property
1870 1877 def forks(self):
1871 1878 """
1872 1879 Return forks of this repo
1873 1880 """
1874 1881 return Repository.get_repo_forks(self.repo_id)
1875 1882
1876 1883 @property
1877 1884 def parent(self):
1878 1885 """
1879 1886 Returns fork parent
1880 1887 """
1881 1888 return self.fork
1882 1889
1883 1890 @property
1884 1891 def just_name(self):
1885 1892 return self.repo_name.split(self.NAME_SEP)[-1]
1886 1893
1887 1894 @property
1888 1895 def groups_with_parents(self):
1889 1896 groups = []
1890 1897 if self.group is None:
1891 1898 return groups
1892 1899
1893 1900 cur_gr = self.group
1894 1901 groups.insert(0, cur_gr)
1895 1902 while 1:
1896 1903 gr = getattr(cur_gr, 'parent_group', None)
1897 1904 cur_gr = cur_gr.parent_group
1898 1905 if gr is None:
1899 1906 break
1900 1907 groups.insert(0, gr)
1901 1908
1902 1909 return groups
1903 1910
1904 1911 @property
1905 1912 def groups_and_repo(self):
1906 1913 return self.groups_with_parents, self
1907 1914
1908 1915 @LazyProperty
1909 1916 def repo_path(self):
1910 1917 """
1911 1918 Returns base full path for that repository means where it actually
1912 1919 exists on a filesystem
1913 1920 """
1914 1921 q = Session().query(RhodeCodeUi).filter(
1915 1922 RhodeCodeUi.ui_key == self.NAME_SEP)
1916 1923 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1917 1924 return q.one().ui_value
1918 1925
1919 1926 @property
1920 1927 def repo_full_path(self):
1921 1928 p = [self.repo_path]
1922 1929 # we need to split the name by / since this is how we store the
1923 1930 # names in the database, but that eventually needs to be converted
1924 1931 # into a valid system path
1925 1932 p += self.repo_name.split(self.NAME_SEP)
1926 1933 return os.path.join(*map(safe_unicode, p))
1927 1934
1928 1935 @property
1929 1936 def cache_keys(self):
1930 1937 """
1931 1938 Returns associated cache keys for that repo
1932 1939 """
1933 1940 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
1934 1941 repo_id=self.repo_id)
1935 1942 return CacheKey.query()\
1936 1943 .filter(CacheKey.cache_args == invalidation_namespace)\
1937 1944 .order_by(CacheKey.cache_key)\
1938 1945 .all()
1939 1946
1940 1947 @property
1941 1948 def cached_diffs_relative_dir(self):
1942 1949 """
1943 1950 Return a relative to the repository store path of cached diffs
1944 1951 used for safe display for users, who shouldn't know the absolute store
1945 1952 path
1946 1953 """
1947 1954 return os.path.join(
1948 1955 os.path.dirname(self.repo_name),
1949 1956 self.cached_diffs_dir.split(os.path.sep)[-1])
1950 1957
1951 1958 @property
1952 1959 def cached_diffs_dir(self):
1953 1960 path = self.repo_full_path
1954 1961 return os.path.join(
1955 1962 os.path.dirname(path),
1956 1963 '.__shadow_diff_cache_repo_{}'.format(self.repo_id))
1957 1964
1958 1965 def cached_diffs(self):
1959 1966 diff_cache_dir = self.cached_diffs_dir
1960 1967 if os.path.isdir(diff_cache_dir):
1961 1968 return os.listdir(diff_cache_dir)
1962 1969 return []
1963 1970
1964 1971 def shadow_repos(self):
1965 1972 shadow_repos_pattern = '.__shadow_repo_{}'.format(self.repo_id)
1966 1973 return [
1967 1974 x for x in os.listdir(os.path.dirname(self.repo_full_path))
1968 1975 if x.startswith(shadow_repos_pattern)]
1969 1976
1970 1977 def get_new_name(self, repo_name):
1971 1978 """
1972 1979 returns new full repository name based on assigned group and new new
1973 1980
1974 1981 :param group_name:
1975 1982 """
1976 1983 path_prefix = self.group.full_path_splitted if self.group else []
1977 1984 return self.NAME_SEP.join(path_prefix + [repo_name])
1978 1985
1979 1986 @property
1980 1987 def _config(self):
1981 1988 """
1982 1989 Returns db based config object.
1983 1990 """
1984 1991 from rhodecode.lib.utils import make_db_config
1985 1992 return make_db_config(clear_session=False, repo=self)
1986 1993
1987 1994 def permissions(self, with_admins=True, with_owner=True,
1988 1995 expand_from_user_groups=False):
1989 1996 """
1990 1997 Permissions for repositories
1991 1998 """
1992 1999 _admin_perm = 'repository.admin'
1993 2000
1994 2001 owner_row = []
1995 2002 if with_owner:
1996 2003 usr = AttributeDict(self.user.get_dict())
1997 2004 usr.owner_row = True
1998 2005 usr.permission = _admin_perm
1999 2006 usr.permission_id = None
2000 2007 owner_row.append(usr)
2001 2008
2002 2009 super_admin_ids = []
2003 2010 super_admin_rows = []
2004 2011 if with_admins:
2005 2012 for usr in User.get_all_super_admins():
2006 2013 super_admin_ids.append(usr.user_id)
2007 2014 # if this admin is also owner, don't double the record
2008 2015 if usr.user_id == owner_row[0].user_id:
2009 2016 owner_row[0].admin_row = True
2010 2017 else:
2011 2018 usr = AttributeDict(usr.get_dict())
2012 2019 usr.admin_row = True
2013 2020 usr.permission = _admin_perm
2014 2021 usr.permission_id = None
2015 2022 super_admin_rows.append(usr)
2016 2023
2017 2024 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2018 2025 q = q.options(joinedload(UserRepoToPerm.repository),
2019 2026 joinedload(UserRepoToPerm.user),
2020 2027 joinedload(UserRepoToPerm.permission),)
2021 2028
2022 2029 # get owners and admins and permissions. We do a trick of re-writing
2023 2030 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2024 2031 # has a global reference and changing one object propagates to all
2025 2032 # others. This means if admin is also an owner admin_row that change
2026 2033 # would propagate to both objects
2027 2034 perm_rows = []
2028 2035 for _usr in q.all():
2029 2036 usr = AttributeDict(_usr.user.get_dict())
2030 2037 # if this user is also owner/admin, mark as duplicate record
2031 2038 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2032 2039 usr.duplicate_perm = True
2033 2040 # also check if this permission is maybe used by branch_permissions
2034 2041 if _usr.branch_perm_entry:
2035 2042 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2036 2043
2037 2044 usr.permission = _usr.permission.permission_name
2038 2045 usr.permission_id = _usr.repo_to_perm_id
2039 2046 perm_rows.append(usr)
2040 2047
2041 2048 # filter the perm rows by 'default' first and then sort them by
2042 2049 # admin,write,read,none permissions sorted again alphabetically in
2043 2050 # each group
2044 2051 perm_rows = sorted(perm_rows, key=display_user_sort)
2045 2052
2046 2053 user_groups_rows = []
2047 2054 if expand_from_user_groups:
2048 2055 for ug in self.permission_user_groups(with_members=True):
2049 2056 for user_data in ug.members:
2050 2057 user_groups_rows.append(user_data)
2051 2058
2052 2059 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2053 2060
2054 2061 def permission_user_groups(self, with_members=True):
2055 2062 q = UserGroupRepoToPerm.query()\
2056 2063 .filter(UserGroupRepoToPerm.repository == self)
2057 2064 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2058 2065 joinedload(UserGroupRepoToPerm.users_group),
2059 2066 joinedload(UserGroupRepoToPerm.permission),)
2060 2067
2061 2068 perm_rows = []
2062 2069 for _user_group in q.all():
2063 2070 entry = AttributeDict(_user_group.users_group.get_dict())
2064 2071 entry.permission = _user_group.permission.permission_name
2065 2072 if with_members:
2066 2073 entry.members = [x.user.get_dict()
2067 2074 for x in _user_group.users_group.members]
2068 2075 perm_rows.append(entry)
2069 2076
2070 2077 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2071 2078 return perm_rows
2072 2079
2073 2080 def get_api_data(self, include_secrets=False):
2074 2081 """
2075 2082 Common function for generating repo api data
2076 2083
2077 2084 :param include_secrets: See :meth:`User.get_api_data`.
2078 2085
2079 2086 """
2080 2087 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2081 2088 # move this methods on models level.
2082 2089 from rhodecode.model.settings import SettingsModel
2083 2090 from rhodecode.model.repo import RepoModel
2084 2091
2085 2092 repo = self
2086 2093 _user_id, _time, _reason = self.locked
2087 2094
2088 2095 data = {
2089 2096 'repo_id': repo.repo_id,
2090 2097 'repo_name': repo.repo_name,
2091 2098 'repo_type': repo.repo_type,
2092 2099 'clone_uri': repo.clone_uri or '',
2093 2100 'push_uri': repo.push_uri or '',
2094 2101 'url': RepoModel().get_url(self),
2095 2102 'private': repo.private,
2096 2103 'created_on': repo.created_on,
2097 2104 'description': repo.description_safe,
2098 2105 'landing_rev': repo.landing_rev,
2099 2106 'owner': repo.user.username,
2100 2107 'fork_of': repo.fork.repo_name if repo.fork else None,
2101 2108 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2102 2109 'enable_statistics': repo.enable_statistics,
2103 2110 'enable_locking': repo.enable_locking,
2104 2111 'enable_downloads': repo.enable_downloads,
2105 2112 'last_changeset': repo.changeset_cache,
2106 2113 'locked_by': User.get(_user_id).get_api_data(
2107 2114 include_secrets=include_secrets) if _user_id else None,
2108 2115 'locked_date': time_to_datetime(_time) if _time else None,
2109 2116 'lock_reason': _reason if _reason else None,
2110 2117 }
2111 2118
2112 2119 # TODO: mikhail: should be per-repo settings here
2113 2120 rc_config = SettingsModel().get_all_settings()
2114 2121 repository_fields = str2bool(
2115 2122 rc_config.get('rhodecode_repository_fields'))
2116 2123 if repository_fields:
2117 2124 for f in self.extra_fields:
2118 2125 data[f.field_key_prefixed] = f.field_value
2119 2126
2120 2127 return data
2121 2128
2122 2129 @classmethod
2123 2130 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2124 2131 if not lock_time:
2125 2132 lock_time = time.time()
2126 2133 if not lock_reason:
2127 2134 lock_reason = cls.LOCK_AUTOMATIC
2128 2135 repo.locked = [user_id, lock_time, lock_reason]
2129 2136 Session().add(repo)
2130 2137 Session().commit()
2131 2138
2132 2139 @classmethod
2133 2140 def unlock(cls, repo):
2134 2141 repo.locked = None
2135 2142 Session().add(repo)
2136 2143 Session().commit()
2137 2144
2138 2145 @classmethod
2139 2146 def getlock(cls, repo):
2140 2147 return repo.locked
2141 2148
2142 2149 def is_user_lock(self, user_id):
2143 2150 if self.lock[0]:
2144 2151 lock_user_id = safe_int(self.lock[0])
2145 2152 user_id = safe_int(user_id)
2146 2153 # both are ints, and they are equal
2147 2154 return all([lock_user_id, user_id]) and lock_user_id == user_id
2148 2155
2149 2156 return False
2150 2157
2151 2158 def get_locking_state(self, action, user_id, only_when_enabled=True):
2152 2159 """
2153 2160 Checks locking on this repository, if locking is enabled and lock is
2154 2161 present returns a tuple of make_lock, locked, locked_by.
2155 2162 make_lock can have 3 states None (do nothing) True, make lock
2156 2163 False release lock, This value is later propagated to hooks, which
2157 2164 do the locking. Think about this as signals passed to hooks what to do.
2158 2165
2159 2166 """
2160 2167 # TODO: johbo: This is part of the business logic and should be moved
2161 2168 # into the RepositoryModel.
2162 2169
2163 2170 if action not in ('push', 'pull'):
2164 2171 raise ValueError("Invalid action value: %s" % repr(action))
2165 2172
2166 2173 # defines if locked error should be thrown to user
2167 2174 currently_locked = False
2168 2175 # defines if new lock should be made, tri-state
2169 2176 make_lock = None
2170 2177 repo = self
2171 2178 user = User.get(user_id)
2172 2179
2173 2180 lock_info = repo.locked
2174 2181
2175 2182 if repo and (repo.enable_locking or not only_when_enabled):
2176 2183 if action == 'push':
2177 2184 # check if it's already locked !, if it is compare users
2178 2185 locked_by_user_id = lock_info[0]
2179 2186 if user.user_id == locked_by_user_id:
2180 2187 log.debug(
2181 2188 'Got `push` action from user %s, now unlocking', user)
2182 2189 # unlock if we have push from user who locked
2183 2190 make_lock = False
2184 2191 else:
2185 2192 # we're not the same user who locked, ban with
2186 2193 # code defined in settings (default is 423 HTTP Locked) !
2187 2194 log.debug('Repo %s is currently locked by %s', repo, user)
2188 2195 currently_locked = True
2189 2196 elif action == 'pull':
2190 2197 # [0] user [1] date
2191 2198 if lock_info[0] and lock_info[1]:
2192 2199 log.debug('Repo %s is currently locked by %s', repo, user)
2193 2200 currently_locked = True
2194 2201 else:
2195 2202 log.debug('Setting lock on repo %s by %s', repo, user)
2196 2203 make_lock = True
2197 2204
2198 2205 else:
2199 2206 log.debug('Repository %s do not have locking enabled', repo)
2200 2207
2201 2208 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2202 2209 make_lock, currently_locked, lock_info)
2203 2210
2204 2211 from rhodecode.lib.auth import HasRepoPermissionAny
2205 2212 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2206 2213 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2207 2214 # if we don't have at least write permission we cannot make a lock
2208 2215 log.debug('lock state reset back to FALSE due to lack '
2209 2216 'of at least read permission')
2210 2217 make_lock = False
2211 2218
2212 2219 return make_lock, currently_locked, lock_info
2213 2220
2214 2221 @property
2215 2222 def last_commit_cache_update_diff(self):
2216 2223 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2217 2224
2218 2225 @property
2219 2226 def last_commit_change(self):
2220 2227 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2221 2228 empty_date = datetime.datetime.fromtimestamp(0)
2222 2229 date_latest = self.changeset_cache.get('date', empty_date)
2223 2230 try:
2224 2231 return parse_datetime(date_latest)
2225 2232 except Exception:
2226 2233 return empty_date
2227 2234
2228 2235 @property
2229 2236 def last_db_change(self):
2230 2237 return self.updated_on
2231 2238
2232 2239 @property
2233 2240 def clone_uri_hidden(self):
2234 2241 clone_uri = self.clone_uri
2235 2242 if clone_uri:
2236 2243 import urlobject
2237 2244 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2238 2245 if url_obj.password:
2239 2246 clone_uri = url_obj.with_password('*****')
2240 2247 return clone_uri
2241 2248
2242 2249 @property
2243 2250 def push_uri_hidden(self):
2244 2251 push_uri = self.push_uri
2245 2252 if push_uri:
2246 2253 import urlobject
2247 2254 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2248 2255 if url_obj.password:
2249 2256 push_uri = url_obj.with_password('*****')
2250 2257 return push_uri
2251 2258
2252 2259 def clone_url(self, **override):
2253 2260 from rhodecode.model.settings import SettingsModel
2254 2261
2255 2262 uri_tmpl = None
2256 2263 if 'with_id' in override:
2257 2264 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2258 2265 del override['with_id']
2259 2266
2260 2267 if 'uri_tmpl' in override:
2261 2268 uri_tmpl = override['uri_tmpl']
2262 2269 del override['uri_tmpl']
2263 2270
2264 2271 ssh = False
2265 2272 if 'ssh' in override:
2266 2273 ssh = True
2267 2274 del override['ssh']
2268 2275
2269 2276 # we didn't override our tmpl from **overrides
2270 2277 request = get_current_request()
2271 2278 if not uri_tmpl:
2272 2279 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2273 2280 rc_config = request.call_context.rc_config
2274 2281 else:
2275 2282 rc_config = SettingsModel().get_all_settings(cache=True)
2276 2283 if ssh:
2277 2284 uri_tmpl = rc_config.get(
2278 2285 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2279 2286 else:
2280 2287 uri_tmpl = rc_config.get(
2281 2288 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2282 2289
2283 2290 return get_clone_url(request=request,
2284 2291 uri_tmpl=uri_tmpl,
2285 2292 repo_name=self.repo_name,
2286 2293 repo_id=self.repo_id, **override)
2287 2294
2288 2295 def set_state(self, state):
2289 2296 self.repo_state = state
2290 2297 Session().add(self)
2291 2298 #==========================================================================
2292 2299 # SCM PROPERTIES
2293 2300 #==========================================================================
2294 2301
2295 2302 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
2296 2303 return get_commit_safe(
2297 2304 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
2298 2305
2299 2306 def get_changeset(self, rev=None, pre_load=None):
2300 2307 warnings.warn("Use get_commit", DeprecationWarning)
2301 2308 commit_id = None
2302 2309 commit_idx = None
2303 2310 if isinstance(rev, compat.string_types):
2304 2311 commit_id = rev
2305 2312 else:
2306 2313 commit_idx = rev
2307 2314 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2308 2315 pre_load=pre_load)
2309 2316
2310 2317 def get_landing_commit(self):
2311 2318 """
2312 2319 Returns landing commit, or if that doesn't exist returns the tip
2313 2320 """
2314 2321 _rev_type, _rev = self.landing_rev
2315 2322 commit = self.get_commit(_rev)
2316 2323 if isinstance(commit, EmptyCommit):
2317 2324 return self.get_commit()
2318 2325 return commit
2319 2326
2320 2327 def flush_commit_cache(self):
2321 2328 self.update_commit_cache(cs_cache={'raw_id':'0'})
2322 2329 self.update_commit_cache()
2323 2330
2324 2331 def update_commit_cache(self, cs_cache=None, config=None):
2325 2332 """
2326 2333 Update cache of last commit for repository, keys should be::
2327 2334
2328 2335 source_repo_id
2329 2336 short_id
2330 2337 raw_id
2331 2338 revision
2332 2339 parents
2333 2340 message
2334 2341 date
2335 2342 author
2336 2343 updated_on
2337 2344
2338 2345 """
2339 2346 from rhodecode.lib.vcs.backends.base import BaseChangeset
2340 2347 if cs_cache is None:
2341 2348 # use no-cache version here
2342 2349 scm_repo = self.scm_instance(cache=False, config=config)
2343 2350
2344 2351 empty = scm_repo is None or scm_repo.is_empty()
2345 2352 if not empty:
2346 2353 cs_cache = scm_repo.get_commit(
2347 2354 pre_load=["author", "date", "message", "parents", "branch"])
2348 2355 else:
2349 2356 cs_cache = EmptyCommit()
2350 2357
2351 2358 if isinstance(cs_cache, BaseChangeset):
2352 2359 cs_cache = cs_cache.__json__()
2353 2360
2354 2361 def is_outdated(new_cs_cache):
2355 2362 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2356 2363 new_cs_cache['revision'] != self.changeset_cache['revision']):
2357 2364 return True
2358 2365 return False
2359 2366
2360 2367 # check if we have maybe already latest cached revision
2361 2368 if is_outdated(cs_cache) or not self.changeset_cache:
2362 2369 _default = datetime.datetime.utcnow()
2363 2370 last_change = cs_cache.get('date') or _default
2364 2371 # we check if last update is newer than the new value
2365 2372 # if yes, we use the current timestamp instead. Imagine you get
2366 2373 # old commit pushed 1y ago, we'd set last update 1y to ago.
2367 2374 last_change_timestamp = datetime_to_time(last_change)
2368 2375 current_timestamp = datetime_to_time(last_change)
2369 2376 if last_change_timestamp > current_timestamp:
2370 2377 cs_cache['date'] = _default
2371 2378
2372 2379 cs_cache['updated_on'] = time.time()
2373 2380 self.changeset_cache = cs_cache
2374 2381 self.updated_on = last_change
2375 2382 Session().add(self)
2376 2383 Session().commit()
2377 2384
2378 2385 log.debug('updated repo `%s` with new commit cache %s',
2379 2386 self.repo_name, cs_cache)
2380 2387 else:
2381 2388 cs_cache = self.changeset_cache
2382 2389 cs_cache['updated_on'] = time.time()
2383 2390 self.changeset_cache = cs_cache
2384 2391 Session().add(self)
2385 2392 Session().commit()
2386 2393
2387 2394 log.debug('Skipping update_commit_cache for repo:`%s` '
2388 2395 'commit already with latest changes', self.repo_name)
2389 2396
2390 2397 @property
2391 2398 def tip(self):
2392 2399 return self.get_commit('tip')
2393 2400
2394 2401 @property
2395 2402 def author(self):
2396 2403 return self.tip.author
2397 2404
2398 2405 @property
2399 2406 def last_change(self):
2400 2407 return self.scm_instance().last_change
2401 2408
2402 2409 def get_comments(self, revisions=None):
2403 2410 """
2404 2411 Returns comments for this repository grouped by revisions
2405 2412
2406 2413 :param revisions: filter query by revisions only
2407 2414 """
2408 2415 cmts = ChangesetComment.query()\
2409 2416 .filter(ChangesetComment.repo == self)
2410 2417 if revisions:
2411 2418 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2412 2419 grouped = collections.defaultdict(list)
2413 2420 for cmt in cmts.all():
2414 2421 grouped[cmt.revision].append(cmt)
2415 2422 return grouped
2416 2423
2417 2424 def statuses(self, revisions=None):
2418 2425 """
2419 2426 Returns statuses for this repository
2420 2427
2421 2428 :param revisions: list of revisions to get statuses for
2422 2429 """
2423 2430 statuses = ChangesetStatus.query()\
2424 2431 .filter(ChangesetStatus.repo == self)\
2425 2432 .filter(ChangesetStatus.version == 0)
2426 2433
2427 2434 if revisions:
2428 2435 # Try doing the filtering in chunks to avoid hitting limits
2429 2436 size = 500
2430 2437 status_results = []
2431 2438 for chunk in xrange(0, len(revisions), size):
2432 2439 status_results += statuses.filter(
2433 2440 ChangesetStatus.revision.in_(
2434 2441 revisions[chunk: chunk+size])
2435 2442 ).all()
2436 2443 else:
2437 2444 status_results = statuses.all()
2438 2445
2439 2446 grouped = {}
2440 2447
2441 2448 # maybe we have open new pullrequest without a status?
2442 2449 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2443 2450 status_lbl = ChangesetStatus.get_status_lbl(stat)
2444 2451 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2445 2452 for rev in pr.revisions:
2446 2453 pr_id = pr.pull_request_id
2447 2454 pr_repo = pr.target_repo.repo_name
2448 2455 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2449 2456
2450 2457 for stat in status_results:
2451 2458 pr_id = pr_repo = None
2452 2459 if stat.pull_request:
2453 2460 pr_id = stat.pull_request.pull_request_id
2454 2461 pr_repo = stat.pull_request.target_repo.repo_name
2455 2462 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2456 2463 pr_id, pr_repo]
2457 2464 return grouped
2458 2465
2459 2466 # ==========================================================================
2460 2467 # SCM CACHE INSTANCE
2461 2468 # ==========================================================================
2462 2469
2463 2470 def scm_instance(self, **kwargs):
2464 2471 import rhodecode
2465 2472
2466 2473 # Passing a config will not hit the cache currently only used
2467 2474 # for repo2dbmapper
2468 2475 config = kwargs.pop('config', None)
2469 2476 cache = kwargs.pop('cache', None)
2470 2477 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2471 2478 if vcs_full_cache is not None:
2472 2479 # allows override global config
2473 2480 full_cache = vcs_full_cache
2474 2481 else:
2475 2482 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2476 2483 # if cache is NOT defined use default global, else we have a full
2477 2484 # control over cache behaviour
2478 2485 if cache is None and full_cache and not config:
2479 2486 log.debug('Initializing pure cached instance for %s', self.repo_path)
2480 2487 return self._get_instance_cached()
2481 2488
2482 2489 # cache here is sent to the "vcs server"
2483 2490 return self._get_instance(cache=bool(cache), config=config)
2484 2491
2485 2492 def _get_instance_cached(self):
2486 2493 from rhodecode.lib import rc_cache
2487 2494
2488 2495 cache_namespace_uid = 'cache_repo_instance.{}'.format(self.repo_id)
2489 2496 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2490 2497 repo_id=self.repo_id)
2491 2498 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2492 2499
2493 2500 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2494 2501 def get_instance_cached(repo_id, context_id, _cache_state_uid):
2495 2502 return self._get_instance(repo_state_uid=_cache_state_uid)
2496 2503
2497 2504 # we must use thread scoped cache here,
2498 2505 # because each thread of gevent needs it's own not shared connection and cache
2499 2506 # we also alter `args` so the cache key is individual for every green thread.
2500 2507 inv_context_manager = rc_cache.InvalidationContext(
2501 2508 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace,
2502 2509 thread_scoped=True)
2503 2510 with inv_context_manager as invalidation_context:
2504 2511 cache_state_uid = invalidation_context.cache_data['cache_state_uid']
2505 2512 args = (self.repo_id, inv_context_manager.cache_key, cache_state_uid)
2506 2513
2507 2514 # re-compute and store cache if we get invalidate signal
2508 2515 if invalidation_context.should_invalidate():
2509 2516 instance = get_instance_cached.refresh(*args)
2510 2517 else:
2511 2518 instance = get_instance_cached(*args)
2512 2519
2513 2520 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2514 2521 return instance
2515 2522
2516 2523 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2517 2524 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2518 2525 self.repo_type, self.repo_path, cache)
2519 2526 config = config or self._config
2520 2527 custom_wire = {
2521 2528 'cache': cache, # controls the vcs.remote cache
2522 2529 'repo_state_uid': repo_state_uid
2523 2530 }
2524 2531 repo = get_vcs_instance(
2525 2532 repo_path=safe_str(self.repo_full_path),
2526 2533 config=config,
2527 2534 with_wire=custom_wire,
2528 2535 create=False,
2529 2536 _vcs_alias=self.repo_type)
2530 2537 if repo is not None:
2531 2538 repo.count() # cache rebuild
2532 2539 return repo
2533 2540
2534 2541 def get_shadow_repository_path(self, workspace_id):
2535 2542 from rhodecode.lib.vcs.backends.base import BaseRepository
2536 2543 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2537 2544 self.repo_full_path, self.repo_id, workspace_id)
2538 2545 return shadow_repo_path
2539 2546
2540 2547 def __json__(self):
2541 2548 return {'landing_rev': self.landing_rev}
2542 2549
2543 2550 def get_dict(self):
2544 2551
2545 2552 # Since we transformed `repo_name` to a hybrid property, we need to
2546 2553 # keep compatibility with the code which uses `repo_name` field.
2547 2554
2548 2555 result = super(Repository, self).get_dict()
2549 2556 result['repo_name'] = result.pop('_repo_name', None)
2550 2557 return result
2551 2558
2552 2559
2553 2560 class RepoGroup(Base, BaseModel):
2554 2561 __tablename__ = 'groups'
2555 2562 __table_args__ = (
2556 2563 UniqueConstraint('group_name', 'group_parent_id'),
2557 2564 base_table_args,
2558 2565 )
2559 2566 __mapper_args__ = {'order_by': 'group_name'}
2560 2567
2561 2568 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2562 2569
2563 2570 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2564 2571 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2565 2572 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2566 2573 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2567 2574 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2568 2575 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2569 2576 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2570 2577 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2571 2578 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2572 2579 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2573 2580 _changeset_cache = Column(
2574 2581 "changeset_cache", LargeBinary(), nullable=True) # JSON data
2575 2582
2576 2583 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2577 2584 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2578 2585 parent_group = relationship('RepoGroup', remote_side=group_id)
2579 2586 user = relationship('User')
2580 2587 integrations = relationship('Integration', cascade="all, delete-orphan")
2581 2588
2589 # no cascade, set NULL
2590 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id')
2591
2582 2592 def __init__(self, group_name='', parent_group=None):
2583 2593 self.group_name = group_name
2584 2594 self.parent_group = parent_group
2585 2595
2586 2596 def __unicode__(self):
2587 2597 return u"<%s('id:%s:%s')>" % (
2588 2598 self.__class__.__name__, self.group_id, self.group_name)
2589 2599
2590 2600 @hybrid_property
2591 2601 def group_name(self):
2592 2602 return self._group_name
2593 2603
2594 2604 @group_name.setter
2595 2605 def group_name(self, value):
2596 2606 self._group_name = value
2597 2607 self.group_name_hash = self.hash_repo_group_name(value)
2598 2608
2599 2609 @hybrid_property
2600 2610 def changeset_cache(self):
2601 2611 from rhodecode.lib.vcs.backends.base import EmptyCommit
2602 2612 dummy = EmptyCommit().__json__()
2603 2613 if not self._changeset_cache:
2604 2614 dummy['source_repo_id'] = ''
2605 2615 return json.loads(json.dumps(dummy))
2606 2616
2607 2617 try:
2608 2618 return json.loads(self._changeset_cache)
2609 2619 except TypeError:
2610 2620 return dummy
2611 2621 except Exception:
2612 2622 log.error(traceback.format_exc())
2613 2623 return dummy
2614 2624
2615 2625 @changeset_cache.setter
2616 2626 def changeset_cache(self, val):
2617 2627 try:
2618 2628 self._changeset_cache = json.dumps(val)
2619 2629 except Exception:
2620 2630 log.error(traceback.format_exc())
2621 2631
2622 2632 @validates('group_parent_id')
2623 2633 def validate_group_parent_id(self, key, val):
2624 2634 """
2625 2635 Check cycle references for a parent group to self
2626 2636 """
2627 2637 if self.group_id and val:
2628 2638 assert val != self.group_id
2629 2639
2630 2640 return val
2631 2641
2632 2642 @hybrid_property
2633 2643 def description_safe(self):
2634 2644 from rhodecode.lib import helpers as h
2635 2645 return h.escape(self.group_description)
2636 2646
2637 2647 @classmethod
2638 2648 def hash_repo_group_name(cls, repo_group_name):
2639 2649 val = remove_formatting(repo_group_name)
2640 2650 val = safe_str(val).lower()
2641 2651 chars = []
2642 2652 for c in val:
2643 2653 if c not in string.ascii_letters:
2644 2654 c = str(ord(c))
2645 2655 chars.append(c)
2646 2656
2647 2657 return ''.join(chars)
2648 2658
2649 2659 @classmethod
2650 2660 def _generate_choice(cls, repo_group):
2651 2661 from webhelpers.html import literal as _literal
2652 2662 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2653 2663 return repo_group.group_id, _name(repo_group.full_path_splitted)
2654 2664
2655 2665 @classmethod
2656 2666 def groups_choices(cls, groups=None, show_empty_group=True):
2657 2667 if not groups:
2658 2668 groups = cls.query().all()
2659 2669
2660 2670 repo_groups = []
2661 2671 if show_empty_group:
2662 2672 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2663 2673
2664 2674 repo_groups.extend([cls._generate_choice(x) for x in groups])
2665 2675
2666 2676 repo_groups = sorted(
2667 2677 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2668 2678 return repo_groups
2669 2679
2670 2680 @classmethod
2671 2681 def url_sep(cls):
2672 2682 return URL_SEP
2673 2683
2674 2684 @classmethod
2675 2685 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2676 2686 if case_insensitive:
2677 2687 gr = cls.query().filter(func.lower(cls.group_name)
2678 2688 == func.lower(group_name))
2679 2689 else:
2680 2690 gr = cls.query().filter(cls.group_name == group_name)
2681 2691 if cache:
2682 2692 name_key = _hash_key(group_name)
2683 2693 gr = gr.options(
2684 2694 FromCache("sql_cache_short", "get_group_%s" % name_key))
2685 2695 return gr.scalar()
2686 2696
2687 2697 @classmethod
2688 2698 def get_user_personal_repo_group(cls, user_id):
2689 2699 user = User.get(user_id)
2690 2700 if user.username == User.DEFAULT_USER:
2691 2701 return None
2692 2702
2693 2703 return cls.query()\
2694 2704 .filter(cls.personal == true()) \
2695 2705 .filter(cls.user == user) \
2696 2706 .order_by(cls.group_id.asc()) \
2697 2707 .first()
2698 2708
2699 2709 @classmethod
2700 2710 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2701 2711 case_insensitive=True):
2702 2712 q = RepoGroup.query()
2703 2713
2704 2714 if not isinstance(user_id, Optional):
2705 2715 q = q.filter(RepoGroup.user_id == user_id)
2706 2716
2707 2717 if not isinstance(group_id, Optional):
2708 2718 q = q.filter(RepoGroup.group_parent_id == group_id)
2709 2719
2710 2720 if case_insensitive:
2711 2721 q = q.order_by(func.lower(RepoGroup.group_name))
2712 2722 else:
2713 2723 q = q.order_by(RepoGroup.group_name)
2714 2724 return q.all()
2715 2725
2716 2726 @property
2717 2727 def parents(self, parents_recursion_limit = 10):
2718 2728 groups = []
2719 2729 if self.parent_group is None:
2720 2730 return groups
2721 2731 cur_gr = self.parent_group
2722 2732 groups.insert(0, cur_gr)
2723 2733 cnt = 0
2724 2734 while 1:
2725 2735 cnt += 1
2726 2736 gr = getattr(cur_gr, 'parent_group', None)
2727 2737 cur_gr = cur_gr.parent_group
2728 2738 if gr is None:
2729 2739 break
2730 2740 if cnt == parents_recursion_limit:
2731 2741 # this will prevent accidental infinit loops
2732 2742 log.error('more than %s parents found for group %s, stopping '
2733 2743 'recursive parent fetching', parents_recursion_limit, self)
2734 2744 break
2735 2745
2736 2746 groups.insert(0, gr)
2737 2747 return groups
2738 2748
2739 2749 @property
2740 2750 def last_commit_cache_update_diff(self):
2741 2751 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2742 2752
2743 2753 @property
2744 2754 def last_commit_change(self):
2745 2755 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2746 2756 empty_date = datetime.datetime.fromtimestamp(0)
2747 2757 date_latest = self.changeset_cache.get('date', empty_date)
2748 2758 try:
2749 2759 return parse_datetime(date_latest)
2750 2760 except Exception:
2751 2761 return empty_date
2752 2762
2753 2763 @property
2754 2764 def last_db_change(self):
2755 2765 return self.updated_on
2756 2766
2757 2767 @property
2758 2768 def children(self):
2759 2769 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2760 2770
2761 2771 @property
2762 2772 def name(self):
2763 2773 return self.group_name.split(RepoGroup.url_sep())[-1]
2764 2774
2765 2775 @property
2766 2776 def full_path(self):
2767 2777 return self.group_name
2768 2778
2769 2779 @property
2770 2780 def full_path_splitted(self):
2771 2781 return self.group_name.split(RepoGroup.url_sep())
2772 2782
2773 2783 @property
2774 2784 def repositories(self):
2775 2785 return Repository.query()\
2776 2786 .filter(Repository.group == self)\
2777 2787 .order_by(Repository.repo_name)
2778 2788
2779 2789 @property
2780 2790 def repositories_recursive_count(self):
2781 2791 cnt = self.repositories.count()
2782 2792
2783 2793 def children_count(group):
2784 2794 cnt = 0
2785 2795 for child in group.children:
2786 2796 cnt += child.repositories.count()
2787 2797 cnt += children_count(child)
2788 2798 return cnt
2789 2799
2790 2800 return cnt + children_count(self)
2791 2801
2792 2802 def _recursive_objects(self, include_repos=True, include_groups=True):
2793 2803 all_ = []
2794 2804
2795 2805 def _get_members(root_gr):
2796 2806 if include_repos:
2797 2807 for r in root_gr.repositories:
2798 2808 all_.append(r)
2799 2809 childs = root_gr.children.all()
2800 2810 if childs:
2801 2811 for gr in childs:
2802 2812 if include_groups:
2803 2813 all_.append(gr)
2804 2814 _get_members(gr)
2805 2815
2806 2816 root_group = []
2807 2817 if include_groups:
2808 2818 root_group = [self]
2809 2819
2810 2820 _get_members(self)
2811 2821 return root_group + all_
2812 2822
2813 2823 def recursive_groups_and_repos(self):
2814 2824 """
2815 2825 Recursive return all groups, with repositories in those groups
2816 2826 """
2817 2827 return self._recursive_objects()
2818 2828
2819 2829 def recursive_groups(self):
2820 2830 """
2821 2831 Returns all children groups for this group including children of children
2822 2832 """
2823 2833 return self._recursive_objects(include_repos=False)
2824 2834
2825 2835 def recursive_repos(self):
2826 2836 """
2827 2837 Returns all children repositories for this group
2828 2838 """
2829 2839 return self._recursive_objects(include_groups=False)
2830 2840
2831 2841 def get_new_name(self, group_name):
2832 2842 """
2833 2843 returns new full group name based on parent and new name
2834 2844
2835 2845 :param group_name:
2836 2846 """
2837 2847 path_prefix = (self.parent_group.full_path_splitted if
2838 2848 self.parent_group else [])
2839 2849 return RepoGroup.url_sep().join(path_prefix + [group_name])
2840 2850
2841 2851 def update_commit_cache(self, config=None):
2842 2852 """
2843 2853 Update cache of last changeset for newest repository inside this group, keys should be::
2844 2854
2845 2855 source_repo_id
2846 2856 short_id
2847 2857 raw_id
2848 2858 revision
2849 2859 parents
2850 2860 message
2851 2861 date
2852 2862 author
2853 2863
2854 2864 """
2855 2865 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2856 2866
2857 2867 def repo_groups_and_repos():
2858 2868 all_entries = OrderedDefaultDict(list)
2859 2869
2860 2870 def _get_members(root_gr, pos=0):
2861 2871
2862 2872 for repo in root_gr.repositories:
2863 2873 all_entries[root_gr].append(repo)
2864 2874
2865 2875 # fill in all parent positions
2866 2876 for parent_group in root_gr.parents:
2867 2877 all_entries[parent_group].extend(all_entries[root_gr])
2868 2878
2869 2879 children_groups = root_gr.children.all()
2870 2880 if children_groups:
2871 2881 for cnt, gr in enumerate(children_groups, 1):
2872 2882 _get_members(gr, pos=pos+cnt)
2873 2883
2874 2884 _get_members(root_gr=self)
2875 2885 return all_entries
2876 2886
2877 2887 empty_date = datetime.datetime.fromtimestamp(0)
2878 2888 for repo_group, repos in repo_groups_and_repos().items():
2879 2889
2880 2890 latest_repo_cs_cache = {}
2881 2891 _date_latest = empty_date
2882 2892 for repo in repos:
2883 2893 repo_cs_cache = repo.changeset_cache
2884 2894 date_latest = latest_repo_cs_cache.get('date', empty_date)
2885 2895 date_current = repo_cs_cache.get('date', empty_date)
2886 2896 current_timestamp = datetime_to_time(parse_datetime(date_latest))
2887 2897 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
2888 2898 latest_repo_cs_cache = repo_cs_cache
2889 2899 latest_repo_cs_cache['source_repo_id'] = repo.repo_id
2890 2900 _date_latest = parse_datetime(latest_repo_cs_cache['date'])
2891 2901
2892 2902 latest_repo_cs_cache['updated_on'] = time.time()
2893 2903 repo_group.changeset_cache = latest_repo_cs_cache
2894 2904 repo_group.updated_on = _date_latest
2895 2905 Session().add(repo_group)
2896 2906 Session().commit()
2897 2907
2898 2908 log.debug('updated repo group `%s` with new commit cache %s',
2899 2909 repo_group.group_name, latest_repo_cs_cache)
2900 2910
2901 2911 def permissions(self, with_admins=True, with_owner=True,
2902 2912 expand_from_user_groups=False):
2903 2913 """
2904 2914 Permissions for repository groups
2905 2915 """
2906 2916 _admin_perm = 'group.admin'
2907 2917
2908 2918 owner_row = []
2909 2919 if with_owner:
2910 2920 usr = AttributeDict(self.user.get_dict())
2911 2921 usr.owner_row = True
2912 2922 usr.permission = _admin_perm
2913 2923 owner_row.append(usr)
2914 2924
2915 2925 super_admin_ids = []
2916 2926 super_admin_rows = []
2917 2927 if with_admins:
2918 2928 for usr in User.get_all_super_admins():
2919 2929 super_admin_ids.append(usr.user_id)
2920 2930 # if this admin is also owner, don't double the record
2921 2931 if usr.user_id == owner_row[0].user_id:
2922 2932 owner_row[0].admin_row = True
2923 2933 else:
2924 2934 usr = AttributeDict(usr.get_dict())
2925 2935 usr.admin_row = True
2926 2936 usr.permission = _admin_perm
2927 2937 super_admin_rows.append(usr)
2928 2938
2929 2939 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2930 2940 q = q.options(joinedload(UserRepoGroupToPerm.group),
2931 2941 joinedload(UserRepoGroupToPerm.user),
2932 2942 joinedload(UserRepoGroupToPerm.permission),)
2933 2943
2934 2944 # get owners and admins and permissions. We do a trick of re-writing
2935 2945 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2936 2946 # has a global reference and changing one object propagates to all
2937 2947 # others. This means if admin is also an owner admin_row that change
2938 2948 # would propagate to both objects
2939 2949 perm_rows = []
2940 2950 for _usr in q.all():
2941 2951 usr = AttributeDict(_usr.user.get_dict())
2942 2952 # if this user is also owner/admin, mark as duplicate record
2943 2953 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2944 2954 usr.duplicate_perm = True
2945 2955 usr.permission = _usr.permission.permission_name
2946 2956 perm_rows.append(usr)
2947 2957
2948 2958 # filter the perm rows by 'default' first and then sort them by
2949 2959 # admin,write,read,none permissions sorted again alphabetically in
2950 2960 # each group
2951 2961 perm_rows = sorted(perm_rows, key=display_user_sort)
2952 2962
2953 2963 user_groups_rows = []
2954 2964 if expand_from_user_groups:
2955 2965 for ug in self.permission_user_groups(with_members=True):
2956 2966 for user_data in ug.members:
2957 2967 user_groups_rows.append(user_data)
2958 2968
2959 2969 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2960 2970
2961 2971 def permission_user_groups(self, with_members=False):
2962 2972 q = UserGroupRepoGroupToPerm.query()\
2963 2973 .filter(UserGroupRepoGroupToPerm.group == self)
2964 2974 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2965 2975 joinedload(UserGroupRepoGroupToPerm.users_group),
2966 2976 joinedload(UserGroupRepoGroupToPerm.permission),)
2967 2977
2968 2978 perm_rows = []
2969 2979 for _user_group in q.all():
2970 2980 entry = AttributeDict(_user_group.users_group.get_dict())
2971 2981 entry.permission = _user_group.permission.permission_name
2972 2982 if with_members:
2973 2983 entry.members = [x.user.get_dict()
2974 2984 for x in _user_group.users_group.members]
2975 2985 perm_rows.append(entry)
2976 2986
2977 2987 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2978 2988 return perm_rows
2979 2989
2980 2990 def get_api_data(self):
2981 2991 """
2982 2992 Common function for generating api data
2983 2993
2984 2994 """
2985 2995 group = self
2986 2996 data = {
2987 2997 'group_id': group.group_id,
2988 2998 'group_name': group.group_name,
2989 2999 'group_description': group.description_safe,
2990 3000 'parent_group': group.parent_group.group_name if group.parent_group else None,
2991 3001 'repositories': [x.repo_name for x in group.repositories],
2992 3002 'owner': group.user.username,
2993 3003 }
2994 3004 return data
2995 3005
2996 3006 def get_dict(self):
2997 3007 # Since we transformed `group_name` to a hybrid property, we need to
2998 3008 # keep compatibility with the code which uses `group_name` field.
2999 3009 result = super(RepoGroup, self).get_dict()
3000 3010 result['group_name'] = result.pop('_group_name', None)
3001 3011 return result
3002 3012
3003 3013
3004 3014 class Permission(Base, BaseModel):
3005 3015 __tablename__ = 'permissions'
3006 3016 __table_args__ = (
3007 3017 Index('p_perm_name_idx', 'permission_name'),
3008 3018 base_table_args,
3009 3019 )
3010 3020
3011 3021 PERMS = [
3012 3022 ('hg.admin', _('RhodeCode Super Administrator')),
3013 3023
3014 3024 ('repository.none', _('Repository no access')),
3015 3025 ('repository.read', _('Repository read access')),
3016 3026 ('repository.write', _('Repository write access')),
3017 3027 ('repository.admin', _('Repository admin access')),
3018 3028
3019 3029 ('group.none', _('Repository group no access')),
3020 3030 ('group.read', _('Repository group read access')),
3021 3031 ('group.write', _('Repository group write access')),
3022 3032 ('group.admin', _('Repository group admin access')),
3023 3033
3024 3034 ('usergroup.none', _('User group no access')),
3025 3035 ('usergroup.read', _('User group read access')),
3026 3036 ('usergroup.write', _('User group write access')),
3027 3037 ('usergroup.admin', _('User group admin access')),
3028 3038
3029 3039 ('branch.none', _('Branch no permissions')),
3030 3040 ('branch.merge', _('Branch access by web merge')),
3031 3041 ('branch.push', _('Branch access by push')),
3032 3042 ('branch.push_force', _('Branch access by push with force')),
3033 3043
3034 3044 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3035 3045 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3036 3046
3037 3047 ('hg.usergroup.create.false', _('User Group creation disabled')),
3038 3048 ('hg.usergroup.create.true', _('User Group creation enabled')),
3039 3049
3040 3050 ('hg.create.none', _('Repository creation disabled')),
3041 3051 ('hg.create.repository', _('Repository creation enabled')),
3042 3052 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3043 3053 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3044 3054
3045 3055 ('hg.fork.none', _('Repository forking disabled')),
3046 3056 ('hg.fork.repository', _('Repository forking enabled')),
3047 3057
3048 3058 ('hg.register.none', _('Registration disabled')),
3049 3059 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3050 3060 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3051 3061
3052 3062 ('hg.password_reset.enabled', _('Password reset enabled')),
3053 3063 ('hg.password_reset.hidden', _('Password reset hidden')),
3054 3064 ('hg.password_reset.disabled', _('Password reset disabled')),
3055 3065
3056 3066 ('hg.extern_activate.manual', _('Manual activation of external account')),
3057 3067 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3058 3068
3059 3069 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3060 3070 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3061 3071 ]
3062 3072
3063 3073 # definition of system default permissions for DEFAULT user, created on
3064 3074 # system setup
3065 3075 DEFAULT_USER_PERMISSIONS = [
3066 3076 # object perms
3067 3077 'repository.read',
3068 3078 'group.read',
3069 3079 'usergroup.read',
3070 3080 # branch, for backward compat we need same value as before so forced pushed
3071 3081 'branch.push_force',
3072 3082 # global
3073 3083 'hg.create.repository',
3074 3084 'hg.repogroup.create.false',
3075 3085 'hg.usergroup.create.false',
3076 3086 'hg.create.write_on_repogroup.true',
3077 3087 'hg.fork.repository',
3078 3088 'hg.register.manual_activate',
3079 3089 'hg.password_reset.enabled',
3080 3090 'hg.extern_activate.auto',
3081 3091 'hg.inherit_default_perms.true',
3082 3092 ]
3083 3093
3084 3094 # defines which permissions are more important higher the more important
3085 3095 # Weight defines which permissions are more important.
3086 3096 # The higher number the more important.
3087 3097 PERM_WEIGHTS = {
3088 3098 'repository.none': 0,
3089 3099 'repository.read': 1,
3090 3100 'repository.write': 3,
3091 3101 'repository.admin': 4,
3092 3102
3093 3103 'group.none': 0,
3094 3104 'group.read': 1,
3095 3105 'group.write': 3,
3096 3106 'group.admin': 4,
3097 3107
3098 3108 'usergroup.none': 0,
3099 3109 'usergroup.read': 1,
3100 3110 'usergroup.write': 3,
3101 3111 'usergroup.admin': 4,
3102 3112
3103 3113 'branch.none': 0,
3104 3114 'branch.merge': 1,
3105 3115 'branch.push': 3,
3106 3116 'branch.push_force': 4,
3107 3117
3108 3118 'hg.repogroup.create.false': 0,
3109 3119 'hg.repogroup.create.true': 1,
3110 3120
3111 3121 'hg.usergroup.create.false': 0,
3112 3122 'hg.usergroup.create.true': 1,
3113 3123
3114 3124 'hg.fork.none': 0,
3115 3125 'hg.fork.repository': 1,
3116 3126 'hg.create.none': 0,
3117 3127 'hg.create.repository': 1
3118 3128 }
3119 3129
3120 3130 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3121 3131 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3122 3132 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3123 3133
3124 3134 def __unicode__(self):
3125 3135 return u"<%s('%s:%s')>" % (
3126 3136 self.__class__.__name__, self.permission_id, self.permission_name
3127 3137 )
3128 3138
3129 3139 @classmethod
3130 3140 def get_by_key(cls, key):
3131 3141 return cls.query().filter(cls.permission_name == key).scalar()
3132 3142
3133 3143 @classmethod
3134 3144 def get_default_repo_perms(cls, user_id, repo_id=None):
3135 3145 q = Session().query(UserRepoToPerm, Repository, Permission)\
3136 3146 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3137 3147 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3138 3148 .filter(UserRepoToPerm.user_id == user_id)
3139 3149 if repo_id:
3140 3150 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3141 3151 return q.all()
3142 3152
3143 3153 @classmethod
3144 3154 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3145 3155 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3146 3156 .join(
3147 3157 Permission,
3148 3158 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3149 3159 .join(
3150 3160 UserRepoToPerm,
3151 3161 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3152 3162 .filter(UserRepoToPerm.user_id == user_id)
3153 3163
3154 3164 if repo_id:
3155 3165 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3156 3166 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3157 3167
3158 3168 @classmethod
3159 3169 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3160 3170 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3161 3171 .join(
3162 3172 Permission,
3163 3173 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3164 3174 .join(
3165 3175 Repository,
3166 3176 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3167 3177 .join(
3168 3178 UserGroup,
3169 3179 UserGroupRepoToPerm.users_group_id ==
3170 3180 UserGroup.users_group_id)\
3171 3181 .join(
3172 3182 UserGroupMember,
3173 3183 UserGroupRepoToPerm.users_group_id ==
3174 3184 UserGroupMember.users_group_id)\
3175 3185 .filter(
3176 3186 UserGroupMember.user_id == user_id,
3177 3187 UserGroup.users_group_active == true())
3178 3188 if repo_id:
3179 3189 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3180 3190 return q.all()
3181 3191
3182 3192 @classmethod
3183 3193 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3184 3194 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3185 3195 .join(
3186 3196 Permission,
3187 3197 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3188 3198 .join(
3189 3199 UserGroupRepoToPerm,
3190 3200 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3191 3201 .join(
3192 3202 UserGroup,
3193 3203 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3194 3204 .join(
3195 3205 UserGroupMember,
3196 3206 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3197 3207 .filter(
3198 3208 UserGroupMember.user_id == user_id,
3199 3209 UserGroup.users_group_active == true())
3200 3210
3201 3211 if repo_id:
3202 3212 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3203 3213 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3204 3214
3205 3215 @classmethod
3206 3216 def get_default_group_perms(cls, user_id, repo_group_id=None):
3207 3217 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3208 3218 .join(
3209 3219 Permission,
3210 3220 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3211 3221 .join(
3212 3222 RepoGroup,
3213 3223 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3214 3224 .filter(UserRepoGroupToPerm.user_id == user_id)
3215 3225 if repo_group_id:
3216 3226 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3217 3227 return q.all()
3218 3228
3219 3229 @classmethod
3220 3230 def get_default_group_perms_from_user_group(
3221 3231 cls, user_id, repo_group_id=None):
3222 3232 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3223 3233 .join(
3224 3234 Permission,
3225 3235 UserGroupRepoGroupToPerm.permission_id ==
3226 3236 Permission.permission_id)\
3227 3237 .join(
3228 3238 RepoGroup,
3229 3239 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3230 3240 .join(
3231 3241 UserGroup,
3232 3242 UserGroupRepoGroupToPerm.users_group_id ==
3233 3243 UserGroup.users_group_id)\
3234 3244 .join(
3235 3245 UserGroupMember,
3236 3246 UserGroupRepoGroupToPerm.users_group_id ==
3237 3247 UserGroupMember.users_group_id)\
3238 3248 .filter(
3239 3249 UserGroupMember.user_id == user_id,
3240 3250 UserGroup.users_group_active == true())
3241 3251 if repo_group_id:
3242 3252 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3243 3253 return q.all()
3244 3254
3245 3255 @classmethod
3246 3256 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3247 3257 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3248 3258 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3249 3259 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3250 3260 .filter(UserUserGroupToPerm.user_id == user_id)
3251 3261 if user_group_id:
3252 3262 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3253 3263 return q.all()
3254 3264
3255 3265 @classmethod
3256 3266 def get_default_user_group_perms_from_user_group(
3257 3267 cls, user_id, user_group_id=None):
3258 3268 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3259 3269 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3260 3270 .join(
3261 3271 Permission,
3262 3272 UserGroupUserGroupToPerm.permission_id ==
3263 3273 Permission.permission_id)\
3264 3274 .join(
3265 3275 TargetUserGroup,
3266 3276 UserGroupUserGroupToPerm.target_user_group_id ==
3267 3277 TargetUserGroup.users_group_id)\
3268 3278 .join(
3269 3279 UserGroup,
3270 3280 UserGroupUserGroupToPerm.user_group_id ==
3271 3281 UserGroup.users_group_id)\
3272 3282 .join(
3273 3283 UserGroupMember,
3274 3284 UserGroupUserGroupToPerm.user_group_id ==
3275 3285 UserGroupMember.users_group_id)\
3276 3286 .filter(
3277 3287 UserGroupMember.user_id == user_id,
3278 3288 UserGroup.users_group_active == true())
3279 3289 if user_group_id:
3280 3290 q = q.filter(
3281 3291 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3282 3292
3283 3293 return q.all()
3284 3294
3285 3295
3286 3296 class UserRepoToPerm(Base, BaseModel):
3287 3297 __tablename__ = 'repo_to_perm'
3288 3298 __table_args__ = (
3289 3299 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3290 3300 base_table_args
3291 3301 )
3292 3302
3293 3303 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3294 3304 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3295 3305 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3296 3306 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3297 3307
3298 3308 user = relationship('User')
3299 3309 repository = relationship('Repository')
3300 3310 permission = relationship('Permission')
3301 3311
3302 3312 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined')
3303 3313
3304 3314 @classmethod
3305 3315 def create(cls, user, repository, permission):
3306 3316 n = cls()
3307 3317 n.user = user
3308 3318 n.repository = repository
3309 3319 n.permission = permission
3310 3320 Session().add(n)
3311 3321 return n
3312 3322
3313 3323 def __unicode__(self):
3314 3324 return u'<%s => %s >' % (self.user, self.repository)
3315 3325
3316 3326
3317 3327 class UserUserGroupToPerm(Base, BaseModel):
3318 3328 __tablename__ = 'user_user_group_to_perm'
3319 3329 __table_args__ = (
3320 3330 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3321 3331 base_table_args
3322 3332 )
3323 3333
3324 3334 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3325 3335 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3326 3336 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3327 3337 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3328 3338
3329 3339 user = relationship('User')
3330 3340 user_group = relationship('UserGroup')
3331 3341 permission = relationship('Permission')
3332 3342
3333 3343 @classmethod
3334 3344 def create(cls, user, user_group, permission):
3335 3345 n = cls()
3336 3346 n.user = user
3337 3347 n.user_group = user_group
3338 3348 n.permission = permission
3339 3349 Session().add(n)
3340 3350 return n
3341 3351
3342 3352 def __unicode__(self):
3343 3353 return u'<%s => %s >' % (self.user, self.user_group)
3344 3354
3345 3355
3346 3356 class UserToPerm(Base, BaseModel):
3347 3357 __tablename__ = 'user_to_perm'
3348 3358 __table_args__ = (
3349 3359 UniqueConstraint('user_id', 'permission_id'),
3350 3360 base_table_args
3351 3361 )
3352 3362
3353 3363 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3354 3364 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3355 3365 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3356 3366
3357 3367 user = relationship('User')
3358 3368 permission = relationship('Permission', lazy='joined')
3359 3369
3360 3370 def __unicode__(self):
3361 3371 return u'<%s => %s >' % (self.user, self.permission)
3362 3372
3363 3373
3364 3374 class UserGroupRepoToPerm(Base, BaseModel):
3365 3375 __tablename__ = 'users_group_repo_to_perm'
3366 3376 __table_args__ = (
3367 3377 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3368 3378 base_table_args
3369 3379 )
3370 3380
3371 3381 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3372 3382 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3373 3383 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3374 3384 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3375 3385
3376 3386 users_group = relationship('UserGroup')
3377 3387 permission = relationship('Permission')
3378 3388 repository = relationship('Repository')
3379 3389 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all')
3380 3390
3381 3391 @classmethod
3382 3392 def create(cls, users_group, repository, permission):
3383 3393 n = cls()
3384 3394 n.users_group = users_group
3385 3395 n.repository = repository
3386 3396 n.permission = permission
3387 3397 Session().add(n)
3388 3398 return n
3389 3399
3390 3400 def __unicode__(self):
3391 3401 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
3392 3402
3393 3403
3394 3404 class UserGroupUserGroupToPerm(Base, BaseModel):
3395 3405 __tablename__ = 'user_group_user_group_to_perm'
3396 3406 __table_args__ = (
3397 3407 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3398 3408 CheckConstraint('target_user_group_id != user_group_id'),
3399 3409 base_table_args
3400 3410 )
3401 3411
3402 3412 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3403 3413 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3404 3414 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3405 3415 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3406 3416
3407 3417 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
3408 3418 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3409 3419 permission = relationship('Permission')
3410 3420
3411 3421 @classmethod
3412 3422 def create(cls, target_user_group, user_group, permission):
3413 3423 n = cls()
3414 3424 n.target_user_group = target_user_group
3415 3425 n.user_group = user_group
3416 3426 n.permission = permission
3417 3427 Session().add(n)
3418 3428 return n
3419 3429
3420 3430 def __unicode__(self):
3421 3431 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
3422 3432
3423 3433
3424 3434 class UserGroupToPerm(Base, BaseModel):
3425 3435 __tablename__ = 'users_group_to_perm'
3426 3436 __table_args__ = (
3427 3437 UniqueConstraint('users_group_id', 'permission_id',),
3428 3438 base_table_args
3429 3439 )
3430 3440
3431 3441 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3432 3442 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3433 3443 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3434 3444
3435 3445 users_group = relationship('UserGroup')
3436 3446 permission = relationship('Permission')
3437 3447
3438 3448
3439 3449 class UserRepoGroupToPerm(Base, BaseModel):
3440 3450 __tablename__ = 'user_repo_group_to_perm'
3441 3451 __table_args__ = (
3442 3452 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3443 3453 base_table_args
3444 3454 )
3445 3455
3446 3456 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3447 3457 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3448 3458 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3449 3459 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3450 3460
3451 3461 user = relationship('User')
3452 3462 group = relationship('RepoGroup')
3453 3463 permission = relationship('Permission')
3454 3464
3455 3465 @classmethod
3456 3466 def create(cls, user, repository_group, permission):
3457 3467 n = cls()
3458 3468 n.user = user
3459 3469 n.group = repository_group
3460 3470 n.permission = permission
3461 3471 Session().add(n)
3462 3472 return n
3463 3473
3464 3474
3465 3475 class UserGroupRepoGroupToPerm(Base, BaseModel):
3466 3476 __tablename__ = 'users_group_repo_group_to_perm'
3467 3477 __table_args__ = (
3468 3478 UniqueConstraint('users_group_id', 'group_id'),
3469 3479 base_table_args
3470 3480 )
3471 3481
3472 3482 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3473 3483 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3474 3484 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3475 3485 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3476 3486
3477 3487 users_group = relationship('UserGroup')
3478 3488 permission = relationship('Permission')
3479 3489 group = relationship('RepoGroup')
3480 3490
3481 3491 @classmethod
3482 3492 def create(cls, user_group, repository_group, permission):
3483 3493 n = cls()
3484 3494 n.users_group = user_group
3485 3495 n.group = repository_group
3486 3496 n.permission = permission
3487 3497 Session().add(n)
3488 3498 return n
3489 3499
3490 3500 def __unicode__(self):
3491 3501 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3492 3502
3493 3503
3494 3504 class Statistics(Base, BaseModel):
3495 3505 __tablename__ = 'statistics'
3496 3506 __table_args__ = (
3497 3507 base_table_args
3498 3508 )
3499 3509
3500 3510 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3501 3511 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3502 3512 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3503 3513 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3504 3514 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3505 3515 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3506 3516
3507 3517 repository = relationship('Repository', single_parent=True)
3508 3518
3509 3519
3510 3520 class UserFollowing(Base, BaseModel):
3511 3521 __tablename__ = 'user_followings'
3512 3522 __table_args__ = (
3513 3523 UniqueConstraint('user_id', 'follows_repository_id'),
3514 3524 UniqueConstraint('user_id', 'follows_user_id'),
3515 3525 base_table_args
3516 3526 )
3517 3527
3518 3528 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3519 3529 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3520 3530 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3521 3531 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3522 3532 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3523 3533
3524 3534 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3525 3535
3526 3536 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3527 3537 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3528 3538
3529 3539 @classmethod
3530 3540 def get_repo_followers(cls, repo_id):
3531 3541 return cls.query().filter(cls.follows_repo_id == repo_id)
3532 3542
3533 3543
3534 3544 class CacheKey(Base, BaseModel):
3535 3545 __tablename__ = 'cache_invalidation'
3536 3546 __table_args__ = (
3537 3547 UniqueConstraint('cache_key'),
3538 3548 Index('key_idx', 'cache_key'),
3539 3549 base_table_args,
3540 3550 )
3541 3551
3542 3552 CACHE_TYPE_FEED = 'FEED'
3543 3553
3544 3554 # namespaces used to register process/thread aware caches
3545 3555 REPO_INVALIDATION_NAMESPACE = 'repo_cache:{repo_id}'
3546 3556 SETTINGS_INVALIDATION_NAMESPACE = 'system_settings'
3547 3557
3548 3558 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3549 3559 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3550 3560 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3551 3561 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3552 3562 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3553 3563
3554 3564 def __init__(self, cache_key, cache_args='', cache_state_uid=None):
3555 3565 self.cache_key = cache_key
3556 3566 self.cache_args = cache_args
3557 3567 self.cache_active = False
3558 3568 # first key should be same for all entries, since all workers should share it
3559 3569 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3560 3570
3561 3571 def __unicode__(self):
3562 3572 return u"<%s('%s:%s[%s]')>" % (
3563 3573 self.__class__.__name__,
3564 3574 self.cache_id, self.cache_key, self.cache_active)
3565 3575
3566 3576 def _cache_key_partition(self):
3567 3577 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3568 3578 return prefix, repo_name, suffix
3569 3579
3570 3580 def get_prefix(self):
3571 3581 """
3572 3582 Try to extract prefix from existing cache key. The key could consist
3573 3583 of prefix, repo_name, suffix
3574 3584 """
3575 3585 # this returns prefix, repo_name, suffix
3576 3586 return self._cache_key_partition()[0]
3577 3587
3578 3588 def get_suffix(self):
3579 3589 """
3580 3590 get suffix that might have been used in _get_cache_key to
3581 3591 generate self.cache_key. Only used for informational purposes
3582 3592 in repo_edit.mako.
3583 3593 """
3584 3594 # prefix, repo_name, suffix
3585 3595 return self._cache_key_partition()[2]
3586 3596
3587 3597 @classmethod
3588 3598 def generate_new_state_uid(cls, based_on=None):
3589 3599 if based_on:
3590 3600 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3591 3601 else:
3592 3602 return str(uuid.uuid4())
3593 3603
3594 3604 @classmethod
3595 3605 def delete_all_cache(cls):
3596 3606 """
3597 3607 Delete all cache keys from database.
3598 3608 Should only be run when all instances are down and all entries
3599 3609 thus stale.
3600 3610 """
3601 3611 cls.query().delete()
3602 3612 Session().commit()
3603 3613
3604 3614 @classmethod
3605 3615 def set_invalidate(cls, cache_uid, delete=False):
3606 3616 """
3607 3617 Mark all caches of a repo as invalid in the database.
3608 3618 """
3609 3619
3610 3620 try:
3611 3621 qry = Session().query(cls).filter(cls.cache_args == cache_uid)
3612 3622 if delete:
3613 3623 qry.delete()
3614 3624 log.debug('cache objects deleted for cache args %s',
3615 3625 safe_str(cache_uid))
3616 3626 else:
3617 3627 qry.update({"cache_active": False,
3618 3628 "cache_state_uid": cls.generate_new_state_uid()})
3619 3629 log.debug('cache objects marked as invalid for cache args %s',
3620 3630 safe_str(cache_uid))
3621 3631
3622 3632 Session().commit()
3623 3633 except Exception:
3624 3634 log.exception(
3625 3635 'Cache key invalidation failed for cache args %s',
3626 3636 safe_str(cache_uid))
3627 3637 Session().rollback()
3628 3638
3629 3639 @classmethod
3630 3640 def get_active_cache(cls, cache_key):
3631 3641 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3632 3642 if inv_obj:
3633 3643 return inv_obj
3634 3644 return None
3635 3645
3636 3646 @classmethod
3637 3647 def get_namespace_map(cls, namespace):
3638 3648 return {
3639 3649 x.cache_key: x
3640 3650 for x in cls.query().filter(cls.cache_args == namespace)}
3641 3651
3642 3652
3643 3653 class ChangesetComment(Base, BaseModel):
3644 3654 __tablename__ = 'changeset_comments'
3645 3655 __table_args__ = (
3646 3656 Index('cc_revision_idx', 'revision'),
3647 3657 base_table_args,
3648 3658 )
3649 3659
3650 3660 COMMENT_OUTDATED = u'comment_outdated'
3651 3661 COMMENT_TYPE_NOTE = u'note'
3652 3662 COMMENT_TYPE_TODO = u'todo'
3653 3663 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3654 3664
3655 3665 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3656 3666 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3657 3667 revision = Column('revision', String(40), nullable=True)
3658 3668 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3659 3669 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3660 3670 line_no = Column('line_no', Unicode(10), nullable=True)
3661 3671 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3662 3672 f_path = Column('f_path', Unicode(1000), nullable=True)
3663 3673 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3664 3674 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3665 3675 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3666 3676 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3667 3677 renderer = Column('renderer', Unicode(64), nullable=True)
3668 3678 display_state = Column('display_state', Unicode(128), nullable=True)
3669 3679
3670 3680 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3671 3681 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3672 3682
3673 3683 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3674 3684 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3675 3685
3676 3686 author = relationship('User', lazy='joined')
3677 3687 repo = relationship('Repository')
3678 3688 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='joined')
3679 3689 pull_request = relationship('PullRequest', lazy='joined')
3680 3690 pull_request_version = relationship('PullRequestVersion')
3681 3691
3682 3692 @classmethod
3683 3693 def get_users(cls, revision=None, pull_request_id=None):
3684 3694 """
3685 3695 Returns user associated with this ChangesetComment. ie those
3686 3696 who actually commented
3687 3697
3688 3698 :param cls:
3689 3699 :param revision:
3690 3700 """
3691 3701 q = Session().query(User)\
3692 3702 .join(ChangesetComment.author)
3693 3703 if revision:
3694 3704 q = q.filter(cls.revision == revision)
3695 3705 elif pull_request_id:
3696 3706 q = q.filter(cls.pull_request_id == pull_request_id)
3697 3707 return q.all()
3698 3708
3699 3709 @classmethod
3700 3710 def get_index_from_version(cls, pr_version, versions):
3701 3711 num_versions = [x.pull_request_version_id for x in versions]
3702 3712 try:
3703 3713 return num_versions.index(pr_version) +1
3704 3714 except (IndexError, ValueError):
3705 3715 return
3706 3716
3707 3717 @property
3708 3718 def outdated(self):
3709 3719 return self.display_state == self.COMMENT_OUTDATED
3710 3720
3711 3721 def outdated_at_version(self, version):
3712 3722 """
3713 3723 Checks if comment is outdated for given pull request version
3714 3724 """
3715 3725 return self.outdated and self.pull_request_version_id != version
3716 3726
3717 3727 def older_than_version(self, version):
3718 3728 """
3719 3729 Checks if comment is made from previous version than given
3720 3730 """
3721 3731 if version is None:
3722 3732 return self.pull_request_version_id is not None
3723 3733
3724 3734 return self.pull_request_version_id < version
3725 3735
3726 3736 @property
3727 3737 def resolved(self):
3728 3738 return self.resolved_by[0] if self.resolved_by else None
3729 3739
3730 3740 @property
3731 3741 def is_todo(self):
3732 3742 return self.comment_type == self.COMMENT_TYPE_TODO
3733 3743
3734 3744 @property
3735 3745 def is_inline(self):
3736 3746 return self.line_no and self.f_path
3737 3747
3738 3748 def get_index_version(self, versions):
3739 3749 return self.get_index_from_version(
3740 3750 self.pull_request_version_id, versions)
3741 3751
3742 3752 def __repr__(self):
3743 3753 if self.comment_id:
3744 3754 return '<DB:Comment #%s>' % self.comment_id
3745 3755 else:
3746 3756 return '<DB:Comment at %#x>' % id(self)
3747 3757
3748 3758 def get_api_data(self):
3749 3759 comment = self
3750 3760 data = {
3751 3761 'comment_id': comment.comment_id,
3752 3762 'comment_type': comment.comment_type,
3753 3763 'comment_text': comment.text,
3754 3764 'comment_status': comment.status_change,
3755 3765 'comment_f_path': comment.f_path,
3756 3766 'comment_lineno': comment.line_no,
3757 3767 'comment_author': comment.author,
3758 3768 'comment_created_on': comment.created_on,
3759 3769 'comment_resolved_by': self.resolved
3760 3770 }
3761 3771 return data
3762 3772
3763 3773 def __json__(self):
3764 3774 data = dict()
3765 3775 data.update(self.get_api_data())
3766 3776 return data
3767 3777
3768 3778
3769 3779 class ChangesetStatus(Base, BaseModel):
3770 3780 __tablename__ = 'changeset_statuses'
3771 3781 __table_args__ = (
3772 3782 Index('cs_revision_idx', 'revision'),
3773 3783 Index('cs_version_idx', 'version'),
3774 3784 UniqueConstraint('repo_id', 'revision', 'version'),
3775 3785 base_table_args
3776 3786 )
3777 3787
3778 3788 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3779 3789 STATUS_APPROVED = 'approved'
3780 3790 STATUS_REJECTED = 'rejected'
3781 3791 STATUS_UNDER_REVIEW = 'under_review'
3782 3792
3783 3793 STATUSES = [
3784 3794 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3785 3795 (STATUS_APPROVED, _("Approved")),
3786 3796 (STATUS_REJECTED, _("Rejected")),
3787 3797 (STATUS_UNDER_REVIEW, _("Under Review")),
3788 3798 ]
3789 3799
3790 3800 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3791 3801 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3792 3802 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3793 3803 revision = Column('revision', String(40), nullable=False)
3794 3804 status = Column('status', String(128), nullable=False, default=DEFAULT)
3795 3805 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3796 3806 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3797 3807 version = Column('version', Integer(), nullable=False, default=0)
3798 3808 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3799 3809
3800 3810 author = relationship('User', lazy='joined')
3801 3811 repo = relationship('Repository')
3802 3812 comment = relationship('ChangesetComment', lazy='joined')
3803 3813 pull_request = relationship('PullRequest', lazy='joined')
3804 3814
3805 3815 def __unicode__(self):
3806 3816 return u"<%s('%s[v%s]:%s')>" % (
3807 3817 self.__class__.__name__,
3808 3818 self.status, self.version, self.author
3809 3819 )
3810 3820
3811 3821 @classmethod
3812 3822 def get_status_lbl(cls, value):
3813 3823 return dict(cls.STATUSES).get(value)
3814 3824
3815 3825 @property
3816 3826 def status_lbl(self):
3817 3827 return ChangesetStatus.get_status_lbl(self.status)
3818 3828
3819 3829 def get_api_data(self):
3820 3830 status = self
3821 3831 data = {
3822 3832 'status_id': status.changeset_status_id,
3823 3833 'status': status.status,
3824 3834 }
3825 3835 return data
3826 3836
3827 3837 def __json__(self):
3828 3838 data = dict()
3829 3839 data.update(self.get_api_data())
3830 3840 return data
3831 3841
3832 3842
3833 3843 class _SetState(object):
3834 3844 """
3835 3845 Context processor allowing changing state for sensitive operation such as
3836 3846 pull request update or merge
3837 3847 """
3838 3848
3839 3849 def __init__(self, pull_request, pr_state, back_state=None):
3840 3850 self._pr = pull_request
3841 3851 self._org_state = back_state or pull_request.pull_request_state
3842 3852 self._pr_state = pr_state
3843 3853 self._current_state = None
3844 3854
3845 3855 def __enter__(self):
3846 3856 log.debug('StateLock: entering set state context, setting state to: `%s`',
3847 3857 self._pr_state)
3848 3858 self.set_pr_state(self._pr_state)
3849 3859 return self
3850 3860
3851 3861 def __exit__(self, exc_type, exc_val, exc_tb):
3852 3862 if exc_val is not None:
3853 3863 log.error(traceback.format_exc(exc_tb))
3854 3864 return None
3855 3865
3856 3866 self.set_pr_state(self._org_state)
3857 3867 log.debug('StateLock: exiting set state context, setting state to: `%s`',
3858 3868 self._org_state)
3859 3869 @property
3860 3870 def state(self):
3861 3871 return self._current_state
3862 3872
3863 3873 def set_pr_state(self, pr_state):
3864 3874 try:
3865 3875 self._pr.pull_request_state = pr_state
3866 3876 Session().add(self._pr)
3867 3877 Session().commit()
3868 3878 self._current_state = pr_state
3869 3879 except Exception:
3870 3880 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
3871 3881 raise
3872 3882
3883
3873 3884 class _PullRequestBase(BaseModel):
3874 3885 """
3875 3886 Common attributes of pull request and version entries.
3876 3887 """
3877 3888
3878 3889 # .status values
3879 3890 STATUS_NEW = u'new'
3880 3891 STATUS_OPEN = u'open'
3881 3892 STATUS_CLOSED = u'closed'
3882 3893
3883 3894 # available states
3884 3895 STATE_CREATING = u'creating'
3885 3896 STATE_UPDATING = u'updating'
3886 3897 STATE_MERGING = u'merging'
3887 3898 STATE_CREATED = u'created'
3888 3899
3889 3900 title = Column('title', Unicode(255), nullable=True)
3890 3901 description = Column(
3891 3902 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3892 3903 nullable=True)
3893 3904 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
3894 3905
3895 3906 # new/open/closed status of pull request (not approve/reject/etc)
3896 3907 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3897 3908 created_on = Column(
3898 3909 'created_on', DateTime(timezone=False), nullable=False,
3899 3910 default=datetime.datetime.now)
3900 3911 updated_on = Column(
3901 3912 'updated_on', DateTime(timezone=False), nullable=False,
3902 3913 default=datetime.datetime.now)
3903 3914
3904 3915 pull_request_state = Column("pull_request_state", String(255), nullable=True)
3905 3916
3906 3917 @declared_attr
3907 3918 def user_id(cls):
3908 3919 return Column(
3909 3920 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3910 3921 unique=None)
3911 3922
3912 3923 # 500 revisions max
3913 3924 _revisions = Column(
3914 3925 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3915 3926
3916 3927 @declared_attr
3917 3928 def source_repo_id(cls):
3918 3929 # TODO: dan: rename column to source_repo_id
3919 3930 return Column(
3920 3931 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3921 3932 nullable=False)
3922 3933
3923 3934 _source_ref = Column('org_ref', Unicode(255), nullable=False)
3924 3935
3925 3936 @hybrid_property
3926 3937 def source_ref(self):
3927 3938 return self._source_ref
3928 3939
3929 3940 @source_ref.setter
3930 3941 def source_ref(self, val):
3931 3942 parts = (val or '').split(':')
3932 3943 if len(parts) != 3:
3933 3944 raise ValueError(
3934 3945 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
3935 3946 self._source_ref = safe_unicode(val)
3936 3947
3937 3948 _target_ref = Column('other_ref', Unicode(255), nullable=False)
3938 3949
3939 3950 @hybrid_property
3940 3951 def target_ref(self):
3941 3952 return self._target_ref
3942 3953
3943 3954 @target_ref.setter
3944 3955 def target_ref(self, val):
3945 3956 parts = (val or '').split(':')
3946 3957 if len(parts) != 3:
3947 3958 raise ValueError(
3948 3959 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
3949 3960 self._target_ref = safe_unicode(val)
3950 3961
3951 3962 @declared_attr
3952 3963 def target_repo_id(cls):
3953 3964 # TODO: dan: rename column to target_repo_id
3954 3965 return Column(
3955 3966 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3956 3967 nullable=False)
3957 3968
3958 3969 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3959 3970
3960 3971 # TODO: dan: rename column to last_merge_source_rev
3961 3972 _last_merge_source_rev = Column(
3962 3973 'last_merge_org_rev', String(40), nullable=True)
3963 3974 # TODO: dan: rename column to last_merge_target_rev
3964 3975 _last_merge_target_rev = Column(
3965 3976 'last_merge_other_rev', String(40), nullable=True)
3966 3977 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3967 3978 merge_rev = Column('merge_rev', String(40), nullable=True)
3968 3979
3969 3980 reviewer_data = Column(
3970 3981 'reviewer_data_json', MutationObj.as_mutable(
3971 3982 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3972 3983
3973 3984 @property
3974 3985 def reviewer_data_json(self):
3975 3986 return json.dumps(self.reviewer_data)
3976 3987
3977 3988 @hybrid_property
3978 3989 def description_safe(self):
3979 3990 from rhodecode.lib import helpers as h
3980 3991 return h.escape(self.description)
3981 3992
3982 3993 @hybrid_property
3983 3994 def revisions(self):
3984 3995 return self._revisions.split(':') if self._revisions else []
3985 3996
3986 3997 @revisions.setter
3987 3998 def revisions(self, val):
3988 3999 self._revisions = u':'.join(val)
3989 4000
3990 4001 @hybrid_property
3991 4002 def last_merge_status(self):
3992 4003 return safe_int(self._last_merge_status)
3993 4004
3994 4005 @last_merge_status.setter
3995 4006 def last_merge_status(self, val):
3996 4007 self._last_merge_status = val
3997 4008
3998 4009 @declared_attr
3999 4010 def author(cls):
4000 4011 return relationship('User', lazy='joined')
4001 4012
4002 4013 @declared_attr
4003 4014 def source_repo(cls):
4004 4015 return relationship(
4005 4016 'Repository',
4006 4017 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
4007 4018
4008 4019 @property
4009 4020 def source_ref_parts(self):
4010 4021 return self.unicode_to_reference(self.source_ref)
4011 4022
4012 4023 @declared_attr
4013 4024 def target_repo(cls):
4014 4025 return relationship(
4015 4026 'Repository',
4016 4027 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
4017 4028
4018 4029 @property
4019 4030 def target_ref_parts(self):
4020 4031 return self.unicode_to_reference(self.target_ref)
4021 4032
4022 4033 @property
4023 4034 def shadow_merge_ref(self):
4024 4035 return self.unicode_to_reference(self._shadow_merge_ref)
4025 4036
4026 4037 @shadow_merge_ref.setter
4027 4038 def shadow_merge_ref(self, ref):
4028 4039 self._shadow_merge_ref = self.reference_to_unicode(ref)
4029 4040
4030 4041 @staticmethod
4031 4042 def unicode_to_reference(raw):
4032 4043 """
4033 4044 Convert a unicode (or string) to a reference object.
4034 4045 If unicode evaluates to False it returns None.
4035 4046 """
4036 4047 if raw:
4037 4048 refs = raw.split(':')
4038 4049 return Reference(*refs)
4039 4050 else:
4040 4051 return None
4041 4052
4042 4053 @staticmethod
4043 4054 def reference_to_unicode(ref):
4044 4055 """
4045 4056 Convert a reference object to unicode.
4046 4057 If reference is None it returns None.
4047 4058 """
4048 4059 if ref:
4049 4060 return u':'.join(ref)
4050 4061 else:
4051 4062 return None
4052 4063
4053 4064 def get_api_data(self, with_merge_state=True):
4054 4065 from rhodecode.model.pull_request import PullRequestModel
4055 4066
4056 4067 pull_request = self
4057 4068 if with_merge_state:
4058 4069 merge_status = PullRequestModel().merge_status(pull_request)
4059 4070 merge_state = {
4060 4071 'status': merge_status[0],
4061 4072 'message': safe_unicode(merge_status[1]),
4062 4073 }
4063 4074 else:
4064 4075 merge_state = {'status': 'not_available',
4065 4076 'message': 'not_available'}
4066 4077
4067 4078 merge_data = {
4068 4079 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4069 4080 'reference': (
4070 4081 pull_request.shadow_merge_ref._asdict()
4071 4082 if pull_request.shadow_merge_ref else None),
4072 4083 }
4073 4084
4074 4085 data = {
4075 4086 'pull_request_id': pull_request.pull_request_id,
4076 4087 'url': PullRequestModel().get_url(pull_request),
4077 4088 'title': pull_request.title,
4078 4089 'description': pull_request.description,
4079 4090 'status': pull_request.status,
4080 4091 'state': pull_request.pull_request_state,
4081 4092 'created_on': pull_request.created_on,
4082 4093 'updated_on': pull_request.updated_on,
4083 4094 'commit_ids': pull_request.revisions,
4084 4095 'review_status': pull_request.calculated_review_status(),
4085 4096 'mergeable': merge_state,
4086 4097 'source': {
4087 4098 'clone_url': pull_request.source_repo.clone_url(),
4088 4099 'repository': pull_request.source_repo.repo_name,
4089 4100 'reference': {
4090 4101 'name': pull_request.source_ref_parts.name,
4091 4102 'type': pull_request.source_ref_parts.type,
4092 4103 'commit_id': pull_request.source_ref_parts.commit_id,
4093 4104 },
4094 4105 },
4095 4106 'target': {
4096 4107 'clone_url': pull_request.target_repo.clone_url(),
4097 4108 'repository': pull_request.target_repo.repo_name,
4098 4109 'reference': {
4099 4110 'name': pull_request.target_ref_parts.name,
4100 4111 'type': pull_request.target_ref_parts.type,
4101 4112 'commit_id': pull_request.target_ref_parts.commit_id,
4102 4113 },
4103 4114 },
4104 4115 'merge': merge_data,
4105 4116 'author': pull_request.author.get_api_data(include_secrets=False,
4106 4117 details='basic'),
4107 4118 'reviewers': [
4108 4119 {
4109 4120 'user': reviewer.get_api_data(include_secrets=False,
4110 4121 details='basic'),
4111 4122 'reasons': reasons,
4112 4123 'review_status': st[0][1].status if st else 'not_reviewed',
4113 4124 }
4114 4125 for obj, reviewer, reasons, mandatory, st in
4115 4126 pull_request.reviewers_statuses()
4116 4127 ]
4117 4128 }
4118 4129
4119 4130 return data
4120 4131
4121 4132 def set_state(self, pull_request_state, final_state=None):
4122 4133 """
4123 4134 # goes from initial state to updating to initial state.
4124 4135 # initial state can be changed by specifying back_state=
4125 4136 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4126 4137 pull_request.merge()
4127 4138
4128 4139 :param pull_request_state:
4129 4140 :param final_state:
4130 4141
4131 4142 """
4132 4143
4133 4144 return _SetState(self, pull_request_state, back_state=final_state)
4134 4145
4135 4146
4136 4147 class PullRequest(Base, _PullRequestBase):
4137 4148 __tablename__ = 'pull_requests'
4138 4149 __table_args__ = (
4139 4150 base_table_args,
4140 4151 )
4141 4152
4142 4153 pull_request_id = Column(
4143 4154 'pull_request_id', Integer(), nullable=False, primary_key=True)
4144 4155
4145 4156 def __repr__(self):
4146 4157 if self.pull_request_id:
4147 4158 return '<DB:PullRequest #%s>' % self.pull_request_id
4148 4159 else:
4149 4160 return '<DB:PullRequest at %#x>' % id(self)
4150 4161
4151 reviewers = relationship('PullRequestReviewers',
4152 cascade="all, delete-orphan")
4153 statuses = relationship('ChangesetStatus',
4154 cascade="all, delete-orphan")
4155 comments = relationship('ChangesetComment',
4156 cascade="all, delete-orphan")
4157 versions = relationship('PullRequestVersion',
4158 cascade="all, delete-orphan",
4162 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan")
4163 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan")
4164 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
4165 versions = relationship('PullRequestVersion', cascade="all, delete-orphan",
4159 4166 lazy='dynamic')
4160 4167
4161 4168 @classmethod
4162 4169 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4163 4170 internal_methods=None):
4164 4171
4165 4172 class PullRequestDisplay(object):
4166 4173 """
4167 4174 Special object wrapper for showing PullRequest data via Versions
4168 4175 It mimics PR object as close as possible. This is read only object
4169 4176 just for display
4170 4177 """
4171 4178
4172 4179 def __init__(self, attrs, internal=None):
4173 4180 self.attrs = attrs
4174 4181 # internal have priority over the given ones via attrs
4175 4182 self.internal = internal or ['versions']
4176 4183
4177 4184 def __getattr__(self, item):
4178 4185 if item in self.internal:
4179 4186 return getattr(self, item)
4180 4187 try:
4181 4188 return self.attrs[item]
4182 4189 except KeyError:
4183 4190 raise AttributeError(
4184 4191 '%s object has no attribute %s' % (self, item))
4185 4192
4186 4193 def __repr__(self):
4187 4194 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
4188 4195
4189 4196 def versions(self):
4190 4197 return pull_request_obj.versions.order_by(
4191 4198 PullRequestVersion.pull_request_version_id).all()
4192 4199
4193 4200 def is_closed(self):
4194 4201 return pull_request_obj.is_closed()
4195 4202
4196 4203 @property
4197 4204 def pull_request_version_id(self):
4198 4205 return getattr(pull_request_obj, 'pull_request_version_id', None)
4199 4206
4200 4207 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4201 4208
4202 4209 attrs.author = StrictAttributeDict(
4203 4210 pull_request_obj.author.get_api_data())
4204 4211 if pull_request_obj.target_repo:
4205 4212 attrs.target_repo = StrictAttributeDict(
4206 4213 pull_request_obj.target_repo.get_api_data())
4207 4214 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4208 4215
4209 4216 if pull_request_obj.source_repo:
4210 4217 attrs.source_repo = StrictAttributeDict(
4211 4218 pull_request_obj.source_repo.get_api_data())
4212 4219 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4213 4220
4214 4221 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4215 4222 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4216 4223 attrs.revisions = pull_request_obj.revisions
4217 4224
4218 4225 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4219 4226 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4220 4227 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4221 4228
4222 4229 return PullRequestDisplay(attrs, internal=internal_methods)
4223 4230
4224 4231 def is_closed(self):
4225 4232 return self.status == self.STATUS_CLOSED
4226 4233
4227 4234 def __json__(self):
4228 4235 return {
4229 4236 'revisions': self.revisions,
4230 4237 }
4231 4238
4232 4239 def calculated_review_status(self):
4233 4240 from rhodecode.model.changeset_status import ChangesetStatusModel
4234 4241 return ChangesetStatusModel().calculated_review_status(self)
4235 4242
4236 4243 def reviewers_statuses(self):
4237 4244 from rhodecode.model.changeset_status import ChangesetStatusModel
4238 4245 return ChangesetStatusModel().reviewers_statuses(self)
4239 4246
4240 4247 @property
4241 4248 def workspace_id(self):
4242 4249 from rhodecode.model.pull_request import PullRequestModel
4243 4250 return PullRequestModel()._workspace_id(self)
4244 4251
4245 4252 def get_shadow_repo(self):
4246 4253 workspace_id = self.workspace_id
4247 4254 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4248 4255 if os.path.isdir(shadow_repository_path):
4249 4256 vcs_obj = self.target_repo.scm_instance()
4250 4257 return vcs_obj.get_shadow_instance(shadow_repository_path)
4251 4258
4252 4259
4253 4260 class PullRequestVersion(Base, _PullRequestBase):
4254 4261 __tablename__ = 'pull_request_versions'
4255 4262 __table_args__ = (
4256 4263 base_table_args,
4257 4264 )
4258 4265
4259 4266 pull_request_version_id = Column(
4260 4267 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
4261 4268 pull_request_id = Column(
4262 4269 'pull_request_id', Integer(),
4263 4270 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4264 4271 pull_request = relationship('PullRequest')
4265 4272
4266 4273 def __repr__(self):
4267 4274 if self.pull_request_version_id:
4268 4275 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
4269 4276 else:
4270 4277 return '<DB:PullRequestVersion at %#x>' % id(self)
4271 4278
4272 4279 @property
4273 4280 def reviewers(self):
4274 4281 return self.pull_request.reviewers
4275 4282
4276 4283 @property
4277 4284 def versions(self):
4278 4285 return self.pull_request.versions
4279 4286
4280 4287 def is_closed(self):
4281 4288 # calculate from original
4282 4289 return self.pull_request.status == self.STATUS_CLOSED
4283 4290
4284 4291 def calculated_review_status(self):
4285 4292 return self.pull_request.calculated_review_status()
4286 4293
4287 4294 def reviewers_statuses(self):
4288 4295 return self.pull_request.reviewers_statuses()
4289 4296
4290 4297
4291 4298 class PullRequestReviewers(Base, BaseModel):
4292 4299 __tablename__ = 'pull_request_reviewers'
4293 4300 __table_args__ = (
4294 4301 base_table_args,
4295 4302 )
4296 4303
4297 4304 @hybrid_property
4298 4305 def reasons(self):
4299 4306 if not self._reasons:
4300 4307 return []
4301 4308 return self._reasons
4302 4309
4303 4310 @reasons.setter
4304 4311 def reasons(self, val):
4305 4312 val = val or []
4306 4313 if any(not isinstance(x, compat.string_types) for x in val):
4307 4314 raise Exception('invalid reasons type, must be list of strings')
4308 4315 self._reasons = val
4309 4316
4310 4317 pull_requests_reviewers_id = Column(
4311 4318 'pull_requests_reviewers_id', Integer(), nullable=False,
4312 4319 primary_key=True)
4313 4320 pull_request_id = Column(
4314 4321 "pull_request_id", Integer(),
4315 4322 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4316 4323 user_id = Column(
4317 4324 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4318 4325 _reasons = Column(
4319 4326 'reason', MutationList.as_mutable(
4320 4327 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4321 4328
4322 4329 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4323 4330 user = relationship('User')
4324 4331 pull_request = relationship('PullRequest')
4325 4332
4326 4333 rule_data = Column(
4327 4334 'rule_data_json',
4328 4335 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4329 4336
4330 4337 def rule_user_group_data(self):
4331 4338 """
4332 4339 Returns the voting user group rule data for this reviewer
4333 4340 """
4334 4341
4335 4342 if self.rule_data and 'vote_rule' in self.rule_data:
4336 4343 user_group_data = {}
4337 4344 if 'rule_user_group_entry_id' in self.rule_data:
4338 4345 # means a group with voting rules !
4339 4346 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4340 4347 user_group_data['name'] = self.rule_data['rule_name']
4341 4348 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4342 4349
4343 4350 return user_group_data
4344 4351
4345 4352 def __unicode__(self):
4346 4353 return u"<%s('id:%s')>" % (self.__class__.__name__,
4347 4354 self.pull_requests_reviewers_id)
4348 4355
4349 4356
4350 4357 class Notification(Base, BaseModel):
4351 4358 __tablename__ = 'notifications'
4352 4359 __table_args__ = (
4353 4360 Index('notification_type_idx', 'type'),
4354 4361 base_table_args,
4355 4362 )
4356 4363
4357 4364 TYPE_CHANGESET_COMMENT = u'cs_comment'
4358 4365 TYPE_MESSAGE = u'message'
4359 4366 TYPE_MENTION = u'mention'
4360 4367 TYPE_REGISTRATION = u'registration'
4361 4368 TYPE_PULL_REQUEST = u'pull_request'
4362 4369 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
4363 4370
4364 4371 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4365 4372 subject = Column('subject', Unicode(512), nullable=True)
4366 4373 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4367 4374 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4368 4375 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4369 4376 type_ = Column('type', Unicode(255))
4370 4377
4371 4378 created_by_user = relationship('User')
4372 4379 notifications_to_users = relationship('UserNotification', lazy='joined',
4373 4380 cascade="all, delete-orphan")
4374 4381
4375 4382 @property
4376 4383 def recipients(self):
4377 4384 return [x.user for x in UserNotification.query()\
4378 4385 .filter(UserNotification.notification == self)\
4379 4386 .order_by(UserNotification.user_id.asc()).all()]
4380 4387
4381 4388 @classmethod
4382 4389 def create(cls, created_by, subject, body, recipients, type_=None):
4383 4390 if type_ is None:
4384 4391 type_ = Notification.TYPE_MESSAGE
4385 4392
4386 4393 notification = cls()
4387 4394 notification.created_by_user = created_by
4388 4395 notification.subject = subject
4389 4396 notification.body = body
4390 4397 notification.type_ = type_
4391 4398 notification.created_on = datetime.datetime.now()
4392 4399
4393 4400 # For each recipient link the created notification to his account
4394 4401 for u in recipients:
4395 4402 assoc = UserNotification()
4396 4403 assoc.user_id = u.user_id
4397 4404 assoc.notification = notification
4398 4405
4399 4406 # if created_by is inside recipients mark his notification
4400 4407 # as read
4401 4408 if u.user_id == created_by.user_id:
4402 4409 assoc.read = True
4403 4410 Session().add(assoc)
4404 4411
4405 4412 Session().add(notification)
4406 4413
4407 4414 return notification
4408 4415
4409 4416
4410 4417 class UserNotification(Base, BaseModel):
4411 4418 __tablename__ = 'user_to_notification'
4412 4419 __table_args__ = (
4413 4420 UniqueConstraint('user_id', 'notification_id'),
4414 4421 base_table_args
4415 4422 )
4416 4423
4417 4424 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4418 4425 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4419 4426 read = Column('read', Boolean, default=False)
4420 4427 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4421 4428
4422 4429 user = relationship('User', lazy="joined")
4423 4430 notification = relationship('Notification', lazy="joined",
4424 4431 order_by=lambda: Notification.created_on.desc(),)
4425 4432
4426 4433 def mark_as_read(self):
4427 4434 self.read = True
4428 4435 Session().add(self)
4429 4436
4430 4437
4431 4438 class Gist(Base, BaseModel):
4432 4439 __tablename__ = 'gists'
4433 4440 __table_args__ = (
4434 4441 Index('g_gist_access_id_idx', 'gist_access_id'),
4435 4442 Index('g_created_on_idx', 'created_on'),
4436 4443 base_table_args
4437 4444 )
4438 4445
4439 4446 GIST_PUBLIC = u'public'
4440 4447 GIST_PRIVATE = u'private'
4441 4448 DEFAULT_FILENAME = u'gistfile1.txt'
4442 4449
4443 4450 ACL_LEVEL_PUBLIC = u'acl_public'
4444 4451 ACL_LEVEL_PRIVATE = u'acl_private'
4445 4452
4446 4453 gist_id = Column('gist_id', Integer(), primary_key=True)
4447 4454 gist_access_id = Column('gist_access_id', Unicode(250))
4448 4455 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4449 4456 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4450 4457 gist_expires = Column('gist_expires', Float(53), nullable=False)
4451 4458 gist_type = Column('gist_type', Unicode(128), nullable=False)
4452 4459 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4453 4460 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4454 4461 acl_level = Column('acl_level', Unicode(128), nullable=True)
4455 4462
4456 4463 owner = relationship('User')
4457 4464
4458 4465 def __repr__(self):
4459 4466 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
4460 4467
4461 4468 @hybrid_property
4462 4469 def description_safe(self):
4463 4470 from rhodecode.lib import helpers as h
4464 4471 return h.escape(self.gist_description)
4465 4472
4466 4473 @classmethod
4467 4474 def get_or_404(cls, id_):
4468 4475 from pyramid.httpexceptions import HTTPNotFound
4469 4476
4470 4477 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4471 4478 if not res:
4472 4479 raise HTTPNotFound()
4473 4480 return res
4474 4481
4475 4482 @classmethod
4476 4483 def get_by_access_id(cls, gist_access_id):
4477 4484 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4478 4485
4479 4486 def gist_url(self):
4480 4487 from rhodecode.model.gist import GistModel
4481 4488 return GistModel().get_url(self)
4482 4489
4483 4490 @classmethod
4484 4491 def base_path(cls):
4485 4492 """
4486 4493 Returns base path when all gists are stored
4487 4494
4488 4495 :param cls:
4489 4496 """
4490 4497 from rhodecode.model.gist import GIST_STORE_LOC
4491 4498 q = Session().query(RhodeCodeUi)\
4492 4499 .filter(RhodeCodeUi.ui_key == URL_SEP)
4493 4500 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4494 4501 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4495 4502
4496 4503 def get_api_data(self):
4497 4504 """
4498 4505 Common function for generating gist related data for API
4499 4506 """
4500 4507 gist = self
4501 4508 data = {
4502 4509 'gist_id': gist.gist_id,
4503 4510 'type': gist.gist_type,
4504 4511 'access_id': gist.gist_access_id,
4505 4512 'description': gist.gist_description,
4506 4513 'url': gist.gist_url(),
4507 4514 'expires': gist.gist_expires,
4508 4515 'created_on': gist.created_on,
4509 4516 'modified_at': gist.modified_at,
4510 4517 'content': None,
4511 4518 'acl_level': gist.acl_level,
4512 4519 }
4513 4520 return data
4514 4521
4515 4522 def __json__(self):
4516 4523 data = dict(
4517 4524 )
4518 4525 data.update(self.get_api_data())
4519 4526 return data
4520 4527 # SCM functions
4521 4528
4522 4529 def scm_instance(self, **kwargs):
4523 4530 """
4524 4531 Get an instance of VCS Repository
4525 4532
4526 4533 :param kwargs:
4527 4534 """
4528 4535 from rhodecode.model.gist import GistModel
4529 4536 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4530 4537 return get_vcs_instance(
4531 4538 repo_path=safe_str(full_repo_path), create=False,
4532 4539 _vcs_alias=GistModel.vcs_backend)
4533 4540
4534 4541
4535 4542 class ExternalIdentity(Base, BaseModel):
4536 4543 __tablename__ = 'external_identities'
4537 4544 __table_args__ = (
4538 4545 Index('local_user_id_idx', 'local_user_id'),
4539 4546 Index('external_id_idx', 'external_id'),
4540 4547 base_table_args
4541 4548 )
4542 4549
4543 4550 external_id = Column('external_id', Unicode(255), default=u'', primary_key=True)
4544 4551 external_username = Column('external_username', Unicode(1024), default=u'')
4545 4552 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4546 4553 provider_name = Column('provider_name', Unicode(255), default=u'', primary_key=True)
4547 4554 access_token = Column('access_token', String(1024), default=u'')
4548 4555 alt_token = Column('alt_token', String(1024), default=u'')
4549 4556 token_secret = Column('token_secret', String(1024), default=u'')
4550 4557
4551 4558 @classmethod
4552 4559 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4553 4560 """
4554 4561 Returns ExternalIdentity instance based on search params
4555 4562
4556 4563 :param external_id:
4557 4564 :param provider_name:
4558 4565 :return: ExternalIdentity
4559 4566 """
4560 4567 query = cls.query()
4561 4568 query = query.filter(cls.external_id == external_id)
4562 4569 query = query.filter(cls.provider_name == provider_name)
4563 4570 if local_user_id:
4564 4571 query = query.filter(cls.local_user_id == local_user_id)
4565 4572 return query.first()
4566 4573
4567 4574 @classmethod
4568 4575 def user_by_external_id_and_provider(cls, external_id, provider_name):
4569 4576 """
4570 4577 Returns User instance based on search params
4571 4578
4572 4579 :param external_id:
4573 4580 :param provider_name:
4574 4581 :return: User
4575 4582 """
4576 4583 query = User.query()
4577 4584 query = query.filter(cls.external_id == external_id)
4578 4585 query = query.filter(cls.provider_name == provider_name)
4579 4586 query = query.filter(User.user_id == cls.local_user_id)
4580 4587 return query.first()
4581 4588
4582 4589 @classmethod
4583 4590 def by_local_user_id(cls, local_user_id):
4584 4591 """
4585 4592 Returns all tokens for user
4586 4593
4587 4594 :param local_user_id:
4588 4595 :return: ExternalIdentity
4589 4596 """
4590 4597 query = cls.query()
4591 4598 query = query.filter(cls.local_user_id == local_user_id)
4592 4599 return query
4593 4600
4594 4601 @classmethod
4595 4602 def load_provider_plugin(cls, plugin_id):
4596 4603 from rhodecode.authentication.base import loadplugin
4597 4604 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
4598 4605 auth_plugin = loadplugin(_plugin_id)
4599 4606 return auth_plugin
4600 4607
4601 4608
4602 4609 class Integration(Base, BaseModel):
4603 4610 __tablename__ = 'integrations'
4604 4611 __table_args__ = (
4605 4612 base_table_args
4606 4613 )
4607 4614
4608 4615 integration_id = Column('integration_id', Integer(), primary_key=True)
4609 4616 integration_type = Column('integration_type', String(255))
4610 4617 enabled = Column('enabled', Boolean(), nullable=False)
4611 4618 name = Column('name', String(255), nullable=False)
4612 4619 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4613 4620 default=False)
4614 4621
4615 4622 settings = Column(
4616 4623 'settings_json', MutationObj.as_mutable(
4617 4624 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4618 4625 repo_id = Column(
4619 4626 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4620 4627 nullable=True, unique=None, default=None)
4621 4628 repo = relationship('Repository', lazy='joined')
4622 4629
4623 4630 repo_group_id = Column(
4624 4631 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4625 4632 nullable=True, unique=None, default=None)
4626 4633 repo_group = relationship('RepoGroup', lazy='joined')
4627 4634
4628 4635 @property
4629 4636 def scope(self):
4630 4637 if self.repo:
4631 4638 return repr(self.repo)
4632 4639 if self.repo_group:
4633 4640 if self.child_repos_only:
4634 4641 return repr(self.repo_group) + ' (child repos only)'
4635 4642 else:
4636 4643 return repr(self.repo_group) + ' (recursive)'
4637 4644 if self.child_repos_only:
4638 4645 return 'root_repos'
4639 4646 return 'global'
4640 4647
4641 4648 def __repr__(self):
4642 4649 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4643 4650
4644 4651
4645 4652 class RepoReviewRuleUser(Base, BaseModel):
4646 4653 __tablename__ = 'repo_review_rules_users'
4647 4654 __table_args__ = (
4648 4655 base_table_args
4649 4656 )
4650 4657
4651 4658 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4652 4659 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4653 4660 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4654 4661 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4655 4662 user = relationship('User')
4656 4663
4657 4664 def rule_data(self):
4658 4665 return {
4659 4666 'mandatory': self.mandatory
4660 4667 }
4661 4668
4662 4669
4663 4670 class RepoReviewRuleUserGroup(Base, BaseModel):
4664 4671 __tablename__ = 'repo_review_rules_users_groups'
4665 4672 __table_args__ = (
4666 4673 base_table_args
4667 4674 )
4668 4675
4669 4676 VOTE_RULE_ALL = -1
4670 4677
4671 4678 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4672 4679 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4673 4680 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4674 4681 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4675 4682 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
4676 4683 users_group = relationship('UserGroup')
4677 4684
4678 4685 def rule_data(self):
4679 4686 return {
4680 4687 'mandatory': self.mandatory,
4681 4688 'vote_rule': self.vote_rule
4682 4689 }
4683 4690
4684 4691 @property
4685 4692 def vote_rule_label(self):
4686 4693 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
4687 4694 return 'all must vote'
4688 4695 else:
4689 4696 return 'min. vote {}'.format(self.vote_rule)
4690 4697
4691 4698
4692 4699 class RepoReviewRule(Base, BaseModel):
4693 4700 __tablename__ = 'repo_review_rules'
4694 4701 __table_args__ = (
4695 4702 base_table_args
4696 4703 )
4697 4704
4698 4705 repo_review_rule_id = Column(
4699 4706 'repo_review_rule_id', Integer(), primary_key=True)
4700 4707 repo_id = Column(
4701 4708 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4702 4709 repo = relationship('Repository', backref='review_rules')
4703 4710
4704 4711 review_rule_name = Column('review_rule_name', String(255))
4705 4712 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4706 4713 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4707 4714 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4708 4715
4709 4716 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4710 4717 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4711 4718 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4712 4719 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4713 4720
4714 4721 rule_users = relationship('RepoReviewRuleUser')
4715 4722 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4716 4723
4717 4724 def _validate_pattern(self, value):
4718 4725 re.compile('^' + glob2re(value) + '$')
4719 4726
4720 4727 @hybrid_property
4721 4728 def source_branch_pattern(self):
4722 4729 return self._branch_pattern or '*'
4723 4730
4724 4731 @source_branch_pattern.setter
4725 4732 def source_branch_pattern(self, value):
4726 4733 self._validate_pattern(value)
4727 4734 self._branch_pattern = value or '*'
4728 4735
4729 4736 @hybrid_property
4730 4737 def target_branch_pattern(self):
4731 4738 return self._target_branch_pattern or '*'
4732 4739
4733 4740 @target_branch_pattern.setter
4734 4741 def target_branch_pattern(self, value):
4735 4742 self._validate_pattern(value)
4736 4743 self._target_branch_pattern = value or '*'
4737 4744
4738 4745 @hybrid_property
4739 4746 def file_pattern(self):
4740 4747 return self._file_pattern or '*'
4741 4748
4742 4749 @file_pattern.setter
4743 4750 def file_pattern(self, value):
4744 4751 self._validate_pattern(value)
4745 4752 self._file_pattern = value or '*'
4746 4753
4747 4754 def matches(self, source_branch, target_branch, files_changed):
4748 4755 """
4749 4756 Check if this review rule matches a branch/files in a pull request
4750 4757
4751 4758 :param source_branch: source branch name for the commit
4752 4759 :param target_branch: target branch name for the commit
4753 4760 :param files_changed: list of file paths changed in the pull request
4754 4761 """
4755 4762
4756 4763 source_branch = source_branch or ''
4757 4764 target_branch = target_branch or ''
4758 4765 files_changed = files_changed or []
4759 4766
4760 4767 branch_matches = True
4761 4768 if source_branch or target_branch:
4762 4769 if self.source_branch_pattern == '*':
4763 4770 source_branch_match = True
4764 4771 else:
4765 4772 if self.source_branch_pattern.startswith('re:'):
4766 4773 source_pattern = self.source_branch_pattern[3:]
4767 4774 else:
4768 4775 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
4769 4776 source_branch_regex = re.compile(source_pattern)
4770 4777 source_branch_match = bool(source_branch_regex.search(source_branch))
4771 4778 if self.target_branch_pattern == '*':
4772 4779 target_branch_match = True
4773 4780 else:
4774 4781 if self.target_branch_pattern.startswith('re:'):
4775 4782 target_pattern = self.target_branch_pattern[3:]
4776 4783 else:
4777 4784 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
4778 4785 target_branch_regex = re.compile(target_pattern)
4779 4786 target_branch_match = bool(target_branch_regex.search(target_branch))
4780 4787
4781 4788 branch_matches = source_branch_match and target_branch_match
4782 4789
4783 4790 files_matches = True
4784 4791 if self.file_pattern != '*':
4785 4792 files_matches = False
4786 4793 if self.file_pattern.startswith('re:'):
4787 4794 file_pattern = self.file_pattern[3:]
4788 4795 else:
4789 4796 file_pattern = glob2re(self.file_pattern)
4790 4797 file_regex = re.compile(file_pattern)
4791 4798 for filename in files_changed:
4792 4799 if file_regex.search(filename):
4793 4800 files_matches = True
4794 4801 break
4795 4802
4796 4803 return branch_matches and files_matches
4797 4804
4798 4805 @property
4799 4806 def review_users(self):
4800 4807 """ Returns the users which this rule applies to """
4801 4808
4802 4809 users = collections.OrderedDict()
4803 4810
4804 4811 for rule_user in self.rule_users:
4805 4812 if rule_user.user.active:
4806 4813 if rule_user.user not in users:
4807 4814 users[rule_user.user.username] = {
4808 4815 'user': rule_user.user,
4809 4816 'source': 'user',
4810 4817 'source_data': {},
4811 4818 'data': rule_user.rule_data()
4812 4819 }
4813 4820
4814 4821 for rule_user_group in self.rule_user_groups:
4815 4822 source_data = {
4816 4823 'user_group_id': rule_user_group.users_group.users_group_id,
4817 4824 'name': rule_user_group.users_group.users_group_name,
4818 4825 'members': len(rule_user_group.users_group.members)
4819 4826 }
4820 4827 for member in rule_user_group.users_group.members:
4821 4828 if member.user.active:
4822 4829 key = member.user.username
4823 4830 if key in users:
4824 4831 # skip this member as we have him already
4825 4832 # this prevents from override the "first" matched
4826 4833 # users with duplicates in multiple groups
4827 4834 continue
4828 4835
4829 4836 users[key] = {
4830 4837 'user': member.user,
4831 4838 'source': 'user_group',
4832 4839 'source_data': source_data,
4833 4840 'data': rule_user_group.rule_data()
4834 4841 }
4835 4842
4836 4843 return users
4837 4844
4838 4845 def user_group_vote_rule(self, user_id):
4839 4846
4840 4847 rules = []
4841 4848 if not self.rule_user_groups:
4842 4849 return rules
4843 4850
4844 4851 for user_group in self.rule_user_groups:
4845 4852 user_group_members = [x.user_id for x in user_group.users_group.members]
4846 4853 if user_id in user_group_members:
4847 4854 rules.append(user_group)
4848 4855 return rules
4849 4856
4850 4857 def __repr__(self):
4851 4858 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4852 4859 self.repo_review_rule_id, self.repo)
4853 4860
4854 4861
4855 4862 class ScheduleEntry(Base, BaseModel):
4856 4863 __tablename__ = 'schedule_entries'
4857 4864 __table_args__ = (
4858 4865 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
4859 4866 UniqueConstraint('task_uid', name='s_task_uid_idx'),
4860 4867 base_table_args,
4861 4868 )
4862 4869
4863 4870 schedule_types = ['crontab', 'timedelta', 'integer']
4864 4871 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
4865 4872
4866 4873 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
4867 4874 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
4868 4875 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
4869 4876
4870 4877 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
4871 4878 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
4872 4879
4873 4880 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
4874 4881 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
4875 4882
4876 4883 # task
4877 4884 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
4878 4885 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
4879 4886 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
4880 4887 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
4881 4888
4882 4889 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4883 4890 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
4884 4891
4885 4892 @hybrid_property
4886 4893 def schedule_type(self):
4887 4894 return self._schedule_type
4888 4895
4889 4896 @schedule_type.setter
4890 4897 def schedule_type(self, val):
4891 4898 if val not in self.schedule_types:
4892 4899 raise ValueError('Value must be on of `{}` and got `{}`'.format(
4893 4900 val, self.schedule_type))
4894 4901
4895 4902 self._schedule_type = val
4896 4903
4897 4904 @classmethod
4898 4905 def get_uid(cls, obj):
4899 4906 args = obj.task_args
4900 4907 kwargs = obj.task_kwargs
4901 4908 if isinstance(args, JsonRaw):
4902 4909 try:
4903 4910 args = json.loads(args)
4904 4911 except ValueError:
4905 4912 args = tuple()
4906 4913
4907 4914 if isinstance(kwargs, JsonRaw):
4908 4915 try:
4909 4916 kwargs = json.loads(kwargs)
4910 4917 except ValueError:
4911 4918 kwargs = dict()
4912 4919
4913 4920 dot_notation = obj.task_dot_notation
4914 4921 val = '.'.join(map(safe_str, [
4915 4922 sorted(dot_notation), args, sorted(kwargs.items())]))
4916 4923 return hashlib.sha1(val).hexdigest()
4917 4924
4918 4925 @classmethod
4919 4926 def get_by_schedule_name(cls, schedule_name):
4920 4927 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
4921 4928
4922 4929 @classmethod
4923 4930 def get_by_schedule_id(cls, schedule_id):
4924 4931 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
4925 4932
4926 4933 @property
4927 4934 def task(self):
4928 4935 return self.task_dot_notation
4929 4936
4930 4937 @property
4931 4938 def schedule(self):
4932 4939 from rhodecode.lib.celerylib.utils import raw_2_schedule
4933 4940 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
4934 4941 return schedule
4935 4942
4936 4943 @property
4937 4944 def args(self):
4938 4945 try:
4939 4946 return list(self.task_args or [])
4940 4947 except ValueError:
4941 4948 return list()
4942 4949
4943 4950 @property
4944 4951 def kwargs(self):
4945 4952 try:
4946 4953 return dict(self.task_kwargs or {})
4947 4954 except ValueError:
4948 4955 return dict()
4949 4956
4950 4957 def _as_raw(self, val):
4951 4958 if hasattr(val, 'de_coerce'):
4952 4959 val = val.de_coerce()
4953 4960 if val:
4954 4961 val = json.dumps(val)
4955 4962
4956 4963 return val
4957 4964
4958 4965 @property
4959 4966 def schedule_definition_raw(self):
4960 4967 return self._as_raw(self.schedule_definition)
4961 4968
4962 4969 @property
4963 4970 def args_raw(self):
4964 4971 return self._as_raw(self.task_args)
4965 4972
4966 4973 @property
4967 4974 def kwargs_raw(self):
4968 4975 return self._as_raw(self.task_kwargs)
4969 4976
4970 4977 def __repr__(self):
4971 4978 return '<DB:ScheduleEntry({}:{})>'.format(
4972 4979 self.schedule_entry_id, self.schedule_name)
4973 4980
4974 4981
4975 4982 @event.listens_for(ScheduleEntry, 'before_update')
4976 4983 def update_task_uid(mapper, connection, target):
4977 4984 target.task_uid = ScheduleEntry.get_uid(target)
4978 4985
4979 4986
4980 4987 @event.listens_for(ScheduleEntry, 'before_insert')
4981 4988 def set_task_uid(mapper, connection, target):
4982 4989 target.task_uid = ScheduleEntry.get_uid(target)
4983 4990
4984 4991
4985 4992 class _BaseBranchPerms(BaseModel):
4986 4993 @classmethod
4987 4994 def compute_hash(cls, value):
4988 4995 return sha1_safe(value)
4989 4996
4990 4997 @hybrid_property
4991 4998 def branch_pattern(self):
4992 4999 return self._branch_pattern or '*'
4993 5000
4994 5001 @hybrid_property
4995 5002 def branch_hash(self):
4996 5003 return self._branch_hash
4997 5004
4998 5005 def _validate_glob(self, value):
4999 5006 re.compile('^' + glob2re(value) + '$')
5000 5007
5001 5008 @branch_pattern.setter
5002 5009 def branch_pattern(self, value):
5003 5010 self._validate_glob(value)
5004 5011 self._branch_pattern = value or '*'
5005 5012 # set the Hash when setting the branch pattern
5006 5013 self._branch_hash = self.compute_hash(self._branch_pattern)
5007 5014
5008 5015 def matches(self, branch):
5009 5016 """
5010 5017 Check if this the branch matches entry
5011 5018
5012 5019 :param branch: branch name for the commit
5013 5020 """
5014 5021
5015 5022 branch = branch or ''
5016 5023
5017 5024 branch_matches = True
5018 5025 if branch:
5019 5026 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5020 5027 branch_matches = bool(branch_regex.search(branch))
5021 5028
5022 5029 return branch_matches
5023 5030
5024 5031
5025 5032 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5026 5033 __tablename__ = 'user_to_repo_branch_permissions'
5027 5034 __table_args__ = (
5028 5035 base_table_args
5029 5036 )
5030 5037
5031 5038 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5032 5039
5033 5040 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5034 5041 repo = relationship('Repository', backref='user_branch_perms')
5035 5042
5036 5043 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5037 5044 permission = relationship('Permission')
5038 5045
5039 5046 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5040 5047 user_repo_to_perm = relationship('UserRepoToPerm')
5041 5048
5042 5049 rule_order = Column('rule_order', Integer(), nullable=False)
5043 5050 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5044 5051 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5045 5052
5046 5053 def __unicode__(self):
5047 5054 return u'<UserBranchPermission(%s => %r)>' % (
5048 5055 self.user_repo_to_perm, self.branch_pattern)
5049 5056
5050 5057
5051 5058 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5052 5059 __tablename__ = 'user_group_to_repo_branch_permissions'
5053 5060 __table_args__ = (
5054 5061 base_table_args
5055 5062 )
5056 5063
5057 5064 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5058 5065
5059 5066 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5060 5067 repo = relationship('Repository', backref='user_group_branch_perms')
5061 5068
5062 5069 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5063 5070 permission = relationship('Permission')
5064 5071
5065 5072 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
5066 5073 user_group_repo_to_perm = relationship('UserGroupRepoToPerm')
5067 5074
5068 5075 rule_order = Column('rule_order', Integer(), nullable=False)
5069 5076 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5070 5077 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5071 5078
5072 5079 def __unicode__(self):
5073 5080 return u'<UserBranchPermission(%s => %r)>' % (
5074 5081 self.user_group_repo_to_perm, self.branch_pattern)
5075 5082
5076 5083
5077 5084 class UserBookmark(Base, BaseModel):
5078 5085 __tablename__ = 'user_bookmarks'
5079 5086 __table_args__ = (
5080 5087 UniqueConstraint('user_id', 'bookmark_repo_id'),
5081 5088 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5082 5089 UniqueConstraint('user_id', 'bookmark_position'),
5083 5090 base_table_args
5084 5091 )
5085 5092
5086 5093 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5087 5094 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5088 5095 position = Column("bookmark_position", Integer(), nullable=False)
5089 5096 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5090 5097 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5091 5098 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5092 5099
5093 5100 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5094 5101 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5095 5102
5096 5103 user = relationship("User")
5097 5104
5098 5105 repository = relationship("Repository")
5099 5106 repository_group = relationship("RepoGroup")
5100 5107
5101 5108 @classmethod
5102 5109 def get_by_position_for_user(cls, position, user_id):
5103 5110 return cls.query() \
5104 5111 .filter(UserBookmark.user_id == user_id) \
5105 5112 .filter(UserBookmark.position == position).scalar()
5106 5113
5107 5114 @classmethod
5108 5115 def get_bookmarks_for_user(cls, user_id):
5109 5116 return cls.query() \
5110 5117 .filter(UserBookmark.user_id == user_id) \
5111 5118 .options(joinedload(UserBookmark.repository)) \
5112 5119 .options(joinedload(UserBookmark.repository_group)) \
5113 5120 .order_by(UserBookmark.position.asc()) \
5114 5121 .all()
5115 5122
5116 5123 def __unicode__(self):
5117 5124 return u'<UserBookmark(%s @ %r)>' % (self.position, self.redirect_url)
5118 5125
5119 5126
5120 5127 class FileStore(Base, BaseModel):
5121 5128 __tablename__ = 'file_store'
5122 5129 __table_args__ = (
5123 5130 base_table_args
5124 5131 )
5125 5132
5126 5133 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5127 5134 file_uid = Column('file_uid', String(1024), nullable=False)
5128 5135 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5129 5136 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5130 5137 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5131 5138
5132 5139 # sha256 hash
5133 5140 file_hash = Column('file_hash', String(512), nullable=False)
5134 5141 file_size = Column('file_size', BigInteger(), nullable=False)
5135 5142
5136 5143 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5137 5144 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5138 5145 accessed_count = Column('accessed_count', Integer(), default=0)
5139 5146
5140 5147 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5141 5148
5142 5149 # if repo/repo_group reference is set, check for permissions
5143 5150 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5144 5151
5145 5152 # hidden defines an attachment that should be hidden from showing in artifact listing
5146 5153 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5147 5154
5148 5155 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5149 5156 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id')
5150 5157
5151 5158 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5152 5159
5153 5160 # scope limited to user, which requester have access to
5154 5161 scope_user_id = Column(
5155 5162 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5156 5163 nullable=True, unique=None, default=None)
5157 5164 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id')
5158 5165
5159 5166 # scope limited to user group, which requester have access to
5160 5167 scope_user_group_id = Column(
5161 5168 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5162 5169 nullable=True, unique=None, default=None)
5163 5170 user_group = relationship('UserGroup', lazy='joined')
5164 5171
5165 5172 # scope limited to repo, which requester have access to
5166 5173 scope_repo_id = Column(
5167 5174 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5168 5175 nullable=True, unique=None, default=None)
5169 5176 repo = relationship('Repository', lazy='joined')
5170 5177
5171 5178 # scope limited to repo group, which requester have access to
5172 5179 scope_repo_group_id = Column(
5173 5180 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5174 5181 nullable=True, unique=None, default=None)
5175 5182 repo_group = relationship('RepoGroup', lazy='joined')
5176 5183
5177 5184 @classmethod
5178 5185 def get_by_store_uid(cls, file_store_uid):
5179 5186 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5180 5187
5181 5188 @classmethod
5182 5189 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5183 5190 file_description='', enabled=True, hidden=False, check_acl=True,
5184 5191 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5185 5192
5186 5193 store_entry = FileStore()
5187 5194 store_entry.file_uid = file_uid
5188 5195 store_entry.file_display_name = file_display_name
5189 5196 store_entry.file_org_name = filename
5190 5197 store_entry.file_size = file_size
5191 5198 store_entry.file_hash = file_hash
5192 5199 store_entry.file_description = file_description
5193 5200
5194 5201 store_entry.check_acl = check_acl
5195 5202 store_entry.enabled = enabled
5196 5203 store_entry.hidden = hidden
5197 5204
5198 5205 store_entry.user_id = user_id
5199 5206 store_entry.scope_user_id = scope_user_id
5200 5207 store_entry.scope_repo_id = scope_repo_id
5201 5208 store_entry.scope_repo_group_id = scope_repo_group_id
5202 5209
5203 5210 return store_entry
5204 5211
5205 5212 @classmethod
5206 5213 def store_metadata(cls, file_store_id, args, commit=True):
5207 5214 file_store = FileStore.get(file_store_id)
5208 5215 if file_store is None:
5209 5216 return
5210 5217
5211 5218 for section, key, value, value_type in args:
5212 5219 has_key = FileStoreMetadata().query() \
5213 5220 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5214 5221 .filter(FileStoreMetadata.file_store_meta_section == section) \
5215 5222 .filter(FileStoreMetadata.file_store_meta_key == key) \
5216 5223 .scalar()
5217 5224 if has_key:
5218 5225 msg = 'key `{}` already defined under section `{}` for this file.'\
5219 5226 .format(key, section)
5220 5227 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5221 5228
5222 5229 # NOTE(marcink): raises ArtifactMetadataBadValueType
5223 5230 FileStoreMetadata.valid_value_type(value_type)
5224 5231
5225 5232 meta_entry = FileStoreMetadata()
5226 5233 meta_entry.file_store = file_store
5227 5234 meta_entry.file_store_meta_section = section
5228 5235 meta_entry.file_store_meta_key = key
5229 5236 meta_entry.file_store_meta_value_type = value_type
5230 5237 meta_entry.file_store_meta_value = value
5231 5238
5232 5239 Session().add(meta_entry)
5233 5240
5234 5241 try:
5235 5242 if commit:
5236 5243 Session().commit()
5237 5244 except IntegrityError:
5238 5245 Session().rollback()
5239 5246 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5240 5247
5241 5248 @classmethod
5242 5249 def bump_access_counter(cls, file_uid, commit=True):
5243 5250 FileStore().query()\
5244 5251 .filter(FileStore.file_uid == file_uid)\
5245 5252 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5246 5253 FileStore.accessed_on: datetime.datetime.now()})
5247 5254 if commit:
5248 5255 Session().commit()
5249 5256
5250 5257 def __json__(self):
5251 5258 data = {
5252 5259 'filename': self.file_display_name,
5253 5260 'filename_org': self.file_org_name,
5254 5261 'file_uid': self.file_uid,
5255 5262 'description': self.file_description,
5256 5263 'hidden': self.hidden,
5257 5264 'size': self.file_size,
5258 5265 'created_on': self.created_on,
5259 5266 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5260 5267 'downloaded_times': self.accessed_count,
5261 5268 'sha256': self.file_hash,
5262 5269 'metadata': self.file_metadata,
5263 5270 }
5264 5271
5265 5272 return data
5266 5273
5267 5274 def __repr__(self):
5268 5275 return '<FileStore({})>'.format(self.file_store_id)
5269 5276
5270 5277
5271 5278 class FileStoreMetadata(Base, BaseModel):
5272 5279 __tablename__ = 'file_store_metadata'
5273 5280 __table_args__ = (
5274 5281 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5275 5282 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5276 5283 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5277 5284 base_table_args
5278 5285 )
5279 5286 SETTINGS_TYPES = {
5280 5287 'str': safe_str,
5281 5288 'int': safe_int,
5282 5289 'unicode': safe_unicode,
5283 5290 'bool': str2bool,
5284 5291 'list': functools.partial(aslist, sep=',')
5285 5292 }
5286 5293
5287 5294 file_store_meta_id = Column(
5288 5295 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5289 5296 primary_key=True)
5290 5297 _file_store_meta_section = Column(
5291 5298 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5292 5299 nullable=True, unique=None, default=None)
5293 5300 _file_store_meta_section_hash = Column(
5294 5301 "file_store_meta_section_hash", String(255),
5295 5302 nullable=True, unique=None, default=None)
5296 5303 _file_store_meta_key = Column(
5297 5304 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5298 5305 nullable=True, unique=None, default=None)
5299 5306 _file_store_meta_key_hash = Column(
5300 5307 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5301 5308 _file_store_meta_value = Column(
5302 5309 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5303 5310 nullable=True, unique=None, default=None)
5304 5311 _file_store_meta_value_type = Column(
5305 5312 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5306 5313 default='unicode')
5307 5314
5308 5315 file_store_id = Column(
5309 5316 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5310 5317 nullable=True, unique=None, default=None)
5311 5318
5312 5319 file_store = relationship('FileStore', lazy='joined')
5313 5320
5314 5321 @classmethod
5315 5322 def valid_value_type(cls, value):
5316 5323 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5317 5324 raise ArtifactMetadataBadValueType(
5318 5325 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5319 5326
5320 5327 @hybrid_property
5321 5328 def file_store_meta_section(self):
5322 5329 return self._file_store_meta_section
5323 5330
5324 5331 @file_store_meta_section.setter
5325 5332 def file_store_meta_section(self, value):
5326 5333 self._file_store_meta_section = value
5327 5334 self._file_store_meta_section_hash = _hash_key(value)
5328 5335
5329 5336 @hybrid_property
5330 5337 def file_store_meta_key(self):
5331 5338 return self._file_store_meta_key
5332 5339
5333 5340 @file_store_meta_key.setter
5334 5341 def file_store_meta_key(self, value):
5335 5342 self._file_store_meta_key = value
5336 5343 self._file_store_meta_key_hash = _hash_key(value)
5337 5344
5338 5345 @hybrid_property
5339 5346 def file_store_meta_value(self):
5340 5347 val = self._file_store_meta_value
5341 5348
5342 5349 if self._file_store_meta_value_type:
5343 5350 # e.g unicode.encrypted == unicode
5344 5351 _type = self._file_store_meta_value_type.split('.')[0]
5345 5352 # decode the encrypted value if it's encrypted field type
5346 5353 if '.encrypted' in self._file_store_meta_value_type:
5347 5354 cipher = EncryptedTextValue()
5348 5355 val = safe_unicode(cipher.process_result_value(val, None))
5349 5356 # do final type conversion
5350 5357 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5351 5358 val = converter(val)
5352 5359
5353 5360 return val
5354 5361
5355 5362 @file_store_meta_value.setter
5356 5363 def file_store_meta_value(self, val):
5357 5364 val = safe_unicode(val)
5358 5365 # encode the encrypted value
5359 5366 if '.encrypted' in self.file_store_meta_value_type:
5360 5367 cipher = EncryptedTextValue()
5361 5368 val = safe_unicode(cipher.process_bind_param(val, None))
5362 5369 self._file_store_meta_value = val
5363 5370
5364 5371 @hybrid_property
5365 5372 def file_store_meta_value_type(self):
5366 5373 return self._file_store_meta_value_type
5367 5374
5368 5375 @file_store_meta_value_type.setter
5369 5376 def file_store_meta_value_type(self, val):
5370 5377 # e.g unicode.encrypted
5371 5378 self.valid_value_type(val)
5372 5379 self._file_store_meta_value_type = val
5373 5380
5374 5381 def __json__(self):
5375 5382 data = {
5376 5383 'artifact': self.file_store.file_uid,
5377 5384 'section': self.file_store_meta_section,
5378 5385 'key': self.file_store_meta_key,
5379 5386 'value': self.file_store_meta_value,
5380 5387 }
5381 5388
5382 5389 return data
5383 5390
5384 5391 def __repr__(self):
5385 5392 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.file_store_meta_section,
5386 5393 self.file_store_meta_key, self.file_store_meta_value)
5387 5394
5388 5395
5389 5396 class DbMigrateVersion(Base, BaseModel):
5390 5397 __tablename__ = 'db_migrate_version'
5391 5398 __table_args__ = (
5392 5399 base_table_args,
5393 5400 )
5394 5401
5395 5402 repository_id = Column('repository_id', String(250), primary_key=True)
5396 5403 repository_path = Column('repository_path', Text)
5397 5404 version = Column('version', Integer)
5398 5405
5399 5406 @classmethod
5400 5407 def set_version(cls, version):
5401 5408 """
5402 5409 Helper for forcing a different version, usually for debugging purposes via ishell.
5403 5410 """
5404 5411 ver = DbMigrateVersion.query().first()
5405 5412 ver.version = version
5406 5413 Session().commit()
5407 5414
5408 5415
5409 5416 class DbSession(Base, BaseModel):
5410 5417 __tablename__ = 'db_session'
5411 5418 __table_args__ = (
5412 5419 base_table_args,
5413 5420 )
5414 5421
5415 5422 def __repr__(self):
5416 5423 return '<DB:DbSession({})>'.format(self.id)
5417 5424
5418 5425 id = Column('id', Integer())
5419 5426 namespace = Column('namespace', String(255), primary_key=True)
5420 5427 accessed = Column('accessed', DateTime, nullable=False)
5421 5428 created = Column('created', DateTime, nullable=False)
5422 5429 data = Column('data', PickleType, nullable=False)
@@ -1,945 +1,979 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 users model for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27 import datetime
28 28 import ipaddress
29 29
30 30 from pyramid.threadlocal import get_current_request
31 31 from sqlalchemy.exc import DatabaseError
32 32
33 33 from rhodecode import events
34 34 from rhodecode.lib.user_log_filter import user_log_filter
35 35 from rhodecode.lib.utils2 import (
36 36 safe_unicode, get_current_rhodecode_user, action_logger_generic,
37 37 AttributeDict, str2bool)
38 38 from rhodecode.lib.exceptions import (
39 39 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
40 UserOwnsUserGroupsException, NotAllowedToCreateUserError)
40 UserOwnsUserGroupsException, NotAllowedToCreateUserError, UserOwnsArtifactsException)
41 41 from rhodecode.lib.caching_query import FromCache
42 42 from rhodecode.model import BaseModel
43 43 from rhodecode.model.auth_token import AuthTokenModel
44 44 from rhodecode.model.db import (
45 45 _hash_key, true, false, or_, joinedload, User, UserToPerm,
46 46 UserEmailMap, UserIpMap, UserLog)
47 47 from rhodecode.model.meta import Session
48 48 from rhodecode.model.repo_group import RepoGroupModel
49 49
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53
54 54 class UserModel(BaseModel):
55 55 cls = User
56 56
57 57 def get(self, user_id, cache=False):
58 58 user = self.sa.query(User)
59 59 if cache:
60 60 user = user.options(
61 61 FromCache("sql_cache_short", "get_user_%s" % user_id))
62 62 return user.get(user_id)
63 63
64 64 def get_user(self, user):
65 65 return self._get_user(user)
66 66
67 67 def _serialize_user(self, user):
68 68 import rhodecode.lib.helpers as h
69 69
70 70 return {
71 71 'id': user.user_id,
72 72 'first_name': user.first_name,
73 73 'last_name': user.last_name,
74 74 'username': user.username,
75 75 'email': user.email,
76 76 'icon_link': h.gravatar_url(user.email, 30),
77 77 'profile_link': h.link_to_user(user),
78 78 'value_display': h.escape(h.person(user)),
79 79 'value': user.username,
80 80 'value_type': 'user',
81 81 'active': user.active,
82 82 }
83 83
84 84 def get_users(self, name_contains=None, limit=20, only_active=True):
85 85
86 86 query = self.sa.query(User)
87 87 if only_active:
88 88 query = query.filter(User.active == true())
89 89
90 90 if name_contains:
91 91 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
92 92 query = query.filter(
93 93 or_(
94 94 User.name.ilike(ilike_expression),
95 95 User.lastname.ilike(ilike_expression),
96 96 User.username.ilike(ilike_expression)
97 97 )
98 98 )
99 99 query = query.limit(limit)
100 100 users = query.all()
101 101
102 102 _users = [
103 103 self._serialize_user(user) for user in users
104 104 ]
105 105 return _users
106 106
107 107 def get_by_username(self, username, cache=False, case_insensitive=False):
108 108
109 109 if case_insensitive:
110 110 user = self.sa.query(User).filter(User.username.ilike(username))
111 111 else:
112 112 user = self.sa.query(User)\
113 113 .filter(User.username == username)
114 114 if cache:
115 115 name_key = _hash_key(username)
116 116 user = user.options(
117 117 FromCache("sql_cache_short", "get_user_%s" % name_key))
118 118 return user.scalar()
119 119
120 120 def get_by_email(self, email, cache=False, case_insensitive=False):
121 121 return User.get_by_email(email, case_insensitive, cache)
122 122
123 123 def get_by_auth_token(self, auth_token, cache=False):
124 124 return User.get_by_auth_token(auth_token, cache)
125 125
126 126 def get_active_user_count(self, cache=False):
127 127 qry = User.query().filter(
128 128 User.active == true()).filter(
129 129 User.username != User.DEFAULT_USER)
130 130 if cache:
131 131 qry = qry.options(
132 132 FromCache("sql_cache_short", "get_active_users"))
133 133 return qry.count()
134 134
135 135 def create(self, form_data, cur_user=None):
136 136 if not cur_user:
137 137 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
138 138
139 139 user_data = {
140 140 'username': form_data['username'],
141 141 'password': form_data['password'],
142 142 'email': form_data['email'],
143 143 'firstname': form_data['firstname'],
144 144 'lastname': form_data['lastname'],
145 145 'active': form_data['active'],
146 146 'extern_type': form_data['extern_type'],
147 147 'extern_name': form_data['extern_name'],
148 148 'admin': False,
149 149 'cur_user': cur_user
150 150 }
151 151
152 152 if 'create_repo_group' in form_data:
153 153 user_data['create_repo_group'] = str2bool(
154 154 form_data.get('create_repo_group'))
155 155
156 156 try:
157 157 if form_data.get('password_change'):
158 158 user_data['force_password_change'] = True
159 159 return UserModel().create_or_update(**user_data)
160 160 except Exception:
161 161 log.error(traceback.format_exc())
162 162 raise
163 163
164 164 def update_user(self, user, skip_attrs=None, **kwargs):
165 165 from rhodecode.lib.auth import get_crypt_password
166 166
167 167 user = self._get_user(user)
168 168 if user.username == User.DEFAULT_USER:
169 169 raise DefaultUserException(
170 170 "You can't edit this user (`%(username)s`) since it's "
171 171 "crucial for entire application" % {
172 172 'username': user.username})
173 173
174 174 # first store only defaults
175 175 user_attrs = {
176 176 'updating_user_id': user.user_id,
177 177 'username': user.username,
178 178 'password': user.password,
179 179 'email': user.email,
180 180 'firstname': user.name,
181 181 'lastname': user.lastname,
182 182 'active': user.active,
183 183 'admin': user.admin,
184 184 'extern_name': user.extern_name,
185 185 'extern_type': user.extern_type,
186 186 'language': user.user_data.get('language')
187 187 }
188 188
189 189 # in case there's new_password, that comes from form, use it to
190 190 # store password
191 191 if kwargs.get('new_password'):
192 192 kwargs['password'] = kwargs['new_password']
193 193
194 194 # cleanups, my_account password change form
195 195 kwargs.pop('current_password', None)
196 196 kwargs.pop('new_password', None)
197 197
198 198 # cleanups, user edit password change form
199 199 kwargs.pop('password_confirmation', None)
200 200 kwargs.pop('password_change', None)
201 201
202 202 # create repo group on user creation
203 203 kwargs.pop('create_repo_group', None)
204 204
205 205 # legacy forms send name, which is the firstname
206 206 firstname = kwargs.pop('name', None)
207 207 if firstname:
208 208 kwargs['firstname'] = firstname
209 209
210 210 for k, v in kwargs.items():
211 211 # skip if we don't want to update this
212 212 if skip_attrs and k in skip_attrs:
213 213 continue
214 214
215 215 user_attrs[k] = v
216 216
217 217 try:
218 218 return self.create_or_update(**user_attrs)
219 219 except Exception:
220 220 log.error(traceback.format_exc())
221 221 raise
222 222
223 223 def create_or_update(
224 224 self, username, password, email, firstname='', lastname='',
225 225 active=True, admin=False, extern_type=None, extern_name=None,
226 226 cur_user=None, plugin=None, force_password_change=False,
227 227 allow_to_create_user=True, create_repo_group=None,
228 228 updating_user_id=None, language=None, strict_creation_check=True):
229 229 """
230 230 Creates a new instance if not found, or updates current one
231 231
232 232 :param username:
233 233 :param password:
234 234 :param email:
235 235 :param firstname:
236 236 :param lastname:
237 237 :param active:
238 238 :param admin:
239 239 :param extern_type:
240 240 :param extern_name:
241 241 :param cur_user:
242 242 :param plugin: optional plugin this method was called from
243 243 :param force_password_change: toggles new or existing user flag
244 244 for password change
245 245 :param allow_to_create_user: Defines if the method can actually create
246 246 new users
247 247 :param create_repo_group: Defines if the method should also
248 248 create an repo group with user name, and owner
249 249 :param updating_user_id: if we set it up this is the user we want to
250 250 update this allows to editing username.
251 251 :param language: language of user from interface.
252 252
253 253 :returns: new User object with injected `is_new_user` attribute.
254 254 """
255 255
256 256 if not cur_user:
257 257 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
258 258
259 259 from rhodecode.lib.auth import (
260 260 get_crypt_password, check_password, generate_auth_token)
261 261 from rhodecode.lib.hooks_base import (
262 262 log_create_user, check_allowed_create_user)
263 263
264 264 def _password_change(new_user, password):
265 265 old_password = new_user.password or ''
266 266 # empty password
267 267 if not old_password:
268 268 return False
269 269
270 270 # password check is only needed for RhodeCode internal auth calls
271 271 # in case it's a plugin we don't care
272 272 if not plugin:
273 273
274 274 # first check if we gave crypted password back, and if it
275 275 # matches it's not password change
276 276 if new_user.password == password:
277 277 return False
278 278
279 279 password_match = check_password(password, old_password)
280 280 if not password_match:
281 281 return True
282 282
283 283 return False
284 284
285 285 # read settings on default personal repo group creation
286 286 if create_repo_group is None:
287 287 default_create_repo_group = RepoGroupModel()\
288 288 .get_default_create_personal_repo_group()
289 289 create_repo_group = default_create_repo_group
290 290
291 291 user_data = {
292 292 'username': username,
293 293 'password': password,
294 294 'email': email,
295 295 'firstname': firstname,
296 296 'lastname': lastname,
297 297 'active': active,
298 298 'admin': admin
299 299 }
300 300
301 301 if updating_user_id:
302 302 log.debug('Checking for existing account in RhodeCode '
303 303 'database with user_id `%s` ', updating_user_id)
304 304 user = User.get(updating_user_id)
305 305 else:
306 306 log.debug('Checking for existing account in RhodeCode '
307 307 'database with username `%s` ', username)
308 308 user = User.get_by_username(username, case_insensitive=True)
309 309
310 310 if user is None:
311 311 # we check internal flag if this method is actually allowed to
312 312 # create new user
313 313 if not allow_to_create_user:
314 314 msg = ('Method wants to create new user, but it is not '
315 315 'allowed to do so')
316 316 log.warning(msg)
317 317 raise NotAllowedToCreateUserError(msg)
318 318
319 319 log.debug('Creating new user %s', username)
320 320
321 321 # only if we create user that is active
322 322 new_active_user = active
323 323 if new_active_user and strict_creation_check:
324 324 # raises UserCreationError if it's not allowed for any reason to
325 325 # create new active user, this also executes pre-create hooks
326 326 check_allowed_create_user(user_data, cur_user, strict_check=True)
327 327 events.trigger(events.UserPreCreate(user_data))
328 328 new_user = User()
329 329 edit = False
330 330 else:
331 331 log.debug('updating user `%s`', username)
332 332 events.trigger(events.UserPreUpdate(user, user_data))
333 333 new_user = user
334 334 edit = True
335 335
336 336 # we're not allowed to edit default user
337 337 if user.username == User.DEFAULT_USER:
338 338 raise DefaultUserException(
339 339 "You can't edit this user (`%(username)s`) since it's "
340 340 "crucial for entire application"
341 341 % {'username': user.username})
342 342
343 343 # inject special attribute that will tell us if User is new or old
344 344 new_user.is_new_user = not edit
345 345 # for users that didn's specify auth type, we use RhodeCode built in
346 346 from rhodecode.authentication.plugins import auth_rhodecode
347 347 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.uid
348 348 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.uid
349 349
350 350 try:
351 351 new_user.username = username
352 352 new_user.admin = admin
353 353 new_user.email = email
354 354 new_user.active = active
355 355 new_user.extern_name = safe_unicode(extern_name)
356 356 new_user.extern_type = safe_unicode(extern_type)
357 357 new_user.name = firstname
358 358 new_user.lastname = lastname
359 359
360 360 # set password only if creating an user or password is changed
361 361 if not edit or _password_change(new_user, password):
362 362 reason = 'new password' if edit else 'new user'
363 363 log.debug('Updating password reason=>%s', reason)
364 364 new_user.password = get_crypt_password(password) if password else None
365 365
366 366 if force_password_change:
367 367 new_user.update_userdata(force_password_change=True)
368 368 if language:
369 369 new_user.update_userdata(language=language)
370 370 new_user.update_userdata(notification_status=True)
371 371
372 372 self.sa.add(new_user)
373 373
374 374 if not edit and create_repo_group:
375 375 RepoGroupModel().create_personal_repo_group(
376 376 new_user, commit_early=False)
377 377
378 378 if not edit:
379 379 # add the RSS token
380 380 self.add_auth_token(
381 381 user=username, lifetime_minutes=-1,
382 382 role=self.auth_token_role.ROLE_FEED,
383 383 description=u'Generated feed token')
384 384
385 385 kwargs = new_user.get_dict()
386 386 # backward compat, require api_keys present
387 387 kwargs['api_keys'] = kwargs['auth_tokens']
388 388 log_create_user(created_by=cur_user, **kwargs)
389 389 events.trigger(events.UserPostCreate(user_data))
390 390 return new_user
391 391 except (DatabaseError,):
392 392 log.error(traceback.format_exc())
393 393 raise
394 394
395 395 def create_registration(self, form_data,
396 396 extern_name='rhodecode', extern_type='rhodecode'):
397 397 from rhodecode.model.notification import NotificationModel
398 398 from rhodecode.model.notification import EmailNotificationModel
399 399
400 400 try:
401 401 form_data['admin'] = False
402 402 form_data['extern_name'] = extern_name
403 403 form_data['extern_type'] = extern_type
404 404 new_user = self.create(form_data)
405 405
406 406 self.sa.add(new_user)
407 407 self.sa.flush()
408 408
409 409 user_data = new_user.get_dict()
410 410 kwargs = {
411 411 # use SQLALCHEMY safe dump of user data
412 412 'user': AttributeDict(user_data),
413 413 'date': datetime.datetime.now()
414 414 }
415 415 notification_type = EmailNotificationModel.TYPE_REGISTRATION
416 416 # pre-generate the subject for notification itself
417 417 (subject,
418 418 _h, _e, # we don't care about those
419 419 body_plaintext) = EmailNotificationModel().render_email(
420 420 notification_type, **kwargs)
421 421
422 422 # create notification objects, and emails
423 423 NotificationModel().create(
424 424 created_by=new_user,
425 425 notification_subject=subject,
426 426 notification_body=body_plaintext,
427 427 notification_type=notification_type,
428 428 recipients=None, # all admins
429 429 email_kwargs=kwargs,
430 430 )
431 431
432 432 return new_user
433 433 except Exception:
434 434 log.error(traceback.format_exc())
435 435 raise
436 436
437 437 def _handle_user_repos(self, username, repositories, handle_mode=None):
438 438 _superadmin = self.cls.get_first_super_admin()
439 439 left_overs = True
440 440
441 441 from rhodecode.model.repo import RepoModel
442 442
443 443 if handle_mode == 'detach':
444 444 for obj in repositories:
445 445 obj.user = _superadmin
446 446 # set description we know why we super admin now owns
447 447 # additional repositories that were orphaned !
448 448 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
449 449 self.sa.add(obj)
450 450 left_overs = False
451 451 elif handle_mode == 'delete':
452 452 for obj in repositories:
453 453 RepoModel().delete(obj, forks='detach')
454 454 left_overs = False
455 455
456 456 # if nothing is done we have left overs left
457 457 return left_overs
458 458
459 459 def _handle_user_repo_groups(self, username, repository_groups,
460 460 handle_mode=None):
461 461 _superadmin = self.cls.get_first_super_admin()
462 462 left_overs = True
463 463
464 464 from rhodecode.model.repo_group import RepoGroupModel
465 465
466 466 if handle_mode == 'detach':
467 467 for r in repository_groups:
468 468 r.user = _superadmin
469 469 # set description we know why we super admin now owns
470 470 # additional repositories that were orphaned !
471 471 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
472 472 r.personal = False
473 473 self.sa.add(r)
474 474 left_overs = False
475 475 elif handle_mode == 'delete':
476 476 for r in repository_groups:
477 477 RepoGroupModel().delete(r)
478 478 left_overs = False
479 479
480 480 # if nothing is done we have left overs left
481 481 return left_overs
482 482
483 483 def _handle_user_user_groups(self, username, user_groups, handle_mode=None):
484 484 _superadmin = self.cls.get_first_super_admin()
485 485 left_overs = True
486 486
487 487 from rhodecode.model.user_group import UserGroupModel
488 488
489 489 if handle_mode == 'detach':
490 490 for r in user_groups:
491 491 for user_user_group_to_perm in r.user_user_group_to_perm:
492 492 if user_user_group_to_perm.user.username == username:
493 493 user_user_group_to_perm.user = _superadmin
494 494 r.user = _superadmin
495 495 # set description we know why we super admin now owns
496 496 # additional repositories that were orphaned !
497 497 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
498 498 self.sa.add(r)
499 499 left_overs = False
500 500 elif handle_mode == 'delete':
501 501 for r in user_groups:
502 502 UserGroupModel().delete(r)
503 503 left_overs = False
504 504
505 505 # if nothing is done we have left overs left
506 506 return left_overs
507 507
508 def _handle_user_artifacts(self, username, artifacts, handle_mode=None):
509 _superadmin = self.cls.get_first_super_admin()
510 left_overs = True
511
512 if handle_mode == 'detach':
513 for a in artifacts:
514 a.upload_user = _superadmin
515 # set description we know why we super admin now owns
516 # additional artifacts that were orphaned !
517 a.file_description += ' \n::detached artifact from deleted user: %s' % (username,)
518 self.sa.add(a)
519 left_overs = False
520 elif handle_mode == 'delete':
521 from rhodecode.apps.file_store import utils as store_utils
522 storage = store_utils.get_file_storage(self.request.registry.settings)
523 for a in artifacts:
524 file_uid = a.file_uid
525 storage.delete(file_uid)
526 self.sa.delete(a)
527
528 left_overs = False
529
530 # if nothing is done we have left overs left
531 return left_overs
532
508 533 def delete(self, user, cur_user=None, handle_repos=None,
509 handle_repo_groups=None, handle_user_groups=None):
534 handle_repo_groups=None, handle_user_groups=None, handle_artifacts=None):
510 535 from rhodecode.lib.hooks_base import log_delete_user
511 536
512 537 if not cur_user:
513 538 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
514 539 user = self._get_user(user)
515 540
516 541 try:
517 542 if user.username == User.DEFAULT_USER:
518 543 raise DefaultUserException(
519 544 u"You can't remove this user since it's"
520 545 u" crucial for entire application")
521 546
522 547 left_overs = self._handle_user_repos(
523 548 user.username, user.repositories, handle_repos)
524 549 if left_overs and user.repositories:
525 550 repos = [x.repo_name for x in user.repositories]
526 551 raise UserOwnsReposException(
527 552 u'user "%(username)s" still owns %(len_repos)s repositories and cannot be '
528 553 u'removed. Switch owners or remove those repositories:%(list_repos)s'
529 554 % {'username': user.username, 'len_repos': len(repos),
530 555 'list_repos': ', '.join(repos)})
531 556
532 557 left_overs = self._handle_user_repo_groups(
533 558 user.username, user.repository_groups, handle_repo_groups)
534 559 if left_overs and user.repository_groups:
535 560 repo_groups = [x.group_name for x in user.repository_groups]
536 561 raise UserOwnsRepoGroupsException(
537 562 u'user "%(username)s" still owns %(len_repo_groups)s repository groups and cannot be '
538 563 u'removed. Switch owners or remove those repository groups:%(list_repo_groups)s'
539 564 % {'username': user.username, 'len_repo_groups': len(repo_groups),
540 565 'list_repo_groups': ', '.join(repo_groups)})
541 566
542 567 left_overs = self._handle_user_user_groups(
543 568 user.username, user.user_groups, handle_user_groups)
544 569 if left_overs and user.user_groups:
545 570 user_groups = [x.users_group_name for x in user.user_groups]
546 571 raise UserOwnsUserGroupsException(
547 572 u'user "%s" still owns %s user groups and cannot be '
548 573 u'removed. Switch owners or remove those user groups:%s'
549 574 % (user.username, len(user_groups), ', '.join(user_groups)))
550 575
576 left_overs = self._handle_user_artifacts(
577 user.username, user.artifacts, handle_artifacts)
578 if left_overs and user.artifacts:
579 artifacts = [x.file_uid for x in user.artifacts]
580 raise UserOwnsArtifactsException(
581 u'user "%s" still owns %s artifacts and cannot be '
582 u'removed. Switch owners or remove those artifacts:%s'
583 % (user.username, len(artifacts), ', '.join(artifacts)))
584
551 585 user_data = user.get_dict() # fetch user data before expire
552 586
553 587 # we might change the user data with detach/delete, make sure
554 588 # the object is marked as expired before actually deleting !
555 589 self.sa.expire(user)
556 590 self.sa.delete(user)
557 591
558 592 log_delete_user(deleted_by=cur_user, **user_data)
559 593 except Exception:
560 594 log.error(traceback.format_exc())
561 595 raise
562 596
563 597 def reset_password_link(self, data, pwd_reset_url):
564 598 from rhodecode.lib.celerylib import tasks, run_task
565 599 from rhodecode.model.notification import EmailNotificationModel
566 600 user_email = data['email']
567 601 try:
568 602 user = User.get_by_email(user_email)
569 603 if user:
570 604 log.debug('password reset user found %s', user)
571 605
572 606 email_kwargs = {
573 607 'password_reset_url': pwd_reset_url,
574 608 'user': user,
575 609 'email': user_email,
576 610 'date': datetime.datetime.now()
577 611 }
578 612
579 613 (subject, headers, email_body,
580 614 email_body_plaintext) = EmailNotificationModel().render_email(
581 615 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
582 616
583 617 recipients = [user_email]
584 618
585 619 action_logger_generic(
586 620 'sending password reset email to user: {}'.format(
587 621 user), namespace='security.password_reset')
588 622
589 623 run_task(tasks.send_email, recipients, subject,
590 624 email_body_plaintext, email_body)
591 625
592 626 else:
593 627 log.debug("password reset email %s not found", user_email)
594 628 except Exception:
595 629 log.error(traceback.format_exc())
596 630 return False
597 631
598 632 return True
599 633
600 634 def reset_password(self, data):
601 635 from rhodecode.lib.celerylib import tasks, run_task
602 636 from rhodecode.model.notification import EmailNotificationModel
603 637 from rhodecode.lib import auth
604 638 user_email = data['email']
605 639 pre_db = True
606 640 try:
607 641 user = User.get_by_email(user_email)
608 642 new_passwd = auth.PasswordGenerator().gen_password(
609 643 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
610 644 if user:
611 645 user.password = auth.get_crypt_password(new_passwd)
612 646 # also force this user to reset his password !
613 647 user.update_userdata(force_password_change=True)
614 648
615 649 Session().add(user)
616 650
617 651 # now delete the token in question
618 652 UserApiKeys = AuthTokenModel.cls
619 653 UserApiKeys().query().filter(
620 654 UserApiKeys.api_key == data['token']).delete()
621 655
622 656 Session().commit()
623 657 log.info('successfully reset password for `%s`', user_email)
624 658
625 659 if new_passwd is None:
626 660 raise Exception('unable to generate new password')
627 661
628 662 pre_db = False
629 663
630 664 email_kwargs = {
631 665 'new_password': new_passwd,
632 666 'user': user,
633 667 'email': user_email,
634 668 'date': datetime.datetime.now()
635 669 }
636 670
637 671 (subject, headers, email_body,
638 672 email_body_plaintext) = EmailNotificationModel().render_email(
639 673 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
640 674 **email_kwargs)
641 675
642 676 recipients = [user_email]
643 677
644 678 action_logger_generic(
645 679 'sent new password to user: {} with email: {}'.format(
646 680 user, user_email), namespace='security.password_reset')
647 681
648 682 run_task(tasks.send_email, recipients, subject,
649 683 email_body_plaintext, email_body)
650 684
651 685 except Exception:
652 686 log.error('Failed to update user password')
653 687 log.error(traceback.format_exc())
654 688 if pre_db:
655 689 # we rollback only if local db stuff fails. If it goes into
656 690 # run_task, we're pass rollback state this wouldn't work then
657 691 Session().rollback()
658 692
659 693 return True
660 694
661 695 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
662 696 """
663 697 Fetches auth_user by user_id,or api_key if present.
664 698 Fills auth_user attributes with those taken from database.
665 699 Additionally set's is_authenitated if lookup fails
666 700 present in database
667 701
668 702 :param auth_user: instance of user to set attributes
669 703 :param user_id: user id to fetch by
670 704 :param api_key: api key to fetch by
671 705 :param username: username to fetch by
672 706 """
673 707 def token_obfuscate(token):
674 708 if token:
675 709 return token[:4] + "****"
676 710
677 711 if user_id is None and api_key is None and username is None:
678 712 raise Exception('You need to pass user_id, api_key or username')
679 713
680 714 log.debug(
681 715 'AuthUser: fill data execution based on: '
682 716 'user_id:%s api_key:%s username:%s', user_id, api_key, username)
683 717 try:
684 718 dbuser = None
685 719 if user_id:
686 720 dbuser = self.get(user_id)
687 721 elif api_key:
688 722 dbuser = self.get_by_auth_token(api_key)
689 723 elif username:
690 724 dbuser = self.get_by_username(username)
691 725
692 726 if not dbuser:
693 727 log.warning(
694 728 'Unable to lookup user by id:%s api_key:%s username:%s',
695 729 user_id, token_obfuscate(api_key), username)
696 730 return False
697 731 if not dbuser.active:
698 732 log.debug('User `%s:%s` is inactive, skipping fill data',
699 733 username, user_id)
700 734 return False
701 735
702 736 log.debug('AuthUser: filling found user:%s data', dbuser)
703 737 user_data = dbuser.get_dict()
704 738
705 739 user_data.update({
706 740 # set explicit the safe escaped values
707 741 'first_name': dbuser.first_name,
708 742 'last_name': dbuser.last_name,
709 743 })
710 744
711 745 for k, v in user_data.items():
712 746 # properties of auth user we dont update
713 747 if k not in ['auth_tokens', 'permissions']:
714 748 setattr(auth_user, k, v)
715 749
716 750 except Exception:
717 751 log.error(traceback.format_exc())
718 752 auth_user.is_authenticated = False
719 753 return False
720 754
721 755 return True
722 756
723 757 def has_perm(self, user, perm):
724 758 perm = self._get_perm(perm)
725 759 user = self._get_user(user)
726 760
727 761 return UserToPerm.query().filter(UserToPerm.user == user)\
728 762 .filter(UserToPerm.permission == perm).scalar() is not None
729 763
730 764 def grant_perm(self, user, perm):
731 765 """
732 766 Grant user global permissions
733 767
734 768 :param user:
735 769 :param perm:
736 770 """
737 771 user = self._get_user(user)
738 772 perm = self._get_perm(perm)
739 773 # if this permission is already granted skip it
740 774 _perm = UserToPerm.query()\
741 775 .filter(UserToPerm.user == user)\
742 776 .filter(UserToPerm.permission == perm)\
743 777 .scalar()
744 778 if _perm:
745 779 return
746 780 new = UserToPerm()
747 781 new.user = user
748 782 new.permission = perm
749 783 self.sa.add(new)
750 784 return new
751 785
752 786 def revoke_perm(self, user, perm):
753 787 """
754 788 Revoke users global permissions
755 789
756 790 :param user:
757 791 :param perm:
758 792 """
759 793 user = self._get_user(user)
760 794 perm = self._get_perm(perm)
761 795
762 796 obj = UserToPerm.query()\
763 797 .filter(UserToPerm.user == user)\
764 798 .filter(UserToPerm.permission == perm)\
765 799 .scalar()
766 800 if obj:
767 801 self.sa.delete(obj)
768 802
769 803 def add_extra_email(self, user, email):
770 804 """
771 805 Adds email address to UserEmailMap
772 806
773 807 :param user:
774 808 :param email:
775 809 """
776 810
777 811 user = self._get_user(user)
778 812
779 813 obj = UserEmailMap()
780 814 obj.user = user
781 815 obj.email = email
782 816 self.sa.add(obj)
783 817 return obj
784 818
785 819 def delete_extra_email(self, user, email_id):
786 820 """
787 821 Removes email address from UserEmailMap
788 822
789 823 :param user:
790 824 :param email_id:
791 825 """
792 826 user = self._get_user(user)
793 827 obj = UserEmailMap.query().get(email_id)
794 828 if obj and obj.user_id == user.user_id:
795 829 self.sa.delete(obj)
796 830
797 831 def parse_ip_range(self, ip_range):
798 832 ip_list = []
799 833
800 834 def make_unique(value):
801 835 seen = []
802 836 return [c for c in value if not (c in seen or seen.append(c))]
803 837
804 838 # firsts split by commas
805 839 for ip_range in ip_range.split(','):
806 840 if not ip_range:
807 841 continue
808 842 ip_range = ip_range.strip()
809 843 if '-' in ip_range:
810 844 start_ip, end_ip = ip_range.split('-', 1)
811 845 start_ip = ipaddress.ip_address(safe_unicode(start_ip.strip()))
812 846 end_ip = ipaddress.ip_address(safe_unicode(end_ip.strip()))
813 847 parsed_ip_range = []
814 848
815 849 for index in xrange(int(start_ip), int(end_ip) + 1):
816 850 new_ip = ipaddress.ip_address(index)
817 851 parsed_ip_range.append(str(new_ip))
818 852 ip_list.extend(parsed_ip_range)
819 853 else:
820 854 ip_list.append(ip_range)
821 855
822 856 return make_unique(ip_list)
823 857
824 858 def add_extra_ip(self, user, ip, description=None):
825 859 """
826 860 Adds ip address to UserIpMap
827 861
828 862 :param user:
829 863 :param ip:
830 864 """
831 865
832 866 user = self._get_user(user)
833 867 obj = UserIpMap()
834 868 obj.user = user
835 869 obj.ip_addr = ip
836 870 obj.description = description
837 871 self.sa.add(obj)
838 872 return obj
839 873
840 874 auth_token_role = AuthTokenModel.cls
841 875
842 876 def add_auth_token(self, user, lifetime_minutes, role, description=u'',
843 877 scope_callback=None):
844 878 """
845 879 Add AuthToken for user.
846 880
847 881 :param user: username/user_id
848 882 :param lifetime_minutes: in minutes the lifetime for token, -1 equals no limit
849 883 :param role: one of AuthTokenModel.cls.ROLE_*
850 884 :param description: optional string description
851 885 """
852 886
853 887 token = AuthTokenModel().create(
854 888 user, description, lifetime_minutes, role)
855 889 if scope_callback and callable(scope_callback):
856 890 # call the callback if we provide, used to attach scope for EE edition
857 891 scope_callback(token)
858 892 return token
859 893
860 894 def delete_extra_ip(self, user, ip_id):
861 895 """
862 896 Removes ip address from UserIpMap
863 897
864 898 :param user:
865 899 :param ip_id:
866 900 """
867 901 user = self._get_user(user)
868 902 obj = UserIpMap.query().get(ip_id)
869 903 if obj and obj.user_id == user.user_id:
870 904 self.sa.delete(obj)
871 905
872 906 def get_accounts_in_creation_order(self, current_user=None):
873 907 """
874 908 Get accounts in order of creation for deactivation for license limits
875 909
876 910 pick currently logged in user, and append to the list in position 0
877 911 pick all super-admins in order of creation date and add it to the list
878 912 pick all other accounts in order of creation and add it to the list.
879 913
880 914 Based on that list, the last accounts can be disabled as they are
881 915 created at the end and don't include any of the super admins as well
882 916 as the current user.
883 917
884 918 :param current_user: optionally current user running this operation
885 919 """
886 920
887 921 if not current_user:
888 922 current_user = get_current_rhodecode_user()
889 923 active_super_admins = [
890 924 x.user_id for x in User.query()
891 925 .filter(User.user_id != current_user.user_id)
892 926 .filter(User.active == true())
893 927 .filter(User.admin == true())
894 928 .order_by(User.created_on.asc())]
895 929
896 930 active_regular_users = [
897 931 x.user_id for x in User.query()
898 932 .filter(User.user_id != current_user.user_id)
899 933 .filter(User.active == true())
900 934 .filter(User.admin == false())
901 935 .order_by(User.created_on.asc())]
902 936
903 937 list_of_accounts = [current_user.user_id]
904 938 list_of_accounts += active_super_admins
905 939 list_of_accounts += active_regular_users
906 940
907 941 return list_of_accounts
908 942
909 943 def deactivate_last_users(self, expected_users, current_user=None):
910 944 """
911 945 Deactivate accounts that are over the license limits.
912 946 Algorithm of which accounts to disabled is based on the formula:
913 947
914 948 Get current user, then super admins in creation order, then regular
915 949 active users in creation order.
916 950
917 951 Using that list we mark all accounts from the end of it as inactive.
918 952 This way we block only latest created accounts.
919 953
920 954 :param expected_users: list of users in special order, we deactivate
921 955 the end N amount of users from that list
922 956 """
923 957
924 958 list_of_accounts = self.get_accounts_in_creation_order(
925 959 current_user=current_user)
926 960
927 961 for acc_id in list_of_accounts[expected_users + 1:]:
928 962 user = User.get(acc_id)
929 963 log.info('Deactivating account %s for license unlock', user)
930 964 user.active = False
931 965 Session().add(user)
932 966 Session().commit()
933 967
934 968 return
935 969
936 970 def get_user_log(self, user, filter_term):
937 971 user_log = UserLog.query()\
938 972 .filter(or_(UserLog.user_id == user.user_id,
939 973 UserLog.username == user.username))\
940 974 .options(joinedload(UserLog.user))\
941 975 .options(joinedload(UserLog.repository))\
942 976 .order_by(UserLog.action_date.desc())
943 977
944 978 user_log = user_log_filter(user_log, filter_term)
945 979 return user_log
@@ -1,171 +1,195 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2
3 3 <%
4 4 elems = [
5 5 (_('User ID'), c.user.user_id, '', ''),
6 6 (_('Created on'), h.format_date(c.user.created_on), '', ''),
7 7 (_('Source of Record'), c.user.extern_type, '', ''),
8 8
9 9 (_('Last login'), c.user.last_login or '-', '', ''),
10 10 (_('Last activity'), c.user.last_activity, '', ''),
11 11
12 12 (_('Repositories'), len(c.user.repositories), '', [x.repo_name for x in c.user.repositories]),
13 13 (_('Repository groups'), len(c.user.repository_groups), '', [x.group_name for x in c.user.repository_groups]),
14 14 (_('User groups'), len(c.user.user_groups), '', [x.users_group_name for x in c.user.user_groups]),
15 15
16 (_('Owned Artifacts'), len(c.user.artifacts), '', [x.file_uid for x in c.user.artifacts]),
17
16 18 (_('Reviewer of pull requests'), len(c.user.reviewer_pull_requests), '', ['Pull Request #{}'.format(x.pull_request.pull_request_id) for x in c.user.reviewer_pull_requests]),
17 19 (_('Assigned to review rules'), len(c.user_to_review_rules), '', [x for x in c.user_to_review_rules]),
18 20
19 21 (_('Member of User groups'), len(c.user.group_member), '', [x.users_group.users_group_name for x in c.user.group_member]),
20 22 (_('Force password change'), c.user.user_data.get('force_password_change', 'False'), '', ''),
21 23 ]
22 24 %>
23 25
24 26 <div class="panel panel-default">
25 27 <div class="panel-heading">
26 <h3 class="panel-title">${_('User: %s') % c.user.username}</h3>
28 <h3 class="panel-title">${_('User: {}').format(c.user.username)}</h3>
27 29 </div>
28 30 <div class="panel-body">
29 ${base.dt_info_panel(elems)}
31 <table class="rctable">
32 <tr>
33 <th>Name</th>
34 <th>Value</th>
35 <th>Action</th>
36 </tr>
37 % for elem in elems:
38 ${base.tr_info_entry(elem)}
39 % endfor
40 </table>
30 41 </div>
31 42 </div>
32 43
33 44 <div class="panel panel-default">
34 45 <div class="panel-heading">
35 46 <h3 class="panel-title">${_('Force Password Reset')}</h3>
36 47 </div>
37 48 <div class="panel-body">
38 49 ${h.secure_form(h.route_path('user_disable_force_password_reset', user_id=c.user.user_id), request=request)}
39 50 <div class="field">
40 51 <button class="btn btn-default" type="submit">
41 52 <i class="icon-unlock"></i> ${_('Disable forced password reset')}
42 53 </button>
43 54 </div>
44 55 <div class="field">
45 56 <span class="help-block">
46 57 ${_("Clear the forced password change flag.")}
47 58 </span>
48 59 </div>
49 60 ${h.end_form()}
50 61
51 62 ${h.secure_form(h.route_path('user_enable_force_password_reset', user_id=c.user.user_id), request=request)}
52 63 <div class="field">
53 64 <button class="btn btn-default" type="submit" onclick="return confirm('${_('Confirm to enable forced password change')}');">
54 65 <i class="icon-lock"></i> ${_('Enable forced password reset')}
55 66 </button>
56 67 </div>
57 68 <div class="field">
58 69 <span class="help-block">
59 70 ${_("When this is enabled user will have to change they password when they next use RhodeCode system. This will also forbid vcs operations until someone makes a password change in the web interface")}
60 71 </span>
61 72 </div>
62 73 ${h.end_form()}
63 74
64 75 </div>
65 76 </div>
66 77
67 78 <div class="panel panel-default">
68 79 <div class="panel-heading">
69 80 <h3 class="panel-title">${_('Personal Repository Group')}</h3>
70 81 </div>
71 82 <div class="panel-body">
72 83 ${h.secure_form(h.route_path('user_create_personal_repo_group', user_id=c.user.user_id), request=request)}
73 84
74 85 %if c.personal_repo_group:
75 86 <div class="panel-body-title-text">${_('Users personal repository group')} : ${h.link_to(c.personal_repo_group.group_name, h.route_path('repo_group_home', repo_group_name=c.personal_repo_group.group_name))}</div>
76 87 %else:
77 88 <div class="panel-body-title-text">
78 89 ${_('This user currently does not have a personal repository group')}
79 90 <br/>
80 91 ${_('New group will be created at: `/%(path)s`') % {'path': c.personal_repo_group_name}}
81 92 </div>
82 93 %endif
83 94 <button class="btn btn-default" type="submit" ${'disabled="disabled"' if c.personal_repo_group else ''}>
84 95 <i class="icon-repo-group"></i>
85 96 ${_('Create personal repository group')}
86 97 </button>
87 98 ${h.end_form()}
88 99 </div>
89 100 </div>
90 101
91 102
92 103 <div class="panel panel-danger">
93 104 <div class="panel-heading">
94 105 <h3 class="panel-title">${_('Delete User')}</h3>
95 106 </div>
96 107 <div class="panel-body">
97 108 ${h.secure_form(h.route_path('user_delete', user_id=c.user.user_id), request=request)}
98 109
99 110 <table class="display rctable">
100 111 <tr>
101 112 <td>
102 113 ${_ungettext('This user owns %s repository.', 'This user owns %s repositories.', len(c.user.repositories)) % len(c.user.repositories)}
103 114 </td>
104 115 <td>
105 116 <input type="radio" id="user_repos_1" name="user_repos" value="detach" checked="checked" ${'disabled=1' if len(c.user.repositories) == 0 else ''} /> <label for="user_repos_1">${_('Detach repositories')}</label>
106 117 </td>
107 118 <td>
108 119 <input type="radio" id="user_repos_2" name="user_repos" value="delete" ${'disabled=1' if len(c.user.repositories) == 0 else ''} /> <label for="user_repos_2">${_('Delete repositories')}</label>
109 120 </td>
110 121 </tr>
111 122
112 123 <tr>
113 124 <td>
114 125 ${_ungettext('This user owns %s repository group.', 'This user owns %s repository groups.', len(c.user.repository_groups)) % len(c.user.repository_groups)}
115 126 </td>
116 127 <td>
117 128 <input type="radio" id="user_repo_groups_1" name="user_repo_groups" value="detach" checked="checked" ${'disabled=1' if len(c.user.repository_groups) == 0 else ''} /> <label for="user_repo_groups_1">${_('Detach repository groups')}</label>
118 129 </td>
119 130 <td>
120 131 <input type="radio" id="user_repo_groups_2" name="user_repo_groups" value="delete" ${'disabled=1' if len(c.user.repository_groups) == 0 else ''}/> <label for="user_repo_groups_2">${_('Delete repositories')}</label>
121 132 </td>
122 133 </tr>
123 134
124 135 <tr>
125 136 <td>
126 137 ${_ungettext('This user owns %s user group.', 'This user owns %s user groups.', len(c.user.user_groups)) % len(c.user.user_groups)}
127 138 </td>
128 139 <td>
129 140 <input type="radio" id="user_user_groups_1" name="user_user_groups" value="detach" checked="checked" ${'disabled=1' if len(c.user.user_groups) == 0 else ''}/> <label for="user_user_groups_1">${_('Detach user groups')}</label>
130 141 </td>
131 142 <td>
132 143 <input type="radio" id="user_user_groups_2" name="user_user_groups" value="delete" ${'disabled=1' if len(c.user.user_groups) == 0 else ''}/> <label for="user_user_groups_2">${_('Delete repositories')}</label>
133 144 </td>
134 145 </tr>
146
147 <tr>
148 <td>
149 ${_ungettext('This user owns %s artifact.', 'This user owns %s artifacts.', len(c.user.artifacts)) % len(c.user.artifacts)}
150 </td>
151 <td>
152 <input type="radio" id="user_artifacts_1" name="user_artifacts" value="detach" checked="checked" ${'disabled=1' if len(c.user.artifacts) == 0 else ''}/> <label for="user_artifacts_1">${_('Detach Artifacts')}</label>
153 </td>
154 <td>
155 <input type="radio" id="user_artifacts_2" name="user_artifacts" value="delete" ${'disabled=1' if len(c.user.artifacts) == 0 else ''}/> <label for="user_artifacts_2">${_('Delete Artifacts')}</label>
156 </td>
157 </tr>
158
135 159 </table>
136 160 <div style="margin: 0 0 20px 0" class="fake-space"></div>
137 161 <div class="pull-left">
138 162 % if len(c.user.repositories) > 0 or len(c.user.repository_groups) > 0 or len(c.user.user_groups) > 0:
139 163 % endif
140 164
141 165 <span style="padding: 0 5px 0 0">${_('New owner for detached objects')}:</span>
142 166 <div class="pull-right">${base.gravatar_with_user(c.first_admin.email, 16)}</div>
143 167 </div>
144 168 <div style="clear: both">
145 169
146 170 <div>
147 171 <p class="help-block">
148 172 ${_("When selecting the detach option, the depending objects owned by this user will be assigned to the above user.")}
149 173 <br/>
150 174 ${_("The delete option will delete the user and all his owned objects!")}
151 175 </p>
152 176 </div>
153 177
154 178 % if c.can_delete_user_message:
155 179 <p class="pre-formatting">${c.can_delete_user_message}</p>
156 180 % endif
157 181 </div>
158 182
159 183 <div style="margin: 0 0 20px 0" class="fake-space"></div>
160 184
161 185 <div class="field">
162 186 <button class="btn btn-small btn-danger" type="submit"
163 187 onclick="return confirm('${_('Confirm to delete this user: %s') % c.user.username}');"
164 188 ${"disabled" if not c.can_delete_user else ""}>
165 189 ${_('Delete this user')}
166 190 </button>
167 191 </div>
168 192
169 193 ${h.end_form()}
170 194 </div>
171 195 </div>
@@ -1,1063 +1,1093 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="root.mako"/>
3 3
4 4 <%include file="/ejs_templates/templates.html"/>
5 5
6 6 <div class="outerwrapper">
7 7 <!-- HEADER -->
8 8 <div class="header">
9 9 <div id="header-inner" class="wrapper">
10 10 <div id="logo">
11 11 <div class="logo-wrapper">
12 12 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-60x60.png')}" alt="RhodeCode"/></a>
13 13 </div>
14 14 % if c.rhodecode_name:
15 15 <div class="branding">
16 16 <a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
17 17 </div>
18 18 % endif
19 19 </div>
20 20 <!-- MENU BAR NAV -->
21 21 ${self.menu_bar_nav()}
22 22 <!-- END MENU BAR NAV -->
23 23 </div>
24 24 </div>
25 25 ${self.menu_bar_subnav()}
26 26 <!-- END HEADER -->
27 27
28 28 <!-- CONTENT -->
29 29 <div id="content" class="wrapper">
30 30
31 31 <rhodecode-toast id="notifications"></rhodecode-toast>
32 32
33 33 <div class="main">
34 34 ${next.main()}
35 35 </div>
36 36 </div>
37 37 <!-- END CONTENT -->
38 38
39 39 </div>
40 40 <!-- FOOTER -->
41 41 <div id="footer">
42 42 <div id="footer-inner" class="title wrapper">
43 43 <div>
44 44 <p class="footer-link-right">
45 45 % if c.visual.show_version:
46 46 RhodeCode Enterprise ${c.rhodecode_version} ${c.rhodecode_edition}
47 47 % endif
48 48 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
49 49 % if c.visual.rhodecode_support_url:
50 50 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
51 51 % endif
52 52 </p>
53 53 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
54 54 <p class="server-instance" style="display:${sid}">
55 55 ## display hidden instance ID if specially defined
56 56 % if c.rhodecode_instanceid:
57 57 ${_('RhodeCode instance id: {}').format(c.rhodecode_instanceid)}
58 58 % endif
59 59 </p>
60 60 </div>
61 61 </div>
62 62 </div>
63 63
64 64 <!-- END FOOTER -->
65 65
66 66 ### MAKO DEFS ###
67 67
68 68 <%def name="menu_bar_subnav()">
69 69 </%def>
70 70
71 71 <%def name="breadcrumbs(class_='breadcrumbs')">
72 72 <div class="${class_}">
73 73 ${self.breadcrumbs_links()}
74 74 </div>
75 75 </%def>
76 76
77 77 <%def name="admin_menu(active=None)">
78 78 <%
79 79 def is_active(selected):
80 80 if selected == active:
81 81 return "active"
82 82 %>
83 83
84 84 <div id="context-bar">
85 85 <div class="wrapper">
86 86 <div class="title">
87 87 <div class="title-content">
88 88 <div class="title-main">
89 89 % if c.is_super_admin:
90 90 ${_('Super Admin Panel')}
91 91 % else:
92 92 ${_('Delegated Admin Panel')}
93 93 % endif
94 94 </div>
95 95 </div>
96 96 </div>
97 97
98 98 <ul id="context-pages" class="navigation horizontal-list">
99 99
100 100 ## super admin case
101 101 % if c.is_super_admin:
102 102 <li class="${is_active('audit_logs')}"><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
103 103 <li class="${is_active('repositories')}"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
104 104 <li class="${is_active('repository_groups')}"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
105 105 <li class="${is_active('users')}"><a href="${h.route_path('users')}">${_('Users')}</a></li>
106 106 <li class="${is_active('user_groups')}"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
107 107 <li class="${is_active('permissions')}"><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
108 108 <li class="${is_active('authentication')}"><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
109 109 <li class="${is_active('integrations')}"><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
110 110 <li class="${is_active('defaults')}"><a href="${h.route_path('admin_defaults_repositories')}">${_('Defaults')}</a></li>
111 111 <li class="${is_active('settings')}"><a href="${h.route_path('admin_settings')}">${_('Settings')}</a></li>
112 112
113 113 ## delegated admin
114 114 % elif c.is_delegated_admin:
115 115 <%
116 116 repositories=c.auth_user.repositories_admin or c.can_create_repo
117 117 repository_groups=c.auth_user.repository_groups_admin or c.can_create_repo_group
118 118 user_groups=c.auth_user.user_groups_admin or c.can_create_user_group
119 119 %>
120 120
121 121 %if repositories:
122 122 <li class="${is_active('repositories')} local-admin-repos"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
123 123 %endif
124 124 %if repository_groups:
125 125 <li class="${is_active('repository_groups')} local-admin-repo-groups"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
126 126 %endif
127 127 %if user_groups:
128 128 <li class="${is_active('user_groups')} local-admin-user-groups"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
129 129 %endif
130 130 % endif
131 131 </ul>
132 132
133 133 </div>
134 134 <div class="clear"></div>
135 135 </div>
136 136 </%def>
137 137
138 138 <%def name="dt_info_panel(elements)">
139 139 <dl class="dl-horizontal">
140 140 %for dt, dd, title, show_items in elements:
141 141 <dt>${dt}:</dt>
142 142 <dd title="${h.tooltip(title)}">
143 143 %if callable(dd):
144 144 ## allow lazy evaluation of elements
145 145 ${dd()}
146 146 %else:
147 147 ${dd}
148 148 %endif
149 149 %if show_items:
150 150 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
151 151 %endif
152 152 </dd>
153 153
154 154 %if show_items:
155 155 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
156 156 %for item in show_items:
157 157 <dt></dt>
158 158 <dd>${item}</dd>
159 159 %endfor
160 160 </div>
161 161 %endif
162 162
163 163 %endfor
164 164 </dl>
165 165 </%def>
166 166
167 <%def name="tr_info_entry(element)">
168 <% key, val, title, show_items = element %>
169
170 <tr>
171 <td style="vertical-align: top">${key}</td>
172 <td title="${h.tooltip(title)}">
173 %if callable(val):
174 ## allow lazy evaluation of elements
175 ${val()}
176 %else:
177 ${val}
178 %endif
179 %if show_items:
180 <div class="collapsable-content" data-toggle="item-${h.md5_safe(val)[:6]}-details" style="display: none">
181 % for item in show_items:
182 <dt></dt>
183 <dd>${item}</dd>
184 % endfor
185 </div>
186 %endif
187 </td>
188 <td style="vertical-align: top">
189 %if show_items:
190 <span class="btn-collapse" data-toggle="item-${h.md5_safe(val)[:6]}-details">${_('Show More')} </span>
191 %endif
192 </td>
193 </tr>
194
195 </%def>
196
167 197 <%def name="gravatar(email, size=16)">
168 198 <%
169 199 if (size > 16):
170 200 gravatar_class = 'gravatar gravatar-large'
171 201 else:
172 202 gravatar_class = 'gravatar'
173 203 %>
174 204 <%doc>
175 205 TODO: johbo: For now we serve double size images to make it smooth
176 206 for retina. This is how it worked until now. Should be replaced
177 207 with a better solution at some point.
178 208 </%doc>
179 209 <img class="${gravatar_class}" src="${h.gravatar_url(email, size * 2)}" height="${size}" width="${size}">
180 210 </%def>
181 211
182 212
183 213 <%def name="gravatar_with_user(contact, size=16, show_disabled=False)">
184 214 <% email = h.email_or_none(contact) %>
185 215 <div class="rc-user tooltip" title="${h.tooltip(h.author_string(email))}">
186 216 ${self.gravatar(email, size)}
187 217 <span class="${'user user-disabled' if show_disabled else 'user'}"> ${h.link_to_user(contact)}</span>
188 218 </div>
189 219 </%def>
190 220
191 221
192 222 <%def name="repo_page_title(repo_instance)">
193 223 <div class="title-content repo-title">
194 224
195 225 <div class="title-main">
196 226 ## SVN/HG/GIT icons
197 227 %if h.is_hg(repo_instance):
198 228 <i class="icon-hg"></i>
199 229 %endif
200 230 %if h.is_git(repo_instance):
201 231 <i class="icon-git"></i>
202 232 %endif
203 233 %if h.is_svn(repo_instance):
204 234 <i class="icon-svn"></i>
205 235 %endif
206 236
207 237 ## public/private
208 238 %if repo_instance.private:
209 239 <i class="icon-repo-private"></i>
210 240 %else:
211 241 <i class="icon-repo-public"></i>
212 242 %endif
213 243
214 244 ## repo name with group name
215 245 ${h.breadcrumb_repo_link(repo_instance)}
216 246
217 247 ## Context Actions
218 248 <div class="pull-right">
219 249 %if c.rhodecode_user.username != h.DEFAULT_USER:
220 250 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid, _query=dict(auth_token=c.rhodecode_user.feed_token))}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
221 251
222 252 <a href="#WatchRepo" onclick="toggleFollowingRepo(this, templateContext.repo_id); return false" title="${_('Watch this Repository and actions on it in your personalized journal')}" class="btn btn-sm ${('watching' if c.repository_is_user_following else '')}">
223 253 % if c.repository_is_user_following:
224 254 <i class="icon-eye-off"></i>${_('Unwatch')}
225 255 % else:
226 256 <i class="icon-eye"></i>${_('Watch')}
227 257 % endif
228 258
229 259 </a>
230 260 %else:
231 261 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid)}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
232 262 %endif
233 263 </div>
234 264
235 265 </div>
236 266
237 267 ## FORKED
238 268 %if repo_instance.fork:
239 269 <p class="discreet">
240 270 <i class="icon-code-fork"></i> ${_('Fork of')}
241 271 ${h.link_to_if(c.has_origin_repo_read_perm,repo_instance.fork.repo_name, h.route_path('repo_summary', repo_name=repo_instance.fork.repo_name))}
242 272 </p>
243 273 %endif
244 274
245 275 ## IMPORTED FROM REMOTE
246 276 %if repo_instance.clone_uri:
247 277 <p class="discreet">
248 278 <i class="icon-code-fork"></i> ${_('Clone from')}
249 279 <a href="${h.safe_str(h.hide_credentials(repo_instance.clone_uri))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
250 280 </p>
251 281 %endif
252 282
253 283 ## LOCKING STATUS
254 284 %if repo_instance.locked[0]:
255 285 <p class="locking_locked discreet">
256 286 <i class="icon-repo-lock"></i>
257 287 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
258 288 </p>
259 289 %elif repo_instance.enable_locking:
260 290 <p class="locking_unlocked discreet">
261 291 <i class="icon-repo-unlock"></i>
262 292 ${_('Repository not locked. Pull repository to lock it.')}
263 293 </p>
264 294 %endif
265 295
266 296 </div>
267 297 </%def>
268 298
269 299 <%def name="repo_menu(active=None)">
270 300 <%
271 301 def is_active(selected):
272 302 if selected == active:
273 303 return "active"
274 304 ## determine if we have "any" option available
275 305 can_lock = h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking
276 306 has_actions = can_lock
277 307
278 308 %>
279 309 % if c.rhodecode_db_repo.archived:
280 310 <div class="alert alert-warning text-center">
281 311 <strong>${_('This repository has been archived. It is now read-only.')}</strong>
282 312 </div>
283 313 % endif
284 314
285 315 <!--- REPO CONTEXT BAR -->
286 316 <div id="context-bar">
287 317 <div class="wrapper">
288 318
289 319 <div class="title">
290 320 ${self.repo_page_title(c.rhodecode_db_repo)}
291 321 </div>
292 322
293 323 <ul id="context-pages" class="navigation horizontal-list">
294 324 <li class="${is_active('summary')}"><a class="menulink" href="${h.route_path('repo_summary', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
295 325 <li class="${is_active('commits')}"><a class="menulink" href="${h.route_path('repo_commits', repo_name=c.repo_name)}"><div class="menulabel">${_('Commits')}</div></a></li>
296 326 <li class="${is_active('files')}"><a class="menulink" href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.rhodecode_db_repo.landing_rev[1], f_path='')}"><div class="menulabel">${_('Files')}</div></a></li>
297 327 <li class="${is_active('compare')}"><a class="menulink" href="${h.route_path('repo_compare_select',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a></li>
298 328
299 329 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
300 330 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
301 331 <li class="${is_active('showpullrequest')}">
302 332 <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${h.tooltip(_('Show Pull Requests for %s') % c.repo_name)}">
303 333 <div class="menulabel">
304 334 ${_('Pull Requests')} <span class="menulink-counter">${c.repository_pull_requests}</span>
305 335 </div>
306 336 </a>
307 337 </li>
308 338 %endif
309 339
310 340 <li class="${is_active('artifacts')}">
311 341 <a class="menulink" href="${h.route_path('repo_artifacts_list',repo_name=c.repo_name)}">
312 342 <div class="menulabel">
313 343 ${_('Artifacts')} <span class="menulink-counter">${c.repository_artifacts}</span>
314 344 </div>
315 345 </a>
316 346 </li>
317 347
318 348 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
319 349 <li class="${is_active('settings')}"><a class="menulink" href="${h.route_path('edit_repo',repo_name=c.repo_name)}"><div class="menulabel">${_('Repository Settings')}</div></a></li>
320 350 %endif
321 351
322 352 <li class="${is_active('options')}">
323 353 % if has_actions:
324 354 <a class="menulink dropdown">
325 355 <div class="menulabel">${_('Options')}<div class="show_more"></div></div>
326 356 </a>
327 357 <ul class="submenu">
328 358 %if can_lock:
329 359 %if c.rhodecode_db_repo.locked[0]:
330 360 <li><a class="locking_del" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Unlock Repository')}</a></li>
331 361 %else:
332 362 <li><a class="locking_add" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Lock Repository')}</a></li>
333 363 %endif
334 364 %endif
335 365 </ul>
336 366 % else:
337 367 <a class="menulink disabled">
338 368 <div class="menulabel">${_('Options')}<div class="show_more"></div></div>
339 369 </a>
340 370 % endif
341 371 </li>
342 372
343 373 </ul>
344 374 </div>
345 375 <div class="clear"></div>
346 376 </div>
347 377
348 378 <!--- REPO END CONTEXT BAR -->
349 379
350 380 </%def>
351 381
352 382 <%def name="repo_group_page_title(repo_group_instance)">
353 383 <div class="title-content">
354 384 <div class="title-main">
355 385 ## Repository Group icon
356 386 <i class="icon-repo-group"></i>
357 387
358 388 ## repo name with group name
359 389 ${h.breadcrumb_repo_group_link(repo_group_instance)}
360 390 </div>
361 391
362 392 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
363 393 <div class="repo-group-desc discreet">
364 394 ${dt.repo_group_desc(repo_group_instance.description_safe, repo_group_instance.personal, c.visual.stylify_metatags)}
365 395 </div>
366 396
367 397 </div>
368 398 </%def>
369 399
370 400
371 401 <%def name="repo_group_menu(active=None)">
372 402 <%
373 403 def is_active(selected):
374 404 if selected == active:
375 405 return "active"
376 406
377 407 gr_name = c.repo_group.group_name if c.repo_group else None
378 408 # create repositories with write permission on group is set to true
379 409 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
380 410
381 411 %>
382 412
383 413
384 414 <!--- REPO GROUP CONTEXT BAR -->
385 415 <div id="context-bar">
386 416 <div class="wrapper">
387 417 <div class="title">
388 418 ${self.repo_group_page_title(c.repo_group)}
389 419 </div>
390 420
391 421 <ul id="context-pages" class="navigation horizontal-list">
392 422 <li class="${is_active('home')}">
393 423 <a class="menulink" href="${h.route_path('repo_group_home', repo_group_name=c.repo_group.group_name)}"><div class="menulabel">${_('Group Home')}</div></a>
394 424 </li>
395 425 % if c.is_super_admin or group_admin:
396 426 <li class="${is_active('settings')}">
397 427 <a class="menulink" href="${h.route_path('edit_repo_group',repo_group_name=c.repo_group.group_name)}" title="${_('You have admin right to this group, and can edit it')}"><div class="menulabel">${_('Group Settings')}</div></a>
398 428 </li>
399 429 % endif
400 430
401 431 </ul>
402 432 </div>
403 433 <div class="clear"></div>
404 434 </div>
405 435
406 436 <!--- REPO GROUP CONTEXT BAR -->
407 437
408 438 </%def>
409 439
410 440
411 441 <%def name="usermenu(active=False)">
412 442 <%
413 443 not_anonymous = c.rhodecode_user.username != h.DEFAULT_USER
414 444
415 445 gr_name = c.repo_group.group_name if (hasattr(c, 'repo_group') and c.repo_group) else None
416 446 # create repositories with write permission on group is set to true
417 447
418 448 can_fork = c.is_super_admin or h.HasPermissionAny('hg.fork.repository')()
419 449 create_on_write = h.HasPermissionAny('hg.create.write_on_repogroup.true')()
420 450 group_write = h.HasRepoGroupPermissionAny('group.write')(gr_name, 'can write into group index page')
421 451 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
422 452
423 453 can_create_repos = c.is_super_admin or c.can_create_repo
424 454 can_create_repo_groups = c.is_super_admin or c.can_create_repo_group
425 455
426 456 can_create_repos_in_group = c.is_super_admin or group_admin or (group_write and create_on_write)
427 457 can_create_repo_groups_in_group = c.is_super_admin or group_admin
428 458 %>
429 459
430 460 % if not_anonymous:
431 461 <%
432 462 default_target_group = dict()
433 463 if c.rhodecode_user.personal_repo_group:
434 464 default_target_group = dict(parent_group=c.rhodecode_user.personal_repo_group.group_id)
435 465 %>
436 466
437 467 ## create action
438 468 <li>
439 469 <a href="#create-actions" onclick="return false;" class="menulink childs">
440 470 <i class="icon-plus-circled"></i>
441 471 </a>
442 472
443 473 <div class="action-menu submenu">
444 474
445 475 <ol>
446 476 ## scope of within a repository
447 477 % if hasattr(c, 'rhodecode_db_repo') and c.rhodecode_db_repo:
448 478 <li class="submenu-title">${_('This Repository')}</li>
449 479 <li>
450 480 <a href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">${_('Create Pull Request')}</a>
451 481 </li>
452 482 % if can_fork:
453 483 <li>
454 484 <a href="${h.route_path('repo_fork_new',repo_name=c.repo_name,_query=default_target_group)}">${_('Fork this repository')}</a>
455 485 </li>
456 486 % endif
457 487 % endif
458 488
459 489 ## scope of within repository groups
460 490 % if hasattr(c, 'repo_group') and c.repo_group and (can_create_repos_in_group or can_create_repo_groups_in_group):
461 491 <li class="submenu-title">${_('This Repository Group')}</li>
462 492
463 493 % if can_create_repos_in_group:
464 494 <li>
465 495 <a href="${h.route_path('repo_new',_query=default_target_group)}">${_('New Repository')}</a>
466 496 </li>
467 497 % endif
468 498
469 499 % if can_create_repo_groups_in_group:
470 500 <li>
471 501 <a href="${h.route_path('repo_group_new',_query=default_target_group)}">${_(u'New Repository Group')}</a>
472 502 </li>
473 503 % endif
474 504 % endif
475 505
476 506 ## personal group
477 507 % if c.rhodecode_user.personal_repo_group:
478 508 <li class="submenu-title">Personal Group</li>
479 509
480 510 <li>
481 511 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}" >${_('New Repository')} </a>
482 512 </li>
483 513
484 514 <li>
485 515 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}">${_('New Repository Group')} </a>
486 516 </li>
487 517 % endif
488 518
489 519 ## Global actions
490 520 <li class="submenu-title">RhodeCode</li>
491 521 % if can_create_repos:
492 522 <li>
493 523 <a href="${h.route_path('repo_new')}" >${_('New Repository')}</a>
494 524 </li>
495 525 % endif
496 526
497 527 % if can_create_repo_groups:
498 528 <li>
499 529 <a href="${h.route_path('repo_group_new')}" >${_(u'New Repository Group')}</a>
500 530 </li>
501 531 % endif
502 532
503 533 <li>
504 534 <a href="${h.route_path('gists_new')}">${_(u'New Gist')}</a>
505 535 </li>
506 536
507 537 </ol>
508 538
509 539 </div>
510 540 </li>
511 541
512 542 ## notifications
513 543 <li>
514 544 <a class="${('empty' if c.unread_notifications == 0 else '')}" href="${h.route_path('notifications_show_all')}">
515 545 ${c.unread_notifications}
516 546 </a>
517 547 </li>
518 548 % endif
519 549
520 550 ## USER MENU
521 551 <li id="quick_login_li" class="${'active' if active else ''}">
522 552 % if c.rhodecode_user.username == h.DEFAULT_USER:
523 553 <a id="quick_login_link" class="menulink childs" href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">
524 554 ${gravatar(c.rhodecode_user.email, 20)}
525 555 <span class="user">
526 556 <span>${_('Sign in')}</span>
527 557 </span>
528 558 </a>
529 559 % else:
530 560 ## logged in user
531 561 <a id="quick_login_link" class="menulink childs">
532 562 ${gravatar(c.rhodecode_user.email, 20)}
533 563 <span class="user">
534 564 <span class="menu_link_user">${c.rhodecode_user.username}</span>
535 565 <div class="show_more"></div>
536 566 </span>
537 567 </a>
538 568 ## subnav with menu for logged in user
539 569 <div class="user-menu submenu">
540 570 <div id="quick_login">
541 571 %if c.rhodecode_user.username != h.DEFAULT_USER:
542 572 <div class="">
543 573 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
544 574 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
545 575 <div class="email">${c.rhodecode_user.email}</div>
546 576 </div>
547 577 <div class="">
548 578 <ol class="links">
549 579 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
550 580 % if c.rhodecode_user.personal_repo_group:
551 581 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
552 582 % endif
553 583 <li>${h.link_to(_(u'Pull Requests'), h.route_path('my_account_pullrequests'))}</li>
554 584
555 585 % if c.debug_style:
556 586 <li>
557 587 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
558 588 <div class="menulabel">${_('[Style]')}</div>
559 589 </a>
560 590 </li>
561 591 % endif
562 592
563 593 ## bookmark-items
564 594 <li class="bookmark-items">
565 595 ${_('Bookmarks')}
566 596 <div class="pull-right">
567 597 <a href="${h.route_path('my_account_bookmarks')}">
568 598
569 599 <i class="icon-cog"></i>
570 600 </a>
571 601 </div>
572 602 </li>
573 603 % if not c.bookmark_items:
574 604 <li>
575 605 <a href="${h.route_path('my_account_bookmarks')}">${_('No Bookmarks yet.')}</a>
576 606 </li>
577 607 % endif
578 608 % for item in c.bookmark_items:
579 609 <li>
580 610 % if item.repository:
581 611 <div>
582 612 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
583 613 <code>${item.position}</code>
584 614 % if item.repository.repo_type == 'hg':
585 615 <i class="icon-hg" title="${_('Repository')}" style="font-size: 16px"></i>
586 616 % elif item.repository.repo_type == 'git':
587 617 <i class="icon-git" title="${_('Repository')}" style="font-size: 16px"></i>
588 618 % elif item.repository.repo_type == 'svn':
589 619 <i class="icon-svn" title="${_('Repository')}" style="font-size: 16px"></i>
590 620 % endif
591 621 ${(item.title or h.shorter(item.repository.repo_name, 30))}
592 622 </a>
593 623 </div>
594 624 % elif item.repository_group:
595 625 <div>
596 626 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
597 627 <code>${item.position}</code>
598 628 <i class="icon-repo-group" title="${_('Repository group')}" style="font-size: 14px"></i>
599 629 ${(item.title or h.shorter(item.repository_group.group_name, 30))}
600 630 </a>
601 631 </div>
602 632 % else:
603 633 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
604 634 <code>${item.position}</code>
605 635 ${item.title}
606 636 </a>
607 637 % endif
608 638 </li>
609 639 % endfor
610 640
611 641 <li class="logout">
612 642 ${h.secure_form(h.route_path('logout'), request=request)}
613 643 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
614 644 ${h.end_form()}
615 645 </li>
616 646 </ol>
617 647 </div>
618 648 %endif
619 649 </div>
620 650 </div>
621 651
622 652 % endif
623 653 </li>
624 654 </%def>
625 655
626 656 <%def name="menu_items(active=None)">
627 657 <%
628 658 def is_active(selected):
629 659 if selected == active:
630 660 return "active"
631 661 return ""
632 662 %>
633 663
634 664 <ul id="quick" class="main_nav navigation horizontal-list">
635 665 ## notice box for important system messages
636 666 <li style="display: none">
637 667 <a class="notice-box" href="#openNotice" onclick="return false">
638 668 <div class="menulabel-notice" >
639 669 0
640 670 </div>
641 671 </a>
642 672 </li>
643 673
644 674 ## Main filter
645 675 <li>
646 676 <div class="menulabel main_filter_box">
647 677 <div class="main_filter_input_box">
648 678 <ul class="searchItems">
649 679
650 680 % if c.template_context['search_context']['repo_id']:
651 681 <li class="searchTag searchTagFilter searchTagHidable" >
652 682 ##<a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">
653 683 <span class="tag">
654 684 This repo
655 685 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
656 686 </span>
657 687 ##</a>
658 688 </li>
659 689 % elif c.template_context['search_context']['repo_group_id']:
660 690 <li class="searchTag searchTagFilter searchTagHidable">
661 691 ##<a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">
662 692 <span class="tag">
663 693 This group
664 694 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
665 695 </span>
666 696 ##</a>
667 697 </li>
668 698 % endif
669 699
670 700 <li class="searchTagInput">
671 701 <input class="main_filter_input" id="main_filter" size="25" type="text" name="main_filter" placeholder="${_('search / go to...')}" value="" />
672 702 </li>
673 703 <li class="searchTag searchTagHelp">
674 704 <a href="#showFilterHelp" onclick="showMainFilterBox(); return false">?</a>
675 705 </li>
676 706 </ul>
677 707 </div>
678 708 </div>
679 709
680 710 <div id="main_filter_help" style="display: none">
681 711 - Use '/' key to quickly access this field.
682 712
683 713 - Enter a name of repository, or repository group for quick search.
684 714
685 715 - Prefix query to allow special search:
686 716
687 717 user:admin, to search for usernames, always global
688 718
689 719 user_group:devops, to search for user groups, always global
690 720
691 721 commit:efced4, to search for commits, scoped to repositories or groups
692 722
693 723 file:models.py, to search for file paths, scoped to repositories or groups
694 724
695 725 % if c.template_context['search_context']['repo_id']:
696 726 For advanced full text search visit: <a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">repository search</a>
697 727 % elif c.template_context['search_context']['repo_group_id']:
698 728 For advanced full text search visit: <a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">repository group search</a>
699 729 % else:
700 730 For advanced full text search visit: <a href="${h.route_path('search')}">global search</a>
701 731 % endif
702 732 </div>
703 733 </li>
704 734
705 735 ## ROOT MENU
706 736 <li class="${is_active('home')}">
707 737 <a class="menulink" title="${_('Home')}" href="${h.route_path('home')}">
708 738 <div class="menulabel">${_('Home')}</div>
709 739 </a>
710 740 </li>
711 741
712 742 %if c.rhodecode_user.username != h.DEFAULT_USER:
713 743 <li class="${is_active('journal')}">
714 744 <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
715 745 <div class="menulabel">${_('Journal')}</div>
716 746 </a>
717 747 </li>
718 748 %else:
719 749 <li class="${is_active('journal')}">
720 750 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
721 751 <div class="menulabel">${_('Public journal')}</div>
722 752 </a>
723 753 </li>
724 754 %endif
725 755
726 756 <li class="${is_active('gists')}">
727 757 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
728 758 <div class="menulabel">${_('Gists')}</div>
729 759 </a>
730 760 </li>
731 761
732 762 % if c.is_super_admin or c.is_delegated_admin:
733 763 <li class="${is_active('admin')}">
734 764 <a class="menulink childs" title="${_('Admin settings')}" href="${h.route_path('admin_home')}">
735 765 <div class="menulabel">${_('Admin')} </div>
736 766 </a>
737 767 </li>
738 768 % endif
739 769
740 770 ## render extra user menu
741 771 ${usermenu(active=(active=='my_account'))}
742 772
743 773 </ul>
744 774
745 775 <script type="text/javascript">
746 776 var visualShowPublicIcon = "${c.visual.show_public_icon}" == "True";
747 777
748 778 var formatRepoResult = function(result, container, query, escapeMarkup) {
749 779 return function(data, escapeMarkup) {
750 780 if (!data.repo_id){
751 781 return data.text; // optgroup text Repositories
752 782 }
753 783
754 784 var tmpl = '';
755 785 var repoType = data['repo_type'];
756 786 var repoName = data['text'];
757 787
758 788 if(data && data.type == 'repo'){
759 789 if(repoType === 'hg'){
760 790 tmpl += '<i class="icon-hg"></i> ';
761 791 }
762 792 else if(repoType === 'git'){
763 793 tmpl += '<i class="icon-git"></i> ';
764 794 }
765 795 else if(repoType === 'svn'){
766 796 tmpl += '<i class="icon-svn"></i> ';
767 797 }
768 798 if(data['private']){
769 799 tmpl += '<i class="icon-lock" ></i> ';
770 800 }
771 801 else if(visualShowPublicIcon){
772 802 tmpl += '<i class="icon-unlock-alt"></i> ';
773 803 }
774 804 }
775 805 tmpl += escapeMarkup(repoName);
776 806 return tmpl;
777 807
778 808 }(result, escapeMarkup);
779 809 };
780 810
781 811 var formatRepoGroupResult = function(result, container, query, escapeMarkup) {
782 812 return function(data, escapeMarkup) {
783 813 if (!data.repo_group_id){
784 814 return data.text; // optgroup text Repositories
785 815 }
786 816
787 817 var tmpl = '';
788 818 var repoGroupName = data['text'];
789 819
790 820 if(data){
791 821
792 822 tmpl += '<i class="icon-repo-group"></i> ';
793 823
794 824 }
795 825 tmpl += escapeMarkup(repoGroupName);
796 826 return tmpl;
797 827
798 828 }(result, escapeMarkup);
799 829 };
800 830
801 831 var escapeRegExChars = function (value) {
802 832 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
803 833 };
804 834
805 835 var getRepoIcon = function(repo_type) {
806 836 if (repo_type === 'hg') {
807 837 return '<i class="icon-hg"></i> ';
808 838 }
809 839 else if (repo_type === 'git') {
810 840 return '<i class="icon-git"></i> ';
811 841 }
812 842 else if (repo_type === 'svn') {
813 843 return '<i class="icon-svn"></i> ';
814 844 }
815 845 return ''
816 846 };
817 847
818 848 var autocompleteMainFilterFormatResult = function (data, value, org_formatter) {
819 849
820 850 if (value.split(':').length === 2) {
821 851 value = value.split(':')[1]
822 852 }
823 853
824 854 var searchType = data['type'];
825 855 var searchSubType = data['subtype'];
826 856 var valueDisplay = data['value_display'];
827 857
828 858 var pattern = '(' + escapeRegExChars(value) + ')';
829 859
830 860 valueDisplay = Select2.util.escapeMarkup(valueDisplay);
831 861
832 862 // highlight match
833 863 if (searchType != 'text') {
834 864 valueDisplay = valueDisplay.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
835 865 }
836 866
837 867 var icon = '';
838 868
839 869 if (searchType === 'hint') {
840 870 icon += '<i class="icon-repo-group"></i> ';
841 871 }
842 872 // full text search/hints
843 873 else if (searchType === 'search') {
844 874 icon += '<i class="icon-more"></i> ';
845 875 if (searchSubType !== undefined && searchSubType == 'repo') {
846 876 valueDisplay += '<div class="pull-right tag">repository</div>';
847 877 }
848 878 else if (searchSubType !== undefined && searchSubType == 'repo_group') {
849 879 valueDisplay += '<div class="pull-right tag">repo group</div>';
850 880 }
851 881 }
852 882 // repository
853 883 else if (searchType === 'repo') {
854 884
855 885 var repoIcon = getRepoIcon(data['repo_type']);
856 886 icon += repoIcon;
857 887
858 888 if (data['private']) {
859 889 icon += '<i class="icon-lock" ></i> ';
860 890 }
861 891 else if (visualShowPublicIcon) {
862 892 icon += '<i class="icon-unlock-alt"></i> ';
863 893 }
864 894 }
865 895 // repository groups
866 896 else if (searchType === 'repo_group') {
867 897 icon += '<i class="icon-repo-group"></i> ';
868 898 }
869 899 // user group
870 900 else if (searchType === 'user_group') {
871 901 icon += '<i class="icon-group"></i> ';
872 902 }
873 903 // user
874 904 else if (searchType === 'user') {
875 905 icon += '<img class="gravatar" src="{0}"/>'.format(data['icon_link']);
876 906 }
877 907 // commit
878 908 else if (searchType === 'commit') {
879 909 var repo_data = data['repo_data'];
880 910 var repoIcon = getRepoIcon(repo_data['repository_type']);
881 911 if (repoIcon) {
882 912 icon += repoIcon;
883 913 } else {
884 914 icon += '<i class="icon-tag"></i>';
885 915 }
886 916 }
887 917 // file
888 918 else if (searchType === 'file') {
889 919 var repo_data = data['repo_data'];
890 920 var repoIcon = getRepoIcon(repo_data['repository_type']);
891 921 if (repoIcon) {
892 922 icon += repoIcon;
893 923 } else {
894 924 icon += '<i class="icon-tag"></i>';
895 925 }
896 926 }
897 927 // generic text
898 928 else if (searchType === 'text') {
899 929 icon = '';
900 930 }
901 931
902 932 var tmpl = '<div class="ac-container-wrap">{0}{1}</div>';
903 933 return tmpl.format(icon, valueDisplay);
904 934 };
905 935
906 936 var handleSelect = function(element, suggestion) {
907 937 if (suggestion.type === "hint") {
908 938 // we skip action
909 939 $('#main_filter').focus();
910 940 }
911 941 else if (suggestion.type === "text") {
912 942 // we skip action
913 943 $('#main_filter').focus();
914 944
915 945 } else {
916 946 window.location = suggestion['url'];
917 947 }
918 948 };
919 949
920 950 var autocompleteMainFilterResult = function (suggestion, originalQuery, queryLowerCase) {
921 951 if (queryLowerCase.split(':').length === 2) {
922 952 queryLowerCase = queryLowerCase.split(':')[1]
923 953 }
924 954 if (suggestion.type === "text") {
925 955 // special case we don't want to "skip" display for
926 956 return true
927 957 }
928 958 return suggestion.value_display.toLowerCase().indexOf(queryLowerCase) !== -1;
929 959 };
930 960
931 961 var cleanContext = {
932 962 repo_view_type: null,
933 963
934 964 repo_id: null,
935 965 repo_name: "",
936 966
937 967 repo_group_id: null,
938 968 repo_group_name: null
939 969 };
940 970 var removeGoToFilter = function () {
941 971 $('.searchTagHidable').hide();
942 972 $('#main_filter').autocomplete(
943 973 'setOptions', {params:{search_context: cleanContext}});
944 974 };
945 975
946 976 $('#main_filter').autocomplete({
947 977 serviceUrl: pyroutes.url('goto_switcher_data'),
948 978 params: {
949 979 "search_context": templateContext.search_context
950 980 },
951 981 minChars:2,
952 982 maxHeight:400,
953 983 deferRequestBy: 300, //miliseconds
954 984 tabDisabled: true,
955 985 autoSelectFirst: false,
956 986 containerClass: 'autocomplete-qfilter-suggestions',
957 987 formatResult: autocompleteMainFilterFormatResult,
958 988 lookupFilter: autocompleteMainFilterResult,
959 989 onSelect: function (element, suggestion) {
960 990 handleSelect(element, suggestion);
961 991 return false;
962 992 },
963 993 onSearchError: function (element, query, jqXHR, textStatus, errorThrown) {
964 994 if (jqXHR !== 'abort') {
965 995 alert("Error during search.\nError code: {0}".format(textStatus));
966 996 window.location = '';
967 997 }
968 998 }
969 999 });
970 1000
971 1001 showMainFilterBox = function () {
972 1002 $('#main_filter_help').toggle();
973 1003 };
974 1004
975 1005 $('#main_filter').on('keydown.autocomplete', function (e) {
976 1006
977 1007 var BACKSPACE = 8;
978 1008 var el = $(e.currentTarget);
979 1009 if(e.which === BACKSPACE){
980 1010 var inputVal = el.val();
981 1011 if (inputVal === ""){
982 1012 removeGoToFilter()
983 1013 }
984 1014 }
985 1015 });
986 1016
987 1017 </script>
988 1018 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
989 1019 </%def>
990 1020
991 1021 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
992 1022 <div class="modal-dialog">
993 1023 <div class="modal-content">
994 1024 <div class="modal-header">
995 1025 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
996 1026 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
997 1027 </div>
998 1028 <div class="modal-body">
999 1029 <div class="block-left">
1000 1030 <table class="keyboard-mappings">
1001 1031 <tbody>
1002 1032 <tr>
1003 1033 <th></th>
1004 1034 <th>${_('Site-wide shortcuts')}</th>
1005 1035 </tr>
1006 1036 <%
1007 1037 elems = [
1008 1038 ('/', 'Use quick search box'),
1009 1039 ('g h', 'Goto home page'),
1010 1040 ('g g', 'Goto my private gists page'),
1011 1041 ('g G', 'Goto my public gists page'),
1012 1042 ('g 0-9', 'Goto bookmarked items from 0-9'),
1013 1043 ('n r', 'New repository page'),
1014 1044 ('n g', 'New gist page'),
1015 1045 ]
1016 1046 %>
1017 1047 %for key, desc in elems:
1018 1048 <tr>
1019 1049 <td class="keys">
1020 1050 <span class="key tag">${key}</span>
1021 1051 </td>
1022 1052 <td>${desc}</td>
1023 1053 </tr>
1024 1054 %endfor
1025 1055 </tbody>
1026 1056 </table>
1027 1057 </div>
1028 1058 <div class="block-left">
1029 1059 <table class="keyboard-mappings">
1030 1060 <tbody>
1031 1061 <tr>
1032 1062 <th></th>
1033 1063 <th>${_('Repositories')}</th>
1034 1064 </tr>
1035 1065 <%
1036 1066 elems = [
1037 1067 ('g s', 'Goto summary page'),
1038 1068 ('g c', 'Goto changelog page'),
1039 1069 ('g f', 'Goto files page'),
1040 1070 ('g F', 'Goto files page with file search activated'),
1041 1071 ('g p', 'Goto pull requests page'),
1042 1072 ('g o', 'Goto repository settings'),
1043 1073 ('g O', 'Goto repository access permissions settings'),
1044 1074 ]
1045 1075 %>
1046 1076 %for key, desc in elems:
1047 1077 <tr>
1048 1078 <td class="keys">
1049 1079 <span class="key tag">${key}</span>
1050 1080 </td>
1051 1081 <td>${desc}</td>
1052 1082 </tr>
1053 1083 %endfor
1054 1084 </tbody>
1055 1085 </table>
1056 1086 </div>
1057 1087 </div>
1058 1088 <div class="modal-footer">
1059 1089 </div>
1060 1090 </div><!-- /.modal-content -->
1061 1091 </div><!-- /.modal-dialog -->
1062 1092 </div><!-- /.modal -->
1063 1093
General Comments 0
You need to be logged in to leave comments. Login now