##// END OF EJS Templates
auth-tokens: remove showing builtin tokens
marcink -
r1479:7da2c024 default
parent child Browse files
Show More
@@ -1,468 +1,461 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2013-2017 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 """
23 23 my account controller for RhodeCode admin
24 24 """
25 25
26 26 import logging
27 27 import datetime
28 28
29 29 import formencode
30 30 from formencode import htmlfill
31 31 from pyramid.threadlocal import get_current_registry
32 32 from pylons import request, tmpl_context as c, url, session
33 33 from pylons.controllers.util import redirect
34 34 from pylons.i18n.translation import _
35 35 from sqlalchemy.orm import joinedload
36 36 from webob.exc import HTTPBadGateway
37 37
38 38 from rhodecode import forms
39 39 from rhodecode.lib import helpers as h
40 40 from rhodecode.lib import auth
41 41 from rhodecode.lib.auth import (
42 42 LoginRequired, NotAnonymous, AuthUser, generate_auth_token)
43 43 from rhodecode.lib.base import BaseController, render
44 44 from rhodecode.lib.utils import jsonify
45 45 from rhodecode.lib.utils2 import safe_int, md5, str2bool
46 46 from rhodecode.lib.ext_json import json
47 47 from rhodecode.lib.channelstream import channelstream_request, \
48 48 ChannelstreamException
49 49
50 50 from rhodecode.model.validation_schema.schemas import user_schema
51 51 from rhodecode.model.db import (
52 52 Repository, PullRequest, UserEmailMap, User, UserFollowing)
53 53 from rhodecode.model.forms import UserForm
54 54 from rhodecode.model.scm import RepoList
55 55 from rhodecode.model.user import UserModel
56 56 from rhodecode.model.repo import RepoModel
57 57 from rhodecode.model.auth_token import AuthTokenModel
58 58 from rhodecode.model.meta import Session
59 59 from rhodecode.model.pull_request import PullRequestModel
60 60 from rhodecode.model.comment import CommentsModel
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64
65 65 class MyAccountController(BaseController):
66 66 """REST Controller styled on the Atom Publishing Protocol"""
67 67 # To properly map this controller, ensure your config/routing.py
68 68 # file has a resource setup:
69 69 # map.resource('setting', 'settings', controller='admin/settings',
70 70 # path_prefix='/admin', name_prefix='admin_')
71 71
72 72 @LoginRequired()
73 73 @NotAnonymous()
74 74 def __before__(self):
75 75 super(MyAccountController, self).__before__()
76 76
77 77 def __load_data(self):
78 78 c.user = User.get(c.rhodecode_user.user_id)
79 79 if c.user.username == User.DEFAULT_USER:
80 80 h.flash(_("You can't edit this user since it's"
81 81 " crucial for entire application"), category='warning')
82 82 return redirect(url('users'))
83 83
84 84 c.auth_user = AuthUser(
85 85 user_id=c.rhodecode_user.user_id, ip_addr=self.ip_addr)
86 86
87 87 def _load_my_repos_data(self, watched=False):
88 88 if watched:
89 89 admin = False
90 90 follows_repos = Session().query(UserFollowing)\
91 91 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
92 92 .options(joinedload(UserFollowing.follows_repository))\
93 93 .all()
94 94 repo_list = [x.follows_repository for x in follows_repos]
95 95 else:
96 96 admin = True
97 97 repo_list = Repository.get_all_repos(
98 98 user_id=c.rhodecode_user.user_id)
99 99 repo_list = RepoList(repo_list, perm_set=[
100 100 'repository.read', 'repository.write', 'repository.admin'])
101 101
102 102 repos_data = RepoModel().get_repos_as_dict(
103 103 repo_list=repo_list, admin=admin)
104 104 # json used to render the grid
105 105 return json.dumps(repos_data)
106 106
107 107 @auth.CSRFRequired()
108 108 def my_account_update(self):
109 109 """
110 110 POST /_admin/my_account Updates info of my account
111 111 """
112 112 # url('my_account')
113 113 c.active = 'profile_edit'
114 114 self.__load_data()
115 115 c.perm_user = c.auth_user
116 116 c.extern_type = c.user.extern_type
117 117 c.extern_name = c.user.extern_name
118 118
119 119 defaults = c.user.get_dict()
120 120 update = False
121 121 _form = UserForm(edit=True,
122 122 old_data={'user_id': c.rhodecode_user.user_id,
123 123 'email': c.rhodecode_user.email})()
124 124 form_result = {}
125 125 try:
126 126 post_data = dict(request.POST)
127 127 post_data['new_password'] = ''
128 128 post_data['password_confirmation'] = ''
129 129 form_result = _form.to_python(post_data)
130 130 # skip updating those attrs for my account
131 131 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
132 132 'new_password', 'password_confirmation']
133 133 # TODO: plugin should define if username can be updated
134 134 if c.extern_type != "rhodecode":
135 135 # forbid updating username for external accounts
136 136 skip_attrs.append('username')
137 137
138 138 UserModel().update_user(
139 139 c.rhodecode_user.user_id, skip_attrs=skip_attrs, **form_result)
140 140 h.flash(_('Your account was updated successfully'),
141 141 category='success')
142 142 Session().commit()
143 143 update = True
144 144
145 145 except formencode.Invalid as errors:
146 146 return htmlfill.render(
147 147 render('admin/my_account/my_account.mako'),
148 148 defaults=errors.value,
149 149 errors=errors.error_dict or {},
150 150 prefix_error=False,
151 151 encoding="UTF-8",
152 152 force_defaults=False)
153 153 except Exception:
154 154 log.exception("Exception updating user")
155 155 h.flash(_('Error occurred during update of user %s')
156 156 % form_result.get('username'), category='error')
157 157
158 158 if update:
159 159 return redirect('my_account')
160 160
161 161 return htmlfill.render(
162 162 render('admin/my_account/my_account.mako'),
163 163 defaults=defaults,
164 164 encoding="UTF-8",
165 165 force_defaults=False
166 166 )
167 167
168 168 def my_account(self):
169 169 """
170 170 GET /_admin/my_account Displays info about my account
171 171 """
172 172 # url('my_account')
173 173 c.active = 'profile'
174 174 self.__load_data()
175 175
176 176 defaults = c.user.get_dict()
177 177 return htmlfill.render(
178 178 render('admin/my_account/my_account.mako'),
179 179 defaults=defaults, encoding="UTF-8", force_defaults=False)
180 180
181 181 def my_account_edit(self):
182 182 """
183 183 GET /_admin/my_account/edit Displays edit form of my account
184 184 """
185 185 c.active = 'profile_edit'
186 186 self.__load_data()
187 187 c.perm_user = c.auth_user
188 188 c.extern_type = c.user.extern_type
189 189 c.extern_name = c.user.extern_name
190 190
191 191 defaults = c.user.get_dict()
192 192 return htmlfill.render(
193 193 render('admin/my_account/my_account.mako'),
194 194 defaults=defaults,
195 195 encoding="UTF-8",
196 196 force_defaults=False
197 197 )
198 198
199 199 @auth.CSRFRequired(except_methods=['GET'])
200 200 def my_account_password(self):
201 201 c.active = 'password'
202 202 self.__load_data()
203 203 c.extern_type = c.user.extern_type
204 204
205 205 schema = user_schema.ChangePasswordSchema().bind(
206 206 username=c.rhodecode_user.username)
207 207
208 208 form = forms.Form(schema,
209 209 buttons=(forms.buttons.save, forms.buttons.reset))
210 210
211 211 if request.method == 'POST' and c.extern_type == 'rhodecode':
212 212 controls = request.POST.items()
213 213 try:
214 214 valid_data = form.validate(controls)
215 215 UserModel().update_user(c.rhodecode_user.user_id, **valid_data)
216 216 instance = c.rhodecode_user.get_instance()
217 217 instance.update_userdata(force_password_change=False)
218 218 Session().commit()
219 219 except forms.ValidationFailure as e:
220 220 request.session.flash(
221 221 _('Error occurred during update of user password'),
222 222 queue='error')
223 223 form = e
224 224 except Exception:
225 225 log.exception("Exception updating password")
226 226 request.session.flash(
227 227 _('Error occurred during update of user password'),
228 228 queue='error')
229 229 else:
230 230 session.setdefault('rhodecode_user', {}).update(
231 231 {'password': md5(instance.password)})
232 232 session.save()
233 233 request.session.flash(
234 234 _("Successfully updated password"), queue='success')
235 235 return redirect(url('my_account_password'))
236 236
237 237 c.form = form
238 238 return render('admin/my_account/my_account.mako')
239 239
240 240 def my_account_repos(self):
241 241 c.active = 'repos'
242 242 self.__load_data()
243 243
244 244 # json used to render the grid
245 245 c.data = self._load_my_repos_data()
246 246 return render('admin/my_account/my_account.mako')
247 247
248 248 def my_account_watched(self):
249 249 c.active = 'watched'
250 250 self.__load_data()
251 251
252 252 # json used to render the grid
253 253 c.data = self._load_my_repos_data(watched=True)
254 254 return render('admin/my_account/my_account.mako')
255 255
256 256 def my_account_perms(self):
257 257 c.active = 'perms'
258 258 self.__load_data()
259 259 c.perm_user = c.auth_user
260 260
261 261 return render('admin/my_account/my_account.mako')
262 262
263 263 def my_account_emails(self):
264 264 c.active = 'emails'
265 265 self.__load_data()
266 266
267 267 c.user_email_map = UserEmailMap.query()\
268 268 .filter(UserEmailMap.user == c.user).all()
269 269 return render('admin/my_account/my_account.mako')
270 270
271 271 @auth.CSRFRequired()
272 272 def my_account_emails_add(self):
273 273 email = request.POST.get('new_email')
274 274
275 275 try:
276 276 UserModel().add_extra_email(c.rhodecode_user.user_id, email)
277 277 Session().commit()
278 278 h.flash(_("Added new email address `%s` for user account") % email,
279 279 category='success')
280 280 except formencode.Invalid as error:
281 281 msg = error.error_dict['email']
282 282 h.flash(msg, category='error')
283 283 except Exception:
284 284 log.exception("Exception in my_account_emails")
285 285 h.flash(_('An error occurred during email saving'),
286 286 category='error')
287 287 return redirect(url('my_account_emails'))
288 288
289 289 @auth.CSRFRequired()
290 290 def my_account_emails_delete(self):
291 291 email_id = request.POST.get('del_email_id')
292 292 user_model = UserModel()
293 293 user_model.delete_extra_email(c.rhodecode_user.user_id, email_id)
294 294 Session().commit()
295 295 h.flash(_("Removed email address from user account"),
296 296 category='success')
297 297 return redirect(url('my_account_emails'))
298 298
299 299 def _extract_ordering(self, request):
300 300 column_index = safe_int(request.GET.get('order[0][column]'))
301 301 order_dir = request.GET.get('order[0][dir]', 'desc')
302 302 order_by = request.GET.get(
303 303 'columns[%s][data][sort]' % column_index, 'name_raw')
304 304 return order_by, order_dir
305 305
306 306 def _get_pull_requests_list(self, statuses):
307 307 start = safe_int(request.GET.get('start'), 0)
308 308 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
309 309 order_by, order_dir = self._extract_ordering(request)
310 310
311 311 pull_requests = PullRequestModel().get_im_participating_in(
312 312 user_id=c.rhodecode_user.user_id,
313 313 statuses=statuses,
314 314 offset=start, length=length, order_by=order_by,
315 315 order_dir=order_dir)
316 316
317 317 pull_requests_total_count = PullRequestModel().count_im_participating_in(
318 318 user_id=c.rhodecode_user.user_id, statuses=statuses)
319 319
320 320 from rhodecode.lib.utils import PartialRenderer
321 321 _render = PartialRenderer('data_table/_dt_elements.mako')
322 322 data = []
323 323 for pr in pull_requests:
324 324 repo_id = pr.target_repo_id
325 325 comments = CommentsModel().get_all_comments(
326 326 repo_id, pull_request=pr)
327 327 owned = pr.user_id == c.rhodecode_user.user_id
328 328 status = pr.calculated_review_status()
329 329
330 330 data.append({
331 331 'target_repo': _render('pullrequest_target_repo',
332 332 pr.target_repo.repo_name),
333 333 'name': _render('pullrequest_name',
334 334 pr.pull_request_id, pr.target_repo.repo_name,
335 335 short=True),
336 336 'name_raw': pr.pull_request_id,
337 337 'status': _render('pullrequest_status', status),
338 338 'title': _render(
339 339 'pullrequest_title', pr.title, pr.description),
340 340 'description': h.escape(pr.description),
341 341 'updated_on': _render('pullrequest_updated_on',
342 342 h.datetime_to_time(pr.updated_on)),
343 343 'updated_on_raw': h.datetime_to_time(pr.updated_on),
344 344 'created_on': _render('pullrequest_updated_on',
345 345 h.datetime_to_time(pr.created_on)),
346 346 'created_on_raw': h.datetime_to_time(pr.created_on),
347 347 'author': _render('pullrequest_author',
348 348 pr.author.full_contact, ),
349 349 'author_raw': pr.author.full_name,
350 350 'comments': _render('pullrequest_comments', len(comments)),
351 351 'comments_raw': len(comments),
352 352 'closed': pr.is_closed(),
353 353 'owned': owned
354 354 })
355 355 # json used to render the grid
356 356 data = ({
357 357 'data': data,
358 358 'recordsTotal': pull_requests_total_count,
359 359 'recordsFiltered': pull_requests_total_count,
360 360 })
361 361 return data
362 362
363 363 def my_account_pullrequests(self):
364 364 c.active = 'pullrequests'
365 365 self.__load_data()
366 366 c.show_closed = str2bool(request.GET.get('pr_show_closed'))
367 367
368 368 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
369 369 if c.show_closed:
370 370 statuses += [PullRequest.STATUS_CLOSED]
371 371 data = self._get_pull_requests_list(statuses)
372 372 if not request.is_xhr:
373 373 c.data_participate = json.dumps(data['data'])
374 374 c.records_total_participate = data['recordsTotal']
375 375 return render('admin/my_account/my_account.mako')
376 376 else:
377 377 return json.dumps(data)
378 378
379 379 def my_account_auth_tokens(self):
380 380 c.active = 'auth_tokens'
381 381 self.__load_data()
382 382 show_expired = True
383 383 c.lifetime_values = [
384 384 (str(-1), _('forever')),
385 385 (str(5), _('5 minutes')),
386 386 (str(60), _('1 hour')),
387 387 (str(60 * 24), _('1 day')),
388 388 (str(60 * 24 * 30), _('1 month')),
389 389 ]
390 390 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
391 391 c.role_values = [(x, AuthTokenModel.cls._get_role_name(x))
392 392 for x in AuthTokenModel.cls.ROLES]
393 393 c.role_options = [(c.role_values, _("Role"))]
394 394 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
395 395 c.rhodecode_user.user_id, show_expired=show_expired)
396 396 return render('admin/my_account/my_account.mako')
397 397
398 398 @auth.CSRFRequired()
399 399 def my_account_auth_tokens_add(self):
400 400 lifetime = safe_int(request.POST.get('lifetime'), -1)
401 401 description = request.POST.get('description')
402 402 role = request.POST.get('role')
403 403 AuthTokenModel().create(c.rhodecode_user.user_id, description, lifetime,
404 404 role)
405 405 Session().commit()
406 406 h.flash(_("Auth token successfully created"), category='success')
407 407 return redirect(url('my_account_auth_tokens'))
408 408
409 409 @auth.CSRFRequired()
410 410 def my_account_auth_tokens_delete(self):
411 auth_token = request.POST.get('del_auth_token')
412 user_id = c.rhodecode_user.user_id
413 if request.POST.get('del_auth_token_builtin'):
414 user = User.get(user_id)
415 if user:
416 user.api_key = generate_auth_token(user.username)
417 Session().add(user)
418 Session().commit()
419 h.flash(_("Auth token successfully reset"), category='success')
420 elif auth_token:
421 AuthTokenModel().delete(auth_token, c.rhodecode_user.user_id)
411 del_auth_token = request.POST.get('del_auth_token')
412
413 if del_auth_token:
414 AuthTokenModel().delete(del_auth_token, c.rhodecode_user.user_id)
422 415 Session().commit()
423 416 h.flash(_("Auth token successfully deleted"), category='success')
424 417
425 418 return redirect(url('my_account_auth_tokens'))
426 419
427 420 def my_notifications(self):
428 421 c.active = 'notifications'
429 422 return render('admin/my_account/my_account.mako')
430 423
431 424 @auth.CSRFRequired()
432 425 @jsonify
433 426 def my_notifications_toggle_visibility(self):
434 427 user = c.rhodecode_user.get_instance()
435 428 new_status = not user.user_data.get('notification_status', True)
436 429 user.update_userdata(notification_status=new_status)
437 430 Session().commit()
438 431 return user.user_data['notification_status']
439 432
440 433 @auth.CSRFRequired()
441 434 @jsonify
442 435 def my_account_notifications_test_channelstream(self):
443 436 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
444 437 c.rhodecode_user.username, datetime.datetime.now())
445 438 payload = {
446 439 'type': 'message',
447 440 'timestamp': datetime.datetime.utcnow(),
448 441 'user': 'system',
449 442 #'channel': 'broadcast',
450 443 'pm_users': [c.rhodecode_user.username],
451 444 'message': {
452 445 'message': message,
453 446 'level': 'info',
454 447 'topic': '/notifications'
455 448 }
456 449 }
457 450
458 451 registry = get_current_registry()
459 452 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
460 453 channelstream_config = rhodecode_plugins.get('channelstream', {})
461 454
462 455 try:
463 456 channelstream_request(channelstream_config, [payload], '/message')
464 457 except ChannelstreamException as e:
465 458 log.exception('Failed to send channelstream data')
466 459 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
467 460 return {"response": 'Channelstream data sent. '
468 461 'You should see a new live message now.'}
@@ -1,748 +1,741 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 crud controller for pylons
23 23 """
24 24
25 25 import logging
26 26 import formencode
27 27
28 28 from formencode import htmlfill
29 29 from pylons import request, tmpl_context as c, url, config
30 30 from pylons.controllers.util import redirect
31 31 from pylons.i18n.translation import _
32 32
33 33 from rhodecode.authentication.plugins import auth_rhodecode
34 34 from rhodecode.lib.exceptions import (
35 35 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
36 36 UserOwnsUserGroupsException, UserCreationError)
37 37 from rhodecode.lib import helpers as h
38 38 from rhodecode.lib import auth
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasPermissionAllDecorator, AuthUser, generate_auth_token)
41 41 from rhodecode.lib.base import BaseController, render
42 42 from rhodecode.model.auth_token import AuthTokenModel
43 43
44 44 from rhodecode.model.db import (
45 45 PullRequestReviewers, User, UserEmailMap, UserIpMap, RepoGroup)
46 46 from rhodecode.model.forms import (
47 47 UserForm, UserPermissionsForm, UserIndividualPermissionsForm)
48 48 from rhodecode.model.repo_group import RepoGroupModel
49 49 from rhodecode.model.user import UserModel
50 50 from rhodecode.model.meta import Session
51 51 from rhodecode.model.permission import PermissionModel
52 52 from rhodecode.lib.utils import action_logger
53 53 from rhodecode.lib.ext_json import json
54 54 from rhodecode.lib.utils2 import datetime_to_time, safe_int, AttributeDict
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 class UsersController(BaseController):
60 60 """REST Controller styled on the Atom Publishing Protocol"""
61 61
62 62 @LoginRequired()
63 63 def __before__(self):
64 64 super(UsersController, self).__before__()
65 65 c.available_permissions = config['available_permissions']
66 66 c.allowed_languages = [
67 67 ('en', 'English (en)'),
68 68 ('de', 'German (de)'),
69 69 ('fr', 'French (fr)'),
70 70 ('it', 'Italian (it)'),
71 71 ('ja', 'Japanese (ja)'),
72 72 ('pl', 'Polish (pl)'),
73 73 ('pt', 'Portuguese (pt)'),
74 74 ('ru', 'Russian (ru)'),
75 75 ('zh', 'Chinese (zh)'),
76 76 ]
77 77 PermissionModel().set_global_permission_choices(c, gettext_translator=_)
78 78
79 79 @HasPermissionAllDecorator('hg.admin')
80 80 def index(self):
81 81 """GET /users: All items in the collection"""
82 82 # url('users')
83 83
84 84 from rhodecode.lib.utils import PartialRenderer
85 85 _render = PartialRenderer('data_table/_dt_elements.mako')
86 86
87 87 def username(user_id, username):
88 88 return _render("user_name", user_id, username)
89 89
90 90 def user_actions(user_id, username):
91 91 return _render("user_actions", user_id, username)
92 92
93 93 # json generate
94 94 c.users_list = User.query()\
95 95 .filter(User.username != User.DEFAULT_USER) \
96 96 .all()
97 97
98 98 users_data = []
99 99 for user in c.users_list:
100 100 users_data.append({
101 101 "username": h.gravatar_with_user(user.username),
102 102 "username_raw": user.username,
103 103 "email": user.email,
104 104 "first_name": h.escape(user.name),
105 105 "last_name": h.escape(user.lastname),
106 106 "last_login": h.format_date(user.last_login),
107 107 "last_login_raw": datetime_to_time(user.last_login),
108 108 "last_activity": h.format_date(
109 109 h.time_to_datetime(user.user_data.get('last_activity', 0))),
110 110 "last_activity_raw": user.user_data.get('last_activity', 0),
111 111 "active": h.bool2icon(user.active),
112 112 "active_raw": user.active,
113 113 "admin": h.bool2icon(user.admin),
114 114 "admin_raw": user.admin,
115 115 "extern_type": user.extern_type,
116 116 "extern_name": user.extern_name,
117 117 "action": user_actions(user.user_id, user.username),
118 118 })
119 119
120 120
121 121 c.data = json.dumps(users_data)
122 122 return render('admin/users/users.mako')
123 123
124 124 def _get_personal_repo_group_template_vars(self):
125 125 DummyUser = AttributeDict({
126 126 'username': '${username}',
127 127 'user_id': '${user_id}',
128 128 })
129 129 c.default_create_repo_group = RepoGroupModel() \
130 130 .get_default_create_personal_repo_group()
131 131 c.personal_repo_group_name = RepoGroupModel() \
132 132 .get_personal_group_name(DummyUser)
133 133
134 134 @HasPermissionAllDecorator('hg.admin')
135 135 @auth.CSRFRequired()
136 136 def create(self):
137 137 """POST /users: Create a new item"""
138 138 # url('users')
139 139 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
140 140 user_model = UserModel()
141 141 user_form = UserForm()()
142 142 try:
143 143 form_result = user_form.to_python(dict(request.POST))
144 144 user = user_model.create(form_result)
145 145 Session().flush()
146 146 username = form_result['username']
147 147 action_logger(c.rhodecode_user, 'admin_created_user:%s' % username,
148 148 None, self.ip_addr, self.sa)
149 149
150 150 user_link = h.link_to(h.escape(username),
151 151 url('edit_user',
152 152 user_id=user.user_id))
153 153 h.flash(h.literal(_('Created user %(user_link)s')
154 154 % {'user_link': user_link}), category='success')
155 155 Session().commit()
156 156 except formencode.Invalid as errors:
157 157 self._get_personal_repo_group_template_vars()
158 158 return htmlfill.render(
159 159 render('admin/users/user_add.mako'),
160 160 defaults=errors.value,
161 161 errors=errors.error_dict or {},
162 162 prefix_error=False,
163 163 encoding="UTF-8",
164 164 force_defaults=False)
165 165 except UserCreationError as e:
166 166 h.flash(e, 'error')
167 167 except Exception:
168 168 log.exception("Exception creation of user")
169 169 h.flash(_('Error occurred during creation of user %s')
170 170 % request.POST.get('username'), category='error')
171 171 return redirect(url('users'))
172 172
173 173 @HasPermissionAllDecorator('hg.admin')
174 174 def new(self):
175 175 """GET /users/new: Form to create a new item"""
176 176 # url('new_user')
177 177 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
178 178 self._get_personal_repo_group_template_vars()
179 179 return render('admin/users/user_add.mako')
180 180
181 181 @HasPermissionAllDecorator('hg.admin')
182 182 @auth.CSRFRequired()
183 183 def update(self, user_id):
184 184 """PUT /users/user_id: Update an existing item"""
185 185 # Forms posted to this method should contain a hidden field:
186 186 # <input type="hidden" name="_method" value="PUT" />
187 187 # Or using helpers:
188 188 # h.form(url('update_user', user_id=ID),
189 189 # method='put')
190 190 # url('user', user_id=ID)
191 191 user_id = safe_int(user_id)
192 192 c.user = User.get_or_404(user_id)
193 193 c.active = 'profile'
194 194 c.extern_type = c.user.extern_type
195 195 c.extern_name = c.user.extern_name
196 196 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
197 197 available_languages = [x[0] for x in c.allowed_languages]
198 198 _form = UserForm(edit=True, available_languages=available_languages,
199 199 old_data={'user_id': user_id,
200 200 'email': c.user.email})()
201 201 form_result = {}
202 202 try:
203 203 form_result = _form.to_python(dict(request.POST))
204 204 skip_attrs = ['extern_type', 'extern_name']
205 205 # TODO: plugin should define if username can be updated
206 206 if c.extern_type != "rhodecode":
207 207 # forbid updating username for external accounts
208 208 skip_attrs.append('username')
209 209
210 210 UserModel().update_user(user_id, skip_attrs=skip_attrs, **form_result)
211 211 usr = form_result['username']
212 212 action_logger(c.rhodecode_user, 'admin_updated_user:%s' % usr,
213 213 None, self.ip_addr, self.sa)
214 214 h.flash(_('User updated successfully'), category='success')
215 215 Session().commit()
216 216 except formencode.Invalid as errors:
217 217 defaults = errors.value
218 218 e = errors.error_dict or {}
219 219
220 220 return htmlfill.render(
221 221 render('admin/users/user_edit.mako'),
222 222 defaults=defaults,
223 223 errors=e,
224 224 prefix_error=False,
225 225 encoding="UTF-8",
226 226 force_defaults=False)
227 227 except UserCreationError as e:
228 228 h.flash(e, 'error')
229 229 except Exception:
230 230 log.exception("Exception updating user")
231 231 h.flash(_('Error occurred during update of user %s')
232 232 % form_result.get('username'), category='error')
233 233 return redirect(url('edit_user', user_id=user_id))
234 234
235 235 @HasPermissionAllDecorator('hg.admin')
236 236 @auth.CSRFRequired()
237 237 def delete(self, user_id):
238 238 """DELETE /users/user_id: Delete an existing item"""
239 239 # Forms posted to this method should contain a hidden field:
240 240 # <input type="hidden" name="_method" value="DELETE" />
241 241 # Or using helpers:
242 242 # h.form(url('delete_user', user_id=ID),
243 243 # method='delete')
244 244 # url('user', user_id=ID)
245 245 user_id = safe_int(user_id)
246 246 c.user = User.get_or_404(user_id)
247 247
248 248 _repos = c.user.repositories
249 249 _repo_groups = c.user.repository_groups
250 250 _user_groups = c.user.user_groups
251 251
252 252 handle_repos = None
253 253 handle_repo_groups = None
254 254 handle_user_groups = None
255 255 # dummy call for flash of handle
256 256 set_handle_flash_repos = lambda: None
257 257 set_handle_flash_repo_groups = lambda: None
258 258 set_handle_flash_user_groups = lambda: None
259 259
260 260 if _repos and request.POST.get('user_repos'):
261 261 do = request.POST['user_repos']
262 262 if do == 'detach':
263 263 handle_repos = 'detach'
264 264 set_handle_flash_repos = lambda: h.flash(
265 265 _('Detached %s repositories') % len(_repos),
266 266 category='success')
267 267 elif do == 'delete':
268 268 handle_repos = 'delete'
269 269 set_handle_flash_repos = lambda: h.flash(
270 270 _('Deleted %s repositories') % len(_repos),
271 271 category='success')
272 272
273 273 if _repo_groups and request.POST.get('user_repo_groups'):
274 274 do = request.POST['user_repo_groups']
275 275 if do == 'detach':
276 276 handle_repo_groups = 'detach'
277 277 set_handle_flash_repo_groups = lambda: h.flash(
278 278 _('Detached %s repository groups') % len(_repo_groups),
279 279 category='success')
280 280 elif do == 'delete':
281 281 handle_repo_groups = 'delete'
282 282 set_handle_flash_repo_groups = lambda: h.flash(
283 283 _('Deleted %s repository groups') % len(_repo_groups),
284 284 category='success')
285 285
286 286 if _user_groups and request.POST.get('user_user_groups'):
287 287 do = request.POST['user_user_groups']
288 288 if do == 'detach':
289 289 handle_user_groups = 'detach'
290 290 set_handle_flash_user_groups = lambda: h.flash(
291 291 _('Detached %s user groups') % len(_user_groups),
292 292 category='success')
293 293 elif do == 'delete':
294 294 handle_user_groups = 'delete'
295 295 set_handle_flash_user_groups = lambda: h.flash(
296 296 _('Deleted %s user groups') % len(_user_groups),
297 297 category='success')
298 298
299 299 try:
300 300 UserModel().delete(c.user, handle_repos=handle_repos,
301 301 handle_repo_groups=handle_repo_groups,
302 302 handle_user_groups=handle_user_groups)
303 303 Session().commit()
304 304 set_handle_flash_repos()
305 305 set_handle_flash_repo_groups()
306 306 set_handle_flash_user_groups()
307 307 h.flash(_('Successfully deleted user'), category='success')
308 308 except (UserOwnsReposException, UserOwnsRepoGroupsException,
309 309 UserOwnsUserGroupsException, DefaultUserException) as e:
310 310 h.flash(e, category='warning')
311 311 except Exception:
312 312 log.exception("Exception during deletion of user")
313 313 h.flash(_('An error occurred during deletion of user'),
314 314 category='error')
315 315 return redirect(url('users'))
316 316
317 317 @HasPermissionAllDecorator('hg.admin')
318 318 @auth.CSRFRequired()
319 319 def reset_password(self, user_id):
320 320 """
321 321 toggle reset password flag for this user
322 322
323 323 :param user_id:
324 324 """
325 325 user_id = safe_int(user_id)
326 326 c.user = User.get_or_404(user_id)
327 327 try:
328 328 old_value = c.user.user_data.get('force_password_change')
329 329 c.user.update_userdata(force_password_change=not old_value)
330 330 Session().commit()
331 331 if old_value:
332 332 msg = _('Force password change disabled for user')
333 333 else:
334 334 msg = _('Force password change enabled for user')
335 335 h.flash(msg, category='success')
336 336 except Exception:
337 337 log.exception("Exception during password reset for user")
338 338 h.flash(_('An error occurred during password reset for user'),
339 339 category='error')
340 340
341 341 return redirect(url('edit_user_advanced', user_id=user_id))
342 342
343 343 @HasPermissionAllDecorator('hg.admin')
344 344 @auth.CSRFRequired()
345 345 def create_personal_repo_group(self, user_id):
346 346 """
347 347 Create personal repository group for this user
348 348
349 349 :param user_id:
350 350 """
351 351 from rhodecode.model.repo_group import RepoGroupModel
352 352
353 353 user_id = safe_int(user_id)
354 354 c.user = User.get_or_404(user_id)
355 355 personal_repo_group = RepoGroup.get_user_personal_repo_group(
356 356 c.user.user_id)
357 357 if personal_repo_group:
358 358 return redirect(url('edit_user_advanced', user_id=user_id))
359 359
360 360 personal_repo_group_name = RepoGroupModel().get_personal_group_name(
361 361 c.user)
362 362 named_personal_group = RepoGroup.get_by_group_name(
363 363 personal_repo_group_name)
364 364 try:
365 365
366 366 if named_personal_group and named_personal_group.user_id == c.user.user_id:
367 367 # migrate the same named group, and mark it as personal
368 368 named_personal_group.personal = True
369 369 Session().add(named_personal_group)
370 370 Session().commit()
371 371 msg = _('Linked repository group `%s` as personal' % (
372 372 personal_repo_group_name,))
373 373 h.flash(msg, category='success')
374 374 elif not named_personal_group:
375 375 RepoGroupModel().create_personal_repo_group(c.user)
376 376
377 377 msg = _('Created repository group `%s`' % (
378 378 personal_repo_group_name,))
379 379 h.flash(msg, category='success')
380 380 else:
381 381 msg = _('Repository group `%s` is already taken' % (
382 382 personal_repo_group_name,))
383 383 h.flash(msg, category='warning')
384 384 except Exception:
385 385 log.exception("Exception during repository group creation")
386 386 msg = _(
387 387 'An error occurred during repository group creation for user')
388 388 h.flash(msg, category='error')
389 389 Session().rollback()
390 390
391 391 return redirect(url('edit_user_advanced', user_id=user_id))
392 392
393 393 @HasPermissionAllDecorator('hg.admin')
394 394 def show(self, user_id):
395 395 """GET /users/user_id: Show a specific item"""
396 396 # url('user', user_id=ID)
397 397 User.get_or_404(-1)
398 398
399 399 @HasPermissionAllDecorator('hg.admin')
400 400 def edit(self, user_id):
401 401 """GET /users/user_id/edit: Form to edit an existing item"""
402 402 # url('edit_user', user_id=ID)
403 403 user_id = safe_int(user_id)
404 404 c.user = User.get_or_404(user_id)
405 405 if c.user.username == User.DEFAULT_USER:
406 406 h.flash(_("You can't edit this user"), category='warning')
407 407 return redirect(url('users'))
408 408
409 409 c.active = 'profile'
410 410 c.extern_type = c.user.extern_type
411 411 c.extern_name = c.user.extern_name
412 412 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
413 413
414 414 defaults = c.user.get_dict()
415 415 defaults.update({'language': c.user.user_data.get('language')})
416 416 return htmlfill.render(
417 417 render('admin/users/user_edit.mako'),
418 418 defaults=defaults,
419 419 encoding="UTF-8",
420 420 force_defaults=False)
421 421
422 422 @HasPermissionAllDecorator('hg.admin')
423 423 def edit_advanced(self, user_id):
424 424 user_id = safe_int(user_id)
425 425 user = c.user = User.get_or_404(user_id)
426 426 if user.username == User.DEFAULT_USER:
427 427 h.flash(_("You can't edit this user"), category='warning')
428 428 return redirect(url('users'))
429 429
430 430 c.active = 'advanced'
431 431 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
432 432 c.personal_repo_group = c.perm_user.personal_repo_group
433 433 c.personal_repo_group_name = RepoGroupModel()\
434 434 .get_personal_group_name(user)
435 435 c.first_admin = User.get_first_super_admin()
436 436 defaults = user.get_dict()
437 437
438 438 # Interim workaround if the user participated on any pull requests as a
439 439 # reviewer.
440 440 has_review = bool(PullRequestReviewers.query().filter(
441 441 PullRequestReviewers.user_id == user_id).first())
442 442 c.can_delete_user = not has_review
443 443 c.can_delete_user_message = _(
444 444 'The user participates as reviewer in pull requests and '
445 445 'cannot be deleted. You can set the user to '
446 446 '"inactive" instead of deleting it.') if has_review else ''
447 447
448 448 return htmlfill.render(
449 449 render('admin/users/user_edit.mako'),
450 450 defaults=defaults,
451 451 encoding="UTF-8",
452 452 force_defaults=False)
453 453
454 454 @HasPermissionAllDecorator('hg.admin')
455 455 def edit_auth_tokens(self, user_id):
456 456 user_id = safe_int(user_id)
457 457 c.user = User.get_or_404(user_id)
458 458 if c.user.username == User.DEFAULT_USER:
459 459 h.flash(_("You can't edit this user"), category='warning')
460 460 return redirect(url('users'))
461 461
462 462 c.active = 'auth_tokens'
463 463 show_expired = True
464 464 c.lifetime_values = [
465 465 (str(-1), _('forever')),
466 466 (str(5), _('5 minutes')),
467 467 (str(60), _('1 hour')),
468 468 (str(60 * 24), _('1 day')),
469 469 (str(60 * 24 * 30), _('1 month')),
470 470 ]
471 471 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
472 472 c.role_values = [(x, AuthTokenModel.cls._get_role_name(x))
473 473 for x in AuthTokenModel.cls.ROLES]
474 474 c.role_options = [(c.role_values, _("Role"))]
475 475 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
476 476 c.user.user_id, show_expired=show_expired)
477 477 defaults = c.user.get_dict()
478 478 return htmlfill.render(
479 479 render('admin/users/user_edit.mako'),
480 480 defaults=defaults,
481 481 encoding="UTF-8",
482 482 force_defaults=False)
483 483
484 484 @HasPermissionAllDecorator('hg.admin')
485 485 @auth.CSRFRequired()
486 486 def add_auth_token(self, user_id):
487 487 user_id = safe_int(user_id)
488 488 c.user = User.get_or_404(user_id)
489 489 if c.user.username == User.DEFAULT_USER:
490 490 h.flash(_("You can't edit this user"), category='warning')
491 491 return redirect(url('users'))
492 492
493 493 lifetime = safe_int(request.POST.get('lifetime'), -1)
494 494 description = request.POST.get('description')
495 495 role = request.POST.get('role')
496 496 AuthTokenModel().create(c.user.user_id, description, lifetime, role)
497 497 Session().commit()
498 498 h.flash(_("Auth token successfully created"), category='success')
499 499 return redirect(url('edit_user_auth_tokens', user_id=c.user.user_id))
500 500
501 501 @HasPermissionAllDecorator('hg.admin')
502 502 @auth.CSRFRequired()
503 503 def delete_auth_token(self, user_id):
504 504 user_id = safe_int(user_id)
505 505 c.user = User.get_or_404(user_id)
506 506 if c.user.username == User.DEFAULT_USER:
507 507 h.flash(_("You can't edit this user"), category='warning')
508 508 return redirect(url('users'))
509 509
510 auth_token = request.POST.get('del_auth_token')
511 if request.POST.get('del_auth_token_builtin'):
512 user = User.get(c.user.user_id)
513 if user:
514 user.api_key = generate_auth_token(user.username)
515 Session().add(user)
516 Session().commit()
517 h.flash(_("Auth token successfully reset"), category='success')
518 elif auth_token:
519 AuthTokenModel().delete(auth_token, c.user.user_id)
510 del_auth_token = request.POST.get('del_auth_token')
511 if del_auth_token:
512 AuthTokenModel().delete(del_auth_token, c.user.user_id)
520 513 Session().commit()
521 514 h.flash(_("Auth token successfully deleted"), category='success')
522 515
523 516 return redirect(url('edit_user_auth_tokens', user_id=c.user.user_id))
524 517
525 518 @HasPermissionAllDecorator('hg.admin')
526 519 def edit_global_perms(self, user_id):
527 520 user_id = safe_int(user_id)
528 521 c.user = User.get_or_404(user_id)
529 522 if c.user.username == User.DEFAULT_USER:
530 523 h.flash(_("You can't edit this user"), category='warning')
531 524 return redirect(url('users'))
532 525
533 526 c.active = 'global_perms'
534 527
535 528 c.default_user = User.get_default_user()
536 529 defaults = c.user.get_dict()
537 530 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
538 531 defaults.update(c.default_user.get_default_perms())
539 532 defaults.update(c.user.get_default_perms())
540 533
541 534 return htmlfill.render(
542 535 render('admin/users/user_edit.mako'),
543 536 defaults=defaults,
544 537 encoding="UTF-8",
545 538 force_defaults=False)
546 539
547 540 @HasPermissionAllDecorator('hg.admin')
548 541 @auth.CSRFRequired()
549 542 def update_global_perms(self, user_id):
550 543 """PUT /users_perm/user_id: Update an existing item"""
551 544 # url('user_perm', user_id=ID, method='put')
552 545 user_id = safe_int(user_id)
553 546 user = User.get_or_404(user_id)
554 547 c.active = 'global_perms'
555 548 try:
556 549 # first stage that verifies the checkbox
557 550 _form = UserIndividualPermissionsForm()
558 551 form_result = _form.to_python(dict(request.POST))
559 552 inherit_perms = form_result['inherit_default_permissions']
560 553 user.inherit_default_permissions = inherit_perms
561 554 Session().add(user)
562 555
563 556 if not inherit_perms:
564 557 # only update the individual ones if we un check the flag
565 558 _form = UserPermissionsForm(
566 559 [x[0] for x in c.repo_create_choices],
567 560 [x[0] for x in c.repo_create_on_write_choices],
568 561 [x[0] for x in c.repo_group_create_choices],
569 562 [x[0] for x in c.user_group_create_choices],
570 563 [x[0] for x in c.fork_choices],
571 564 [x[0] for x in c.inherit_default_permission_choices])()
572 565
573 566 form_result = _form.to_python(dict(request.POST))
574 567 form_result.update({'perm_user_id': user.user_id})
575 568
576 569 PermissionModel().update_user_permissions(form_result)
577 570
578 571 Session().commit()
579 572 h.flash(_('User global permissions updated successfully'),
580 573 category='success')
581 574
582 575 Session().commit()
583 576 except formencode.Invalid as errors:
584 577 defaults = errors.value
585 578 c.user = user
586 579 return htmlfill.render(
587 580 render('admin/users/user_edit.mako'),
588 581 defaults=defaults,
589 582 errors=errors.error_dict or {},
590 583 prefix_error=False,
591 584 encoding="UTF-8",
592 585 force_defaults=False)
593 586 except Exception:
594 587 log.exception("Exception during permissions saving")
595 588 h.flash(_('An error occurred during permissions saving'),
596 589 category='error')
597 590 return redirect(url('edit_user_global_perms', user_id=user_id))
598 591
599 592 @HasPermissionAllDecorator('hg.admin')
600 593 def edit_perms_summary(self, user_id):
601 594 user_id = safe_int(user_id)
602 595 c.user = User.get_or_404(user_id)
603 596 if c.user.username == User.DEFAULT_USER:
604 597 h.flash(_("You can't edit this user"), category='warning')
605 598 return redirect(url('users'))
606 599
607 600 c.active = 'perms_summary'
608 601 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
609 602
610 603 return render('admin/users/user_edit.mako')
611 604
612 605 @HasPermissionAllDecorator('hg.admin')
613 606 def edit_emails(self, user_id):
614 607 user_id = safe_int(user_id)
615 608 c.user = User.get_or_404(user_id)
616 609 if c.user.username == User.DEFAULT_USER:
617 610 h.flash(_("You can't edit this user"), category='warning')
618 611 return redirect(url('users'))
619 612
620 613 c.active = 'emails'
621 614 c.user_email_map = UserEmailMap.query() \
622 615 .filter(UserEmailMap.user == c.user).all()
623 616
624 617 defaults = c.user.get_dict()
625 618 return htmlfill.render(
626 619 render('admin/users/user_edit.mako'),
627 620 defaults=defaults,
628 621 encoding="UTF-8",
629 622 force_defaults=False)
630 623
631 624 @HasPermissionAllDecorator('hg.admin')
632 625 @auth.CSRFRequired()
633 626 def add_email(self, user_id):
634 627 """POST /user_emails:Add an existing item"""
635 628 # url('user_emails', user_id=ID, method='put')
636 629 user_id = safe_int(user_id)
637 630 c.user = User.get_or_404(user_id)
638 631
639 632 email = request.POST.get('new_email')
640 633 user_model = UserModel()
641 634
642 635 try:
643 636 user_model.add_extra_email(user_id, email)
644 637 Session().commit()
645 638 h.flash(_("Added new email address `%s` for user account") % email,
646 639 category='success')
647 640 except formencode.Invalid as error:
648 641 msg = error.error_dict['email']
649 642 h.flash(msg, category='error')
650 643 except Exception:
651 644 log.exception("Exception during email saving")
652 645 h.flash(_('An error occurred during email saving'),
653 646 category='error')
654 647 return redirect(url('edit_user_emails', user_id=user_id))
655 648
656 649 @HasPermissionAllDecorator('hg.admin')
657 650 @auth.CSRFRequired()
658 651 def delete_email(self, user_id):
659 652 """DELETE /user_emails_delete/user_id: Delete an existing item"""
660 653 # url('user_emails_delete', user_id=ID, method='delete')
661 654 user_id = safe_int(user_id)
662 655 c.user = User.get_or_404(user_id)
663 656 email_id = request.POST.get('del_email_id')
664 657 user_model = UserModel()
665 658 user_model.delete_extra_email(user_id, email_id)
666 659 Session().commit()
667 660 h.flash(_("Removed email address from user account"), category='success')
668 661 return redirect(url('edit_user_emails', user_id=user_id))
669 662
670 663 @HasPermissionAllDecorator('hg.admin')
671 664 def edit_ips(self, user_id):
672 665 user_id = safe_int(user_id)
673 666 c.user = User.get_or_404(user_id)
674 667 if c.user.username == User.DEFAULT_USER:
675 668 h.flash(_("You can't edit this user"), category='warning')
676 669 return redirect(url('users'))
677 670
678 671 c.active = 'ips'
679 672 c.user_ip_map = UserIpMap.query() \
680 673 .filter(UserIpMap.user == c.user).all()
681 674
682 675 c.inherit_default_ips = c.user.inherit_default_permissions
683 676 c.default_user_ip_map = UserIpMap.query() \
684 677 .filter(UserIpMap.user == User.get_default_user()).all()
685 678
686 679 defaults = c.user.get_dict()
687 680 return htmlfill.render(
688 681 render('admin/users/user_edit.mako'),
689 682 defaults=defaults,
690 683 encoding="UTF-8",
691 684 force_defaults=False)
692 685
693 686 @HasPermissionAllDecorator('hg.admin')
694 687 @auth.CSRFRequired()
695 688 def add_ip(self, user_id):
696 689 """POST /user_ips:Add an existing item"""
697 690 # url('user_ips', user_id=ID, method='put')
698 691
699 692 user_id = safe_int(user_id)
700 693 c.user = User.get_or_404(user_id)
701 694 user_model = UserModel()
702 695 try:
703 696 ip_list = user_model.parse_ip_range(request.POST.get('new_ip'))
704 697 except Exception as e:
705 698 ip_list = []
706 699 log.exception("Exception during ip saving")
707 700 h.flash(_('An error occurred during ip saving:%s' % (e,)),
708 701 category='error')
709 702
710 703 desc = request.POST.get('description')
711 704 added = []
712 705 for ip in ip_list:
713 706 try:
714 707 user_model.add_extra_ip(user_id, ip, desc)
715 708 Session().commit()
716 709 added.append(ip)
717 710 except formencode.Invalid as error:
718 711 msg = error.error_dict['ip']
719 712 h.flash(msg, category='error')
720 713 except Exception:
721 714 log.exception("Exception during ip saving")
722 715 h.flash(_('An error occurred during ip saving'),
723 716 category='error')
724 717 if added:
725 718 h.flash(
726 719 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
727 720 category='success')
728 721 if 'default_user' in request.POST:
729 722 return redirect(url('admin_permissions_ips'))
730 723 return redirect(url('edit_user_ips', user_id=user_id))
731 724
732 725 @HasPermissionAllDecorator('hg.admin')
733 726 @auth.CSRFRequired()
734 727 def delete_ip(self, user_id):
735 728 """DELETE /user_ips_delete/user_id: Delete an existing item"""
736 729 # url('user_ips_delete', user_id=ID, method='delete')
737 730 user_id = safe_int(user_id)
738 731 c.user = User.get_or_404(user_id)
739 732
740 733 ip_id = request.POST.get('del_ip_id')
741 734 user_model = UserModel()
742 735 user_model.delete_extra_ip(user_id, ip_id)
743 736 Session().commit()
744 737 h.flash(_("Removed ip address from user whitelist"), category='success')
745 738
746 739 if 'default_user' in request.POST:
747 740 return redirect(url('admin_permissions_ips'))
748 741 return redirect(url('edit_user_ips', user_id=user_id))
@@ -1,3909 +1,3902 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 hashlib
29 29 import logging
30 30 import datetime
31 31 import warnings
32 32 import ipaddress
33 33 import functools
34 34 import traceback
35 35 import collections
36 36
37 37
38 38 from sqlalchemy import *
39 39 from sqlalchemy.ext.declarative import declared_attr
40 40 from sqlalchemy.ext.hybrid import hybrid_property
41 41 from sqlalchemy.orm import (
42 42 relationship, joinedload, class_mapper, validates, aliased)
43 43 from sqlalchemy.sql.expression import true
44 44 from beaker.cache import cache_region
45 45 from webob.exc import HTTPNotFound
46 46 from zope.cachedescriptors.property import Lazy as LazyProperty
47 47
48 48 from pylons import url
49 49 from pylons.i18n.translation import lazy_ugettext as _
50 50
51 51 from rhodecode.lib.vcs import get_vcs_instance
52 52 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
53 53 from rhodecode.lib.utils2 import (
54 54 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
55 55 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
56 56 glob2re, StrictAttributeDict, cleaned_uri)
57 57 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
58 58 from rhodecode.lib.ext_json import json
59 59 from rhodecode.lib.caching_query import FromCache
60 60 from rhodecode.lib.encrypt import AESCipher
61 61
62 62 from rhodecode.model.meta import Base, Session
63 63
64 64 URL_SEP = '/'
65 65 log = logging.getLogger(__name__)
66 66
67 67 # =============================================================================
68 68 # BASE CLASSES
69 69 # =============================================================================
70 70
71 71 # this is propagated from .ini file rhodecode.encrypted_values.secret or
72 72 # beaker.session.secret if first is not set.
73 73 # and initialized at environment.py
74 74 ENCRYPTION_KEY = None
75 75
76 76 # used to sort permissions by types, '#' used here is not allowed to be in
77 77 # usernames, and it's very early in sorted string.printable table.
78 78 PERMISSION_TYPE_SORT = {
79 79 'admin': '####',
80 80 'write': '###',
81 81 'read': '##',
82 82 'none': '#',
83 83 }
84 84
85 85
86 86 def display_sort(obj):
87 87 """
88 88 Sort function used to sort permissions in .permissions() function of
89 89 Repository, RepoGroup, UserGroup. Also it put the default user in front
90 90 of all other resources
91 91 """
92 92
93 93 if obj.username == User.DEFAULT_USER:
94 94 return '#####'
95 95 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
96 96 return prefix + obj.username
97 97
98 98
99 99 def _hash_key(k):
100 100 return md5_safe(k)
101 101
102 102
103 103 class EncryptedTextValue(TypeDecorator):
104 104 """
105 105 Special column for encrypted long text data, use like::
106 106
107 107 value = Column("encrypted_value", EncryptedValue(), nullable=False)
108 108
109 109 This column is intelligent so if value is in unencrypted form it return
110 110 unencrypted form, but on save it always encrypts
111 111 """
112 112 impl = Text
113 113
114 114 def process_bind_param(self, value, dialect):
115 115 if not value:
116 116 return value
117 117 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
118 118 # protect against double encrypting if someone manually starts
119 119 # doing
120 120 raise ValueError('value needs to be in unencrypted format, ie. '
121 121 'not starting with enc$aes')
122 122 return 'enc$aes_hmac$%s' % AESCipher(
123 123 ENCRYPTION_KEY, hmac=True).encrypt(value)
124 124
125 125 def process_result_value(self, value, dialect):
126 126 import rhodecode
127 127
128 128 if not value:
129 129 return value
130 130
131 131 parts = value.split('$', 3)
132 132 if not len(parts) == 3:
133 133 # probably not encrypted values
134 134 return value
135 135 else:
136 136 if parts[0] != 'enc':
137 137 # parts ok but without our header ?
138 138 return value
139 139 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
140 140 'rhodecode.encrypted_values.strict') or True)
141 141 # at that stage we know it's our encryption
142 142 if parts[1] == 'aes':
143 143 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
144 144 elif parts[1] == 'aes_hmac':
145 145 decrypted_data = AESCipher(
146 146 ENCRYPTION_KEY, hmac=True,
147 147 strict_verification=enc_strict_mode).decrypt(parts[2])
148 148 else:
149 149 raise ValueError(
150 150 'Encryption type part is wrong, must be `aes` '
151 151 'or `aes_hmac`, got `%s` instead' % (parts[1]))
152 152 return decrypted_data
153 153
154 154
155 155 class BaseModel(object):
156 156 """
157 157 Base Model for all classes
158 158 """
159 159
160 160 @classmethod
161 161 def _get_keys(cls):
162 162 """return column names for this model """
163 163 return class_mapper(cls).c.keys()
164 164
165 165 def get_dict(self):
166 166 """
167 167 return dict with keys and values corresponding
168 168 to this model data """
169 169
170 170 d = {}
171 171 for k in self._get_keys():
172 172 d[k] = getattr(self, k)
173 173
174 174 # also use __json__() if present to get additional fields
175 175 _json_attr = getattr(self, '__json__', None)
176 176 if _json_attr:
177 177 # update with attributes from __json__
178 178 if callable(_json_attr):
179 179 _json_attr = _json_attr()
180 180 for k, val in _json_attr.iteritems():
181 181 d[k] = val
182 182 return d
183 183
184 184 def get_appstruct(self):
185 185 """return list with keys and values tuples corresponding
186 186 to this model data """
187 187
188 188 l = []
189 189 for k in self._get_keys():
190 190 l.append((k, getattr(self, k),))
191 191 return l
192 192
193 193 def populate_obj(self, populate_dict):
194 194 """populate model with data from given populate_dict"""
195 195
196 196 for k in self._get_keys():
197 197 if k in populate_dict:
198 198 setattr(self, k, populate_dict[k])
199 199
200 200 @classmethod
201 201 def query(cls):
202 202 return Session().query(cls)
203 203
204 204 @classmethod
205 205 def get(cls, id_):
206 206 if id_:
207 207 return cls.query().get(id_)
208 208
209 209 @classmethod
210 210 def get_or_404(cls, id_):
211 211 try:
212 212 id_ = int(id_)
213 213 except (TypeError, ValueError):
214 214 raise HTTPNotFound
215 215
216 216 res = cls.query().get(id_)
217 217 if not res:
218 218 raise HTTPNotFound
219 219 return res
220 220
221 221 @classmethod
222 222 def getAll(cls):
223 223 # deprecated and left for backward compatibility
224 224 return cls.get_all()
225 225
226 226 @classmethod
227 227 def get_all(cls):
228 228 return cls.query().all()
229 229
230 230 @classmethod
231 231 def delete(cls, id_):
232 232 obj = cls.query().get(id_)
233 233 Session().delete(obj)
234 234
235 235 @classmethod
236 236 def identity_cache(cls, session, attr_name, value):
237 237 exist_in_session = []
238 238 for (item_cls, pkey), instance in session.identity_map.items():
239 239 if cls == item_cls and getattr(instance, attr_name) == value:
240 240 exist_in_session.append(instance)
241 241 if exist_in_session:
242 242 if len(exist_in_session) == 1:
243 243 return exist_in_session[0]
244 244 log.exception(
245 245 'multiple objects with attr %s and '
246 246 'value %s found with same name: %r',
247 247 attr_name, value, exist_in_session)
248 248
249 249 def __repr__(self):
250 250 if hasattr(self, '__unicode__'):
251 251 # python repr needs to return str
252 252 try:
253 253 return safe_str(self.__unicode__())
254 254 except UnicodeDecodeError:
255 255 pass
256 256 return '<DB:%s>' % (self.__class__.__name__)
257 257
258 258
259 259 class RhodeCodeSetting(Base, BaseModel):
260 260 __tablename__ = 'rhodecode_settings'
261 261 __table_args__ = (
262 262 UniqueConstraint('app_settings_name'),
263 263 {'extend_existing': True, 'mysql_engine': 'InnoDB',
264 264 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
265 265 )
266 266
267 267 SETTINGS_TYPES = {
268 268 'str': safe_str,
269 269 'int': safe_int,
270 270 'unicode': safe_unicode,
271 271 'bool': str2bool,
272 272 'list': functools.partial(aslist, sep=',')
273 273 }
274 274 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
275 275 GLOBAL_CONF_KEY = 'app_settings'
276 276
277 277 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
278 278 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
279 279 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
280 280 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
281 281
282 282 def __init__(self, key='', val='', type='unicode'):
283 283 self.app_settings_name = key
284 284 self.app_settings_type = type
285 285 self.app_settings_value = val
286 286
287 287 @validates('_app_settings_value')
288 288 def validate_settings_value(self, key, val):
289 289 assert type(val) == unicode
290 290 return val
291 291
292 292 @hybrid_property
293 293 def app_settings_value(self):
294 294 v = self._app_settings_value
295 295 _type = self.app_settings_type
296 296 if _type:
297 297 _type = self.app_settings_type.split('.')[0]
298 298 # decode the encrypted value
299 299 if 'encrypted' in self.app_settings_type:
300 300 cipher = EncryptedTextValue()
301 301 v = safe_unicode(cipher.process_result_value(v, None))
302 302
303 303 converter = self.SETTINGS_TYPES.get(_type) or \
304 304 self.SETTINGS_TYPES['unicode']
305 305 return converter(v)
306 306
307 307 @app_settings_value.setter
308 308 def app_settings_value(self, val):
309 309 """
310 310 Setter that will always make sure we use unicode in app_settings_value
311 311
312 312 :param val:
313 313 """
314 314 val = safe_unicode(val)
315 315 # encode the encrypted value
316 316 if 'encrypted' in self.app_settings_type:
317 317 cipher = EncryptedTextValue()
318 318 val = safe_unicode(cipher.process_bind_param(val, None))
319 319 self._app_settings_value = val
320 320
321 321 @hybrid_property
322 322 def app_settings_type(self):
323 323 return self._app_settings_type
324 324
325 325 @app_settings_type.setter
326 326 def app_settings_type(self, val):
327 327 if val.split('.')[0] not in self.SETTINGS_TYPES:
328 328 raise Exception('type must be one of %s got %s'
329 329 % (self.SETTINGS_TYPES.keys(), val))
330 330 self._app_settings_type = val
331 331
332 332 def __unicode__(self):
333 333 return u"<%s('%s:%s[%s]')>" % (
334 334 self.__class__.__name__,
335 335 self.app_settings_name, self.app_settings_value,
336 336 self.app_settings_type
337 337 )
338 338
339 339
340 340 class RhodeCodeUi(Base, BaseModel):
341 341 __tablename__ = 'rhodecode_ui'
342 342 __table_args__ = (
343 343 UniqueConstraint('ui_key'),
344 344 {'extend_existing': True, 'mysql_engine': 'InnoDB',
345 345 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
346 346 )
347 347
348 348 HOOK_REPO_SIZE = 'changegroup.repo_size'
349 349 # HG
350 350 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
351 351 HOOK_PULL = 'outgoing.pull_logger'
352 352 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
353 353 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
354 354 HOOK_PUSH = 'changegroup.push_logger'
355 355
356 356 # TODO: johbo: Unify way how hooks are configured for git and hg,
357 357 # git part is currently hardcoded.
358 358
359 359 # SVN PATTERNS
360 360 SVN_BRANCH_ID = 'vcs_svn_branch'
361 361 SVN_TAG_ID = 'vcs_svn_tag'
362 362
363 363 ui_id = Column(
364 364 "ui_id", Integer(), nullable=False, unique=True, default=None,
365 365 primary_key=True)
366 366 ui_section = Column(
367 367 "ui_section", String(255), nullable=True, unique=None, default=None)
368 368 ui_key = Column(
369 369 "ui_key", String(255), nullable=True, unique=None, default=None)
370 370 ui_value = Column(
371 371 "ui_value", String(255), nullable=True, unique=None, default=None)
372 372 ui_active = Column(
373 373 "ui_active", Boolean(), nullable=True, unique=None, default=True)
374 374
375 375 def __repr__(self):
376 376 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
377 377 self.ui_key, self.ui_value)
378 378
379 379
380 380 class RepoRhodeCodeSetting(Base, BaseModel):
381 381 __tablename__ = 'repo_rhodecode_settings'
382 382 __table_args__ = (
383 383 UniqueConstraint(
384 384 'app_settings_name', 'repository_id',
385 385 name='uq_repo_rhodecode_setting_name_repo_id'),
386 386 {'extend_existing': True, 'mysql_engine': 'InnoDB',
387 387 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
388 388 )
389 389
390 390 repository_id = Column(
391 391 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
392 392 nullable=False)
393 393 app_settings_id = Column(
394 394 "app_settings_id", Integer(), nullable=False, unique=True,
395 395 default=None, primary_key=True)
396 396 app_settings_name = Column(
397 397 "app_settings_name", String(255), nullable=True, unique=None,
398 398 default=None)
399 399 _app_settings_value = Column(
400 400 "app_settings_value", String(4096), nullable=True, unique=None,
401 401 default=None)
402 402 _app_settings_type = Column(
403 403 "app_settings_type", String(255), nullable=True, unique=None,
404 404 default=None)
405 405
406 406 repository = relationship('Repository')
407 407
408 408 def __init__(self, repository_id, key='', val='', type='unicode'):
409 409 self.repository_id = repository_id
410 410 self.app_settings_name = key
411 411 self.app_settings_type = type
412 412 self.app_settings_value = val
413 413
414 414 @validates('_app_settings_value')
415 415 def validate_settings_value(self, key, val):
416 416 assert type(val) == unicode
417 417 return val
418 418
419 419 @hybrid_property
420 420 def app_settings_value(self):
421 421 v = self._app_settings_value
422 422 type_ = self.app_settings_type
423 423 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
424 424 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
425 425 return converter(v)
426 426
427 427 @app_settings_value.setter
428 428 def app_settings_value(self, val):
429 429 """
430 430 Setter that will always make sure we use unicode in app_settings_value
431 431
432 432 :param val:
433 433 """
434 434 self._app_settings_value = safe_unicode(val)
435 435
436 436 @hybrid_property
437 437 def app_settings_type(self):
438 438 return self._app_settings_type
439 439
440 440 @app_settings_type.setter
441 441 def app_settings_type(self, val):
442 442 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
443 443 if val not in SETTINGS_TYPES:
444 444 raise Exception('type must be one of %s got %s'
445 445 % (SETTINGS_TYPES.keys(), val))
446 446 self._app_settings_type = val
447 447
448 448 def __unicode__(self):
449 449 return u"<%s('%s:%s:%s[%s]')>" % (
450 450 self.__class__.__name__, self.repository.repo_name,
451 451 self.app_settings_name, self.app_settings_value,
452 452 self.app_settings_type
453 453 )
454 454
455 455
456 456 class RepoRhodeCodeUi(Base, BaseModel):
457 457 __tablename__ = 'repo_rhodecode_ui'
458 458 __table_args__ = (
459 459 UniqueConstraint(
460 460 'repository_id', 'ui_section', 'ui_key',
461 461 name='uq_repo_rhodecode_ui_repository_id_section_key'),
462 462 {'extend_existing': True, 'mysql_engine': 'InnoDB',
463 463 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
464 464 )
465 465
466 466 repository_id = Column(
467 467 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
468 468 nullable=False)
469 469 ui_id = Column(
470 470 "ui_id", Integer(), nullable=False, unique=True, default=None,
471 471 primary_key=True)
472 472 ui_section = Column(
473 473 "ui_section", String(255), nullable=True, unique=None, default=None)
474 474 ui_key = Column(
475 475 "ui_key", String(255), nullable=True, unique=None, default=None)
476 476 ui_value = Column(
477 477 "ui_value", String(255), nullable=True, unique=None, default=None)
478 478 ui_active = Column(
479 479 "ui_active", Boolean(), nullable=True, unique=None, default=True)
480 480
481 481 repository = relationship('Repository')
482 482
483 483 def __repr__(self):
484 484 return '<%s[%s:%s]%s=>%s]>' % (
485 485 self.__class__.__name__, self.repository.repo_name,
486 486 self.ui_section, self.ui_key, self.ui_value)
487 487
488 488
489 489 class User(Base, BaseModel):
490 490 __tablename__ = 'users'
491 491 __table_args__ = (
492 492 UniqueConstraint('username'), UniqueConstraint('email'),
493 493 Index('u_username_idx', 'username'),
494 494 Index('u_email_idx', 'email'),
495 495 {'extend_existing': True, 'mysql_engine': 'InnoDB',
496 496 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
497 497 )
498 498 DEFAULT_USER = 'default'
499 499 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
500 500 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
501 501
502 502 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
503 503 username = Column("username", String(255), nullable=True, unique=None, default=None)
504 504 password = Column("password", String(255), nullable=True, unique=None, default=None)
505 505 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
506 506 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
507 507 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
508 508 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
509 509 _email = Column("email", String(255), nullable=True, unique=None, default=None)
510 510 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
511 511 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
512 512 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
513 513 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
514 514 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
515 515 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
516 516 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
517 517
518 518 user_log = relationship('UserLog')
519 519 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
520 520
521 521 repositories = relationship('Repository')
522 522 repository_groups = relationship('RepoGroup')
523 523 user_groups = relationship('UserGroup')
524 524
525 525 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
526 526 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
527 527
528 528 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
529 529 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
530 530 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
531 531
532 532 group_member = relationship('UserGroupMember', cascade='all')
533 533
534 534 notifications = relationship('UserNotification', cascade='all')
535 535 # notifications assigned to this user
536 536 user_created_notifications = relationship('Notification', cascade='all')
537 537 # comments created by this user
538 538 user_comments = relationship('ChangesetComment', cascade='all')
539 539 # user profile extra info
540 540 user_emails = relationship('UserEmailMap', cascade='all')
541 541 user_ip_map = relationship('UserIpMap', cascade='all')
542 542 user_auth_tokens = relationship('UserApiKeys', cascade='all')
543 543 # gists
544 544 user_gists = relationship('Gist', cascade='all')
545 545 # user pull requests
546 546 user_pull_requests = relationship('PullRequest', cascade='all')
547 547 # external identities
548 548 extenal_identities = relationship(
549 549 'ExternalIdentity',
550 550 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
551 551 cascade='all')
552 552
553 553 def __unicode__(self):
554 554 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
555 555 self.user_id, self.username)
556 556
557 557 @hybrid_property
558 558 def email(self):
559 559 return self._email
560 560
561 561 @email.setter
562 562 def email(self, val):
563 563 self._email = val.lower() if val else None
564 564
565 565 @property
566 566 def firstname(self):
567 567 # alias for future
568 568 return self.name
569 569
570 570 @property
571 571 def emails(self):
572 572 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
573 573 return [self.email] + [x.email for x in other]
574 574
575 575 @property
576 576 def auth_tokens(self):
577 577 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
578 578
579 579 @property
580 580 def extra_auth_tokens(self):
581 581 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
582 582
583 583 @property
584 584 def feed_token(self):
585 585 return self.get_feed_token()
586 586
587 587 def get_feed_token(self):
588 588 feed_tokens = UserApiKeys.query()\
589 589 .filter(UserApiKeys.user == self)\
590 590 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
591 591 .all()
592 592 if feed_tokens:
593 593 return feed_tokens[0].api_key
594 594 return 'NO_FEED_TOKEN_AVAILABLE'
595 595
596 596 @classmethod
597 597 def extra_valid_auth_tokens(cls, user, role=None):
598 598 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
599 599 .filter(or_(UserApiKeys.expires == -1,
600 600 UserApiKeys.expires >= time.time()))
601 601 if role:
602 602 tokens = tokens.filter(or_(UserApiKeys.role == role,
603 603 UserApiKeys.role == UserApiKeys.ROLE_ALL))
604 604 return tokens.all()
605 605
606 606 def authenticate_by_token(self, auth_token, roles=None,
607 607 include_builtin_token=False):
608 608 from rhodecode.lib import auth
609 609
610 610 log.debug('Trying to authenticate user: %s via auth-token, '
611 611 'and roles: %s', self, roles)
612 612
613 613 if not auth_token:
614 614 return False
615 615
616 616 crypto_backend = auth.crypto_backend()
617 617
618 618 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
619 619 tokens_q = UserApiKeys.query()\
620 620 .filter(UserApiKeys.user_id == self.user_id)\
621 621 .filter(or_(UserApiKeys.expires == -1,
622 622 UserApiKeys.expires >= time.time()))
623 623
624 624 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
625 625
626 626 maybe_builtin = []
627 627 if include_builtin_token:
628 628 maybe_builtin = [AttributeDict({'api_key': self.api_key})]
629 629
630 630 plain_tokens = []
631 631 hash_tokens = []
632 632
633 633 for token in tokens_q.all() + maybe_builtin:
634 634 if token.api_key.startswith(crypto_backend.ENC_PREF):
635 635 hash_tokens.append(token.api_key)
636 636 else:
637 637 plain_tokens.append(token.api_key)
638 638
639 639 is_plain_match = auth_token in plain_tokens
640 640 if is_plain_match:
641 641 return True
642 642
643 643 for hashed in hash_tokens:
644 644 # marcink: this is expensive to calculate, but the most secure
645 645 match = crypto_backend.hash_check(auth_token, hashed)
646 646 if match:
647 647 return True
648 648
649 649 return False
650 650
651 651 @property
652 def builtin_token_roles(self):
653 roles = [
654 UserApiKeys.ROLE_API, UserApiKeys.ROLE_FEED, UserApiKeys.ROLE_HTTP
655 ]
656 return map(UserApiKeys._get_role_name, roles)
657
658 @property
659 652 def ip_addresses(self):
660 653 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
661 654 return [x.ip_addr for x in ret]
662 655
663 656 @property
664 657 def username_and_name(self):
665 658 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
666 659
667 660 @property
668 661 def username_or_name_or_email(self):
669 662 full_name = self.full_name if self.full_name is not ' ' else None
670 663 return self.username or full_name or self.email
671 664
672 665 @property
673 666 def full_name(self):
674 667 return '%s %s' % (self.firstname, self.lastname)
675 668
676 669 @property
677 670 def full_name_or_username(self):
678 671 return ('%s %s' % (self.firstname, self.lastname)
679 672 if (self.firstname and self.lastname) else self.username)
680 673
681 674 @property
682 675 def full_contact(self):
683 676 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
684 677
685 678 @property
686 679 def short_contact(self):
687 680 return '%s %s' % (self.firstname, self.lastname)
688 681
689 682 @property
690 683 def is_admin(self):
691 684 return self.admin
692 685
693 686 @property
694 687 def AuthUser(self):
695 688 """
696 689 Returns instance of AuthUser for this user
697 690 """
698 691 from rhodecode.lib.auth import AuthUser
699 692 return AuthUser(user_id=self.user_id, api_key=self.api_key,
700 693 username=self.username)
701 694
702 695 @hybrid_property
703 696 def user_data(self):
704 697 if not self._user_data:
705 698 return {}
706 699
707 700 try:
708 701 return json.loads(self._user_data)
709 702 except TypeError:
710 703 return {}
711 704
712 705 @user_data.setter
713 706 def user_data(self, val):
714 707 if not isinstance(val, dict):
715 708 raise Exception('user_data must be dict, got %s' % type(val))
716 709 try:
717 710 self._user_data = json.dumps(val)
718 711 except Exception:
719 712 log.error(traceback.format_exc())
720 713
721 714 @classmethod
722 715 def get_by_username(cls, username, case_insensitive=False,
723 716 cache=False, identity_cache=False):
724 717 session = Session()
725 718
726 719 if case_insensitive:
727 720 q = cls.query().filter(
728 721 func.lower(cls.username) == func.lower(username))
729 722 else:
730 723 q = cls.query().filter(cls.username == username)
731 724
732 725 if cache:
733 726 if identity_cache:
734 727 val = cls.identity_cache(session, 'username', username)
735 728 if val:
736 729 return val
737 730 else:
738 731 q = q.options(
739 732 FromCache("sql_cache_short",
740 733 "get_user_by_name_%s" % _hash_key(username)))
741 734
742 735 return q.scalar()
743 736
744 737 @classmethod
745 738 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
746 739 q = cls.query().filter(cls.api_key == auth_token)
747 740
748 741 if cache:
749 742 q = q.options(FromCache("sql_cache_short",
750 743 "get_auth_token_%s" % auth_token))
751 744 res = q.scalar()
752 745
753 746 if fallback and not res:
754 747 #fallback to additional keys
755 748 _res = UserApiKeys.query()\
756 749 .filter(UserApiKeys.api_key == auth_token)\
757 750 .filter(or_(UserApiKeys.expires == -1,
758 751 UserApiKeys.expires >= time.time()))\
759 752 .first()
760 753 if _res:
761 754 res = _res.user
762 755 return res
763 756
764 757 @classmethod
765 758 def get_by_email(cls, email, case_insensitive=False, cache=False):
766 759
767 760 if case_insensitive:
768 761 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
769 762
770 763 else:
771 764 q = cls.query().filter(cls.email == email)
772 765
773 766 if cache:
774 767 q = q.options(FromCache("sql_cache_short",
775 768 "get_email_key_%s" % _hash_key(email)))
776 769
777 770 ret = q.scalar()
778 771 if ret is None:
779 772 q = UserEmailMap.query()
780 773 # try fetching in alternate email map
781 774 if case_insensitive:
782 775 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
783 776 else:
784 777 q = q.filter(UserEmailMap.email == email)
785 778 q = q.options(joinedload(UserEmailMap.user))
786 779 if cache:
787 780 q = q.options(FromCache("sql_cache_short",
788 781 "get_email_map_key_%s" % email))
789 782 ret = getattr(q.scalar(), 'user', None)
790 783
791 784 return ret
792 785
793 786 @classmethod
794 787 def get_from_cs_author(cls, author):
795 788 """
796 789 Tries to get User objects out of commit author string
797 790
798 791 :param author:
799 792 """
800 793 from rhodecode.lib.helpers import email, author_name
801 794 # Valid email in the attribute passed, see if they're in the system
802 795 _email = email(author)
803 796 if _email:
804 797 user = cls.get_by_email(_email, case_insensitive=True)
805 798 if user:
806 799 return user
807 800 # Maybe we can match by username?
808 801 _author = author_name(author)
809 802 user = cls.get_by_username(_author, case_insensitive=True)
810 803 if user:
811 804 return user
812 805
813 806 def update_userdata(self, **kwargs):
814 807 usr = self
815 808 old = usr.user_data
816 809 old.update(**kwargs)
817 810 usr.user_data = old
818 811 Session().add(usr)
819 812 log.debug('updated userdata with ', kwargs)
820 813
821 814 def update_lastlogin(self):
822 815 """Update user lastlogin"""
823 816 self.last_login = datetime.datetime.now()
824 817 Session().add(self)
825 818 log.debug('updated user %s lastlogin', self.username)
826 819
827 820 def update_lastactivity(self):
828 821 """Update user lastactivity"""
829 822 usr = self
830 823 old = usr.user_data
831 824 old.update({'last_activity': time.time()})
832 825 usr.user_data = old
833 826 Session().add(usr)
834 827 log.debug('updated user %s lastactivity', usr.username)
835 828
836 829 def update_password(self, new_password, change_api_key=False):
837 830 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
838 831
839 832 self.password = get_crypt_password(new_password)
840 833 if change_api_key:
841 834 self.api_key = generate_auth_token(self.username)
842 835 Session().add(self)
843 836
844 837 @classmethod
845 838 def get_first_super_admin(cls):
846 839 user = User.query().filter(User.admin == true()).first()
847 840 if user is None:
848 841 raise Exception('FATAL: Missing administrative account!')
849 842 return user
850 843
851 844 @classmethod
852 845 def get_all_super_admins(cls):
853 846 """
854 847 Returns all admin accounts sorted by username
855 848 """
856 849 return User.query().filter(User.admin == true())\
857 850 .order_by(User.username.asc()).all()
858 851
859 852 @classmethod
860 853 def get_default_user(cls, cache=False):
861 854 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
862 855 if user is None:
863 856 raise Exception('FATAL: Missing default account!')
864 857 return user
865 858
866 859 def _get_default_perms(self, user, suffix=''):
867 860 from rhodecode.model.permission import PermissionModel
868 861 return PermissionModel().get_default_perms(user.user_perms, suffix)
869 862
870 863 def get_default_perms(self, suffix=''):
871 864 return self._get_default_perms(self, suffix)
872 865
873 866 def get_api_data(self, include_secrets=False, details='full'):
874 867 """
875 868 Common function for generating user related data for API
876 869
877 870 :param include_secrets: By default secrets in the API data will be replaced
878 871 by a placeholder value to prevent exposing this data by accident. In case
879 872 this data shall be exposed, set this flag to ``True``.
880 873
881 874 :param details: details can be 'basic|full' basic gives only a subset of
882 875 the available user information that includes user_id, name and emails.
883 876 """
884 877 user = self
885 878 user_data = self.user_data
886 879 data = {
887 880 'user_id': user.user_id,
888 881 'username': user.username,
889 882 'firstname': user.name,
890 883 'lastname': user.lastname,
891 884 'email': user.email,
892 885 'emails': user.emails,
893 886 }
894 887 if details == 'basic':
895 888 return data
896 889
897 890 api_key_length = 40
898 891 api_key_replacement = '*' * api_key_length
899 892
900 893 extras = {
901 894 'api_key': api_key_replacement,
902 895 'api_keys': [api_key_replacement],
903 896 'active': user.active,
904 897 'admin': user.admin,
905 898 'extern_type': user.extern_type,
906 899 'extern_name': user.extern_name,
907 900 'last_login': user.last_login,
908 901 'ip_addresses': user.ip_addresses,
909 902 'language': user_data.get('language')
910 903 }
911 904 data.update(extras)
912 905
913 906 if include_secrets:
914 907 data['api_key'] = user.api_key
915 908 data['api_keys'] = user.auth_tokens
916 909 return data
917 910
918 911 def __json__(self):
919 912 data = {
920 913 'full_name': self.full_name,
921 914 'full_name_or_username': self.full_name_or_username,
922 915 'short_contact': self.short_contact,
923 916 'full_contact': self.full_contact,
924 917 }
925 918 data.update(self.get_api_data())
926 919 return data
927 920
928 921
929 922 class UserApiKeys(Base, BaseModel):
930 923 __tablename__ = 'user_api_keys'
931 924 __table_args__ = (
932 925 Index('uak_api_key_idx', 'api_key'),
933 926 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
934 927 UniqueConstraint('api_key'),
935 928 {'extend_existing': True, 'mysql_engine': 'InnoDB',
936 929 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
937 930 )
938 931 __mapper_args__ = {}
939 932
940 933 # ApiKey role
941 934 ROLE_ALL = 'token_role_all'
942 935 ROLE_HTTP = 'token_role_http'
943 936 ROLE_VCS = 'token_role_vcs'
944 937 ROLE_API = 'token_role_api'
945 938 ROLE_FEED = 'token_role_feed'
946 939 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
947 940
948 941 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
949 942 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
950 943 api_key = Column("api_key", String(255), nullable=False, unique=True)
951 944 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
952 945 expires = Column('expires', Float(53), nullable=False)
953 946 role = Column('role', String(255), nullable=True)
954 947 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
955 948
956 949 user = relationship('User', lazy='joined')
957 950
958 951 @classmethod
959 952 def _get_role_name(cls, role):
960 953 return {
961 954 cls.ROLE_ALL: _('all'),
962 955 cls.ROLE_HTTP: _('http/web interface'),
963 956 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
964 957 cls.ROLE_API: _('api calls'),
965 958 cls.ROLE_FEED: _('feed access'),
966 959 }.get(role, role)
967 960
968 961 @property
969 962 def expired(self):
970 963 if self.expires == -1:
971 964 return False
972 965 return time.time() > self.expires
973 966
974 967 @property
975 968 def role_humanized(self):
976 969 return self._get_role_name(self.role)
977 970
978 971
979 972 class UserEmailMap(Base, BaseModel):
980 973 __tablename__ = 'user_email_map'
981 974 __table_args__ = (
982 975 Index('uem_email_idx', 'email'),
983 976 UniqueConstraint('email'),
984 977 {'extend_existing': True, 'mysql_engine': 'InnoDB',
985 978 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
986 979 )
987 980 __mapper_args__ = {}
988 981
989 982 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
990 983 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
991 984 _email = Column("email", String(255), nullable=True, unique=False, default=None)
992 985 user = relationship('User', lazy='joined')
993 986
994 987 @validates('_email')
995 988 def validate_email(self, key, email):
996 989 # check if this email is not main one
997 990 main_email = Session().query(User).filter(User.email == email).scalar()
998 991 if main_email is not None:
999 992 raise AttributeError('email %s is present is user table' % email)
1000 993 return email
1001 994
1002 995 @hybrid_property
1003 996 def email(self):
1004 997 return self._email
1005 998
1006 999 @email.setter
1007 1000 def email(self, val):
1008 1001 self._email = val.lower() if val else None
1009 1002
1010 1003
1011 1004 class UserIpMap(Base, BaseModel):
1012 1005 __tablename__ = 'user_ip_map'
1013 1006 __table_args__ = (
1014 1007 UniqueConstraint('user_id', 'ip_addr'),
1015 1008 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1016 1009 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1017 1010 )
1018 1011 __mapper_args__ = {}
1019 1012
1020 1013 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1021 1014 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1022 1015 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1023 1016 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1024 1017 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1025 1018 user = relationship('User', lazy='joined')
1026 1019
1027 1020 @classmethod
1028 1021 def _get_ip_range(cls, ip_addr):
1029 1022 net = ipaddress.ip_network(ip_addr, strict=False)
1030 1023 return [str(net.network_address), str(net.broadcast_address)]
1031 1024
1032 1025 def __json__(self):
1033 1026 return {
1034 1027 'ip_addr': self.ip_addr,
1035 1028 'ip_range': self._get_ip_range(self.ip_addr),
1036 1029 }
1037 1030
1038 1031 def __unicode__(self):
1039 1032 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1040 1033 self.user_id, self.ip_addr)
1041 1034
1042 1035 class UserLog(Base, BaseModel):
1043 1036 __tablename__ = 'user_logs'
1044 1037 __table_args__ = (
1045 1038 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1046 1039 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1047 1040 )
1048 1041 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1049 1042 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1050 1043 username = Column("username", String(255), nullable=True, unique=None, default=None)
1051 1044 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1052 1045 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1053 1046 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1054 1047 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1055 1048 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1056 1049
1057 1050 def __unicode__(self):
1058 1051 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1059 1052 self.repository_name,
1060 1053 self.action)
1061 1054
1062 1055 @property
1063 1056 def action_as_day(self):
1064 1057 return datetime.date(*self.action_date.timetuple()[:3])
1065 1058
1066 1059 user = relationship('User')
1067 1060 repository = relationship('Repository', cascade='')
1068 1061
1069 1062
1070 1063 class UserGroup(Base, BaseModel):
1071 1064 __tablename__ = 'users_groups'
1072 1065 __table_args__ = (
1073 1066 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1074 1067 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1075 1068 )
1076 1069
1077 1070 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1078 1071 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1079 1072 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1080 1073 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1081 1074 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1082 1075 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1083 1076 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1084 1077 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1085 1078
1086 1079 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1087 1080 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1088 1081 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1089 1082 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1090 1083 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1091 1084 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1092 1085
1093 1086 user = relationship('User')
1094 1087
1095 1088 @hybrid_property
1096 1089 def group_data(self):
1097 1090 if not self._group_data:
1098 1091 return {}
1099 1092
1100 1093 try:
1101 1094 return json.loads(self._group_data)
1102 1095 except TypeError:
1103 1096 return {}
1104 1097
1105 1098 @group_data.setter
1106 1099 def group_data(self, val):
1107 1100 try:
1108 1101 self._group_data = json.dumps(val)
1109 1102 except Exception:
1110 1103 log.error(traceback.format_exc())
1111 1104
1112 1105 def __unicode__(self):
1113 1106 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1114 1107 self.users_group_id,
1115 1108 self.users_group_name)
1116 1109
1117 1110 @classmethod
1118 1111 def get_by_group_name(cls, group_name, cache=False,
1119 1112 case_insensitive=False):
1120 1113 if case_insensitive:
1121 1114 q = cls.query().filter(func.lower(cls.users_group_name) ==
1122 1115 func.lower(group_name))
1123 1116
1124 1117 else:
1125 1118 q = cls.query().filter(cls.users_group_name == group_name)
1126 1119 if cache:
1127 1120 q = q.options(FromCache(
1128 1121 "sql_cache_short",
1129 1122 "get_group_%s" % _hash_key(group_name)))
1130 1123 return q.scalar()
1131 1124
1132 1125 @classmethod
1133 1126 def get(cls, user_group_id, cache=False):
1134 1127 user_group = cls.query()
1135 1128 if cache:
1136 1129 user_group = user_group.options(FromCache("sql_cache_short",
1137 1130 "get_users_group_%s" % user_group_id))
1138 1131 return user_group.get(user_group_id)
1139 1132
1140 1133 def permissions(self, with_admins=True, with_owner=True):
1141 1134 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1142 1135 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1143 1136 joinedload(UserUserGroupToPerm.user),
1144 1137 joinedload(UserUserGroupToPerm.permission),)
1145 1138
1146 1139 # get owners and admins and permissions. We do a trick of re-writing
1147 1140 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1148 1141 # has a global reference and changing one object propagates to all
1149 1142 # others. This means if admin is also an owner admin_row that change
1150 1143 # would propagate to both objects
1151 1144 perm_rows = []
1152 1145 for _usr in q.all():
1153 1146 usr = AttributeDict(_usr.user.get_dict())
1154 1147 usr.permission = _usr.permission.permission_name
1155 1148 perm_rows.append(usr)
1156 1149
1157 1150 # filter the perm rows by 'default' first and then sort them by
1158 1151 # admin,write,read,none permissions sorted again alphabetically in
1159 1152 # each group
1160 1153 perm_rows = sorted(perm_rows, key=display_sort)
1161 1154
1162 1155 _admin_perm = 'usergroup.admin'
1163 1156 owner_row = []
1164 1157 if with_owner:
1165 1158 usr = AttributeDict(self.user.get_dict())
1166 1159 usr.owner_row = True
1167 1160 usr.permission = _admin_perm
1168 1161 owner_row.append(usr)
1169 1162
1170 1163 super_admin_rows = []
1171 1164 if with_admins:
1172 1165 for usr in User.get_all_super_admins():
1173 1166 # if this admin is also owner, don't double the record
1174 1167 if usr.user_id == owner_row[0].user_id:
1175 1168 owner_row[0].admin_row = True
1176 1169 else:
1177 1170 usr = AttributeDict(usr.get_dict())
1178 1171 usr.admin_row = True
1179 1172 usr.permission = _admin_perm
1180 1173 super_admin_rows.append(usr)
1181 1174
1182 1175 return super_admin_rows + owner_row + perm_rows
1183 1176
1184 1177 def permission_user_groups(self):
1185 1178 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1186 1179 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1187 1180 joinedload(UserGroupUserGroupToPerm.target_user_group),
1188 1181 joinedload(UserGroupUserGroupToPerm.permission),)
1189 1182
1190 1183 perm_rows = []
1191 1184 for _user_group in q.all():
1192 1185 usr = AttributeDict(_user_group.user_group.get_dict())
1193 1186 usr.permission = _user_group.permission.permission_name
1194 1187 perm_rows.append(usr)
1195 1188
1196 1189 return perm_rows
1197 1190
1198 1191 def _get_default_perms(self, user_group, suffix=''):
1199 1192 from rhodecode.model.permission import PermissionModel
1200 1193 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1201 1194
1202 1195 def get_default_perms(self, suffix=''):
1203 1196 return self._get_default_perms(self, suffix)
1204 1197
1205 1198 def get_api_data(self, with_group_members=True, include_secrets=False):
1206 1199 """
1207 1200 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1208 1201 basically forwarded.
1209 1202
1210 1203 """
1211 1204 user_group = self
1212 1205
1213 1206 data = {
1214 1207 'users_group_id': user_group.users_group_id,
1215 1208 'group_name': user_group.users_group_name,
1216 1209 'group_description': user_group.user_group_description,
1217 1210 'active': user_group.users_group_active,
1218 1211 'owner': user_group.user.username,
1219 1212 }
1220 1213 if with_group_members:
1221 1214 users = []
1222 1215 for user in user_group.members:
1223 1216 user = user.user
1224 1217 users.append(user.get_api_data(include_secrets=include_secrets))
1225 1218 data['users'] = users
1226 1219
1227 1220 return data
1228 1221
1229 1222
1230 1223 class UserGroupMember(Base, BaseModel):
1231 1224 __tablename__ = 'users_groups_members'
1232 1225 __table_args__ = (
1233 1226 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1234 1227 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1235 1228 )
1236 1229
1237 1230 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1238 1231 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1239 1232 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1240 1233
1241 1234 user = relationship('User', lazy='joined')
1242 1235 users_group = relationship('UserGroup')
1243 1236
1244 1237 def __init__(self, gr_id='', u_id=''):
1245 1238 self.users_group_id = gr_id
1246 1239 self.user_id = u_id
1247 1240
1248 1241
1249 1242 class RepositoryField(Base, BaseModel):
1250 1243 __tablename__ = 'repositories_fields'
1251 1244 __table_args__ = (
1252 1245 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1253 1246 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1254 1247 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1255 1248 )
1256 1249 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1257 1250
1258 1251 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1259 1252 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1260 1253 field_key = Column("field_key", String(250))
1261 1254 field_label = Column("field_label", String(1024), nullable=False)
1262 1255 field_value = Column("field_value", String(10000), nullable=False)
1263 1256 field_desc = Column("field_desc", String(1024), nullable=False)
1264 1257 field_type = Column("field_type", String(255), nullable=False, unique=None)
1265 1258 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1266 1259
1267 1260 repository = relationship('Repository')
1268 1261
1269 1262 @property
1270 1263 def field_key_prefixed(self):
1271 1264 return 'ex_%s' % self.field_key
1272 1265
1273 1266 @classmethod
1274 1267 def un_prefix_key(cls, key):
1275 1268 if key.startswith(cls.PREFIX):
1276 1269 return key[len(cls.PREFIX):]
1277 1270 return key
1278 1271
1279 1272 @classmethod
1280 1273 def get_by_key_name(cls, key, repo):
1281 1274 row = cls.query()\
1282 1275 .filter(cls.repository == repo)\
1283 1276 .filter(cls.field_key == key).scalar()
1284 1277 return row
1285 1278
1286 1279
1287 1280 class Repository(Base, BaseModel):
1288 1281 __tablename__ = 'repositories'
1289 1282 __table_args__ = (
1290 1283 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1291 1284 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1292 1285 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1293 1286 )
1294 1287 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1295 1288 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1296 1289
1297 1290 STATE_CREATED = 'repo_state_created'
1298 1291 STATE_PENDING = 'repo_state_pending'
1299 1292 STATE_ERROR = 'repo_state_error'
1300 1293
1301 1294 LOCK_AUTOMATIC = 'lock_auto'
1302 1295 LOCK_API = 'lock_api'
1303 1296 LOCK_WEB = 'lock_web'
1304 1297 LOCK_PULL = 'lock_pull'
1305 1298
1306 1299 NAME_SEP = URL_SEP
1307 1300
1308 1301 repo_id = Column(
1309 1302 "repo_id", Integer(), nullable=False, unique=True, default=None,
1310 1303 primary_key=True)
1311 1304 _repo_name = Column(
1312 1305 "repo_name", Text(), nullable=False, default=None)
1313 1306 _repo_name_hash = Column(
1314 1307 "repo_name_hash", String(255), nullable=False, unique=True)
1315 1308 repo_state = Column("repo_state", String(255), nullable=True)
1316 1309
1317 1310 clone_uri = Column(
1318 1311 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1319 1312 default=None)
1320 1313 repo_type = Column(
1321 1314 "repo_type", String(255), nullable=False, unique=False, default=None)
1322 1315 user_id = Column(
1323 1316 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1324 1317 unique=False, default=None)
1325 1318 private = Column(
1326 1319 "private", Boolean(), nullable=True, unique=None, default=None)
1327 1320 enable_statistics = Column(
1328 1321 "statistics", Boolean(), nullable=True, unique=None, default=True)
1329 1322 enable_downloads = Column(
1330 1323 "downloads", Boolean(), nullable=True, unique=None, default=True)
1331 1324 description = Column(
1332 1325 "description", String(10000), nullable=True, unique=None, default=None)
1333 1326 created_on = Column(
1334 1327 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1335 1328 default=datetime.datetime.now)
1336 1329 updated_on = Column(
1337 1330 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1338 1331 default=datetime.datetime.now)
1339 1332 _landing_revision = Column(
1340 1333 "landing_revision", String(255), nullable=False, unique=False,
1341 1334 default=None)
1342 1335 enable_locking = Column(
1343 1336 "enable_locking", Boolean(), nullable=False, unique=None,
1344 1337 default=False)
1345 1338 _locked = Column(
1346 1339 "locked", String(255), nullable=True, unique=False, default=None)
1347 1340 _changeset_cache = Column(
1348 1341 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1349 1342
1350 1343 fork_id = Column(
1351 1344 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1352 1345 nullable=True, unique=False, default=None)
1353 1346 group_id = Column(
1354 1347 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1355 1348 unique=False, default=None)
1356 1349
1357 1350 user = relationship('User', lazy='joined')
1358 1351 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1359 1352 group = relationship('RepoGroup', lazy='joined')
1360 1353 repo_to_perm = relationship(
1361 1354 'UserRepoToPerm', cascade='all',
1362 1355 order_by='UserRepoToPerm.repo_to_perm_id')
1363 1356 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1364 1357 stats = relationship('Statistics', cascade='all', uselist=False)
1365 1358
1366 1359 followers = relationship(
1367 1360 'UserFollowing',
1368 1361 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1369 1362 cascade='all')
1370 1363 extra_fields = relationship(
1371 1364 'RepositoryField', cascade="all, delete, delete-orphan")
1372 1365 logs = relationship('UserLog')
1373 1366 comments = relationship(
1374 1367 'ChangesetComment', cascade="all, delete, delete-orphan")
1375 1368 pull_requests_source = relationship(
1376 1369 'PullRequest',
1377 1370 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1378 1371 cascade="all, delete, delete-orphan")
1379 1372 pull_requests_target = relationship(
1380 1373 'PullRequest',
1381 1374 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1382 1375 cascade="all, delete, delete-orphan")
1383 1376 ui = relationship('RepoRhodeCodeUi', cascade="all")
1384 1377 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1385 1378 integrations = relationship('Integration',
1386 1379 cascade="all, delete, delete-orphan")
1387 1380
1388 1381 def __unicode__(self):
1389 1382 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1390 1383 safe_unicode(self.repo_name))
1391 1384
1392 1385 @hybrid_property
1393 1386 def landing_rev(self):
1394 1387 # always should return [rev_type, rev]
1395 1388 if self._landing_revision:
1396 1389 _rev_info = self._landing_revision.split(':')
1397 1390 if len(_rev_info) < 2:
1398 1391 _rev_info.insert(0, 'rev')
1399 1392 return [_rev_info[0], _rev_info[1]]
1400 1393 return [None, None]
1401 1394
1402 1395 @landing_rev.setter
1403 1396 def landing_rev(self, val):
1404 1397 if ':' not in val:
1405 1398 raise ValueError('value must be delimited with `:` and consist '
1406 1399 'of <rev_type>:<rev>, got %s instead' % val)
1407 1400 self._landing_revision = val
1408 1401
1409 1402 @hybrid_property
1410 1403 def locked(self):
1411 1404 if self._locked:
1412 1405 user_id, timelocked, reason = self._locked.split(':')
1413 1406 lock_values = int(user_id), timelocked, reason
1414 1407 else:
1415 1408 lock_values = [None, None, None]
1416 1409 return lock_values
1417 1410
1418 1411 @locked.setter
1419 1412 def locked(self, val):
1420 1413 if val and isinstance(val, (list, tuple)):
1421 1414 self._locked = ':'.join(map(str, val))
1422 1415 else:
1423 1416 self._locked = None
1424 1417
1425 1418 @hybrid_property
1426 1419 def changeset_cache(self):
1427 1420 from rhodecode.lib.vcs.backends.base import EmptyCommit
1428 1421 dummy = EmptyCommit().__json__()
1429 1422 if not self._changeset_cache:
1430 1423 return dummy
1431 1424 try:
1432 1425 return json.loads(self._changeset_cache)
1433 1426 except TypeError:
1434 1427 return dummy
1435 1428 except Exception:
1436 1429 log.error(traceback.format_exc())
1437 1430 return dummy
1438 1431
1439 1432 @changeset_cache.setter
1440 1433 def changeset_cache(self, val):
1441 1434 try:
1442 1435 self._changeset_cache = json.dumps(val)
1443 1436 except Exception:
1444 1437 log.error(traceback.format_exc())
1445 1438
1446 1439 @hybrid_property
1447 1440 def repo_name(self):
1448 1441 return self._repo_name
1449 1442
1450 1443 @repo_name.setter
1451 1444 def repo_name(self, value):
1452 1445 self._repo_name = value
1453 1446 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1454 1447
1455 1448 @classmethod
1456 1449 def normalize_repo_name(cls, repo_name):
1457 1450 """
1458 1451 Normalizes os specific repo_name to the format internally stored inside
1459 1452 database using URL_SEP
1460 1453
1461 1454 :param cls:
1462 1455 :param repo_name:
1463 1456 """
1464 1457 return cls.NAME_SEP.join(repo_name.split(os.sep))
1465 1458
1466 1459 @classmethod
1467 1460 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1468 1461 session = Session()
1469 1462 q = session.query(cls).filter(cls.repo_name == repo_name)
1470 1463
1471 1464 if cache:
1472 1465 if identity_cache:
1473 1466 val = cls.identity_cache(session, 'repo_name', repo_name)
1474 1467 if val:
1475 1468 return val
1476 1469 else:
1477 1470 q = q.options(
1478 1471 FromCache("sql_cache_short",
1479 1472 "get_repo_by_name_%s" % _hash_key(repo_name)))
1480 1473
1481 1474 return q.scalar()
1482 1475
1483 1476 @classmethod
1484 1477 def get_by_full_path(cls, repo_full_path):
1485 1478 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1486 1479 repo_name = cls.normalize_repo_name(repo_name)
1487 1480 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1488 1481
1489 1482 @classmethod
1490 1483 def get_repo_forks(cls, repo_id):
1491 1484 return cls.query().filter(Repository.fork_id == repo_id)
1492 1485
1493 1486 @classmethod
1494 1487 def base_path(cls):
1495 1488 """
1496 1489 Returns base path when all repos are stored
1497 1490
1498 1491 :param cls:
1499 1492 """
1500 1493 q = Session().query(RhodeCodeUi)\
1501 1494 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1502 1495 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1503 1496 return q.one().ui_value
1504 1497
1505 1498 @classmethod
1506 1499 def is_valid(cls, repo_name):
1507 1500 """
1508 1501 returns True if given repo name is a valid filesystem repository
1509 1502
1510 1503 :param cls:
1511 1504 :param repo_name:
1512 1505 """
1513 1506 from rhodecode.lib.utils import is_valid_repo
1514 1507
1515 1508 return is_valid_repo(repo_name, cls.base_path())
1516 1509
1517 1510 @classmethod
1518 1511 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1519 1512 case_insensitive=True):
1520 1513 q = Repository.query()
1521 1514
1522 1515 if not isinstance(user_id, Optional):
1523 1516 q = q.filter(Repository.user_id == user_id)
1524 1517
1525 1518 if not isinstance(group_id, Optional):
1526 1519 q = q.filter(Repository.group_id == group_id)
1527 1520
1528 1521 if case_insensitive:
1529 1522 q = q.order_by(func.lower(Repository.repo_name))
1530 1523 else:
1531 1524 q = q.order_by(Repository.repo_name)
1532 1525 return q.all()
1533 1526
1534 1527 @property
1535 1528 def forks(self):
1536 1529 """
1537 1530 Return forks of this repo
1538 1531 """
1539 1532 return Repository.get_repo_forks(self.repo_id)
1540 1533
1541 1534 @property
1542 1535 def parent(self):
1543 1536 """
1544 1537 Returns fork parent
1545 1538 """
1546 1539 return self.fork
1547 1540
1548 1541 @property
1549 1542 def just_name(self):
1550 1543 return self.repo_name.split(self.NAME_SEP)[-1]
1551 1544
1552 1545 @property
1553 1546 def groups_with_parents(self):
1554 1547 groups = []
1555 1548 if self.group is None:
1556 1549 return groups
1557 1550
1558 1551 cur_gr = self.group
1559 1552 groups.insert(0, cur_gr)
1560 1553 while 1:
1561 1554 gr = getattr(cur_gr, 'parent_group', None)
1562 1555 cur_gr = cur_gr.parent_group
1563 1556 if gr is None:
1564 1557 break
1565 1558 groups.insert(0, gr)
1566 1559
1567 1560 return groups
1568 1561
1569 1562 @property
1570 1563 def groups_and_repo(self):
1571 1564 return self.groups_with_parents, self
1572 1565
1573 1566 @LazyProperty
1574 1567 def repo_path(self):
1575 1568 """
1576 1569 Returns base full path for that repository means where it actually
1577 1570 exists on a filesystem
1578 1571 """
1579 1572 q = Session().query(RhodeCodeUi).filter(
1580 1573 RhodeCodeUi.ui_key == self.NAME_SEP)
1581 1574 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1582 1575 return q.one().ui_value
1583 1576
1584 1577 @property
1585 1578 def repo_full_path(self):
1586 1579 p = [self.repo_path]
1587 1580 # we need to split the name by / since this is how we store the
1588 1581 # names in the database, but that eventually needs to be converted
1589 1582 # into a valid system path
1590 1583 p += self.repo_name.split(self.NAME_SEP)
1591 1584 return os.path.join(*map(safe_unicode, p))
1592 1585
1593 1586 @property
1594 1587 def cache_keys(self):
1595 1588 """
1596 1589 Returns associated cache keys for that repo
1597 1590 """
1598 1591 return CacheKey.query()\
1599 1592 .filter(CacheKey.cache_args == self.repo_name)\
1600 1593 .order_by(CacheKey.cache_key)\
1601 1594 .all()
1602 1595
1603 1596 def get_new_name(self, repo_name):
1604 1597 """
1605 1598 returns new full repository name based on assigned group and new new
1606 1599
1607 1600 :param group_name:
1608 1601 """
1609 1602 path_prefix = self.group.full_path_splitted if self.group else []
1610 1603 return self.NAME_SEP.join(path_prefix + [repo_name])
1611 1604
1612 1605 @property
1613 1606 def _config(self):
1614 1607 """
1615 1608 Returns db based config object.
1616 1609 """
1617 1610 from rhodecode.lib.utils import make_db_config
1618 1611 return make_db_config(clear_session=False, repo=self)
1619 1612
1620 1613 def permissions(self, with_admins=True, with_owner=True):
1621 1614 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1622 1615 q = q.options(joinedload(UserRepoToPerm.repository),
1623 1616 joinedload(UserRepoToPerm.user),
1624 1617 joinedload(UserRepoToPerm.permission),)
1625 1618
1626 1619 # get owners and admins and permissions. We do a trick of re-writing
1627 1620 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1628 1621 # has a global reference and changing one object propagates to all
1629 1622 # others. This means if admin is also an owner admin_row that change
1630 1623 # would propagate to both objects
1631 1624 perm_rows = []
1632 1625 for _usr in q.all():
1633 1626 usr = AttributeDict(_usr.user.get_dict())
1634 1627 usr.permission = _usr.permission.permission_name
1635 1628 perm_rows.append(usr)
1636 1629
1637 1630 # filter the perm rows by 'default' first and then sort them by
1638 1631 # admin,write,read,none permissions sorted again alphabetically in
1639 1632 # each group
1640 1633 perm_rows = sorted(perm_rows, key=display_sort)
1641 1634
1642 1635 _admin_perm = 'repository.admin'
1643 1636 owner_row = []
1644 1637 if with_owner:
1645 1638 usr = AttributeDict(self.user.get_dict())
1646 1639 usr.owner_row = True
1647 1640 usr.permission = _admin_perm
1648 1641 owner_row.append(usr)
1649 1642
1650 1643 super_admin_rows = []
1651 1644 if with_admins:
1652 1645 for usr in User.get_all_super_admins():
1653 1646 # if this admin is also owner, don't double the record
1654 1647 if usr.user_id == owner_row[0].user_id:
1655 1648 owner_row[0].admin_row = True
1656 1649 else:
1657 1650 usr = AttributeDict(usr.get_dict())
1658 1651 usr.admin_row = True
1659 1652 usr.permission = _admin_perm
1660 1653 super_admin_rows.append(usr)
1661 1654
1662 1655 return super_admin_rows + owner_row + perm_rows
1663 1656
1664 1657 def permission_user_groups(self):
1665 1658 q = UserGroupRepoToPerm.query().filter(
1666 1659 UserGroupRepoToPerm.repository == self)
1667 1660 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1668 1661 joinedload(UserGroupRepoToPerm.users_group),
1669 1662 joinedload(UserGroupRepoToPerm.permission),)
1670 1663
1671 1664 perm_rows = []
1672 1665 for _user_group in q.all():
1673 1666 usr = AttributeDict(_user_group.users_group.get_dict())
1674 1667 usr.permission = _user_group.permission.permission_name
1675 1668 perm_rows.append(usr)
1676 1669
1677 1670 return perm_rows
1678 1671
1679 1672 def get_api_data(self, include_secrets=False):
1680 1673 """
1681 1674 Common function for generating repo api data
1682 1675
1683 1676 :param include_secrets: See :meth:`User.get_api_data`.
1684 1677
1685 1678 """
1686 1679 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1687 1680 # move this methods on models level.
1688 1681 from rhodecode.model.settings import SettingsModel
1689 1682
1690 1683 repo = self
1691 1684 _user_id, _time, _reason = self.locked
1692 1685
1693 1686 data = {
1694 1687 'repo_id': repo.repo_id,
1695 1688 'repo_name': repo.repo_name,
1696 1689 'repo_type': repo.repo_type,
1697 1690 'clone_uri': repo.clone_uri or '',
1698 1691 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1699 1692 'private': repo.private,
1700 1693 'created_on': repo.created_on,
1701 1694 'description': repo.description,
1702 1695 'landing_rev': repo.landing_rev,
1703 1696 'owner': repo.user.username,
1704 1697 'fork_of': repo.fork.repo_name if repo.fork else None,
1705 1698 'enable_statistics': repo.enable_statistics,
1706 1699 'enable_locking': repo.enable_locking,
1707 1700 'enable_downloads': repo.enable_downloads,
1708 1701 'last_changeset': repo.changeset_cache,
1709 1702 'locked_by': User.get(_user_id).get_api_data(
1710 1703 include_secrets=include_secrets) if _user_id else None,
1711 1704 'locked_date': time_to_datetime(_time) if _time else None,
1712 1705 'lock_reason': _reason if _reason else None,
1713 1706 }
1714 1707
1715 1708 # TODO: mikhail: should be per-repo settings here
1716 1709 rc_config = SettingsModel().get_all_settings()
1717 1710 repository_fields = str2bool(
1718 1711 rc_config.get('rhodecode_repository_fields'))
1719 1712 if repository_fields:
1720 1713 for f in self.extra_fields:
1721 1714 data[f.field_key_prefixed] = f.field_value
1722 1715
1723 1716 return data
1724 1717
1725 1718 @classmethod
1726 1719 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1727 1720 if not lock_time:
1728 1721 lock_time = time.time()
1729 1722 if not lock_reason:
1730 1723 lock_reason = cls.LOCK_AUTOMATIC
1731 1724 repo.locked = [user_id, lock_time, lock_reason]
1732 1725 Session().add(repo)
1733 1726 Session().commit()
1734 1727
1735 1728 @classmethod
1736 1729 def unlock(cls, repo):
1737 1730 repo.locked = None
1738 1731 Session().add(repo)
1739 1732 Session().commit()
1740 1733
1741 1734 @classmethod
1742 1735 def getlock(cls, repo):
1743 1736 return repo.locked
1744 1737
1745 1738 def is_user_lock(self, user_id):
1746 1739 if self.lock[0]:
1747 1740 lock_user_id = safe_int(self.lock[0])
1748 1741 user_id = safe_int(user_id)
1749 1742 # both are ints, and they are equal
1750 1743 return all([lock_user_id, user_id]) and lock_user_id == user_id
1751 1744
1752 1745 return False
1753 1746
1754 1747 def get_locking_state(self, action, user_id, only_when_enabled=True):
1755 1748 """
1756 1749 Checks locking on this repository, if locking is enabled and lock is
1757 1750 present returns a tuple of make_lock, locked, locked_by.
1758 1751 make_lock can have 3 states None (do nothing) True, make lock
1759 1752 False release lock, This value is later propagated to hooks, which
1760 1753 do the locking. Think about this as signals passed to hooks what to do.
1761 1754
1762 1755 """
1763 1756 # TODO: johbo: This is part of the business logic and should be moved
1764 1757 # into the RepositoryModel.
1765 1758
1766 1759 if action not in ('push', 'pull'):
1767 1760 raise ValueError("Invalid action value: %s" % repr(action))
1768 1761
1769 1762 # defines if locked error should be thrown to user
1770 1763 currently_locked = False
1771 1764 # defines if new lock should be made, tri-state
1772 1765 make_lock = None
1773 1766 repo = self
1774 1767 user = User.get(user_id)
1775 1768
1776 1769 lock_info = repo.locked
1777 1770
1778 1771 if repo and (repo.enable_locking or not only_when_enabled):
1779 1772 if action == 'push':
1780 1773 # check if it's already locked !, if it is compare users
1781 1774 locked_by_user_id = lock_info[0]
1782 1775 if user.user_id == locked_by_user_id:
1783 1776 log.debug(
1784 1777 'Got `push` action from user %s, now unlocking', user)
1785 1778 # unlock if we have push from user who locked
1786 1779 make_lock = False
1787 1780 else:
1788 1781 # we're not the same user who locked, ban with
1789 1782 # code defined in settings (default is 423 HTTP Locked) !
1790 1783 log.debug('Repo %s is currently locked by %s', repo, user)
1791 1784 currently_locked = True
1792 1785 elif action == 'pull':
1793 1786 # [0] user [1] date
1794 1787 if lock_info[0] and lock_info[1]:
1795 1788 log.debug('Repo %s is currently locked by %s', repo, user)
1796 1789 currently_locked = True
1797 1790 else:
1798 1791 log.debug('Setting lock on repo %s by %s', repo, user)
1799 1792 make_lock = True
1800 1793
1801 1794 else:
1802 1795 log.debug('Repository %s do not have locking enabled', repo)
1803 1796
1804 1797 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1805 1798 make_lock, currently_locked, lock_info)
1806 1799
1807 1800 from rhodecode.lib.auth import HasRepoPermissionAny
1808 1801 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1809 1802 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1810 1803 # if we don't have at least write permission we cannot make a lock
1811 1804 log.debug('lock state reset back to FALSE due to lack '
1812 1805 'of at least read permission')
1813 1806 make_lock = False
1814 1807
1815 1808 return make_lock, currently_locked, lock_info
1816 1809
1817 1810 @property
1818 1811 def last_db_change(self):
1819 1812 return self.updated_on
1820 1813
1821 1814 @property
1822 1815 def clone_uri_hidden(self):
1823 1816 clone_uri = self.clone_uri
1824 1817 if clone_uri:
1825 1818 import urlobject
1826 1819 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
1827 1820 if url_obj.password:
1828 1821 clone_uri = url_obj.with_password('*****')
1829 1822 return clone_uri
1830 1823
1831 1824 def clone_url(self, **override):
1832 1825 qualified_home_url = url('home', qualified=True)
1833 1826
1834 1827 uri_tmpl = None
1835 1828 if 'with_id' in override:
1836 1829 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1837 1830 del override['with_id']
1838 1831
1839 1832 if 'uri_tmpl' in override:
1840 1833 uri_tmpl = override['uri_tmpl']
1841 1834 del override['uri_tmpl']
1842 1835
1843 1836 # we didn't override our tmpl from **overrides
1844 1837 if not uri_tmpl:
1845 1838 uri_tmpl = self.DEFAULT_CLONE_URI
1846 1839 try:
1847 1840 from pylons import tmpl_context as c
1848 1841 uri_tmpl = c.clone_uri_tmpl
1849 1842 except Exception:
1850 1843 # in any case if we call this outside of request context,
1851 1844 # ie, not having tmpl_context set up
1852 1845 pass
1853 1846
1854 1847 return get_clone_url(uri_tmpl=uri_tmpl,
1855 1848 qualifed_home_url=qualified_home_url,
1856 1849 repo_name=self.repo_name,
1857 1850 repo_id=self.repo_id, **override)
1858 1851
1859 1852 def set_state(self, state):
1860 1853 self.repo_state = state
1861 1854 Session().add(self)
1862 1855 #==========================================================================
1863 1856 # SCM PROPERTIES
1864 1857 #==========================================================================
1865 1858
1866 1859 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1867 1860 return get_commit_safe(
1868 1861 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1869 1862
1870 1863 def get_changeset(self, rev=None, pre_load=None):
1871 1864 warnings.warn("Use get_commit", DeprecationWarning)
1872 1865 commit_id = None
1873 1866 commit_idx = None
1874 1867 if isinstance(rev, basestring):
1875 1868 commit_id = rev
1876 1869 else:
1877 1870 commit_idx = rev
1878 1871 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1879 1872 pre_load=pre_load)
1880 1873
1881 1874 def get_landing_commit(self):
1882 1875 """
1883 1876 Returns landing commit, or if that doesn't exist returns the tip
1884 1877 """
1885 1878 _rev_type, _rev = self.landing_rev
1886 1879 commit = self.get_commit(_rev)
1887 1880 if isinstance(commit, EmptyCommit):
1888 1881 return self.get_commit()
1889 1882 return commit
1890 1883
1891 1884 def update_commit_cache(self, cs_cache=None, config=None):
1892 1885 """
1893 1886 Update cache of last changeset for repository, keys should be::
1894 1887
1895 1888 short_id
1896 1889 raw_id
1897 1890 revision
1898 1891 parents
1899 1892 message
1900 1893 date
1901 1894 author
1902 1895
1903 1896 :param cs_cache:
1904 1897 """
1905 1898 from rhodecode.lib.vcs.backends.base import BaseChangeset
1906 1899 if cs_cache is None:
1907 1900 # use no-cache version here
1908 1901 scm_repo = self.scm_instance(cache=False, config=config)
1909 1902 if scm_repo:
1910 1903 cs_cache = scm_repo.get_commit(
1911 1904 pre_load=["author", "date", "message", "parents"])
1912 1905 else:
1913 1906 cs_cache = EmptyCommit()
1914 1907
1915 1908 if isinstance(cs_cache, BaseChangeset):
1916 1909 cs_cache = cs_cache.__json__()
1917 1910
1918 1911 def is_outdated(new_cs_cache):
1919 1912 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1920 1913 new_cs_cache['revision'] != self.changeset_cache['revision']):
1921 1914 return True
1922 1915 return False
1923 1916
1924 1917 # check if we have maybe already latest cached revision
1925 1918 if is_outdated(cs_cache) or not self.changeset_cache:
1926 1919 _default = datetime.datetime.fromtimestamp(0)
1927 1920 last_change = cs_cache.get('date') or _default
1928 1921 log.debug('updated repo %s with new cs cache %s',
1929 1922 self.repo_name, cs_cache)
1930 1923 self.updated_on = last_change
1931 1924 self.changeset_cache = cs_cache
1932 1925 Session().add(self)
1933 1926 Session().commit()
1934 1927 else:
1935 1928 log.debug('Skipping update_commit_cache for repo:`%s` '
1936 1929 'commit already with latest changes', self.repo_name)
1937 1930
1938 1931 @property
1939 1932 def tip(self):
1940 1933 return self.get_commit('tip')
1941 1934
1942 1935 @property
1943 1936 def author(self):
1944 1937 return self.tip.author
1945 1938
1946 1939 @property
1947 1940 def last_change(self):
1948 1941 return self.scm_instance().last_change
1949 1942
1950 1943 def get_comments(self, revisions=None):
1951 1944 """
1952 1945 Returns comments for this repository grouped by revisions
1953 1946
1954 1947 :param revisions: filter query by revisions only
1955 1948 """
1956 1949 cmts = ChangesetComment.query()\
1957 1950 .filter(ChangesetComment.repo == self)
1958 1951 if revisions:
1959 1952 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1960 1953 grouped = collections.defaultdict(list)
1961 1954 for cmt in cmts.all():
1962 1955 grouped[cmt.revision].append(cmt)
1963 1956 return grouped
1964 1957
1965 1958 def statuses(self, revisions=None):
1966 1959 """
1967 1960 Returns statuses for this repository
1968 1961
1969 1962 :param revisions: list of revisions to get statuses for
1970 1963 """
1971 1964 statuses = ChangesetStatus.query()\
1972 1965 .filter(ChangesetStatus.repo == self)\
1973 1966 .filter(ChangesetStatus.version == 0)
1974 1967
1975 1968 if revisions:
1976 1969 # Try doing the filtering in chunks to avoid hitting limits
1977 1970 size = 500
1978 1971 status_results = []
1979 1972 for chunk in xrange(0, len(revisions), size):
1980 1973 status_results += statuses.filter(
1981 1974 ChangesetStatus.revision.in_(
1982 1975 revisions[chunk: chunk+size])
1983 1976 ).all()
1984 1977 else:
1985 1978 status_results = statuses.all()
1986 1979
1987 1980 grouped = {}
1988 1981
1989 1982 # maybe we have open new pullrequest without a status?
1990 1983 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1991 1984 status_lbl = ChangesetStatus.get_status_lbl(stat)
1992 1985 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1993 1986 for rev in pr.revisions:
1994 1987 pr_id = pr.pull_request_id
1995 1988 pr_repo = pr.target_repo.repo_name
1996 1989 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1997 1990
1998 1991 for stat in status_results:
1999 1992 pr_id = pr_repo = None
2000 1993 if stat.pull_request:
2001 1994 pr_id = stat.pull_request.pull_request_id
2002 1995 pr_repo = stat.pull_request.target_repo.repo_name
2003 1996 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2004 1997 pr_id, pr_repo]
2005 1998 return grouped
2006 1999
2007 2000 # ==========================================================================
2008 2001 # SCM CACHE INSTANCE
2009 2002 # ==========================================================================
2010 2003
2011 2004 def scm_instance(self, **kwargs):
2012 2005 import rhodecode
2013 2006
2014 2007 # Passing a config will not hit the cache currently only used
2015 2008 # for repo2dbmapper
2016 2009 config = kwargs.pop('config', None)
2017 2010 cache = kwargs.pop('cache', None)
2018 2011 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2019 2012 # if cache is NOT defined use default global, else we have a full
2020 2013 # control over cache behaviour
2021 2014 if cache is None and full_cache and not config:
2022 2015 return self._get_instance_cached()
2023 2016 return self._get_instance(cache=bool(cache), config=config)
2024 2017
2025 2018 def _get_instance_cached(self):
2026 2019 @cache_region('long_term')
2027 2020 def _get_repo(cache_key):
2028 2021 return self._get_instance()
2029 2022
2030 2023 invalidator_context = CacheKey.repo_context_cache(
2031 2024 _get_repo, self.repo_name, None, thread_scoped=True)
2032 2025
2033 2026 with invalidator_context as context:
2034 2027 context.invalidate()
2035 2028 repo = context.compute()
2036 2029
2037 2030 return repo
2038 2031
2039 2032 def _get_instance(self, cache=True, config=None):
2040 2033 config = config or self._config
2041 2034 custom_wire = {
2042 2035 'cache': cache # controls the vcs.remote cache
2043 2036 }
2044 2037 repo = get_vcs_instance(
2045 2038 repo_path=safe_str(self.repo_full_path),
2046 2039 config=config,
2047 2040 with_wire=custom_wire,
2048 2041 create=False,
2049 2042 _vcs_alias=self.repo_type)
2050 2043
2051 2044 return repo
2052 2045
2053 2046 def __json__(self):
2054 2047 return {'landing_rev': self.landing_rev}
2055 2048
2056 2049 def get_dict(self):
2057 2050
2058 2051 # Since we transformed `repo_name` to a hybrid property, we need to
2059 2052 # keep compatibility with the code which uses `repo_name` field.
2060 2053
2061 2054 result = super(Repository, self).get_dict()
2062 2055 result['repo_name'] = result.pop('_repo_name', None)
2063 2056 return result
2064 2057
2065 2058
2066 2059 class RepoGroup(Base, BaseModel):
2067 2060 __tablename__ = 'groups'
2068 2061 __table_args__ = (
2069 2062 UniqueConstraint('group_name', 'group_parent_id'),
2070 2063 CheckConstraint('group_id != group_parent_id'),
2071 2064 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2072 2065 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2073 2066 )
2074 2067 __mapper_args__ = {'order_by': 'group_name'}
2075 2068
2076 2069 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2077 2070
2078 2071 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2079 2072 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2080 2073 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2081 2074 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2082 2075 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2083 2076 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2084 2077 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2085 2078 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2086 2079
2087 2080 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2088 2081 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2089 2082 parent_group = relationship('RepoGroup', remote_side=group_id)
2090 2083 user = relationship('User')
2091 2084 integrations = relationship('Integration',
2092 2085 cascade="all, delete, delete-orphan")
2093 2086
2094 2087 def __init__(self, group_name='', parent_group=None):
2095 2088 self.group_name = group_name
2096 2089 self.parent_group = parent_group
2097 2090
2098 2091 def __unicode__(self):
2099 2092 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2100 2093 self.group_name)
2101 2094
2102 2095 @classmethod
2103 2096 def _generate_choice(cls, repo_group):
2104 2097 from webhelpers.html import literal as _literal
2105 2098 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2106 2099 return repo_group.group_id, _name(repo_group.full_path_splitted)
2107 2100
2108 2101 @classmethod
2109 2102 def groups_choices(cls, groups=None, show_empty_group=True):
2110 2103 if not groups:
2111 2104 groups = cls.query().all()
2112 2105
2113 2106 repo_groups = []
2114 2107 if show_empty_group:
2115 2108 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2116 2109
2117 2110 repo_groups.extend([cls._generate_choice(x) for x in groups])
2118 2111
2119 2112 repo_groups = sorted(
2120 2113 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2121 2114 return repo_groups
2122 2115
2123 2116 @classmethod
2124 2117 def url_sep(cls):
2125 2118 return URL_SEP
2126 2119
2127 2120 @classmethod
2128 2121 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2129 2122 if case_insensitive:
2130 2123 gr = cls.query().filter(func.lower(cls.group_name)
2131 2124 == func.lower(group_name))
2132 2125 else:
2133 2126 gr = cls.query().filter(cls.group_name == group_name)
2134 2127 if cache:
2135 2128 gr = gr.options(FromCache(
2136 2129 "sql_cache_short",
2137 2130 "get_group_%s" % _hash_key(group_name)))
2138 2131 return gr.scalar()
2139 2132
2140 2133 @classmethod
2141 2134 def get_user_personal_repo_group(cls, user_id):
2142 2135 user = User.get(user_id)
2143 2136 return cls.query()\
2144 2137 .filter(cls.personal == true())\
2145 2138 .filter(cls.user == user).scalar()
2146 2139
2147 2140 @classmethod
2148 2141 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2149 2142 case_insensitive=True):
2150 2143 q = RepoGroup.query()
2151 2144
2152 2145 if not isinstance(user_id, Optional):
2153 2146 q = q.filter(RepoGroup.user_id == user_id)
2154 2147
2155 2148 if not isinstance(group_id, Optional):
2156 2149 q = q.filter(RepoGroup.group_parent_id == group_id)
2157 2150
2158 2151 if case_insensitive:
2159 2152 q = q.order_by(func.lower(RepoGroup.group_name))
2160 2153 else:
2161 2154 q = q.order_by(RepoGroup.group_name)
2162 2155 return q.all()
2163 2156
2164 2157 @property
2165 2158 def parents(self):
2166 2159 parents_recursion_limit = 10
2167 2160 groups = []
2168 2161 if self.parent_group is None:
2169 2162 return groups
2170 2163 cur_gr = self.parent_group
2171 2164 groups.insert(0, cur_gr)
2172 2165 cnt = 0
2173 2166 while 1:
2174 2167 cnt += 1
2175 2168 gr = getattr(cur_gr, 'parent_group', None)
2176 2169 cur_gr = cur_gr.parent_group
2177 2170 if gr is None:
2178 2171 break
2179 2172 if cnt == parents_recursion_limit:
2180 2173 # this will prevent accidental infinit loops
2181 2174 log.error(('more than %s parents found for group %s, stopping '
2182 2175 'recursive parent fetching' % (parents_recursion_limit, self)))
2183 2176 break
2184 2177
2185 2178 groups.insert(0, gr)
2186 2179 return groups
2187 2180
2188 2181 @property
2189 2182 def children(self):
2190 2183 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2191 2184
2192 2185 @property
2193 2186 def name(self):
2194 2187 return self.group_name.split(RepoGroup.url_sep())[-1]
2195 2188
2196 2189 @property
2197 2190 def full_path(self):
2198 2191 return self.group_name
2199 2192
2200 2193 @property
2201 2194 def full_path_splitted(self):
2202 2195 return self.group_name.split(RepoGroup.url_sep())
2203 2196
2204 2197 @property
2205 2198 def repositories(self):
2206 2199 return Repository.query()\
2207 2200 .filter(Repository.group == self)\
2208 2201 .order_by(Repository.repo_name)
2209 2202
2210 2203 @property
2211 2204 def repositories_recursive_count(self):
2212 2205 cnt = self.repositories.count()
2213 2206
2214 2207 def children_count(group):
2215 2208 cnt = 0
2216 2209 for child in group.children:
2217 2210 cnt += child.repositories.count()
2218 2211 cnt += children_count(child)
2219 2212 return cnt
2220 2213
2221 2214 return cnt + children_count(self)
2222 2215
2223 2216 def _recursive_objects(self, include_repos=True):
2224 2217 all_ = []
2225 2218
2226 2219 def _get_members(root_gr):
2227 2220 if include_repos:
2228 2221 for r in root_gr.repositories:
2229 2222 all_.append(r)
2230 2223 childs = root_gr.children.all()
2231 2224 if childs:
2232 2225 for gr in childs:
2233 2226 all_.append(gr)
2234 2227 _get_members(gr)
2235 2228
2236 2229 _get_members(self)
2237 2230 return [self] + all_
2238 2231
2239 2232 def recursive_groups_and_repos(self):
2240 2233 """
2241 2234 Recursive return all groups, with repositories in those groups
2242 2235 """
2243 2236 return self._recursive_objects()
2244 2237
2245 2238 def recursive_groups(self):
2246 2239 """
2247 2240 Returns all children groups for this group including children of children
2248 2241 """
2249 2242 return self._recursive_objects(include_repos=False)
2250 2243
2251 2244 def get_new_name(self, group_name):
2252 2245 """
2253 2246 returns new full group name based on parent and new name
2254 2247
2255 2248 :param group_name:
2256 2249 """
2257 2250 path_prefix = (self.parent_group.full_path_splitted if
2258 2251 self.parent_group else [])
2259 2252 return RepoGroup.url_sep().join(path_prefix + [group_name])
2260 2253
2261 2254 def permissions(self, with_admins=True, with_owner=True):
2262 2255 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2263 2256 q = q.options(joinedload(UserRepoGroupToPerm.group),
2264 2257 joinedload(UserRepoGroupToPerm.user),
2265 2258 joinedload(UserRepoGroupToPerm.permission),)
2266 2259
2267 2260 # get owners and admins and permissions. We do a trick of re-writing
2268 2261 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2269 2262 # has a global reference and changing one object propagates to all
2270 2263 # others. This means if admin is also an owner admin_row that change
2271 2264 # would propagate to both objects
2272 2265 perm_rows = []
2273 2266 for _usr in q.all():
2274 2267 usr = AttributeDict(_usr.user.get_dict())
2275 2268 usr.permission = _usr.permission.permission_name
2276 2269 perm_rows.append(usr)
2277 2270
2278 2271 # filter the perm rows by 'default' first and then sort them by
2279 2272 # admin,write,read,none permissions sorted again alphabetically in
2280 2273 # each group
2281 2274 perm_rows = sorted(perm_rows, key=display_sort)
2282 2275
2283 2276 _admin_perm = 'group.admin'
2284 2277 owner_row = []
2285 2278 if with_owner:
2286 2279 usr = AttributeDict(self.user.get_dict())
2287 2280 usr.owner_row = True
2288 2281 usr.permission = _admin_perm
2289 2282 owner_row.append(usr)
2290 2283
2291 2284 super_admin_rows = []
2292 2285 if with_admins:
2293 2286 for usr in User.get_all_super_admins():
2294 2287 # if this admin is also owner, don't double the record
2295 2288 if usr.user_id == owner_row[0].user_id:
2296 2289 owner_row[0].admin_row = True
2297 2290 else:
2298 2291 usr = AttributeDict(usr.get_dict())
2299 2292 usr.admin_row = True
2300 2293 usr.permission = _admin_perm
2301 2294 super_admin_rows.append(usr)
2302 2295
2303 2296 return super_admin_rows + owner_row + perm_rows
2304 2297
2305 2298 def permission_user_groups(self):
2306 2299 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2307 2300 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2308 2301 joinedload(UserGroupRepoGroupToPerm.users_group),
2309 2302 joinedload(UserGroupRepoGroupToPerm.permission),)
2310 2303
2311 2304 perm_rows = []
2312 2305 for _user_group in q.all():
2313 2306 usr = AttributeDict(_user_group.users_group.get_dict())
2314 2307 usr.permission = _user_group.permission.permission_name
2315 2308 perm_rows.append(usr)
2316 2309
2317 2310 return perm_rows
2318 2311
2319 2312 def get_api_data(self):
2320 2313 """
2321 2314 Common function for generating api data
2322 2315
2323 2316 """
2324 2317 group = self
2325 2318 data = {
2326 2319 'group_id': group.group_id,
2327 2320 'group_name': group.group_name,
2328 2321 'group_description': group.group_description,
2329 2322 'parent_group': group.parent_group.group_name if group.parent_group else None,
2330 2323 'repositories': [x.repo_name for x in group.repositories],
2331 2324 'owner': group.user.username,
2332 2325 }
2333 2326 return data
2334 2327
2335 2328
2336 2329 class Permission(Base, BaseModel):
2337 2330 __tablename__ = 'permissions'
2338 2331 __table_args__ = (
2339 2332 Index('p_perm_name_idx', 'permission_name'),
2340 2333 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2341 2334 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2342 2335 )
2343 2336 PERMS = [
2344 2337 ('hg.admin', _('RhodeCode Super Administrator')),
2345 2338
2346 2339 ('repository.none', _('Repository no access')),
2347 2340 ('repository.read', _('Repository read access')),
2348 2341 ('repository.write', _('Repository write access')),
2349 2342 ('repository.admin', _('Repository admin access')),
2350 2343
2351 2344 ('group.none', _('Repository group no access')),
2352 2345 ('group.read', _('Repository group read access')),
2353 2346 ('group.write', _('Repository group write access')),
2354 2347 ('group.admin', _('Repository group admin access')),
2355 2348
2356 2349 ('usergroup.none', _('User group no access')),
2357 2350 ('usergroup.read', _('User group read access')),
2358 2351 ('usergroup.write', _('User group write access')),
2359 2352 ('usergroup.admin', _('User group admin access')),
2360 2353
2361 2354 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2362 2355 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2363 2356
2364 2357 ('hg.usergroup.create.false', _('User Group creation disabled')),
2365 2358 ('hg.usergroup.create.true', _('User Group creation enabled')),
2366 2359
2367 2360 ('hg.create.none', _('Repository creation disabled')),
2368 2361 ('hg.create.repository', _('Repository creation enabled')),
2369 2362 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2370 2363 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2371 2364
2372 2365 ('hg.fork.none', _('Repository forking disabled')),
2373 2366 ('hg.fork.repository', _('Repository forking enabled')),
2374 2367
2375 2368 ('hg.register.none', _('Registration disabled')),
2376 2369 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2377 2370 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2378 2371
2379 2372 ('hg.password_reset.enabled', _('Password reset enabled')),
2380 2373 ('hg.password_reset.hidden', _('Password reset hidden')),
2381 2374 ('hg.password_reset.disabled', _('Password reset disabled')),
2382 2375
2383 2376 ('hg.extern_activate.manual', _('Manual activation of external account')),
2384 2377 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2385 2378
2386 2379 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2387 2380 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2388 2381 ]
2389 2382
2390 2383 # definition of system default permissions for DEFAULT user
2391 2384 DEFAULT_USER_PERMISSIONS = [
2392 2385 'repository.read',
2393 2386 'group.read',
2394 2387 'usergroup.read',
2395 2388 'hg.create.repository',
2396 2389 'hg.repogroup.create.false',
2397 2390 'hg.usergroup.create.false',
2398 2391 'hg.create.write_on_repogroup.true',
2399 2392 'hg.fork.repository',
2400 2393 'hg.register.manual_activate',
2401 2394 'hg.password_reset.enabled',
2402 2395 'hg.extern_activate.auto',
2403 2396 'hg.inherit_default_perms.true',
2404 2397 ]
2405 2398
2406 2399 # defines which permissions are more important higher the more important
2407 2400 # Weight defines which permissions are more important.
2408 2401 # The higher number the more important.
2409 2402 PERM_WEIGHTS = {
2410 2403 'repository.none': 0,
2411 2404 'repository.read': 1,
2412 2405 'repository.write': 3,
2413 2406 'repository.admin': 4,
2414 2407
2415 2408 'group.none': 0,
2416 2409 'group.read': 1,
2417 2410 'group.write': 3,
2418 2411 'group.admin': 4,
2419 2412
2420 2413 'usergroup.none': 0,
2421 2414 'usergroup.read': 1,
2422 2415 'usergroup.write': 3,
2423 2416 'usergroup.admin': 4,
2424 2417
2425 2418 'hg.repogroup.create.false': 0,
2426 2419 'hg.repogroup.create.true': 1,
2427 2420
2428 2421 'hg.usergroup.create.false': 0,
2429 2422 'hg.usergroup.create.true': 1,
2430 2423
2431 2424 'hg.fork.none': 0,
2432 2425 'hg.fork.repository': 1,
2433 2426 'hg.create.none': 0,
2434 2427 'hg.create.repository': 1
2435 2428 }
2436 2429
2437 2430 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2438 2431 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2439 2432 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2440 2433
2441 2434 def __unicode__(self):
2442 2435 return u"<%s('%s:%s')>" % (
2443 2436 self.__class__.__name__, self.permission_id, self.permission_name
2444 2437 )
2445 2438
2446 2439 @classmethod
2447 2440 def get_by_key(cls, key):
2448 2441 return cls.query().filter(cls.permission_name == key).scalar()
2449 2442
2450 2443 @classmethod
2451 2444 def get_default_repo_perms(cls, user_id, repo_id=None):
2452 2445 q = Session().query(UserRepoToPerm, Repository, Permission)\
2453 2446 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2454 2447 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2455 2448 .filter(UserRepoToPerm.user_id == user_id)
2456 2449 if repo_id:
2457 2450 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2458 2451 return q.all()
2459 2452
2460 2453 @classmethod
2461 2454 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2462 2455 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2463 2456 .join(
2464 2457 Permission,
2465 2458 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2466 2459 .join(
2467 2460 Repository,
2468 2461 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2469 2462 .join(
2470 2463 UserGroup,
2471 2464 UserGroupRepoToPerm.users_group_id ==
2472 2465 UserGroup.users_group_id)\
2473 2466 .join(
2474 2467 UserGroupMember,
2475 2468 UserGroupRepoToPerm.users_group_id ==
2476 2469 UserGroupMember.users_group_id)\
2477 2470 .filter(
2478 2471 UserGroupMember.user_id == user_id,
2479 2472 UserGroup.users_group_active == true())
2480 2473 if repo_id:
2481 2474 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2482 2475 return q.all()
2483 2476
2484 2477 @classmethod
2485 2478 def get_default_group_perms(cls, user_id, repo_group_id=None):
2486 2479 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2487 2480 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2488 2481 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2489 2482 .filter(UserRepoGroupToPerm.user_id == user_id)
2490 2483 if repo_group_id:
2491 2484 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2492 2485 return q.all()
2493 2486
2494 2487 @classmethod
2495 2488 def get_default_group_perms_from_user_group(
2496 2489 cls, user_id, repo_group_id=None):
2497 2490 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2498 2491 .join(
2499 2492 Permission,
2500 2493 UserGroupRepoGroupToPerm.permission_id ==
2501 2494 Permission.permission_id)\
2502 2495 .join(
2503 2496 RepoGroup,
2504 2497 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2505 2498 .join(
2506 2499 UserGroup,
2507 2500 UserGroupRepoGroupToPerm.users_group_id ==
2508 2501 UserGroup.users_group_id)\
2509 2502 .join(
2510 2503 UserGroupMember,
2511 2504 UserGroupRepoGroupToPerm.users_group_id ==
2512 2505 UserGroupMember.users_group_id)\
2513 2506 .filter(
2514 2507 UserGroupMember.user_id == user_id,
2515 2508 UserGroup.users_group_active == true())
2516 2509 if repo_group_id:
2517 2510 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2518 2511 return q.all()
2519 2512
2520 2513 @classmethod
2521 2514 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2522 2515 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2523 2516 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2524 2517 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2525 2518 .filter(UserUserGroupToPerm.user_id == user_id)
2526 2519 if user_group_id:
2527 2520 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2528 2521 return q.all()
2529 2522
2530 2523 @classmethod
2531 2524 def get_default_user_group_perms_from_user_group(
2532 2525 cls, user_id, user_group_id=None):
2533 2526 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2534 2527 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2535 2528 .join(
2536 2529 Permission,
2537 2530 UserGroupUserGroupToPerm.permission_id ==
2538 2531 Permission.permission_id)\
2539 2532 .join(
2540 2533 TargetUserGroup,
2541 2534 UserGroupUserGroupToPerm.target_user_group_id ==
2542 2535 TargetUserGroup.users_group_id)\
2543 2536 .join(
2544 2537 UserGroup,
2545 2538 UserGroupUserGroupToPerm.user_group_id ==
2546 2539 UserGroup.users_group_id)\
2547 2540 .join(
2548 2541 UserGroupMember,
2549 2542 UserGroupUserGroupToPerm.user_group_id ==
2550 2543 UserGroupMember.users_group_id)\
2551 2544 .filter(
2552 2545 UserGroupMember.user_id == user_id,
2553 2546 UserGroup.users_group_active == true())
2554 2547 if user_group_id:
2555 2548 q = q.filter(
2556 2549 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2557 2550
2558 2551 return q.all()
2559 2552
2560 2553
2561 2554 class UserRepoToPerm(Base, BaseModel):
2562 2555 __tablename__ = 'repo_to_perm'
2563 2556 __table_args__ = (
2564 2557 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2565 2558 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2566 2559 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2567 2560 )
2568 2561 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2569 2562 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2570 2563 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2571 2564 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2572 2565
2573 2566 user = relationship('User')
2574 2567 repository = relationship('Repository')
2575 2568 permission = relationship('Permission')
2576 2569
2577 2570 @classmethod
2578 2571 def create(cls, user, repository, permission):
2579 2572 n = cls()
2580 2573 n.user = user
2581 2574 n.repository = repository
2582 2575 n.permission = permission
2583 2576 Session().add(n)
2584 2577 return n
2585 2578
2586 2579 def __unicode__(self):
2587 2580 return u'<%s => %s >' % (self.user, self.repository)
2588 2581
2589 2582
2590 2583 class UserUserGroupToPerm(Base, BaseModel):
2591 2584 __tablename__ = 'user_user_group_to_perm'
2592 2585 __table_args__ = (
2593 2586 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2594 2587 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2595 2588 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2596 2589 )
2597 2590 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2598 2591 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2599 2592 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2600 2593 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2601 2594
2602 2595 user = relationship('User')
2603 2596 user_group = relationship('UserGroup')
2604 2597 permission = relationship('Permission')
2605 2598
2606 2599 @classmethod
2607 2600 def create(cls, user, user_group, permission):
2608 2601 n = cls()
2609 2602 n.user = user
2610 2603 n.user_group = user_group
2611 2604 n.permission = permission
2612 2605 Session().add(n)
2613 2606 return n
2614 2607
2615 2608 def __unicode__(self):
2616 2609 return u'<%s => %s >' % (self.user, self.user_group)
2617 2610
2618 2611
2619 2612 class UserToPerm(Base, BaseModel):
2620 2613 __tablename__ = 'user_to_perm'
2621 2614 __table_args__ = (
2622 2615 UniqueConstraint('user_id', 'permission_id'),
2623 2616 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2624 2617 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2625 2618 )
2626 2619 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2627 2620 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2628 2621 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2629 2622
2630 2623 user = relationship('User')
2631 2624 permission = relationship('Permission', lazy='joined')
2632 2625
2633 2626 def __unicode__(self):
2634 2627 return u'<%s => %s >' % (self.user, self.permission)
2635 2628
2636 2629
2637 2630 class UserGroupRepoToPerm(Base, BaseModel):
2638 2631 __tablename__ = 'users_group_repo_to_perm'
2639 2632 __table_args__ = (
2640 2633 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2641 2634 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2642 2635 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2643 2636 )
2644 2637 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2645 2638 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2646 2639 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2647 2640 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2648 2641
2649 2642 users_group = relationship('UserGroup')
2650 2643 permission = relationship('Permission')
2651 2644 repository = relationship('Repository')
2652 2645
2653 2646 @classmethod
2654 2647 def create(cls, users_group, repository, permission):
2655 2648 n = cls()
2656 2649 n.users_group = users_group
2657 2650 n.repository = repository
2658 2651 n.permission = permission
2659 2652 Session().add(n)
2660 2653 return n
2661 2654
2662 2655 def __unicode__(self):
2663 2656 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2664 2657
2665 2658
2666 2659 class UserGroupUserGroupToPerm(Base, BaseModel):
2667 2660 __tablename__ = 'user_group_user_group_to_perm'
2668 2661 __table_args__ = (
2669 2662 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2670 2663 CheckConstraint('target_user_group_id != user_group_id'),
2671 2664 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2672 2665 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2673 2666 )
2674 2667 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)
2675 2668 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2676 2669 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2677 2670 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2678 2671
2679 2672 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2680 2673 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2681 2674 permission = relationship('Permission')
2682 2675
2683 2676 @classmethod
2684 2677 def create(cls, target_user_group, user_group, permission):
2685 2678 n = cls()
2686 2679 n.target_user_group = target_user_group
2687 2680 n.user_group = user_group
2688 2681 n.permission = permission
2689 2682 Session().add(n)
2690 2683 return n
2691 2684
2692 2685 def __unicode__(self):
2693 2686 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2694 2687
2695 2688
2696 2689 class UserGroupToPerm(Base, BaseModel):
2697 2690 __tablename__ = 'users_group_to_perm'
2698 2691 __table_args__ = (
2699 2692 UniqueConstraint('users_group_id', 'permission_id',),
2700 2693 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2701 2694 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2702 2695 )
2703 2696 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2704 2697 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2705 2698 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2706 2699
2707 2700 users_group = relationship('UserGroup')
2708 2701 permission = relationship('Permission')
2709 2702
2710 2703
2711 2704 class UserRepoGroupToPerm(Base, BaseModel):
2712 2705 __tablename__ = 'user_repo_group_to_perm'
2713 2706 __table_args__ = (
2714 2707 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2715 2708 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2716 2709 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2717 2710 )
2718 2711
2719 2712 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2720 2713 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2721 2714 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2722 2715 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2723 2716
2724 2717 user = relationship('User')
2725 2718 group = relationship('RepoGroup')
2726 2719 permission = relationship('Permission')
2727 2720
2728 2721 @classmethod
2729 2722 def create(cls, user, repository_group, permission):
2730 2723 n = cls()
2731 2724 n.user = user
2732 2725 n.group = repository_group
2733 2726 n.permission = permission
2734 2727 Session().add(n)
2735 2728 return n
2736 2729
2737 2730
2738 2731 class UserGroupRepoGroupToPerm(Base, BaseModel):
2739 2732 __tablename__ = 'users_group_repo_group_to_perm'
2740 2733 __table_args__ = (
2741 2734 UniqueConstraint('users_group_id', 'group_id'),
2742 2735 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2743 2736 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2744 2737 )
2745 2738
2746 2739 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)
2747 2740 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2748 2741 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2749 2742 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2750 2743
2751 2744 users_group = relationship('UserGroup')
2752 2745 permission = relationship('Permission')
2753 2746 group = relationship('RepoGroup')
2754 2747
2755 2748 @classmethod
2756 2749 def create(cls, user_group, repository_group, permission):
2757 2750 n = cls()
2758 2751 n.users_group = user_group
2759 2752 n.group = repository_group
2760 2753 n.permission = permission
2761 2754 Session().add(n)
2762 2755 return n
2763 2756
2764 2757 def __unicode__(self):
2765 2758 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2766 2759
2767 2760
2768 2761 class Statistics(Base, BaseModel):
2769 2762 __tablename__ = 'statistics'
2770 2763 __table_args__ = (
2771 2764 UniqueConstraint('repository_id'),
2772 2765 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2773 2766 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2774 2767 )
2775 2768 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2776 2769 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2777 2770 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2778 2771 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2779 2772 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2780 2773 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2781 2774
2782 2775 repository = relationship('Repository', single_parent=True)
2783 2776
2784 2777
2785 2778 class UserFollowing(Base, BaseModel):
2786 2779 __tablename__ = 'user_followings'
2787 2780 __table_args__ = (
2788 2781 UniqueConstraint('user_id', 'follows_repository_id'),
2789 2782 UniqueConstraint('user_id', 'follows_user_id'),
2790 2783 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2791 2784 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2792 2785 )
2793 2786
2794 2787 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2795 2788 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2796 2789 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2797 2790 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2798 2791 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2799 2792
2800 2793 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2801 2794
2802 2795 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2803 2796 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2804 2797
2805 2798 @classmethod
2806 2799 def get_repo_followers(cls, repo_id):
2807 2800 return cls.query().filter(cls.follows_repo_id == repo_id)
2808 2801
2809 2802
2810 2803 class CacheKey(Base, BaseModel):
2811 2804 __tablename__ = 'cache_invalidation'
2812 2805 __table_args__ = (
2813 2806 UniqueConstraint('cache_key'),
2814 2807 Index('key_idx', 'cache_key'),
2815 2808 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2816 2809 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2817 2810 )
2818 2811 CACHE_TYPE_ATOM = 'ATOM'
2819 2812 CACHE_TYPE_RSS = 'RSS'
2820 2813 CACHE_TYPE_README = 'README'
2821 2814
2822 2815 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2823 2816 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2824 2817 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2825 2818 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2826 2819
2827 2820 def __init__(self, cache_key, cache_args=''):
2828 2821 self.cache_key = cache_key
2829 2822 self.cache_args = cache_args
2830 2823 self.cache_active = False
2831 2824
2832 2825 def __unicode__(self):
2833 2826 return u"<%s('%s:%s[%s]')>" % (
2834 2827 self.__class__.__name__,
2835 2828 self.cache_id, self.cache_key, self.cache_active)
2836 2829
2837 2830 def _cache_key_partition(self):
2838 2831 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2839 2832 return prefix, repo_name, suffix
2840 2833
2841 2834 def get_prefix(self):
2842 2835 """
2843 2836 Try to extract prefix from existing cache key. The key could consist
2844 2837 of prefix, repo_name, suffix
2845 2838 """
2846 2839 # this returns prefix, repo_name, suffix
2847 2840 return self._cache_key_partition()[0]
2848 2841
2849 2842 def get_suffix(self):
2850 2843 """
2851 2844 get suffix that might have been used in _get_cache_key to
2852 2845 generate self.cache_key. Only used for informational purposes
2853 2846 in repo_edit.mako.
2854 2847 """
2855 2848 # prefix, repo_name, suffix
2856 2849 return self._cache_key_partition()[2]
2857 2850
2858 2851 @classmethod
2859 2852 def delete_all_cache(cls):
2860 2853 """
2861 2854 Delete all cache keys from database.
2862 2855 Should only be run when all instances are down and all entries
2863 2856 thus stale.
2864 2857 """
2865 2858 cls.query().delete()
2866 2859 Session().commit()
2867 2860
2868 2861 @classmethod
2869 2862 def get_cache_key(cls, repo_name, cache_type):
2870 2863 """
2871 2864
2872 2865 Generate a cache key for this process of RhodeCode instance.
2873 2866 Prefix most likely will be process id or maybe explicitly set
2874 2867 instance_id from .ini file.
2875 2868 """
2876 2869 import rhodecode
2877 2870 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2878 2871
2879 2872 repo_as_unicode = safe_unicode(repo_name)
2880 2873 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2881 2874 if cache_type else repo_as_unicode
2882 2875
2883 2876 return u'{}{}'.format(prefix, key)
2884 2877
2885 2878 @classmethod
2886 2879 def set_invalidate(cls, repo_name, delete=False):
2887 2880 """
2888 2881 Mark all caches of a repo as invalid in the database.
2889 2882 """
2890 2883
2891 2884 try:
2892 2885 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2893 2886 if delete:
2894 2887 log.debug('cache objects deleted for repo %s',
2895 2888 safe_str(repo_name))
2896 2889 qry.delete()
2897 2890 else:
2898 2891 log.debug('cache objects marked as invalid for repo %s',
2899 2892 safe_str(repo_name))
2900 2893 qry.update({"cache_active": False})
2901 2894
2902 2895 Session().commit()
2903 2896 except Exception:
2904 2897 log.exception(
2905 2898 'Cache key invalidation failed for repository %s',
2906 2899 safe_str(repo_name))
2907 2900 Session().rollback()
2908 2901
2909 2902 @classmethod
2910 2903 def get_active_cache(cls, cache_key):
2911 2904 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2912 2905 if inv_obj:
2913 2906 return inv_obj
2914 2907 return None
2915 2908
2916 2909 @classmethod
2917 2910 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2918 2911 thread_scoped=False):
2919 2912 """
2920 2913 @cache_region('long_term')
2921 2914 def _heavy_calculation(cache_key):
2922 2915 return 'result'
2923 2916
2924 2917 cache_context = CacheKey.repo_context_cache(
2925 2918 _heavy_calculation, repo_name, cache_type)
2926 2919
2927 2920 with cache_context as context:
2928 2921 context.invalidate()
2929 2922 computed = context.compute()
2930 2923
2931 2924 assert computed == 'result'
2932 2925 """
2933 2926 from rhodecode.lib import caches
2934 2927 return caches.InvalidationContext(
2935 2928 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2936 2929
2937 2930
2938 2931 class ChangesetComment(Base, BaseModel):
2939 2932 __tablename__ = 'changeset_comments'
2940 2933 __table_args__ = (
2941 2934 Index('cc_revision_idx', 'revision'),
2942 2935 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2943 2936 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2944 2937 )
2945 2938
2946 2939 COMMENT_OUTDATED = u'comment_outdated'
2947 2940 COMMENT_TYPE_NOTE = u'note'
2948 2941 COMMENT_TYPE_TODO = u'todo'
2949 2942 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
2950 2943
2951 2944 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2952 2945 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2953 2946 revision = Column('revision', String(40), nullable=True)
2954 2947 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2955 2948 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2956 2949 line_no = Column('line_no', Unicode(10), nullable=True)
2957 2950 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2958 2951 f_path = Column('f_path', Unicode(1000), nullable=True)
2959 2952 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2960 2953 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2961 2954 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2962 2955 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2963 2956 renderer = Column('renderer', Unicode(64), nullable=True)
2964 2957 display_state = Column('display_state', Unicode(128), nullable=True)
2965 2958
2966 2959 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
2967 2960 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
2968 2961 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
2969 2962 author = relationship('User', lazy='joined')
2970 2963 repo = relationship('Repository')
2971 2964 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
2972 2965 pull_request = relationship('PullRequest', lazy='joined')
2973 2966 pull_request_version = relationship('PullRequestVersion')
2974 2967
2975 2968 @classmethod
2976 2969 def get_users(cls, revision=None, pull_request_id=None):
2977 2970 """
2978 2971 Returns user associated with this ChangesetComment. ie those
2979 2972 who actually commented
2980 2973
2981 2974 :param cls:
2982 2975 :param revision:
2983 2976 """
2984 2977 q = Session().query(User)\
2985 2978 .join(ChangesetComment.author)
2986 2979 if revision:
2987 2980 q = q.filter(cls.revision == revision)
2988 2981 elif pull_request_id:
2989 2982 q = q.filter(cls.pull_request_id == pull_request_id)
2990 2983 return q.all()
2991 2984
2992 2985 @classmethod
2993 2986 def get_index_from_version(cls, pr_version, versions):
2994 2987 num_versions = [x.pull_request_version_id for x in versions]
2995 2988 try:
2996 2989 return num_versions.index(pr_version) +1
2997 2990 except (IndexError, ValueError):
2998 2991 return
2999 2992
3000 2993 @property
3001 2994 def outdated(self):
3002 2995 return self.display_state == self.COMMENT_OUTDATED
3003 2996
3004 2997 def outdated_at_version(self, version):
3005 2998 """
3006 2999 Checks if comment is outdated for given pull request version
3007 3000 """
3008 3001 return self.outdated and self.pull_request_version_id != version
3009 3002
3010 3003 def older_than_version(self, version):
3011 3004 """
3012 3005 Checks if comment is made from previous version than given
3013 3006 """
3014 3007 if version is None:
3015 3008 return self.pull_request_version_id is not None
3016 3009
3017 3010 return self.pull_request_version_id < version
3018 3011
3019 3012 @property
3020 3013 def resolved(self):
3021 3014 return self.resolved_by[0] if self.resolved_by else None
3022 3015
3023 3016 @property
3024 3017 def is_todo(self):
3025 3018 return self.comment_type == self.COMMENT_TYPE_TODO
3026 3019
3027 3020 def get_index_version(self, versions):
3028 3021 return self.get_index_from_version(
3029 3022 self.pull_request_version_id, versions)
3030 3023
3031 3024 def render(self, mentions=False):
3032 3025 from rhodecode.lib import helpers as h
3033 3026 return h.render(self.text, renderer=self.renderer, mentions=mentions)
3034 3027
3035 3028 def __repr__(self):
3036 3029 if self.comment_id:
3037 3030 return '<DB:Comment #%s>' % self.comment_id
3038 3031 else:
3039 3032 return '<DB:Comment at %#x>' % id(self)
3040 3033
3041 3034
3042 3035 class ChangesetStatus(Base, BaseModel):
3043 3036 __tablename__ = 'changeset_statuses'
3044 3037 __table_args__ = (
3045 3038 Index('cs_revision_idx', 'revision'),
3046 3039 Index('cs_version_idx', 'version'),
3047 3040 UniqueConstraint('repo_id', 'revision', 'version'),
3048 3041 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3049 3042 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3050 3043 )
3051 3044 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3052 3045 STATUS_APPROVED = 'approved'
3053 3046 STATUS_REJECTED = 'rejected'
3054 3047 STATUS_UNDER_REVIEW = 'under_review'
3055 3048
3056 3049 STATUSES = [
3057 3050 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3058 3051 (STATUS_APPROVED, _("Approved")),
3059 3052 (STATUS_REJECTED, _("Rejected")),
3060 3053 (STATUS_UNDER_REVIEW, _("Under Review")),
3061 3054 ]
3062 3055
3063 3056 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3064 3057 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3065 3058 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3066 3059 revision = Column('revision', String(40), nullable=False)
3067 3060 status = Column('status', String(128), nullable=False, default=DEFAULT)
3068 3061 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3069 3062 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3070 3063 version = Column('version', Integer(), nullable=False, default=0)
3071 3064 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3072 3065
3073 3066 author = relationship('User', lazy='joined')
3074 3067 repo = relationship('Repository')
3075 3068 comment = relationship('ChangesetComment', lazy='joined')
3076 3069 pull_request = relationship('PullRequest', lazy='joined')
3077 3070
3078 3071 def __unicode__(self):
3079 3072 return u"<%s('%s[v%s]:%s')>" % (
3080 3073 self.__class__.__name__,
3081 3074 self.status, self.version, self.author
3082 3075 )
3083 3076
3084 3077 @classmethod
3085 3078 def get_status_lbl(cls, value):
3086 3079 return dict(cls.STATUSES).get(value)
3087 3080
3088 3081 @property
3089 3082 def status_lbl(self):
3090 3083 return ChangesetStatus.get_status_lbl(self.status)
3091 3084
3092 3085
3093 3086 class _PullRequestBase(BaseModel):
3094 3087 """
3095 3088 Common attributes of pull request and version entries.
3096 3089 """
3097 3090
3098 3091 # .status values
3099 3092 STATUS_NEW = u'new'
3100 3093 STATUS_OPEN = u'open'
3101 3094 STATUS_CLOSED = u'closed'
3102 3095
3103 3096 title = Column('title', Unicode(255), nullable=True)
3104 3097 description = Column(
3105 3098 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3106 3099 nullable=True)
3107 3100 # new/open/closed status of pull request (not approve/reject/etc)
3108 3101 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3109 3102 created_on = Column(
3110 3103 'created_on', DateTime(timezone=False), nullable=False,
3111 3104 default=datetime.datetime.now)
3112 3105 updated_on = Column(
3113 3106 'updated_on', DateTime(timezone=False), nullable=False,
3114 3107 default=datetime.datetime.now)
3115 3108
3116 3109 @declared_attr
3117 3110 def user_id(cls):
3118 3111 return Column(
3119 3112 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3120 3113 unique=None)
3121 3114
3122 3115 # 500 revisions max
3123 3116 _revisions = Column(
3124 3117 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3125 3118
3126 3119 @declared_attr
3127 3120 def source_repo_id(cls):
3128 3121 # TODO: dan: rename column to source_repo_id
3129 3122 return Column(
3130 3123 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3131 3124 nullable=False)
3132 3125
3133 3126 source_ref = Column('org_ref', Unicode(255), nullable=False)
3134 3127
3135 3128 @declared_attr
3136 3129 def target_repo_id(cls):
3137 3130 # TODO: dan: rename column to target_repo_id
3138 3131 return Column(
3139 3132 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3140 3133 nullable=False)
3141 3134
3142 3135 target_ref = Column('other_ref', Unicode(255), nullable=False)
3143 3136 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3144 3137
3145 3138 # TODO: dan: rename column to last_merge_source_rev
3146 3139 _last_merge_source_rev = Column(
3147 3140 'last_merge_org_rev', String(40), nullable=True)
3148 3141 # TODO: dan: rename column to last_merge_target_rev
3149 3142 _last_merge_target_rev = Column(
3150 3143 'last_merge_other_rev', String(40), nullable=True)
3151 3144 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3152 3145 merge_rev = Column('merge_rev', String(40), nullable=True)
3153 3146
3154 3147 @hybrid_property
3155 3148 def revisions(self):
3156 3149 return self._revisions.split(':') if self._revisions else []
3157 3150
3158 3151 @revisions.setter
3159 3152 def revisions(self, val):
3160 3153 self._revisions = ':'.join(val)
3161 3154
3162 3155 @declared_attr
3163 3156 def author(cls):
3164 3157 return relationship('User', lazy='joined')
3165 3158
3166 3159 @declared_attr
3167 3160 def source_repo(cls):
3168 3161 return relationship(
3169 3162 'Repository',
3170 3163 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3171 3164
3172 3165 @property
3173 3166 def source_ref_parts(self):
3174 3167 return self.unicode_to_reference(self.source_ref)
3175 3168
3176 3169 @declared_attr
3177 3170 def target_repo(cls):
3178 3171 return relationship(
3179 3172 'Repository',
3180 3173 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3181 3174
3182 3175 @property
3183 3176 def target_ref_parts(self):
3184 3177 return self.unicode_to_reference(self.target_ref)
3185 3178
3186 3179 @property
3187 3180 def shadow_merge_ref(self):
3188 3181 return self.unicode_to_reference(self._shadow_merge_ref)
3189 3182
3190 3183 @shadow_merge_ref.setter
3191 3184 def shadow_merge_ref(self, ref):
3192 3185 self._shadow_merge_ref = self.reference_to_unicode(ref)
3193 3186
3194 3187 def unicode_to_reference(self, raw):
3195 3188 """
3196 3189 Convert a unicode (or string) to a reference object.
3197 3190 If unicode evaluates to False it returns None.
3198 3191 """
3199 3192 if raw:
3200 3193 refs = raw.split(':')
3201 3194 return Reference(*refs)
3202 3195 else:
3203 3196 return None
3204 3197
3205 3198 def reference_to_unicode(self, ref):
3206 3199 """
3207 3200 Convert a reference object to unicode.
3208 3201 If reference is None it returns None.
3209 3202 """
3210 3203 if ref:
3211 3204 return u':'.join(ref)
3212 3205 else:
3213 3206 return None
3214 3207
3215 3208 def get_api_data(self):
3216 3209 from rhodecode.model.pull_request import PullRequestModel
3217 3210 pull_request = self
3218 3211 merge_status = PullRequestModel().merge_status(pull_request)
3219 3212
3220 3213 pull_request_url = url(
3221 3214 'pullrequest_show', repo_name=self.target_repo.repo_name,
3222 3215 pull_request_id=self.pull_request_id, qualified=True)
3223 3216
3224 3217 merge_data = {
3225 3218 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3226 3219 'reference': (
3227 3220 pull_request.shadow_merge_ref._asdict()
3228 3221 if pull_request.shadow_merge_ref else None),
3229 3222 }
3230 3223
3231 3224 data = {
3232 3225 'pull_request_id': pull_request.pull_request_id,
3233 3226 'url': pull_request_url,
3234 3227 'title': pull_request.title,
3235 3228 'description': pull_request.description,
3236 3229 'status': pull_request.status,
3237 3230 'created_on': pull_request.created_on,
3238 3231 'updated_on': pull_request.updated_on,
3239 3232 'commit_ids': pull_request.revisions,
3240 3233 'review_status': pull_request.calculated_review_status(),
3241 3234 'mergeable': {
3242 3235 'status': merge_status[0],
3243 3236 'message': unicode(merge_status[1]),
3244 3237 },
3245 3238 'source': {
3246 3239 'clone_url': pull_request.source_repo.clone_url(),
3247 3240 'repository': pull_request.source_repo.repo_name,
3248 3241 'reference': {
3249 3242 'name': pull_request.source_ref_parts.name,
3250 3243 'type': pull_request.source_ref_parts.type,
3251 3244 'commit_id': pull_request.source_ref_parts.commit_id,
3252 3245 },
3253 3246 },
3254 3247 'target': {
3255 3248 'clone_url': pull_request.target_repo.clone_url(),
3256 3249 'repository': pull_request.target_repo.repo_name,
3257 3250 'reference': {
3258 3251 'name': pull_request.target_ref_parts.name,
3259 3252 'type': pull_request.target_ref_parts.type,
3260 3253 'commit_id': pull_request.target_ref_parts.commit_id,
3261 3254 },
3262 3255 },
3263 3256 'merge': merge_data,
3264 3257 'author': pull_request.author.get_api_data(include_secrets=False,
3265 3258 details='basic'),
3266 3259 'reviewers': [
3267 3260 {
3268 3261 'user': reviewer.get_api_data(include_secrets=False,
3269 3262 details='basic'),
3270 3263 'reasons': reasons,
3271 3264 'review_status': st[0][1].status if st else 'not_reviewed',
3272 3265 }
3273 3266 for reviewer, reasons, st in pull_request.reviewers_statuses()
3274 3267 ]
3275 3268 }
3276 3269
3277 3270 return data
3278 3271
3279 3272
3280 3273 class PullRequest(Base, _PullRequestBase):
3281 3274 __tablename__ = 'pull_requests'
3282 3275 __table_args__ = (
3283 3276 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3284 3277 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3285 3278 )
3286 3279
3287 3280 pull_request_id = Column(
3288 3281 'pull_request_id', Integer(), nullable=False, primary_key=True)
3289 3282
3290 3283 def __repr__(self):
3291 3284 if self.pull_request_id:
3292 3285 return '<DB:PullRequest #%s>' % self.pull_request_id
3293 3286 else:
3294 3287 return '<DB:PullRequest at %#x>' % id(self)
3295 3288
3296 3289 reviewers = relationship('PullRequestReviewers',
3297 3290 cascade="all, delete, delete-orphan")
3298 3291 statuses = relationship('ChangesetStatus')
3299 3292 comments = relationship('ChangesetComment',
3300 3293 cascade="all, delete, delete-orphan")
3301 3294 versions = relationship('PullRequestVersion',
3302 3295 cascade="all, delete, delete-orphan",
3303 3296 lazy='dynamic')
3304 3297
3305 3298 @classmethod
3306 3299 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3307 3300 internal_methods=None):
3308 3301
3309 3302 class PullRequestDisplay(object):
3310 3303 """
3311 3304 Special object wrapper for showing PullRequest data via Versions
3312 3305 It mimics PR object as close as possible. This is read only object
3313 3306 just for display
3314 3307 """
3315 3308
3316 3309 def __init__(self, attrs, internal=None):
3317 3310 self.attrs = attrs
3318 3311 # internal have priority over the given ones via attrs
3319 3312 self.internal = internal or ['versions']
3320 3313
3321 3314 def __getattr__(self, item):
3322 3315 if item in self.internal:
3323 3316 return getattr(self, item)
3324 3317 try:
3325 3318 return self.attrs[item]
3326 3319 except KeyError:
3327 3320 raise AttributeError(
3328 3321 '%s object has no attribute %s' % (self, item))
3329 3322
3330 3323 def __repr__(self):
3331 3324 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3332 3325
3333 3326 def versions(self):
3334 3327 return pull_request_obj.versions.order_by(
3335 3328 PullRequestVersion.pull_request_version_id).all()
3336 3329
3337 3330 def is_closed(self):
3338 3331 return pull_request_obj.is_closed()
3339 3332
3340 3333 @property
3341 3334 def pull_request_version_id(self):
3342 3335 return getattr(pull_request_obj, 'pull_request_version_id', None)
3343 3336
3344 3337 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3345 3338
3346 3339 attrs.author = StrictAttributeDict(
3347 3340 pull_request_obj.author.get_api_data())
3348 3341 if pull_request_obj.target_repo:
3349 3342 attrs.target_repo = StrictAttributeDict(
3350 3343 pull_request_obj.target_repo.get_api_data())
3351 3344 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3352 3345
3353 3346 if pull_request_obj.source_repo:
3354 3347 attrs.source_repo = StrictAttributeDict(
3355 3348 pull_request_obj.source_repo.get_api_data())
3356 3349 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3357 3350
3358 3351 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3359 3352 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3360 3353 attrs.revisions = pull_request_obj.revisions
3361 3354
3362 3355 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3363 3356
3364 3357 return PullRequestDisplay(attrs, internal=internal_methods)
3365 3358
3366 3359 def is_closed(self):
3367 3360 return self.status == self.STATUS_CLOSED
3368 3361
3369 3362 def __json__(self):
3370 3363 return {
3371 3364 'revisions': self.revisions,
3372 3365 }
3373 3366
3374 3367 def calculated_review_status(self):
3375 3368 from rhodecode.model.changeset_status import ChangesetStatusModel
3376 3369 return ChangesetStatusModel().calculated_review_status(self)
3377 3370
3378 3371 def reviewers_statuses(self):
3379 3372 from rhodecode.model.changeset_status import ChangesetStatusModel
3380 3373 return ChangesetStatusModel().reviewers_statuses(self)
3381 3374
3382 3375 @property
3383 3376 def workspace_id(self):
3384 3377 from rhodecode.model.pull_request import PullRequestModel
3385 3378 return PullRequestModel()._workspace_id(self)
3386 3379
3387 3380 def get_shadow_repo(self):
3388 3381 workspace_id = self.workspace_id
3389 3382 vcs_obj = self.target_repo.scm_instance()
3390 3383 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3391 3384 workspace_id)
3392 3385 return vcs_obj._get_shadow_instance(shadow_repository_path)
3393 3386
3394 3387
3395 3388 class PullRequestVersion(Base, _PullRequestBase):
3396 3389 __tablename__ = 'pull_request_versions'
3397 3390 __table_args__ = (
3398 3391 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3399 3392 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3400 3393 )
3401 3394
3402 3395 pull_request_version_id = Column(
3403 3396 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3404 3397 pull_request_id = Column(
3405 3398 'pull_request_id', Integer(),
3406 3399 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3407 3400 pull_request = relationship('PullRequest')
3408 3401
3409 3402 def __repr__(self):
3410 3403 if self.pull_request_version_id:
3411 3404 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3412 3405 else:
3413 3406 return '<DB:PullRequestVersion at %#x>' % id(self)
3414 3407
3415 3408 @property
3416 3409 def reviewers(self):
3417 3410 return self.pull_request.reviewers
3418 3411
3419 3412 @property
3420 3413 def versions(self):
3421 3414 return self.pull_request.versions
3422 3415
3423 3416 def is_closed(self):
3424 3417 # calculate from original
3425 3418 return self.pull_request.status == self.STATUS_CLOSED
3426 3419
3427 3420 def calculated_review_status(self):
3428 3421 return self.pull_request.calculated_review_status()
3429 3422
3430 3423 def reviewers_statuses(self):
3431 3424 return self.pull_request.reviewers_statuses()
3432 3425
3433 3426
3434 3427 class PullRequestReviewers(Base, BaseModel):
3435 3428 __tablename__ = 'pull_request_reviewers'
3436 3429 __table_args__ = (
3437 3430 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3438 3431 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3439 3432 )
3440 3433
3441 3434 def __init__(self, user=None, pull_request=None, reasons=None):
3442 3435 self.user = user
3443 3436 self.pull_request = pull_request
3444 3437 self.reasons = reasons or []
3445 3438
3446 3439 @hybrid_property
3447 3440 def reasons(self):
3448 3441 if not self._reasons:
3449 3442 return []
3450 3443 return self._reasons
3451 3444
3452 3445 @reasons.setter
3453 3446 def reasons(self, val):
3454 3447 val = val or []
3455 3448 if any(not isinstance(x, basestring) for x in val):
3456 3449 raise Exception('invalid reasons type, must be list of strings')
3457 3450 self._reasons = val
3458 3451
3459 3452 pull_requests_reviewers_id = Column(
3460 3453 'pull_requests_reviewers_id', Integer(), nullable=False,
3461 3454 primary_key=True)
3462 3455 pull_request_id = Column(
3463 3456 "pull_request_id", Integer(),
3464 3457 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3465 3458 user_id = Column(
3466 3459 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3467 3460 _reasons = Column(
3468 3461 'reason', MutationList.as_mutable(
3469 3462 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3470 3463
3471 3464 user = relationship('User')
3472 3465 pull_request = relationship('PullRequest')
3473 3466
3474 3467
3475 3468 class Notification(Base, BaseModel):
3476 3469 __tablename__ = 'notifications'
3477 3470 __table_args__ = (
3478 3471 Index('notification_type_idx', 'type'),
3479 3472 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3480 3473 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3481 3474 )
3482 3475
3483 3476 TYPE_CHANGESET_COMMENT = u'cs_comment'
3484 3477 TYPE_MESSAGE = u'message'
3485 3478 TYPE_MENTION = u'mention'
3486 3479 TYPE_REGISTRATION = u'registration'
3487 3480 TYPE_PULL_REQUEST = u'pull_request'
3488 3481 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3489 3482
3490 3483 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3491 3484 subject = Column('subject', Unicode(512), nullable=True)
3492 3485 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3493 3486 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3494 3487 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3495 3488 type_ = Column('type', Unicode(255))
3496 3489
3497 3490 created_by_user = relationship('User')
3498 3491 notifications_to_users = relationship('UserNotification', lazy='joined',
3499 3492 cascade="all, delete, delete-orphan")
3500 3493
3501 3494 @property
3502 3495 def recipients(self):
3503 3496 return [x.user for x in UserNotification.query()\
3504 3497 .filter(UserNotification.notification == self)\
3505 3498 .order_by(UserNotification.user_id.asc()).all()]
3506 3499
3507 3500 @classmethod
3508 3501 def create(cls, created_by, subject, body, recipients, type_=None):
3509 3502 if type_ is None:
3510 3503 type_ = Notification.TYPE_MESSAGE
3511 3504
3512 3505 notification = cls()
3513 3506 notification.created_by_user = created_by
3514 3507 notification.subject = subject
3515 3508 notification.body = body
3516 3509 notification.type_ = type_
3517 3510 notification.created_on = datetime.datetime.now()
3518 3511
3519 3512 for u in recipients:
3520 3513 assoc = UserNotification()
3521 3514 assoc.notification = notification
3522 3515
3523 3516 # if created_by is inside recipients mark his notification
3524 3517 # as read
3525 3518 if u.user_id == created_by.user_id:
3526 3519 assoc.read = True
3527 3520
3528 3521 u.notifications.append(assoc)
3529 3522 Session().add(notification)
3530 3523
3531 3524 return notification
3532 3525
3533 3526 @property
3534 3527 def description(self):
3535 3528 from rhodecode.model.notification import NotificationModel
3536 3529 return NotificationModel().make_description(self)
3537 3530
3538 3531
3539 3532 class UserNotification(Base, BaseModel):
3540 3533 __tablename__ = 'user_to_notification'
3541 3534 __table_args__ = (
3542 3535 UniqueConstraint('user_id', 'notification_id'),
3543 3536 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3544 3537 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3545 3538 )
3546 3539 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3547 3540 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3548 3541 read = Column('read', Boolean, default=False)
3549 3542 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3550 3543
3551 3544 user = relationship('User', lazy="joined")
3552 3545 notification = relationship('Notification', lazy="joined",
3553 3546 order_by=lambda: Notification.created_on.desc(),)
3554 3547
3555 3548 def mark_as_read(self):
3556 3549 self.read = True
3557 3550 Session().add(self)
3558 3551
3559 3552
3560 3553 class Gist(Base, BaseModel):
3561 3554 __tablename__ = 'gists'
3562 3555 __table_args__ = (
3563 3556 Index('g_gist_access_id_idx', 'gist_access_id'),
3564 3557 Index('g_created_on_idx', 'created_on'),
3565 3558 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3566 3559 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3567 3560 )
3568 3561 GIST_PUBLIC = u'public'
3569 3562 GIST_PRIVATE = u'private'
3570 3563 DEFAULT_FILENAME = u'gistfile1.txt'
3571 3564
3572 3565 ACL_LEVEL_PUBLIC = u'acl_public'
3573 3566 ACL_LEVEL_PRIVATE = u'acl_private'
3574 3567
3575 3568 gist_id = Column('gist_id', Integer(), primary_key=True)
3576 3569 gist_access_id = Column('gist_access_id', Unicode(250))
3577 3570 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3578 3571 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3579 3572 gist_expires = Column('gist_expires', Float(53), nullable=False)
3580 3573 gist_type = Column('gist_type', Unicode(128), nullable=False)
3581 3574 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3582 3575 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3583 3576 acl_level = Column('acl_level', Unicode(128), nullable=True)
3584 3577
3585 3578 owner = relationship('User')
3586 3579
3587 3580 def __repr__(self):
3588 3581 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3589 3582
3590 3583 @classmethod
3591 3584 def get_or_404(cls, id_):
3592 3585 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3593 3586 if not res:
3594 3587 raise HTTPNotFound
3595 3588 return res
3596 3589
3597 3590 @classmethod
3598 3591 def get_by_access_id(cls, gist_access_id):
3599 3592 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3600 3593
3601 3594 def gist_url(self):
3602 3595 import rhodecode
3603 3596 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3604 3597 if alias_url:
3605 3598 return alias_url.replace('{gistid}', self.gist_access_id)
3606 3599
3607 3600 return url('gist', gist_id=self.gist_access_id, qualified=True)
3608 3601
3609 3602 @classmethod
3610 3603 def base_path(cls):
3611 3604 """
3612 3605 Returns base path when all gists are stored
3613 3606
3614 3607 :param cls:
3615 3608 """
3616 3609 from rhodecode.model.gist import GIST_STORE_LOC
3617 3610 q = Session().query(RhodeCodeUi)\
3618 3611 .filter(RhodeCodeUi.ui_key == URL_SEP)
3619 3612 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3620 3613 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3621 3614
3622 3615 def get_api_data(self):
3623 3616 """
3624 3617 Common function for generating gist related data for API
3625 3618 """
3626 3619 gist = self
3627 3620 data = {
3628 3621 'gist_id': gist.gist_id,
3629 3622 'type': gist.gist_type,
3630 3623 'access_id': gist.gist_access_id,
3631 3624 'description': gist.gist_description,
3632 3625 'url': gist.gist_url(),
3633 3626 'expires': gist.gist_expires,
3634 3627 'created_on': gist.created_on,
3635 3628 'modified_at': gist.modified_at,
3636 3629 'content': None,
3637 3630 'acl_level': gist.acl_level,
3638 3631 }
3639 3632 return data
3640 3633
3641 3634 def __json__(self):
3642 3635 data = dict(
3643 3636 )
3644 3637 data.update(self.get_api_data())
3645 3638 return data
3646 3639 # SCM functions
3647 3640
3648 3641 def scm_instance(self, **kwargs):
3649 3642 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3650 3643 return get_vcs_instance(
3651 3644 repo_path=safe_str(full_repo_path), create=False)
3652 3645
3653 3646
3654 3647 class ExternalIdentity(Base, BaseModel):
3655 3648 __tablename__ = 'external_identities'
3656 3649 __table_args__ = (
3657 3650 Index('local_user_id_idx', 'local_user_id'),
3658 3651 Index('external_id_idx', 'external_id'),
3659 3652 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3660 3653 'mysql_charset': 'utf8'})
3661 3654
3662 3655 external_id = Column('external_id', Unicode(255), default=u'',
3663 3656 primary_key=True)
3664 3657 external_username = Column('external_username', Unicode(1024), default=u'')
3665 3658 local_user_id = Column('local_user_id', Integer(),
3666 3659 ForeignKey('users.user_id'), primary_key=True)
3667 3660 provider_name = Column('provider_name', Unicode(255), default=u'',
3668 3661 primary_key=True)
3669 3662 access_token = Column('access_token', String(1024), default=u'')
3670 3663 alt_token = Column('alt_token', String(1024), default=u'')
3671 3664 token_secret = Column('token_secret', String(1024), default=u'')
3672 3665
3673 3666 @classmethod
3674 3667 def by_external_id_and_provider(cls, external_id, provider_name,
3675 3668 local_user_id=None):
3676 3669 """
3677 3670 Returns ExternalIdentity instance based on search params
3678 3671
3679 3672 :param external_id:
3680 3673 :param provider_name:
3681 3674 :return: ExternalIdentity
3682 3675 """
3683 3676 query = cls.query()
3684 3677 query = query.filter(cls.external_id == external_id)
3685 3678 query = query.filter(cls.provider_name == provider_name)
3686 3679 if local_user_id:
3687 3680 query = query.filter(cls.local_user_id == local_user_id)
3688 3681 return query.first()
3689 3682
3690 3683 @classmethod
3691 3684 def user_by_external_id_and_provider(cls, external_id, provider_name):
3692 3685 """
3693 3686 Returns User instance based on search params
3694 3687
3695 3688 :param external_id:
3696 3689 :param provider_name:
3697 3690 :return: User
3698 3691 """
3699 3692 query = User.query()
3700 3693 query = query.filter(cls.external_id == external_id)
3701 3694 query = query.filter(cls.provider_name == provider_name)
3702 3695 query = query.filter(User.user_id == cls.local_user_id)
3703 3696 return query.first()
3704 3697
3705 3698 @classmethod
3706 3699 def by_local_user_id(cls, local_user_id):
3707 3700 """
3708 3701 Returns all tokens for user
3709 3702
3710 3703 :param local_user_id:
3711 3704 :return: ExternalIdentity
3712 3705 """
3713 3706 query = cls.query()
3714 3707 query = query.filter(cls.local_user_id == local_user_id)
3715 3708 return query
3716 3709
3717 3710
3718 3711 class Integration(Base, BaseModel):
3719 3712 __tablename__ = 'integrations'
3720 3713 __table_args__ = (
3721 3714 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3722 3715 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3723 3716 )
3724 3717
3725 3718 integration_id = Column('integration_id', Integer(), primary_key=True)
3726 3719 integration_type = Column('integration_type', String(255))
3727 3720 enabled = Column('enabled', Boolean(), nullable=False)
3728 3721 name = Column('name', String(255), nullable=False)
3729 3722 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3730 3723 default=False)
3731 3724
3732 3725 settings = Column(
3733 3726 'settings_json', MutationObj.as_mutable(
3734 3727 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3735 3728 repo_id = Column(
3736 3729 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3737 3730 nullable=True, unique=None, default=None)
3738 3731 repo = relationship('Repository', lazy='joined')
3739 3732
3740 3733 repo_group_id = Column(
3741 3734 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3742 3735 nullable=True, unique=None, default=None)
3743 3736 repo_group = relationship('RepoGroup', lazy='joined')
3744 3737
3745 3738 @property
3746 3739 def scope(self):
3747 3740 if self.repo:
3748 3741 return repr(self.repo)
3749 3742 if self.repo_group:
3750 3743 if self.child_repos_only:
3751 3744 return repr(self.repo_group) + ' (child repos only)'
3752 3745 else:
3753 3746 return repr(self.repo_group) + ' (recursive)'
3754 3747 if self.child_repos_only:
3755 3748 return 'root_repos'
3756 3749 return 'global'
3757 3750
3758 3751 def __repr__(self):
3759 3752 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3760 3753
3761 3754
3762 3755 class RepoReviewRuleUser(Base, BaseModel):
3763 3756 __tablename__ = 'repo_review_rules_users'
3764 3757 __table_args__ = (
3765 3758 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3766 3759 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3767 3760 )
3768 3761 repo_review_rule_user_id = Column(
3769 3762 'repo_review_rule_user_id', Integer(), primary_key=True)
3770 3763 repo_review_rule_id = Column("repo_review_rule_id",
3771 3764 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3772 3765 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3773 3766 nullable=False)
3774 3767 user = relationship('User')
3775 3768
3776 3769
3777 3770 class RepoReviewRuleUserGroup(Base, BaseModel):
3778 3771 __tablename__ = 'repo_review_rules_users_groups'
3779 3772 __table_args__ = (
3780 3773 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3781 3774 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3782 3775 )
3783 3776 repo_review_rule_users_group_id = Column(
3784 3777 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3785 3778 repo_review_rule_id = Column("repo_review_rule_id",
3786 3779 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3787 3780 users_group_id = Column("users_group_id", Integer(),
3788 3781 ForeignKey('users_groups.users_group_id'), nullable=False)
3789 3782 users_group = relationship('UserGroup')
3790 3783
3791 3784
3792 3785 class RepoReviewRule(Base, BaseModel):
3793 3786 __tablename__ = 'repo_review_rules'
3794 3787 __table_args__ = (
3795 3788 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3796 3789 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3797 3790 )
3798 3791
3799 3792 repo_review_rule_id = Column(
3800 3793 'repo_review_rule_id', Integer(), primary_key=True)
3801 3794 repo_id = Column(
3802 3795 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3803 3796 repo = relationship('Repository', backref='review_rules')
3804 3797
3805 3798 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3806 3799 default=u'*') # glob
3807 3800 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3808 3801 default=u'*') # glob
3809 3802
3810 3803 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3811 3804 nullable=False, default=False)
3812 3805 rule_users = relationship('RepoReviewRuleUser')
3813 3806 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3814 3807
3815 3808 @hybrid_property
3816 3809 def branch_pattern(self):
3817 3810 return self._branch_pattern or '*'
3818 3811
3819 3812 def _validate_glob(self, value):
3820 3813 re.compile('^' + glob2re(value) + '$')
3821 3814
3822 3815 @branch_pattern.setter
3823 3816 def branch_pattern(self, value):
3824 3817 self._validate_glob(value)
3825 3818 self._branch_pattern = value or '*'
3826 3819
3827 3820 @hybrid_property
3828 3821 def file_pattern(self):
3829 3822 return self._file_pattern or '*'
3830 3823
3831 3824 @file_pattern.setter
3832 3825 def file_pattern(self, value):
3833 3826 self._validate_glob(value)
3834 3827 self._file_pattern = value or '*'
3835 3828
3836 3829 def matches(self, branch, files_changed):
3837 3830 """
3838 3831 Check if this review rule matches a branch/files in a pull request
3839 3832
3840 3833 :param branch: branch name for the commit
3841 3834 :param files_changed: list of file paths changed in the pull request
3842 3835 """
3843 3836
3844 3837 branch = branch or ''
3845 3838 files_changed = files_changed or []
3846 3839
3847 3840 branch_matches = True
3848 3841 if branch:
3849 3842 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3850 3843 branch_matches = bool(branch_regex.search(branch))
3851 3844
3852 3845 files_matches = True
3853 3846 if self.file_pattern != '*':
3854 3847 files_matches = False
3855 3848 file_regex = re.compile(glob2re(self.file_pattern))
3856 3849 for filename in files_changed:
3857 3850 if file_regex.search(filename):
3858 3851 files_matches = True
3859 3852 break
3860 3853
3861 3854 return branch_matches and files_matches
3862 3855
3863 3856 @property
3864 3857 def review_users(self):
3865 3858 """ Returns the users which this rule applies to """
3866 3859
3867 3860 users = set()
3868 3861 users |= set([
3869 3862 rule_user.user for rule_user in self.rule_users
3870 3863 if rule_user.user.active])
3871 3864 users |= set(
3872 3865 member.user
3873 3866 for rule_user_group in self.rule_user_groups
3874 3867 for member in rule_user_group.users_group.members
3875 3868 if member.user.active
3876 3869 )
3877 3870 return users
3878 3871
3879 3872 def __repr__(self):
3880 3873 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3881 3874 self.repo_review_rule_id, self.repo)
3882 3875
3883 3876
3884 3877 class DbMigrateVersion(Base, BaseModel):
3885 3878 __tablename__ = 'db_migrate_version'
3886 3879 __table_args__ = (
3887 3880 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3888 3881 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3889 3882 )
3890 3883 repository_id = Column('repository_id', String(250), primary_key=True)
3891 3884 repository_path = Column('repository_path', Text)
3892 3885 version = Column('version', Integer)
3893 3886
3894 3887
3895 3888 class DbSession(Base, BaseModel):
3896 3889 __tablename__ = 'db_session'
3897 3890 __table_args__ = (
3898 3891 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3899 3892 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3900 3893 )
3901 3894
3902 3895 def __repr__(self):
3903 3896 return '<DB:DbSession({})>'.format(self.id)
3904 3897
3905 3898 id = Column('id', Integer())
3906 3899 namespace = Column('namespace', String(255), primary_key=True)
3907 3900 accessed = Column('accessed', DateTime, nullable=False)
3908 3901 created = Column('created', DateTime, nullable=False)
3909 3902 data = Column('data', PickleType, nullable=False)
@@ -1,3915 +1,3908 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 hashlib
29 29 import logging
30 30 import datetime
31 31 import warnings
32 32 import ipaddress
33 33 import functools
34 34 import traceback
35 35 import collections
36 36
37 37
38 38 from sqlalchemy import *
39 39 from sqlalchemy.ext.declarative import declared_attr
40 40 from sqlalchemy.ext.hybrid import hybrid_property
41 41 from sqlalchemy.orm import (
42 42 relationship, joinedload, class_mapper, validates, aliased)
43 43 from sqlalchemy.sql.expression import true
44 44 from beaker.cache import cache_region
45 45 from webob.exc import HTTPNotFound
46 46 from zope.cachedescriptors.property import Lazy as LazyProperty
47 47
48 48 from pylons import url
49 49 from pylons.i18n.translation import lazy_ugettext as _
50 50
51 51 from rhodecode.lib.vcs import get_vcs_instance
52 52 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
53 53 from rhodecode.lib.utils2 import (
54 54 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
55 55 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
56 56 glob2re, StrictAttributeDict, cleaned_uri)
57 57 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
58 58 from rhodecode.lib.ext_json import json
59 59 from rhodecode.lib.caching_query import FromCache
60 60 from rhodecode.lib.encrypt import AESCipher
61 61
62 62 from rhodecode.model.meta import Base, Session
63 63
64 64 URL_SEP = '/'
65 65 log = logging.getLogger(__name__)
66 66
67 67 # =============================================================================
68 68 # BASE CLASSES
69 69 # =============================================================================
70 70
71 71 # this is propagated from .ini file rhodecode.encrypted_values.secret or
72 72 # beaker.session.secret if first is not set.
73 73 # and initialized at environment.py
74 74 ENCRYPTION_KEY = None
75 75
76 76 # used to sort permissions by types, '#' used here is not allowed to be in
77 77 # usernames, and it's very early in sorted string.printable table.
78 78 PERMISSION_TYPE_SORT = {
79 79 'admin': '####',
80 80 'write': '###',
81 81 'read': '##',
82 82 'none': '#',
83 83 }
84 84
85 85
86 86 def display_sort(obj):
87 87 """
88 88 Sort function used to sort permissions in .permissions() function of
89 89 Repository, RepoGroup, UserGroup. Also it put the default user in front
90 90 of all other resources
91 91 """
92 92
93 93 if obj.username == User.DEFAULT_USER:
94 94 return '#####'
95 95 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
96 96 return prefix + obj.username
97 97
98 98
99 99 def _hash_key(k):
100 100 return md5_safe(k)
101 101
102 102
103 103 class EncryptedTextValue(TypeDecorator):
104 104 """
105 105 Special column for encrypted long text data, use like::
106 106
107 107 value = Column("encrypted_value", EncryptedValue(), nullable=False)
108 108
109 109 This column is intelligent so if value is in unencrypted form it return
110 110 unencrypted form, but on save it always encrypts
111 111 """
112 112 impl = Text
113 113
114 114 def process_bind_param(self, value, dialect):
115 115 if not value:
116 116 return value
117 117 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
118 118 # protect against double encrypting if someone manually starts
119 119 # doing
120 120 raise ValueError('value needs to be in unencrypted format, ie. '
121 121 'not starting with enc$aes')
122 122 return 'enc$aes_hmac$%s' % AESCipher(
123 123 ENCRYPTION_KEY, hmac=True).encrypt(value)
124 124
125 125 def process_result_value(self, value, dialect):
126 126 import rhodecode
127 127
128 128 if not value:
129 129 return value
130 130
131 131 parts = value.split('$', 3)
132 132 if not len(parts) == 3:
133 133 # probably not encrypted values
134 134 return value
135 135 else:
136 136 if parts[0] != 'enc':
137 137 # parts ok but without our header ?
138 138 return value
139 139 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
140 140 'rhodecode.encrypted_values.strict') or True)
141 141 # at that stage we know it's our encryption
142 142 if parts[1] == 'aes':
143 143 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
144 144 elif parts[1] == 'aes_hmac':
145 145 decrypted_data = AESCipher(
146 146 ENCRYPTION_KEY, hmac=True,
147 147 strict_verification=enc_strict_mode).decrypt(parts[2])
148 148 else:
149 149 raise ValueError(
150 150 'Encryption type part is wrong, must be `aes` '
151 151 'or `aes_hmac`, got `%s` instead' % (parts[1]))
152 152 return decrypted_data
153 153
154 154
155 155 class BaseModel(object):
156 156 """
157 157 Base Model for all classes
158 158 """
159 159
160 160 @classmethod
161 161 def _get_keys(cls):
162 162 """return column names for this model """
163 163 return class_mapper(cls).c.keys()
164 164
165 165 def get_dict(self):
166 166 """
167 167 return dict with keys and values corresponding
168 168 to this model data """
169 169
170 170 d = {}
171 171 for k in self._get_keys():
172 172 d[k] = getattr(self, k)
173 173
174 174 # also use __json__() if present to get additional fields
175 175 _json_attr = getattr(self, '__json__', None)
176 176 if _json_attr:
177 177 # update with attributes from __json__
178 178 if callable(_json_attr):
179 179 _json_attr = _json_attr()
180 180 for k, val in _json_attr.iteritems():
181 181 d[k] = val
182 182 return d
183 183
184 184 def get_appstruct(self):
185 185 """return list with keys and values tuples corresponding
186 186 to this model data """
187 187
188 188 l = []
189 189 for k in self._get_keys():
190 190 l.append((k, getattr(self, k),))
191 191 return l
192 192
193 193 def populate_obj(self, populate_dict):
194 194 """populate model with data from given populate_dict"""
195 195
196 196 for k in self._get_keys():
197 197 if k in populate_dict:
198 198 setattr(self, k, populate_dict[k])
199 199
200 200 @classmethod
201 201 def query(cls):
202 202 return Session().query(cls)
203 203
204 204 @classmethod
205 205 def get(cls, id_):
206 206 if id_:
207 207 return cls.query().get(id_)
208 208
209 209 @classmethod
210 210 def get_or_404(cls, id_):
211 211 try:
212 212 id_ = int(id_)
213 213 except (TypeError, ValueError):
214 214 raise HTTPNotFound
215 215
216 216 res = cls.query().get(id_)
217 217 if not res:
218 218 raise HTTPNotFound
219 219 return res
220 220
221 221 @classmethod
222 222 def getAll(cls):
223 223 # deprecated and left for backward compatibility
224 224 return cls.get_all()
225 225
226 226 @classmethod
227 227 def get_all(cls):
228 228 return cls.query().all()
229 229
230 230 @classmethod
231 231 def delete(cls, id_):
232 232 obj = cls.query().get(id_)
233 233 Session().delete(obj)
234 234
235 235 @classmethod
236 236 def identity_cache(cls, session, attr_name, value):
237 237 exist_in_session = []
238 238 for (item_cls, pkey), instance in session.identity_map.items():
239 239 if cls == item_cls and getattr(instance, attr_name) == value:
240 240 exist_in_session.append(instance)
241 241 if exist_in_session:
242 242 if len(exist_in_session) == 1:
243 243 return exist_in_session[0]
244 244 log.exception(
245 245 'multiple objects with attr %s and '
246 246 'value %s found with same name: %r',
247 247 attr_name, value, exist_in_session)
248 248
249 249 def __repr__(self):
250 250 if hasattr(self, '__unicode__'):
251 251 # python repr needs to return str
252 252 try:
253 253 return safe_str(self.__unicode__())
254 254 except UnicodeDecodeError:
255 255 pass
256 256 return '<DB:%s>' % (self.__class__.__name__)
257 257
258 258
259 259 class RhodeCodeSetting(Base, BaseModel):
260 260 __tablename__ = 'rhodecode_settings'
261 261 __table_args__ = (
262 262 UniqueConstraint('app_settings_name'),
263 263 {'extend_existing': True, 'mysql_engine': 'InnoDB',
264 264 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
265 265 )
266 266
267 267 SETTINGS_TYPES = {
268 268 'str': safe_str,
269 269 'int': safe_int,
270 270 'unicode': safe_unicode,
271 271 'bool': str2bool,
272 272 'list': functools.partial(aslist, sep=',')
273 273 }
274 274 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
275 275 GLOBAL_CONF_KEY = 'app_settings'
276 276
277 277 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
278 278 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
279 279 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
280 280 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
281 281
282 282 def __init__(self, key='', val='', type='unicode'):
283 283 self.app_settings_name = key
284 284 self.app_settings_type = type
285 285 self.app_settings_value = val
286 286
287 287 @validates('_app_settings_value')
288 288 def validate_settings_value(self, key, val):
289 289 assert type(val) == unicode
290 290 return val
291 291
292 292 @hybrid_property
293 293 def app_settings_value(self):
294 294 v = self._app_settings_value
295 295 _type = self.app_settings_type
296 296 if _type:
297 297 _type = self.app_settings_type.split('.')[0]
298 298 # decode the encrypted value
299 299 if 'encrypted' in self.app_settings_type:
300 300 cipher = EncryptedTextValue()
301 301 v = safe_unicode(cipher.process_result_value(v, None))
302 302
303 303 converter = self.SETTINGS_TYPES.get(_type) or \
304 304 self.SETTINGS_TYPES['unicode']
305 305 return converter(v)
306 306
307 307 @app_settings_value.setter
308 308 def app_settings_value(self, val):
309 309 """
310 310 Setter that will always make sure we use unicode in app_settings_value
311 311
312 312 :param val:
313 313 """
314 314 val = safe_unicode(val)
315 315 # encode the encrypted value
316 316 if 'encrypted' in self.app_settings_type:
317 317 cipher = EncryptedTextValue()
318 318 val = safe_unicode(cipher.process_bind_param(val, None))
319 319 self._app_settings_value = val
320 320
321 321 @hybrid_property
322 322 def app_settings_type(self):
323 323 return self._app_settings_type
324 324
325 325 @app_settings_type.setter
326 326 def app_settings_type(self, val):
327 327 if val.split('.')[0] not in self.SETTINGS_TYPES:
328 328 raise Exception('type must be one of %s got %s'
329 329 % (self.SETTINGS_TYPES.keys(), val))
330 330 self._app_settings_type = val
331 331
332 332 def __unicode__(self):
333 333 return u"<%s('%s:%s[%s]')>" % (
334 334 self.__class__.__name__,
335 335 self.app_settings_name, self.app_settings_value,
336 336 self.app_settings_type
337 337 )
338 338
339 339
340 340 class RhodeCodeUi(Base, BaseModel):
341 341 __tablename__ = 'rhodecode_ui'
342 342 __table_args__ = (
343 343 UniqueConstraint('ui_key'),
344 344 {'extend_existing': True, 'mysql_engine': 'InnoDB',
345 345 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
346 346 )
347 347
348 348 HOOK_REPO_SIZE = 'changegroup.repo_size'
349 349 # HG
350 350 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
351 351 HOOK_PULL = 'outgoing.pull_logger'
352 352 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
353 353 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
354 354 HOOK_PUSH = 'changegroup.push_logger'
355 355
356 356 # TODO: johbo: Unify way how hooks are configured for git and hg,
357 357 # git part is currently hardcoded.
358 358
359 359 # SVN PATTERNS
360 360 SVN_BRANCH_ID = 'vcs_svn_branch'
361 361 SVN_TAG_ID = 'vcs_svn_tag'
362 362
363 363 ui_id = Column(
364 364 "ui_id", Integer(), nullable=False, unique=True, default=None,
365 365 primary_key=True)
366 366 ui_section = Column(
367 367 "ui_section", String(255), nullable=True, unique=None, default=None)
368 368 ui_key = Column(
369 369 "ui_key", String(255), nullable=True, unique=None, default=None)
370 370 ui_value = Column(
371 371 "ui_value", String(255), nullable=True, unique=None, default=None)
372 372 ui_active = Column(
373 373 "ui_active", Boolean(), nullable=True, unique=None, default=True)
374 374
375 375 def __repr__(self):
376 376 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
377 377 self.ui_key, self.ui_value)
378 378
379 379
380 380 class RepoRhodeCodeSetting(Base, BaseModel):
381 381 __tablename__ = 'repo_rhodecode_settings'
382 382 __table_args__ = (
383 383 UniqueConstraint(
384 384 'app_settings_name', 'repository_id',
385 385 name='uq_repo_rhodecode_setting_name_repo_id'),
386 386 {'extend_existing': True, 'mysql_engine': 'InnoDB',
387 387 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
388 388 )
389 389
390 390 repository_id = Column(
391 391 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
392 392 nullable=False)
393 393 app_settings_id = Column(
394 394 "app_settings_id", Integer(), nullable=False, unique=True,
395 395 default=None, primary_key=True)
396 396 app_settings_name = Column(
397 397 "app_settings_name", String(255), nullable=True, unique=None,
398 398 default=None)
399 399 _app_settings_value = Column(
400 400 "app_settings_value", String(4096), nullable=True, unique=None,
401 401 default=None)
402 402 _app_settings_type = Column(
403 403 "app_settings_type", String(255), nullable=True, unique=None,
404 404 default=None)
405 405
406 406 repository = relationship('Repository')
407 407
408 408 def __init__(self, repository_id, key='', val='', type='unicode'):
409 409 self.repository_id = repository_id
410 410 self.app_settings_name = key
411 411 self.app_settings_type = type
412 412 self.app_settings_value = val
413 413
414 414 @validates('_app_settings_value')
415 415 def validate_settings_value(self, key, val):
416 416 assert type(val) == unicode
417 417 return val
418 418
419 419 @hybrid_property
420 420 def app_settings_value(self):
421 421 v = self._app_settings_value
422 422 type_ = self.app_settings_type
423 423 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
424 424 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
425 425 return converter(v)
426 426
427 427 @app_settings_value.setter
428 428 def app_settings_value(self, val):
429 429 """
430 430 Setter that will always make sure we use unicode in app_settings_value
431 431
432 432 :param val:
433 433 """
434 434 self._app_settings_value = safe_unicode(val)
435 435
436 436 @hybrid_property
437 437 def app_settings_type(self):
438 438 return self._app_settings_type
439 439
440 440 @app_settings_type.setter
441 441 def app_settings_type(self, val):
442 442 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
443 443 if val not in SETTINGS_TYPES:
444 444 raise Exception('type must be one of %s got %s'
445 445 % (SETTINGS_TYPES.keys(), val))
446 446 self._app_settings_type = val
447 447
448 448 def __unicode__(self):
449 449 return u"<%s('%s:%s:%s[%s]')>" % (
450 450 self.__class__.__name__, self.repository.repo_name,
451 451 self.app_settings_name, self.app_settings_value,
452 452 self.app_settings_type
453 453 )
454 454
455 455
456 456 class RepoRhodeCodeUi(Base, BaseModel):
457 457 __tablename__ = 'repo_rhodecode_ui'
458 458 __table_args__ = (
459 459 UniqueConstraint(
460 460 'repository_id', 'ui_section', 'ui_key',
461 461 name='uq_repo_rhodecode_ui_repository_id_section_key'),
462 462 {'extend_existing': True, 'mysql_engine': 'InnoDB',
463 463 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
464 464 )
465 465
466 466 repository_id = Column(
467 467 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
468 468 nullable=False)
469 469 ui_id = Column(
470 470 "ui_id", Integer(), nullable=False, unique=True, default=None,
471 471 primary_key=True)
472 472 ui_section = Column(
473 473 "ui_section", String(255), nullable=True, unique=None, default=None)
474 474 ui_key = Column(
475 475 "ui_key", String(255), nullable=True, unique=None, default=None)
476 476 ui_value = Column(
477 477 "ui_value", String(255), nullable=True, unique=None, default=None)
478 478 ui_active = Column(
479 479 "ui_active", Boolean(), nullable=True, unique=None, default=True)
480 480
481 481 repository = relationship('Repository')
482 482
483 483 def __repr__(self):
484 484 return '<%s[%s:%s]%s=>%s]>' % (
485 485 self.__class__.__name__, self.repository.repo_name,
486 486 self.ui_section, self.ui_key, self.ui_value)
487 487
488 488
489 489 class User(Base, BaseModel):
490 490 __tablename__ = 'users'
491 491 __table_args__ = (
492 492 UniqueConstraint('username'), UniqueConstraint('email'),
493 493 Index('u_username_idx', 'username'),
494 494 Index('u_email_idx', 'email'),
495 495 {'extend_existing': True, 'mysql_engine': 'InnoDB',
496 496 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
497 497 )
498 498 DEFAULT_USER = 'default'
499 499 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
500 500 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
501 501
502 502 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
503 503 username = Column("username", String(255), nullable=True, unique=None, default=None)
504 504 password = Column("password", String(255), nullable=True, unique=None, default=None)
505 505 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
506 506 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
507 507 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
508 508 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
509 509 _email = Column("email", String(255), nullable=True, unique=None, default=None)
510 510 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
511 511 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
512 512 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
513 513 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
514 514 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
515 515 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
516 516 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
517 517
518 518 user_log = relationship('UserLog')
519 519 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
520 520
521 521 repositories = relationship('Repository')
522 522 repository_groups = relationship('RepoGroup')
523 523 user_groups = relationship('UserGroup')
524 524
525 525 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
526 526 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
527 527
528 528 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
529 529 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
530 530 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
531 531
532 532 group_member = relationship('UserGroupMember', cascade='all')
533 533
534 534 notifications = relationship('UserNotification', cascade='all')
535 535 # notifications assigned to this user
536 536 user_created_notifications = relationship('Notification', cascade='all')
537 537 # comments created by this user
538 538 user_comments = relationship('ChangesetComment', cascade='all')
539 539 # user profile extra info
540 540 user_emails = relationship('UserEmailMap', cascade='all')
541 541 user_ip_map = relationship('UserIpMap', cascade='all')
542 542 user_auth_tokens = relationship('UserApiKeys', cascade='all')
543 543 # gists
544 544 user_gists = relationship('Gist', cascade='all')
545 545 # user pull requests
546 546 user_pull_requests = relationship('PullRequest', cascade='all')
547 547 # external identities
548 548 extenal_identities = relationship(
549 549 'ExternalIdentity',
550 550 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
551 551 cascade='all')
552 552
553 553 def __unicode__(self):
554 554 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
555 555 self.user_id, self.username)
556 556
557 557 @hybrid_property
558 558 def email(self):
559 559 return self._email
560 560
561 561 @email.setter
562 562 def email(self, val):
563 563 self._email = val.lower() if val else None
564 564
565 565 @property
566 566 def firstname(self):
567 567 # alias for future
568 568 return self.name
569 569
570 570 @property
571 571 def emails(self):
572 572 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
573 573 return [self.email] + [x.email for x in other]
574 574
575 575 @property
576 576 def auth_tokens(self):
577 577 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
578 578
579 579 @property
580 580 def extra_auth_tokens(self):
581 581 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
582 582
583 583 @property
584 584 def feed_token(self):
585 585 return self.get_feed_token()
586 586
587 587 def get_feed_token(self):
588 588 feed_tokens = UserApiKeys.query()\
589 589 .filter(UserApiKeys.user == self)\
590 590 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
591 591 .all()
592 592 if feed_tokens:
593 593 return feed_tokens[0].api_key
594 594 return 'NO_FEED_TOKEN_AVAILABLE'
595 595
596 596 @classmethod
597 597 def extra_valid_auth_tokens(cls, user, role=None):
598 598 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
599 599 .filter(or_(UserApiKeys.expires == -1,
600 600 UserApiKeys.expires >= time.time()))
601 601 if role:
602 602 tokens = tokens.filter(or_(UserApiKeys.role == role,
603 603 UserApiKeys.role == UserApiKeys.ROLE_ALL))
604 604 return tokens.all()
605 605
606 606 def authenticate_by_token(self, auth_token, roles=None):
607 607 from rhodecode.lib import auth
608 608
609 609 log.debug('Trying to authenticate user: %s via auth-token, '
610 610 'and roles: %s', self, roles)
611 611
612 612 if not auth_token:
613 613 return False
614 614
615 615 crypto_backend = auth.crypto_backend()
616 616
617 617 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
618 618 tokens_q = UserApiKeys.query()\
619 619 .filter(UserApiKeys.user_id == self.user_id)\
620 620 .filter(or_(UserApiKeys.expires == -1,
621 621 UserApiKeys.expires >= time.time()))
622 622
623 623 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
624 624
625 625 plain_tokens = []
626 626 hash_tokens = []
627 627
628 628 for token in tokens_q.all():
629 629 if token.api_key.startswith(crypto_backend.ENC_PREF):
630 630 hash_tokens.append(token.api_key)
631 631 else:
632 632 plain_tokens.append(token.api_key)
633 633
634 634 is_plain_match = auth_token in plain_tokens
635 635 if is_plain_match:
636 636 return True
637 637
638 638 for hashed in hash_tokens:
639 639 # marcink: this is expensive to calculate, but the most secure
640 640 match = crypto_backend.hash_check(auth_token, hashed)
641 641 if match:
642 642 return True
643 643
644 644 return False
645 645
646 646 @property
647 def builtin_token_roles(self):
648 roles = [
649 UserApiKeys.ROLE_API, UserApiKeys.ROLE_FEED, UserApiKeys.ROLE_HTTP
650 ]
651 return map(UserApiKeys._get_role_name, roles)
652
653 @property
654 647 def ip_addresses(self):
655 648 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
656 649 return [x.ip_addr for x in ret]
657 650
658 651 @property
659 652 def username_and_name(self):
660 653 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
661 654
662 655 @property
663 656 def username_or_name_or_email(self):
664 657 full_name = self.full_name if self.full_name is not ' ' else None
665 658 return self.username or full_name or self.email
666 659
667 660 @property
668 661 def full_name(self):
669 662 return '%s %s' % (self.firstname, self.lastname)
670 663
671 664 @property
672 665 def full_name_or_username(self):
673 666 return ('%s %s' % (self.firstname, self.lastname)
674 667 if (self.firstname and self.lastname) else self.username)
675 668
676 669 @property
677 670 def full_contact(self):
678 671 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
679 672
680 673 @property
681 674 def short_contact(self):
682 675 return '%s %s' % (self.firstname, self.lastname)
683 676
684 677 @property
685 678 def is_admin(self):
686 679 return self.admin
687 680
688 681 @property
689 682 def AuthUser(self):
690 683 """
691 684 Returns instance of AuthUser for this user
692 685 """
693 686 from rhodecode.lib.auth import AuthUser
694 687 return AuthUser(user_id=self.user_id, api_key=self.api_key,
695 688 username=self.username)
696 689
697 690 @hybrid_property
698 691 def user_data(self):
699 692 if not self._user_data:
700 693 return {}
701 694
702 695 try:
703 696 return json.loads(self._user_data)
704 697 except TypeError:
705 698 return {}
706 699
707 700 @user_data.setter
708 701 def user_data(self, val):
709 702 if not isinstance(val, dict):
710 703 raise Exception('user_data must be dict, got %s' % type(val))
711 704 try:
712 705 self._user_data = json.dumps(val)
713 706 except Exception:
714 707 log.error(traceback.format_exc())
715 708
716 709 @classmethod
717 710 def get_by_username(cls, username, case_insensitive=False,
718 711 cache=False, identity_cache=False):
719 712 session = Session()
720 713
721 714 if case_insensitive:
722 715 q = cls.query().filter(
723 716 func.lower(cls.username) == func.lower(username))
724 717 else:
725 718 q = cls.query().filter(cls.username == username)
726 719
727 720 if cache:
728 721 if identity_cache:
729 722 val = cls.identity_cache(session, 'username', username)
730 723 if val:
731 724 return val
732 725 else:
733 726 q = q.options(
734 727 FromCache("sql_cache_short",
735 728 "get_user_by_name_%s" % _hash_key(username)))
736 729
737 730 return q.scalar()
738 731
739 732 @classmethod
740 733 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
741 734 q = cls.query().filter(cls.api_key == auth_token)
742 735
743 736 if cache:
744 737 q = q.options(FromCache("sql_cache_short",
745 738 "get_auth_token_%s" % auth_token))
746 739 res = q.scalar()
747 740
748 741 if fallback and not res:
749 742 #fallback to additional keys
750 743 _res = UserApiKeys.query()\
751 744 .filter(UserApiKeys.api_key == auth_token)\
752 745 .filter(or_(UserApiKeys.expires == -1,
753 746 UserApiKeys.expires >= time.time()))\
754 747 .first()
755 748 if _res:
756 749 res = _res.user
757 750 return res
758 751
759 752 @classmethod
760 753 def get_by_email(cls, email, case_insensitive=False, cache=False):
761 754
762 755 if case_insensitive:
763 756 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
764 757
765 758 else:
766 759 q = cls.query().filter(cls.email == email)
767 760
768 761 if cache:
769 762 q = q.options(FromCache("sql_cache_short",
770 763 "get_email_key_%s" % _hash_key(email)))
771 764
772 765 ret = q.scalar()
773 766 if ret is None:
774 767 q = UserEmailMap.query()
775 768 # try fetching in alternate email map
776 769 if case_insensitive:
777 770 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
778 771 else:
779 772 q = q.filter(UserEmailMap.email == email)
780 773 q = q.options(joinedload(UserEmailMap.user))
781 774 if cache:
782 775 q = q.options(FromCache("sql_cache_short",
783 776 "get_email_map_key_%s" % email))
784 777 ret = getattr(q.scalar(), 'user', None)
785 778
786 779 return ret
787 780
788 781 @classmethod
789 782 def get_from_cs_author(cls, author):
790 783 """
791 784 Tries to get User objects out of commit author string
792 785
793 786 :param author:
794 787 """
795 788 from rhodecode.lib.helpers import email, author_name
796 789 # Valid email in the attribute passed, see if they're in the system
797 790 _email = email(author)
798 791 if _email:
799 792 user = cls.get_by_email(_email, case_insensitive=True)
800 793 if user:
801 794 return user
802 795 # Maybe we can match by username?
803 796 _author = author_name(author)
804 797 user = cls.get_by_username(_author, case_insensitive=True)
805 798 if user:
806 799 return user
807 800
808 801 def update_userdata(self, **kwargs):
809 802 usr = self
810 803 old = usr.user_data
811 804 old.update(**kwargs)
812 805 usr.user_data = old
813 806 Session().add(usr)
814 807 log.debug('updated userdata with ', kwargs)
815 808
816 809 def update_lastlogin(self):
817 810 """Update user lastlogin"""
818 811 self.last_login = datetime.datetime.now()
819 812 Session().add(self)
820 813 log.debug('updated user %s lastlogin', self.username)
821 814
822 815 def update_lastactivity(self):
823 816 """Update user lastactivity"""
824 817 usr = self
825 818 old = usr.user_data
826 819 old.update({'last_activity': time.time()})
827 820 usr.user_data = old
828 821 Session().add(usr)
829 822 log.debug('updated user %s lastactivity', usr.username)
830 823
831 824 def update_password(self, new_password):
832 825 from rhodecode.lib.auth import get_crypt_password
833 826
834 827 self.password = get_crypt_password(new_password)
835 828 Session().add(self)
836 829
837 830 @classmethod
838 831 def get_first_super_admin(cls):
839 832 user = User.query().filter(User.admin == true()).first()
840 833 if user is None:
841 834 raise Exception('FATAL: Missing administrative account!')
842 835 return user
843 836
844 837 @classmethod
845 838 def get_all_super_admins(cls):
846 839 """
847 840 Returns all admin accounts sorted by username
848 841 """
849 842 return User.query().filter(User.admin == true())\
850 843 .order_by(User.username.asc()).all()
851 844
852 845 @classmethod
853 846 def get_default_user(cls, cache=False):
854 847 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
855 848 if user is None:
856 849 raise Exception('FATAL: Missing default account!')
857 850 return user
858 851
859 852 def _get_default_perms(self, user, suffix=''):
860 853 from rhodecode.model.permission import PermissionModel
861 854 return PermissionModel().get_default_perms(user.user_perms, suffix)
862 855
863 856 def get_default_perms(self, suffix=''):
864 857 return self._get_default_perms(self, suffix)
865 858
866 859 def get_api_data(self, include_secrets=False, details='full'):
867 860 """
868 861 Common function for generating user related data for API
869 862
870 863 :param include_secrets: By default secrets in the API data will be replaced
871 864 by a placeholder value to prevent exposing this data by accident. In case
872 865 this data shall be exposed, set this flag to ``True``.
873 866
874 867 :param details: details can be 'basic|full' basic gives only a subset of
875 868 the available user information that includes user_id, name and emails.
876 869 """
877 870 user = self
878 871 user_data = self.user_data
879 872 data = {
880 873 'user_id': user.user_id,
881 874 'username': user.username,
882 875 'firstname': user.name,
883 876 'lastname': user.lastname,
884 877 'email': user.email,
885 878 'emails': user.emails,
886 879 }
887 880 if details == 'basic':
888 881 return data
889 882
890 883 api_key_length = 40
891 884 api_key_replacement = '*' * api_key_length
892 885
893 886 extras = {
894 887 'api_key': api_key_replacement,
895 888 'api_keys': [api_key_replacement],
896 889 'active': user.active,
897 890 'admin': user.admin,
898 891 'extern_type': user.extern_type,
899 892 'extern_name': user.extern_name,
900 893 'last_login': user.last_login,
901 894 'ip_addresses': user.ip_addresses,
902 895 'language': user_data.get('language')
903 896 }
904 897 data.update(extras)
905 898
906 899 if include_secrets:
907 900 data['api_key'] = user.api_key
908 901 data['api_keys'] = user.auth_tokens
909 902 return data
910 903
911 904 def __json__(self):
912 905 data = {
913 906 'full_name': self.full_name,
914 907 'full_name_or_username': self.full_name_or_username,
915 908 'short_contact': self.short_contact,
916 909 'full_contact': self.full_contact,
917 910 }
918 911 data.update(self.get_api_data())
919 912 return data
920 913
921 914
922 915 class UserApiKeys(Base, BaseModel):
923 916 __tablename__ = 'user_api_keys'
924 917 __table_args__ = (
925 918 Index('uak_api_key_idx', 'api_key'),
926 919 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
927 920 UniqueConstraint('api_key'),
928 921 {'extend_existing': True, 'mysql_engine': 'InnoDB',
929 922 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
930 923 )
931 924 __mapper_args__ = {}
932 925
933 926 # ApiKey role
934 927 ROLE_ALL = 'token_role_all'
935 928 ROLE_HTTP = 'token_role_http'
936 929 ROLE_VCS = 'token_role_vcs'
937 930 ROLE_API = 'token_role_api'
938 931 ROLE_FEED = 'token_role_feed'
939 932 ROLE_PASSWORD_RESET = 'token_password_reset'
940 933
941 934 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
942 935
943 936 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
944 937 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
945 938 api_key = Column("api_key", String(255), nullable=False, unique=True)
946 939 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
947 940 expires = Column('expires', Float(53), nullable=False)
948 941 role = Column('role', String(255), nullable=True)
949 942 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
950 943
951 944 # scope columns
952 945 repo_id = Column(
953 946 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
954 947 nullable=True, unique=None, default=None)
955 948 repo = relationship('Repository', lazy='joined')
956 949
957 950 repo_group_id = Column(
958 951 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
959 952 nullable=True, unique=None, default=None)
960 953 repo_group = relationship('RepoGroup', lazy='joined')
961 954
962 955 user = relationship('User', lazy='joined')
963 956
964 957 @classmethod
965 958 def _get_role_name(cls, role):
966 959 return {
967 960 cls.ROLE_ALL: _('all'),
968 961 cls.ROLE_HTTP: _('http/web interface'),
969 962 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
970 963 cls.ROLE_API: _('api calls'),
971 964 cls.ROLE_FEED: _('feed access'),
972 965 }.get(role, role)
973 966
974 967 @property
975 968 def expired(self):
976 969 if self.expires == -1:
977 970 return False
978 971 return time.time() > self.expires
979 972
980 973 @property
981 974 def role_humanized(self):
982 975 return self._get_role_name(self.role)
983 976
984 977
985 978 class UserEmailMap(Base, BaseModel):
986 979 __tablename__ = 'user_email_map'
987 980 __table_args__ = (
988 981 Index('uem_email_idx', 'email'),
989 982 UniqueConstraint('email'),
990 983 {'extend_existing': True, 'mysql_engine': 'InnoDB',
991 984 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
992 985 )
993 986 __mapper_args__ = {}
994 987
995 988 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
996 989 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
997 990 _email = Column("email", String(255), nullable=True, unique=False, default=None)
998 991 user = relationship('User', lazy='joined')
999 992
1000 993 @validates('_email')
1001 994 def validate_email(self, key, email):
1002 995 # check if this email is not main one
1003 996 main_email = Session().query(User).filter(User.email == email).scalar()
1004 997 if main_email is not None:
1005 998 raise AttributeError('email %s is present is user table' % email)
1006 999 return email
1007 1000
1008 1001 @hybrid_property
1009 1002 def email(self):
1010 1003 return self._email
1011 1004
1012 1005 @email.setter
1013 1006 def email(self, val):
1014 1007 self._email = val.lower() if val else None
1015 1008
1016 1009
1017 1010 class UserIpMap(Base, BaseModel):
1018 1011 __tablename__ = 'user_ip_map'
1019 1012 __table_args__ = (
1020 1013 UniqueConstraint('user_id', 'ip_addr'),
1021 1014 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1022 1015 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1023 1016 )
1024 1017 __mapper_args__ = {}
1025 1018
1026 1019 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1027 1020 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1028 1021 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1029 1022 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1030 1023 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1031 1024 user = relationship('User', lazy='joined')
1032 1025
1033 1026 @classmethod
1034 1027 def _get_ip_range(cls, ip_addr):
1035 1028 net = ipaddress.ip_network(ip_addr, strict=False)
1036 1029 return [str(net.network_address), str(net.broadcast_address)]
1037 1030
1038 1031 def __json__(self):
1039 1032 return {
1040 1033 'ip_addr': self.ip_addr,
1041 1034 'ip_range': self._get_ip_range(self.ip_addr),
1042 1035 }
1043 1036
1044 1037 def __unicode__(self):
1045 1038 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1046 1039 self.user_id, self.ip_addr)
1047 1040
1048 1041 class UserLog(Base, BaseModel):
1049 1042 __tablename__ = 'user_logs'
1050 1043 __table_args__ = (
1051 1044 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1052 1045 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1053 1046 )
1054 1047 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1055 1048 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1056 1049 username = Column("username", String(255), nullable=True, unique=None, default=None)
1057 1050 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1058 1051 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1059 1052 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1060 1053 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1061 1054 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1062 1055
1063 1056 def __unicode__(self):
1064 1057 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1065 1058 self.repository_name,
1066 1059 self.action)
1067 1060
1068 1061 @property
1069 1062 def action_as_day(self):
1070 1063 return datetime.date(*self.action_date.timetuple()[:3])
1071 1064
1072 1065 user = relationship('User')
1073 1066 repository = relationship('Repository', cascade='')
1074 1067
1075 1068
1076 1069 class UserGroup(Base, BaseModel):
1077 1070 __tablename__ = 'users_groups'
1078 1071 __table_args__ = (
1079 1072 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1080 1073 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1081 1074 )
1082 1075
1083 1076 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1084 1077 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1085 1078 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1086 1079 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1087 1080 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1088 1081 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1089 1082 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1090 1083 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1091 1084
1092 1085 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1093 1086 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1094 1087 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1095 1088 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1096 1089 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1097 1090 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1098 1091
1099 1092 user = relationship('User')
1100 1093
1101 1094 @hybrid_property
1102 1095 def group_data(self):
1103 1096 if not self._group_data:
1104 1097 return {}
1105 1098
1106 1099 try:
1107 1100 return json.loads(self._group_data)
1108 1101 except TypeError:
1109 1102 return {}
1110 1103
1111 1104 @group_data.setter
1112 1105 def group_data(self, val):
1113 1106 try:
1114 1107 self._group_data = json.dumps(val)
1115 1108 except Exception:
1116 1109 log.error(traceback.format_exc())
1117 1110
1118 1111 def __unicode__(self):
1119 1112 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1120 1113 self.users_group_id,
1121 1114 self.users_group_name)
1122 1115
1123 1116 @classmethod
1124 1117 def get_by_group_name(cls, group_name, cache=False,
1125 1118 case_insensitive=False):
1126 1119 if case_insensitive:
1127 1120 q = cls.query().filter(func.lower(cls.users_group_name) ==
1128 1121 func.lower(group_name))
1129 1122
1130 1123 else:
1131 1124 q = cls.query().filter(cls.users_group_name == group_name)
1132 1125 if cache:
1133 1126 q = q.options(FromCache(
1134 1127 "sql_cache_short",
1135 1128 "get_group_%s" % _hash_key(group_name)))
1136 1129 return q.scalar()
1137 1130
1138 1131 @classmethod
1139 1132 def get(cls, user_group_id, cache=False):
1140 1133 user_group = cls.query()
1141 1134 if cache:
1142 1135 user_group = user_group.options(FromCache("sql_cache_short",
1143 1136 "get_users_group_%s" % user_group_id))
1144 1137 return user_group.get(user_group_id)
1145 1138
1146 1139 def permissions(self, with_admins=True, with_owner=True):
1147 1140 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1148 1141 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1149 1142 joinedload(UserUserGroupToPerm.user),
1150 1143 joinedload(UserUserGroupToPerm.permission),)
1151 1144
1152 1145 # get owners and admins and permissions. We do a trick of re-writing
1153 1146 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1154 1147 # has a global reference and changing one object propagates to all
1155 1148 # others. This means if admin is also an owner admin_row that change
1156 1149 # would propagate to both objects
1157 1150 perm_rows = []
1158 1151 for _usr in q.all():
1159 1152 usr = AttributeDict(_usr.user.get_dict())
1160 1153 usr.permission = _usr.permission.permission_name
1161 1154 perm_rows.append(usr)
1162 1155
1163 1156 # filter the perm rows by 'default' first and then sort them by
1164 1157 # admin,write,read,none permissions sorted again alphabetically in
1165 1158 # each group
1166 1159 perm_rows = sorted(perm_rows, key=display_sort)
1167 1160
1168 1161 _admin_perm = 'usergroup.admin'
1169 1162 owner_row = []
1170 1163 if with_owner:
1171 1164 usr = AttributeDict(self.user.get_dict())
1172 1165 usr.owner_row = True
1173 1166 usr.permission = _admin_perm
1174 1167 owner_row.append(usr)
1175 1168
1176 1169 super_admin_rows = []
1177 1170 if with_admins:
1178 1171 for usr in User.get_all_super_admins():
1179 1172 # if this admin is also owner, don't double the record
1180 1173 if usr.user_id == owner_row[0].user_id:
1181 1174 owner_row[0].admin_row = True
1182 1175 else:
1183 1176 usr = AttributeDict(usr.get_dict())
1184 1177 usr.admin_row = True
1185 1178 usr.permission = _admin_perm
1186 1179 super_admin_rows.append(usr)
1187 1180
1188 1181 return super_admin_rows + owner_row + perm_rows
1189 1182
1190 1183 def permission_user_groups(self):
1191 1184 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1192 1185 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1193 1186 joinedload(UserGroupUserGroupToPerm.target_user_group),
1194 1187 joinedload(UserGroupUserGroupToPerm.permission),)
1195 1188
1196 1189 perm_rows = []
1197 1190 for _user_group in q.all():
1198 1191 usr = AttributeDict(_user_group.user_group.get_dict())
1199 1192 usr.permission = _user_group.permission.permission_name
1200 1193 perm_rows.append(usr)
1201 1194
1202 1195 return perm_rows
1203 1196
1204 1197 def _get_default_perms(self, user_group, suffix=''):
1205 1198 from rhodecode.model.permission import PermissionModel
1206 1199 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1207 1200
1208 1201 def get_default_perms(self, suffix=''):
1209 1202 return self._get_default_perms(self, suffix)
1210 1203
1211 1204 def get_api_data(self, with_group_members=True, include_secrets=False):
1212 1205 """
1213 1206 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1214 1207 basically forwarded.
1215 1208
1216 1209 """
1217 1210 user_group = self
1218 1211
1219 1212 data = {
1220 1213 'users_group_id': user_group.users_group_id,
1221 1214 'group_name': user_group.users_group_name,
1222 1215 'group_description': user_group.user_group_description,
1223 1216 'active': user_group.users_group_active,
1224 1217 'owner': user_group.user.username,
1225 1218 }
1226 1219 if with_group_members:
1227 1220 users = []
1228 1221 for user in user_group.members:
1229 1222 user = user.user
1230 1223 users.append(user.get_api_data(include_secrets=include_secrets))
1231 1224 data['users'] = users
1232 1225
1233 1226 return data
1234 1227
1235 1228
1236 1229 class UserGroupMember(Base, BaseModel):
1237 1230 __tablename__ = 'users_groups_members'
1238 1231 __table_args__ = (
1239 1232 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1240 1233 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1241 1234 )
1242 1235
1243 1236 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1244 1237 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1245 1238 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1246 1239
1247 1240 user = relationship('User', lazy='joined')
1248 1241 users_group = relationship('UserGroup')
1249 1242
1250 1243 def __init__(self, gr_id='', u_id=''):
1251 1244 self.users_group_id = gr_id
1252 1245 self.user_id = u_id
1253 1246
1254 1247
1255 1248 class RepositoryField(Base, BaseModel):
1256 1249 __tablename__ = 'repositories_fields'
1257 1250 __table_args__ = (
1258 1251 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1259 1252 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1260 1253 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1261 1254 )
1262 1255 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1263 1256
1264 1257 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1265 1258 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1266 1259 field_key = Column("field_key", String(250))
1267 1260 field_label = Column("field_label", String(1024), nullable=False)
1268 1261 field_value = Column("field_value", String(10000), nullable=False)
1269 1262 field_desc = Column("field_desc", String(1024), nullable=False)
1270 1263 field_type = Column("field_type", String(255), nullable=False, unique=None)
1271 1264 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1272 1265
1273 1266 repository = relationship('Repository')
1274 1267
1275 1268 @property
1276 1269 def field_key_prefixed(self):
1277 1270 return 'ex_%s' % self.field_key
1278 1271
1279 1272 @classmethod
1280 1273 def un_prefix_key(cls, key):
1281 1274 if key.startswith(cls.PREFIX):
1282 1275 return key[len(cls.PREFIX):]
1283 1276 return key
1284 1277
1285 1278 @classmethod
1286 1279 def get_by_key_name(cls, key, repo):
1287 1280 row = cls.query()\
1288 1281 .filter(cls.repository == repo)\
1289 1282 .filter(cls.field_key == key).scalar()
1290 1283 return row
1291 1284
1292 1285
1293 1286 class Repository(Base, BaseModel):
1294 1287 __tablename__ = 'repositories'
1295 1288 __table_args__ = (
1296 1289 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1297 1290 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1298 1291 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1299 1292 )
1300 1293 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1301 1294 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1302 1295
1303 1296 STATE_CREATED = 'repo_state_created'
1304 1297 STATE_PENDING = 'repo_state_pending'
1305 1298 STATE_ERROR = 'repo_state_error'
1306 1299
1307 1300 LOCK_AUTOMATIC = 'lock_auto'
1308 1301 LOCK_API = 'lock_api'
1309 1302 LOCK_WEB = 'lock_web'
1310 1303 LOCK_PULL = 'lock_pull'
1311 1304
1312 1305 NAME_SEP = URL_SEP
1313 1306
1314 1307 repo_id = Column(
1315 1308 "repo_id", Integer(), nullable=False, unique=True, default=None,
1316 1309 primary_key=True)
1317 1310 _repo_name = Column(
1318 1311 "repo_name", Text(), nullable=False, default=None)
1319 1312 _repo_name_hash = Column(
1320 1313 "repo_name_hash", String(255), nullable=False, unique=True)
1321 1314 repo_state = Column("repo_state", String(255), nullable=True)
1322 1315
1323 1316 clone_uri = Column(
1324 1317 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1325 1318 default=None)
1326 1319 repo_type = Column(
1327 1320 "repo_type", String(255), nullable=False, unique=False, default=None)
1328 1321 user_id = Column(
1329 1322 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1330 1323 unique=False, default=None)
1331 1324 private = Column(
1332 1325 "private", Boolean(), nullable=True, unique=None, default=None)
1333 1326 enable_statistics = Column(
1334 1327 "statistics", Boolean(), nullable=True, unique=None, default=True)
1335 1328 enable_downloads = Column(
1336 1329 "downloads", Boolean(), nullable=True, unique=None, default=True)
1337 1330 description = Column(
1338 1331 "description", String(10000), nullable=True, unique=None, default=None)
1339 1332 created_on = Column(
1340 1333 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1341 1334 default=datetime.datetime.now)
1342 1335 updated_on = Column(
1343 1336 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1344 1337 default=datetime.datetime.now)
1345 1338 _landing_revision = Column(
1346 1339 "landing_revision", String(255), nullable=False, unique=False,
1347 1340 default=None)
1348 1341 enable_locking = Column(
1349 1342 "enable_locking", Boolean(), nullable=False, unique=None,
1350 1343 default=False)
1351 1344 _locked = Column(
1352 1345 "locked", String(255), nullable=True, unique=False, default=None)
1353 1346 _changeset_cache = Column(
1354 1347 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1355 1348
1356 1349 fork_id = Column(
1357 1350 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1358 1351 nullable=True, unique=False, default=None)
1359 1352 group_id = Column(
1360 1353 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1361 1354 unique=False, default=None)
1362 1355
1363 1356 user = relationship('User', lazy='joined')
1364 1357 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1365 1358 group = relationship('RepoGroup', lazy='joined')
1366 1359 repo_to_perm = relationship(
1367 1360 'UserRepoToPerm', cascade='all',
1368 1361 order_by='UserRepoToPerm.repo_to_perm_id')
1369 1362 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1370 1363 stats = relationship('Statistics', cascade='all', uselist=False)
1371 1364
1372 1365 followers = relationship(
1373 1366 'UserFollowing',
1374 1367 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1375 1368 cascade='all')
1376 1369 extra_fields = relationship(
1377 1370 'RepositoryField', cascade="all, delete, delete-orphan")
1378 1371 logs = relationship('UserLog')
1379 1372 comments = relationship(
1380 1373 'ChangesetComment', cascade="all, delete, delete-orphan")
1381 1374 pull_requests_source = relationship(
1382 1375 'PullRequest',
1383 1376 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1384 1377 cascade="all, delete, delete-orphan")
1385 1378 pull_requests_target = relationship(
1386 1379 'PullRequest',
1387 1380 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1388 1381 cascade="all, delete, delete-orphan")
1389 1382 ui = relationship('RepoRhodeCodeUi', cascade="all")
1390 1383 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1391 1384 integrations = relationship('Integration',
1392 1385 cascade="all, delete, delete-orphan")
1393 1386
1394 1387 def __unicode__(self):
1395 1388 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1396 1389 safe_unicode(self.repo_name))
1397 1390
1398 1391 @hybrid_property
1399 1392 def landing_rev(self):
1400 1393 # always should return [rev_type, rev]
1401 1394 if self._landing_revision:
1402 1395 _rev_info = self._landing_revision.split(':')
1403 1396 if len(_rev_info) < 2:
1404 1397 _rev_info.insert(0, 'rev')
1405 1398 return [_rev_info[0], _rev_info[1]]
1406 1399 return [None, None]
1407 1400
1408 1401 @landing_rev.setter
1409 1402 def landing_rev(self, val):
1410 1403 if ':' not in val:
1411 1404 raise ValueError('value must be delimited with `:` and consist '
1412 1405 'of <rev_type>:<rev>, got %s instead' % val)
1413 1406 self._landing_revision = val
1414 1407
1415 1408 @hybrid_property
1416 1409 def locked(self):
1417 1410 if self._locked:
1418 1411 user_id, timelocked, reason = self._locked.split(':')
1419 1412 lock_values = int(user_id), timelocked, reason
1420 1413 else:
1421 1414 lock_values = [None, None, None]
1422 1415 return lock_values
1423 1416
1424 1417 @locked.setter
1425 1418 def locked(self, val):
1426 1419 if val and isinstance(val, (list, tuple)):
1427 1420 self._locked = ':'.join(map(str, val))
1428 1421 else:
1429 1422 self._locked = None
1430 1423
1431 1424 @hybrid_property
1432 1425 def changeset_cache(self):
1433 1426 from rhodecode.lib.vcs.backends.base import EmptyCommit
1434 1427 dummy = EmptyCommit().__json__()
1435 1428 if not self._changeset_cache:
1436 1429 return dummy
1437 1430 try:
1438 1431 return json.loads(self._changeset_cache)
1439 1432 except TypeError:
1440 1433 return dummy
1441 1434 except Exception:
1442 1435 log.error(traceback.format_exc())
1443 1436 return dummy
1444 1437
1445 1438 @changeset_cache.setter
1446 1439 def changeset_cache(self, val):
1447 1440 try:
1448 1441 self._changeset_cache = json.dumps(val)
1449 1442 except Exception:
1450 1443 log.error(traceback.format_exc())
1451 1444
1452 1445 @hybrid_property
1453 1446 def repo_name(self):
1454 1447 return self._repo_name
1455 1448
1456 1449 @repo_name.setter
1457 1450 def repo_name(self, value):
1458 1451 self._repo_name = value
1459 1452 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1460 1453
1461 1454 @classmethod
1462 1455 def normalize_repo_name(cls, repo_name):
1463 1456 """
1464 1457 Normalizes os specific repo_name to the format internally stored inside
1465 1458 database using URL_SEP
1466 1459
1467 1460 :param cls:
1468 1461 :param repo_name:
1469 1462 """
1470 1463 return cls.NAME_SEP.join(repo_name.split(os.sep))
1471 1464
1472 1465 @classmethod
1473 1466 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1474 1467 session = Session()
1475 1468 q = session.query(cls).filter(cls.repo_name == repo_name)
1476 1469
1477 1470 if cache:
1478 1471 if identity_cache:
1479 1472 val = cls.identity_cache(session, 'repo_name', repo_name)
1480 1473 if val:
1481 1474 return val
1482 1475 else:
1483 1476 q = q.options(
1484 1477 FromCache("sql_cache_short",
1485 1478 "get_repo_by_name_%s" % _hash_key(repo_name)))
1486 1479
1487 1480 return q.scalar()
1488 1481
1489 1482 @classmethod
1490 1483 def get_by_full_path(cls, repo_full_path):
1491 1484 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1492 1485 repo_name = cls.normalize_repo_name(repo_name)
1493 1486 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1494 1487
1495 1488 @classmethod
1496 1489 def get_repo_forks(cls, repo_id):
1497 1490 return cls.query().filter(Repository.fork_id == repo_id)
1498 1491
1499 1492 @classmethod
1500 1493 def base_path(cls):
1501 1494 """
1502 1495 Returns base path when all repos are stored
1503 1496
1504 1497 :param cls:
1505 1498 """
1506 1499 q = Session().query(RhodeCodeUi)\
1507 1500 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1508 1501 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1509 1502 return q.one().ui_value
1510 1503
1511 1504 @classmethod
1512 1505 def is_valid(cls, repo_name):
1513 1506 """
1514 1507 returns True if given repo name is a valid filesystem repository
1515 1508
1516 1509 :param cls:
1517 1510 :param repo_name:
1518 1511 """
1519 1512 from rhodecode.lib.utils import is_valid_repo
1520 1513
1521 1514 return is_valid_repo(repo_name, cls.base_path())
1522 1515
1523 1516 @classmethod
1524 1517 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1525 1518 case_insensitive=True):
1526 1519 q = Repository.query()
1527 1520
1528 1521 if not isinstance(user_id, Optional):
1529 1522 q = q.filter(Repository.user_id == user_id)
1530 1523
1531 1524 if not isinstance(group_id, Optional):
1532 1525 q = q.filter(Repository.group_id == group_id)
1533 1526
1534 1527 if case_insensitive:
1535 1528 q = q.order_by(func.lower(Repository.repo_name))
1536 1529 else:
1537 1530 q = q.order_by(Repository.repo_name)
1538 1531 return q.all()
1539 1532
1540 1533 @property
1541 1534 def forks(self):
1542 1535 """
1543 1536 Return forks of this repo
1544 1537 """
1545 1538 return Repository.get_repo_forks(self.repo_id)
1546 1539
1547 1540 @property
1548 1541 def parent(self):
1549 1542 """
1550 1543 Returns fork parent
1551 1544 """
1552 1545 return self.fork
1553 1546
1554 1547 @property
1555 1548 def just_name(self):
1556 1549 return self.repo_name.split(self.NAME_SEP)[-1]
1557 1550
1558 1551 @property
1559 1552 def groups_with_parents(self):
1560 1553 groups = []
1561 1554 if self.group is None:
1562 1555 return groups
1563 1556
1564 1557 cur_gr = self.group
1565 1558 groups.insert(0, cur_gr)
1566 1559 while 1:
1567 1560 gr = getattr(cur_gr, 'parent_group', None)
1568 1561 cur_gr = cur_gr.parent_group
1569 1562 if gr is None:
1570 1563 break
1571 1564 groups.insert(0, gr)
1572 1565
1573 1566 return groups
1574 1567
1575 1568 @property
1576 1569 def groups_and_repo(self):
1577 1570 return self.groups_with_parents, self
1578 1571
1579 1572 @LazyProperty
1580 1573 def repo_path(self):
1581 1574 """
1582 1575 Returns base full path for that repository means where it actually
1583 1576 exists on a filesystem
1584 1577 """
1585 1578 q = Session().query(RhodeCodeUi).filter(
1586 1579 RhodeCodeUi.ui_key == self.NAME_SEP)
1587 1580 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1588 1581 return q.one().ui_value
1589 1582
1590 1583 @property
1591 1584 def repo_full_path(self):
1592 1585 p = [self.repo_path]
1593 1586 # we need to split the name by / since this is how we store the
1594 1587 # names in the database, but that eventually needs to be converted
1595 1588 # into a valid system path
1596 1589 p += self.repo_name.split(self.NAME_SEP)
1597 1590 return os.path.join(*map(safe_unicode, p))
1598 1591
1599 1592 @property
1600 1593 def cache_keys(self):
1601 1594 """
1602 1595 Returns associated cache keys for that repo
1603 1596 """
1604 1597 return CacheKey.query()\
1605 1598 .filter(CacheKey.cache_args == self.repo_name)\
1606 1599 .order_by(CacheKey.cache_key)\
1607 1600 .all()
1608 1601
1609 1602 def get_new_name(self, repo_name):
1610 1603 """
1611 1604 returns new full repository name based on assigned group and new new
1612 1605
1613 1606 :param group_name:
1614 1607 """
1615 1608 path_prefix = self.group.full_path_splitted if self.group else []
1616 1609 return self.NAME_SEP.join(path_prefix + [repo_name])
1617 1610
1618 1611 @property
1619 1612 def _config(self):
1620 1613 """
1621 1614 Returns db based config object.
1622 1615 """
1623 1616 from rhodecode.lib.utils import make_db_config
1624 1617 return make_db_config(clear_session=False, repo=self)
1625 1618
1626 1619 def permissions(self, with_admins=True, with_owner=True):
1627 1620 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1628 1621 q = q.options(joinedload(UserRepoToPerm.repository),
1629 1622 joinedload(UserRepoToPerm.user),
1630 1623 joinedload(UserRepoToPerm.permission),)
1631 1624
1632 1625 # get owners and admins and permissions. We do a trick of re-writing
1633 1626 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1634 1627 # has a global reference and changing one object propagates to all
1635 1628 # others. This means if admin is also an owner admin_row that change
1636 1629 # would propagate to both objects
1637 1630 perm_rows = []
1638 1631 for _usr in q.all():
1639 1632 usr = AttributeDict(_usr.user.get_dict())
1640 1633 usr.permission = _usr.permission.permission_name
1641 1634 perm_rows.append(usr)
1642 1635
1643 1636 # filter the perm rows by 'default' first and then sort them by
1644 1637 # admin,write,read,none permissions sorted again alphabetically in
1645 1638 # each group
1646 1639 perm_rows = sorted(perm_rows, key=display_sort)
1647 1640
1648 1641 _admin_perm = 'repository.admin'
1649 1642 owner_row = []
1650 1643 if with_owner:
1651 1644 usr = AttributeDict(self.user.get_dict())
1652 1645 usr.owner_row = True
1653 1646 usr.permission = _admin_perm
1654 1647 owner_row.append(usr)
1655 1648
1656 1649 super_admin_rows = []
1657 1650 if with_admins:
1658 1651 for usr in User.get_all_super_admins():
1659 1652 # if this admin is also owner, don't double the record
1660 1653 if usr.user_id == owner_row[0].user_id:
1661 1654 owner_row[0].admin_row = True
1662 1655 else:
1663 1656 usr = AttributeDict(usr.get_dict())
1664 1657 usr.admin_row = True
1665 1658 usr.permission = _admin_perm
1666 1659 super_admin_rows.append(usr)
1667 1660
1668 1661 return super_admin_rows + owner_row + perm_rows
1669 1662
1670 1663 def permission_user_groups(self):
1671 1664 q = UserGroupRepoToPerm.query().filter(
1672 1665 UserGroupRepoToPerm.repository == self)
1673 1666 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1674 1667 joinedload(UserGroupRepoToPerm.users_group),
1675 1668 joinedload(UserGroupRepoToPerm.permission),)
1676 1669
1677 1670 perm_rows = []
1678 1671 for _user_group in q.all():
1679 1672 usr = AttributeDict(_user_group.users_group.get_dict())
1680 1673 usr.permission = _user_group.permission.permission_name
1681 1674 perm_rows.append(usr)
1682 1675
1683 1676 return perm_rows
1684 1677
1685 1678 def get_api_data(self, include_secrets=False):
1686 1679 """
1687 1680 Common function for generating repo api data
1688 1681
1689 1682 :param include_secrets: See :meth:`User.get_api_data`.
1690 1683
1691 1684 """
1692 1685 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1693 1686 # move this methods on models level.
1694 1687 from rhodecode.model.settings import SettingsModel
1695 1688
1696 1689 repo = self
1697 1690 _user_id, _time, _reason = self.locked
1698 1691
1699 1692 data = {
1700 1693 'repo_id': repo.repo_id,
1701 1694 'repo_name': repo.repo_name,
1702 1695 'repo_type': repo.repo_type,
1703 1696 'clone_uri': repo.clone_uri or '',
1704 1697 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1705 1698 'private': repo.private,
1706 1699 'created_on': repo.created_on,
1707 1700 'description': repo.description,
1708 1701 'landing_rev': repo.landing_rev,
1709 1702 'owner': repo.user.username,
1710 1703 'fork_of': repo.fork.repo_name if repo.fork else None,
1711 1704 'enable_statistics': repo.enable_statistics,
1712 1705 'enable_locking': repo.enable_locking,
1713 1706 'enable_downloads': repo.enable_downloads,
1714 1707 'last_changeset': repo.changeset_cache,
1715 1708 'locked_by': User.get(_user_id).get_api_data(
1716 1709 include_secrets=include_secrets) if _user_id else None,
1717 1710 'locked_date': time_to_datetime(_time) if _time else None,
1718 1711 'lock_reason': _reason if _reason else None,
1719 1712 }
1720 1713
1721 1714 # TODO: mikhail: should be per-repo settings here
1722 1715 rc_config = SettingsModel().get_all_settings()
1723 1716 repository_fields = str2bool(
1724 1717 rc_config.get('rhodecode_repository_fields'))
1725 1718 if repository_fields:
1726 1719 for f in self.extra_fields:
1727 1720 data[f.field_key_prefixed] = f.field_value
1728 1721
1729 1722 return data
1730 1723
1731 1724 @classmethod
1732 1725 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1733 1726 if not lock_time:
1734 1727 lock_time = time.time()
1735 1728 if not lock_reason:
1736 1729 lock_reason = cls.LOCK_AUTOMATIC
1737 1730 repo.locked = [user_id, lock_time, lock_reason]
1738 1731 Session().add(repo)
1739 1732 Session().commit()
1740 1733
1741 1734 @classmethod
1742 1735 def unlock(cls, repo):
1743 1736 repo.locked = None
1744 1737 Session().add(repo)
1745 1738 Session().commit()
1746 1739
1747 1740 @classmethod
1748 1741 def getlock(cls, repo):
1749 1742 return repo.locked
1750 1743
1751 1744 def is_user_lock(self, user_id):
1752 1745 if self.lock[0]:
1753 1746 lock_user_id = safe_int(self.lock[0])
1754 1747 user_id = safe_int(user_id)
1755 1748 # both are ints, and they are equal
1756 1749 return all([lock_user_id, user_id]) and lock_user_id == user_id
1757 1750
1758 1751 return False
1759 1752
1760 1753 def get_locking_state(self, action, user_id, only_when_enabled=True):
1761 1754 """
1762 1755 Checks locking on this repository, if locking is enabled and lock is
1763 1756 present returns a tuple of make_lock, locked, locked_by.
1764 1757 make_lock can have 3 states None (do nothing) True, make lock
1765 1758 False release lock, This value is later propagated to hooks, which
1766 1759 do the locking. Think about this as signals passed to hooks what to do.
1767 1760
1768 1761 """
1769 1762 # TODO: johbo: This is part of the business logic and should be moved
1770 1763 # into the RepositoryModel.
1771 1764
1772 1765 if action not in ('push', 'pull'):
1773 1766 raise ValueError("Invalid action value: %s" % repr(action))
1774 1767
1775 1768 # defines if locked error should be thrown to user
1776 1769 currently_locked = False
1777 1770 # defines if new lock should be made, tri-state
1778 1771 make_lock = None
1779 1772 repo = self
1780 1773 user = User.get(user_id)
1781 1774
1782 1775 lock_info = repo.locked
1783 1776
1784 1777 if repo and (repo.enable_locking or not only_when_enabled):
1785 1778 if action == 'push':
1786 1779 # check if it's already locked !, if it is compare users
1787 1780 locked_by_user_id = lock_info[0]
1788 1781 if user.user_id == locked_by_user_id:
1789 1782 log.debug(
1790 1783 'Got `push` action from user %s, now unlocking', user)
1791 1784 # unlock if we have push from user who locked
1792 1785 make_lock = False
1793 1786 else:
1794 1787 # we're not the same user who locked, ban with
1795 1788 # code defined in settings (default is 423 HTTP Locked) !
1796 1789 log.debug('Repo %s is currently locked by %s', repo, user)
1797 1790 currently_locked = True
1798 1791 elif action == 'pull':
1799 1792 # [0] user [1] date
1800 1793 if lock_info[0] and lock_info[1]:
1801 1794 log.debug('Repo %s is currently locked by %s', repo, user)
1802 1795 currently_locked = True
1803 1796 else:
1804 1797 log.debug('Setting lock on repo %s by %s', repo, user)
1805 1798 make_lock = True
1806 1799
1807 1800 else:
1808 1801 log.debug('Repository %s do not have locking enabled', repo)
1809 1802
1810 1803 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1811 1804 make_lock, currently_locked, lock_info)
1812 1805
1813 1806 from rhodecode.lib.auth import HasRepoPermissionAny
1814 1807 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1815 1808 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1816 1809 # if we don't have at least write permission we cannot make a lock
1817 1810 log.debug('lock state reset back to FALSE due to lack '
1818 1811 'of at least read permission')
1819 1812 make_lock = False
1820 1813
1821 1814 return make_lock, currently_locked, lock_info
1822 1815
1823 1816 @property
1824 1817 def last_db_change(self):
1825 1818 return self.updated_on
1826 1819
1827 1820 @property
1828 1821 def clone_uri_hidden(self):
1829 1822 clone_uri = self.clone_uri
1830 1823 if clone_uri:
1831 1824 import urlobject
1832 1825 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
1833 1826 if url_obj.password:
1834 1827 clone_uri = url_obj.with_password('*****')
1835 1828 return clone_uri
1836 1829
1837 1830 def clone_url(self, **override):
1838 1831 qualified_home_url = url('home', qualified=True)
1839 1832
1840 1833 uri_tmpl = None
1841 1834 if 'with_id' in override:
1842 1835 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1843 1836 del override['with_id']
1844 1837
1845 1838 if 'uri_tmpl' in override:
1846 1839 uri_tmpl = override['uri_tmpl']
1847 1840 del override['uri_tmpl']
1848 1841
1849 1842 # we didn't override our tmpl from **overrides
1850 1843 if not uri_tmpl:
1851 1844 uri_tmpl = self.DEFAULT_CLONE_URI
1852 1845 try:
1853 1846 from pylons import tmpl_context as c
1854 1847 uri_tmpl = c.clone_uri_tmpl
1855 1848 except Exception:
1856 1849 # in any case if we call this outside of request context,
1857 1850 # ie, not having tmpl_context set up
1858 1851 pass
1859 1852
1860 1853 return get_clone_url(uri_tmpl=uri_tmpl,
1861 1854 qualifed_home_url=qualified_home_url,
1862 1855 repo_name=self.repo_name,
1863 1856 repo_id=self.repo_id, **override)
1864 1857
1865 1858 def set_state(self, state):
1866 1859 self.repo_state = state
1867 1860 Session().add(self)
1868 1861 #==========================================================================
1869 1862 # SCM PROPERTIES
1870 1863 #==========================================================================
1871 1864
1872 1865 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1873 1866 return get_commit_safe(
1874 1867 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1875 1868
1876 1869 def get_changeset(self, rev=None, pre_load=None):
1877 1870 warnings.warn("Use get_commit", DeprecationWarning)
1878 1871 commit_id = None
1879 1872 commit_idx = None
1880 1873 if isinstance(rev, basestring):
1881 1874 commit_id = rev
1882 1875 else:
1883 1876 commit_idx = rev
1884 1877 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1885 1878 pre_load=pre_load)
1886 1879
1887 1880 def get_landing_commit(self):
1888 1881 """
1889 1882 Returns landing commit, or if that doesn't exist returns the tip
1890 1883 """
1891 1884 _rev_type, _rev = self.landing_rev
1892 1885 commit = self.get_commit(_rev)
1893 1886 if isinstance(commit, EmptyCommit):
1894 1887 return self.get_commit()
1895 1888 return commit
1896 1889
1897 1890 def update_commit_cache(self, cs_cache=None, config=None):
1898 1891 """
1899 1892 Update cache of last changeset for repository, keys should be::
1900 1893
1901 1894 short_id
1902 1895 raw_id
1903 1896 revision
1904 1897 parents
1905 1898 message
1906 1899 date
1907 1900 author
1908 1901
1909 1902 :param cs_cache:
1910 1903 """
1911 1904 from rhodecode.lib.vcs.backends.base import BaseChangeset
1912 1905 if cs_cache is None:
1913 1906 # use no-cache version here
1914 1907 scm_repo = self.scm_instance(cache=False, config=config)
1915 1908 if scm_repo:
1916 1909 cs_cache = scm_repo.get_commit(
1917 1910 pre_load=["author", "date", "message", "parents"])
1918 1911 else:
1919 1912 cs_cache = EmptyCommit()
1920 1913
1921 1914 if isinstance(cs_cache, BaseChangeset):
1922 1915 cs_cache = cs_cache.__json__()
1923 1916
1924 1917 def is_outdated(new_cs_cache):
1925 1918 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1926 1919 new_cs_cache['revision'] != self.changeset_cache['revision']):
1927 1920 return True
1928 1921 return False
1929 1922
1930 1923 # check if we have maybe already latest cached revision
1931 1924 if is_outdated(cs_cache) or not self.changeset_cache:
1932 1925 _default = datetime.datetime.fromtimestamp(0)
1933 1926 last_change = cs_cache.get('date') or _default
1934 1927 log.debug('updated repo %s with new cs cache %s',
1935 1928 self.repo_name, cs_cache)
1936 1929 self.updated_on = last_change
1937 1930 self.changeset_cache = cs_cache
1938 1931 Session().add(self)
1939 1932 Session().commit()
1940 1933 else:
1941 1934 log.debug('Skipping update_commit_cache for repo:`%s` '
1942 1935 'commit already with latest changes', self.repo_name)
1943 1936
1944 1937 @property
1945 1938 def tip(self):
1946 1939 return self.get_commit('tip')
1947 1940
1948 1941 @property
1949 1942 def author(self):
1950 1943 return self.tip.author
1951 1944
1952 1945 @property
1953 1946 def last_change(self):
1954 1947 return self.scm_instance().last_change
1955 1948
1956 1949 def get_comments(self, revisions=None):
1957 1950 """
1958 1951 Returns comments for this repository grouped by revisions
1959 1952
1960 1953 :param revisions: filter query by revisions only
1961 1954 """
1962 1955 cmts = ChangesetComment.query()\
1963 1956 .filter(ChangesetComment.repo == self)
1964 1957 if revisions:
1965 1958 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1966 1959 grouped = collections.defaultdict(list)
1967 1960 for cmt in cmts.all():
1968 1961 grouped[cmt.revision].append(cmt)
1969 1962 return grouped
1970 1963
1971 1964 def statuses(self, revisions=None):
1972 1965 """
1973 1966 Returns statuses for this repository
1974 1967
1975 1968 :param revisions: list of revisions to get statuses for
1976 1969 """
1977 1970 statuses = ChangesetStatus.query()\
1978 1971 .filter(ChangesetStatus.repo == self)\
1979 1972 .filter(ChangesetStatus.version == 0)
1980 1973
1981 1974 if revisions:
1982 1975 # Try doing the filtering in chunks to avoid hitting limits
1983 1976 size = 500
1984 1977 status_results = []
1985 1978 for chunk in xrange(0, len(revisions), size):
1986 1979 status_results += statuses.filter(
1987 1980 ChangesetStatus.revision.in_(
1988 1981 revisions[chunk: chunk+size])
1989 1982 ).all()
1990 1983 else:
1991 1984 status_results = statuses.all()
1992 1985
1993 1986 grouped = {}
1994 1987
1995 1988 # maybe we have open new pullrequest without a status?
1996 1989 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1997 1990 status_lbl = ChangesetStatus.get_status_lbl(stat)
1998 1991 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1999 1992 for rev in pr.revisions:
2000 1993 pr_id = pr.pull_request_id
2001 1994 pr_repo = pr.target_repo.repo_name
2002 1995 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2003 1996
2004 1997 for stat in status_results:
2005 1998 pr_id = pr_repo = None
2006 1999 if stat.pull_request:
2007 2000 pr_id = stat.pull_request.pull_request_id
2008 2001 pr_repo = stat.pull_request.target_repo.repo_name
2009 2002 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2010 2003 pr_id, pr_repo]
2011 2004 return grouped
2012 2005
2013 2006 # ==========================================================================
2014 2007 # SCM CACHE INSTANCE
2015 2008 # ==========================================================================
2016 2009
2017 2010 def scm_instance(self, **kwargs):
2018 2011 import rhodecode
2019 2012
2020 2013 # Passing a config will not hit the cache currently only used
2021 2014 # for repo2dbmapper
2022 2015 config = kwargs.pop('config', None)
2023 2016 cache = kwargs.pop('cache', None)
2024 2017 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2025 2018 # if cache is NOT defined use default global, else we have a full
2026 2019 # control over cache behaviour
2027 2020 if cache is None and full_cache and not config:
2028 2021 return self._get_instance_cached()
2029 2022 return self._get_instance(cache=bool(cache), config=config)
2030 2023
2031 2024 def _get_instance_cached(self):
2032 2025 @cache_region('long_term')
2033 2026 def _get_repo(cache_key):
2034 2027 return self._get_instance()
2035 2028
2036 2029 invalidator_context = CacheKey.repo_context_cache(
2037 2030 _get_repo, self.repo_name, None, thread_scoped=True)
2038 2031
2039 2032 with invalidator_context as context:
2040 2033 context.invalidate()
2041 2034 repo = context.compute()
2042 2035
2043 2036 return repo
2044 2037
2045 2038 def _get_instance(self, cache=True, config=None):
2046 2039 config = config or self._config
2047 2040 custom_wire = {
2048 2041 'cache': cache # controls the vcs.remote cache
2049 2042 }
2050 2043 repo = get_vcs_instance(
2051 2044 repo_path=safe_str(self.repo_full_path),
2052 2045 config=config,
2053 2046 with_wire=custom_wire,
2054 2047 create=False,
2055 2048 _vcs_alias=self.repo_type)
2056 2049
2057 2050 return repo
2058 2051
2059 2052 def __json__(self):
2060 2053 return {'landing_rev': self.landing_rev}
2061 2054
2062 2055 def get_dict(self):
2063 2056
2064 2057 # Since we transformed `repo_name` to a hybrid property, we need to
2065 2058 # keep compatibility with the code which uses `repo_name` field.
2066 2059
2067 2060 result = super(Repository, self).get_dict()
2068 2061 result['repo_name'] = result.pop('_repo_name', None)
2069 2062 return result
2070 2063
2071 2064
2072 2065 class RepoGroup(Base, BaseModel):
2073 2066 __tablename__ = 'groups'
2074 2067 __table_args__ = (
2075 2068 UniqueConstraint('group_name', 'group_parent_id'),
2076 2069 CheckConstraint('group_id != group_parent_id'),
2077 2070 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2078 2071 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2079 2072 )
2080 2073 __mapper_args__ = {'order_by': 'group_name'}
2081 2074
2082 2075 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2083 2076
2084 2077 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2085 2078 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2086 2079 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2087 2080 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2088 2081 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2089 2082 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2090 2083 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2091 2084 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2092 2085
2093 2086 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2094 2087 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2095 2088 parent_group = relationship('RepoGroup', remote_side=group_id)
2096 2089 user = relationship('User')
2097 2090 integrations = relationship('Integration',
2098 2091 cascade="all, delete, delete-orphan")
2099 2092
2100 2093 def __init__(self, group_name='', parent_group=None):
2101 2094 self.group_name = group_name
2102 2095 self.parent_group = parent_group
2103 2096
2104 2097 def __unicode__(self):
2105 2098 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2106 2099 self.group_name)
2107 2100
2108 2101 @classmethod
2109 2102 def _generate_choice(cls, repo_group):
2110 2103 from webhelpers.html import literal as _literal
2111 2104 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2112 2105 return repo_group.group_id, _name(repo_group.full_path_splitted)
2113 2106
2114 2107 @classmethod
2115 2108 def groups_choices(cls, groups=None, show_empty_group=True):
2116 2109 if not groups:
2117 2110 groups = cls.query().all()
2118 2111
2119 2112 repo_groups = []
2120 2113 if show_empty_group:
2121 2114 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2122 2115
2123 2116 repo_groups.extend([cls._generate_choice(x) for x in groups])
2124 2117
2125 2118 repo_groups = sorted(
2126 2119 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2127 2120 return repo_groups
2128 2121
2129 2122 @classmethod
2130 2123 def url_sep(cls):
2131 2124 return URL_SEP
2132 2125
2133 2126 @classmethod
2134 2127 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2135 2128 if case_insensitive:
2136 2129 gr = cls.query().filter(func.lower(cls.group_name)
2137 2130 == func.lower(group_name))
2138 2131 else:
2139 2132 gr = cls.query().filter(cls.group_name == group_name)
2140 2133 if cache:
2141 2134 gr = gr.options(FromCache(
2142 2135 "sql_cache_short",
2143 2136 "get_group_%s" % _hash_key(group_name)))
2144 2137 return gr.scalar()
2145 2138
2146 2139 @classmethod
2147 2140 def get_user_personal_repo_group(cls, user_id):
2148 2141 user = User.get(user_id)
2149 2142 return cls.query()\
2150 2143 .filter(cls.personal == true())\
2151 2144 .filter(cls.user == user).scalar()
2152 2145
2153 2146 @classmethod
2154 2147 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2155 2148 case_insensitive=True):
2156 2149 q = RepoGroup.query()
2157 2150
2158 2151 if not isinstance(user_id, Optional):
2159 2152 q = q.filter(RepoGroup.user_id == user_id)
2160 2153
2161 2154 if not isinstance(group_id, Optional):
2162 2155 q = q.filter(RepoGroup.group_parent_id == group_id)
2163 2156
2164 2157 if case_insensitive:
2165 2158 q = q.order_by(func.lower(RepoGroup.group_name))
2166 2159 else:
2167 2160 q = q.order_by(RepoGroup.group_name)
2168 2161 return q.all()
2169 2162
2170 2163 @property
2171 2164 def parents(self):
2172 2165 parents_recursion_limit = 10
2173 2166 groups = []
2174 2167 if self.parent_group is None:
2175 2168 return groups
2176 2169 cur_gr = self.parent_group
2177 2170 groups.insert(0, cur_gr)
2178 2171 cnt = 0
2179 2172 while 1:
2180 2173 cnt += 1
2181 2174 gr = getattr(cur_gr, 'parent_group', None)
2182 2175 cur_gr = cur_gr.parent_group
2183 2176 if gr is None:
2184 2177 break
2185 2178 if cnt == parents_recursion_limit:
2186 2179 # this will prevent accidental infinit loops
2187 2180 log.error(('more than %s parents found for group %s, stopping '
2188 2181 'recursive parent fetching' % (parents_recursion_limit, self)))
2189 2182 break
2190 2183
2191 2184 groups.insert(0, gr)
2192 2185 return groups
2193 2186
2194 2187 @property
2195 2188 def children(self):
2196 2189 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2197 2190
2198 2191 @property
2199 2192 def name(self):
2200 2193 return self.group_name.split(RepoGroup.url_sep())[-1]
2201 2194
2202 2195 @property
2203 2196 def full_path(self):
2204 2197 return self.group_name
2205 2198
2206 2199 @property
2207 2200 def full_path_splitted(self):
2208 2201 return self.group_name.split(RepoGroup.url_sep())
2209 2202
2210 2203 @property
2211 2204 def repositories(self):
2212 2205 return Repository.query()\
2213 2206 .filter(Repository.group == self)\
2214 2207 .order_by(Repository.repo_name)
2215 2208
2216 2209 @property
2217 2210 def repositories_recursive_count(self):
2218 2211 cnt = self.repositories.count()
2219 2212
2220 2213 def children_count(group):
2221 2214 cnt = 0
2222 2215 for child in group.children:
2223 2216 cnt += child.repositories.count()
2224 2217 cnt += children_count(child)
2225 2218 return cnt
2226 2219
2227 2220 return cnt + children_count(self)
2228 2221
2229 2222 def _recursive_objects(self, include_repos=True):
2230 2223 all_ = []
2231 2224
2232 2225 def _get_members(root_gr):
2233 2226 if include_repos:
2234 2227 for r in root_gr.repositories:
2235 2228 all_.append(r)
2236 2229 childs = root_gr.children.all()
2237 2230 if childs:
2238 2231 for gr in childs:
2239 2232 all_.append(gr)
2240 2233 _get_members(gr)
2241 2234
2242 2235 _get_members(self)
2243 2236 return [self] + all_
2244 2237
2245 2238 def recursive_groups_and_repos(self):
2246 2239 """
2247 2240 Recursive return all groups, with repositories in those groups
2248 2241 """
2249 2242 return self._recursive_objects()
2250 2243
2251 2244 def recursive_groups(self):
2252 2245 """
2253 2246 Returns all children groups for this group including children of children
2254 2247 """
2255 2248 return self._recursive_objects(include_repos=False)
2256 2249
2257 2250 def get_new_name(self, group_name):
2258 2251 """
2259 2252 returns new full group name based on parent and new name
2260 2253
2261 2254 :param group_name:
2262 2255 """
2263 2256 path_prefix = (self.parent_group.full_path_splitted if
2264 2257 self.parent_group else [])
2265 2258 return RepoGroup.url_sep().join(path_prefix + [group_name])
2266 2259
2267 2260 def permissions(self, with_admins=True, with_owner=True):
2268 2261 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2269 2262 q = q.options(joinedload(UserRepoGroupToPerm.group),
2270 2263 joinedload(UserRepoGroupToPerm.user),
2271 2264 joinedload(UserRepoGroupToPerm.permission),)
2272 2265
2273 2266 # get owners and admins and permissions. We do a trick of re-writing
2274 2267 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2275 2268 # has a global reference and changing one object propagates to all
2276 2269 # others. This means if admin is also an owner admin_row that change
2277 2270 # would propagate to both objects
2278 2271 perm_rows = []
2279 2272 for _usr in q.all():
2280 2273 usr = AttributeDict(_usr.user.get_dict())
2281 2274 usr.permission = _usr.permission.permission_name
2282 2275 perm_rows.append(usr)
2283 2276
2284 2277 # filter the perm rows by 'default' first and then sort them by
2285 2278 # admin,write,read,none permissions sorted again alphabetically in
2286 2279 # each group
2287 2280 perm_rows = sorted(perm_rows, key=display_sort)
2288 2281
2289 2282 _admin_perm = 'group.admin'
2290 2283 owner_row = []
2291 2284 if with_owner:
2292 2285 usr = AttributeDict(self.user.get_dict())
2293 2286 usr.owner_row = True
2294 2287 usr.permission = _admin_perm
2295 2288 owner_row.append(usr)
2296 2289
2297 2290 super_admin_rows = []
2298 2291 if with_admins:
2299 2292 for usr in User.get_all_super_admins():
2300 2293 # if this admin is also owner, don't double the record
2301 2294 if usr.user_id == owner_row[0].user_id:
2302 2295 owner_row[0].admin_row = True
2303 2296 else:
2304 2297 usr = AttributeDict(usr.get_dict())
2305 2298 usr.admin_row = True
2306 2299 usr.permission = _admin_perm
2307 2300 super_admin_rows.append(usr)
2308 2301
2309 2302 return super_admin_rows + owner_row + perm_rows
2310 2303
2311 2304 def permission_user_groups(self):
2312 2305 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2313 2306 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2314 2307 joinedload(UserGroupRepoGroupToPerm.users_group),
2315 2308 joinedload(UserGroupRepoGroupToPerm.permission),)
2316 2309
2317 2310 perm_rows = []
2318 2311 for _user_group in q.all():
2319 2312 usr = AttributeDict(_user_group.users_group.get_dict())
2320 2313 usr.permission = _user_group.permission.permission_name
2321 2314 perm_rows.append(usr)
2322 2315
2323 2316 return perm_rows
2324 2317
2325 2318 def get_api_data(self):
2326 2319 """
2327 2320 Common function for generating api data
2328 2321
2329 2322 """
2330 2323 group = self
2331 2324 data = {
2332 2325 'group_id': group.group_id,
2333 2326 'group_name': group.group_name,
2334 2327 'group_description': group.group_description,
2335 2328 'parent_group': group.parent_group.group_name if group.parent_group else None,
2336 2329 'repositories': [x.repo_name for x in group.repositories],
2337 2330 'owner': group.user.username,
2338 2331 }
2339 2332 return data
2340 2333
2341 2334
2342 2335 class Permission(Base, BaseModel):
2343 2336 __tablename__ = 'permissions'
2344 2337 __table_args__ = (
2345 2338 Index('p_perm_name_idx', 'permission_name'),
2346 2339 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2347 2340 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2348 2341 )
2349 2342 PERMS = [
2350 2343 ('hg.admin', _('RhodeCode Super Administrator')),
2351 2344
2352 2345 ('repository.none', _('Repository no access')),
2353 2346 ('repository.read', _('Repository read access')),
2354 2347 ('repository.write', _('Repository write access')),
2355 2348 ('repository.admin', _('Repository admin access')),
2356 2349
2357 2350 ('group.none', _('Repository group no access')),
2358 2351 ('group.read', _('Repository group read access')),
2359 2352 ('group.write', _('Repository group write access')),
2360 2353 ('group.admin', _('Repository group admin access')),
2361 2354
2362 2355 ('usergroup.none', _('User group no access')),
2363 2356 ('usergroup.read', _('User group read access')),
2364 2357 ('usergroup.write', _('User group write access')),
2365 2358 ('usergroup.admin', _('User group admin access')),
2366 2359
2367 2360 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2368 2361 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2369 2362
2370 2363 ('hg.usergroup.create.false', _('User Group creation disabled')),
2371 2364 ('hg.usergroup.create.true', _('User Group creation enabled')),
2372 2365
2373 2366 ('hg.create.none', _('Repository creation disabled')),
2374 2367 ('hg.create.repository', _('Repository creation enabled')),
2375 2368 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2376 2369 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2377 2370
2378 2371 ('hg.fork.none', _('Repository forking disabled')),
2379 2372 ('hg.fork.repository', _('Repository forking enabled')),
2380 2373
2381 2374 ('hg.register.none', _('Registration disabled')),
2382 2375 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2383 2376 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2384 2377
2385 2378 ('hg.password_reset.enabled', _('Password reset enabled')),
2386 2379 ('hg.password_reset.hidden', _('Password reset hidden')),
2387 2380 ('hg.password_reset.disabled', _('Password reset disabled')),
2388 2381
2389 2382 ('hg.extern_activate.manual', _('Manual activation of external account')),
2390 2383 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2391 2384
2392 2385 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2393 2386 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2394 2387 ]
2395 2388
2396 2389 # definition of system default permissions for DEFAULT user
2397 2390 DEFAULT_USER_PERMISSIONS = [
2398 2391 'repository.read',
2399 2392 'group.read',
2400 2393 'usergroup.read',
2401 2394 'hg.create.repository',
2402 2395 'hg.repogroup.create.false',
2403 2396 'hg.usergroup.create.false',
2404 2397 'hg.create.write_on_repogroup.true',
2405 2398 'hg.fork.repository',
2406 2399 'hg.register.manual_activate',
2407 2400 'hg.password_reset.enabled',
2408 2401 'hg.extern_activate.auto',
2409 2402 'hg.inherit_default_perms.true',
2410 2403 ]
2411 2404
2412 2405 # defines which permissions are more important higher the more important
2413 2406 # Weight defines which permissions are more important.
2414 2407 # The higher number the more important.
2415 2408 PERM_WEIGHTS = {
2416 2409 'repository.none': 0,
2417 2410 'repository.read': 1,
2418 2411 'repository.write': 3,
2419 2412 'repository.admin': 4,
2420 2413
2421 2414 'group.none': 0,
2422 2415 'group.read': 1,
2423 2416 'group.write': 3,
2424 2417 'group.admin': 4,
2425 2418
2426 2419 'usergroup.none': 0,
2427 2420 'usergroup.read': 1,
2428 2421 'usergroup.write': 3,
2429 2422 'usergroup.admin': 4,
2430 2423
2431 2424 'hg.repogroup.create.false': 0,
2432 2425 'hg.repogroup.create.true': 1,
2433 2426
2434 2427 'hg.usergroup.create.false': 0,
2435 2428 'hg.usergroup.create.true': 1,
2436 2429
2437 2430 'hg.fork.none': 0,
2438 2431 'hg.fork.repository': 1,
2439 2432 'hg.create.none': 0,
2440 2433 'hg.create.repository': 1
2441 2434 }
2442 2435
2443 2436 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2444 2437 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2445 2438 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2446 2439
2447 2440 def __unicode__(self):
2448 2441 return u"<%s('%s:%s')>" % (
2449 2442 self.__class__.__name__, self.permission_id, self.permission_name
2450 2443 )
2451 2444
2452 2445 @classmethod
2453 2446 def get_by_key(cls, key):
2454 2447 return cls.query().filter(cls.permission_name == key).scalar()
2455 2448
2456 2449 @classmethod
2457 2450 def get_default_repo_perms(cls, user_id, repo_id=None):
2458 2451 q = Session().query(UserRepoToPerm, Repository, Permission)\
2459 2452 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2460 2453 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2461 2454 .filter(UserRepoToPerm.user_id == user_id)
2462 2455 if repo_id:
2463 2456 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2464 2457 return q.all()
2465 2458
2466 2459 @classmethod
2467 2460 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2468 2461 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2469 2462 .join(
2470 2463 Permission,
2471 2464 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2472 2465 .join(
2473 2466 Repository,
2474 2467 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2475 2468 .join(
2476 2469 UserGroup,
2477 2470 UserGroupRepoToPerm.users_group_id ==
2478 2471 UserGroup.users_group_id)\
2479 2472 .join(
2480 2473 UserGroupMember,
2481 2474 UserGroupRepoToPerm.users_group_id ==
2482 2475 UserGroupMember.users_group_id)\
2483 2476 .filter(
2484 2477 UserGroupMember.user_id == user_id,
2485 2478 UserGroup.users_group_active == true())
2486 2479 if repo_id:
2487 2480 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2488 2481 return q.all()
2489 2482
2490 2483 @classmethod
2491 2484 def get_default_group_perms(cls, user_id, repo_group_id=None):
2492 2485 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2493 2486 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2494 2487 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2495 2488 .filter(UserRepoGroupToPerm.user_id == user_id)
2496 2489 if repo_group_id:
2497 2490 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2498 2491 return q.all()
2499 2492
2500 2493 @classmethod
2501 2494 def get_default_group_perms_from_user_group(
2502 2495 cls, user_id, repo_group_id=None):
2503 2496 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2504 2497 .join(
2505 2498 Permission,
2506 2499 UserGroupRepoGroupToPerm.permission_id ==
2507 2500 Permission.permission_id)\
2508 2501 .join(
2509 2502 RepoGroup,
2510 2503 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2511 2504 .join(
2512 2505 UserGroup,
2513 2506 UserGroupRepoGroupToPerm.users_group_id ==
2514 2507 UserGroup.users_group_id)\
2515 2508 .join(
2516 2509 UserGroupMember,
2517 2510 UserGroupRepoGroupToPerm.users_group_id ==
2518 2511 UserGroupMember.users_group_id)\
2519 2512 .filter(
2520 2513 UserGroupMember.user_id == user_id,
2521 2514 UserGroup.users_group_active == true())
2522 2515 if repo_group_id:
2523 2516 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2524 2517 return q.all()
2525 2518
2526 2519 @classmethod
2527 2520 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2528 2521 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2529 2522 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2530 2523 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2531 2524 .filter(UserUserGroupToPerm.user_id == user_id)
2532 2525 if user_group_id:
2533 2526 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2534 2527 return q.all()
2535 2528
2536 2529 @classmethod
2537 2530 def get_default_user_group_perms_from_user_group(
2538 2531 cls, user_id, user_group_id=None):
2539 2532 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2540 2533 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2541 2534 .join(
2542 2535 Permission,
2543 2536 UserGroupUserGroupToPerm.permission_id ==
2544 2537 Permission.permission_id)\
2545 2538 .join(
2546 2539 TargetUserGroup,
2547 2540 UserGroupUserGroupToPerm.target_user_group_id ==
2548 2541 TargetUserGroup.users_group_id)\
2549 2542 .join(
2550 2543 UserGroup,
2551 2544 UserGroupUserGroupToPerm.user_group_id ==
2552 2545 UserGroup.users_group_id)\
2553 2546 .join(
2554 2547 UserGroupMember,
2555 2548 UserGroupUserGroupToPerm.user_group_id ==
2556 2549 UserGroupMember.users_group_id)\
2557 2550 .filter(
2558 2551 UserGroupMember.user_id == user_id,
2559 2552 UserGroup.users_group_active == true())
2560 2553 if user_group_id:
2561 2554 q = q.filter(
2562 2555 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2563 2556
2564 2557 return q.all()
2565 2558
2566 2559
2567 2560 class UserRepoToPerm(Base, BaseModel):
2568 2561 __tablename__ = 'repo_to_perm'
2569 2562 __table_args__ = (
2570 2563 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2571 2564 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2572 2565 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2573 2566 )
2574 2567 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2575 2568 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2576 2569 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2577 2570 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2578 2571
2579 2572 user = relationship('User')
2580 2573 repository = relationship('Repository')
2581 2574 permission = relationship('Permission')
2582 2575
2583 2576 @classmethod
2584 2577 def create(cls, user, repository, permission):
2585 2578 n = cls()
2586 2579 n.user = user
2587 2580 n.repository = repository
2588 2581 n.permission = permission
2589 2582 Session().add(n)
2590 2583 return n
2591 2584
2592 2585 def __unicode__(self):
2593 2586 return u'<%s => %s >' % (self.user, self.repository)
2594 2587
2595 2588
2596 2589 class UserUserGroupToPerm(Base, BaseModel):
2597 2590 __tablename__ = 'user_user_group_to_perm'
2598 2591 __table_args__ = (
2599 2592 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2600 2593 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2601 2594 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2602 2595 )
2603 2596 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2604 2597 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2605 2598 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2606 2599 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2607 2600
2608 2601 user = relationship('User')
2609 2602 user_group = relationship('UserGroup')
2610 2603 permission = relationship('Permission')
2611 2604
2612 2605 @classmethod
2613 2606 def create(cls, user, user_group, permission):
2614 2607 n = cls()
2615 2608 n.user = user
2616 2609 n.user_group = user_group
2617 2610 n.permission = permission
2618 2611 Session().add(n)
2619 2612 return n
2620 2613
2621 2614 def __unicode__(self):
2622 2615 return u'<%s => %s >' % (self.user, self.user_group)
2623 2616
2624 2617
2625 2618 class UserToPerm(Base, BaseModel):
2626 2619 __tablename__ = 'user_to_perm'
2627 2620 __table_args__ = (
2628 2621 UniqueConstraint('user_id', 'permission_id'),
2629 2622 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2630 2623 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2631 2624 )
2632 2625 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2633 2626 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2634 2627 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2635 2628
2636 2629 user = relationship('User')
2637 2630 permission = relationship('Permission', lazy='joined')
2638 2631
2639 2632 def __unicode__(self):
2640 2633 return u'<%s => %s >' % (self.user, self.permission)
2641 2634
2642 2635
2643 2636 class UserGroupRepoToPerm(Base, BaseModel):
2644 2637 __tablename__ = 'users_group_repo_to_perm'
2645 2638 __table_args__ = (
2646 2639 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2647 2640 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2648 2641 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2649 2642 )
2650 2643 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2651 2644 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2652 2645 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2653 2646 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2654 2647
2655 2648 users_group = relationship('UserGroup')
2656 2649 permission = relationship('Permission')
2657 2650 repository = relationship('Repository')
2658 2651
2659 2652 @classmethod
2660 2653 def create(cls, users_group, repository, permission):
2661 2654 n = cls()
2662 2655 n.users_group = users_group
2663 2656 n.repository = repository
2664 2657 n.permission = permission
2665 2658 Session().add(n)
2666 2659 return n
2667 2660
2668 2661 def __unicode__(self):
2669 2662 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2670 2663
2671 2664
2672 2665 class UserGroupUserGroupToPerm(Base, BaseModel):
2673 2666 __tablename__ = 'user_group_user_group_to_perm'
2674 2667 __table_args__ = (
2675 2668 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2676 2669 CheckConstraint('target_user_group_id != user_group_id'),
2677 2670 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2678 2671 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2679 2672 )
2680 2673 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)
2681 2674 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2682 2675 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2683 2676 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2684 2677
2685 2678 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2686 2679 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2687 2680 permission = relationship('Permission')
2688 2681
2689 2682 @classmethod
2690 2683 def create(cls, target_user_group, user_group, permission):
2691 2684 n = cls()
2692 2685 n.target_user_group = target_user_group
2693 2686 n.user_group = user_group
2694 2687 n.permission = permission
2695 2688 Session().add(n)
2696 2689 return n
2697 2690
2698 2691 def __unicode__(self):
2699 2692 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2700 2693
2701 2694
2702 2695 class UserGroupToPerm(Base, BaseModel):
2703 2696 __tablename__ = 'users_group_to_perm'
2704 2697 __table_args__ = (
2705 2698 UniqueConstraint('users_group_id', 'permission_id',),
2706 2699 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2707 2700 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2708 2701 )
2709 2702 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2710 2703 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2711 2704 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2712 2705
2713 2706 users_group = relationship('UserGroup')
2714 2707 permission = relationship('Permission')
2715 2708
2716 2709
2717 2710 class UserRepoGroupToPerm(Base, BaseModel):
2718 2711 __tablename__ = 'user_repo_group_to_perm'
2719 2712 __table_args__ = (
2720 2713 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2721 2714 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2722 2715 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2723 2716 )
2724 2717
2725 2718 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2726 2719 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2727 2720 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2728 2721 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2729 2722
2730 2723 user = relationship('User')
2731 2724 group = relationship('RepoGroup')
2732 2725 permission = relationship('Permission')
2733 2726
2734 2727 @classmethod
2735 2728 def create(cls, user, repository_group, permission):
2736 2729 n = cls()
2737 2730 n.user = user
2738 2731 n.group = repository_group
2739 2732 n.permission = permission
2740 2733 Session().add(n)
2741 2734 return n
2742 2735
2743 2736
2744 2737 class UserGroupRepoGroupToPerm(Base, BaseModel):
2745 2738 __tablename__ = 'users_group_repo_group_to_perm'
2746 2739 __table_args__ = (
2747 2740 UniqueConstraint('users_group_id', 'group_id'),
2748 2741 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2749 2742 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2750 2743 )
2751 2744
2752 2745 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)
2753 2746 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2754 2747 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2755 2748 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2756 2749
2757 2750 users_group = relationship('UserGroup')
2758 2751 permission = relationship('Permission')
2759 2752 group = relationship('RepoGroup')
2760 2753
2761 2754 @classmethod
2762 2755 def create(cls, user_group, repository_group, permission):
2763 2756 n = cls()
2764 2757 n.users_group = user_group
2765 2758 n.group = repository_group
2766 2759 n.permission = permission
2767 2760 Session().add(n)
2768 2761 return n
2769 2762
2770 2763 def __unicode__(self):
2771 2764 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2772 2765
2773 2766
2774 2767 class Statistics(Base, BaseModel):
2775 2768 __tablename__ = 'statistics'
2776 2769 __table_args__ = (
2777 2770 UniqueConstraint('repository_id'),
2778 2771 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2779 2772 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2780 2773 )
2781 2774 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2782 2775 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2783 2776 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2784 2777 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2785 2778 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2786 2779 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2787 2780
2788 2781 repository = relationship('Repository', single_parent=True)
2789 2782
2790 2783
2791 2784 class UserFollowing(Base, BaseModel):
2792 2785 __tablename__ = 'user_followings'
2793 2786 __table_args__ = (
2794 2787 UniqueConstraint('user_id', 'follows_repository_id'),
2795 2788 UniqueConstraint('user_id', 'follows_user_id'),
2796 2789 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2797 2790 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2798 2791 )
2799 2792
2800 2793 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2801 2794 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2802 2795 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2803 2796 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2804 2797 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2805 2798
2806 2799 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2807 2800
2808 2801 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2809 2802 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2810 2803
2811 2804 @classmethod
2812 2805 def get_repo_followers(cls, repo_id):
2813 2806 return cls.query().filter(cls.follows_repo_id == repo_id)
2814 2807
2815 2808
2816 2809 class CacheKey(Base, BaseModel):
2817 2810 __tablename__ = 'cache_invalidation'
2818 2811 __table_args__ = (
2819 2812 UniqueConstraint('cache_key'),
2820 2813 Index('key_idx', 'cache_key'),
2821 2814 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2822 2815 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2823 2816 )
2824 2817 CACHE_TYPE_ATOM = 'ATOM'
2825 2818 CACHE_TYPE_RSS = 'RSS'
2826 2819 CACHE_TYPE_README = 'README'
2827 2820
2828 2821 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2829 2822 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2830 2823 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2831 2824 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2832 2825
2833 2826 def __init__(self, cache_key, cache_args=''):
2834 2827 self.cache_key = cache_key
2835 2828 self.cache_args = cache_args
2836 2829 self.cache_active = False
2837 2830
2838 2831 def __unicode__(self):
2839 2832 return u"<%s('%s:%s[%s]')>" % (
2840 2833 self.__class__.__name__,
2841 2834 self.cache_id, self.cache_key, self.cache_active)
2842 2835
2843 2836 def _cache_key_partition(self):
2844 2837 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2845 2838 return prefix, repo_name, suffix
2846 2839
2847 2840 def get_prefix(self):
2848 2841 """
2849 2842 Try to extract prefix from existing cache key. The key could consist
2850 2843 of prefix, repo_name, suffix
2851 2844 """
2852 2845 # this returns prefix, repo_name, suffix
2853 2846 return self._cache_key_partition()[0]
2854 2847
2855 2848 def get_suffix(self):
2856 2849 """
2857 2850 get suffix that might have been used in _get_cache_key to
2858 2851 generate self.cache_key. Only used for informational purposes
2859 2852 in repo_edit.mako.
2860 2853 """
2861 2854 # prefix, repo_name, suffix
2862 2855 return self._cache_key_partition()[2]
2863 2856
2864 2857 @classmethod
2865 2858 def delete_all_cache(cls):
2866 2859 """
2867 2860 Delete all cache keys from database.
2868 2861 Should only be run when all instances are down and all entries
2869 2862 thus stale.
2870 2863 """
2871 2864 cls.query().delete()
2872 2865 Session().commit()
2873 2866
2874 2867 @classmethod
2875 2868 def get_cache_key(cls, repo_name, cache_type):
2876 2869 """
2877 2870
2878 2871 Generate a cache key for this process of RhodeCode instance.
2879 2872 Prefix most likely will be process id or maybe explicitly set
2880 2873 instance_id from .ini file.
2881 2874 """
2882 2875 import rhodecode
2883 2876 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2884 2877
2885 2878 repo_as_unicode = safe_unicode(repo_name)
2886 2879 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2887 2880 if cache_type else repo_as_unicode
2888 2881
2889 2882 return u'{}{}'.format(prefix, key)
2890 2883
2891 2884 @classmethod
2892 2885 def set_invalidate(cls, repo_name, delete=False):
2893 2886 """
2894 2887 Mark all caches of a repo as invalid in the database.
2895 2888 """
2896 2889
2897 2890 try:
2898 2891 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2899 2892 if delete:
2900 2893 log.debug('cache objects deleted for repo %s',
2901 2894 safe_str(repo_name))
2902 2895 qry.delete()
2903 2896 else:
2904 2897 log.debug('cache objects marked as invalid for repo %s',
2905 2898 safe_str(repo_name))
2906 2899 qry.update({"cache_active": False})
2907 2900
2908 2901 Session().commit()
2909 2902 except Exception:
2910 2903 log.exception(
2911 2904 'Cache key invalidation failed for repository %s',
2912 2905 safe_str(repo_name))
2913 2906 Session().rollback()
2914 2907
2915 2908 @classmethod
2916 2909 def get_active_cache(cls, cache_key):
2917 2910 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2918 2911 if inv_obj:
2919 2912 return inv_obj
2920 2913 return None
2921 2914
2922 2915 @classmethod
2923 2916 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2924 2917 thread_scoped=False):
2925 2918 """
2926 2919 @cache_region('long_term')
2927 2920 def _heavy_calculation(cache_key):
2928 2921 return 'result'
2929 2922
2930 2923 cache_context = CacheKey.repo_context_cache(
2931 2924 _heavy_calculation, repo_name, cache_type)
2932 2925
2933 2926 with cache_context as context:
2934 2927 context.invalidate()
2935 2928 computed = context.compute()
2936 2929
2937 2930 assert computed == 'result'
2938 2931 """
2939 2932 from rhodecode.lib import caches
2940 2933 return caches.InvalidationContext(
2941 2934 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2942 2935
2943 2936
2944 2937 class ChangesetComment(Base, BaseModel):
2945 2938 __tablename__ = 'changeset_comments'
2946 2939 __table_args__ = (
2947 2940 Index('cc_revision_idx', 'revision'),
2948 2941 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2949 2942 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2950 2943 )
2951 2944
2952 2945 COMMENT_OUTDATED = u'comment_outdated'
2953 2946 COMMENT_TYPE_NOTE = u'note'
2954 2947 COMMENT_TYPE_TODO = u'todo'
2955 2948 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
2956 2949
2957 2950 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2958 2951 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2959 2952 revision = Column('revision', String(40), nullable=True)
2960 2953 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2961 2954 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2962 2955 line_no = Column('line_no', Unicode(10), nullable=True)
2963 2956 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2964 2957 f_path = Column('f_path', Unicode(1000), nullable=True)
2965 2958 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2966 2959 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2967 2960 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2968 2961 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2969 2962 renderer = Column('renderer', Unicode(64), nullable=True)
2970 2963 display_state = Column('display_state', Unicode(128), nullable=True)
2971 2964
2972 2965 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
2973 2966 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
2974 2967 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
2975 2968 author = relationship('User', lazy='joined')
2976 2969 repo = relationship('Repository')
2977 2970 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
2978 2971 pull_request = relationship('PullRequest', lazy='joined')
2979 2972 pull_request_version = relationship('PullRequestVersion')
2980 2973
2981 2974 @classmethod
2982 2975 def get_users(cls, revision=None, pull_request_id=None):
2983 2976 """
2984 2977 Returns user associated with this ChangesetComment. ie those
2985 2978 who actually commented
2986 2979
2987 2980 :param cls:
2988 2981 :param revision:
2989 2982 """
2990 2983 q = Session().query(User)\
2991 2984 .join(ChangesetComment.author)
2992 2985 if revision:
2993 2986 q = q.filter(cls.revision == revision)
2994 2987 elif pull_request_id:
2995 2988 q = q.filter(cls.pull_request_id == pull_request_id)
2996 2989 return q.all()
2997 2990
2998 2991 @classmethod
2999 2992 def get_index_from_version(cls, pr_version, versions):
3000 2993 num_versions = [x.pull_request_version_id for x in versions]
3001 2994 try:
3002 2995 return num_versions.index(pr_version) +1
3003 2996 except (IndexError, ValueError):
3004 2997 return
3005 2998
3006 2999 @property
3007 3000 def outdated(self):
3008 3001 return self.display_state == self.COMMENT_OUTDATED
3009 3002
3010 3003 def outdated_at_version(self, version):
3011 3004 """
3012 3005 Checks if comment is outdated for given pull request version
3013 3006 """
3014 3007 return self.outdated and self.pull_request_version_id != version
3015 3008
3016 3009 def older_than_version(self, version):
3017 3010 """
3018 3011 Checks if comment is made from previous version than given
3019 3012 """
3020 3013 if version is None:
3021 3014 return self.pull_request_version_id is not None
3022 3015
3023 3016 return self.pull_request_version_id < version
3024 3017
3025 3018 @property
3026 3019 def resolved(self):
3027 3020 return self.resolved_by[0] if self.resolved_by else None
3028 3021
3029 3022 @property
3030 3023 def is_todo(self):
3031 3024 return self.comment_type == self.COMMENT_TYPE_TODO
3032 3025
3033 3026 def get_index_version(self, versions):
3034 3027 return self.get_index_from_version(
3035 3028 self.pull_request_version_id, versions)
3036 3029
3037 3030 def render(self, mentions=False):
3038 3031 from rhodecode.lib import helpers as h
3039 3032 return h.render(self.text, renderer=self.renderer, mentions=mentions)
3040 3033
3041 3034 def __repr__(self):
3042 3035 if self.comment_id:
3043 3036 return '<DB:Comment #%s>' % self.comment_id
3044 3037 else:
3045 3038 return '<DB:Comment at %#x>' % id(self)
3046 3039
3047 3040
3048 3041 class ChangesetStatus(Base, BaseModel):
3049 3042 __tablename__ = 'changeset_statuses'
3050 3043 __table_args__ = (
3051 3044 Index('cs_revision_idx', 'revision'),
3052 3045 Index('cs_version_idx', 'version'),
3053 3046 UniqueConstraint('repo_id', 'revision', 'version'),
3054 3047 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3055 3048 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3056 3049 )
3057 3050 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3058 3051 STATUS_APPROVED = 'approved'
3059 3052 STATUS_REJECTED = 'rejected'
3060 3053 STATUS_UNDER_REVIEW = 'under_review'
3061 3054
3062 3055 STATUSES = [
3063 3056 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3064 3057 (STATUS_APPROVED, _("Approved")),
3065 3058 (STATUS_REJECTED, _("Rejected")),
3066 3059 (STATUS_UNDER_REVIEW, _("Under Review")),
3067 3060 ]
3068 3061
3069 3062 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3070 3063 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3071 3064 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3072 3065 revision = Column('revision', String(40), nullable=False)
3073 3066 status = Column('status', String(128), nullable=False, default=DEFAULT)
3074 3067 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3075 3068 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3076 3069 version = Column('version', Integer(), nullable=False, default=0)
3077 3070 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3078 3071
3079 3072 author = relationship('User', lazy='joined')
3080 3073 repo = relationship('Repository')
3081 3074 comment = relationship('ChangesetComment', lazy='joined')
3082 3075 pull_request = relationship('PullRequest', lazy='joined')
3083 3076
3084 3077 def __unicode__(self):
3085 3078 return u"<%s('%s[v%s]:%s')>" % (
3086 3079 self.__class__.__name__,
3087 3080 self.status, self.version, self.author
3088 3081 )
3089 3082
3090 3083 @classmethod
3091 3084 def get_status_lbl(cls, value):
3092 3085 return dict(cls.STATUSES).get(value)
3093 3086
3094 3087 @property
3095 3088 def status_lbl(self):
3096 3089 return ChangesetStatus.get_status_lbl(self.status)
3097 3090
3098 3091
3099 3092 class _PullRequestBase(BaseModel):
3100 3093 """
3101 3094 Common attributes of pull request and version entries.
3102 3095 """
3103 3096
3104 3097 # .status values
3105 3098 STATUS_NEW = u'new'
3106 3099 STATUS_OPEN = u'open'
3107 3100 STATUS_CLOSED = u'closed'
3108 3101
3109 3102 title = Column('title', Unicode(255), nullable=True)
3110 3103 description = Column(
3111 3104 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3112 3105 nullable=True)
3113 3106 # new/open/closed status of pull request (not approve/reject/etc)
3114 3107 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3115 3108 created_on = Column(
3116 3109 'created_on', DateTime(timezone=False), nullable=False,
3117 3110 default=datetime.datetime.now)
3118 3111 updated_on = Column(
3119 3112 'updated_on', DateTime(timezone=False), nullable=False,
3120 3113 default=datetime.datetime.now)
3121 3114
3122 3115 @declared_attr
3123 3116 def user_id(cls):
3124 3117 return Column(
3125 3118 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3126 3119 unique=None)
3127 3120
3128 3121 # 500 revisions max
3129 3122 _revisions = Column(
3130 3123 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3131 3124
3132 3125 @declared_attr
3133 3126 def source_repo_id(cls):
3134 3127 # TODO: dan: rename column to source_repo_id
3135 3128 return Column(
3136 3129 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3137 3130 nullable=False)
3138 3131
3139 3132 source_ref = Column('org_ref', Unicode(255), nullable=False)
3140 3133
3141 3134 @declared_attr
3142 3135 def target_repo_id(cls):
3143 3136 # TODO: dan: rename column to target_repo_id
3144 3137 return Column(
3145 3138 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3146 3139 nullable=False)
3147 3140
3148 3141 target_ref = Column('other_ref', Unicode(255), nullable=False)
3149 3142 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3150 3143
3151 3144 # TODO: dan: rename column to last_merge_source_rev
3152 3145 _last_merge_source_rev = Column(
3153 3146 'last_merge_org_rev', String(40), nullable=True)
3154 3147 # TODO: dan: rename column to last_merge_target_rev
3155 3148 _last_merge_target_rev = Column(
3156 3149 'last_merge_other_rev', String(40), nullable=True)
3157 3150 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3158 3151 merge_rev = Column('merge_rev', String(40), nullable=True)
3159 3152
3160 3153 @hybrid_property
3161 3154 def revisions(self):
3162 3155 return self._revisions.split(':') if self._revisions else []
3163 3156
3164 3157 @revisions.setter
3165 3158 def revisions(self, val):
3166 3159 self._revisions = ':'.join(val)
3167 3160
3168 3161 @declared_attr
3169 3162 def author(cls):
3170 3163 return relationship('User', lazy='joined')
3171 3164
3172 3165 @declared_attr
3173 3166 def source_repo(cls):
3174 3167 return relationship(
3175 3168 'Repository',
3176 3169 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3177 3170
3178 3171 @property
3179 3172 def source_ref_parts(self):
3180 3173 return self.unicode_to_reference(self.source_ref)
3181 3174
3182 3175 @declared_attr
3183 3176 def target_repo(cls):
3184 3177 return relationship(
3185 3178 'Repository',
3186 3179 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3187 3180
3188 3181 @property
3189 3182 def target_ref_parts(self):
3190 3183 return self.unicode_to_reference(self.target_ref)
3191 3184
3192 3185 @property
3193 3186 def shadow_merge_ref(self):
3194 3187 return self.unicode_to_reference(self._shadow_merge_ref)
3195 3188
3196 3189 @shadow_merge_ref.setter
3197 3190 def shadow_merge_ref(self, ref):
3198 3191 self._shadow_merge_ref = self.reference_to_unicode(ref)
3199 3192
3200 3193 def unicode_to_reference(self, raw):
3201 3194 """
3202 3195 Convert a unicode (or string) to a reference object.
3203 3196 If unicode evaluates to False it returns None.
3204 3197 """
3205 3198 if raw:
3206 3199 refs = raw.split(':')
3207 3200 return Reference(*refs)
3208 3201 else:
3209 3202 return None
3210 3203
3211 3204 def reference_to_unicode(self, ref):
3212 3205 """
3213 3206 Convert a reference object to unicode.
3214 3207 If reference is None it returns None.
3215 3208 """
3216 3209 if ref:
3217 3210 return u':'.join(ref)
3218 3211 else:
3219 3212 return None
3220 3213
3221 3214 def get_api_data(self):
3222 3215 from rhodecode.model.pull_request import PullRequestModel
3223 3216 pull_request = self
3224 3217 merge_status = PullRequestModel().merge_status(pull_request)
3225 3218
3226 3219 pull_request_url = url(
3227 3220 'pullrequest_show', repo_name=self.target_repo.repo_name,
3228 3221 pull_request_id=self.pull_request_id, qualified=True)
3229 3222
3230 3223 merge_data = {
3231 3224 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3232 3225 'reference': (
3233 3226 pull_request.shadow_merge_ref._asdict()
3234 3227 if pull_request.shadow_merge_ref else None),
3235 3228 }
3236 3229
3237 3230 data = {
3238 3231 'pull_request_id': pull_request.pull_request_id,
3239 3232 'url': pull_request_url,
3240 3233 'title': pull_request.title,
3241 3234 'description': pull_request.description,
3242 3235 'status': pull_request.status,
3243 3236 'created_on': pull_request.created_on,
3244 3237 'updated_on': pull_request.updated_on,
3245 3238 'commit_ids': pull_request.revisions,
3246 3239 'review_status': pull_request.calculated_review_status(),
3247 3240 'mergeable': {
3248 3241 'status': merge_status[0],
3249 3242 'message': unicode(merge_status[1]),
3250 3243 },
3251 3244 'source': {
3252 3245 'clone_url': pull_request.source_repo.clone_url(),
3253 3246 'repository': pull_request.source_repo.repo_name,
3254 3247 'reference': {
3255 3248 'name': pull_request.source_ref_parts.name,
3256 3249 'type': pull_request.source_ref_parts.type,
3257 3250 'commit_id': pull_request.source_ref_parts.commit_id,
3258 3251 },
3259 3252 },
3260 3253 'target': {
3261 3254 'clone_url': pull_request.target_repo.clone_url(),
3262 3255 'repository': pull_request.target_repo.repo_name,
3263 3256 'reference': {
3264 3257 'name': pull_request.target_ref_parts.name,
3265 3258 'type': pull_request.target_ref_parts.type,
3266 3259 'commit_id': pull_request.target_ref_parts.commit_id,
3267 3260 },
3268 3261 },
3269 3262 'merge': merge_data,
3270 3263 'author': pull_request.author.get_api_data(include_secrets=False,
3271 3264 details='basic'),
3272 3265 'reviewers': [
3273 3266 {
3274 3267 'user': reviewer.get_api_data(include_secrets=False,
3275 3268 details='basic'),
3276 3269 'reasons': reasons,
3277 3270 'review_status': st[0][1].status if st else 'not_reviewed',
3278 3271 }
3279 3272 for reviewer, reasons, st in pull_request.reviewers_statuses()
3280 3273 ]
3281 3274 }
3282 3275
3283 3276 return data
3284 3277
3285 3278
3286 3279 class PullRequest(Base, _PullRequestBase):
3287 3280 __tablename__ = 'pull_requests'
3288 3281 __table_args__ = (
3289 3282 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3290 3283 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3291 3284 )
3292 3285
3293 3286 pull_request_id = Column(
3294 3287 'pull_request_id', Integer(), nullable=False, primary_key=True)
3295 3288
3296 3289 def __repr__(self):
3297 3290 if self.pull_request_id:
3298 3291 return '<DB:PullRequest #%s>' % self.pull_request_id
3299 3292 else:
3300 3293 return '<DB:PullRequest at %#x>' % id(self)
3301 3294
3302 3295 reviewers = relationship('PullRequestReviewers',
3303 3296 cascade="all, delete, delete-orphan")
3304 3297 statuses = relationship('ChangesetStatus')
3305 3298 comments = relationship('ChangesetComment',
3306 3299 cascade="all, delete, delete-orphan")
3307 3300 versions = relationship('PullRequestVersion',
3308 3301 cascade="all, delete, delete-orphan",
3309 3302 lazy='dynamic')
3310 3303
3311 3304 @classmethod
3312 3305 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3313 3306 internal_methods=None):
3314 3307
3315 3308 class PullRequestDisplay(object):
3316 3309 """
3317 3310 Special object wrapper for showing PullRequest data via Versions
3318 3311 It mimics PR object as close as possible. This is read only object
3319 3312 just for display
3320 3313 """
3321 3314
3322 3315 def __init__(self, attrs, internal=None):
3323 3316 self.attrs = attrs
3324 3317 # internal have priority over the given ones via attrs
3325 3318 self.internal = internal or ['versions']
3326 3319
3327 3320 def __getattr__(self, item):
3328 3321 if item in self.internal:
3329 3322 return getattr(self, item)
3330 3323 try:
3331 3324 return self.attrs[item]
3332 3325 except KeyError:
3333 3326 raise AttributeError(
3334 3327 '%s object has no attribute %s' % (self, item))
3335 3328
3336 3329 def __repr__(self):
3337 3330 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3338 3331
3339 3332 def versions(self):
3340 3333 return pull_request_obj.versions.order_by(
3341 3334 PullRequestVersion.pull_request_version_id).all()
3342 3335
3343 3336 def is_closed(self):
3344 3337 return pull_request_obj.is_closed()
3345 3338
3346 3339 @property
3347 3340 def pull_request_version_id(self):
3348 3341 return getattr(pull_request_obj, 'pull_request_version_id', None)
3349 3342
3350 3343 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3351 3344
3352 3345 attrs.author = StrictAttributeDict(
3353 3346 pull_request_obj.author.get_api_data())
3354 3347 if pull_request_obj.target_repo:
3355 3348 attrs.target_repo = StrictAttributeDict(
3356 3349 pull_request_obj.target_repo.get_api_data())
3357 3350 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3358 3351
3359 3352 if pull_request_obj.source_repo:
3360 3353 attrs.source_repo = StrictAttributeDict(
3361 3354 pull_request_obj.source_repo.get_api_data())
3362 3355 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3363 3356
3364 3357 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3365 3358 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3366 3359 attrs.revisions = pull_request_obj.revisions
3367 3360
3368 3361 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3369 3362
3370 3363 return PullRequestDisplay(attrs, internal=internal_methods)
3371 3364
3372 3365 def is_closed(self):
3373 3366 return self.status == self.STATUS_CLOSED
3374 3367
3375 3368 def __json__(self):
3376 3369 return {
3377 3370 'revisions': self.revisions,
3378 3371 }
3379 3372
3380 3373 def calculated_review_status(self):
3381 3374 from rhodecode.model.changeset_status import ChangesetStatusModel
3382 3375 return ChangesetStatusModel().calculated_review_status(self)
3383 3376
3384 3377 def reviewers_statuses(self):
3385 3378 from rhodecode.model.changeset_status import ChangesetStatusModel
3386 3379 return ChangesetStatusModel().reviewers_statuses(self)
3387 3380
3388 3381 @property
3389 3382 def workspace_id(self):
3390 3383 from rhodecode.model.pull_request import PullRequestModel
3391 3384 return PullRequestModel()._workspace_id(self)
3392 3385
3393 3386 def get_shadow_repo(self):
3394 3387 workspace_id = self.workspace_id
3395 3388 vcs_obj = self.target_repo.scm_instance()
3396 3389 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3397 3390 workspace_id)
3398 3391 return vcs_obj._get_shadow_instance(shadow_repository_path)
3399 3392
3400 3393
3401 3394 class PullRequestVersion(Base, _PullRequestBase):
3402 3395 __tablename__ = 'pull_request_versions'
3403 3396 __table_args__ = (
3404 3397 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3405 3398 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3406 3399 )
3407 3400
3408 3401 pull_request_version_id = Column(
3409 3402 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3410 3403 pull_request_id = Column(
3411 3404 'pull_request_id', Integer(),
3412 3405 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3413 3406 pull_request = relationship('PullRequest')
3414 3407
3415 3408 def __repr__(self):
3416 3409 if self.pull_request_version_id:
3417 3410 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3418 3411 else:
3419 3412 return '<DB:PullRequestVersion at %#x>' % id(self)
3420 3413
3421 3414 @property
3422 3415 def reviewers(self):
3423 3416 return self.pull_request.reviewers
3424 3417
3425 3418 @property
3426 3419 def versions(self):
3427 3420 return self.pull_request.versions
3428 3421
3429 3422 def is_closed(self):
3430 3423 # calculate from original
3431 3424 return self.pull_request.status == self.STATUS_CLOSED
3432 3425
3433 3426 def calculated_review_status(self):
3434 3427 return self.pull_request.calculated_review_status()
3435 3428
3436 3429 def reviewers_statuses(self):
3437 3430 return self.pull_request.reviewers_statuses()
3438 3431
3439 3432
3440 3433 class PullRequestReviewers(Base, BaseModel):
3441 3434 __tablename__ = 'pull_request_reviewers'
3442 3435 __table_args__ = (
3443 3436 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3444 3437 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3445 3438 )
3446 3439
3447 3440 def __init__(self, user=None, pull_request=None, reasons=None):
3448 3441 self.user = user
3449 3442 self.pull_request = pull_request
3450 3443 self.reasons = reasons or []
3451 3444
3452 3445 @hybrid_property
3453 3446 def reasons(self):
3454 3447 if not self._reasons:
3455 3448 return []
3456 3449 return self._reasons
3457 3450
3458 3451 @reasons.setter
3459 3452 def reasons(self, val):
3460 3453 val = val or []
3461 3454 if any(not isinstance(x, basestring) for x in val):
3462 3455 raise Exception('invalid reasons type, must be list of strings')
3463 3456 self._reasons = val
3464 3457
3465 3458 pull_requests_reviewers_id = Column(
3466 3459 'pull_requests_reviewers_id', Integer(), nullable=False,
3467 3460 primary_key=True)
3468 3461 pull_request_id = Column(
3469 3462 "pull_request_id", Integer(),
3470 3463 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3471 3464 user_id = Column(
3472 3465 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3473 3466 _reasons = Column(
3474 3467 'reason', MutationList.as_mutable(
3475 3468 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3476 3469
3477 3470 user = relationship('User')
3478 3471 pull_request = relationship('PullRequest')
3479 3472
3480 3473
3481 3474 class Notification(Base, BaseModel):
3482 3475 __tablename__ = 'notifications'
3483 3476 __table_args__ = (
3484 3477 Index('notification_type_idx', 'type'),
3485 3478 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3486 3479 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3487 3480 )
3488 3481
3489 3482 TYPE_CHANGESET_COMMENT = u'cs_comment'
3490 3483 TYPE_MESSAGE = u'message'
3491 3484 TYPE_MENTION = u'mention'
3492 3485 TYPE_REGISTRATION = u'registration'
3493 3486 TYPE_PULL_REQUEST = u'pull_request'
3494 3487 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3495 3488
3496 3489 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3497 3490 subject = Column('subject', Unicode(512), nullable=True)
3498 3491 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3499 3492 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3500 3493 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3501 3494 type_ = Column('type', Unicode(255))
3502 3495
3503 3496 created_by_user = relationship('User')
3504 3497 notifications_to_users = relationship('UserNotification', lazy='joined',
3505 3498 cascade="all, delete, delete-orphan")
3506 3499
3507 3500 @property
3508 3501 def recipients(self):
3509 3502 return [x.user for x in UserNotification.query()\
3510 3503 .filter(UserNotification.notification == self)\
3511 3504 .order_by(UserNotification.user_id.asc()).all()]
3512 3505
3513 3506 @classmethod
3514 3507 def create(cls, created_by, subject, body, recipients, type_=None):
3515 3508 if type_ is None:
3516 3509 type_ = Notification.TYPE_MESSAGE
3517 3510
3518 3511 notification = cls()
3519 3512 notification.created_by_user = created_by
3520 3513 notification.subject = subject
3521 3514 notification.body = body
3522 3515 notification.type_ = type_
3523 3516 notification.created_on = datetime.datetime.now()
3524 3517
3525 3518 for u in recipients:
3526 3519 assoc = UserNotification()
3527 3520 assoc.notification = notification
3528 3521
3529 3522 # if created_by is inside recipients mark his notification
3530 3523 # as read
3531 3524 if u.user_id == created_by.user_id:
3532 3525 assoc.read = True
3533 3526
3534 3527 u.notifications.append(assoc)
3535 3528 Session().add(notification)
3536 3529
3537 3530 return notification
3538 3531
3539 3532 @property
3540 3533 def description(self):
3541 3534 from rhodecode.model.notification import NotificationModel
3542 3535 return NotificationModel().make_description(self)
3543 3536
3544 3537
3545 3538 class UserNotification(Base, BaseModel):
3546 3539 __tablename__ = 'user_to_notification'
3547 3540 __table_args__ = (
3548 3541 UniqueConstraint('user_id', 'notification_id'),
3549 3542 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3550 3543 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3551 3544 )
3552 3545 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3553 3546 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3554 3547 read = Column('read', Boolean, default=False)
3555 3548 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3556 3549
3557 3550 user = relationship('User', lazy="joined")
3558 3551 notification = relationship('Notification', lazy="joined",
3559 3552 order_by=lambda: Notification.created_on.desc(),)
3560 3553
3561 3554 def mark_as_read(self):
3562 3555 self.read = True
3563 3556 Session().add(self)
3564 3557
3565 3558
3566 3559 class Gist(Base, BaseModel):
3567 3560 __tablename__ = 'gists'
3568 3561 __table_args__ = (
3569 3562 Index('g_gist_access_id_idx', 'gist_access_id'),
3570 3563 Index('g_created_on_idx', 'created_on'),
3571 3564 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3572 3565 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3573 3566 )
3574 3567 GIST_PUBLIC = u'public'
3575 3568 GIST_PRIVATE = u'private'
3576 3569 DEFAULT_FILENAME = u'gistfile1.txt'
3577 3570
3578 3571 ACL_LEVEL_PUBLIC = u'acl_public'
3579 3572 ACL_LEVEL_PRIVATE = u'acl_private'
3580 3573
3581 3574 gist_id = Column('gist_id', Integer(), primary_key=True)
3582 3575 gist_access_id = Column('gist_access_id', Unicode(250))
3583 3576 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3584 3577 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3585 3578 gist_expires = Column('gist_expires', Float(53), nullable=False)
3586 3579 gist_type = Column('gist_type', Unicode(128), nullable=False)
3587 3580 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3588 3581 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3589 3582 acl_level = Column('acl_level', Unicode(128), nullable=True)
3590 3583
3591 3584 owner = relationship('User')
3592 3585
3593 3586 def __repr__(self):
3594 3587 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3595 3588
3596 3589 @classmethod
3597 3590 def get_or_404(cls, id_):
3598 3591 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3599 3592 if not res:
3600 3593 raise HTTPNotFound
3601 3594 return res
3602 3595
3603 3596 @classmethod
3604 3597 def get_by_access_id(cls, gist_access_id):
3605 3598 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3606 3599
3607 3600 def gist_url(self):
3608 3601 import rhodecode
3609 3602 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3610 3603 if alias_url:
3611 3604 return alias_url.replace('{gistid}', self.gist_access_id)
3612 3605
3613 3606 return url('gist', gist_id=self.gist_access_id, qualified=True)
3614 3607
3615 3608 @classmethod
3616 3609 def base_path(cls):
3617 3610 """
3618 3611 Returns base path when all gists are stored
3619 3612
3620 3613 :param cls:
3621 3614 """
3622 3615 from rhodecode.model.gist import GIST_STORE_LOC
3623 3616 q = Session().query(RhodeCodeUi)\
3624 3617 .filter(RhodeCodeUi.ui_key == URL_SEP)
3625 3618 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3626 3619 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3627 3620
3628 3621 def get_api_data(self):
3629 3622 """
3630 3623 Common function for generating gist related data for API
3631 3624 """
3632 3625 gist = self
3633 3626 data = {
3634 3627 'gist_id': gist.gist_id,
3635 3628 'type': gist.gist_type,
3636 3629 'access_id': gist.gist_access_id,
3637 3630 'description': gist.gist_description,
3638 3631 'url': gist.gist_url(),
3639 3632 'expires': gist.gist_expires,
3640 3633 'created_on': gist.created_on,
3641 3634 'modified_at': gist.modified_at,
3642 3635 'content': None,
3643 3636 'acl_level': gist.acl_level,
3644 3637 }
3645 3638 return data
3646 3639
3647 3640 def __json__(self):
3648 3641 data = dict(
3649 3642 )
3650 3643 data.update(self.get_api_data())
3651 3644 return data
3652 3645 # SCM functions
3653 3646
3654 3647 def scm_instance(self, **kwargs):
3655 3648 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3656 3649 return get_vcs_instance(
3657 3650 repo_path=safe_str(full_repo_path), create=False)
3658 3651
3659 3652
3660 3653 class ExternalIdentity(Base, BaseModel):
3661 3654 __tablename__ = 'external_identities'
3662 3655 __table_args__ = (
3663 3656 Index('local_user_id_idx', 'local_user_id'),
3664 3657 Index('external_id_idx', 'external_id'),
3665 3658 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3666 3659 'mysql_charset': 'utf8'})
3667 3660
3668 3661 external_id = Column('external_id', Unicode(255), default=u'',
3669 3662 primary_key=True)
3670 3663 external_username = Column('external_username', Unicode(1024), default=u'')
3671 3664 local_user_id = Column('local_user_id', Integer(),
3672 3665 ForeignKey('users.user_id'), primary_key=True)
3673 3666 provider_name = Column('provider_name', Unicode(255), default=u'',
3674 3667 primary_key=True)
3675 3668 access_token = Column('access_token', String(1024), default=u'')
3676 3669 alt_token = Column('alt_token', String(1024), default=u'')
3677 3670 token_secret = Column('token_secret', String(1024), default=u'')
3678 3671
3679 3672 @classmethod
3680 3673 def by_external_id_and_provider(cls, external_id, provider_name,
3681 3674 local_user_id=None):
3682 3675 """
3683 3676 Returns ExternalIdentity instance based on search params
3684 3677
3685 3678 :param external_id:
3686 3679 :param provider_name:
3687 3680 :return: ExternalIdentity
3688 3681 """
3689 3682 query = cls.query()
3690 3683 query = query.filter(cls.external_id == external_id)
3691 3684 query = query.filter(cls.provider_name == provider_name)
3692 3685 if local_user_id:
3693 3686 query = query.filter(cls.local_user_id == local_user_id)
3694 3687 return query.first()
3695 3688
3696 3689 @classmethod
3697 3690 def user_by_external_id_and_provider(cls, external_id, provider_name):
3698 3691 """
3699 3692 Returns User instance based on search params
3700 3693
3701 3694 :param external_id:
3702 3695 :param provider_name:
3703 3696 :return: User
3704 3697 """
3705 3698 query = User.query()
3706 3699 query = query.filter(cls.external_id == external_id)
3707 3700 query = query.filter(cls.provider_name == provider_name)
3708 3701 query = query.filter(User.user_id == cls.local_user_id)
3709 3702 return query.first()
3710 3703
3711 3704 @classmethod
3712 3705 def by_local_user_id(cls, local_user_id):
3713 3706 """
3714 3707 Returns all tokens for user
3715 3708
3716 3709 :param local_user_id:
3717 3710 :return: ExternalIdentity
3718 3711 """
3719 3712 query = cls.query()
3720 3713 query = query.filter(cls.local_user_id == local_user_id)
3721 3714 return query
3722 3715
3723 3716
3724 3717 class Integration(Base, BaseModel):
3725 3718 __tablename__ = 'integrations'
3726 3719 __table_args__ = (
3727 3720 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3728 3721 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3729 3722 )
3730 3723
3731 3724 integration_id = Column('integration_id', Integer(), primary_key=True)
3732 3725 integration_type = Column('integration_type', String(255))
3733 3726 enabled = Column('enabled', Boolean(), nullable=False)
3734 3727 name = Column('name', String(255), nullable=False)
3735 3728 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3736 3729 default=False)
3737 3730
3738 3731 settings = Column(
3739 3732 'settings_json', MutationObj.as_mutable(
3740 3733 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3741 3734 repo_id = Column(
3742 3735 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3743 3736 nullable=True, unique=None, default=None)
3744 3737 repo = relationship('Repository', lazy='joined')
3745 3738
3746 3739 repo_group_id = Column(
3747 3740 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3748 3741 nullable=True, unique=None, default=None)
3749 3742 repo_group = relationship('RepoGroup', lazy='joined')
3750 3743
3751 3744 @property
3752 3745 def scope(self):
3753 3746 if self.repo:
3754 3747 return repr(self.repo)
3755 3748 if self.repo_group:
3756 3749 if self.child_repos_only:
3757 3750 return repr(self.repo_group) + ' (child repos only)'
3758 3751 else:
3759 3752 return repr(self.repo_group) + ' (recursive)'
3760 3753 if self.child_repos_only:
3761 3754 return 'root_repos'
3762 3755 return 'global'
3763 3756
3764 3757 def __repr__(self):
3765 3758 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3766 3759
3767 3760
3768 3761 class RepoReviewRuleUser(Base, BaseModel):
3769 3762 __tablename__ = 'repo_review_rules_users'
3770 3763 __table_args__ = (
3771 3764 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3772 3765 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3773 3766 )
3774 3767 repo_review_rule_user_id = Column(
3775 3768 'repo_review_rule_user_id', Integer(), primary_key=True)
3776 3769 repo_review_rule_id = Column("repo_review_rule_id",
3777 3770 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3778 3771 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3779 3772 nullable=False)
3780 3773 user = relationship('User')
3781 3774
3782 3775
3783 3776 class RepoReviewRuleUserGroup(Base, BaseModel):
3784 3777 __tablename__ = 'repo_review_rules_users_groups'
3785 3778 __table_args__ = (
3786 3779 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3787 3780 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3788 3781 )
3789 3782 repo_review_rule_users_group_id = Column(
3790 3783 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3791 3784 repo_review_rule_id = Column("repo_review_rule_id",
3792 3785 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3793 3786 users_group_id = Column("users_group_id", Integer(),
3794 3787 ForeignKey('users_groups.users_group_id'), nullable=False)
3795 3788 users_group = relationship('UserGroup')
3796 3789
3797 3790
3798 3791 class RepoReviewRule(Base, BaseModel):
3799 3792 __tablename__ = 'repo_review_rules'
3800 3793 __table_args__ = (
3801 3794 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3802 3795 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3803 3796 )
3804 3797
3805 3798 repo_review_rule_id = Column(
3806 3799 'repo_review_rule_id', Integer(), primary_key=True)
3807 3800 repo_id = Column(
3808 3801 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3809 3802 repo = relationship('Repository', backref='review_rules')
3810 3803
3811 3804 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3812 3805 default=u'*') # glob
3813 3806 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3814 3807 default=u'*') # glob
3815 3808
3816 3809 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3817 3810 nullable=False, default=False)
3818 3811 rule_users = relationship('RepoReviewRuleUser')
3819 3812 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3820 3813
3821 3814 @hybrid_property
3822 3815 def branch_pattern(self):
3823 3816 return self._branch_pattern or '*'
3824 3817
3825 3818 def _validate_glob(self, value):
3826 3819 re.compile('^' + glob2re(value) + '$')
3827 3820
3828 3821 @branch_pattern.setter
3829 3822 def branch_pattern(self, value):
3830 3823 self._validate_glob(value)
3831 3824 self._branch_pattern = value or '*'
3832 3825
3833 3826 @hybrid_property
3834 3827 def file_pattern(self):
3835 3828 return self._file_pattern or '*'
3836 3829
3837 3830 @file_pattern.setter
3838 3831 def file_pattern(self, value):
3839 3832 self._validate_glob(value)
3840 3833 self._file_pattern = value or '*'
3841 3834
3842 3835 def matches(self, branch, files_changed):
3843 3836 """
3844 3837 Check if this review rule matches a branch/files in a pull request
3845 3838
3846 3839 :param branch: branch name for the commit
3847 3840 :param files_changed: list of file paths changed in the pull request
3848 3841 """
3849 3842
3850 3843 branch = branch or ''
3851 3844 files_changed = files_changed or []
3852 3845
3853 3846 branch_matches = True
3854 3847 if branch:
3855 3848 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3856 3849 branch_matches = bool(branch_regex.search(branch))
3857 3850
3858 3851 files_matches = True
3859 3852 if self.file_pattern != '*':
3860 3853 files_matches = False
3861 3854 file_regex = re.compile(glob2re(self.file_pattern))
3862 3855 for filename in files_changed:
3863 3856 if file_regex.search(filename):
3864 3857 files_matches = True
3865 3858 break
3866 3859
3867 3860 return branch_matches and files_matches
3868 3861
3869 3862 @property
3870 3863 def review_users(self):
3871 3864 """ Returns the users which this rule applies to """
3872 3865
3873 3866 users = set()
3874 3867 users |= set([
3875 3868 rule_user.user for rule_user in self.rule_users
3876 3869 if rule_user.user.active])
3877 3870 users |= set(
3878 3871 member.user
3879 3872 for rule_user_group in self.rule_user_groups
3880 3873 for member in rule_user_group.users_group.members
3881 3874 if member.user.active
3882 3875 )
3883 3876 return users
3884 3877
3885 3878 def __repr__(self):
3886 3879 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3887 3880 self.repo_review_rule_id, self.repo)
3888 3881
3889 3882
3890 3883 class DbMigrateVersion(Base, BaseModel):
3891 3884 __tablename__ = 'db_migrate_version'
3892 3885 __table_args__ = (
3893 3886 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3894 3887 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3895 3888 )
3896 3889 repository_id = Column('repository_id', String(250), primary_key=True)
3897 3890 repository_path = Column('repository_path', Text)
3898 3891 version = Column('version', Integer)
3899 3892
3900 3893
3901 3894 class DbSession(Base, BaseModel):
3902 3895 __tablename__ = 'db_session'
3903 3896 __table_args__ = (
3904 3897 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3905 3898 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3906 3899 )
3907 3900
3908 3901 def __repr__(self):
3909 3902 return '<DB:DbSession({})>'.format(self.id)
3910 3903
3911 3904 id = Column('id', Integer())
3912 3905 namespace = Column('namespace', String(255), primary_key=True)
3913 3906 accessed = Column('accessed', DateTime, nullable=False)
3914 3907 created = Column('created', DateTime, nullable=False)
3915 3908 data = Column('data', PickleType, nullable=False)
@@ -1,106 +1,80 b''
1 1 <div class="panel panel-default">
2 2 <div class="panel-heading">
3 3 <h3 class="panel-title">${_('Authentication Tokens')}</h3>
4 4 </div>
5 5 <div class="panel-body">
6 6 <p>
7 ${_('Built-in tokens can be used to authenticate with all possible options.')}<br/>
8 7 ${_('Each token can have a role. VCS tokens can be used together with the authtoken auth plugin for git/hg/svn operations.')}
9 8 </p>
10 9 <table class="rctable auth_tokens">
11 <tr>
12 <td class="truncate-wrap td-authtoken"><div class="user_auth_tokens truncate autoexpand"><code>${c.user.api_key}</code></div></td>
13 <td class="td-tags">
14 <span class="tag disabled">${_('Built-in')}</span>
15 </td>
16 <td class="td-tags">
17 % for token in c.user.builtin_token_roles:
18 <span class="tag disabled">
19 ${token}
20 </span>
21 % endfor
22 </td>
23 <td class="td-exp">${_('expires')}: ${_('never')}</td>
24 <td class="td-action">
25 ${h.secure_form(url('my_account_auth_tokens'),method='delete')}
26 ${h.hidden('del_auth_token',c.user.api_key)}
27 ${h.hidden('del_auth_token_builtin',1)}
28 <button class="btn-link btn-danger" type="submit"
29 onclick="return confirm('${_('Confirm to reset this auth token: %s') % c.user.api_key}');">
30 <i class="icon-refresh"></i>
31 ${_('Reset')}
32 </button>
33 ${h.end_form()}
34 </td>
35 </tr>
36 10 %if c.user_auth_tokens:
37 11 %for auth_token in c.user_auth_tokens:
38 12 <tr class="${'expired' if auth_token.expired else ''}">
39 13 <td class="truncate-wrap td-authtoken"><div class="user_auth_tokens truncate autoexpand"><code>${auth_token.api_key}</code></div></td>
40 14 <td class="td-wrap">${auth_token.description}</td>
41 15 <td class="td-tags">
42 16 <span class="tag disabled">${auth_token.role_humanized}</span>
43 17 </td>
44 18 <td class="td-exp">
45 19 %if auth_token.expires == -1:
46 20 ${_('expires')}: ${_('never')}
47 21 %else:
48 22 %if auth_token.expired:
49 23 ${_('expired')}: ${h.age_component(h.time_to_utcdatetime(auth_token.expires))}
50 24 %else:
51 25 ${_('expires')}: ${h.age_component(h.time_to_utcdatetime(auth_token.expires))}
52 26 %endif
53 27 %endif
54 28 </td>
55 29 <td class="td-action">
56 30 ${h.secure_form(url('my_account_auth_tokens'),method='delete')}
57 31 ${h.hidden('del_auth_token',auth_token.api_key)}
58 32 <button class="btn btn-link btn-danger" type="submit"
59 33 onclick="return confirm('${_('Confirm to remove this auth token: %s') % auth_token.api_key}');">
60 34 ${_('Delete')}
61 35 </button>
62 36 ${h.end_form()}
63 37 </td>
64 38 </tr>
65 39 %endfor
66 40 %else:
67 41 <tr><td><div class="ip">${_('No additional auth token specified')}</div></td></tr>
68 42 %endif
69 43 </table>
70 44
71 45 <div class="user_auth_tokens">
72 46 ${h.secure_form(url('my_account_auth_tokens'), method='post')}
73 47 <div class="form form-vertical">
74 48 <!-- fields -->
75 49 <div class="fields">
76 50 <div class="field">
77 51 <div class="label">
78 52 <label for="new_email">${_('New authentication token')}:</label>
79 53 </div>
80 54 <div class="input">
81 55 ${h.text('description', placeholder=_('Description'))}
82 56 ${h.select('lifetime', '', c.lifetime_options)}
83 57 ${h.select('role', '', c.role_options)}
84 58 </div>
85 59 </div>
86 60 <div class="buttons">
87 61 ${h.submit('save',_('Add'),class_="btn")}
88 62 ${h.reset('reset',_('Reset'),class_="btn")}
89 63 </div>
90 64 </div>
91 65 </div>
92 66 ${h.end_form()}
93 67 </div>
94 68 </div>
95 69 </div>
96 70 <script>
97 71 $(document).ready(function(){
98 72 var select2Options = {
99 73 'containerCssClass': "drop-menu",
100 74 'dropdownCssClass': "drop-menu-dropdown",
101 75 'dropdownAutoWidth': true
102 76 };
103 77 $("#lifetime").select2(select2Options);
104 78 $("#role").select2(select2Options);
105 79 });
106 80 </script>
@@ -1,107 +1,83 b''
1 1 <div class="panel panel-default">
2 2 <div class="panel-heading">
3 3 <h3 class="panel-title">${_('Authentication Access Tokens')}</h3>
4 4 </div>
5 5 <div class="panel-body">
6 6 <div class="apikeys_wrap">
7 7 <table class="rctable auth_tokens">
8 <tr>
9 <td class="truncate-wrap td-authtoken"><div class="user_auth_tokens truncate autoexpand"><code>${c.user.api_key}</code></div></td>
10 <td class="td-tags">
11 <span class="tag disabled">${_('Built-in')}</span>
12 </td>
13 <td class="td-tags">
14 % for token in c.user.builtin_token_roles:
15 <span class="tag disabled">
16 ${token}
17 </span>
18 % endfor
19 </td>
20 <td class="td-exp">${_('expires')}: ${_('never')}</td>
21 <td class="td-action">
22 ${h.secure_form(url('edit_user_auth_tokens', user_id=c.user.user_id),method='delete')}
23 ${h.hidden('del_auth_token',c.user.api_key)}
24 ${h.hidden('del_auth_token_builtin',1)}
25 <button class="btn btn-link btn-danger" type="submit"
26 onclick="return confirm('${_('Confirm to reset this auth token: %s') % c.user.api_key}');">
27 ${_('Reset')}
28 </button>
29 ${h.end_form()}
30 </td>
31 </tr>
32 8 %if c.user_auth_tokens:
33 9 %for auth_token in c.user_auth_tokens:
34 10 <tr class="${'expired' if auth_token.expired else ''}">
35 11 <td class="truncate-wrap td-authtoken"><div class="user_auth_tokens truncate autoexpand"><code>${auth_token.api_key}</code></div></td>
36 12 <td class="td-wrap">${auth_token.description}</td>
37 13 <td class="td-tags">
38 14 <span class="tag">${auth_token.role_humanized}</span>
39 15 </td>
40 16 <td class="td-exp">
41 17 %if auth_token.expires == -1:
42 18 ${_('expires')}: ${_('never')}
43 19 %else:
44 20 %if auth_token.expired:
45 21 ${_('expired')}: ${h.age_component(h.time_to_utcdatetime(auth_token.expires))}
46 22 %else:
47 23 ${_('expires')}: ${h.age_component(h.time_to_utcdatetime(auth_token.expires))}
48 24 %endif
49 25 %endif
50 26 </td>
51 27 <td>
52 28 ${h.secure_form(url('edit_user_auth_tokens', user_id=c.user.user_id),method='delete')}
53 29 ${h.hidden('del_auth_token',auth_token.api_key)}
54 30 <button class="btn btn-link btn-danger" type="submit"
55 31 onclick="return confirm('${_('Confirm to remove this auth token: %s') % auth_token.api_key}');">
56 32 ${_('Delete')}
57 33 </button>
58 34 ${h.end_form()}
59 35 </td>
60 36 </tr>
61 37 %endfor
62 38 %else:
63 39 <tr><td><div class="ip">${_('No additional auth tokens specified')}</div></td></tr>
64 40 %endif
65 41 </table>
66 42 </div>
67 43
68 44 <div class="user_auth_tokens">
69 45 ${h.secure_form(url('edit_user_auth_tokens', user_id=c.user.user_id), method='put')}
70 46 <div class="form form-vertical">
71 47 <!-- fields -->
72 48 <div class="fields">
73 49 <div class="field">
74 50 <div class="label">
75 51 <label for="new_email">${_('New auth token')}:</label>
76 52 </div>
77 53 <div class="input">
78 54 ${h.text('description', class_='medium', placeholder=_('Description'))}
79 55 ${h.select('lifetime', '', c.lifetime_options)}
80 56 ${h.select('role', '', c.role_options)}
81 57 </div>
82 58 </div>
83 59 <div class="buttons">
84 60 ${h.submit('save',_('Add'),class_="btn btn-small")}
85 61 ${h.reset('reset',_('Reset'),class_="btn btn-small")}
86 62 </div>
87 63 </div>
88 64 </div>
89 65 ${h.end_form()}
90 66 </div>
91 67 </div>
92 68 </div>
93 69
94 70 <script>
95 71 $(document).ready(function(){
96 72 $("#lifetime").select2({
97 73 'containerCssClass': "drop-menu",
98 74 'dropdownCssClass': "drop-menu-dropdown",
99 75 'dropdownAutoWidth': true
100 76 });
101 77 $("#role").select2({
102 78 'containerCssClass': "drop-menu",
103 79 'dropdownCssClass': "drop-menu-dropdown",
104 80 'dropdownAutoWidth': true
105 81 });
106 82 })
107 83 </script>
@@ -1,400 +1,384 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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
23 23 from rhodecode.lib import helpers as h
24 24 from rhodecode.lib.auth import check_password
25 25 from rhodecode.model.db import User, UserFollowing, Repository, UserApiKeys
26 26 from rhodecode.model.meta import Session
27 27 from rhodecode.tests import (
28 28 TestController, url, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL,
29 29 assert_session_flash)
30 30 from rhodecode.tests.fixture import Fixture
31 31 from rhodecode.tests.utils import AssertResponse
32 32
33 33 fixture = Fixture()
34 34
35 35
36 36 class TestMyAccountController(TestController):
37 37 test_user_1 = 'testme'
38 38 test_user_1_password = '0jd83nHNS/d23n'
39 39 destroy_users = set()
40 40
41 41 @classmethod
42 42 def teardown_class(cls):
43 43 fixture.destroy_users(cls.destroy_users)
44 44
45 45 def test_my_account(self):
46 46 self.log_user()
47 47 response = self.app.get(url('my_account'))
48 48
49 49 response.mustcontain('test_admin')
50 50 response.mustcontain('href="/_admin/my_account/edit"')
51 51
52 52 def test_logout_form_contains_csrf(self, autologin_user, csrf_token):
53 53 response = self.app.get(url('my_account'))
54 54 assert_response = AssertResponse(response)
55 55 element = assert_response.get_element('.logout #csrf_token')
56 56 assert element.value == csrf_token
57 57
58 58 def test_my_account_edit(self):
59 59 self.log_user()
60 60 response = self.app.get(url('my_account_edit'))
61 61
62 62 response.mustcontain('value="test_admin')
63 63
64 64 def test_my_account_my_repos(self):
65 65 self.log_user()
66 66 response = self.app.get(url('my_account_repos'))
67 67 repos = Repository.query().filter(
68 68 Repository.user == User.get_by_username(
69 69 TEST_USER_ADMIN_LOGIN)).all()
70 70 for repo in repos:
71 71 response.mustcontain('"name_raw": "%s"' % repo.repo_name)
72 72
73 73 def test_my_account_my_watched(self):
74 74 self.log_user()
75 75 response = self.app.get(url('my_account_watched'))
76 76
77 77 repos = UserFollowing.query().filter(
78 78 UserFollowing.user == User.get_by_username(
79 79 TEST_USER_ADMIN_LOGIN)).all()
80 80 for repo in repos:
81 81 response.mustcontain(
82 82 '"name_raw": "%s"' % repo.follows_repository.repo_name)
83 83
84 84 @pytest.mark.backends("git", "hg")
85 85 def test_my_account_my_pullrequests(self, pr_util):
86 86 self.log_user()
87 87 response = self.app.get(url('my_account_pullrequests'))
88 88 response.mustcontain('There are currently no open pull '
89 89 'requests requiring your participation.')
90 90
91 91 pr = pr_util.create_pull_request(title='TestMyAccountPR')
92 92 response = self.app.get(url('my_account_pullrequests'))
93 93 response.mustcontain('"name_raw": %s' % pr.pull_request_id)
94 94 response.mustcontain('TestMyAccountPR')
95 95
96 96 def test_my_account_my_emails(self):
97 97 self.log_user()
98 98 response = self.app.get(url('my_account_emails'))
99 99 response.mustcontain('No additional emails specified')
100 100
101 101 def test_my_account_my_emails_add_existing_email(self):
102 102 self.log_user()
103 103 response = self.app.get(url('my_account_emails'))
104 104 response.mustcontain('No additional emails specified')
105 105 response = self.app.post(url('my_account_emails'),
106 106 {'new_email': TEST_USER_REGULAR_EMAIL,
107 107 'csrf_token': self.csrf_token})
108 108 assert_session_flash(response, 'This e-mail address is already taken')
109 109
110 110 def test_my_account_my_emails_add_mising_email_in_form(self):
111 111 self.log_user()
112 112 response = self.app.get(url('my_account_emails'))
113 113 response.mustcontain('No additional emails specified')
114 114 response = self.app.post(url('my_account_emails'),
115 115 {'csrf_token': self.csrf_token})
116 116 assert_session_flash(response, 'Please enter an email address')
117 117
118 118 def test_my_account_my_emails_add_remove(self):
119 119 self.log_user()
120 120 response = self.app.get(url('my_account_emails'))
121 121 response.mustcontain('No additional emails specified')
122 122
123 123 response = self.app.post(url('my_account_emails'),
124 124 {'new_email': 'foo@barz.com',
125 125 'csrf_token': self.csrf_token})
126 126
127 127 response = self.app.get(url('my_account_emails'))
128 128
129 129 from rhodecode.model.db import UserEmailMap
130 130 email_id = UserEmailMap.query().filter(
131 131 UserEmailMap.user == User.get_by_username(
132 132 TEST_USER_ADMIN_LOGIN)).filter(
133 133 UserEmailMap.email == 'foo@barz.com').one().email_id
134 134
135 135 response.mustcontain('foo@barz.com')
136 136 response.mustcontain('<input id="del_email_id" name="del_email_id" '
137 137 'type="hidden" value="%s" />' % email_id)
138 138
139 139 response = self.app.post(
140 140 url('my_account_emails'), {
141 141 'del_email_id': email_id, '_method': 'delete',
142 142 'csrf_token': self.csrf_token})
143 143 assert_session_flash(response, 'Removed email address from user account')
144 144 response = self.app.get(url('my_account_emails'))
145 145 response.mustcontain('No additional emails specified')
146 146
147 147 @pytest.mark.parametrize(
148 148 "name, attrs", [
149 149 ('firstname', {'firstname': 'new_username'}),
150 150 ('lastname', {'lastname': 'new_username'}),
151 151 ('admin', {'admin': True}),
152 152 ('admin', {'admin': False}),
153 153 ('extern_type', {'extern_type': 'ldap'}),
154 154 ('extern_type', {'extern_type': None}),
155 155 # ('extern_name', {'extern_name': 'test'}),
156 156 # ('extern_name', {'extern_name': None}),
157 157 ('active', {'active': False}),
158 158 ('active', {'active': True}),
159 159 ('email', {'email': 'some@email.com'}),
160 160 ])
161 161 def test_my_account_update(self, name, attrs):
162 162 usr = fixture.create_user(self.test_user_1,
163 163 password=self.test_user_1_password,
164 164 email='testme@rhodecode.org',
165 165 extern_type='rhodecode',
166 166 extern_name=self.test_user_1,
167 167 skip_if_exists=True)
168 168 self.destroy_users.add(self.test_user_1)
169 169
170 170 params = usr.get_api_data() # current user data
171 171 user_id = usr.user_id
172 172 self.log_user(
173 173 username=self.test_user_1, password=self.test_user_1_password)
174 174
175 175 params.update({'password_confirmation': ''})
176 176 params.update({'new_password': ''})
177 177 params.update({'extern_type': 'rhodecode'})
178 178 params.update({'extern_name': self.test_user_1})
179 179 params.update({'csrf_token': self.csrf_token})
180 180
181 181 params.update(attrs)
182 182 # my account page cannot set language param yet, only for admins
183 183 del params['language']
184 184 response = self.app.post(url('my_account'), params)
185 185
186 186 assert_session_flash(
187 187 response, 'Your account was updated successfully')
188 188
189 189 del params['csrf_token']
190 190
191 191 updated_user = User.get_by_username(self.test_user_1)
192 192 updated_params = updated_user.get_api_data()
193 193 updated_params.update({'password_confirmation': ''})
194 194 updated_params.update({'new_password': ''})
195 195
196 196 params['last_login'] = updated_params['last_login']
197 197 # my account page cannot set language param yet, only for admins
198 198 # but we get this info from API anyway
199 199 params['language'] = updated_params['language']
200 200
201 201 if name == 'email':
202 202 params['emails'] = [attrs['email']]
203 203 if name == 'extern_type':
204 204 # cannot update this via form, expected value is original one
205 205 params['extern_type'] = "rhodecode"
206 206 if name == 'extern_name':
207 207 # cannot update this via form, expected value is original one
208 208 params['extern_name'] = str(user_id)
209 209 if name == 'active':
210 210 # my account cannot deactivate account
211 211 params['active'] = True
212 212 if name == 'admin':
213 213 # my account cannot make you an admin !
214 214 params['admin'] = False
215 215
216 216 assert params == updated_params
217 217
218 218 def test_my_account_update_err_email_exists(self):
219 219 self.log_user()
220 220
221 221 new_email = 'test_regular@mail.com' # already exisitn email
222 222 response = self.app.post(url('my_account'),
223 223 params={
224 224 'username': 'test_admin',
225 225 'new_password': 'test12',
226 226 'password_confirmation': 'test122',
227 227 'firstname': 'NewName',
228 228 'lastname': 'NewLastname',
229 229 'email': new_email,
230 230 'csrf_token': self.csrf_token,
231 231 })
232 232
233 233 response.mustcontain('This e-mail address is already taken')
234 234
235 235 def test_my_account_update_err(self):
236 236 self.log_user('test_regular2', 'test12')
237 237
238 238 new_email = 'newmail.pl'
239 239 response = self.app.post(url('my_account'),
240 240 params={
241 241 'username': 'test_admin',
242 242 'new_password': 'test12',
243 243 'password_confirmation': 'test122',
244 244 'firstname': 'NewName',
245 245 'lastname': 'NewLastname',
246 246 'email': new_email,
247 247 'csrf_token': self.csrf_token,
248 248 })
249 249
250 250 response.mustcontain('An email address must contain a single @')
251 251 from rhodecode.model import validators
252 252 msg = validators.ValidUsername(
253 253 edit=False, old_data={})._messages['username_exists']
254 254 msg = h.html_escape(msg % {'username': 'test_admin'})
255 255 response.mustcontain(u"%s" % msg)
256 256
257 257 def test_my_account_auth_tokens(self):
258 258 usr = self.log_user('test_regular2', 'test12')
259 259 user = User.get(usr['user_id'])
260 260 response = self.app.get(url('my_account_auth_tokens'))
261 261 response.mustcontain(user.api_key)
262 262 response.mustcontain('expires: never')
263 263
264 264 @pytest.mark.parametrize("desc, lifetime", [
265 265 ('forever', -1),
266 266 ('5mins', 60*5),
267 267 ('30days', 60*60*24*30),
268 268 ])
269 269 def test_my_account_add_auth_tokens(self, desc, lifetime):
270 270 usr = self.log_user('test_regular2', 'test12')
271 271 user = User.get(usr['user_id'])
272 272 response = self.app.post(url('my_account_auth_tokens'),
273 273 {'description': desc, 'lifetime': lifetime,
274 274 'csrf_token': self.csrf_token})
275 275 assert_session_flash(response, 'Auth token successfully created')
276 276 try:
277 277 response = response.follow()
278 278 user = User.get(usr['user_id'])
279 279 for auth_token in user.auth_tokens:
280 280 response.mustcontain(auth_token)
281 281 finally:
282 282 for auth_token in UserApiKeys.query().all():
283 283 Session().delete(auth_token)
284 284 Session().commit()
285 285
286 286 def test_my_account_remove_auth_token(self, user_util):
287 287 user = user_util.create_user(password=self.test_user_1_password)
288 288 user_id = user.user_id
289 289 self.log_user(user.username, self.test_user_1_password)
290 290
291 291 user = User.get(user_id)
292 292 keys = user.extra_auth_tokens
293 293 assert 1 == len(keys)
294 294
295 295 response = self.app.post(url('my_account_auth_tokens'),
296 296 {'description': 'desc', 'lifetime': -1,
297 297 'csrf_token': self.csrf_token})
298 298 assert_session_flash(response, 'Auth token successfully created')
299 299 response.follow()
300 300
301 301 user = User.get(user_id)
302 302 keys = user.extra_auth_tokens
303 303 assert 2 == len(keys)
304 304
305 305 response = self.app.post(
306 306 url('my_account_auth_tokens'),
307 307 {'_method': 'delete', 'del_auth_token': keys[0].api_key,
308 308 'csrf_token': self.csrf_token})
309 309 assert_session_flash(response, 'Auth token successfully deleted')
310 310
311 311 user = User.get(user_id)
312 312 keys = user.extra_auth_tokens
313 313 assert 1 == len(keys)
314 314
315 def test_my_account_reset_main_auth_token(self):
316 usr = self.log_user('test_regular2', 'test12')
317 user = User.get(usr['user_id'])
318 api_key = user.api_key
319 response = self.app.get(url('my_account_auth_tokens'))
320 response.mustcontain(api_key)
321 response.mustcontain('expires: never')
322
323 response = self.app.post(
324 url('my_account_auth_tokens'),
325 {'_method': 'delete', 'del_auth_token_builtin': api_key,
326 'csrf_token': self.csrf_token})
327 assert_session_flash(response, 'Auth token successfully reset')
328 response = response.follow()
329 response.mustcontain(no=[api_key])
330
331 315 def test_valid_change_password(self, user_util):
332 316 new_password = 'my_new_valid_password'
333 317 user = user_util.create_user(password=self.test_user_1_password)
334 318 session = self.log_user(user.username, self.test_user_1_password)
335 319 form_data = [
336 320 ('current_password', self.test_user_1_password),
337 321 ('__start__', 'new_password:mapping'),
338 322 ('new_password', new_password),
339 323 ('new_password-confirm', new_password),
340 324 ('__end__', 'new_password:mapping'),
341 325 ('csrf_token', self.csrf_token),
342 326 ]
343 327 response = self.app.post(url('my_account_password'), form_data).follow()
344 328 assert 'Successfully updated password' in response
345 329
346 330 # check_password depends on user being in session
347 331 Session().add(user)
348 332 try:
349 333 assert check_password(new_password, user.password)
350 334 finally:
351 335 Session().expunge(user)
352 336
353 337 @pytest.mark.parametrize('current_pw,new_pw,confirm_pw', [
354 338 ('', 'abcdef123', 'abcdef123'),
355 339 ('wrong_pw', 'abcdef123', 'abcdef123'),
356 340 (test_user_1_password, test_user_1_password, test_user_1_password),
357 341 (test_user_1_password, '', ''),
358 342 (test_user_1_password, 'abcdef123', ''),
359 343 (test_user_1_password, '', 'abcdef123'),
360 344 (test_user_1_password, 'not_the', 'same_pw'),
361 345 (test_user_1_password, 'short', 'short'),
362 346 ])
363 347 def test_invalid_change_password(self, current_pw, new_pw, confirm_pw,
364 348 user_util):
365 349 user = user_util.create_user(password=self.test_user_1_password)
366 350 session = self.log_user(user.username, self.test_user_1_password)
367 351 old_password_hash = session['password']
368 352 form_data = [
369 353 ('current_password', current_pw),
370 354 ('__start__', 'new_password:mapping'),
371 355 ('new_password', new_pw),
372 356 ('new_password-confirm', confirm_pw),
373 357 ('__end__', 'new_password:mapping'),
374 358 ('csrf_token', self.csrf_token),
375 359 ]
376 360 response = self.app.post(url('my_account_password'), form_data)
377 361 assert 'Error occurred' in response
378 362
379 363 def test_password_is_updated_in_session_on_password_change(self, user_util):
380 364 old_password = 'abcdef123'
381 365 new_password = 'abcdef124'
382 366
383 367 user = user_util.create_user(password=old_password)
384 368 session = self.log_user(user.username, old_password)
385 369 old_password_hash = session['password']
386 370
387 371 form_data = [
388 372 ('current_password', old_password),
389 373 ('__start__', 'new_password:mapping'),
390 374 ('new_password', new_password),
391 375 ('new_password-confirm', new_password),
392 376 ('__end__', 'new_password:mapping'),
393 377 ('csrf_token', self.csrf_token),
394 378 ]
395 379 self.app.post(url('my_account_password'), form_data)
396 380
397 381 response = self.app.get(url('home'))
398 382 new_password_hash = response.session['rhodecode_user']['password']
399 383
400 384 assert old_password_hash != new_password_hash
@@ -1,644 +1,627 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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.auth import check_password
25 25 from rhodecode.lib import helpers as h
26 26 from rhodecode.model import validators
27 27 from rhodecode.model.db import User, UserIpMap, UserApiKeys
28 28 from rhodecode.model.meta import Session
29 29 from rhodecode.model.user import UserModel
30 30 from rhodecode.tests import (
31 31 TestController, url, link_to, TEST_USER_ADMIN_LOGIN,
32 32 TEST_USER_REGULAR_LOGIN, assert_session_flash)
33 33 from rhodecode.tests.fixture import Fixture
34 34 from rhodecode.tests.utils import AssertResponse
35 35
36 36 fixture = Fixture()
37 37
38 38
39 39 class TestAdminUsersController(TestController):
40 40 test_user_1 = 'testme'
41 41 destroy_users = set()
42 42
43 43 @classmethod
44 44 def teardown_method(cls, method):
45 45 fixture.destroy_users(cls.destroy_users)
46 46
47 47 def test_index(self):
48 48 self.log_user()
49 49 self.app.get(url('users'))
50 50
51 51 def test_create(self):
52 52 self.log_user()
53 53 username = 'newtestuser'
54 54 password = 'test12'
55 55 password_confirmation = password
56 56 name = 'name'
57 57 lastname = 'lastname'
58 58 email = 'mail@mail.com'
59 59
60 60 response = self.app.get(url('new_user'))
61 61
62 62 response = self.app.post(url('users'), params={
63 63 'username': username,
64 64 'password': password,
65 65 'password_confirmation': password_confirmation,
66 66 'firstname': name,
67 67 'active': True,
68 68 'lastname': lastname,
69 69 'extern_name': 'rhodecode',
70 70 'extern_type': 'rhodecode',
71 71 'email': email,
72 72 'csrf_token': self.csrf_token,
73 73 })
74 74 user_link = link_to(
75 75 username,
76 76 url('edit_user', user_id=User.get_by_username(username).user_id))
77 77 assert_session_flash(response, 'Created user %s' % (user_link,))
78 78 self.destroy_users.add(username)
79 79
80 80 new_user = User.query().filter(User.username == username).one()
81 81
82 82 assert new_user.username == username
83 83 assert check_password(password, new_user.password)
84 84 assert new_user.name == name
85 85 assert new_user.lastname == lastname
86 86 assert new_user.email == email
87 87
88 88 response.follow()
89 89 response = response.follow()
90 90 response.mustcontain(username)
91 91
92 92 def test_create_err(self):
93 93 self.log_user()
94 94 username = 'new_user'
95 95 password = ''
96 96 name = 'name'
97 97 lastname = 'lastname'
98 98 email = 'errmail.com'
99 99
100 100 response = self.app.get(url('new_user'))
101 101
102 102 response = self.app.post(url('users'), params={
103 103 'username': username,
104 104 'password': password,
105 105 'name': name,
106 106 'active': False,
107 107 'lastname': lastname,
108 108 'email': email,
109 109 'csrf_token': self.csrf_token,
110 110 })
111 111
112 112 msg = validators.ValidUsername(
113 113 False, {})._messages['system_invalid_username']
114 114 msg = h.html_escape(msg % {'username': 'new_user'})
115 115 response.mustcontain('<span class="error-message">%s</span>' % msg)
116 116 response.mustcontain(
117 117 '<span class="error-message">Please enter a value</span>')
118 118 response.mustcontain(
119 119 '<span class="error-message">An email address must contain a'
120 120 ' single @</span>')
121 121
122 122 def get_user():
123 123 Session().query(User).filter(User.username == username).one()
124 124
125 125 with pytest.raises(NoResultFound):
126 126 get_user()
127 127
128 128 def test_new(self):
129 129 self.log_user()
130 130 self.app.get(url('new_user'))
131 131
132 132 @pytest.mark.parametrize("name, attrs", [
133 133 ('firstname', {'firstname': 'new_username'}),
134 134 ('lastname', {'lastname': 'new_username'}),
135 135 ('admin', {'admin': True}),
136 136 ('admin', {'admin': False}),
137 137 ('extern_type', {'extern_type': 'ldap'}),
138 138 ('extern_type', {'extern_type': None}),
139 139 ('extern_name', {'extern_name': 'test'}),
140 140 ('extern_name', {'extern_name': None}),
141 141 ('active', {'active': False}),
142 142 ('active', {'active': True}),
143 143 ('email', {'email': 'some@email.com'}),
144 144 ('language', {'language': 'de'}),
145 145 ('language', {'language': 'en'}),
146 146 # ('new_password', {'new_password': 'foobar123',
147 147 # 'password_confirmation': 'foobar123'})
148 148 ])
149 149 def test_update(self, name, attrs):
150 150 self.log_user()
151 151 usr = fixture.create_user(self.test_user_1, password='qweqwe',
152 152 email='testme@rhodecode.org',
153 153 extern_type='rhodecode',
154 154 extern_name=self.test_user_1,
155 155 skip_if_exists=True)
156 156 Session().commit()
157 157 self.destroy_users.add(self.test_user_1)
158 158 params = usr.get_api_data()
159 159 cur_lang = params['language'] or 'en'
160 160 params.update({
161 161 'password_confirmation': '',
162 162 'new_password': '',
163 163 'language': cur_lang,
164 164 '_method': 'put',
165 165 'csrf_token': self.csrf_token,
166 166 })
167 167 params.update({'new_password': ''})
168 168 params.update(attrs)
169 169 if name == 'email':
170 170 params['emails'] = [attrs['email']]
171 171 elif name == 'extern_type':
172 172 # cannot update this via form, expected value is original one
173 173 params['extern_type'] = "rhodecode"
174 174 elif name == 'extern_name':
175 175 # cannot update this via form, expected value is original one
176 176 params['extern_name'] = self.test_user_1
177 177 # special case since this user is not
178 178 # logged in yet his data is not filled
179 179 # so we use creation data
180 180
181 181 response = self.app.post(url('user', user_id=usr.user_id), params)
182 182 assert response.status_int == 302
183 183 assert_session_flash(response, 'User updated successfully')
184 184
185 185 updated_user = User.get_by_username(self.test_user_1)
186 186 updated_params = updated_user.get_api_data()
187 187 updated_params.update({'password_confirmation': ''})
188 188 updated_params.update({'new_password': ''})
189 189
190 190 del params['_method']
191 191 del params['csrf_token']
192 192 assert params == updated_params
193 193
194 194 def test_update_and_migrate_password(
195 195 self, autologin_user, real_crypto_backend):
196 196 from rhodecode.lib import auth
197 197
198 198 # create new user, with sha256 password
199 199 temp_user = 'test_admin_sha256'
200 200 user = fixture.create_user(temp_user)
201 201 user.password = auth._RhodeCodeCryptoSha256().hash_create(
202 202 b'test123')
203 203 Session().add(user)
204 204 Session().commit()
205 205 self.destroy_users.add('test_admin_sha256')
206 206
207 207 params = user.get_api_data()
208 208
209 209 params.update({
210 210 'password_confirmation': 'qweqwe123',
211 211 'new_password': 'qweqwe123',
212 212 'language': 'en',
213 213 '_method': 'put',
214 214 'csrf_token': autologin_user.csrf_token,
215 215 })
216 216
217 217 response = self.app.post(url('user', user_id=user.user_id), params)
218 218 assert response.status_int == 302
219 219 assert_session_flash(response, 'User updated successfully')
220 220
221 221 # new password should be bcrypted, after log-in and transfer
222 222 user = User.get_by_username(temp_user)
223 223 assert user.password.startswith('$')
224 224
225 225 updated_user = User.get_by_username(temp_user)
226 226 updated_params = updated_user.get_api_data()
227 227 updated_params.update({'password_confirmation': 'qweqwe123'})
228 228 updated_params.update({'new_password': 'qweqwe123'})
229 229
230 230 del params['_method']
231 231 del params['csrf_token']
232 232 assert params == updated_params
233 233
234 234 def test_delete(self):
235 235 self.log_user()
236 236 username = 'newtestuserdeleteme'
237 237
238 238 fixture.create_user(name=username)
239 239
240 240 new_user = Session().query(User)\
241 241 .filter(User.username == username).one()
242 242 response = self.app.post(url('user', user_id=new_user.user_id),
243 243 params={'_method': 'delete',
244 244 'csrf_token': self.csrf_token})
245 245
246 246 assert_session_flash(response, 'Successfully deleted user')
247 247
248 248 def test_delete_owner_of_repository(self):
249 249 self.log_user()
250 250 username = 'newtestuserdeleteme_repo_owner'
251 251 obj_name = 'test_repo'
252 252 usr = fixture.create_user(name=username)
253 253 self.destroy_users.add(username)
254 254 fixture.create_repo(obj_name, cur_user=usr.username)
255 255
256 256 new_user = Session().query(User)\
257 257 .filter(User.username == username).one()
258 258 response = self.app.post(url('user', user_id=new_user.user_id),
259 259 params={'_method': 'delete',
260 260 'csrf_token': self.csrf_token})
261 261
262 262 msg = 'user "%s" still owns 1 repositories and cannot be removed. ' \
263 263 'Switch owners or remove those repositories:%s' % (username,
264 264 obj_name)
265 265 assert_session_flash(response, msg)
266 266 fixture.destroy_repo(obj_name)
267 267
268 268 def test_delete_owner_of_repository_detaching(self):
269 269 self.log_user()
270 270 username = 'newtestuserdeleteme_repo_owner_detach'
271 271 obj_name = 'test_repo'
272 272 usr = fixture.create_user(name=username)
273 273 self.destroy_users.add(username)
274 274 fixture.create_repo(obj_name, cur_user=usr.username)
275 275
276 276 new_user = Session().query(User)\
277 277 .filter(User.username == username).one()
278 278 response = self.app.post(url('user', user_id=new_user.user_id),
279 279 params={'_method': 'delete',
280 280 'user_repos': 'detach',
281 281 'csrf_token': self.csrf_token})
282 282
283 283 msg = 'Detached 1 repositories'
284 284 assert_session_flash(response, msg)
285 285 fixture.destroy_repo(obj_name)
286 286
287 287 def test_delete_owner_of_repository_deleting(self):
288 288 self.log_user()
289 289 username = 'newtestuserdeleteme_repo_owner_delete'
290 290 obj_name = 'test_repo'
291 291 usr = fixture.create_user(name=username)
292 292 self.destroy_users.add(username)
293 293 fixture.create_repo(obj_name, cur_user=usr.username)
294 294
295 295 new_user = Session().query(User)\
296 296 .filter(User.username == username).one()
297 297 response = self.app.post(url('user', user_id=new_user.user_id),
298 298 params={'_method': 'delete',
299 299 'user_repos': 'delete',
300 300 'csrf_token': self.csrf_token})
301 301
302 302 msg = 'Deleted 1 repositories'
303 303 assert_session_flash(response, msg)
304 304
305 305 def test_delete_owner_of_repository_group(self):
306 306 self.log_user()
307 307 username = 'newtestuserdeleteme_repo_group_owner'
308 308 obj_name = 'test_group'
309 309 usr = fixture.create_user(name=username)
310 310 self.destroy_users.add(username)
311 311 fixture.create_repo_group(obj_name, cur_user=usr.username)
312 312
313 313 new_user = Session().query(User)\
314 314 .filter(User.username == username).one()
315 315 response = self.app.post(url('user', user_id=new_user.user_id),
316 316 params={'_method': 'delete',
317 317 'csrf_token': self.csrf_token})
318 318
319 319 msg = 'user "%s" still owns 1 repository groups and cannot be removed. ' \
320 320 'Switch owners or remove those repository groups:%s' % (username,
321 321 obj_name)
322 322 assert_session_flash(response, msg)
323 323 fixture.destroy_repo_group(obj_name)
324 324
325 325 def test_delete_owner_of_repository_group_detaching(self):
326 326 self.log_user()
327 327 username = 'newtestuserdeleteme_repo_group_owner_detach'
328 328 obj_name = 'test_group'
329 329 usr = fixture.create_user(name=username)
330 330 self.destroy_users.add(username)
331 331 fixture.create_repo_group(obj_name, cur_user=usr.username)
332 332
333 333 new_user = Session().query(User)\
334 334 .filter(User.username == username).one()
335 335 response = self.app.post(url('user', user_id=new_user.user_id),
336 336 params={'_method': 'delete',
337 337 'user_repo_groups': 'delete',
338 338 'csrf_token': self.csrf_token})
339 339
340 340 msg = 'Deleted 1 repository groups'
341 341 assert_session_flash(response, msg)
342 342
343 343 def test_delete_owner_of_repository_group_deleting(self):
344 344 self.log_user()
345 345 username = 'newtestuserdeleteme_repo_group_owner_delete'
346 346 obj_name = 'test_group'
347 347 usr = fixture.create_user(name=username)
348 348 self.destroy_users.add(username)
349 349 fixture.create_repo_group(obj_name, cur_user=usr.username)
350 350
351 351 new_user = Session().query(User)\
352 352 .filter(User.username == username).one()
353 353 response = self.app.post(url('user', user_id=new_user.user_id),
354 354 params={'_method': 'delete',
355 355 'user_repo_groups': 'detach',
356 356 'csrf_token': self.csrf_token})
357 357
358 358 msg = 'Detached 1 repository groups'
359 359 assert_session_flash(response, msg)
360 360 fixture.destroy_repo_group(obj_name)
361 361
362 362 def test_delete_owner_of_user_group(self):
363 363 self.log_user()
364 364 username = 'newtestuserdeleteme_user_group_owner'
365 365 obj_name = 'test_user_group'
366 366 usr = fixture.create_user(name=username)
367 367 self.destroy_users.add(username)
368 368 fixture.create_user_group(obj_name, cur_user=usr.username)
369 369
370 370 new_user = Session().query(User)\
371 371 .filter(User.username == username).one()
372 372 response = self.app.post(url('user', user_id=new_user.user_id),
373 373 params={'_method': 'delete',
374 374 'csrf_token': self.csrf_token})
375 375
376 376 msg = 'user "%s" still owns 1 user groups and cannot be removed. ' \
377 377 'Switch owners or remove those user groups:%s' % (username,
378 378 obj_name)
379 379 assert_session_flash(response, msg)
380 380 fixture.destroy_user_group(obj_name)
381 381
382 382 def test_delete_owner_of_user_group_detaching(self):
383 383 self.log_user()
384 384 username = 'newtestuserdeleteme_user_group_owner_detaching'
385 385 obj_name = 'test_user_group'
386 386 usr = fixture.create_user(name=username)
387 387 self.destroy_users.add(username)
388 388 fixture.create_user_group(obj_name, cur_user=usr.username)
389 389
390 390 new_user = Session().query(User)\
391 391 .filter(User.username == username).one()
392 392 try:
393 393 response = self.app.post(url('user', user_id=new_user.user_id),
394 394 params={'_method': 'delete',
395 395 'user_user_groups': 'detach',
396 396 'csrf_token': self.csrf_token})
397 397
398 398 msg = 'Detached 1 user groups'
399 399 assert_session_flash(response, msg)
400 400 finally:
401 401 fixture.destroy_user_group(obj_name)
402 402
403 403 def test_delete_owner_of_user_group_deleting(self):
404 404 self.log_user()
405 405 username = 'newtestuserdeleteme_user_group_owner_deleting'
406 406 obj_name = 'test_user_group'
407 407 usr = fixture.create_user(name=username)
408 408 self.destroy_users.add(username)
409 409 fixture.create_user_group(obj_name, cur_user=usr.username)
410 410
411 411 new_user = Session().query(User)\
412 412 .filter(User.username == username).one()
413 413 response = self.app.post(url('user', user_id=new_user.user_id),
414 414 params={'_method': 'delete',
415 415 'user_user_groups': 'delete',
416 416 'csrf_token': self.csrf_token})
417 417
418 418 msg = 'Deleted 1 user groups'
419 419 assert_session_flash(response, msg)
420 420
421 421 def test_show(self):
422 422 self.app.get(url('user', user_id=1))
423 423
424 424 def test_edit(self):
425 425 self.log_user()
426 426 user = User.get_by_username(TEST_USER_ADMIN_LOGIN)
427 427 self.app.get(url('edit_user', user_id=user.user_id))
428 428
429 429 @pytest.mark.parametrize(
430 430 'repo_create, repo_create_write, user_group_create, repo_group_create,'
431 431 'fork_create, inherit_default_permissions, expect_error,'
432 432 'expect_form_error', [
433 433 ('hg.create.none', 'hg.create.write_on_repogroup.false',
434 434 'hg.usergroup.create.false', 'hg.repogroup.create.false',
435 435 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
436 436 ('hg.create.repository', 'hg.create.write_on_repogroup.false',
437 437 'hg.usergroup.create.false', 'hg.repogroup.create.false',
438 438 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
439 439 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
440 440 'hg.usergroup.create.true', 'hg.repogroup.create.true',
441 441 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
442 442 False),
443 443 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
444 444 'hg.usergroup.create.true', 'hg.repogroup.create.true',
445 445 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
446 446 True),
447 447 ('', '', '', '', '', '', True, False),
448 448 ])
449 449 def test_global_perms_on_user(
450 450 self, repo_create, repo_create_write, user_group_create,
451 451 repo_group_create, fork_create, expect_error, expect_form_error,
452 452 inherit_default_permissions):
453 453 self.log_user()
454 454 user = fixture.create_user('dummy')
455 455 uid = user.user_id
456 456
457 457 # ENABLE REPO CREATE ON A GROUP
458 458 perm_params = {
459 459 'inherit_default_permissions': False,
460 460 'default_repo_create': repo_create,
461 461 'default_repo_create_on_write': repo_create_write,
462 462 'default_user_group_create': user_group_create,
463 463 'default_repo_group_create': repo_group_create,
464 464 'default_fork_create': fork_create,
465 465 'default_inherit_default_permissions': inherit_default_permissions,
466 466 '_method': 'put',
467 467 'csrf_token': self.csrf_token,
468 468 }
469 469 response = self.app.post(
470 470 url('edit_user_global_perms', user_id=uid),
471 471 params=perm_params)
472 472
473 473 if expect_form_error:
474 474 assert response.status_int == 200
475 475 response.mustcontain('Value must be one of')
476 476 else:
477 477 if expect_error:
478 478 msg = 'An error occurred during permissions saving'
479 479 else:
480 480 msg = 'User global permissions updated successfully'
481 481 ug = User.get(uid)
482 482 del perm_params['_method']
483 483 del perm_params['inherit_default_permissions']
484 484 del perm_params['csrf_token']
485 485 assert perm_params == ug.get_default_perms()
486 486 assert_session_flash(response, msg)
487 487 fixture.destroy_user(uid)
488 488
489 489 def test_global_permissions_initial_values(self, user_util):
490 490 self.log_user()
491 491 user = user_util.create_user()
492 492 uid = user.user_id
493 493 response = self.app.get(url('edit_user_global_perms', user_id=uid))
494 494 default_user = User.get_default_user()
495 495 default_permissions = default_user.get_default_perms()
496 496 assert_response = AssertResponse(response)
497 497 expected_permissions = (
498 498 'default_repo_create', 'default_repo_create_on_write',
499 499 'default_fork_create', 'default_repo_group_create',
500 500 'default_user_group_create', 'default_inherit_default_permissions')
501 501 for permission in expected_permissions:
502 502 css_selector = '[name={}][checked=checked]'.format(permission)
503 503 element = assert_response.get_element(css_selector)
504 504 assert element.value == default_permissions[permission]
505 505
506 506 def test_ips(self):
507 507 self.log_user()
508 508 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
509 509 response = self.app.get(url('edit_user_ips', user_id=user.user_id))
510 510 response.mustcontain('All IP addresses are allowed')
511 511
512 512 @pytest.mark.parametrize("test_name, ip, ip_range, failure", [
513 513 ('127/24', '127.0.0.1/24', '127.0.0.0 - 127.0.0.255', False),
514 514 ('10/32', '10.0.0.10/32', '10.0.0.10 - 10.0.0.10', False),
515 515 ('0/16', '0.0.0.0/16', '0.0.0.0 - 0.0.255.255', False),
516 516 ('0/8', '0.0.0.0/8', '0.0.0.0 - 0.255.255.255', False),
517 517 ('127_bad_mask', '127.0.0.1/99', '127.0.0.1 - 127.0.0.1', True),
518 518 ('127_bad_ip', 'foobar', 'foobar', True),
519 519 ])
520 520 def test_add_ip(self, test_name, ip, ip_range, failure):
521 521 self.log_user()
522 522 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
523 523 user_id = user.user_id
524 524
525 525 response = self.app.post(url('edit_user_ips', user_id=user_id),
526 526 params={'new_ip': ip, '_method': 'put',
527 527 'csrf_token': self.csrf_token})
528 528
529 529 if failure:
530 530 assert_session_flash(
531 531 response, 'Please enter a valid IPv4 or IpV6 address')
532 532 response = self.app.get(url('edit_user_ips', user_id=user_id))
533 533 response.mustcontain(no=[ip])
534 534 response.mustcontain(no=[ip_range])
535 535
536 536 else:
537 537 response = self.app.get(url('edit_user_ips', user_id=user_id))
538 538 response.mustcontain(ip)
539 539 response.mustcontain(ip_range)
540 540
541 541 # cleanup
542 542 for del_ip in UserIpMap.query().filter(
543 543 UserIpMap.user_id == user_id).all():
544 544 Session().delete(del_ip)
545 545 Session().commit()
546 546
547 547 def test_delete_ip(self):
548 548 self.log_user()
549 549 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
550 550 user_id = user.user_id
551 551 ip = '127.0.0.1/32'
552 552 ip_range = '127.0.0.1 - 127.0.0.1'
553 553 new_ip = UserModel().add_extra_ip(user_id, ip)
554 554 Session().commit()
555 555 new_ip_id = new_ip.ip_id
556 556
557 557 response = self.app.get(url('edit_user_ips', user_id=user_id))
558 558 response.mustcontain(ip)
559 559 response.mustcontain(ip_range)
560 560
561 561 self.app.post(url('edit_user_ips', user_id=user_id),
562 562 params={'_method': 'delete', 'del_ip_id': new_ip_id,
563 563 'csrf_token': self.csrf_token})
564 564
565 565 response = self.app.get(url('edit_user_ips', user_id=user_id))
566 566 response.mustcontain('All IP addresses are allowed')
567 567 response.mustcontain(no=[ip])
568 568 response.mustcontain(no=[ip_range])
569 569
570 570 def test_auth_tokens(self):
571 571 self.log_user()
572 572
573 573 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
574 574 response = self.app.get(
575 575 url('edit_user_auth_tokens', user_id=user.user_id))
576 576 response.mustcontain(user.api_key)
577 577 response.mustcontain('expires: never')
578 578
579 579 @pytest.mark.parametrize("desc, lifetime", [
580 580 ('forever', -1),
581 581 ('5mins', 60*5),
582 582 ('30days', 60*60*24*30),
583 583 ])
584 584 def test_add_auth_token(self, desc, lifetime):
585 585 self.log_user()
586 586 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
587 587 user_id = user.user_id
588 588
589 589 response = self.app.post(
590 590 url('edit_user_auth_tokens', user_id=user_id),
591 591 {'_method': 'put', 'description': desc, 'lifetime': lifetime,
592 592 'csrf_token': self.csrf_token})
593 593 assert_session_flash(response, 'Auth token successfully created')
594 594 try:
595 595 response = response.follow()
596 596 user = User.get(user_id)
597 597 for auth_token in user.auth_tokens:
598 598 response.mustcontain(auth_token)
599 599 finally:
600 600 for api_key in UserApiKeys.query().filter(
601 601 UserApiKeys.user_id == user_id).all():
602 602 Session().delete(api_key)
603 603 Session().commit()
604 604
605 605 def test_remove_auth_token(self):
606 606 self.log_user()
607 607 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
608 608 user_id = user.user_id
609 609
610 610 response = self.app.post(
611 611 url('edit_user_auth_tokens', user_id=user_id),
612 612 {'_method': 'put', 'description': 'desc', 'lifetime': -1,
613 613 'csrf_token': self.csrf_token})
614 614 assert_session_flash(response, 'Auth token successfully created')
615 615 response = response.follow()
616 616
617 617 # now delete our key
618 618 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
619 619 assert 1 == len(keys)
620 620
621 621 response = self.app.post(
622 622 url('edit_user_auth_tokens', user_id=user_id),
623 623 {'_method': 'delete', 'del_auth_token': keys[0].api_key,
624 624 'csrf_token': self.csrf_token})
625 625 assert_session_flash(response, 'Auth token successfully deleted')
626 626 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
627 627 assert 0 == len(keys)
628
629 def test_reset_main_auth_token(self):
630 self.log_user()
631 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
632 user_id = user.user_id
633 api_key = user.api_key
634 response = self.app.get(url('edit_user_auth_tokens', user_id=user_id))
635 response.mustcontain(api_key)
636 response.mustcontain('expires: never')
637
638 response = self.app.post(
639 url('edit_user_auth_tokens', user_id=user_id),
640 {'_method': 'delete', 'del_auth_token_builtin': api_key,
641 'csrf_token': self.csrf_token})
642 assert_session_flash(response, 'Auth token successfully reset')
643 response = response.follow()
644 response.mustcontain(no=[api_key])
General Comments 0
You need to be logged in to leave comments. Login now