##// END OF EJS Templates
core: use new style pyramid partial renderer where possible.
marcink -
r1897:01df07bd default
parent child Browse files
Show More
@@ -1,358 +1,357 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import time
22 22 import logging
23 23
24 24 from pyramid.httpexceptions import HTTPFound
25 25
26 26 from rhodecode.lib import helpers as h
27 from rhodecode.lib.utils import PartialRenderer
28 27 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time
29 28 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
30 29 from rhodecode.lib.ext_json import json
31 30 from rhodecode.model import repo
32 31 from rhodecode.model import repo_group
33 32 from rhodecode.model.db import User
34 33 from rhodecode.model.scm import ScmModel
35 34
36 35 log = logging.getLogger(__name__)
37 36
38 37
39 38 ADMIN_PREFIX = '/_admin'
40 39 STATIC_FILE_PREFIX = '/_static'
41 40
42 41
43 42 def add_route_with_slash(config,name, pattern, **kw):
44 43 config.add_route(name, pattern, **kw)
45 44 if not pattern.endswith('/'):
46 45 config.add_route(name + '_slash', pattern + '/', **kw)
47 46
48 47
49 48 def get_format_ref_id(repo):
50 49 """Returns a `repo` specific reference formatter function"""
51 50 if h.is_svn(repo):
52 51 return _format_ref_id_svn
53 52 else:
54 53 return _format_ref_id
55 54
56 55
57 56 def _format_ref_id(name, raw_id):
58 57 """Default formatting of a given reference `name`"""
59 58 return name
60 59
61 60
62 61 def _format_ref_id_svn(name, raw_id):
63 62 """Special way of formatting a reference for Subversion including path"""
64 63 return '%s@%s' % (name, raw_id)
65 64
66 65
67 66 class TemplateArgs(StrictAttributeDict):
68 67 pass
69 68
70 69
71 70 class BaseAppView(object):
72 71
73 72 def __init__(self, context, request):
74 73 self.request = request
75 74 self.context = context
76 75 self.session = request.session
77 76 self._rhodecode_user = request.user # auth user
78 77 self._rhodecode_db_user = self._rhodecode_user.get_instance()
79 78 self._maybe_needs_password_change(
80 79 request.matched_route.name, self._rhodecode_db_user)
81 80
82 81 def _maybe_needs_password_change(self, view_name, user_obj):
83 82 log.debug('Checking if user %s needs password change on view %s',
84 83 user_obj, view_name)
85 84 skip_user_views = [
86 85 'logout', 'login',
87 86 'my_account_password', 'my_account_password_update'
88 87 ]
89 88
90 89 if not user_obj:
91 90 return
92 91
93 92 if user_obj.username == User.DEFAULT_USER:
94 93 return
95 94
96 95 now = time.time()
97 96 should_change = user_obj.user_data.get('force_password_change')
98 97 change_after = safe_int(should_change) or 0
99 98 if should_change and now > change_after:
100 99 log.debug('User %s requires password change', user_obj)
101 100 h.flash('You are required to change your password', 'warning',
102 101 ignore_duplicate=True)
103 102
104 103 if view_name not in skip_user_views:
105 104 raise HTTPFound(
106 105 self.request.route_path('my_account_password'))
107 106
108 107 def _get_local_tmpl_context(self, include_app_defaults=False):
109 108 c = TemplateArgs()
110 109 c.auth_user = self.request.user
111 110 if include_app_defaults:
112 111 # NOTE(marcink): after full pyramid migration include_app_defaults
113 112 # should be turned on by default
114 113 from rhodecode.lib.base import attach_context_attributes
115 114 attach_context_attributes(c, self.request, self.request.user.user_id)
116 115 return c
117 116
118 117 def _register_global_c(self, tmpl_args):
119 118 """
120 119 Registers attributes to pylons global `c`
121 120 """
122 121 # TODO(marcink): remove once pyramid migration is finished
123 122 from pylons import tmpl_context as c
124 123 for k, v in tmpl_args.items():
125 124 setattr(c, k, v)
126 125
127 126 def _get_template_context(self, tmpl_args):
128 127 self._register_global_c(tmpl_args)
129 128
130 129 local_tmpl_args = {
131 130 'defaults': {},
132 131 'errors': {},
133 132 }
134 133 local_tmpl_args.update(tmpl_args)
135 134 return local_tmpl_args
136 135
137 136 def load_default_context(self):
138 137 """
139 138 example:
140 139
141 140 def load_default_context(self):
142 141 c = self._get_local_tmpl_context()
143 142 c.custom_var = 'foobar'
144 143 self._register_global_c(c)
145 144 return c
146 145 """
147 146 raise NotImplementedError('Needs implementation in view class')
148 147
149 148
150 149 class RepoAppView(BaseAppView):
151 150
152 151 def __init__(self, context, request):
153 152 super(RepoAppView, self).__init__(context, request)
154 153 self.db_repo = request.db_repo
155 154 self.db_repo_name = self.db_repo.repo_name
156 155 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
157 156
158 157 def _handle_missing_requirements(self, error):
159 158 log.error(
160 159 'Requirements are missing for repository %s: %s',
161 160 self.db_repo_name, error.message)
162 161
163 162 def _get_local_tmpl_context(self, include_app_defaults=False):
164 163 c = super(RepoAppView, self)._get_local_tmpl_context(
165 164 include_app_defaults=include_app_defaults)
166 165
167 166 # register common vars for this type of view
168 167 c.rhodecode_db_repo = self.db_repo
169 168 c.repo_name = self.db_repo_name
170 169 c.repository_pull_requests = self.db_repo_pull_requests
171 170
172 171 c.repository_requirements_missing = False
173 172 try:
174 173 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
175 174 except RepositoryRequirementError as e:
176 175 c.repository_requirements_missing = True
177 176 self._handle_missing_requirements(e)
178 177
179 178 return c
180 179
181 180
182 181 class DataGridAppView(object):
183 182 """
184 183 Common class to have re-usable grid rendering components
185 184 """
186 185
187 186 def _extract_ordering(self, request, column_map=None):
188 187 column_map = column_map or {}
189 188 column_index = safe_int(request.GET.get('order[0][column]'))
190 189 order_dir = request.GET.get(
191 190 'order[0][dir]', 'desc')
192 191 order_by = request.GET.get(
193 192 'columns[%s][data][sort]' % column_index, 'name_raw')
194 193
195 194 # translate datatable to DB columns
196 195 order_by = column_map.get(order_by) or order_by
197 196
198 197 search_q = request.GET.get('search[value]')
199 198 return search_q, order_by, order_dir
200 199
201 200 def _extract_chunk(self, request):
202 201 start = safe_int(request.GET.get('start'), 0)
203 202 length = safe_int(request.GET.get('length'), 25)
204 203 draw = safe_int(request.GET.get('draw'))
205 204 return draw, start, length
206 205
207 206
208 207 class BaseReferencesView(RepoAppView):
209 208 """
210 209 Base for reference view for branches, tags and bookmarks.
211 210 """
212 211 def load_default_context(self):
213 212 c = self._get_local_tmpl_context()
214 213
215 214 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
216 215 c.repo_info = self.db_repo
217 216
218 217 self._register_global_c(c)
219 218 return c
220 219
221 220 def load_refs_context(self, ref_items, partials_template):
222 _render = PartialRenderer(partials_template)
223 221 _data = []
222 _render = self.request.get_partial_renderer(partials_template)
224 223 pre_load = ["author", "date", "message"]
225 224
226 225 is_svn = h.is_svn(self.rhodecode_vcs_repo)
227 226 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
228 227
229 228 for ref_name, commit_id in ref_items:
230 229 commit = self.rhodecode_vcs_repo.get_commit(
231 230 commit_id=commit_id, pre_load=pre_load)
232 231
233 232 # TODO: johbo: Unify generation of reference links
234 233 use_commit_id = '/' in ref_name or is_svn
235 234 files_url = h.url(
236 235 'files_home',
237 236 repo_name=c.repo_name,
238 237 f_path=ref_name if is_svn else '',
239 238 revision=commit_id if use_commit_id else ref_name,
240 239 at=ref_name)
241 240
242 241 _data.append({
243 242 "name": _render('name', ref_name, files_url),
244 243 "name_raw": ref_name,
245 244 "date": _render('date', commit.date),
246 245 "date_raw": datetime_to_time(commit.date),
247 246 "author": _render('author', commit.author),
248 247 "commit": _render(
249 248 'commit', commit.message, commit.raw_id, commit.idx),
250 249 "commit_raw": commit.idx,
251 250 "compare": _render(
252 251 'compare', format_ref_id(ref_name, commit.raw_id)),
253 252 })
254 253 c.has_references = bool(_data)
255 254 c.data = json.dumps(_data)
256 255
257 256
258 257 class RepoRoutePredicate(object):
259 258 def __init__(self, val, config):
260 259 self.val = val
261 260
262 261 def text(self):
263 262 return 'repo_route = %s' % self.val
264 263
265 264 phash = text
266 265
267 266 def __call__(self, info, request):
268 267
269 268 if hasattr(request, 'vcs_call'):
270 269 # skip vcs calls
271 270 return
272 271
273 272 repo_name = info['match']['repo_name']
274 273 repo_model = repo.RepoModel()
275 274 by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
276 275
277 276 if by_name_match:
278 277 # register this as request object we can re-use later
279 278 request.db_repo = by_name_match
280 279 return True
281 280
282 281 by_id_match = repo_model.get_repo_by_id(repo_name)
283 282 if by_id_match:
284 283 request.db_repo = by_id_match
285 284 return True
286 285
287 286 return False
288 287
289 288
290 289 class RepoTypeRoutePredicate(object):
291 290 def __init__(self, val, config):
292 291 self.val = val or ['hg', 'git', 'svn']
293 292
294 293 def text(self):
295 294 return 'repo_accepted_type = %s' % self.val
296 295
297 296 phash = text
298 297
299 298 def __call__(self, info, request):
300 299 if hasattr(request, 'vcs_call'):
301 300 # skip vcs calls
302 301 return
303 302
304 303 rhodecode_db_repo = request.db_repo
305 304
306 305 log.debug(
307 306 '%s checking repo type for %s in %s',
308 307 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
309 308
310 309 if rhodecode_db_repo.repo_type in self.val:
311 310 return True
312 311 else:
313 312 log.warning('Current view is not supported for repo type:%s',
314 313 rhodecode_db_repo.repo_type)
315 314 #
316 315 # h.flash(h.literal(
317 316 # _('Action not supported for %s.' % rhodecode_repo.alias)),
318 317 # category='warning')
319 318 # return redirect(
320 319 # route_path('repo_summary', repo_name=cls.rhodecode_db_repo.repo_name))
321 320
322 321 return False
323 322
324 323
325 324 class RepoGroupRoutePredicate(object):
326 325 def __init__(self, val, config):
327 326 self.val = val
328 327
329 328 def text(self):
330 329 return 'repo_group_route = %s' % self.val
331 330
332 331 phash = text
333 332
334 333 def __call__(self, info, request):
335 334 if hasattr(request, 'vcs_call'):
336 335 # skip vcs calls
337 336 return
338 337
339 338 repo_group_name = info['match']['repo_group_name']
340 339 repo_group_model = repo_group.RepoGroupModel()
341 340 by_name_match = repo_group_model.get_by_group_name(
342 341 repo_group_name, cache=True)
343 342
344 343 if by_name_match:
345 344 # register this as request object we can re-use later
346 345 request.db_repo_group = by_name_match
347 346 return True
348 347
349 348 return False
350 349
351 350
352 351 def includeme(config):
353 352 config.add_route_predicate(
354 353 'repo_route', RepoRoutePredicate)
355 354 config.add_route_predicate(
356 355 'repo_accepted_types', RepoTypeRoutePredicate)
357 356 config.add_route_predicate(
358 357 'repo_group_route', RepoGroupRoutePredicate)
@@ -1,505 +1,505 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23 import formencode
24 24
25 25 from pyramid.httpexceptions import HTTPFound
26 26 from pyramid.view import view_config
27 27 from sqlalchemy.sql.functions import coalesce
28 28
29 29 from rhodecode.apps._base import BaseAppView, DataGridAppView
30 30
31 31 from rhodecode.lib import audit_logger
32 32 from rhodecode.lib.ext_json import json
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
35 35 from rhodecode.lib import helpers as h
36 from rhodecode.lib.utils import PartialRenderer
37 36 from rhodecode.lib.utils2 import safe_int, safe_unicode
38 37 from rhodecode.model.auth_token import AuthTokenModel
39 38 from rhodecode.model.user import UserModel
40 39 from rhodecode.model.user_group import UserGroupModel
41 40 from rhodecode.model.db import User, or_, UserIpMap, UserEmailMap, UserApiKeys
42 41 from rhodecode.model.meta import Session
43 42
44 43 log = logging.getLogger(__name__)
45 44
46 45
47 46 class AdminUsersView(BaseAppView, DataGridAppView):
48 47 ALLOW_SCOPED_TOKENS = False
49 48 """
50 49 This view has alternative version inside EE, if modified please take a look
51 50 in there as well.
52 51 """
53 52
54 53 def load_default_context(self):
55 54 c = self._get_local_tmpl_context()
56 55 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
57 56 self._register_global_c(c)
58 57 return c
59 58
60 59 def _redirect_for_default_user(self, username):
61 60 _ = self.request.translate
62 61 if username == User.DEFAULT_USER:
63 62 h.flash(_("You can't edit this user"), category='warning')
64 63 # TODO(marcink): redirect to 'users' admin panel once this
65 64 # is a pyramid view
66 65 raise HTTPFound('/')
67 66
68 67 @HasPermissionAllDecorator('hg.admin')
69 68 @view_config(
70 69 route_name='users', request_method='GET',
71 70 renderer='rhodecode:templates/admin/users/users.mako')
72 71 def users_list(self):
73 72 c = self.load_default_context()
74 73 return self._get_template_context(c)
75 74
76 75 @HasPermissionAllDecorator('hg.admin')
77 76 @view_config(
78 77 # renderer defined below
79 78 route_name='users_data', request_method='GET',
80 79 renderer='json_ext', xhr=True)
81 80 def users_list_data(self):
82 81 draw, start, limit = self._extract_chunk(self.request)
83 82 search_q, order_by, order_dir = self._extract_ordering(self.request)
84 83
85 _render = PartialRenderer('data_table/_dt_elements.mako')
84 _render = self.request.get_partial_renderer(
85 'data_table/_dt_elements.mako')
86 86
87 87 def user_actions(user_id, username):
88 88 return _render("user_actions", user_id, username)
89 89
90 90 users_data_total_count = User.query()\
91 91 .filter(User.username != User.DEFAULT_USER) \
92 92 .count()
93 93
94 94 # json generate
95 95 base_q = User.query().filter(User.username != User.DEFAULT_USER)
96 96
97 97 if search_q:
98 98 like_expression = u'%{}%'.format(safe_unicode(search_q))
99 99 base_q = base_q.filter(or_(
100 100 User.username.ilike(like_expression),
101 101 User._email.ilike(like_expression),
102 102 User.name.ilike(like_expression),
103 103 User.lastname.ilike(like_expression),
104 104 ))
105 105
106 106 users_data_total_filtered_count = base_q.count()
107 107
108 108 sort_col = getattr(User, order_by, None)
109 109 if sort_col:
110 110 if order_dir == 'asc':
111 111 # handle null values properly to order by NULL last
112 112 if order_by in ['last_activity']:
113 113 sort_col = coalesce(sort_col, datetime.date.max)
114 114 sort_col = sort_col.asc()
115 115 else:
116 116 # handle null values properly to order by NULL last
117 117 if order_by in ['last_activity']:
118 118 sort_col = coalesce(sort_col, datetime.date.min)
119 119 sort_col = sort_col.desc()
120 120
121 121 base_q = base_q.order_by(sort_col)
122 122 base_q = base_q.offset(start).limit(limit)
123 123
124 124 users_list = base_q.all()
125 125
126 126 users_data = []
127 127 for user in users_list:
128 128 users_data.append({
129 129 "username": h.gravatar_with_user(user.username),
130 130 "email": user.email,
131 131 "first_name": user.first_name,
132 132 "last_name": user.last_name,
133 133 "last_login": h.format_date(user.last_login),
134 134 "last_activity": h.format_date(user.last_activity),
135 135 "active": h.bool2icon(user.active),
136 136 "active_raw": user.active,
137 137 "admin": h.bool2icon(user.admin),
138 138 "extern_type": user.extern_type,
139 139 "extern_name": user.extern_name,
140 140 "action": user_actions(user.user_id, user.username),
141 141 })
142 142
143 143 data = ({
144 144 'draw': draw,
145 145 'data': users_data,
146 146 'recordsTotal': users_data_total_count,
147 147 'recordsFiltered': users_data_total_filtered_count,
148 148 })
149 149
150 150 return data
151 151
152 152 @LoginRequired()
153 153 @HasPermissionAllDecorator('hg.admin')
154 154 @view_config(
155 155 route_name='edit_user_auth_tokens', request_method='GET',
156 156 renderer='rhodecode:templates/admin/users/user_edit.mako')
157 157 def auth_tokens(self):
158 158 _ = self.request.translate
159 159 c = self.load_default_context()
160 160
161 161 user_id = self.request.matchdict.get('user_id')
162 162 c.user = User.get_or_404(user_id, pyramid_exc=True)
163 163 self._redirect_for_default_user(c.user.username)
164 164
165 165 c.active = 'auth_tokens'
166 166
167 167 c.lifetime_values = [
168 168 (str(-1), _('forever')),
169 169 (str(5), _('5 minutes')),
170 170 (str(60), _('1 hour')),
171 171 (str(60 * 24), _('1 day')),
172 172 (str(60 * 24 * 30), _('1 month')),
173 173 ]
174 174 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
175 175 c.role_values = [
176 176 (x, AuthTokenModel.cls._get_role_name(x))
177 177 for x in AuthTokenModel.cls.ROLES]
178 178 c.role_options = [(c.role_values, _("Role"))]
179 179 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
180 180 c.user.user_id, show_expired=True)
181 181 return self._get_template_context(c)
182 182
183 183 def maybe_attach_token_scope(self, token):
184 184 # implemented in EE edition
185 185 pass
186 186
187 187 @LoginRequired()
188 188 @HasPermissionAllDecorator('hg.admin')
189 189 @CSRFRequired()
190 190 @view_config(
191 191 route_name='edit_user_auth_tokens_add', request_method='POST')
192 192 def auth_tokens_add(self):
193 193 _ = self.request.translate
194 194 c = self.load_default_context()
195 195
196 196 user_id = self.request.matchdict.get('user_id')
197 197 c.user = User.get_or_404(user_id, pyramid_exc=True)
198 198
199 199 self._redirect_for_default_user(c.user.username)
200 200
201 201 user_data = c.user.get_api_data()
202 202 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
203 203 description = self.request.POST.get('description')
204 204 role = self.request.POST.get('role')
205 205
206 206 token = AuthTokenModel().create(
207 207 c.user.user_id, description, lifetime, role)
208 208 token_data = token.get_api_data()
209 209
210 210 self.maybe_attach_token_scope(token)
211 211 audit_logger.store_web(
212 212 'user.edit.token.add', action_data={
213 213 'data': {'token': token_data, 'user': user_data}},
214 214 user=self._rhodecode_user, )
215 215 Session().commit()
216 216
217 217 h.flash(_("Auth token successfully created"), category='success')
218 218 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
219 219
220 220 @LoginRequired()
221 221 @HasPermissionAllDecorator('hg.admin')
222 222 @CSRFRequired()
223 223 @view_config(
224 224 route_name='edit_user_auth_tokens_delete', request_method='POST')
225 225 def auth_tokens_delete(self):
226 226 _ = self.request.translate
227 227 c = self.load_default_context()
228 228
229 229 user_id = self.request.matchdict.get('user_id')
230 230 c.user = User.get_or_404(user_id, pyramid_exc=True)
231 231 self._redirect_for_default_user(c.user.username)
232 232 user_data = c.user.get_api_data()
233 233
234 234 del_auth_token = self.request.POST.get('del_auth_token')
235 235
236 236 if del_auth_token:
237 237 token = UserApiKeys.get_or_404(del_auth_token, pyramid_exc=True)
238 238 token_data = token.get_api_data()
239 239
240 240 AuthTokenModel().delete(del_auth_token, c.user.user_id)
241 241 audit_logger.store_web(
242 242 'user.edit.token.delete', action_data={
243 243 'data': {'token': token_data, 'user': user_data}},
244 244 user=self._rhodecode_user,)
245 245 Session().commit()
246 246 h.flash(_("Auth token successfully deleted"), category='success')
247 247
248 248 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
249 249
250 250 @LoginRequired()
251 251 @HasPermissionAllDecorator('hg.admin')
252 252 @view_config(
253 253 route_name='edit_user_emails', request_method='GET',
254 254 renderer='rhodecode:templates/admin/users/user_edit.mako')
255 255 def emails(self):
256 256 _ = self.request.translate
257 257 c = self.load_default_context()
258 258
259 259 user_id = self.request.matchdict.get('user_id')
260 260 c.user = User.get_or_404(user_id, pyramid_exc=True)
261 261 self._redirect_for_default_user(c.user.username)
262 262
263 263 c.active = 'emails'
264 264 c.user_email_map = UserEmailMap.query() \
265 265 .filter(UserEmailMap.user == c.user).all()
266 266
267 267 return self._get_template_context(c)
268 268
269 269 @LoginRequired()
270 270 @HasPermissionAllDecorator('hg.admin')
271 271 @CSRFRequired()
272 272 @view_config(
273 273 route_name='edit_user_emails_add', request_method='POST')
274 274 def emails_add(self):
275 275 _ = self.request.translate
276 276 c = self.load_default_context()
277 277
278 278 user_id = self.request.matchdict.get('user_id')
279 279 c.user = User.get_or_404(user_id, pyramid_exc=True)
280 280 self._redirect_for_default_user(c.user.username)
281 281
282 282 email = self.request.POST.get('new_email')
283 283 user_data = c.user.get_api_data()
284 284 try:
285 285 UserModel().add_extra_email(c.user.user_id, email)
286 286 audit_logger.store_web(
287 287 'user.edit.email.add', action_data={'email': email, 'user': user_data},
288 288 user=self._rhodecode_user)
289 289 Session().commit()
290 290 h.flash(_("Added new email address `%s` for user account") % email,
291 291 category='success')
292 292 except formencode.Invalid as error:
293 293 h.flash(h.escape(error.error_dict['email']), category='error')
294 294 except Exception:
295 295 log.exception("Exception during email saving")
296 296 h.flash(_('An error occurred during email saving'),
297 297 category='error')
298 298 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
299 299
300 300 @LoginRequired()
301 301 @HasPermissionAllDecorator('hg.admin')
302 302 @CSRFRequired()
303 303 @view_config(
304 304 route_name='edit_user_emails_delete', request_method='POST')
305 305 def emails_delete(self):
306 306 _ = self.request.translate
307 307 c = self.load_default_context()
308 308
309 309 user_id = self.request.matchdict.get('user_id')
310 310 c.user = User.get_or_404(user_id, pyramid_exc=True)
311 311 self._redirect_for_default_user(c.user.username)
312 312
313 313 email_id = self.request.POST.get('del_email_id')
314 314 user_model = UserModel()
315 315
316 316 email = UserEmailMap.query().get(email_id).email
317 317 user_data = c.user.get_api_data()
318 318 user_model.delete_extra_email(c.user.user_id, email_id)
319 319 audit_logger.store_web(
320 320 'user.edit.email.delete', action_data={'email': email, 'user': user_data},
321 321 user=self._rhodecode_user)
322 322 Session().commit()
323 323 h.flash(_("Removed email address from user account"),
324 324 category='success')
325 325 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
326 326
327 327 @LoginRequired()
328 328 @HasPermissionAllDecorator('hg.admin')
329 329 @view_config(
330 330 route_name='edit_user_ips', request_method='GET',
331 331 renderer='rhodecode:templates/admin/users/user_edit.mako')
332 332 def ips(self):
333 333 _ = self.request.translate
334 334 c = self.load_default_context()
335 335
336 336 user_id = self.request.matchdict.get('user_id')
337 337 c.user = User.get_or_404(user_id, pyramid_exc=True)
338 338 self._redirect_for_default_user(c.user.username)
339 339
340 340 c.active = 'ips'
341 341 c.user_ip_map = UserIpMap.query() \
342 342 .filter(UserIpMap.user == c.user).all()
343 343
344 344 c.inherit_default_ips = c.user.inherit_default_permissions
345 345 c.default_user_ip_map = UserIpMap.query() \
346 346 .filter(UserIpMap.user == User.get_default_user()).all()
347 347
348 348 return self._get_template_context(c)
349 349
350 350 @LoginRequired()
351 351 @HasPermissionAllDecorator('hg.admin')
352 352 @CSRFRequired()
353 353 @view_config(
354 354 route_name='edit_user_ips_add', request_method='POST')
355 355 def ips_add(self):
356 356 _ = self.request.translate
357 357 c = self.load_default_context()
358 358
359 359 user_id = self.request.matchdict.get('user_id')
360 360 c.user = User.get_or_404(user_id, pyramid_exc=True)
361 361 # NOTE(marcink): this view is allowed for default users, as we can
362 362 # edit their IP white list
363 363
364 364 user_model = UserModel()
365 365 desc = self.request.POST.get('description')
366 366 try:
367 367 ip_list = user_model.parse_ip_range(
368 368 self.request.POST.get('new_ip'))
369 369 except Exception as e:
370 370 ip_list = []
371 371 log.exception("Exception during ip saving")
372 372 h.flash(_('An error occurred during ip saving:%s' % (e,)),
373 373 category='error')
374 374 added = []
375 375 user_data = c.user.get_api_data()
376 376 for ip in ip_list:
377 377 try:
378 378 user_model.add_extra_ip(c.user.user_id, ip, desc)
379 379 audit_logger.store_web(
380 380 'user.edit.ip.add', action_data={'ip': ip, 'user': user_data},
381 381 user=self._rhodecode_user)
382 382 Session().commit()
383 383 added.append(ip)
384 384 except formencode.Invalid as error:
385 385 msg = error.error_dict['ip']
386 386 h.flash(msg, category='error')
387 387 except Exception:
388 388 log.exception("Exception during ip saving")
389 389 h.flash(_('An error occurred during ip saving'),
390 390 category='error')
391 391 if added:
392 392 h.flash(
393 393 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
394 394 category='success')
395 395 if 'default_user' in self.request.POST:
396 396 # case for editing global IP list we do it for 'DEFAULT' user
397 397 raise HTTPFound(h.route_path('admin_permissions_ips'))
398 398 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
399 399
400 400 @LoginRequired()
401 401 @HasPermissionAllDecorator('hg.admin')
402 402 @CSRFRequired()
403 403 @view_config(
404 404 route_name='edit_user_ips_delete', request_method='POST')
405 405 def ips_delete(self):
406 406 _ = self.request.translate
407 407 c = self.load_default_context()
408 408
409 409 user_id = self.request.matchdict.get('user_id')
410 410 c.user = User.get_or_404(user_id, pyramid_exc=True)
411 411 # NOTE(marcink): this view is allowed for default users, as we can
412 412 # edit their IP white list
413 413
414 414 ip_id = self.request.POST.get('del_ip_id')
415 415 user_model = UserModel()
416 416 user_data = c.user.get_api_data()
417 417 ip = UserIpMap.query().get(ip_id).ip_addr
418 418 user_model.delete_extra_ip(c.user.user_id, ip_id)
419 419 audit_logger.store_web(
420 420 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
421 421 user=self._rhodecode_user)
422 422 Session().commit()
423 423 h.flash(_("Removed ip address from user whitelist"), category='success')
424 424
425 425 if 'default_user' in self.request.POST:
426 426 # case for editing global IP list we do it for 'DEFAULT' user
427 427 raise HTTPFound(h.route_path('admin_permissions_ips'))
428 428 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
429 429
430 430 @LoginRequired()
431 431 @HasPermissionAllDecorator('hg.admin')
432 432 @view_config(
433 433 route_name='edit_user_groups_management', request_method='GET',
434 434 renderer='rhodecode:templates/admin/users/user_edit.mako')
435 435 def groups_management(self):
436 436 c = self.load_default_context()
437 437
438 438 user_id = self.request.matchdict.get('user_id')
439 439 c.user = User.get_or_404(user_id, pyramid_exc=True)
440 440 c.data = c.user.group_member
441 441 self._redirect_for_default_user(c.user.username)
442 442 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
443 443 for group in c.user.group_member]
444 444 c.groups = json.dumps(groups)
445 445 c.active = 'groups'
446 446
447 447 return self._get_template_context(c)
448 448
449 449 @LoginRequired()
450 450 @HasPermissionAllDecorator('hg.admin')
451 451 @CSRFRequired()
452 452 @view_config(
453 453 route_name='edit_user_groups_management_updates', request_method='POST')
454 454 def groups_management_updates(self):
455 455 _ = self.request.translate
456 456 c = self.load_default_context()
457 457
458 458 user_id = self.request.matchdict.get('user_id')
459 459 c.user = User.get_or_404(user_id, pyramid_exc=True)
460 460 self._redirect_for_default_user(c.user.username)
461 461
462 462 users_groups = set(self.request.POST.getall('users_group_id'))
463 463 users_groups_model = []
464 464
465 465 for ugid in users_groups:
466 466 users_groups_model.append(UserGroupModel().get_group(safe_int(ugid)))
467 467 user_group_model = UserGroupModel()
468 468 user_group_model.change_groups(c.user, users_groups_model)
469 469
470 470 Session().commit()
471 471 c.active = 'user_groups_management'
472 472 h.flash(_("Groups successfully changed"), category='success')
473 473
474 474 return HTTPFound(h.route_path(
475 475 'edit_user_groups_management', user_id=user_id))
476 476
477 477 @LoginRequired()
478 478 @HasPermissionAllDecorator('hg.admin')
479 479 @view_config(
480 480 route_name='edit_user_audit_logs', request_method='GET',
481 481 renderer='rhodecode:templates/admin/users/user_edit.mako')
482 482 def user_audit_logs(self):
483 483 _ = self.request.translate
484 484 c = self.load_default_context()
485 485
486 486 user_id = self.request.matchdict.get('user_id')
487 487 c.user = User.get_or_404(user_id, pyramid_exc=True)
488 488 self._redirect_for_default_user(c.user.username)
489 489 c.active = 'audit'
490 490
491 491 p = safe_int(self.request.GET.get('page', 1), 1)
492 492
493 493 filter_term = self.request.GET.get('filter')
494 494 user_log = UserModel().get_user_log(c.user, filter_term)
495 495
496 496 def url_generator(**kw):
497 497 if filter_term:
498 498 kw['filter'] = filter_term
499 499 return self.request.current_route_path(_query=kw)
500 500
501 501 c.audit_logs = h.Page(
502 502 user_log, page=p, items_per_page=10, url=url_generator)
503 503 c.filter_term = filter_term
504 504 return self._get_template_context(c)
505 505
@@ -1,412 +1,412 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2013-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import time
22 22 import logging
23 23
24 24 import formencode
25 25 import peppercorn
26 26
27 27 from pyramid.httpexceptions import HTTPNotFound, HTTPForbidden, HTTPFound
28 28 from pyramid.view import view_config
29 29 from pyramid.renderers import render
30 30 from pyramid.response import Response
31 31
32 32 from rhodecode.apps._base import BaseAppView
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
35 35 from rhodecode.lib.utils2 import time_to_datetime
36 36 from rhodecode.lib.ext_json import json
37 37 from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError
38 38 from rhodecode.model.gist import GistModel
39 39 from rhodecode.model.meta import Session
40 40 from rhodecode.model.db import Gist, User, or_
41 41 from rhodecode.model import validation_schema
42 42 from rhodecode.model.validation_schema.schemas import gist_schema
43 43
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 class GistView(BaseAppView):
49 49
50 50 def load_default_context(self):
51 51 _ = self.request.translate
52 52 c = self._get_local_tmpl_context()
53 53 c.user = c.auth_user.get_instance()
54 54
55 55 c.lifetime_values = [
56 56 (-1, _('forever')),
57 57 (5, _('5 minutes')),
58 58 (60, _('1 hour')),
59 59 (60 * 24, _('1 day')),
60 60 (60 * 24 * 30, _('1 month')),
61 61 ]
62 62
63 63 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
64 64 c.acl_options = [
65 65 (Gist.ACL_LEVEL_PRIVATE, _("Requires registered account")),
66 66 (Gist.ACL_LEVEL_PUBLIC, _("Can be accessed by anonymous users"))
67 67 ]
68 68
69 69 self._register_global_c(c)
70 70 return c
71 71
72 72 @LoginRequired()
73 73 @view_config(
74 74 route_name='gists_show', request_method='GET',
75 75 renderer='rhodecode:templates/admin/gists/index.mako')
76 76 def gist_show_all(self):
77 77 c = self.load_default_context()
78 78
79 79 not_default_user = self._rhodecode_user.username != User.DEFAULT_USER
80 80 c.show_private = self.request.GET.get('private') and not_default_user
81 81 c.show_public = self.request.GET.get('public') and not_default_user
82 82 c.show_all = self.request.GET.get('all') and self._rhodecode_user.admin
83 83
84 84 gists = _gists = Gist().query()\
85 85 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
86 86 .order_by(Gist.created_on.desc())
87 87
88 88 c.active = 'public'
89 89 # MY private
90 90 if c.show_private and not c.show_public:
91 91 gists = _gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
92 92 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
93 93 c.active = 'my_private'
94 94 # MY public
95 95 elif c.show_public and not c.show_private:
96 96 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\
97 97 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
98 98 c.active = 'my_public'
99 99 # MY public+private
100 100 elif c.show_private and c.show_public:
101 101 gists = _gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
102 102 Gist.gist_type == Gist.GIST_PRIVATE))\
103 103 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
104 104 c.active = 'my_all'
105 105 # Show all by super-admin
106 106 elif c.show_all:
107 107 c.active = 'all'
108 108 gists = _gists
109 109
110 110 # default show ALL public gists
111 111 if not c.show_public and not c.show_private and not c.show_all:
112 112 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
113 113 c.active = 'public'
114 114
115 from rhodecode.lib.utils import PartialRenderer
116 _render = PartialRenderer('data_table/_dt_elements.mako')
115 _render = self.request.get_partial_renderer(
116 'data_table/_dt_elements.mako')
117 117
118 118 data = []
119 119
120 120 for gist in gists:
121 121 data.append({
122 122 'created_on': _render('gist_created', gist.created_on),
123 123 'created_on_raw': gist.created_on,
124 124 'type': _render('gist_type', gist.gist_type),
125 125 'access_id': _render('gist_access_id', gist.gist_access_id, gist.owner.full_contact),
126 126 'author': _render('gist_author', gist.owner.full_contact, gist.created_on, gist.gist_expires),
127 127 'author_raw': h.escape(gist.owner.full_contact),
128 128 'expires': _render('gist_expires', gist.gist_expires),
129 129 'description': _render('gist_description', gist.gist_description)
130 130 })
131 131 c.data = json.dumps(data)
132 132
133 133 return self._get_template_context(c)
134 134
135 135 @LoginRequired()
136 136 @NotAnonymous()
137 137 @view_config(
138 138 route_name='gists_new', request_method='GET',
139 139 renderer='rhodecode:templates/admin/gists/new.mako')
140 140 def gist_new(self):
141 141 c = self.load_default_context()
142 142 return self._get_template_context(c)
143 143
144 144 @LoginRequired()
145 145 @NotAnonymous()
146 146 @CSRFRequired()
147 147 @view_config(
148 148 route_name='gists_create', request_method='POST',
149 149 renderer='rhodecode:templates/admin/gists/new.mako')
150 150 def gist_create(self):
151 151 _ = self.request.translate
152 152 c = self.load_default_context()
153 153
154 154 data = dict(self.request.POST)
155 155 data['filename'] = data.get('filename') or Gist.DEFAULT_FILENAME
156 156 data['nodes'] = [{
157 157 'filename': data['filename'],
158 158 'content': data.get('content'),
159 159 'mimetype': data.get('mimetype') # None is autodetect
160 160 }]
161 161
162 162 data['gist_type'] = (
163 163 Gist.GIST_PUBLIC if data.get('public') else Gist.GIST_PRIVATE)
164 164 data['gist_acl_level'] = (
165 165 data.get('gist_acl_level') or Gist.ACL_LEVEL_PRIVATE)
166 166
167 167 schema = gist_schema.GistSchema().bind(
168 168 lifetime_options=[x[0] for x in c.lifetime_values])
169 169
170 170 try:
171 171
172 172 schema_data = schema.deserialize(data)
173 173 # convert to safer format with just KEYs so we sure no duplicates
174 174 schema_data['nodes'] = gist_schema.sequence_to_nodes(
175 175 schema_data['nodes'])
176 176
177 177 gist = GistModel().create(
178 178 gist_id=schema_data['gistid'], # custom access id not real ID
179 179 description=schema_data['description'],
180 180 owner=self._rhodecode_user.user_id,
181 181 gist_mapping=schema_data['nodes'],
182 182 gist_type=schema_data['gist_type'],
183 183 lifetime=schema_data['lifetime'],
184 184 gist_acl_level=schema_data['gist_acl_level']
185 185 )
186 186 Session().commit()
187 187 new_gist_id = gist.gist_access_id
188 188 except validation_schema.Invalid as errors:
189 189 defaults = data
190 190 errors = errors.asdict()
191 191
192 192 if 'nodes.0.content' in errors:
193 193 errors['content'] = errors['nodes.0.content']
194 194 del errors['nodes.0.content']
195 195 if 'nodes.0.filename' in errors:
196 196 errors['filename'] = errors['nodes.0.filename']
197 197 del errors['nodes.0.filename']
198 198
199 199 data = render('rhodecode:templates/admin/gists/new.mako',
200 200 self._get_template_context(c), self.request)
201 201 html = formencode.htmlfill.render(
202 202 data,
203 203 defaults=defaults,
204 204 errors=errors,
205 205 prefix_error=False,
206 206 encoding="UTF-8",
207 207 force_defaults=False
208 208 )
209 209 return Response(html)
210 210
211 211 except Exception:
212 212 log.exception("Exception while trying to create a gist")
213 213 h.flash(_('Error occurred during gist creation'), category='error')
214 214 raise HTTPFound(h.route_url('gists_new'))
215 215 raise HTTPFound(h.route_url('gist_show', gist_id=new_gist_id))
216 216
217 217 @LoginRequired()
218 218 @NotAnonymous()
219 219 @CSRFRequired()
220 220 @view_config(
221 221 route_name='gist_delete', request_method='POST')
222 222 def gist_delete(self):
223 223 _ = self.request.translate
224 224 gist_id = self.request.matchdict['gist_id']
225 225
226 226 c = self.load_default_context()
227 227 c.gist = Gist.get_or_404(gist_id)
228 228
229 229 owner = c.gist.gist_owner == self._rhodecode_user.user_id
230 230 if not (h.HasPermissionAny('hg.admin')() or owner):
231 231 log.warning('Deletion of Gist was forbidden '
232 232 'by unauthorized user: `%s`', self._rhodecode_user)
233 233 raise HTTPNotFound()
234 234
235 235 GistModel().delete(c.gist)
236 236 Session().commit()
237 237 h.flash(_('Deleted gist %s') % c.gist.gist_access_id, category='success')
238 238
239 239 raise HTTPFound(h.route_url('gists_show'))
240 240
241 241 def _get_gist(self, gist_id):
242 242
243 243 gist = Gist.get_or_404(gist_id)
244 244
245 245 # Check if this gist is expired
246 246 if gist.gist_expires != -1:
247 247 if time.time() > gist.gist_expires:
248 248 log.error(
249 249 'Gist expired at %s', time_to_datetime(gist.gist_expires))
250 250 raise HTTPNotFound()
251 251
252 252 # check if this gist requires a login
253 253 is_default_user = self._rhodecode_user.username == User.DEFAULT_USER
254 254 if gist.acl_level == Gist.ACL_LEVEL_PRIVATE and is_default_user:
255 255 log.error("Anonymous user %s tried to access protected gist `%s`",
256 256 self._rhodecode_user, gist_id)
257 257 raise HTTPNotFound()
258 258 return gist
259 259
260 260 @LoginRequired()
261 261 @view_config(
262 262 route_name='gist_show', request_method='GET',
263 263 renderer='rhodecode:templates/admin/gists/show.mako')
264 264 @view_config(
265 265 route_name='gist_show_rev', request_method='GET',
266 266 renderer='rhodecode:templates/admin/gists/show.mako')
267 267 @view_config(
268 268 route_name='gist_show_formatted', request_method='GET',
269 269 renderer=None)
270 270 @view_config(
271 271 route_name='gist_show_formatted_path', request_method='GET',
272 272 renderer=None)
273 273 def show(self):
274 274 gist_id = self.request.matchdict['gist_id']
275 275
276 276 # TODO(marcink): expose those via matching dict
277 277 revision = self.request.matchdict.get('revision', 'tip')
278 278 f_path = self.request.matchdict.get('f_path', None)
279 279 return_format = self.request.matchdict.get('format')
280 280
281 281 c = self.load_default_context()
282 282 c.gist = self._get_gist(gist_id)
283 283 c.render = not self.request.GET.get('no-render', False)
284 284
285 285 try:
286 286 c.file_last_commit, c.files = GistModel().get_gist_files(
287 287 gist_id, revision=revision)
288 288 except VCSError:
289 289 log.exception("Exception in gist show")
290 290 raise HTTPNotFound()
291 291
292 292 if return_format == 'raw':
293 293 content = '\n\n'.join([f.content for f in c.files
294 294 if (f_path is None or f.path == f_path)])
295 295 response = Response(content)
296 296 response.content_type = 'text/plain'
297 297 return response
298 298
299 299 return self._get_template_context(c)
300 300
301 301 @LoginRequired()
302 302 @NotAnonymous()
303 303 @view_config(
304 304 route_name='gist_edit', request_method='GET',
305 305 renderer='rhodecode:templates/admin/gists/edit.mako')
306 306 def gist_edit(self):
307 307 _ = self.request.translate
308 308 gist_id = self.request.matchdict['gist_id']
309 309 c = self.load_default_context()
310 310 c.gist = self._get_gist(gist_id)
311 311
312 312 owner = c.gist.gist_owner == self._rhodecode_user.user_id
313 313 if not (h.HasPermissionAny('hg.admin')() or owner):
314 314 raise HTTPNotFound()
315 315
316 316 try:
317 317 c.file_last_commit, c.files = GistModel().get_gist_files(gist_id)
318 318 except VCSError:
319 319 log.exception("Exception in gist edit")
320 320 raise HTTPNotFound()
321 321
322 322 if c.gist.gist_expires == -1:
323 323 expiry = _('never')
324 324 else:
325 325 # this cannot use timeago, since it's used in select2 as a value
326 326 expiry = h.age(h.time_to_datetime(c.gist.gist_expires))
327 327
328 328 c.lifetime_values.append(
329 329 (0, _('%(expiry)s - current value') % {'expiry': _(expiry)})
330 330 )
331 331
332 332 return self._get_template_context(c)
333 333
334 334 @LoginRequired()
335 335 @NotAnonymous()
336 336 @CSRFRequired()
337 337 @view_config(
338 338 route_name='gist_update', request_method='POST',
339 339 renderer='rhodecode:templates/admin/gists/edit.mako')
340 340 def gist_update(self):
341 341 _ = self.request.translate
342 342 gist_id = self.request.matchdict['gist_id']
343 343 c = self.load_default_context()
344 344 c.gist = self._get_gist(gist_id)
345 345
346 346 owner = c.gist.gist_owner == self._rhodecode_user.user_id
347 347 if not (h.HasPermissionAny('hg.admin')() or owner):
348 348 raise HTTPNotFound()
349 349
350 350 data = peppercorn.parse(self.request.POST.items())
351 351
352 352 schema = gist_schema.GistSchema()
353 353 schema = schema.bind(
354 354 # '0' is special value to leave lifetime untouched
355 355 lifetime_options=[x[0] for x in c.lifetime_values] + [0],
356 356 )
357 357
358 358 try:
359 359 schema_data = schema.deserialize(data)
360 360 # convert to safer format with just KEYs so we sure no duplicates
361 361 schema_data['nodes'] = gist_schema.sequence_to_nodes(
362 362 schema_data['nodes'])
363 363
364 364 GistModel().update(
365 365 gist=c.gist,
366 366 description=schema_data['description'],
367 367 owner=c.gist.owner,
368 368 gist_mapping=schema_data['nodes'],
369 369 lifetime=schema_data['lifetime'],
370 370 gist_acl_level=schema_data['gist_acl_level']
371 371 )
372 372
373 373 Session().commit()
374 374 h.flash(_('Successfully updated gist content'), category='success')
375 375 except NodeNotChangedError:
376 376 # raised if nothing was changed in repo itself. We anyway then
377 377 # store only DB stuff for gist
378 378 Session().commit()
379 379 h.flash(_('Successfully updated gist data'), category='success')
380 380 except validation_schema.Invalid as errors:
381 381 errors = errors.asdict()
382 382 h.flash(_('Error occurred during update of gist {}: {}').format(
383 383 gist_id, errors), category='error')
384 384 except Exception:
385 385 log.exception("Exception in gist edit")
386 386 h.flash(_('Error occurred during update of gist %s') % gist_id,
387 387 category='error')
388 388
389 389 raise HTTPFound(h.route_url('gist_show', gist_id=gist_id))
390 390
391 391 @LoginRequired()
392 392 @NotAnonymous()
393 393 @view_config(
394 394 route_name='gist_edit_check_revision', request_method='GET',
395 395 renderer='json_ext')
396 396 def gist_edit_check_revision(self):
397 397 _ = self.request.translate
398 398 gist_id = self.request.matchdict['gist_id']
399 399 c = self.load_default_context()
400 400 c.gist = self._get_gist(gist_id)
401 401
402 402 last_rev = c.gist.scm_instance().get_commit()
403 403 success = True
404 404 revision = self.request.GET.get('revision')
405 405
406 406 if revision != last_rev.raw_id:
407 407 log.error('Last revision %s is different then submitted %s'
408 408 % (revision, last_rev))
409 409 # our gist has newer version than we
410 410 success = False
411 411
412 412 return {'success': success}
@@ -1,584 +1,584 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23
24 24 import formencode
25 25 from pyramid.httpexceptions import HTTPFound
26 26 from pyramid.view import view_config
27 27 from pyramid.renderers import render
28 28 from pyramid.response import Response
29 29
30 30 from rhodecode.apps._base import BaseAppView, DataGridAppView
31 31 from rhodecode import forms
32 32 from rhodecode.lib import helpers as h
33 33 from rhodecode.lib import audit_logger
34 34 from rhodecode.lib.ext_json import json
35 35 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
36 36 from rhodecode.lib.channelstream import channelstream_request, \
37 37 ChannelstreamException
38 from rhodecode.lib.utils import PartialRenderer
39 38 from rhodecode.lib.utils2 import safe_int, md5, str2bool
40 39 from rhodecode.model.auth_token import AuthTokenModel
41 40 from rhodecode.model.comment import CommentsModel
42 41 from rhodecode.model.db import (
43 42 Repository, UserEmailMap, UserApiKeys, UserFollowing, joinedload,
44 43 PullRequest)
45 44 from rhodecode.model.forms import UserForm
46 45 from rhodecode.model.meta import Session
47 46 from rhodecode.model.pull_request import PullRequestModel
48 47 from rhodecode.model.scm import RepoList
49 48 from rhodecode.model.user import UserModel
50 49 from rhodecode.model.repo import RepoModel
51 50 from rhodecode.model.validation_schema.schemas import user_schema
52 51
53 52 log = logging.getLogger(__name__)
54 53
55 54
56 55 class MyAccountView(BaseAppView, DataGridAppView):
57 56 ALLOW_SCOPED_TOKENS = False
58 57 """
59 58 This view has alternative version inside EE, if modified please take a look
60 59 in there as well.
61 60 """
62 61
63 62 def load_default_context(self):
64 63 c = self._get_local_tmpl_context()
65 64 c.user = c.auth_user.get_instance()
66 65 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
67 66 self._register_global_c(c)
68 67 return c
69 68
70 69 @LoginRequired()
71 70 @NotAnonymous()
72 71 @view_config(
73 72 route_name='my_account_profile', request_method='GET',
74 73 renderer='rhodecode:templates/admin/my_account/my_account.mako')
75 74 def my_account_profile(self):
76 75 c = self.load_default_context()
77 76 c.active = 'profile'
78 77 return self._get_template_context(c)
79 78
80 79 @LoginRequired()
81 80 @NotAnonymous()
82 81 @view_config(
83 82 route_name='my_account_password', request_method='GET',
84 83 renderer='rhodecode:templates/admin/my_account/my_account.mako')
85 84 def my_account_password(self):
86 85 c = self.load_default_context()
87 86 c.active = 'password'
88 87 c.extern_type = c.user.extern_type
89 88
90 89 schema = user_schema.ChangePasswordSchema().bind(
91 90 username=c.user.username)
92 91
93 92 form = forms.Form(
94 93 schema, buttons=(forms.buttons.save, forms.buttons.reset))
95 94
96 95 c.form = form
97 96 return self._get_template_context(c)
98 97
99 98 @LoginRequired()
100 99 @NotAnonymous()
101 100 @CSRFRequired()
102 101 @view_config(
103 102 route_name='my_account_password', request_method='POST',
104 103 renderer='rhodecode:templates/admin/my_account/my_account.mako')
105 104 def my_account_password_update(self):
106 105 _ = self.request.translate
107 106 c = self.load_default_context()
108 107 c.active = 'password'
109 108 c.extern_type = c.user.extern_type
110 109
111 110 schema = user_schema.ChangePasswordSchema().bind(
112 111 username=c.user.username)
113 112
114 113 form = forms.Form(
115 114 schema, buttons=(forms.buttons.save, forms.buttons.reset))
116 115
117 116 if c.extern_type != 'rhodecode':
118 117 raise HTTPFound(self.request.route_path('my_account_password'))
119 118
120 119 controls = self.request.POST.items()
121 120 try:
122 121 valid_data = form.validate(controls)
123 122 UserModel().update_user(c.user.user_id, **valid_data)
124 123 c.user.update_userdata(force_password_change=False)
125 124 Session().commit()
126 125 except forms.ValidationFailure as e:
127 126 c.form = e
128 127 return self._get_template_context(c)
129 128
130 129 except Exception:
131 130 log.exception("Exception updating password")
132 131 h.flash(_('Error occurred during update of user password'),
133 132 category='error')
134 133 else:
135 134 instance = c.auth_user.get_instance()
136 135 self.session.setdefault('rhodecode_user', {}).update(
137 136 {'password': md5(instance.password)})
138 137 self.session.save()
139 138 h.flash(_("Successfully updated password"), category='success')
140 139
141 140 raise HTTPFound(self.request.route_path('my_account_password'))
142 141
143 142 @LoginRequired()
144 143 @NotAnonymous()
145 144 @view_config(
146 145 route_name='my_account_auth_tokens', request_method='GET',
147 146 renderer='rhodecode:templates/admin/my_account/my_account.mako')
148 147 def my_account_auth_tokens(self):
149 148 _ = self.request.translate
150 149
151 150 c = self.load_default_context()
152 151 c.active = 'auth_tokens'
153 152
154 153 c.lifetime_values = [
155 154 (str(-1), _('forever')),
156 155 (str(5), _('5 minutes')),
157 156 (str(60), _('1 hour')),
158 157 (str(60 * 24), _('1 day')),
159 158 (str(60 * 24 * 30), _('1 month')),
160 159 ]
161 160 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
162 161 c.role_values = [
163 162 (x, AuthTokenModel.cls._get_role_name(x))
164 163 for x in AuthTokenModel.cls.ROLES]
165 164 c.role_options = [(c.role_values, _("Role"))]
166 165 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
167 166 c.user.user_id, show_expired=True)
168 167 return self._get_template_context(c)
169 168
170 169 def maybe_attach_token_scope(self, token):
171 170 # implemented in EE edition
172 171 pass
173 172
174 173 @LoginRequired()
175 174 @NotAnonymous()
176 175 @CSRFRequired()
177 176 @view_config(
178 177 route_name='my_account_auth_tokens_add', request_method='POST',)
179 178 def my_account_auth_tokens_add(self):
180 179 _ = self.request.translate
181 180 c = self.load_default_context()
182 181
183 182 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
184 183 description = self.request.POST.get('description')
185 184 role = self.request.POST.get('role')
186 185
187 186 token = AuthTokenModel().create(
188 187 c.user.user_id, description, lifetime, role)
189 188 token_data = token.get_api_data()
190 189
191 190 self.maybe_attach_token_scope(token)
192 191 audit_logger.store_web(
193 192 'user.edit.token.add', action_data={
194 193 'data': {'token': token_data, 'user': 'self'}},
195 194 user=self._rhodecode_user, )
196 195 Session().commit()
197 196
198 197 h.flash(_("Auth token successfully created"), category='success')
199 198 return HTTPFound(h.route_path('my_account_auth_tokens'))
200 199
201 200 @LoginRequired()
202 201 @NotAnonymous()
203 202 @CSRFRequired()
204 203 @view_config(
205 204 route_name='my_account_auth_tokens_delete', request_method='POST')
206 205 def my_account_auth_tokens_delete(self):
207 206 _ = self.request.translate
208 207 c = self.load_default_context()
209 208
210 209 del_auth_token = self.request.POST.get('del_auth_token')
211 210
212 211 if del_auth_token:
213 212 token = UserApiKeys.get_or_404(del_auth_token, pyramid_exc=True)
214 213 token_data = token.get_api_data()
215 214
216 215 AuthTokenModel().delete(del_auth_token, c.user.user_id)
217 216 audit_logger.store_web(
218 217 'user.edit.token.delete', action_data={
219 218 'data': {'token': token_data, 'user': 'self'}},
220 219 user=self._rhodecode_user,)
221 220 Session().commit()
222 221 h.flash(_("Auth token successfully deleted"), category='success')
223 222
224 223 return HTTPFound(h.route_path('my_account_auth_tokens'))
225 224
226 225 @LoginRequired()
227 226 @NotAnonymous()
228 227 @view_config(
229 228 route_name='my_account_emails', request_method='GET',
230 229 renderer='rhodecode:templates/admin/my_account/my_account.mako')
231 230 def my_account_emails(self):
232 231 _ = self.request.translate
233 232
234 233 c = self.load_default_context()
235 234 c.active = 'emails'
236 235
237 236 c.user_email_map = UserEmailMap.query()\
238 237 .filter(UserEmailMap.user == c.user).all()
239 238 return self._get_template_context(c)
240 239
241 240 @LoginRequired()
242 241 @NotAnonymous()
243 242 @CSRFRequired()
244 243 @view_config(
245 244 route_name='my_account_emails_add', request_method='POST')
246 245 def my_account_emails_add(self):
247 246 _ = self.request.translate
248 247 c = self.load_default_context()
249 248
250 249 email = self.request.POST.get('new_email')
251 250
252 251 try:
253 252 UserModel().add_extra_email(c.user.user_id, email)
254 253 audit_logger.store_web(
255 254 'user.edit.email.add', action_data={
256 255 'data': {'email': email, 'user': 'self'}},
257 256 user=self._rhodecode_user,)
258 257
259 258 Session().commit()
260 259 h.flash(_("Added new email address `%s` for user account") % email,
261 260 category='success')
262 261 except formencode.Invalid as error:
263 262 h.flash(h.escape(error.error_dict['email']), category='error')
264 263 except Exception:
265 264 log.exception("Exception in my_account_emails")
266 265 h.flash(_('An error occurred during email saving'),
267 266 category='error')
268 267 return HTTPFound(h.route_path('my_account_emails'))
269 268
270 269 @LoginRequired()
271 270 @NotAnonymous()
272 271 @CSRFRequired()
273 272 @view_config(
274 273 route_name='my_account_emails_delete', request_method='POST')
275 274 def my_account_emails_delete(self):
276 275 _ = self.request.translate
277 276 c = self.load_default_context()
278 277
279 278 del_email_id = self.request.POST.get('del_email_id')
280 279 if del_email_id:
281 280 email = UserEmailMap.get_or_404(del_email_id, pyramid_exc=True).email
282 281 UserModel().delete_extra_email(c.user.user_id, del_email_id)
283 282 audit_logger.store_web(
284 283 'user.edit.email.delete', action_data={
285 284 'data': {'email': email, 'user': 'self'}},
286 285 user=self._rhodecode_user,)
287 286 Session().commit()
288 287 h.flash(_("Email successfully deleted"),
289 288 category='success')
290 289 return HTTPFound(h.route_path('my_account_emails'))
291 290
292 291 @LoginRequired()
293 292 @NotAnonymous()
294 293 @CSRFRequired()
295 294 @view_config(
296 295 route_name='my_account_notifications_test_channelstream',
297 296 request_method='POST', renderer='json_ext')
298 297 def my_account_notifications_test_channelstream(self):
299 298 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
300 299 self._rhodecode_user.username, datetime.datetime.now())
301 300 payload = {
302 301 # 'channel': 'broadcast',
303 302 'type': 'message',
304 303 'timestamp': datetime.datetime.utcnow(),
305 304 'user': 'system',
306 305 'pm_users': [self._rhodecode_user.username],
307 306 'message': {
308 307 'message': message,
309 308 'level': 'info',
310 309 'topic': '/notifications'
311 310 }
312 311 }
313 312
314 313 registry = self.request.registry
315 314 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
316 315 channelstream_config = rhodecode_plugins.get('channelstream', {})
317 316
318 317 try:
319 318 channelstream_request(channelstream_config, [payload], '/message')
320 319 except ChannelstreamException as e:
321 320 log.exception('Failed to send channelstream data')
322 321 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
323 322 return {"response": 'Channelstream data sent. '
324 323 'You should see a new live message now.'}
325 324
326 325 def _load_my_repos_data(self, watched=False):
327 326 if watched:
328 327 admin = False
329 328 follows_repos = Session().query(UserFollowing)\
330 329 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
331 330 .options(joinedload(UserFollowing.follows_repository))\
332 331 .all()
333 332 repo_list = [x.follows_repository for x in follows_repos]
334 333 else:
335 334 admin = True
336 335 repo_list = Repository.get_all_repos(
337 336 user_id=self._rhodecode_user.user_id)
338 337 repo_list = RepoList(repo_list, perm_set=[
339 338 'repository.read', 'repository.write', 'repository.admin'])
340 339
341 340 repos_data = RepoModel().get_repos_as_dict(
342 341 repo_list=repo_list, admin=admin)
343 342 # json used to render the grid
344 343 return json.dumps(repos_data)
345 344
346 345 @LoginRequired()
347 346 @NotAnonymous()
348 347 @view_config(
349 348 route_name='my_account_repos', request_method='GET',
350 349 renderer='rhodecode:templates/admin/my_account/my_account.mako')
351 350 def my_account_repos(self):
352 351 c = self.load_default_context()
353 352 c.active = 'repos'
354 353
355 354 # json used to render the grid
356 355 c.data = self._load_my_repos_data()
357 356 return self._get_template_context(c)
358 357
359 358 @LoginRequired()
360 359 @NotAnonymous()
361 360 @view_config(
362 361 route_name='my_account_watched', request_method='GET',
363 362 renderer='rhodecode:templates/admin/my_account/my_account.mako')
364 363 def my_account_watched(self):
365 364 c = self.load_default_context()
366 365 c.active = 'watched'
367 366
368 367 # json used to render the grid
369 368 c.data = self._load_my_repos_data(watched=True)
370 369 return self._get_template_context(c)
371 370
372 371 @LoginRequired()
373 372 @NotAnonymous()
374 373 @view_config(
375 374 route_name='my_account_perms', request_method='GET',
376 375 renderer='rhodecode:templates/admin/my_account/my_account.mako')
377 376 def my_account_perms(self):
378 377 c = self.load_default_context()
379 378 c.active = 'perms'
380 379
381 380 c.perm_user = c.auth_user
382 381 return self._get_template_context(c)
383 382
384 383 @LoginRequired()
385 384 @NotAnonymous()
386 385 @view_config(
387 386 route_name='my_account_notifications', request_method='GET',
388 387 renderer='rhodecode:templates/admin/my_account/my_account.mako')
389 388 def my_notifications(self):
390 389 c = self.load_default_context()
391 390 c.active = 'notifications'
392 391
393 392 return self._get_template_context(c)
394 393
395 394 @LoginRequired()
396 395 @NotAnonymous()
397 396 @CSRFRequired()
398 397 @view_config(
399 398 route_name='my_account_notifications_toggle_visibility',
400 399 request_method='POST', renderer='json_ext')
401 400 def my_notifications_toggle_visibility(self):
402 401 user = self._rhodecode_db_user
403 402 new_status = not user.user_data.get('notification_status', True)
404 403 user.update_userdata(notification_status=new_status)
405 404 Session().commit()
406 405 return user.user_data['notification_status']
407 406
408 407 @LoginRequired()
409 408 @NotAnonymous()
410 409 @view_config(
411 410 route_name='my_account_edit',
412 411 request_method='GET',
413 412 renderer='rhodecode:templates/admin/my_account/my_account.mako')
414 413 def my_account_edit(self):
415 414 c = self.load_default_context()
416 415 c.active = 'profile_edit'
417 416
418 417 c.perm_user = c.auth_user
419 418 c.extern_type = c.user.extern_type
420 419 c.extern_name = c.user.extern_name
421 420
422 421 defaults = c.user.get_dict()
423 422
424 423 data = render('rhodecode:templates/admin/my_account/my_account.mako',
425 424 self._get_template_context(c), self.request)
426 425 html = formencode.htmlfill.render(
427 426 data,
428 427 defaults=defaults,
429 428 encoding="UTF-8",
430 429 force_defaults=False
431 430 )
432 431 return Response(html)
433 432
434 433 @LoginRequired()
435 434 @NotAnonymous()
436 435 @CSRFRequired()
437 436 @view_config(
438 437 route_name='my_account_update',
439 438 request_method='POST',
440 439 renderer='rhodecode:templates/admin/my_account/my_account.mako')
441 440 def my_account_update(self):
442 441 _ = self.request.translate
443 442 c = self.load_default_context()
444 443 c.active = 'profile_edit'
445 444
446 445 c.perm_user = c.auth_user
447 446 c.extern_type = c.user.extern_type
448 447 c.extern_name = c.user.extern_name
449 448
450 449 _form = UserForm(edit=True,
451 450 old_data={'user_id': self._rhodecode_user.user_id,
452 451 'email': self._rhodecode_user.email})()
453 452 form_result = {}
454 453 try:
455 454 post_data = dict(self.request.POST)
456 455 post_data['new_password'] = ''
457 456 post_data['password_confirmation'] = ''
458 457 form_result = _form.to_python(post_data)
459 458 # skip updating those attrs for my account
460 459 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
461 460 'new_password', 'password_confirmation']
462 461 # TODO: plugin should define if username can be updated
463 462 if c.extern_type != "rhodecode":
464 463 # forbid updating username for external accounts
465 464 skip_attrs.append('username')
466 465
467 466 UserModel().update_user(
468 467 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
469 468 **form_result)
470 469 h.flash(_('Your account was updated successfully'),
471 470 category='success')
472 471 Session().commit()
473 472
474 473 except formencode.Invalid as errors:
475 474 data = render(
476 475 'rhodecode:templates/admin/my_account/my_account.mako',
477 476 self._get_template_context(c), self.request)
478 477
479 478 html = formencode.htmlfill.render(
480 479 data,
481 480 defaults=errors.value,
482 481 errors=errors.error_dict or {},
483 482 prefix_error=False,
484 483 encoding="UTF-8",
485 484 force_defaults=False)
486 485 return Response(html)
487 486
488 487 except Exception:
489 488 log.exception("Exception updating user")
490 489 h.flash(_('Error occurred during update of user %s')
491 490 % form_result.get('username'), category='error')
492 491 raise HTTPFound(h.route_path('my_account_profile'))
493 492
494 493 raise HTTPFound(h.route_path('my_account_profile'))
495 494
496 495 def _get_pull_requests_list(self, statuses):
497 496 draw, start, limit = self._extract_chunk(self.request)
498 497 search_q, order_by, order_dir = self._extract_ordering(self.request)
499 _render = PartialRenderer('data_table/_dt_elements.mako')
498 _render = self.request.get_partial_renderer(
499 'data_table/_dt_elements.mako')
500 500
501 501 pull_requests = PullRequestModel().get_im_participating_in(
502 502 user_id=self._rhodecode_user.user_id,
503 503 statuses=statuses,
504 504 offset=start, length=limit, order_by=order_by,
505 505 order_dir=order_dir)
506 506
507 507 pull_requests_total_count = PullRequestModel().count_im_participating_in(
508 508 user_id=self._rhodecode_user.user_id, statuses=statuses)
509 509
510 510 data = []
511 511 comments_model = CommentsModel()
512 512 for pr in pull_requests:
513 513 repo_id = pr.target_repo_id
514 514 comments = comments_model.get_all_comments(
515 515 repo_id, pull_request=pr)
516 516 owned = pr.user_id == self._rhodecode_user.user_id
517 517
518 518 data.append({
519 519 'target_repo': _render('pullrequest_target_repo',
520 520 pr.target_repo.repo_name),
521 521 'name': _render('pullrequest_name',
522 522 pr.pull_request_id, pr.target_repo.repo_name,
523 523 short=True),
524 524 'name_raw': pr.pull_request_id,
525 525 'status': _render('pullrequest_status',
526 526 pr.calculated_review_status()),
527 527 'title': _render(
528 528 'pullrequest_title', pr.title, pr.description),
529 529 'description': h.escape(pr.description),
530 530 'updated_on': _render('pullrequest_updated_on',
531 531 h.datetime_to_time(pr.updated_on)),
532 532 'updated_on_raw': h.datetime_to_time(pr.updated_on),
533 533 'created_on': _render('pullrequest_updated_on',
534 534 h.datetime_to_time(pr.created_on)),
535 535 'created_on_raw': h.datetime_to_time(pr.created_on),
536 536 'author': _render('pullrequest_author',
537 537 pr.author.full_contact, ),
538 538 'author_raw': pr.author.full_name,
539 539 'comments': _render('pullrequest_comments', len(comments)),
540 540 'comments_raw': len(comments),
541 541 'closed': pr.is_closed(),
542 542 'owned': owned
543 543 })
544 544
545 545 # json used to render the grid
546 546 data = ({
547 547 'draw': draw,
548 548 'data': data,
549 549 'recordsTotal': pull_requests_total_count,
550 550 'recordsFiltered': pull_requests_total_count,
551 551 })
552 552 return data
553 553
554 554 @LoginRequired()
555 555 @NotAnonymous()
556 556 @view_config(
557 557 route_name='my_account_pullrequests',
558 558 request_method='GET',
559 559 renderer='rhodecode:templates/admin/my_account/my_account.mako')
560 560 def my_account_pullrequests(self):
561 561 c = self.load_default_context()
562 562 c.active = 'pullrequests'
563 563 req_get = self.request.GET
564 564
565 565 c.closed = str2bool(req_get.get('pr_show_closed'))
566 566
567 567 return self._get_template_context(c)
568 568
569 569 @LoginRequired()
570 570 @NotAnonymous()
571 571 @view_config(
572 572 route_name='my_account_pullrequests_data',
573 573 request_method='GET', renderer='json_ext')
574 574 def my_account_pullrequests_data(self):
575 575 req_get = self.request.GET
576 576 closed = str2bool(req_get.get('closed'))
577 577
578 578 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
579 579 if closed:
580 580 statuses += [PullRequest.STATUS_CLOSED]
581 581
582 582 data = self._get_pull_requests_list(statuses=statuses)
583 583 return data
584 584
@@ -1,584 +1,584 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 import collections
24 24 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
25 25 from pyramid.view import view_config
26 26
27 27 from rhodecode.apps._base import RepoAppView, DataGridAppView
28 28 from rhodecode.lib import helpers as h, diffs, codeblocks
29 29 from rhodecode.lib.auth import (
30 30 LoginRequired, HasRepoPermissionAnyDecorator)
31 from rhodecode.lib.utils import PartialRenderer
32 31 from rhodecode.lib.utils2 import str2bool, safe_int, safe_str
33 32 from rhodecode.lib.vcs.backends.base import EmptyCommit
34 33 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError, \
35 34 RepositoryRequirementError, NodeDoesNotExistError
36 35 from rhodecode.model.comment import CommentsModel
37 36 from rhodecode.model.db import PullRequest, PullRequestVersion, \
38 37 ChangesetComment, ChangesetStatus
39 38 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
40 39
41 40 log = logging.getLogger(__name__)
42 41
43 42
44 43 class RepoPullRequestsView(RepoAppView, DataGridAppView):
45 44
46 45 def load_default_context(self):
47 46 c = self._get_local_tmpl_context(include_app_defaults=True)
48 47 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
49 48 c.repo_info = self.db_repo
50 49 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
51 50 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
52 51 self._register_global_c(c)
53 52 return c
54 53
55 54 def _get_pull_requests_list(
56 55 self, repo_name, source, filter_type, opened_by, statuses):
57 56
58 57 draw, start, limit = self._extract_chunk(self.request)
59 58 search_q, order_by, order_dir = self._extract_ordering(self.request)
60 _render = PartialRenderer('data_table/_dt_elements.mako')
59 _render = self.request.get_partial_renderer(
60 'data_table/_dt_elements.mako')
61 61
62 62 # pagination
63 63
64 64 if filter_type == 'awaiting_review':
65 65 pull_requests = PullRequestModel().get_awaiting_review(
66 66 repo_name, source=source, opened_by=opened_by,
67 67 statuses=statuses, offset=start, length=limit,
68 68 order_by=order_by, order_dir=order_dir)
69 69 pull_requests_total_count = PullRequestModel().count_awaiting_review(
70 70 repo_name, source=source, statuses=statuses,
71 71 opened_by=opened_by)
72 72 elif filter_type == 'awaiting_my_review':
73 73 pull_requests = PullRequestModel().get_awaiting_my_review(
74 74 repo_name, source=source, opened_by=opened_by,
75 75 user_id=self._rhodecode_user.user_id, statuses=statuses,
76 76 offset=start, length=limit, order_by=order_by,
77 77 order_dir=order_dir)
78 78 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
79 79 repo_name, source=source, user_id=self._rhodecode_user.user_id,
80 80 statuses=statuses, opened_by=opened_by)
81 81 else:
82 82 pull_requests = PullRequestModel().get_all(
83 83 repo_name, source=source, opened_by=opened_by,
84 84 statuses=statuses, offset=start, length=limit,
85 85 order_by=order_by, order_dir=order_dir)
86 86 pull_requests_total_count = PullRequestModel().count_all(
87 87 repo_name, source=source, statuses=statuses,
88 88 opened_by=opened_by)
89 89
90 90 data = []
91 91 comments_model = CommentsModel()
92 92 for pr in pull_requests:
93 93 comments = comments_model.get_all_comments(
94 94 self.db_repo.repo_id, pull_request=pr)
95 95
96 96 data.append({
97 97 'name': _render('pullrequest_name',
98 98 pr.pull_request_id, pr.target_repo.repo_name),
99 99 'name_raw': pr.pull_request_id,
100 100 'status': _render('pullrequest_status',
101 101 pr.calculated_review_status()),
102 102 'title': _render(
103 103 'pullrequest_title', pr.title, pr.description),
104 104 'description': h.escape(pr.description),
105 105 'updated_on': _render('pullrequest_updated_on',
106 106 h.datetime_to_time(pr.updated_on)),
107 107 'updated_on_raw': h.datetime_to_time(pr.updated_on),
108 108 'created_on': _render('pullrequest_updated_on',
109 109 h.datetime_to_time(pr.created_on)),
110 110 'created_on_raw': h.datetime_to_time(pr.created_on),
111 111 'author': _render('pullrequest_author',
112 112 pr.author.full_contact, ),
113 113 'author_raw': pr.author.full_name,
114 114 'comments': _render('pullrequest_comments', len(comments)),
115 115 'comments_raw': len(comments),
116 116 'closed': pr.is_closed(),
117 117 })
118 118
119 119 data = ({
120 120 'draw': draw,
121 121 'data': data,
122 122 'recordsTotal': pull_requests_total_count,
123 123 'recordsFiltered': pull_requests_total_count,
124 124 })
125 125 return data
126 126
127 127 @LoginRequired()
128 128 @HasRepoPermissionAnyDecorator(
129 129 'repository.read', 'repository.write', 'repository.admin')
130 130 @view_config(
131 131 route_name='pullrequest_show_all', request_method='GET',
132 132 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
133 133 def pull_request_list(self):
134 134 c = self.load_default_context()
135 135
136 136 req_get = self.request.GET
137 137 c.source = str2bool(req_get.get('source'))
138 138 c.closed = str2bool(req_get.get('closed'))
139 139 c.my = str2bool(req_get.get('my'))
140 140 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
141 141 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
142 142
143 143 c.active = 'open'
144 144 if c.my:
145 145 c.active = 'my'
146 146 if c.closed:
147 147 c.active = 'closed'
148 148 if c.awaiting_review and not c.source:
149 149 c.active = 'awaiting'
150 150 if c.source and not c.awaiting_review:
151 151 c.active = 'source'
152 152 if c.awaiting_my_review:
153 153 c.active = 'awaiting_my'
154 154
155 155 return self._get_template_context(c)
156 156
157 157 @LoginRequired()
158 158 @HasRepoPermissionAnyDecorator(
159 159 'repository.read', 'repository.write', 'repository.admin')
160 160 @view_config(
161 161 route_name='pullrequest_show_all_data', request_method='GET',
162 162 renderer='json_ext', xhr=True)
163 163 def pull_request_list_data(self):
164 164
165 165 # additional filters
166 166 req_get = self.request.GET
167 167 source = str2bool(req_get.get('source'))
168 168 closed = str2bool(req_get.get('closed'))
169 169 my = str2bool(req_get.get('my'))
170 170 awaiting_review = str2bool(req_get.get('awaiting_review'))
171 171 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
172 172
173 173 filter_type = 'awaiting_review' if awaiting_review \
174 174 else 'awaiting_my_review' if awaiting_my_review \
175 175 else None
176 176
177 177 opened_by = None
178 178 if my:
179 179 opened_by = [self._rhodecode_user.user_id]
180 180
181 181 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
182 182 if closed:
183 183 statuses = [PullRequest.STATUS_CLOSED]
184 184
185 185 data = self._get_pull_requests_list(
186 186 repo_name=self.db_repo_name, source=source,
187 187 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
188 188
189 189 return data
190 190
191 191 def _get_pr_version(self, pull_request_id, version=None):
192 192 pull_request_id = safe_int(pull_request_id)
193 193 at_version = None
194 194
195 195 if version and version == 'latest':
196 196 pull_request_ver = PullRequest.get(pull_request_id)
197 197 pull_request_obj = pull_request_ver
198 198 _org_pull_request_obj = pull_request_obj
199 199 at_version = 'latest'
200 200 elif version:
201 201 pull_request_ver = PullRequestVersion.get_or_404(version)
202 202 pull_request_obj = pull_request_ver
203 203 _org_pull_request_obj = pull_request_ver.pull_request
204 204 at_version = pull_request_ver.pull_request_version_id
205 205 else:
206 206 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
207 207 pull_request_id)
208 208
209 209 pull_request_display_obj = PullRequest.get_pr_display_object(
210 210 pull_request_obj, _org_pull_request_obj)
211 211
212 212 return _org_pull_request_obj, pull_request_obj, \
213 213 pull_request_display_obj, at_version
214 214
215 215 def _get_diffset(self, source_repo_name, source_repo,
216 216 source_ref_id, target_ref_id,
217 217 target_commit, source_commit, diff_limit, fulldiff,
218 218 file_limit, display_inline_comments):
219 219
220 220 vcs_diff = PullRequestModel().get_diff(
221 221 source_repo, source_ref_id, target_ref_id)
222 222
223 223 diff_processor = diffs.DiffProcessor(
224 224 vcs_diff, format='newdiff', diff_limit=diff_limit,
225 225 file_limit=file_limit, show_full_diff=fulldiff)
226 226
227 227 _parsed = diff_processor.prepare()
228 228
229 229 def _node_getter(commit):
230 230 def get_node(fname):
231 231 try:
232 232 return commit.get_node(fname)
233 233 except NodeDoesNotExistError:
234 234 return None
235 235
236 236 return get_node
237 237
238 238 diffset = codeblocks.DiffSet(
239 239 repo_name=self.db_repo_name,
240 240 source_repo_name=source_repo_name,
241 241 source_node_getter=_node_getter(target_commit),
242 242 target_node_getter=_node_getter(source_commit),
243 243 comments=display_inline_comments
244 244 )
245 245 diffset = diffset.render_patchset(
246 246 _parsed, target_commit.raw_id, source_commit.raw_id)
247 247
248 248 return diffset
249 249
250 250 @LoginRequired()
251 251 @HasRepoPermissionAnyDecorator(
252 252 'repository.read', 'repository.write', 'repository.admin')
253 253 # @view_config(
254 254 # route_name='pullrequest_show', request_method='GET',
255 255 # renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
256 256 def pull_request_show(self):
257 257 pull_request_id = safe_int(
258 258 self.request.matchdict.get('pull_request_id'))
259 259 c = self.load_default_context()
260 260
261 261 version = self.request.GET.get('version')
262 262 from_version = self.request.GET.get('from_version') or version
263 263 merge_checks = self.request.GET.get('merge_checks')
264 264 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
265 265
266 266 (pull_request_latest,
267 267 pull_request_at_ver,
268 268 pull_request_display_obj,
269 269 at_version) = self._get_pr_version(
270 270 pull_request_id, version=version)
271 271 pr_closed = pull_request_latest.is_closed()
272 272
273 273 if pr_closed and (version or from_version):
274 274 # not allow to browse versions
275 275 raise HTTPFound(h.route_path(
276 276 'pullrequest_show', repo_name=self.db_repo_name,
277 277 pull_request_id=pull_request_id))
278 278
279 279 versions = pull_request_display_obj.versions()
280 280
281 281 c.at_version = at_version
282 282 c.at_version_num = (at_version
283 283 if at_version and at_version != 'latest'
284 284 else None)
285 285 c.at_version_pos = ChangesetComment.get_index_from_version(
286 286 c.at_version_num, versions)
287 287
288 288 (prev_pull_request_latest,
289 289 prev_pull_request_at_ver,
290 290 prev_pull_request_display_obj,
291 291 prev_at_version) = self._get_pr_version(
292 292 pull_request_id, version=from_version)
293 293
294 294 c.from_version = prev_at_version
295 295 c.from_version_num = (prev_at_version
296 296 if prev_at_version and prev_at_version != 'latest'
297 297 else None)
298 298 c.from_version_pos = ChangesetComment.get_index_from_version(
299 299 c.from_version_num, versions)
300 300
301 301 # define if we're in COMPARE mode or VIEW at version mode
302 302 compare = at_version != prev_at_version
303 303
304 304 # pull_requests repo_name we opened it against
305 305 # ie. target_repo must match
306 306 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
307 307 raise HTTPNotFound()
308 308
309 309 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
310 310 pull_request_at_ver)
311 311
312 312 c.pull_request = pull_request_display_obj
313 313 c.pull_request_latest = pull_request_latest
314 314
315 315 if compare or (at_version and not at_version == 'latest'):
316 316 c.allowed_to_change_status = False
317 317 c.allowed_to_update = False
318 318 c.allowed_to_merge = False
319 319 c.allowed_to_delete = False
320 320 c.allowed_to_comment = False
321 321 c.allowed_to_close = False
322 322 else:
323 323 can_change_status = PullRequestModel().check_user_change_status(
324 324 pull_request_at_ver, self._rhodecode_user)
325 325 c.allowed_to_change_status = can_change_status and not pr_closed
326 326
327 327 c.allowed_to_update = PullRequestModel().check_user_update(
328 328 pull_request_latest, self._rhodecode_user) and not pr_closed
329 329 c.allowed_to_merge = PullRequestModel().check_user_merge(
330 330 pull_request_latest, self._rhodecode_user) and not pr_closed
331 331 c.allowed_to_delete = PullRequestModel().check_user_delete(
332 332 pull_request_latest, self._rhodecode_user) and not pr_closed
333 333 c.allowed_to_comment = not pr_closed
334 334 c.allowed_to_close = c.allowed_to_merge and not pr_closed
335 335
336 336 c.forbid_adding_reviewers = False
337 337 c.forbid_author_to_review = False
338 338 c.forbid_commit_author_to_review = False
339 339
340 340 if pull_request_latest.reviewer_data and \
341 341 'rules' in pull_request_latest.reviewer_data:
342 342 rules = pull_request_latest.reviewer_data['rules'] or {}
343 343 try:
344 344 c.forbid_adding_reviewers = rules.get(
345 345 'forbid_adding_reviewers')
346 346 c.forbid_author_to_review = rules.get(
347 347 'forbid_author_to_review')
348 348 c.forbid_commit_author_to_review = rules.get(
349 349 'forbid_commit_author_to_review')
350 350 except Exception:
351 351 pass
352 352
353 353 # check merge capabilities
354 354 _merge_check = MergeCheck.validate(
355 355 pull_request_latest, user=self._rhodecode_user)
356 356 c.pr_merge_errors = _merge_check.error_details
357 357 c.pr_merge_possible = not _merge_check.failed
358 358 c.pr_merge_message = _merge_check.merge_msg
359 359
360 360 c.pull_request_review_status = _merge_check.review_status
361 361 if merge_checks:
362 362 self.request.override_renderer = \
363 363 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
364 364 return self._get_template_context(c)
365 365
366 366 comments_model = CommentsModel()
367 367
368 368 # reviewers and statuses
369 369 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
370 370 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
371 371
372 372 # GENERAL COMMENTS with versions #
373 373 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
374 374 q = q.order_by(ChangesetComment.comment_id.asc())
375 375 general_comments = q
376 376
377 377 # pick comments we want to render at current version
378 378 c.comment_versions = comments_model.aggregate_comments(
379 379 general_comments, versions, c.at_version_num)
380 380 c.comments = c.comment_versions[c.at_version_num]['until']
381 381
382 382 # INLINE COMMENTS with versions #
383 383 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
384 384 q = q.order_by(ChangesetComment.comment_id.asc())
385 385 inline_comments = q
386 386
387 387 c.inline_versions = comments_model.aggregate_comments(
388 388 inline_comments, versions, c.at_version_num, inline=True)
389 389
390 390 # inject latest version
391 391 latest_ver = PullRequest.get_pr_display_object(
392 392 pull_request_latest, pull_request_latest)
393 393
394 394 c.versions = versions + [latest_ver]
395 395
396 396 # if we use version, then do not show later comments
397 397 # than current version
398 398 display_inline_comments = collections.defaultdict(
399 399 lambda: collections.defaultdict(list))
400 400 for co in inline_comments:
401 401 if c.at_version_num:
402 402 # pick comments that are at least UPTO given version, so we
403 403 # don't render comments for higher version
404 404 should_render = co.pull_request_version_id and \
405 405 co.pull_request_version_id <= c.at_version_num
406 406 else:
407 407 # showing all, for 'latest'
408 408 should_render = True
409 409
410 410 if should_render:
411 411 display_inline_comments[co.f_path][co.line_no].append(co)
412 412
413 413 # load diff data into template context, if we use compare mode then
414 414 # diff is calculated based on changes between versions of PR
415 415
416 416 source_repo = pull_request_at_ver.source_repo
417 417 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
418 418
419 419 target_repo = pull_request_at_ver.target_repo
420 420 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
421 421
422 422 if compare:
423 423 # in compare switch the diff base to latest commit from prev version
424 424 target_ref_id = prev_pull_request_display_obj.revisions[0]
425 425
426 426 # despite opening commits for bookmarks/branches/tags, we always
427 427 # convert this to rev to prevent changes after bookmark or branch change
428 428 c.source_ref_type = 'rev'
429 429 c.source_ref = source_ref_id
430 430
431 431 c.target_ref_type = 'rev'
432 432 c.target_ref = target_ref_id
433 433
434 434 c.source_repo = source_repo
435 435 c.target_repo = target_repo
436 436
437 437 c.commit_ranges = []
438 438 source_commit = EmptyCommit()
439 439 target_commit = EmptyCommit()
440 440 c.missing_requirements = False
441 441
442 442 source_scm = source_repo.scm_instance()
443 443 target_scm = target_repo.scm_instance()
444 444
445 445 # try first shadow repo, fallback to regular repo
446 446 try:
447 447 commits_source_repo = pull_request_latest.get_shadow_repo()
448 448 except Exception:
449 449 log.debug('Failed to get shadow repo', exc_info=True)
450 450 commits_source_repo = source_scm
451 451
452 452 c.commits_source_repo = commits_source_repo
453 453 commit_cache = {}
454 454 try:
455 455 pre_load = ["author", "branch", "date", "message"]
456 456 show_revs = pull_request_at_ver.revisions
457 457 for rev in show_revs:
458 458 comm = commits_source_repo.get_commit(
459 459 commit_id=rev, pre_load=pre_load)
460 460 c.commit_ranges.append(comm)
461 461 commit_cache[comm.raw_id] = comm
462 462
463 463 # Order here matters, we first need to get target, and then
464 464 # the source
465 465 target_commit = commits_source_repo.get_commit(
466 466 commit_id=safe_str(target_ref_id))
467 467
468 468 source_commit = commits_source_repo.get_commit(
469 469 commit_id=safe_str(source_ref_id))
470 470
471 471 except CommitDoesNotExistError:
472 472 log.warning(
473 473 'Failed to get commit from `{}` repo'.format(
474 474 commits_source_repo), exc_info=True)
475 475 except RepositoryRequirementError:
476 476 log.warning(
477 477 'Failed to get all required data from repo', exc_info=True)
478 478 c.missing_requirements = True
479 479
480 480 c.ancestor = None # set it to None, to hide it from PR view
481 481
482 482 try:
483 483 ancestor_id = source_scm.get_common_ancestor(
484 484 source_commit.raw_id, target_commit.raw_id, target_scm)
485 485 c.ancestor_commit = source_scm.get_commit(ancestor_id)
486 486 except Exception:
487 487 c.ancestor_commit = None
488 488
489 489 c.statuses = source_repo.statuses(
490 490 [x.raw_id for x in c.commit_ranges])
491 491
492 492 # auto collapse if we have more than limit
493 493 collapse_limit = diffs.DiffProcessor._collapse_commits_over
494 494 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
495 495 c.compare_mode = compare
496 496
497 497 # diff_limit is the old behavior, will cut off the whole diff
498 498 # if the limit is applied otherwise will just hide the
499 499 # big files from the front-end
500 500 diff_limit = c.visual.cut_off_limit_diff
501 501 file_limit = c.visual.cut_off_limit_file
502 502
503 503 c.missing_commits = False
504 504 if (c.missing_requirements
505 505 or isinstance(source_commit, EmptyCommit)
506 506 or source_commit == target_commit):
507 507
508 508 c.missing_commits = True
509 509 else:
510 510
511 511 c.diffset = self._get_diffset(
512 512 c.source_repo.repo_name, commits_source_repo,
513 513 source_ref_id, target_ref_id,
514 514 target_commit, source_commit,
515 515 diff_limit, c.fulldiff, file_limit, display_inline_comments)
516 516
517 517 c.limited_diff = c.diffset.limited_diff
518 518
519 519 # calculate removed files that are bound to comments
520 520 comment_deleted_files = [
521 521 fname for fname in display_inline_comments
522 522 if fname not in c.diffset.file_stats]
523 523
524 524 c.deleted_files_comments = collections.defaultdict(dict)
525 525 for fname, per_line_comments in display_inline_comments.items():
526 526 if fname in comment_deleted_files:
527 527 c.deleted_files_comments[fname]['stats'] = 0
528 528 c.deleted_files_comments[fname]['comments'] = list()
529 529 for lno, comments in per_line_comments.items():
530 530 c.deleted_files_comments[fname]['comments'].extend(
531 531 comments)
532 532
533 533 # this is a hack to properly display links, when creating PR, the
534 534 # compare view and others uses different notation, and
535 535 # compare_commits.mako renders links based on the target_repo.
536 536 # We need to swap that here to generate it properly on the html side
537 537 c.target_repo = c.source_repo
538 538
539 539 c.commit_statuses = ChangesetStatus.STATUSES
540 540
541 541 c.show_version_changes = not pr_closed
542 542 if c.show_version_changes:
543 543 cur_obj = pull_request_at_ver
544 544 prev_obj = prev_pull_request_at_ver
545 545
546 546 old_commit_ids = prev_obj.revisions
547 547 new_commit_ids = cur_obj.revisions
548 548 commit_changes = PullRequestModel()._calculate_commit_id_changes(
549 549 old_commit_ids, new_commit_ids)
550 550 c.commit_changes_summary = commit_changes
551 551
552 552 # calculate the diff for commits between versions
553 553 c.commit_changes = []
554 554 mark = lambda cs, fw: list(
555 555 h.itertools.izip_longest([], cs, fillvalue=fw))
556 556 for c_type, raw_id in mark(commit_changes.added, 'a') \
557 557 + mark(commit_changes.removed, 'r') \
558 558 + mark(commit_changes.common, 'c'):
559 559
560 560 if raw_id in commit_cache:
561 561 commit = commit_cache[raw_id]
562 562 else:
563 563 try:
564 564 commit = commits_source_repo.get_commit(raw_id)
565 565 except CommitDoesNotExistError:
566 566 # in case we fail extracting still use "dummy" commit
567 567 # for display in commit diff
568 568 commit = h.AttributeDict(
569 569 {'raw_id': raw_id,
570 570 'message': 'EMPTY or MISSING COMMIT'})
571 571 c.commit_changes.append([c_type, commit])
572 572
573 573 # current user review statuses for each version
574 574 c.review_versions = {}
575 575 if self._rhodecode_user.user_id in allowed_reviewers:
576 576 for co in general_comments:
577 577 if co.author.user_id == self._rhodecode_user.user_id:
578 578 # each comment has a status change
579 579 status = co.status_change
580 580 if status:
581 581 _ver_pr = status[0].comment.pull_request_version_id
582 582 c.review_versions[_ver_pr] = status[0]
583 583
584 584 return self._get_template_context(c)
@@ -1,513 +1,514 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 User Groups crud controller for pylons
23 23 """
24 24
25 25 import logging
26 26 import formencode
27 27
28 28 import peppercorn
29 29 from formencode import htmlfill
30 30 from pylons import request, tmpl_context as c, url, config
31 31 from pylons.controllers.util import redirect
32 32 from pylons.i18n.translation import _
33 33
34 34 from sqlalchemy.orm import joinedload
35 35
36 36 from rhodecode.lib import auth
37 37 from rhodecode.lib import helpers as h
38 38 from rhodecode.lib import audit_logger
39 39 from rhodecode.lib.ext_json import json
40 40 from rhodecode.lib.exceptions import UserGroupAssignedException,\
41 41 RepoGroupAssignmentError
42 42 from rhodecode.lib.utils import jsonify
43 43 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
44 44 from rhodecode.lib.auth import (
45 45 LoginRequired, NotAnonymous, HasUserGroupPermissionAnyDecorator,
46 46 HasPermissionAnyDecorator, XHRRequired)
47 47 from rhodecode.lib.base import BaseController, render
48 48 from rhodecode.model.permission import PermissionModel
49 49 from rhodecode.model.scm import UserGroupList
50 50 from rhodecode.model.user_group import UserGroupModel
51 51 from rhodecode.model.db import (
52 52 User, UserGroup, UserGroupRepoToPerm, UserGroupRepoGroupToPerm)
53 53 from rhodecode.model.forms import (
54 54 UserGroupForm, UserGroupPermsForm, UserIndividualPermissionsForm,
55 55 UserPermissionsForm)
56 56 from rhodecode.model.meta import Session
57 57
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61
62 62 class UserGroupsController(BaseController):
63 63 """REST Controller styled on the Atom Publishing Protocol"""
64 64
65 65 @LoginRequired()
66 66 def __before__(self):
67 67 super(UserGroupsController, self).__before__()
68 68 c.available_permissions = config['available_permissions']
69 69 PermissionModel().set_global_permission_choices(c, gettext_translator=_)
70 70
71 71 def __load_data(self, user_group_id):
72 72 c.group_members_obj = [x.user for x in c.user_group.members]
73 73 c.group_members_obj.sort(key=lambda u: u.username.lower())
74 74 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
75 75
76 76 def __load_defaults(self, user_group_id):
77 77 """
78 78 Load defaults settings for edit, and update
79 79
80 80 :param user_group_id:
81 81 """
82 82 user_group = UserGroup.get_or_404(user_group_id)
83 83 data = user_group.get_dict()
84 84 # fill owner
85 85 if user_group.user:
86 86 data.update({'user': user_group.user.username})
87 87 else:
88 88 replacement_user = User.get_first_super_admin().username
89 89 data.update({'user': replacement_user})
90 90 return data
91 91
92 92 def _revoke_perms_on_yourself(self, form_result):
93 93 _updates = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
94 94 form_result['perm_updates'])
95 95 _additions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
96 96 form_result['perm_additions'])
97 97 _deletions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
98 98 form_result['perm_deletions'])
99 99 admin_perm = 'usergroup.admin'
100 100 if _updates and _updates[0][1] != admin_perm or \
101 101 _additions and _additions[0][1] != admin_perm or \
102 102 _deletions and _deletions[0][1] != admin_perm:
103 103 return True
104 104 return False
105 105
106 106 # permission check inside
107 107 @NotAnonymous()
108 108 def index(self):
109
110 from rhodecode.lib.utils import PartialRenderer
111 _render = PartialRenderer('data_table/_dt_elements.mako')
109 # TODO(marcink): remove bind to self.request after pyramid migration
110 self.request = c.pyramid_request
111 _render = self.request.get_partial_renderer(
112 'data_table/_dt_elements.mako')
112 113
113 114 def user_group_name(user_group_id, user_group_name):
114 115 return _render("user_group_name", user_group_id, user_group_name)
115 116
116 117 def user_group_actions(user_group_id, user_group_name):
117 118 return _render("user_group_actions", user_group_id, user_group_name)
118 119
119 120 # json generate
120 121 group_iter = UserGroupList(UserGroup.query().all(),
121 122 perm_set=['usergroup.admin'])
122 123
123 124 user_groups_data = []
124 125 for user_gr in group_iter:
125 126 user_groups_data.append({
126 127 "group_name": user_group_name(
127 128 user_gr.users_group_id, h.escape(user_gr.users_group_name)),
128 129 "group_name_raw": user_gr.users_group_name,
129 130 "desc": h.escape(user_gr.user_group_description),
130 131 "members": len(user_gr.members),
131 132 "sync": user_gr.group_data.get('extern_type'),
132 133 "active": h.bool2icon(user_gr.users_group_active),
133 134 "owner": h.escape(h.link_to_user(user_gr.user.username)),
134 135 "action": user_group_actions(
135 136 user_gr.users_group_id, user_gr.users_group_name)
136 137 })
137 138
138 139 c.data = json.dumps(user_groups_data)
139 140 return render('admin/user_groups/user_groups.mako')
140 141
141 142 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
142 143 @auth.CSRFRequired()
143 144 def create(self):
144 145
145 146 users_group_form = UserGroupForm()()
146 147 try:
147 148 form_result = users_group_form.to_python(dict(request.POST))
148 149 user_group = UserGroupModel().create(
149 150 name=form_result['users_group_name'],
150 151 description=form_result['user_group_description'],
151 152 owner=c.rhodecode_user.user_id,
152 153 active=form_result['users_group_active'])
153 154 Session().flush()
154 155 creation_data = user_group.get_api_data()
155 156 user_group_name = form_result['users_group_name']
156 157
157 158 audit_logger.store_web(
158 159 'user_group.create', action_data={'data': creation_data},
159 160 user=c.rhodecode_user)
160 161
161 162 user_group_link = h.link_to(
162 163 h.escape(user_group_name),
163 164 url('edit_users_group', user_group_id=user_group.users_group_id))
164 165 h.flash(h.literal(_('Created user group %(user_group_link)s')
165 166 % {'user_group_link': user_group_link}),
166 167 category='success')
167 168 Session().commit()
168 169 except formencode.Invalid as errors:
169 170 return htmlfill.render(
170 171 render('admin/user_groups/user_group_add.mako'),
171 172 defaults=errors.value,
172 173 errors=errors.error_dict or {},
173 174 prefix_error=False,
174 175 encoding="UTF-8",
175 176 force_defaults=False)
176 177 except Exception:
177 178 log.exception("Exception creating user group")
178 179 h.flash(_('Error occurred during creation of user group %s') \
179 180 % request.POST.get('users_group_name'), category='error')
180 181
181 182 return redirect(
182 183 url('edit_users_group', user_group_id=user_group.users_group_id))
183 184
184 185 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
185 186 def new(self):
186 187 """GET /user_groups/new: Form to create a new item"""
187 188 # url('new_users_group')
188 189 return render('admin/user_groups/user_group_add.mako')
189 190
190 191 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
191 192 @auth.CSRFRequired()
192 193 def update(self, user_group_id):
193 194
194 195 user_group_id = safe_int(user_group_id)
195 196 c.user_group = UserGroup.get_or_404(user_group_id)
196 197 c.active = 'settings'
197 198 self.__load_data(user_group_id)
198 199
199 200 users_group_form = UserGroupForm(
200 201 edit=True, old_data=c.user_group.get_dict(), allow_disabled=True)()
201 202
202 203 old_values = c.user_group.get_api_data()
203 204 try:
204 205 form_result = users_group_form.to_python(request.POST)
205 206 pstruct = peppercorn.parse(request.POST.items())
206 207 form_result['users_group_members'] = pstruct['user_group_members']
207 208
208 209 user_group, added_members, removed_members = \
209 210 UserGroupModel().update(c.user_group, form_result)
210 211 updated_user_group = form_result['users_group_name']
211 212
212 213 audit_logger.store_web(
213 214 'user_group.edit', action_data={'old_data': old_values},
214 215 user=c.rhodecode_user)
215 216
216 217 # TODO(marcink): use added/removed to set user_group.edit.member.add
217 218
218 219 h.flash(_('Updated user group %s') % updated_user_group,
219 220 category='success')
220 221 Session().commit()
221 222 except formencode.Invalid as errors:
222 223 defaults = errors.value
223 224 e = errors.error_dict or {}
224 225
225 226 return htmlfill.render(
226 227 render('admin/user_groups/user_group_edit.mako'),
227 228 defaults=defaults,
228 229 errors=e,
229 230 prefix_error=False,
230 231 encoding="UTF-8",
231 232 force_defaults=False)
232 233 except Exception:
233 234 log.exception("Exception during update of user group")
234 235 h.flash(_('Error occurred during update of user group %s')
235 236 % request.POST.get('users_group_name'), category='error')
236 237
237 238 return redirect(url('edit_users_group', user_group_id=user_group_id))
238 239
239 240 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
240 241 @auth.CSRFRequired()
241 242 def delete(self, user_group_id):
242 243 user_group_id = safe_int(user_group_id)
243 244 c.user_group = UserGroup.get_or_404(user_group_id)
244 245 force = str2bool(request.POST.get('force'))
245 246
246 247 old_values = c.user_group.get_api_data()
247 248 try:
248 249 UserGroupModel().delete(c.user_group, force=force)
249 250 audit_logger.store_web(
250 251 'user.delete', action_data={'old_data': old_values},
251 252 user=c.rhodecode_user)
252 253 Session().commit()
253 254 h.flash(_('Successfully deleted user group'), category='success')
254 255 except UserGroupAssignedException as e:
255 256 h.flash(str(e), category='error')
256 257 except Exception:
257 258 log.exception("Exception during deletion of user group")
258 259 h.flash(_('An error occurred during deletion of user group'),
259 260 category='error')
260 261 return redirect(url('users_groups'))
261 262
262 263 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
263 264 def edit(self, user_group_id):
264 265 """GET /user_groups/user_group_id/edit: Form to edit an existing item"""
265 266 # url('edit_users_group', user_group_id=ID)
266 267
267 268 user_group_id = safe_int(user_group_id)
268 269 c.user_group = UserGroup.get_or_404(user_group_id)
269 270 c.active = 'settings'
270 271 self.__load_data(user_group_id)
271 272
272 273 defaults = self.__load_defaults(user_group_id)
273 274
274 275 return htmlfill.render(
275 276 render('admin/user_groups/user_group_edit.mako'),
276 277 defaults=defaults,
277 278 encoding="UTF-8",
278 279 force_defaults=False
279 280 )
280 281
281 282 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
282 283 def edit_perms(self, user_group_id):
283 284 user_group_id = safe_int(user_group_id)
284 285 c.user_group = UserGroup.get_or_404(user_group_id)
285 286 c.active = 'perms'
286 287
287 288 defaults = {}
288 289 # fill user group users
289 290 for p in c.user_group.user_user_group_to_perm:
290 291 defaults.update({'u_perm_%s' % p.user.user_id:
291 292 p.permission.permission_name})
292 293
293 294 for p in c.user_group.user_group_user_group_to_perm:
294 295 defaults.update({'g_perm_%s' % p.user_group.users_group_id:
295 296 p.permission.permission_name})
296 297
297 298 return htmlfill.render(
298 299 render('admin/user_groups/user_group_edit.mako'),
299 300 defaults=defaults,
300 301 encoding="UTF-8",
301 302 force_defaults=False
302 303 )
303 304
304 305 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
305 306 @auth.CSRFRequired()
306 307 def update_perms(self, user_group_id):
307 308 """
308 309 grant permission for given usergroup
309 310
310 311 :param user_group_id:
311 312 """
312 313 user_group_id = safe_int(user_group_id)
313 314 c.user_group = UserGroup.get_or_404(user_group_id)
314 315 form = UserGroupPermsForm()().to_python(request.POST)
315 316
316 317 if not c.rhodecode_user.is_admin:
317 318 if self._revoke_perms_on_yourself(form):
318 319 msg = _('Cannot change permission for yourself as admin')
319 320 h.flash(msg, category='warning')
320 321 return redirect(url('edit_user_group_perms', user_group_id=user_group_id))
321 322
322 323 try:
323 324 UserGroupModel().update_permissions(user_group_id,
324 325 form['perm_additions'], form['perm_updates'], form['perm_deletions'])
325 326 except RepoGroupAssignmentError:
326 327 h.flash(_('Target group cannot be the same'), category='error')
327 328 return redirect(url('edit_user_group_perms', user_group_id=user_group_id))
328 329
329 330 # TODO(marcink): implement global permissions
330 331 # audit_log.store_web('user_group.edit.permissions')
331 332 Session().commit()
332 333 h.flash(_('User Group permissions updated'), category='success')
333 334 return redirect(url('edit_user_group_perms', user_group_id=user_group_id))
334 335
335 336 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
336 337 def edit_perms_summary(self, user_group_id):
337 338 user_group_id = safe_int(user_group_id)
338 339 c.user_group = UserGroup.get_or_404(user_group_id)
339 340 c.active = 'perms_summary'
340 341 permissions = {
341 342 'repositories': {},
342 343 'repositories_groups': {},
343 344 }
344 345 ugroup_repo_perms = UserGroupRepoToPerm.query()\
345 346 .options(joinedload(UserGroupRepoToPerm.permission))\
346 347 .options(joinedload(UserGroupRepoToPerm.repository))\
347 348 .filter(UserGroupRepoToPerm.users_group_id == user_group_id)\
348 349 .all()
349 350
350 351 for gr in ugroup_repo_perms:
351 352 permissions['repositories'][gr.repository.repo_name] \
352 353 = gr.permission.permission_name
353 354
354 355 ugroup_group_perms = UserGroupRepoGroupToPerm.query()\
355 356 .options(joinedload(UserGroupRepoGroupToPerm.permission))\
356 357 .options(joinedload(UserGroupRepoGroupToPerm.group))\
357 358 .filter(UserGroupRepoGroupToPerm.users_group_id == user_group_id)\
358 359 .all()
359 360
360 361 for gr in ugroup_group_perms:
361 362 permissions['repositories_groups'][gr.group.group_name] \
362 363 = gr.permission.permission_name
363 364 c.permissions = permissions
364 365 return render('admin/user_groups/user_group_edit.mako')
365 366
366 367 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
367 368 def edit_global_perms(self, user_group_id):
368 369 user_group_id = safe_int(user_group_id)
369 370 c.user_group = UserGroup.get_or_404(user_group_id)
370 371 c.active = 'global_perms'
371 372
372 373 c.default_user = User.get_default_user()
373 374 defaults = c.user_group.get_dict()
374 375 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
375 376 defaults.update(c.user_group.get_default_perms())
376 377
377 378 return htmlfill.render(
378 379 render('admin/user_groups/user_group_edit.mako'),
379 380 defaults=defaults,
380 381 encoding="UTF-8",
381 382 force_defaults=False
382 383 )
383 384
384 385 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
385 386 @auth.CSRFRequired()
386 387 def update_global_perms(self, user_group_id):
387 388 user_group_id = safe_int(user_group_id)
388 389 user_group = UserGroup.get_or_404(user_group_id)
389 390 c.active = 'global_perms'
390 391
391 392 try:
392 393 # first stage that verifies the checkbox
393 394 _form = UserIndividualPermissionsForm()
394 395 form_result = _form.to_python(dict(request.POST))
395 396 inherit_perms = form_result['inherit_default_permissions']
396 397 user_group.inherit_default_permissions = inherit_perms
397 398 Session().add(user_group)
398 399
399 400 if not inherit_perms:
400 401 # only update the individual ones if we un check the flag
401 402 _form = UserPermissionsForm(
402 403 [x[0] for x in c.repo_create_choices],
403 404 [x[0] for x in c.repo_create_on_write_choices],
404 405 [x[0] for x in c.repo_group_create_choices],
405 406 [x[0] for x in c.user_group_create_choices],
406 407 [x[0] for x in c.fork_choices],
407 408 [x[0] for x in c.inherit_default_permission_choices])()
408 409
409 410 form_result = _form.to_python(dict(request.POST))
410 411 form_result.update({'perm_user_group_id': user_group.users_group_id})
411 412
412 413 PermissionModel().update_user_group_permissions(form_result)
413 414
414 415 Session().commit()
415 416 h.flash(_('User Group global permissions updated successfully'),
416 417 category='success')
417 418
418 419 except formencode.Invalid as errors:
419 420 defaults = errors.value
420 421 c.user_group = user_group
421 422 return htmlfill.render(
422 423 render('admin/user_groups/user_group_edit.mako'),
423 424 defaults=defaults,
424 425 errors=errors.error_dict or {},
425 426 prefix_error=False,
426 427 encoding="UTF-8",
427 428 force_defaults=False)
428 429 except Exception:
429 430 log.exception("Exception during permissions saving")
430 431 h.flash(_('An error occurred during permissions saving'),
431 432 category='error')
432 433
433 434 return redirect(url('edit_user_group_global_perms', user_group_id=user_group_id))
434 435
435 436 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
436 437 def edit_advanced(self, user_group_id):
437 438 user_group_id = safe_int(user_group_id)
438 439 c.user_group = UserGroup.get_or_404(user_group_id)
439 440 c.active = 'advanced'
440 441 c.group_members_obj = sorted(
441 442 (x.user for x in c.user_group.members),
442 443 key=lambda u: u.username.lower())
443 444
444 445 c.group_to_repos = sorted(
445 446 (x.repository for x in c.user_group.users_group_repo_to_perm),
446 447 key=lambda u: u.repo_name.lower())
447 448
448 449 c.group_to_repo_groups = sorted(
449 450 (x.group for x in c.user_group.users_group_repo_group_to_perm),
450 451 key=lambda u: u.group_name.lower())
451 452
452 453 return render('admin/user_groups/user_group_edit.mako')
453 454
454 455 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
455 456 def edit_advanced_set_synchronization(self, user_group_id):
456 457 user_group_id = safe_int(user_group_id)
457 458 user_group = UserGroup.get_or_404(user_group_id)
458 459
459 460 existing = user_group.group_data.get('extern_type')
460 461
461 462 if existing:
462 463 new_state = user_group.group_data
463 464 new_state['extern_type'] = None
464 465 else:
465 466 new_state = user_group.group_data
466 467 new_state['extern_type'] = 'manual'
467 468 new_state['extern_type_set_by'] = c.rhodecode_user.username
468 469
469 470 try:
470 471 user_group.group_data = new_state
471 472 Session().add(user_group)
472 473 Session().commit()
473 474
474 475 h.flash(_('User Group synchronization updated successfully'),
475 476 category='success')
476 477 except Exception:
477 478 log.exception("Exception during sync settings saving")
478 479 h.flash(_('An error occurred during synchronization update'),
479 480 category='error')
480 481
481 482 return redirect(
482 483 url('edit_user_group_advanced', user_group_id=user_group_id))
483 484
484 485 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
485 486 @XHRRequired()
486 487 @jsonify
487 488 def user_group_members(self, user_group_id):
488 489 """
489 490 Return members of given user group
490 491 """
491 492 user_group_id = safe_int(user_group_id)
492 493 user_group = UserGroup.get_or_404(user_group_id)
493 494 group_members_obj = sorted((x.user for x in user_group.members),
494 495 key=lambda u: u.username.lower())
495 496
496 497 group_members = [
497 498 {
498 499 'id': user.user_id,
499 500 'first_name': user.first_name,
500 501 'last_name': user.last_name,
501 502 'username': user.username,
502 503 'icon_link': h.gravatar_url(user.email, 30),
503 504 'value_display': h.person(user.email),
504 505 'value': user.username,
505 506 'value_type': 'user',
506 507 'active': user.active,
507 508 }
508 509 for user in group_members_obj
509 510 ]
510 511
511 512 return {
512 513 'members': group_members
513 514 }
@@ -1,1029 +1,1028 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Repository model for rhodecode
23 23 """
24 24
25 25 import logging
26 26 import os
27 27 import re
28 28 import shutil
29 29 import time
30 30 import traceback
31 31 from datetime import datetime, timedelta
32 32
33 33 from pyramid.threadlocal import get_current_request
34 34 from zope.cachedescriptors.property import Lazy as LazyProperty
35 35
36 36 from rhodecode import events
37 37 from rhodecode.lib import helpers as h
38 38 from rhodecode.lib.auth import HasUserGroupPermissionAny
39 39 from rhodecode.lib.caching_query import FromCache
40 40 from rhodecode.lib.exceptions import AttachedForksError
41 41 from rhodecode.lib.hooks_base import log_delete_repository
42 42 from rhodecode.lib.utils import make_db_config
43 43 from rhodecode.lib.utils2 import (
44 44 safe_str, safe_unicode, remove_prefix, obfuscate_url_pw,
45 45 get_current_rhodecode_user, safe_int, datetime_to_time, action_logger_generic)
46 46 from rhodecode.lib.vcs.backends import get_backend
47 47 from rhodecode.model import BaseModel
48 48 from rhodecode.model.db import (_hash_key,
49 49 Repository, UserRepoToPerm, UserGroupRepoToPerm, UserRepoGroupToPerm,
50 50 UserGroupRepoGroupToPerm, User, Permission, Statistics, UserGroup,
51 51 RepoGroup, RepositoryField)
52 52
53 53 from rhodecode.model.settings import VcsSettingsModel
54 54
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 class RepoModel(BaseModel):
60 60
61 61 cls = Repository
62 62
63 63 def _get_user_group(self, users_group):
64 64 return self._get_instance(UserGroup, users_group,
65 65 callback=UserGroup.get_by_group_name)
66 66
67 67 def _get_repo_group(self, repo_group):
68 68 return self._get_instance(RepoGroup, repo_group,
69 69 callback=RepoGroup.get_by_group_name)
70 70
71 71 def _create_default_perms(self, repository, private):
72 72 # create default permission
73 73 default = 'repository.read'
74 74 def_user = User.get_default_user()
75 75 for p in def_user.user_perms:
76 76 if p.permission.permission_name.startswith('repository.'):
77 77 default = p.permission.permission_name
78 78 break
79 79
80 80 default_perm = 'repository.none' if private else default
81 81
82 82 repo_to_perm = UserRepoToPerm()
83 83 repo_to_perm.permission = Permission.get_by_key(default_perm)
84 84
85 85 repo_to_perm.repository = repository
86 86 repo_to_perm.user_id = def_user.user_id
87 87
88 88 return repo_to_perm
89 89
90 90 @LazyProperty
91 91 def repos_path(self):
92 92 """
93 93 Gets the repositories root path from database
94 94 """
95 95 settings_model = VcsSettingsModel(sa=self.sa)
96 96 return settings_model.get_repos_location()
97 97
98 98 def get(self, repo_id, cache=False):
99 99 repo = self.sa.query(Repository) \
100 100 .filter(Repository.repo_id == repo_id)
101 101
102 102 if cache:
103 103 repo = repo.options(
104 104 FromCache("sql_cache_short", "get_repo_%s" % repo_id))
105 105 return repo.scalar()
106 106
107 107 def get_repo(self, repository):
108 108 return self._get_repo(repository)
109 109
110 110 def get_by_repo_name(self, repo_name, cache=False):
111 111 repo = self.sa.query(Repository) \
112 112 .filter(Repository.repo_name == repo_name)
113 113
114 114 if cache:
115 115 name_key = _hash_key(repo_name)
116 116 repo = repo.options(
117 117 FromCache("sql_cache_short", "get_repo_%s" % name_key))
118 118 return repo.scalar()
119 119
120 120 def _extract_id_from_repo_name(self, repo_name):
121 121 if repo_name.startswith('/'):
122 122 repo_name = repo_name.lstrip('/')
123 123 by_id_match = re.match(r'^_(\d{1,})', repo_name)
124 124 if by_id_match:
125 125 return by_id_match.groups()[0]
126 126
127 127 def get_repo_by_id(self, repo_name):
128 128 """
129 129 Extracts repo_name by id from special urls.
130 130 Example url is _11/repo_name
131 131
132 132 :param repo_name:
133 133 :return: repo object if matched else None
134 134 """
135 135
136 136 try:
137 137 _repo_id = self._extract_id_from_repo_name(repo_name)
138 138 if _repo_id:
139 139 return self.get(_repo_id)
140 140 except Exception:
141 141 log.exception('Failed to extract repo_name from URL')
142 142
143 143 return None
144 144
145 145 def get_repos_for_root(self, root, traverse=False):
146 146 if traverse:
147 147 like_expression = u'{}%'.format(safe_unicode(root))
148 148 repos = Repository.query().filter(
149 149 Repository.repo_name.like(like_expression)).all()
150 150 else:
151 151 if root and not isinstance(root, RepoGroup):
152 152 raise ValueError(
153 153 'Root must be an instance '
154 154 'of RepoGroup, got:{} instead'.format(type(root)))
155 155 repos = Repository.query().filter(Repository.group == root).all()
156 156 return repos
157 157
158 158 def get_url(self, repo, request=None, permalink=False):
159 159 if not request:
160 160 request = get_current_request()
161 161
162 162 if not request:
163 163 return
164 164
165 165 if permalink:
166 166 return request.route_url(
167 167 'repo_summary', repo_name=safe_str(repo.repo_id))
168 168 else:
169 169 return request.route_url(
170 170 'repo_summary', repo_name=safe_str(repo.repo_name))
171 171
172 172 def get_commit_url(self, repo, commit_id, request=None, permalink=False):
173 173 if not request:
174 174 request = get_current_request()
175 175
176 176 if not request:
177 177 return
178 178
179 179 if permalink:
180 180 return request.route_url(
181 181 'repo_commit', repo_name=safe_str(repo.repo_id),
182 182 commit_id=commit_id)
183 183
184 184 else:
185 185 return request.route_url(
186 186 'repo_commit', repo_name=safe_str(repo.repo_name),
187 187 commit_id=commit_id)
188 188
189 189 @classmethod
190 190 def update_repoinfo(cls, repositories=None):
191 191 if not repositories:
192 192 repositories = Repository.getAll()
193 193 for repo in repositories:
194 194 repo.update_commit_cache()
195 195
196 196 def get_repos_as_dict(self, repo_list=None, admin=False,
197 197 super_user_actions=False):
198
199 from rhodecode.lib.utils import PartialRenderer
200 _render = PartialRenderer('data_table/_dt_elements.mako')
201 c = _render.c
198 _render = get_current_request().get_partial_renderer(
199 'data_table/_dt_elements.mako')
200 c = _render.get_call_context()
202 201
203 202 def quick_menu(repo_name):
204 203 return _render('quick_menu', repo_name)
205 204
206 205 def repo_lnk(name, rtype, rstate, private, fork_of):
207 206 return _render('repo_name', name, rtype, rstate, private, fork_of,
208 207 short_name=not admin, admin=False)
209 208
210 209 def last_change(last_change):
211 210 if admin and isinstance(last_change, datetime) and not last_change.tzinfo:
212 211 last_change = last_change + timedelta(seconds=
213 212 (datetime.now() - datetime.utcnow()).seconds)
214 213 return _render("last_change", last_change)
215 214
216 215 def rss_lnk(repo_name):
217 216 return _render("rss", repo_name)
218 217
219 218 def atom_lnk(repo_name):
220 219 return _render("atom", repo_name)
221 220
222 221 def last_rev(repo_name, cs_cache):
223 222 return _render('revision', repo_name, cs_cache.get('revision'),
224 223 cs_cache.get('raw_id'), cs_cache.get('author'),
225 224 cs_cache.get('message'))
226 225
227 226 def desc(desc):
228 227 if c.visual.stylify_metatags:
229 228 desc = h.urlify_text(h.escaped_stylize(desc))
230 229 else:
231 230 desc = h.urlify_text(h.html_escape(desc))
232 231
233 232 return _render('repo_desc', desc)
234 233
235 234 def state(repo_state):
236 235 return _render("repo_state", repo_state)
237 236
238 237 def repo_actions(repo_name):
239 238 return _render('repo_actions', repo_name, super_user_actions)
240 239
241 240 def user_profile(username):
242 241 return _render('user_profile', username)
243 242
244 243 repos_data = []
245 244 for repo in repo_list:
246 245 cs_cache = repo.changeset_cache
247 246 row = {
248 247 "menu": quick_menu(repo.repo_name),
249 248
250 249 "name": repo_lnk(repo.repo_name, repo.repo_type,
251 250 repo.repo_state, repo.private, repo.fork),
252 251 "name_raw": repo.repo_name.lower(),
253 252
254 253 "last_change": last_change(repo.last_db_change),
255 254 "last_change_raw": datetime_to_time(repo.last_db_change),
256 255
257 256 "last_changeset": last_rev(repo.repo_name, cs_cache),
258 257 "last_changeset_raw": cs_cache.get('revision'),
259 258
260 259 "desc": desc(repo.description_safe),
261 260 "owner": user_profile(repo.user.username),
262 261
263 262 "state": state(repo.repo_state),
264 263 "rss": rss_lnk(repo.repo_name),
265 264
266 265 "atom": atom_lnk(repo.repo_name),
267 266 }
268 267 if admin:
269 268 row.update({
270 269 "action": repo_actions(repo.repo_name),
271 270 })
272 271 repos_data.append(row)
273 272
274 273 return repos_data
275 274
276 275 def _get_defaults(self, repo_name):
277 276 """
278 277 Gets information about repository, and returns a dict for
279 278 usage in forms
280 279
281 280 :param repo_name:
282 281 """
283 282
284 283 repo_info = Repository.get_by_repo_name(repo_name)
285 284
286 285 if repo_info is None:
287 286 return None
288 287
289 288 defaults = repo_info.get_dict()
290 289 defaults['repo_name'] = repo_info.just_name
291 290
292 291 groups = repo_info.groups_with_parents
293 292 parent_group = groups[-1] if groups else None
294 293
295 294 # we use -1 as this is how in HTML, we mark an empty group
296 295 defaults['repo_group'] = getattr(parent_group, 'group_id', -1)
297 296
298 297 keys_to_process = (
299 298 {'k': 'repo_type', 'strip': False},
300 299 {'k': 'repo_enable_downloads', 'strip': True},
301 300 {'k': 'repo_description', 'strip': True},
302 301 {'k': 'repo_enable_locking', 'strip': True},
303 302 {'k': 'repo_landing_rev', 'strip': True},
304 303 {'k': 'clone_uri', 'strip': False},
305 304 {'k': 'repo_private', 'strip': True},
306 305 {'k': 'repo_enable_statistics', 'strip': True}
307 306 )
308 307
309 308 for item in keys_to_process:
310 309 attr = item['k']
311 310 if item['strip']:
312 311 attr = remove_prefix(item['k'], 'repo_')
313 312
314 313 val = defaults[attr]
315 314 if item['k'] == 'repo_landing_rev':
316 315 val = ':'.join(defaults[attr])
317 316 defaults[item['k']] = val
318 317 if item['k'] == 'clone_uri':
319 318 defaults['clone_uri_hidden'] = repo_info.clone_uri_hidden
320 319
321 320 # fill owner
322 321 if repo_info.user:
323 322 defaults.update({'user': repo_info.user.username})
324 323 else:
325 324 replacement_user = User.get_first_super_admin().username
326 325 defaults.update({'user': replacement_user})
327 326
328 327 return defaults
329 328
330 329 def update(self, repo, **kwargs):
331 330 try:
332 331 cur_repo = self._get_repo(repo)
333 332 source_repo_name = cur_repo.repo_name
334 333 if 'user' in kwargs:
335 334 cur_repo.user = User.get_by_username(kwargs['user'])
336 335
337 336 if 'repo_group' in kwargs:
338 337 cur_repo.group = RepoGroup.get(kwargs['repo_group'])
339 338 log.debug('Updating repo %s with params:%s', cur_repo, kwargs)
340 339
341 340 update_keys = [
342 341 (1, 'repo_description'),
343 342 (1, 'repo_landing_rev'),
344 343 (1, 'repo_private'),
345 344 (1, 'repo_enable_downloads'),
346 345 (1, 'repo_enable_locking'),
347 346 (1, 'repo_enable_statistics'),
348 347 (0, 'clone_uri'),
349 348 (0, 'fork_id')
350 349 ]
351 350 for strip, k in update_keys:
352 351 if k in kwargs:
353 352 val = kwargs[k]
354 353 if strip:
355 354 k = remove_prefix(k, 'repo_')
356 355
357 356 setattr(cur_repo, k, val)
358 357
359 358 new_name = cur_repo.get_new_name(kwargs['repo_name'])
360 359 cur_repo.repo_name = new_name
361 360
362 361 # if private flag is set, reset default permission to NONE
363 362 if kwargs.get('repo_private'):
364 363 EMPTY_PERM = 'repository.none'
365 364 RepoModel().grant_user_permission(
366 365 repo=cur_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM
367 366 )
368 367
369 368 # handle extra fields
370 369 for field in filter(lambda k: k.startswith(RepositoryField.PREFIX),
371 370 kwargs):
372 371 k = RepositoryField.un_prefix_key(field)
373 372 ex_field = RepositoryField.get_by_key_name(
374 373 key=k, repo=cur_repo)
375 374 if ex_field:
376 375 ex_field.field_value = kwargs[field]
377 376 self.sa.add(ex_field)
378 377 self.sa.add(cur_repo)
379 378
380 379 if source_repo_name != new_name:
381 380 # rename repository
382 381 self._rename_filesystem_repo(
383 382 old=source_repo_name, new=new_name)
384 383
385 384 return cur_repo
386 385 except Exception:
387 386 log.error(traceback.format_exc())
388 387 raise
389 388
390 389 def _create_repo(self, repo_name, repo_type, description, owner,
391 390 private=False, clone_uri=None, repo_group=None,
392 391 landing_rev='rev:tip', fork_of=None,
393 392 copy_fork_permissions=False, enable_statistics=False,
394 393 enable_locking=False, enable_downloads=False,
395 394 copy_group_permissions=False,
396 395 state=Repository.STATE_PENDING):
397 396 """
398 397 Create repository inside database with PENDING state, this should be
399 398 only executed by create() repo. With exception of importing existing
400 399 repos
401 400 """
402 401 from rhodecode.model.scm import ScmModel
403 402
404 403 owner = self._get_user(owner)
405 404 fork_of = self._get_repo(fork_of)
406 405 repo_group = self._get_repo_group(safe_int(repo_group))
407 406
408 407 try:
409 408 repo_name = safe_unicode(repo_name)
410 409 description = safe_unicode(description)
411 410 # repo name is just a name of repository
412 411 # while repo_name_full is a full qualified name that is combined
413 412 # with name and path of group
414 413 repo_name_full = repo_name
415 414 repo_name = repo_name.split(Repository.NAME_SEP)[-1]
416 415
417 416 new_repo = Repository()
418 417 new_repo.repo_state = state
419 418 new_repo.enable_statistics = False
420 419 new_repo.repo_name = repo_name_full
421 420 new_repo.repo_type = repo_type
422 421 new_repo.user = owner
423 422 new_repo.group = repo_group
424 423 new_repo.description = description or repo_name
425 424 new_repo.private = private
426 425 new_repo.clone_uri = clone_uri
427 426 new_repo.landing_rev = landing_rev
428 427
429 428 new_repo.enable_statistics = enable_statistics
430 429 new_repo.enable_locking = enable_locking
431 430 new_repo.enable_downloads = enable_downloads
432 431
433 432 if repo_group:
434 433 new_repo.enable_locking = repo_group.enable_locking
435 434
436 435 if fork_of:
437 436 parent_repo = fork_of
438 437 new_repo.fork = parent_repo
439 438
440 439 events.trigger(events.RepoPreCreateEvent(new_repo))
441 440
442 441 self.sa.add(new_repo)
443 442
444 443 EMPTY_PERM = 'repository.none'
445 444 if fork_of and copy_fork_permissions:
446 445 repo = fork_of
447 446 user_perms = UserRepoToPerm.query() \
448 447 .filter(UserRepoToPerm.repository == repo).all()
449 448 group_perms = UserGroupRepoToPerm.query() \
450 449 .filter(UserGroupRepoToPerm.repository == repo).all()
451 450
452 451 for perm in user_perms:
453 452 UserRepoToPerm.create(
454 453 perm.user, new_repo, perm.permission)
455 454
456 455 for perm in group_perms:
457 456 UserGroupRepoToPerm.create(
458 457 perm.users_group, new_repo, perm.permission)
459 458 # in case we copy permissions and also set this repo to private
460 459 # override the default user permission to make it a private
461 460 # repo
462 461 if private:
463 462 RepoModel(self.sa).grant_user_permission(
464 463 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
465 464
466 465 elif repo_group and copy_group_permissions:
467 466 user_perms = UserRepoGroupToPerm.query() \
468 467 .filter(UserRepoGroupToPerm.group == repo_group).all()
469 468
470 469 group_perms = UserGroupRepoGroupToPerm.query() \
471 470 .filter(UserGroupRepoGroupToPerm.group == repo_group).all()
472 471
473 472 for perm in user_perms:
474 473 perm_name = perm.permission.permission_name.replace(
475 474 'group.', 'repository.')
476 475 perm_obj = Permission.get_by_key(perm_name)
477 476 UserRepoToPerm.create(perm.user, new_repo, perm_obj)
478 477
479 478 for perm in group_perms:
480 479 perm_name = perm.permission.permission_name.replace(
481 480 'group.', 'repository.')
482 481 perm_obj = Permission.get_by_key(perm_name)
483 482 UserGroupRepoToPerm.create(
484 483 perm.users_group, new_repo, perm_obj)
485 484
486 485 if private:
487 486 RepoModel(self.sa).grant_user_permission(
488 487 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
489 488
490 489 else:
491 490 perm_obj = self._create_default_perms(new_repo, private)
492 491 self.sa.add(perm_obj)
493 492
494 493 # now automatically start following this repository as owner
495 494 ScmModel(self.sa).toggle_following_repo(new_repo.repo_id,
496 495 owner.user_id)
497 496
498 497 # we need to flush here, in order to check if database won't
499 498 # throw any exceptions, create filesystem dirs at the very end
500 499 self.sa.flush()
501 500 events.trigger(events.RepoCreateEvent(new_repo))
502 501 return new_repo
503 502
504 503 except Exception:
505 504 log.error(traceback.format_exc())
506 505 raise
507 506
508 507 def create(self, form_data, cur_user):
509 508 """
510 509 Create repository using celery tasks
511 510
512 511 :param form_data:
513 512 :param cur_user:
514 513 """
515 514 from rhodecode.lib.celerylib import tasks, run_task
516 515 return run_task(tasks.create_repo, form_data, cur_user)
517 516
518 517 def update_permissions(self, repo, perm_additions=None, perm_updates=None,
519 518 perm_deletions=None, check_perms=True,
520 519 cur_user=None):
521 520 if not perm_additions:
522 521 perm_additions = []
523 522 if not perm_updates:
524 523 perm_updates = []
525 524 if not perm_deletions:
526 525 perm_deletions = []
527 526
528 527 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
529 528
530 529 changes = {
531 530 'added': [],
532 531 'updated': [],
533 532 'deleted': []
534 533 }
535 534 # update permissions
536 535 for member_id, perm, member_type in perm_updates:
537 536 member_id = int(member_id)
538 537 if member_type == 'user':
539 538 member_name = User.get(member_id).username
540 539 # this updates also current one if found
541 540 self.grant_user_permission(
542 541 repo=repo, user=member_id, perm=perm)
543 542 else: # set for user group
544 543 # check if we have permissions to alter this usergroup
545 544 member_name = UserGroup.get(member_id).users_group_name
546 545 if not check_perms or HasUserGroupPermissionAny(
547 546 *req_perms)(member_name, user=cur_user):
548 547 self.grant_user_group_permission(
549 548 repo=repo, group_name=member_id, perm=perm)
550 549
551 550 changes['updated'].append({'type': member_type, 'id': member_id,
552 551 'name': member_name, 'new_perm': perm})
553 552
554 553 # set new permissions
555 554 for member_id, perm, member_type in perm_additions:
556 555 member_id = int(member_id)
557 556 if member_type == 'user':
558 557 member_name = User.get(member_id).username
559 558 self.grant_user_permission(
560 559 repo=repo, user=member_id, perm=perm)
561 560 else: # set for user group
562 561 # check if we have permissions to alter this usergroup
563 562 member_name = UserGroup.get(member_id).users_group_name
564 563 if not check_perms or HasUserGroupPermissionAny(
565 564 *req_perms)(member_name, user=cur_user):
566 565 self.grant_user_group_permission(
567 566 repo=repo, group_name=member_id, perm=perm)
568 567 changes['added'].append({'type': member_type, 'id': member_id,
569 568 'name': member_name, 'new_perm': perm})
570 569 # delete permissions
571 570 for member_id, perm, member_type in perm_deletions:
572 571 member_id = int(member_id)
573 572 if member_type == 'user':
574 573 member_name = User.get(member_id).username
575 574 self.revoke_user_permission(repo=repo, user=member_id)
576 575 else: # set for user group
577 576 # check if we have permissions to alter this usergroup
578 577 member_name = UserGroup.get(member_id).users_group_name
579 578 if not check_perms or HasUserGroupPermissionAny(
580 579 *req_perms)(member_name, user=cur_user):
581 580 self.revoke_user_group_permission(
582 581 repo=repo, group_name=member_id)
583 582
584 583 changes['deleted'].append({'type': member_type, 'id': member_id,
585 584 'name': member_name, 'new_perm': perm})
586 585 return changes
587 586
588 587 def create_fork(self, form_data, cur_user):
589 588 """
590 589 Simple wrapper into executing celery task for fork creation
591 590
592 591 :param form_data:
593 592 :param cur_user:
594 593 """
595 594 from rhodecode.lib.celerylib import tasks, run_task
596 595 return run_task(tasks.create_repo_fork, form_data, cur_user)
597 596
598 597 def delete(self, repo, forks=None, fs_remove=True, cur_user=None):
599 598 """
600 599 Delete given repository, forks parameter defines what do do with
601 600 attached forks. Throws AttachedForksError if deleted repo has attached
602 601 forks
603 602
604 603 :param repo:
605 604 :param forks: str 'delete' or 'detach'
606 605 :param fs_remove: remove(archive) repo from filesystem
607 606 """
608 607 if not cur_user:
609 608 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
610 609 repo = self._get_repo(repo)
611 610 if repo:
612 611 if forks == 'detach':
613 612 for r in repo.forks:
614 613 r.fork = None
615 614 self.sa.add(r)
616 615 elif forks == 'delete':
617 616 for r in repo.forks:
618 617 self.delete(r, forks='delete')
619 618 elif [f for f in repo.forks]:
620 619 raise AttachedForksError()
621 620
622 621 old_repo_dict = repo.get_dict()
623 622 events.trigger(events.RepoPreDeleteEvent(repo))
624 623 try:
625 624 self.sa.delete(repo)
626 625 if fs_remove:
627 626 self._delete_filesystem_repo(repo)
628 627 else:
629 628 log.debug('skipping removal from filesystem')
630 629 old_repo_dict.update({
631 630 'deleted_by': cur_user,
632 631 'deleted_on': time.time(),
633 632 })
634 633 log_delete_repository(**old_repo_dict)
635 634 events.trigger(events.RepoDeleteEvent(repo))
636 635 except Exception:
637 636 log.error(traceback.format_exc())
638 637 raise
639 638
640 639 def grant_user_permission(self, repo, user, perm):
641 640 """
642 641 Grant permission for user on given repository, or update existing one
643 642 if found
644 643
645 644 :param repo: Instance of Repository, repository_id, or repository name
646 645 :param user: Instance of User, user_id or username
647 646 :param perm: Instance of Permission, or permission_name
648 647 """
649 648 user = self._get_user(user)
650 649 repo = self._get_repo(repo)
651 650 permission = self._get_perm(perm)
652 651
653 652 # check if we have that permission already
654 653 obj = self.sa.query(UserRepoToPerm) \
655 654 .filter(UserRepoToPerm.user == user) \
656 655 .filter(UserRepoToPerm.repository == repo) \
657 656 .scalar()
658 657 if obj is None:
659 658 # create new !
660 659 obj = UserRepoToPerm()
661 660 obj.repository = repo
662 661 obj.user = user
663 662 obj.permission = permission
664 663 self.sa.add(obj)
665 664 log.debug('Granted perm %s to %s on %s', perm, user, repo)
666 665 action_logger_generic(
667 666 'granted permission: {} to user: {} on repo: {}'.format(
668 667 perm, user, repo), namespace='security.repo')
669 668 return obj
670 669
671 670 def revoke_user_permission(self, repo, user):
672 671 """
673 672 Revoke permission for user on given repository
674 673
675 674 :param repo: Instance of Repository, repository_id, or repository name
676 675 :param user: Instance of User, user_id or username
677 676 """
678 677
679 678 user = self._get_user(user)
680 679 repo = self._get_repo(repo)
681 680
682 681 obj = self.sa.query(UserRepoToPerm) \
683 682 .filter(UserRepoToPerm.repository == repo) \
684 683 .filter(UserRepoToPerm.user == user) \
685 684 .scalar()
686 685 if obj:
687 686 self.sa.delete(obj)
688 687 log.debug('Revoked perm on %s on %s', repo, user)
689 688 action_logger_generic(
690 689 'revoked permission from user: {} on repo: {}'.format(
691 690 user, repo), namespace='security.repo')
692 691
693 692 def grant_user_group_permission(self, repo, group_name, perm):
694 693 """
695 694 Grant permission for user group on given repository, or update
696 695 existing one if found
697 696
698 697 :param repo: Instance of Repository, repository_id, or repository name
699 698 :param group_name: Instance of UserGroup, users_group_id,
700 699 or user group name
701 700 :param perm: Instance of Permission, or permission_name
702 701 """
703 702 repo = self._get_repo(repo)
704 703 group_name = self._get_user_group(group_name)
705 704 permission = self._get_perm(perm)
706 705
707 706 # check if we have that permission already
708 707 obj = self.sa.query(UserGroupRepoToPerm) \
709 708 .filter(UserGroupRepoToPerm.users_group == group_name) \
710 709 .filter(UserGroupRepoToPerm.repository == repo) \
711 710 .scalar()
712 711
713 712 if obj is None:
714 713 # create new
715 714 obj = UserGroupRepoToPerm()
716 715
717 716 obj.repository = repo
718 717 obj.users_group = group_name
719 718 obj.permission = permission
720 719 self.sa.add(obj)
721 720 log.debug('Granted perm %s to %s on %s', perm, group_name, repo)
722 721 action_logger_generic(
723 722 'granted permission: {} to usergroup: {} on repo: {}'.format(
724 723 perm, group_name, repo), namespace='security.repo')
725 724
726 725 return obj
727 726
728 727 def revoke_user_group_permission(self, repo, group_name):
729 728 """
730 729 Revoke permission for user group on given repository
731 730
732 731 :param repo: Instance of Repository, repository_id, or repository name
733 732 :param group_name: Instance of UserGroup, users_group_id,
734 733 or user group name
735 734 """
736 735 repo = self._get_repo(repo)
737 736 group_name = self._get_user_group(group_name)
738 737
739 738 obj = self.sa.query(UserGroupRepoToPerm) \
740 739 .filter(UserGroupRepoToPerm.repository == repo) \
741 740 .filter(UserGroupRepoToPerm.users_group == group_name) \
742 741 .scalar()
743 742 if obj:
744 743 self.sa.delete(obj)
745 744 log.debug('Revoked perm to %s on %s', repo, group_name)
746 745 action_logger_generic(
747 746 'revoked permission from usergroup: {} on repo: {}'.format(
748 747 group_name, repo), namespace='security.repo')
749 748
750 749 def delete_stats(self, repo_name):
751 750 """
752 751 removes stats for given repo
753 752
754 753 :param repo_name:
755 754 """
756 755 repo = self._get_repo(repo_name)
757 756 try:
758 757 obj = self.sa.query(Statistics) \
759 758 .filter(Statistics.repository == repo).scalar()
760 759 if obj:
761 760 self.sa.delete(obj)
762 761 except Exception:
763 762 log.error(traceback.format_exc())
764 763 raise
765 764
766 765 def add_repo_field(self, repo_name, field_key, field_label, field_value='',
767 766 field_type='str', field_desc=''):
768 767
769 768 repo = self._get_repo(repo_name)
770 769
771 770 new_field = RepositoryField()
772 771 new_field.repository = repo
773 772 new_field.field_key = field_key
774 773 new_field.field_type = field_type # python type
775 774 new_field.field_value = field_value
776 775 new_field.field_desc = field_desc
777 776 new_field.field_label = field_label
778 777 self.sa.add(new_field)
779 778 return new_field
780 779
781 780 def delete_repo_field(self, repo_name, field_key):
782 781 repo = self._get_repo(repo_name)
783 782 field = RepositoryField.get_by_key_name(field_key, repo)
784 783 if field:
785 784 self.sa.delete(field)
786 785
787 786 def _create_filesystem_repo(self, repo_name, repo_type, repo_group,
788 787 clone_uri=None, repo_store_location=None,
789 788 use_global_config=False):
790 789 """
791 790 makes repository on filesystem. It's group aware means it'll create
792 791 a repository within a group, and alter the paths accordingly of
793 792 group location
794 793
795 794 :param repo_name:
796 795 :param alias:
797 796 :param parent:
798 797 :param clone_uri:
799 798 :param repo_store_location:
800 799 """
801 800 from rhodecode.lib.utils import is_valid_repo, is_valid_repo_group
802 801 from rhodecode.model.scm import ScmModel
803 802
804 803 if Repository.NAME_SEP in repo_name:
805 804 raise ValueError(
806 805 'repo_name must not contain groups got `%s`' % repo_name)
807 806
808 807 if isinstance(repo_group, RepoGroup):
809 808 new_parent_path = os.sep.join(repo_group.full_path_splitted)
810 809 else:
811 810 new_parent_path = repo_group or ''
812 811
813 812 if repo_store_location:
814 813 _paths = [repo_store_location]
815 814 else:
816 815 _paths = [self.repos_path, new_parent_path, repo_name]
817 816 # we need to make it str for mercurial
818 817 repo_path = os.path.join(*map(lambda x: safe_str(x), _paths))
819 818
820 819 # check if this path is not a repository
821 820 if is_valid_repo(repo_path, self.repos_path):
822 821 raise Exception('This path %s is a valid repository' % repo_path)
823 822
824 823 # check if this path is a group
825 824 if is_valid_repo_group(repo_path, self.repos_path):
826 825 raise Exception('This path %s is a valid group' % repo_path)
827 826
828 827 log.info('creating repo %s in %s from url: `%s`',
829 828 repo_name, safe_unicode(repo_path),
830 829 obfuscate_url_pw(clone_uri))
831 830
832 831 backend = get_backend(repo_type)
833 832
834 833 config_repo = None if use_global_config else repo_name
835 834 if config_repo and new_parent_path:
836 835 config_repo = Repository.NAME_SEP.join(
837 836 (new_parent_path, config_repo))
838 837 config = make_db_config(clear_session=False, repo=config_repo)
839 838 config.set('extensions', 'largefiles', '')
840 839
841 840 # patch and reset hooks section of UI config to not run any
842 841 # hooks on creating remote repo
843 842 config.clear_section('hooks')
844 843
845 844 # TODO: johbo: Unify this, hardcoded "bare=True" does not look nice
846 845 if repo_type == 'git':
847 846 repo = backend(
848 847 repo_path, config=config, create=True, src_url=clone_uri,
849 848 bare=True)
850 849 else:
851 850 repo = backend(
852 851 repo_path, config=config, create=True, src_url=clone_uri)
853 852
854 853 ScmModel().install_hooks(repo, repo_type=repo_type)
855 854
856 855 log.debug('Created repo %s with %s backend',
857 856 safe_unicode(repo_name), safe_unicode(repo_type))
858 857 return repo
859 858
860 859 def _rename_filesystem_repo(self, old, new):
861 860 """
862 861 renames repository on filesystem
863 862
864 863 :param old: old name
865 864 :param new: new name
866 865 """
867 866 log.info('renaming repo from %s to %s', old, new)
868 867
869 868 old_path = os.path.join(self.repos_path, old)
870 869 new_path = os.path.join(self.repos_path, new)
871 870 if os.path.isdir(new_path):
872 871 raise Exception(
873 872 'Was trying to rename to already existing dir %s' % new_path
874 873 )
875 874 shutil.move(old_path, new_path)
876 875
877 876 def _delete_filesystem_repo(self, repo):
878 877 """
879 878 removes repo from filesystem, the removal is acctually made by
880 879 added rm__ prefix into dir, and rename internat .hg/.git dirs so this
881 880 repository is no longer valid for rhodecode, can be undeleted later on
882 881 by reverting the renames on this repository
883 882
884 883 :param repo: repo object
885 884 """
886 885 rm_path = os.path.join(self.repos_path, repo.repo_name)
887 886 repo_group = repo.group
888 887 log.info("Removing repository %s", rm_path)
889 888 # disable hg/git internal that it doesn't get detected as repo
890 889 alias = repo.repo_type
891 890
892 891 config = make_db_config(clear_session=False)
893 892 config.set('extensions', 'largefiles', '')
894 893 bare = getattr(repo.scm_instance(config=config), 'bare', False)
895 894
896 895 # skip this for bare git repos
897 896 if not bare:
898 897 # disable VCS repo
899 898 vcs_path = os.path.join(rm_path, '.%s' % alias)
900 899 if os.path.exists(vcs_path):
901 900 shutil.move(vcs_path, os.path.join(rm_path, 'rm__.%s' % alias))
902 901
903 902 _now = datetime.now()
904 903 _ms = str(_now.microsecond).rjust(6, '0')
905 904 _d = 'rm__%s__%s' % (_now.strftime('%Y%m%d_%H%M%S_' + _ms),
906 905 repo.just_name)
907 906 if repo_group:
908 907 # if repository is in group, prefix the removal path with the group
909 908 args = repo_group.full_path_splitted + [_d]
910 909 _d = os.path.join(*args)
911 910
912 911 if os.path.isdir(rm_path):
913 912 shutil.move(rm_path, os.path.join(self.repos_path, _d))
914 913
915 914
916 915 class ReadmeFinder:
917 916 """
918 917 Utility which knows how to find a readme for a specific commit.
919 918
920 919 The main idea is that this is a configurable algorithm. When creating an
921 920 instance you can define parameters, currently only the `default_renderer`.
922 921 Based on this configuration the method :meth:`search` behaves slightly
923 922 different.
924 923 """
925 924
926 925 readme_re = re.compile(r'^readme(\.[^\.]+)?$', re.IGNORECASE)
927 926 path_re = re.compile(r'^docs?', re.IGNORECASE)
928 927
929 928 default_priorities = {
930 929 None: 0,
931 930 '.text': 2,
932 931 '.txt': 3,
933 932 '.rst': 1,
934 933 '.rest': 2,
935 934 '.md': 1,
936 935 '.mkdn': 2,
937 936 '.mdown': 3,
938 937 '.markdown': 4,
939 938 }
940 939
941 940 path_priority = {
942 941 'doc': 0,
943 942 'docs': 1,
944 943 }
945 944
946 945 FALLBACK_PRIORITY = 99
947 946
948 947 RENDERER_TO_EXTENSION = {
949 948 'rst': ['.rst', '.rest'],
950 949 'markdown': ['.md', 'mkdn', '.mdown', '.markdown'],
951 950 }
952 951
953 952 def __init__(self, default_renderer=None):
954 953 self._default_renderer = default_renderer
955 954 self._renderer_extensions = self.RENDERER_TO_EXTENSION.get(
956 955 default_renderer, [])
957 956
958 957 def search(self, commit, path='/'):
959 958 """
960 959 Find a readme in the given `commit`.
961 960 """
962 961 nodes = commit.get_nodes(path)
963 962 matches = self._match_readmes(nodes)
964 963 matches = self._sort_according_to_priority(matches)
965 964 if matches:
966 965 return matches[0].node
967 966
968 967 paths = self._match_paths(nodes)
969 968 paths = self._sort_paths_according_to_priority(paths)
970 969 for path in paths:
971 970 match = self.search(commit, path=path)
972 971 if match:
973 972 return match
974 973
975 974 return None
976 975
977 976 def _match_readmes(self, nodes):
978 977 for node in nodes:
979 978 if not node.is_file():
980 979 continue
981 980 path = node.path.rsplit('/', 1)[-1]
982 981 match = self.readme_re.match(path)
983 982 if match:
984 983 extension = match.group(1)
985 984 yield ReadmeMatch(node, match, self._priority(extension))
986 985
987 986 def _match_paths(self, nodes):
988 987 for node in nodes:
989 988 if not node.is_dir():
990 989 continue
991 990 match = self.path_re.match(node.path)
992 991 if match:
993 992 yield node.path
994 993
995 994 def _priority(self, extension):
996 995 renderer_priority = (
997 996 0 if extension in self._renderer_extensions else 1)
998 997 extension_priority = self.default_priorities.get(
999 998 extension, self.FALLBACK_PRIORITY)
1000 999 return (renderer_priority, extension_priority)
1001 1000
1002 1001 def _sort_according_to_priority(self, matches):
1003 1002
1004 1003 def priority_and_path(match):
1005 1004 return (match.priority, match.path)
1006 1005
1007 1006 return sorted(matches, key=priority_and_path)
1008 1007
1009 1008 def _sort_paths_according_to_priority(self, paths):
1010 1009
1011 1010 def priority_and_path(path):
1012 1011 return (self.path_priority.get(path, self.FALLBACK_PRIORITY), path)
1013 1012
1014 1013 return sorted(paths, key=priority_and_path)
1015 1014
1016 1015
1017 1016 class ReadmeMatch:
1018 1017
1019 1018 def __init__(self, node, match, priority):
1020 1019 self.node = node
1021 1020 self._match = match
1022 1021 self.priority = priority
1023 1022
1024 1023 @property
1025 1024 def path(self):
1026 1025 return self.node.path
1027 1026
1028 1027 def __repr__(self):
1029 1028 return '<ReadmeMatch {} priority={}'.format(self.path, self.priority)
@@ -1,733 +1,734 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 repo group model for RhodeCode
24 24 """
25 25
26 26 import os
27 27 import datetime
28 28 import itertools
29 29 import logging
30 30 import shutil
31 31 import traceback
32 32 import string
33 33
34 34 from zope.cachedescriptors.property import Lazy as LazyProperty
35 35
36 36 from rhodecode import events
37 37 from rhodecode.model import BaseModel
38 38 from rhodecode.model.db import (_hash_key,
39 39 RepoGroup, UserRepoGroupToPerm, User, Permission, UserGroupRepoGroupToPerm,
40 40 UserGroup, Repository)
41 41 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
42 42 from rhodecode.lib.caching_query import FromCache
43 43 from rhodecode.lib.utils2 import action_logger_generic
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 class RepoGroupModel(BaseModel):
49 49
50 50 cls = RepoGroup
51 51 PERSONAL_GROUP_DESC = 'personal repo group of user `%(username)s`'
52 52 PERSONAL_GROUP_PATTERN = '${username}' # default
53 53
54 54 def _get_user_group(self, users_group):
55 55 return self._get_instance(UserGroup, users_group,
56 56 callback=UserGroup.get_by_group_name)
57 57
58 58 def _get_repo_group(self, repo_group):
59 59 return self._get_instance(RepoGroup, repo_group,
60 60 callback=RepoGroup.get_by_group_name)
61 61
62 62 @LazyProperty
63 63 def repos_path(self):
64 64 """
65 65 Gets the repositories root path from database
66 66 """
67 67
68 68 settings_model = VcsSettingsModel(sa=self.sa)
69 69 return settings_model.get_repos_location()
70 70
71 71 def get_by_group_name(self, repo_group_name, cache=None):
72 72 repo = self.sa.query(RepoGroup) \
73 73 .filter(RepoGroup.group_name == repo_group_name)
74 74
75 75 if cache:
76 76 name_key = _hash_key(repo_group_name)
77 77 repo = repo.options(
78 78 FromCache("sql_cache_short", "get_repo_group_%s" % name_key))
79 79 return repo.scalar()
80 80
81 81 def get_default_create_personal_repo_group(self):
82 82 value = SettingsModel().get_setting_by_name(
83 83 'create_personal_repo_group')
84 84 return value.app_settings_value if value else None or False
85 85
86 86 def get_personal_group_name_pattern(self):
87 87 value = SettingsModel().get_setting_by_name(
88 88 'personal_repo_group_pattern')
89 89 val = value.app_settings_value if value else None
90 90 group_template = val or self.PERSONAL_GROUP_PATTERN
91 91
92 92 group_template = group_template.lstrip('/')
93 93 return group_template
94 94
95 95 def get_personal_group_name(self, user):
96 96 template = self.get_personal_group_name_pattern()
97 97 return string.Template(template).safe_substitute(
98 98 username=user.username,
99 99 user_id=user.user_id,
100 100 )
101 101
102 102 def create_personal_repo_group(self, user, commit_early=True):
103 103 desc = self.PERSONAL_GROUP_DESC % {'username': user.username}
104 104 personal_repo_group_name = self.get_personal_group_name(user)
105 105
106 106 # create a new one
107 107 RepoGroupModel().create(
108 108 group_name=personal_repo_group_name,
109 109 group_description=desc,
110 110 owner=user.username,
111 111 personal=True,
112 112 commit_early=commit_early)
113 113
114 114 def _create_default_perms(self, new_group):
115 115 # create default permission
116 116 default_perm = 'group.read'
117 117 def_user = User.get_default_user()
118 118 for p in def_user.user_perms:
119 119 if p.permission.permission_name.startswith('group.'):
120 120 default_perm = p.permission.permission_name
121 121 break
122 122
123 123 repo_group_to_perm = UserRepoGroupToPerm()
124 124 repo_group_to_perm.permission = Permission.get_by_key(default_perm)
125 125
126 126 repo_group_to_perm.group = new_group
127 127 repo_group_to_perm.user_id = def_user.user_id
128 128 return repo_group_to_perm
129 129
130 130 def _get_group_name_and_parent(self, group_name_full, repo_in_path=False,
131 131 get_object=False):
132 132 """
133 133 Get's the group name and a parent group name from given group name.
134 134 If repo_in_path is set to truth, we asume the full path also includes
135 135 repo name, in such case we clean the last element.
136 136
137 137 :param group_name_full:
138 138 """
139 139 split_paths = 1
140 140 if repo_in_path:
141 141 split_paths = 2
142 142 _parts = group_name_full.rsplit(RepoGroup.url_sep(), split_paths)
143 143
144 144 if repo_in_path and len(_parts) > 1:
145 145 # such case last element is the repo_name
146 146 _parts.pop(-1)
147 147 group_name_cleaned = _parts[-1] # just the group name
148 148 parent_repo_group_name = None
149 149
150 150 if len(_parts) > 1:
151 151 parent_repo_group_name = _parts[0]
152 152
153 153 parent_group = None
154 154 if parent_repo_group_name:
155 155 parent_group = RepoGroup.get_by_group_name(parent_repo_group_name)
156 156
157 157 if get_object:
158 158 return group_name_cleaned, parent_repo_group_name, parent_group
159 159
160 160 return group_name_cleaned, parent_repo_group_name
161 161
162 162 def check_exist_filesystem(self, group_name, exc_on_failure=True):
163 163 create_path = os.path.join(self.repos_path, group_name)
164 164 log.debug('creating new group in %s', create_path)
165 165
166 166 if os.path.isdir(create_path):
167 167 if exc_on_failure:
168 168 abs_create_path = os.path.abspath(create_path)
169 169 raise Exception('Directory `{}` already exists !'.format(abs_create_path))
170 170 return False
171 171 return True
172 172
173 173 def _create_group(self, group_name):
174 174 """
175 175 makes repository group on filesystem
176 176
177 177 :param repo_name:
178 178 :param parent_id:
179 179 """
180 180
181 181 self.check_exist_filesystem(group_name)
182 182 create_path = os.path.join(self.repos_path, group_name)
183 183 log.debug('creating new group in %s', create_path)
184 184 os.makedirs(create_path, mode=0755)
185 185 log.debug('created group in %s', create_path)
186 186
187 187 def _rename_group(self, old, new):
188 188 """
189 189 Renames a group on filesystem
190 190
191 191 :param group_name:
192 192 """
193 193
194 194 if old == new:
195 195 log.debug('skipping group rename')
196 196 return
197 197
198 198 log.debug('renaming repository group from %s to %s', old, new)
199 199
200 200 old_path = os.path.join(self.repos_path, old)
201 201 new_path = os.path.join(self.repos_path, new)
202 202
203 203 log.debug('renaming repos paths from %s to %s', old_path, new_path)
204 204
205 205 if os.path.isdir(new_path):
206 206 raise Exception('Was trying to rename to already '
207 207 'existing dir %s' % new_path)
208 208 shutil.move(old_path, new_path)
209 209
210 210 def _delete_filesystem_group(self, group, force_delete=False):
211 211 """
212 212 Deletes a group from a filesystem
213 213
214 214 :param group: instance of group from database
215 215 :param force_delete: use shutil rmtree to remove all objects
216 216 """
217 217 paths = group.full_path.split(RepoGroup.url_sep())
218 218 paths = os.sep.join(paths)
219 219
220 220 rm_path = os.path.join(self.repos_path, paths)
221 221 log.info("Removing group %s", rm_path)
222 222 # delete only if that path really exists
223 223 if os.path.isdir(rm_path):
224 224 if force_delete:
225 225 shutil.rmtree(rm_path)
226 226 else:
227 227 # archive that group`
228 228 _now = datetime.datetime.now()
229 229 _ms = str(_now.microsecond).rjust(6, '0')
230 230 _d = 'rm__%s_GROUP_%s' % (
231 231 _now.strftime('%Y%m%d_%H%M%S_' + _ms), group.name)
232 232 shutil.move(rm_path, os.path.join(self.repos_path, _d))
233 233
234 234 def create(self, group_name, group_description, owner, just_db=False,
235 235 copy_permissions=False, personal=None, commit_early=True):
236 236
237 237 (group_name_cleaned,
238 238 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(group_name)
239 239
240 240 parent_group = None
241 241 if parent_group_name:
242 242 parent_group = self._get_repo_group(parent_group_name)
243 243 if not parent_group:
244 244 # we tried to create a nested group, but the parent is not
245 245 # existing
246 246 raise ValueError(
247 247 'Parent group `%s` given in `%s` group name '
248 248 'is not yet existing.' % (parent_group_name, group_name))
249 249
250 250 # because we are doing a cleanup, we need to check if such directory
251 251 # already exists. If we don't do that we can accidentally delete
252 252 # existing directory via cleanup that can cause data issues, since
253 253 # delete does a folder rename to special syntax later cleanup
254 254 # functions can delete this
255 255 cleanup_group = self.check_exist_filesystem(group_name,
256 256 exc_on_failure=False)
257 257 try:
258 258 user = self._get_user(owner)
259 259 new_repo_group = RepoGroup()
260 260 new_repo_group.user = user
261 261 new_repo_group.group_description = group_description or group_name
262 262 new_repo_group.parent_group = parent_group
263 263 new_repo_group.group_name = group_name
264 264 new_repo_group.personal = personal
265 265
266 266 self.sa.add(new_repo_group)
267 267
268 268 # create an ADMIN permission for owner except if we're super admin,
269 269 # later owner should go into the owner field of groups
270 270 if not user.is_admin:
271 271 self.grant_user_permission(repo_group=new_repo_group,
272 272 user=owner, perm='group.admin')
273 273
274 274 if parent_group and copy_permissions:
275 275 # copy permissions from parent
276 276 user_perms = UserRepoGroupToPerm.query() \
277 277 .filter(UserRepoGroupToPerm.group == parent_group).all()
278 278
279 279 group_perms = UserGroupRepoGroupToPerm.query() \
280 280 .filter(UserGroupRepoGroupToPerm.group == parent_group).all()
281 281
282 282 for perm in user_perms:
283 283 # don't copy over the permission for user who is creating
284 284 # this group, if he is not super admin he get's admin
285 285 # permission set above
286 286 if perm.user != user or user.is_admin:
287 287 UserRepoGroupToPerm.create(
288 288 perm.user, new_repo_group, perm.permission)
289 289
290 290 for perm in group_perms:
291 291 UserGroupRepoGroupToPerm.create(
292 292 perm.users_group, new_repo_group, perm.permission)
293 293 else:
294 294 perm_obj = self._create_default_perms(new_repo_group)
295 295 self.sa.add(perm_obj)
296 296
297 297 # now commit the changes, earlier so we are sure everything is in
298 298 # the database.
299 299 if commit_early:
300 300 self.sa.commit()
301 301 if not just_db:
302 302 self._create_group(new_repo_group.group_name)
303 303
304 304 # trigger the post hook
305 305 from rhodecode.lib.hooks_base import log_create_repository_group
306 306 repo_group = RepoGroup.get_by_group_name(group_name)
307 307 log_create_repository_group(
308 308 created_by=user.username, **repo_group.get_dict())
309 309
310 310 # Trigger create event.
311 311 events.trigger(events.RepoGroupCreateEvent(repo_group))
312 312
313 313 return new_repo_group
314 314 except Exception:
315 315 self.sa.rollback()
316 316 log.exception('Exception occurred when creating repository group, '
317 317 'doing cleanup...')
318 318 # rollback things manually !
319 319 repo_group = RepoGroup.get_by_group_name(group_name)
320 320 if repo_group:
321 321 RepoGroup.delete(repo_group.group_id)
322 322 self.sa.commit()
323 323 if cleanup_group:
324 324 RepoGroupModel()._delete_filesystem_group(repo_group)
325 325 raise
326 326
327 327 def update_permissions(
328 328 self, repo_group, perm_additions=None, perm_updates=None,
329 329 perm_deletions=None, recursive=None, check_perms=True,
330 330 cur_user=None):
331 331 from rhodecode.model.repo import RepoModel
332 332 from rhodecode.lib.auth import HasUserGroupPermissionAny
333 333
334 334 if not perm_additions:
335 335 perm_additions = []
336 336 if not perm_updates:
337 337 perm_updates = []
338 338 if not perm_deletions:
339 339 perm_deletions = []
340 340
341 341 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
342 342
343 343 changes = {
344 344 'added': [],
345 345 'updated': [],
346 346 'deleted': []
347 347 }
348 348
349 349 def _set_perm_user(obj, user, perm):
350 350 if isinstance(obj, RepoGroup):
351 351 self.grant_user_permission(
352 352 repo_group=obj, user=user, perm=perm)
353 353 elif isinstance(obj, Repository):
354 354 # private repos will not allow to change the default
355 355 # permissions using recursive mode
356 356 if obj.private and user == User.DEFAULT_USER:
357 357 return
358 358
359 359 # we set group permission but we have to switch to repo
360 360 # permission
361 361 perm = perm.replace('group.', 'repository.')
362 362 RepoModel().grant_user_permission(
363 363 repo=obj, user=user, perm=perm)
364 364
365 365 def _set_perm_group(obj, users_group, perm):
366 366 if isinstance(obj, RepoGroup):
367 367 self.grant_user_group_permission(
368 368 repo_group=obj, group_name=users_group, perm=perm)
369 369 elif isinstance(obj, Repository):
370 370 # we set group permission but we have to switch to repo
371 371 # permission
372 372 perm = perm.replace('group.', 'repository.')
373 373 RepoModel().grant_user_group_permission(
374 374 repo=obj, group_name=users_group, perm=perm)
375 375
376 376 def _revoke_perm_user(obj, user):
377 377 if isinstance(obj, RepoGroup):
378 378 self.revoke_user_permission(repo_group=obj, user=user)
379 379 elif isinstance(obj, Repository):
380 380 RepoModel().revoke_user_permission(repo=obj, user=user)
381 381
382 382 def _revoke_perm_group(obj, user_group):
383 383 if isinstance(obj, RepoGroup):
384 384 self.revoke_user_group_permission(
385 385 repo_group=obj, group_name=user_group)
386 386 elif isinstance(obj, Repository):
387 387 RepoModel().revoke_user_group_permission(
388 388 repo=obj, group_name=user_group)
389 389
390 390 # start updates
391 391 log.debug('Now updating permissions for %s in recursive mode:%s',
392 392 repo_group, recursive)
393 393
394 394 # initialize check function, we'll call that multiple times
395 395 has_group_perm = HasUserGroupPermissionAny(*req_perms)
396 396
397 397 for obj in repo_group.recursive_groups_and_repos():
398 398 # iterated obj is an instance of a repos group or repository in
399 399 # that group, recursive option can be: none, repos, groups, all
400 400 if recursive == 'all':
401 401 obj = obj
402 402 elif recursive == 'repos':
403 403 # skip groups, other than this one
404 404 if isinstance(obj, RepoGroup) and not obj == repo_group:
405 405 continue
406 406 elif recursive == 'groups':
407 407 # skip repos
408 408 if isinstance(obj, Repository):
409 409 continue
410 410 else: # recursive == 'none':
411 411 # DEFAULT option - don't apply to iterated objects
412 412 # also we do a break at the end of this loop. if we are not
413 413 # in recursive mode
414 414 obj = repo_group
415 415
416 416 change_obj = obj.get_api_data()
417 417
418 418 # update permissions
419 419 for member_id, perm, member_type in perm_updates:
420 420 member_id = int(member_id)
421 421 if member_type == 'user':
422 422 member_name = User.get(member_id).username
423 423 # this updates also current one if found
424 424 _set_perm_user(obj, user=member_id, perm=perm)
425 425 else: # set for user group
426 426 member_name = UserGroup.get(member_id).users_group_name
427 427 if not check_perms or has_group_perm(member_name,
428 428 user=cur_user):
429 429 _set_perm_group(obj, users_group=member_id, perm=perm)
430 430
431 431 changes['updated'].append(
432 432 {'change_obj': change_obj, 'type': member_type,
433 433 'id': member_id, 'name': member_name, 'new_perm': perm})
434 434
435 435 # set new permissions
436 436 for member_id, perm, member_type in perm_additions:
437 437 member_id = int(member_id)
438 438 if member_type == 'user':
439 439 member_name = User.get(member_id).username
440 440 _set_perm_user(obj, user=member_id, perm=perm)
441 441 else: # set for user group
442 442 # check if we have permissions to alter this usergroup
443 443 member_name = UserGroup.get(member_id).users_group_name
444 444 if not check_perms or has_group_perm(member_name,
445 445 user=cur_user):
446 446 _set_perm_group(obj, users_group=member_id, perm=perm)
447 447
448 448 changes['added'].append(
449 449 {'change_obj': change_obj, 'type': member_type,
450 450 'id': member_id, 'name': member_name, 'new_perm': perm})
451 451
452 452 # delete permissions
453 453 for member_id, perm, member_type in perm_deletions:
454 454 member_id = int(member_id)
455 455 if member_type == 'user':
456 456 member_name = User.get(member_id).username
457 457 _revoke_perm_user(obj, user=member_id)
458 458 else: # set for user group
459 459 # check if we have permissions to alter this usergroup
460 460 member_name = UserGroup.get(member_id).users_group_name
461 461 if not check_perms or has_group_perm(member_name,
462 462 user=cur_user):
463 463 _revoke_perm_group(obj, user_group=member_id)
464 464
465 465 changes['deleted'].append(
466 466 {'change_obj': change_obj, 'type': member_type,
467 467 'id': member_id, 'name': member_name, 'new_perm': perm})
468 468
469 469 # if it's not recursive call for all,repos,groups
470 470 # break the loop and don't proceed with other changes
471 471 if recursive not in ['all', 'repos', 'groups']:
472 472 break
473 473
474 474 return changes
475 475
476 476 def update(self, repo_group, form_data):
477 477 try:
478 478 repo_group = self._get_repo_group(repo_group)
479 479 old_path = repo_group.full_path
480 480
481 481 # change properties
482 482 if 'group_description' in form_data:
483 483 repo_group.group_description = form_data['group_description']
484 484
485 485 if 'enable_locking' in form_data:
486 486 repo_group.enable_locking = form_data['enable_locking']
487 487
488 488 if 'group_parent_id' in form_data:
489 489 parent_group = (
490 490 self._get_repo_group(form_data['group_parent_id']))
491 491 repo_group.group_parent_id = (
492 492 parent_group.group_id if parent_group else None)
493 493 repo_group.parent_group = parent_group
494 494
495 495 # mikhail: to update the full_path, we have to explicitly
496 496 # update group_name
497 497 group_name = form_data.get('group_name', repo_group.name)
498 498 repo_group.group_name = repo_group.get_new_name(group_name)
499 499
500 500 new_path = repo_group.full_path
501 501
502 502 if 'user' in form_data:
503 503 repo_group.user = User.get_by_username(form_data['user'])
504 504
505 505 self.sa.add(repo_group)
506 506
507 507 # iterate over all members of this groups and do fixes
508 508 # set locking if given
509 509 # if obj is a repoGroup also fix the name of the group according
510 510 # to the parent
511 511 # if obj is a Repo fix it's name
512 512 # this can be potentially heavy operation
513 513 for obj in repo_group.recursive_groups_and_repos():
514 514 # set the value from it's parent
515 515 obj.enable_locking = repo_group.enable_locking
516 516 if isinstance(obj, RepoGroup):
517 517 new_name = obj.get_new_name(obj.name)
518 518 log.debug('Fixing group %s to new name %s',
519 519 obj.group_name, new_name)
520 520 obj.group_name = new_name
521 521 elif isinstance(obj, Repository):
522 522 # we need to get all repositories from this new group and
523 523 # rename them accordingly to new group path
524 524 new_name = obj.get_new_name(obj.just_name)
525 525 log.debug('Fixing repo %s to new name %s',
526 526 obj.repo_name, new_name)
527 527 obj.repo_name = new_name
528 528 self.sa.add(obj)
529 529
530 530 self._rename_group(old_path, new_path)
531 531
532 532 # Trigger update event.
533 533 events.trigger(events.RepoGroupUpdateEvent(repo_group))
534 534
535 535 return repo_group
536 536 except Exception:
537 537 log.error(traceback.format_exc())
538 538 raise
539 539
540 540 def delete(self, repo_group, force_delete=False, fs_remove=True):
541 541 repo_group = self._get_repo_group(repo_group)
542 542 if not repo_group:
543 543 return False
544 544 try:
545 545 self.sa.delete(repo_group)
546 546 if fs_remove:
547 547 self._delete_filesystem_group(repo_group, force_delete)
548 548 else:
549 549 log.debug('skipping removal from filesystem')
550 550
551 551 # Trigger delete event.
552 552 events.trigger(events.RepoGroupDeleteEvent(repo_group))
553 553 return True
554 554
555 555 except Exception:
556 556 log.error('Error removing repo_group %s', repo_group)
557 557 raise
558 558
559 559 def grant_user_permission(self, repo_group, user, perm):
560 560 """
561 561 Grant permission for user on given repository group, or update
562 562 existing one if found
563 563
564 564 :param repo_group: Instance of RepoGroup, repositories_group_id,
565 565 or repositories_group name
566 566 :param user: Instance of User, user_id or username
567 567 :param perm: Instance of Permission, or permission_name
568 568 """
569 569
570 570 repo_group = self._get_repo_group(repo_group)
571 571 user = self._get_user(user)
572 572 permission = self._get_perm(perm)
573 573
574 574 # check if we have that permission already
575 575 obj = self.sa.query(UserRepoGroupToPerm)\
576 576 .filter(UserRepoGroupToPerm.user == user)\
577 577 .filter(UserRepoGroupToPerm.group == repo_group)\
578 578 .scalar()
579 579 if obj is None:
580 580 # create new !
581 581 obj = UserRepoGroupToPerm()
582 582 obj.group = repo_group
583 583 obj.user = user
584 584 obj.permission = permission
585 585 self.sa.add(obj)
586 586 log.debug('Granted perm %s to %s on %s', perm, user, repo_group)
587 587 action_logger_generic(
588 588 'granted permission: {} to user: {} on repogroup: {}'.format(
589 589 perm, user, repo_group), namespace='security.repogroup')
590 590 return obj
591 591
592 592 def revoke_user_permission(self, repo_group, user):
593 593 """
594 594 Revoke permission for user on given repository group
595 595
596 596 :param repo_group: Instance of RepoGroup, repositories_group_id,
597 597 or repositories_group name
598 598 :param user: Instance of User, user_id or username
599 599 """
600 600
601 601 repo_group = self._get_repo_group(repo_group)
602 602 user = self._get_user(user)
603 603
604 604 obj = self.sa.query(UserRepoGroupToPerm)\
605 605 .filter(UserRepoGroupToPerm.user == user)\
606 606 .filter(UserRepoGroupToPerm.group == repo_group)\
607 607 .scalar()
608 608 if obj:
609 609 self.sa.delete(obj)
610 610 log.debug('Revoked perm on %s on %s', repo_group, user)
611 611 action_logger_generic(
612 612 'revoked permission from user: {} on repogroup: {}'.format(
613 613 user, repo_group), namespace='security.repogroup')
614 614
615 615 def grant_user_group_permission(self, repo_group, group_name, perm):
616 616 """
617 617 Grant permission for user group on given repository group, or update
618 618 existing one if found
619 619
620 620 :param repo_group: Instance of RepoGroup, repositories_group_id,
621 621 or repositories_group name
622 622 :param group_name: Instance of UserGroup, users_group_id,
623 623 or user group name
624 624 :param perm: Instance of Permission, or permission_name
625 625 """
626 626 repo_group = self._get_repo_group(repo_group)
627 627 group_name = self._get_user_group(group_name)
628 628 permission = self._get_perm(perm)
629 629
630 630 # check if we have that permission already
631 631 obj = self.sa.query(UserGroupRepoGroupToPerm)\
632 632 .filter(UserGroupRepoGroupToPerm.group == repo_group)\
633 633 .filter(UserGroupRepoGroupToPerm.users_group == group_name)\
634 634 .scalar()
635 635
636 636 if obj is None:
637 637 # create new
638 638 obj = UserGroupRepoGroupToPerm()
639 639
640 640 obj.group = repo_group
641 641 obj.users_group = group_name
642 642 obj.permission = permission
643 643 self.sa.add(obj)
644 644 log.debug('Granted perm %s to %s on %s', perm, group_name, repo_group)
645 645 action_logger_generic(
646 646 'granted permission: {} to usergroup: {} on repogroup: {}'.format(
647 647 perm, group_name, repo_group), namespace='security.repogroup')
648 648 return obj
649 649
650 650 def revoke_user_group_permission(self, repo_group, group_name):
651 651 """
652 652 Revoke permission for user group on given repository group
653 653
654 654 :param repo_group: Instance of RepoGroup, repositories_group_id,
655 655 or repositories_group name
656 656 :param group_name: Instance of UserGroup, users_group_id,
657 657 or user group name
658 658 """
659 659 repo_group = self._get_repo_group(repo_group)
660 660 group_name = self._get_user_group(group_name)
661 661
662 662 obj = self.sa.query(UserGroupRepoGroupToPerm)\
663 663 .filter(UserGroupRepoGroupToPerm.group == repo_group)\
664 664 .filter(UserGroupRepoGroupToPerm.users_group == group_name)\
665 665 .scalar()
666 666 if obj:
667 667 self.sa.delete(obj)
668 668 log.debug('Revoked perm to %s on %s', repo_group, group_name)
669 669 action_logger_generic(
670 670 'revoked permission from usergroup: {} on repogroup: {}'.format(
671 671 group_name, repo_group), namespace='security.repogroup')
672 672
673 673 def get_repo_groups_as_dict(self, repo_group_list=None, admin=False,
674 674 super_user_actions=False):
675 675
676 from rhodecode.lib.utils import PartialRenderer
677 _render = PartialRenderer('data_table/_dt_elements.mako')
678 c = _render.c
679 h = _render.h
676 from pyramid.threadlocal import get_current_request
677 _render = get_current_request().get_partial_renderer(
678 'data_table/_dt_elements.mako')
679 c = _render.get_call_context()
680 h = _render.get_helpers()
680 681
681 682 def quick_menu(repo_group_name):
682 683 return _render('quick_repo_group_menu', repo_group_name)
683 684
684 685 def repo_group_lnk(repo_group_name):
685 686 return _render('repo_group_name', repo_group_name)
686 687
687 688 def desc(desc, personal):
688 689 prefix = h.escaped_stylize(u'[personal] ') if personal else ''
689 690
690 691 if c.visual.stylify_metatags:
691 692 desc = h.urlify_text(prefix + h.escaped_stylize(desc))
692 693 else:
693 694 desc = h.urlify_text(prefix + h.html_escape(desc))
694 695
695 696 return _render('repo_group_desc', desc)
696 697
697 698 def repo_group_actions(repo_group_id, repo_group_name, gr_count):
698 699 return _render(
699 700 'repo_group_actions', repo_group_id, repo_group_name, gr_count)
700 701
701 702 def repo_group_name(repo_group_name, children_groups):
702 703 return _render("repo_group_name", repo_group_name, children_groups)
703 704
704 705 def user_profile(username):
705 706 return _render('user_profile', username)
706 707
707 708 repo_group_data = []
708 709 for group in repo_group_list:
709 710
710 711 row = {
711 712 "menu": quick_menu(group.group_name),
712 713 "name": repo_group_lnk(group.group_name),
713 714 "name_raw": group.group_name,
714 715 "desc": desc(group.description_safe, group.personal),
715 716 "top_level_repos": 0,
716 717 "owner": user_profile(group.user.username)
717 718 }
718 719 if admin:
719 720 repo_count = group.repositories.count()
720 721 children_groups = map(
721 722 h.safe_unicode,
722 723 itertools.chain((g.name for g in group.parents),
723 724 (x.name for x in [group])))
724 725 row.update({
725 726 "action": repo_group_actions(
726 727 group.group_id, group.group_name, repo_count),
727 728 "top_level_repos": repo_count,
728 729 "name": repo_group_name(group.group_name, children_groups),
729 730
730 731 })
731 732 repo_group_data.append(row)
732 733
733 734 return repo_group_data
General Comments 0
You need to be logged in to leave comments. Login now