##// END OF EJS Templates
Turbogears2 migration: remove some references to Pylons in comments...
Thomas De Schampheleire -
r6178:5eec7942 default
parent child Browse files
Show More
@@ -1,63 +1,63 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea
16 16 ~~~~~~~~~
17 17
18 Kallithea, a web based repository management based on pylons
19 versioning implementation: http://www.python.org/dev/peps/pep-0386/
18 Kallithea, a web based repository management system.
19
20 Versioning implementation: http://www.python.org/dev/peps/pep-0386/
20 21
21 22 This file was forked by the Kallithea project in July 2014.
22 23 Original author and date, and relevant copyright and licensing information is below:
23 24 :created_on: Apr 9, 2010
24 25 :author: marcink
25 26 :copyright: (c) 2013 RhodeCode GmbH, (C) 2014 Bradley M. Kuhn, and others.
26 27 :license: GPLv3, see LICENSE.md for more details.
27 28 """
28 29
29 30 import sys
30 31 import platform
31 32
32 33 VERSION = (0, 3, 99)
33 34 BACKENDS = {
34 35 'hg': 'Mercurial repository',
35 36 'git': 'Git repository',
36 37 }
37 38
38 39 CELERY_ON = False
39 40 CELERY_EAGER = False
40 41
41 # link to config for pylons
42 42 CONFIG = {}
43 43
44 44 # Linked module for extensions
45 45 EXTENSIONS = {}
46 46
47 47 try:
48 48 import kallithea.brand
49 49 except ImportError:
50 50 pass
51 51 else:
52 52 assert False, 'Database rebranding is no longer supported; see README.'
53 53
54 54
55 55 __version__ = '.'.join(str(each) for each in VERSION)
56 56 __platform__ = platform.system()
57 57 __license__ = 'GPLv3'
58 58 __py_version__ = sys.version_info
59 59 __author__ = "Various Authors"
60 60 __url__ = 'https://kallithea-scm.org/'
61 61
62 62 is_windows = __platform__ in ['Windows']
63 63 is_unix = not is_windows
@@ -1,428 +1,428 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.admin.user_groups
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 User Groups crud controller for pylons
18 User Groups crud controller
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Jan 25, 2011
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import traceback
30 30 import formencode
31 31
32 32 from formencode import htmlfill
33 33 from pylons import request, tmpl_context as c, url, config
34 34 from pylons.i18n.translation import _
35 35 from webob.exc import HTTPFound
36 36
37 37 from sqlalchemy.orm import joinedload
38 38 from sqlalchemy.sql.expression import func
39 39 from webob.exc import HTTPInternalServerError
40 40
41 41 import kallithea
42 42 from kallithea.lib import helpers as h
43 43 from kallithea.lib.exceptions import UserGroupsAssignedException, \
44 44 RepoGroupAssignmentError
45 45 from kallithea.lib.utils2 import safe_unicode, safe_int
46 46 from kallithea.lib.auth import LoginRequired, \
47 47 HasUserGroupPermissionAnyDecorator, HasPermissionAnyDecorator
48 48 from kallithea.lib.base import BaseController, render
49 49 from kallithea.model.scm import UserGroupList
50 50 from kallithea.model.user_group import UserGroupModel
51 51 from kallithea.model.repo import RepoModel
52 52 from kallithea.model.db import User, UserGroup, UserGroupToPerm, \
53 53 UserGroupRepoToPerm, UserGroupRepoGroupToPerm
54 54 from kallithea.model.forms import UserGroupForm, UserGroupPermsForm, \
55 55 CustomDefaultPermissionsForm
56 56 from kallithea.model.meta import Session
57 57 from kallithea.lib.utils import action_logger
58 58 from kallithea.lib.compat import json
59 59
60 60 log = logging.getLogger(__name__)
61 61
62 62
63 63 class UserGroupsController(BaseController):
64 64 """REST Controller styled on the Atom Publishing Protocol"""
65 65
66 66 @LoginRequired()
67 67 def __before__(self):
68 68 super(UserGroupsController, self).__before__()
69 69 c.available_permissions = config['available_permissions']
70 70
71 71 def __load_data(self, user_group_id):
72 72 c.group_members_obj = sorted((x.user for x in c.user_group.members),
73 73 key=lambda u: u.username.lower())
74 74
75 75 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
76 76 c.available_members = sorted(((x.user_id, x.username) for x in
77 77 User.query().all()),
78 78 key=lambda u: u[1].lower())
79 79
80 80 def __load_defaults(self, user_group_id):
81 81 """
82 82 Load defaults settings for edit, and update
83 83
84 84 :param user_group_id:
85 85 """
86 86 user_group = UserGroup.get_or_404(user_group_id)
87 87 data = user_group.get_dict()
88 88 return data
89 89
90 90 def index(self, format='html'):
91 91 _list = UserGroup.query() \
92 92 .order_by(func.lower(UserGroup.users_group_name)) \
93 93 .all()
94 94 group_iter = UserGroupList(_list, perm_set=['usergroup.admin'])
95 95 user_groups_data = []
96 96 total_records = len(group_iter)
97 97 _tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
98 98 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
99 99
100 100 user_group_name = lambda user_group_id, user_group_name: (
101 101 template.get_def("user_group_name")
102 102 .render(user_group_id, user_group_name, _=_, h=h, c=c)
103 103 )
104 104 user_group_actions = lambda user_group_id, user_group_name: (
105 105 template.get_def("user_group_actions")
106 106 .render(user_group_id, user_group_name, _=_, h=h, c=c)
107 107 )
108 108 for user_gr in group_iter:
109 109
110 110 user_groups_data.append({
111 111 "raw_name": user_gr.users_group_name,
112 112 "group_name": user_group_name(user_gr.users_group_id,
113 113 user_gr.users_group_name),
114 114 "desc": h.escape(user_gr.user_group_description),
115 115 "members": len(user_gr.members),
116 116 "active": h.boolicon(user_gr.users_group_active),
117 117 "owner": h.person(user_gr.user.username),
118 118 "action": user_group_actions(user_gr.users_group_id, user_gr.users_group_name)
119 119 })
120 120
121 121 c.data = json.dumps({
122 122 "totalRecords": total_records,
123 123 "startIndex": 0,
124 124 "sort": None,
125 125 "dir": "asc",
126 126 "records": user_groups_data
127 127 })
128 128
129 129 return render('admin/user_groups/user_groups.html')
130 130
131 131 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
132 132 def create(self):
133 133 users_group_form = UserGroupForm()()
134 134 try:
135 135 form_result = users_group_form.to_python(dict(request.POST))
136 136 ug = UserGroupModel().create(name=form_result['users_group_name'],
137 137 description=form_result['user_group_description'],
138 138 owner=self.authuser.user_id,
139 139 active=form_result['users_group_active'])
140 140
141 141 gr = form_result['users_group_name']
142 142 action_logger(self.authuser,
143 143 'admin_created_users_group:%s' % gr,
144 144 None, self.ip_addr, self.sa)
145 145 h.flash(h.literal(_('Created user group %s') % h.link_to(h.escape(gr), url('edit_users_group', id=ug.users_group_id))),
146 146 category='success')
147 147 Session().commit()
148 148 except formencode.Invalid as errors:
149 149 return htmlfill.render(
150 150 render('admin/user_groups/user_group_add.html'),
151 151 defaults=errors.value,
152 152 errors=errors.error_dict or {},
153 153 prefix_error=False,
154 154 encoding="UTF-8",
155 155 force_defaults=False)
156 156 except Exception:
157 157 log.error(traceback.format_exc())
158 158 h.flash(_('Error occurred during creation of user group %s') \
159 159 % request.POST.get('users_group_name'), category='error')
160 160
161 161 raise HTTPFound(location=url('users_groups'))
162 162
163 163 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
164 164 def new(self, format='html'):
165 165 return render('admin/user_groups/user_group_add.html')
166 166
167 167 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
168 168 def update(self, id):
169 169 c.user_group = UserGroup.get_or_404(id)
170 170 c.active = 'settings'
171 171 self.__load_data(id)
172 172
173 173 available_members = [safe_unicode(x[0]) for x in c.available_members]
174 174
175 175 users_group_form = UserGroupForm(edit=True,
176 176 old_data=c.user_group.get_dict(),
177 177 available_members=available_members)()
178 178
179 179 try:
180 180 form_result = users_group_form.to_python(request.POST)
181 181 UserGroupModel().update(c.user_group, form_result)
182 182 gr = form_result['users_group_name']
183 183 action_logger(self.authuser,
184 184 'admin_updated_users_group:%s' % gr,
185 185 None, self.ip_addr, self.sa)
186 186 h.flash(_('Updated user group %s') % gr, category='success')
187 187 Session().commit()
188 188 except formencode.Invalid as errors:
189 189 ug_model = UserGroupModel()
190 190 defaults = errors.value
191 191 e = errors.error_dict or {}
192 192 defaults.update({
193 193 'create_repo_perm': ug_model.has_perm(id,
194 194 'hg.create.repository'),
195 195 'fork_repo_perm': ug_model.has_perm(id,
196 196 'hg.fork.repository'),
197 197 })
198 198
199 199 return htmlfill.render(
200 200 render('admin/user_groups/user_group_edit.html'),
201 201 defaults=defaults,
202 202 errors=e,
203 203 prefix_error=False,
204 204 encoding="UTF-8",
205 205 force_defaults=False)
206 206 except Exception:
207 207 log.error(traceback.format_exc())
208 208 h.flash(_('Error occurred during update of user group %s') \
209 209 % request.POST.get('users_group_name'), category='error')
210 210
211 211 raise HTTPFound(location=url('edit_users_group', id=id))
212 212
213 213 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
214 214 def delete(self, id):
215 215 usr_gr = UserGroup.get_or_404(id)
216 216 try:
217 217 UserGroupModel().delete(usr_gr)
218 218 Session().commit()
219 219 h.flash(_('Successfully deleted user group'), category='success')
220 220 except UserGroupsAssignedException as e:
221 221 h.flash(e, category='error')
222 222 except Exception:
223 223 log.error(traceback.format_exc())
224 224 h.flash(_('An error occurred during deletion of user group'),
225 225 category='error')
226 226 raise HTTPFound(location=url('users_groups'))
227 227
228 228 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
229 229 def edit(self, id, format='html'):
230 230 c.user_group = UserGroup.get_or_404(id)
231 231 c.active = 'settings'
232 232 self.__load_data(id)
233 233
234 234 defaults = self.__load_defaults(id)
235 235
236 236 return htmlfill.render(
237 237 render('admin/user_groups/user_group_edit.html'),
238 238 defaults=defaults,
239 239 encoding="UTF-8",
240 240 force_defaults=False
241 241 )
242 242
243 243 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
244 244 def edit_perms(self, id):
245 245 c.user_group = UserGroup.get_or_404(id)
246 246 c.active = 'perms'
247 247
248 248 repo_model = RepoModel()
249 249 c.users_array = repo_model.get_users_js()
250 250 c.user_groups_array = repo_model.get_user_groups_js()
251 251
252 252 defaults = {}
253 253 # fill user group users
254 254 for p in c.user_group.user_user_group_to_perm:
255 255 defaults.update({'u_perm_%s' % p.user.username:
256 256 p.permission.permission_name})
257 257
258 258 for p in c.user_group.user_group_user_group_to_perm:
259 259 defaults.update({'g_perm_%s' % p.user_group.users_group_name:
260 260 p.permission.permission_name})
261 261
262 262 return htmlfill.render(
263 263 render('admin/user_groups/user_group_edit.html'),
264 264 defaults=defaults,
265 265 encoding="UTF-8",
266 266 force_defaults=False
267 267 )
268 268
269 269 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
270 270 def update_perms(self, id):
271 271 """
272 272 grant permission for given usergroup
273 273
274 274 :param id:
275 275 """
276 276 user_group = UserGroup.get_or_404(id)
277 277 form = UserGroupPermsForm()().to_python(request.POST)
278 278
279 279 # set the permissions !
280 280 try:
281 281 UserGroupModel()._update_permissions(user_group, form['perms_new'],
282 282 form['perms_updates'])
283 283 except RepoGroupAssignmentError:
284 284 h.flash(_('Target group cannot be the same'), category='error')
285 285 raise HTTPFound(location=url('edit_user_group_perms', id=id))
286 286 #TODO: implement this
287 287 #action_logger(self.authuser, 'admin_changed_repo_permissions',
288 288 # repo_name, self.ip_addr, self.sa)
289 289 Session().commit()
290 290 h.flash(_('User group permissions updated'), category='success')
291 291 raise HTTPFound(location=url('edit_user_group_perms', id=id))
292 292
293 293 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
294 294 def delete_perms(self, id):
295 295 try:
296 296 obj_type = request.POST.get('obj_type')
297 297 obj_id = None
298 298 if obj_type == 'user':
299 299 obj_id = safe_int(request.POST.get('user_id'))
300 300 elif obj_type == 'user_group':
301 301 obj_id = safe_int(request.POST.get('user_group_id'))
302 302
303 303 if not c.authuser.is_admin:
304 304 if obj_type == 'user' and c.authuser.user_id == obj_id:
305 305 msg = _('Cannot revoke permission for yourself as admin')
306 306 h.flash(msg, category='warning')
307 307 raise Exception('revoke admin permission on self')
308 308 if obj_type == 'user':
309 309 UserGroupModel().revoke_user_permission(user_group=id,
310 310 user=obj_id)
311 311 elif obj_type == 'user_group':
312 312 UserGroupModel().revoke_user_group_permission(target_user_group=id,
313 313 user_group=obj_id)
314 314 Session().commit()
315 315 except Exception:
316 316 log.error(traceback.format_exc())
317 317 h.flash(_('An error occurred during revoking of permission'),
318 318 category='error')
319 319 raise HTTPInternalServerError()
320 320
321 321 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
322 322 def edit_default_perms(self, id):
323 323 c.user_group = UserGroup.get_or_404(id)
324 324 c.active = 'default_perms'
325 325
326 326 permissions = {
327 327 'repositories': {},
328 328 'repositories_groups': {}
329 329 }
330 330 ugroup_repo_perms = UserGroupRepoToPerm.query() \
331 331 .options(joinedload(UserGroupRepoToPerm.permission)) \
332 332 .options(joinedload(UserGroupRepoToPerm.repository)) \
333 333 .filter(UserGroupRepoToPerm.users_group_id == id) \
334 334 .all()
335 335
336 336 for gr in ugroup_repo_perms:
337 337 permissions['repositories'][gr.repository.repo_name] \
338 338 = gr.permission.permission_name
339 339
340 340 ugroup_group_perms = UserGroupRepoGroupToPerm.query() \
341 341 .options(joinedload(UserGroupRepoGroupToPerm.permission)) \
342 342 .options(joinedload(UserGroupRepoGroupToPerm.group)) \
343 343 .filter(UserGroupRepoGroupToPerm.users_group_id == id) \
344 344 .all()
345 345
346 346 for gr in ugroup_group_perms:
347 347 permissions['repositories_groups'][gr.group.group_name] \
348 348 = gr.permission.permission_name
349 349 c.permissions = permissions
350 350
351 351 ug_model = UserGroupModel()
352 352
353 353 defaults = c.user_group.get_dict()
354 354 defaults.update({
355 355 'create_repo_perm': ug_model.has_perm(c.user_group,
356 356 'hg.create.repository'),
357 357 'create_user_group_perm': ug_model.has_perm(c.user_group,
358 358 'hg.usergroup.create.true'),
359 359 'fork_repo_perm': ug_model.has_perm(c.user_group,
360 360 'hg.fork.repository'),
361 361 })
362 362
363 363 return htmlfill.render(
364 364 render('admin/user_groups/user_group_edit.html'),
365 365 defaults=defaults,
366 366 encoding="UTF-8",
367 367 force_defaults=False
368 368 )
369 369
370 370 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
371 371 def update_default_perms(self, id):
372 372 user_group = UserGroup.get_or_404(id)
373 373
374 374 try:
375 375 form = CustomDefaultPermissionsForm()()
376 376 form_result = form.to_python(request.POST)
377 377
378 378 inherit_perms = form_result['inherit_default_permissions']
379 379 user_group.inherit_default_permissions = inherit_perms
380 380 Session().add(user_group)
381 381 usergroup_model = UserGroupModel()
382 382
383 383 defs = UserGroupToPerm.query() \
384 384 .filter(UserGroupToPerm.users_group == user_group) \
385 385 .all()
386 386 for ug in defs:
387 387 Session().delete(ug)
388 388
389 389 if form_result['create_repo_perm']:
390 390 usergroup_model.grant_perm(id, 'hg.create.repository')
391 391 else:
392 392 usergroup_model.grant_perm(id, 'hg.create.none')
393 393 if form_result['create_user_group_perm']:
394 394 usergroup_model.grant_perm(id, 'hg.usergroup.create.true')
395 395 else:
396 396 usergroup_model.grant_perm(id, 'hg.usergroup.create.false')
397 397 if form_result['fork_repo_perm']:
398 398 usergroup_model.grant_perm(id, 'hg.fork.repository')
399 399 else:
400 400 usergroup_model.grant_perm(id, 'hg.fork.none')
401 401
402 402 h.flash(_("Updated permissions"), category='success')
403 403 Session().commit()
404 404 except Exception:
405 405 log.error(traceback.format_exc())
406 406 h.flash(_('An error occurred during permissions saving'),
407 407 category='error')
408 408
409 409 raise HTTPFound(location=url('edit_user_group_default_perms', id=id))
410 410
411 411 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
412 412 def edit_advanced(self, id):
413 413 c.user_group = UserGroup.get_or_404(id)
414 414 c.active = 'advanced'
415 415 c.group_members_obj = sorted((x.user for x in c.user_group.members),
416 416 key=lambda u: u.username.lower())
417 417 return render('admin/user_groups/user_group_edit.html')
418 418
419 419
420 420 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
421 421 def edit_members(self, id):
422 422 c.user_group = UserGroup.get_or_404(id)
423 423 c.active = 'members'
424 424 c.group_members_obj = sorted((x.user for x in c.user_group.members),
425 425 key=lambda u: u.username.lower())
426 426
427 427 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
428 428 return render('admin/user_groups/user_group_edit.html')
@@ -1,443 +1,443 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.admin.users
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 Users crud controller for pylons
18 Users crud controller
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 4, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import traceback
30 30 import formencode
31 31
32 32 from formencode import htmlfill
33 33 from pylons import request, tmpl_context as c, url, config
34 34 from pylons.i18n.translation import _
35 35 from sqlalchemy.sql.expression import func
36 36 from webob.exc import HTTPFound, HTTPNotFound
37 37
38 38 import kallithea
39 39 from kallithea.lib.exceptions import DefaultUserException, \
40 40 UserOwnsReposException, UserCreationError
41 41 from kallithea.lib import helpers as h
42 42 from kallithea.lib.auth import LoginRequired, HasPermissionAnyDecorator, \
43 43 AuthUser
44 44 from kallithea.lib import auth_modules
45 45 from kallithea.lib.base import BaseController, render
46 46 from kallithea.model.api_key import ApiKeyModel
47 47
48 48 from kallithea.model.db import User, UserEmailMap, UserIpMap, UserToPerm
49 49 from kallithea.model.forms import UserForm, CustomDefaultPermissionsForm
50 50 from kallithea.model.user import UserModel
51 51 from kallithea.model.meta import Session
52 52 from kallithea.lib.utils import action_logger
53 53 from kallithea.lib.compat import json
54 54 from kallithea.lib.utils2 import datetime_to_time, safe_int, generate_api_key
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 class UsersController(BaseController):
60 60 """REST Controller styled on the Atom Publishing Protocol"""
61 61
62 62 @LoginRequired()
63 63 @HasPermissionAnyDecorator('hg.admin')
64 64 def __before__(self):
65 65 super(UsersController, self).__before__()
66 66 c.available_permissions = config['available_permissions']
67 67
68 68 def index(self, format='html'):
69 69 c.users_list = User.query().order_by(User.username) \
70 70 .filter(User.username != User.DEFAULT_USER) \
71 71 .order_by(func.lower(User.username)) \
72 72 .all()
73 73
74 74 users_data = []
75 75 total_records = len(c.users_list)
76 76 _tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
77 77 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
78 78
79 79 grav_tmpl = '<div class="gravatar">%s</div>'
80 80
81 81 username = lambda user_id, username: (
82 82 template.get_def("user_name")
83 83 .render(user_id, username, _=_, h=h, c=c))
84 84
85 85 user_actions = lambda user_id, username: (
86 86 template.get_def("user_actions")
87 87 .render(user_id, username, _=_, h=h, c=c))
88 88
89 89 for user in c.users_list:
90 90 users_data.append({
91 91 "gravatar": grav_tmpl % h.gravatar(user.email, size=20),
92 92 "raw_name": user.username,
93 93 "username": username(user.user_id, user.username),
94 94 "firstname": h.escape(user.name),
95 95 "lastname": h.escape(user.lastname),
96 96 "last_login": h.fmt_date(user.last_login),
97 97 "last_login_raw": datetime_to_time(user.last_login),
98 98 "active": h.boolicon(user.active),
99 99 "admin": h.boolicon(user.admin),
100 100 "extern_type": user.extern_type,
101 101 "extern_name": user.extern_name,
102 102 "action": user_actions(user.user_id, user.username),
103 103 })
104 104
105 105 c.data = json.dumps({
106 106 "totalRecords": total_records,
107 107 "startIndex": 0,
108 108 "sort": None,
109 109 "dir": "asc",
110 110 "records": users_data
111 111 })
112 112
113 113 return render('admin/users/users.html')
114 114
115 115 def create(self):
116 116 c.default_extern_type = User.DEFAULT_AUTH_TYPE
117 117 c.default_extern_name = ''
118 118 user_model = UserModel()
119 119 user_form = UserForm()()
120 120 try:
121 121 form_result = user_form.to_python(dict(request.POST))
122 122 user = user_model.create(form_result)
123 123 action_logger(self.authuser, 'admin_created_user:%s' % user.username,
124 124 None, self.ip_addr, self.sa)
125 125 h.flash(_('Created user %s') % user.username,
126 126 category='success')
127 127 Session().commit()
128 128 except formencode.Invalid as errors:
129 129 return htmlfill.render(
130 130 render('admin/users/user_add.html'),
131 131 defaults=errors.value,
132 132 errors=errors.error_dict or {},
133 133 prefix_error=False,
134 134 encoding="UTF-8",
135 135 force_defaults=False)
136 136 except UserCreationError as e:
137 137 h.flash(e, 'error')
138 138 except Exception:
139 139 log.error(traceback.format_exc())
140 140 h.flash(_('Error occurred during creation of user %s') \
141 141 % request.POST.get('username'), category='error')
142 142 raise HTTPFound(location=url('edit_user', id=user.user_id))
143 143
144 144 def new(self, format='html'):
145 145 c.default_extern_type = User.DEFAULT_AUTH_TYPE
146 146 c.default_extern_name = ''
147 147 return render('admin/users/user_add.html')
148 148
149 149 def update(self, id):
150 150 user_model = UserModel()
151 151 user = user_model.get(id)
152 152 _form = UserForm(edit=True, old_data={'user_id': id,
153 153 'email': user.email})()
154 154 form_result = {}
155 155 try:
156 156 form_result = _form.to_python(dict(request.POST))
157 157 skip_attrs = ['extern_type', 'extern_name',
158 158 ] + auth_modules.get_managed_fields(user)
159 159
160 160 user_model.update(id, form_result, skip_attrs=skip_attrs)
161 161 usr = form_result['username']
162 162 action_logger(self.authuser, 'admin_updated_user:%s' % usr,
163 163 None, self.ip_addr, self.sa)
164 164 h.flash(_('User updated successfully'), category='success')
165 165 Session().commit()
166 166 except formencode.Invalid as errors:
167 167 defaults = errors.value
168 168 e = errors.error_dict or {}
169 169 defaults.update({
170 170 'create_repo_perm': user_model.has_perm(id,
171 171 'hg.create.repository'),
172 172 'fork_repo_perm': user_model.has_perm(id, 'hg.fork.repository'),
173 173 })
174 174 return htmlfill.render(
175 175 self._render_edit_profile(user),
176 176 defaults=defaults,
177 177 errors=e,
178 178 prefix_error=False,
179 179 encoding="UTF-8",
180 180 force_defaults=False)
181 181 except Exception:
182 182 log.error(traceback.format_exc())
183 183 h.flash(_('Error occurred during update of user %s') \
184 184 % form_result.get('username'), category='error')
185 185 raise HTTPFound(location=url('edit_user', id=id))
186 186
187 187 def delete(self, id):
188 188 usr = User.get_or_404(id)
189 189 try:
190 190 UserModel().delete(usr)
191 191 Session().commit()
192 192 h.flash(_('Successfully deleted user'), category='success')
193 193 except (UserOwnsReposException, DefaultUserException) as e:
194 194 h.flash(e, category='warning')
195 195 except Exception:
196 196 log.error(traceback.format_exc())
197 197 h.flash(_('An error occurred during deletion of user'),
198 198 category='error')
199 199 raise HTTPFound(location=url('users'))
200 200
201 201 def _get_user_or_raise_if_default(self, id):
202 202 try:
203 203 return User.get_or_404(id, allow_default=False)
204 204 except DefaultUserException:
205 205 h.flash(_("The default user cannot be edited"), category='warning')
206 206 raise HTTPNotFound
207 207
208 208 def _render_edit_profile(self, user):
209 209 c.user = user
210 210 c.active = 'profile'
211 211 c.perm_user = AuthUser(dbuser=user)
212 212 c.ip_addr = self.ip_addr
213 213 managed_fields = auth_modules.get_managed_fields(user)
214 214 c.readonly = lambda n: 'readonly' if n in managed_fields else None
215 215 return render('admin/users/user_edit.html')
216 216
217 217 def edit(self, id, format='html'):
218 218 user = self._get_user_or_raise_if_default(id)
219 219 defaults = user.get_dict()
220 220
221 221 return htmlfill.render(
222 222 self._render_edit_profile(user),
223 223 defaults=defaults,
224 224 encoding="UTF-8",
225 225 force_defaults=False)
226 226
227 227 def edit_advanced(self, id):
228 228 c.user = self._get_user_or_raise_if_default(id)
229 229 c.active = 'advanced'
230 230 c.perm_user = AuthUser(user_id=id)
231 231 c.ip_addr = self.ip_addr
232 232
233 233 umodel = UserModel()
234 234 defaults = c.user.get_dict()
235 235 defaults.update({
236 236 'create_repo_perm': umodel.has_perm(c.user, 'hg.create.repository'),
237 237 'create_user_group_perm': umodel.has_perm(c.user,
238 238 'hg.usergroup.create.true'),
239 239 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'),
240 240 })
241 241 return htmlfill.render(
242 242 render('admin/users/user_edit.html'),
243 243 defaults=defaults,
244 244 encoding="UTF-8",
245 245 force_defaults=False)
246 246
247 247 def edit_api_keys(self, id):
248 248 c.user = self._get_user_or_raise_if_default(id)
249 249 c.active = 'api_keys'
250 250 show_expired = True
251 251 c.lifetime_values = [
252 252 (str(-1), _('Forever')),
253 253 (str(5), _('5 minutes')),
254 254 (str(60), _('1 hour')),
255 255 (str(60 * 24), _('1 day')),
256 256 (str(60 * 24 * 30), _('1 month')),
257 257 ]
258 258 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
259 259 c.user_api_keys = ApiKeyModel().get_api_keys(c.user.user_id,
260 260 show_expired=show_expired)
261 261 defaults = c.user.get_dict()
262 262 return htmlfill.render(
263 263 render('admin/users/user_edit.html'),
264 264 defaults=defaults,
265 265 encoding="UTF-8",
266 266 force_defaults=False)
267 267
268 268 def add_api_key(self, id):
269 269 c.user = self._get_user_or_raise_if_default(id)
270 270
271 271 lifetime = safe_int(request.POST.get('lifetime'), -1)
272 272 description = request.POST.get('description')
273 273 ApiKeyModel().create(c.user.user_id, description, lifetime)
274 274 Session().commit()
275 275 h.flash(_("API key successfully created"), category='success')
276 276 raise HTTPFound(location=url('edit_user_api_keys', id=c.user.user_id))
277 277
278 278 def delete_api_key(self, id):
279 279 c.user = self._get_user_or_raise_if_default(id)
280 280
281 281 api_key = request.POST.get('del_api_key')
282 282 if request.POST.get('del_api_key_builtin'):
283 283 user = User.get(c.user.user_id)
284 284 if user is not None:
285 285 user.api_key = generate_api_key()
286 286 Session().add(user)
287 287 Session().commit()
288 288 h.flash(_("API key successfully reset"), category='success')
289 289 elif api_key:
290 290 ApiKeyModel().delete(api_key, c.user.user_id)
291 291 Session().commit()
292 292 h.flash(_("API key successfully deleted"), category='success')
293 293
294 294 raise HTTPFound(location=url('edit_user_api_keys', id=c.user.user_id))
295 295
296 296 def update_account(self, id):
297 297 pass
298 298
299 299 def edit_perms(self, id):
300 300 c.user = self._get_user_or_raise_if_default(id)
301 301 c.active = 'perms'
302 302 c.perm_user = AuthUser(user_id=id)
303 303 c.ip_addr = self.ip_addr
304 304
305 305 umodel = UserModel()
306 306 defaults = c.user.get_dict()
307 307 defaults.update({
308 308 'create_repo_perm': umodel.has_perm(c.user, 'hg.create.repository'),
309 309 'create_user_group_perm': umodel.has_perm(c.user,
310 310 'hg.usergroup.create.true'),
311 311 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'),
312 312 })
313 313 return htmlfill.render(
314 314 render('admin/users/user_edit.html'),
315 315 defaults=defaults,
316 316 encoding="UTF-8",
317 317 force_defaults=False)
318 318
319 319 def update_perms(self, id):
320 320 user = self._get_user_or_raise_if_default(id)
321 321
322 322 try:
323 323 form = CustomDefaultPermissionsForm()()
324 324 form_result = form.to_python(request.POST)
325 325
326 326 inherit_perms = form_result['inherit_default_permissions']
327 327 user.inherit_default_permissions = inherit_perms
328 328 Session().add(user)
329 329 user_model = UserModel()
330 330
331 331 defs = UserToPerm.query() \
332 332 .filter(UserToPerm.user == user) \
333 333 .all()
334 334 for ug in defs:
335 335 Session().delete(ug)
336 336
337 337 if form_result['create_repo_perm']:
338 338 user_model.grant_perm(id, 'hg.create.repository')
339 339 else:
340 340 user_model.grant_perm(id, 'hg.create.none')
341 341 if form_result['create_user_group_perm']:
342 342 user_model.grant_perm(id, 'hg.usergroup.create.true')
343 343 else:
344 344 user_model.grant_perm(id, 'hg.usergroup.create.false')
345 345 if form_result['fork_repo_perm']:
346 346 user_model.grant_perm(id, 'hg.fork.repository')
347 347 else:
348 348 user_model.grant_perm(id, 'hg.fork.none')
349 349 h.flash(_("Updated permissions"), category='success')
350 350 Session().commit()
351 351 except Exception:
352 352 log.error(traceback.format_exc())
353 353 h.flash(_('An error occurred during permissions saving'),
354 354 category='error')
355 355 raise HTTPFound(location=url('edit_user_perms', id=id))
356 356
357 357 def edit_emails(self, id):
358 358 c.user = self._get_user_or_raise_if_default(id)
359 359 c.active = 'emails'
360 360 c.user_email_map = UserEmailMap.query() \
361 361 .filter(UserEmailMap.user == c.user).all()
362 362
363 363 defaults = c.user.get_dict()
364 364 return htmlfill.render(
365 365 render('admin/users/user_edit.html'),
366 366 defaults=defaults,
367 367 encoding="UTF-8",
368 368 force_defaults=False)
369 369
370 370 def add_email(self, id):
371 371 user = self._get_user_or_raise_if_default(id)
372 372 email = request.POST.get('new_email')
373 373 user_model = UserModel()
374 374
375 375 try:
376 376 user_model.add_extra_email(id, email)
377 377 Session().commit()
378 378 h.flash(_("Added email %s to user") % email, category='success')
379 379 except formencode.Invalid as error:
380 380 msg = error.error_dict['email']
381 381 h.flash(msg, category='error')
382 382 except Exception:
383 383 log.error(traceback.format_exc())
384 384 h.flash(_('An error occurred during email saving'),
385 385 category='error')
386 386 raise HTTPFound(location=url('edit_user_emails', id=id))
387 387
388 388 def delete_email(self, id):
389 389 user = self._get_user_or_raise_if_default(id)
390 390 email_id = request.POST.get('del_email_id')
391 391 user_model = UserModel()
392 392 user_model.delete_extra_email(id, email_id)
393 393 Session().commit()
394 394 h.flash(_("Removed email from user"), category='success')
395 395 raise HTTPFound(location=url('edit_user_emails', id=id))
396 396
397 397 def edit_ips(self, id):
398 398 c.user = self._get_user_or_raise_if_default(id)
399 399 c.active = 'ips'
400 400 c.user_ip_map = UserIpMap.query() \
401 401 .filter(UserIpMap.user == c.user).all()
402 402
403 403 c.inherit_default_ips = c.user.inherit_default_permissions
404 404 c.default_user_ip_map = UserIpMap.query() \
405 405 .filter(UserIpMap.user == User.get_default_user()).all()
406 406
407 407 defaults = c.user.get_dict()
408 408 return htmlfill.render(
409 409 render('admin/users/user_edit.html'),
410 410 defaults=defaults,
411 411 encoding="UTF-8",
412 412 force_defaults=False)
413 413
414 414 def add_ip(self, id):
415 415 ip = request.POST.get('new_ip')
416 416 user_model = UserModel()
417 417
418 418 try:
419 419 user_model.add_extra_ip(id, ip)
420 420 Session().commit()
421 421 h.flash(_("Added IP address %s to user whitelist") % ip, category='success')
422 422 except formencode.Invalid as error:
423 423 msg = error.error_dict['ip']
424 424 h.flash(msg, category='error')
425 425 except Exception:
426 426 log.error(traceback.format_exc())
427 427 h.flash(_('An error occurred while adding IP address'),
428 428 category='error')
429 429
430 430 if 'default_user' in request.POST:
431 431 raise HTTPFound(location=url('admin_permissions_ips'))
432 432 raise HTTPFound(location=url('edit_user_ips', id=id))
433 433
434 434 def delete_ip(self, id):
435 435 ip_id = request.POST.get('del_ip_id')
436 436 user_model = UserModel()
437 437 user_model.delete_extra_ip(id, ip_id)
438 438 Session().commit()
439 439 h.flash(_("Removed IP address from user whitelist"), category='success')
440 440
441 441 if 'default_user' in request.POST:
442 442 raise HTTPFound(location=url('admin_permissions_ips'))
443 443 raise HTTPFound(location=url('edit_user_ips', id=id))
@@ -1,474 +1,473 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.changeset
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 changeset controller for pylons showing changes between
19 revisions
18 changeset controller showing changes between revisions
20 19
21 20 This file was forked by the Kallithea project in July 2014.
22 21 Original author and date, and relevant copyright and licensing information is below:
23 22 :created_on: Apr 25, 2010
24 23 :author: marcink
25 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
26 25 :license: GPLv3, see LICENSE.md for more details.
27 26 """
28 27
29 28 import logging
30 29 import traceback
31 30 from collections import defaultdict
32 31
33 32 from pylons import tmpl_context as c, request, response
34 33 from pylons.i18n.translation import _
35 34 from webob.exc import HTTPFound, HTTPForbidden, HTTPBadRequest, HTTPNotFound
36 35
37 36 from kallithea.lib.utils import jsonify
38 37 from kallithea.lib.vcs.exceptions import RepositoryError, \
39 38 ChangesetDoesNotExistError, EmptyRepositoryError
40 39
41 40 from kallithea.lib.compat import json
42 41 import kallithea.lib.helpers as h
43 42 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
44 43 NotAnonymous
45 44 from kallithea.lib.base import BaseRepoController, render
46 45 from kallithea.lib.utils import action_logger
47 46 from kallithea.lib.compat import OrderedDict
48 47 from kallithea.lib import diffs
49 48 from kallithea.model.db import ChangesetComment, ChangesetStatus
50 49 from kallithea.model.comment import ChangesetCommentsModel
51 50 from kallithea.model.changeset_status import ChangesetStatusModel
52 51 from kallithea.model.meta import Session
53 52 from kallithea.model.repo import RepoModel
54 53 from kallithea.lib.diffs import LimitedDiffContainer
55 54 from kallithea.lib.exceptions import StatusChangeOnClosedPullRequestError
56 55 from kallithea.lib.vcs.backends.base import EmptyChangeset
57 56 from kallithea.lib.utils2 import safe_unicode
58 57 from kallithea.lib.graphmod import graph_data
59 58
60 59 log = logging.getLogger(__name__)
61 60
62 61
63 62 def _update_with_GET(params, GET):
64 63 for k in ['diff1', 'diff2', 'diff']:
65 64 params[k] += GET.getall(k)
66 65
67 66
68 67 def anchor_url(revision, path, GET):
69 68 fid = h.FID(revision, path)
70 69 return h.url.current(anchor=fid, **dict(GET))
71 70
72 71
73 72 def get_ignore_ws(fid, GET):
74 73 ig_ws_global = GET.get('ignorews')
75 74 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
76 75 if ig_ws:
77 76 try:
78 77 return int(ig_ws[0].split(':')[-1])
79 78 except ValueError:
80 79 raise HTTPBadRequest()
81 80 return ig_ws_global
82 81
83 82
84 83 def _ignorews_url(GET, fileid=None):
85 84 fileid = str(fileid) if fileid else None
86 85 params = defaultdict(list)
87 86 _update_with_GET(params, GET)
88 87 lbl = _('Show whitespace')
89 88 ig_ws = get_ignore_ws(fileid, GET)
90 89 ln_ctx = get_line_ctx(fileid, GET)
91 90 # global option
92 91 if fileid is None:
93 92 if ig_ws is None:
94 93 params['ignorews'] += [1]
95 94 lbl = _('Ignore whitespace')
96 95 ctx_key = 'context'
97 96 ctx_val = ln_ctx
98 97 # per file options
99 98 else:
100 99 if ig_ws is None:
101 100 params[fileid] += ['WS:1']
102 101 lbl = _('Ignore whitespace')
103 102
104 103 ctx_key = fileid
105 104 ctx_val = 'C:%s' % ln_ctx
106 105 # if we have passed in ln_ctx pass it along to our params
107 106 if ln_ctx:
108 107 params[ctx_key] += [ctx_val]
109 108
110 109 params['anchor'] = fileid
111 110 icon = h.literal('<i class="icon-strike"></i>')
112 111 return h.link_to(icon, h.url.current(**params), title=lbl, class_='tooltip')
113 112
114 113
115 114 def get_line_ctx(fid, GET):
116 115 ln_ctx_global = GET.get('context')
117 116 if fid:
118 117 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
119 118 else:
120 119 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
121 120 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
122 121 if ln_ctx:
123 122 ln_ctx = [ln_ctx]
124 123
125 124 if ln_ctx:
126 125 retval = ln_ctx[0].split(':')[-1]
127 126 else:
128 127 retval = ln_ctx_global
129 128
130 129 try:
131 130 return int(retval)
132 131 except Exception:
133 132 return 3
134 133
135 134
136 135 def _context_url(GET, fileid=None):
137 136 """
138 137 Generates url for context lines
139 138
140 139 :param fileid:
141 140 """
142 141
143 142 fileid = str(fileid) if fileid else None
144 143 ig_ws = get_ignore_ws(fileid, GET)
145 144 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
146 145
147 146 params = defaultdict(list)
148 147 _update_with_GET(params, GET)
149 148
150 149 # global option
151 150 if fileid is None:
152 151 if ln_ctx > 0:
153 152 params['context'] += [ln_ctx]
154 153
155 154 if ig_ws:
156 155 ig_ws_key = 'ignorews'
157 156 ig_ws_val = 1
158 157
159 158 # per file option
160 159 else:
161 160 params[fileid] += ['C:%s' % ln_ctx]
162 161 ig_ws_key = fileid
163 162 ig_ws_val = 'WS:%s' % 1
164 163
165 164 if ig_ws:
166 165 params[ig_ws_key] += [ig_ws_val]
167 166
168 167 lbl = _('Increase diff context to %(num)s lines') % {'num': ln_ctx}
169 168
170 169 params['anchor'] = fileid
171 170 icon = h.literal('<i class="icon-sort"></i>')
172 171 return h.link_to(icon, h.url.current(**params), title=lbl, class_='tooltip')
173 172
174 173
175 174 # Could perhaps be nice to have in the model but is too high level ...
176 175 def create_comment(text, status, f_path, line_no, revision=None, pull_request_id=None, closing_pr=None):
177 176 """Comment functionality shared between changesets and pullrequests"""
178 177 f_path = f_path or None
179 178 line_no = line_no or None
180 179
181 180 comment = ChangesetCommentsModel().create(
182 181 text=text,
183 182 repo=c.db_repo.repo_id,
184 183 user=c.authuser.user_id,
185 184 revision=revision,
186 185 pull_request=pull_request_id,
187 186 f_path=f_path,
188 187 line_no=line_no,
189 188 status_change=ChangesetStatus.get_status_lbl(status) if status else None,
190 189 closing_pr=closing_pr,
191 190 )
192 191
193 192 return comment
194 193
195 194
196 195 class ChangesetController(BaseRepoController):
197 196
198 197 def __before__(self):
199 198 super(ChangesetController, self).__before__()
200 199 c.affected_files_cut_off = 60
201 200
202 201 def __load_data(self):
203 202 repo_model = RepoModel()
204 203 c.users_array = repo_model.get_users_js()
205 204 c.user_groups_array = repo_model.get_user_groups_js()
206 205
207 206 def _index(self, revision, method):
208 207 c.pull_request = None
209 208 c.anchor_url = anchor_url
210 209 c.ignorews_url = _ignorews_url
211 210 c.context_url = _context_url
212 211 c.fulldiff = fulldiff = request.GET.get('fulldiff')
213 212 #get ranges of revisions if preset
214 213 rev_range = revision.split('...')[:2]
215 214 enable_comments = True
216 215 c.cs_repo = c.db_repo
217 216 try:
218 217 if len(rev_range) == 2:
219 218 enable_comments = False
220 219 rev_start = rev_range[0]
221 220 rev_end = rev_range[1]
222 221 rev_ranges = c.db_repo_scm_instance.get_changesets(start=rev_start,
223 222 end=rev_end)
224 223 else:
225 224 rev_ranges = [c.db_repo_scm_instance.get_changeset(revision)]
226 225
227 226 c.cs_ranges = list(rev_ranges)
228 227 if not c.cs_ranges:
229 228 raise RepositoryError('Changeset range returned empty result')
230 229
231 230 except (ChangesetDoesNotExistError, EmptyRepositoryError):
232 231 log.debug(traceback.format_exc())
233 232 msg = _('Such revision does not exist for this repository')
234 233 h.flash(msg, category='error')
235 234 raise HTTPNotFound()
236 235
237 236 c.changes = OrderedDict()
238 237
239 238 c.lines_added = 0 # count of lines added
240 239 c.lines_deleted = 0 # count of lines removes
241 240
242 241 c.changeset_statuses = ChangesetStatus.STATUSES
243 242 comments = dict()
244 243 c.statuses = []
245 244 c.inline_comments = []
246 245 c.inline_cnt = 0
247 246
248 247 # Iterate over ranges (default changeset view is always one changeset)
249 248 for changeset in c.cs_ranges:
250 249 if method == 'show':
251 250 c.statuses.extend([ChangesetStatusModel().get_status(
252 251 c.db_repo.repo_id, changeset.raw_id)])
253 252
254 253 # Changeset comments
255 254 comments.update((com.comment_id, com)
256 255 for com in ChangesetCommentsModel()
257 256 .get_comments(c.db_repo.repo_id,
258 257 revision=changeset.raw_id))
259 258
260 259 # Status change comments - mostly from pull requests
261 260 comments.update((st.changeset_comment_id, st.comment)
262 261 for st in ChangesetStatusModel()
263 262 .get_statuses(c.db_repo.repo_id,
264 263 changeset.raw_id, with_revisions=True)
265 264 if st.changeset_comment_id is not None)
266 265
267 266 inlines = ChangesetCommentsModel() \
268 267 .get_inline_comments(c.db_repo.repo_id,
269 268 revision=changeset.raw_id)
270 269 c.inline_comments.extend(inlines)
271 270
272 271 cs2 = changeset.raw_id
273 272 cs1 = changeset.parents[0].raw_id if changeset.parents else EmptyChangeset().raw_id
274 273 context_lcl = get_line_ctx('', request.GET)
275 274 ign_whitespace_lcl = get_ignore_ws('', request.GET)
276 275
277 276 _diff = c.db_repo_scm_instance.get_diff(cs1, cs2,
278 277 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
279 278 diff_limit = self.cut_off_limit if not fulldiff else None
280 279 diff_processor = diffs.DiffProcessor(_diff,
281 280 vcs=c.db_repo_scm_instance.alias,
282 281 format='gitdiff',
283 282 diff_limit=diff_limit)
284 283 file_diff_data = OrderedDict()
285 284 if method == 'show':
286 285 _parsed = diff_processor.prepare()
287 286 c.limited_diff = False
288 287 if isinstance(_parsed, LimitedDiffContainer):
289 288 c.limited_diff = True
290 289 for f in _parsed:
291 290 st = f['stats']
292 291 c.lines_added += st['added']
293 292 c.lines_deleted += st['deleted']
294 293 filename = f['filename']
295 294 fid = h.FID(changeset.raw_id, filename)
296 295 url_fid = h.FID('', filename)
297 296 diff = diff_processor.as_html(enable_comments=enable_comments,
298 297 parsed_lines=[f])
299 298 file_diff_data[fid] = (url_fid, f['operation'], f['old_filename'], filename, diff, st)
300 299 else:
301 300 # downloads/raw we only need RAW diff nothing else
302 301 diff = diff_processor.as_raw()
303 302 file_diff_data[''] = (None, None, None, diff, None)
304 303 c.changes[changeset.raw_id] = (cs1, cs2, file_diff_data)
305 304
306 305 #sort comments in creation order
307 306 c.comments = [com for com_id, com in sorted(comments.items())]
308 307
309 308 # count inline comments
310 309 for __, lines in c.inline_comments:
311 310 for comments in lines.values():
312 311 c.inline_cnt += len(comments)
313 312
314 313 if len(c.cs_ranges) == 1:
315 314 c.changeset = c.cs_ranges[0]
316 315 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id
317 316 for x in c.changeset.parents])
318 317 if method == 'download':
319 318 response.content_type = 'text/plain'
320 319 response.content_disposition = 'attachment; filename=%s.diff' \
321 320 % revision[:12]
322 321 return diff
323 322 elif method == 'patch':
324 323 response.content_type = 'text/plain'
325 324 c.diff = safe_unicode(diff)
326 325 return render('changeset/patch_changeset.html')
327 326 elif method == 'raw':
328 327 response.content_type = 'text/plain'
329 328 return diff
330 329 elif method == 'show':
331 330 self.__load_data()
332 331 if len(c.cs_ranges) == 1:
333 332 return render('changeset/changeset.html')
334 333 else:
335 334 c.cs_ranges_org = None
336 335 c.cs_comments = {}
337 336 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
338 337 c.jsdata = json.dumps(graph_data(c.db_repo_scm_instance, revs))
339 338 return render('changeset/changeset_range.html')
340 339
341 340 @LoginRequired()
342 341 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
343 342 'repository.admin')
344 343 def index(self, revision, method='show'):
345 344 return self._index(revision, method=method)
346 345
347 346 @LoginRequired()
348 347 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
349 348 'repository.admin')
350 349 def changeset_raw(self, revision):
351 350 return self._index(revision, method='raw')
352 351
353 352 @LoginRequired()
354 353 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
355 354 'repository.admin')
356 355 def changeset_patch(self, revision):
357 356 return self._index(revision, method='patch')
358 357
359 358 @LoginRequired()
360 359 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
361 360 'repository.admin')
362 361 def changeset_download(self, revision):
363 362 return self._index(revision, method='download')
364 363
365 364 @LoginRequired()
366 365 @NotAnonymous()
367 366 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
368 367 'repository.admin')
369 368 @jsonify
370 369 def comment(self, repo_name, revision):
371 370 assert request.environ.get('HTTP_X_PARTIAL_XHR')
372 371
373 372 status = request.POST.get('changeset_status')
374 373 text = request.POST.get('text', '').strip()
375 374
376 375 c.comment = create_comment(
377 376 text,
378 377 status,
379 378 revision=revision,
380 379 f_path=request.POST.get('f_path'),
381 380 line_no=request.POST.get('line'),
382 381 )
383 382
384 383 # get status if set !
385 384 if status:
386 385 # if latest status was from pull request and it's closed
387 386 # disallow changing status ! RLY?
388 387 try:
389 388 ChangesetStatusModel().set_status(
390 389 c.db_repo.repo_id,
391 390 status,
392 391 c.authuser.user_id,
393 392 c.comment,
394 393 revision=revision,
395 394 dont_allow_on_closed_pull_request=True,
396 395 )
397 396 except StatusChangeOnClosedPullRequestError:
398 397 log.debug('cannot change status on %s with closed pull request', revision)
399 398 raise HTTPBadRequest()
400 399
401 400 action_logger(self.authuser,
402 401 'user_commented_revision:%s' % revision,
403 402 c.db_repo, self.ip_addr, self.sa)
404 403
405 404 Session().commit()
406 405
407 406 data = {
408 407 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
409 408 }
410 409 if c.comment is not None:
411 410 data.update(c.comment.get_dict())
412 411 data.update({'rendered_text':
413 412 render('changeset/changeset_comment_block.html')})
414 413
415 414 return data
416 415
417 416 @LoginRequired()
418 417 @NotAnonymous()
419 418 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
420 419 'repository.admin')
421 420 @jsonify
422 421 def delete_comment(self, repo_name, comment_id):
423 422 co = ChangesetComment.get_or_404(comment_id)
424 423 if co.repo.repo_name != repo_name:
425 424 raise HTTPNotFound()
426 425 owner = co.author.user_id == c.authuser.user_id
427 426 repo_admin = h.HasRepoPermissionAny('repository.admin')(repo_name)
428 427 if h.HasPermissionAny('hg.admin')() or repo_admin or owner:
429 428 ChangesetCommentsModel().delete(comment=co)
430 429 Session().commit()
431 430 return True
432 431 else:
433 432 raise HTTPForbidden()
434 433
435 434 @LoginRequired()
436 435 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
437 436 'repository.admin')
438 437 @jsonify
439 438 def changeset_info(self, repo_name, revision):
440 439 if request.is_xhr:
441 440 try:
442 441 return c.db_repo_scm_instance.get_changeset(revision)
443 442 except ChangesetDoesNotExistError as e:
444 443 return EmptyChangeset(message=str(e))
445 444 else:
446 445 raise HTTPBadRequest()
447 446
448 447 @LoginRequired()
449 448 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
450 449 'repository.admin')
451 450 @jsonify
452 451 def changeset_children(self, repo_name, revision):
453 452 if request.is_xhr:
454 453 changeset = c.db_repo_scm_instance.get_changeset(revision)
455 454 result = {"results": []}
456 455 if changeset.children:
457 456 result = {"results": changeset.children}
458 457 return result
459 458 else:
460 459 raise HTTPBadRequest()
461 460
462 461 @LoginRequired()
463 462 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
464 463 'repository.admin')
465 464 @jsonify
466 465 def changeset_parents(self, repo_name, revision):
467 466 if request.is_xhr:
468 467 changeset = c.db_repo_scm_instance.get_changeset(revision)
469 468 result = {"results": []}
470 469 if changeset.parents:
471 470 result = {"results": changeset.parents}
472 471 return result
473 472 else:
474 473 raise HTTPBadRequest()
@@ -1,293 +1,293 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.compare
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 compare controller for pylons showing differences between two
18 compare controller showing differences between two
19 19 repos, branches, bookmarks or tips
20 20
21 21 This file was forked by the Kallithea project in July 2014.
22 22 Original author and date, and relevant copyright and licensing information is below:
23 23 :created_on: May 6, 2012
24 24 :author: marcink
25 25 :copyright: (c) 2013 RhodeCode GmbH, and others.
26 26 :license: GPLv3, see LICENSE.md for more details.
27 27 """
28 28
29 29
30 30 import logging
31 31 import re
32 32
33 33 from pylons import request, tmpl_context as c, url
34 34 from pylons.i18n.translation import _
35 35 from webob.exc import HTTPFound, HTTPBadRequest
36 36
37 37 from kallithea.lib.utils2 import safe_str, safe_int
38 38 from kallithea.lib.vcs.utils.hgcompat import unionrepo
39 39 from kallithea.lib import helpers as h
40 40 from kallithea.lib.base import BaseRepoController, render
41 41 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
42 42 from kallithea.lib import diffs
43 43 from kallithea.model.db import Repository
44 44 from kallithea.lib.diffs import LimitedDiffContainer
45 45 from kallithea.controllers.changeset import _ignorews_url, _context_url
46 46 from kallithea.lib.graphmod import graph_data
47 47 from kallithea.lib.compat import json, OrderedDict
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51
52 52 class CompareController(BaseRepoController):
53 53
54 54 def __before__(self):
55 55 super(CompareController, self).__before__()
56 56
57 57 # The base repository has already been retrieved.
58 58 c.a_repo = c.db_repo
59 59
60 60 # Retrieve the "changeset" repository (default: same as base).
61 61 other_repo = request.GET.get('other_repo', None)
62 62 if other_repo is None:
63 63 c.cs_repo = c.a_repo
64 64 else:
65 65 c.cs_repo = Repository.get_by_repo_name(other_repo)
66 66 if c.cs_repo is None:
67 67 msg = _('Could not find other repository %s') % other_repo
68 68 h.flash(msg, category='error')
69 69 raise HTTPFound(location=url('compare_home', repo_name=c.a_repo.repo_name))
70 70
71 71 # Verify that it's even possible to compare these two repositories.
72 72 if c.a_repo.scm_instance.alias != c.cs_repo.scm_instance.alias:
73 73 msg = _('Cannot compare repositories of different types')
74 74 h.flash(msg, category='error')
75 75 raise HTTPFound(location=url('compare_home', repo_name=c.a_repo.repo_name))
76 76
77 77 @staticmethod
78 78 def _get_changesets(alias, org_repo, org_rev, other_repo, other_rev):
79 79 """
80 80 Returns lists of changesets that can be merged from org_repo@org_rev
81 81 to other_repo@other_rev
82 82 ... and the other way
83 83 ... and the ancestor that would be used for merge
84 84
85 85 :param org_repo: repo object, that is most likely the original repo we forked from
86 86 :param org_rev: the revision we want our compare to be made
87 87 :param other_repo: repo object, most likely the fork of org_repo. It has
88 88 all changesets that we need to obtain
89 89 :param other_rev: revision we want out compare to be made on other_repo
90 90 """
91 91 ancestor = None
92 92 if org_rev == other_rev:
93 93 org_changesets = []
94 94 other_changesets = []
95 95 ancestor = org_rev
96 96
97 97 elif alias == 'hg':
98 98 #case two independent repos
99 99 if org_repo != other_repo:
100 100 hgrepo = unionrepo.unionrepository(other_repo.baseui,
101 101 other_repo.path,
102 102 org_repo.path)
103 103 # all ancestors of other_rev will be in other_repo and
104 104 # rev numbers from hgrepo can be used in other_repo - org_rev ancestors cannot
105 105
106 106 #no remote compare do it on the same repository
107 107 else:
108 108 hgrepo = other_repo._repo
109 109
110 110 if org_repo.EMPTY_CHANGESET in (org_rev, other_rev):
111 111 # work around unexpected behaviour in Mercurial < 3.4
112 112 ancestor = org_repo.EMPTY_CHANGESET
113 113 else:
114 114 ancestors = hgrepo.revs("ancestor(id(%s), id(%s))", org_rev, other_rev)
115 115 if ancestors:
116 116 # FIXME: picks arbitrary ancestor - but there is usually only one
117 117 try:
118 118 ancestor = hgrepo[ancestors.first()].hex()
119 119 except AttributeError:
120 120 # removed in hg 3.2
121 121 ancestor = hgrepo[ancestors[0]].hex()
122 122
123 123 other_revs = hgrepo.revs("ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
124 124 other_rev, org_rev, org_rev)
125 125 other_changesets = [other_repo.get_changeset(rev) for rev in other_revs]
126 126 org_revs = hgrepo.revs("ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
127 127 org_rev, other_rev, other_rev)
128 128
129 129 org_changesets = [org_repo.get_changeset(hgrepo[rev].hex()) for rev in org_revs]
130 130
131 131 elif alias == 'git':
132 132 if org_repo != other_repo:
133 133 from dulwich.repo import Repo
134 134 from dulwich.client import SubprocessGitClient
135 135
136 136 gitrepo = Repo(org_repo.path)
137 137 SubprocessGitClient(thin_packs=False).fetch(safe_str(other_repo.path), gitrepo)
138 138
139 139 gitrepo_remote = Repo(other_repo.path)
140 140 SubprocessGitClient(thin_packs=False).fetch(safe_str(org_repo.path), gitrepo_remote)
141 141
142 142 revs = []
143 143 for x in gitrepo_remote.get_walker(include=[other_rev],
144 144 exclude=[org_rev]):
145 145 revs.append(x.commit.id)
146 146
147 147 other_changesets = [other_repo.get_changeset(rev) for rev in reversed(revs)]
148 148 if other_changesets:
149 149 ancestor = other_changesets[0].parents[0].raw_id
150 150 else:
151 151 # no changesets from other repo, ancestor is the other_rev
152 152 ancestor = other_rev
153 153
154 154 # dulwich 0.9.9 doesn't have a Repo.close() so we have to mess with internals:
155 155 gitrepo.object_store.close()
156 156 gitrepo_remote.object_store.close()
157 157
158 158 else:
159 159 so, se = org_repo.run_git_command(
160 160 ['log', '--reverse', '--pretty=format:%H',
161 161 '-s', '%s..%s' % (org_rev, other_rev)]
162 162 )
163 163 other_changesets = [org_repo.get_changeset(cs)
164 164 for cs in re.findall(r'[0-9a-fA-F]{40}', so)]
165 165 so, se = org_repo.run_git_command(
166 166 ['merge-base', org_rev, other_rev]
167 167 )
168 168 ancestor = re.findall(r'[0-9a-fA-F]{40}', so)[0]
169 169 org_changesets = []
170 170
171 171 else:
172 172 raise Exception('Bad alias only git and hg is allowed')
173 173
174 174 return other_changesets, org_changesets, ancestor
175 175
176 176 @LoginRequired()
177 177 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
178 178 'repository.admin')
179 179 def index(self, repo_name):
180 180 c.compare_home = True
181 181 c.a_ref_name = c.cs_ref_name = _('Select changeset')
182 182 return render('compare/compare_diff.html')
183 183
184 184 @LoginRequired()
185 185 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
186 186 'repository.admin')
187 187 def compare(self, repo_name, org_ref_type, org_ref_name, other_ref_type, other_ref_name):
188 188 org_ref_name = org_ref_name.strip()
189 189 other_ref_name = other_ref_name.strip()
190 190
191 191 # If merge is True:
192 192 # Show what org would get if merged with other:
193 193 # List changesets that are ancestors of other but not of org.
194 194 # New changesets in org is thus ignored.
195 195 # Diff will be from common ancestor, and merges of org to other will thus be ignored.
196 196 # If merge is False:
197 197 # Make a raw diff from org to other, no matter if related or not.
198 198 # Changesets in one and not in the other will be ignored
199 199 merge = bool(request.GET.get('merge'))
200 200 # fulldiff disables cut_off_limit
201 201 c.fulldiff = request.GET.get('fulldiff')
202 202 # partial uses compare_cs.html template directly
203 203 partial = request.environ.get('HTTP_X_PARTIAL_XHR')
204 204 # as_form puts hidden input field with changeset revisions
205 205 c.as_form = partial and request.GET.get('as_form')
206 206 # swap url for compare_diff page - never partial and never as_form
207 207 c.swap_url = h.url('compare_url',
208 208 repo_name=c.cs_repo.repo_name,
209 209 org_ref_type=other_ref_type, org_ref_name=other_ref_name,
210 210 other_repo=c.a_repo.repo_name,
211 211 other_ref_type=org_ref_type, other_ref_name=org_ref_name,
212 212 merge=merge or '')
213 213
214 214 # set callbacks for generating markup for icons
215 215 c.ignorews_url = _ignorews_url
216 216 c.context_url = _context_url
217 217 ignore_whitespace = request.GET.get('ignorews') == '1'
218 218 line_context = safe_int(request.GET.get('context'), 3)
219 219
220 220 c.a_rev = self._get_ref_rev(c.a_repo, org_ref_type, org_ref_name,
221 221 returnempty=True)
222 222 c.cs_rev = self._get_ref_rev(c.cs_repo, other_ref_type, other_ref_name)
223 223
224 224 c.compare_home = False
225 225 c.a_ref_name = org_ref_name
226 226 c.a_ref_type = org_ref_type
227 227 c.cs_ref_name = other_ref_name
228 228 c.cs_ref_type = other_ref_type
229 229
230 230 c.cs_ranges, c.cs_ranges_org, c.ancestor = self._get_changesets(
231 231 c.a_repo.scm_instance.alias, c.a_repo.scm_instance, c.a_rev,
232 232 c.cs_repo.scm_instance, c.cs_rev)
233 233 raw_ids = [x.raw_id for x in c.cs_ranges]
234 234 c.cs_comments = c.cs_repo.get_comments(raw_ids)
235 235 c.statuses = c.cs_repo.statuses(raw_ids)
236 236
237 237 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
238 238 c.jsdata = json.dumps(graph_data(c.cs_repo.scm_instance, revs))
239 239
240 240 if partial:
241 241 return render('compare/compare_cs.html')
242 242
243 243 org_repo = c.a_repo
244 244 other_repo = c.cs_repo
245 245
246 246 if merge and c.ancestor:
247 247 # case we want a simple diff without incoming changesets,
248 248 # previewing what will be merged.
249 249 # Make the diff on the other repo (which is known to have other_rev)
250 250 log.debug('Using ancestor %s as rev1 instead of %s',
251 251 c.ancestor, c.a_rev)
252 252 rev1 = c.ancestor
253 253 org_repo = other_repo
254 254 else: # comparing tips, not necessarily linearly related
255 255 if merge:
256 256 log.error('Unable to find ancestor revision')
257 257 if org_repo != other_repo:
258 258 # TODO: we could do this by using hg unionrepo
259 259 log.error('cannot compare across repos %s and %s', org_repo, other_repo)
260 260 h.flash(_('Cannot compare repositories without using common ancestor'), category='error')
261 261 raise HTTPBadRequest
262 262 rev1 = c.a_rev
263 263
264 264 diff_limit = self.cut_off_limit if not c.fulldiff else None
265 265
266 266 log.debug('running diff between %s and %s in %s',
267 267 rev1, c.cs_rev, org_repo.scm_instance.path)
268 268 txtdiff = org_repo.scm_instance.get_diff(rev1=rev1, rev2=c.cs_rev,
269 269 ignore_whitespace=ignore_whitespace,
270 270 context=line_context)
271 271
272 272 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
273 273 diff_limit=diff_limit)
274 274 _parsed = diff_processor.prepare()
275 275
276 276 c.limited_diff = False
277 277 if isinstance(_parsed, LimitedDiffContainer):
278 278 c.limited_diff = True
279 279
280 280 c.file_diff_data = OrderedDict()
281 281 c.lines_added = 0
282 282 c.lines_deleted = 0
283 283 for f in _parsed:
284 284 st = f['stats']
285 285 c.lines_added += st['added']
286 286 c.lines_deleted += st['deleted']
287 287 filename = f['filename']
288 288 fid = h.FID('', filename)
289 289 diff = diff_processor.as_html(enable_comments=False,
290 290 parsed_lines=[f])
291 291 c.file_diff_data[fid] = (None, f['operation'], f['old_filename'], filename, diff, st)
292 292
293 293 return render('compare/compare_diff.html')
@@ -1,371 +1,371 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.journal
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 Journal controller for pylons
18 Journal controller
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Nov 21, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26
27 27 """
28 28
29 29 import logging
30 30 import traceback
31 31 from itertools import groupby
32 32
33 33 from sqlalchemy import or_
34 34 from sqlalchemy.orm import joinedload
35 35 from sqlalchemy.sql.expression import func
36 36
37 37 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
38 38
39 39 from webob.exc import HTTPBadRequest
40 40 from pylons import request, tmpl_context as c, response, url
41 41 from pylons.i18n.translation import _
42 42
43 43 from kallithea.controllers.admin.admin import _journal_filter
44 44 from kallithea.model.db import UserLog, UserFollowing, Repository, User
45 45 from kallithea.model.meta import Session
46 46 from kallithea.model.repo import RepoModel
47 47 import kallithea.lib.helpers as h
48 48 from kallithea.lib.helpers import Page
49 49 from kallithea.lib.auth import LoginRequired, NotAnonymous
50 50 from kallithea.lib.base import BaseController, render
51 51 from kallithea.lib.utils2 import safe_int, AttributeDict
52 52 from kallithea.lib.compat import json
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 class JournalController(BaseController):
58 58
59 59 def __before__(self):
60 60 super(JournalController, self).__before__()
61 61 self.language = 'en-us'
62 62 self.ttl = "5"
63 63 self.feed_nr = 20
64 64 c.search_term = request.GET.get('filter')
65 65
66 66 def _get_daily_aggregate(self, journal):
67 67 groups = []
68 68 for k, g in groupby(journal, lambda x: x.action_as_day):
69 69 user_group = []
70 70 #groupby username if it's a present value, else fallback to journal username
71 71 for _unused, g2 in groupby(list(g), lambda x: x.user.username if x.user else x.username):
72 72 l = list(g2)
73 73 user_group.append((l[0].user, l))
74 74
75 75 groups.append((k, user_group,))
76 76
77 77 return groups
78 78
79 79 def _get_journal_data(self, following_repos):
80 80 repo_ids = [x.follows_repository.repo_id for x in following_repos
81 81 if x.follows_repository is not None]
82 82 user_ids = [x.follows_user.user_id for x in following_repos
83 83 if x.follows_user is not None]
84 84
85 85 filtering_criterion = None
86 86
87 87 if repo_ids and user_ids:
88 88 filtering_criterion = or_(UserLog.repository_id.in_(repo_ids),
89 89 UserLog.user_id.in_(user_ids))
90 90 if repo_ids and not user_ids:
91 91 filtering_criterion = UserLog.repository_id.in_(repo_ids)
92 92 if not repo_ids and user_ids:
93 93 filtering_criterion = UserLog.user_id.in_(user_ids)
94 94 if filtering_criterion is not None:
95 95 journal = self.sa.query(UserLog) \
96 96 .options(joinedload(UserLog.user)) \
97 97 .options(joinedload(UserLog.repository))
98 98 #filter
99 99 journal = _journal_filter(journal, c.search_term)
100 100 journal = journal.filter(filtering_criterion) \
101 101 .order_by(UserLog.action_date.desc())
102 102 else:
103 103 journal = []
104 104
105 105 return journal
106 106
107 107 def _atom_feed(self, repos, public=True):
108 108 journal = self._get_journal_data(repos)
109 109 if public:
110 110 _link = h.canonical_url('public_journal_atom')
111 111 _desc = '%s %s %s' % (c.site_name, _('Public Journal'),
112 112 'atom feed')
113 113 else:
114 114 _link = h.canonical_url('journal_atom')
115 115 _desc = '%s %s %s' % (c.site_name, _('Journal'), 'atom feed')
116 116
117 117 feed = Atom1Feed(title=_desc,
118 118 link=_link,
119 119 description=_desc,
120 120 language=self.language,
121 121 ttl=self.ttl)
122 122
123 123 for entry in journal[:self.feed_nr]:
124 124 user = entry.user
125 125 if user is None:
126 126 #fix deleted users
127 127 user = AttributeDict({'short_contact': entry.username,
128 128 'email': '',
129 129 'full_contact': ''})
130 130 action, action_extra, ico = h.action_parser(entry, feed=True)
131 131 title = "%s - %s %s" % (user.short_contact, action(),
132 132 entry.repository.repo_name)
133 133 desc = action_extra()
134 134 _url = None
135 135 if entry.repository is not None:
136 136 _url = h.canonical_url('changelog_home',
137 137 repo_name=entry.repository.repo_name)
138 138
139 139 feed.add_item(title=title,
140 140 pubdate=entry.action_date,
141 141 link=_url or h.canonical_url(''),
142 142 author_email=user.email,
143 143 author_name=user.full_contact,
144 144 description=desc)
145 145
146 146 response.content_type = feed.mime_type
147 147 return feed.writeString('utf-8')
148 148
149 149 def _rss_feed(self, repos, public=True):
150 150 journal = self._get_journal_data(repos)
151 151 if public:
152 152 _link = h.canonical_url('public_journal_atom')
153 153 _desc = '%s %s %s' % (c.site_name, _('Public Journal'),
154 154 'rss feed')
155 155 else:
156 156 _link = h.canonical_url('journal_atom')
157 157 _desc = '%s %s %s' % (c.site_name, _('Journal'), 'rss feed')
158 158
159 159 feed = Rss201rev2Feed(title=_desc,
160 160 link=_link,
161 161 description=_desc,
162 162 language=self.language,
163 163 ttl=self.ttl)
164 164
165 165 for entry in journal[:self.feed_nr]:
166 166 user = entry.user
167 167 if user is None:
168 168 #fix deleted users
169 169 user = AttributeDict({'short_contact': entry.username,
170 170 'email': '',
171 171 'full_contact': ''})
172 172 action, action_extra, ico = h.action_parser(entry, feed=True)
173 173 title = "%s - %s %s" % (user.short_contact, action(),
174 174 entry.repository.repo_name)
175 175 desc = action_extra()
176 176 _url = None
177 177 if entry.repository is not None:
178 178 _url = h.canonical_url('changelog_home',
179 179 repo_name=entry.repository.repo_name)
180 180
181 181 feed.add_item(title=title,
182 182 pubdate=entry.action_date,
183 183 link=_url or h.canonical_url(''),
184 184 author_email=user.email,
185 185 author_name=user.full_contact,
186 186 description=desc)
187 187
188 188 response.content_type = feed.mime_type
189 189 return feed.writeString('utf-8')
190 190
191 191 @LoginRequired()
192 192 @NotAnonymous()
193 193 def index(self):
194 194 # Return a rendered template
195 195 p = safe_int(request.GET.get('page'), 1)
196 196 c.user = User.get(self.authuser.user_id)
197 197 c.following = self.sa.query(UserFollowing) \
198 198 .filter(UserFollowing.user_id == self.authuser.user_id) \
199 199 .options(joinedload(UserFollowing.follows_repository)) \
200 200 .all()
201 201
202 202 journal = self._get_journal_data(c.following)
203 203
204 204 def url_generator(**kw):
205 205 return url.current(filter=c.search_term, **kw)
206 206
207 207 c.journal_pager = Page(journal, page=p, items_per_page=20, url=url_generator)
208 208 c.journal_day_aggregate = self._get_daily_aggregate(c.journal_pager)
209 209
210 210 if request.environ.get('HTTP_X_PARTIAL_XHR'):
211 211 return render('journal/journal_data.html')
212 212
213 213 repos_list = Session().query(Repository) \
214 214 .filter(Repository.user_id ==
215 215 self.authuser.user_id) \
216 216 .order_by(func.lower(Repository.repo_name)).all()
217 217
218 218 repos_data = RepoModel().get_repos_as_dict(repos_list=repos_list,
219 219 admin=True)
220 220 #json used to render the grid
221 221 c.data = json.dumps(repos_data)
222 222
223 223 watched_repos_data = []
224 224
225 225 ## watched repos
226 226 _render = RepoModel._render_datatable
227 227
228 228 def quick_menu(repo_name):
229 229 return _render('quick_menu', repo_name)
230 230
231 231 def repo_lnk(name, rtype, rstate, private, fork_of):
232 232 return _render('repo_name', name, rtype, rstate, private, fork_of,
233 233 short_name=False, admin=False)
234 234
235 235 def last_rev(repo_name, cs_cache):
236 236 return _render('revision', repo_name, cs_cache.get('revision'),
237 237 cs_cache.get('raw_id'), cs_cache.get('author'),
238 238 cs_cache.get('message'))
239 239
240 240 def desc(desc):
241 241 from pylons import tmpl_context as c
242 242 return h.urlify_text(desc, truncate=60, stylize=c.visual.stylify_metatags)
243 243
244 244 def repo_actions(repo_name):
245 245 return _render('repo_actions', repo_name)
246 246
247 247 def owner_actions(user_id, username):
248 248 return _render('user_name', user_id, username)
249 249
250 250 def toogle_follow(repo_id):
251 251 return _render('toggle_follow', repo_id)
252 252
253 253 for entry in c.following:
254 254 repo = entry.follows_repository
255 255 cs_cache = repo.changeset_cache
256 256 row = {
257 257 "menu": quick_menu(repo.repo_name),
258 258 "raw_name": repo.repo_name,
259 259 "name": repo_lnk(repo.repo_name, repo.repo_type,
260 260 repo.repo_state, repo.private, repo.fork),
261 261 "last_changeset": last_rev(repo.repo_name, cs_cache),
262 262 "last_rev_raw": cs_cache.get('revision'),
263 263 "action": toogle_follow(repo.repo_id)
264 264 }
265 265
266 266 watched_repos_data.append(row)
267 267
268 268 c.watched_data = json.dumps({
269 269 "totalRecords": len(c.following),
270 270 "startIndex": 0,
271 271 "sort": "name",
272 272 "dir": "asc",
273 273 "records": watched_repos_data
274 274 })
275 275 return render('journal/journal.html')
276 276
277 277 @LoginRequired(api_access=True)
278 278 @NotAnonymous()
279 279 def journal_atom(self):
280 280 """
281 281 Produce an atom-1.0 feed via feedgenerator module
282 282 """
283 283 following = self.sa.query(UserFollowing) \
284 284 .filter(UserFollowing.user_id == self.authuser.user_id) \
285 285 .options(joinedload(UserFollowing.follows_repository)) \
286 286 .all()
287 287 return self._atom_feed(following, public=False)
288 288
289 289 @LoginRequired(api_access=True)
290 290 @NotAnonymous()
291 291 def journal_rss(self):
292 292 """
293 293 Produce an rss feed via feedgenerator module
294 294 """
295 295 following = self.sa.query(UserFollowing) \
296 296 .filter(UserFollowing.user_id == self.authuser.user_id) \
297 297 .options(joinedload(UserFollowing.follows_repository)) \
298 298 .all()
299 299 return self._rss_feed(following, public=False)
300 300
301 301 @LoginRequired()
302 302 @NotAnonymous()
303 303 def toggle_following(self):
304 304 user_id = request.POST.get('follows_user_id')
305 305 if user_id:
306 306 try:
307 307 self.scm_model.toggle_following_user(user_id,
308 308 self.authuser.user_id)
309 309 Session.commit()
310 310 return 'ok'
311 311 except Exception:
312 312 log.error(traceback.format_exc())
313 313 raise HTTPBadRequest()
314 314
315 315 repo_id = request.POST.get('follows_repo_id')
316 316 if repo_id:
317 317 try:
318 318 self.scm_model.toggle_following_repo(repo_id,
319 319 self.authuser.user_id)
320 320 Session.commit()
321 321 return 'ok'
322 322 except Exception:
323 323 log.error(traceback.format_exc())
324 324 raise HTTPBadRequest()
325 325
326 326 raise HTTPBadRequest()
327 327
328 328 @LoginRequired()
329 329 def public_journal(self):
330 330 # Return a rendered template
331 331 p = safe_int(request.GET.get('page'), 1)
332 332
333 333 c.following = self.sa.query(UserFollowing) \
334 334 .filter(UserFollowing.user_id == self.authuser.user_id) \
335 335 .options(joinedload(UserFollowing.follows_repository)) \
336 336 .all()
337 337
338 338 journal = self._get_journal_data(c.following)
339 339
340 340 c.journal_pager = Page(journal, page=p, items_per_page=20)
341 341
342 342 c.journal_day_aggregate = self._get_daily_aggregate(c.journal_pager)
343 343
344 344 if request.environ.get('HTTP_X_PARTIAL_XHR'):
345 345 return render('journal/journal_data.html')
346 346
347 347 return render('journal/public_journal.html')
348 348
349 349 @LoginRequired(api_access=True)
350 350 def public_journal_atom(self):
351 351 """
352 352 Produce an atom-1.0 feed via feedgenerator module
353 353 """
354 354 c.following = self.sa.query(UserFollowing) \
355 355 .filter(UserFollowing.user_id == self.authuser.user_id) \
356 356 .options(joinedload(UserFollowing.follows_repository)) \
357 357 .all()
358 358
359 359 return self._atom_feed(c.following)
360 360
361 361 @LoginRequired(api_access=True)
362 362 def public_journal_rss(self):
363 363 """
364 364 Produce an rss2 feed via feedgenerator module
365 365 """
366 366 c.following = self.sa.query(UserFollowing) \
367 367 .filter(UserFollowing.user_id == self.authuser.user_id) \
368 368 .options(joinedload(UserFollowing.follows_repository)) \
369 369 .all()
370 370
371 371 return self._rss_feed(c.following)
@@ -1,1083 +1,1083 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.lib.auth
16 16 ~~~~~~~~~~~~~~~~~~
17 17
18 18 authentication and permission libraries
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 4, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27 import time
28 28 import os
29 29 import logging
30 30 import traceback
31 31 import hashlib
32 32 import itertools
33 33 import collections
34 34
35 35 from decorator import decorator
36 36
37 37 from pylons import url, request, session
38 38 from pylons.i18n.translation import _
39 39 from webhelpers.pylonslib import secure_form
40 40 from sqlalchemy import or_
41 41 from sqlalchemy.orm.exc import ObjectDeletedError
42 42 from sqlalchemy.orm import joinedload
43 43 from webob.exc import HTTPFound, HTTPBadRequest, HTTPForbidden, HTTPMethodNotAllowed
44 44
45 45 from kallithea import __platform__, is_windows, is_unix
46 46 from kallithea.lib.vcs.utils.lazy import LazyProperty
47 47 from kallithea.model import meta
48 48 from kallithea.model.meta import Session
49 49 from kallithea.model.user import UserModel
50 50 from kallithea.model.db import User, Repository, Permission, \
51 51 UserToPerm, UserGroupRepoToPerm, UserGroupToPerm, UserGroupMember, \
52 52 RepoGroup, UserGroupRepoGroupToPerm, UserIpMap, UserGroupUserGroupToPerm, \
53 53 UserGroup, UserApiKeys
54 54
55 55 from kallithea.lib.utils2 import safe_str, safe_unicode, aslist
56 56 from kallithea.lib.utils import get_repo_slug, get_repo_group_slug, \
57 57 get_user_group_slug, conditional_cache
58 58 from kallithea.lib.caching_query import FromCache
59 59
60 60
61 61 log = logging.getLogger(__name__)
62 62
63 63
64 64 class PasswordGenerator(object):
65 65 """
66 66 This is a simple class for generating password from different sets of
67 67 characters
68 68 usage::
69 69
70 70 passwd_gen = PasswordGenerator()
71 71 #print 8-letter password containing only big and small letters
72 72 of alphabet
73 73 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
74 74 """
75 75 ALPHABETS_NUM = r'''1234567890'''
76 76 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
77 77 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
78 78 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
79 79 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
80 80 + ALPHABETS_NUM + ALPHABETS_SPECIAL
81 81 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
82 82 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
83 83 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
84 84 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
85 85
86 86 def gen_password(self, length, alphabet=ALPHABETS_FULL):
87 87 assert len(alphabet) <= 256, alphabet
88 88 l = []
89 89 while len(l) < length:
90 90 i = ord(os.urandom(1))
91 91 if i < len(alphabet):
92 92 l.append(alphabet[i])
93 93 return ''.join(l)
94 94
95 95
96 96 class KallitheaCrypto(object):
97 97
98 98 @classmethod
99 99 def hash_string(cls, str_):
100 100 """
101 101 Cryptographic function used for password hashing based on pybcrypt
102 102 or Python's own OpenSSL wrapper on windows
103 103
104 104 :param password: password to hash
105 105 """
106 106 if is_windows:
107 107 return hashlib.sha256(str_).hexdigest()
108 108 elif is_unix:
109 109 import bcrypt
110 110 return bcrypt.hashpw(safe_str(str_), bcrypt.gensalt(10))
111 111 else:
112 112 raise Exception('Unknown or unsupported platform %s' \
113 113 % __platform__)
114 114
115 115 @classmethod
116 116 def hash_check(cls, password, hashed):
117 117 """
118 118 Checks matching password with it's hashed value, runs different
119 119 implementation based on platform it runs on
120 120
121 121 :param password: password
122 122 :param hashed: password in hashed form
123 123 """
124 124
125 125 if is_windows:
126 126 return hashlib.sha256(password).hexdigest() == hashed
127 127 elif is_unix:
128 128 import bcrypt
129 129 return bcrypt.checkpw(safe_str(password), safe_str(hashed))
130 130 else:
131 131 raise Exception('Unknown or unsupported platform %s' \
132 132 % __platform__)
133 133
134 134
135 135 def get_crypt_password(password):
136 136 return KallitheaCrypto.hash_string(password)
137 137
138 138
139 139 def check_password(password, hashed):
140 140 return KallitheaCrypto.hash_check(password, hashed)
141 141
142 142
143 143
144 144 def _cached_perms_data(user_id, user_is_admin, user_inherit_default_permissions,
145 145 explicit, algo):
146 146 RK = 'repositories'
147 147 GK = 'repositories_groups'
148 148 UK = 'user_groups'
149 149 GLOBAL = 'global'
150 150 PERM_WEIGHTS = Permission.PERM_WEIGHTS
151 151 permissions = {RK: {}, GK: {}, UK: {}, GLOBAL: set()}
152 152
153 153 def _choose_perm(new_perm, cur_perm):
154 154 new_perm_val = PERM_WEIGHTS[new_perm]
155 155 cur_perm_val = PERM_WEIGHTS[cur_perm]
156 156 if algo == 'higherwin':
157 157 if new_perm_val > cur_perm_val:
158 158 return new_perm
159 159 return cur_perm
160 160 elif algo == 'lowerwin':
161 161 if new_perm_val < cur_perm_val:
162 162 return new_perm
163 163 return cur_perm
164 164
165 165 #======================================================================
166 166 # fetch default permissions
167 167 #======================================================================
168 168 default_user = User.get_by_username('default', cache=True)
169 169 default_user_id = default_user.user_id
170 170
171 171 default_repo_perms = Permission.get_default_perms(default_user_id)
172 172 default_repo_groups_perms = Permission.get_default_group_perms(default_user_id)
173 173 default_user_group_perms = Permission.get_default_user_group_perms(default_user_id)
174 174
175 175 if user_is_admin:
176 176 #==================================================================
177 177 # admin users have all rights;
178 178 # based on default permissions, just set everything to admin
179 179 #==================================================================
180 180 permissions[GLOBAL].add('hg.admin')
181 181 permissions[GLOBAL].add('hg.create.write_on_repogroup.true')
182 182
183 183 # repositories
184 184 for perm in default_repo_perms:
185 185 r_k = perm.UserRepoToPerm.repository.repo_name
186 186 p = 'repository.admin'
187 187 permissions[RK][r_k] = p
188 188
189 189 # repository groups
190 190 for perm in default_repo_groups_perms:
191 191 rg_k = perm.UserRepoGroupToPerm.group.group_name
192 192 p = 'group.admin'
193 193 permissions[GK][rg_k] = p
194 194
195 195 # user groups
196 196 for perm in default_user_group_perms:
197 197 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
198 198 p = 'usergroup.admin'
199 199 permissions[UK][u_k] = p
200 200 return permissions
201 201
202 202 #==================================================================
203 203 # SET DEFAULTS GLOBAL, REPOS, REPOSITORY GROUPS
204 204 #==================================================================
205 205
206 206 # default global permissions taken from the default user
207 207 default_global_perms = UserToPerm.query() \
208 208 .filter(UserToPerm.user_id == default_user_id) \
209 209 .options(joinedload(UserToPerm.permission))
210 210
211 211 for perm in default_global_perms:
212 212 permissions[GLOBAL].add(perm.permission.permission_name)
213 213
214 214 # defaults for repositories, taken from default user
215 215 for perm in default_repo_perms:
216 216 r_k = perm.UserRepoToPerm.repository.repo_name
217 217 if perm.Repository.private and not (perm.Repository.user_id == user_id):
218 218 # disable defaults for private repos,
219 219 p = 'repository.none'
220 220 elif perm.Repository.user_id == user_id:
221 221 # set admin if owner
222 222 p = 'repository.admin'
223 223 else:
224 224 p = perm.Permission.permission_name
225 225
226 226 permissions[RK][r_k] = p
227 227
228 228 # defaults for repository groups taken from default user permission
229 229 # on given group
230 230 for perm in default_repo_groups_perms:
231 231 rg_k = perm.UserRepoGroupToPerm.group.group_name
232 232 p = perm.Permission.permission_name
233 233 permissions[GK][rg_k] = p
234 234
235 235 # defaults for user groups taken from default user permission
236 236 # on given user group
237 237 for perm in default_user_group_perms:
238 238 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
239 239 p = perm.Permission.permission_name
240 240 permissions[UK][u_k] = p
241 241
242 242 #======================================================================
243 243 # !! OVERRIDE GLOBALS !! with user permissions if any found
244 244 #======================================================================
245 245 # those can be configured from groups or users explicitly
246 246 _configurable = set([
247 247 'hg.fork.none', 'hg.fork.repository',
248 248 'hg.create.none', 'hg.create.repository',
249 249 'hg.usergroup.create.false', 'hg.usergroup.create.true'
250 250 ])
251 251
252 252 # USER GROUPS comes first
253 253 # user group global permissions
254 254 user_perms_from_users_groups = Session().query(UserGroupToPerm) \
255 255 .options(joinedload(UserGroupToPerm.permission)) \
256 256 .join((UserGroupMember, UserGroupToPerm.users_group_id ==
257 257 UserGroupMember.users_group_id)) \
258 258 .filter(UserGroupMember.user_id == user_id) \
259 259 .join((UserGroup, UserGroupMember.users_group_id ==
260 260 UserGroup.users_group_id)) \
261 261 .filter(UserGroup.users_group_active == True) \
262 262 .order_by(UserGroupToPerm.users_group_id) \
263 263 .all()
264 264 # need to group here by groups since user can be in more than
265 265 # one group
266 266 _grouped = [[x, list(y)] for x, y in
267 267 itertools.groupby(user_perms_from_users_groups,
268 268 lambda x:x.users_group)]
269 269 for gr, perms in _grouped:
270 270 # since user can be in multiple groups iterate over them and
271 271 # select the lowest permissions first (more explicit)
272 272 ##TODO: do this^^
273 273 if not gr.inherit_default_permissions:
274 274 # NEED TO IGNORE all configurable permissions and
275 275 # replace them with explicitly set
276 276 permissions[GLOBAL] = permissions[GLOBAL] \
277 277 .difference(_configurable)
278 278 for perm in perms:
279 279 permissions[GLOBAL].add(perm.permission.permission_name)
280 280
281 281 # user specific global permissions
282 282 user_perms = Session().query(UserToPerm) \
283 283 .options(joinedload(UserToPerm.permission)) \
284 284 .filter(UserToPerm.user_id == user_id).all()
285 285
286 286 if not user_inherit_default_permissions:
287 287 # NEED TO IGNORE all configurable permissions and
288 288 # replace them with explicitly set
289 289 permissions[GLOBAL] = permissions[GLOBAL] \
290 290 .difference(_configurable)
291 291
292 292 for perm in user_perms:
293 293 permissions[GLOBAL].add(perm.permission.permission_name)
294 294 ## END GLOBAL PERMISSIONS
295 295
296 296 #======================================================================
297 297 # !! PERMISSIONS FOR REPOSITORIES !!
298 298 #======================================================================
299 299 #======================================================================
300 300 # check if user is part of user groups for this repository and
301 301 # fill in his permission from it. _choose_perm decides of which
302 302 # permission should be selected based on selected method
303 303 #======================================================================
304 304
305 305 # user group for repositories permissions
306 306 user_repo_perms_from_users_groups = \
307 307 Session().query(UserGroupRepoToPerm, Permission, Repository,) \
308 308 .join((Repository, UserGroupRepoToPerm.repository_id ==
309 309 Repository.repo_id)) \
310 310 .join((Permission, UserGroupRepoToPerm.permission_id ==
311 311 Permission.permission_id)) \
312 312 .join((UserGroup, UserGroupRepoToPerm.users_group_id ==
313 313 UserGroup.users_group_id)) \
314 314 .filter(UserGroup.users_group_active == True) \
315 315 .join((UserGroupMember, UserGroupRepoToPerm.users_group_id ==
316 316 UserGroupMember.users_group_id)) \
317 317 .filter(UserGroupMember.user_id == user_id) \
318 318 .all()
319 319
320 320 multiple_counter = collections.defaultdict(int)
321 321 for perm in user_repo_perms_from_users_groups:
322 322 r_k = perm.UserGroupRepoToPerm.repository.repo_name
323 323 multiple_counter[r_k] += 1
324 324 p = perm.Permission.permission_name
325 325 cur_perm = permissions[RK][r_k]
326 326
327 327 if perm.Repository.user_id == user_id:
328 328 # set admin if owner
329 329 p = 'repository.admin'
330 330 else:
331 331 if multiple_counter[r_k] > 1:
332 332 p = _choose_perm(p, cur_perm)
333 333 permissions[RK][r_k] = p
334 334
335 335 # user explicit permissions for repositories, overrides any specified
336 336 # by the group permission
337 337 user_repo_perms = Permission.get_default_perms(user_id)
338 338 for perm in user_repo_perms:
339 339 r_k = perm.UserRepoToPerm.repository.repo_name
340 340 cur_perm = permissions[RK][r_k]
341 341 # set admin if owner
342 342 if perm.Repository.user_id == user_id:
343 343 p = 'repository.admin'
344 344 else:
345 345 p = perm.Permission.permission_name
346 346 if not explicit:
347 347 p = _choose_perm(p, cur_perm)
348 348 permissions[RK][r_k] = p
349 349
350 350 #======================================================================
351 351 # !! PERMISSIONS FOR REPOSITORY GROUPS !!
352 352 #======================================================================
353 353 #======================================================================
354 354 # check if user is part of user groups for this repository groups and
355 355 # fill in his permission from it. _choose_perm decides of which
356 356 # permission should be selected based on selected method
357 357 #======================================================================
358 358 # user group for repo groups permissions
359 359 user_repo_group_perms_from_users_groups = \
360 360 Session().query(UserGroupRepoGroupToPerm, Permission, RepoGroup) \
361 361 .join((RepoGroup, UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)) \
362 362 .join((Permission, UserGroupRepoGroupToPerm.permission_id
363 363 == Permission.permission_id)) \
364 364 .join((UserGroup, UserGroupRepoGroupToPerm.users_group_id ==
365 365 UserGroup.users_group_id)) \
366 366 .filter(UserGroup.users_group_active == True) \
367 367 .join((UserGroupMember, UserGroupRepoGroupToPerm.users_group_id
368 368 == UserGroupMember.users_group_id)) \
369 369 .filter(UserGroupMember.user_id == user_id) \
370 370 .all()
371 371
372 372 multiple_counter = collections.defaultdict(int)
373 373 for perm in user_repo_group_perms_from_users_groups:
374 374 g_k = perm.UserGroupRepoGroupToPerm.group.group_name
375 375 multiple_counter[g_k] += 1
376 376 p = perm.Permission.permission_name
377 377 cur_perm = permissions[GK][g_k]
378 378 if multiple_counter[g_k] > 1:
379 379 p = _choose_perm(p, cur_perm)
380 380 permissions[GK][g_k] = p
381 381
382 382 # user explicit permissions for repository groups
383 383 user_repo_groups_perms = Permission.get_default_group_perms(user_id)
384 384 for perm in user_repo_groups_perms:
385 385 rg_k = perm.UserRepoGroupToPerm.group.group_name
386 386 p = perm.Permission.permission_name
387 387 cur_perm = permissions[GK][rg_k]
388 388 if not explicit:
389 389 p = _choose_perm(p, cur_perm)
390 390 permissions[GK][rg_k] = p
391 391
392 392 #======================================================================
393 393 # !! PERMISSIONS FOR USER GROUPS !!
394 394 #======================================================================
395 395 # user group for user group permissions
396 396 user_group_user_groups_perms = \
397 397 Session().query(UserGroupUserGroupToPerm, Permission, UserGroup) \
398 398 .join((UserGroup, UserGroupUserGroupToPerm.target_user_group_id
399 399 == UserGroup.users_group_id)) \
400 400 .join((Permission, UserGroupUserGroupToPerm.permission_id
401 401 == Permission.permission_id)) \
402 402 .join((UserGroupMember, UserGroupUserGroupToPerm.user_group_id
403 403 == UserGroupMember.users_group_id)) \
404 404 .filter(UserGroupMember.user_id == user_id) \
405 405 .join((UserGroup, UserGroupMember.users_group_id ==
406 406 UserGroup.users_group_id), aliased=True, from_joinpoint=True) \
407 407 .filter(UserGroup.users_group_active == True) \
408 408 .all()
409 409
410 410 multiple_counter = collections.defaultdict(int)
411 411 for perm in user_group_user_groups_perms:
412 412 g_k = perm.UserGroupUserGroupToPerm.target_user_group.users_group_name
413 413 multiple_counter[g_k] += 1
414 414 p = perm.Permission.permission_name
415 415 cur_perm = permissions[UK][g_k]
416 416 if multiple_counter[g_k] > 1:
417 417 p = _choose_perm(p, cur_perm)
418 418 permissions[UK][g_k] = p
419 419
420 420 #user explicit permission for user groups
421 421 user_user_groups_perms = Permission.get_default_user_group_perms(user_id)
422 422 for perm in user_user_groups_perms:
423 423 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
424 424 p = perm.Permission.permission_name
425 425 cur_perm = permissions[UK][u_k]
426 426 if not explicit:
427 427 p = _choose_perm(p, cur_perm)
428 428 permissions[UK][u_k] = p
429 429
430 430 return permissions
431 431
432 432
433 433 def allowed_api_access(controller_name, whitelist=None, api_key=None):
434 434 """
435 435 Check if given controller_name is in whitelist API access
436 436 """
437 437 if not whitelist:
438 438 from kallithea import CONFIG
439 439 whitelist = aslist(CONFIG.get('api_access_controllers_whitelist'),
440 440 sep=',')
441 441 log.debug('whitelist of API access is: %s', whitelist)
442 442 api_access_valid = controller_name in whitelist
443 443 if api_access_valid:
444 444 log.debug('controller:%s is in API whitelist', controller_name)
445 445 else:
446 446 msg = 'controller: %s is *NOT* in API whitelist' % (controller_name)
447 447 if api_key:
448 448 #if we use API key and don't have access it's a warning
449 449 log.warning(msg)
450 450 else:
451 451 log.debug(msg)
452 452 return api_access_valid
453 453
454 454
455 455 class AuthUser(object):
456 456 """
457 457 Represents a Kallithea user, including various authentication and
458 458 authorization information. Typically used to store the current user,
459 459 but is also used as a generic user information data structure in
460 460 parts of the code, e.g. user management.
461 461
462 462 Constructed from a database `User` object, a user ID or cookie dict,
463 463 it looks up the user (if needed) and copies all attributes to itself,
464 464 adding various non-persistent data. If lookup fails but anonymous
465 465 access to Kallithea is enabled, the default user is loaded instead.
466 466
467 467 `AuthUser` does not by itself authenticate users and the constructor
468 468 sets the `is_authenticated` field to False. It's up to other parts
469 469 of the code to check e.g. if a supplied password is correct, and if
470 470 so, set `is_authenticated` to True.
471 471
472 472 However, `AuthUser` does refuse to load a user that is not `active`.
473 473 """
474 474
475 475 def __init__(self, user_id=None, dbuser=None,
476 476 is_external_auth=False):
477 477
478 478 self.is_authenticated = False
479 479 self.is_external_auth = is_external_auth
480 480
481 481 user_model = UserModel()
482 482 self.anonymous_user = User.get_default_user(cache=True)
483 483
484 484 # These attributes will be overridden by fill_data, below, unless the
485 485 # requested user cannot be found and the default anonymous user is
486 486 # not enabled.
487 487 self.user_id = None
488 488 self.username = None
489 489 self.api_key = None
490 490 self.name = ''
491 491 self.lastname = ''
492 492 self.email = ''
493 493 self.admin = False
494 494 self.inherit_default_permissions = False
495 495
496 496 # Look up database user, if necessary.
497 497 if user_id is not None:
498 498 log.debug('Auth User lookup by USER ID %s', user_id)
499 499 dbuser = user_model.get(user_id)
500 500 else:
501 501 # Note: dbuser is allowed to be None.
502 502 log.debug('Auth User lookup by database user %s', dbuser)
503 503
504 504 is_user_loaded = self._fill_data(dbuser)
505 505
506 506 # If user cannot be found, try falling back to anonymous.
507 507 if not is_user_loaded:
508 508 is_user_loaded = self._fill_data(self.anonymous_user)
509 509
510 510 self.is_default_user = (self.user_id == self.anonymous_user.user_id)
511 511
512 512 if not self.username:
513 513 self.username = 'None'
514 514
515 515 log.debug('Auth User is now %s', self)
516 516
517 517 def _fill_data(self, dbuser):
518 518 """
519 519 Copies database fields from a `db.User` to this `AuthUser`. Does
520 520 not copy `api_keys` and `permissions` attributes.
521 521
522 522 Checks that `dbuser` is `active` (and not None) before copying;
523 523 returns True on success.
524 524 """
525 525 if dbuser is not None and dbuser.active:
526 526 log.debug('filling %s data', dbuser)
527 527 for k, v in dbuser.get_dict().iteritems():
528 528 assert k not in ['api_keys', 'permissions']
529 529 setattr(self, k, v)
530 530 return True
531 531 return False
532 532
533 533 @LazyProperty
534 534 def permissions(self):
535 535 return self.__get_perms(user=self, cache=False)
536 536
537 537 @property
538 538 def api_keys(self):
539 539 return self._get_api_keys()
540 540
541 541 def __get_perms(self, user, explicit=True, algo='higherwin', cache=False):
542 542 """
543 543 Fills user permission attribute with permissions taken from database
544 544 works for permissions given for repositories, and for permissions that
545 545 are granted to groups
546 546
547 547 :param user: `AuthUser` instance
548 548 :param explicit: In case there are permissions both for user and a group
549 549 that user is part of, explicit flag will define if user will
550 550 explicitly override permissions from group, if it's False it will
551 551 make decision based on the algo
552 552 :param algo: algorithm to decide what permission should be choose if
553 553 it's multiple defined, eg user in two different groups. It also
554 554 decides if explicit flag is turned off how to specify the permission
555 555 for case when user is in a group + have defined separate permission
556 556 """
557 557 user_id = user.user_id
558 558 user_is_admin = user.is_admin
559 559 user_inherit_default_permissions = user.inherit_default_permissions
560 560
561 561 log.debug('Getting PERMISSION tree')
562 562 compute = conditional_cache('short_term', 'cache_desc',
563 563 condition=cache, func=_cached_perms_data)
564 564 return compute(user_id, user_is_admin,
565 565 user_inherit_default_permissions, explicit, algo)
566 566
567 567 def _get_api_keys(self):
568 568 api_keys = [self.api_key]
569 569 for api_key in UserApiKeys.query() \
570 570 .filter(UserApiKeys.user_id == self.user_id) \
571 571 .filter(or_(UserApiKeys.expires == -1,
572 572 UserApiKeys.expires >= time.time())).all():
573 573 api_keys.append(api_key.api_key)
574 574
575 575 return api_keys
576 576
577 577 @property
578 578 def is_admin(self):
579 579 return self.admin
580 580
581 581 @property
582 582 def repositories_admin(self):
583 583 """
584 584 Returns list of repositories you're an admin of
585 585 """
586 586 return [x[0] for x in self.permissions['repositories'].iteritems()
587 587 if x[1] == 'repository.admin']
588 588
589 589 @property
590 590 def repository_groups_admin(self):
591 591 """
592 592 Returns list of repository groups you're an admin of
593 593 """
594 594 return [x[0] for x in self.permissions['repositories_groups'].iteritems()
595 595 if x[1] == 'group.admin']
596 596
597 597 @property
598 598 def user_groups_admin(self):
599 599 """
600 600 Returns list of user groups you're an admin of
601 601 """
602 602 return [x[0] for x in self.permissions['user_groups'].iteritems()
603 603 if x[1] == 'usergroup.admin']
604 604
605 605 @staticmethod
606 606 def check_ip_allowed(user, ip_addr):
607 607 """
608 608 Check if the given IP address (a `str`) is allowed for the given
609 609 user (an `AuthUser` or `db.User`).
610 610 """
611 611 allowed_ips = AuthUser.get_allowed_ips(user.user_id, cache=True,
612 612 inherit_from_default=user.inherit_default_permissions)
613 613 if check_ip_access(source_ip=ip_addr, allowed_ips=allowed_ips):
614 614 log.debug('IP:%s is in range of %s', ip_addr, allowed_ips)
615 615 return True
616 616 else:
617 617 log.info('Access for IP:%s forbidden, '
618 618 'not in %s' % (ip_addr, allowed_ips))
619 619 return False
620 620
621 621 def __repr__(self):
622 622 return "<AuthUser('id:%s[%s] auth:%s')>" \
623 623 % (self.user_id, self.username, (self.is_authenticated or self.is_default_user))
624 624
625 625 def to_cookie(self):
626 626 """ Serializes this login session to a cookie `dict`. """
627 627 return {
628 628 'user_id': self.user_id,
629 629 'is_external_auth': self.is_external_auth,
630 630 }
631 631
632 632 @staticmethod
633 633 def from_cookie(cookie):
634 634 """
635 635 Deserializes an `AuthUser` from a cookie `dict`.
636 636 """
637 637
638 638 au = AuthUser(
639 639 user_id=cookie.get('user_id'),
640 640 is_external_auth=cookie.get('is_external_auth', False),
641 641 )
642 642 au.is_authenticated = True
643 643 return au
644 644
645 645 @classmethod
646 646 def get_allowed_ips(cls, user_id, cache=False, inherit_from_default=False):
647 647 _set = set()
648 648
649 649 if inherit_from_default:
650 650 default_ips = UserIpMap.query().filter(UserIpMap.user ==
651 651 User.get_default_user(cache=True))
652 652 if cache:
653 653 default_ips = default_ips.options(FromCache("sql_cache_short",
654 654 "get_user_ips_default"))
655 655
656 656 # populate from default user
657 657 for ip in default_ips:
658 658 try:
659 659 _set.add(ip.ip_addr)
660 660 except ObjectDeletedError:
661 661 # since we use heavy caching sometimes it happens that we get
662 662 # deleted objects here, we just skip them
663 663 pass
664 664
665 665 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
666 666 if cache:
667 667 user_ips = user_ips.options(FromCache("sql_cache_short",
668 668 "get_user_ips_%s" % user_id))
669 669
670 670 for ip in user_ips:
671 671 try:
672 672 _set.add(ip.ip_addr)
673 673 except ObjectDeletedError:
674 674 # since we use heavy caching sometimes it happens that we get
675 675 # deleted objects here, we just skip them
676 676 pass
677 677 return _set or set(['0.0.0.0/0', '::/0'])
678 678
679 679
680 680 def set_available_permissions(config):
681 681 """
682 This function will propagate pylons globals with all available defined
682 This function will propagate globals with all available defined
683 683 permission given in db. We don't want to check each time from db for new
684 684 permissions since adding a new permission also requires application restart
685 685 ie. to decorate new views with the newly created permission
686 686
687 :param config: current pylons config instance
687 :param config: current config instance
688 688
689 689 """
690 690 log.info('getting information about all available permissions')
691 691 try:
692 692 sa = meta.Session
693 693 all_perms = sa.query(Permission).all()
694 694 config['available_permissions'] = [x.permission_name for x in all_perms]
695 695 finally:
696 696 meta.Session.remove()
697 697
698 698
699 699 #==============================================================================
700 700 # CHECK DECORATORS
701 701 #==============================================================================
702 702
703 703 def _redirect_to_login(message=None):
704 704 """Return an exception that must be raised. It will redirect to the login
705 705 page which will redirect back to the current URL after authentication.
706 706 The optional message will be shown in a flash message."""
707 707 from kallithea.lib import helpers as h
708 708 if message:
709 709 h.flash(h.literal(message), category='warning')
710 710 p = request.path_qs
711 711 log.debug('Redirecting to login page, origin: %s', p)
712 712 return HTTPFound(location=url('login_home', came_from=p))
713 713
714 714
715 715 class LoginRequired(object):
716 716 """
717 717 Must be logged in to execute this function else
718 718 redirect to login page
719 719
720 720 :param api_access: if enabled this checks only for valid auth token
721 721 and grants access based on valid token
722 722 """
723 723
724 724 def __init__(self, api_access=False):
725 725 self.api_access = api_access
726 726
727 727 def __call__(self, func):
728 728 return decorator(self.__wrapper, func)
729 729
730 730 def __wrapper(self, func, *fargs, **fkwargs):
731 731 controller = fargs[0]
732 732 user = controller.authuser
733 733 loc = "%s:%s" % (controller.__class__.__name__, func.__name__)
734 734 log.debug('Checking access for user %s @ %s', user, loc)
735 735
736 736 if not AuthUser.check_ip_allowed(user, controller.ip_addr):
737 737 raise _redirect_to_login(_('IP %s not allowed') % controller.ip_addr)
738 738
739 739 # check if we used an API key and it's a valid one
740 740 api_key = request.GET.get('api_key')
741 741 if api_key is not None:
742 742 # explicit controller is enabled or API is in our whitelist
743 743 if self.api_access or allowed_api_access(loc, api_key=api_key):
744 744 if api_key in user.api_keys:
745 745 log.info('user %s authenticated with API key ****%s @ %s',
746 746 user, api_key[-4:], loc)
747 747 return func(*fargs, **fkwargs)
748 748 else:
749 749 log.warning('API key ****%s is NOT valid', api_key[-4:])
750 750 raise _redirect_to_login(_('Invalid API key'))
751 751 else:
752 752 # controller does not allow API access
753 753 log.warning('API access to %s is not allowed', loc)
754 754 raise HTTPForbidden()
755 755
756 756 # Only allow the following HTTP request methods.
757 757 if request.method not in ['GET', 'HEAD', 'POST']:
758 758 raise HTTPMethodNotAllowed()
759 759
760 760 # Also verify the _method override - no longer allowed
761 761 _method = request.params.get('_method')
762 762 if _method is None:
763 763 pass # no override, no problem
764 764 else:
765 765 raise HTTPMethodNotAllowed()
766 766
767 767 # Make sure CSRF token never appears in the URL. If so, invalidate it.
768 768 if secure_form.token_key in request.GET:
769 769 log.error('CSRF key leak detected')
770 770 session.pop(secure_form.token_key, None)
771 771 session.save()
772 772 from kallithea.lib import helpers as h
773 773 h.flash(_("CSRF token leak has been detected - all form tokens have been expired"),
774 774 category='error')
775 775
776 776 # CSRF protection: Whenever a request has ambient authority (whether
777 777 # through a session cookie or its origin IP address), it must include
778 778 # the correct token, unless the HTTP method is GET or HEAD (and thus
779 779 # guaranteed to be side effect free. In practice, the only situation
780 780 # where we allow side effects without ambient authority is when the
781 781 # authority comes from an API key; and that is handled above.
782 782 if request.method not in ['GET', 'HEAD']:
783 783 token = request.POST.get(secure_form.token_key)
784 784 if not token or token != secure_form.authentication_token():
785 785 log.error('CSRF check failed')
786 786 raise HTTPForbidden()
787 787
788 788 # WebOb already ignores request payload parameters for anything other
789 789 # than POST/PUT, but double-check since other Kallithea code relies on
790 790 # this assumption.
791 791 if request.method not in ['POST', 'PUT'] and request.POST:
792 792 log.error('%r request with payload parameters; WebOb should have stopped this', request.method)
793 793 raise HTTPBadRequest()
794 794
795 795 # regular user authentication
796 796 if user.is_authenticated or user.is_default_user:
797 797 log.info('user %s authenticated with regular auth @ %s', user, loc)
798 798 return func(*fargs, **fkwargs)
799 799 else:
800 800 log.warning('user %s NOT authenticated with regular auth @ %s', user, loc)
801 801 raise _redirect_to_login()
802 802
803 803 class NotAnonymous(object):
804 804 """
805 805 Must be logged in to execute this function else
806 806 redirect to login page"""
807 807
808 808 def __call__(self, func):
809 809 return decorator(self.__wrapper, func)
810 810
811 811 def __wrapper(self, func, *fargs, **fkwargs):
812 812 cls = fargs[0]
813 813 self.user = cls.authuser
814 814
815 815 log.debug('Checking if user is not anonymous @%s', cls)
816 816
817 817 if self.user.is_default_user:
818 818 raise _redirect_to_login(_('You need to be a registered user to '
819 819 'perform this action'))
820 820 else:
821 821 return func(*fargs, **fkwargs)
822 822
823 823
824 824 class PermsDecorator(object):
825 825 """Base class for controller decorators"""
826 826
827 827 def __init__(self, *required_perms):
828 828 self.required_perms = set(required_perms)
829 829 self.user_perms = None
830 830
831 831 def __call__(self, func):
832 832 return decorator(self.__wrapper, func)
833 833
834 834 def __wrapper(self, func, *fargs, **fkwargs):
835 835 cls = fargs[0]
836 836 self.user = cls.authuser
837 837 self.user_perms = self.user.permissions
838 838 log.debug('checking %s permissions %s for %s %s',
839 839 self.__class__.__name__, self.required_perms, cls, self.user)
840 840
841 841 if self.check_permissions():
842 842 log.debug('Permission granted for %s %s', cls, self.user)
843 843 return func(*fargs, **fkwargs)
844 844
845 845 else:
846 846 log.debug('Permission denied for %s %s', cls, self.user)
847 847 if self.user.is_default_user:
848 848 raise _redirect_to_login(_('You need to be signed in to view this page'))
849 849 else:
850 850 raise HTTPForbidden()
851 851
852 852 def check_permissions(self):
853 853 """Dummy function for overriding"""
854 854 raise Exception('You have to write this function in child class')
855 855
856 856
857 857 class HasPermissionAnyDecorator(PermsDecorator):
858 858 """
859 859 Checks for access permission for any of given predicates. In order to
860 860 fulfill the request any of predicates must be meet
861 861 """
862 862
863 863 def check_permissions(self):
864 864 if self.required_perms.intersection(self.user_perms.get('global')):
865 865 return True
866 866 return False
867 867
868 868
869 869 class HasRepoPermissionAnyDecorator(PermsDecorator):
870 870 """
871 871 Checks for access permission for any of given predicates for specific
872 872 repository. In order to fulfill the request any of predicates must be meet
873 873 """
874 874
875 875 def check_permissions(self):
876 876 repo_name = get_repo_slug(request)
877 877 try:
878 878 user_perms = set([self.user_perms['repositories'][repo_name]])
879 879 except KeyError:
880 880 return False
881 881
882 882 if self.required_perms.intersection(user_perms):
883 883 return True
884 884 return False
885 885
886 886
887 887 class HasRepoGroupPermissionAnyDecorator(PermsDecorator):
888 888 """
889 889 Checks for access permission for any of given predicates for specific
890 890 repository group. In order to fulfill the request any of predicates must be meet
891 891 """
892 892
893 893 def check_permissions(self):
894 894 group_name = get_repo_group_slug(request)
895 895 try:
896 896 user_perms = set([self.user_perms['repositories_groups'][group_name]])
897 897 except KeyError:
898 898 return False
899 899
900 900 if self.required_perms.intersection(user_perms):
901 901 return True
902 902 return False
903 903
904 904
905 905 class HasUserGroupPermissionAnyDecorator(PermsDecorator):
906 906 """
907 907 Checks for access permission for any of given predicates for specific
908 908 user group. In order to fulfill the request any of predicates must be meet
909 909 """
910 910
911 911 def check_permissions(self):
912 912 group_name = get_user_group_slug(request)
913 913 try:
914 914 user_perms = set([self.user_perms['user_groups'][group_name]])
915 915 except KeyError:
916 916 return False
917 917
918 918 if self.required_perms.intersection(user_perms):
919 919 return True
920 920 return False
921 921
922 922
923 923 #==============================================================================
924 924 # CHECK FUNCTIONS
925 925 #==============================================================================
926 926 class PermsFunction(object):
927 927 """Base function for other check functions"""
928 928
929 929 def __init__(self, *perms):
930 930 self.required_perms = set(perms)
931 931 self.user_perms = None
932 932 self.repo_name = None
933 933 self.group_name = None
934 934
935 935 def __nonzero__(self):
936 936 """ Defend against accidentally forgetting to call the object
937 937 and instead evaluating it directly in a boolean context,
938 938 which could have security implications.
939 939 """
940 940 raise AssertionError(self.__class__.__name__ + ' is not a bool and must be called!')
941 941
942 942 def __call__(self, check_location='unspecified location'):
943 943 user = request.user
944 944 assert user
945 945 assert isinstance(user, AuthUser), user
946 946
947 947 cls_name = self.__class__.__name__
948 948 check_scope = self._scope()
949 949 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
950 950 self.required_perms, user, check_scope,
951 951 check_location)
952 952 self.user_perms = user.permissions
953 953
954 954 result = self.check_permissions()
955 955 result_text = 'granted' if result else 'denied'
956 956 log.debug('Permission to %s %s for user: %s @ %s',
957 957 check_scope, result_text, user, check_location)
958 958 return result
959 959
960 960 def check_permissions(self):
961 961 """Dummy function for overriding"""
962 962 raise Exception('You have to write this function in child class')
963 963
964 964 def _scope(self):
965 965 return '(unknown scope)'
966 966
967 967
968 968 class HasPermissionAny(PermsFunction):
969 969 def check_permissions(self):
970 970 if self.required_perms.intersection(self.user_perms.get('global')):
971 971 return True
972 972 return False
973 973
974 974
975 975 class HasRepoPermissionAny(PermsFunction):
976 976 def __call__(self, repo_name=None, check_location=''):
977 977 self.repo_name = repo_name
978 978 return super(HasRepoPermissionAny, self).__call__(check_location)
979 979
980 980 def check_permissions(self):
981 981 if not self.repo_name:
982 982 self.repo_name = get_repo_slug(request)
983 983
984 984 try:
985 985 self._user_perms = set(
986 986 [self.user_perms['repositories'][self.repo_name]]
987 987 )
988 988 except KeyError:
989 989 return False
990 990 if self.required_perms.intersection(self._user_perms):
991 991 return True
992 992 return False
993 993
994 994 def _scope(self):
995 995 return 'repo:%s' % self.repo_name
996 996
997 997
998 998 class HasRepoGroupPermissionAny(PermsFunction):
999 999 def __call__(self, group_name=None, check_location=''):
1000 1000 self.group_name = group_name
1001 1001 return super(HasRepoGroupPermissionAny, self).__call__(check_location)
1002 1002
1003 1003 def check_permissions(self):
1004 1004 try:
1005 1005 self._user_perms = set(
1006 1006 [self.user_perms['repositories_groups'][self.group_name]]
1007 1007 )
1008 1008 except KeyError:
1009 1009 return False
1010 1010 if self.required_perms.intersection(self._user_perms):
1011 1011 return True
1012 1012 return False
1013 1013
1014 1014 def _scope(self):
1015 1015 return 'repogroup:%s' % self.group_name
1016 1016
1017 1017
1018 1018 class HasUserGroupPermissionAny(PermsFunction):
1019 1019 def __call__(self, user_group_name=None, check_location=''):
1020 1020 self.user_group_name = user_group_name
1021 1021 return super(HasUserGroupPermissionAny, self).__call__(check_location)
1022 1022
1023 1023 def check_permissions(self):
1024 1024 try:
1025 1025 self._user_perms = set(
1026 1026 [self.user_perms['user_groups'][self.user_group_name]]
1027 1027 )
1028 1028 except KeyError:
1029 1029 return False
1030 1030 if self.required_perms.intersection(self._user_perms):
1031 1031 return True
1032 1032 return False
1033 1033
1034 1034 def _scope(self):
1035 1035 return 'usergroup:%s' % self.user_group_name
1036 1036
1037 1037
1038 1038 #==============================================================================
1039 1039 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
1040 1040 #==============================================================================
1041 1041 class HasPermissionAnyMiddleware(object):
1042 1042 def __init__(self, *perms):
1043 1043 self.required_perms = set(perms)
1044 1044
1045 1045 def __call__(self, user, repo_name):
1046 1046 # repo_name MUST be unicode, since we handle keys in permission
1047 1047 # dict by unicode
1048 1048 repo_name = safe_unicode(repo_name)
1049 1049 usr = AuthUser(user.user_id)
1050 1050 self.user_perms = set([usr.permissions['repositories'][repo_name]])
1051 1051 self.username = user.username
1052 1052 self.repo_name = repo_name
1053 1053 return self.check_permissions()
1054 1054
1055 1055 def check_permissions(self):
1056 1056 log.debug('checking VCS protocol '
1057 1057 'permissions %s for user:%s repository:%s', self.user_perms,
1058 1058 self.username, self.repo_name)
1059 1059 if self.required_perms.intersection(self.user_perms):
1060 1060 log.debug('Permission to repo: %s granted for user: %s @ %s',
1061 1061 self.repo_name, self.username, 'PermissionMiddleware')
1062 1062 return True
1063 1063 log.debug('Permission to repo: %s denied for user: %s @ %s',
1064 1064 self.repo_name, self.username, 'PermissionMiddleware')
1065 1065 return False
1066 1066
1067 1067
1068 1068 def check_ip_access(source_ip, allowed_ips=None):
1069 1069 """
1070 1070 Checks if source_ip is a subnet of any of allowed_ips.
1071 1071
1072 1072 :param source_ip:
1073 1073 :param allowed_ips: list of allowed ips together with mask
1074 1074 """
1075 1075 from kallithea.lib import ipaddr
1076 1076 log.debug('checking if ip:%s is subnet of %s', source_ip, allowed_ips)
1077 1077 if isinstance(allowed_ips, (tuple, list, set)):
1078 1078 for ip in allowed_ips:
1079 1079 if ipaddr.IPAddress(source_ip) in ipaddr.IPNetwork(ip):
1080 1080 log.debug('IP %s is network %s',
1081 1081 ipaddr.IPAddress(source_ip), ipaddr.IPNetwork(ip))
1082 1082 return True
1083 1083 return False
@@ -1,466 +1,466 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.lib.hooks
16 16 ~~~~~~~~~~~~~~~~~~~
17 17
18 18 Hooks run by Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Aug 6, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import os
29 29 import sys
30 30 import time
31 31 import binascii
32 32
33 33 from kallithea.lib.vcs.utils.hgcompat import nullrev, revrange
34 34 from kallithea.lib import helpers as h
35 35 from kallithea.lib.utils import action_logger
36 36 from kallithea.lib.vcs.backends.base import EmptyChangeset
37 37 from kallithea.lib.exceptions import HTTPLockedRC, UserCreationError
38 38 from kallithea.lib.utils2 import safe_str, safe_unicode, _extract_extras
39 39 from kallithea.model.db import Repository, User
40 40
41 41
42 42 def _get_scm_size(alias, root_path):
43 43
44 44 if not alias.startswith('.'):
45 45 alias += '.'
46 46
47 47 size_scm, size_root = 0, 0
48 48 for path, dirs, files in os.walk(safe_str(root_path)):
49 49 if path.find(alias) != -1:
50 50 for f in files:
51 51 try:
52 52 size_scm += os.path.getsize(os.path.join(path, f))
53 53 except OSError:
54 54 pass
55 55 else:
56 56 for f in files:
57 57 try:
58 58 size_root += os.path.getsize(os.path.join(path, f))
59 59 except OSError:
60 60 pass
61 61
62 62 size_scm_f = h.format_byte_size(size_scm)
63 63 size_root_f = h.format_byte_size(size_root)
64 64 size_total_f = h.format_byte_size(size_root + size_scm)
65 65
66 66 return size_scm_f, size_root_f, size_total_f
67 67
68 68
69 69 def repo_size(ui, repo, hooktype=None, **kwargs):
70 70 """
71 71 Presents size of repository after push
72 72
73 73 :param ui:
74 74 :param repo:
75 75 :param hooktype:
76 76 """
77 77
78 78 size_hg_f, size_root_f, size_total_f = _get_scm_size('.hg', repo.root)
79 79
80 80 last_cs = repo[len(repo) - 1]
81 81
82 82 msg = ('Repository size .hg:%s repo:%s total:%s\n'
83 83 'Last revision is now r%s:%s\n') % (
84 84 size_hg_f, size_root_f, size_total_f, last_cs.rev(), last_cs.hex()[:12]
85 85 )
86 86 ui.status(msg)
87 87
88 88
89 89 def pre_push(ui, repo, **kwargs):
90 90 # pre push function, currently used to ban pushing when
91 91 # repository is locked
92 92 ex = _extract_extras()
93 93
94 94 usr = User.get_by_username(ex.username)
95 95 if ex.locked_by[0] and usr.user_id != int(ex.locked_by[0]):
96 96 locked_by = User.get(ex.locked_by[0]).username
97 97 # this exception is interpreted in git/hg middlewares and based
98 98 # on that proper return code is server to client
99 99 _http_ret = HTTPLockedRC(ex.repository, locked_by)
100 100 if str(_http_ret.code).startswith('2'):
101 101 #2xx Codes don't raise exceptions
102 102 ui.status(safe_str(_http_ret.title))
103 103 else:
104 104 raise _http_ret
105 105
106 106
107 107 def pre_pull(ui, repo, **kwargs):
108 108 # pre pull function ...
109 109 ex = _extract_extras()
110 110 if ex.locked_by[0]:
111 111 locked_by = User.get(ex.locked_by[0]).username
112 112 # this exception is interpreted in git/hg middlewares and based
113 113 # on that proper return code is server to client
114 114 _http_ret = HTTPLockedRC(ex.repository, locked_by)
115 115 if str(_http_ret.code).startswith('2'):
116 116 #2xx Codes don't raise exceptions
117 117 ui.status(safe_str(_http_ret.title))
118 118 else:
119 119 raise _http_ret
120 120
121 121
122 122 def log_pull_action(ui, repo, **kwargs):
123 123 """
124 124 Logs user last pull action
125 125
126 126 :param ui:
127 127 :param repo:
128 128 """
129 129 ex = _extract_extras()
130 130
131 131 user = User.get_by_username(ex.username)
132 132 action = 'pull'
133 133 action_logger(user, action, ex.repository, ex.ip, commit=True)
134 134 # extension hook call
135 135 from kallithea import EXTENSIONS
136 136 callback = getattr(EXTENSIONS, 'PULL_HOOK', None)
137 137 if callable(callback):
138 138 kw = {}
139 139 kw.update(ex)
140 140 callback(**kw)
141 141
142 142 if ex.make_lock is not None and ex.make_lock:
143 143 Repository.lock(Repository.get_by_repo_name(ex.repository), user.user_id)
144 144 #msg = 'Made lock on repo `%s`' % repository
145 145 #ui.status(msg)
146 146
147 147 if ex.locked_by[0]:
148 148 locked_by = User.get(ex.locked_by[0]).username
149 149 _http_ret = HTTPLockedRC(ex.repository, locked_by)
150 150 if str(_http_ret.code).startswith('2'):
151 151 #2xx Codes don't raise exceptions
152 152 ui.status(safe_str(_http_ret.title))
153 153 return 0
154 154
155 155
156 156 def log_push_action(ui, repo, **kwargs):
157 157 """
158 158 Register that changes have been pushed.
159 159 Mercurial invokes this directly as a hook, git uses handle_git_receive.
160 160 """
161 161
162 162 ex = _extract_extras()
163 163
164 164 action_tmpl = ex.action + ':%s'
165 165 revs = []
166 166 if ex.scm == 'hg':
167 167 node = kwargs['node']
168 168
169 169 def get_revs(repo, rev_opt):
170 170 if rev_opt:
171 171 revs = revrange(repo, rev_opt)
172 172
173 173 if len(revs) == 0:
174 174 return (nullrev, nullrev)
175 175 return max(revs), min(revs)
176 176 else:
177 177 return len(repo) - 1, 0
178 178
179 179 stop, start = get_revs(repo, [node + ':'])
180 180 _h = binascii.hexlify
181 181 revs = [_h(repo[r].node()) for r in xrange(start, stop + 1)]
182 182 elif ex.scm == 'git':
183 183 revs = kwargs.get('_git_revs', [])
184 184 if '_git_revs' in kwargs:
185 185 kwargs.pop('_git_revs')
186 186
187 187 action = action_tmpl % ','.join(revs)
188 188 action_logger(ex.username, action, ex.repository, ex.ip, commit=True)
189 189
190 190 # extension hook call
191 191 from kallithea import EXTENSIONS
192 192 callback = getattr(EXTENSIONS, 'PUSH_HOOK', None)
193 193 if callable(callback):
194 194 kw = {'pushed_revs': revs}
195 195 kw.update(ex)
196 196 callback(**kw)
197 197
198 198 if ex.make_lock is not None and not ex.make_lock:
199 199 Repository.unlock(Repository.get_by_repo_name(ex.repository))
200 200 ui.status(safe_str('Released lock on repo `%s`\n' % ex.repository))
201 201
202 202 if ex.locked_by[0]:
203 203 locked_by = User.get(ex.locked_by[0]).username
204 204 _http_ret = HTTPLockedRC(ex.repository, locked_by)
205 205 if str(_http_ret.code).startswith('2'):
206 206 #2xx Codes don't raise exceptions
207 207 ui.status(safe_str(_http_ret.title))
208 208
209 209 return 0
210 210
211 211
212 212 def log_create_repository(repository_dict, created_by, **kwargs):
213 213 """
214 214 Post create repository Hook.
215 215
216 216 :param repository: dict dump of repository object
217 217 :param created_by: username who created repository
218 218
219 219 available keys of repository_dict:
220 220
221 221 'repo_type',
222 222 'description',
223 223 'private',
224 224 'created_on',
225 225 'enable_downloads',
226 226 'repo_id',
227 227 'user_id',
228 228 'enable_statistics',
229 229 'clone_uri',
230 230 'fork_id',
231 231 'group_id',
232 232 'repo_name'
233 233
234 234 """
235 235 from kallithea import EXTENSIONS
236 236 callback = getattr(EXTENSIONS, 'CREATE_REPO_HOOK', None)
237 237 if callable(callback):
238 238 kw = {}
239 239 kw.update(repository_dict)
240 240 kw.update({'created_by': created_by})
241 241 kw.update(kwargs)
242 242 return callback(**kw)
243 243
244 244 return 0
245 245
246 246
247 247 def check_allowed_create_user(user_dict, created_by, **kwargs):
248 248 # pre create hooks
249 249 from kallithea import EXTENSIONS
250 250 callback = getattr(EXTENSIONS, 'PRE_CREATE_USER_HOOK', None)
251 251 if callable(callback):
252 252 allowed, reason = callback(created_by=created_by, **user_dict)
253 253 if not allowed:
254 254 raise UserCreationError(reason)
255 255
256 256
257 257 def log_create_user(user_dict, created_by, **kwargs):
258 258 """
259 259 Post create user Hook.
260 260
261 261 :param user_dict: dict dump of user object
262 262
263 263 available keys for user_dict:
264 264
265 265 'username',
266 266 'full_name_or_username',
267 267 'full_contact',
268 268 'user_id',
269 269 'name',
270 270 'firstname',
271 271 'short_contact',
272 272 'admin',
273 273 'lastname',
274 274 'ip_addresses',
275 275 'ldap_dn',
276 276 'email',
277 277 'api_key',
278 278 'last_login',
279 279 'full_name',
280 280 'active',
281 281 'password',
282 282 'emails',
283 283 'inherit_default_permissions'
284 284
285 285 """
286 286 from kallithea import EXTENSIONS
287 287 callback = getattr(EXTENSIONS, 'CREATE_USER_HOOK', None)
288 288 if callable(callback):
289 289 return callback(created_by=created_by, **user_dict)
290 290
291 291 return 0
292 292
293 293
294 294 def log_delete_repository(repository_dict, deleted_by, **kwargs):
295 295 """
296 296 Post delete repository Hook.
297 297
298 298 :param repository: dict dump of repository object
299 299 :param deleted_by: username who deleted the repository
300 300
301 301 available keys of repository_dict:
302 302
303 303 'repo_type',
304 304 'description',
305 305 'private',
306 306 'created_on',
307 307 'enable_downloads',
308 308 'repo_id',
309 309 'user_id',
310 310 'enable_statistics',
311 311 'clone_uri',
312 312 'fork_id',
313 313 'group_id',
314 314 'repo_name'
315 315
316 316 """
317 317 from kallithea import EXTENSIONS
318 318 callback = getattr(EXTENSIONS, 'DELETE_REPO_HOOK', None)
319 319 if callable(callback):
320 320 kw = {}
321 321 kw.update(repository_dict)
322 322 kw.update({'deleted_by': deleted_by,
323 323 'deleted_on': time.time()})
324 324 kw.update(kwargs)
325 325 return callback(**kw)
326 326
327 327 return 0
328 328
329 329
330 330 def log_delete_user(user_dict, deleted_by, **kwargs):
331 331 """
332 332 Post delete user Hook.
333 333
334 334 :param user_dict: dict dump of user object
335 335
336 336 available keys for user_dict:
337 337
338 338 'username',
339 339 'full_name_or_username',
340 340 'full_contact',
341 341 'user_id',
342 342 'name',
343 343 'firstname',
344 344 'short_contact',
345 345 'admin',
346 346 'lastname',
347 347 'ip_addresses',
348 348 'ldap_dn',
349 349 'email',
350 350 'api_key',
351 351 'last_login',
352 352 'full_name',
353 353 'active',
354 354 'password',
355 355 'emails',
356 356 'inherit_default_permissions'
357 357
358 358 """
359 359 from kallithea import EXTENSIONS
360 360 callback = getattr(EXTENSIONS, 'DELETE_USER_HOOK', None)
361 361 if callable(callback):
362 362 return callback(deleted_by=deleted_by, **user_dict)
363 363
364 364 return 0
365 365
366 366
367 367 def handle_git_pre_receive(repo_path, revs, env):
368 368 return handle_git_receive(repo_path, revs, env, hook_type='pre')
369 369
370 370 def handle_git_post_receive(repo_path, revs, env):
371 371 return handle_git_receive(repo_path, revs, env, hook_type='post')
372 372
373 373 def handle_git_receive(repo_path, revs, env, hook_type):
374 374 """
375 375 A really hacky method that is run by git post-receive hook and logs
376 an push action together with pushed revisions. It's executed by subprocess
377 thus needs all info to be able to create a on the fly pylons environment,
376 a push action together with pushed revisions. It's executed by subprocess
377 thus needs all info to be able to create an on the fly app environment,
378 378 connect to database and run the logging code. Hacky as sh*t but works.
379 379
380 380 :param repo_path:
381 381 :param revs:
382 382 :param env:
383 383 """
384 384 from paste.deploy import appconfig
385 385 from sqlalchemy import engine_from_config
386 386 from kallithea.config.environment import load_environment
387 387 from kallithea.model import init_model
388 388 from kallithea.model.db import Ui
389 389 from kallithea.lib.utils import make_ui
390 390 extras = _extract_extras(env)
391 391
392 392 repo_path = safe_unicode(repo_path)
393 393 path, ini_name = os.path.split(extras['config'])
394 394 conf = appconfig('config:%s' % ini_name, relative_to=path)
395 395 load_environment(conf.global_conf, conf.local_conf, test_env=False,
396 396 test_index=False)
397 397
398 398 engine = engine_from_config(conf, 'sqlalchemy.')
399 399 init_model(engine)
400 400
401 401 baseui = make_ui('db')
402 402 # fix if it's not a bare repo
403 403 if repo_path.endswith(os.sep + '.git'):
404 404 repo_path = repo_path[:-5]
405 405
406 406 repo = Repository.get_by_full_path(repo_path)
407 407 if not repo:
408 408 raise OSError('Repository %s not found in database'
409 409 % (safe_str(repo_path)))
410 410
411 411 _hooks = dict(baseui.configitems('hooks')) or {}
412 412
413 413 if hook_type == 'pre':
414 414 repo = repo.scm_instance
415 415 else:
416 416 #post push shouldn't use the cached instance never
417 417 repo = repo.scm_instance_no_cache()
418 418
419 419 if hook_type == 'pre':
420 420 pre_push(baseui, repo)
421 421
422 422 # if push hook is enabled via web interface
423 423 elif hook_type == 'post' and _hooks.get(Ui.HOOK_PUSH):
424 424 rev_data = []
425 425 for l in revs:
426 426 old_rev, new_rev, ref = l.strip().split(' ')
427 427 _ref_data = ref.split('/')
428 428 if _ref_data[1] in ['tags', 'heads']:
429 429 rev_data.append({'old_rev': old_rev,
430 430 'new_rev': new_rev,
431 431 'ref': ref,
432 432 'type': _ref_data[1],
433 433 'name': '/'.join(_ref_data[2:])})
434 434
435 435 git_revs = []
436 436
437 437 for push_ref in rev_data:
438 438 _type = push_ref['type']
439 439 if _type == 'heads':
440 440 if push_ref['old_rev'] == EmptyChangeset().raw_id:
441 441 # update the symbolic ref if we push new repo
442 442 if repo.is_empty():
443 443 repo._repo.refs.set_symbolic_ref('HEAD',
444 444 'refs/heads/%s' % push_ref['name'])
445 445
446 446 cmd = ['for-each-ref', '--format=%(refname)','refs/heads/*']
447 447 heads = repo.run_git_command(cmd)[0]
448 448 cmd = ['log', push_ref['new_rev'],
449 449 '--reverse', '--pretty=format:%H', '--not']
450 450 heads = heads.replace(push_ref['ref'], '')
451 451 for l in heads.splitlines():
452 452 cmd.append(l.strip())
453 453 git_revs += repo.run_git_command(cmd)[0].splitlines()
454 454
455 455 elif push_ref['new_rev'] == EmptyChangeset().raw_id:
456 456 #delete branch case
457 457 git_revs += ['delete_branch=>%s' % push_ref['name']]
458 458 else:
459 459 cmd = ['log', '%(old_rev)s..%(new_rev)s' % push_ref,
460 460 '--reverse', '--pretty=format:%H']
461 461 git_revs += repo.run_git_command(cmd)[0].splitlines()
462 462
463 463 elif _type == 'tags':
464 464 git_revs += ['tag=>%s' % push_ref['name']]
465 465
466 466 log_push_action(baseui, repo, _git_revs=git_revs)
@@ -1,107 +1,107 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.lib.paster_commands.common
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 Common code for Paster commands.
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 18, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import os
29 29 import logging
30 30
31 31 import paste
32 32 from paste.script.command import Command, BadCommand
33 33
34 34 from kallithea.lib.utils import setup_cache_regions
35 35
36 36
37 37 def ask_ok(prompt, retries=4, complaint='Yes or no please!'):
38 38 while True:
39 39 ok = raw_input(prompt)
40 40 if ok in ('y', 'ye', 'yes'):
41 41 return True
42 42 if ok in ('n', 'no', 'nop', 'nope'):
43 43 return False
44 44 retries = retries - 1
45 45 if retries < 0:
46 46 raise IOError
47 47 print complaint
48 48
49 49
50 50 class BasePasterCommand(Command):
51 51 """
52 52 Abstract Base Class for paster commands.
53 53 """
54 54 min_args = 1
55 55 min_args_error = "Please provide a paster config file as an argument."
56 56 takes_config_file = 1
57 57 requires_config_file = True
58 58
59 59 def run(self, args):
60 60 """
61 61 Overrides Command.run
62 62
63 63 Checks for a config file argument and loads it.
64 64 """
65 65 if len(args) < self.min_args:
66 66 raise BadCommand(
67 67 self.min_args_error % {'min_args': self.min_args,
68 68 'actual_args': len(args)})
69 69
70 70 # Decrement because we're going to lob off the first argument.
71 71 # @@ This is hacky
72 72 self.min_args -= 1
73 73 self.bootstrap_config(args[0])
74 74 self.update_parser()
75 75 return super(BasePasterCommand, self).run(args[1:])
76 76
77 77 def update_parser(self):
78 78 """
79 79 Abstract method. Allows for the class's parser to be updated
80 80 before the superclass's `run` method is called. Necessary to
81 81 allow options/arguments to be passed through to the underlying
82 82 celery command.
83 83 """
84 84 raise NotImplementedError("Abstract Method.")
85 85
86 86 def bootstrap_config(self, conf):
87 87 """
88 Loads the pylons configuration.
88 Loads the app configuration.
89 89 """
90 90 from pylons import config as pylonsconfig
91 91
92 92 self.path_to_ini_file = os.path.realpath(conf)
93 93 conf = paste.deploy.appconfig('config:' + self.path_to_ini_file)
94 94 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
95 95
96 96 def _init_session(self):
97 97 """
98 98 Inits SqlAlchemy Session
99 99 """
100 100 logging.config.fileConfig(self.path_to_ini_file)
101 101
102 102 from pylons import config
103 103 from kallithea.model import init_model
104 104 from kallithea.lib.utils2 import engine_from_config
105 105 setup_cache_regions(config)
106 106 engine = engine_from_config(config, 'sqlalchemy.')
107 107 init_model(engine)
@@ -1,799 +1,799 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.lib.utils
16 16 ~~~~~~~~~~~~~~~~~~~
17 17
18 18 Utilities library for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 18, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import os
29 29 import re
30 30 import logging
31 31 import datetime
32 32 import traceback
33 33 import paste
34 34 import beaker
35 35 import tarfile
36 36 import shutil
37 37 import decorator
38 38 import warnings
39 39 from os.path import abspath
40 40 from os.path import dirname
41 41
42 42 from webhelpers.text import collapse, remove_formatting, strip_tags
43 43 from beaker.cache import _cache_decorate
44 44
45 45 from kallithea.lib.vcs.utils.hgcompat import ui, config
46 46 from kallithea.lib.vcs.utils.helpers import get_scm
47 47 from kallithea.lib.vcs.exceptions import VCSError
48 48
49 49 from kallithea.model import meta
50 50 from kallithea.model.db import Repository, User, Ui, \
51 51 UserLog, RepoGroup, Setting, UserGroup
52 52 from kallithea.model.meta import Session
53 53 from kallithea.model.repo_group import RepoGroupModel
54 54 from kallithea.lib.utils2 import safe_str, safe_unicode, get_current_authuser
55 55 from kallithea.lib.vcs.utils.fakemod import create_module
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
62 62 def recursive_replace(str_, replace=' '):
63 63 """
64 64 Recursive replace of given sign to just one instance
65 65
66 66 :param str_: given string
67 67 :param replace: char to find and replace multiple instances
68 68
69 69 Examples::
70 70 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
71 71 'Mighty-Mighty-Bo-sstones'
72 72 """
73 73
74 74 if str_.find(replace * 2) == -1:
75 75 return str_
76 76 else:
77 77 str_ = str_.replace(replace * 2, replace)
78 78 return recursive_replace(str_, replace)
79 79
80 80
81 81 def repo_name_slug(value):
82 82 """
83 83 Return slug of name of repository
84 84 This function is called on each creation/modification
85 85 of repository to prevent bad names in repo
86 86 """
87 87
88 88 slug = remove_formatting(value)
89 89 slug = strip_tags(slug)
90 90
91 91 for c in """`?=[]\;'"<>,/~!@#$%^&*()+{}|: """:
92 92 slug = slug.replace(c, '-')
93 93 slug = recursive_replace(slug, '-')
94 94 slug = collapse(slug, '-')
95 95 return slug
96 96
97 97
98 98 #==============================================================================
99 99 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
100 100 #==============================================================================
101 101 def get_repo_slug(request):
102 102 _repo = request.environ['pylons.routes_dict'].get('repo_name')
103 103 if _repo:
104 104 _repo = _repo.rstrip('/')
105 105 return _repo
106 106
107 107
108 108 def get_repo_group_slug(request):
109 109 _group = request.environ['pylons.routes_dict'].get('group_name')
110 110 if _group:
111 111 _group = _group.rstrip('/')
112 112 return _group
113 113
114 114
115 115 def get_user_group_slug(request):
116 116 _group = request.environ['pylons.routes_dict'].get('id')
117 117 _group = UserGroup.get(_group)
118 118 if _group:
119 119 return _group.users_group_name
120 120 return None
121 121
122 122
123 123 def _extract_id_from_repo_name(repo_name):
124 124 if repo_name.startswith('/'):
125 125 repo_name = repo_name.lstrip('/')
126 126 by_id_match = re.match(r'^_(\d{1,})', repo_name)
127 127 if by_id_match:
128 128 return by_id_match.groups()[0]
129 129
130 130
131 131 def get_repo_by_id(repo_name):
132 132 """
133 133 Extracts repo_name by id from special urls. Example url is _11/repo_name
134 134
135 135 :param repo_name:
136 136 :return: repo_name if matched else None
137 137 """
138 138 _repo_id = _extract_id_from_repo_name(repo_name)
139 139 if _repo_id:
140 140 from kallithea.model.db import Repository
141 141 repo = Repository.get(_repo_id)
142 142 if repo:
143 143 # TODO: return repo instead of reponame? or would that be a layering violation?
144 144 return repo.repo_name
145 145 return None
146 146
147 147
148 148 def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
149 149 """
150 150 Action logger for various actions made by users
151 151
152 152 :param user: user that made this action, can be a unique username string or
153 153 object containing user_id attribute
154 154 :param action: action to log, should be on of predefined unique actions for
155 155 easy translations
156 156 :param repo: string name of repository or object containing repo_id,
157 157 that action was made on
158 158 :param ipaddr: optional IP address from what the action was made
159 159 :param sa: optional sqlalchemy session
160 160
161 161 """
162 162
163 163 if not sa:
164 164 sa = meta.Session()
165 165 # if we don't get explicit IP address try to get one from registered user
166 166 # in tmpl context var
167 167 if not ipaddr:
168 168 ipaddr = getattr(get_current_authuser(), 'ip_addr', '')
169 169
170 170 if getattr(user, 'user_id', None):
171 171 user_obj = User.get(user.user_id)
172 172 elif isinstance(user, basestring):
173 173 user_obj = User.get_by_username(user)
174 174 else:
175 175 raise Exception('You have to provide a user object or a username')
176 176
177 177 if getattr(repo, 'repo_id', None):
178 178 repo_obj = Repository.get(repo.repo_id)
179 179 repo_name = repo_obj.repo_name
180 180 elif isinstance(repo, basestring):
181 181 repo_name = repo.lstrip('/')
182 182 repo_obj = Repository.get_by_repo_name(repo_name)
183 183 else:
184 184 repo_obj = None
185 185 repo_name = u''
186 186
187 187 user_log = UserLog()
188 188 user_log.user_id = user_obj.user_id
189 189 user_log.username = user_obj.username
190 190 user_log.action = safe_unicode(action)
191 191
192 192 user_log.repository = repo_obj
193 193 user_log.repository_name = repo_name
194 194
195 195 user_log.action_date = datetime.datetime.now()
196 196 user_log.user_ip = ipaddr
197 197 sa.add(user_log)
198 198
199 199 log.info('Logging action:%s on %s by user:%s ip:%s',
200 200 action, safe_unicode(repo), user_obj, ipaddr)
201 201 if commit:
202 202 sa.commit()
203 203
204 204
205 205 def get_filesystem_repos(path):
206 206 """
207 207 Scans given path for repos and return (name,(type,path)) tuple
208 208
209 209 :param path: path to scan for repositories
210 210 :param recursive: recursive search and return names with subdirs in front
211 211 """
212 212
213 213 # remove ending slash for better results
214 214 path = safe_str(path.rstrip(os.sep))
215 215 log.debug('now scanning in %s', path)
216 216
217 217 def isdir(*n):
218 218 return os.path.isdir(os.path.join(*n))
219 219
220 220 for root, dirs, _files in os.walk(path):
221 221 recurse_dirs = []
222 222 for subdir in dirs:
223 223 # skip removed repos
224 224 if REMOVED_REPO_PAT.match(subdir):
225 225 continue
226 226
227 227 #skip .<something> dirs TODO: rly? then we should prevent creating them ...
228 228 if subdir.startswith('.'):
229 229 continue
230 230
231 231 cur_path = os.path.join(root, subdir)
232 232 if (isdir(cur_path, '.hg') or
233 233 isdir(cur_path, '.git') or
234 234 isdir(cur_path, '.svn') or
235 235 isdir(cur_path, 'objects') and (isdir(cur_path, 'refs') or isfile(cur_path, 'packed-refs'))):
236 236
237 237 if not os.access(cur_path, os.R_OK) or not os.access(cur_path, os.X_OK):
238 238 log.warning('ignoring repo path without access: %s', cur_path)
239 239 continue
240 240
241 241 if not os.access(cur_path, os.W_OK):
242 242 log.warning('repo path without write access: %s', cur_path)
243 243
244 244 try:
245 245 scm_info = get_scm(cur_path)
246 246 assert cur_path.startswith(path)
247 247 repo_path = cur_path[len(path) + 1:]
248 248 yield repo_path, scm_info
249 249 continue # no recursion
250 250 except VCSError:
251 251 # We should perhaps ignore such broken repos, but especially
252 252 # the bare git detection is unreliable so we dive into it
253 253 pass
254 254
255 255 recurse_dirs.append(subdir)
256 256
257 257 dirs[:] = recurse_dirs
258 258
259 259
260 260 def is_valid_repo(repo_name, base_path, scm=None):
261 261 """
262 262 Returns True if given path is a valid repository False otherwise.
263 263 If scm param is given also compare if given scm is the same as expected
264 264 from scm parameter
265 265
266 266 :param repo_name:
267 267 :param base_path:
268 268 :param scm:
269 269
270 270 :return True: if given path is a valid repository
271 271 """
272 272 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
273 273
274 274 try:
275 275 scm_ = get_scm(full_path)
276 276 if scm:
277 277 return scm_[0] == scm
278 278 return True
279 279 except VCSError:
280 280 return False
281 281
282 282
283 283 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
284 284 """
285 285 Returns True if given path is a repository group False otherwise
286 286
287 287 :param repo_name:
288 288 :param base_path:
289 289 """
290 290 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
291 291
292 292 # check if it's not a repo
293 293 if is_valid_repo(repo_group_name, base_path):
294 294 return False
295 295
296 296 try:
297 297 # we need to check bare git repos at higher level
298 298 # since we might match branches/hooks/info/objects or possible
299 299 # other things inside bare git repo
300 300 get_scm(os.path.dirname(full_path))
301 301 return False
302 302 except VCSError:
303 303 pass
304 304
305 305 # check if it's a valid path
306 306 if skip_path_check or os.path.isdir(full_path):
307 307 return True
308 308
309 309 return False
310 310
311 311
312 312 #propagated from mercurial documentation
313 313 ui_sections = ['alias', 'auth',
314 314 'decode/encode', 'defaults',
315 315 'diff', 'email',
316 316 'extensions', 'format',
317 317 'merge-patterns', 'merge-tools',
318 318 'hooks', 'http_proxy',
319 319 'smtp', 'patch',
320 320 'paths', 'profiling',
321 321 'server', 'trusted',
322 322 'ui', 'web', ]
323 323
324 324
325 325 def make_ui(read_from='file', path=None, checkpaths=True, clear_session=True):
326 326 """
327 327 A function that will read python rc files or database
328 328 and make an mercurial ui object from read options
329 329
330 330 :param path: path to mercurial config file
331 331 :param checkpaths: check the path
332 332 :param read_from: read from 'file' or 'db'
333 333 """
334 334
335 335 baseui = ui.ui()
336 336
337 337 # clean the baseui object
338 338 baseui._ocfg = config.config()
339 339 baseui._ucfg = config.config()
340 340 baseui._tcfg = config.config()
341 341
342 342 if read_from == 'file':
343 343 if not os.path.isfile(path):
344 344 log.debug('hgrc file is not present at %s, skipping...', path)
345 345 return False
346 346 log.debug('reading hgrc from %s', path)
347 347 cfg = config.config()
348 348 cfg.read(path)
349 349 for section in ui_sections:
350 350 for k, v in cfg.items(section):
351 351 log.debug('settings ui from file: [%s] %s=%s', section, k, v)
352 352 baseui.setconfig(safe_str(section), safe_str(k), safe_str(v))
353 353
354 354 elif read_from == 'db':
355 355 sa = meta.Session()
356 356 ret = sa.query(Ui).all()
357 357
358 358 hg_ui = ret
359 359 for ui_ in hg_ui:
360 360 if ui_.ui_active:
361 361 ui_val = '' if ui_.ui_value is None else safe_str(ui_.ui_value)
362 362 log.debug('settings ui from db: [%s] %s=%r', ui_.ui_section,
363 363 ui_.ui_key, ui_val)
364 364 baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
365 365 ui_val)
366 366 if clear_session:
367 367 meta.Session.remove()
368 368
369 369 # force set push_ssl requirement to False, Kallithea handles that
370 370 baseui.setconfig('web', 'push_ssl', False)
371 371 baseui.setconfig('web', 'allow_push', '*')
372 372 # prevent interactive questions for ssh password / passphrase
373 373 ssh = baseui.config('ui', 'ssh', default='ssh')
374 374 baseui.setconfig('ui', 'ssh', '%s -oBatchMode=yes -oIdentitiesOnly=yes' % ssh)
375 375
376 376 return baseui
377 377
378 378
379 379 def set_app_settings(config):
380 380 """
381 Updates pylons config with new settings from database
381 Updates app config with new settings from database
382 382
383 383 :param config:
384 384 """
385 385 hgsettings = Setting.get_app_settings()
386 386
387 387 for k, v in hgsettings.items():
388 388 config[k] = v
389 389
390 390
391 391 def set_vcs_config(config):
392 392 """
393 393 Patch VCS config with some Kallithea specific stuff
394 394
395 395 :param config: kallithea.CONFIG
396 396 """
397 397 from kallithea.lib.vcs import conf
398 398 from kallithea.lib.utils2 import aslist
399 399 conf.settings.BACKENDS = {
400 400 'hg': 'kallithea.lib.vcs.backends.hg.MercurialRepository',
401 401 'git': 'kallithea.lib.vcs.backends.git.GitRepository',
402 402 }
403 403
404 404 conf.settings.GIT_EXECUTABLE_PATH = config.get('git_path', 'git')
405 405 conf.settings.GIT_REV_FILTER = config.get('git_rev_filter', '--all').strip()
406 406 conf.settings.DEFAULT_ENCODINGS = aslist(config.get('default_encoding',
407 407 'utf8'), sep=',')
408 408
409 409
410 410 def set_indexer_config(config):
411 411 """
412 412 Update Whoosh index mapping
413 413
414 414 :param config: kallithea.CONFIG
415 415 """
416 416 from kallithea.config import conf
417 417
418 418 log.debug('adding extra into INDEX_EXTENSIONS')
419 419 conf.INDEX_EXTENSIONS.extend(re.split('\s+', config.get('index.extensions', '')))
420 420
421 421 log.debug('adding extra into INDEX_FILENAMES')
422 422 conf.INDEX_FILENAMES.extend(re.split('\s+', config.get('index.filenames', '')))
423 423
424 424
425 425 def map_groups(path):
426 426 """
427 427 Given a full path to a repository, create all nested groups that this
428 428 repo is inside. This function creates parent-child relationships between
429 429 groups and creates default perms for all new groups.
430 430
431 431 :param paths: full path to repository
432 432 """
433 433 sa = meta.Session()
434 434 groups = path.split(Repository.url_sep())
435 435 parent = None
436 436 group = None
437 437
438 438 # last element is repo in nested groups structure
439 439 groups = groups[:-1]
440 440 rgm = RepoGroupModel(sa)
441 441 owner = User.get_first_admin()
442 442 for lvl, group_name in enumerate(groups):
443 443 group_name = u'/'.join(groups[:lvl] + [group_name])
444 444 group = RepoGroup.get_by_group_name(group_name)
445 445 desc = '%s group' % group_name
446 446
447 447 # skip folders that are now removed repos
448 448 if REMOVED_REPO_PAT.match(group_name):
449 449 break
450 450
451 451 if group is None:
452 452 log.debug('creating group level: %s group_name: %s',
453 453 lvl, group_name)
454 454 group = RepoGroup(group_name, parent)
455 455 group.group_description = desc
456 456 group.user = owner
457 457 sa.add(group)
458 458 perm_obj = rgm._create_default_perms(group)
459 459 sa.add(perm_obj)
460 460 sa.flush()
461 461
462 462 parent = group
463 463 return group
464 464
465 465
466 466 def repo2db_mapper(initial_repo_list, remove_obsolete=False,
467 467 install_git_hooks=False, user=None, overwrite_git_hooks=False):
468 468 """
469 469 maps all repos given in initial_repo_list, non existing repositories
470 470 are created, if remove_obsolete is True it also check for db entries
471 471 that are not in initial_repo_list and removes them.
472 472
473 473 :param initial_repo_list: list of repositories found by scanning methods
474 474 :param remove_obsolete: check for obsolete entries in database
475 475 :param install_git_hooks: if this is True, also check and install git hook
476 476 for a repo if missing
477 477 :param overwrite_git_hooks: if this is True, overwrite any existing git hooks
478 478 that may be encountered (even if user-deployed)
479 479 """
480 480 from kallithea.model.repo import RepoModel
481 481 from kallithea.model.scm import ScmModel
482 482 sa = meta.Session()
483 483 repo_model = RepoModel()
484 484 if user is None:
485 485 user = User.get_first_admin()
486 486 added = []
487 487
488 488 ##creation defaults
489 489 defs = Setting.get_default_repo_settings(strip_prefix=True)
490 490 enable_statistics = defs.get('repo_enable_statistics')
491 491 enable_locking = defs.get('repo_enable_locking')
492 492 enable_downloads = defs.get('repo_enable_downloads')
493 493 private = defs.get('repo_private')
494 494
495 495 for name, repo in initial_repo_list.items():
496 496 group = map_groups(name)
497 497 unicode_name = safe_unicode(name)
498 498 db_repo = repo_model.get_by_repo_name(unicode_name)
499 499 # found repo that is on filesystem not in Kallithea database
500 500 if not db_repo:
501 501 log.info('repository %s not found, creating now', name)
502 502 added.append(name)
503 503 desc = (repo.description
504 504 if repo.description != 'unknown'
505 505 else '%s repository' % name)
506 506
507 507 new_repo = repo_model._create_repo(
508 508 repo_name=name,
509 509 repo_type=repo.alias,
510 510 description=desc,
511 511 repo_group=getattr(group, 'group_id', None),
512 512 owner=user,
513 513 enable_locking=enable_locking,
514 514 enable_downloads=enable_downloads,
515 515 enable_statistics=enable_statistics,
516 516 private=private,
517 517 state=Repository.STATE_CREATED
518 518 )
519 519 sa.commit()
520 520 # we added that repo just now, and make sure it has githook
521 521 # installed, and updated server info
522 522 if new_repo.repo_type == 'git':
523 523 git_repo = new_repo.scm_instance
524 524 ScmModel().install_git_hooks(git_repo)
525 525 # update repository server-info
526 526 log.debug('Running update server info')
527 527 git_repo._update_server_info()
528 528 new_repo.update_changeset_cache()
529 529 elif install_git_hooks:
530 530 if db_repo.repo_type == 'git':
531 531 ScmModel().install_git_hooks(db_repo.scm_instance, force_create=overwrite_git_hooks)
532 532
533 533 removed = []
534 534 # remove from database those repositories that are not in the filesystem
535 535 unicode_initial_repo_list = set(safe_unicode(name) for name in initial_repo_list)
536 536 for repo in sa.query(Repository).all():
537 537 if repo.repo_name not in unicode_initial_repo_list:
538 538 if remove_obsolete:
539 539 log.debug("Removing non-existing repository found in db `%s`",
540 540 repo.repo_name)
541 541 try:
542 542 RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
543 543 sa.commit()
544 544 except Exception:
545 545 #don't hold further removals on error
546 546 log.error(traceback.format_exc())
547 547 sa.rollback()
548 548 removed.append(repo.repo_name)
549 549 return added, removed
550 550
551 551
552 552 def load_rcextensions(root_path):
553 553 import kallithea
554 554 from kallithea.config import conf
555 555
556 556 path = os.path.join(root_path, 'rcextensions', '__init__.py')
557 557 if os.path.isfile(path):
558 558 rcext = create_module('rc', path)
559 559 EXT = kallithea.EXTENSIONS = rcext
560 560 log.debug('Found rcextensions now loading %s...', rcext)
561 561
562 562 # Additional mappings that are not present in the pygments lexers
563 563 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
564 564
565 565 #OVERRIDE OUR EXTENSIONS FROM RC-EXTENSIONS (if present)
566 566
567 567 if getattr(EXT, 'INDEX_EXTENSIONS', []):
568 568 log.debug('settings custom INDEX_EXTENSIONS')
569 569 conf.INDEX_EXTENSIONS = getattr(EXT, 'INDEX_EXTENSIONS', [])
570 570
571 571 #ADDITIONAL MAPPINGS
572 572 log.debug('adding extra into INDEX_EXTENSIONS')
573 573 conf.INDEX_EXTENSIONS.extend(getattr(EXT, 'EXTRA_INDEX_EXTENSIONS', []))
574 574
575 575 # auto check if the module is not missing any data, set to default if is
576 576 # this will help autoupdate new feature of rcext module
577 577 #from kallithea.config import rcextensions
578 578 #for k in dir(rcextensions):
579 579 # if not k.startswith('_') and not hasattr(EXT, k):
580 580 # setattr(EXT, k, getattr(rcextensions, k))
581 581
582 582
583 583 def get_custom_lexer(extension):
584 584 """
585 585 returns a custom lexer if it's defined in rcextensions module, or None
586 586 if there's no custom lexer defined
587 587 """
588 588 import kallithea
589 589 from pygments import lexers
590 590 #check if we didn't define this extension as other lexer
591 591 if kallithea.EXTENSIONS and extension in kallithea.EXTENSIONS.EXTRA_LEXERS:
592 592 _lexer_name = kallithea.EXTENSIONS.EXTRA_LEXERS[extension]
593 593 return lexers.get_lexer_by_name(_lexer_name)
594 594
595 595
596 596 #==============================================================================
597 597 # TEST FUNCTIONS AND CREATORS
598 598 #==============================================================================
599 599 def create_test_index(repo_location, config, full_index):
600 600 """
601 601 Makes default test index
602 602
603 603 :param config: test config
604 604 :param full_index:
605 605 """
606 606
607 607 from kallithea.lib.indexers.daemon import WhooshIndexingDaemon
608 608 from kallithea.lib.pidlock import DaemonLock, LockHeld
609 609
610 610 repo_location = repo_location
611 611
612 612 index_location = os.path.join(config['app_conf']['index_dir'])
613 613 if not os.path.exists(index_location):
614 614 os.makedirs(index_location)
615 615
616 616 try:
617 617 l = DaemonLock(file_=os.path.join(dirname(index_location), 'make_index.lock'))
618 618 WhooshIndexingDaemon(index_location=index_location,
619 619 repo_location=repo_location) \
620 620 .run(full_index=full_index)
621 621 l.release()
622 622 except LockHeld:
623 623 pass
624 624
625 625
626 626 def create_test_env(repos_test_path, config):
627 627 """
628 628 Makes a fresh database and
629 629 install test repository into tmp dir
630 630 """
631 631 from kallithea.lib.db_manage import DbManage
632 632 from kallithea.tests import HG_REPO, GIT_REPO, TESTS_TMP_PATH
633 633
634 634 # PART ONE create db
635 635 dbconf = config['sqlalchemy.url']
636 636 log.debug('making test db %s', dbconf)
637 637
638 638 # create test dir if it doesn't exist
639 639 if not os.path.isdir(repos_test_path):
640 640 log.debug('Creating testdir %s', repos_test_path)
641 641 os.makedirs(repos_test_path)
642 642
643 643 dbmanage = DbManage(log_sql=True, dbconf=dbconf, root=config['here'],
644 644 tests=True)
645 645 dbmanage.create_tables(override=True)
646 646 # for tests dynamically set new root paths based on generated content
647 647 dbmanage.create_settings(dbmanage.config_prompt(repos_test_path))
648 648 dbmanage.create_default_user()
649 649 dbmanage.admin_prompt()
650 650 dbmanage.create_permissions()
651 651 dbmanage.populate_default_permissions()
652 652 Session().commit()
653 653 # PART TWO make test repo
654 654 log.debug('making test vcs repositories')
655 655
656 656 idx_path = config['app_conf']['index_dir']
657 657 data_path = config['app_conf']['cache_dir']
658 658
659 659 #clean index and data
660 660 if idx_path and os.path.exists(idx_path):
661 661 log.debug('remove %s', idx_path)
662 662 shutil.rmtree(idx_path)
663 663
664 664 if data_path and os.path.exists(data_path):
665 665 log.debug('remove %s', data_path)
666 666 shutil.rmtree(data_path)
667 667
668 668 #CREATE DEFAULT TEST REPOS
669 669 cur_dir = dirname(dirname(abspath(__file__)))
670 670 tar = tarfile.open(os.path.join(cur_dir, 'tests', 'fixtures', "vcs_test_hg.tar.gz"))
671 671 tar.extractall(os.path.join(TESTS_TMP_PATH, HG_REPO))
672 672 tar.close()
673 673
674 674 cur_dir = dirname(dirname(abspath(__file__)))
675 675 tar = tarfile.open(os.path.join(cur_dir, 'tests', 'fixtures', "vcs_test_git.tar.gz"))
676 676 tar.extractall(os.path.join(TESTS_TMP_PATH, GIT_REPO))
677 677 tar.close()
678 678
679 679 #LOAD VCS test stuff
680 680 from kallithea.tests.vcs import setup_package
681 681 setup_package()
682 682
683 683
684 684 def check_git_version():
685 685 """
686 686 Checks what version of git is installed in system, and issues a warning
687 687 if it's too old for Kallithea to work properly.
688 688 """
689 689 from kallithea import BACKENDS
690 690 from kallithea.lib.vcs.backends.git.repository import GitRepository
691 691 from kallithea.lib.vcs.conf import settings
692 692 from distutils.version import StrictVersion
693 693
694 694 if 'git' not in BACKENDS:
695 695 return None
696 696
697 697 stdout, stderr = GitRepository._run_git_command(['--version'], _bare=True,
698 698 _safe=True)
699 699
700 700 m = re.search("\d+.\d+.\d+", stdout)
701 701 if m:
702 702 ver = StrictVersion(m.group(0))
703 703 else:
704 704 ver = StrictVersion('0.0.0')
705 705
706 706 req_ver = StrictVersion('1.7.4')
707 707
708 708 log.debug('Git executable: "%s" version %s detected: %s',
709 709 settings.GIT_EXECUTABLE_PATH, ver, stdout)
710 710 if stderr:
711 711 log.warning('Error detecting git version: %r', stderr)
712 712 elif ver < req_ver:
713 713 log.warning('Kallithea detected git version %s, which is too old '
714 714 'for the system to function properly. '
715 715 'Please upgrade to version %s or later.' % (ver, req_ver))
716 716 return ver
717 717
718 718
719 719 @decorator.decorator
720 720 def jsonify(func, *args, **kwargs):
721 721 """Action decorator that formats output for JSON
722 722
723 723 Given a function that will return content, this decorator will turn
724 724 the result into JSON, with a content-type of 'application/json' and
725 725 output it.
726 726
727 727 """
728 728 from pylons.decorators.util import get_pylons
729 729 from kallithea.lib.compat import json
730 730 pylons = get_pylons(args)
731 731 pylons.response.headers['Content-Type'] = 'application/json; charset=utf-8'
732 732 data = func(*args, **kwargs)
733 733 if isinstance(data, (list, tuple)):
734 734 msg = "JSON responses with Array envelopes are susceptible to " \
735 735 "cross-site data leak attacks, see " \
736 736 "http://wiki.pylonshq.com/display/pylonsfaq/Warnings"
737 737 warnings.warn(msg, Warning, 2)
738 738 log.warning(msg)
739 739 log.debug("Returning JSON wrapped action output")
740 740 return json.dumps(data, encoding='utf-8')
741 741
742 742
743 743 #===============================================================================
744 744 # CACHE RELATED METHODS
745 745 #===============================================================================
746 746
747 747 # set cache regions for beaker so celery can utilise it
748 748 def setup_cache_regions(settings):
749 749 cache_settings = {'regions': None}
750 750 for key in settings.keys():
751 751 for prefix in ['beaker.cache.', 'cache.']:
752 752 if key.startswith(prefix):
753 753 name = key.split(prefix)[1].strip()
754 754 cache_settings[name] = settings[key].strip()
755 755 if cache_settings['regions']:
756 756 for region in cache_settings['regions'].split(','):
757 757 region = region.strip()
758 758 region_settings = {}
759 759 for key, value in cache_settings.items():
760 760 if key.startswith(region):
761 761 region_settings[key.split('.')[1]] = value
762 762 region_settings['expire'] = int(region_settings.get('expire',
763 763 60))
764 764 region_settings.setdefault('lock_dir',
765 765 cache_settings.get('lock_dir'))
766 766 region_settings.setdefault('data_dir',
767 767 cache_settings.get('data_dir'))
768 768
769 769 if 'type' not in region_settings:
770 770 region_settings['type'] = cache_settings.get('type',
771 771 'memory')
772 772 beaker.cache.cache_regions[region] = region_settings
773 773
774 774
775 775 def conditional_cache(region, prefix, condition, func):
776 776 """
777 777
778 778 Conditional caching function use like::
779 779 def _c(arg):
780 780 #heavy computation function
781 781 return data
782 782
783 783 # depending from condition the compute is wrapped in cache or not
784 784 compute = conditional_cache('short_term', 'cache_desc', condition=True, func=func)
785 785 return compute(arg)
786 786
787 787 :param region: name of cache region
788 788 :param prefix: cache region prefix
789 789 :param condition: condition for cache to be triggered, and return data cached
790 790 :param func: wrapped heavy function to compute
791 791
792 792 """
793 793 wrapped = func
794 794 if condition:
795 795 log.debug('conditional_cache: True, wrapping call of '
796 796 'func: %s into %s region cache' % (region, func))
797 797 wrapped = _cache_decorate((prefix,), None, None, region)(func)
798 798
799 799 return wrapped
@@ -1,238 +1,233 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14
15 15 """
16 Pylons application test package
17
18 This package assumes the Pylons environment is already loaded.
19
20 This module initializes the application via ``websetup`` (`paster
21 setup-app`) and provides the base testing objects.
16 Kallithea test package
22 17
23 18 Refer to docs/contributing.rst for details on running the test suite.
24 19 """
25 20 import os
26 21 import re
27 22 import time
28 23 import logging
29 24 import datetime
30 25 import hashlib
31 26 import tempfile
32 27
33 28 from tempfile import _RandomNameSequence
34 29
35 30 import pylons
36 31 import pylons.test
37 32 from pylons import config, url
38 33 from pylons.i18n.translation import _get_translator
39 34 from pylons.util import ContextObj
40 35
41 36 from routes.util import URLGenerator
42 37 from webtest import TestApp
43 38 import pytest
44 39
45 40 from kallithea.lib.compat import unittest
46 41 from kallithea import is_windows
47 42 from kallithea.model.db import Notification, User, UserNotification
48 43 from kallithea.model.meta import Session
49 44 from kallithea.lib.utils2 import safe_str
50 45
51 46
52 47 os.environ['TZ'] = 'UTC'
53 48 if not is_windows:
54 49 time.tzset()
55 50
56 51 log = logging.getLogger(__name__)
57 52
58 53 skipif = pytest.mark.skipif
59 54 parametrize = pytest.mark.parametrize
60 55
61 56 __all__ = [
62 57 'skipif', 'parametrize', 'environ', 'url', 'TestController',
63 58 'ldap_lib_installed', 'pam_lib_installed', 'invalidate_all_caches',
64 59 'TESTS_TMP_PATH', 'HG_REPO', 'GIT_REPO', 'NEW_HG_REPO', 'NEW_GIT_REPO',
65 60 'HG_FORK', 'GIT_FORK', 'TEST_USER_ADMIN_LOGIN', 'TEST_USER_ADMIN_PASS',
66 61 'TEST_USER_ADMIN_EMAIL', 'TEST_USER_REGULAR_LOGIN', 'TEST_USER_REGULAR_PASS',
67 62 'TEST_USER_REGULAR_EMAIL', 'TEST_USER_REGULAR2_LOGIN',
68 63 'TEST_USER_REGULAR2_PASS', 'TEST_USER_REGULAR2_EMAIL', 'TEST_HG_REPO',
69 64 'TEST_HG_REPO_CLONE', 'TEST_HG_REPO_PULL', 'TEST_GIT_REPO',
70 65 'TEST_GIT_REPO_CLONE', 'TEST_GIT_REPO_PULL', 'HG_REMOTE_REPO',
71 66 'GIT_REMOTE_REPO', 'SCM_TESTS',
72 67 ]
73 68
74 69 # Invoke websetup with the current config file
75 70 # SetupCommand('setup-app').run([config_file])
76 71
77 72 environ = {}
78 73
79 74 #SOME GLOBALS FOR TESTS
80 75
81 76 TESTS_TMP_PATH = os.path.join(tempfile.gettempdir(), 'rc_test_%s' % _RandomNameSequence().next())
82 77 TEST_USER_ADMIN_LOGIN = 'test_admin'
83 78 TEST_USER_ADMIN_PASS = 'test12'
84 79 TEST_USER_ADMIN_EMAIL = 'test_admin@example.com'
85 80
86 81 TEST_USER_REGULAR_LOGIN = 'test_regular'
87 82 TEST_USER_REGULAR_PASS = 'test12'
88 83 TEST_USER_REGULAR_EMAIL = 'test_regular@example.com'
89 84
90 85 TEST_USER_REGULAR2_LOGIN = 'test_regular2'
91 86 TEST_USER_REGULAR2_PASS = 'test12'
92 87 TEST_USER_REGULAR2_EMAIL = 'test_regular2@example.com'
93 88
94 89 HG_REPO = u'vcs_test_hg'
95 90 GIT_REPO = u'vcs_test_git'
96 91
97 92 NEW_HG_REPO = u'vcs_test_hg_new'
98 93 NEW_GIT_REPO = u'vcs_test_git_new'
99 94
100 95 HG_FORK = u'vcs_test_hg_fork'
101 96 GIT_FORK = u'vcs_test_git_fork'
102 97
103 98 ## VCS
104 99 SCM_TESTS = ['hg', 'git']
105 100 uniq_suffix = str(int(time.mktime(datetime.datetime.now().timetuple())))
106 101
107 102 GIT_REMOTE_REPO = 'git://github.com/codeinn/vcs.git'
108 103
109 104 TEST_GIT_REPO = os.path.join(TESTS_TMP_PATH, GIT_REPO)
110 105 TEST_GIT_REPO_CLONE = os.path.join(TESTS_TMP_PATH, 'vcsgitclone%s' % uniq_suffix)
111 106 TEST_GIT_REPO_PULL = os.path.join(TESTS_TMP_PATH, 'vcsgitpull%s' % uniq_suffix)
112 107
113 108
114 109 HG_REMOTE_REPO = 'http://bitbucket.org/marcinkuzminski/vcs'
115 110
116 111 TEST_HG_REPO = os.path.join(TESTS_TMP_PATH, HG_REPO)
117 112 TEST_HG_REPO_CLONE = os.path.join(TESTS_TMP_PATH, 'vcshgclone%s' % uniq_suffix)
118 113 TEST_HG_REPO_PULL = os.path.join(TESTS_TMP_PATH, 'vcshgpull%s' % uniq_suffix)
119 114
120 115 TEST_DIR = tempfile.gettempdir()
121 116 TEST_REPO_PREFIX = 'vcs-test'
122 117
123 118 # cached repos if any !
124 119 # comment out to get some other repos from bb or github
125 120 GIT_REMOTE_REPO = os.path.join(TESTS_TMP_PATH, GIT_REPO)
126 121 HG_REMOTE_REPO = os.path.join(TESTS_TMP_PATH, HG_REPO)
127 122
128 123 #skip ldap tests if LDAP lib is not installed
129 124 ldap_lib_installed = False
130 125 try:
131 126 import ldap
132 127 ldap.API_VERSION
133 128 ldap_lib_installed = True
134 129 except ImportError:
135 130 # means that python-ldap is not installed
136 131 pass
137 132
138 133 try:
139 134 import pam
140 135 pam.PAM_TEXT_INFO
141 136 pam_lib_installed = True
142 137 except ImportError:
143 138 pam_lib_installed = False
144 139
145 140 def invalidate_all_caches():
146 141 """Invalidate all beaker caches currently configured.
147 142 Useful when manipulating IP permissions in a test and changes need to take
148 143 effect immediately.
149 144 Note: Any use of this function is probably a workaround - it should be
150 145 replaced with a more specific cache invalidation in code or test."""
151 146 from beaker.cache import cache_managers
152 147 for cache in cache_managers.values():
153 148 cache.clear()
154 149
155 150 class NullHandler(logging.Handler):
156 151 def emit(self, record):
157 152 pass
158 153
159 154 class TestController(object):
160 155 """Pytest-style test controller"""
161 156
162 157 # Note: pytest base classes cannot have an __init__ method
163 158
164 159 @pytest.fixture(autouse=True)
165 160 def app_fixture(self):
166 161 self.wsgiapp = pylons.test.pylonsapp
167 162 self.init_stack(self.wsgiapp.config)
168 163 self.app = TestApp(self.wsgiapp)
169 164 self.maxDiff = None
170 165 self.index_location = config['app_conf']['index_dir']
171 166 return self.app
172 167
173 168 def init_stack(self, config=None):
174 169 if not config:
175 170 config = pylons.test.pylonsapp.config
176 171 url._push_object(URLGenerator(config['routes.map'], environ))
177 172 pylons.app_globals._push_object(config['pylons.app_globals'])
178 173 pylons.config._push_object(config)
179 174 pylons.tmpl_context._push_object(ContextObj())
180 175 # Initialize a translator for tests that utilize i18n
181 176 translator = _get_translator(pylons.config.get('lang'))
182 177 pylons.translator._push_object(translator)
183 178 h = NullHandler()
184 179 logging.getLogger("kallithea").addHandler(h)
185 180
186 181 def remove_all_notifications(self):
187 182 # query().delete() does not (by default) trigger cascades
188 183 # ( http://docs.sqlalchemy.org/en/rel_0_7/orm/collections.html#passive-deletes )
189 184 # so delete the UserNotification first to ensure referential integrity.
190 185 UserNotification.query().delete()
191 186
192 187 Notification.query().delete()
193 188 Session().commit()
194 189
195 190 def log_user(self, username=TEST_USER_ADMIN_LOGIN,
196 191 password=TEST_USER_ADMIN_PASS):
197 192 self._logged_username = username
198 193 response = self.app.post(url(controller='login', action='index'),
199 194 {'username': username,
200 195 'password': password})
201 196
202 197 if 'Invalid username or password' in response.body:
203 198 pytest.fail('could not login using %s %s' % (username, password))
204 199
205 200 assert response.status == '302 Found'
206 201 self.assert_authenticated_user(response, username)
207 202
208 203 response = response.follow()
209 204 return response.session['authuser']
210 205
211 206 def _get_logged_user(self):
212 207 return User.get_by_username(self._logged_username)
213 208
214 209 def assert_authenticated_user(self, response, expected_username):
215 210 cookie = response.session.get('authuser')
216 211 user = cookie and cookie.get('user_id')
217 212 user = user and User.get(user)
218 213 user = user and user.username
219 214 assert user == expected_username
220 215
221 216 def authentication_token(self):
222 217 return self.app.get(url('authentication_token')).body
223 218
224 219 def checkSessionFlash(self, response, msg=None, skip=0, _matcher=lambda msg, m: msg in m):
225 220 if 'flash' not in response.session:
226 221 pytest.fail(safe_str(u'msg `%s` not found - session has no flash:\n%s' % (msg, response)))
227 222 try:
228 223 level, m = response.session['flash'][-1 - skip]
229 224 if _matcher(msg, m):
230 225 return
231 226 except IndexError:
232 227 pass
233 228 pytest.fail(safe_str(u'msg `%s` not found in session flash (skipping %s): %s' %
234 229 (msg, skip,
235 230 ', '.join('`%s`' % m for level, m in response.session['flash']))))
236 231
237 232 def checkSessionFlashRegex(self, response, regex, skip=0):
238 233 self.checkSessionFlash(response, regex, skip=skip, _matcher=re.search)
General Comments 0
You need to be logged in to leave comments. Login now