##// END OF EJS Templates
refactor: renamed get_first_admin to get_first_super_admin which...
marcink -
r278:25d706fe default
parent child Browse files
Show More
@@ -1,406 +1,406 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 Repository groups controller for RhodeCode
24 24 """
25 25
26 26 import logging
27 27 import formencode
28 28
29 29 from formencode import htmlfill
30 30
31 31 from pylons import request, tmpl_context as c, url
32 32 from pylons.controllers.util import abort, redirect
33 33 from pylons.i18n.translation import _, ungettext
34 34
35 35 from rhodecode.lib import auth
36 36 from rhodecode.lib import helpers as h
37 37 from rhodecode.lib.ext_json import json
38 38 from rhodecode.lib.auth import (
39 39 LoginRequired, NotAnonymous, HasPermissionAll,
40 40 HasRepoGroupPermissionAll, HasRepoGroupPermissionAnyDecorator)
41 41 from rhodecode.lib.base import BaseController, render
42 42 from rhodecode.model.db import RepoGroup, User
43 43 from rhodecode.model.scm import RepoGroupList
44 44 from rhodecode.model.repo_group import RepoGroupModel
45 45 from rhodecode.model.forms import RepoGroupForm, RepoGroupPermsForm
46 46 from rhodecode.model.meta import Session
47 47 from rhodecode.lib.utils2 import safe_int
48 48
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 class RepoGroupsController(BaseController):
54 54 """REST Controller styled on the Atom Publishing Protocol"""
55 55
56 56 @LoginRequired()
57 57 def __before__(self):
58 58 super(RepoGroupsController, self).__before__()
59 59
60 60 def __load_defaults(self, allow_empty_group=False, repo_group=None):
61 61 if self._can_create_repo_group():
62 62 # we're global admin, we're ok and we can create TOP level groups
63 63 allow_empty_group = True
64 64
65 65 # override the choices for this form, we need to filter choices
66 66 # and display only those we have ADMIN right
67 67 groups_with_admin_rights = RepoGroupList(
68 68 RepoGroup.query().all(),
69 69 perm_set=['group.admin'])
70 70 c.repo_groups = RepoGroup.groups_choices(
71 71 groups=groups_with_admin_rights,
72 72 show_empty_group=allow_empty_group)
73 73
74 74 if repo_group:
75 75 # exclude filtered ids
76 76 exclude_group_ids = [repo_group.group_id]
77 77 c.repo_groups = filter(lambda x: x[0] not in exclude_group_ids,
78 78 c.repo_groups)
79 79 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
80 80 parent_group = repo_group.parent_group
81 81
82 82 add_parent_group = (parent_group and (
83 83 unicode(parent_group.group_id) not in c.repo_groups_choices))
84 84 if add_parent_group:
85 85 c.repo_groups_choices.append(unicode(parent_group.group_id))
86 86 c.repo_groups.append(RepoGroup._generate_choice(parent_group))
87 87
88 88 def __load_data(self, group_id):
89 89 """
90 90 Load defaults settings for edit, and update
91 91
92 92 :param group_id:
93 93 """
94 94 repo_group = RepoGroup.get_or_404(group_id)
95 95 data = repo_group.get_dict()
96 96 data['group_name'] = repo_group.name
97 97
98 98 # fill owner
99 99 if repo_group.user:
100 100 data.update({'user': repo_group.user.username})
101 101 else:
102 replacement_user = User.get_first_admin().username
102 replacement_user = User.get_first_super_admin().username
103 103 data.update({'user': replacement_user})
104 104
105 105 # fill repository group users
106 106 for p in repo_group.repo_group_to_perm:
107 107 data.update({
108 108 'u_perm_%s' % p.user.user_id: p.permission.permission_name})
109 109
110 110 # fill repository group user groups
111 111 for p in repo_group.users_group_to_perm:
112 112 data.update({
113 113 'g_perm_%s' % p.users_group.users_group_id:
114 114 p.permission.permission_name})
115 115 # html and form expects -1 as empty parent group
116 116 data['group_parent_id'] = data['group_parent_id'] or -1
117 117 return data
118 118
119 119 def _revoke_perms_on_yourself(self, form_result):
120 120 _updates = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
121 121 form_result['perm_updates'])
122 122 _additions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
123 123 form_result['perm_additions'])
124 124 _deletions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
125 125 form_result['perm_deletions'])
126 126 admin_perm = 'group.admin'
127 127 if _updates and _updates[0][1] != admin_perm or \
128 128 _additions and _additions[0][1] != admin_perm or \
129 129 _deletions and _deletions[0][1] != admin_perm:
130 130 return True
131 131 return False
132 132
133 133 def _can_create_repo_group(self, parent_group_id=None):
134 134 is_admin = HasPermissionAll('hg.admin')('group create controller')
135 135 create_repo_group = HasPermissionAll(
136 136 'hg.repogroup.create.true')('group create controller')
137 137 if is_admin or (create_repo_group and not parent_group_id):
138 138 # we're global admin, or we have global repo group create
139 139 # permission
140 140 # we're ok and we can create TOP level groups
141 141 return True
142 142 elif parent_group_id:
143 143 # we check the permission if we can write to parent group
144 144 group = RepoGroup.get(parent_group_id)
145 145 group_name = group.group_name if group else None
146 146 if HasRepoGroupPermissionAll('group.admin')(
147 147 group_name, 'check if user is an admin of group'):
148 148 # we're an admin of passed in group, we're ok.
149 149 return True
150 150 else:
151 151 return False
152 152 return False
153 153
154 154 @NotAnonymous()
155 155 def index(self):
156 156 """GET /repo_groups: All items in the collection"""
157 157 # url('repo_groups')
158 158
159 159 repo_group_list = RepoGroup.get_all_repo_groups()
160 160 _perms = ['group.admin']
161 161 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
162 162 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
163 163 repo_group_list=repo_group_list_acl, admin=True)
164 164 c.data = json.dumps(repo_group_data)
165 165 return render('admin/repo_groups/repo_groups.html')
166 166
167 167 # perm checks inside
168 168 @NotAnonymous()
169 169 @auth.CSRFRequired()
170 170 def create(self):
171 171 """POST /repo_groups: Create a new item"""
172 172 # url('repo_groups')
173 173
174 174 parent_group_id = safe_int(request.POST.get('group_parent_id'))
175 175 can_create = self._can_create_repo_group(parent_group_id)
176 176
177 177 self.__load_defaults()
178 178 # permissions for can create group based on parent_id are checked
179 179 # here in the Form
180 180 available_groups = map(lambda k: unicode(k[0]), c.repo_groups)
181 181 repo_group_form = RepoGroupForm(available_groups=available_groups,
182 182 can_create_in_root=can_create)()
183 183 try:
184 184 owner = c.rhodecode_user
185 185 form_result = repo_group_form.to_python(dict(request.POST))
186 186 RepoGroupModel().create(
187 187 group_name=form_result['group_name_full'],
188 188 group_description=form_result['group_description'],
189 189 owner=owner.user_id,
190 190 copy_permissions=form_result['group_copy_permissions']
191 191 )
192 192 Session().commit()
193 193 _new_group_name = form_result['group_name_full']
194 194 repo_group_url = h.link_to(
195 195 _new_group_name,
196 196 h.url('repo_group_home', group_name=_new_group_name))
197 197 h.flash(h.literal(_('Created repository group %s')
198 198 % repo_group_url), category='success')
199 199 # TODO: in futureaction_logger(, '', '', '', self.sa)
200 200 except formencode.Invalid as errors:
201 201 return htmlfill.render(
202 202 render('admin/repo_groups/repo_group_add.html'),
203 203 defaults=errors.value,
204 204 errors=errors.error_dict or {},
205 205 prefix_error=False,
206 206 encoding="UTF-8",
207 207 force_defaults=False)
208 208 except Exception:
209 209 log.exception("Exception during creation of repository group")
210 210 h.flash(_('Error occurred during creation of repository group %s')
211 211 % request.POST.get('group_name'), category='error')
212 212
213 213 # TODO: maybe we should get back to the main view, not the admin one
214 214 return redirect(url('repo_groups', parent_group=parent_group_id))
215 215
216 216 # perm checks inside
217 217 @NotAnonymous()
218 218 def new(self):
219 219 """GET /repo_groups/new: Form to create a new item"""
220 220 # url('new_repo_group')
221 221 # perm check for admin, create_group perm or admin of parent_group
222 222 parent_group_id = safe_int(request.GET.get('parent_group'))
223 223 if not self._can_create_repo_group(parent_group_id):
224 224 return abort(403)
225 225
226 226 self.__load_defaults()
227 227 return render('admin/repo_groups/repo_group_add.html')
228 228
229 229 @HasRepoGroupPermissionAnyDecorator('group.admin')
230 230 @auth.CSRFRequired()
231 231 def update(self, group_name):
232 232 """PUT /repo_groups/group_name: Update an existing item"""
233 233 # Forms posted to this method should contain a hidden field:
234 234 # <input type="hidden" name="_method" value="PUT" />
235 235 # Or using helpers:
236 236 # h.form(url('repos_group', group_name=GROUP_NAME), method='put')
237 237 # url('repo_group_home', group_name=GROUP_NAME)
238 238
239 239 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
240 240 can_create_in_root = self._can_create_repo_group()
241 241 show_root_location = can_create_in_root
242 242 if not c.repo_group.parent_group:
243 243 # this group don't have a parrent so we should show empty value
244 244 show_root_location = True
245 245 self.__load_defaults(allow_empty_group=show_root_location,
246 246 repo_group=c.repo_group)
247 247
248 248 repo_group_form = RepoGroupForm(
249 249 edit=True, old_data=c.repo_group.get_dict(),
250 250 available_groups=c.repo_groups_choices,
251 251 can_create_in_root=can_create_in_root, allow_disabled=True)()
252 252
253 253 try:
254 254 form_result = repo_group_form.to_python(dict(request.POST))
255 255 gr_name = form_result['group_name']
256 256 new_gr = RepoGroupModel().update(group_name, form_result)
257 257 Session().commit()
258 258 h.flash(_('Updated repository group %s') % (gr_name,),
259 259 category='success')
260 260 # we now have new name !
261 261 group_name = new_gr.group_name
262 262 # TODO: in future action_logger(, '', '', '', self.sa)
263 263 except formencode.Invalid as errors:
264 264 c.active = 'settings'
265 265 return htmlfill.render(
266 266 render('admin/repo_groups/repo_group_edit.html'),
267 267 defaults=errors.value,
268 268 errors=errors.error_dict or {},
269 269 prefix_error=False,
270 270 encoding="UTF-8",
271 271 force_defaults=False)
272 272 except Exception:
273 273 log.exception("Exception during update or repository group")
274 274 h.flash(_('Error occurred during update of repository group %s')
275 275 % request.POST.get('group_name'), category='error')
276 276
277 277 return redirect(url('edit_repo_group', group_name=group_name))
278 278
279 279 @HasRepoGroupPermissionAnyDecorator('group.admin')
280 280 @auth.CSRFRequired()
281 281 def delete(self, group_name):
282 282 """DELETE /repo_groups/group_name: Delete an existing item"""
283 283 # Forms posted to this method should contain a hidden field:
284 284 # <input type="hidden" name="_method" value="DELETE" />
285 285 # Or using helpers:
286 286 # h.form(url('repos_group', group_name=GROUP_NAME), method='delete')
287 287 # url('repo_group_home', group_name=GROUP_NAME)
288 288
289 289 gr = c.repo_group = RepoGroupModel()._get_repo_group(group_name)
290 290 repos = gr.repositories.all()
291 291 if repos:
292 292 msg = ungettext(
293 293 'This group contains %(num)d repository and cannot be deleted',
294 294 'This group contains %(num)d repositories and cannot be'
295 295 ' deleted',
296 296 len(repos)) % {'num': len(repos)}
297 297 h.flash(msg, category='warning')
298 298 return redirect(url('repo_groups'))
299 299
300 300 children = gr.children.all()
301 301 if children:
302 302 msg = ungettext(
303 303 'This group contains %(num)d subgroup and cannot be deleted',
304 304 'This group contains %(num)d subgroups and cannot be deleted',
305 305 len(children)) % {'num': len(children)}
306 306 h.flash(msg, category='warning')
307 307 return redirect(url('repo_groups'))
308 308
309 309 try:
310 310 RepoGroupModel().delete(group_name)
311 311 Session().commit()
312 312 h.flash(_('Removed repository group %s') % group_name,
313 313 category='success')
314 314 # TODO: in future action_logger(, '', '', '', self.sa)
315 315 except Exception:
316 316 log.exception("Exception during deletion of repository group")
317 317 h.flash(_('Error occurred during deletion of repository group %s')
318 318 % group_name, category='error')
319 319
320 320 return redirect(url('repo_groups'))
321 321
322 322 @HasRepoGroupPermissionAnyDecorator('group.admin')
323 323 def edit(self, group_name):
324 324 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
325 325 # url('edit_repo_group', group_name=GROUP_NAME)
326 326 c.active = 'settings'
327 327
328 328 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
329 329 # we can only allow moving empty group if it's already a top-level
330 330 # group, ie has no parents, or we're admin
331 331 can_create_in_root = self._can_create_repo_group()
332 332 show_root_location = can_create_in_root
333 333 if not c.repo_group.parent_group:
334 334 # this group don't have a parrent so we should show empty value
335 335 show_root_location = True
336 336 self.__load_defaults(allow_empty_group=show_root_location,
337 337 repo_group=c.repo_group)
338 338 defaults = self.__load_data(c.repo_group.group_id)
339 339
340 340 return htmlfill.render(
341 341 render('admin/repo_groups/repo_group_edit.html'),
342 342 defaults=defaults,
343 343 encoding="UTF-8",
344 344 force_defaults=False
345 345 )
346 346
347 347 @HasRepoGroupPermissionAnyDecorator('group.admin')
348 348 def edit_repo_group_advanced(self, group_name):
349 349 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
350 350 # url('edit_repo_group', group_name=GROUP_NAME)
351 351 c.active = 'advanced'
352 352 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
353 353
354 354 return render('admin/repo_groups/repo_group_edit.html')
355 355
356 356 @HasRepoGroupPermissionAnyDecorator('group.admin')
357 357 def edit_repo_group_perms(self, group_name):
358 358 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
359 359 # url('edit_repo_group', group_name=GROUP_NAME)
360 360 c.active = 'perms'
361 361 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
362 362 self.__load_defaults()
363 363 defaults = self.__load_data(c.repo_group.group_id)
364 364
365 365 return htmlfill.render(
366 366 render('admin/repo_groups/repo_group_edit.html'),
367 367 defaults=defaults,
368 368 encoding="UTF-8",
369 369 force_defaults=False
370 370 )
371 371
372 372 @HasRepoGroupPermissionAnyDecorator('group.admin')
373 373 @auth.CSRFRequired()
374 374 def update_perms(self, group_name):
375 375 """
376 376 Update permissions for given repository group
377 377
378 378 :param group_name:
379 379 """
380 380
381 381 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
382 382 valid_recursive_choices = ['none', 'repos', 'groups', 'all']
383 383 form = RepoGroupPermsForm(valid_recursive_choices)().to_python(
384 384 request.POST)
385 385
386 386 if not c.rhodecode_user.is_admin:
387 387 if self._revoke_perms_on_yourself(form):
388 388 msg = _('Cannot change permission for yourself as admin')
389 389 h.flash(msg, category='warning')
390 390 return redirect(
391 391 url('edit_repo_group_perms', group_name=group_name))
392 392
393 393 # iterate over all members(if in recursive mode) of this groups and
394 394 # set the permissions !
395 395 # this can be potentially heavy operation
396 396 RepoGroupModel().update_permissions(
397 397 c.repo_group,
398 398 form['perm_additions'], form['perm_updates'],
399 399 form['perm_deletions'], form['recursive'])
400 400
401 401 # TODO: implement this
402 402 # action_logger(c.rhodecode_user, 'admin_changed_repo_permissions',
403 403 # repo_name, self.ip_addr, self.sa)
404 404 Session().commit()
405 405 h.flash(_('Repository Group permissions updated'), category='success')
406 406 return redirect(url('edit_repo_group_perms', group_name=group_name))
@@ -1,480 +1,480 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2016 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 User Groups 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 sqlalchemy.orm import joinedload
34 34
35 35 from rhodecode.lib import auth
36 36 from rhodecode.lib import helpers as h
37 37 from rhodecode.lib.exceptions import UserGroupAssignedException,\
38 38 RepoGroupAssignmentError
39 39 from rhodecode.lib.utils import jsonify, action_logger
40 40 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
41 41 from rhodecode.lib.auth import (
42 42 LoginRequired, NotAnonymous, HasUserGroupPermissionAnyDecorator,
43 43 HasPermissionAnyDecorator)
44 44 from rhodecode.lib.base import BaseController, render
45 45 from rhodecode.model.permission import PermissionModel
46 46 from rhodecode.model.scm import UserGroupList
47 47 from rhodecode.model.user_group import UserGroupModel
48 48 from rhodecode.model.db import (
49 49 User, UserGroup, UserGroupRepoToPerm, UserGroupRepoGroupToPerm)
50 50 from rhodecode.model.forms import (
51 51 UserGroupForm, UserGroupPermsForm, UserIndividualPermissionsForm,
52 52 UserPermissionsForm)
53 53 from rhodecode.model.meta import Session
54 54 from rhodecode.lib.utils import action_logger
55 55 from rhodecode.lib.ext_json import json
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 class UserGroupsController(BaseController):
61 61 """REST Controller styled on the Atom Publishing Protocol"""
62 62
63 63 @LoginRequired()
64 64 def __before__(self):
65 65 super(UserGroupsController, self).__before__()
66 66 c.available_permissions = config['available_permissions']
67 67 PermissionModel().set_global_permission_choices(c, translator=_)
68 68
69 69 def __load_data(self, user_group_id):
70 70 c.group_members_obj = [x.user for x in c.user_group.members]
71 71 c.group_members_obj.sort(key=lambda u: u.username.lower())
72 72
73 73 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
74 74
75 75 c.available_members = [(x.user_id, x.username)
76 76 for x in User.query().all()]
77 77 c.available_members.sort(key=lambda u: u[1].lower())
78 78
79 79 def __load_defaults(self, user_group_id):
80 80 """
81 81 Load defaults settings for edit, and update
82 82
83 83 :param user_group_id:
84 84 """
85 85 user_group = UserGroup.get_or_404(user_group_id)
86 86 data = user_group.get_dict()
87 87 # fill owner
88 88 if user_group.user:
89 89 data.update({'user': user_group.user.username})
90 90 else:
91 replacement_user = User.get_first_admin().username
91 replacement_user = User.get_first_super_admin().username
92 92 data.update({'user': replacement_user})
93 93 return data
94 94
95 95 def _revoke_perms_on_yourself(self, form_result):
96 96 _updates = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
97 97 form_result['perm_updates'])
98 98 _additions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
99 99 form_result['perm_additions'])
100 100 _deletions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
101 101 form_result['perm_deletions'])
102 102 admin_perm = 'usergroup.admin'
103 103 if _updates and _updates[0][1] != admin_perm or \
104 104 _additions and _additions[0][1] != admin_perm or \
105 105 _deletions and _deletions[0][1] != admin_perm:
106 106 return True
107 107 return False
108 108
109 109 # permission check inside
110 110 @NotAnonymous()
111 111 def index(self):
112 112 """GET /users_groups: All items in the collection"""
113 113 # url('users_groups')
114 114
115 115 from rhodecode.lib.utils import PartialRenderer
116 116 _render = PartialRenderer('data_table/_dt_elements.html')
117 117
118 118 def user_group_name(user_group_id, user_group_name):
119 119 return _render("user_group_name", user_group_id, user_group_name)
120 120
121 121 def user_group_actions(user_group_id, user_group_name):
122 122 return _render("user_group_actions", user_group_id, user_group_name)
123 123
124 124 ## json generate
125 125 group_iter = UserGroupList(UserGroup.query().all(),
126 126 perm_set=['usergroup.admin'])
127 127
128 128 user_groups_data = []
129 129 for user_gr in group_iter:
130 130 user_groups_data.append({
131 131 "group_name": user_group_name(
132 132 user_gr.users_group_id, h.escape(user_gr.users_group_name)),
133 133 "group_name_raw": user_gr.users_group_name,
134 134 "desc": h.escape(user_gr.user_group_description),
135 135 "members": len(user_gr.members),
136 136 "active": h.bool2icon(user_gr.users_group_active),
137 137 "owner": h.escape(h.link_to_user(user_gr.user.username)),
138 138 "action": user_group_actions(
139 139 user_gr.users_group_id, user_gr.users_group_name)
140 140 })
141 141
142 142 c.data = json.dumps(user_groups_data)
143 143 return render('admin/user_groups/user_groups.html')
144 144
145 145 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
146 146 @auth.CSRFRequired()
147 147 def create(self):
148 148 """POST /users_groups: Create a new item"""
149 149 # url('users_groups')
150 150
151 151 users_group_form = UserGroupForm()()
152 152 try:
153 153 form_result = users_group_form.to_python(dict(request.POST))
154 154 user_group = UserGroupModel().create(
155 155 name=form_result['users_group_name'],
156 156 description=form_result['user_group_description'],
157 157 owner=c.rhodecode_user.user_id,
158 158 active=form_result['users_group_active'])
159 159 Session().flush()
160 160
161 161 user_group_name = form_result['users_group_name']
162 162 action_logger(c.rhodecode_user,
163 163 'admin_created_users_group:%s' % user_group_name,
164 164 None, self.ip_addr, self.sa)
165 165 user_group_link = h.link_to(h.escape(user_group_name),
166 166 url('edit_users_group',
167 167 user_group_id=user_group.users_group_id))
168 168 h.flash(h.literal(_('Created user group %(user_group_link)s')
169 169 % {'user_group_link': user_group_link}),
170 170 category='success')
171 171 Session().commit()
172 172 except formencode.Invalid as errors:
173 173 return htmlfill.render(
174 174 render('admin/user_groups/user_group_add.html'),
175 175 defaults=errors.value,
176 176 errors=errors.error_dict or {},
177 177 prefix_error=False,
178 178 encoding="UTF-8",
179 179 force_defaults=False)
180 180 except Exception:
181 181 log.exception("Exception creating user group")
182 182 h.flash(_('Error occurred during creation of user group %s') \
183 183 % request.POST.get('users_group_name'), category='error')
184 184
185 185 return redirect(
186 186 url('edit_users_group', user_group_id=user_group.users_group_id))
187 187
188 188 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
189 189 def new(self):
190 190 """GET /user_groups/new: Form to create a new item"""
191 191 # url('new_users_group')
192 192 return render('admin/user_groups/user_group_add.html')
193 193
194 194 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
195 195 @auth.CSRFRequired()
196 196 def update(self, user_group_id):
197 197 """PUT /user_groups/user_group_id: Update an existing item"""
198 198 # Forms posted to this method should contain a hidden field:
199 199 # <input type="hidden" name="_method" value="PUT" />
200 200 # Or using helpers:
201 201 # h.form(url('users_group', user_group_id=ID),
202 202 # method='put')
203 203 # url('users_group', user_group_id=ID)
204 204
205 205 user_group_id = safe_int(user_group_id)
206 206 c.user_group = UserGroup.get_or_404(user_group_id)
207 207 c.active = 'settings'
208 208 self.__load_data(user_group_id)
209 209
210 210 available_members = [safe_unicode(x[0]) for x in c.available_members]
211 211
212 212 users_group_form = UserGroupForm(
213 213 edit=True, old_data=c.user_group.get_dict(),
214 214 available_members=available_members, allow_disabled=True)()
215 215
216 216 try:
217 217 form_result = users_group_form.to_python(request.POST)
218 218 UserGroupModel().update(c.user_group, form_result)
219 219 gr = form_result['users_group_name']
220 220 action_logger(c.rhodecode_user,
221 221 'admin_updated_users_group:%s' % gr,
222 222 None, self.ip_addr, self.sa)
223 223 h.flash(_('Updated user group %s') % gr, category='success')
224 224 Session().commit()
225 225 except formencode.Invalid as errors:
226 226 defaults = errors.value
227 227 e = errors.error_dict or {}
228 228
229 229 return htmlfill.render(
230 230 render('admin/user_groups/user_group_edit.html'),
231 231 defaults=defaults,
232 232 errors=e,
233 233 prefix_error=False,
234 234 encoding="UTF-8",
235 235 force_defaults=False)
236 236 except Exception:
237 237 log.exception("Exception during update of user group")
238 238 h.flash(_('Error occurred during update of user group %s')
239 239 % request.POST.get('users_group_name'), category='error')
240 240
241 241 return redirect(url('edit_users_group', user_group_id=user_group_id))
242 242
243 243 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
244 244 @auth.CSRFRequired()
245 245 def delete(self, user_group_id):
246 246 """DELETE /user_groups/user_group_id: Delete an existing item"""
247 247 # Forms posted to this method should contain a hidden field:
248 248 # <input type="hidden" name="_method" value="DELETE" />
249 249 # Or using helpers:
250 250 # h.form(url('users_group', user_group_id=ID),
251 251 # method='delete')
252 252 # url('users_group', user_group_id=ID)
253 253 user_group_id = safe_int(user_group_id)
254 254 c.user_group = UserGroup.get_or_404(user_group_id)
255 255 force = str2bool(request.POST.get('force'))
256 256
257 257 try:
258 258 UserGroupModel().delete(c.user_group, force=force)
259 259 Session().commit()
260 260 h.flash(_('Successfully deleted user group'), category='success')
261 261 except UserGroupAssignedException as e:
262 262 h.flash(str(e), category='error')
263 263 except Exception:
264 264 log.exception("Exception during deletion of user group")
265 265 h.flash(_('An error occurred during deletion of user group'),
266 266 category='error')
267 267 return redirect(url('users_groups'))
268 268
269 269 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
270 270 def edit(self, user_group_id):
271 271 """GET /user_groups/user_group_id/edit: Form to edit an existing item"""
272 272 # url('edit_users_group', user_group_id=ID)
273 273
274 274 user_group_id = safe_int(user_group_id)
275 275 c.user_group = UserGroup.get_or_404(user_group_id)
276 276 c.active = 'settings'
277 277 self.__load_data(user_group_id)
278 278
279 279 defaults = self.__load_defaults(user_group_id)
280 280
281 281 return htmlfill.render(
282 282 render('admin/user_groups/user_group_edit.html'),
283 283 defaults=defaults,
284 284 encoding="UTF-8",
285 285 force_defaults=False
286 286 )
287 287
288 288 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
289 289 def edit_perms(self, user_group_id):
290 290 user_group_id = safe_int(user_group_id)
291 291 c.user_group = UserGroup.get_or_404(user_group_id)
292 292 c.active = 'perms'
293 293
294 294 defaults = {}
295 295 # fill user group users
296 296 for p in c.user_group.user_user_group_to_perm:
297 297 defaults.update({'u_perm_%s' % p.user.user_id:
298 298 p.permission.permission_name})
299 299
300 300 for p in c.user_group.user_group_user_group_to_perm:
301 301 defaults.update({'g_perm_%s' % p.user_group.users_group_id:
302 302 p.permission.permission_name})
303 303
304 304 return htmlfill.render(
305 305 render('admin/user_groups/user_group_edit.html'),
306 306 defaults=defaults,
307 307 encoding="UTF-8",
308 308 force_defaults=False
309 309 )
310 310
311 311 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
312 312 @auth.CSRFRequired()
313 313 def update_perms(self, user_group_id):
314 314 """
315 315 grant permission for given usergroup
316 316
317 317 :param user_group_id:
318 318 """
319 319 user_group_id = safe_int(user_group_id)
320 320 c.user_group = UserGroup.get_or_404(user_group_id)
321 321 form = UserGroupPermsForm()().to_python(request.POST)
322 322
323 323 if not c.rhodecode_user.is_admin:
324 324 if self._revoke_perms_on_yourself(form):
325 325 msg = _('Cannot change permission for yourself as admin')
326 326 h.flash(msg, category='warning')
327 327 return redirect(url('edit_user_group_perms', user_group_id=user_group_id))
328 328
329 329 try:
330 330 UserGroupModel().update_permissions(user_group_id,
331 331 form['perm_additions'], form['perm_updates'], form['perm_deletions'])
332 332 except RepoGroupAssignmentError:
333 333 h.flash(_('Target group cannot be the same'), category='error')
334 334 return redirect(url('edit_user_group_perms', user_group_id=user_group_id))
335 335 #TODO: implement this
336 336 #action_logger(c.rhodecode_user, 'admin_changed_repo_permissions',
337 337 # repo_name, self.ip_addr, self.sa)
338 338 Session().commit()
339 339 h.flash(_('User Group permissions updated'), category='success')
340 340 return redirect(url('edit_user_group_perms', user_group_id=user_group_id))
341 341
342 342 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
343 343 def edit_perms_summary(self, user_group_id):
344 344 user_group_id = safe_int(user_group_id)
345 345 c.user_group = UserGroup.get_or_404(user_group_id)
346 346 c.active = 'perms_summary'
347 347 permissions = {
348 348 'repositories': {},
349 349 'repositories_groups': {},
350 350 }
351 351 ugroup_repo_perms = UserGroupRepoToPerm.query()\
352 352 .options(joinedload(UserGroupRepoToPerm.permission))\
353 353 .options(joinedload(UserGroupRepoToPerm.repository))\
354 354 .filter(UserGroupRepoToPerm.users_group_id == user_group_id)\
355 355 .all()
356 356
357 357 for gr in ugroup_repo_perms:
358 358 permissions['repositories'][gr.repository.repo_name] \
359 359 = gr.permission.permission_name
360 360
361 361 ugroup_group_perms = UserGroupRepoGroupToPerm.query()\
362 362 .options(joinedload(UserGroupRepoGroupToPerm.permission))\
363 363 .options(joinedload(UserGroupRepoGroupToPerm.group))\
364 364 .filter(UserGroupRepoGroupToPerm.users_group_id == user_group_id)\
365 365 .all()
366 366
367 367 for gr in ugroup_group_perms:
368 368 permissions['repositories_groups'][gr.group.group_name] \
369 369 = gr.permission.permission_name
370 370 c.permissions = permissions
371 371 return render('admin/user_groups/user_group_edit.html')
372 372
373 373 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
374 374 def edit_global_perms(self, user_group_id):
375 375 user_group_id = safe_int(user_group_id)
376 376 c.user_group = UserGroup.get_or_404(user_group_id)
377 377 c.active = 'global_perms'
378 378
379 379 c.default_user = User.get_default_user()
380 380 defaults = c.user_group.get_dict()
381 381 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
382 382 defaults.update(c.user_group.get_default_perms())
383 383
384 384 return htmlfill.render(
385 385 render('admin/user_groups/user_group_edit.html'),
386 386 defaults=defaults,
387 387 encoding="UTF-8",
388 388 force_defaults=False
389 389 )
390 390
391 391 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
392 392 @auth.CSRFRequired()
393 393 def update_global_perms(self, user_group_id):
394 394 """PUT /users_perm/user_group_id: Update an existing item"""
395 395 # url('users_group_perm', user_group_id=ID, method='put')
396 396 user_group_id = safe_int(user_group_id)
397 397 user_group = UserGroup.get_or_404(user_group_id)
398 398 c.active = 'global_perms'
399 399
400 400 try:
401 401 # first stage that verifies the checkbox
402 402 _form = UserIndividualPermissionsForm()
403 403 form_result = _form.to_python(dict(request.POST))
404 404 inherit_perms = form_result['inherit_default_permissions']
405 405 user_group.inherit_default_permissions = inherit_perms
406 406 Session().add(user_group)
407 407
408 408 if not inherit_perms:
409 409 # only update the individual ones if we un check the flag
410 410 _form = UserPermissionsForm(
411 411 [x[0] for x in c.repo_create_choices],
412 412 [x[0] for x in c.repo_create_on_write_choices],
413 413 [x[0] for x in c.repo_group_create_choices],
414 414 [x[0] for x in c.user_group_create_choices],
415 415 [x[0] for x in c.fork_choices],
416 416 [x[0] for x in c.inherit_default_permission_choices])()
417 417
418 418 form_result = _form.to_python(dict(request.POST))
419 419 form_result.update({'perm_user_group_id': user_group.users_group_id})
420 420
421 421 PermissionModel().update_user_group_permissions(form_result)
422 422
423 423 Session().commit()
424 424 h.flash(_('User Group global permissions updated successfully'),
425 425 category='success')
426 426
427 427 except formencode.Invalid as errors:
428 428 defaults = errors.value
429 429 c.user_group = user_group
430 430 return htmlfill.render(
431 431 render('admin/user_groups/user_group_edit.html'),
432 432 defaults=defaults,
433 433 errors=errors.error_dict or {},
434 434 prefix_error=False,
435 435 encoding="UTF-8",
436 436 force_defaults=False)
437 437
438 438 except Exception:
439 439 log.exception("Exception during permissions saving")
440 440 h.flash(_('An error occurred during permissions saving'),
441 441 category='error')
442 442
443 443 return redirect(url('edit_user_group_global_perms', user_group_id=user_group_id))
444 444
445 445 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
446 446 def edit_advanced(self, user_group_id):
447 447 user_group_id = safe_int(user_group_id)
448 448 c.user_group = UserGroup.get_or_404(user_group_id)
449 449 c.active = 'advanced'
450 450 c.group_members_obj = sorted(
451 451 (x.user for x in c.user_group.members),
452 452 key=lambda u: u.username.lower())
453 453
454 454 c.group_to_repos = sorted(
455 455 (x.repository for x in c.user_group.users_group_repo_to_perm),
456 456 key=lambda u: u.repo_name.lower())
457 457
458 458 c.group_to_repo_groups = sorted(
459 459 (x.group for x in c.user_group.users_group_repo_group_to_perm),
460 460 key=lambda u: u.group_name.lower())
461 461
462 462 return render('admin/user_groups/user_group_edit.html')
463 463
464 464 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
465 465 def edit_members(self, user_group_id):
466 466 user_group_id = safe_int(user_group_id)
467 467 c.user_group = UserGroup.get_or_404(user_group_id)
468 468 c.active = 'members'
469 469 c.group_members_obj = sorted((x.user for x in c.user_group.members),
470 470 key=lambda u: u.username.lower())
471 471
472 472 group_members = [(x.user_id, x.username) for x in c.group_members_obj]
473 473
474 474 if request.is_xhr:
475 475 return jsonify(lambda *a, **k: {
476 476 'members': group_members
477 477 })
478 478
479 479 c.group_members = group_members
480 480 return render('admin/user_groups/user_group_edit.html')
@@ -1,719 +1,719 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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.user import UserModel
49 49 from rhodecode.model.meta import Session
50 50 from rhodecode.model.permission import PermissionModel
51 51 from rhodecode.lib.utils import action_logger
52 52 from rhodecode.lib.ext_json import json
53 53 from rhodecode.lib.utils2 import datetime_to_time, safe_int
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class UsersController(BaseController):
59 59 """REST Controller styled on the Atom Publishing Protocol"""
60 60
61 61 @LoginRequired()
62 62 def __before__(self):
63 63 super(UsersController, self).__before__()
64 64 c.available_permissions = config['available_permissions']
65 65 c.allowed_languages = [
66 66 ('en', 'English (en)'),
67 67 ('de', 'German (de)'),
68 68 ('fr', 'French (fr)'),
69 69 ('it', 'Italian (it)'),
70 70 ('ja', 'Japanese (ja)'),
71 71 ('pl', 'Polish (pl)'),
72 72 ('pt', 'Portuguese (pt)'),
73 73 ('ru', 'Russian (ru)'),
74 74 ('zh', 'Chinese (zh)'),
75 75 ]
76 76 PermissionModel().set_global_permission_choices(c, translator=_)
77 77
78 78 @HasPermissionAllDecorator('hg.admin')
79 79 def index(self):
80 80 """GET /users: All items in the collection"""
81 81 # url('users')
82 82
83 83 from rhodecode.lib.utils import PartialRenderer
84 84 _render = PartialRenderer('data_table/_dt_elements.html')
85 85
86 86 def grav_tmpl(user_email, size):
87 87 return _render("user_gravatar", user_email, size)
88 88
89 89 def username(user_id, username):
90 90 return _render("user_name", user_id, username)
91 91
92 92 def user_actions(user_id, username):
93 93 return _render("user_actions", user_id, username)
94 94
95 95 # json generate
96 96 c.users_list = User.query()\
97 97 .filter(User.username != User.DEFAULT_USER) \
98 98 .all()
99 99
100 100 users_data = []
101 101 for user in c.users_list:
102 102 users_data.append({
103 103 "gravatar": grav_tmpl(user.email, 20),
104 104 "username": h.link_to(
105 105 user.username, h.url('user_profile', username=user.username)),
106 106 "username_raw": user.username,
107 107 "email": user.email,
108 108 "first_name": h.escape(user.name),
109 109 "last_name": h.escape(user.lastname),
110 110 "last_login": h.format_date(user.last_login),
111 111 "last_login_raw": datetime_to_time(user.last_login),
112 112 "last_activity": h.format_date(
113 113 h.time_to_datetime(user.user_data.get('last_activity', 0))),
114 114 "last_activity_raw": user.user_data.get('last_activity', 0),
115 115 "active": h.bool2icon(user.active),
116 116 "active_raw": user.active,
117 117 "admin": h.bool2icon(user.admin),
118 118 "admin_raw": user.admin,
119 119 "extern_type": user.extern_type,
120 120 "extern_name": user.extern_name,
121 121 "action": user_actions(user.user_id, user.username),
122 122 })
123 123
124 124
125 125 c.data = json.dumps(users_data)
126 126 return render('admin/users/users.html')
127 127
128 128 @HasPermissionAllDecorator('hg.admin')
129 129 @auth.CSRFRequired()
130 130 def create(self):
131 131 """POST /users: Create a new item"""
132 132 # url('users')
133 133 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
134 134 user_model = UserModel()
135 135 user_form = UserForm()()
136 136 try:
137 137 form_result = user_form.to_python(dict(request.POST))
138 138 user = user_model.create(form_result)
139 139 Session().flush()
140 140 username = form_result['username']
141 141 action_logger(c.rhodecode_user, 'admin_created_user:%s' % username,
142 142 None, self.ip_addr, self.sa)
143 143
144 144 user_link = h.link_to(h.escape(username),
145 145 url('edit_user',
146 146 user_id=user.user_id))
147 147 h.flash(h.literal(_('Created user %(user_link)s')
148 148 % {'user_link': user_link}), category='success')
149 149 Session().commit()
150 150 except formencode.Invalid as errors:
151 151 return htmlfill.render(
152 152 render('admin/users/user_add.html'),
153 153 defaults=errors.value,
154 154 errors=errors.error_dict or {},
155 155 prefix_error=False,
156 156 encoding="UTF-8",
157 157 force_defaults=False)
158 158 except UserCreationError as e:
159 159 h.flash(e, 'error')
160 160 except Exception:
161 161 log.exception("Exception creation of user")
162 162 h.flash(_('Error occurred during creation of user %s')
163 163 % request.POST.get('username'), category='error')
164 164 return redirect(url('users'))
165 165
166 166 @HasPermissionAllDecorator('hg.admin')
167 167 def new(self):
168 168 """GET /users/new: Form to create a new item"""
169 169 # url('new_user')
170 170 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
171 171 return render('admin/users/user_add.html')
172 172
173 173 @HasPermissionAllDecorator('hg.admin')
174 174 @auth.CSRFRequired()
175 175 def update(self, user_id):
176 176 """PUT /users/user_id: Update an existing item"""
177 177 # Forms posted to this method should contain a hidden field:
178 178 # <input type="hidden" name="_method" value="PUT" />
179 179 # Or using helpers:
180 180 # h.form(url('update_user', user_id=ID),
181 181 # method='put')
182 182 # url('user', user_id=ID)
183 183 user_id = safe_int(user_id)
184 184 c.user = User.get_or_404(user_id)
185 185 c.active = 'profile'
186 186 c.extern_type = c.user.extern_type
187 187 c.extern_name = c.user.extern_name
188 188 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
189 189 available_languages = [x[0] for x in c.allowed_languages]
190 190 _form = UserForm(edit=True, available_languages=available_languages,
191 191 old_data={'user_id': user_id,
192 192 'email': c.user.email})()
193 193 form_result = {}
194 194 try:
195 195 form_result = _form.to_python(dict(request.POST))
196 196 skip_attrs = ['extern_type', 'extern_name']
197 197 # TODO: plugin should define if username can be updated
198 198 if c.extern_type != "rhodecode":
199 199 # forbid updating username for external accounts
200 200 skip_attrs.append('username')
201 201
202 202 UserModel().update_user(user_id, skip_attrs=skip_attrs, **form_result)
203 203 usr = form_result['username']
204 204 action_logger(c.rhodecode_user, 'admin_updated_user:%s' % usr,
205 205 None, self.ip_addr, self.sa)
206 206 h.flash(_('User updated successfully'), category='success')
207 207 Session().commit()
208 208 except formencode.Invalid as errors:
209 209 defaults = errors.value
210 210 e = errors.error_dict or {}
211 211
212 212 return htmlfill.render(
213 213 render('admin/users/user_edit.html'),
214 214 defaults=defaults,
215 215 errors=e,
216 216 prefix_error=False,
217 217 encoding="UTF-8",
218 218 force_defaults=False)
219 219 except UserCreationError as e:
220 220 h.flash(e, 'error')
221 221 except Exception:
222 222 log.exception("Exception updating user")
223 223 h.flash(_('Error occurred during update of user %s')
224 224 % form_result.get('username'), category='error')
225 225 return redirect(url('edit_user', user_id=user_id))
226 226
227 227 @HasPermissionAllDecorator('hg.admin')
228 228 @auth.CSRFRequired()
229 229 def delete(self, user_id):
230 230 """DELETE /users/user_id: Delete an existing item"""
231 231 # Forms posted to this method should contain a hidden field:
232 232 # <input type="hidden" name="_method" value="DELETE" />
233 233 # Or using helpers:
234 234 # h.form(url('delete_user', user_id=ID),
235 235 # method='delete')
236 236 # url('user', user_id=ID)
237 237 user_id = safe_int(user_id)
238 238 c.user = User.get_or_404(user_id)
239 239
240 240 _repos = c.user.repositories
241 241 _repo_groups = c.user.repository_groups
242 242 _user_groups = c.user.user_groups
243 243
244 244 handle_repos = None
245 245 handle_repo_groups = None
246 246 handle_user_groups = None
247 247 # dummy call for flash of handle
248 248 set_handle_flash_repos = lambda: None
249 249 set_handle_flash_repo_groups = lambda: None
250 250 set_handle_flash_user_groups = lambda: None
251 251
252 252 if _repos and request.POST.get('user_repos'):
253 253 do = request.POST['user_repos']
254 254 if do == 'detach':
255 255 handle_repos = 'detach'
256 256 set_handle_flash_repos = lambda: h.flash(
257 257 _('Detached %s repositories') % len(_repos),
258 258 category='success')
259 259 elif do == 'delete':
260 260 handle_repos = 'delete'
261 261 set_handle_flash_repos = lambda: h.flash(
262 262 _('Deleted %s repositories') % len(_repos),
263 263 category='success')
264 264
265 265 if _repo_groups and request.POST.get('user_repo_groups'):
266 266 do = request.POST['user_repo_groups']
267 267 if do == 'detach':
268 268 handle_repo_groups = 'detach'
269 269 set_handle_flash_repo_groups = lambda: h.flash(
270 270 _('Detached %s repository groups') % len(_repo_groups),
271 271 category='success')
272 272 elif do == 'delete':
273 273 handle_repo_groups = 'delete'
274 274 set_handle_flash_repo_groups = lambda: h.flash(
275 275 _('Deleted %s repository groups') % len(_repo_groups),
276 276 category='success')
277 277
278 278 if _user_groups and request.POST.get('user_user_groups'):
279 279 do = request.POST['user_user_groups']
280 280 if do == 'detach':
281 281 handle_user_groups = 'detach'
282 282 set_handle_flash_user_groups = lambda: h.flash(
283 283 _('Detached %s user groups') % len(_user_groups),
284 284 category='success')
285 285 elif do == 'delete':
286 286 handle_user_groups = 'delete'
287 287 set_handle_flash_user_groups = lambda: h.flash(
288 288 _('Deleted %s user groups') % len(_user_groups),
289 289 category='success')
290 290
291 291 try:
292 292 UserModel().delete(c.user, handle_repos=handle_repos,
293 293 handle_repo_groups=handle_repo_groups,
294 294 handle_user_groups=handle_user_groups)
295 295 Session().commit()
296 296 set_handle_flash_repos()
297 297 set_handle_flash_repo_groups()
298 298 set_handle_flash_user_groups()
299 299 h.flash(_('Successfully deleted user'), category='success')
300 300 except (UserOwnsReposException, UserOwnsRepoGroupsException,
301 301 UserOwnsUserGroupsException, DefaultUserException) as e:
302 302 h.flash(e, category='warning')
303 303 except Exception:
304 304 log.exception("Exception during deletion of user")
305 305 h.flash(_('An error occurred during deletion of user'),
306 306 category='error')
307 307 return redirect(url('users'))
308 308
309 309 @HasPermissionAllDecorator('hg.admin')
310 310 @auth.CSRFRequired()
311 311 def reset_password(self, user_id):
312 312 """
313 313 toggle reset password flag for this user
314 314
315 315 :param user_id:
316 316 """
317 317 user_id = safe_int(user_id)
318 318 c.user = User.get_or_404(user_id)
319 319 try:
320 320 old_value = c.user.user_data.get('force_password_change')
321 321 c.user.update_userdata(force_password_change=not old_value)
322 322 Session().commit()
323 323 if old_value:
324 324 msg = _('Force password change disabled for user')
325 325 else:
326 326 msg = _('Force password change enabled for user')
327 327 h.flash(msg, category='success')
328 328 except Exception:
329 329 log.exception("Exception during password reset for user")
330 330 h.flash(_('An error occurred during password reset for user'),
331 331 category='error')
332 332
333 333 return redirect(url('edit_user_advanced', user_id=user_id))
334 334
335 335 @HasPermissionAllDecorator('hg.admin')
336 336 @auth.CSRFRequired()
337 337 def create_personal_repo_group(self, user_id):
338 338 """
339 339 Create personal repository group for this user
340 340
341 341 :param user_id:
342 342 """
343 343 from rhodecode.model.repo_group import RepoGroupModel
344 344
345 345 user_id = safe_int(user_id)
346 346 c.user = User.get_or_404(user_id)
347 347
348 348 try:
349 349 desc = RepoGroupModel.PERSONAL_GROUP_DESC % {
350 350 'username': c.user.username}
351 351 if not RepoGroup.get_by_group_name(c.user.username):
352 352 RepoGroupModel().create(group_name=c.user.username,
353 353 group_description=desc,
354 354 owner=c.user.username)
355 355
356 356 msg = _('Created repository group `%s`' % (c.user.username,))
357 357 h.flash(msg, category='success')
358 358 except Exception:
359 359 log.exception("Exception during repository group creation")
360 360 msg = _(
361 361 'An error occurred during repository group creation for user')
362 362 h.flash(msg, category='error')
363 363
364 364 return redirect(url('edit_user_advanced', user_id=user_id))
365 365
366 366 @HasPermissionAllDecorator('hg.admin')
367 367 def show(self, user_id):
368 368 """GET /users/user_id: Show a specific item"""
369 369 # url('user', user_id=ID)
370 370 User.get_or_404(-1)
371 371
372 372 @HasPermissionAllDecorator('hg.admin')
373 373 def edit(self, user_id):
374 374 """GET /users/user_id/edit: Form to edit an existing item"""
375 375 # url('edit_user', user_id=ID)
376 376 user_id = safe_int(user_id)
377 377 c.user = User.get_or_404(user_id)
378 378 if c.user.username == User.DEFAULT_USER:
379 379 h.flash(_("You can't edit this user"), category='warning')
380 380 return redirect(url('users'))
381 381
382 382 c.active = 'profile'
383 383 c.extern_type = c.user.extern_type
384 384 c.extern_name = c.user.extern_name
385 385 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
386 386
387 387 defaults = c.user.get_dict()
388 388 defaults.update({'language': c.user.user_data.get('language')})
389 389 return htmlfill.render(
390 390 render('admin/users/user_edit.html'),
391 391 defaults=defaults,
392 392 encoding="UTF-8",
393 393 force_defaults=False)
394 394
395 395 @HasPermissionAllDecorator('hg.admin')
396 396 def edit_advanced(self, user_id):
397 397 user_id = safe_int(user_id)
398 398 user = c.user = User.get_or_404(user_id)
399 399 if user.username == User.DEFAULT_USER:
400 400 h.flash(_("You can't edit this user"), category='warning')
401 401 return redirect(url('users'))
402 402
403 403 c.active = 'advanced'
404 404 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
405 405 c.personal_repo_group = RepoGroup.get_by_group_name(user.username)
406 c.first_admin = User.get_first_admin()
406 c.first_admin = User.get_first_super_admin()
407 407 defaults = user.get_dict()
408 408
409 409 # Interim workaround if the user participated on any pull requests as a
410 410 # reviewer.
411 411 has_review = bool(PullRequestReviewers.query().filter(
412 412 PullRequestReviewers.user_id == user_id).first())
413 413 c.can_delete_user = not has_review
414 414 c.can_delete_user_message = _(
415 415 'The user participates as reviewer in pull requests and '
416 416 'cannot be deleted. You can set the user to '
417 417 '"inactive" instead of deleting it.') if has_review else ''
418 418
419 419 return htmlfill.render(
420 420 render('admin/users/user_edit.html'),
421 421 defaults=defaults,
422 422 encoding="UTF-8",
423 423 force_defaults=False)
424 424
425 425 @HasPermissionAllDecorator('hg.admin')
426 426 def edit_auth_tokens(self, user_id):
427 427 user_id = safe_int(user_id)
428 428 c.user = User.get_or_404(user_id)
429 429 if c.user.username == User.DEFAULT_USER:
430 430 h.flash(_("You can't edit this user"), category='warning')
431 431 return redirect(url('users'))
432 432
433 433 c.active = 'auth_tokens'
434 434 show_expired = True
435 435 c.lifetime_values = [
436 436 (str(-1), _('forever')),
437 437 (str(5), _('5 minutes')),
438 438 (str(60), _('1 hour')),
439 439 (str(60 * 24), _('1 day')),
440 440 (str(60 * 24 * 30), _('1 month')),
441 441 ]
442 442 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
443 443 c.role_values = [(x, AuthTokenModel.cls._get_role_name(x))
444 444 for x in AuthTokenModel.cls.ROLES]
445 445 c.role_options = [(c.role_values, _("Role"))]
446 446 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
447 447 c.user.user_id, show_expired=show_expired)
448 448 defaults = c.user.get_dict()
449 449 return htmlfill.render(
450 450 render('admin/users/user_edit.html'),
451 451 defaults=defaults,
452 452 encoding="UTF-8",
453 453 force_defaults=False)
454 454
455 455 @HasPermissionAllDecorator('hg.admin')
456 456 @auth.CSRFRequired()
457 457 def add_auth_token(self, user_id):
458 458 user_id = safe_int(user_id)
459 459 c.user = User.get_or_404(user_id)
460 460 if c.user.username == User.DEFAULT_USER:
461 461 h.flash(_("You can't edit this user"), category='warning')
462 462 return redirect(url('users'))
463 463
464 464 lifetime = safe_int(request.POST.get('lifetime'), -1)
465 465 description = request.POST.get('description')
466 466 role = request.POST.get('role')
467 467 AuthTokenModel().create(c.user.user_id, description, lifetime, role)
468 468 Session().commit()
469 469 h.flash(_("Auth token successfully created"), category='success')
470 470 return redirect(url('edit_user_auth_tokens', user_id=c.user.user_id))
471 471
472 472 @HasPermissionAllDecorator('hg.admin')
473 473 @auth.CSRFRequired()
474 474 def delete_auth_token(self, user_id):
475 475 user_id = safe_int(user_id)
476 476 c.user = User.get_or_404(user_id)
477 477 if c.user.username == User.DEFAULT_USER:
478 478 h.flash(_("You can't edit this user"), category='warning')
479 479 return redirect(url('users'))
480 480
481 481 auth_token = request.POST.get('del_auth_token')
482 482 if request.POST.get('del_auth_token_builtin'):
483 483 user = User.get(c.user.user_id)
484 484 if user:
485 485 user.api_key = generate_auth_token(user.username)
486 486 Session().add(user)
487 487 Session().commit()
488 488 h.flash(_("Auth token successfully reset"), category='success')
489 489 elif auth_token:
490 490 AuthTokenModel().delete(auth_token, c.user.user_id)
491 491 Session().commit()
492 492 h.flash(_("Auth token successfully deleted"), category='success')
493 493
494 494 return redirect(url('edit_user_auth_tokens', user_id=c.user.user_id))
495 495
496 496 @HasPermissionAllDecorator('hg.admin')
497 497 def edit_global_perms(self, user_id):
498 498 user_id = safe_int(user_id)
499 499 c.user = User.get_or_404(user_id)
500 500 if c.user.username == User.DEFAULT_USER:
501 501 h.flash(_("You can't edit this user"), category='warning')
502 502 return redirect(url('users'))
503 503
504 504 c.active = 'global_perms'
505 505
506 506 c.default_user = User.get_default_user()
507 507 defaults = c.user.get_dict()
508 508 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
509 509 defaults.update(c.default_user.get_default_perms())
510 510 defaults.update(c.user.get_default_perms())
511 511
512 512 return htmlfill.render(
513 513 render('admin/users/user_edit.html'),
514 514 defaults=defaults,
515 515 encoding="UTF-8",
516 516 force_defaults=False)
517 517
518 518 @HasPermissionAllDecorator('hg.admin')
519 519 @auth.CSRFRequired()
520 520 def update_global_perms(self, user_id):
521 521 """PUT /users_perm/user_id: Update an existing item"""
522 522 # url('user_perm', user_id=ID, method='put')
523 523 user_id = safe_int(user_id)
524 524 user = User.get_or_404(user_id)
525 525 c.active = 'global_perms'
526 526 try:
527 527 # first stage that verifies the checkbox
528 528 _form = UserIndividualPermissionsForm()
529 529 form_result = _form.to_python(dict(request.POST))
530 530 inherit_perms = form_result['inherit_default_permissions']
531 531 user.inherit_default_permissions = inherit_perms
532 532 Session().add(user)
533 533
534 534 if not inherit_perms:
535 535 # only update the individual ones if we un check the flag
536 536 _form = UserPermissionsForm(
537 537 [x[0] for x in c.repo_create_choices],
538 538 [x[0] for x in c.repo_create_on_write_choices],
539 539 [x[0] for x in c.repo_group_create_choices],
540 540 [x[0] for x in c.user_group_create_choices],
541 541 [x[0] for x in c.fork_choices],
542 542 [x[0] for x in c.inherit_default_permission_choices])()
543 543
544 544 form_result = _form.to_python(dict(request.POST))
545 545 form_result.update({'perm_user_id': user.user_id})
546 546
547 547 PermissionModel().update_user_permissions(form_result)
548 548
549 549 Session().commit()
550 550 h.flash(_('User global permissions updated successfully'),
551 551 category='success')
552 552
553 553 Session().commit()
554 554 except formencode.Invalid as errors:
555 555 defaults = errors.value
556 556 c.user = user
557 557 return htmlfill.render(
558 558 render('admin/users/user_edit.html'),
559 559 defaults=defaults,
560 560 errors=errors.error_dict or {},
561 561 prefix_error=False,
562 562 encoding="UTF-8",
563 563 force_defaults=False)
564 564 except Exception:
565 565 log.exception("Exception during permissions saving")
566 566 h.flash(_('An error occurred during permissions saving'),
567 567 category='error')
568 568 return redirect(url('edit_user_global_perms', user_id=user_id))
569 569
570 570 @HasPermissionAllDecorator('hg.admin')
571 571 def edit_perms_summary(self, user_id):
572 572 user_id = safe_int(user_id)
573 573 c.user = User.get_or_404(user_id)
574 574 if c.user.username == User.DEFAULT_USER:
575 575 h.flash(_("You can't edit this user"), category='warning')
576 576 return redirect(url('users'))
577 577
578 578 c.active = 'perms_summary'
579 579 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
580 580
581 581 return render('admin/users/user_edit.html')
582 582
583 583 @HasPermissionAllDecorator('hg.admin')
584 584 def edit_emails(self, user_id):
585 585 user_id = safe_int(user_id)
586 586 c.user = User.get_or_404(user_id)
587 587 if c.user.username == User.DEFAULT_USER:
588 588 h.flash(_("You can't edit this user"), category='warning')
589 589 return redirect(url('users'))
590 590
591 591 c.active = 'emails'
592 592 c.user_email_map = UserEmailMap.query() \
593 593 .filter(UserEmailMap.user == c.user).all()
594 594
595 595 defaults = c.user.get_dict()
596 596 return htmlfill.render(
597 597 render('admin/users/user_edit.html'),
598 598 defaults=defaults,
599 599 encoding="UTF-8",
600 600 force_defaults=False)
601 601
602 602 @HasPermissionAllDecorator('hg.admin')
603 603 @auth.CSRFRequired()
604 604 def add_email(self, user_id):
605 605 """POST /user_emails:Add an existing item"""
606 606 # url('user_emails', user_id=ID, method='put')
607 607 user_id = safe_int(user_id)
608 608 c.user = User.get_or_404(user_id)
609 609
610 610 email = request.POST.get('new_email')
611 611 user_model = UserModel()
612 612
613 613 try:
614 614 user_model.add_extra_email(user_id, email)
615 615 Session().commit()
616 616 h.flash(_("Added new email address `%s` for user account") % email,
617 617 category='success')
618 618 except formencode.Invalid as error:
619 619 msg = error.error_dict['email']
620 620 h.flash(msg, category='error')
621 621 except Exception:
622 622 log.exception("Exception during email saving")
623 623 h.flash(_('An error occurred during email saving'),
624 624 category='error')
625 625 return redirect(url('edit_user_emails', user_id=user_id))
626 626
627 627 @HasPermissionAllDecorator('hg.admin')
628 628 @auth.CSRFRequired()
629 629 def delete_email(self, user_id):
630 630 """DELETE /user_emails_delete/user_id: Delete an existing item"""
631 631 # url('user_emails_delete', user_id=ID, method='delete')
632 632 user_id = safe_int(user_id)
633 633 c.user = User.get_or_404(user_id)
634 634 email_id = request.POST.get('del_email_id')
635 635 user_model = UserModel()
636 636 user_model.delete_extra_email(user_id, email_id)
637 637 Session().commit()
638 638 h.flash(_("Removed email address from user account"), category='success')
639 639 return redirect(url('edit_user_emails', user_id=user_id))
640 640
641 641 @HasPermissionAllDecorator('hg.admin')
642 642 def edit_ips(self, user_id):
643 643 user_id = safe_int(user_id)
644 644 c.user = User.get_or_404(user_id)
645 645 if c.user.username == User.DEFAULT_USER:
646 646 h.flash(_("You can't edit this user"), category='warning')
647 647 return redirect(url('users'))
648 648
649 649 c.active = 'ips'
650 650 c.user_ip_map = UserIpMap.query() \
651 651 .filter(UserIpMap.user == c.user).all()
652 652
653 653 c.inherit_default_ips = c.user.inherit_default_permissions
654 654 c.default_user_ip_map = UserIpMap.query() \
655 655 .filter(UserIpMap.user == User.get_default_user()).all()
656 656
657 657 defaults = c.user.get_dict()
658 658 return htmlfill.render(
659 659 render('admin/users/user_edit.html'),
660 660 defaults=defaults,
661 661 encoding="UTF-8",
662 662 force_defaults=False)
663 663
664 664 @HasPermissionAllDecorator('hg.admin')
665 665 @auth.CSRFRequired()
666 666 def add_ip(self, user_id):
667 667 """POST /user_ips:Add an existing item"""
668 668 # url('user_ips', user_id=ID, method='put')
669 669
670 670 user_id = safe_int(user_id)
671 671 c.user = User.get_or_404(user_id)
672 672 user_model = UserModel()
673 673 try:
674 674 ip_list = user_model.parse_ip_range(request.POST.get('new_ip'))
675 675 except Exception as e:
676 676 ip_list = []
677 677 log.exception("Exception during ip saving")
678 678 h.flash(_('An error occurred during ip saving:%s' % (e,)),
679 679 category='error')
680 680
681 681 desc = request.POST.get('description')
682 682 added = []
683 683 for ip in ip_list:
684 684 try:
685 685 user_model.add_extra_ip(user_id, ip, desc)
686 686 Session().commit()
687 687 added.append(ip)
688 688 except formencode.Invalid as error:
689 689 msg = error.error_dict['ip']
690 690 h.flash(msg, category='error')
691 691 except Exception:
692 692 log.exception("Exception during ip saving")
693 693 h.flash(_('An error occurred during ip saving'),
694 694 category='error')
695 695 if added:
696 696 h.flash(
697 697 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
698 698 category='success')
699 699 if 'default_user' in request.POST:
700 700 return redirect(url('admin_permissions_ips'))
701 701 return redirect(url('edit_user_ips', user_id=user_id))
702 702
703 703 @HasPermissionAllDecorator('hg.admin')
704 704 @auth.CSRFRequired()
705 705 def delete_ip(self, user_id):
706 706 """DELETE /user_ips_delete/user_id: Delete an existing item"""
707 707 # url('user_ips_delete', user_id=ID, method='delete')
708 708 user_id = safe_int(user_id)
709 709 c.user = User.get_or_404(user_id)
710 710
711 711 ip_id = request.POST.get('del_ip_id')
712 712 user_model = UserModel()
713 713 user_model.delete_extra_ip(user_id, ip_id)
714 714 Session().commit()
715 715 h.flash(_("Removed ip address from user whitelist"), category='success')
716 716
717 717 if 'default_user' in request.POST:
718 718 return redirect(url('admin_permissions_ips'))
719 719 return redirect(url('edit_user_ips', user_id=user_id))
@@ -1,977 +1,977 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 Utilities library for RhodeCode
23 23 """
24 24
25 25 import datetime
26 26 import decorator
27 27 import json
28 28 import logging
29 29 import os
30 30 import re
31 31 import shutil
32 32 import tempfile
33 33 import traceback
34 34 import tarfile
35 35 import warnings
36 36 from os.path import abspath
37 37 from os.path import dirname as dn, join as jn
38 38
39 39 import paste
40 40 import pkg_resources
41 41 from paste.script.command import Command, BadCommand
42 42 from webhelpers.text import collapse, remove_formatting, strip_tags
43 43 from mako import exceptions
44 44
45 45 from rhodecode.lib.fakemod import create_module
46 46 from rhodecode.lib.vcs.backends.base import Config
47 47 from rhodecode.lib.vcs.exceptions import VCSError
48 48 from rhodecode.lib.vcs.utils.helpers import get_scm, get_scm_backend
49 49 from rhodecode.lib.utils2 import (
50 50 safe_str, safe_unicode, get_current_rhodecode_user, md5)
51 51 from rhodecode.model import meta
52 52 from rhodecode.model.db import (
53 53 Repository, User, RhodeCodeUi, UserLog, RepoGroup, UserGroup)
54 54 from rhodecode.model.meta import Session
55 55
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
60 60
61 61 _license_cache = None
62 62
63 63
64 64 def recursive_replace(str_, replace=' '):
65 65 """
66 66 Recursive replace of given sign to just one instance
67 67
68 68 :param str_: given string
69 69 :param replace: char to find and replace multiple instances
70 70
71 71 Examples::
72 72 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
73 73 'Mighty-Mighty-Bo-sstones'
74 74 """
75 75
76 76 if str_.find(replace * 2) == -1:
77 77 return str_
78 78 else:
79 79 str_ = str_.replace(replace * 2, replace)
80 80 return recursive_replace(str_, replace)
81 81
82 82
83 83 def repo_name_slug(value):
84 84 """
85 85 Return slug of name of repository
86 86 This function is called on each creation/modification
87 87 of repository to prevent bad names in repo
88 88 """
89 89
90 90 slug = remove_formatting(value)
91 91 slug = strip_tags(slug)
92 92
93 93 for c in """`?=[]\;'"<>,/~!@#$%^&*()+{}|: """:
94 94 slug = slug.replace(c, '-')
95 95 slug = recursive_replace(slug, '-')
96 96 slug = collapse(slug, '-')
97 97 return slug
98 98
99 99
100 100 #==============================================================================
101 101 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
102 102 #==============================================================================
103 103 def get_repo_slug(request):
104 104 _repo = request.environ['pylons.routes_dict'].get('repo_name')
105 105 if _repo:
106 106 _repo = _repo.rstrip('/')
107 107 return _repo
108 108
109 109
110 110 def get_repo_group_slug(request):
111 111 _group = request.environ['pylons.routes_dict'].get('group_name')
112 112 if _group:
113 113 _group = _group.rstrip('/')
114 114 return _group
115 115
116 116
117 117 def get_user_group_slug(request):
118 118 _group = request.environ['pylons.routes_dict'].get('user_group_id')
119 119 try:
120 120 _group = UserGroup.get(_group)
121 121 if _group:
122 122 _group = _group.users_group_name
123 123 except Exception:
124 124 log.debug(traceback.format_exc())
125 125 #catch all failures here
126 126 pass
127 127
128 128 return _group
129 129
130 130
131 131 def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
132 132 """
133 133 Action logger for various actions made by users
134 134
135 135 :param user: user that made this action, can be a unique username string or
136 136 object containing user_id attribute
137 137 :param action: action to log, should be on of predefined unique actions for
138 138 easy translations
139 139 :param repo: string name of repository or object containing repo_id,
140 140 that action was made on
141 141 :param ipaddr: optional ip address from what the action was made
142 142 :param sa: optional sqlalchemy session
143 143
144 144 """
145 145
146 146 if not sa:
147 147 sa = meta.Session()
148 148 # if we don't get explicit IP address try to get one from registered user
149 149 # in tmpl context var
150 150 if not ipaddr:
151 151 ipaddr = getattr(get_current_rhodecode_user(), 'ip_addr', '')
152 152
153 153 try:
154 154 if getattr(user, 'user_id', None):
155 155 user_obj = User.get(user.user_id)
156 156 elif isinstance(user, basestring):
157 157 user_obj = User.get_by_username(user)
158 158 else:
159 159 raise Exception('You have to provide a user object or a username')
160 160
161 161 if getattr(repo, 'repo_id', None):
162 162 repo_obj = Repository.get(repo.repo_id)
163 163 repo_name = repo_obj.repo_name
164 164 elif isinstance(repo, basestring):
165 165 repo_name = repo.lstrip('/')
166 166 repo_obj = Repository.get_by_repo_name(repo_name)
167 167 else:
168 168 repo_obj = None
169 169 repo_name = ''
170 170
171 171 user_log = UserLog()
172 172 user_log.user_id = user_obj.user_id
173 173 user_log.username = user_obj.username
174 174 action = safe_unicode(action)
175 175 user_log.action = action[:1200000]
176 176
177 177 user_log.repository = repo_obj
178 178 user_log.repository_name = repo_name
179 179
180 180 user_log.action_date = datetime.datetime.now()
181 181 user_log.user_ip = ipaddr
182 182 sa.add(user_log)
183 183
184 184 log.info('Logging action:`%s` on repo:`%s` by user:%s ip:%s',
185 185 action, safe_unicode(repo), user_obj, ipaddr)
186 186 if commit:
187 187 sa.commit()
188 188 except Exception:
189 189 log.error(traceback.format_exc())
190 190 raise
191 191
192 192
193 193 def get_filesystem_repos(path, recursive=False, skip_removed_repos=True):
194 194 """
195 195 Scans given path for repos and return (name,(type,path)) tuple
196 196
197 197 :param path: path to scan for repositories
198 198 :param recursive: recursive search and return names with subdirs in front
199 199 """
200 200
201 201 # remove ending slash for better results
202 202 path = path.rstrip(os.sep)
203 203 log.debug('now scanning in %s location recursive:%s...', path, recursive)
204 204
205 205 def _get_repos(p):
206 206 dirpaths = _get_dirpaths(p)
207 207 if not _is_dir_writable(p):
208 208 log.warning('repo path without write access: %s', p)
209 209
210 210 for dirpath in dirpaths:
211 211 if os.path.isfile(os.path.join(p, dirpath)):
212 212 continue
213 213 cur_path = os.path.join(p, dirpath)
214 214
215 215 # skip removed repos
216 216 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
217 217 continue
218 218
219 219 #skip .<somethin> dirs
220 220 if dirpath.startswith('.'):
221 221 continue
222 222
223 223 try:
224 224 scm_info = get_scm(cur_path)
225 225 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
226 226 except VCSError:
227 227 if not recursive:
228 228 continue
229 229 #check if this dir containts other repos for recursive scan
230 230 rec_path = os.path.join(p, dirpath)
231 231 if os.path.isdir(rec_path):
232 232 for inner_scm in _get_repos(rec_path):
233 233 yield inner_scm
234 234
235 235 return _get_repos(path)
236 236
237 237
238 238 def _get_dirpaths(p):
239 239 try:
240 240 # OS-independable way of checking if we have at least read-only
241 241 # access or not.
242 242 dirpaths = os.listdir(p)
243 243 except OSError:
244 244 log.warning('ignoring repo path without read access: %s', p)
245 245 return []
246 246
247 247 # os.listpath has a tweak: If a unicode is passed into it, then it tries to
248 248 # decode paths and suddenly returns unicode objects itself. The items it
249 249 # cannot decode are returned as strings and cause issues.
250 250 #
251 251 # Those paths are ignored here until a solid solution for path handling has
252 252 # been built.
253 253 expected_type = type(p)
254 254
255 255 def _has_correct_type(item):
256 256 if type(item) is not expected_type:
257 257 log.error(
258 258 u"Ignoring path %s since it cannot be decoded into unicode.",
259 259 # Using "repr" to make sure that we see the byte value in case
260 260 # of support.
261 261 repr(item))
262 262 return False
263 263 return True
264 264
265 265 dirpaths = [item for item in dirpaths if _has_correct_type(item)]
266 266
267 267 return dirpaths
268 268
269 269
270 270 def _is_dir_writable(path):
271 271 """
272 272 Probe if `path` is writable.
273 273
274 274 Due to trouble on Cygwin / Windows, this is actually probing if it is
275 275 possible to create a file inside of `path`, stat does not produce reliable
276 276 results in this case.
277 277 """
278 278 try:
279 279 with tempfile.TemporaryFile(dir=path):
280 280 pass
281 281 except OSError:
282 282 return False
283 283 return True
284 284
285 285
286 286 def is_valid_repo(repo_name, base_path, expect_scm=None, explicit_scm=None):
287 287 """
288 288 Returns True if given path is a valid repository False otherwise.
289 289 If expect_scm param is given also, compare if given scm is the same
290 290 as expected from scm parameter. If explicit_scm is given don't try to
291 291 detect the scm, just use the given one to check if repo is valid
292 292
293 293 :param repo_name:
294 294 :param base_path:
295 295 :param expect_scm:
296 296 :param explicit_scm:
297 297
298 298 :return True: if given path is a valid repository
299 299 """
300 300 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
301 301 log.debug('Checking if `%s` is a valid path for repository', repo_name)
302 302
303 303 try:
304 304 if explicit_scm:
305 305 detected_scms = [get_scm_backend(explicit_scm)]
306 306 else:
307 307 detected_scms = get_scm(full_path)
308 308
309 309 if expect_scm:
310 310 return detected_scms[0] == expect_scm
311 311 log.debug('path: %s is an vcs object:%s', full_path, detected_scms)
312 312 return True
313 313 except VCSError:
314 314 log.debug('path: %s is not a valid repo !', full_path)
315 315 return False
316 316
317 317
318 318 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
319 319 """
320 320 Returns True if given path is a repository group, False otherwise
321 321
322 322 :param repo_name:
323 323 :param base_path:
324 324 """
325 325 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
326 326 log.debug('Checking if `%s` is a valid path for repository group',
327 327 repo_group_name)
328 328
329 329 # check if it's not a repo
330 330 if is_valid_repo(repo_group_name, base_path):
331 331 log.debug('Repo called %s exist, it is not a valid '
332 332 'repo group' % repo_group_name)
333 333 return False
334 334
335 335 try:
336 336 # we need to check bare git repos at higher level
337 337 # since we might match branches/hooks/info/objects or possible
338 338 # other things inside bare git repo
339 339 scm_ = get_scm(os.path.dirname(full_path))
340 340 log.debug('path: %s is a vcs object:%s, not valid '
341 341 'repo group' % (full_path, scm_))
342 342 return False
343 343 except VCSError:
344 344 pass
345 345
346 346 # check if it's a valid path
347 347 if skip_path_check or os.path.isdir(full_path):
348 348 log.debug('path: %s is a valid repo group !', full_path)
349 349 return True
350 350
351 351 log.debug('path: %s is not a valid repo group !', full_path)
352 352 return False
353 353
354 354
355 355 def ask_ok(prompt, retries=4, complaint='Yes or no please!'):
356 356 while True:
357 357 ok = raw_input(prompt)
358 358 if ok in ('y', 'ye', 'yes'):
359 359 return True
360 360 if ok in ('n', 'no', 'nop', 'nope'):
361 361 return False
362 362 retries = retries - 1
363 363 if retries < 0:
364 364 raise IOError
365 365 print complaint
366 366
367 367 # propagated from mercurial documentation
368 368 ui_sections = [
369 369 'alias', 'auth',
370 370 'decode/encode', 'defaults',
371 371 'diff', 'email',
372 372 'extensions', 'format',
373 373 'merge-patterns', 'merge-tools',
374 374 'hooks', 'http_proxy',
375 375 'smtp', 'patch',
376 376 'paths', 'profiling',
377 377 'server', 'trusted',
378 378 'ui', 'web', ]
379 379
380 380
381 381 def config_data_from_db(clear_session=True, repo=None):
382 382 """
383 383 Read the configuration data from the database and return configuration
384 384 tuples.
385 385 """
386 386 from rhodecode.model.settings import VcsSettingsModel
387 387
388 388 config = []
389 389
390 390 sa = meta.Session()
391 391 settings_model = VcsSettingsModel(repo=repo, sa=sa)
392 392
393 393 ui_settings = settings_model.get_ui_settings()
394 394
395 395 for setting in ui_settings:
396 396 if setting.active:
397 397 log.debug(
398 398 'settings ui from db: [%s] %s=%s',
399 399 setting.section, setting.key, setting.value)
400 400 config.append((
401 401 safe_str(setting.section), safe_str(setting.key),
402 402 safe_str(setting.value)))
403 403 if setting.key == 'push_ssl':
404 404 # force set push_ssl requirement to False, rhodecode
405 405 # handles that
406 406 config.append((
407 407 safe_str(setting.section), safe_str(setting.key), False))
408 408 if clear_session:
409 409 meta.Session.remove()
410 410
411 411 # TODO: mikhail: probably it makes no sense to re-read hooks information.
412 412 # It's already there and activated/deactivated
413 413 skip_entries = []
414 414 enabled_hook_classes = get_enabled_hook_classes(ui_settings)
415 415 if 'pull' not in enabled_hook_classes:
416 416 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PULL))
417 417 if 'push' not in enabled_hook_classes:
418 418 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PUSH))
419 419
420 420 config = [entry for entry in config if entry[:2] not in skip_entries]
421 421
422 422 return config
423 423
424 424
425 425 def make_db_config(clear_session=True, repo=None):
426 426 """
427 427 Create a :class:`Config` instance based on the values in the database.
428 428 """
429 429 config = Config()
430 430 config_data = config_data_from_db(clear_session=clear_session, repo=repo)
431 431 for section, option, value in config_data:
432 432 config.set(section, option, value)
433 433 return config
434 434
435 435
436 436 def get_enabled_hook_classes(ui_settings):
437 437 """
438 438 Return the enabled hook classes.
439 439
440 440 :param ui_settings: List of ui_settings as returned
441 441 by :meth:`VcsSettingsModel.get_ui_settings`
442 442
443 443 :return: a list with the enabled hook classes. The order is not guaranteed.
444 444 :rtype: list
445 445 """
446 446 enabled_hooks = []
447 447 active_hook_keys = [
448 448 key for section, key, value, active in ui_settings
449 449 if section == 'hooks' and active]
450 450
451 451 hook_names = {
452 452 RhodeCodeUi.HOOK_PUSH: 'push',
453 453 RhodeCodeUi.HOOK_PULL: 'pull',
454 454 RhodeCodeUi.HOOK_REPO_SIZE: 'repo_size'
455 455 }
456 456
457 457 for key in active_hook_keys:
458 458 hook = hook_names.get(key)
459 459 if hook:
460 460 enabled_hooks.append(hook)
461 461
462 462 return enabled_hooks
463 463
464 464
465 465 def set_rhodecode_config(config):
466 466 """
467 467 Updates pylons config with new settings from database
468 468
469 469 :param config:
470 470 """
471 471 from rhodecode.model.settings import SettingsModel
472 472 app_settings = SettingsModel().get_all_settings()
473 473
474 474 for k, v in app_settings.items():
475 475 config[k] = v
476 476
477 477
478 478 def map_groups(path):
479 479 """
480 480 Given a full path to a repository, create all nested groups that this
481 481 repo is inside. This function creates parent-child relationships between
482 482 groups and creates default perms for all new groups.
483 483
484 484 :param paths: full path to repository
485 485 """
486 486 from rhodecode.model.repo_group import RepoGroupModel
487 487 sa = meta.Session()
488 488 groups = path.split(Repository.NAME_SEP)
489 489 parent = None
490 490 group = None
491 491
492 492 # last element is repo in nested groups structure
493 493 groups = groups[:-1]
494 494 rgm = RepoGroupModel(sa)
495 owner = User.get_first_admin()
495 owner = User.get_first_super_admin()
496 496 for lvl, group_name in enumerate(groups):
497 497 group_name = '/'.join(groups[:lvl] + [group_name])
498 498 group = RepoGroup.get_by_group_name(group_name)
499 499 desc = '%s group' % group_name
500 500
501 501 # skip folders that are now removed repos
502 502 if REMOVED_REPO_PAT.match(group_name):
503 503 break
504 504
505 505 if group is None:
506 506 log.debug('creating group level: %s group_name: %s',
507 507 lvl, group_name)
508 508 group = RepoGroup(group_name, parent)
509 509 group.group_description = desc
510 510 group.user = owner
511 511 sa.add(group)
512 512 perm_obj = rgm._create_default_perms(group)
513 513 sa.add(perm_obj)
514 514 sa.flush()
515 515
516 516 parent = group
517 517 return group
518 518
519 519
520 520 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
521 521 """
522 522 maps all repos given in initial_repo_list, non existing repositories
523 523 are created, if remove_obsolete is True it also checks for db entries
524 524 that are not in initial_repo_list and removes them.
525 525
526 526 :param initial_repo_list: list of repositories found by scanning methods
527 527 :param remove_obsolete: check for obsolete entries in database
528 528 """
529 529 from rhodecode.model.repo import RepoModel
530 530 from rhodecode.model.scm import ScmModel
531 531 from rhodecode.model.repo_group import RepoGroupModel
532 532 from rhodecode.model.settings import SettingsModel
533 533
534 534 sa = meta.Session()
535 535 repo_model = RepoModel()
536 user = User.get_first_admin()
536 user = User.get_first_super_admin()
537 537 added = []
538 538
539 539 # creation defaults
540 540 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
541 541 enable_statistics = defs.get('repo_enable_statistics')
542 542 enable_locking = defs.get('repo_enable_locking')
543 543 enable_downloads = defs.get('repo_enable_downloads')
544 544 private = defs.get('repo_private')
545 545
546 546 for name, repo in initial_repo_list.items():
547 547 group = map_groups(name)
548 548 unicode_name = safe_unicode(name)
549 549 db_repo = repo_model.get_by_repo_name(unicode_name)
550 550 # found repo that is on filesystem not in RhodeCode database
551 551 if not db_repo:
552 552 log.info('repository %s not found, creating now', name)
553 553 added.append(name)
554 554 desc = (repo.description
555 555 if repo.description != 'unknown'
556 556 else '%s repository' % name)
557 557
558 558 db_repo = repo_model._create_repo(
559 559 repo_name=name,
560 560 repo_type=repo.alias,
561 561 description=desc,
562 562 repo_group=getattr(group, 'group_id', None),
563 563 owner=user,
564 564 enable_locking=enable_locking,
565 565 enable_downloads=enable_downloads,
566 566 enable_statistics=enable_statistics,
567 567 private=private,
568 568 state=Repository.STATE_CREATED
569 569 )
570 570 sa.commit()
571 571 # we added that repo just now, and make sure we updated server info
572 572 if db_repo.repo_type == 'git':
573 573 git_repo = db_repo.scm_instance()
574 574 # update repository server-info
575 575 log.debug('Running update server info')
576 576 git_repo._update_server_info()
577 577
578 578 db_repo.update_commit_cache()
579 579
580 580 config = db_repo._config
581 581 config.set('extensions', 'largefiles', '')
582 582 ScmModel().install_hooks(
583 583 db_repo.scm_instance(config=config),
584 584 repo_type=db_repo.repo_type)
585 585
586 586 removed = []
587 587 if remove_obsolete:
588 588 # remove from database those repositories that are not in the filesystem
589 589 for repo in sa.query(Repository).all():
590 590 if repo.repo_name not in initial_repo_list.keys():
591 591 log.debug("Removing non-existing repository found in db `%s`",
592 592 repo.repo_name)
593 593 try:
594 594 RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
595 595 sa.commit()
596 596 removed.append(repo.repo_name)
597 597 except Exception:
598 598 # don't hold further removals on error
599 599 log.error(traceback.format_exc())
600 600 sa.rollback()
601 601
602 602 def splitter(full_repo_name):
603 603 _parts = full_repo_name.rsplit(RepoGroup.url_sep(), 1)
604 604 gr_name = None
605 605 if len(_parts) == 2:
606 606 gr_name = _parts[0]
607 607 return gr_name
608 608
609 609 initial_repo_group_list = [splitter(x) for x in
610 610 initial_repo_list.keys() if splitter(x)]
611 611
612 612 # remove from database those repository groups that are not in the
613 613 # filesystem due to parent child relationships we need to delete them
614 614 # in a specific order of most nested first
615 615 all_groups = [x.group_name for x in sa.query(RepoGroup).all()]
616 616 nested_sort = lambda gr: len(gr.split('/'))
617 617 for group_name in sorted(all_groups, key=nested_sort, reverse=True):
618 618 if group_name not in initial_repo_group_list:
619 619 repo_group = RepoGroup.get_by_group_name(group_name)
620 620 if (repo_group.children.all() or
621 621 not RepoGroupModel().check_exist_filesystem(
622 622 group_name=group_name, exc_on_failure=False)):
623 623 continue
624 624
625 625 log.info(
626 626 'Removing non-existing repository group found in db `%s`',
627 627 group_name)
628 628 try:
629 629 RepoGroupModel(sa).delete(group_name, fs_remove=False)
630 630 sa.commit()
631 631 removed.append(group_name)
632 632 except Exception:
633 633 # don't hold further removals on error
634 634 log.exception(
635 635 'Unable to remove repository group `%s`',
636 636 group_name)
637 637 sa.rollback()
638 638 raise
639 639
640 640 return added, removed
641 641
642 642
643 643 def get_default_cache_settings(settings):
644 644 cache_settings = {}
645 645 for key in settings.keys():
646 646 for prefix in ['beaker.cache.', 'cache.']:
647 647 if key.startswith(prefix):
648 648 name = key.split(prefix)[1].strip()
649 649 cache_settings[name] = settings[key].strip()
650 650 return cache_settings
651 651
652 652
653 653 # set cache regions for beaker so celery can utilise it
654 654 def add_cache(settings):
655 655 from rhodecode.lib import caches
656 656 cache_settings = {'regions': None}
657 657 # main cache settings used as default ...
658 658 cache_settings.update(get_default_cache_settings(settings))
659 659
660 660 if cache_settings['regions']:
661 661 for region in cache_settings['regions'].split(','):
662 662 region = region.strip()
663 663 region_settings = {}
664 664 for key, value in cache_settings.items():
665 665 if key.startswith(region):
666 666 region_settings[key.split('.')[1]] = value
667 667
668 668 caches.configure_cache_region(
669 669 region, region_settings, cache_settings)
670 670
671 671
672 672 def load_rcextensions(root_path):
673 673 import rhodecode
674 674 from rhodecode.config import conf
675 675
676 676 path = os.path.join(root_path, 'rcextensions', '__init__.py')
677 677 if os.path.isfile(path):
678 678 rcext = create_module('rc', path)
679 679 EXT = rhodecode.EXTENSIONS = rcext
680 680 log.debug('Found rcextensions now loading %s...', rcext)
681 681
682 682 # Additional mappings that are not present in the pygments lexers
683 683 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
684 684
685 685 # auto check if the module is not missing any data, set to default if is
686 686 # this will help autoupdate new feature of rcext module
687 687 #from rhodecode.config import rcextensions
688 688 #for k in dir(rcextensions):
689 689 # if not k.startswith('_') and not hasattr(EXT, k):
690 690 # setattr(EXT, k, getattr(rcextensions, k))
691 691
692 692
693 693 def get_custom_lexer(extension):
694 694 """
695 695 returns a custom lexer if it is defined in rcextensions module, or None
696 696 if there's no custom lexer defined
697 697 """
698 698 import rhodecode
699 699 from pygments import lexers
700 700 # check if we didn't define this extension as other lexer
701 701 extensions = rhodecode.EXTENSIONS and getattr(rhodecode.EXTENSIONS, 'EXTRA_LEXERS', None)
702 702 if extensions and extension in rhodecode.EXTENSIONS.EXTRA_LEXERS:
703 703 _lexer_name = rhodecode.EXTENSIONS.EXTRA_LEXERS[extension]
704 704 return lexers.get_lexer_by_name(_lexer_name)
705 705
706 706
707 707 #==============================================================================
708 708 # TEST FUNCTIONS AND CREATORS
709 709 #==============================================================================
710 710 def create_test_index(repo_location, config):
711 711 """
712 712 Makes default test index.
713 713 """
714 714 import rc_testdata
715 715
716 716 rc_testdata.extract_search_index(
717 717 'vcs_search_index', os.path.dirname(config['search.location']))
718 718
719 719
720 720 def create_test_directory(test_path):
721 721 """
722 722 Create test directory if it doesn't exist.
723 723 """
724 724 if not os.path.isdir(test_path):
725 725 log.debug('Creating testdir %s', test_path)
726 726 os.makedirs(test_path)
727 727
728 728
729 729 def create_test_database(test_path, config):
730 730 """
731 731 Makes a fresh database.
732 732 """
733 733 from rhodecode.lib.db_manage import DbManage
734 734
735 735 # PART ONE create db
736 736 dbconf = config['sqlalchemy.db1.url']
737 737 log.debug('making test db %s', dbconf)
738 738
739 739 dbmanage = DbManage(log_sql=False, dbconf=dbconf, root=config['here'],
740 740 tests=True, cli_args={'force_ask': True})
741 741 dbmanage.create_tables(override=True)
742 742 dbmanage.set_db_version()
743 743 # for tests dynamically set new root paths based on generated content
744 744 dbmanage.create_settings(dbmanage.config_prompt(test_path))
745 745 dbmanage.create_default_user()
746 746 dbmanage.create_test_admin_and_users()
747 747 dbmanage.create_permissions()
748 748 dbmanage.populate_default_permissions()
749 749 Session().commit()
750 750
751 751
752 752 def create_test_repositories(test_path, config):
753 753 """
754 754 Creates test repositories in the temporary directory. Repositories are
755 755 extracted from archives within the rc_testdata package.
756 756 """
757 757 import rc_testdata
758 758 from rhodecode.tests import HG_REPO, GIT_REPO, SVN_REPO
759 759
760 760 log.debug('making test vcs repositories')
761 761
762 762 idx_path = config['search.location']
763 763 data_path = config['cache_dir']
764 764
765 765 # clean index and data
766 766 if idx_path and os.path.exists(idx_path):
767 767 log.debug('remove %s', idx_path)
768 768 shutil.rmtree(idx_path)
769 769
770 770 if data_path and os.path.exists(data_path):
771 771 log.debug('remove %s', data_path)
772 772 shutil.rmtree(data_path)
773 773
774 774 rc_testdata.extract_hg_dump('vcs_test_hg', jn(test_path, HG_REPO))
775 775 rc_testdata.extract_git_dump('vcs_test_git', jn(test_path, GIT_REPO))
776 776
777 777 # Note: Subversion is in the process of being integrated with the system,
778 778 # until we have a properly packed version of the test svn repository, this
779 779 # tries to copy over the repo from a package "rc_testdata"
780 780 svn_repo_path = rc_testdata.get_svn_repo_archive()
781 781 with tarfile.open(svn_repo_path) as tar:
782 782 tar.extractall(jn(test_path, SVN_REPO))
783 783
784 784
785 785 #==============================================================================
786 786 # PASTER COMMANDS
787 787 #==============================================================================
788 788 class BasePasterCommand(Command):
789 789 """
790 790 Abstract Base Class for paster commands.
791 791
792 792 The celery commands are somewhat aggressive about loading
793 793 celery.conf, and since our module sets the `CELERY_LOADER`
794 794 environment variable to our loader, we have to bootstrap a bit and
795 795 make sure we've had a chance to load the pylons config off of the
796 796 command line, otherwise everything fails.
797 797 """
798 798 min_args = 1
799 799 min_args_error = "Please provide a paster config file as an argument."
800 800 takes_config_file = 1
801 801 requires_config_file = True
802 802
803 803 def notify_msg(self, msg, log=False):
804 804 """Make a notification to user, additionally if logger is passed
805 805 it logs this action using given logger
806 806
807 807 :param msg: message that will be printed to user
808 808 :param log: logging instance, to use to additionally log this message
809 809
810 810 """
811 811 if log and isinstance(log, logging):
812 812 log(msg)
813 813
814 814 def run(self, args):
815 815 """
816 816 Overrides Command.run
817 817
818 818 Checks for a config file argument and loads it.
819 819 """
820 820 if len(args) < self.min_args:
821 821 raise BadCommand(
822 822 self.min_args_error % {'min_args': self.min_args,
823 823 'actual_args': len(args)})
824 824
825 825 # Decrement because we're going to lob off the first argument.
826 826 # @@ This is hacky
827 827 self.min_args -= 1
828 828 self.bootstrap_config(args[0])
829 829 self.update_parser()
830 830 return super(BasePasterCommand, self).run(args[1:])
831 831
832 832 def update_parser(self):
833 833 """
834 834 Abstract method. Allows for the class' parser to be updated
835 835 before the superclass' `run` method is called. Necessary to
836 836 allow options/arguments to be passed through to the underlying
837 837 celery command.
838 838 """
839 839 raise NotImplementedError("Abstract Method.")
840 840
841 841 def bootstrap_config(self, conf):
842 842 """
843 843 Loads the pylons configuration.
844 844 """
845 845 from pylons import config as pylonsconfig
846 846
847 847 self.path_to_ini_file = os.path.realpath(conf)
848 848 conf = paste.deploy.appconfig('config:' + self.path_to_ini_file)
849 849 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
850 850
851 851 def _init_session(self):
852 852 """
853 853 Inits SqlAlchemy Session
854 854 """
855 855 logging.config.fileConfig(self.path_to_ini_file)
856 856 from pylons import config
857 857 from rhodecode.config.utils import initialize_database
858 858
859 859 # get to remove repos !!
860 860 add_cache(config)
861 861 initialize_database(config)
862 862
863 863
864 864 @decorator.decorator
865 865 def jsonify(func, *args, **kwargs):
866 866 """Action decorator that formats output for JSON
867 867
868 868 Given a function that will return content, this decorator will turn
869 869 the result into JSON, with a content-type of 'application/json' and
870 870 output it.
871 871
872 872 """
873 873 from pylons.decorators.util import get_pylons
874 874 from rhodecode.lib.ext_json import json
875 875 pylons = get_pylons(args)
876 876 pylons.response.headers['Content-Type'] = 'application/json; charset=utf-8'
877 877 data = func(*args, **kwargs)
878 878 if isinstance(data, (list, tuple)):
879 879 msg = "JSON responses with Array envelopes are susceptible to " \
880 880 "cross-site data leak attacks, see " \
881 881 "http://wiki.pylonshq.com/display/pylonsfaq/Warnings"
882 882 warnings.warn(msg, Warning, 2)
883 883 log.warning(msg)
884 884 log.debug("Returning JSON wrapped action output")
885 885 return json.dumps(data, encoding='utf-8')
886 886
887 887
888 888 class PartialRenderer(object):
889 889 """
890 890 Partial renderer used to render chunks of html used in datagrids
891 891 use like::
892 892
893 893 _render = PartialRenderer('data_table/_dt_elements.html')
894 894 _render('quick_menu', args, kwargs)
895 895 PartialRenderer.h,
896 896 c,
897 897 _,
898 898 ungettext
899 899 are the template stuff initialized inside and can be re-used later
900 900
901 901 :param tmpl_name: template path relate to /templates/ dir
902 902 """
903 903
904 904 def __init__(self, tmpl_name):
905 905 import rhodecode
906 906 from pylons import request, tmpl_context as c
907 907 from pylons.i18n.translation import _, ungettext
908 908 from rhodecode.lib import helpers as h
909 909
910 910 self.tmpl_name = tmpl_name
911 911 self.rhodecode = rhodecode
912 912 self.c = c
913 913 self._ = _
914 914 self.ungettext = ungettext
915 915 self.h = h
916 916 self.request = request
917 917
918 918 def _mako_lookup(self):
919 919 _tmpl_lookup = self.rhodecode.CONFIG['pylons.app_globals'].mako_lookup
920 920 return _tmpl_lookup.get_template(self.tmpl_name)
921 921
922 922 def _update_kwargs_for_render(self, kwargs):
923 923 """
924 924 Inject params required for Mako rendering
925 925 """
926 926 _kwargs = {
927 927 '_': self._,
928 928 'h': self.h,
929 929 'c': self.c,
930 930 'request': self.request,
931 931 'ungettext': self.ungettext,
932 932 }
933 933 _kwargs.update(kwargs)
934 934 return _kwargs
935 935
936 936 def _render_with_exc(self, render_func, args, kwargs):
937 937 try:
938 938 return render_func.render(*args, **kwargs)
939 939 except:
940 940 log.error(exceptions.text_error_template().render())
941 941 raise
942 942
943 943 def _get_template(self, template_obj, def_name):
944 944 if def_name:
945 945 tmpl = template_obj.get_def(def_name)
946 946 else:
947 947 tmpl = template_obj
948 948 return tmpl
949 949
950 950 def render(self, def_name, *args, **kwargs):
951 951 lookup_obj = self._mako_lookup()
952 952 tmpl = self._get_template(lookup_obj, def_name=def_name)
953 953 kwargs = self._update_kwargs_for_render(kwargs)
954 954 return self._render_with_exc(tmpl, args, kwargs)
955 955
956 956 def __call__(self, tmpl, *args, **kwargs):
957 957 return self.render(tmpl, *args, **kwargs)
958 958
959 959
960 960 def password_changed(auth_user, session):
961 961 if auth_user.username == User.DEFAULT_USER:
962 962 return False
963 963 password_hash = md5(auth_user.password) if auth_user.password else None
964 964 rhodecode_user = session.get('rhodecode_user', {})
965 965 session_password_hash = rhodecode_user.get('password', '')
966 966 return password_hash != session_password_hash
967 967
968 968
969 969 def read_opensource_licenses():
970 970 global _license_cache
971 971
972 972 if not _license_cache:
973 973 licenses = pkg_resources.resource_string(
974 974 'rhodecode', 'config/licenses.json')
975 975 _license_cache = json.loads(licenses)
976 976
977 977 return _license_cache
@@ -1,3462 +1,3462 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 os
26 26 import sys
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.exc import IntegrityError
40 40 from sqlalchemy.ext.declarative import declared_attr
41 41 from sqlalchemy.ext.hybrid import hybrid_property
42 42 from sqlalchemy.orm import (
43 43 relationship, joinedload, class_mapper, validates, aliased)
44 44 from sqlalchemy.sql.expression import true
45 45 from beaker.cache import cache_region, region_invalidate
46 46 from webob.exc import HTTPNotFound
47 47 from zope.cachedescriptors.property import Lazy as LazyProperty
48 48
49 49 from pylons import url
50 50 from pylons.i18n.translation import lazy_ugettext as _
51 51
52 52 from rhodecode.lib.vcs import get_backend
53 53 from rhodecode.lib.vcs.utils.helpers import get_scm
54 54 from rhodecode.lib.vcs.exceptions import VCSError
55 55 from rhodecode.lib.vcs.backends.base import (
56 56 EmptyCommit, Reference, MergeFailureReason)
57 57 from rhodecode.lib.utils2 import (
58 58 str2bool, safe_str, get_commit_safe, safe_unicode, remove_prefix, md5_safe,
59 59 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict)
60 60 from rhodecode.lib.ext_json import json
61 61 from rhodecode.lib.caching_query import FromCache
62 62 from rhodecode.lib.encrypt import AESCipher
63 63
64 64 from rhodecode.model.meta import Base, Session
65 65
66 66 URL_SEP = '/'
67 67 log = logging.getLogger(__name__)
68 68
69 69 # =============================================================================
70 70 # BASE CLASSES
71 71 # =============================================================================
72 72
73 73 # this is propagated from .ini file beaker.session.secret
74 74 # and initialized at environment.py
75 75 ENCRYPTION_KEY = None
76 76
77 77 # used to sort permissions by types, '#' used here is not allowed to be in
78 78 # usernames, and it's very early in sorted string.printable table.
79 79 PERMISSION_TYPE_SORT = {
80 80 'admin': '####',
81 81 'write': '###',
82 82 'read': '##',
83 83 'none': '#',
84 84 }
85 85
86 86
87 87 def display_sort(obj):
88 88 """
89 89 Sort function used to sort permissions in .permissions() function of
90 90 Repository, RepoGroup, UserGroup. Also it put the default user in front
91 91 of all other resources
92 92 """
93 93
94 94 if obj.username == User.DEFAULT_USER:
95 95 return '#####'
96 96 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
97 97 return prefix + obj.username
98 98
99 99
100 100 def _hash_key(k):
101 101 return md5_safe(k)
102 102
103 103
104 104 class EncryptedTextValue(TypeDecorator):
105 105 """
106 106 Special column for encrypted long text data, use like::
107 107
108 108 value = Column("encrypted_value", EncryptedValue(), nullable=False)
109 109
110 110 This column is intelligent so if value is in unencrypted form it return
111 111 unencrypted form, but on save it always encrypts
112 112 """
113 113 impl = Text
114 114
115 115 def process_bind_param(self, value, dialect):
116 116 if not value:
117 117 return value
118 118 if value.startswith('enc$aes$'):
119 119 # protect against double encrypting if someone manually starts
120 120 # doing
121 121 raise ValueError('value needs to be in unencrypted format, ie. '
122 122 'not starting with enc$aes$')
123 123 return 'enc$aes$%s' % AESCipher(ENCRYPTION_KEY).encrypt(value)
124 124
125 125 def process_result_value(self, value, dialect):
126 126 if not value:
127 127 return value
128 128
129 129 parts = value.split('$', 3)
130 130 if not len(parts) == 3:
131 131 # probably not encrypted values
132 132 return value
133 133 else:
134 134 if parts[0] != 'enc':
135 135 # parts ok but without our header ?
136 136 return value
137 137
138 138 # at that stage we know it's our encryption
139 139 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
140 140 return decrypted_data
141 141
142 142
143 143 class BaseModel(object):
144 144 """
145 145 Base Model for all classes
146 146 """
147 147
148 148 @classmethod
149 149 def _get_keys(cls):
150 150 """return column names for this model """
151 151 return class_mapper(cls).c.keys()
152 152
153 153 def get_dict(self):
154 154 """
155 155 return dict with keys and values corresponding
156 156 to this model data """
157 157
158 158 d = {}
159 159 for k in self._get_keys():
160 160 d[k] = getattr(self, k)
161 161
162 162 # also use __json__() if present to get additional fields
163 163 _json_attr = getattr(self, '__json__', None)
164 164 if _json_attr:
165 165 # update with attributes from __json__
166 166 if callable(_json_attr):
167 167 _json_attr = _json_attr()
168 168 for k, val in _json_attr.iteritems():
169 169 d[k] = val
170 170 return d
171 171
172 172 def get_appstruct(self):
173 173 """return list with keys and values tuples corresponding
174 174 to this model data """
175 175
176 176 l = []
177 177 for k in self._get_keys():
178 178 l.append((k, getattr(self, k),))
179 179 return l
180 180
181 181 def populate_obj(self, populate_dict):
182 182 """populate model with data from given populate_dict"""
183 183
184 184 for k in self._get_keys():
185 185 if k in populate_dict:
186 186 setattr(self, k, populate_dict[k])
187 187
188 188 @classmethod
189 189 def query(cls):
190 190 return Session().query(cls)
191 191
192 192 @classmethod
193 193 def get(cls, id_):
194 194 if id_:
195 195 return cls.query().get(id_)
196 196
197 197 @classmethod
198 198 def get_or_404(cls, id_):
199 199 try:
200 200 id_ = int(id_)
201 201 except (TypeError, ValueError):
202 202 raise HTTPNotFound
203 203
204 204 res = cls.query().get(id_)
205 205 if not res:
206 206 raise HTTPNotFound
207 207 return res
208 208
209 209 @classmethod
210 210 def getAll(cls):
211 211 # deprecated and left for backward compatibility
212 212 return cls.get_all()
213 213
214 214 @classmethod
215 215 def get_all(cls):
216 216 return cls.query().all()
217 217
218 218 @classmethod
219 219 def delete(cls, id_):
220 220 obj = cls.query().get(id_)
221 221 Session().delete(obj)
222 222
223 223 @classmethod
224 224 def identity_cache(cls, session, attr_name, value):
225 225 exist_in_session = []
226 226 for (item_cls, pkey), instance in session.identity_map.items():
227 227 if cls == item_cls and getattr(instance, attr_name) == value:
228 228 exist_in_session.append(instance)
229 229 if exist_in_session:
230 230 if len(exist_in_session) == 1:
231 231 return exist_in_session[0]
232 232 log.exception(
233 233 'multiple objects with attr %s and '
234 234 'value %s found with same name: %r',
235 235 attr_name, value, exist_in_session)
236 236
237 237 def __repr__(self):
238 238 if hasattr(self, '__unicode__'):
239 239 # python repr needs to return str
240 240 try:
241 241 return safe_str(self.__unicode__())
242 242 except UnicodeDecodeError:
243 243 pass
244 244 return '<DB:%s>' % (self.__class__.__name__)
245 245
246 246
247 247 class RhodeCodeSetting(Base, BaseModel):
248 248 __tablename__ = 'rhodecode_settings'
249 249 __table_args__ = (
250 250 UniqueConstraint('app_settings_name'),
251 251 {'extend_existing': True, 'mysql_engine': 'InnoDB',
252 252 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
253 253 )
254 254
255 255 SETTINGS_TYPES = {
256 256 'str': safe_str,
257 257 'int': safe_int,
258 258 'unicode': safe_unicode,
259 259 'bool': str2bool,
260 260 'list': functools.partial(aslist, sep=',')
261 261 }
262 262 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
263 263 GLOBAL_CONF_KEY = 'app_settings'
264 264
265 265 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
266 266 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
267 267 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
268 268 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
269 269
270 270 def __init__(self, key='', val='', type='unicode'):
271 271 self.app_settings_name = key
272 272 self.app_settings_type = type
273 273 self.app_settings_value = val
274 274
275 275 @validates('_app_settings_value')
276 276 def validate_settings_value(self, key, val):
277 277 assert type(val) == unicode
278 278 return val
279 279
280 280 @hybrid_property
281 281 def app_settings_value(self):
282 282 v = self._app_settings_value
283 283 _type = self.app_settings_type
284 284 if _type:
285 285 _type = self.app_settings_type.split('.')[0]
286 286 # decode the encrypted value
287 287 if 'encrypted' in self.app_settings_type:
288 288 cipher = EncryptedTextValue()
289 289 v = safe_unicode(cipher.process_result_value(v, None))
290 290
291 291 converter = self.SETTINGS_TYPES.get(_type) or \
292 292 self.SETTINGS_TYPES['unicode']
293 293 return converter(v)
294 294
295 295 @app_settings_value.setter
296 296 def app_settings_value(self, val):
297 297 """
298 298 Setter that will always make sure we use unicode in app_settings_value
299 299
300 300 :param val:
301 301 """
302 302 val = safe_unicode(val)
303 303 # encode the encrypted value
304 304 if 'encrypted' in self.app_settings_type:
305 305 cipher = EncryptedTextValue()
306 306 val = safe_unicode(cipher.process_bind_param(val, None))
307 307 self._app_settings_value = val
308 308
309 309 @hybrid_property
310 310 def app_settings_type(self):
311 311 return self._app_settings_type
312 312
313 313 @app_settings_type.setter
314 314 def app_settings_type(self, val):
315 315 if val.split('.')[0] not in self.SETTINGS_TYPES:
316 316 raise Exception('type must be one of %s got %s'
317 317 % (self.SETTINGS_TYPES.keys(), val))
318 318 self._app_settings_type = val
319 319
320 320 def __unicode__(self):
321 321 return u"<%s('%s:%s[%s]')>" % (
322 322 self.__class__.__name__,
323 323 self.app_settings_name, self.app_settings_value,
324 324 self.app_settings_type
325 325 )
326 326
327 327
328 328 class RhodeCodeUi(Base, BaseModel):
329 329 __tablename__ = 'rhodecode_ui'
330 330 __table_args__ = (
331 331 UniqueConstraint('ui_key'),
332 332 {'extend_existing': True, 'mysql_engine': 'InnoDB',
333 333 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
334 334 )
335 335
336 336 HOOK_REPO_SIZE = 'changegroup.repo_size'
337 337 # HG
338 338 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
339 339 HOOK_PULL = 'outgoing.pull_logger'
340 340 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
341 341 HOOK_PUSH = 'changegroup.push_logger'
342 342
343 343 # TODO: johbo: Unify way how hooks are configured for git and hg,
344 344 # git part is currently hardcoded.
345 345
346 346 # SVN PATTERNS
347 347 SVN_BRANCH_ID = 'vcs_svn_branch'
348 348 SVN_TAG_ID = 'vcs_svn_tag'
349 349
350 350 ui_id = Column(
351 351 "ui_id", Integer(), nullable=False, unique=True, default=None,
352 352 primary_key=True)
353 353 ui_section = Column(
354 354 "ui_section", String(255), nullable=True, unique=None, default=None)
355 355 ui_key = Column(
356 356 "ui_key", String(255), nullable=True, unique=None, default=None)
357 357 ui_value = Column(
358 358 "ui_value", String(255), nullable=True, unique=None, default=None)
359 359 ui_active = Column(
360 360 "ui_active", Boolean(), nullable=True, unique=None, default=True)
361 361
362 362 def __repr__(self):
363 363 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
364 364 self.ui_key, self.ui_value)
365 365
366 366
367 367 class RepoRhodeCodeSetting(Base, BaseModel):
368 368 __tablename__ = 'repo_rhodecode_settings'
369 369 __table_args__ = (
370 370 UniqueConstraint(
371 371 'app_settings_name', 'repository_id',
372 372 name='uq_repo_rhodecode_setting_name_repo_id'),
373 373 {'extend_existing': True, 'mysql_engine': 'InnoDB',
374 374 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
375 375 )
376 376
377 377 repository_id = Column(
378 378 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
379 379 nullable=False)
380 380 app_settings_id = Column(
381 381 "app_settings_id", Integer(), nullable=False, unique=True,
382 382 default=None, primary_key=True)
383 383 app_settings_name = Column(
384 384 "app_settings_name", String(255), nullable=True, unique=None,
385 385 default=None)
386 386 _app_settings_value = Column(
387 387 "app_settings_value", String(4096), nullable=True, unique=None,
388 388 default=None)
389 389 _app_settings_type = Column(
390 390 "app_settings_type", String(255), nullable=True, unique=None,
391 391 default=None)
392 392
393 393 repository = relationship('Repository')
394 394
395 395 def __init__(self, repository_id, key='', val='', type='unicode'):
396 396 self.repository_id = repository_id
397 397 self.app_settings_name = key
398 398 self.app_settings_type = type
399 399 self.app_settings_value = val
400 400
401 401 @validates('_app_settings_value')
402 402 def validate_settings_value(self, key, val):
403 403 assert type(val) == unicode
404 404 return val
405 405
406 406 @hybrid_property
407 407 def app_settings_value(self):
408 408 v = self._app_settings_value
409 409 type_ = self.app_settings_type
410 410 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
411 411 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
412 412 return converter(v)
413 413
414 414 @app_settings_value.setter
415 415 def app_settings_value(self, val):
416 416 """
417 417 Setter that will always make sure we use unicode in app_settings_value
418 418
419 419 :param val:
420 420 """
421 421 self._app_settings_value = safe_unicode(val)
422 422
423 423 @hybrid_property
424 424 def app_settings_type(self):
425 425 return self._app_settings_type
426 426
427 427 @app_settings_type.setter
428 428 def app_settings_type(self, val):
429 429 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
430 430 if val not in SETTINGS_TYPES:
431 431 raise Exception('type must be one of %s got %s'
432 432 % (SETTINGS_TYPES.keys(), val))
433 433 self._app_settings_type = val
434 434
435 435 def __unicode__(self):
436 436 return u"<%s('%s:%s:%s[%s]')>" % (
437 437 self.__class__.__name__, self.repository.repo_name,
438 438 self.app_settings_name, self.app_settings_value,
439 439 self.app_settings_type
440 440 )
441 441
442 442
443 443 class RepoRhodeCodeUi(Base, BaseModel):
444 444 __tablename__ = 'repo_rhodecode_ui'
445 445 __table_args__ = (
446 446 UniqueConstraint(
447 447 'repository_id', 'ui_section', 'ui_key',
448 448 name='uq_repo_rhodecode_ui_repository_id_section_key'),
449 449 {'extend_existing': True, 'mysql_engine': 'InnoDB',
450 450 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
451 451 )
452 452
453 453 repository_id = Column(
454 454 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
455 455 nullable=False)
456 456 ui_id = Column(
457 457 "ui_id", Integer(), nullable=False, unique=True, default=None,
458 458 primary_key=True)
459 459 ui_section = Column(
460 460 "ui_section", String(255), nullable=True, unique=None, default=None)
461 461 ui_key = Column(
462 462 "ui_key", String(255), nullable=True, unique=None, default=None)
463 463 ui_value = Column(
464 464 "ui_value", String(255), nullable=True, unique=None, default=None)
465 465 ui_active = Column(
466 466 "ui_active", Boolean(), nullable=True, unique=None, default=True)
467 467
468 468 repository = relationship('Repository')
469 469
470 470 def __repr__(self):
471 471 return '<%s[%s:%s]%s=>%s]>' % (
472 472 self.__class__.__name__, self.repository.repo_name,
473 473 self.ui_section, self.ui_key, self.ui_value)
474 474
475 475
476 476 class User(Base, BaseModel):
477 477 __tablename__ = 'users'
478 478 __table_args__ = (
479 479 UniqueConstraint('username'), UniqueConstraint('email'),
480 480 Index('u_username_idx', 'username'),
481 481 Index('u_email_idx', 'email'),
482 482 {'extend_existing': True, 'mysql_engine': 'InnoDB',
483 483 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
484 484 )
485 485 DEFAULT_USER = 'default'
486 486 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
487 487 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
488 488
489 489 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
490 490 username = Column("username", String(255), nullable=True, unique=None, default=None)
491 491 password = Column("password", String(255), nullable=True, unique=None, default=None)
492 492 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
493 493 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
494 494 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
495 495 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
496 496 _email = Column("email", String(255), nullable=True, unique=None, default=None)
497 497 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
498 498 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
499 499 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
500 500 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
501 501 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
502 502 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
503 503 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
504 504
505 505 user_log = relationship('UserLog')
506 506 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
507 507
508 508 repositories = relationship('Repository')
509 509 repository_groups = relationship('RepoGroup')
510 510 user_groups = relationship('UserGroup')
511 511
512 512 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
513 513 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
514 514
515 515 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
516 516 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
517 517 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
518 518
519 519 group_member = relationship('UserGroupMember', cascade='all')
520 520
521 521 notifications = relationship('UserNotification', cascade='all')
522 522 # notifications assigned to this user
523 523 user_created_notifications = relationship('Notification', cascade='all')
524 524 # comments created by this user
525 525 user_comments = relationship('ChangesetComment', cascade='all')
526 526 # user profile extra info
527 527 user_emails = relationship('UserEmailMap', cascade='all')
528 528 user_ip_map = relationship('UserIpMap', cascade='all')
529 529 user_auth_tokens = relationship('UserApiKeys', cascade='all')
530 530 # gists
531 531 user_gists = relationship('Gist', cascade='all')
532 532 # user pull requests
533 533 user_pull_requests = relationship('PullRequest', cascade='all')
534 534 # external identities
535 535 extenal_identities = relationship(
536 536 'ExternalIdentity',
537 537 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
538 538 cascade='all')
539 539
540 540 def __unicode__(self):
541 541 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
542 542 self.user_id, self.username)
543 543
544 544 @hybrid_property
545 545 def email(self):
546 546 return self._email
547 547
548 548 @email.setter
549 549 def email(self, val):
550 550 self._email = val.lower() if val else None
551 551
552 552 @property
553 553 def firstname(self):
554 554 # alias for future
555 555 return self.name
556 556
557 557 @property
558 558 def emails(self):
559 559 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
560 560 return [self.email] + [x.email for x in other]
561 561
562 562 @property
563 563 def auth_tokens(self):
564 564 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
565 565
566 566 @property
567 567 def extra_auth_tokens(self):
568 568 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
569 569
570 570 @property
571 571 def feed_token(self):
572 572 feed_tokens = UserApiKeys.query()\
573 573 .filter(UserApiKeys.user == self)\
574 574 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
575 575 .all()
576 576 if feed_tokens:
577 577 return feed_tokens[0].api_key
578 578 else:
579 579 # use the main token so we don't end up with nothing...
580 580 return self.api_key
581 581
582 582 @classmethod
583 583 def extra_valid_auth_tokens(cls, user, role=None):
584 584 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
585 585 .filter(or_(UserApiKeys.expires == -1,
586 586 UserApiKeys.expires >= time.time()))
587 587 if role:
588 588 tokens = tokens.filter(or_(UserApiKeys.role == role,
589 589 UserApiKeys.role == UserApiKeys.ROLE_ALL))
590 590 return tokens.all()
591 591
592 592 @property
593 593 def ip_addresses(self):
594 594 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
595 595 return [x.ip_addr for x in ret]
596 596
597 597 @property
598 598 def username_and_name(self):
599 599 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
600 600
601 601 @property
602 602 def username_or_name_or_email(self):
603 603 full_name = self.full_name if self.full_name is not ' ' else None
604 604 return self.username or full_name or self.email
605 605
606 606 @property
607 607 def full_name(self):
608 608 return '%s %s' % (self.firstname, self.lastname)
609 609
610 610 @property
611 611 def full_name_or_username(self):
612 612 return ('%s %s' % (self.firstname, self.lastname)
613 613 if (self.firstname and self.lastname) else self.username)
614 614
615 615 @property
616 616 def full_contact(self):
617 617 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
618 618
619 619 @property
620 620 def short_contact(self):
621 621 return '%s %s' % (self.firstname, self.lastname)
622 622
623 623 @property
624 624 def is_admin(self):
625 625 return self.admin
626 626
627 627 @property
628 628 def AuthUser(self):
629 629 """
630 630 Returns instance of AuthUser for this user
631 631 """
632 632 from rhodecode.lib.auth import AuthUser
633 633 return AuthUser(user_id=self.user_id, api_key=self.api_key,
634 634 username=self.username)
635 635
636 636 @hybrid_property
637 637 def user_data(self):
638 638 if not self._user_data:
639 639 return {}
640 640
641 641 try:
642 642 return json.loads(self._user_data)
643 643 except TypeError:
644 644 return {}
645 645
646 646 @user_data.setter
647 647 def user_data(self, val):
648 648 if not isinstance(val, dict):
649 649 raise Exception('user_data must be dict, got %s' % type(val))
650 650 try:
651 651 self._user_data = json.dumps(val)
652 652 except Exception:
653 653 log.error(traceback.format_exc())
654 654
655 655 @classmethod
656 656 def get_by_username(cls, username, case_insensitive=False,
657 657 cache=False, identity_cache=False):
658 658 session = Session()
659 659
660 660 if case_insensitive:
661 661 q = cls.query().filter(
662 662 func.lower(cls.username) == func.lower(username))
663 663 else:
664 664 q = cls.query().filter(cls.username == username)
665 665
666 666 if cache:
667 667 if identity_cache:
668 668 val = cls.identity_cache(session, 'username', username)
669 669 if val:
670 670 return val
671 671 else:
672 672 q = q.options(
673 673 FromCache("sql_cache_short",
674 674 "get_user_by_name_%s" % _hash_key(username)))
675 675
676 676 return q.scalar()
677 677
678 678 @classmethod
679 679 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
680 680 q = cls.query().filter(cls.api_key == auth_token)
681 681
682 682 if cache:
683 683 q = q.options(FromCache("sql_cache_short",
684 684 "get_auth_token_%s" % auth_token))
685 685 res = q.scalar()
686 686
687 687 if fallback and not res:
688 688 #fallback to additional keys
689 689 _res = UserApiKeys.query()\
690 690 .filter(UserApiKeys.api_key == auth_token)\
691 691 .filter(or_(UserApiKeys.expires == -1,
692 692 UserApiKeys.expires >= time.time()))\
693 693 .first()
694 694 if _res:
695 695 res = _res.user
696 696 return res
697 697
698 698 @classmethod
699 699 def get_by_email(cls, email, case_insensitive=False, cache=False):
700 700
701 701 if case_insensitive:
702 702 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
703 703
704 704 else:
705 705 q = cls.query().filter(cls.email == email)
706 706
707 707 if cache:
708 708 q = q.options(FromCache("sql_cache_short",
709 709 "get_email_key_%s" % email))
710 710
711 711 ret = q.scalar()
712 712 if ret is None:
713 713 q = UserEmailMap.query()
714 714 # try fetching in alternate email map
715 715 if case_insensitive:
716 716 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
717 717 else:
718 718 q = q.filter(UserEmailMap.email == email)
719 719 q = q.options(joinedload(UserEmailMap.user))
720 720 if cache:
721 721 q = q.options(FromCache("sql_cache_short",
722 722 "get_email_map_key_%s" % email))
723 723 ret = getattr(q.scalar(), 'user', None)
724 724
725 725 return ret
726 726
727 727 @classmethod
728 728 def get_from_cs_author(cls, author):
729 729 """
730 730 Tries to get User objects out of commit author string
731 731
732 732 :param author:
733 733 """
734 734 from rhodecode.lib.helpers import email, author_name
735 735 # Valid email in the attribute passed, see if they're in the system
736 736 _email = email(author)
737 737 if _email:
738 738 user = cls.get_by_email(_email, case_insensitive=True)
739 739 if user:
740 740 return user
741 741 # Maybe we can match by username?
742 742 _author = author_name(author)
743 743 user = cls.get_by_username(_author, case_insensitive=True)
744 744 if user:
745 745 return user
746 746
747 747 def update_userdata(self, **kwargs):
748 748 usr = self
749 749 old = usr.user_data
750 750 old.update(**kwargs)
751 751 usr.user_data = old
752 752 Session().add(usr)
753 753 log.debug('updated userdata with ', kwargs)
754 754
755 755 def update_lastlogin(self):
756 756 """Update user lastlogin"""
757 757 self.last_login = datetime.datetime.now()
758 758 Session().add(self)
759 759 log.debug('updated user %s lastlogin', self.username)
760 760
761 761 def update_lastactivity(self):
762 762 """Update user lastactivity"""
763 763 usr = self
764 764 old = usr.user_data
765 765 old.update({'last_activity': time.time()})
766 766 usr.user_data = old
767 767 Session().add(usr)
768 768 log.debug('updated user %s lastactivity', usr.username)
769 769
770 770 def update_password(self, new_password, change_api_key=False):
771 771 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
772 772
773 773 self.password = get_crypt_password(new_password)
774 774 if change_api_key:
775 775 self.api_key = generate_auth_token(self.username)
776 776 Session().add(self)
777 777
778 778 @classmethod
779 def get_first_admin(cls):
780 user = User.query().filter(User.admin == True).first()
779 def get_first_super_admin(cls):
780 user = User.query().filter(User.admin == true()).first()
781 781 if user is None:
782 782 raise Exception('Missing administrative account!')
783 783 return user
784 784
785 785 @classmethod
786 786 def get_all_super_admins(cls):
787 787 """
788 788 Returns all admin accounts sorted by username
789 789 """
790 790 return User.query().filter(User.admin == true())\
791 791 .order_by(User.username.asc()).all()
792 792
793 793 @classmethod
794 794 def get_default_user(cls, cache=False):
795 795 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
796 796 if user is None:
797 797 raise Exception('Missing default account!')
798 798 return user
799 799
800 800 def _get_default_perms(self, user, suffix=''):
801 801 from rhodecode.model.permission import PermissionModel
802 802 return PermissionModel().get_default_perms(user.user_perms, suffix)
803 803
804 804 def get_default_perms(self, suffix=''):
805 805 return self._get_default_perms(self, suffix)
806 806
807 807 def get_api_data(self, include_secrets=False, details='full'):
808 808 """
809 809 Common function for generating user related data for API
810 810
811 811 :param include_secrets: By default secrets in the API data will be replaced
812 812 by a placeholder value to prevent exposing this data by accident. In case
813 813 this data shall be exposed, set this flag to ``True``.
814 814
815 815 :param details: details can be 'basic|full' basic gives only a subset of
816 816 the available user information that includes user_id, name and emails.
817 817 """
818 818 user = self
819 819 user_data = self.user_data
820 820 data = {
821 821 'user_id': user.user_id,
822 822 'username': user.username,
823 823 'firstname': user.name,
824 824 'lastname': user.lastname,
825 825 'email': user.email,
826 826 'emails': user.emails,
827 827 }
828 828 if details == 'basic':
829 829 return data
830 830
831 831 api_key_length = 40
832 832 api_key_replacement = '*' * api_key_length
833 833
834 834 extras = {
835 835 'api_key': api_key_replacement,
836 836 'api_keys': [api_key_replacement],
837 837 'active': user.active,
838 838 'admin': user.admin,
839 839 'extern_type': user.extern_type,
840 840 'extern_name': user.extern_name,
841 841 'last_login': user.last_login,
842 842 'ip_addresses': user.ip_addresses,
843 843 'language': user_data.get('language')
844 844 }
845 845 data.update(extras)
846 846
847 847 if include_secrets:
848 848 data['api_key'] = user.api_key
849 849 data['api_keys'] = user.auth_tokens
850 850 return data
851 851
852 852 def __json__(self):
853 853 data = {
854 854 'full_name': self.full_name,
855 855 'full_name_or_username': self.full_name_or_username,
856 856 'short_contact': self.short_contact,
857 857 'full_contact': self.full_contact,
858 858 }
859 859 data.update(self.get_api_data())
860 860 return data
861 861
862 862
863 863 class UserApiKeys(Base, BaseModel):
864 864 __tablename__ = 'user_api_keys'
865 865 __table_args__ = (
866 866 Index('uak_api_key_idx', 'api_key'),
867 867 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
868 868 UniqueConstraint('api_key'),
869 869 {'extend_existing': True, 'mysql_engine': 'InnoDB',
870 870 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
871 871 )
872 872 __mapper_args__ = {}
873 873
874 874 # ApiKey role
875 875 ROLE_ALL = 'token_role_all'
876 876 ROLE_HTTP = 'token_role_http'
877 877 ROLE_VCS = 'token_role_vcs'
878 878 ROLE_API = 'token_role_api'
879 879 ROLE_FEED = 'token_role_feed'
880 880 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
881 881
882 882 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
883 883 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
884 884 api_key = Column("api_key", String(255), nullable=False, unique=True)
885 885 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
886 886 expires = Column('expires', Float(53), nullable=False)
887 887 role = Column('role', String(255), nullable=True)
888 888 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
889 889
890 890 user = relationship('User', lazy='joined')
891 891
892 892 @classmethod
893 893 def _get_role_name(cls, role):
894 894 return {
895 895 cls.ROLE_ALL: _('all'),
896 896 cls.ROLE_HTTP: _('http/web interface'),
897 897 cls.ROLE_VCS: _('vcs (git/hg protocol)'),
898 898 cls.ROLE_API: _('api calls'),
899 899 cls.ROLE_FEED: _('feed access'),
900 900 }.get(role, role)
901 901
902 902 @property
903 903 def expired(self):
904 904 if self.expires == -1:
905 905 return False
906 906 return time.time() > self.expires
907 907
908 908 @property
909 909 def role_humanized(self):
910 910 return self._get_role_name(self.role)
911 911
912 912
913 913 class UserEmailMap(Base, BaseModel):
914 914 __tablename__ = 'user_email_map'
915 915 __table_args__ = (
916 916 Index('uem_email_idx', 'email'),
917 917 UniqueConstraint('email'),
918 918 {'extend_existing': True, 'mysql_engine': 'InnoDB',
919 919 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
920 920 )
921 921 __mapper_args__ = {}
922 922
923 923 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
924 924 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
925 925 _email = Column("email", String(255), nullable=True, unique=False, default=None)
926 926 user = relationship('User', lazy='joined')
927 927
928 928 @validates('_email')
929 929 def validate_email(self, key, email):
930 930 # check if this email is not main one
931 931 main_email = Session().query(User).filter(User.email == email).scalar()
932 932 if main_email is not None:
933 933 raise AttributeError('email %s is present is user table' % email)
934 934 return email
935 935
936 936 @hybrid_property
937 937 def email(self):
938 938 return self._email
939 939
940 940 @email.setter
941 941 def email(self, val):
942 942 self._email = val.lower() if val else None
943 943
944 944
945 945 class UserIpMap(Base, BaseModel):
946 946 __tablename__ = 'user_ip_map'
947 947 __table_args__ = (
948 948 UniqueConstraint('user_id', 'ip_addr'),
949 949 {'extend_existing': True, 'mysql_engine': 'InnoDB',
950 950 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
951 951 )
952 952 __mapper_args__ = {}
953 953
954 954 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
955 955 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
956 956 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
957 957 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
958 958 description = Column("description", String(10000), nullable=True, unique=None, default=None)
959 959 user = relationship('User', lazy='joined')
960 960
961 961 @classmethod
962 962 def _get_ip_range(cls, ip_addr):
963 963 net = ipaddress.ip_network(ip_addr, strict=False)
964 964 return [str(net.network_address), str(net.broadcast_address)]
965 965
966 966 def __json__(self):
967 967 return {
968 968 'ip_addr': self.ip_addr,
969 969 'ip_range': self._get_ip_range(self.ip_addr),
970 970 }
971 971
972 972 def __unicode__(self):
973 973 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
974 974 self.user_id, self.ip_addr)
975 975
976 976 class UserLog(Base, BaseModel):
977 977 __tablename__ = 'user_logs'
978 978 __table_args__ = (
979 979 {'extend_existing': True, 'mysql_engine': 'InnoDB',
980 980 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
981 981 )
982 982 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
983 983 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
984 984 username = Column("username", String(255), nullable=True, unique=None, default=None)
985 985 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
986 986 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
987 987 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
988 988 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
989 989 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
990 990
991 991 def __unicode__(self):
992 992 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
993 993 self.repository_name,
994 994 self.action)
995 995
996 996 @property
997 997 def action_as_day(self):
998 998 return datetime.date(*self.action_date.timetuple()[:3])
999 999
1000 1000 user = relationship('User')
1001 1001 repository = relationship('Repository', cascade='')
1002 1002
1003 1003
1004 1004 class UserGroup(Base, BaseModel):
1005 1005 __tablename__ = 'users_groups'
1006 1006 __table_args__ = (
1007 1007 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1008 1008 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1009 1009 )
1010 1010
1011 1011 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1012 1012 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1013 1013 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1014 1014 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1015 1015 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1016 1016 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1017 1017 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1018 1018 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1019 1019
1020 1020 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1021 1021 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1022 1022 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1023 1023 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1024 1024 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1025 1025 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1026 1026
1027 1027 user = relationship('User')
1028 1028
1029 1029 @hybrid_property
1030 1030 def group_data(self):
1031 1031 if not self._group_data:
1032 1032 return {}
1033 1033
1034 1034 try:
1035 1035 return json.loads(self._group_data)
1036 1036 except TypeError:
1037 1037 return {}
1038 1038
1039 1039 @group_data.setter
1040 1040 def group_data(self, val):
1041 1041 try:
1042 1042 self._group_data = json.dumps(val)
1043 1043 except Exception:
1044 1044 log.error(traceback.format_exc())
1045 1045
1046 1046 def __unicode__(self):
1047 1047 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1048 1048 self.users_group_id,
1049 1049 self.users_group_name)
1050 1050
1051 1051 @classmethod
1052 1052 def get_by_group_name(cls, group_name, cache=False,
1053 1053 case_insensitive=False):
1054 1054 if case_insensitive:
1055 1055 q = cls.query().filter(func.lower(cls.users_group_name) ==
1056 1056 func.lower(group_name))
1057 1057
1058 1058 else:
1059 1059 q = cls.query().filter(cls.users_group_name == group_name)
1060 1060 if cache:
1061 1061 q = q.options(FromCache(
1062 1062 "sql_cache_short",
1063 1063 "get_group_%s" % _hash_key(group_name)))
1064 1064 return q.scalar()
1065 1065
1066 1066 @classmethod
1067 1067 def get(cls, user_group_id, cache=False):
1068 1068 user_group = cls.query()
1069 1069 if cache:
1070 1070 user_group = user_group.options(FromCache("sql_cache_short",
1071 1071 "get_users_group_%s" % user_group_id))
1072 1072 return user_group.get(user_group_id)
1073 1073
1074 1074 def permissions(self, with_admins=True, with_owner=True):
1075 1075 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1076 1076 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1077 1077 joinedload(UserUserGroupToPerm.user),
1078 1078 joinedload(UserUserGroupToPerm.permission),)
1079 1079
1080 1080 # get owners and admins and permissions. We do a trick of re-writing
1081 1081 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1082 1082 # has a global reference and changing one object propagates to all
1083 1083 # others. This means if admin is also an owner admin_row that change
1084 1084 # would propagate to both objects
1085 1085 perm_rows = []
1086 1086 for _usr in q.all():
1087 1087 usr = AttributeDict(_usr.user.get_dict())
1088 1088 usr.permission = _usr.permission.permission_name
1089 1089 perm_rows.append(usr)
1090 1090
1091 1091 # filter the perm rows by 'default' first and then sort them by
1092 1092 # admin,write,read,none permissions sorted again alphabetically in
1093 1093 # each group
1094 1094 perm_rows = sorted(perm_rows, key=display_sort)
1095 1095
1096 1096 _admin_perm = 'usergroup.admin'
1097 1097 owner_row = []
1098 1098 if with_owner:
1099 1099 usr = AttributeDict(self.user.get_dict())
1100 1100 usr.owner_row = True
1101 1101 usr.permission = _admin_perm
1102 1102 owner_row.append(usr)
1103 1103
1104 1104 super_admin_rows = []
1105 1105 if with_admins:
1106 1106 for usr in User.get_all_super_admins():
1107 1107 # if this admin is also owner, don't double the record
1108 1108 if usr.user_id == owner_row[0].user_id:
1109 1109 owner_row[0].admin_row = True
1110 1110 else:
1111 1111 usr = AttributeDict(usr.get_dict())
1112 1112 usr.admin_row = True
1113 1113 usr.permission = _admin_perm
1114 1114 super_admin_rows.append(usr)
1115 1115
1116 1116 return super_admin_rows + owner_row + perm_rows
1117 1117
1118 1118 def permission_user_groups(self):
1119 1119 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1120 1120 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1121 1121 joinedload(UserGroupUserGroupToPerm.target_user_group),
1122 1122 joinedload(UserGroupUserGroupToPerm.permission),)
1123 1123
1124 1124 perm_rows = []
1125 1125 for _user_group in q.all():
1126 1126 usr = AttributeDict(_user_group.user_group.get_dict())
1127 1127 usr.permission = _user_group.permission.permission_name
1128 1128 perm_rows.append(usr)
1129 1129
1130 1130 return perm_rows
1131 1131
1132 1132 def _get_default_perms(self, user_group, suffix=''):
1133 1133 from rhodecode.model.permission import PermissionModel
1134 1134 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1135 1135
1136 1136 def get_default_perms(self, suffix=''):
1137 1137 return self._get_default_perms(self, suffix)
1138 1138
1139 1139 def get_api_data(self, with_group_members=True, include_secrets=False):
1140 1140 """
1141 1141 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1142 1142 basically forwarded.
1143 1143
1144 1144 """
1145 1145 user_group = self
1146 1146
1147 1147 data = {
1148 1148 'users_group_id': user_group.users_group_id,
1149 1149 'group_name': user_group.users_group_name,
1150 1150 'group_description': user_group.user_group_description,
1151 1151 'active': user_group.users_group_active,
1152 1152 'owner': user_group.user.username,
1153 1153 }
1154 1154 if with_group_members:
1155 1155 users = []
1156 1156 for user in user_group.members:
1157 1157 user = user.user
1158 1158 users.append(user.get_api_data(include_secrets=include_secrets))
1159 1159 data['users'] = users
1160 1160
1161 1161 return data
1162 1162
1163 1163
1164 1164 class UserGroupMember(Base, BaseModel):
1165 1165 __tablename__ = 'users_groups_members'
1166 1166 __table_args__ = (
1167 1167 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1168 1168 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1169 1169 )
1170 1170
1171 1171 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1172 1172 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1173 1173 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1174 1174
1175 1175 user = relationship('User', lazy='joined')
1176 1176 users_group = relationship('UserGroup')
1177 1177
1178 1178 def __init__(self, gr_id='', u_id=''):
1179 1179 self.users_group_id = gr_id
1180 1180 self.user_id = u_id
1181 1181
1182 1182
1183 1183 class RepositoryField(Base, BaseModel):
1184 1184 __tablename__ = 'repositories_fields'
1185 1185 __table_args__ = (
1186 1186 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1187 1187 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1188 1188 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1189 1189 )
1190 1190 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1191 1191
1192 1192 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1193 1193 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1194 1194 field_key = Column("field_key", String(250))
1195 1195 field_label = Column("field_label", String(1024), nullable=False)
1196 1196 field_value = Column("field_value", String(10000), nullable=False)
1197 1197 field_desc = Column("field_desc", String(1024), nullable=False)
1198 1198 field_type = Column("field_type", String(255), nullable=False, unique=None)
1199 1199 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1200 1200
1201 1201 repository = relationship('Repository')
1202 1202
1203 1203 @property
1204 1204 def field_key_prefixed(self):
1205 1205 return 'ex_%s' % self.field_key
1206 1206
1207 1207 @classmethod
1208 1208 def un_prefix_key(cls, key):
1209 1209 if key.startswith(cls.PREFIX):
1210 1210 return key[len(cls.PREFIX):]
1211 1211 return key
1212 1212
1213 1213 @classmethod
1214 1214 def get_by_key_name(cls, key, repo):
1215 1215 row = cls.query()\
1216 1216 .filter(cls.repository == repo)\
1217 1217 .filter(cls.field_key == key).scalar()
1218 1218 return row
1219 1219
1220 1220
1221 1221 class Repository(Base, BaseModel):
1222 1222 __tablename__ = 'repositories'
1223 1223 __table_args__ = (
1224 1224 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1225 1225 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1226 1226 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1227 1227 )
1228 1228 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1229 1229 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1230 1230
1231 1231 STATE_CREATED = 'repo_state_created'
1232 1232 STATE_PENDING = 'repo_state_pending'
1233 1233 STATE_ERROR = 'repo_state_error'
1234 1234
1235 1235 LOCK_AUTOMATIC = 'lock_auto'
1236 1236 LOCK_API = 'lock_api'
1237 1237 LOCK_WEB = 'lock_web'
1238 1238 LOCK_PULL = 'lock_pull'
1239 1239
1240 1240 NAME_SEP = URL_SEP
1241 1241
1242 1242 repo_id = Column(
1243 1243 "repo_id", Integer(), nullable=False, unique=True, default=None,
1244 1244 primary_key=True)
1245 1245 _repo_name = Column(
1246 1246 "repo_name", Text(), nullable=False, default=None)
1247 1247 _repo_name_hash = Column(
1248 1248 "repo_name_hash", String(255), nullable=False, unique=True)
1249 1249 repo_state = Column("repo_state", String(255), nullable=True)
1250 1250
1251 1251 clone_uri = Column(
1252 1252 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1253 1253 default=None)
1254 1254 repo_type = Column(
1255 1255 "repo_type", String(255), nullable=False, unique=False, default=None)
1256 1256 user_id = Column(
1257 1257 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1258 1258 unique=False, default=None)
1259 1259 private = Column(
1260 1260 "private", Boolean(), nullable=True, unique=None, default=None)
1261 1261 enable_statistics = Column(
1262 1262 "statistics", Boolean(), nullable=True, unique=None, default=True)
1263 1263 enable_downloads = Column(
1264 1264 "downloads", Boolean(), nullable=True, unique=None, default=True)
1265 1265 description = Column(
1266 1266 "description", String(10000), nullable=True, unique=None, default=None)
1267 1267 created_on = Column(
1268 1268 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1269 1269 default=datetime.datetime.now)
1270 1270 updated_on = Column(
1271 1271 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1272 1272 default=datetime.datetime.now)
1273 1273 _landing_revision = Column(
1274 1274 "landing_revision", String(255), nullable=False, unique=False,
1275 1275 default=None)
1276 1276 enable_locking = Column(
1277 1277 "enable_locking", Boolean(), nullable=False, unique=None,
1278 1278 default=False)
1279 1279 _locked = Column(
1280 1280 "locked", String(255), nullable=True, unique=False, default=None)
1281 1281 _changeset_cache = Column(
1282 1282 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1283 1283
1284 1284 fork_id = Column(
1285 1285 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1286 1286 nullable=True, unique=False, default=None)
1287 1287 group_id = Column(
1288 1288 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1289 1289 unique=False, default=None)
1290 1290
1291 1291 user = relationship('User', lazy='joined')
1292 1292 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1293 1293 group = relationship('RepoGroup', lazy='joined')
1294 1294 repo_to_perm = relationship(
1295 1295 'UserRepoToPerm', cascade='all',
1296 1296 order_by='UserRepoToPerm.repo_to_perm_id')
1297 1297 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1298 1298 stats = relationship('Statistics', cascade='all', uselist=False)
1299 1299
1300 1300 followers = relationship(
1301 1301 'UserFollowing',
1302 1302 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1303 1303 cascade='all')
1304 1304 extra_fields = relationship(
1305 1305 'RepositoryField', cascade="all, delete, delete-orphan")
1306 1306 logs = relationship('UserLog')
1307 1307 comments = relationship(
1308 1308 'ChangesetComment', cascade="all, delete, delete-orphan")
1309 1309 pull_requests_source = relationship(
1310 1310 'PullRequest',
1311 1311 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1312 1312 cascade="all, delete, delete-orphan")
1313 1313 pull_requests_target = relationship(
1314 1314 'PullRequest',
1315 1315 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1316 1316 cascade="all, delete, delete-orphan")
1317 1317 ui = relationship('RepoRhodeCodeUi', cascade="all")
1318 1318 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1319 1319
1320 1320 def __unicode__(self):
1321 1321 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1322 1322 safe_unicode(self.repo_name))
1323 1323
1324 1324 @hybrid_property
1325 1325 def landing_rev(self):
1326 1326 # always should return [rev_type, rev]
1327 1327 if self._landing_revision:
1328 1328 _rev_info = self._landing_revision.split(':')
1329 1329 if len(_rev_info) < 2:
1330 1330 _rev_info.insert(0, 'rev')
1331 1331 return [_rev_info[0], _rev_info[1]]
1332 1332 return [None, None]
1333 1333
1334 1334 @landing_rev.setter
1335 1335 def landing_rev(self, val):
1336 1336 if ':' not in val:
1337 1337 raise ValueError('value must be delimited with `:` and consist '
1338 1338 'of <rev_type>:<rev>, got %s instead' % val)
1339 1339 self._landing_revision = val
1340 1340
1341 1341 @hybrid_property
1342 1342 def locked(self):
1343 1343 if self._locked:
1344 1344 user_id, timelocked, reason = self._locked.split(':')
1345 1345 lock_values = int(user_id), timelocked, reason
1346 1346 else:
1347 1347 lock_values = [None, None, None]
1348 1348 return lock_values
1349 1349
1350 1350 @locked.setter
1351 1351 def locked(self, val):
1352 1352 if val and isinstance(val, (list, tuple)):
1353 1353 self._locked = ':'.join(map(str, val))
1354 1354 else:
1355 1355 self._locked = None
1356 1356
1357 1357 @hybrid_property
1358 1358 def changeset_cache(self):
1359 1359 from rhodecode.lib.vcs.backends.base import EmptyCommit
1360 1360 dummy = EmptyCommit().__json__()
1361 1361 if not self._changeset_cache:
1362 1362 return dummy
1363 1363 try:
1364 1364 return json.loads(self._changeset_cache)
1365 1365 except TypeError:
1366 1366 return dummy
1367 1367 except Exception:
1368 1368 log.error(traceback.format_exc())
1369 1369 return dummy
1370 1370
1371 1371 @changeset_cache.setter
1372 1372 def changeset_cache(self, val):
1373 1373 try:
1374 1374 self._changeset_cache = json.dumps(val)
1375 1375 except Exception:
1376 1376 log.error(traceback.format_exc())
1377 1377
1378 1378 @hybrid_property
1379 1379 def repo_name(self):
1380 1380 return self._repo_name
1381 1381
1382 1382 @repo_name.setter
1383 1383 def repo_name(self, value):
1384 1384 self._repo_name = value
1385 1385 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1386 1386
1387 1387 @classmethod
1388 1388 def normalize_repo_name(cls, repo_name):
1389 1389 """
1390 1390 Normalizes os specific repo_name to the format internally stored inside
1391 1391 database using URL_SEP
1392 1392
1393 1393 :param cls:
1394 1394 :param repo_name:
1395 1395 """
1396 1396 return cls.NAME_SEP.join(repo_name.split(os.sep))
1397 1397
1398 1398 @classmethod
1399 1399 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1400 1400 session = Session()
1401 1401 q = session.query(cls).filter(cls.repo_name == repo_name)
1402 1402
1403 1403 if cache:
1404 1404 if identity_cache:
1405 1405 val = cls.identity_cache(session, 'repo_name', repo_name)
1406 1406 if val:
1407 1407 return val
1408 1408 else:
1409 1409 q = q.options(
1410 1410 FromCache("sql_cache_short",
1411 1411 "get_repo_by_name_%s" % _hash_key(repo_name)))
1412 1412
1413 1413 return q.scalar()
1414 1414
1415 1415 @classmethod
1416 1416 def get_by_full_path(cls, repo_full_path):
1417 1417 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1418 1418 repo_name = cls.normalize_repo_name(repo_name)
1419 1419 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1420 1420
1421 1421 @classmethod
1422 1422 def get_repo_forks(cls, repo_id):
1423 1423 return cls.query().filter(Repository.fork_id == repo_id)
1424 1424
1425 1425 @classmethod
1426 1426 def base_path(cls):
1427 1427 """
1428 1428 Returns base path when all repos are stored
1429 1429
1430 1430 :param cls:
1431 1431 """
1432 1432 q = Session().query(RhodeCodeUi)\
1433 1433 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1434 1434 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1435 1435 return q.one().ui_value
1436 1436
1437 1437 @classmethod
1438 1438 def is_valid(cls, repo_name):
1439 1439 """
1440 1440 returns True if given repo name is a valid filesystem repository
1441 1441
1442 1442 :param cls:
1443 1443 :param repo_name:
1444 1444 """
1445 1445 from rhodecode.lib.utils import is_valid_repo
1446 1446
1447 1447 return is_valid_repo(repo_name, cls.base_path())
1448 1448
1449 1449 @classmethod
1450 1450 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1451 1451 case_insensitive=True):
1452 1452 q = Repository.query()
1453 1453
1454 1454 if not isinstance(user_id, Optional):
1455 1455 q = q.filter(Repository.user_id == user_id)
1456 1456
1457 1457 if not isinstance(group_id, Optional):
1458 1458 q = q.filter(Repository.group_id == group_id)
1459 1459
1460 1460 if case_insensitive:
1461 1461 q = q.order_by(func.lower(Repository.repo_name))
1462 1462 else:
1463 1463 q = q.order_by(Repository.repo_name)
1464 1464 return q.all()
1465 1465
1466 1466 @property
1467 1467 def forks(self):
1468 1468 """
1469 1469 Return forks of this repo
1470 1470 """
1471 1471 return Repository.get_repo_forks(self.repo_id)
1472 1472
1473 1473 @property
1474 1474 def parent(self):
1475 1475 """
1476 1476 Returns fork parent
1477 1477 """
1478 1478 return self.fork
1479 1479
1480 1480 @property
1481 1481 def just_name(self):
1482 1482 return self.repo_name.split(self.NAME_SEP)[-1]
1483 1483
1484 1484 @property
1485 1485 def groups_with_parents(self):
1486 1486 groups = []
1487 1487 if self.group is None:
1488 1488 return groups
1489 1489
1490 1490 cur_gr = self.group
1491 1491 groups.insert(0, cur_gr)
1492 1492 while 1:
1493 1493 gr = getattr(cur_gr, 'parent_group', None)
1494 1494 cur_gr = cur_gr.parent_group
1495 1495 if gr is None:
1496 1496 break
1497 1497 groups.insert(0, gr)
1498 1498
1499 1499 return groups
1500 1500
1501 1501 @property
1502 1502 def groups_and_repo(self):
1503 1503 return self.groups_with_parents, self
1504 1504
1505 1505 @LazyProperty
1506 1506 def repo_path(self):
1507 1507 """
1508 1508 Returns base full path for that repository means where it actually
1509 1509 exists on a filesystem
1510 1510 """
1511 1511 q = Session().query(RhodeCodeUi).filter(
1512 1512 RhodeCodeUi.ui_key == self.NAME_SEP)
1513 1513 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1514 1514 return q.one().ui_value
1515 1515
1516 1516 @property
1517 1517 def repo_full_path(self):
1518 1518 p = [self.repo_path]
1519 1519 # we need to split the name by / since this is how we store the
1520 1520 # names in the database, but that eventually needs to be converted
1521 1521 # into a valid system path
1522 1522 p += self.repo_name.split(self.NAME_SEP)
1523 1523 return os.path.join(*map(safe_unicode, p))
1524 1524
1525 1525 @property
1526 1526 def cache_keys(self):
1527 1527 """
1528 1528 Returns associated cache keys for that repo
1529 1529 """
1530 1530 return CacheKey.query()\
1531 1531 .filter(CacheKey.cache_args == self.repo_name)\
1532 1532 .order_by(CacheKey.cache_key)\
1533 1533 .all()
1534 1534
1535 1535 def get_new_name(self, repo_name):
1536 1536 """
1537 1537 returns new full repository name based on assigned group and new new
1538 1538
1539 1539 :param group_name:
1540 1540 """
1541 1541 path_prefix = self.group.full_path_splitted if self.group else []
1542 1542 return self.NAME_SEP.join(path_prefix + [repo_name])
1543 1543
1544 1544 @property
1545 1545 def _config(self):
1546 1546 """
1547 1547 Returns db based config object.
1548 1548 """
1549 1549 from rhodecode.lib.utils import make_db_config
1550 1550 return make_db_config(clear_session=False, repo=self)
1551 1551
1552 1552 def permissions(self, with_admins=True, with_owner=True):
1553 1553 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1554 1554 q = q.options(joinedload(UserRepoToPerm.repository),
1555 1555 joinedload(UserRepoToPerm.user),
1556 1556 joinedload(UserRepoToPerm.permission),)
1557 1557
1558 1558 # get owners and admins and permissions. We do a trick of re-writing
1559 1559 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1560 1560 # has a global reference and changing one object propagates to all
1561 1561 # others. This means if admin is also an owner admin_row that change
1562 1562 # would propagate to both objects
1563 1563 perm_rows = []
1564 1564 for _usr in q.all():
1565 1565 usr = AttributeDict(_usr.user.get_dict())
1566 1566 usr.permission = _usr.permission.permission_name
1567 1567 perm_rows.append(usr)
1568 1568
1569 1569 # filter the perm rows by 'default' first and then sort them by
1570 1570 # admin,write,read,none permissions sorted again alphabetically in
1571 1571 # each group
1572 1572 perm_rows = sorted(perm_rows, key=display_sort)
1573 1573
1574 1574 _admin_perm = 'repository.admin'
1575 1575 owner_row = []
1576 1576 if with_owner:
1577 1577 usr = AttributeDict(self.user.get_dict())
1578 1578 usr.owner_row = True
1579 1579 usr.permission = _admin_perm
1580 1580 owner_row.append(usr)
1581 1581
1582 1582 super_admin_rows = []
1583 1583 if with_admins:
1584 1584 for usr in User.get_all_super_admins():
1585 1585 # if this admin is also owner, don't double the record
1586 1586 if usr.user_id == owner_row[0].user_id:
1587 1587 owner_row[0].admin_row = True
1588 1588 else:
1589 1589 usr = AttributeDict(usr.get_dict())
1590 1590 usr.admin_row = True
1591 1591 usr.permission = _admin_perm
1592 1592 super_admin_rows.append(usr)
1593 1593
1594 1594 return super_admin_rows + owner_row + perm_rows
1595 1595
1596 1596 def permission_user_groups(self):
1597 1597 q = UserGroupRepoToPerm.query().filter(
1598 1598 UserGroupRepoToPerm.repository == self)
1599 1599 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1600 1600 joinedload(UserGroupRepoToPerm.users_group),
1601 1601 joinedload(UserGroupRepoToPerm.permission),)
1602 1602
1603 1603 perm_rows = []
1604 1604 for _user_group in q.all():
1605 1605 usr = AttributeDict(_user_group.users_group.get_dict())
1606 1606 usr.permission = _user_group.permission.permission_name
1607 1607 perm_rows.append(usr)
1608 1608
1609 1609 return perm_rows
1610 1610
1611 1611 def get_api_data(self, include_secrets=False):
1612 1612 """
1613 1613 Common function for generating repo api data
1614 1614
1615 1615 :param include_secrets: See :meth:`User.get_api_data`.
1616 1616
1617 1617 """
1618 1618 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1619 1619 # move this methods on models level.
1620 1620 from rhodecode.model.settings import SettingsModel
1621 1621
1622 1622 repo = self
1623 1623 _user_id, _time, _reason = self.locked
1624 1624
1625 1625 data = {
1626 1626 'repo_id': repo.repo_id,
1627 1627 'repo_name': repo.repo_name,
1628 1628 'repo_type': repo.repo_type,
1629 1629 'clone_uri': repo.clone_uri or '',
1630 1630 'private': repo.private,
1631 1631 'created_on': repo.created_on,
1632 1632 'description': repo.description,
1633 1633 'landing_rev': repo.landing_rev,
1634 1634 'owner': repo.user.username,
1635 1635 'fork_of': repo.fork.repo_name if repo.fork else None,
1636 1636 'enable_statistics': repo.enable_statistics,
1637 1637 'enable_locking': repo.enable_locking,
1638 1638 'enable_downloads': repo.enable_downloads,
1639 1639 'last_changeset': repo.changeset_cache,
1640 1640 'locked_by': User.get(_user_id).get_api_data(
1641 1641 include_secrets=include_secrets) if _user_id else None,
1642 1642 'locked_date': time_to_datetime(_time) if _time else None,
1643 1643 'lock_reason': _reason if _reason else None,
1644 1644 }
1645 1645
1646 1646 # TODO: mikhail: should be per-repo settings here
1647 1647 rc_config = SettingsModel().get_all_settings()
1648 1648 repository_fields = str2bool(
1649 1649 rc_config.get('rhodecode_repository_fields'))
1650 1650 if repository_fields:
1651 1651 for f in self.extra_fields:
1652 1652 data[f.field_key_prefixed] = f.field_value
1653 1653
1654 1654 return data
1655 1655
1656 1656 @classmethod
1657 1657 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1658 1658 if not lock_time:
1659 1659 lock_time = time.time()
1660 1660 if not lock_reason:
1661 1661 lock_reason = cls.LOCK_AUTOMATIC
1662 1662 repo.locked = [user_id, lock_time, lock_reason]
1663 1663 Session().add(repo)
1664 1664 Session().commit()
1665 1665
1666 1666 @classmethod
1667 1667 def unlock(cls, repo):
1668 1668 repo.locked = None
1669 1669 Session().add(repo)
1670 1670 Session().commit()
1671 1671
1672 1672 @classmethod
1673 1673 def getlock(cls, repo):
1674 1674 return repo.locked
1675 1675
1676 1676 def is_user_lock(self, user_id):
1677 1677 if self.lock[0]:
1678 1678 lock_user_id = safe_int(self.lock[0])
1679 1679 user_id = safe_int(user_id)
1680 1680 # both are ints, and they are equal
1681 1681 return all([lock_user_id, user_id]) and lock_user_id == user_id
1682 1682
1683 1683 return False
1684 1684
1685 1685 def get_locking_state(self, action, user_id, only_when_enabled=True):
1686 1686 """
1687 1687 Checks locking on this repository, if locking is enabled and lock is
1688 1688 present returns a tuple of make_lock, locked, locked_by.
1689 1689 make_lock can have 3 states None (do nothing) True, make lock
1690 1690 False release lock, This value is later propagated to hooks, which
1691 1691 do the locking. Think about this as signals passed to hooks what to do.
1692 1692
1693 1693 """
1694 1694 # TODO: johbo: This is part of the business logic and should be moved
1695 1695 # into the RepositoryModel.
1696 1696
1697 1697 if action not in ('push', 'pull'):
1698 1698 raise ValueError("Invalid action value: %s" % repr(action))
1699 1699
1700 1700 # defines if locked error should be thrown to user
1701 1701 currently_locked = False
1702 1702 # defines if new lock should be made, tri-state
1703 1703 make_lock = None
1704 1704 repo = self
1705 1705 user = User.get(user_id)
1706 1706
1707 1707 lock_info = repo.locked
1708 1708
1709 1709 if repo and (repo.enable_locking or not only_when_enabled):
1710 1710 if action == 'push':
1711 1711 # check if it's already locked !, if it is compare users
1712 1712 locked_by_user_id = lock_info[0]
1713 1713 if user.user_id == locked_by_user_id:
1714 1714 log.debug(
1715 1715 'Got `push` action from user %s, now unlocking', user)
1716 1716 # unlock if we have push from user who locked
1717 1717 make_lock = False
1718 1718 else:
1719 1719 # we're not the same user who locked, ban with
1720 1720 # code defined in settings (default is 423 HTTP Locked) !
1721 1721 log.debug('Repo %s is currently locked by %s', repo, user)
1722 1722 currently_locked = True
1723 1723 elif action == 'pull':
1724 1724 # [0] user [1] date
1725 1725 if lock_info[0] and lock_info[1]:
1726 1726 log.debug('Repo %s is currently locked by %s', repo, user)
1727 1727 currently_locked = True
1728 1728 else:
1729 1729 log.debug('Setting lock on repo %s by %s', repo, user)
1730 1730 make_lock = True
1731 1731
1732 1732 else:
1733 1733 log.debug('Repository %s do not have locking enabled', repo)
1734 1734
1735 1735 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1736 1736 make_lock, currently_locked, lock_info)
1737 1737
1738 1738 from rhodecode.lib.auth import HasRepoPermissionAny
1739 1739 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1740 1740 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1741 1741 # if we don't have at least write permission we cannot make a lock
1742 1742 log.debug('lock state reset back to FALSE due to lack '
1743 1743 'of at least read permission')
1744 1744 make_lock = False
1745 1745
1746 1746 return make_lock, currently_locked, lock_info
1747 1747
1748 1748 @property
1749 1749 def last_db_change(self):
1750 1750 return self.updated_on
1751 1751
1752 1752 @property
1753 1753 def clone_uri_hidden(self):
1754 1754 clone_uri = self.clone_uri
1755 1755 if clone_uri:
1756 1756 import urlobject
1757 1757 url_obj = urlobject.URLObject(self.clone_uri)
1758 1758 if url_obj.password:
1759 1759 clone_uri = url_obj.with_password('*****')
1760 1760 return clone_uri
1761 1761
1762 1762 def clone_url(self, **override):
1763 1763 qualified_home_url = url('home', qualified=True)
1764 1764
1765 1765 uri_tmpl = None
1766 1766 if 'with_id' in override:
1767 1767 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1768 1768 del override['with_id']
1769 1769
1770 1770 if 'uri_tmpl' in override:
1771 1771 uri_tmpl = override['uri_tmpl']
1772 1772 del override['uri_tmpl']
1773 1773
1774 1774 # we didn't override our tmpl from **overrides
1775 1775 if not uri_tmpl:
1776 1776 uri_tmpl = self.DEFAULT_CLONE_URI
1777 1777 try:
1778 1778 from pylons import tmpl_context as c
1779 1779 uri_tmpl = c.clone_uri_tmpl
1780 1780 except Exception:
1781 1781 # in any case if we call this outside of request context,
1782 1782 # ie, not having tmpl_context set up
1783 1783 pass
1784 1784
1785 1785 return get_clone_url(uri_tmpl=uri_tmpl,
1786 1786 qualifed_home_url=qualified_home_url,
1787 1787 repo_name=self.repo_name,
1788 1788 repo_id=self.repo_id, **override)
1789 1789
1790 1790 def set_state(self, state):
1791 1791 self.repo_state = state
1792 1792 Session().add(self)
1793 1793 #==========================================================================
1794 1794 # SCM PROPERTIES
1795 1795 #==========================================================================
1796 1796
1797 1797 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1798 1798 return get_commit_safe(
1799 1799 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1800 1800
1801 1801 def get_changeset(self, rev=None, pre_load=None):
1802 1802 warnings.warn("Use get_commit", DeprecationWarning)
1803 1803 commit_id = None
1804 1804 commit_idx = None
1805 1805 if isinstance(rev, basestring):
1806 1806 commit_id = rev
1807 1807 else:
1808 1808 commit_idx = rev
1809 1809 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1810 1810 pre_load=pre_load)
1811 1811
1812 1812 def get_landing_commit(self):
1813 1813 """
1814 1814 Returns landing commit, or if that doesn't exist returns the tip
1815 1815 """
1816 1816 _rev_type, _rev = self.landing_rev
1817 1817 commit = self.get_commit(_rev)
1818 1818 if isinstance(commit, EmptyCommit):
1819 1819 return self.get_commit()
1820 1820 return commit
1821 1821
1822 1822 def update_commit_cache(self, cs_cache=None, config=None):
1823 1823 """
1824 1824 Update cache of last changeset for repository, keys should be::
1825 1825
1826 1826 short_id
1827 1827 raw_id
1828 1828 revision
1829 1829 parents
1830 1830 message
1831 1831 date
1832 1832 author
1833 1833
1834 1834 :param cs_cache:
1835 1835 """
1836 1836 from rhodecode.lib.vcs.backends.base import BaseChangeset
1837 1837 if cs_cache is None:
1838 1838 # use no-cache version here
1839 1839 scm_repo = self.scm_instance(cache=False, config=config)
1840 1840 if scm_repo:
1841 1841 cs_cache = scm_repo.get_commit(
1842 1842 pre_load=["author", "date", "message", "parents"])
1843 1843 else:
1844 1844 cs_cache = EmptyCommit()
1845 1845
1846 1846 if isinstance(cs_cache, BaseChangeset):
1847 1847 cs_cache = cs_cache.__json__()
1848 1848
1849 1849 def is_outdated(new_cs_cache):
1850 1850 if new_cs_cache['raw_id'] != self.changeset_cache['raw_id']:
1851 1851 return True
1852 1852 return False
1853 1853
1854 1854 # check if we have maybe already latest cached revision
1855 1855 if is_outdated(cs_cache) or not self.changeset_cache:
1856 1856 _default = datetime.datetime.fromtimestamp(0)
1857 1857 last_change = cs_cache.get('date') or _default
1858 1858 log.debug('updated repo %s with new cs cache %s',
1859 1859 self.repo_name, cs_cache)
1860 1860 self.updated_on = last_change
1861 1861 self.changeset_cache = cs_cache
1862 1862 Session().add(self)
1863 1863 Session().commit()
1864 1864 else:
1865 1865 log.debug('Skipping update_commit_cache for repo:`%s` '
1866 1866 'commit already with latest changes', self.repo_name)
1867 1867
1868 1868 @property
1869 1869 def tip(self):
1870 1870 return self.get_commit('tip')
1871 1871
1872 1872 @property
1873 1873 def author(self):
1874 1874 return self.tip.author
1875 1875
1876 1876 @property
1877 1877 def last_change(self):
1878 1878 return self.scm_instance().last_change
1879 1879
1880 1880 def get_comments(self, revisions=None):
1881 1881 """
1882 1882 Returns comments for this repository grouped by revisions
1883 1883
1884 1884 :param revisions: filter query by revisions only
1885 1885 """
1886 1886 cmts = ChangesetComment.query()\
1887 1887 .filter(ChangesetComment.repo == self)
1888 1888 if revisions:
1889 1889 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1890 1890 grouped = collections.defaultdict(list)
1891 1891 for cmt in cmts.all():
1892 1892 grouped[cmt.revision].append(cmt)
1893 1893 return grouped
1894 1894
1895 1895 def statuses(self, revisions=None):
1896 1896 """
1897 1897 Returns statuses for this repository
1898 1898
1899 1899 :param revisions: list of revisions to get statuses for
1900 1900 """
1901 1901 statuses = ChangesetStatus.query()\
1902 1902 .filter(ChangesetStatus.repo == self)\
1903 1903 .filter(ChangesetStatus.version == 0)
1904 1904
1905 1905 if revisions:
1906 1906 # Try doing the filtering in chunks to avoid hitting limits
1907 1907 size = 500
1908 1908 status_results = []
1909 1909 for chunk in xrange(0, len(revisions), size):
1910 1910 status_results += statuses.filter(
1911 1911 ChangesetStatus.revision.in_(
1912 1912 revisions[chunk: chunk+size])
1913 1913 ).all()
1914 1914 else:
1915 1915 status_results = statuses.all()
1916 1916
1917 1917 grouped = {}
1918 1918
1919 1919 # maybe we have open new pullrequest without a status?
1920 1920 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1921 1921 status_lbl = ChangesetStatus.get_status_lbl(stat)
1922 1922 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1923 1923 for rev in pr.revisions:
1924 1924 pr_id = pr.pull_request_id
1925 1925 pr_repo = pr.target_repo.repo_name
1926 1926 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1927 1927
1928 1928 for stat in status_results:
1929 1929 pr_id = pr_repo = None
1930 1930 if stat.pull_request:
1931 1931 pr_id = stat.pull_request.pull_request_id
1932 1932 pr_repo = stat.pull_request.target_repo.repo_name
1933 1933 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1934 1934 pr_id, pr_repo]
1935 1935 return grouped
1936 1936
1937 1937 # ==========================================================================
1938 1938 # SCM CACHE INSTANCE
1939 1939 # ==========================================================================
1940 1940
1941 1941 def scm_instance(self, **kwargs):
1942 1942 import rhodecode
1943 1943
1944 1944 # Passing a config will not hit the cache currently only used
1945 1945 # for repo2dbmapper
1946 1946 config = kwargs.pop('config', None)
1947 1947 cache = kwargs.pop('cache', None)
1948 1948 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1949 1949 # if cache is NOT defined use default global, else we have a full
1950 1950 # control over cache behaviour
1951 1951 if cache is None and full_cache and not config:
1952 1952 return self._get_instance_cached()
1953 1953 return self._get_instance(cache=bool(cache), config=config)
1954 1954
1955 1955 def _get_instance_cached(self):
1956 1956 @cache_region('long_term')
1957 1957 def _get_repo(cache_key):
1958 1958 return self._get_instance()
1959 1959
1960 1960 invalidator_context = CacheKey.repo_context_cache(
1961 1961 _get_repo, self.repo_name, None)
1962 1962
1963 1963 with invalidator_context as context:
1964 1964 context.invalidate()
1965 1965 repo = context.compute()
1966 1966
1967 1967 return repo
1968 1968
1969 1969 def _get_instance(self, cache=True, config=None):
1970 1970 repo_full_path = self.repo_full_path
1971 1971 try:
1972 1972 vcs_alias = get_scm(repo_full_path)[0]
1973 1973 log.debug(
1974 1974 'Creating instance of %s repository from %s',
1975 1975 vcs_alias, repo_full_path)
1976 1976 backend = get_backend(vcs_alias)
1977 1977 except VCSError:
1978 1978 log.exception(
1979 1979 'Perhaps this repository is in db and not in '
1980 1980 'filesystem run rescan repositories with '
1981 1981 '"destroy old data" option from admin panel')
1982 1982 return
1983 1983
1984 1984 config = config or self._config
1985 1985 custom_wire = {
1986 1986 'cache': cache # controls the vcs.remote cache
1987 1987 }
1988 1988 repo = backend(
1989 1989 safe_str(repo_full_path), config=config, create=False,
1990 1990 with_wire=custom_wire)
1991 1991
1992 1992 return repo
1993 1993
1994 1994 def __json__(self):
1995 1995 return {'landing_rev': self.landing_rev}
1996 1996
1997 1997 def get_dict(self):
1998 1998
1999 1999 # Since we transformed `repo_name` to a hybrid property, we need to
2000 2000 # keep compatibility with the code which uses `repo_name` field.
2001 2001
2002 2002 result = super(Repository, self).get_dict()
2003 2003 result['repo_name'] = result.pop('_repo_name', None)
2004 2004 return result
2005 2005
2006 2006
2007 2007 class RepoGroup(Base, BaseModel):
2008 2008 __tablename__ = 'groups'
2009 2009 __table_args__ = (
2010 2010 UniqueConstraint('group_name', 'group_parent_id'),
2011 2011 CheckConstraint('group_id != group_parent_id'),
2012 2012 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2013 2013 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2014 2014 )
2015 2015 __mapper_args__ = {'order_by': 'group_name'}
2016 2016
2017 2017 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2018 2018
2019 2019 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2020 2020 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2021 2021 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2022 2022 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2023 2023 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2024 2024 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2025 2025 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2026 2026
2027 2027 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2028 2028 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2029 2029 parent_group = relationship('RepoGroup', remote_side=group_id)
2030 2030 user = relationship('User')
2031 2031
2032 2032 def __init__(self, group_name='', parent_group=None):
2033 2033 self.group_name = group_name
2034 2034 self.parent_group = parent_group
2035 2035
2036 2036 def __unicode__(self):
2037 2037 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2038 2038 self.group_name)
2039 2039
2040 2040 @classmethod
2041 2041 def _generate_choice(cls, repo_group):
2042 2042 from webhelpers.html import literal as _literal
2043 2043 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2044 2044 return repo_group.group_id, _name(repo_group.full_path_splitted)
2045 2045
2046 2046 @classmethod
2047 2047 def groups_choices(cls, groups=None, show_empty_group=True):
2048 2048 if not groups:
2049 2049 groups = cls.query().all()
2050 2050
2051 2051 repo_groups = []
2052 2052 if show_empty_group:
2053 2053 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2054 2054
2055 2055 repo_groups.extend([cls._generate_choice(x) for x in groups])
2056 2056
2057 2057 repo_groups = sorted(
2058 2058 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2059 2059 return repo_groups
2060 2060
2061 2061 @classmethod
2062 2062 def url_sep(cls):
2063 2063 return URL_SEP
2064 2064
2065 2065 @classmethod
2066 2066 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2067 2067 if case_insensitive:
2068 2068 gr = cls.query().filter(func.lower(cls.group_name)
2069 2069 == func.lower(group_name))
2070 2070 else:
2071 2071 gr = cls.query().filter(cls.group_name == group_name)
2072 2072 if cache:
2073 2073 gr = gr.options(FromCache(
2074 2074 "sql_cache_short",
2075 2075 "get_group_%s" % _hash_key(group_name)))
2076 2076 return gr.scalar()
2077 2077
2078 2078 @classmethod
2079 2079 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2080 2080 case_insensitive=True):
2081 2081 q = RepoGroup.query()
2082 2082
2083 2083 if not isinstance(user_id, Optional):
2084 2084 q = q.filter(RepoGroup.user_id == user_id)
2085 2085
2086 2086 if not isinstance(group_id, Optional):
2087 2087 q = q.filter(RepoGroup.group_parent_id == group_id)
2088 2088
2089 2089 if case_insensitive:
2090 2090 q = q.order_by(func.lower(RepoGroup.group_name))
2091 2091 else:
2092 2092 q = q.order_by(RepoGroup.group_name)
2093 2093 return q.all()
2094 2094
2095 2095 @property
2096 2096 def parents(self):
2097 2097 parents_recursion_limit = 10
2098 2098 groups = []
2099 2099 if self.parent_group is None:
2100 2100 return groups
2101 2101 cur_gr = self.parent_group
2102 2102 groups.insert(0, cur_gr)
2103 2103 cnt = 0
2104 2104 while 1:
2105 2105 cnt += 1
2106 2106 gr = getattr(cur_gr, 'parent_group', None)
2107 2107 cur_gr = cur_gr.parent_group
2108 2108 if gr is None:
2109 2109 break
2110 2110 if cnt == parents_recursion_limit:
2111 2111 # this will prevent accidental infinit loops
2112 2112 log.error(('more than %s parents found for group %s, stopping '
2113 2113 'recursive parent fetching' % (parents_recursion_limit, self)))
2114 2114 break
2115 2115
2116 2116 groups.insert(0, gr)
2117 2117 return groups
2118 2118
2119 2119 @property
2120 2120 def children(self):
2121 2121 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2122 2122
2123 2123 @property
2124 2124 def name(self):
2125 2125 return self.group_name.split(RepoGroup.url_sep())[-1]
2126 2126
2127 2127 @property
2128 2128 def full_path(self):
2129 2129 return self.group_name
2130 2130
2131 2131 @property
2132 2132 def full_path_splitted(self):
2133 2133 return self.group_name.split(RepoGroup.url_sep())
2134 2134
2135 2135 @property
2136 2136 def repositories(self):
2137 2137 return Repository.query()\
2138 2138 .filter(Repository.group == self)\
2139 2139 .order_by(Repository.repo_name)
2140 2140
2141 2141 @property
2142 2142 def repositories_recursive_count(self):
2143 2143 cnt = self.repositories.count()
2144 2144
2145 2145 def children_count(group):
2146 2146 cnt = 0
2147 2147 for child in group.children:
2148 2148 cnt += child.repositories.count()
2149 2149 cnt += children_count(child)
2150 2150 return cnt
2151 2151
2152 2152 return cnt + children_count(self)
2153 2153
2154 2154 def _recursive_objects(self, include_repos=True):
2155 2155 all_ = []
2156 2156
2157 2157 def _get_members(root_gr):
2158 2158 if include_repos:
2159 2159 for r in root_gr.repositories:
2160 2160 all_.append(r)
2161 2161 childs = root_gr.children.all()
2162 2162 if childs:
2163 2163 for gr in childs:
2164 2164 all_.append(gr)
2165 2165 _get_members(gr)
2166 2166
2167 2167 _get_members(self)
2168 2168 return [self] + all_
2169 2169
2170 2170 def recursive_groups_and_repos(self):
2171 2171 """
2172 2172 Recursive return all groups, with repositories in those groups
2173 2173 """
2174 2174 return self._recursive_objects()
2175 2175
2176 2176 def recursive_groups(self):
2177 2177 """
2178 2178 Returns all children groups for this group including children of children
2179 2179 """
2180 2180 return self._recursive_objects(include_repos=False)
2181 2181
2182 2182 def get_new_name(self, group_name):
2183 2183 """
2184 2184 returns new full group name based on parent and new name
2185 2185
2186 2186 :param group_name:
2187 2187 """
2188 2188 path_prefix = (self.parent_group.full_path_splitted if
2189 2189 self.parent_group else [])
2190 2190 return RepoGroup.url_sep().join(path_prefix + [group_name])
2191 2191
2192 2192 def permissions(self, with_admins=True, with_owner=True):
2193 2193 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2194 2194 q = q.options(joinedload(UserRepoGroupToPerm.group),
2195 2195 joinedload(UserRepoGroupToPerm.user),
2196 2196 joinedload(UserRepoGroupToPerm.permission),)
2197 2197
2198 2198 # get owners and admins and permissions. We do a trick of re-writing
2199 2199 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2200 2200 # has a global reference and changing one object propagates to all
2201 2201 # others. This means if admin is also an owner admin_row that change
2202 2202 # would propagate to both objects
2203 2203 perm_rows = []
2204 2204 for _usr in q.all():
2205 2205 usr = AttributeDict(_usr.user.get_dict())
2206 2206 usr.permission = _usr.permission.permission_name
2207 2207 perm_rows.append(usr)
2208 2208
2209 2209 # filter the perm rows by 'default' first and then sort them by
2210 2210 # admin,write,read,none permissions sorted again alphabetically in
2211 2211 # each group
2212 2212 perm_rows = sorted(perm_rows, key=display_sort)
2213 2213
2214 2214 _admin_perm = 'group.admin'
2215 2215 owner_row = []
2216 2216 if with_owner:
2217 2217 usr = AttributeDict(self.user.get_dict())
2218 2218 usr.owner_row = True
2219 2219 usr.permission = _admin_perm
2220 2220 owner_row.append(usr)
2221 2221
2222 2222 super_admin_rows = []
2223 2223 if with_admins:
2224 2224 for usr in User.get_all_super_admins():
2225 2225 # if this admin is also owner, don't double the record
2226 2226 if usr.user_id == owner_row[0].user_id:
2227 2227 owner_row[0].admin_row = True
2228 2228 else:
2229 2229 usr = AttributeDict(usr.get_dict())
2230 2230 usr.admin_row = True
2231 2231 usr.permission = _admin_perm
2232 2232 super_admin_rows.append(usr)
2233 2233
2234 2234 return super_admin_rows + owner_row + perm_rows
2235 2235
2236 2236 def permission_user_groups(self):
2237 2237 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2238 2238 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2239 2239 joinedload(UserGroupRepoGroupToPerm.users_group),
2240 2240 joinedload(UserGroupRepoGroupToPerm.permission),)
2241 2241
2242 2242 perm_rows = []
2243 2243 for _user_group in q.all():
2244 2244 usr = AttributeDict(_user_group.users_group.get_dict())
2245 2245 usr.permission = _user_group.permission.permission_name
2246 2246 perm_rows.append(usr)
2247 2247
2248 2248 return perm_rows
2249 2249
2250 2250 def get_api_data(self):
2251 2251 """
2252 2252 Common function for generating api data
2253 2253
2254 2254 """
2255 2255 group = self
2256 2256 data = {
2257 2257 'group_id': group.group_id,
2258 2258 'group_name': group.group_name,
2259 2259 'group_description': group.group_description,
2260 2260 'parent_group': group.parent_group.group_name if group.parent_group else None,
2261 2261 'repositories': [x.repo_name for x in group.repositories],
2262 2262 'owner': group.user.username,
2263 2263 }
2264 2264 return data
2265 2265
2266 2266
2267 2267 class Permission(Base, BaseModel):
2268 2268 __tablename__ = 'permissions'
2269 2269 __table_args__ = (
2270 2270 Index('p_perm_name_idx', 'permission_name'),
2271 2271 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2272 2272 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2273 2273 )
2274 2274 PERMS = [
2275 2275 ('hg.admin', _('RhodeCode Super Administrator')),
2276 2276
2277 2277 ('repository.none', _('Repository no access')),
2278 2278 ('repository.read', _('Repository read access')),
2279 2279 ('repository.write', _('Repository write access')),
2280 2280 ('repository.admin', _('Repository admin access')),
2281 2281
2282 2282 ('group.none', _('Repository group no access')),
2283 2283 ('group.read', _('Repository group read access')),
2284 2284 ('group.write', _('Repository group write access')),
2285 2285 ('group.admin', _('Repository group admin access')),
2286 2286
2287 2287 ('usergroup.none', _('User group no access')),
2288 2288 ('usergroup.read', _('User group read access')),
2289 2289 ('usergroup.write', _('User group write access')),
2290 2290 ('usergroup.admin', _('User group admin access')),
2291 2291
2292 2292 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2293 2293 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2294 2294
2295 2295 ('hg.usergroup.create.false', _('User Group creation disabled')),
2296 2296 ('hg.usergroup.create.true', _('User Group creation enabled')),
2297 2297
2298 2298 ('hg.create.none', _('Repository creation disabled')),
2299 2299 ('hg.create.repository', _('Repository creation enabled')),
2300 2300 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2301 2301 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2302 2302
2303 2303 ('hg.fork.none', _('Repository forking disabled')),
2304 2304 ('hg.fork.repository', _('Repository forking enabled')),
2305 2305
2306 2306 ('hg.register.none', _('Registration disabled')),
2307 2307 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2308 2308 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2309 2309
2310 2310 ('hg.extern_activate.manual', _('Manual activation of external account')),
2311 2311 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2312 2312
2313 2313 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2314 2314 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2315 2315 ]
2316 2316
2317 2317 # definition of system default permissions for DEFAULT user
2318 2318 DEFAULT_USER_PERMISSIONS = [
2319 2319 'repository.read',
2320 2320 'group.read',
2321 2321 'usergroup.read',
2322 2322 'hg.create.repository',
2323 2323 'hg.repogroup.create.false',
2324 2324 'hg.usergroup.create.false',
2325 2325 'hg.create.write_on_repogroup.true',
2326 2326 'hg.fork.repository',
2327 2327 'hg.register.manual_activate',
2328 2328 'hg.extern_activate.auto',
2329 2329 'hg.inherit_default_perms.true',
2330 2330 ]
2331 2331
2332 2332 # defines which permissions are more important higher the more important
2333 2333 # Weight defines which permissions are more important.
2334 2334 # The higher number the more important.
2335 2335 PERM_WEIGHTS = {
2336 2336 'repository.none': 0,
2337 2337 'repository.read': 1,
2338 2338 'repository.write': 3,
2339 2339 'repository.admin': 4,
2340 2340
2341 2341 'group.none': 0,
2342 2342 'group.read': 1,
2343 2343 'group.write': 3,
2344 2344 'group.admin': 4,
2345 2345
2346 2346 'usergroup.none': 0,
2347 2347 'usergroup.read': 1,
2348 2348 'usergroup.write': 3,
2349 2349 'usergroup.admin': 4,
2350 2350
2351 2351 'hg.repogroup.create.false': 0,
2352 2352 'hg.repogroup.create.true': 1,
2353 2353
2354 2354 'hg.usergroup.create.false': 0,
2355 2355 'hg.usergroup.create.true': 1,
2356 2356
2357 2357 'hg.fork.none': 0,
2358 2358 'hg.fork.repository': 1,
2359 2359 'hg.create.none': 0,
2360 2360 'hg.create.repository': 1
2361 2361 }
2362 2362
2363 2363 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2364 2364 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2365 2365 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2366 2366
2367 2367 def __unicode__(self):
2368 2368 return u"<%s('%s:%s')>" % (
2369 2369 self.__class__.__name__, self.permission_id, self.permission_name
2370 2370 )
2371 2371
2372 2372 @classmethod
2373 2373 def get_by_key(cls, key):
2374 2374 return cls.query().filter(cls.permission_name == key).scalar()
2375 2375
2376 2376 @classmethod
2377 2377 def get_default_repo_perms(cls, user_id, repo_id=None):
2378 2378 q = Session().query(UserRepoToPerm, Repository, Permission)\
2379 2379 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2380 2380 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2381 2381 .filter(UserRepoToPerm.user_id == user_id)
2382 2382 if repo_id:
2383 2383 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2384 2384 return q.all()
2385 2385
2386 2386 @classmethod
2387 2387 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2388 2388 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2389 2389 .join(
2390 2390 Permission,
2391 2391 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2392 2392 .join(
2393 2393 Repository,
2394 2394 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2395 2395 .join(
2396 2396 UserGroup,
2397 2397 UserGroupRepoToPerm.users_group_id ==
2398 2398 UserGroup.users_group_id)\
2399 2399 .join(
2400 2400 UserGroupMember,
2401 2401 UserGroupRepoToPerm.users_group_id ==
2402 2402 UserGroupMember.users_group_id)\
2403 2403 .filter(
2404 2404 UserGroupMember.user_id == user_id,
2405 2405 UserGroup.users_group_active == true())
2406 2406 if repo_id:
2407 2407 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2408 2408 return q.all()
2409 2409
2410 2410 @classmethod
2411 2411 def get_default_group_perms(cls, user_id, repo_group_id=None):
2412 2412 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2413 2413 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2414 2414 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2415 2415 .filter(UserRepoGroupToPerm.user_id == user_id)
2416 2416 if repo_group_id:
2417 2417 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2418 2418 return q.all()
2419 2419
2420 2420 @classmethod
2421 2421 def get_default_group_perms_from_user_group(
2422 2422 cls, user_id, repo_group_id=None):
2423 2423 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2424 2424 .join(
2425 2425 Permission,
2426 2426 UserGroupRepoGroupToPerm.permission_id ==
2427 2427 Permission.permission_id)\
2428 2428 .join(
2429 2429 RepoGroup,
2430 2430 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2431 2431 .join(
2432 2432 UserGroup,
2433 2433 UserGroupRepoGroupToPerm.users_group_id ==
2434 2434 UserGroup.users_group_id)\
2435 2435 .join(
2436 2436 UserGroupMember,
2437 2437 UserGroupRepoGroupToPerm.users_group_id ==
2438 2438 UserGroupMember.users_group_id)\
2439 2439 .filter(
2440 2440 UserGroupMember.user_id == user_id,
2441 2441 UserGroup.users_group_active == true())
2442 2442 if repo_group_id:
2443 2443 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2444 2444 return q.all()
2445 2445
2446 2446 @classmethod
2447 2447 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2448 2448 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2449 2449 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2450 2450 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2451 2451 .filter(UserUserGroupToPerm.user_id == user_id)
2452 2452 if user_group_id:
2453 2453 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2454 2454 return q.all()
2455 2455
2456 2456 @classmethod
2457 2457 def get_default_user_group_perms_from_user_group(
2458 2458 cls, user_id, user_group_id=None):
2459 2459 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2460 2460 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2461 2461 .join(
2462 2462 Permission,
2463 2463 UserGroupUserGroupToPerm.permission_id ==
2464 2464 Permission.permission_id)\
2465 2465 .join(
2466 2466 TargetUserGroup,
2467 2467 UserGroupUserGroupToPerm.target_user_group_id ==
2468 2468 TargetUserGroup.users_group_id)\
2469 2469 .join(
2470 2470 UserGroup,
2471 2471 UserGroupUserGroupToPerm.user_group_id ==
2472 2472 UserGroup.users_group_id)\
2473 2473 .join(
2474 2474 UserGroupMember,
2475 2475 UserGroupUserGroupToPerm.user_group_id ==
2476 2476 UserGroupMember.users_group_id)\
2477 2477 .filter(
2478 2478 UserGroupMember.user_id == user_id,
2479 2479 UserGroup.users_group_active == true())
2480 2480 if user_group_id:
2481 2481 q = q.filter(
2482 2482 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2483 2483
2484 2484 return q.all()
2485 2485
2486 2486
2487 2487 class UserRepoToPerm(Base, BaseModel):
2488 2488 __tablename__ = 'repo_to_perm'
2489 2489 __table_args__ = (
2490 2490 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2491 2491 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2492 2492 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2493 2493 )
2494 2494 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2495 2495 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2496 2496 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2497 2497 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2498 2498
2499 2499 user = relationship('User')
2500 2500 repository = relationship('Repository')
2501 2501 permission = relationship('Permission')
2502 2502
2503 2503 @classmethod
2504 2504 def create(cls, user, repository, permission):
2505 2505 n = cls()
2506 2506 n.user = user
2507 2507 n.repository = repository
2508 2508 n.permission = permission
2509 2509 Session().add(n)
2510 2510 return n
2511 2511
2512 2512 def __unicode__(self):
2513 2513 return u'<%s => %s >' % (self.user, self.repository)
2514 2514
2515 2515
2516 2516 class UserUserGroupToPerm(Base, BaseModel):
2517 2517 __tablename__ = 'user_user_group_to_perm'
2518 2518 __table_args__ = (
2519 2519 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2520 2520 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2521 2521 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2522 2522 )
2523 2523 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2524 2524 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2525 2525 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2526 2526 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2527 2527
2528 2528 user = relationship('User')
2529 2529 user_group = relationship('UserGroup')
2530 2530 permission = relationship('Permission')
2531 2531
2532 2532 @classmethod
2533 2533 def create(cls, user, user_group, permission):
2534 2534 n = cls()
2535 2535 n.user = user
2536 2536 n.user_group = user_group
2537 2537 n.permission = permission
2538 2538 Session().add(n)
2539 2539 return n
2540 2540
2541 2541 def __unicode__(self):
2542 2542 return u'<%s => %s >' % (self.user, self.user_group)
2543 2543
2544 2544
2545 2545 class UserToPerm(Base, BaseModel):
2546 2546 __tablename__ = 'user_to_perm'
2547 2547 __table_args__ = (
2548 2548 UniqueConstraint('user_id', 'permission_id'),
2549 2549 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2550 2550 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2551 2551 )
2552 2552 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2553 2553 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2554 2554 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2555 2555
2556 2556 user = relationship('User')
2557 2557 permission = relationship('Permission', lazy='joined')
2558 2558
2559 2559 def __unicode__(self):
2560 2560 return u'<%s => %s >' % (self.user, self.permission)
2561 2561
2562 2562
2563 2563 class UserGroupRepoToPerm(Base, BaseModel):
2564 2564 __tablename__ = 'users_group_repo_to_perm'
2565 2565 __table_args__ = (
2566 2566 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2567 2567 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2568 2568 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2569 2569 )
2570 2570 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2571 2571 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2572 2572 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2573 2573 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2574 2574
2575 2575 users_group = relationship('UserGroup')
2576 2576 permission = relationship('Permission')
2577 2577 repository = relationship('Repository')
2578 2578
2579 2579 @classmethod
2580 2580 def create(cls, users_group, repository, permission):
2581 2581 n = cls()
2582 2582 n.users_group = users_group
2583 2583 n.repository = repository
2584 2584 n.permission = permission
2585 2585 Session().add(n)
2586 2586 return n
2587 2587
2588 2588 def __unicode__(self):
2589 2589 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2590 2590
2591 2591
2592 2592 class UserGroupUserGroupToPerm(Base, BaseModel):
2593 2593 __tablename__ = 'user_group_user_group_to_perm'
2594 2594 __table_args__ = (
2595 2595 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2596 2596 CheckConstraint('target_user_group_id != user_group_id'),
2597 2597 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2598 2598 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2599 2599 )
2600 2600 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)
2601 2601 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2602 2602 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2603 2603 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2604 2604
2605 2605 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2606 2606 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2607 2607 permission = relationship('Permission')
2608 2608
2609 2609 @classmethod
2610 2610 def create(cls, target_user_group, user_group, permission):
2611 2611 n = cls()
2612 2612 n.target_user_group = target_user_group
2613 2613 n.user_group = user_group
2614 2614 n.permission = permission
2615 2615 Session().add(n)
2616 2616 return n
2617 2617
2618 2618 def __unicode__(self):
2619 2619 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2620 2620
2621 2621
2622 2622 class UserGroupToPerm(Base, BaseModel):
2623 2623 __tablename__ = 'users_group_to_perm'
2624 2624 __table_args__ = (
2625 2625 UniqueConstraint('users_group_id', 'permission_id',),
2626 2626 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2627 2627 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2628 2628 )
2629 2629 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2630 2630 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2631 2631 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2632 2632
2633 2633 users_group = relationship('UserGroup')
2634 2634 permission = relationship('Permission')
2635 2635
2636 2636
2637 2637 class UserRepoGroupToPerm(Base, BaseModel):
2638 2638 __tablename__ = 'user_repo_group_to_perm'
2639 2639 __table_args__ = (
2640 2640 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2641 2641 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2642 2642 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2643 2643 )
2644 2644
2645 2645 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2646 2646 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2647 2647 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2648 2648 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2649 2649
2650 2650 user = relationship('User')
2651 2651 group = relationship('RepoGroup')
2652 2652 permission = relationship('Permission')
2653 2653
2654 2654 @classmethod
2655 2655 def create(cls, user, repository_group, permission):
2656 2656 n = cls()
2657 2657 n.user = user
2658 2658 n.group = repository_group
2659 2659 n.permission = permission
2660 2660 Session().add(n)
2661 2661 return n
2662 2662
2663 2663
2664 2664 class UserGroupRepoGroupToPerm(Base, BaseModel):
2665 2665 __tablename__ = 'users_group_repo_group_to_perm'
2666 2666 __table_args__ = (
2667 2667 UniqueConstraint('users_group_id', 'group_id'),
2668 2668 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2669 2669 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2670 2670 )
2671 2671
2672 2672 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)
2673 2673 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2674 2674 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2675 2675 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2676 2676
2677 2677 users_group = relationship('UserGroup')
2678 2678 permission = relationship('Permission')
2679 2679 group = relationship('RepoGroup')
2680 2680
2681 2681 @classmethod
2682 2682 def create(cls, user_group, repository_group, permission):
2683 2683 n = cls()
2684 2684 n.users_group = user_group
2685 2685 n.group = repository_group
2686 2686 n.permission = permission
2687 2687 Session().add(n)
2688 2688 return n
2689 2689
2690 2690 def __unicode__(self):
2691 2691 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2692 2692
2693 2693
2694 2694 class Statistics(Base, BaseModel):
2695 2695 __tablename__ = 'statistics'
2696 2696 __table_args__ = (
2697 2697 UniqueConstraint('repository_id'),
2698 2698 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2699 2699 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2700 2700 )
2701 2701 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2702 2702 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2703 2703 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2704 2704 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2705 2705 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2706 2706 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2707 2707
2708 2708 repository = relationship('Repository', single_parent=True)
2709 2709
2710 2710
2711 2711 class UserFollowing(Base, BaseModel):
2712 2712 __tablename__ = 'user_followings'
2713 2713 __table_args__ = (
2714 2714 UniqueConstraint('user_id', 'follows_repository_id'),
2715 2715 UniqueConstraint('user_id', 'follows_user_id'),
2716 2716 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2717 2717 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2718 2718 )
2719 2719
2720 2720 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2721 2721 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2722 2722 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2723 2723 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2724 2724 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2725 2725
2726 2726 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2727 2727
2728 2728 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2729 2729 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2730 2730
2731 2731 @classmethod
2732 2732 def get_repo_followers(cls, repo_id):
2733 2733 return cls.query().filter(cls.follows_repo_id == repo_id)
2734 2734
2735 2735
2736 2736 class CacheKey(Base, BaseModel):
2737 2737 __tablename__ = 'cache_invalidation'
2738 2738 __table_args__ = (
2739 2739 UniqueConstraint('cache_key'),
2740 2740 Index('key_idx', 'cache_key'),
2741 2741 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2742 2742 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2743 2743 )
2744 2744 CACHE_TYPE_ATOM = 'ATOM'
2745 2745 CACHE_TYPE_RSS = 'RSS'
2746 2746 CACHE_TYPE_README = 'README'
2747 2747
2748 2748 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2749 2749 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2750 2750 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2751 2751 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2752 2752
2753 2753 def __init__(self, cache_key, cache_args=''):
2754 2754 self.cache_key = cache_key
2755 2755 self.cache_args = cache_args
2756 2756 self.cache_active = False
2757 2757
2758 2758 def __unicode__(self):
2759 2759 return u"<%s('%s:%s[%s]')>" % (
2760 2760 self.__class__.__name__,
2761 2761 self.cache_id, self.cache_key, self.cache_active)
2762 2762
2763 2763 def _cache_key_partition(self):
2764 2764 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2765 2765 return prefix, repo_name, suffix
2766 2766
2767 2767 def get_prefix(self):
2768 2768 """
2769 2769 Try to extract prefix from existing cache key. The key could consist
2770 2770 of prefix, repo_name, suffix
2771 2771 """
2772 2772 # this returns prefix, repo_name, suffix
2773 2773 return self._cache_key_partition()[0]
2774 2774
2775 2775 def get_suffix(self):
2776 2776 """
2777 2777 get suffix that might have been used in _get_cache_key to
2778 2778 generate self.cache_key. Only used for informational purposes
2779 2779 in repo_edit.html.
2780 2780 """
2781 2781 # prefix, repo_name, suffix
2782 2782 return self._cache_key_partition()[2]
2783 2783
2784 2784 @classmethod
2785 2785 def delete_all_cache(cls):
2786 2786 """
2787 2787 Delete all cache keys from database.
2788 2788 Should only be run when all instances are down and all entries
2789 2789 thus stale.
2790 2790 """
2791 2791 cls.query().delete()
2792 2792 Session().commit()
2793 2793
2794 2794 @classmethod
2795 2795 def get_cache_key(cls, repo_name, cache_type):
2796 2796 """
2797 2797
2798 2798 Generate a cache key for this process of RhodeCode instance.
2799 2799 Prefix most likely will be process id or maybe explicitly set
2800 2800 instance_id from .ini file.
2801 2801 """
2802 2802 import rhodecode
2803 2803 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2804 2804
2805 2805 repo_as_unicode = safe_unicode(repo_name)
2806 2806 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2807 2807 if cache_type else repo_as_unicode
2808 2808
2809 2809 return u'{}{}'.format(prefix, key)
2810 2810
2811 2811 @classmethod
2812 2812 def set_invalidate(cls, repo_name, delete=False):
2813 2813 """
2814 2814 Mark all caches of a repo as invalid in the database.
2815 2815 """
2816 2816
2817 2817 try:
2818 2818 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2819 2819 if delete:
2820 2820 log.debug('cache objects deleted for repo %s',
2821 2821 safe_str(repo_name))
2822 2822 qry.delete()
2823 2823 else:
2824 2824 log.debug('cache objects marked as invalid for repo %s',
2825 2825 safe_str(repo_name))
2826 2826 qry.update({"cache_active": False})
2827 2827
2828 2828 Session().commit()
2829 2829 except Exception:
2830 2830 log.exception(
2831 2831 'Cache key invalidation failed for repository %s',
2832 2832 safe_str(repo_name))
2833 2833 Session().rollback()
2834 2834
2835 2835 @classmethod
2836 2836 def get_active_cache(cls, cache_key):
2837 2837 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2838 2838 if inv_obj:
2839 2839 return inv_obj
2840 2840 return None
2841 2841
2842 2842 @classmethod
2843 2843 def repo_context_cache(cls, compute_func, repo_name, cache_type):
2844 2844 """
2845 2845 @cache_region('long_term')
2846 2846 def _heavy_calculation(cache_key):
2847 2847 return 'result'
2848 2848
2849 2849 cache_context = CacheKey.repo_context_cache(
2850 2850 _heavy_calculation, repo_name, cache_type)
2851 2851
2852 2852 with cache_context as context:
2853 2853 context.invalidate()
2854 2854 computed = context.compute()
2855 2855
2856 2856 assert computed == 'result'
2857 2857 """
2858 2858 from rhodecode.lib import caches
2859 2859 return caches.InvalidationContext(compute_func, repo_name, cache_type)
2860 2860
2861 2861
2862 2862 class ChangesetComment(Base, BaseModel):
2863 2863 __tablename__ = 'changeset_comments'
2864 2864 __table_args__ = (
2865 2865 Index('cc_revision_idx', 'revision'),
2866 2866 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2867 2867 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2868 2868 )
2869 2869
2870 2870 COMMENT_OUTDATED = u'comment_outdated'
2871 2871
2872 2872 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2873 2873 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2874 2874 revision = Column('revision', String(40), nullable=True)
2875 2875 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2876 2876 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2877 2877 line_no = Column('line_no', Unicode(10), nullable=True)
2878 2878 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2879 2879 f_path = Column('f_path', Unicode(1000), nullable=True)
2880 2880 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2881 2881 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2882 2882 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2883 2883 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2884 2884 renderer = Column('renderer', Unicode(64), nullable=True)
2885 2885 display_state = Column('display_state', Unicode(128), nullable=True)
2886 2886
2887 2887 author = relationship('User', lazy='joined')
2888 2888 repo = relationship('Repository')
2889 2889 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2890 2890 pull_request = relationship('PullRequest', lazy='joined')
2891 2891 pull_request_version = relationship('PullRequestVersion')
2892 2892
2893 2893 @classmethod
2894 2894 def get_users(cls, revision=None, pull_request_id=None):
2895 2895 """
2896 2896 Returns user associated with this ChangesetComment. ie those
2897 2897 who actually commented
2898 2898
2899 2899 :param cls:
2900 2900 :param revision:
2901 2901 """
2902 2902 q = Session().query(User)\
2903 2903 .join(ChangesetComment.author)
2904 2904 if revision:
2905 2905 q = q.filter(cls.revision == revision)
2906 2906 elif pull_request_id:
2907 2907 q = q.filter(cls.pull_request_id == pull_request_id)
2908 2908 return q.all()
2909 2909
2910 2910 def render(self, mentions=False):
2911 2911 from rhodecode.lib import helpers as h
2912 2912 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2913 2913
2914 2914 def __repr__(self):
2915 2915 if self.comment_id:
2916 2916 return '<DB:ChangesetComment #%s>' % self.comment_id
2917 2917 else:
2918 2918 return '<DB:ChangesetComment at %#x>' % id(self)
2919 2919
2920 2920
2921 2921 class ChangesetStatus(Base, BaseModel):
2922 2922 __tablename__ = 'changeset_statuses'
2923 2923 __table_args__ = (
2924 2924 Index('cs_revision_idx', 'revision'),
2925 2925 Index('cs_version_idx', 'version'),
2926 2926 UniqueConstraint('repo_id', 'revision', 'version'),
2927 2927 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2928 2928 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2929 2929 )
2930 2930 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2931 2931 STATUS_APPROVED = 'approved'
2932 2932 STATUS_REJECTED = 'rejected'
2933 2933 STATUS_UNDER_REVIEW = 'under_review'
2934 2934
2935 2935 STATUSES = [
2936 2936 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2937 2937 (STATUS_APPROVED, _("Approved")),
2938 2938 (STATUS_REJECTED, _("Rejected")),
2939 2939 (STATUS_UNDER_REVIEW, _("Under Review")),
2940 2940 ]
2941 2941
2942 2942 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
2943 2943 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2944 2944 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
2945 2945 revision = Column('revision', String(40), nullable=False)
2946 2946 status = Column('status', String(128), nullable=False, default=DEFAULT)
2947 2947 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
2948 2948 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
2949 2949 version = Column('version', Integer(), nullable=False, default=0)
2950 2950 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2951 2951
2952 2952 author = relationship('User', lazy='joined')
2953 2953 repo = relationship('Repository')
2954 2954 comment = relationship('ChangesetComment', lazy='joined')
2955 2955 pull_request = relationship('PullRequest', lazy='joined')
2956 2956
2957 2957 def __unicode__(self):
2958 2958 return u"<%s('%s[%s]:%s')>" % (
2959 2959 self.__class__.__name__,
2960 2960 self.status, self.version, self.author
2961 2961 )
2962 2962
2963 2963 @classmethod
2964 2964 def get_status_lbl(cls, value):
2965 2965 return dict(cls.STATUSES).get(value)
2966 2966
2967 2967 @property
2968 2968 def status_lbl(self):
2969 2969 return ChangesetStatus.get_status_lbl(self.status)
2970 2970
2971 2971
2972 2972 class _PullRequestBase(BaseModel):
2973 2973 """
2974 2974 Common attributes of pull request and version entries.
2975 2975 """
2976 2976
2977 2977 # .status values
2978 2978 STATUS_NEW = u'new'
2979 2979 STATUS_OPEN = u'open'
2980 2980 STATUS_CLOSED = u'closed'
2981 2981
2982 2982 title = Column('title', Unicode(255), nullable=True)
2983 2983 description = Column(
2984 2984 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
2985 2985 nullable=True)
2986 2986 # new/open/closed status of pull request (not approve/reject/etc)
2987 2987 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
2988 2988 created_on = Column(
2989 2989 'created_on', DateTime(timezone=False), nullable=False,
2990 2990 default=datetime.datetime.now)
2991 2991 updated_on = Column(
2992 2992 'updated_on', DateTime(timezone=False), nullable=False,
2993 2993 default=datetime.datetime.now)
2994 2994
2995 2995 @declared_attr
2996 2996 def user_id(cls):
2997 2997 return Column(
2998 2998 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
2999 2999 unique=None)
3000 3000
3001 3001 # 500 revisions max
3002 3002 _revisions = Column(
3003 3003 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3004 3004
3005 3005 @declared_attr
3006 3006 def source_repo_id(cls):
3007 3007 # TODO: dan: rename column to source_repo_id
3008 3008 return Column(
3009 3009 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3010 3010 nullable=False)
3011 3011
3012 3012 source_ref = Column('org_ref', Unicode(255), nullable=False)
3013 3013
3014 3014 @declared_attr
3015 3015 def target_repo_id(cls):
3016 3016 # TODO: dan: rename column to target_repo_id
3017 3017 return Column(
3018 3018 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3019 3019 nullable=False)
3020 3020
3021 3021 target_ref = Column('other_ref', Unicode(255), nullable=False)
3022 3022
3023 3023 # TODO: dan: rename column to last_merge_source_rev
3024 3024 _last_merge_source_rev = Column(
3025 3025 'last_merge_org_rev', String(40), nullable=True)
3026 3026 # TODO: dan: rename column to last_merge_target_rev
3027 3027 _last_merge_target_rev = Column(
3028 3028 'last_merge_other_rev', String(40), nullable=True)
3029 3029 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3030 3030 merge_rev = Column('merge_rev', String(40), nullable=True)
3031 3031
3032 3032 @hybrid_property
3033 3033 def revisions(self):
3034 3034 return self._revisions.split(':') if self._revisions else []
3035 3035
3036 3036 @revisions.setter
3037 3037 def revisions(self, val):
3038 3038 self._revisions = ':'.join(val)
3039 3039
3040 3040 @declared_attr
3041 3041 def author(cls):
3042 3042 return relationship('User', lazy='joined')
3043 3043
3044 3044 @declared_attr
3045 3045 def source_repo(cls):
3046 3046 return relationship(
3047 3047 'Repository',
3048 3048 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3049 3049
3050 3050 @property
3051 3051 def source_ref_parts(self):
3052 3052 refs = self.source_ref.split(':')
3053 3053 return Reference(refs[0], refs[1], refs[2])
3054 3054
3055 3055 @declared_attr
3056 3056 def target_repo(cls):
3057 3057 return relationship(
3058 3058 'Repository',
3059 3059 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3060 3060
3061 3061 @property
3062 3062 def target_ref_parts(self):
3063 3063 refs = self.target_ref.split(':')
3064 3064 return Reference(refs[0], refs[1], refs[2])
3065 3065
3066 3066
3067 3067 class PullRequest(Base, _PullRequestBase):
3068 3068 __tablename__ = 'pull_requests'
3069 3069 __table_args__ = (
3070 3070 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3071 3071 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3072 3072 )
3073 3073
3074 3074 pull_request_id = Column(
3075 3075 'pull_request_id', Integer(), nullable=False, primary_key=True)
3076 3076
3077 3077 def __repr__(self):
3078 3078 if self.pull_request_id:
3079 3079 return '<DB:PullRequest #%s>' % self.pull_request_id
3080 3080 else:
3081 3081 return '<DB:PullRequest at %#x>' % id(self)
3082 3082
3083 3083 reviewers = relationship('PullRequestReviewers',
3084 3084 cascade="all, delete, delete-orphan")
3085 3085 statuses = relationship('ChangesetStatus')
3086 3086 comments = relationship('ChangesetComment',
3087 3087 cascade="all, delete, delete-orphan")
3088 3088 versions = relationship('PullRequestVersion',
3089 3089 cascade="all, delete, delete-orphan")
3090 3090
3091 3091 def is_closed(self):
3092 3092 return self.status == self.STATUS_CLOSED
3093 3093
3094 3094 def get_api_data(self):
3095 3095 from rhodecode.model.pull_request import PullRequestModel
3096 3096 pull_request = self
3097 3097 merge_status = PullRequestModel().merge_status(pull_request)
3098 3098 data = {
3099 3099 'pull_request_id': pull_request.pull_request_id,
3100 3100 'url': url('pullrequest_show',
3101 3101 repo_name=pull_request.target_repo.repo_name,
3102 3102 pull_request_id=pull_request.pull_request_id,
3103 3103 qualified=True),
3104 3104 'title': pull_request.title,
3105 3105 'description': pull_request.description,
3106 3106 'status': pull_request.status,
3107 3107 'created_on': pull_request.created_on,
3108 3108 'updated_on': pull_request.updated_on,
3109 3109 'commit_ids': pull_request.revisions,
3110 3110 'review_status': pull_request.calculated_review_status(),
3111 3111 'mergeable': {
3112 3112 'status': merge_status[0],
3113 3113 'message': unicode(merge_status[1]),
3114 3114 },
3115 3115 'source': {
3116 3116 'clone_url': pull_request.source_repo.clone_url(),
3117 3117 'repository': pull_request.source_repo.repo_name,
3118 3118 'reference': {
3119 3119 'name': pull_request.source_ref_parts.name,
3120 3120 'type': pull_request.source_ref_parts.type,
3121 3121 'commit_id': pull_request.source_ref_parts.commit_id,
3122 3122 },
3123 3123 },
3124 3124 'target': {
3125 3125 'clone_url': pull_request.target_repo.clone_url(),
3126 3126 'repository': pull_request.target_repo.repo_name,
3127 3127 'reference': {
3128 3128 'name': pull_request.target_ref_parts.name,
3129 3129 'type': pull_request.target_ref_parts.type,
3130 3130 'commit_id': pull_request.target_ref_parts.commit_id,
3131 3131 },
3132 3132 },
3133 3133 'author': pull_request.author.get_api_data(include_secrets=False,
3134 3134 details='basic'),
3135 3135 'reviewers': [
3136 3136 {
3137 3137 'user': reviewer.get_api_data(include_secrets=False,
3138 3138 details='basic'),
3139 3139 'review_status': st[0][1].status if st else 'not_reviewed',
3140 3140 }
3141 3141 for reviewer, st in pull_request.reviewers_statuses()
3142 3142 ]
3143 3143 }
3144 3144
3145 3145 return data
3146 3146
3147 3147 def __json__(self):
3148 3148 return {
3149 3149 'revisions': self.revisions,
3150 3150 }
3151 3151
3152 3152 def calculated_review_status(self):
3153 3153 # TODO: anderson: 13.05.15 Used only on templates/my_account_pullrequests.html
3154 3154 # because it's tricky on how to use ChangesetStatusModel from there
3155 3155 warnings.warn("Use calculated_review_status from ChangesetStatusModel", DeprecationWarning)
3156 3156 from rhodecode.model.changeset_status import ChangesetStatusModel
3157 3157 return ChangesetStatusModel().calculated_review_status(self)
3158 3158
3159 3159 def reviewers_statuses(self):
3160 3160 warnings.warn("Use reviewers_statuses from ChangesetStatusModel", DeprecationWarning)
3161 3161 from rhodecode.model.changeset_status import ChangesetStatusModel
3162 3162 return ChangesetStatusModel().reviewers_statuses(self)
3163 3163
3164 3164
3165 3165 class PullRequestVersion(Base, _PullRequestBase):
3166 3166 __tablename__ = 'pull_request_versions'
3167 3167 __table_args__ = (
3168 3168 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3169 3169 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3170 3170 )
3171 3171
3172 3172 pull_request_version_id = Column(
3173 3173 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3174 3174 pull_request_id = Column(
3175 3175 'pull_request_id', Integer(),
3176 3176 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3177 3177 pull_request = relationship('PullRequest')
3178 3178
3179 3179 def __repr__(self):
3180 3180 if self.pull_request_version_id:
3181 3181 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3182 3182 else:
3183 3183 return '<DB:PullRequestVersion at %#x>' % id(self)
3184 3184
3185 3185
3186 3186 class PullRequestReviewers(Base, BaseModel):
3187 3187 __tablename__ = 'pull_request_reviewers'
3188 3188 __table_args__ = (
3189 3189 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3190 3190 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3191 3191 )
3192 3192
3193 3193 def __init__(self, user=None, pull_request=None):
3194 3194 self.user = user
3195 3195 self.pull_request = pull_request
3196 3196
3197 3197 pull_requests_reviewers_id = Column(
3198 3198 'pull_requests_reviewers_id', Integer(), nullable=False,
3199 3199 primary_key=True)
3200 3200 pull_request_id = Column(
3201 3201 "pull_request_id", Integer(),
3202 3202 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3203 3203 user_id = Column(
3204 3204 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3205 3205
3206 3206 user = relationship('User')
3207 3207 pull_request = relationship('PullRequest')
3208 3208
3209 3209
3210 3210 class Notification(Base, BaseModel):
3211 3211 __tablename__ = 'notifications'
3212 3212 __table_args__ = (
3213 3213 Index('notification_type_idx', 'type'),
3214 3214 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3215 3215 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3216 3216 )
3217 3217
3218 3218 TYPE_CHANGESET_COMMENT = u'cs_comment'
3219 3219 TYPE_MESSAGE = u'message'
3220 3220 TYPE_MENTION = u'mention'
3221 3221 TYPE_REGISTRATION = u'registration'
3222 3222 TYPE_PULL_REQUEST = u'pull_request'
3223 3223 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3224 3224
3225 3225 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3226 3226 subject = Column('subject', Unicode(512), nullable=True)
3227 3227 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3228 3228 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3229 3229 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3230 3230 type_ = Column('type', Unicode(255))
3231 3231
3232 3232 created_by_user = relationship('User')
3233 3233 notifications_to_users = relationship('UserNotification', lazy='joined',
3234 3234 cascade="all, delete, delete-orphan")
3235 3235
3236 3236 @property
3237 3237 def recipients(self):
3238 3238 return [x.user for x in UserNotification.query()\
3239 3239 .filter(UserNotification.notification == self)\
3240 3240 .order_by(UserNotification.user_id.asc()).all()]
3241 3241
3242 3242 @classmethod
3243 3243 def create(cls, created_by, subject, body, recipients, type_=None):
3244 3244 if type_ is None:
3245 3245 type_ = Notification.TYPE_MESSAGE
3246 3246
3247 3247 notification = cls()
3248 3248 notification.created_by_user = created_by
3249 3249 notification.subject = subject
3250 3250 notification.body = body
3251 3251 notification.type_ = type_
3252 3252 notification.created_on = datetime.datetime.now()
3253 3253
3254 3254 for u in recipients:
3255 3255 assoc = UserNotification()
3256 3256 assoc.notification = notification
3257 3257
3258 3258 # if created_by is inside recipients mark his notification
3259 3259 # as read
3260 3260 if u.user_id == created_by.user_id:
3261 3261 assoc.read = True
3262 3262
3263 3263 u.notifications.append(assoc)
3264 3264 Session().add(notification)
3265 3265
3266 3266 return notification
3267 3267
3268 3268 @property
3269 3269 def description(self):
3270 3270 from rhodecode.model.notification import NotificationModel
3271 3271 return NotificationModel().make_description(self)
3272 3272
3273 3273
3274 3274 class UserNotification(Base, BaseModel):
3275 3275 __tablename__ = 'user_to_notification'
3276 3276 __table_args__ = (
3277 3277 UniqueConstraint('user_id', 'notification_id'),
3278 3278 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3279 3279 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3280 3280 )
3281 3281 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3282 3282 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3283 3283 read = Column('read', Boolean, default=False)
3284 3284 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3285 3285
3286 3286 user = relationship('User', lazy="joined")
3287 3287 notification = relationship('Notification', lazy="joined",
3288 3288 order_by=lambda: Notification.created_on.desc(),)
3289 3289
3290 3290 def mark_as_read(self):
3291 3291 self.read = True
3292 3292 Session().add(self)
3293 3293
3294 3294
3295 3295 class Gist(Base, BaseModel):
3296 3296 __tablename__ = 'gists'
3297 3297 __table_args__ = (
3298 3298 Index('g_gist_access_id_idx', 'gist_access_id'),
3299 3299 Index('g_created_on_idx', 'created_on'),
3300 3300 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3301 3301 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3302 3302 )
3303 3303 GIST_PUBLIC = u'public'
3304 3304 GIST_PRIVATE = u'private'
3305 3305 DEFAULT_FILENAME = u'gistfile1.txt'
3306 3306
3307 3307 ACL_LEVEL_PUBLIC = u'acl_public'
3308 3308 ACL_LEVEL_PRIVATE = u'acl_private'
3309 3309
3310 3310 gist_id = Column('gist_id', Integer(), primary_key=True)
3311 3311 gist_access_id = Column('gist_access_id', Unicode(250))
3312 3312 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3313 3313 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3314 3314 gist_expires = Column('gist_expires', Float(53), nullable=False)
3315 3315 gist_type = Column('gist_type', Unicode(128), nullable=False)
3316 3316 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3317 3317 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3318 3318 acl_level = Column('acl_level', Unicode(128), nullable=True)
3319 3319
3320 3320 owner = relationship('User')
3321 3321
3322 3322 def __repr__(self):
3323 3323 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3324 3324
3325 3325 @classmethod
3326 3326 def get_or_404(cls, id_):
3327 3327 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3328 3328 if not res:
3329 3329 raise HTTPNotFound
3330 3330 return res
3331 3331
3332 3332 @classmethod
3333 3333 def get_by_access_id(cls, gist_access_id):
3334 3334 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3335 3335
3336 3336 def gist_url(self):
3337 3337 import rhodecode
3338 3338 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3339 3339 if alias_url:
3340 3340 return alias_url.replace('{gistid}', self.gist_access_id)
3341 3341
3342 3342 return url('gist', gist_id=self.gist_access_id, qualified=True)
3343 3343
3344 3344 @classmethod
3345 3345 def base_path(cls):
3346 3346 """
3347 3347 Returns base path when all gists are stored
3348 3348
3349 3349 :param cls:
3350 3350 """
3351 3351 from rhodecode.model.gist import GIST_STORE_LOC
3352 3352 q = Session().query(RhodeCodeUi)\
3353 3353 .filter(RhodeCodeUi.ui_key == URL_SEP)
3354 3354 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3355 3355 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3356 3356
3357 3357 def get_api_data(self):
3358 3358 """
3359 3359 Common function for generating gist related data for API
3360 3360 """
3361 3361 gist = self
3362 3362 data = {
3363 3363 'gist_id': gist.gist_id,
3364 3364 'type': gist.gist_type,
3365 3365 'access_id': gist.gist_access_id,
3366 3366 'description': gist.gist_description,
3367 3367 'url': gist.gist_url(),
3368 3368 'expires': gist.gist_expires,
3369 3369 'created_on': gist.created_on,
3370 3370 'modified_at': gist.modified_at,
3371 3371 'content': None,
3372 3372 'acl_level': gist.acl_level,
3373 3373 }
3374 3374 return data
3375 3375
3376 3376 def __json__(self):
3377 3377 data = dict(
3378 3378 )
3379 3379 data.update(self.get_api_data())
3380 3380 return data
3381 3381 # SCM functions
3382 3382
3383 3383 def scm_instance(self, **kwargs):
3384 3384 from rhodecode.lib.vcs import get_repo
3385 3385 base_path = self.base_path()
3386 3386 return get_repo(os.path.join(*map(safe_str,
3387 3387 [base_path, self.gist_access_id])))
3388 3388
3389 3389
3390 3390 class DbMigrateVersion(Base, BaseModel):
3391 3391 __tablename__ = 'db_migrate_version'
3392 3392 __table_args__ = (
3393 3393 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3394 3394 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3395 3395 )
3396 3396 repository_id = Column('repository_id', String(250), primary_key=True)
3397 3397 repository_path = Column('repository_path', Text)
3398 3398 version = Column('version', Integer)
3399 3399
3400 3400
3401 3401 class ExternalIdentity(Base, BaseModel):
3402 3402 __tablename__ = 'external_identities'
3403 3403 __table_args__ = (
3404 3404 Index('local_user_id_idx', 'local_user_id'),
3405 3405 Index('external_id_idx', 'external_id'),
3406 3406 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3407 3407 'mysql_charset': 'utf8'})
3408 3408
3409 3409 external_id = Column('external_id', Unicode(255), default=u'',
3410 3410 primary_key=True)
3411 3411 external_username = Column('external_username', Unicode(1024), default=u'')
3412 3412 local_user_id = Column('local_user_id', Integer(),
3413 3413 ForeignKey('users.user_id'), primary_key=True)
3414 3414 provider_name = Column('provider_name', Unicode(255), default=u'',
3415 3415 primary_key=True)
3416 3416 access_token = Column('access_token', String(1024), default=u'')
3417 3417 alt_token = Column('alt_token', String(1024), default=u'')
3418 3418 token_secret = Column('token_secret', String(1024), default=u'')
3419 3419
3420 3420 @classmethod
3421 3421 def by_external_id_and_provider(cls, external_id, provider_name,
3422 3422 local_user_id=None):
3423 3423 """
3424 3424 Returns ExternalIdentity instance based on search params
3425 3425
3426 3426 :param external_id:
3427 3427 :param provider_name:
3428 3428 :return: ExternalIdentity
3429 3429 """
3430 3430 query = cls.query()
3431 3431 query = query.filter(cls.external_id == external_id)
3432 3432 query = query.filter(cls.provider_name == provider_name)
3433 3433 if local_user_id:
3434 3434 query = query.filter(cls.local_user_id == local_user_id)
3435 3435 return query.first()
3436 3436
3437 3437 @classmethod
3438 3438 def user_by_external_id_and_provider(cls, external_id, provider_name):
3439 3439 """
3440 3440 Returns User instance based on search params
3441 3441
3442 3442 :param external_id:
3443 3443 :param provider_name:
3444 3444 :return: User
3445 3445 """
3446 3446 query = User.query()
3447 3447 query = query.filter(cls.external_id == external_id)
3448 3448 query = query.filter(cls.provider_name == provider_name)
3449 3449 query = query.filter(User.user_id == cls.local_user_id)
3450 3450 return query.first()
3451 3451
3452 3452 @classmethod
3453 3453 def by_local_user_id(cls, local_user_id):
3454 3454 """
3455 3455 Returns all tokens for user
3456 3456
3457 3457 :param local_user_id:
3458 3458 :return: ExternalIdentity
3459 3459 """
3460 3460 query = cls.query()
3461 3461 query = query.filter(cls.local_user_id == local_user_id)
3462 3462 return query
@@ -1,924 +1,924 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 Repository model for rhodecode
23 23 """
24 24
25 25 import logging
26 26 import os
27 27 import re
28 28 import shutil
29 29 import time
30 30 import traceback
31 31 from datetime import datetime
32 32
33 33 from sqlalchemy.sql import func
34 34 from sqlalchemy.sql.expression import true, or_
35 35 from zope.cachedescriptors.property import Lazy as LazyProperty
36 36
37 37 from rhodecode.lib import helpers as h
38 38 from rhodecode.lib.auth import HasUserGroupPermissionAny
39 39 from rhodecode.lib.caching_query import FromCache
40 40 from rhodecode.lib.exceptions import AttachedForksError
41 41 from rhodecode.lib.hooks_base import log_delete_repository
42 42 from rhodecode.lib.utils import make_db_config
43 43 from rhodecode.lib.utils2 import (
44 44 safe_str, safe_unicode, remove_prefix, obfuscate_url_pw,
45 45 get_current_rhodecode_user, safe_int, datetime_to_time, action_logger_generic)
46 46 from rhodecode.lib.vcs.backends import get_backend
47 47 from rhodecode.model import BaseModel
48 48 from rhodecode.model.db import (
49 49 Repository, UserRepoToPerm, UserGroupRepoToPerm, UserRepoGroupToPerm,
50 50 UserGroupRepoGroupToPerm, User, Permission, Statistics, UserGroup,
51 51 RepoGroup, RepositoryField)
52 52 from rhodecode.model.scm import UserGroupList
53 53 from rhodecode.model.settings import VcsSettingsModel
54 54
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 class RepoModel(BaseModel):
60 60
61 61 cls = Repository
62 62
63 63 def _get_user_group(self, users_group):
64 64 return self._get_instance(UserGroup, users_group,
65 65 callback=UserGroup.get_by_group_name)
66 66
67 67 def _get_repo_group(self, repo_group):
68 68 return self._get_instance(RepoGroup, repo_group,
69 69 callback=RepoGroup.get_by_group_name)
70 70
71 71 def _create_default_perms(self, repository, private):
72 72 # create default permission
73 73 default = 'repository.read'
74 74 def_user = User.get_default_user()
75 75 for p in def_user.user_perms:
76 76 if p.permission.permission_name.startswith('repository.'):
77 77 default = p.permission.permission_name
78 78 break
79 79
80 80 default_perm = 'repository.none' if private else default
81 81
82 82 repo_to_perm = UserRepoToPerm()
83 83 repo_to_perm.permission = Permission.get_by_key(default_perm)
84 84
85 85 repo_to_perm.repository = repository
86 86 repo_to_perm.user_id = def_user.user_id
87 87
88 88 return repo_to_perm
89 89
90 90 @LazyProperty
91 91 def repos_path(self):
92 92 """
93 93 Gets the repositories root path from database
94 94 """
95 95 settings_model = VcsSettingsModel(sa=self.sa)
96 96 return settings_model.get_repos_location()
97 97
98 98 def get(self, repo_id, cache=False):
99 99 repo = self.sa.query(Repository) \
100 100 .filter(Repository.repo_id == repo_id)
101 101
102 102 if cache:
103 103 repo = repo.options(FromCache("sql_cache_short",
104 104 "get_repo_%s" % repo_id))
105 105 return repo.scalar()
106 106
107 107 def get_repo(self, repository):
108 108 return self._get_repo(repository)
109 109
110 110 def get_by_repo_name(self, repo_name, cache=False):
111 111 repo = self.sa.query(Repository) \
112 112 .filter(Repository.repo_name == repo_name)
113 113
114 114 if cache:
115 115 repo = repo.options(FromCache("sql_cache_short",
116 116 "get_repo_%s" % repo_name))
117 117 return repo.scalar()
118 118
119 119 def _extract_id_from_repo_name(self, repo_name):
120 120 if repo_name.startswith('/'):
121 121 repo_name = repo_name.lstrip('/')
122 122 by_id_match = re.match(r'^_(\d{1,})', repo_name)
123 123 if by_id_match:
124 124 return by_id_match.groups()[0]
125 125
126 126 def get_repo_by_id(self, repo_name):
127 127 """
128 128 Extracts repo_name by id from special urls.
129 129 Example url is _11/repo_name
130 130
131 131 :param repo_name:
132 132 :return: repo object if matched else None
133 133 """
134 134 try:
135 135 _repo_id = self._extract_id_from_repo_name(repo_name)
136 136 if _repo_id:
137 137 return self.get(_repo_id)
138 138 except Exception:
139 139 log.exception('Failed to extract repo_name from URL')
140 140
141 141 return None
142 142
143 143 def get_users(self, name_contains=None, limit=20, only_active=True):
144 144 # TODO: mikhail: move this method to the UserModel.
145 145 query = self.sa.query(User)
146 146 if only_active:
147 147 query = query.filter(User.active == true())
148 148
149 149 if name_contains:
150 150 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
151 151 query = query.filter(
152 152 or_(
153 153 User.name.ilike(ilike_expression),
154 154 User.lastname.ilike(ilike_expression),
155 155 User.username.ilike(ilike_expression)
156 156 )
157 157 )
158 158 query = query.limit(limit)
159 159 users = query.all()
160 160
161 161 _users = [
162 162 {
163 163 'id': user.user_id,
164 164 'first_name': user.name,
165 165 'last_name': user.lastname,
166 166 'username': user.username,
167 167 'icon_link': h.gravatar_url(user.email, 14),
168 168 'value_display': h.person(user.email),
169 169 'value': user.username,
170 170 'value_type': 'user',
171 171 'active': user.active,
172 172 }
173 173 for user in users
174 174 ]
175 175 return _users
176 176
177 177 def get_user_groups(self, name_contains=None, limit=20, only_active=True):
178 178 # TODO: mikhail: move this method to the UserGroupModel.
179 179 query = self.sa.query(UserGroup)
180 180 if only_active:
181 181 query = query.filter(UserGroup.users_group_active == true())
182 182
183 183 if name_contains:
184 184 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
185 185 query = query.filter(
186 186 UserGroup.users_group_name.ilike(ilike_expression))\
187 187 .order_by(func.length(UserGroup.users_group_name))\
188 188 .order_by(UserGroup.users_group_name)
189 189
190 190 query = query.limit(limit)
191 191 user_groups = query.all()
192 192 perm_set = ['usergroup.read', 'usergroup.write', 'usergroup.admin']
193 193 user_groups = UserGroupList(user_groups, perm_set=perm_set)
194 194
195 195 _groups = [
196 196 {
197 197 'id': group.users_group_id,
198 198 # TODO: marcink figure out a way to generate the url for the
199 199 # icon
200 200 'icon_link': '',
201 201 'value_display': 'Group: %s (%d members)' % (
202 202 group.users_group_name, len(group.members),),
203 203 'value': group.users_group_name,
204 204 'value_type': 'user_group',
205 205 'active': group.users_group_active,
206 206 }
207 207 for group in user_groups
208 208 ]
209 209 return _groups
210 210
211 211 @classmethod
212 212 def update_repoinfo(cls, repositories=None):
213 213 if not repositories:
214 214 repositories = Repository.getAll()
215 215 for repo in repositories:
216 216 repo.update_commit_cache()
217 217
218 218 def get_repos_as_dict(self, repo_list=None, admin=False,
219 219 super_user_actions=False):
220 220
221 221 from rhodecode.lib.utils import PartialRenderer
222 222 _render = PartialRenderer('data_table/_dt_elements.html')
223 223 c = _render.c
224 224
225 225 def quick_menu(repo_name):
226 226 return _render('quick_menu', repo_name)
227 227
228 228 def repo_lnk(name, rtype, rstate, private, fork_of):
229 229 return _render('repo_name', name, rtype, rstate, private, fork_of,
230 230 short_name=not admin, admin=False)
231 231
232 232 def last_change(last_change):
233 233 return _render("last_change", last_change)
234 234
235 235 def rss_lnk(repo_name):
236 236 return _render("rss", repo_name)
237 237
238 238 def atom_lnk(repo_name):
239 239 return _render("atom", repo_name)
240 240
241 241 def last_rev(repo_name, cs_cache):
242 242 return _render('revision', repo_name, cs_cache.get('revision'),
243 243 cs_cache.get('raw_id'), cs_cache.get('author'),
244 244 cs_cache.get('message'))
245 245
246 246 def desc(desc):
247 247 if c.visual.stylify_metatags:
248 248 return h.urlify_text(h.escaped_stylize(h.truncate(desc, 60)))
249 249 else:
250 250 return h.urlify_text(h.html_escape(h.truncate(desc, 60)))
251 251
252 252 def state(repo_state):
253 253 return _render("repo_state", repo_state)
254 254
255 255 def repo_actions(repo_name):
256 256 return _render('repo_actions', repo_name, super_user_actions)
257 257
258 258 def user_profile(username):
259 259 return _render('user_profile', username)
260 260
261 261 repos_data = []
262 262 for repo in repo_list:
263 263 cs_cache = repo.changeset_cache
264 264 row = {
265 265 "menu": quick_menu(repo.repo_name),
266 266
267 267 "name": repo_lnk(repo.repo_name, repo.repo_type,
268 268 repo.repo_state, repo.private, repo.fork),
269 269 "name_raw": repo.repo_name.lower(),
270 270
271 271 "last_change": last_change(repo.last_db_change),
272 272 "last_change_raw": datetime_to_time(repo.last_db_change),
273 273
274 274 "last_changeset": last_rev(repo.repo_name, cs_cache),
275 275 "last_changeset_raw": cs_cache.get('revision'),
276 276
277 277 "desc": desc(repo.description),
278 278 "owner": user_profile(repo.user.username),
279 279
280 280 "state": state(repo.repo_state),
281 281 "rss": rss_lnk(repo.repo_name),
282 282
283 283 "atom": atom_lnk(repo.repo_name),
284 284 }
285 285 if admin:
286 286 row.update({
287 287 "action": repo_actions(repo.repo_name),
288 288 })
289 289 repos_data.append(row)
290 290
291 291 return repos_data
292 292
293 293 def _get_defaults(self, repo_name):
294 294 """
295 295 Gets information about repository, and returns a dict for
296 296 usage in forms
297 297
298 298 :param repo_name:
299 299 """
300 300
301 301 repo_info = Repository.get_by_repo_name(repo_name)
302 302
303 303 if repo_info is None:
304 304 return None
305 305
306 306 defaults = repo_info.get_dict()
307 307 defaults['repo_name'] = repo_info.just_name
308 308
309 309 groups = repo_info.groups_with_parents
310 310 parent_group = groups[-1] if groups else None
311 311
312 312 # we use -1 as this is how in HTML, we mark an empty group
313 313 defaults['repo_group'] = getattr(parent_group, 'group_id', -1)
314 314
315 315 keys_to_process = (
316 316 {'k': 'repo_type', 'strip': False},
317 317 {'k': 'repo_enable_downloads', 'strip': True},
318 318 {'k': 'repo_description', 'strip': True},
319 319 {'k': 'repo_enable_locking', 'strip': True},
320 320 {'k': 'repo_landing_rev', 'strip': True},
321 321 {'k': 'clone_uri', 'strip': False},
322 322 {'k': 'repo_private', 'strip': True},
323 323 {'k': 'repo_enable_statistics', 'strip': True}
324 324 )
325 325
326 326 for item in keys_to_process:
327 327 attr = item['k']
328 328 if item['strip']:
329 329 attr = remove_prefix(item['k'], 'repo_')
330 330
331 331 val = defaults[attr]
332 332 if item['k'] == 'repo_landing_rev':
333 333 val = ':'.join(defaults[attr])
334 334 defaults[item['k']] = val
335 335 if item['k'] == 'clone_uri':
336 336 defaults['clone_uri_hidden'] = repo_info.clone_uri_hidden
337 337
338 338 # fill owner
339 339 if repo_info.user:
340 340 defaults.update({'user': repo_info.user.username})
341 341 else:
342 replacement_user = User.get_first_admin().username
342 replacement_user = User.get_first_super_admin().username
343 343 defaults.update({'user': replacement_user})
344 344
345 345 # fill repository users
346 346 for p in repo_info.repo_to_perm:
347 347 defaults.update({'u_perm_%s' % p.user.user_id:
348 348 p.permission.permission_name})
349 349
350 350 # fill repository groups
351 351 for p in repo_info.users_group_to_perm:
352 352 defaults.update({'g_perm_%s' % p.users_group.users_group_id:
353 353 p.permission.permission_name})
354 354
355 355 return defaults
356 356
357 357 def update(self, repo, **kwargs):
358 358 try:
359 359 cur_repo = self._get_repo(repo)
360 360 source_repo_name = cur_repo.repo_name
361 361 if 'user' in kwargs:
362 362 cur_repo.user = User.get_by_username(kwargs['user'])
363 363
364 364 if 'repo_group' in kwargs:
365 365 cur_repo.group = RepoGroup.get(kwargs['repo_group'])
366 366 log.debug('Updating repo %s with params:%s', cur_repo, kwargs)
367 367
368 368 update_keys = [
369 369 (1, 'repo_enable_downloads'),
370 370 (1, 'repo_description'),
371 371 (1, 'repo_enable_locking'),
372 372 (1, 'repo_landing_rev'),
373 373 (1, 'repo_private'),
374 374 (1, 'repo_enable_statistics'),
375 375 (0, 'clone_uri'),
376 376 (0, 'fork_id')
377 377 ]
378 378 for strip, k in update_keys:
379 379 if k in kwargs:
380 380 val = kwargs[k]
381 381 if strip:
382 382 k = remove_prefix(k, 'repo_')
383 383 if k == 'clone_uri':
384 384 from rhodecode.model.validators import Missing
385 385 _change = kwargs.get('clone_uri_change')
386 386 if _change in [Missing, 'OLD']:
387 387 # we don't change the value, so use original one
388 388 val = cur_repo.clone_uri
389 389
390 390 setattr(cur_repo, k, val)
391 391
392 392 new_name = cur_repo.get_new_name(kwargs['repo_name'])
393 393 cur_repo.repo_name = new_name
394 394
395 395 # if private flag is set, reset default permission to NONE
396 396 if kwargs.get('repo_private'):
397 397 EMPTY_PERM = 'repository.none'
398 398 RepoModel().grant_user_permission(
399 399 repo=cur_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM
400 400 )
401 401
402 402 # handle extra fields
403 403 for field in filter(lambda k: k.startswith(RepositoryField.PREFIX),
404 404 kwargs):
405 405 k = RepositoryField.un_prefix_key(field)
406 406 ex_field = RepositoryField.get_by_key_name(
407 407 key=k, repo=cur_repo)
408 408 if ex_field:
409 409 ex_field.field_value = kwargs[field]
410 410 self.sa.add(ex_field)
411 411 self.sa.add(cur_repo)
412 412
413 413 if source_repo_name != new_name:
414 414 # rename repository
415 415 self._rename_filesystem_repo(
416 416 old=source_repo_name, new=new_name)
417 417
418 418 return cur_repo
419 419 except Exception:
420 420 log.error(traceback.format_exc())
421 421 raise
422 422
423 423 def _create_repo(self, repo_name, repo_type, description, owner,
424 424 private=False, clone_uri=None, repo_group=None,
425 425 landing_rev='rev:tip', fork_of=None,
426 426 copy_fork_permissions=False, enable_statistics=False,
427 427 enable_locking=False, enable_downloads=False,
428 428 copy_group_permissions=False,
429 429 state=Repository.STATE_PENDING):
430 430 """
431 431 Create repository inside database with PENDING state, this should be
432 432 only executed by create() repo. With exception of importing existing
433 433 repos
434 434 """
435 435 from rhodecode.model.scm import ScmModel
436 436
437 437 owner = self._get_user(owner)
438 438 fork_of = self._get_repo(fork_of)
439 439 repo_group = self._get_repo_group(safe_int(repo_group))
440 440
441 441 try:
442 442 repo_name = safe_unicode(repo_name)
443 443 description = safe_unicode(description)
444 444 # repo name is just a name of repository
445 445 # while repo_name_full is a full qualified name that is combined
446 446 # with name and path of group
447 447 repo_name_full = repo_name
448 448 repo_name = repo_name.split(Repository.NAME_SEP)[-1]
449 449
450 450 new_repo = Repository()
451 451 new_repo.repo_state = state
452 452 new_repo.enable_statistics = False
453 453 new_repo.repo_name = repo_name_full
454 454 new_repo.repo_type = repo_type
455 455 new_repo.user = owner
456 456 new_repo.group = repo_group
457 457 new_repo.description = description or repo_name
458 458 new_repo.private = private
459 459 new_repo.clone_uri = clone_uri
460 460 new_repo.landing_rev = landing_rev
461 461
462 462 new_repo.enable_statistics = enable_statistics
463 463 new_repo.enable_locking = enable_locking
464 464 new_repo.enable_downloads = enable_downloads
465 465
466 466 if repo_group:
467 467 new_repo.enable_locking = repo_group.enable_locking
468 468
469 469 if fork_of:
470 470 parent_repo = fork_of
471 471 new_repo.fork = parent_repo
472 472
473 473 self.sa.add(new_repo)
474 474
475 475 EMPTY_PERM = 'repository.none'
476 476 if fork_of and copy_fork_permissions:
477 477 repo = fork_of
478 478 user_perms = UserRepoToPerm.query() \
479 479 .filter(UserRepoToPerm.repository == repo).all()
480 480 group_perms = UserGroupRepoToPerm.query() \
481 481 .filter(UserGroupRepoToPerm.repository == repo).all()
482 482
483 483 for perm in user_perms:
484 484 UserRepoToPerm.create(
485 485 perm.user, new_repo, perm.permission)
486 486
487 487 for perm in group_perms:
488 488 UserGroupRepoToPerm.create(
489 489 perm.users_group, new_repo, perm.permission)
490 490 # in case we copy permissions and also set this repo to private
491 491 # override the default user permission to make it a private
492 492 # repo
493 493 if private:
494 494 RepoModel(self.sa).grant_user_permission(
495 495 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
496 496
497 497 elif repo_group and copy_group_permissions:
498 498 user_perms = UserRepoGroupToPerm.query() \
499 499 .filter(UserRepoGroupToPerm.group == repo_group).all()
500 500
501 501 group_perms = UserGroupRepoGroupToPerm.query() \
502 502 .filter(UserGroupRepoGroupToPerm.group == repo_group).all()
503 503
504 504 for perm in user_perms:
505 505 perm_name = perm.permission.permission_name.replace(
506 506 'group.', 'repository.')
507 507 perm_obj = Permission.get_by_key(perm_name)
508 508 UserRepoToPerm.create(perm.user, new_repo, perm_obj)
509 509
510 510 for perm in group_perms:
511 511 perm_name = perm.permission.permission_name.replace(
512 512 'group.', 'repository.')
513 513 perm_obj = Permission.get_by_key(perm_name)
514 514 UserGroupRepoToPerm.create(
515 515 perm.users_group, new_repo, perm_obj)
516 516
517 517 if private:
518 518 RepoModel(self.sa).grant_user_permission(
519 519 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
520 520
521 521 else:
522 522 perm_obj = self._create_default_perms(new_repo, private)
523 523 self.sa.add(perm_obj)
524 524
525 525 # now automatically start following this repository as owner
526 526 ScmModel(self.sa).toggle_following_repo(new_repo.repo_id,
527 527 owner.user_id)
528 528 # we need to flush here, in order to check if database won't
529 529 # throw any exceptions, create filesystem dirs at the very end
530 530 self.sa.flush()
531 531
532 532 return new_repo
533 533 except Exception:
534 534 log.error(traceback.format_exc())
535 535 raise
536 536
537 537 def create(self, form_data, cur_user):
538 538 """
539 539 Create repository using celery tasks
540 540
541 541 :param form_data:
542 542 :param cur_user:
543 543 """
544 544 from rhodecode.lib.celerylib import tasks, run_task
545 545 return run_task(tasks.create_repo, form_data, cur_user)
546 546
547 547 def update_permissions(self, repo, perm_additions=None, perm_updates=None,
548 548 perm_deletions=None, check_perms=True,
549 549 cur_user=None):
550 550 if not perm_additions:
551 551 perm_additions = []
552 552 if not perm_updates:
553 553 perm_updates = []
554 554 if not perm_deletions:
555 555 perm_deletions = []
556 556
557 557 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
558 558
559 559 # update permissions
560 560 for member_id, perm, member_type in perm_updates:
561 561 member_id = int(member_id)
562 562 if member_type == 'user':
563 563 # this updates also current one if found
564 564 self.grant_user_permission(
565 565 repo=repo, user=member_id, perm=perm)
566 566 else: # set for user group
567 567 # check if we have permissions to alter this usergroup
568 568 member_name = UserGroup.get(member_id).users_group_name
569 569 if not check_perms or HasUserGroupPermissionAny(
570 570 *req_perms)(member_name, user=cur_user):
571 571 self.grant_user_group_permission(
572 572 repo=repo, group_name=member_id, perm=perm)
573 573
574 574 # set new permissions
575 575 for member_id, perm, member_type in perm_additions:
576 576 member_id = int(member_id)
577 577 if member_type == 'user':
578 578 self.grant_user_permission(
579 579 repo=repo, user=member_id, perm=perm)
580 580 else: # set for user group
581 581 # check if we have permissions to alter this usergroup
582 582 member_name = UserGroup.get(member_id).users_group_name
583 583 if not check_perms or HasUserGroupPermissionAny(
584 584 *req_perms)(member_name, user=cur_user):
585 585 self.grant_user_group_permission(
586 586 repo=repo, group_name=member_id, perm=perm)
587 587
588 588 # delete permissions
589 589 for member_id, perm, member_type in perm_deletions:
590 590 member_id = int(member_id)
591 591 if member_type == 'user':
592 592 self.revoke_user_permission(repo=repo, user=member_id)
593 593 else: # set for user group
594 594 # check if we have permissions to alter this usergroup
595 595 member_name = UserGroup.get(member_id).users_group_name
596 596 if not check_perms or HasUserGroupPermissionAny(
597 597 *req_perms)(member_name, user=cur_user):
598 598 self.revoke_user_group_permission(
599 599 repo=repo, group_name=member_id)
600 600
601 601 def create_fork(self, form_data, cur_user):
602 602 """
603 603 Simple wrapper into executing celery task for fork creation
604 604
605 605 :param form_data:
606 606 :param cur_user:
607 607 """
608 608 from rhodecode.lib.celerylib import tasks, run_task
609 609 return run_task(tasks.create_repo_fork, form_data, cur_user)
610 610
611 611 def delete(self, repo, forks=None, fs_remove=True, cur_user=None):
612 612 """
613 613 Delete given repository, forks parameter defines what do do with
614 614 attached forks. Throws AttachedForksError if deleted repo has attached
615 615 forks
616 616
617 617 :param repo:
618 618 :param forks: str 'delete' or 'detach'
619 619 :param fs_remove: remove(archive) repo from filesystem
620 620 """
621 621 if not cur_user:
622 622 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
623 623 repo = self._get_repo(repo)
624 624 if repo:
625 625 if forks == 'detach':
626 626 for r in repo.forks:
627 627 r.fork = None
628 628 self.sa.add(r)
629 629 elif forks == 'delete':
630 630 for r in repo.forks:
631 631 self.delete(r, forks='delete')
632 632 elif [f for f in repo.forks]:
633 633 raise AttachedForksError()
634 634
635 635 old_repo_dict = repo.get_dict()
636 636 try:
637 637 self.sa.delete(repo)
638 638 if fs_remove:
639 639 self._delete_filesystem_repo(repo)
640 640 else:
641 641 log.debug('skipping removal from filesystem')
642 642 old_repo_dict.update({
643 643 'deleted_by': cur_user,
644 644 'deleted_on': time.time(),
645 645 })
646 646 log_delete_repository(**old_repo_dict)
647 647 except Exception:
648 648 log.error(traceback.format_exc())
649 649 raise
650 650
651 651 def grant_user_permission(self, repo, user, perm):
652 652 """
653 653 Grant permission for user on given repository, or update existing one
654 654 if found
655 655
656 656 :param repo: Instance of Repository, repository_id, or repository name
657 657 :param user: Instance of User, user_id or username
658 658 :param perm: Instance of Permission, or permission_name
659 659 """
660 660 user = self._get_user(user)
661 661 repo = self._get_repo(repo)
662 662 permission = self._get_perm(perm)
663 663
664 664 # check if we have that permission already
665 665 obj = self.sa.query(UserRepoToPerm) \
666 666 .filter(UserRepoToPerm.user == user) \
667 667 .filter(UserRepoToPerm.repository == repo) \
668 668 .scalar()
669 669 if obj is None:
670 670 # create new !
671 671 obj = UserRepoToPerm()
672 672 obj.repository = repo
673 673 obj.user = user
674 674 obj.permission = permission
675 675 self.sa.add(obj)
676 676 log.debug('Granted perm %s to %s on %s', perm, user, repo)
677 677 action_logger_generic(
678 678 'granted permission: {} to user: {} on repo: {}'.format(
679 679 perm, user, repo), namespace='security.repo')
680 680 return obj
681 681
682 682 def revoke_user_permission(self, repo, user):
683 683 """
684 684 Revoke permission for user on given repository
685 685
686 686 :param repo: Instance of Repository, repository_id, or repository name
687 687 :param user: Instance of User, user_id or username
688 688 """
689 689
690 690 user = self._get_user(user)
691 691 repo = self._get_repo(repo)
692 692
693 693 obj = self.sa.query(UserRepoToPerm) \
694 694 .filter(UserRepoToPerm.repository == repo) \
695 695 .filter(UserRepoToPerm.user == user) \
696 696 .scalar()
697 697 if obj:
698 698 self.sa.delete(obj)
699 699 log.debug('Revoked perm on %s on %s', repo, user)
700 700 action_logger_generic(
701 701 'revoked permission from user: {} on repo: {}'.format(
702 702 user, repo), namespace='security.repo')
703 703
704 704 def grant_user_group_permission(self, repo, group_name, perm):
705 705 """
706 706 Grant permission for user group on given repository, or update
707 707 existing one if found
708 708
709 709 :param repo: Instance of Repository, repository_id, or repository name
710 710 :param group_name: Instance of UserGroup, users_group_id,
711 711 or user group name
712 712 :param perm: Instance of Permission, or permission_name
713 713 """
714 714 repo = self._get_repo(repo)
715 715 group_name = self._get_user_group(group_name)
716 716 permission = self._get_perm(perm)
717 717
718 718 # check if we have that permission already
719 719 obj = self.sa.query(UserGroupRepoToPerm) \
720 720 .filter(UserGroupRepoToPerm.users_group == group_name) \
721 721 .filter(UserGroupRepoToPerm.repository == repo) \
722 722 .scalar()
723 723
724 724 if obj is None:
725 725 # create new
726 726 obj = UserGroupRepoToPerm()
727 727
728 728 obj.repository = repo
729 729 obj.users_group = group_name
730 730 obj.permission = permission
731 731 self.sa.add(obj)
732 732 log.debug('Granted perm %s to %s on %s', perm, group_name, repo)
733 733 action_logger_generic(
734 734 'granted permission: {} to usergroup: {} on repo: {}'.format(
735 735 perm, group_name, repo), namespace='security.repo')
736 736
737 737 return obj
738 738
739 739 def revoke_user_group_permission(self, repo, group_name):
740 740 """
741 741 Revoke permission for user group on given repository
742 742
743 743 :param repo: Instance of Repository, repository_id, or repository name
744 744 :param group_name: Instance of UserGroup, users_group_id,
745 745 or user group name
746 746 """
747 747 repo = self._get_repo(repo)
748 748 group_name = self._get_user_group(group_name)
749 749
750 750 obj = self.sa.query(UserGroupRepoToPerm) \
751 751 .filter(UserGroupRepoToPerm.repository == repo) \
752 752 .filter(UserGroupRepoToPerm.users_group == group_name) \
753 753 .scalar()
754 754 if obj:
755 755 self.sa.delete(obj)
756 756 log.debug('Revoked perm to %s on %s', repo, group_name)
757 757 action_logger_generic(
758 758 'revoked permission from usergroup: {} on repo: {}'.format(
759 759 group_name, repo), namespace='security.repo')
760 760
761 761 def delete_stats(self, repo_name):
762 762 """
763 763 removes stats for given repo
764 764
765 765 :param repo_name:
766 766 """
767 767 repo = self._get_repo(repo_name)
768 768 try:
769 769 obj = self.sa.query(Statistics) \
770 770 .filter(Statistics.repository == repo).scalar()
771 771 if obj:
772 772 self.sa.delete(obj)
773 773 except Exception:
774 774 log.error(traceback.format_exc())
775 775 raise
776 776
777 777 def add_repo_field(self, repo_name, field_key, field_label, field_value='',
778 778 field_type='str', field_desc=''):
779 779
780 780 repo = self._get_repo(repo_name)
781 781
782 782 new_field = RepositoryField()
783 783 new_field.repository = repo
784 784 new_field.field_key = field_key
785 785 new_field.field_type = field_type # python type
786 786 new_field.field_value = field_value
787 787 new_field.field_desc = field_desc
788 788 new_field.field_label = field_label
789 789 self.sa.add(new_field)
790 790 return new_field
791 791
792 792 def delete_repo_field(self, repo_name, field_key):
793 793 repo = self._get_repo(repo_name)
794 794 field = RepositoryField.get_by_key_name(field_key, repo)
795 795 if field:
796 796 self.sa.delete(field)
797 797
798 798 def _create_filesystem_repo(self, repo_name, repo_type, repo_group,
799 799 clone_uri=None, repo_store_location=None,
800 800 use_global_config=False):
801 801 """
802 802 makes repository on filesystem. It's group aware means it'll create
803 803 a repository within a group, and alter the paths accordingly of
804 804 group location
805 805
806 806 :param repo_name:
807 807 :param alias:
808 808 :param parent:
809 809 :param clone_uri:
810 810 :param repo_store_location:
811 811 """
812 812 from rhodecode.lib.utils import is_valid_repo, is_valid_repo_group
813 813 from rhodecode.model.scm import ScmModel
814 814
815 815 if Repository.NAME_SEP in repo_name:
816 816 raise ValueError(
817 817 'repo_name must not contain groups got `%s`' % repo_name)
818 818
819 819 if isinstance(repo_group, RepoGroup):
820 820 new_parent_path = os.sep.join(repo_group.full_path_splitted)
821 821 else:
822 822 new_parent_path = repo_group or ''
823 823
824 824 if repo_store_location:
825 825 _paths = [repo_store_location]
826 826 else:
827 827 _paths = [self.repos_path, new_parent_path, repo_name]
828 828 # we need to make it str for mercurial
829 829 repo_path = os.path.join(*map(lambda x: safe_str(x), _paths))
830 830
831 831 # check if this path is not a repository
832 832 if is_valid_repo(repo_path, self.repos_path):
833 833 raise Exception('This path %s is a valid repository' % repo_path)
834 834
835 835 # check if this path is a group
836 836 if is_valid_repo_group(repo_path, self.repos_path):
837 837 raise Exception('This path %s is a valid group' % repo_path)
838 838
839 839 log.info('creating repo %s in %s from url: `%s`',
840 840 repo_name, safe_unicode(repo_path),
841 841 obfuscate_url_pw(clone_uri))
842 842
843 843 backend = get_backend(repo_type)
844 844
845 845 config_repo = None if use_global_config else repo_name
846 846 if config_repo and new_parent_path:
847 847 config_repo = Repository.NAME_SEP.join(
848 848 (new_parent_path, config_repo))
849 849 config = make_db_config(clear_session=False, repo=config_repo)
850 850 config.set('extensions', 'largefiles', '')
851 851
852 852 # patch and reset hooks section of UI config to not run any
853 853 # hooks on creating remote repo
854 854 config.clear_section('hooks')
855 855
856 856 # TODO: johbo: Unify this, hardcoded "bare=True" does not look nice
857 857 if repo_type == 'git':
858 858 repo = backend(
859 859 repo_path, config=config, create=True, src_url=clone_uri,
860 860 bare=True)
861 861 else:
862 862 repo = backend(
863 863 repo_path, config=config, create=True, src_url=clone_uri)
864 864
865 865 ScmModel().install_hooks(repo, repo_type=repo_type)
866 866
867 867 log.debug('Created repo %s with %s backend',
868 868 safe_unicode(repo_name), safe_unicode(repo_type))
869 869 return repo
870 870
871 871 def _rename_filesystem_repo(self, old, new):
872 872 """
873 873 renames repository on filesystem
874 874
875 875 :param old: old name
876 876 :param new: new name
877 877 """
878 878 log.info('renaming repo from %s to %s', old, new)
879 879
880 880 old_path = os.path.join(self.repos_path, old)
881 881 new_path = os.path.join(self.repos_path, new)
882 882 if os.path.isdir(new_path):
883 883 raise Exception(
884 884 'Was trying to rename to already existing dir %s' % new_path
885 885 )
886 886 shutil.move(old_path, new_path)
887 887
888 888 def _delete_filesystem_repo(self, repo):
889 889 """
890 890 removes repo from filesystem, the removal is acctually made by
891 891 added rm__ prefix into dir, and rename internat .hg/.git dirs so this
892 892 repository is no longer valid for rhodecode, can be undeleted later on
893 893 by reverting the renames on this repository
894 894
895 895 :param repo: repo object
896 896 """
897 897 rm_path = os.path.join(self.repos_path, repo.repo_name)
898 898 repo_group = repo.group
899 899 log.info("Removing repository %s", rm_path)
900 900 # disable hg/git internal that it doesn't get detected as repo
901 901 alias = repo.repo_type
902 902
903 903 config = make_db_config(clear_session=False)
904 904 config.set('extensions', 'largefiles', '')
905 905 bare = getattr(repo.scm_instance(config=config), 'bare', False)
906 906
907 907 # skip this for bare git repos
908 908 if not bare:
909 909 # disable VCS repo
910 910 vcs_path = os.path.join(rm_path, '.%s' % alias)
911 911 if os.path.exists(vcs_path):
912 912 shutil.move(vcs_path, os.path.join(rm_path, 'rm__.%s' % alias))
913 913
914 914 _now = datetime.now()
915 915 _ms = str(_now.microsecond).rjust(6, '0')
916 916 _d = 'rm__%s__%s' % (_now.strftime('%Y%m%d_%H%M%S_' + _ms),
917 917 repo.just_name)
918 918 if repo_group:
919 919 # if repository is in group, prefix the removal path with the group
920 920 args = repo_group.full_path_splitted + [_d]
921 921 _d = os.path.join(*args)
922 922
923 923 if os.path.isdir(rm_path):
924 924 shutil.move(rm_path, os.path.join(self.repos_path, _d))
@@ -1,838 +1,838 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 users model for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27
28 28 import datetime
29 29 from pylons.i18n.translation import _
30 30
31 31 import ipaddress
32 32 from sqlalchemy.exc import DatabaseError
33 33 from sqlalchemy.sql.expression import true, false
34 34
35 35 from rhodecode.events import UserPreCreate, UserPreUpdate
36 36 from rhodecode.lib.utils2 import (
37 37 safe_unicode, get_current_rhodecode_user, action_logger_generic,
38 38 AttributeDict)
39 39 from rhodecode.lib.caching_query import FromCache
40 40 from rhodecode.model import BaseModel
41 41 from rhodecode.model.auth_token import AuthTokenModel
42 42 from rhodecode.model.db import (
43 43 User, UserToPerm, UserEmailMap, UserIpMap)
44 44 from rhodecode.lib.exceptions import (
45 45 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
46 46 UserOwnsUserGroupsException, NotAllowedToCreateUserError)
47 47 from rhodecode.model.meta import Session
48 48 from rhodecode.model.repo_group import RepoGroupModel
49 49
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53
54 54 class UserModel(BaseModel):
55 55 cls = User
56 56
57 57 def get(self, user_id, cache=False):
58 58 user = self.sa.query(User)
59 59 if cache:
60 60 user = user.options(FromCache("sql_cache_short",
61 61 "get_user_%s" % user_id))
62 62 return user.get(user_id)
63 63
64 64 def get_user(self, user):
65 65 return self._get_user(user)
66 66
67 67 def get_by_username(self, username, cache=False, case_insensitive=False):
68 68
69 69 if case_insensitive:
70 70 user = self.sa.query(User).filter(User.username.ilike(username))
71 71 else:
72 72 user = self.sa.query(User)\
73 73 .filter(User.username == username)
74 74 if cache:
75 75 user = user.options(FromCache("sql_cache_short",
76 76 "get_user_%s" % username))
77 77 return user.scalar()
78 78
79 79 def get_by_email(self, email, cache=False, case_insensitive=False):
80 80 return User.get_by_email(email, case_insensitive, cache)
81 81
82 82 def get_by_auth_token(self, auth_token, cache=False):
83 83 return User.get_by_auth_token(auth_token, cache)
84 84
85 85 def get_active_user_count(self, cache=False):
86 86 return User.query().filter(
87 87 User.active == True).filter(
88 88 User.username != User.DEFAULT_USER).count()
89 89
90 90 def create(self, form_data, cur_user=None):
91 91 if not cur_user:
92 92 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
93 93
94 94 user_data = {
95 95 'username': form_data['username'],
96 96 'password': form_data['password'],
97 97 'email': form_data['email'],
98 98 'firstname': form_data['firstname'],
99 99 'lastname': form_data['lastname'],
100 100 'active': form_data['active'],
101 101 'extern_type': form_data['extern_type'],
102 102 'extern_name': form_data['extern_name'],
103 103 'admin': False,
104 104 'cur_user': cur_user
105 105 }
106 106
107 107 try:
108 108 if form_data.get('create_repo_group'):
109 109 user_data['create_repo_group'] = True
110 110 if form_data.get('password_change'):
111 111 user_data['force_password_change'] = True
112 112
113 113 return UserModel().create_or_update(**user_data)
114 114 except Exception:
115 115 log.error(traceback.format_exc())
116 116 raise
117 117
118 118 def update_user(self, user, skip_attrs=None, **kwargs):
119 119 from rhodecode.lib.auth import get_crypt_password
120 120
121 121 user = self._get_user(user)
122 122 if user.username == User.DEFAULT_USER:
123 123 raise DefaultUserException(
124 124 _("You can't Edit this user since it's"
125 125 " crucial for entire application"))
126 126
127 127 # first store only defaults
128 128 user_attrs = {
129 129 'updating_user_id': user.user_id,
130 130 'username': user.username,
131 131 'password': user.password,
132 132 'email': user.email,
133 133 'firstname': user.name,
134 134 'lastname': user.lastname,
135 135 'active': user.active,
136 136 'admin': user.admin,
137 137 'extern_name': user.extern_name,
138 138 'extern_type': user.extern_type,
139 139 'language': user.user_data.get('language')
140 140 }
141 141
142 142 # in case there's new_password, that comes from form, use it to
143 143 # store password
144 144 if kwargs.get('new_password'):
145 145 kwargs['password'] = kwargs['new_password']
146 146
147 147 # cleanups, my_account password change form
148 148 kwargs.pop('current_password', None)
149 149 kwargs.pop('new_password', None)
150 150 kwargs.pop('new_password_confirmation', None)
151 151
152 152 # cleanups, user edit password change form
153 153 kwargs.pop('password_confirmation', None)
154 154 kwargs.pop('password_change', None)
155 155
156 156 # create repo group on user creation
157 157 kwargs.pop('create_repo_group', None)
158 158
159 159 # legacy forms send name, which is the firstname
160 160 firstname = kwargs.pop('name', None)
161 161 if firstname:
162 162 kwargs['firstname'] = firstname
163 163
164 164 for k, v in kwargs.items():
165 165 # skip if we don't want to update this
166 166 if skip_attrs and k in skip_attrs:
167 167 continue
168 168
169 169 user_attrs[k] = v
170 170
171 171 try:
172 172 return self.create_or_update(**user_attrs)
173 173 except Exception:
174 174 log.error(traceback.format_exc())
175 175 raise
176 176
177 177 def create_or_update(
178 178 self, username, password, email, firstname='', lastname='',
179 179 active=True, admin=False, extern_type=None, extern_name=None,
180 180 cur_user=None, plugin=None, force_password_change=False,
181 181 allow_to_create_user=True, create_repo_group=False,
182 182 updating_user_id=None, language=None, strict_creation_check=True):
183 183 """
184 184 Creates a new instance if not found, or updates current one
185 185
186 186 :param username:
187 187 :param password:
188 188 :param email:
189 189 :param firstname:
190 190 :param lastname:
191 191 :param active:
192 192 :param admin:
193 193 :param extern_type:
194 194 :param extern_name:
195 195 :param cur_user:
196 196 :param plugin: optional plugin this method was called from
197 197 :param force_password_change: toggles new or existing user flag
198 198 for password change
199 199 :param allow_to_create_user: Defines if the method can actually create
200 200 new users
201 201 :param create_repo_group: Defines if the method should also
202 202 create an repo group with user name, and owner
203 203 :param updating_user_id: if we set it up this is the user we want to
204 204 update this allows to editing username.
205 205 :param language: language of user from interface.
206 206
207 207 :returns: new User object with injected `is_new_user` attribute.
208 208 """
209 209 if not cur_user:
210 210 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
211 211
212 212 from rhodecode.lib.auth import (
213 213 get_crypt_password, check_password, generate_auth_token)
214 214 from rhodecode.lib.hooks_base import (
215 215 log_create_user, check_allowed_create_user)
216 216
217 217 def _password_change(new_user, password):
218 218 # empty password
219 219 if not new_user.password:
220 220 return False
221 221
222 222 # password check is only needed for RhodeCode internal auth calls
223 223 # in case it's a plugin we don't care
224 224 if not plugin:
225 225
226 226 # first check if we gave crypted password back, and if it matches
227 227 # it's not password change
228 228 if new_user.password == password:
229 229 return False
230 230
231 231 password_match = check_password(password, new_user.password)
232 232 if not password_match:
233 233 return True
234 234
235 235 return False
236 236
237 237 user_data = {
238 238 'username': username,
239 239 'password': password,
240 240 'email': email,
241 241 'firstname': firstname,
242 242 'lastname': lastname,
243 243 'active': active,
244 244 'admin': admin
245 245 }
246 246
247 247 if updating_user_id:
248 248 log.debug('Checking for existing account in RhodeCode '
249 249 'database with user_id `%s` ' % (updating_user_id,))
250 250 user = User.get(updating_user_id)
251 251 else:
252 252 log.debug('Checking for existing account in RhodeCode '
253 253 'database with username `%s` ' % (username,))
254 254 user = User.get_by_username(username, case_insensitive=True)
255 255
256 256 if user is None:
257 257 # we check internal flag if this method is actually allowed to
258 258 # create new user
259 259 if not allow_to_create_user:
260 260 msg = ('Method wants to create new user, but it is not '
261 261 'allowed to do so')
262 262 log.warning(msg)
263 263 raise NotAllowedToCreateUserError(msg)
264 264
265 265 log.debug('Creating new user %s', username)
266 266
267 267 # only if we create user that is active
268 268 new_active_user = active
269 269 if new_active_user and strict_creation_check:
270 270 # raises UserCreationError if it's not allowed for any reason to
271 271 # create new active user, this also executes pre-create hooks
272 272 check_allowed_create_user(user_data, cur_user, strict_check=True)
273 273 self.send_event(UserPreCreate(user_data))
274 274 new_user = User()
275 275 edit = False
276 276 else:
277 277 log.debug('updating user %s', username)
278 278 self.send_event(UserPreUpdate(user, user_data))
279 279 new_user = user
280 280 edit = True
281 281
282 282 # we're not allowed to edit default user
283 283 if user.username == User.DEFAULT_USER:
284 284 raise DefaultUserException(
285 285 _("You can't edit this user (`%(username)s`) since it's "
286 286 "crucial for entire application") % {'username': user.username})
287 287
288 288 # inject special attribute that will tell us if User is new or old
289 289 new_user.is_new_user = not edit
290 290 # for users that didn's specify auth type, we use RhodeCode built in
291 291 from rhodecode.authentication.plugins import auth_rhodecode
292 292 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.name
293 293 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.name
294 294
295 295 try:
296 296 new_user.username = username
297 297 new_user.admin = admin
298 298 new_user.email = email
299 299 new_user.active = active
300 300 new_user.extern_name = safe_unicode(extern_name)
301 301 new_user.extern_type = safe_unicode(extern_type)
302 302 new_user.name = firstname
303 303 new_user.lastname = lastname
304 304
305 305 if not edit:
306 306 new_user.api_key = generate_auth_token(username)
307 307
308 308 # set password only if creating an user or password is changed
309 309 if not edit or _password_change(new_user, password):
310 310 reason = 'new password' if edit else 'new user'
311 311 log.debug('Updating password reason=>%s', reason)
312 312 new_user.password = get_crypt_password(password) if password else None
313 313
314 314 if force_password_change:
315 315 new_user.update_userdata(force_password_change=True)
316 316 if language:
317 317 new_user.update_userdata(language=language)
318 318
319 319 self.sa.add(new_user)
320 320
321 321 if not edit and create_repo_group:
322 322 # create new group same as username, and make this user an owner
323 323 desc = RepoGroupModel.PERSONAL_GROUP_DESC % {'username': username}
324 324 RepoGroupModel().create(group_name=username,
325 325 group_description=desc,
326 326 owner=username, commit_early=False)
327 327 if not edit:
328 328 # add the RSS token
329 329 AuthTokenModel().create(username,
330 330 description='Generated feed token',
331 331 role=AuthTokenModel.cls.ROLE_FEED)
332 332 log_create_user(created_by=cur_user, **new_user.get_dict())
333 333 return new_user
334 334 except (DatabaseError,):
335 335 log.error(traceback.format_exc())
336 336 raise
337 337
338 338 def create_registration(self, form_data):
339 339 from rhodecode.model.notification import NotificationModel
340 340 from rhodecode.model.notification import EmailNotificationModel
341 341
342 342 try:
343 343 form_data['admin'] = False
344 344 form_data['extern_name'] = 'rhodecode'
345 345 form_data['extern_type'] = 'rhodecode'
346 346 new_user = self.create(form_data)
347 347
348 348 self.sa.add(new_user)
349 349 self.sa.flush()
350 350
351 351 user_data = new_user.get_dict()
352 352 kwargs = {
353 353 # use SQLALCHEMY safe dump of user data
354 354 'user': AttributeDict(user_data),
355 355 'date': datetime.datetime.now()
356 356 }
357 357 notification_type = EmailNotificationModel.TYPE_REGISTRATION
358 358 # pre-generate the subject for notification itself
359 359 (subject,
360 360 _h, _e, # we don't care about those
361 361 body_plaintext) = EmailNotificationModel().render_email(
362 362 notification_type, **kwargs)
363 363
364 364 # create notification objects, and emails
365 365 NotificationModel().create(
366 366 created_by=new_user,
367 367 notification_subject=subject,
368 368 notification_body=body_plaintext,
369 369 notification_type=notification_type,
370 370 recipients=None, # all admins
371 371 email_kwargs=kwargs,
372 372 )
373 373
374 374 return new_user
375 375 except Exception:
376 376 log.error(traceback.format_exc())
377 377 raise
378 378
379 379 def _handle_user_repos(self, username, repositories, handle_mode=None):
380 _superadmin = self.cls.get_first_admin()
380 _superadmin = self.cls.get_first_super_admin()
381 381 left_overs = True
382 382
383 383 from rhodecode.model.repo import RepoModel
384 384
385 385 if handle_mode == 'detach':
386 386 for obj in repositories:
387 387 obj.user = _superadmin
388 388 # set description we know why we super admin now owns
389 389 # additional repositories that were orphaned !
390 390 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
391 391 self.sa.add(obj)
392 392 left_overs = False
393 393 elif handle_mode == 'delete':
394 394 for obj in repositories:
395 395 RepoModel().delete(obj, forks='detach')
396 396 left_overs = False
397 397
398 398 # if nothing is done we have left overs left
399 399 return left_overs
400 400
401 401 def _handle_user_repo_groups(self, username, repository_groups,
402 402 handle_mode=None):
403 _superadmin = self.cls.get_first_admin()
403 _superadmin = self.cls.get_first_super_admin()
404 404 left_overs = True
405 405
406 406 from rhodecode.model.repo_group import RepoGroupModel
407 407
408 408 if handle_mode == 'detach':
409 409 for r in repository_groups:
410 410 r.user = _superadmin
411 411 # set description we know why we super admin now owns
412 412 # additional repositories that were orphaned !
413 413 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
414 414 self.sa.add(r)
415 415 left_overs = False
416 416 elif handle_mode == 'delete':
417 417 for r in repository_groups:
418 418 RepoGroupModel().delete(r)
419 419 left_overs = False
420 420
421 421 # if nothing is done we have left overs left
422 422 return left_overs
423 423
424 424 def _handle_user_user_groups(self, username, user_groups, handle_mode=None):
425 _superadmin = self.cls.get_first_admin()
425 _superadmin = self.cls.get_first_super_admin()
426 426 left_overs = True
427 427
428 428 from rhodecode.model.user_group import UserGroupModel
429 429
430 430 if handle_mode == 'detach':
431 431 for r in user_groups:
432 432 for user_user_group_to_perm in r.user_user_group_to_perm:
433 433 if user_user_group_to_perm.user.username == username:
434 434 user_user_group_to_perm.user = _superadmin
435 435 r.user = _superadmin
436 436 # set description we know why we super admin now owns
437 437 # additional repositories that were orphaned !
438 438 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
439 439 self.sa.add(r)
440 440 left_overs = False
441 441 elif handle_mode == 'delete':
442 442 for r in user_groups:
443 443 UserGroupModel().delete(r)
444 444 left_overs = False
445 445
446 446 # if nothing is done we have left overs left
447 447 return left_overs
448 448
449 449 def delete(self, user, cur_user=None, handle_repos=None,
450 450 handle_repo_groups=None, handle_user_groups=None):
451 451 if not cur_user:
452 452 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
453 453 user = self._get_user(user)
454 454
455 455 try:
456 456 if user.username == User.DEFAULT_USER:
457 457 raise DefaultUserException(
458 458 _(u"You can't remove this user since it's"
459 459 u" crucial for entire application"))
460 460
461 461 left_overs = self._handle_user_repos(
462 462 user.username, user.repositories, handle_repos)
463 463 if left_overs and user.repositories:
464 464 repos = [x.repo_name for x in user.repositories]
465 465 raise UserOwnsReposException(
466 466 _(u'user "%s" still owns %s repositories and cannot be '
467 467 u'removed. Switch owners or remove those repositories:%s')
468 468 % (user.username, len(repos), ', '.join(repos)))
469 469
470 470 left_overs = self._handle_user_repo_groups(
471 471 user.username, user.repository_groups, handle_repo_groups)
472 472 if left_overs and user.repository_groups:
473 473 repo_groups = [x.group_name for x in user.repository_groups]
474 474 raise UserOwnsRepoGroupsException(
475 475 _(u'user "%s" still owns %s repository groups and cannot be '
476 476 u'removed. Switch owners or remove those repository groups:%s')
477 477 % (user.username, len(repo_groups), ', '.join(repo_groups)))
478 478
479 479 left_overs = self._handle_user_user_groups(
480 480 user.username, user.user_groups, handle_user_groups)
481 481 if left_overs and user.user_groups:
482 482 user_groups = [x.users_group_name for x in user.user_groups]
483 483 raise UserOwnsUserGroupsException(
484 484 _(u'user "%s" still owns %s user groups and cannot be '
485 485 u'removed. Switch owners or remove those user groups:%s')
486 486 % (user.username, len(user_groups), ', '.join(user_groups)))
487 487
488 488 # we might change the user data with detach/delete, make sure
489 489 # the object is marked as expired before actually deleting !
490 490 self.sa.expire(user)
491 491 self.sa.delete(user)
492 492 from rhodecode.lib.hooks_base import log_delete_user
493 493 log_delete_user(deleted_by=cur_user, **user.get_dict())
494 494 except Exception:
495 495 log.error(traceback.format_exc())
496 496 raise
497 497
498 498 def reset_password_link(self, data, pwd_reset_url):
499 499 from rhodecode.lib.celerylib import tasks, run_task
500 500 from rhodecode.model.notification import EmailNotificationModel
501 501 user_email = data['email']
502 502 try:
503 503 user = User.get_by_email(user_email)
504 504 if user:
505 505 log.debug('password reset user found %s', user)
506 506
507 507 email_kwargs = {
508 508 'password_reset_url': pwd_reset_url,
509 509 'user': user,
510 510 'email': user_email,
511 511 'date': datetime.datetime.now()
512 512 }
513 513
514 514 (subject, headers, email_body,
515 515 email_body_plaintext) = EmailNotificationModel().render_email(
516 516 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
517 517
518 518 recipients = [user_email]
519 519
520 520 action_logger_generic(
521 521 'sending password reset email to user: {}'.format(
522 522 user), namespace='security.password_reset')
523 523
524 524 run_task(tasks.send_email, recipients, subject,
525 525 email_body_plaintext, email_body)
526 526
527 527 else:
528 528 log.debug("password reset email %s not found", user_email)
529 529 except Exception:
530 530 log.error(traceback.format_exc())
531 531 return False
532 532
533 533 return True
534 534
535 535 def reset_password(self, data):
536 536 from rhodecode.lib.celerylib import tasks, run_task
537 537 from rhodecode.model.notification import EmailNotificationModel
538 538 from rhodecode.lib import auth
539 539 user_email = data['email']
540 540 pre_db = True
541 541 try:
542 542 user = User.get_by_email(user_email)
543 543 new_passwd = auth.PasswordGenerator().gen_password(
544 544 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
545 545 if user:
546 546 user.password = auth.get_crypt_password(new_passwd)
547 547 # also force this user to reset his password !
548 548 user.update_userdata(force_password_change=True)
549 549
550 550 Session().add(user)
551 551 Session().commit()
552 552 log.info('change password for %s', user_email)
553 553 if new_passwd is None:
554 554 raise Exception('unable to generate new password')
555 555
556 556 pre_db = False
557 557
558 558 email_kwargs = {
559 559 'new_password': new_passwd,
560 560 'user': user,
561 561 'email': user_email,
562 562 'date': datetime.datetime.now()
563 563 }
564 564
565 565 (subject, headers, email_body,
566 566 email_body_plaintext) = EmailNotificationModel().render_email(
567 567 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION, **email_kwargs)
568 568
569 569 recipients = [user_email]
570 570
571 571 action_logger_generic(
572 572 'sent new password to user: {} with email: {}'.format(
573 573 user, user_email), namespace='security.password_reset')
574 574
575 575 run_task(tasks.send_email, recipients, subject,
576 576 email_body_plaintext, email_body)
577 577
578 578 except Exception:
579 579 log.error('Failed to update user password')
580 580 log.error(traceback.format_exc())
581 581 if pre_db:
582 582 # we rollback only if local db stuff fails. If it goes into
583 583 # run_task, we're pass rollback state this wouldn't work then
584 584 Session().rollback()
585 585
586 586 return True
587 587
588 588 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
589 589 """
590 590 Fetches auth_user by user_id,or api_key if present.
591 591 Fills auth_user attributes with those taken from database.
592 592 Additionally set's is_authenitated if lookup fails
593 593 present in database
594 594
595 595 :param auth_user: instance of user to set attributes
596 596 :param user_id: user id to fetch by
597 597 :param api_key: api key to fetch by
598 598 :param username: username to fetch by
599 599 """
600 600 if user_id is None and api_key is None and username is None:
601 601 raise Exception('You need to pass user_id, api_key or username')
602 602
603 603 log.debug(
604 604 'doing fill data based on: user_id:%s api_key:%s username:%s',
605 605 user_id, api_key, username)
606 606 try:
607 607 dbuser = None
608 608 if user_id:
609 609 dbuser = self.get(user_id)
610 610 elif api_key:
611 611 dbuser = self.get_by_auth_token(api_key)
612 612 elif username:
613 613 dbuser = self.get_by_username(username)
614 614
615 615 if not dbuser:
616 616 log.warning(
617 617 'Unable to lookup user by id:%s api_key:%s username:%s',
618 618 user_id, api_key, username)
619 619 return False
620 620 if not dbuser.active:
621 621 log.debug('User `%s` is inactive, skipping fill data', username)
622 622 return False
623 623
624 624 log.debug('filling user:%s data', dbuser)
625 625
626 626 # TODO: johbo: Think about this and find a clean solution
627 627 user_data = dbuser.get_dict()
628 628 user_data.update(dbuser.get_api_data(include_secrets=True))
629 629
630 630 for k, v in user_data.iteritems():
631 631 # properties of auth user we dont update
632 632 if k not in ['auth_tokens', 'permissions']:
633 633 setattr(auth_user, k, v)
634 634
635 635 # few extras
636 636 setattr(auth_user, 'feed_token', dbuser.feed_token)
637 637 except Exception:
638 638 log.error(traceback.format_exc())
639 639 auth_user.is_authenticated = False
640 640 return False
641 641
642 642 return True
643 643
644 644 def has_perm(self, user, perm):
645 645 perm = self._get_perm(perm)
646 646 user = self._get_user(user)
647 647
648 648 return UserToPerm.query().filter(UserToPerm.user == user)\
649 649 .filter(UserToPerm.permission == perm).scalar() is not None
650 650
651 651 def grant_perm(self, user, perm):
652 652 """
653 653 Grant user global permissions
654 654
655 655 :param user:
656 656 :param perm:
657 657 """
658 658 user = self._get_user(user)
659 659 perm = self._get_perm(perm)
660 660 # if this permission is already granted skip it
661 661 _perm = UserToPerm.query()\
662 662 .filter(UserToPerm.user == user)\
663 663 .filter(UserToPerm.permission == perm)\
664 664 .scalar()
665 665 if _perm:
666 666 return
667 667 new = UserToPerm()
668 668 new.user = user
669 669 new.permission = perm
670 670 self.sa.add(new)
671 671 return new
672 672
673 673 def revoke_perm(self, user, perm):
674 674 """
675 675 Revoke users global permissions
676 676
677 677 :param user:
678 678 :param perm:
679 679 """
680 680 user = self._get_user(user)
681 681 perm = self._get_perm(perm)
682 682
683 683 obj = UserToPerm.query()\
684 684 .filter(UserToPerm.user == user)\
685 685 .filter(UserToPerm.permission == perm)\
686 686 .scalar()
687 687 if obj:
688 688 self.sa.delete(obj)
689 689
690 690 def add_extra_email(self, user, email):
691 691 """
692 692 Adds email address to UserEmailMap
693 693
694 694 :param user:
695 695 :param email:
696 696 """
697 697 from rhodecode.model import forms
698 698 form = forms.UserExtraEmailForm()()
699 699 data = form.to_python({'email': email})
700 700 user = self._get_user(user)
701 701
702 702 obj = UserEmailMap()
703 703 obj.user = user
704 704 obj.email = data['email']
705 705 self.sa.add(obj)
706 706 return obj
707 707
708 708 def delete_extra_email(self, user, email_id):
709 709 """
710 710 Removes email address from UserEmailMap
711 711
712 712 :param user:
713 713 :param email_id:
714 714 """
715 715 user = self._get_user(user)
716 716 obj = UserEmailMap.query().get(email_id)
717 717 if obj:
718 718 self.sa.delete(obj)
719 719
720 720 def parse_ip_range(self, ip_range):
721 721 ip_list = []
722 722 def make_unique(value):
723 723 seen = []
724 724 return [c for c in value if not (c in seen or seen.append(c))]
725 725
726 726 # firsts split by commas
727 727 for ip_range in ip_range.split(','):
728 728 if not ip_range:
729 729 continue
730 730 ip_range = ip_range.strip()
731 731 if '-' in ip_range:
732 732 start_ip, end_ip = ip_range.split('-', 1)
733 733 start_ip = ipaddress.ip_address(start_ip.strip())
734 734 end_ip = ipaddress.ip_address(end_ip.strip())
735 735 parsed_ip_range = []
736 736
737 737 for index in xrange(int(start_ip), int(end_ip) + 1):
738 738 new_ip = ipaddress.ip_address(index)
739 739 parsed_ip_range.append(str(new_ip))
740 740 ip_list.extend(parsed_ip_range)
741 741 else:
742 742 ip_list.append(ip_range)
743 743
744 744 return make_unique(ip_list)
745 745
746 746 def add_extra_ip(self, user, ip, description=None):
747 747 """
748 748 Adds ip address to UserIpMap
749 749
750 750 :param user:
751 751 :param ip:
752 752 """
753 753 from rhodecode.model import forms
754 754 form = forms.UserExtraIpForm()()
755 755 data = form.to_python({'ip': ip})
756 756 user = self._get_user(user)
757 757
758 758 obj = UserIpMap()
759 759 obj.user = user
760 760 obj.ip_addr = data['ip']
761 761 obj.description = description
762 762 self.sa.add(obj)
763 763 return obj
764 764
765 765 def delete_extra_ip(self, user, ip_id):
766 766 """
767 767 Removes ip address from UserIpMap
768 768
769 769 :param user:
770 770 :param ip_id:
771 771 """
772 772 user = self._get_user(user)
773 773 obj = UserIpMap.query().get(ip_id)
774 774 if obj:
775 775 self.sa.delete(obj)
776 776
777 777 def get_accounts_in_creation_order(self, current_user=None):
778 778 """
779 779 Get accounts in order of creation for deactivation for license limits
780 780
781 781 pick currently logged in user, and append to the list in position 0
782 782 pick all super-admins in order of creation date and add it to the list
783 783 pick all other accounts in order of creation and add it to the list.
784 784
785 785 Based on that list, the last accounts can be disabled as they are
786 786 created at the end and don't include any of the super admins as well
787 787 as the current user.
788 788
789 789 :param current_user: optionally current user running this operation
790 790 """
791 791
792 792 if not current_user:
793 793 current_user = get_current_rhodecode_user()
794 794 active_super_admins = [
795 795 x.user_id for x in User.query()
796 796 .filter(User.user_id != current_user.user_id)
797 797 .filter(User.active == true())
798 798 .filter(User.admin == true())
799 799 .order_by(User.created_on.asc())]
800 800
801 801 active_regular_users = [
802 802 x.user_id for x in User.query()
803 803 .filter(User.user_id != current_user.user_id)
804 804 .filter(User.active == true())
805 805 .filter(User.admin == false())
806 806 .order_by(User.created_on.asc())]
807 807
808 808 list_of_accounts = [current_user.user_id]
809 809 list_of_accounts += active_super_admins
810 810 list_of_accounts += active_regular_users
811 811
812 812 return list_of_accounts
813 813
814 814 def deactivate_last_users(self, expected_users):
815 815 """
816 816 Deactivate accounts that are over the license limits.
817 817 Algorithm of which accounts to disabled is based on the formula:
818 818
819 819 Get current user, then super admins in creation order, then regular
820 820 active users in creation order.
821 821
822 822 Using that list we mark all accounts from the end of it as inactive.
823 823 This way we block only latest created accounts.
824 824
825 825 :param expected_users: list of users in special order, we deactivate
826 826 the end N ammoun of users from that list
827 827 """
828 828
829 829 list_of_accounts = self.get_accounts_in_creation_order()
830 830
831 831 for acc_id in list_of_accounts[expected_users + 1:]:
832 832 user = User.get(acc_id)
833 833 log.info('Deactivating account %s for license unlock', user)
834 834 user.active = False
835 835 Session().add(user)
836 836 Session().commit()
837 837
838 838 return
@@ -1,517 +1,517 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2016 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 user group model for RhodeCode
24 24 """
25 25
26 26
27 27 import logging
28 28 import traceback
29 29
30 30 from rhodecode.lib.utils2 import safe_str
31 31 from rhodecode.model import BaseModel
32 32 from rhodecode.model.db import UserGroupMember, UserGroup,\
33 33 UserGroupRepoToPerm, Permission, UserGroupToPerm, User, UserUserGroupToPerm,\
34 34 UserGroupUserGroupToPerm, UserGroupRepoGroupToPerm
35 35 from rhodecode.lib.exceptions import UserGroupAssignedException,\
36 36 RepoGroupAssignmentError
37 37 from rhodecode.lib.utils2 import get_current_rhodecode_user, action_logger_generic
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 class UserGroupModel(BaseModel):
43 43
44 44 cls = UserGroup
45 45
46 46 def _get_user_group(self, user_group):
47 47 return self._get_instance(UserGroup, user_group,
48 48 callback=UserGroup.get_by_group_name)
49 49
50 50 def _create_default_perms(self, user_group):
51 51 # create default permission
52 52 default_perm = 'usergroup.read'
53 53 def_user = User.get_default_user()
54 54 for p in def_user.user_perms:
55 55 if p.permission.permission_name.startswith('usergroup.'):
56 56 default_perm = p.permission.permission_name
57 57 break
58 58
59 59 user_group_to_perm = UserUserGroupToPerm()
60 60 user_group_to_perm.permission = Permission.get_by_key(default_perm)
61 61
62 62 user_group_to_perm.user_group = user_group
63 63 user_group_to_perm.user_id = def_user.user_id
64 64 return user_group_to_perm
65 65
66 66 def update_permissions(self, user_group, perm_additions=None, perm_updates=None,
67 67 perm_deletions=None, check_perms=True, cur_user=None):
68 68 from rhodecode.lib.auth import HasUserGroupPermissionAny
69 69 if not perm_additions:
70 70 perm_additions = []
71 71 if not perm_updates:
72 72 perm_updates = []
73 73 if not perm_deletions:
74 74 perm_deletions = []
75 75
76 76 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
77 77
78 78 # update permissions
79 79 for member_id, perm, member_type in perm_updates:
80 80 member_id = int(member_id)
81 81 if member_type == 'user':
82 82 # this updates existing one
83 83 self.grant_user_permission(
84 84 user_group=user_group, user=member_id, perm=perm
85 85 )
86 86 else:
87 87 # check if we have permissions to alter this usergroup
88 88 member_name = UserGroup.get(member_id).users_group_name
89 89 if not check_perms or HasUserGroupPermissionAny(*req_perms)(member_name, user=cur_user):
90 90 self.grant_user_group_permission(
91 91 target_user_group=user_group, user_group=member_id, perm=perm
92 92 )
93 93
94 94 # set new permissions
95 95 for member_id, perm, member_type in perm_additions:
96 96 member_id = int(member_id)
97 97 if member_type == 'user':
98 98 self.grant_user_permission(
99 99 user_group=user_group, user=member_id, perm=perm
100 100 )
101 101 else:
102 102 # check if we have permissions to alter this usergroup
103 103 member_name = UserGroup.get(member_id).users_group_name
104 104 if not check_perms or HasUserGroupPermissionAny(*req_perms)(member_name, user=cur_user):
105 105 self.grant_user_group_permission(
106 106 target_user_group=user_group, user_group=member_id, perm=perm
107 107 )
108 108
109 109 # delete permissions
110 110 for member_id, perm, member_type in perm_deletions:
111 111 member_id = int(member_id)
112 112 if member_type == 'user':
113 113 self.revoke_user_permission(user_group=user_group, user=member_id)
114 114 else:
115 115 #check if we have permissions to alter this usergroup
116 116 member_name = UserGroup.get(member_id).users_group_name
117 117 if not check_perms or HasUserGroupPermissionAny(*req_perms)(member_name, user=cur_user):
118 118 self.revoke_user_group_permission(
119 119 target_user_group=user_group, user_group=member_id
120 120 )
121 121
122 122 def get(self, user_group_id, cache=False):
123 123 return UserGroup.get(user_group_id)
124 124
125 125 def get_group(self, user_group):
126 126 return self._get_user_group(user_group)
127 127
128 128 def get_by_name(self, name, cache=False, case_insensitive=False):
129 129 return UserGroup.get_by_group_name(name, cache, case_insensitive)
130 130
131 131 def create(self, name, description, owner, active=True, group_data=None):
132 132 try:
133 133 new_user_group = UserGroup()
134 134 new_user_group.user = self._get_user(owner)
135 135 new_user_group.users_group_name = name
136 136 new_user_group.user_group_description = description
137 137 new_user_group.users_group_active = active
138 138 if group_data:
139 139 new_user_group.group_data = group_data
140 140 self.sa.add(new_user_group)
141 141 perm_obj = self._create_default_perms(new_user_group)
142 142 self.sa.add(perm_obj)
143 143
144 144 self.grant_user_permission(user_group=new_user_group,
145 145 user=owner, perm='usergroup.admin')
146 146
147 147 return new_user_group
148 148 except Exception:
149 149 log.error(traceback.format_exc())
150 150 raise
151 151
152 152 def _get_memberships_for_user_ids(self, user_group, user_id_list):
153 153 members = []
154 154 for user_id in user_id_list:
155 155 member = self._get_membership(user_group.users_group_id, user_id)
156 156 members.append(member)
157 157 return members
158 158
159 159 def _get_added_and_removed_user_ids(self, user_group, user_id_list):
160 160 current_members = user_group.members or []
161 161 current_members_ids = [m.user.user_id for m in current_members]
162 162
163 163 added_members = [
164 164 user_id for user_id in user_id_list
165 165 if user_id not in current_members_ids]
166 166 if user_id_list == []:
167 167 # all members were deleted
168 168 deleted_members = current_members_ids
169 169 else:
170 170 deleted_members = [
171 171 user_id for user_id in current_members_ids
172 172 if user_id not in user_id_list]
173 173
174 174 return (added_members, deleted_members)
175 175
176 176 def _set_users_as_members(self, user_group, user_ids):
177 177 user_group.members = []
178 178 self.sa.flush()
179 179 members = self._get_memberships_for_user_ids(
180 180 user_group, user_ids)
181 181 user_group.members = members
182 182 self.sa.add(user_group)
183 183
184 184 def _update_members_from_user_ids(self, user_group, user_ids):
185 185 added, removed = self._get_added_and_removed_user_ids(
186 186 user_group, user_ids)
187 187 self._set_users_as_members(user_group, user_ids)
188 188 self._log_user_changes('added to', user_group, added)
189 189 self._log_user_changes('removed from', user_group, removed)
190 190
191 191 def _clean_members_data(self, members_data):
192 192 # TODO: anderson: this should be in the form validation but I couldn't
193 193 # make it work there as it conflicts with the other validator
194 194 if not members_data:
195 195 members_data = []
196 196
197 197 if isinstance(members_data, basestring):
198 198 new_members = [members_data]
199 199 else:
200 200 new_members = members_data
201 201
202 202 new_members = [int(uid) for uid in new_members]
203 203 return new_members
204 204
205 205 def update(self, user_group, form_data):
206 206 user_group = self._get_user_group(user_group)
207 207 if 'users_group_name' in form_data:
208 208 user_group.users_group_name = form_data['users_group_name']
209 209 if 'users_group_active' in form_data:
210 210 user_group.users_group_active = form_data['users_group_active']
211 211 if 'user_group_description' in form_data:
212 212 user_group.user_group_description = form_data[
213 213 'user_group_description']
214 214
215 215 # handle owner change
216 216 if 'user' in form_data:
217 217 owner = form_data['user']
218 218 if isinstance(owner, basestring):
219 219 owner = User.get_by_username(form_data['user'])
220 220
221 221 if not isinstance(owner, User):
222 222 raise ValueError(
223 223 'invalid owner for user group: %s' % form_data['user'])
224 224
225 225 user_group.user = owner
226 226
227 227 if 'users_group_members' in form_data:
228 228 members_id_list = self._clean_members_data(
229 229 form_data['users_group_members'])
230 230 self._update_members_from_user_ids(user_group, members_id_list)
231 231
232 232 self.sa.add(user_group)
233 233
234 234 def delete(self, user_group, force=False):
235 235 """
236 236 Deletes repository group, unless force flag is used
237 237 raises exception if there are members in that group, else deletes
238 238 group and users
239 239
240 240 :param user_group:
241 241 :param force:
242 242 """
243 243 user_group = self._get_user_group(user_group)
244 244 try:
245 245 # check if this group is not assigned to repo
246 246 assigned_to_repo = [x.repository for x in UserGroupRepoToPerm.query()\
247 247 .filter(UserGroupRepoToPerm.users_group == user_group).all()]
248 248 # check if this group is not assigned to repo
249 249 assigned_to_repo_group = [x.group for x in UserGroupRepoGroupToPerm.query()\
250 250 .filter(UserGroupRepoGroupToPerm.users_group == user_group).all()]
251 251
252 252 if (assigned_to_repo or assigned_to_repo_group) and not force:
253 253 assigned = ','.join(map(safe_str,
254 254 assigned_to_repo+assigned_to_repo_group))
255 255
256 256 raise UserGroupAssignedException(
257 257 'UserGroup assigned to %s' % (assigned,))
258 258 self.sa.delete(user_group)
259 259 except Exception:
260 260 log.error(traceback.format_exc())
261 261 raise
262 262
263 263 def _log_user_changes(self, action, user_group, user_or_users):
264 264 users = user_or_users
265 265 if not isinstance(users, (list, tuple)):
266 266 users = [users]
267 267 rhodecode_user = get_current_rhodecode_user()
268 268 ipaddr = getattr(rhodecode_user, 'ip_addr', '')
269 269 group_name = user_group.users_group_name
270 270
271 271 for user_or_user_id in users:
272 272 user = self._get_user(user_or_user_id)
273 273 log_text = 'User {user} {action} {group}'.format(
274 274 action=action, user=user.username, group=group_name)
275 275 log.info('Logging action: {0} by {1} ip:{2}'.format(
276 276 log_text, rhodecode_user, ipaddr))
277 277
278 278 def _find_user_in_group(self, user, user_group):
279 279 user_group_member = None
280 280 for m in user_group.members:
281 281 if m.user_id == user.user_id:
282 282 # Found this user's membership row
283 283 user_group_member = m
284 284 break
285 285
286 286 return user_group_member
287 287
288 288 def _get_membership(self, user_group_id, user_id):
289 289 user_group_member = UserGroupMember(user_group_id, user_id)
290 290 return user_group_member
291 291
292 292 def add_user_to_group(self, user_group, user):
293 293 user_group = self._get_user_group(user_group)
294 294 user = self._get_user(user)
295 295 user_member = self._find_user_in_group(user, user_group)
296 296 if user_member:
297 297 # user already in the group, skip
298 298 return True
299 299
300 300 member = self._get_membership(
301 301 user_group.users_group_id, user.user_id)
302 302 user_group.members.append(member)
303 303
304 304 try:
305 305 self.sa.add(member)
306 306 except Exception:
307 307 # what could go wrong here?
308 308 log.error(traceback.format_exc())
309 309 raise
310 310
311 311 self._log_user_changes('added to', user_group, user)
312 312 return member
313 313
314 314 def remove_user_from_group(self, user_group, user):
315 315 user_group = self._get_user_group(user_group)
316 316 user = self._get_user(user)
317 317 user_group_member = self._find_user_in_group(user, user_group)
318 318
319 319 if not user_group_member:
320 320 # User isn't in that group
321 321 return False
322 322
323 323 try:
324 324 self.sa.delete(user_group_member)
325 325 except Exception:
326 326 log.error(traceback.format_exc())
327 327 raise
328 328
329 329 self._log_user_changes('removed from', user_group, user)
330 330 return True
331 331
332 332 def has_perm(self, user_group, perm):
333 333 user_group = self._get_user_group(user_group)
334 334 perm = self._get_perm(perm)
335 335
336 336 return UserGroupToPerm.query()\
337 337 .filter(UserGroupToPerm.users_group == user_group)\
338 338 .filter(UserGroupToPerm.permission == perm).scalar() is not None
339 339
340 340 def grant_perm(self, user_group, perm):
341 341 user_group = self._get_user_group(user_group)
342 342 perm = self._get_perm(perm)
343 343
344 344 # if this permission is already granted skip it
345 345 _perm = UserGroupToPerm.query()\
346 346 .filter(UserGroupToPerm.users_group == user_group)\
347 347 .filter(UserGroupToPerm.permission == perm)\
348 348 .scalar()
349 349 if _perm:
350 350 return
351 351
352 352 new = UserGroupToPerm()
353 353 new.users_group = user_group
354 354 new.permission = perm
355 355 self.sa.add(new)
356 356 return new
357 357
358 358 def revoke_perm(self, user_group, perm):
359 359 user_group = self._get_user_group(user_group)
360 360 perm = self._get_perm(perm)
361 361
362 362 obj = UserGroupToPerm.query()\
363 363 .filter(UserGroupToPerm.users_group == user_group)\
364 364 .filter(UserGroupToPerm.permission == perm).scalar()
365 365 if obj:
366 366 self.sa.delete(obj)
367 367
368 368 def grant_user_permission(self, user_group, user, perm):
369 369 """
370 370 Grant permission for user on given user group, or update
371 371 existing one if found
372 372
373 373 :param user_group: Instance of UserGroup, users_group_id,
374 374 or users_group_name
375 375 :param user: Instance of User, user_id or username
376 376 :param perm: Instance of Permission, or permission_name
377 377 """
378 378
379 379 user_group = self._get_user_group(user_group)
380 380 user = self._get_user(user)
381 381 permission = self._get_perm(perm)
382 382
383 383 # check if we have that permission already
384 384 obj = self.sa.query(UserUserGroupToPerm)\
385 385 .filter(UserUserGroupToPerm.user == user)\
386 386 .filter(UserUserGroupToPerm.user_group == user_group)\
387 387 .scalar()
388 388 if obj is None:
389 389 # create new !
390 390 obj = UserUserGroupToPerm()
391 391 obj.user_group = user_group
392 392 obj.user = user
393 393 obj.permission = permission
394 394 self.sa.add(obj)
395 395 log.debug('Granted perm %s to %s on %s', perm, user, user_group)
396 396 action_logger_generic(
397 397 'granted permission: {} to user: {} on usergroup: {}'.format(
398 398 perm, user, user_group), namespace='security.usergroup')
399 399
400 400 return obj
401 401
402 402 def revoke_user_permission(self, user_group, user):
403 403 """
404 404 Revoke permission for user on given user group
405 405
406 406 :param user_group: Instance of UserGroup, users_group_id,
407 407 or users_group name
408 408 :param user: Instance of User, user_id or username
409 409 """
410 410
411 411 user_group = self._get_user_group(user_group)
412 412 user = self._get_user(user)
413 413
414 414 obj = self.sa.query(UserUserGroupToPerm)\
415 415 .filter(UserUserGroupToPerm.user == user)\
416 416 .filter(UserUserGroupToPerm.user_group == user_group)\
417 417 .scalar()
418 418 if obj:
419 419 self.sa.delete(obj)
420 420 log.debug('Revoked perm on %s on %s', user_group, user)
421 421 action_logger_generic(
422 422 'revoked permission from user: {} on usergroup: {}'.format(
423 423 user, user_group), namespace='security.usergroup')
424 424
425 425 def grant_user_group_permission(self, target_user_group, user_group, perm):
426 426 """
427 427 Grant user group permission for given target_user_group
428 428
429 429 :param target_user_group:
430 430 :param user_group:
431 431 :param perm:
432 432 """
433 433 target_user_group = self._get_user_group(target_user_group)
434 434 user_group = self._get_user_group(user_group)
435 435 permission = self._get_perm(perm)
436 436 # forbid assigning same user group to itself
437 437 if target_user_group == user_group:
438 438 raise RepoGroupAssignmentError('target repo:%s cannot be '
439 439 'assigned to itself' % target_user_group)
440 440
441 441 # check if we have that permission already
442 442 obj = self.sa.query(UserGroupUserGroupToPerm)\
443 443 .filter(UserGroupUserGroupToPerm.target_user_group == target_user_group)\
444 444 .filter(UserGroupUserGroupToPerm.user_group == user_group)\
445 445 .scalar()
446 446 if obj is None:
447 447 # create new !
448 448 obj = UserGroupUserGroupToPerm()
449 449 obj.user_group = user_group
450 450 obj.target_user_group = target_user_group
451 451 obj.permission = permission
452 452 self.sa.add(obj)
453 453 log.debug(
454 454 'Granted perm %s to %s on %s', perm, target_user_group, user_group)
455 455 action_logger_generic(
456 456 'granted permission: {} to usergroup: {} on usergroup: {}'.format(
457 457 perm, user_group, target_user_group),
458 458 namespace='security.usergroup')
459 459
460 460 return obj
461 461
462 462 def revoke_user_group_permission(self, target_user_group, user_group):
463 463 """
464 464 Revoke user group permission for given target_user_group
465 465
466 466 :param target_user_group:
467 467 :param user_group:
468 468 """
469 469 target_user_group = self._get_user_group(target_user_group)
470 470 user_group = self._get_user_group(user_group)
471 471
472 472 obj = self.sa.query(UserGroupUserGroupToPerm)\
473 473 .filter(UserGroupUserGroupToPerm.target_user_group == target_user_group)\
474 474 .filter(UserGroupUserGroupToPerm.user_group == user_group)\
475 475 .scalar()
476 476 if obj:
477 477 self.sa.delete(obj)
478 478 log.debug(
479 479 'Revoked perm on %s on %s', target_user_group, user_group)
480 480 action_logger_generic(
481 481 'revoked permission from usergroup: {} on usergroup: {}'.format(
482 482 user_group, target_user_group),
483 483 namespace='security.repogroup')
484 484
485 485 def enforce_groups(self, user, groups, extern_type=None):
486 486 user = self._get_user(user)
487 487 log.debug('Enforcing groups %s on user %s', groups, user)
488 488 current_groups = user.group_member
489 489 # find the external created groups
490 490 externals = [x.users_group for x in current_groups
491 491 if 'extern_type' in x.users_group.group_data]
492 492
493 493 # calculate from what groups user should be removed
494 494 # externals that are not in groups
495 495 for gr in externals:
496 496 if gr.users_group_name not in groups:
497 497 log.debug('Removing user %s from user group %s', user, gr)
498 498 self.remove_user_from_group(gr, user)
499 499
500 500 # now we calculate in which groups user should be == groups params
501 owner = User.get_first_admin().username
501 owner = User.get_first_super_admin().username
502 502 for gr in set(groups):
503 503 existing_group = UserGroup.get_by_group_name(gr)
504 504 if not existing_group:
505 505 desc = 'Automatically created from plugin:%s' % extern_type
506 506 # we use first admin account to set the owner of the group
507 507 existing_group = UserGroupModel().create(gr, desc, owner,
508 508 group_data={'extern_type': extern_type})
509 509
510 510 # we can only add users to special groups created via plugins
511 511 managed = 'extern_type' in existing_group.group_data
512 512 if managed:
513 513 log.debug('Adding user %s to user group %s', user, gr)
514 514 UserGroupModel().add_user_to_group(existing_group, user)
515 515 else:
516 516 log.debug('Skipping addition to group %s since it is '
517 517 'not managed by auth plugins' % gr)
@@ -1,54 +1,54 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 from rhodecode.model.db import User
22 22 from rhodecode.tests import *
23 23
24 24
25 25 class TestFeedController(TestController):
26 26
27 27 def test_rss(self, backend):
28 28 self.log_user()
29 29 response = self.app.get(url(controller='feed', action='rss',
30 30 repo_name=backend.repo_name))
31 31
32 32 assert response.content_type == "application/rss+xml"
33 33 assert """<rss version="2.0">""" in response
34 34
35 35 def test_rss_with_auth_token(self, backend):
36 auth_token = User.get_first_admin().feed_token
36 auth_token = User.get_first_super_admin().feed_token
37 37 assert auth_token != ''
38 38 response = self.app.get(url(controller='feed', action='rss',
39 39 repo_name=backend.repo_name, auth_token=auth_token))
40 40
41 41 assert response.content_type == "application/rss+xml"
42 42 assert """<rss version="2.0">""" in response
43 43
44 44 def test_atom(self, backend):
45 45 self.log_user()
46 46 response = self.app.get(url(controller='feed', action='atom',
47 47 repo_name=backend.repo_name))
48 48
49 49 assert response.content_type == """application/atom+xml"""
50 50 assert """<?xml version="1.0" encoding="utf-8"?>""" in response
51 51
52 52 tag1 = '<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-us">'
53 53 tag2 = '<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom">'
54 54 assert tag1 in response or tag2 in response
@@ -1,519 +1,519 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import urlparse
22 22
23 23 import mock
24 24 import pytest
25 25
26 26 from rhodecode.config.routing import ADMIN_PREFIX
27 27 from rhodecode.tests import (
28 28 assert_session_flash, url, HG_REPO, TEST_USER_ADMIN_LOGIN)
29 29 from rhodecode.tests.fixture import Fixture
30 30 from rhodecode.tests.utils import AssertResponse, get_session_from_response
31 31 from rhodecode.lib.auth import check_password, generate_auth_token
32 32 from rhodecode.lib import helpers as h
33 33 from rhodecode.model.auth_token import AuthTokenModel
34 34 from rhodecode.model import validators
35 35 from rhodecode.model.db import User, Notification
36 36 from rhodecode.model.meta import Session
37 37
38 38 fixture = Fixture()
39 39
40 40 # Hardcode URLs because we don't have a request object to use
41 41 # pyramids URL generation methods.
42 42 login_url = ADMIN_PREFIX + '/login'
43 43 logut_url = ADMIN_PREFIX + '/logout'
44 44 register_url = ADMIN_PREFIX + '/register'
45 45 pwd_reset_url = ADMIN_PREFIX + '/password_reset'
46 46 pwd_reset_confirm_url = ADMIN_PREFIX + '/password_reset_confirmation'
47 47
48 48
49 49 @pytest.mark.usefixtures('app')
50 50 class TestLoginController:
51 51 destroy_users = set()
52 52
53 53 @classmethod
54 54 def teardown_class(cls):
55 55 fixture.destroy_users(cls.destroy_users)
56 56
57 57 def teardown_method(self, method):
58 58 for n in Notification.query().all():
59 59 Session().delete(n)
60 60
61 61 Session().commit()
62 62 assert Notification.query().all() == []
63 63
64 64 def test_index(self):
65 65 response = self.app.get(login_url)
66 66 assert response.status == '200 OK'
67 67 # Test response...
68 68
69 69 def test_login_admin_ok(self):
70 70 response = self.app.post(login_url,
71 71 {'username': 'test_admin',
72 72 'password': 'test12'})
73 73 assert response.status == '302 Found'
74 74 session = get_session_from_response(response)
75 75 username = session['rhodecode_user'].get('username')
76 76 assert username == 'test_admin'
77 77 response = response.follow()
78 78 response.mustcontain('/%s' % HG_REPO)
79 79
80 80 def test_login_regular_ok(self):
81 81 response = self.app.post(login_url,
82 82 {'username': 'test_regular',
83 83 'password': 'test12'})
84 84
85 85 assert response.status == '302 Found'
86 86 session = get_session_from_response(response)
87 87 username = session['rhodecode_user'].get('username')
88 88 assert username == 'test_regular'
89 89 response = response.follow()
90 90 response.mustcontain('/%s' % HG_REPO)
91 91
92 92 def test_login_ok_came_from(self):
93 93 test_came_from = '/_admin/users?branch=stable'
94 94 _url = '{}?came_from={}'.format(login_url, test_came_from)
95 95 response = self.app.post(
96 96 _url, {'username': 'test_admin', 'password': 'test12'})
97 97 assert response.status == '302 Found'
98 98 assert 'branch=stable' in response.location
99 99 response = response.follow()
100 100
101 101 assert response.status == '200 OK'
102 102 response.mustcontain('Users administration')
103 103
104 104 def test_redirect_to_login_with_get_args(self):
105 105 with fixture.anon_access(False):
106 106 kwargs = {'branch': 'stable'}
107 107 response = self.app.get(
108 108 url('summary_home', repo_name=HG_REPO, **kwargs))
109 109 assert response.status == '302 Found'
110 110 response_query = urlparse.parse_qsl(response.location)
111 111 assert 'branch=stable' in response_query[0][1]
112 112
113 113 def test_login_form_with_get_args(self):
114 114 _url = '{}?came_from=/_admin/users,branch=stable'.format(login_url)
115 115 response = self.app.get(_url)
116 116 assert 'branch%3Dstable' in response.form.action
117 117
118 118 @pytest.mark.parametrize("url_came_from", [
119 119 'data:text/html,<script>window.alert("xss")</script>',
120 120 'mailto:test@rhodecode.org',
121 121 'file:///etc/passwd',
122 122 'ftp://some.ftp.server',
123 123 'http://other.domain',
124 124 '/\r\nX-Forwarded-Host: http://example.org',
125 125 ])
126 126 def test_login_bad_came_froms(self, url_came_from):
127 127 _url = '{}?came_from={}'.format(login_url, url_came_from)
128 128 response = self.app.post(
129 129 _url,
130 130 {'username': 'test_admin', 'password': 'test12'})
131 131 assert response.status == '302 Found'
132 132 response = response.follow()
133 133 assert response.status == '200 OK'
134 134 assert response.request.path == '/'
135 135
136 136 def test_login_short_password(self):
137 137 response = self.app.post(login_url,
138 138 {'username': 'test_admin',
139 139 'password': 'as'})
140 140 assert response.status == '200 OK'
141 141
142 142 response.mustcontain('Enter 3 characters or more')
143 143
144 144 def test_login_wrong_non_ascii_password(self, user_regular):
145 145 response = self.app.post(
146 146 login_url,
147 147 {'username': user_regular.username,
148 148 'password': u'invalid-non-asci\xe4'.encode('utf8')})
149 149
150 150 response.mustcontain('invalid user name')
151 151 response.mustcontain('invalid password')
152 152
153 153 def test_login_with_non_ascii_password(self, user_util):
154 154 password = u'valid-non-ascii\xe4'
155 155 user = user_util.create_user(password=password)
156 156 response = self.app.post(
157 157 login_url,
158 158 {'username': user.username,
159 159 'password': password.encode('utf-8')})
160 160 assert response.status_code == 302
161 161
162 162 def test_login_wrong_username_password(self):
163 163 response = self.app.post(login_url,
164 164 {'username': 'error',
165 165 'password': 'test12'})
166 166
167 167 response.mustcontain('invalid user name')
168 168 response.mustcontain('invalid password')
169 169
170 170 def test_login_admin_ok_password_migration(self, real_crypto_backend):
171 171 from rhodecode.lib import auth
172 172
173 173 # create new user, with sha256 password
174 174 temp_user = 'test_admin_sha256'
175 175 user = fixture.create_user(temp_user)
176 176 user.password = auth._RhodeCodeCryptoSha256().hash_create(
177 177 b'test123')
178 178 Session().add(user)
179 179 Session().commit()
180 180 self.destroy_users.add(temp_user)
181 181 response = self.app.post(login_url,
182 182 {'username': temp_user,
183 183 'password': 'test123'})
184 184
185 185 assert response.status == '302 Found'
186 186 session = get_session_from_response(response)
187 187 username = session['rhodecode_user'].get('username')
188 188 assert username == temp_user
189 189 response = response.follow()
190 190 response.mustcontain('/%s' % HG_REPO)
191 191
192 192 # new password should be bcrypted, after log-in and transfer
193 193 user = User.get_by_username(temp_user)
194 194 assert user.password.startswith('$')
195 195
196 196 # REGISTRATIONS
197 197 def test_register(self):
198 198 response = self.app.get(register_url)
199 199 response.mustcontain('Create an Account')
200 200
201 201 def test_register_err_same_username(self):
202 202 uname = 'test_admin'
203 203 response = self.app.post(
204 204 register_url,
205 205 {
206 206 'username': uname,
207 207 'password': 'test12',
208 208 'password_confirmation': 'test12',
209 209 'email': 'goodmail@domain.com',
210 210 'firstname': 'test',
211 211 'lastname': 'test'
212 212 }
213 213 )
214 214
215 215 assertr = AssertResponse(response)
216 216 msg = validators.ValidUsername()._messages['username_exists']
217 217 msg = msg % {'username': uname}
218 218 assertr.element_contains('#username+.error-message', msg)
219 219
220 220 def test_register_err_same_email(self):
221 221 response = self.app.post(
222 222 register_url,
223 223 {
224 224 'username': 'test_admin_0',
225 225 'password': 'test12',
226 226 'password_confirmation': 'test12',
227 227 'email': 'test_admin@mail.com',
228 228 'firstname': 'test',
229 229 'lastname': 'test'
230 230 }
231 231 )
232 232
233 233 assertr = AssertResponse(response)
234 234 msg = validators.UniqSystemEmail()()._messages['email_taken']
235 235 assertr.element_contains('#email+.error-message', msg)
236 236
237 237 def test_register_err_same_email_case_sensitive(self):
238 238 response = self.app.post(
239 239 register_url,
240 240 {
241 241 'username': 'test_admin_1',
242 242 'password': 'test12',
243 243 'password_confirmation': 'test12',
244 244 'email': 'TesT_Admin@mail.COM',
245 245 'firstname': 'test',
246 246 'lastname': 'test'
247 247 }
248 248 )
249 249 assertr = AssertResponse(response)
250 250 msg = validators.UniqSystemEmail()()._messages['email_taken']
251 251 assertr.element_contains('#email+.error-message', msg)
252 252
253 253 def test_register_err_wrong_data(self):
254 254 response = self.app.post(
255 255 register_url,
256 256 {
257 257 'username': 'xs',
258 258 'password': 'test',
259 259 'password_confirmation': 'test',
260 260 'email': 'goodmailm',
261 261 'firstname': 'test',
262 262 'lastname': 'test'
263 263 }
264 264 )
265 265 assert response.status == '200 OK'
266 266 response.mustcontain('An email address must contain a single @')
267 267 response.mustcontain('Enter a value 6 characters long or more')
268 268
269 269 def test_register_err_username(self):
270 270 response = self.app.post(
271 271 register_url,
272 272 {
273 273 'username': 'error user',
274 274 'password': 'test12',
275 275 'password_confirmation': 'test12',
276 276 'email': 'goodmailm',
277 277 'firstname': 'test',
278 278 'lastname': 'test'
279 279 }
280 280 )
281 281
282 282 response.mustcontain('An email address must contain a single @')
283 283 response.mustcontain(
284 284 'Username may only contain '
285 285 'alphanumeric characters underscores, '
286 286 'periods or dashes and must begin with '
287 287 'alphanumeric character')
288 288
289 289 def test_register_err_case_sensitive(self):
290 290 usr = 'Test_Admin'
291 291 response = self.app.post(
292 292 register_url,
293 293 {
294 294 'username': usr,
295 295 'password': 'test12',
296 296 'password_confirmation': 'test12',
297 297 'email': 'goodmailm',
298 298 'firstname': 'test',
299 299 'lastname': 'test'
300 300 }
301 301 )
302 302
303 303 assertr = AssertResponse(response)
304 304 msg = validators.ValidUsername()._messages['username_exists']
305 305 msg = msg % {'username': usr}
306 306 assertr.element_contains('#username+.error-message', msg)
307 307
308 308 def test_register_special_chars(self):
309 309 response = self.app.post(
310 310 register_url,
311 311 {
312 312 'username': 'xxxaxn',
313 313 'password': 'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
314 314 'password_confirmation': 'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
315 315 'email': 'goodmailm@test.plx',
316 316 'firstname': 'test',
317 317 'lastname': 'test'
318 318 }
319 319 )
320 320
321 321 msg = validators.ValidPassword()._messages['invalid_password']
322 322 response.mustcontain(msg)
323 323
324 324 def test_register_password_mismatch(self):
325 325 response = self.app.post(
326 326 register_url,
327 327 {
328 328 'username': 'xs',
329 329 'password': '123qwe',
330 330 'password_confirmation': 'qwe123',
331 331 'email': 'goodmailm@test.plxa',
332 332 'firstname': 'test',
333 333 'lastname': 'test'
334 334 }
335 335 )
336 336 msg = validators.ValidPasswordsMatch()._messages['password_mismatch']
337 337 response.mustcontain(msg)
338 338
339 339 def test_register_ok(self):
340 340 username = 'test_regular4'
341 341 password = 'qweqwe'
342 342 email = 'marcin@test.com'
343 343 name = 'testname'
344 344 lastname = 'testlastname'
345 345
346 346 response = self.app.post(
347 347 register_url,
348 348 {
349 349 'username': username,
350 350 'password': password,
351 351 'password_confirmation': password,
352 352 'email': email,
353 353 'firstname': name,
354 354 'lastname': lastname,
355 355 'admin': True
356 356 }
357 357 ) # This should be overriden
358 358 assert response.status == '302 Found'
359 359 assert_session_flash(
360 360 response, 'You have successfully registered with RhodeCode')
361 361
362 362 ret = Session().query(User).filter(
363 363 User.username == 'test_regular4').one()
364 364 assert ret.username == username
365 365 assert check_password(password, ret.password)
366 366 assert ret.email == email
367 367 assert ret.name == name
368 368 assert ret.lastname == lastname
369 369 assert ret.api_key is not None
370 370 assert not ret.admin
371 371
372 372 def test_forgot_password_wrong_mail(self):
373 373 bad_email = 'marcin@wrongmail.org'
374 374 response = self.app.post(
375 375 pwd_reset_url,
376 376 {'email': bad_email, }
377 377 )
378 378
379 379 msg = validators.ValidSystemEmail()._messages['non_existing_email']
380 380 msg = h.html_escape(msg % {'email': bad_email})
381 381 response.mustcontain()
382 382
383 383 def test_forgot_password(self):
384 384 response = self.app.get(pwd_reset_url)
385 385 assert response.status == '200 OK'
386 386
387 387 username = 'test_password_reset_1'
388 388 password = 'qweqwe'
389 389 email = 'marcin@python-works.com'
390 390 name = 'passwd'
391 391 lastname = 'reset'
392 392
393 393 new = User()
394 394 new.username = username
395 395 new.password = password
396 396 new.email = email
397 397 new.name = name
398 398 new.lastname = lastname
399 399 new.api_key = generate_auth_token(username)
400 400 Session().add(new)
401 401 Session().commit()
402 402
403 403 response = self.app.post(pwd_reset_url,
404 404 {'email': email, })
405 405
406 406 assert_session_flash(
407 407 response, 'Your password reset link was sent')
408 408
409 409 response = response.follow()
410 410
411 411 # BAD KEY
412 412
413 413 key = "bad"
414 414 confirm_url = '{}?key={}'.format(pwd_reset_confirm_url, key)
415 415 response = self.app.get(confirm_url)
416 416 assert response.status == '302 Found'
417 417 assert response.location.endswith(pwd_reset_url)
418 418
419 419 # GOOD KEY
420 420
421 421 key = User.get_by_username(username).api_key
422 422 confirm_url = '{}?key={}'.format(pwd_reset_confirm_url, key)
423 423 response = self.app.get(confirm_url)
424 424 assert response.status == '302 Found'
425 425 assert response.location.endswith(login_url)
426 426
427 427 assert_session_flash(
428 428 response,
429 429 'Your password reset was successful, '
430 430 'a new password has been sent to your email')
431 431
432 432 response = response.follow()
433 433
434 434 def _get_api_whitelist(self, values=None):
435 435 config = {'api_access_controllers_whitelist': values or []}
436 436 return config
437 437
438 438 @pytest.mark.parametrize("test_name, auth_token", [
439 439 ('none', None),
440 440 ('empty_string', ''),
441 441 ('fake_number', '123456'),
442 442 ('proper_auth_token', None)
443 443 ])
444 444 def test_access_not_whitelisted_page_via_auth_token(self, test_name,
445 445 auth_token):
446 446 whitelist = self._get_api_whitelist([])
447 447 with mock.patch.dict('rhodecode.CONFIG', whitelist):
448 448 assert [] == whitelist['api_access_controllers_whitelist']
449 449 if test_name == 'proper_auth_token':
450 450 # use builtin if api_key is None
451 auth_token = User.get_first_admin().api_key
451 auth_token = User.get_first_super_admin().api_key
452 452
453 453 with fixture.anon_access(False):
454 454 self.app.get(url(controller='changeset',
455 455 action='changeset_raw',
456 456 repo_name=HG_REPO, revision='tip',
457 457 api_key=auth_token),
458 458 status=302)
459 459
460 460 @pytest.mark.parametrize("test_name, auth_token, code", [
461 461 ('none', None, 302),
462 462 ('empty_string', '', 302),
463 463 ('fake_number', '123456', 302),
464 464 ('proper_auth_token', None, 200)
465 465 ])
466 466 def test_access_whitelisted_page_via_auth_token(self, test_name,
467 467 auth_token, code):
468 468 whitelist = self._get_api_whitelist(
469 469 ['ChangesetController:changeset_raw'])
470 470 with mock.patch.dict('rhodecode.CONFIG', whitelist):
471 471 assert ['ChangesetController:changeset_raw'] == \
472 472 whitelist['api_access_controllers_whitelist']
473 473 if test_name == 'proper_auth_token':
474 auth_token = User.get_first_admin().api_key
474 auth_token = User.get_first_super_admin().api_key
475 475
476 476 with fixture.anon_access(False):
477 477 self.app.get(url(controller='changeset',
478 478 action='changeset_raw',
479 479 repo_name=HG_REPO, revision='tip',
480 480 api_key=auth_token),
481 481 status=code)
482 482
483 483 def test_access_page_via_extra_auth_token(self):
484 484 whitelist = self._get_api_whitelist(
485 485 ['ChangesetController:changeset_raw'])
486 486 with mock.patch.dict('rhodecode.CONFIG', whitelist):
487 487 assert ['ChangesetController:changeset_raw'] == \
488 488 whitelist['api_access_controllers_whitelist']
489 489
490 490 new_auth_token = AuthTokenModel().create(
491 491 TEST_USER_ADMIN_LOGIN, 'test')
492 492 Session().commit()
493 493 with fixture.anon_access(False):
494 494 self.app.get(url(controller='changeset',
495 495 action='changeset_raw',
496 496 repo_name=HG_REPO, revision='tip',
497 497 api_key=new_auth_token.api_key),
498 498 status=200)
499 499
500 500 def test_access_page_via_expired_auth_token(self):
501 501 whitelist = self._get_api_whitelist(
502 502 ['ChangesetController:changeset_raw'])
503 503 with mock.patch.dict('rhodecode.CONFIG', whitelist):
504 504 assert ['ChangesetController:changeset_raw'] == \
505 505 whitelist['api_access_controllers_whitelist']
506 506
507 507 new_auth_token = AuthTokenModel().create(
508 508 TEST_USER_ADMIN_LOGIN, 'test')
509 509 Session().commit()
510 510 # patch the api key and make it expired
511 511 new_auth_token.expires = 0
512 512 Session().add(new_auth_token)
513 513 Session().commit()
514 514 with fixture.anon_access(False):
515 515 self.app.get(url(controller='changeset',
516 516 action='changeset_raw',
517 517 repo_name=HG_REPO, revision='tip',
518 518 api_key=new_auth_token.api_key),
519 519 status=302)
General Comments 0
You need to be logged in to leave comments. Login now