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