##// END OF EJS Templates
tests: added tests for permission update views to catch obvious form errors.
marcink -
r2827:e2835069 default
parent child Browse files
Show More
@@ -0,0 +1,77 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import pytest
22
23 from rhodecode.tests.utils import permission_update_data_generator
24
25
26 def route_path(name, params=None, **kwargs):
27 import urllib
28
29 base_url = {
30 'edit_repo_perms': '/{repo_name}/settings/permissions'
31 # update is the same url
32 }[name].format(**kwargs)
33
34 if params:
35 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
36 return base_url
37
38
39 @pytest.mark.usefixtures("app")
40 class TestRepoPermissionsView(object):
41
42 def test_edit_perms_view(self, user_util, autologin_user):
43 repo = user_util.create_repo()
44 self.app.get(
45 route_path('edit_repo_perms',
46 repo_name=repo.repo_name), status=200)
47
48 def test_update_permissions(self, csrf_token, user_util):
49 repo = user_util.create_repo()
50 repo_name = repo.repo_name
51 user = user_util.create_user()
52 user_id = user.user_id
53 username = user.username
54
55 # grant new
56 form_data = permission_update_data_generator(
57 csrf_token,
58 default='repository.write',
59 grant=[(user_id, 'repository.write', username, 'user')])
60
61 response = self.app.post(
62 route_path('edit_repo_perms',
63 repo_name=repo_name), form_data).follow()
64
65 assert 'Repository permissions updated' in response
66
67 # revoke given
68 form_data = permission_update_data_generator(
69 csrf_token,
70 default='repository.read',
71 revoke=[(user_id, 'user')])
72
73 response = self.app.post(
74 route_path('edit_repo_perms',
75 repo_name=repo_name), form_data).follow()
76
77 assert 'Repository permissions updated' in response
@@ -0,0 +1,80 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import pytest
22
23 from rhodecode.tests.utils import permission_update_data_generator
24
25
26 def route_path(name, params=None, **kwargs):
27 import urllib
28 from rhodecode.apps._base import ADMIN_PREFIX
29
30 base_url = {
31 'edit_user_group_perms':
32 ADMIN_PREFIX + '/user_groups/{user_group_id}/edit/permissions',
33 'edit_user_group_perms_update':
34 ADMIN_PREFIX + '/user_groups/{user_group_id}/edit/permissions/update',
35 }[name].format(**kwargs)
36
37 if params:
38 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
39 return base_url
40
41
42 @pytest.mark.usefixtures("app")
43 class TestUserGroupPermissionsView(object):
44
45 def test_edit_perms_view(self, user_util, autologin_user):
46 user_group = user_util.create_user_group()
47 self.app.get(
48 route_path('edit_user_group_perms',
49 user_group_id=user_group.users_group_id), status=200)
50
51 def test_update_permissions(self, csrf_token, user_util):
52 user_group = user_util.create_user_group()
53 user_group_id = user_group.users_group_id
54 user = user_util.create_user()
55 user_id = user.user_id
56 username = user.username
57
58 # grant new
59 form_data = permission_update_data_generator(
60 csrf_token,
61 default='usergroup.write',
62 grant=[(user_id, 'usergroup.write', username, 'user')])
63
64 response = self.app.post(
65 route_path('edit_user_group_perms_update',
66 user_group_id=user_group_id), form_data).follow()
67
68 assert 'User Group permissions updated' in response
69
70 # revoke given
71 form_data = permission_update_data_generator(
72 csrf_token,
73 default='usergroup.read',
74 revoke=[(user_id, 'user')])
75
76 response = self.app.post(
77 route_path('edit_user_group_perms_update',
78 user_group_id=user_group_id), form_data).follow()
79
80 assert 'User Group permissions updated' in response
@@ -1,49 +1,86 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 from rhodecode.tests.utils import permission_update_data_generator
24
23 25
24 26 def route_path(name, params=None, **kwargs):
25 27 import urllib
26 28
27 29 base_url = {
28 30 'edit_repo_group_perms':
29 31 '/{repo_group_name:}/_settings/permissions',
30 32 'edit_repo_group_perms_update':
31 33 '/{repo_group_name}/_settings/permissions/update',
32 34 }[name].format(**kwargs)
33 35
34 36 if params:
35 37 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
36 38 return base_url
37 39
38 40
39 41 @pytest.mark.usefixtures("app")
40 class TestRepoGroupsPermissionsView(object):
42 class TestRepoGroupPermissionsView(object):
41 43
42 def test_edit_repo_group_perms(self, user_util, autologin_user):
44 def test_edit_perms_view(self, user_util, autologin_user):
43 45 repo_group = user_util.create_repo_group()
46
44 47 self.app.get(
45 48 route_path('edit_repo_group_perms',
46 49 repo_group_name=repo_group.group_name), status=200)
47 50
48 def test_update_permissions(self):
49 pass
51 def test_update_permissions(self, csrf_token, user_util):
52 repo_group = user_util.create_repo_group()
53 repo_group_name = repo_group.group_name
54 user = user_util.create_user()
55 user_id = user.user_id
56 username = user.username
57
58 # grant new
59 form_data = permission_update_data_generator(
60 csrf_token,
61 default='group.write',
62 grant=[(user_id, 'group.write', username, 'user')])
63
64 # recursive flag required for repo groups
65 form_data.extend([('recursive', u'none')])
66
67 response = self.app.post(
68 route_path('edit_repo_group_perms_update',
69 repo_group_name=repo_group_name), form_data).follow()
70
71 assert 'Repository Group permissions updated' in response
72
73 # revoke given
74 form_data = permission_update_data_generator(
75 csrf_token,
76 default='group.read',
77 revoke=[(user_id, 'user')])
78
79 # recursive flag required for repo groups
80 form_data.extend([('recursive', u'none')])
81
82 response = self.app.post(
83 route_path('edit_repo_group_perms_update',
84 repo_group_name=repo_group_name), form_data).follow()
85
86 assert 'Repository Group permissions updated' in response
@@ -1,1041 +1,1050 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 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 os
22 22 import re
23 23 import shutil
24 24 import time
25 25 import logging
26 26 import traceback
27 27 import datetime
28 28
29 29 from pyramid.threadlocal import get_current_request
30 30 from zope.cachedescriptors.property import Lazy as LazyProperty
31 31
32 32 from rhodecode import events
33 33 from rhodecode.lib.auth import HasUserGroupPermissionAny
34 34 from rhodecode.lib.caching_query import FromCache
35 35 from rhodecode.lib.exceptions import AttachedForksError
36 36 from rhodecode.lib.hooks_base import log_delete_repository
37 37 from rhodecode.lib.user_log_filter import user_log_filter
38 38 from rhodecode.lib.utils import make_db_config
39 39 from rhodecode.lib.utils2 import (
40 40 safe_str, safe_unicode, remove_prefix, obfuscate_url_pw,
41 41 get_current_rhodecode_user, safe_int, datetime_to_time,
42 42 action_logger_generic)
43 43 from rhodecode.lib.vcs.backends import get_backend
44 44 from rhodecode.model import BaseModel
45 45 from rhodecode.model.db import (
46 46 _hash_key, joinedload, or_, Repository, UserRepoToPerm, UserGroupRepoToPerm,
47 47 UserRepoGroupToPerm, UserGroupRepoGroupToPerm, User, Permission,
48 48 Statistics, UserGroup, RepoGroup, RepositoryField, UserLog)
49 49
50 50 from rhodecode.model.settings import VcsSettingsModel
51 51
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55
56 56 class RepoModel(BaseModel):
57 57
58 58 cls = Repository
59 59
60 60 def _get_user_group(self, users_group):
61 61 return self._get_instance(UserGroup, users_group,
62 62 callback=UserGroup.get_by_group_name)
63 63
64 64 def _get_repo_group(self, repo_group):
65 65 return self._get_instance(RepoGroup, repo_group,
66 66 callback=RepoGroup.get_by_group_name)
67 67
68 68 def _create_default_perms(self, repository, private):
69 69 # create default permission
70 70 default = 'repository.read'
71 71 def_user = User.get_default_user()
72 72 for p in def_user.user_perms:
73 73 if p.permission.permission_name.startswith('repository.'):
74 74 default = p.permission.permission_name
75 75 break
76 76
77 77 default_perm = 'repository.none' if private else default
78 78
79 79 repo_to_perm = UserRepoToPerm()
80 80 repo_to_perm.permission = Permission.get_by_key(default_perm)
81 81
82 82 repo_to_perm.repository = repository
83 83 repo_to_perm.user_id = def_user.user_id
84 84
85 85 return repo_to_perm
86 86
87 87 @LazyProperty
88 88 def repos_path(self):
89 89 """
90 90 Gets the repositories root path from database
91 91 """
92 92 settings_model = VcsSettingsModel(sa=self.sa)
93 93 return settings_model.get_repos_location()
94 94
95 95 def get(self, repo_id, cache=False):
96 96 repo = self.sa.query(Repository) \
97 97 .filter(Repository.repo_id == repo_id)
98 98
99 99 if cache:
100 100 repo = repo.options(
101 101 FromCache("sql_cache_short", "get_repo_%s" % repo_id))
102 102 return repo.scalar()
103 103
104 104 def get_repo(self, repository):
105 105 return self._get_repo(repository)
106 106
107 107 def get_by_repo_name(self, repo_name, cache=False):
108 108 repo = self.sa.query(Repository) \
109 109 .filter(Repository.repo_name == repo_name)
110 110
111 111 if cache:
112 112 name_key = _hash_key(repo_name)
113 113 repo = repo.options(
114 114 FromCache("sql_cache_short", "get_repo_%s" % name_key))
115 115 return repo.scalar()
116 116
117 117 def _extract_id_from_repo_name(self, repo_name):
118 118 if repo_name.startswith('/'):
119 119 repo_name = repo_name.lstrip('/')
120 120 by_id_match = re.match(r'^_(\d{1,})', repo_name)
121 121 if by_id_match:
122 122 return by_id_match.groups()[0]
123 123
124 124 def get_repo_by_id(self, repo_name):
125 125 """
126 126 Extracts repo_name by id from special urls.
127 127 Example url is _11/repo_name
128 128
129 129 :param repo_name:
130 130 :return: repo object if matched else None
131 131 """
132 132
133 133 try:
134 134 _repo_id = self._extract_id_from_repo_name(repo_name)
135 135 if _repo_id:
136 136 return self.get(_repo_id)
137 137 except Exception:
138 138 log.exception('Failed to extract repo_name from URL')
139 139
140 140 return None
141 141
142 142 def get_repos_for_root(self, root, traverse=False):
143 143 if traverse:
144 144 like_expression = u'{}%'.format(safe_unicode(root))
145 145 repos = Repository.query().filter(
146 146 Repository.repo_name.like(like_expression)).all()
147 147 else:
148 148 if root and not isinstance(root, RepoGroup):
149 149 raise ValueError(
150 150 'Root must be an instance '
151 151 'of RepoGroup, got:{} instead'.format(type(root)))
152 152 repos = Repository.query().filter(Repository.group == root).all()
153 153 return repos
154 154
155 155 def get_url(self, repo, request=None, permalink=False):
156 156 if not request:
157 157 request = get_current_request()
158 158
159 159 if not request:
160 160 return
161 161
162 162 if permalink:
163 163 return request.route_url(
164 164 'repo_summary', repo_name='_{}'.format(safe_str(repo.repo_id)))
165 165 else:
166 166 return request.route_url(
167 167 'repo_summary', repo_name=safe_str(repo.repo_name))
168 168
169 169 def get_commit_url(self, repo, commit_id, request=None, permalink=False):
170 170 if not request:
171 171 request = get_current_request()
172 172
173 173 if not request:
174 174 return
175 175
176 176 if permalink:
177 177 return request.route_url(
178 178 'repo_commit', repo_name=safe_str(repo.repo_id),
179 179 commit_id=commit_id)
180 180
181 181 else:
182 182 return request.route_url(
183 183 'repo_commit', repo_name=safe_str(repo.repo_name),
184 184 commit_id=commit_id)
185 185
186 186 def get_repo_log(self, repo, filter_term):
187 187 repo_log = UserLog.query()\
188 188 .filter(or_(UserLog.repository_id == repo.repo_id,
189 189 UserLog.repository_name == repo.repo_name))\
190 190 .options(joinedload(UserLog.user))\
191 191 .options(joinedload(UserLog.repository))\
192 192 .order_by(UserLog.action_date.desc())
193 193
194 194 repo_log = user_log_filter(repo_log, filter_term)
195 195 return repo_log
196 196
197 197 @classmethod
198 198 def update_repoinfo(cls, repositories=None):
199 199 if not repositories:
200 200 repositories = Repository.getAll()
201 201 for repo in repositories:
202 202 repo.update_commit_cache()
203 203
204 204 def get_repos_as_dict(self, repo_list=None, admin=False,
205 205 super_user_actions=False):
206 206 _render = get_current_request().get_partial_renderer(
207 207 'rhodecode:templates/data_table/_dt_elements.mako')
208 208 c = _render.get_call_context()
209 209
210 210 def quick_menu(repo_name):
211 211 return _render('quick_menu', repo_name)
212 212
213 213 def repo_lnk(name, rtype, rstate, private, fork_of):
214 214 return _render('repo_name', name, rtype, rstate, private, fork_of,
215 215 short_name=not admin, admin=False)
216 216
217 217 def last_change(last_change):
218 218 if admin and isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
219 219 last_change = last_change + datetime.timedelta(seconds=
220 220 (datetime.datetime.now() - datetime.datetime.utcnow()).seconds)
221 221 return _render("last_change", last_change)
222 222
223 223 def rss_lnk(repo_name):
224 224 return _render("rss", repo_name)
225 225
226 226 def atom_lnk(repo_name):
227 227 return _render("atom", repo_name)
228 228
229 229 def last_rev(repo_name, cs_cache):
230 230 return _render('revision', repo_name, cs_cache.get('revision'),
231 231 cs_cache.get('raw_id'), cs_cache.get('author'),
232 232 cs_cache.get('message'), cs_cache.get('date'))
233 233
234 234 def desc(desc):
235 235 return _render('repo_desc', desc, c.visual.stylify_metatags)
236 236
237 237 def state(repo_state):
238 238 return _render("repo_state", repo_state)
239 239
240 240 def repo_actions(repo_name):
241 241 return _render('repo_actions', repo_name, super_user_actions)
242 242
243 243 def user_profile(username):
244 244 return _render('user_profile', username)
245 245
246 246 repos_data = []
247 247 for repo in repo_list:
248 248 cs_cache = repo.changeset_cache
249 249 row = {
250 250 "menu": quick_menu(repo.repo_name),
251 251
252 252 "name": repo_lnk(repo.repo_name, repo.repo_type,
253 253 repo.repo_state, repo.private, repo.fork),
254 254 "name_raw": repo.repo_name.lower(),
255 255
256 256 "last_change": last_change(repo.last_db_change),
257 257 "last_change_raw": datetime_to_time(repo.last_db_change),
258 258
259 259 "last_changeset": last_rev(repo.repo_name, cs_cache),
260 260 "last_changeset_raw": cs_cache.get('revision'),
261 261
262 262 "desc": desc(repo.description_safe),
263 263 "owner": user_profile(repo.user.username),
264 264
265 265 "state": state(repo.repo_state),
266 266 "rss": rss_lnk(repo.repo_name),
267 267
268 268 "atom": atom_lnk(repo.repo_name),
269 269 }
270 270 if admin:
271 271 row.update({
272 272 "action": repo_actions(repo.repo_name),
273 273 })
274 274 repos_data.append(row)
275 275
276 276 return repos_data
277 277
278 278 def _get_defaults(self, repo_name):
279 279 """
280 280 Gets information about repository, and returns a dict for
281 281 usage in forms
282 282
283 283 :param repo_name:
284 284 """
285 285
286 286 repo_info = Repository.get_by_repo_name(repo_name)
287 287
288 288 if repo_info is None:
289 289 return None
290 290
291 291 defaults = repo_info.get_dict()
292 292 defaults['repo_name'] = repo_info.just_name
293 293
294 294 groups = repo_info.groups_with_parents
295 295 parent_group = groups[-1] if groups else None
296 296
297 297 # we use -1 as this is how in HTML, we mark an empty group
298 298 defaults['repo_group'] = getattr(parent_group, 'group_id', -1)
299 299
300 300 keys_to_process = (
301 301 {'k': 'repo_type', 'strip': False},
302 302 {'k': 'repo_enable_downloads', 'strip': True},
303 303 {'k': 'repo_description', 'strip': True},
304 304 {'k': 'repo_enable_locking', 'strip': True},
305 305 {'k': 'repo_landing_rev', 'strip': True},
306 306 {'k': 'clone_uri', 'strip': False},
307 307 {'k': 'push_uri', 'strip': False},
308 308 {'k': 'repo_private', 'strip': True},
309 309 {'k': 'repo_enable_statistics', 'strip': True}
310 310 )
311 311
312 312 for item in keys_to_process:
313 313 attr = item['k']
314 314 if item['strip']:
315 315 attr = remove_prefix(item['k'], 'repo_')
316 316
317 317 val = defaults[attr]
318 318 if item['k'] == 'repo_landing_rev':
319 319 val = ':'.join(defaults[attr])
320 320 defaults[item['k']] = val
321 321 if item['k'] == 'clone_uri':
322 322 defaults['clone_uri_hidden'] = repo_info.clone_uri_hidden
323 323 if item['k'] == 'push_uri':
324 324 defaults['push_uri_hidden'] = repo_info.push_uri_hidden
325 325
326 326 # fill owner
327 327 if repo_info.user:
328 328 defaults.update({'user': repo_info.user.username})
329 329 else:
330 330 replacement_user = User.get_first_super_admin().username
331 331 defaults.update({'user': replacement_user})
332 332
333 333 return defaults
334 334
335 335 def update(self, repo, **kwargs):
336 336 try:
337 337 cur_repo = self._get_repo(repo)
338 338 source_repo_name = cur_repo.repo_name
339 339 if 'user' in kwargs:
340 340 cur_repo.user = User.get_by_username(kwargs['user'])
341 341
342 342 if 'repo_group' in kwargs:
343 343 cur_repo.group = RepoGroup.get(kwargs['repo_group'])
344 344 log.debug('Updating repo %s with params:%s', cur_repo, kwargs)
345 345
346 346 update_keys = [
347 347 (1, 'repo_description'),
348 348 (1, 'repo_landing_rev'),
349 349 (1, 'repo_private'),
350 350 (1, 'repo_enable_downloads'),
351 351 (1, 'repo_enable_locking'),
352 352 (1, 'repo_enable_statistics'),
353 353 (0, 'clone_uri'),
354 354 (0, 'push_uri'),
355 355 (0, 'fork_id')
356 356 ]
357 357 for strip, k in update_keys:
358 358 if k in kwargs:
359 359 val = kwargs[k]
360 360 if strip:
361 361 k = remove_prefix(k, 'repo_')
362 362
363 363 setattr(cur_repo, k, val)
364 364
365 365 new_name = cur_repo.get_new_name(kwargs['repo_name'])
366 366 cur_repo.repo_name = new_name
367 367
368 368 # if private flag is set, reset default permission to NONE
369 369 if kwargs.get('repo_private'):
370 370 EMPTY_PERM = 'repository.none'
371 371 RepoModel().grant_user_permission(
372 372 repo=cur_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM
373 373 )
374 374
375 375 # handle extra fields
376 376 for field in filter(lambda k: k.startswith(RepositoryField.PREFIX),
377 377 kwargs):
378 378 k = RepositoryField.un_prefix_key(field)
379 379 ex_field = RepositoryField.get_by_key_name(
380 380 key=k, repo=cur_repo)
381 381 if ex_field:
382 382 ex_field.field_value = kwargs[field]
383 383 self.sa.add(ex_field)
384 384 cur_repo.updated_on = datetime.datetime.now()
385 385 self.sa.add(cur_repo)
386 386
387 387 if source_repo_name != new_name:
388 388 # rename repository
389 389 self._rename_filesystem_repo(
390 390 old=source_repo_name, new=new_name)
391 391
392 392 return cur_repo
393 393 except Exception:
394 394 log.error(traceback.format_exc())
395 395 raise
396 396
397 397 def _create_repo(self, repo_name, repo_type, description, owner,
398 398 private=False, clone_uri=None, repo_group=None,
399 399 landing_rev='rev:tip', fork_of=None,
400 400 copy_fork_permissions=False, enable_statistics=False,
401 401 enable_locking=False, enable_downloads=False,
402 402 copy_group_permissions=False,
403 403 state=Repository.STATE_PENDING):
404 404 """
405 405 Create repository inside database with PENDING state, this should be
406 406 only executed by create() repo. With exception of importing existing
407 407 repos
408 408 """
409 409 from rhodecode.model.scm import ScmModel
410 410
411 411 owner = self._get_user(owner)
412 412 fork_of = self._get_repo(fork_of)
413 413 repo_group = self._get_repo_group(safe_int(repo_group))
414 414
415 415 try:
416 416 repo_name = safe_unicode(repo_name)
417 417 description = safe_unicode(description)
418 418 # repo name is just a name of repository
419 419 # while repo_name_full is a full qualified name that is combined
420 420 # with name and path of group
421 421 repo_name_full = repo_name
422 422 repo_name = repo_name.split(Repository.NAME_SEP)[-1]
423 423
424 424 new_repo = Repository()
425 425 new_repo.repo_state = state
426 426 new_repo.enable_statistics = False
427 427 new_repo.repo_name = repo_name_full
428 428 new_repo.repo_type = repo_type
429 429 new_repo.user = owner
430 430 new_repo.group = repo_group
431 431 new_repo.description = description or repo_name
432 432 new_repo.private = private
433 433 new_repo.clone_uri = clone_uri
434 434 new_repo.landing_rev = landing_rev
435 435
436 436 new_repo.enable_statistics = enable_statistics
437 437 new_repo.enable_locking = enable_locking
438 438 new_repo.enable_downloads = enable_downloads
439 439
440 440 if repo_group:
441 441 new_repo.enable_locking = repo_group.enable_locking
442 442
443 443 if fork_of:
444 444 parent_repo = fork_of
445 445 new_repo.fork = parent_repo
446 446
447 447 events.trigger(events.RepoPreCreateEvent(new_repo))
448 448
449 449 self.sa.add(new_repo)
450 450
451 451 EMPTY_PERM = 'repository.none'
452 452 if fork_of and copy_fork_permissions:
453 453 repo = fork_of
454 454 user_perms = UserRepoToPerm.query() \
455 455 .filter(UserRepoToPerm.repository == repo).all()
456 456 group_perms = UserGroupRepoToPerm.query() \
457 457 .filter(UserGroupRepoToPerm.repository == repo).all()
458 458
459 459 for perm in user_perms:
460 460 UserRepoToPerm.create(
461 461 perm.user, new_repo, perm.permission)
462 462
463 463 for perm in group_perms:
464 464 UserGroupRepoToPerm.create(
465 465 perm.users_group, new_repo, perm.permission)
466 466 # in case we copy permissions and also set this repo to private
467 467 # override the default user permission to make it a private
468 468 # repo
469 469 if private:
470 470 RepoModel(self.sa).grant_user_permission(
471 471 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
472 472
473 473 elif repo_group and copy_group_permissions:
474 474 user_perms = UserRepoGroupToPerm.query() \
475 475 .filter(UserRepoGroupToPerm.group == repo_group).all()
476 476
477 477 group_perms = UserGroupRepoGroupToPerm.query() \
478 478 .filter(UserGroupRepoGroupToPerm.group == repo_group).all()
479 479
480 480 for perm in user_perms:
481 481 perm_name = perm.permission.permission_name.replace(
482 482 'group.', 'repository.')
483 483 perm_obj = Permission.get_by_key(perm_name)
484 484 UserRepoToPerm.create(perm.user, new_repo, perm_obj)
485 485
486 486 for perm in group_perms:
487 487 perm_name = perm.permission.permission_name.replace(
488 488 'group.', 'repository.')
489 489 perm_obj = Permission.get_by_key(perm_name)
490 490 UserGroupRepoToPerm.create(
491 491 perm.users_group, new_repo, perm_obj)
492 492
493 493 if private:
494 494 RepoModel(self.sa).grant_user_permission(
495 495 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
496 496
497 497 else:
498 498 perm_obj = self._create_default_perms(new_repo, private)
499 499 self.sa.add(perm_obj)
500 500
501 501 # now automatically start following this repository as owner
502 502 ScmModel(self.sa).toggle_following_repo(new_repo.repo_id,
503 503 owner.user_id)
504 504
505 505 # we need to flush here, in order to check if database won't
506 506 # throw any exceptions, create filesystem dirs at the very end
507 507 self.sa.flush()
508 508 events.trigger(events.RepoCreateEvent(new_repo))
509 509 return new_repo
510 510
511 511 except Exception:
512 512 log.error(traceback.format_exc())
513 513 raise
514 514
515 515 def create(self, form_data, cur_user):
516 516 """
517 517 Create repository using celery tasks
518 518
519 519 :param form_data:
520 520 :param cur_user:
521 521 """
522 522 from rhodecode.lib.celerylib import tasks, run_task
523 523 return run_task(tasks.create_repo, form_data, cur_user)
524 524
525 525 def update_permissions(self, repo, perm_additions=None, perm_updates=None,
526 526 perm_deletions=None, check_perms=True,
527 527 cur_user=None):
528 528 if not perm_additions:
529 529 perm_additions = []
530 530 if not perm_updates:
531 531 perm_updates = []
532 532 if not perm_deletions:
533 533 perm_deletions = []
534 534
535 535 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
536 536
537 537 changes = {
538 538 'added': [],
539 539 'updated': [],
540 540 'deleted': []
541 541 }
542 542 # update permissions
543 543 for member_id, perm, member_type in perm_updates:
544 544 member_id = int(member_id)
545 545 if member_type == 'user':
546 546 member_name = User.get(member_id).username
547 547 # this updates also current one if found
548 548 self.grant_user_permission(
549 549 repo=repo, user=member_id, perm=perm)
550 else: # set for user group
550 elif member_type == 'user_group':
551 551 # check if we have permissions to alter this usergroup
552 552 member_name = UserGroup.get(member_id).users_group_name
553 553 if not check_perms or HasUserGroupPermissionAny(
554 554 *req_perms)(member_name, user=cur_user):
555 555 self.grant_user_group_permission(
556 556 repo=repo, group_name=member_id, perm=perm)
557
557 else:
558 raise ValueError("member_type must be 'user' or 'user_group' "
559 "got {} instead".format(member_type))
558 560 changes['updated'].append({'type': member_type, 'id': member_id,
559 561 'name': member_name, 'new_perm': perm})
560 562
561 563 # set new permissions
562 564 for member_id, perm, member_type in perm_additions:
563 565 member_id = int(member_id)
564 566 if member_type == 'user':
565 567 member_name = User.get(member_id).username
566 568 self.grant_user_permission(
567 569 repo=repo, user=member_id, perm=perm)
568 else: # set for user group
570 elif member_type == 'user_group':
569 571 # check if we have permissions to alter this usergroup
570 572 member_name = UserGroup.get(member_id).users_group_name
571 573 if not check_perms or HasUserGroupPermissionAny(
572 574 *req_perms)(member_name, user=cur_user):
573 575 self.grant_user_group_permission(
574 576 repo=repo, group_name=member_id, perm=perm)
577 else:
578 raise ValueError("member_type must be 'user' or 'user_group' "
579 "got {} instead".format(member_type))
580
575 581 changes['added'].append({'type': member_type, 'id': member_id,
576 582 'name': member_name, 'new_perm': perm})
577 583 # delete permissions
578 584 for member_id, perm, member_type in perm_deletions:
579 585 member_id = int(member_id)
580 586 if member_type == 'user':
581 587 member_name = User.get(member_id).username
582 588 self.revoke_user_permission(repo=repo, user=member_id)
583 else: # set for user group
589 elif member_type == 'user_group':
584 590 # check if we have permissions to alter this usergroup
585 591 member_name = UserGroup.get(member_id).users_group_name
586 592 if not check_perms or HasUserGroupPermissionAny(
587 593 *req_perms)(member_name, user=cur_user):
588 594 self.revoke_user_group_permission(
589 595 repo=repo, group_name=member_id)
596 else:
597 raise ValueError("member_type must be 'user' or 'user_group' "
598 "got {} instead".format(member_type))
590 599
591 600 changes['deleted'].append({'type': member_type, 'id': member_id,
592 601 'name': member_name, 'new_perm': perm})
593 602 return changes
594 603
595 604 def create_fork(self, form_data, cur_user):
596 605 """
597 606 Simple wrapper into executing celery task for fork creation
598 607
599 608 :param form_data:
600 609 :param cur_user:
601 610 """
602 611 from rhodecode.lib.celerylib import tasks, run_task
603 612 return run_task(tasks.create_repo_fork, form_data, cur_user)
604 613
605 614 def delete(self, repo, forks=None, fs_remove=True, cur_user=None):
606 615 """
607 616 Delete given repository, forks parameter defines what do do with
608 617 attached forks. Throws AttachedForksError if deleted repo has attached
609 618 forks
610 619
611 620 :param repo:
612 621 :param forks: str 'delete' or 'detach'
613 622 :param fs_remove: remove(archive) repo from filesystem
614 623 """
615 624 if not cur_user:
616 625 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
617 626 repo = self._get_repo(repo)
618 627 if repo:
619 628 if forks == 'detach':
620 629 for r in repo.forks:
621 630 r.fork = None
622 631 self.sa.add(r)
623 632 elif forks == 'delete':
624 633 for r in repo.forks:
625 634 self.delete(r, forks='delete')
626 635 elif [f for f in repo.forks]:
627 636 raise AttachedForksError()
628 637
629 638 old_repo_dict = repo.get_dict()
630 639 events.trigger(events.RepoPreDeleteEvent(repo))
631 640 try:
632 641 self.sa.delete(repo)
633 642 if fs_remove:
634 643 self._delete_filesystem_repo(repo)
635 644 else:
636 645 log.debug('skipping removal from filesystem')
637 646 old_repo_dict.update({
638 647 'deleted_by': cur_user,
639 648 'deleted_on': time.time(),
640 649 })
641 650 log_delete_repository(**old_repo_dict)
642 651 events.trigger(events.RepoDeleteEvent(repo))
643 652 except Exception:
644 653 log.error(traceback.format_exc())
645 654 raise
646 655
647 656 def grant_user_permission(self, repo, user, perm):
648 657 """
649 658 Grant permission for user on given repository, or update existing one
650 659 if found
651 660
652 661 :param repo: Instance of Repository, repository_id, or repository name
653 662 :param user: Instance of User, user_id or username
654 663 :param perm: Instance of Permission, or permission_name
655 664 """
656 665 user = self._get_user(user)
657 666 repo = self._get_repo(repo)
658 667 permission = self._get_perm(perm)
659 668
660 669 # check if we have that permission already
661 670 obj = self.sa.query(UserRepoToPerm) \
662 671 .filter(UserRepoToPerm.user == user) \
663 672 .filter(UserRepoToPerm.repository == repo) \
664 673 .scalar()
665 674 if obj is None:
666 675 # create new !
667 676 obj = UserRepoToPerm()
668 677 obj.repository = repo
669 678 obj.user = user
670 679 obj.permission = permission
671 680 self.sa.add(obj)
672 681 log.debug('Granted perm %s to %s on %s', perm, user, repo)
673 682 action_logger_generic(
674 683 'granted permission: {} to user: {} on repo: {}'.format(
675 684 perm, user, repo), namespace='security.repo')
676 685 return obj
677 686
678 687 def revoke_user_permission(self, repo, user):
679 688 """
680 689 Revoke permission for user on given repository
681 690
682 691 :param repo: Instance of Repository, repository_id, or repository name
683 692 :param user: Instance of User, user_id or username
684 693 """
685 694
686 695 user = self._get_user(user)
687 696 repo = self._get_repo(repo)
688 697
689 698 obj = self.sa.query(UserRepoToPerm) \
690 699 .filter(UserRepoToPerm.repository == repo) \
691 700 .filter(UserRepoToPerm.user == user) \
692 701 .scalar()
693 702 if obj:
694 703 self.sa.delete(obj)
695 704 log.debug('Revoked perm on %s on %s', repo, user)
696 705 action_logger_generic(
697 706 'revoked permission from user: {} on repo: {}'.format(
698 707 user, repo), namespace='security.repo')
699 708
700 709 def grant_user_group_permission(self, repo, group_name, perm):
701 710 """
702 711 Grant permission for user group on given repository, or update
703 712 existing one if found
704 713
705 714 :param repo: Instance of Repository, repository_id, or repository name
706 715 :param group_name: Instance of UserGroup, users_group_id,
707 716 or user group name
708 717 :param perm: Instance of Permission, or permission_name
709 718 """
710 719 repo = self._get_repo(repo)
711 720 group_name = self._get_user_group(group_name)
712 721 permission = self._get_perm(perm)
713 722
714 723 # check if we have that permission already
715 724 obj = self.sa.query(UserGroupRepoToPerm) \
716 725 .filter(UserGroupRepoToPerm.users_group == group_name) \
717 726 .filter(UserGroupRepoToPerm.repository == repo) \
718 727 .scalar()
719 728
720 729 if obj is None:
721 730 # create new
722 731 obj = UserGroupRepoToPerm()
723 732
724 733 obj.repository = repo
725 734 obj.users_group = group_name
726 735 obj.permission = permission
727 736 self.sa.add(obj)
728 737 log.debug('Granted perm %s to %s on %s', perm, group_name, repo)
729 738 action_logger_generic(
730 739 'granted permission: {} to usergroup: {} on repo: {}'.format(
731 740 perm, group_name, repo), namespace='security.repo')
732 741
733 742 return obj
734 743
735 744 def revoke_user_group_permission(self, repo, group_name):
736 745 """
737 746 Revoke permission for user group on given repository
738 747
739 748 :param repo: Instance of Repository, repository_id, or repository name
740 749 :param group_name: Instance of UserGroup, users_group_id,
741 750 or user group name
742 751 """
743 752 repo = self._get_repo(repo)
744 753 group_name = self._get_user_group(group_name)
745 754
746 755 obj = self.sa.query(UserGroupRepoToPerm) \
747 756 .filter(UserGroupRepoToPerm.repository == repo) \
748 757 .filter(UserGroupRepoToPerm.users_group == group_name) \
749 758 .scalar()
750 759 if obj:
751 760 self.sa.delete(obj)
752 761 log.debug('Revoked perm to %s on %s', repo, group_name)
753 762 action_logger_generic(
754 763 'revoked permission from usergroup: {} on repo: {}'.format(
755 764 group_name, repo), namespace='security.repo')
756 765
757 766 def delete_stats(self, repo_name):
758 767 """
759 768 removes stats for given repo
760 769
761 770 :param repo_name:
762 771 """
763 772 repo = self._get_repo(repo_name)
764 773 try:
765 774 obj = self.sa.query(Statistics) \
766 775 .filter(Statistics.repository == repo).scalar()
767 776 if obj:
768 777 self.sa.delete(obj)
769 778 except Exception:
770 779 log.error(traceback.format_exc())
771 780 raise
772 781
773 782 def add_repo_field(self, repo_name, field_key, field_label, field_value='',
774 783 field_type='str', field_desc=''):
775 784
776 785 repo = self._get_repo(repo_name)
777 786
778 787 new_field = RepositoryField()
779 788 new_field.repository = repo
780 789 new_field.field_key = field_key
781 790 new_field.field_type = field_type # python type
782 791 new_field.field_value = field_value
783 792 new_field.field_desc = field_desc
784 793 new_field.field_label = field_label
785 794 self.sa.add(new_field)
786 795 return new_field
787 796
788 797 def delete_repo_field(self, repo_name, field_key):
789 798 repo = self._get_repo(repo_name)
790 799 field = RepositoryField.get_by_key_name(field_key, repo)
791 800 if field:
792 801 self.sa.delete(field)
793 802
794 803 def _create_filesystem_repo(self, repo_name, repo_type, repo_group,
795 804 clone_uri=None, repo_store_location=None,
796 805 use_global_config=False):
797 806 """
798 807 makes repository on filesystem. It's group aware means it'll create
799 808 a repository within a group, and alter the paths accordingly of
800 809 group location
801 810
802 811 :param repo_name:
803 812 :param alias:
804 813 :param parent:
805 814 :param clone_uri:
806 815 :param repo_store_location:
807 816 """
808 817 from rhodecode.lib.utils import is_valid_repo, is_valid_repo_group
809 818 from rhodecode.model.scm import ScmModel
810 819
811 820 if Repository.NAME_SEP in repo_name:
812 821 raise ValueError(
813 822 'repo_name must not contain groups got `%s`' % repo_name)
814 823
815 824 if isinstance(repo_group, RepoGroup):
816 825 new_parent_path = os.sep.join(repo_group.full_path_splitted)
817 826 else:
818 827 new_parent_path = repo_group or ''
819 828
820 829 if repo_store_location:
821 830 _paths = [repo_store_location]
822 831 else:
823 832 _paths = [self.repos_path, new_parent_path, repo_name]
824 833 # we need to make it str for mercurial
825 834 repo_path = os.path.join(*map(lambda x: safe_str(x), _paths))
826 835
827 836 # check if this path is not a repository
828 837 if is_valid_repo(repo_path, self.repos_path):
829 838 raise Exception('This path %s is a valid repository' % repo_path)
830 839
831 840 # check if this path is a group
832 841 if is_valid_repo_group(repo_path, self.repos_path):
833 842 raise Exception('This path %s is a valid group' % repo_path)
834 843
835 844 log.info('creating repo %s in %s from url: `%s`',
836 845 repo_name, safe_unicode(repo_path),
837 846 obfuscate_url_pw(clone_uri))
838 847
839 848 backend = get_backend(repo_type)
840 849
841 850 config_repo = None if use_global_config else repo_name
842 851 if config_repo and new_parent_path:
843 852 config_repo = Repository.NAME_SEP.join(
844 853 (new_parent_path, config_repo))
845 854 config = make_db_config(clear_session=False, repo=config_repo)
846 855 config.set('extensions', 'largefiles', '')
847 856
848 857 # patch and reset hooks section of UI config to not run any
849 858 # hooks on creating remote repo
850 859 config.clear_section('hooks')
851 860
852 861 # TODO: johbo: Unify this, hardcoded "bare=True" does not look nice
853 862 if repo_type == 'git':
854 863 repo = backend(
855 864 repo_path, config=config, create=True, src_url=clone_uri,
856 865 bare=True)
857 866 else:
858 867 repo = backend(
859 868 repo_path, config=config, create=True, src_url=clone_uri)
860 869
861 870 repo.install_hooks()
862 871
863 872 log.debug('Created repo %s with %s backend',
864 873 safe_unicode(repo_name), safe_unicode(repo_type))
865 874 return repo
866 875
867 876 def _rename_filesystem_repo(self, old, new):
868 877 """
869 878 renames repository on filesystem
870 879
871 880 :param old: old name
872 881 :param new: new name
873 882 """
874 883 log.info('renaming repo from %s to %s', old, new)
875 884
876 885 old_path = os.path.join(self.repos_path, old)
877 886 new_path = os.path.join(self.repos_path, new)
878 887 if os.path.isdir(new_path):
879 888 raise Exception(
880 889 'Was trying to rename to already existing dir %s' % new_path
881 890 )
882 891 shutil.move(old_path, new_path)
883 892
884 893 def _delete_filesystem_repo(self, repo):
885 894 """
886 895 removes repo from filesystem, the removal is acctually made by
887 896 added rm__ prefix into dir, and rename internat .hg/.git dirs so this
888 897 repository is no longer valid for rhodecode, can be undeleted later on
889 898 by reverting the renames on this repository
890 899
891 900 :param repo: repo object
892 901 """
893 902 rm_path = os.path.join(self.repos_path, repo.repo_name)
894 903 repo_group = repo.group
895 904 log.info("Removing repository %s", rm_path)
896 905 # disable hg/git internal that it doesn't get detected as repo
897 906 alias = repo.repo_type
898 907
899 908 config = make_db_config(clear_session=False)
900 909 config.set('extensions', 'largefiles', '')
901 910 bare = getattr(repo.scm_instance(config=config), 'bare', False)
902 911
903 912 # skip this for bare git repos
904 913 if not bare:
905 914 # disable VCS repo
906 915 vcs_path = os.path.join(rm_path, '.%s' % alias)
907 916 if os.path.exists(vcs_path):
908 917 shutil.move(vcs_path, os.path.join(rm_path, 'rm__.%s' % alias))
909 918
910 919 _now = datetime.datetime.now()
911 920 _ms = str(_now.microsecond).rjust(6, '0')
912 921 _d = 'rm__%s__%s' % (_now.strftime('%Y%m%d_%H%M%S_' + _ms),
913 922 repo.just_name)
914 923 if repo_group:
915 924 # if repository is in group, prefix the removal path with the group
916 925 args = repo_group.full_path_splitted + [_d]
917 926 _d = os.path.join(*args)
918 927
919 928 if os.path.isdir(rm_path):
920 929 shutil.move(rm_path, os.path.join(self.repos_path, _d))
921 930
922 931 # finally cleanup diff-cache if it exists
923 932 cached_diffs_dir = repo.cached_diffs_dir
924 933 if os.path.isdir(cached_diffs_dir):
925 934 shutil.rmtree(cached_diffs_dir)
926 935
927 936
928 937 class ReadmeFinder:
929 938 """
930 939 Utility which knows how to find a readme for a specific commit.
931 940
932 941 The main idea is that this is a configurable algorithm. When creating an
933 942 instance you can define parameters, currently only the `default_renderer`.
934 943 Based on this configuration the method :meth:`search` behaves slightly
935 944 different.
936 945 """
937 946
938 947 readme_re = re.compile(r'^readme(\.[^\.]+)?$', re.IGNORECASE)
939 948 path_re = re.compile(r'^docs?', re.IGNORECASE)
940 949
941 950 default_priorities = {
942 951 None: 0,
943 952 '.text': 2,
944 953 '.txt': 3,
945 954 '.rst': 1,
946 955 '.rest': 2,
947 956 '.md': 1,
948 957 '.mkdn': 2,
949 958 '.mdown': 3,
950 959 '.markdown': 4,
951 960 }
952 961
953 962 path_priority = {
954 963 'doc': 0,
955 964 'docs': 1,
956 965 }
957 966
958 967 FALLBACK_PRIORITY = 99
959 968
960 969 RENDERER_TO_EXTENSION = {
961 970 'rst': ['.rst', '.rest'],
962 971 'markdown': ['.md', 'mkdn', '.mdown', '.markdown'],
963 972 }
964 973
965 974 def __init__(self, default_renderer=None):
966 975 self._default_renderer = default_renderer
967 976 self._renderer_extensions = self.RENDERER_TO_EXTENSION.get(
968 977 default_renderer, [])
969 978
970 979 def search(self, commit, path='/'):
971 980 """
972 981 Find a readme in the given `commit`.
973 982 """
974 983 nodes = commit.get_nodes(path)
975 984 matches = self._match_readmes(nodes)
976 985 matches = self._sort_according_to_priority(matches)
977 986 if matches:
978 987 return matches[0].node
979 988
980 989 paths = self._match_paths(nodes)
981 990 paths = self._sort_paths_according_to_priority(paths)
982 991 for path in paths:
983 992 match = self.search(commit, path=path)
984 993 if match:
985 994 return match
986 995
987 996 return None
988 997
989 998 def _match_readmes(self, nodes):
990 999 for node in nodes:
991 1000 if not node.is_file():
992 1001 continue
993 1002 path = node.path.rsplit('/', 1)[-1]
994 1003 match = self.readme_re.match(path)
995 1004 if match:
996 1005 extension = match.group(1)
997 1006 yield ReadmeMatch(node, match, self._priority(extension))
998 1007
999 1008 def _match_paths(self, nodes):
1000 1009 for node in nodes:
1001 1010 if not node.is_dir():
1002 1011 continue
1003 1012 match = self.path_re.match(node.path)
1004 1013 if match:
1005 1014 yield node.path
1006 1015
1007 1016 def _priority(self, extension):
1008 1017 renderer_priority = (
1009 1018 0 if extension in self._renderer_extensions else 1)
1010 1019 extension_priority = self.default_priorities.get(
1011 1020 extension, self.FALLBACK_PRIORITY)
1012 1021 return (renderer_priority, extension_priority)
1013 1022
1014 1023 def _sort_according_to_priority(self, matches):
1015 1024
1016 1025 def priority_and_path(match):
1017 1026 return (match.priority, match.path)
1018 1027
1019 1028 return sorted(matches, key=priority_and_path)
1020 1029
1021 1030 def _sort_paths_according_to_priority(self, paths):
1022 1031
1023 1032 def priority_and_path(path):
1024 1033 return (self.path_priority.get(path, self.FALLBACK_PRIORITY), path)
1025 1034
1026 1035 return sorted(paths, key=priority_and_path)
1027 1036
1028 1037
1029 1038 class ReadmeMatch:
1030 1039
1031 1040 def __init__(self, node, match, priority):
1032 1041 self.node = node
1033 1042 self._match = match
1034 1043 self.priority = priority
1035 1044
1036 1045 @property
1037 1046 def path(self):
1038 1047 return self.node.path
1039 1048
1040 1049 def __repr__(self):
1041 1050 return '<ReadmeMatch {} priority={}'.format(self.path, self.priority)
@@ -1,764 +1,773 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2018 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, datetime_to_time
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 user = self._get_user(owner)
258 258 if not user:
259 259 raise ValueError('Owner %s not found as rhodecode user', owner)
260 260
261 261 try:
262 262 new_repo_group = RepoGroup()
263 263 new_repo_group.user = user
264 264 new_repo_group.group_description = group_description or group_name
265 265 new_repo_group.parent_group = parent_group
266 266 new_repo_group.group_name = group_name
267 267 new_repo_group.personal = personal
268 268
269 269 self.sa.add(new_repo_group)
270 270
271 271 # create an ADMIN permission for owner except if we're super admin,
272 272 # later owner should go into the owner field of groups
273 273 if not user.is_admin:
274 274 self.grant_user_permission(repo_group=new_repo_group,
275 275 user=owner, perm='group.admin')
276 276
277 277 if parent_group and copy_permissions:
278 278 # copy permissions from parent
279 279 user_perms = UserRepoGroupToPerm.query() \
280 280 .filter(UserRepoGroupToPerm.group == parent_group).all()
281 281
282 282 group_perms = UserGroupRepoGroupToPerm.query() \
283 283 .filter(UserGroupRepoGroupToPerm.group == parent_group).all()
284 284
285 285 for perm in user_perms:
286 286 # don't copy over the permission for user who is creating
287 287 # this group, if he is not super admin he get's admin
288 288 # permission set above
289 289 if perm.user != user or user.is_admin:
290 290 UserRepoGroupToPerm.create(
291 291 perm.user, new_repo_group, perm.permission)
292 292
293 293 for perm in group_perms:
294 294 UserGroupRepoGroupToPerm.create(
295 295 perm.users_group, new_repo_group, perm.permission)
296 296 else:
297 297 perm_obj = self._create_default_perms(new_repo_group)
298 298 self.sa.add(perm_obj)
299 299
300 300 # now commit the changes, earlier so we are sure everything is in
301 301 # the database.
302 302 if commit_early:
303 303 self.sa.commit()
304 304 if not just_db:
305 305 self._create_group(new_repo_group.group_name)
306 306
307 307 # trigger the post hook
308 308 from rhodecode.lib.hooks_base import log_create_repository_group
309 309 repo_group = RepoGroup.get_by_group_name(group_name)
310 310 log_create_repository_group(
311 311 created_by=user.username, **repo_group.get_dict())
312 312
313 313 # Trigger create event.
314 314 events.trigger(events.RepoGroupCreateEvent(repo_group))
315 315
316 316 return new_repo_group
317 317 except Exception:
318 318 self.sa.rollback()
319 319 log.exception('Exception occurred when creating repository group, '
320 320 'doing cleanup...')
321 321 # rollback things manually !
322 322 repo_group = RepoGroup.get_by_group_name(group_name)
323 323 if repo_group:
324 324 RepoGroup.delete(repo_group.group_id)
325 325 self.sa.commit()
326 326 if cleanup_group:
327 327 RepoGroupModel()._delete_filesystem_group(repo_group)
328 328 raise
329 329
330 330 def update_permissions(
331 331 self, repo_group, perm_additions=None, perm_updates=None,
332 332 perm_deletions=None, recursive=None, check_perms=True,
333 333 cur_user=None):
334 334 from rhodecode.model.repo import RepoModel
335 335 from rhodecode.lib.auth import HasUserGroupPermissionAny
336 336
337 337 if not perm_additions:
338 338 perm_additions = []
339 339 if not perm_updates:
340 340 perm_updates = []
341 341 if not perm_deletions:
342 342 perm_deletions = []
343 343
344 344 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
345 345
346 346 changes = {
347 347 'added': [],
348 348 'updated': [],
349 349 'deleted': []
350 350 }
351 351
352 352 def _set_perm_user(obj, user, perm):
353 353 if isinstance(obj, RepoGroup):
354 354 self.grant_user_permission(
355 355 repo_group=obj, user=user, perm=perm)
356 356 elif isinstance(obj, Repository):
357 357 # private repos will not allow to change the default
358 358 # permissions using recursive mode
359 359 if obj.private and user == User.DEFAULT_USER:
360 360 return
361 361
362 362 # we set group permission but we have to switch to repo
363 363 # permission
364 364 perm = perm.replace('group.', 'repository.')
365 365 RepoModel().grant_user_permission(
366 366 repo=obj, user=user, perm=perm)
367 367
368 368 def _set_perm_group(obj, users_group, perm):
369 369 if isinstance(obj, RepoGroup):
370 370 self.grant_user_group_permission(
371 371 repo_group=obj, group_name=users_group, perm=perm)
372 372 elif isinstance(obj, Repository):
373 373 # we set group permission but we have to switch to repo
374 374 # permission
375 375 perm = perm.replace('group.', 'repository.')
376 376 RepoModel().grant_user_group_permission(
377 377 repo=obj, group_name=users_group, perm=perm)
378 378
379 379 def _revoke_perm_user(obj, user):
380 380 if isinstance(obj, RepoGroup):
381 381 self.revoke_user_permission(repo_group=obj, user=user)
382 382 elif isinstance(obj, Repository):
383 383 RepoModel().revoke_user_permission(repo=obj, user=user)
384 384
385 385 def _revoke_perm_group(obj, user_group):
386 386 if isinstance(obj, RepoGroup):
387 387 self.revoke_user_group_permission(
388 388 repo_group=obj, group_name=user_group)
389 389 elif isinstance(obj, Repository):
390 390 RepoModel().revoke_user_group_permission(
391 391 repo=obj, group_name=user_group)
392 392
393 393 # start updates
394 394 log.debug('Now updating permissions for %s in recursive mode:%s',
395 395 repo_group, recursive)
396 396
397 397 # initialize check function, we'll call that multiple times
398 398 has_group_perm = HasUserGroupPermissionAny(*req_perms)
399 399
400 400 for obj in repo_group.recursive_groups_and_repos():
401 401 # iterated obj is an instance of a repos group or repository in
402 402 # that group, recursive option can be: none, repos, groups, all
403 403 if recursive == 'all':
404 404 obj = obj
405 405 elif recursive == 'repos':
406 406 # skip groups, other than this one
407 407 if isinstance(obj, RepoGroup) and not obj == repo_group:
408 408 continue
409 409 elif recursive == 'groups':
410 410 # skip repos
411 411 if isinstance(obj, Repository):
412 412 continue
413 413 else: # recursive == 'none':
414 414 # DEFAULT option - don't apply to iterated objects
415 415 # also we do a break at the end of this loop. if we are not
416 416 # in recursive mode
417 417 obj = repo_group
418 418
419 419 change_obj = obj.get_api_data()
420 420
421 421 # update permissions
422 422 for member_id, perm, member_type in perm_updates:
423 423 member_id = int(member_id)
424 424 if member_type == 'user':
425 425 member_name = User.get(member_id).username
426 426 # this updates also current one if found
427 427 _set_perm_user(obj, user=member_id, perm=perm)
428 else: # set for user group
428 elif member_type == 'user_group':
429 429 member_name = UserGroup.get(member_id).users_group_name
430 430 if not check_perms or has_group_perm(member_name,
431 431 user=cur_user):
432 432 _set_perm_group(obj, users_group=member_id, perm=perm)
433 else:
434 raise ValueError("member_type must be 'user' or 'user_group' "
435 "got {} instead".format(member_type))
433 436
434 437 changes['updated'].append(
435 438 {'change_obj': change_obj, 'type': member_type,
436 439 'id': member_id, 'name': member_name, 'new_perm': perm})
437 440
438 441 # set new permissions
439 442 for member_id, perm, member_type in perm_additions:
440 443 member_id = int(member_id)
441 444 if member_type == 'user':
442 445 member_name = User.get(member_id).username
443 446 _set_perm_user(obj, user=member_id, perm=perm)
444 else: # set for user group
447 elif member_type == 'user_group':
445 448 # check if we have permissions to alter this usergroup
446 449 member_name = UserGroup.get(member_id).users_group_name
447 450 if not check_perms or has_group_perm(member_name,
448 451 user=cur_user):
449 452 _set_perm_group(obj, users_group=member_id, perm=perm)
453 else:
454 raise ValueError("member_type must be 'user' or 'user_group' "
455 "got {} instead".format(member_type))
450 456
451 457 changes['added'].append(
452 458 {'change_obj': change_obj, 'type': member_type,
453 459 'id': member_id, 'name': member_name, 'new_perm': perm})
454 460
455 461 # delete permissions
456 462 for member_id, perm, member_type in perm_deletions:
457 463 member_id = int(member_id)
458 464 if member_type == 'user':
459 465 member_name = User.get(member_id).username
460 466 _revoke_perm_user(obj, user=member_id)
461 else: # set for user group
467 elif member_type == 'user_group':
462 468 # check if we have permissions to alter this usergroup
463 469 member_name = UserGroup.get(member_id).users_group_name
464 470 if not check_perms or has_group_perm(member_name,
465 471 user=cur_user):
466 472 _revoke_perm_group(obj, user_group=member_id)
473 else:
474 raise ValueError("member_type must be 'user' or 'user_group' "
475 "got {} instead".format(member_type))
467 476
468 477 changes['deleted'].append(
469 478 {'change_obj': change_obj, 'type': member_type,
470 479 'id': member_id, 'name': member_name, 'new_perm': perm})
471 480
472 481 # if it's not recursive call for all,repos,groups
473 482 # break the loop and don't proceed with other changes
474 483 if recursive not in ['all', 'repos', 'groups']:
475 484 break
476 485
477 486 return changes
478 487
479 488 def update(self, repo_group, form_data):
480 489 try:
481 490 repo_group = self._get_repo_group(repo_group)
482 491 old_path = repo_group.full_path
483 492
484 493 # change properties
485 494 if 'group_description' in form_data:
486 495 repo_group.group_description = form_data['group_description']
487 496
488 497 if 'enable_locking' in form_data:
489 498 repo_group.enable_locking = form_data['enable_locking']
490 499
491 500 if 'group_parent_id' in form_data:
492 501 parent_group = (
493 502 self._get_repo_group(form_data['group_parent_id']))
494 503 repo_group.group_parent_id = (
495 504 parent_group.group_id if parent_group else None)
496 505 repo_group.parent_group = parent_group
497 506
498 507 # mikhail: to update the full_path, we have to explicitly
499 508 # update group_name
500 509 group_name = form_data.get('group_name', repo_group.name)
501 510 repo_group.group_name = repo_group.get_new_name(group_name)
502 511
503 512 new_path = repo_group.full_path
504 513
505 514 if 'user' in form_data:
506 515 repo_group.user = User.get_by_username(form_data['user'])
507 516 repo_group.updated_on = datetime.datetime.now()
508 517 self.sa.add(repo_group)
509 518
510 519 # iterate over all members of this groups and do fixes
511 520 # set locking if given
512 521 # if obj is a repoGroup also fix the name of the group according
513 522 # to the parent
514 523 # if obj is a Repo fix it's name
515 524 # this can be potentially heavy operation
516 525 for obj in repo_group.recursive_groups_and_repos():
517 526 # set the value from it's parent
518 527 obj.enable_locking = repo_group.enable_locking
519 528 if isinstance(obj, RepoGroup):
520 529 new_name = obj.get_new_name(obj.name)
521 530 log.debug('Fixing group %s to new name %s',
522 531 obj.group_name, new_name)
523 532 obj.group_name = new_name
524 533 obj.updated_on = datetime.datetime.now()
525 534 elif isinstance(obj, Repository):
526 535 # we need to get all repositories from this new group and
527 536 # rename them accordingly to new group path
528 537 new_name = obj.get_new_name(obj.just_name)
529 538 log.debug('Fixing repo %s to new name %s',
530 539 obj.repo_name, new_name)
531 540 obj.repo_name = new_name
532 541 obj.updated_on = datetime.datetime.now()
533 542 self.sa.add(obj)
534 543
535 544 self._rename_group(old_path, new_path)
536 545
537 546 # Trigger update event.
538 547 events.trigger(events.RepoGroupUpdateEvent(repo_group))
539 548
540 549 return repo_group
541 550 except Exception:
542 551 log.error(traceback.format_exc())
543 552 raise
544 553
545 554 def delete(self, repo_group, force_delete=False, fs_remove=True):
546 555 repo_group = self._get_repo_group(repo_group)
547 556 if not repo_group:
548 557 return False
549 558 try:
550 559 self.sa.delete(repo_group)
551 560 if fs_remove:
552 561 self._delete_filesystem_group(repo_group, force_delete)
553 562 else:
554 563 log.debug('skipping removal from filesystem')
555 564
556 565 # Trigger delete event.
557 566 events.trigger(events.RepoGroupDeleteEvent(repo_group))
558 567 return True
559 568
560 569 except Exception:
561 570 log.error('Error removing repo_group %s', repo_group)
562 571 raise
563 572
564 573 def grant_user_permission(self, repo_group, user, perm):
565 574 """
566 575 Grant permission for user on given repository group, or update
567 576 existing one if found
568 577
569 578 :param repo_group: Instance of RepoGroup, repositories_group_id,
570 579 or repositories_group name
571 580 :param user: Instance of User, user_id or username
572 581 :param perm: Instance of Permission, or permission_name
573 582 """
574 583
575 584 repo_group = self._get_repo_group(repo_group)
576 585 user = self._get_user(user)
577 586 permission = self._get_perm(perm)
578 587
579 588 # check if we have that permission already
580 589 obj = self.sa.query(UserRepoGroupToPerm)\
581 590 .filter(UserRepoGroupToPerm.user == user)\
582 591 .filter(UserRepoGroupToPerm.group == repo_group)\
583 592 .scalar()
584 593 if obj is None:
585 594 # create new !
586 595 obj = UserRepoGroupToPerm()
587 596 obj.group = repo_group
588 597 obj.user = user
589 598 obj.permission = permission
590 599 self.sa.add(obj)
591 600 log.debug('Granted perm %s to %s on %s', perm, user, repo_group)
592 601 action_logger_generic(
593 602 'granted permission: {} to user: {} on repogroup: {}'.format(
594 603 perm, user, repo_group), namespace='security.repogroup')
595 604 return obj
596 605
597 606 def revoke_user_permission(self, repo_group, user):
598 607 """
599 608 Revoke permission for user on given repository group
600 609
601 610 :param repo_group: Instance of RepoGroup, repositories_group_id,
602 611 or repositories_group name
603 612 :param user: Instance of User, user_id or username
604 613 """
605 614
606 615 repo_group = self._get_repo_group(repo_group)
607 616 user = self._get_user(user)
608 617
609 618 obj = self.sa.query(UserRepoGroupToPerm)\
610 619 .filter(UserRepoGroupToPerm.user == user)\
611 620 .filter(UserRepoGroupToPerm.group == repo_group)\
612 621 .scalar()
613 622 if obj:
614 623 self.sa.delete(obj)
615 624 log.debug('Revoked perm on %s on %s', repo_group, user)
616 625 action_logger_generic(
617 626 'revoked permission from user: {} on repogroup: {}'.format(
618 627 user, repo_group), namespace='security.repogroup')
619 628
620 629 def grant_user_group_permission(self, repo_group, group_name, perm):
621 630 """
622 631 Grant permission for user group on given repository group, or update
623 632 existing one if found
624 633
625 634 :param repo_group: Instance of RepoGroup, repositories_group_id,
626 635 or repositories_group name
627 636 :param group_name: Instance of UserGroup, users_group_id,
628 637 or user group name
629 638 :param perm: Instance of Permission, or permission_name
630 639 """
631 640 repo_group = self._get_repo_group(repo_group)
632 641 group_name = self._get_user_group(group_name)
633 642 permission = self._get_perm(perm)
634 643
635 644 # check if we have that permission already
636 645 obj = self.sa.query(UserGroupRepoGroupToPerm)\
637 646 .filter(UserGroupRepoGroupToPerm.group == repo_group)\
638 647 .filter(UserGroupRepoGroupToPerm.users_group == group_name)\
639 648 .scalar()
640 649
641 650 if obj is None:
642 651 # create new
643 652 obj = UserGroupRepoGroupToPerm()
644 653
645 654 obj.group = repo_group
646 655 obj.users_group = group_name
647 656 obj.permission = permission
648 657 self.sa.add(obj)
649 658 log.debug('Granted perm %s to %s on %s', perm, group_name, repo_group)
650 659 action_logger_generic(
651 660 'granted permission: {} to usergroup: {} on repogroup: {}'.format(
652 661 perm, group_name, repo_group), namespace='security.repogroup')
653 662 return obj
654 663
655 664 def revoke_user_group_permission(self, repo_group, group_name):
656 665 """
657 666 Revoke permission for user group on given repository group
658 667
659 668 :param repo_group: Instance of RepoGroup, repositories_group_id,
660 669 or repositories_group name
661 670 :param group_name: Instance of UserGroup, users_group_id,
662 671 or user group name
663 672 """
664 673 repo_group = self._get_repo_group(repo_group)
665 674 group_name = self._get_user_group(group_name)
666 675
667 676 obj = self.sa.query(UserGroupRepoGroupToPerm)\
668 677 .filter(UserGroupRepoGroupToPerm.group == repo_group)\
669 678 .filter(UserGroupRepoGroupToPerm.users_group == group_name)\
670 679 .scalar()
671 680 if obj:
672 681 self.sa.delete(obj)
673 682 log.debug('Revoked perm to %s on %s', repo_group, group_name)
674 683 action_logger_generic(
675 684 'revoked permission from usergroup: {} on repogroup: {}'.format(
676 685 group_name, repo_group), namespace='security.repogroup')
677 686
678 687 def get_repo_groups_as_dict(self, repo_group_list=None, admin=False,
679 688 super_user_actions=False):
680 689
681 690 from pyramid.threadlocal import get_current_request
682 691 _render = get_current_request().get_partial_renderer(
683 692 'rhodecode:templates/data_table/_dt_elements.mako')
684 693 c = _render.get_call_context()
685 694 h = _render.get_helpers()
686 695
687 696 def quick_menu(repo_group_name):
688 697 return _render('quick_repo_group_menu', repo_group_name)
689 698
690 699 def repo_group_lnk(repo_group_name):
691 700 return _render('repo_group_name', repo_group_name)
692 701
693 702 def last_change(last_change):
694 703 if admin and isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
695 704 last_change = last_change + datetime.timedelta(seconds=
696 705 (datetime.datetime.now() - datetime.datetime.utcnow()).seconds)
697 706 return _render("last_change", last_change)
698 707
699 708 def desc(desc, personal):
700 709 return _render(
701 710 'repo_group_desc', desc, personal, c.visual.stylify_metatags)
702 711
703 712 def repo_group_actions(repo_group_id, repo_group_name, gr_count):
704 713 return _render(
705 714 'repo_group_actions', repo_group_id, repo_group_name, gr_count)
706 715
707 716 def repo_group_name(repo_group_name, children_groups):
708 717 return _render("repo_group_name", repo_group_name, children_groups)
709 718
710 719 def user_profile(username):
711 720 return _render('user_profile', username)
712 721
713 722 repo_group_data = []
714 723 for group in repo_group_list:
715 724
716 725 row = {
717 726 "menu": quick_menu(group.group_name),
718 727 "name": repo_group_lnk(group.group_name),
719 728 "name_raw": group.group_name,
720 729 "last_change": last_change(group.last_db_change),
721 730 "last_change_raw": datetime_to_time(group.last_db_change),
722 731 "desc": desc(group.description_safe, group.personal),
723 732 "top_level_repos": 0,
724 733 "owner": user_profile(group.user.username)
725 734 }
726 735 if admin:
727 736 repo_count = group.repositories.count()
728 737 children_groups = map(
729 738 h.safe_unicode,
730 739 itertools.chain((g.name for g in group.parents),
731 740 (x.name for x in [group])))
732 741 row.update({
733 742 "action": repo_group_actions(
734 743 group.group_id, group.group_name, repo_count),
735 744 "top_level_repos": repo_count,
736 745 "name": repo_group_name(group.group_name, children_groups),
737 746
738 747 })
739 748 repo_group_data.append(row)
740 749
741 750 return repo_group_data
742 751
743 752 def _get_defaults(self, repo_group_name):
744 753 repo_group = RepoGroup.get_by_group_name(repo_group_name)
745 754
746 755 if repo_group is None:
747 756 return None
748 757
749 758 defaults = repo_group.get_dict()
750 759 defaults['repo_group_name'] = repo_group.name
751 760 defaults['repo_group_description'] = repo_group.group_description
752 761 defaults['repo_group_enable_locking'] = repo_group.enable_locking
753 762
754 763 # we use -1 as this is how in HTML, we mark an empty group
755 764 defaults['repo_group'] = defaults['group_parent_id'] or -1
756 765
757 766 # fill owner
758 767 if repo_group.user:
759 768 defaults.update({'user': repo_group.user.username})
760 769 else:
761 770 replacement_user = User.get_first_super_admin().username
762 771 defaults.update({'user': replacement_user})
763 772
764 773 return defaults
@@ -1,745 +1,754 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2018 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 traceback
23 23
24 24 from rhodecode.lib.utils2 import safe_str, safe_unicode
25 25 from rhodecode.lib.exceptions import (
26 26 UserGroupAssignedException, RepoGroupAssignmentError)
27 27 from rhodecode.lib.utils2 import (
28 28 get_current_rhodecode_user, action_logger_generic)
29 29 from rhodecode.model import BaseModel
30 30 from rhodecode.model.scm import UserGroupList
31 31 from rhodecode.model.db import (
32 32 joinedload, true, func, User, UserGroupMember, UserGroup,
33 33 UserGroupRepoToPerm, Permission, UserGroupToPerm, UserUserGroupToPerm,
34 34 UserGroupUserGroupToPerm, UserGroupRepoGroupToPerm)
35 35
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 class UserGroupModel(BaseModel):
41 41
42 42 cls = UserGroup
43 43
44 44 def _get_user_group(self, user_group):
45 45 return self._get_instance(UserGroup, user_group,
46 46 callback=UserGroup.get_by_group_name)
47 47
48 48 def _create_default_perms(self, user_group):
49 49 # create default permission
50 50 default_perm = 'usergroup.read'
51 51 def_user = User.get_default_user()
52 52 for p in def_user.user_perms:
53 53 if p.permission.permission_name.startswith('usergroup.'):
54 54 default_perm = p.permission.permission_name
55 55 break
56 56
57 57 user_group_to_perm = UserUserGroupToPerm()
58 58 user_group_to_perm.permission = Permission.get_by_key(default_perm)
59 59
60 60 user_group_to_perm.user_group = user_group
61 61 user_group_to_perm.user_id = def_user.user_id
62 62 return user_group_to_perm
63 63
64 64 def update_permissions(
65 65 self, user_group, perm_additions=None, perm_updates=None,
66 66 perm_deletions=None, check_perms=True, cur_user=None):
67 67
68 68 from rhodecode.lib.auth import HasUserGroupPermissionAny
69 69 if not perm_additions:
70 70 perm_additions = []
71 71 if not perm_updates:
72 72 perm_updates = []
73 73 if not perm_deletions:
74 74 perm_deletions = []
75 75
76 76 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
77 77
78 78 changes = {
79 79 'added': [],
80 80 'updated': [],
81 81 'deleted': []
82 82 }
83 83 change_obj = user_group.get_api_data()
84 84 # update permissions
85 85 for member_id, perm, member_type in perm_updates:
86 86 member_id = int(member_id)
87 87 if member_type == 'user':
88 88 member_name = User.get(member_id).username
89 89 # this updates existing one
90 90 self.grant_user_permission(
91 91 user_group=user_group, user=member_id, perm=perm
92 92 )
93 else:
93 elif member_type == 'user_group':
94 94 # check if we have permissions to alter this usergroup
95 95 member_name = UserGroup.get(member_id).users_group_name
96 96 if not check_perms or HasUserGroupPermissionAny(
97 97 *req_perms)(member_name, user=cur_user):
98 98 self.grant_user_group_permission(
99 99 target_user_group=user_group, user_group=member_id, perm=perm)
100 else:
101 raise ValueError("member_type must be 'user' or 'user_group' "
102 "got {} instead".format(member_type))
100 103
101 104 changes['updated'].append({
102 105 'change_obj': change_obj,
103 106 'type': member_type, 'id': member_id,
104 107 'name': member_name, 'new_perm': perm})
105 108
106 109 # set new permissions
107 110 for member_id, perm, member_type in perm_additions:
108 111 member_id = int(member_id)
109 112 if member_type == 'user':
110 113 member_name = User.get(member_id).username
111 114 self.grant_user_permission(
112 115 user_group=user_group, user=member_id, perm=perm)
113 else:
116 elif member_type == 'user_group':
114 117 # check if we have permissions to alter this usergroup
115 118 member_name = UserGroup.get(member_id).users_group_name
116 119 if not check_perms or HasUserGroupPermissionAny(
117 120 *req_perms)(member_name, user=cur_user):
118 121 self.grant_user_group_permission(
119 122 target_user_group=user_group, user_group=member_id, perm=perm)
123 else:
124 raise ValueError("member_type must be 'user' or 'user_group' "
125 "got {} instead".format(member_type))
120 126
121 127 changes['added'].append({
122 128 'change_obj': change_obj,
123 129 'type': member_type, 'id': member_id,
124 130 'name': member_name, 'new_perm': perm})
125 131
126 132 # delete permissions
127 133 for member_id, perm, member_type in perm_deletions:
128 134 member_id = int(member_id)
129 135 if member_type == 'user':
130 136 member_name = User.get(member_id).username
131 137 self.revoke_user_permission(user_group=user_group, user=member_id)
132 else:
138 elif member_type == 'user_group':
133 139 # check if we have permissions to alter this usergroup
134 140 member_name = UserGroup.get(member_id).users_group_name
135 141 if not check_perms or HasUserGroupPermissionAny(
136 142 *req_perms)(member_name, user=cur_user):
137 143 self.revoke_user_group_permission(
138 144 target_user_group=user_group, user_group=member_id)
145 else:
146 raise ValueError("member_type must be 'user' or 'user_group' "
147 "got {} instead".format(member_type))
139 148
140 149 changes['deleted'].append({
141 150 'change_obj': change_obj,
142 151 'type': member_type, 'id': member_id,
143 152 'name': member_name, 'new_perm': perm})
144 153
145 154 return changes
146 155
147 156 def get(self, user_group_id, cache=False):
148 157 return UserGroup.get(user_group_id)
149 158
150 159 def get_group(self, user_group):
151 160 return self._get_user_group(user_group)
152 161
153 162 def get_by_name(self, name, cache=False, case_insensitive=False):
154 163 return UserGroup.get_by_group_name(name, cache, case_insensitive)
155 164
156 165 def create(self, name, description, owner, active=True, group_data=None):
157 166 try:
158 167 new_user_group = UserGroup()
159 168 new_user_group.user = self._get_user(owner)
160 169 new_user_group.users_group_name = name
161 170 new_user_group.user_group_description = description
162 171 new_user_group.users_group_active = active
163 172 if group_data:
164 173 new_user_group.group_data = group_data
165 174 self.sa.add(new_user_group)
166 175 perm_obj = self._create_default_perms(new_user_group)
167 176 self.sa.add(perm_obj)
168 177
169 178 self.grant_user_permission(user_group=new_user_group,
170 179 user=owner, perm='usergroup.admin')
171 180
172 181 return new_user_group
173 182 except Exception:
174 183 log.error(traceback.format_exc())
175 184 raise
176 185
177 186 def _get_memberships_for_user_ids(self, user_group, user_id_list):
178 187 members = []
179 188 for user_id in user_id_list:
180 189 member = self._get_membership(user_group.users_group_id, user_id)
181 190 members.append(member)
182 191 return members
183 192
184 193 def _get_added_and_removed_user_ids(self, user_group, user_id_list):
185 194 current_members = user_group.members or []
186 195 current_members_ids = [m.user.user_id for m in current_members]
187 196
188 197 added_members = [
189 198 user_id for user_id in user_id_list
190 199 if user_id not in current_members_ids]
191 200 if user_id_list == []:
192 201 # all members were deleted
193 202 deleted_members = current_members_ids
194 203 else:
195 204 deleted_members = [
196 205 user_id for user_id in current_members_ids
197 206 if user_id not in user_id_list]
198 207
199 208 return added_members, deleted_members
200 209
201 210 def _set_users_as_members(self, user_group, user_ids):
202 211 user_group.members = []
203 212 self.sa.flush()
204 213 members = self._get_memberships_for_user_ids(
205 214 user_group, user_ids)
206 215 user_group.members = members
207 216 self.sa.add(user_group)
208 217
209 218 def _update_members_from_user_ids(self, user_group, user_ids):
210 219 added, removed = self._get_added_and_removed_user_ids(
211 220 user_group, user_ids)
212 221 self._set_users_as_members(user_group, user_ids)
213 222 self._log_user_changes('added to', user_group, added)
214 223 self._log_user_changes('removed from', user_group, removed)
215 224 return added, removed
216 225
217 226 def _clean_members_data(self, members_data):
218 227 if not members_data:
219 228 members_data = []
220 229
221 230 members = []
222 231 for user in members_data:
223 232 uid = int(user['member_user_id'])
224 233 if uid not in members and user['type'] in ['new', 'existing']:
225 234 members.append(uid)
226 235 return members
227 236
228 237 def update(self, user_group, form_data, group_data=None):
229 238 user_group = self._get_user_group(user_group)
230 239 if 'users_group_name' in form_data:
231 240 user_group.users_group_name = form_data['users_group_name']
232 241 if 'users_group_active' in form_data:
233 242 user_group.users_group_active = form_data['users_group_active']
234 243 if 'user_group_description' in form_data:
235 244 user_group.user_group_description = form_data[
236 245 'user_group_description']
237 246
238 247 # handle owner change
239 248 if 'user' in form_data:
240 249 owner = form_data['user']
241 250 if isinstance(owner, basestring):
242 251 owner = User.get_by_username(form_data['user'])
243 252
244 253 if not isinstance(owner, User):
245 254 raise ValueError(
246 255 'invalid owner for user group: %s' % form_data['user'])
247 256
248 257 user_group.user = owner
249 258
250 259 added_user_ids = []
251 260 removed_user_ids = []
252 261 if 'users_group_members' in form_data:
253 262 members_id_list = self._clean_members_data(
254 263 form_data['users_group_members'])
255 264 added_user_ids, removed_user_ids = \
256 265 self._update_members_from_user_ids(user_group, members_id_list)
257 266
258 267 if group_data:
259 268 new_group_data = {}
260 269 new_group_data.update(group_data)
261 270 user_group.group_data = new_group_data
262 271
263 272 self.sa.add(user_group)
264 273 return user_group, added_user_ids, removed_user_ids
265 274
266 275 def delete(self, user_group, force=False):
267 276 """
268 277 Deletes repository group, unless force flag is used
269 278 raises exception if there are members in that group, else deletes
270 279 group and users
271 280
272 281 :param user_group:
273 282 :param force:
274 283 """
275 284 user_group = self._get_user_group(user_group)
276 285 if not user_group:
277 286 return
278 287
279 288 try:
280 289 # check if this group is not assigned to repo
281 290 assigned_to_repo = [x.repository for x in UserGroupRepoToPerm.query()\
282 291 .filter(UserGroupRepoToPerm.users_group == user_group).all()]
283 292 # check if this group is not assigned to repo
284 293 assigned_to_repo_group = [x.group for x in UserGroupRepoGroupToPerm.query()\
285 294 .filter(UserGroupRepoGroupToPerm.users_group == user_group).all()]
286 295
287 296 if (assigned_to_repo or assigned_to_repo_group) and not force:
288 297 assigned = ','.join(map(safe_str,
289 298 assigned_to_repo+assigned_to_repo_group))
290 299
291 300 raise UserGroupAssignedException(
292 301 'UserGroup assigned to %s' % (assigned,))
293 302 self.sa.delete(user_group)
294 303 except Exception:
295 304 log.error(traceback.format_exc())
296 305 raise
297 306
298 307 def _log_user_changes(self, action, user_group, user_or_users):
299 308 users = user_or_users
300 309 if not isinstance(users, (list, tuple)):
301 310 users = [users]
302 311
303 312 group_name = user_group.users_group_name
304 313
305 314 for user_or_user_id in users:
306 315 user = self._get_user(user_or_user_id)
307 316 log_text = 'User {user} {action} {group}'.format(
308 317 action=action, user=user.username, group=group_name)
309 318 action_logger_generic(log_text)
310 319
311 320 def _find_user_in_group(self, user, user_group):
312 321 user_group_member = None
313 322 for m in user_group.members:
314 323 if m.user_id == user.user_id:
315 324 # Found this user's membership row
316 325 user_group_member = m
317 326 break
318 327
319 328 return user_group_member
320 329
321 330 def _get_membership(self, user_group_id, user_id):
322 331 user_group_member = UserGroupMember(user_group_id, user_id)
323 332 return user_group_member
324 333
325 334 def add_user_to_group(self, user_group, user):
326 335 user_group = self._get_user_group(user_group)
327 336 user = self._get_user(user)
328 337 user_member = self._find_user_in_group(user, user_group)
329 338 if user_member:
330 339 # user already in the group, skip
331 340 return True
332 341
333 342 member = self._get_membership(
334 343 user_group.users_group_id, user.user_id)
335 344 user_group.members.append(member)
336 345
337 346 try:
338 347 self.sa.add(member)
339 348 except Exception:
340 349 # what could go wrong here?
341 350 log.error(traceback.format_exc())
342 351 raise
343 352
344 353 self._log_user_changes('added to', user_group, user)
345 354 return member
346 355
347 356 def remove_user_from_group(self, user_group, user):
348 357 user_group = self._get_user_group(user_group)
349 358 user = self._get_user(user)
350 359 user_group_member = self._find_user_in_group(user, user_group)
351 360
352 361 if not user_group_member:
353 362 # User isn't in that group
354 363 return False
355 364
356 365 try:
357 366 self.sa.delete(user_group_member)
358 367 except Exception:
359 368 log.error(traceback.format_exc())
360 369 raise
361 370
362 371 self._log_user_changes('removed from', user_group, user)
363 372 return True
364 373
365 374 def has_perm(self, user_group, perm):
366 375 user_group = self._get_user_group(user_group)
367 376 perm = self._get_perm(perm)
368 377
369 378 return UserGroupToPerm.query()\
370 379 .filter(UserGroupToPerm.users_group == user_group)\
371 380 .filter(UserGroupToPerm.permission == perm).scalar() is not None
372 381
373 382 def grant_perm(self, user_group, perm):
374 383 user_group = self._get_user_group(user_group)
375 384 perm = self._get_perm(perm)
376 385
377 386 # if this permission is already granted skip it
378 387 _perm = UserGroupToPerm.query()\
379 388 .filter(UserGroupToPerm.users_group == user_group)\
380 389 .filter(UserGroupToPerm.permission == perm)\
381 390 .scalar()
382 391 if _perm:
383 392 return
384 393
385 394 new = UserGroupToPerm()
386 395 new.users_group = user_group
387 396 new.permission = perm
388 397 self.sa.add(new)
389 398 return new
390 399
391 400 def revoke_perm(self, user_group, perm):
392 401 user_group = self._get_user_group(user_group)
393 402 perm = self._get_perm(perm)
394 403
395 404 obj = UserGroupToPerm.query()\
396 405 .filter(UserGroupToPerm.users_group == user_group)\
397 406 .filter(UserGroupToPerm.permission == perm).scalar()
398 407 if obj:
399 408 self.sa.delete(obj)
400 409
401 410 def grant_user_permission(self, user_group, user, perm):
402 411 """
403 412 Grant permission for user on given user group, or update
404 413 existing one if found
405 414
406 415 :param user_group: Instance of UserGroup, users_group_id,
407 416 or users_group_name
408 417 :param user: Instance of User, user_id or username
409 418 :param perm: Instance of Permission, or permission_name
410 419 """
411 420 changes = {
412 421 'added': [],
413 422 'updated': [],
414 423 'deleted': []
415 424 }
416 425
417 426 user_group = self._get_user_group(user_group)
418 427 user = self._get_user(user)
419 428 permission = self._get_perm(perm)
420 429 perm_name = permission.permission_name
421 430 member_id = user.user_id
422 431 member_name = user.username
423 432
424 433 # check if we have that permission already
425 434 obj = self.sa.query(UserUserGroupToPerm)\
426 435 .filter(UserUserGroupToPerm.user == user)\
427 436 .filter(UserUserGroupToPerm.user_group == user_group)\
428 437 .scalar()
429 438 if obj is None:
430 439 # create new !
431 440 obj = UserUserGroupToPerm()
432 441 obj.user_group = user_group
433 442 obj.user = user
434 443 obj.permission = permission
435 444 self.sa.add(obj)
436 445 log.debug('Granted perm %s to %s on %s', perm, user, user_group)
437 446 action_logger_generic(
438 447 'granted permission: {} to user: {} on usergroup: {}'.format(
439 448 perm, user, user_group), namespace='security.usergroup')
440 449
441 450 changes['added'].append({
442 451 'change_obj': user_group.get_api_data(),
443 452 'type': 'user', 'id': member_id,
444 453 'name': member_name, 'new_perm': perm_name})
445 454
446 455 return changes
447 456
448 457 def revoke_user_permission(self, user_group, user):
449 458 """
450 459 Revoke permission for user on given user group
451 460
452 461 :param user_group: Instance of UserGroup, users_group_id,
453 462 or users_group name
454 463 :param user: Instance of User, user_id or username
455 464 """
456 465 changes = {
457 466 'added': [],
458 467 'updated': [],
459 468 'deleted': []
460 469 }
461 470
462 471 user_group = self._get_user_group(user_group)
463 472 user = self._get_user(user)
464 473 perm_name = 'usergroup.none'
465 474 member_id = user.user_id
466 475 member_name = user.username
467 476
468 477 obj = self.sa.query(UserUserGroupToPerm)\
469 478 .filter(UserUserGroupToPerm.user == user)\
470 479 .filter(UserUserGroupToPerm.user_group == user_group)\
471 480 .scalar()
472 481 if obj:
473 482 self.sa.delete(obj)
474 483 log.debug('Revoked perm on %s on %s', user_group, user)
475 484 action_logger_generic(
476 485 'revoked permission from user: {} on usergroup: {}'.format(
477 486 user, user_group), namespace='security.usergroup')
478 487
479 488 changes['deleted'].append({
480 489 'change_obj': user_group.get_api_data(),
481 490 'type': 'user', 'id': member_id,
482 491 'name': member_name, 'new_perm': perm_name})
483 492
484 493 return changes
485 494
486 495 def grant_user_group_permission(self, target_user_group, user_group, perm):
487 496 """
488 497 Grant user group permission for given target_user_group
489 498
490 499 :param target_user_group:
491 500 :param user_group:
492 501 :param perm:
493 502 """
494 503 changes = {
495 504 'added': [],
496 505 'updated': [],
497 506 'deleted': []
498 507 }
499 508
500 509 target_user_group = self._get_user_group(target_user_group)
501 510 user_group = self._get_user_group(user_group)
502 511 permission = self._get_perm(perm)
503 512 perm_name = permission.permission_name
504 513 member_id = user_group.users_group_id
505 514 member_name = user_group.users_group_name
506 515
507 516 # forbid assigning same user group to itself
508 517 if target_user_group == user_group:
509 518 raise RepoGroupAssignmentError('target repo:%s cannot be '
510 519 'assigned to itself' % target_user_group)
511 520
512 521 # check if we have that permission already
513 522 obj = self.sa.query(UserGroupUserGroupToPerm)\
514 523 .filter(UserGroupUserGroupToPerm.target_user_group == target_user_group)\
515 524 .filter(UserGroupUserGroupToPerm.user_group == user_group)\
516 525 .scalar()
517 526 if obj is None:
518 527 # create new !
519 528 obj = UserGroupUserGroupToPerm()
520 529 obj.user_group = user_group
521 530 obj.target_user_group = target_user_group
522 531 obj.permission = permission
523 532 self.sa.add(obj)
524 533 log.debug(
525 534 'Granted perm %s to %s on %s', perm, target_user_group, user_group)
526 535 action_logger_generic(
527 536 'granted permission: {} to usergroup: {} on usergroup: {}'.format(
528 537 perm, user_group, target_user_group),
529 538 namespace='security.usergroup')
530 539
531 540 changes['added'].append({
532 541 'change_obj': target_user_group.get_api_data(),
533 542 'type': 'user_group', 'id': member_id,
534 543 'name': member_name, 'new_perm': perm_name})
535 544
536 545 return changes
537 546
538 547 def revoke_user_group_permission(self, target_user_group, user_group):
539 548 """
540 549 Revoke user group permission for given target_user_group
541 550
542 551 :param target_user_group:
543 552 :param user_group:
544 553 """
545 554 changes = {
546 555 'added': [],
547 556 'updated': [],
548 557 'deleted': []
549 558 }
550 559
551 560 target_user_group = self._get_user_group(target_user_group)
552 561 user_group = self._get_user_group(user_group)
553 562 perm_name = 'usergroup.none'
554 563 member_id = user_group.users_group_id
555 564 member_name = user_group.users_group_name
556 565
557 566 obj = self.sa.query(UserGroupUserGroupToPerm)\
558 567 .filter(UserGroupUserGroupToPerm.target_user_group == target_user_group)\
559 568 .filter(UserGroupUserGroupToPerm.user_group == user_group)\
560 569 .scalar()
561 570 if obj:
562 571 self.sa.delete(obj)
563 572 log.debug(
564 573 'Revoked perm on %s on %s', target_user_group, user_group)
565 574 action_logger_generic(
566 575 'revoked permission from usergroup: {} on usergroup: {}'.format(
567 576 user_group, target_user_group),
568 577 namespace='security.repogroup')
569 578
570 579 changes['deleted'].append({
571 580 'change_obj': target_user_group.get_api_data(),
572 581 'type': 'user_group', 'id': member_id,
573 582 'name': member_name, 'new_perm': perm_name})
574 583
575 584 return changes
576 585
577 586 def get_perms_summary(self, user_group_id):
578 587 permissions = {
579 588 'repositories': {},
580 589 'repositories_groups': {},
581 590 }
582 591 ugroup_repo_perms = UserGroupRepoToPerm.query()\
583 592 .options(joinedload(UserGroupRepoToPerm.permission))\
584 593 .options(joinedload(UserGroupRepoToPerm.repository))\
585 594 .filter(UserGroupRepoToPerm.users_group_id == user_group_id)\
586 595 .all()
587 596
588 597 for gr in ugroup_repo_perms:
589 598 permissions['repositories'][gr.repository.repo_name] \
590 599 = gr.permission.permission_name
591 600
592 601 ugroup_group_perms = UserGroupRepoGroupToPerm.query()\
593 602 .options(joinedload(UserGroupRepoGroupToPerm.permission))\
594 603 .options(joinedload(UserGroupRepoGroupToPerm.group))\
595 604 .filter(UserGroupRepoGroupToPerm.users_group_id == user_group_id)\
596 605 .all()
597 606
598 607 for gr in ugroup_group_perms:
599 608 permissions['repositories_groups'][gr.group.group_name] \
600 609 = gr.permission.permission_name
601 610 return permissions
602 611
603 612 def enforce_groups(self, user, groups, extern_type=None):
604 613 user = self._get_user(user)
605 614 current_groups = user.group_member
606 615
607 616 # find the external created groups, i.e automatically created
608 617 log.debug('Enforcing user group set `%s` on user %s', groups, user)
609 618 # calculate from what groups user should be removed
610 619 # external_groups that are not in groups
611 620 for gr in [x.users_group for x in current_groups]:
612 621 managed = gr.group_data.get('extern_type')
613 622 if managed:
614 623 if gr.users_group_name not in groups:
615 624 log.debug('Removing user %s from user group %s. '
616 625 'Group sync managed by: %s', user, gr, managed)
617 626 self.remove_user_from_group(gr, user)
618 627 else:
619 628 log.debug('Skipping removal from group %s since it is '
620 629 'not set to be automatically synchronized' % gr)
621 630
622 631 # now we calculate in which groups user should be == groups params
623 632 owner = User.get_first_super_admin().username
624 633 for gr in set(groups):
625 634 existing_group = UserGroup.get_by_group_name(gr)
626 635 if not existing_group:
627 636 desc = 'Automatically created from plugin:%s' % extern_type
628 637 # we use first admin account to set the owner of the group
629 638 existing_group = UserGroupModel().create(
630 639 gr, desc, owner, group_data={'extern_type': extern_type})
631 640
632 641 # we can only add users to groups which have set sync flag via
633 642 # extern_type attribute.
634 643 # This is either set and created via plugins, or manually
635 644 managed = existing_group.group_data.get('extern_type')
636 645 if managed:
637 646 log.debug('Adding user %s to user group %s', user, gr)
638 647 UserGroupModel().add_user_to_group(existing_group, user)
639 648 else:
640 649 log.debug('Skipping addition to group %s since it is '
641 650 'not set to be automatically synchronized' % gr)
642 651
643 652 def change_groups(self, user, groups):
644 653 """
645 654 This method changes user group assignment
646 655 :param user: User
647 656 :param groups: array of UserGroupModel
648 657 """
649 658 user = self._get_user(user)
650 659 log.debug('Changing user(%s) assignment to groups(%s)', user, groups)
651 660 current_groups = user.group_member
652 661 current_groups = [x.users_group for x in current_groups]
653 662
654 663 # calculate from what groups user should be removed/add
655 664 groups = set(groups)
656 665 current_groups = set(current_groups)
657 666
658 667 groups_to_remove = current_groups - groups
659 668 groups_to_add = groups - current_groups
660 669
661 670 removed_from_groups = []
662 671 added_to_groups = []
663 672 for gr in groups_to_remove:
664 673 log.debug('Removing user %s from user group %s',
665 674 user.username, gr.users_group_name)
666 675 removed_from_groups.append(gr.users_group_id)
667 676 self.remove_user_from_group(gr.users_group_name, user.username)
668 677 for gr in groups_to_add:
669 678 log.debug('Adding user %s to user group %s',
670 679 user.username, gr.users_group_name)
671 680 added_to_groups.append(gr.users_group_id)
672 681 UserGroupModel().add_user_to_group(
673 682 gr.users_group_name, user.username)
674 683
675 684 return added_to_groups, removed_from_groups
676 685
677 686 def _serialize_user_group(self, user_group):
678 687 import rhodecode.lib.helpers as h
679 688 return {
680 689 'id': user_group.users_group_id,
681 690 # TODO: marcink figure out a way to generate the url for the
682 691 # icon
683 692 'icon_link': '',
684 693 'value_display': 'Group: %s (%d members)' % (
685 694 user_group.users_group_name, len(user_group.members),),
686 695 'value': user_group.users_group_name,
687 696 'description': user_group.user_group_description,
688 697 'owner': user_group.user.username,
689 698
690 699 'owner_icon': h.gravatar_url(user_group.user.email, 30),
691 700 'value_display_owner': h.person(user_group.user.email),
692 701
693 702 'value_type': 'user_group',
694 703 'active': user_group.users_group_active,
695 704 }
696 705
697 706 def get_user_groups(self, name_contains=None, limit=20, only_active=True,
698 707 expand_groups=False):
699 708 query = self.sa.query(UserGroup)
700 709 if only_active:
701 710 query = query.filter(UserGroup.users_group_active == true())
702 711
703 712 if name_contains:
704 713 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
705 714 query = query.filter(
706 715 UserGroup.users_group_name.ilike(ilike_expression))\
707 716 .order_by(func.length(UserGroup.users_group_name))\
708 717 .order_by(UserGroup.users_group_name)
709 718
710 719 query = query.limit(limit)
711 720 user_groups = query.all()
712 721 perm_set = ['usergroup.read', 'usergroup.write', 'usergroup.admin']
713 722 user_groups = UserGroupList(user_groups, perm_set=perm_set)
714 723
715 724 # store same serialize method to extract data from User
716 725 from rhodecode.model.user import UserModel
717 726 serialize_user = UserModel()._serialize_user
718 727
719 728 _groups = []
720 729 for group in user_groups:
721 730 entry = self._serialize_user_group(group)
722 731 if expand_groups:
723 732 expanded_members = []
724 733 for member in group.members:
725 734 expanded_members.append(serialize_user(member.user))
726 735 entry['members'] = expanded_members
727 736 _groups.append(entry)
728 737 return _groups
729 738
730 739 @staticmethod
731 740 def get_user_groups_as_dict(user_group):
732 741 import rhodecode.lib.helpers as h
733 742
734 743 data = {
735 744 'users_group_id': user_group.users_group_id,
736 745 'group_name': h.link_to_group(user_group.users_group_name),
737 746 'group_description': user_group.user_group_description,
738 747 'active': user_group.users_group_active,
739 748 "owner": user_group.user.username,
740 749 'owner_icon': h.gravatar_url(user_group.user.email, 30),
741 750 "owner_data": {
742 751 'owner': user_group.user.username,
743 752 'owner_icon': h.gravatar_url(user_group.user.email, 30)}
744 753 }
745 754 return data
@@ -1,1115 +1,1115 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 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 Set of generic validators
23 23 """
24 24
25 25
26 26 import os
27 27 import re
28 28 import logging
29 29 import collections
30 30
31 31 import formencode
32 32 import ipaddress
33 33 from formencode.validators import (
34 34 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set,
35 35 NotEmpty, IPAddress, CIDR, String, FancyValidator
36 36 )
37 37
38 38 from sqlalchemy.sql.expression import true
39 39 from sqlalchemy.util import OrderedSet
40 40
41 41 from rhodecode.authentication import (
42 42 legacy_plugin_prefix, _import_legacy_plugin)
43 43 from rhodecode.authentication.base import loadplugin
44 44 from rhodecode.apps._base import ADMIN_PREFIX
45 45 from rhodecode.lib.auth import HasRepoGroupPermissionAny, HasPermissionAny
46 46 from rhodecode.lib.utils import repo_name_slug, make_db_config
47 47 from rhodecode.lib.utils2 import safe_int, str2bool, aslist, md5, safe_unicode
48 48 from rhodecode.lib.vcs.backends.git.repository import GitRepository
49 49 from rhodecode.lib.vcs.backends.hg.repository import MercurialRepository
50 50 from rhodecode.lib.vcs.backends.svn.repository import SubversionRepository
51 51 from rhodecode.model.db import (
52 52 RepoGroup, Repository, UserGroup, User, ChangesetStatus, Gist)
53 53 from rhodecode.model.settings import VcsSettingsModel
54 54
55 55 # silence warnings and pylint
56 56 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \
57 57 NotEmpty, IPAddress, CIDR, String, FancyValidator
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61
62 62 class _Missing(object):
63 63 pass
64 64
65 65
66 66 Missing = _Missing()
67 67
68 68
69 69 def M(self, key, state, **kwargs):
70 70 """
71 71 returns string from self.message based on given key,
72 72 passed kw params are used to substitute %(named)s params inside
73 73 translated strings
74 74
75 75 :param msg:
76 76 :param state:
77 77 """
78 78
79 79 #state._ = staticmethod(_)
80 80 # inject validator into state object
81 81 return self.message(key, state, **kwargs)
82 82
83 83
84 84 def UniqueList(localizer, convert=None):
85 85 _ = localizer
86 86
87 87 class _validator(formencode.FancyValidator):
88 88 """
89 89 Unique List !
90 90 """
91 91 messages = {
92 92 'empty': _(u'Value cannot be an empty list'),
93 93 'missing_value': _(u'Value cannot be an empty list'),
94 94 }
95 95
96 96 def _to_python(self, value, state):
97 97 ret_val = []
98 98
99 99 def make_unique(value):
100 100 seen = []
101 101 return [c for c in value if not (c in seen or seen.append(c))]
102 102
103 103 if isinstance(value, list):
104 104 ret_val = make_unique(value)
105 105 elif isinstance(value, set):
106 106 ret_val = make_unique(list(value))
107 107 elif isinstance(value, tuple):
108 108 ret_val = make_unique(list(value))
109 109 elif value is None:
110 110 ret_val = []
111 111 else:
112 112 ret_val = [value]
113 113
114 114 if convert:
115 115 ret_val = map(convert, ret_val)
116 116 return ret_val
117 117
118 118 def empty_value(self, value):
119 119 return []
120 120 return _validator
121 121
122 122
123 123 def UniqueListFromString(localizer):
124 124 _ = localizer
125 125
126 126 class _validator(UniqueList(localizer)):
127 127 def _to_python(self, value, state):
128 128 if isinstance(value, basestring):
129 129 value = aslist(value, ',')
130 130 return super(_validator, self)._to_python(value, state)
131 131 return _validator
132 132
133 133
134 134 def ValidSvnPattern(localizer, section, repo_name=None):
135 135 _ = localizer
136 136
137 137 class _validator(formencode.validators.FancyValidator):
138 138 messages = {
139 139 'pattern_exists': _(u'Pattern already exists'),
140 140 }
141 141
142 142 def validate_python(self, value, state):
143 143 if not value:
144 144 return
145 145 model = VcsSettingsModel(repo=repo_name)
146 146 ui_settings = model.get_svn_patterns(section=section)
147 147 for entry in ui_settings:
148 148 if value == entry.value:
149 149 msg = M(self, 'pattern_exists', state)
150 150 raise formencode.Invalid(msg, value, state)
151 151 return _validator
152 152
153 153
154 154 def ValidUsername(localizer, edit=False, old_data=None):
155 155 _ = localizer
156 156 old_data = old_data or {}
157 157
158 158 class _validator(formencode.validators.FancyValidator):
159 159 messages = {
160 160 'username_exists': _(u'Username "%(username)s" already exists'),
161 161 'system_invalid_username':
162 162 _(u'Username "%(username)s" is forbidden'),
163 163 'invalid_username':
164 164 _(u'Username may only contain alphanumeric characters '
165 165 u'underscores, periods or dashes and must begin with '
166 166 u'alphanumeric character or underscore')
167 167 }
168 168
169 169 def validate_python(self, value, state):
170 170 if value in ['default', 'new_user']:
171 171 msg = M(self, 'system_invalid_username', state, username=value)
172 172 raise formencode.Invalid(msg, value, state)
173 173 # check if user is unique
174 174 old_un = None
175 175 if edit:
176 176 old_un = User.get(old_data.get('user_id')).username
177 177
178 178 if old_un != value or not edit:
179 179 if User.get_by_username(value, case_insensitive=True):
180 180 msg = M(self, 'username_exists', state, username=value)
181 181 raise formencode.Invalid(msg, value, state)
182 182
183 183 if (re.match(r'^[\w]{1}[\w\-\.]{0,254}$', value)
184 184 is None):
185 185 msg = M(self, 'invalid_username', state)
186 186 raise formencode.Invalid(msg, value, state)
187 187 return _validator
188 188
189 189
190 190 def ValidRepoUser(localizer, allow_disabled=False):
191 191 _ = localizer
192 192
193 193 class _validator(formencode.validators.FancyValidator):
194 194 messages = {
195 195 'invalid_username': _(u'Username %(username)s is not valid'),
196 196 'disabled_username': _(u'Username %(username)s is disabled')
197 197 }
198 198
199 199 def validate_python(self, value, state):
200 200 try:
201 201 user = User.query().filter(User.username == value).one()
202 202 except Exception:
203 203 msg = M(self, 'invalid_username', state, username=value)
204 204 raise formencode.Invalid(
205 205 msg, value, state, error_dict={'username': msg}
206 206 )
207 207 if user and (not allow_disabled and not user.active):
208 208 msg = M(self, 'disabled_username', state, username=value)
209 209 raise formencode.Invalid(
210 210 msg, value, state, error_dict={'username': msg}
211 211 )
212 212 return _validator
213 213
214 214
215 215 def ValidUserGroup(localizer, edit=False, old_data=None):
216 216 _ = localizer
217 217 old_data = old_data or {}
218 218
219 219 class _validator(formencode.validators.FancyValidator):
220 220 messages = {
221 221 'invalid_group': _(u'Invalid user group name'),
222 222 'group_exist': _(u'User group `%(usergroup)s` already exists'),
223 223 'invalid_usergroup_name':
224 224 _(u'user group name may only contain alphanumeric '
225 225 u'characters underscores, periods or dashes and must begin '
226 226 u'with alphanumeric character')
227 227 }
228 228
229 229 def validate_python(self, value, state):
230 230 if value in ['default']:
231 231 msg = M(self, 'invalid_group', state)
232 232 raise formencode.Invalid(
233 233 msg, value, state, error_dict={'users_group_name': msg}
234 234 )
235 235 # check if group is unique
236 236 old_ugname = None
237 237 if edit:
238 238 old_id = old_data.get('users_group_id')
239 239 old_ugname = UserGroup.get(old_id).users_group_name
240 240
241 241 if old_ugname != value or not edit:
242 242 is_existing_group = UserGroup.get_by_group_name(
243 243 value, case_insensitive=True)
244 244 if is_existing_group:
245 245 msg = M(self, 'group_exist', state, usergroup=value)
246 246 raise formencode.Invalid(
247 247 msg, value, state, error_dict={'users_group_name': msg}
248 248 )
249 249
250 250 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
251 251 msg = M(self, 'invalid_usergroup_name', state)
252 252 raise formencode.Invalid(
253 253 msg, value, state, error_dict={'users_group_name': msg}
254 254 )
255 255 return _validator
256 256
257 257
258 258 def ValidRepoGroup(localizer, edit=False, old_data=None, can_create_in_root=False):
259 259 _ = localizer
260 260 old_data = old_data or {}
261 261
262 262 class _validator(formencode.validators.FancyValidator):
263 263 messages = {
264 264 'group_parent_id': _(u'Cannot assign this group as parent'),
265 265 'group_exists': _(u'Group "%(group_name)s" already exists'),
266 266 'repo_exists': _(u'Repository with name "%(group_name)s" '
267 267 u'already exists'),
268 268 'permission_denied': _(u"no permission to store repository group"
269 269 u"in this location"),
270 270 'permission_denied_root': _(
271 271 u"no permission to store repository group "
272 272 u"in root location")
273 273 }
274 274
275 275 def _to_python(self, value, state):
276 276 group_name = repo_name_slug(value.get('group_name', ''))
277 277 group_parent_id = safe_int(value.get('group_parent_id'))
278 278 gr = RepoGroup.get(group_parent_id)
279 279 if gr:
280 280 parent_group_path = gr.full_path
281 281 # value needs to be aware of group name in order to check
282 282 # db key This is an actual just the name to store in the
283 283 # database
284 284 group_name_full = (
285 285 parent_group_path + RepoGroup.url_sep() + group_name)
286 286 else:
287 287 group_name_full = group_name
288 288
289 289 value['group_name'] = group_name
290 290 value['group_name_full'] = group_name_full
291 291 value['group_parent_id'] = group_parent_id
292 292 return value
293 293
294 294 def validate_python(self, value, state):
295 295
296 296 old_group_name = None
297 297 group_name = value.get('group_name')
298 298 group_name_full = value.get('group_name_full')
299 299 group_parent_id = safe_int(value.get('group_parent_id'))
300 300 if group_parent_id == -1:
301 301 group_parent_id = None
302 302
303 303 group_obj = RepoGroup.get(old_data.get('group_id'))
304 304 parent_group_changed = False
305 305 if edit:
306 306 old_group_name = group_obj.group_name
307 307 old_group_parent_id = group_obj.group_parent_id
308 308
309 309 if group_parent_id != old_group_parent_id:
310 310 parent_group_changed = True
311 311
312 312 # TODO: mikhail: the following if statement is not reached
313 313 # since group_parent_id's OneOf validation fails before.
314 314 # Can be removed.
315 315
316 316 # check against setting a parent of self
317 317 parent_of_self = (
318 318 old_data['group_id'] == group_parent_id
319 319 if group_parent_id else False
320 320 )
321 321 if parent_of_self:
322 322 msg = M(self, 'group_parent_id', state)
323 323 raise formencode.Invalid(
324 324 msg, value, state, error_dict={'group_parent_id': msg}
325 325 )
326 326
327 327 # group we're moving current group inside
328 328 child_group = None
329 329 if group_parent_id:
330 330 child_group = RepoGroup.query().filter(
331 331 RepoGroup.group_id == group_parent_id).scalar()
332 332
333 333 # do a special check that we cannot move a group to one of
334 334 # it's children
335 335 if edit and child_group:
336 336 parents = [x.group_id for x in child_group.parents]
337 337 move_to_children = old_data['group_id'] in parents
338 338 if move_to_children:
339 339 msg = M(self, 'group_parent_id', state)
340 340 raise formencode.Invalid(
341 341 msg, value, state, error_dict={'group_parent_id': msg})
342 342
343 343 # Check if we have permission to store in the parent.
344 344 # Only check if the parent group changed.
345 345 if parent_group_changed:
346 346 if child_group is None:
347 347 if not can_create_in_root:
348 348 msg = M(self, 'permission_denied_root', state)
349 349 raise formencode.Invalid(
350 350 msg, value, state,
351 351 error_dict={'group_parent_id': msg})
352 352 else:
353 353 valid = HasRepoGroupPermissionAny('group.admin')
354 354 forbidden = not valid(
355 355 child_group.group_name, 'can create group validator')
356 356 if forbidden:
357 357 msg = M(self, 'permission_denied', state)
358 358 raise formencode.Invalid(
359 359 msg, value, state,
360 360 error_dict={'group_parent_id': msg})
361 361
362 362 # if we change the name or it's new group, check for existing names
363 363 # or repositories with the same name
364 364 if old_group_name != group_name_full or not edit:
365 365 # check group
366 366 gr = RepoGroup.get_by_group_name(group_name_full)
367 367 if gr:
368 368 msg = M(self, 'group_exists', state, group_name=group_name)
369 369 raise formencode.Invalid(
370 370 msg, value, state, error_dict={'group_name': msg})
371 371
372 372 # check for same repo
373 373 repo = Repository.get_by_repo_name(group_name_full)
374 374 if repo:
375 375 msg = M(self, 'repo_exists', state, group_name=group_name)
376 376 raise formencode.Invalid(
377 377 msg, value, state, error_dict={'group_name': msg})
378 378 return _validator
379 379
380 380
381 381 def ValidPassword(localizer):
382 382 _ = localizer
383 383
384 384 class _validator(formencode.validators.FancyValidator):
385 385 messages = {
386 386 'invalid_password':
387 387 _(u'Invalid characters (non-ascii) in password')
388 388 }
389 389
390 390 def validate_python(self, value, state):
391 391 try:
392 392 (value or '').decode('ascii')
393 393 except UnicodeError:
394 394 msg = M(self, 'invalid_password', state)
395 395 raise formencode.Invalid(msg, value, state,)
396 396 return _validator
397 397
398 398
399 399 def ValidPasswordsMatch(
400 400 localizer, passwd='new_password',
401 401 passwd_confirmation='password_confirmation'):
402 402 _ = localizer
403 403
404 404 class _validator(formencode.validators.FancyValidator):
405 405 messages = {
406 406 'password_mismatch': _(u'Passwords do not match'),
407 407 }
408 408
409 409 def validate_python(self, value, state):
410 410
411 411 pass_val = value.get('password') or value.get(passwd)
412 412 if pass_val != value[passwd_confirmation]:
413 413 msg = M(self, 'password_mismatch', state)
414 414 raise formencode.Invalid(
415 415 msg, value, state,
416 416 error_dict={passwd: msg, passwd_confirmation: msg}
417 417 )
418 418 return _validator
419 419
420 420
421 421 def ValidAuth(localizer):
422 422 _ = localizer
423 423
424 424 class _validator(formencode.validators.FancyValidator):
425 425 messages = {
426 426 'invalid_password': _(u'invalid password'),
427 427 'invalid_username': _(u'invalid user name'),
428 428 'disabled_account': _(u'Your account is disabled')
429 429 }
430 430
431 431 def validate_python(self, value, state):
432 432 from rhodecode.authentication.base import authenticate, HTTP_TYPE
433 433
434 434 password = value['password']
435 435 username = value['username']
436 436
437 437 if not authenticate(username, password, '', HTTP_TYPE,
438 438 skip_missing=True):
439 439 user = User.get_by_username(username)
440 440 if user and not user.active:
441 441 log.warning('user %s is disabled', username)
442 442 msg = M(self, 'disabled_account', state)
443 443 raise formencode.Invalid(
444 444 msg, value, state, error_dict={'username': msg}
445 445 )
446 446 else:
447 447 log.warning('user `%s` failed to authenticate', username)
448 448 msg = M(self, 'invalid_username', state)
449 449 msg2 = M(self, 'invalid_password', state)
450 450 raise formencode.Invalid(
451 451 msg, value, state,
452 452 error_dict={'username': msg, 'password': msg2}
453 453 )
454 454 return _validator
455 455
456 456
457 457 def ValidRepoName(localizer, edit=False, old_data=None):
458 458 old_data = old_data or {}
459 459 _ = localizer
460 460
461 461 class _validator(formencode.validators.FancyValidator):
462 462 messages = {
463 463 'invalid_repo_name':
464 464 _(u'Repository name %(repo)s is disallowed'),
465 465 # top level
466 466 'repository_exists': _(u'Repository with name %(repo)s '
467 467 u'already exists'),
468 468 'group_exists': _(u'Repository group with name "%(repo)s" '
469 469 u'already exists'),
470 470 # inside a group
471 471 'repository_in_group_exists': _(u'Repository with name %(repo)s '
472 472 u'exists in group "%(group)s"'),
473 473 'group_in_group_exists': _(
474 474 u'Repository group with name "%(repo)s" '
475 475 u'exists in group "%(group)s"'),
476 476 }
477 477
478 478 def _to_python(self, value, state):
479 479 repo_name = repo_name_slug(value.get('repo_name', ''))
480 480 repo_group = value.get('repo_group')
481 481 if repo_group:
482 482 gr = RepoGroup.get(repo_group)
483 483 group_path = gr.full_path
484 484 group_name = gr.group_name
485 485 # value needs to be aware of group name in order to check
486 486 # db key This is an actual just the name to store in the
487 487 # database
488 488 repo_name_full = group_path + RepoGroup.url_sep() + repo_name
489 489 else:
490 490 group_name = group_path = ''
491 491 repo_name_full = repo_name
492 492
493 493 value['repo_name'] = repo_name
494 494 value['repo_name_full'] = repo_name_full
495 495 value['group_path'] = group_path
496 496 value['group_name'] = group_name
497 497 return value
498 498
499 499 def validate_python(self, value, state):
500 500
501 501 repo_name = value.get('repo_name')
502 502 repo_name_full = value.get('repo_name_full')
503 503 group_path = value.get('group_path')
504 504 group_name = value.get('group_name')
505 505
506 506 if repo_name in [ADMIN_PREFIX, '']:
507 507 msg = M(self, 'invalid_repo_name', state, repo=repo_name)
508 508 raise formencode.Invalid(
509 509 msg, value, state, error_dict={'repo_name': msg})
510 510
511 511 rename = old_data.get('repo_name') != repo_name_full
512 512 create = not edit
513 513 if rename or create:
514 514
515 515 if group_path:
516 516 if Repository.get_by_repo_name(repo_name_full):
517 517 msg = M(self, 'repository_in_group_exists', state,
518 518 repo=repo_name, group=group_name)
519 519 raise formencode.Invalid(
520 520 msg, value, state, error_dict={'repo_name': msg})
521 521 if RepoGroup.get_by_group_name(repo_name_full):
522 522 msg = M(self, 'group_in_group_exists', state,
523 523 repo=repo_name, group=group_name)
524 524 raise formencode.Invalid(
525 525 msg, value, state, error_dict={'repo_name': msg})
526 526 else:
527 527 if RepoGroup.get_by_group_name(repo_name_full):
528 528 msg = M(self, 'group_exists', state, repo=repo_name)
529 529 raise formencode.Invalid(
530 530 msg, value, state, error_dict={'repo_name': msg})
531 531
532 532 if Repository.get_by_repo_name(repo_name_full):
533 533 msg = M(
534 534 self, 'repository_exists', state, repo=repo_name)
535 535 raise formencode.Invalid(
536 536 msg, value, state, error_dict={'repo_name': msg})
537 537 return value
538 538 return _validator
539 539
540 540
541 541 def ValidForkName(localizer, *args, **kwargs):
542 542 _ = localizer
543 543
544 544 return ValidRepoName(localizer, *args, **kwargs)
545 545
546 546
547 547 def SlugifyName(localizer):
548 548 _ = localizer
549 549
550 550 class _validator(formencode.validators.FancyValidator):
551 551
552 552 def _to_python(self, value, state):
553 553 return repo_name_slug(value)
554 554
555 555 def validate_python(self, value, state):
556 556 pass
557 557 return _validator
558 558
559 559
560 560 def CannotHaveGitSuffix(localizer):
561 561 _ = localizer
562 562
563 563 class _validator(formencode.validators.FancyValidator):
564 564 messages = {
565 565 'has_git_suffix':
566 566 _(u'Repository name cannot end with .git'),
567 567 }
568 568
569 569 def _to_python(self, value, state):
570 570 return value
571 571
572 572 def validate_python(self, value, state):
573 573 if value and value.endswith('.git'):
574 574 msg = M(
575 575 self, 'has_git_suffix', state)
576 576 raise formencode.Invalid(
577 577 msg, value, state, error_dict={'repo_name': msg})
578 578 return _validator
579 579
580 580
581 581 def ValidCloneUri(localizer):
582 582 _ = localizer
583 583
584 584 class InvalidCloneUrl(Exception):
585 585 allowed_prefixes = ()
586 586
587 587 def url_handler(repo_type, url):
588 588 config = make_db_config(clear_session=False)
589 589 if repo_type == 'hg':
590 590 allowed_prefixes = ('http', 'svn+http', 'git+http')
591 591
592 592 if 'http' in url[:4]:
593 593 # initially check if it's at least the proper URL
594 594 # or does it pass basic auth
595 595 MercurialRepository.check_url(url, config)
596 596 elif 'svn+http' in url[:8]: # svn->hg import
597 597 SubversionRepository.check_url(url, config)
598 598 elif 'git+http' in url[:8]: # git->hg import
599 599 raise NotImplementedError()
600 600 else:
601 601 exc = InvalidCloneUrl('Clone from URI %s not allowed. '
602 602 'Allowed url must start with one of %s'
603 603 % (url, ','.join(allowed_prefixes)))
604 604 exc.allowed_prefixes = allowed_prefixes
605 605 raise exc
606 606
607 607 elif repo_type == 'git':
608 608 allowed_prefixes = ('http', 'svn+http', 'hg+http')
609 609 if 'http' in url[:4]:
610 610 # initially check if it's at least the proper URL
611 611 # or does it pass basic auth
612 612 GitRepository.check_url(url, config)
613 613 elif 'svn+http' in url[:8]: # svn->git import
614 614 raise NotImplementedError()
615 615 elif 'hg+http' in url[:8]: # hg->git import
616 616 raise NotImplementedError()
617 617 else:
618 618 exc = InvalidCloneUrl('Clone from URI %s not allowed. '
619 619 'Allowed url must start with one of %s'
620 620 % (url, ','.join(allowed_prefixes)))
621 621 exc.allowed_prefixes = allowed_prefixes
622 622 raise exc
623 623
624 624 class _validator(formencode.validators.FancyValidator):
625 625 messages = {
626 626 'clone_uri': _(u'invalid clone url for %(rtype)s repository'),
627 627 'invalid_clone_uri': _(
628 628 u'Invalid clone url, provide a valid clone '
629 629 u'url starting with one of %(allowed_prefixes)s')
630 630 }
631 631
632 632 def validate_python(self, value, state):
633 633 repo_type = value.get('repo_type')
634 634 url = value.get('clone_uri')
635 635
636 636 if url:
637 637 try:
638 638 url_handler(repo_type, url)
639 639 except InvalidCloneUrl as e:
640 640 log.warning(e)
641 641 msg = M(self, 'invalid_clone_uri', state, rtype=repo_type,
642 642 allowed_prefixes=','.join(e.allowed_prefixes))
643 643 raise formencode.Invalid(msg, value, state,
644 644 error_dict={'clone_uri': msg})
645 645 except Exception:
646 646 log.exception('Url validation failed')
647 647 msg = M(self, 'clone_uri', state, rtype=repo_type)
648 648 raise formencode.Invalid(msg, value, state,
649 649 error_dict={'clone_uri': msg})
650 650 return _validator
651 651
652 652
653 653 def ValidForkType(localizer, old_data=None):
654 654 _ = localizer
655 655 old_data = old_data or {}
656 656
657 657 class _validator(formencode.validators.FancyValidator):
658 658 messages = {
659 659 'invalid_fork_type': _(u'Fork have to be the same type as parent')
660 660 }
661 661
662 662 def validate_python(self, value, state):
663 663 if old_data['repo_type'] != value:
664 664 msg = M(self, 'invalid_fork_type', state)
665 665 raise formencode.Invalid(
666 666 msg, value, state, error_dict={'repo_type': msg}
667 667 )
668 668 return _validator
669 669
670 670
671 671 def CanWriteGroup(localizer, old_data=None):
672 672 _ = localizer
673 673
674 674 class _validator(formencode.validators.FancyValidator):
675 675 messages = {
676 676 'permission_denied': _(
677 677 u"You do not have the permission "
678 678 u"to create repositories in this group."),
679 679 'permission_denied_root': _(
680 680 u"You do not have the permission to store repositories in "
681 681 u"the root location.")
682 682 }
683 683
684 684 def _to_python(self, value, state):
685 685 # root location
686 686 if value in [-1, "-1"]:
687 687 return None
688 688 return value
689 689
690 690 def validate_python(self, value, state):
691 691 gr = RepoGroup.get(value)
692 692 gr_name = gr.group_name if gr else None # None means ROOT location
693 693 # create repositories with write permission on group is set to true
694 694 create_on_write = HasPermissionAny(
695 695 'hg.create.write_on_repogroup.true')()
696 696 group_admin = HasRepoGroupPermissionAny('group.admin')(
697 697 gr_name, 'can write into group validator')
698 698 group_write = HasRepoGroupPermissionAny('group.write')(
699 699 gr_name, 'can write into group validator')
700 700 forbidden = not (group_admin or (group_write and create_on_write))
701 701 can_create_repos = HasPermissionAny(
702 702 'hg.admin', 'hg.create.repository')
703 703 gid = (old_data['repo_group'].get('group_id')
704 704 if (old_data and 'repo_group' in old_data) else None)
705 705 value_changed = gid != safe_int(value)
706 706 new = not old_data
707 707 # do check if we changed the value, there's a case that someone got
708 708 # revoked write permissions to a repository, he still created, we
709 709 # don't need to check permission if he didn't change the value of
710 710 # groups in form box
711 711 if value_changed or new:
712 712 # parent group need to be existing
713 713 if gr and forbidden:
714 714 msg = M(self, 'permission_denied', state)
715 715 raise formencode.Invalid(
716 716 msg, value, state, error_dict={'repo_type': msg}
717 717 )
718 718 # check if we can write to root location !
719 719 elif gr is None and not can_create_repos():
720 720 msg = M(self, 'permission_denied_root', state)
721 721 raise formencode.Invalid(
722 722 msg, value, state, error_dict={'repo_type': msg}
723 723 )
724 724 return _validator
725 725
726 726
727 727 def ValidPerms(localizer, type_='repo'):
728 728 _ = localizer
729 729 if type_ == 'repo_group':
730 730 EMPTY_PERM = 'group.none'
731 731 elif type_ == 'repo':
732 732 EMPTY_PERM = 'repository.none'
733 733 elif type_ == 'user_group':
734 734 EMPTY_PERM = 'usergroup.none'
735 735
736 736 class _validator(formencode.validators.FancyValidator):
737 737 messages = {
738 738 'perm_new_member_name':
739 739 _(u'This username or user group name is not valid')
740 740 }
741 741
742 742 def _to_python(self, value, state):
743 743 perm_updates = OrderedSet()
744 744 perm_additions = OrderedSet()
745 745 perm_deletions = OrderedSet()
746 746 # build a list of permission to update/delete and new permission
747 747
748 748 # Read the perm_new_member/perm_del_member attributes and group
749 749 # them by they IDs
750 750 new_perms_group = collections.defaultdict(dict)
751 751 del_perms_group = collections.defaultdict(dict)
752 752 for k, v in value.copy().iteritems():
753 753 if k.startswith('perm_del_member'):
754 754 # delete from org storage so we don't process that later
755 755 del value[k]
756 756 # part is `id`, `type`
757 757 _type, part = k.split('perm_del_member_')
758 758 args = part.split('_')
759 759 if len(args) == 2:
760 760 _key, pos = args
761 761 del_perms_group[pos][_key] = v
762 762 if k.startswith('perm_new_member'):
763 763 # delete from org storage so we don't process that later
764 764 del value[k]
765 765 # part is `id`, `type`, `perm`
766 766 _type, part = k.split('perm_new_member_')
767 767 args = part.split('_')
768 768 if len(args) == 2:
769 769 _key, pos = args
770 770 new_perms_group[pos][_key] = v
771 771
772 772 # store the deletes
773 773 for k in sorted(del_perms_group.keys()):
774 774 perm_dict = del_perms_group[k]
775 775 del_member = perm_dict.get('id')
776 776 del_type = perm_dict.get('type')
777 777 if del_member and del_type:
778 778 perm_deletions.add(
779 779 (del_member, None, del_type))
780 780
781 781 # store additions in order of how they were added in web form
782 782 for k in sorted(new_perms_group.keys()):
783 783 perm_dict = new_perms_group[k]
784 784 new_member = perm_dict.get('id')
785 785 new_type = perm_dict.get('type')
786 786 new_perm = perm_dict.get('perm')
787 787 if new_member and new_perm and new_type:
788 788 perm_additions.add(
789 789 (new_member, new_perm, new_type))
790 790
791 791 # get updates of permissions
792 792 # (read the existing radio button states)
793 793 default_user_id = User.get_default_user().user_id
794 794
795 795 for k, update_value in value.iteritems():
796 796 if k.startswith('u_perm_') or k.startswith('g_perm_'):
797 797 obj_type = k[0]
798 798 obj_id = k[7:]
799 799 update_type = {'u': 'user',
800 'g': 'users_group'}[obj_type]
800 'g': 'user_group'}[obj_type]
801 801
802 802 if obj_type == 'u' and safe_int(obj_id) == default_user_id:
803 803 if str2bool(value.get('repo_private')):
804 804 # prevent from updating default user permissions
805 805 # when this repository is marked as private
806 806 update_value = EMPTY_PERM
807 807
808 808 perm_updates.add(
809 809 (obj_id, update_value, update_type))
810 810
811 811 value['perm_additions'] = [] # propagated later
812 812 value['perm_updates'] = list(perm_updates)
813 813 value['perm_deletions'] = list(perm_deletions)
814 814
815 815 updates_map = dict(
816 816 (x[0], (x[1], x[2])) for x in value['perm_updates'])
817 817 # make sure Additions don't override updates.
818 818 for member_id, perm, member_type in list(perm_additions):
819 819 if member_id in updates_map:
820 820 perm = updates_map[member_id][0]
821 821 value['perm_additions'].append((member_id, perm, member_type))
822 822
823 823 # on new entries validate users they exist and they are active !
824 824 # this leaves feedback to the form
825 825 try:
826 826 if member_type == 'user':
827 827 User.query()\
828 828 .filter(User.active == true())\
829 829 .filter(User.user_id == member_id).one()
830 if member_type == 'users_group':
830 if member_type == 'user_group':
831 831 UserGroup.query()\
832 832 .filter(UserGroup.users_group_active == true())\
833 833 .filter(UserGroup.users_group_id == member_id)\
834 834 .one()
835 835
836 836 except Exception:
837 837 log.exception('Updated permission failed: org_exc:')
838 838 msg = M(self, 'perm_new_member_type', state)
839 839 raise formencode.Invalid(
840 840 msg, value, state, error_dict={
841 841 'perm_new_member_name': msg}
842 842 )
843 843 return value
844 844 return _validator
845 845
846 846
847 847 def ValidPath(localizer):
848 848 _ = localizer
849 849
850 850 class _validator(formencode.validators.FancyValidator):
851 851 messages = {
852 852 'invalid_path': _(u'This is not a valid path')
853 853 }
854 854
855 855 def validate_python(self, value, state):
856 856 if not os.path.isdir(value):
857 857 msg = M(self, 'invalid_path', state)
858 858 raise formencode.Invalid(
859 859 msg, value, state, error_dict={'paths_root_path': msg}
860 860 )
861 861 return _validator
862 862
863 863
864 864 def UniqSystemEmail(localizer, old_data=None):
865 865 _ = localizer
866 866 old_data = old_data or {}
867 867
868 868 class _validator(formencode.validators.FancyValidator):
869 869 messages = {
870 870 'email_taken': _(u'This e-mail address is already taken')
871 871 }
872 872
873 873 def _to_python(self, value, state):
874 874 return value.lower()
875 875
876 876 def validate_python(self, value, state):
877 877 if (old_data.get('email') or '').lower() != value:
878 878 user = User.get_by_email(value, case_insensitive=True)
879 879 if user:
880 880 msg = M(self, 'email_taken', state)
881 881 raise formencode.Invalid(
882 882 msg, value, state, error_dict={'email': msg}
883 883 )
884 884 return _validator
885 885
886 886
887 887 def ValidSystemEmail(localizer):
888 888 _ = localizer
889 889
890 890 class _validator(formencode.validators.FancyValidator):
891 891 messages = {
892 892 'non_existing_email': _(u'e-mail "%(email)s" does not exist.')
893 893 }
894 894
895 895 def _to_python(self, value, state):
896 896 return value.lower()
897 897
898 898 def validate_python(self, value, state):
899 899 user = User.get_by_email(value, case_insensitive=True)
900 900 if user is None:
901 901 msg = M(self, 'non_existing_email', state, email=value)
902 902 raise formencode.Invalid(
903 903 msg, value, state, error_dict={'email': msg}
904 904 )
905 905 return _validator
906 906
907 907
908 908 def NotReviewedRevisions(localizer, repo_id):
909 909 _ = localizer
910 910 class _validator(formencode.validators.FancyValidator):
911 911 messages = {
912 912 'rev_already_reviewed':
913 913 _(u'Revisions %(revs)s are already part of pull request '
914 914 u'or have set status'),
915 915 }
916 916
917 917 def validate_python(self, value, state):
918 918 # check revisions if they are not reviewed, or a part of another
919 919 # pull request
920 920 statuses = ChangesetStatus.query()\
921 921 .filter(ChangesetStatus.revision.in_(value))\
922 922 .filter(ChangesetStatus.repo_id == repo_id)\
923 923 .all()
924 924
925 925 errors = []
926 926 for status in statuses:
927 927 if status.pull_request_id:
928 928 errors.append(['pull_req', status.revision[:12]])
929 929 elif status.status:
930 930 errors.append(['status', status.revision[:12]])
931 931
932 932 if errors:
933 933 revs = ','.join([x[1] for x in errors])
934 934 msg = M(self, 'rev_already_reviewed', state, revs=revs)
935 935 raise formencode.Invalid(
936 936 msg, value, state, error_dict={'revisions': revs})
937 937
938 938 return _validator
939 939
940 940
941 941 def ValidIp(localizer):
942 942 _ = localizer
943 943
944 944 class _validator(CIDR):
945 945 messages = {
946 946 'badFormat': _(u'Please enter a valid IPv4 or IpV6 address'),
947 947 'illegalBits': _(
948 948 u'The network size (bits) must be within the range '
949 949 u'of 0-32 (not %(bits)r)'),
950 950 }
951 951
952 952 # we ovveride the default to_python() call
953 953 def to_python(self, value, state):
954 954 v = super(_validator, self).to_python(value, state)
955 955 v = safe_unicode(v.strip())
956 956 net = ipaddress.ip_network(address=v, strict=False)
957 957 return str(net)
958 958
959 959 def validate_python(self, value, state):
960 960 try:
961 961 addr = safe_unicode(value.strip())
962 962 # this raises an ValueError if address is not IpV4 or IpV6
963 963 ipaddress.ip_network(addr, strict=False)
964 964 except ValueError:
965 965 raise formencode.Invalid(self.message('badFormat', state),
966 966 value, state)
967 967 return _validator
968 968
969 969
970 970 def FieldKey(localizer):
971 971 _ = localizer
972 972
973 973 class _validator(formencode.validators.FancyValidator):
974 974 messages = {
975 975 'badFormat': _(
976 976 u'Key name can only consist of letters, '
977 977 u'underscore, dash or numbers'),
978 978 }
979 979
980 980 def validate_python(self, value, state):
981 981 if not re.match('[a-zA-Z0-9_-]+$', value):
982 982 raise formencode.Invalid(self.message('badFormat', state),
983 983 value, state)
984 984 return _validator
985 985
986 986
987 987 def ValidAuthPlugins(localizer):
988 988 _ = localizer
989 989
990 990 class _validator(formencode.validators.FancyValidator):
991 991 messages = {
992 992 'import_duplicate': _(
993 993 u'Plugins %(loaded)s and %(next_to_load)s '
994 994 u'both export the same name'),
995 995 'missing_includeme': _(
996 996 u'The plugin "%(plugin_id)s" is missing an includeme '
997 997 u'function.'),
998 998 'import_error': _(
999 999 u'Can not load plugin "%(plugin_id)s"'),
1000 1000 'no_plugin': _(
1001 1001 u'No plugin available with ID "%(plugin_id)s"'),
1002 1002 }
1003 1003
1004 1004 def _to_python(self, value, state):
1005 1005 # filter empty values
1006 1006 return filter(lambda s: s not in [None, ''], value)
1007 1007
1008 1008 def _validate_legacy_plugin_id(self, plugin_id, value, state):
1009 1009 """
1010 1010 Validates that the plugin import works. It also checks that the
1011 1011 plugin has an includeme attribute.
1012 1012 """
1013 1013 try:
1014 1014 plugin = _import_legacy_plugin(plugin_id)
1015 1015 except Exception as e:
1016 1016 log.exception(
1017 1017 'Exception during import of auth legacy plugin "{}"'
1018 1018 .format(plugin_id))
1019 1019 msg = M(self, 'import_error', state, plugin_id=plugin_id)
1020 1020 raise formencode.Invalid(msg, value, state)
1021 1021
1022 1022 if not hasattr(plugin, 'includeme'):
1023 1023 msg = M(self, 'missing_includeme', state, plugin_id=plugin_id)
1024 1024 raise formencode.Invalid(msg, value, state)
1025 1025
1026 1026 return plugin
1027 1027
1028 1028 def _validate_plugin_id(self, plugin_id, value, state):
1029 1029 """
1030 1030 Plugins are already imported during app start up. Therefore this
1031 1031 validation only retrieves the plugin from the plugin registry and
1032 1032 if it returns something not None everything is OK.
1033 1033 """
1034 1034 plugin = loadplugin(plugin_id)
1035 1035
1036 1036 if plugin is None:
1037 1037 msg = M(self, 'no_plugin', state, plugin_id=plugin_id)
1038 1038 raise formencode.Invalid(msg, value, state)
1039 1039
1040 1040 return plugin
1041 1041
1042 1042 def validate_python(self, value, state):
1043 1043 unique_names = {}
1044 1044 for plugin_id in value:
1045 1045
1046 1046 # Validate legacy or normal plugin.
1047 1047 if plugin_id.startswith(legacy_plugin_prefix):
1048 1048 plugin = self._validate_legacy_plugin_id(
1049 1049 plugin_id, value, state)
1050 1050 else:
1051 1051 plugin = self._validate_plugin_id(plugin_id, value, state)
1052 1052
1053 1053 # Only allow unique plugin names.
1054 1054 if plugin.name in unique_names:
1055 1055 msg = M(self, 'import_duplicate', state,
1056 1056 loaded=unique_names[plugin.name],
1057 1057 next_to_load=plugin)
1058 1058 raise formencode.Invalid(msg, value, state)
1059 1059 unique_names[plugin.name] = plugin
1060 1060 return _validator
1061 1061
1062 1062
1063 1063 def ValidPattern(localizer):
1064 1064 _ = localizer
1065 1065
1066 1066 class _validator(formencode.validators.FancyValidator):
1067 1067 messages = {
1068 1068 'bad_format': _(u'Url must start with http or /'),
1069 1069 }
1070 1070
1071 1071 def _to_python(self, value, state):
1072 1072 patterns = []
1073 1073
1074 1074 prefix = 'new_pattern'
1075 1075 for name, v in value.iteritems():
1076 1076 pattern_name = '_'.join((prefix, 'pattern'))
1077 1077 if name.startswith(pattern_name):
1078 1078 new_item_id = name[len(pattern_name)+1:]
1079 1079
1080 1080 def _field(name):
1081 1081 return '%s_%s_%s' % (prefix, name, new_item_id)
1082 1082
1083 1083 values = {
1084 1084 'issuetracker_pat': value.get(_field('pattern')),
1085 1085 'issuetracker_url': value.get(_field('url')),
1086 1086 'issuetracker_pref': value.get(_field('prefix')),
1087 1087 'issuetracker_desc': value.get(_field('description'))
1088 1088 }
1089 1089 new_uid = md5(values['issuetracker_pat'])
1090 1090
1091 1091 has_required_fields = (
1092 1092 values['issuetracker_pat']
1093 1093 and values['issuetracker_url'])
1094 1094
1095 1095 if has_required_fields:
1096 1096 # validate url that it starts with http or /
1097 1097 # otherwise it can lead to JS injections
1098 1098 # e.g specifig javascript:<malicios code>
1099 1099 if not values['issuetracker_url'].startswith(('http', '/')):
1100 1100 raise formencode.Invalid(
1101 1101 self.message('bad_format', state),
1102 1102 value, state)
1103 1103
1104 1104 settings = [
1105 1105 ('_'.join((key, new_uid)), values[key], 'unicode')
1106 1106 for key in values]
1107 1107 patterns.append(settings)
1108 1108
1109 1109 value['patterns'] = patterns
1110 1110 delete_patterns = value.get('uid') or []
1111 1111 if not isinstance(delete_patterns, (list, tuple)):
1112 1112 delete_patterns = [delete_patterns]
1113 1113 value['delete_patterns'] = delete_patterns
1114 1114 return value
1115 1115 return _validator
@@ -1,236 +1,236 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 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 functools
22 22
23 23 import pytest
24 24
25 25 from rhodecode.model.db import RepoGroup
26 26 from rhodecode.model.meta import Session
27 27 from rhodecode.model.repo_group import RepoGroupModel
28 28 from rhodecode.model.user_group import UserGroupModel
29 29 from rhodecode.tests.models.common import (
30 30 _create_project_tree, check_tree_perms, _get_perms, _check_expected_count,
31 31 expected_count, _destroy_project_tree)
32 32 from rhodecode.tests.fixture import Fixture
33 33
34 34
35 35 fixture = Fixture()
36 36
37 37 test_u2_id = None
38 38 test_u2_gr_id = None
39 39 _get_repo_perms = None
40 40 _get_group_perms = None
41 41
42 42
43 43 def permissions_setup_func_orig(
44 44 group_name='g0', perm='group.read', recursive='all'):
45 45 """
46 46 Resets all permissions to perm attribute
47 47 """
48 48 repo_group = RepoGroup.get_by_group_name(group_name=group_name)
49 49 if not repo_group:
50 50 raise Exception('Cannot get group %s' % group_name)
51 perm_updates = [[test_u2_gr_id, perm, 'users_group']]
51 perm_updates = [[test_u2_gr_id, perm, 'user_group']]
52 52 RepoGroupModel().update_permissions(repo_group,
53 53 perm_updates=perm_updates,
54 54 recursive=recursive, check_perms=False)
55 55 Session().commit()
56 56
57 57
58 58 def permissions_setup_func(*args, **kwargs):
59 59 # TODO: workaround, remove once the generative tests have been adapted
60 60 # to py.test's style
61 61 permissions_setup_func_orig()
62 62 permissions_setup_func_orig(*args, **kwargs)
63 63
64 64
65 65 @pytest.fixture(scope='module', autouse=True)
66 66 def prepare(request):
67 67 global test_u2_id, test_u2_gr_id, _get_repo_perms, _get_group_perms
68 68 test_u2 = _create_project_tree()
69 69 Session().commit()
70 70 test_u2_id = test_u2.user_id
71 71
72 72 gr1 = fixture.create_user_group('perms_group_1')
73 73 Session().commit()
74 74 test_u2_gr_id = gr1.users_group_id
75 75 UserGroupModel().add_user_to_group(gr1, user=test_u2_id)
76 76 Session().commit()
77 77
78 78 _get_repo_perms = functools.partial(_get_perms, key='repositories',
79 79 test_u1_id=test_u2_id)
80 80 _get_group_perms = functools.partial(_get_perms, key='repositories_groups',
81 81 test_u1_id=test_u2_id)
82 82
83 83 @request.addfinalizer
84 84 def cleanup():
85 85 fixture.destroy_user_group('perms_group_1')
86 86 _destroy_project_tree(test_u2_id)
87 87
88 88
89 89 def test_user_permissions_on_group_without_recursive_mode():
90 90 # set permission to g0 non-recursive mode
91 91 recursive = 'none'
92 92 group = 'g0'
93 93 permissions_setup_func(group, 'group.write', recursive=recursive)
94 94
95 95 items = [x for x in _get_repo_perms(group, recursive)]
96 96 expected = 0
97 97 assert len(items) == expected, ' %s != %s' % (len(items), expected)
98 98 for name, perm in items:
99 99 check_tree_perms(name, perm, group, 'repository.read')
100 100
101 101 items = [x for x in _get_group_perms(group, recursive)]
102 102 expected = 1
103 103 assert len(items) == expected, ' %s != %s' % (len(items), expected)
104 104 for name, perm in items:
105 105 check_tree_perms(name, perm, group, 'group.write')
106 106
107 107
108 108 def test_user_permissions_on_group_without_recursive_mode_subgroup():
109 109 # set permission to g0 non-recursive mode
110 110 recursive = 'none'
111 111 group = 'g0/g0_1'
112 112 permissions_setup_func(group, 'group.write', recursive=recursive)
113 113
114 114 items = [x for x in _get_repo_perms(group, recursive)]
115 115 expected = 0
116 116 assert len(items) == expected, ' %s != %s' % (len(items), expected)
117 117 for name, perm in items:
118 118 check_tree_perms(name, perm, group, 'repository.read')
119 119
120 120 items = [x for x in _get_group_perms(group, recursive)]
121 121 expected = 1
122 122 assert len(items) == expected, ' %s != %s' % (len(items), expected)
123 123 for name, perm in items:
124 124 check_tree_perms(name, perm, group, 'group.write')
125 125
126 126
127 127 def test_user_permissions_on_group_with_recursive_mode():
128 128
129 129 # set permission to g0 recursive mode, all children including
130 130 # other repos and groups should have this permission now set !
131 131 recursive = 'all'
132 132 group = 'g0'
133 133 permissions_setup_func(group, 'group.write', recursive=recursive)
134 134
135 135 repo_items = [x for x in _get_repo_perms(group, recursive)]
136 136 items = [x for x in _get_group_perms(group, recursive)]
137 137 _check_expected_count(items, repo_items, expected_count(group, True))
138 138
139 139 for name, perm in repo_items:
140 140 check_tree_perms(name, perm, group, 'repository.write')
141 141
142 142 for name, perm in items:
143 143 check_tree_perms(name, perm, group, 'group.write')
144 144
145 145
146 146 def test_user_permissions_on_group_with_recursive_mode_inner_group():
147 147 # set permission to g0_3 group to none
148 148 recursive = 'all'
149 149 group = 'g0/g0_3'
150 150 permissions_setup_func(group, 'group.none', recursive=recursive)
151 151
152 152 repo_items = [x for x in _get_repo_perms(group, recursive)]
153 153 items = [x for x in _get_group_perms(group, recursive)]
154 154 _check_expected_count(items, repo_items, expected_count(group, True))
155 155
156 156 for name, perm in repo_items:
157 157 check_tree_perms(name, perm, group, 'repository.none')
158 158
159 159 for name, perm in items:
160 160 check_tree_perms(name, perm, group, 'group.none')
161 161
162 162
163 163 def test_user_permissions_on_group_with_recursive_mode_deepest():
164 164 # set permission to g0_3 group to none
165 165 recursive = 'all'
166 166 group = 'g0/g0_1/g0_1_1'
167 167 permissions_setup_func(group, 'group.write', recursive=recursive)
168 168
169 169 repo_items = [x for x in _get_repo_perms(group, recursive)]
170 170 items = [x for x in _get_group_perms(group, recursive)]
171 171 _check_expected_count(items, repo_items, expected_count(group, True))
172 172
173 173 for name, perm in repo_items:
174 174 check_tree_perms(name, perm, group, 'repository.write')
175 175
176 176 for name, perm in items:
177 177 check_tree_perms(name, perm, group, 'group.write')
178 178
179 179
180 180 def test_user_permissions_on_group_with_recursive_mode_only_with_repos():
181 181 # set permission to g0_3 group to none
182 182 recursive = 'all'
183 183 group = 'g0/g0_2'
184 184 permissions_setup_func(group, 'group.admin', recursive=recursive)
185 185
186 186 repo_items = [x for x in _get_repo_perms(group, recursive)]
187 187 items = [x for x in _get_group_perms(group, recursive)]
188 188 _check_expected_count(items, repo_items, expected_count(group, True))
189 189
190 190 for name, perm in repo_items:
191 191 check_tree_perms(name, perm, group, 'repository.admin')
192 192
193 193 for name, perm in items:
194 194 check_tree_perms(name, perm, group, 'group.admin')
195 195
196 196
197 197 def test_user_permissions_on_group_with_recursive_mode_on_repos():
198 198 # set permission to g0/g0_1 with recursive mode on just repositories
199 199 recursive = 'repos'
200 200 group = 'g0/g0_1'
201 201 perm = 'group.write'
202 202 permissions_setup_func(group, perm, recursive=recursive)
203 203
204 204 repo_items = [x for x in _get_repo_perms(group, recursive)]
205 205 items = [x for x in _get_group_perms(group, recursive)]
206 206 _check_expected_count(items, repo_items, expected_count(group, True))
207 207
208 208 for name, perm in repo_items:
209 209 check_tree_perms(name, perm, group, 'repository.write')
210 210
211 211 for name, perm in items:
212 212 # permission is set with repos only mode, but we also change the
213 213 # permission on the group we trigger the apply to children from, thus
214 214 # we need to change its permission check
215 215 old_perm = 'group.read'
216 216 if name == group:
217 217 old_perm = perm
218 218 check_tree_perms(name, perm, group, old_perm)
219 219
220 220
221 221 def test_user_permissions_on_group_with_recursive_mode_on_repo_groups():
222 222 # set permission to g0/g0_1 with recursive mode on just repository groups
223 223 recursive = 'groups'
224 224 group = 'g0/g0_1'
225 225 perm = 'group.none'
226 226 permissions_setup_func(group, perm, recursive=recursive)
227 227
228 228 repo_items = [x for x in _get_repo_perms(group, recursive)]
229 229 items = [x for x in _get_group_perms(group, recursive)]
230 230 _check_expected_count(items, repo_items, expected_count(group, True))
231 231
232 232 for name, perm in repo_items:
233 233 check_tree_perms(name, perm, group, 'repository.read')
234 234
235 235 for name, perm in items:
236 236 check_tree_perms(name, perm, group, 'group.none')
@@ -1,429 +1,458 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 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 threading
22 22 import time
23 23 import logging
24 24 import os.path
25 25 import subprocess32
26 26 import tempfile
27 27 import urllib2
28 28 from lxml.html import fromstring, tostring
29 29 from lxml.cssselect import CSSSelector
30 30 from urlparse import urlparse, parse_qsl
31 31 from urllib import unquote_plus
32 32 import webob
33 33
34 34 from webtest.app import TestResponse, TestApp, string_types
35 35 from webtest.compat import print_stderr
36 36
37 37 import pytest
38 38 import rc_testdata
39 39
40 40 from rhodecode.model.db import User, Repository
41 41 from rhodecode.model.meta import Session
42 42 from rhodecode.model.scm import ScmModel
43 43 from rhodecode.lib.vcs.backends.svn.repository import SubversionRepository
44 44 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 45 from rhodecode.tests import login_user_session
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 class CustomTestResponse(TestResponse):
51 51 def _save_output(self, out):
52 52 f = tempfile.NamedTemporaryFile(
53 53 delete=False, prefix='rc-test-', suffix='.html')
54 54 f.write(out)
55 55 return f.name
56 56
57 57 def mustcontain(self, *strings, **kw):
58 58 """
59 59 Assert that the response contains all of the strings passed
60 60 in as arguments.
61 61
62 62 Equivalent to::
63 63
64 64 assert string in res
65 65 """
66 66 if 'no' in kw:
67 67 no = kw['no']
68 68 del kw['no']
69 69 if isinstance(no, string_types):
70 70 no = [no]
71 71 else:
72 72 no = []
73 73 if kw:
74 74 raise TypeError(
75 75 "The only keyword argument allowed is 'no' got %s" % kw)
76 76
77 77 f = self._save_output(str(self))
78 78
79 79 for s in strings:
80 80 if not s in self:
81 81 print_stderr("Actual response (no %r):" % s)
82 82 print_stderr(str(self))
83 83 raise IndexError(
84 84 "Body does not contain string %r, output saved as %s" % (
85 85 s, f))
86 86
87 87 for no_s in no:
88 88 if no_s in self:
89 89 print_stderr("Actual response (has %r)" % no_s)
90 90 print_stderr(str(self))
91 91 raise IndexError(
92 92 "Body contains bad string %r, output saved as %s" % (
93 93 no_s, f))
94 94
95 95 def assert_response(self):
96 96 return AssertResponse(self)
97 97
98 98 def get_session_from_response(self):
99 99 """
100 100 This returns the session from a response object.
101 101 """
102 102
103 103 from pyramid_beaker import session_factory_from_settings
104 104 session = session_factory_from_settings(
105 105 self.test_app.app.config.get_settings())
106 106 return session(self.request)
107 107
108 108
109 109 class TestRequest(webob.BaseRequest):
110 110
111 111 # for py.test
112 112 disabled = True
113 113 ResponseClass = CustomTestResponse
114 114
115 115 def add_response_callback(self, callback):
116 116 pass
117 117
118 118
119 119 class CustomTestApp(TestApp):
120 120 """
121 121 Custom app to make mustcontain more usefull, and extract special methods
122 122 """
123 123 RequestClass = TestRequest
124 124 rc_login_data = {}
125 125 rc_current_session = None
126 126
127 127 def login(self, username=None, password=None):
128 128 from rhodecode.lib import auth
129 129
130 130 if username and password:
131 131 session = login_user_session(self, username, password)
132 132 else:
133 133 session = login_user_session(self)
134 134
135 135 self.rc_login_data['csrf_token'] = auth.get_csrf_token(session)
136 136 self.rc_current_session = session
137 137 return session['rhodecode_user']
138 138
139 139 @property
140 140 def csrf_token(self):
141 141 return self.rc_login_data['csrf_token']
142 142
143 143
144 144 def set_anonymous_access(enabled):
145 145 """(Dis)allows anonymous access depending on parameter `enabled`"""
146 146 user = User.get_default_user()
147 147 user.active = enabled
148 148 Session().add(user)
149 149 Session().commit()
150 150 time.sleep(1.5) # must sleep for cache (1s to expire)
151 151 log.info('anonymous access is now: %s', enabled)
152 152 assert enabled == User.get_default_user().active, (
153 153 'Cannot set anonymous access')
154 154
155 155
156 156 def check_xfail_backends(node, backend_alias):
157 157 # Using "xfail_backends" here intentionally, since this marks work
158 158 # which is "to be done" soon.
159 159 skip_marker = node.get_marker('xfail_backends')
160 160 if skip_marker and backend_alias in skip_marker.args:
161 161 msg = "Support for backend %s to be developed." % (backend_alias, )
162 162 msg = skip_marker.kwargs.get('reason', msg)
163 163 pytest.xfail(msg)
164 164
165 165
166 166 def check_skip_backends(node, backend_alias):
167 167 # Using "skip_backends" here intentionally, since this marks work which is
168 168 # not supported.
169 169 skip_marker = node.get_marker('skip_backends')
170 170 if skip_marker and backend_alias in skip_marker.args:
171 171 msg = "Feature not supported for backend %s." % (backend_alias, )
172 172 msg = skip_marker.kwargs.get('reason', msg)
173 173 pytest.skip(msg)
174 174
175 175
176 176 def extract_git_repo_from_dump(dump_name, repo_name):
177 177 """Create git repo `repo_name` from dump `dump_name`."""
178 178 repos_path = ScmModel().repos_path
179 179 target_path = os.path.join(repos_path, repo_name)
180 180 rc_testdata.extract_git_dump(dump_name, target_path)
181 181 return target_path
182 182
183 183
184 184 def extract_hg_repo_from_dump(dump_name, repo_name):
185 185 """Create hg repo `repo_name` from dump `dump_name`."""
186 186 repos_path = ScmModel().repos_path
187 187 target_path = os.path.join(repos_path, repo_name)
188 188 rc_testdata.extract_hg_dump(dump_name, target_path)
189 189 return target_path
190 190
191 191
192 192 def extract_svn_repo_from_dump(dump_name, repo_name):
193 193 """Create a svn repo `repo_name` from dump `dump_name`."""
194 194 repos_path = ScmModel().repos_path
195 195 target_path = os.path.join(repos_path, repo_name)
196 196 SubversionRepository(target_path, create=True)
197 197 _load_svn_dump_into_repo(dump_name, target_path)
198 198 return target_path
199 199
200 200
201 201 def assert_message_in_log(log_records, message, levelno, module):
202 202 messages = [
203 203 r.message for r in log_records
204 204 if r.module == module and r.levelno == levelno
205 205 ]
206 206 assert message in messages
207 207
208 208
209 209 def _load_svn_dump_into_repo(dump_name, repo_path):
210 210 """
211 211 Utility to populate a svn repository with a named dump
212 212
213 213 Currently the dumps are in rc_testdata. They might later on be
214 214 integrated with the main repository once they stabilize more.
215 215 """
216 216 dump = rc_testdata.load_svn_dump(dump_name)
217 217 load_dump = subprocess32.Popen(
218 218 ['svnadmin', 'load', repo_path],
219 219 stdin=subprocess32.PIPE, stdout=subprocess32.PIPE,
220 220 stderr=subprocess32.PIPE)
221 221 out, err = load_dump.communicate(dump)
222 222 if load_dump.returncode != 0:
223 223 log.error("Output of load_dump command: %s", out)
224 224 log.error("Error output of load_dump command: %s", err)
225 225 raise Exception(
226 226 'Failed to load dump "%s" into repository at path "%s".'
227 227 % (dump_name, repo_path))
228 228
229 229
230 230 class AssertResponse(object):
231 231 """
232 232 Utility that helps to assert things about a given HTML response.
233 233 """
234 234
235 235 def __init__(self, response):
236 236 self.response = response
237 237
238 238 def get_imports(self):
239 239 return fromstring, tostring, CSSSelector
240 240
241 241 def one_element_exists(self, css_selector):
242 242 self.get_element(css_selector)
243 243
244 244 def no_element_exists(self, css_selector):
245 245 assert not self._get_elements(css_selector)
246 246
247 247 def element_equals_to(self, css_selector, expected_content):
248 248 element = self.get_element(css_selector)
249 249 element_text = self._element_to_string(element)
250 250 assert expected_content in element_text
251 251
252 252 def element_contains(self, css_selector, expected_content):
253 253 element = self.get_element(css_selector)
254 254 assert expected_content in element.text_content()
255 255
256 256 def element_value_contains(self, css_selector, expected_content):
257 257 element = self.get_element(css_selector)
258 258 assert expected_content in element.value
259 259
260 260 def contains_one_link(self, link_text, href):
261 261 fromstring, tostring, CSSSelector = self.get_imports()
262 262 doc = fromstring(self.response.body)
263 263 sel = CSSSelector('a[href]')
264 264 elements = [
265 265 e for e in sel(doc) if e.text_content().strip() == link_text]
266 266 assert len(elements) == 1, "Did not find link or found multiple links"
267 267 self._ensure_url_equal(elements[0].attrib.get('href'), href)
268 268
269 269 def contains_one_anchor(self, anchor_id):
270 270 fromstring, tostring, CSSSelector = self.get_imports()
271 271 doc = fromstring(self.response.body)
272 272 sel = CSSSelector('#' + anchor_id)
273 273 elements = sel(doc)
274 274 assert len(elements) == 1, 'cannot find 1 element {}'.format(anchor_id)
275 275
276 276 def _ensure_url_equal(self, found, expected):
277 277 assert _Url(found) == _Url(expected)
278 278
279 279 def get_element(self, css_selector):
280 280 elements = self._get_elements(css_selector)
281 281 assert len(elements) == 1, 'cannot find 1 element {}'.format(css_selector)
282 282 return elements[0]
283 283
284 284 def get_elements(self, css_selector):
285 285 return self._get_elements(css_selector)
286 286
287 287 def _get_elements(self, css_selector):
288 288 fromstring, tostring, CSSSelector = self.get_imports()
289 289 doc = fromstring(self.response.body)
290 290 sel = CSSSelector(css_selector)
291 291 elements = sel(doc)
292 292 return elements
293 293
294 294 def _element_to_string(self, element):
295 295 fromstring, tostring, CSSSelector = self.get_imports()
296 296 return tostring(element)
297 297
298 298
299 299 class _Url(object):
300 300 """
301 301 A url object that can be compared with other url orbjects
302 302 without regard to the vagaries of encoding, escaping, and ordering
303 303 of parameters in query strings.
304 304
305 305 Inspired by
306 306 http://stackoverflow.com/questions/5371992/comparing-two-urls-in-python
307 307 """
308 308
309 309 def __init__(self, url):
310 310 parts = urlparse(url)
311 311 _query = frozenset(parse_qsl(parts.query))
312 312 _path = unquote_plus(parts.path)
313 313 parts = parts._replace(query=_query, path=_path)
314 314 self.parts = parts
315 315
316 316 def __eq__(self, other):
317 317 return self.parts == other.parts
318 318
319 319 def __hash__(self):
320 320 return hash(self.parts)
321 321
322 322
323 323 def run_test_concurrently(times, raise_catched_exc=True):
324 324 """
325 325 Add this decorator to small pieces of code that you want to test
326 326 concurrently
327 327
328 328 ex:
329 329
330 330 @test_concurrently(25)
331 331 def my_test_function():
332 332 ...
333 333 """
334 334 def test_concurrently_decorator(test_func):
335 335 def wrapper(*args, **kwargs):
336 336 exceptions = []
337 337
338 338 def call_test_func():
339 339 try:
340 340 test_func(*args, **kwargs)
341 341 except Exception as e:
342 342 exceptions.append(e)
343 343 if raise_catched_exc:
344 344 raise
345 345 threads = []
346 346 for i in range(times):
347 347 threads.append(threading.Thread(target=call_test_func))
348 348 for t in threads:
349 349 t.start()
350 350 for t in threads:
351 351 t.join()
352 352 if exceptions:
353 353 raise Exception(
354 354 'test_concurrently intercepted %s exceptions: %s' % (
355 355 len(exceptions), exceptions))
356 356 return wrapper
357 357 return test_concurrently_decorator
358 358
359 359
360 360 def wait_for_url(url, timeout=10):
361 361 """
362 362 Wait until URL becomes reachable.
363 363
364 364 It polls the URL until the timeout is reached or it became reachable.
365 365 If will call to `py.test.fail` in case the URL is not reachable.
366 366 """
367 367 timeout = time.time() + timeout
368 368 last = 0
369 369 wait = 0.1
370 370
371 371 while timeout > last:
372 372 last = time.time()
373 373 if is_url_reachable(url):
374 374 break
375 375 elif (last + wait) > time.time():
376 376 # Go to sleep because not enough time has passed since last check.
377 377 time.sleep(wait)
378 378 else:
379 379 pytest.fail("Timeout while waiting for URL {}".format(url))
380 380
381 381
382 382 def is_url_reachable(url):
383 383 try:
384 384 urllib2.urlopen(url)
385 385 except urllib2.URLError:
386 386 return False
387 387 return True
388 388
389 389
390 390 def repo_on_filesystem(repo_name):
391 391 from rhodecode.lib import vcs
392 392 from rhodecode.tests import TESTS_TMP_PATH
393 393 repo = vcs.get_vcs_instance(
394 394 os.path.join(TESTS_TMP_PATH, repo_name), create=False)
395 395 return repo is not None
396 396
397 397
398 398 def commit_change(
399 399 repo, filename, content, message, vcs_type, parent=None, newfile=False):
400 400 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
401 401
402 402 repo = Repository.get_by_repo_name(repo)
403 403 _commit = parent
404 404 if not parent:
405 405 _commit = EmptyCommit(alias=vcs_type)
406 406
407 407 if newfile:
408 408 nodes = {
409 409 filename: {
410 410 'content': content
411 411 }
412 412 }
413 413 commit = ScmModel().create_nodes(
414 414 user=TEST_USER_ADMIN_LOGIN, repo=repo,
415 415 message=message,
416 416 nodes=nodes,
417 417 parent_commit=_commit,
418 418 author=TEST_USER_ADMIN_LOGIN,
419 419 )
420 420 else:
421 421 commit = ScmModel().commit_change(
422 422 repo=repo.scm_instance(), repo_name=repo.repo_name,
423 423 commit=parent, user=TEST_USER_ADMIN_LOGIN,
424 424 author=TEST_USER_ADMIN_LOGIN,
425 425 message=message,
426 426 content=content,
427 427 f_path=filename
428 428 )
429 429 return commit
430
431
432 def permission_update_data_generator(csrf_token, default=None, grant=None, revoke=None):
433 if not default:
434 raise ValueError('Permission for default user must be given')
435 form_data = [(
436 'csrf_token', csrf_token
437 )]
438 # add default
439 form_data.extend([
440 ('u_perm_1', default)
441 ])
442
443 if grant:
444 for cnt, (obj_id, perm, obj_name, obj_type) in enumerate(grant, 1):
445 form_data.extend([
446 ('perm_new_member_perm_new{}'.format(cnt), perm),
447 ('perm_new_member_id_new{}'.format(cnt), obj_id),
448 ('perm_new_member_name_new{}'.format(cnt), obj_name),
449 ('perm_new_member_type_new{}'.format(cnt), obj_type),
450
451 ])
452 if revoke:
453 for obj_id, obj_type in revoke:
454 form_data.extend([
455 ('perm_del_member_id_{}'.format(obj_id), obj_id),
456 ('perm_del_member_type_{}'.format(obj_id), obj_type),
457 ])
458 return form_data
General Comments 0
You need to be logged in to leave comments. Login now