##// END OF EJS Templates
caches: cleanup code...
super-admin -
r5009:4102d6ca default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,1197 +1,1197 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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, AttachedPullRequestsError
36 36 from rhodecode.lib import hooks_base
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, action_logger_generic)
42 42 from rhodecode.lib.vcs.backends import get_backend
43 43 from rhodecode.model import BaseModel
44 44 from rhodecode.model.db import (
45 45 _hash_key, func, case, joinedload, or_, in_filter_generator,
46 46 Session, Repository, UserRepoToPerm, UserGroupRepoToPerm,
47 47 UserRepoGroupToPerm, UserGroupRepoGroupToPerm, User, Permission,
48 48 Statistics, UserGroup, RepoGroup, RepositoryField, UserLog)
49 49 from rhodecode.model.permission import PermissionModel
50 50 from rhodecode.model.settings import VcsSettingsModel
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54
55 55 class RepoModel(BaseModel):
56 56
57 57 cls = Repository
58 58
59 59 def _get_user_group(self, users_group):
60 60 return self._get_instance(UserGroup, users_group,
61 61 callback=UserGroup.get_by_group_name)
62 62
63 63 def _get_repo_group(self, repo_group):
64 64 return self._get_instance(RepoGroup, repo_group,
65 65 callback=RepoGroup.get_by_group_name)
66 66
67 67 def _create_default_perms(self, repository, private):
68 68 # create default permission
69 69 default = 'repository.read'
70 70 def_user = User.get_default_user()
71 71 for p in def_user.user_perms:
72 72 if p.permission.permission_name.startswith('repository.'):
73 73 default = p.permission.permission_name
74 74 break
75 75
76 76 default_perm = 'repository.none' if private else default
77 77
78 78 repo_to_perm = UserRepoToPerm()
79 79 repo_to_perm.permission = Permission.get_by_key(default_perm)
80 80
81 81 repo_to_perm.repository = repository
82 82 repo_to_perm.user_id = def_user.user_id
83 83
84 84 return repo_to_perm
85 85
86 86 @LazyProperty
87 87 def repos_path(self):
88 88 """
89 89 Gets the repositories root path from database
90 90 """
91 91 settings_model = VcsSettingsModel(sa=self.sa)
92 92 return settings_model.get_repos_location()
93 93
94 94 def get(self, repo_id):
95 95 repo = self.sa.query(Repository) \
96 96 .filter(Repository.repo_id == repo_id)
97 97
98 98 return repo.scalar()
99 99
100 100 def get_repo(self, repository):
101 101 return self._get_repo(repository)
102 102
103 103 def get_by_repo_name(self, repo_name, cache=False):
104 104 repo = self.sa.query(Repository) \
105 105 .filter(Repository.repo_name == repo_name)
106 106
107 107 if cache:
108 108 name_key = _hash_key(repo_name)
109 109 repo = repo.options(
110 FromCache("sql_cache_short", "get_repo_%s" % name_key))
110 FromCache("sql_cache_short", f"get_repo_{name_key}"))
111 111 return repo.scalar()
112 112
113 113 def _extract_id_from_repo_name(self, repo_name):
114 114 if repo_name.startswith('/'):
115 115 repo_name = repo_name.lstrip('/')
116 116 by_id_match = re.match(r'^_(\d{1,})', repo_name)
117 117 if by_id_match:
118 118 return by_id_match.groups()[0]
119 119
120 120 def get_repo_by_id(self, repo_name):
121 121 """
122 122 Extracts repo_name by id from special urls.
123 123 Example url is _11/repo_name
124 124
125 125 :param repo_name:
126 126 :return: repo object if matched else None
127 127 """
128 128 _repo_id = None
129 129 try:
130 130 _repo_id = self._extract_id_from_repo_name(repo_name)
131 131 if _repo_id:
132 132 return self.get(_repo_id)
133 133 except Exception:
134 134 log.exception('Failed to extract repo_name from URL')
135 135 if _repo_id:
136 136 Session().rollback()
137 137
138 138 return None
139 139
140 140 def get_repos_for_root(self, root, traverse=False):
141 141 if traverse:
142 142 like_expression = u'{}%'.format(safe_unicode(root))
143 143 repos = Repository.query().filter(
144 144 Repository.repo_name.like(like_expression)).all()
145 145 else:
146 146 if root and not isinstance(root, RepoGroup):
147 147 raise ValueError(
148 148 'Root must be an instance '
149 149 'of RepoGroup, got:{} instead'.format(type(root)))
150 150 repos = Repository.query().filter(Repository.group == root).all()
151 151 return repos
152 152
153 153 def get_url(self, repo, request=None, permalink=False):
154 154 if not request:
155 155 request = get_current_request()
156 156
157 157 if not request:
158 158 return
159 159
160 160 if permalink:
161 161 return request.route_url(
162 162 'repo_summary', repo_name='_{}'.format(safe_str(repo.repo_id)))
163 163 else:
164 164 return request.route_url(
165 165 'repo_summary', repo_name=safe_str(repo.repo_name))
166 166
167 167 def get_commit_url(self, repo, commit_id, request=None, permalink=False):
168 168 if not request:
169 169 request = get_current_request()
170 170
171 171 if not request:
172 172 return
173 173
174 174 if permalink:
175 175 return request.route_url(
176 176 'repo_commit', repo_name=safe_str(repo.repo_id),
177 177 commit_id=commit_id)
178 178
179 179 else:
180 180 return request.route_url(
181 181 'repo_commit', repo_name=safe_str(repo.repo_name),
182 182 commit_id=commit_id)
183 183
184 184 def get_repo_log(self, repo, filter_term):
185 185 repo_log = UserLog.query()\
186 186 .filter(or_(UserLog.repository_id == repo.repo_id,
187 187 UserLog.repository_name == repo.repo_name))\
188 188 .options(joinedload(UserLog.user))\
189 189 .options(joinedload(UserLog.repository))\
190 190 .order_by(UserLog.action_date.desc())
191 191
192 192 repo_log = user_log_filter(repo_log, filter_term)
193 193 return repo_log
194 194
195 195 @classmethod
196 196 def update_commit_cache(cls, repositories=None):
197 197 if not repositories:
198 198 repositories = Repository.getAll()
199 199 for repo in repositories:
200 200 repo.update_commit_cache()
201 201
202 202 def get_repos_as_dict(self, repo_list=None, admin=False,
203 203 super_user_actions=False, short_name=None):
204 204
205 205 _render = get_current_request().get_partial_renderer(
206 206 'rhodecode:templates/data_table/_dt_elements.mako')
207 207 c = _render.get_call_context()
208 208 h = _render.get_helpers()
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, archived, fork_of):
214 214 if short_name is not None:
215 215 short_name_var = short_name
216 216 else:
217 217 short_name_var = not admin
218 218 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
219 219 short_name=short_name_var, admin=False)
220 220
221 221 def last_change(last_change):
222 222 if admin and isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
223 223 ts = time.time()
224 224 utc_offset = (datetime.datetime.fromtimestamp(ts)
225 225 - datetime.datetime.utcfromtimestamp(ts)).total_seconds()
226 226 last_change = last_change + datetime.timedelta(seconds=utc_offset)
227 227
228 228 return _render("last_change", last_change)
229 229
230 230 def rss_lnk(repo_name):
231 231 return _render("rss", repo_name)
232 232
233 233 def atom_lnk(repo_name):
234 234 return _render("atom", repo_name)
235 235
236 236 def last_rev(repo_name, cs_cache):
237 237 return _render('revision', repo_name, cs_cache.get('revision'),
238 238 cs_cache.get('raw_id'), cs_cache.get('author'),
239 239 cs_cache.get('message'), cs_cache.get('date'))
240 240
241 241 def desc(desc):
242 242 return _render('repo_desc', desc, c.visual.stylify_metatags)
243 243
244 244 def state(repo_state):
245 245 return _render("repo_state", repo_state)
246 246
247 247 def repo_actions(repo_name):
248 248 return _render('repo_actions', repo_name, super_user_actions)
249 249
250 250 def user_profile(username):
251 251 return _render('user_profile', username)
252 252
253 253 repos_data = []
254 254 for repo in repo_list:
255 255 # NOTE(marcink): because we use only raw column we need to load it like that
256 256 changeset_cache = Repository._load_changeset_cache(
257 257 repo.repo_id, repo._changeset_cache)
258 258
259 259 row = {
260 260 "menu": quick_menu(repo.repo_name),
261 261
262 262 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
263 263 repo.private, repo.archived, repo.fork),
264 264
265 265 "desc": desc(h.escape(repo.description)),
266 266
267 267 "last_change": last_change(repo.updated_on),
268 268
269 269 "last_changeset": last_rev(repo.repo_name, changeset_cache),
270 270 "last_changeset_raw": changeset_cache.get('revision'),
271 271
272 272 "owner": user_profile(repo.User.username),
273 273
274 274 "state": state(repo.repo_state),
275 275 "rss": rss_lnk(repo.repo_name),
276 276 "atom": atom_lnk(repo.repo_name),
277 277 }
278 278 if admin:
279 279 row.update({
280 280 "action": repo_actions(repo.repo_name),
281 281 })
282 282 repos_data.append(row)
283 283
284 284 return repos_data
285 285
286 286 def get_repos_data_table(
287 287 self, draw, start, limit,
288 288 search_q, order_by, order_dir,
289 289 auth_user, repo_group_id):
290 290 from rhodecode.model.scm import RepoList
291 291
292 292 _perms = ['repository.read', 'repository.write', 'repository.admin']
293 293
294 294 repos = Repository.query() \
295 295 .filter(Repository.group_id == repo_group_id) \
296 296 .all()
297 297 auth_repo_list = RepoList(
298 298 repos, perm_set=_perms,
299 299 extra_kwargs=dict(user=auth_user))
300 300
301 301 allowed_ids = [-1]
302 302 for repo in auth_repo_list:
303 303 allowed_ids.append(repo.repo_id)
304 304
305 305 repos_data_total_count = Repository.query() \
306 306 .filter(Repository.group_id == repo_group_id) \
307 307 .filter(or_(
308 308 # generate multiple IN to fix limitation problems
309 309 *in_filter_generator(Repository.repo_id, allowed_ids))
310 310 ) \
311 311 .count()
312 312
313 313 base_q = Session.query(
314 314 Repository.repo_id,
315 315 Repository.repo_name,
316 316 Repository.description,
317 317 Repository.repo_type,
318 318 Repository.repo_state,
319 319 Repository.private,
320 320 Repository.archived,
321 321 Repository.fork,
322 322 Repository.updated_on,
323 323 Repository._changeset_cache,
324 324 User,
325 325 ) \
326 326 .filter(Repository.group_id == repo_group_id) \
327 327 .filter(or_(
328 328 # generate multiple IN to fix limitation problems
329 329 *in_filter_generator(Repository.repo_id, allowed_ids))
330 330 ) \
331 331 .join(User, User.user_id == Repository.user_id) \
332 332 .group_by(Repository, User)
333 333
334 334 repos_data_total_filtered_count = base_q.count()
335 335
336 336 sort_defined = False
337 337 if order_by == 'repo_name':
338 338 sort_col = func.lower(Repository.repo_name)
339 339 sort_defined = True
340 340 elif order_by == 'user_username':
341 341 sort_col = User.username
342 342 else:
343 343 sort_col = getattr(Repository, order_by, None)
344 344
345 345 if sort_defined or sort_col:
346 346 if order_dir == 'asc':
347 347 sort_col = sort_col.asc()
348 348 else:
349 349 sort_col = sort_col.desc()
350 350
351 351 base_q = base_q.order_by(sort_col)
352 352 base_q = base_q.offset(start).limit(limit)
353 353
354 354 repos_list = base_q.all()
355 355
356 356 repos_data = RepoModel().get_repos_as_dict(
357 357 repo_list=repos_list, admin=False)
358 358
359 359 data = ({
360 360 'draw': draw,
361 361 'data': repos_data,
362 362 'recordsTotal': repos_data_total_count,
363 363 'recordsFiltered': repos_data_total_filtered_count,
364 364 })
365 365 return data
366 366
367 367 def _get_defaults(self, repo_name):
368 368 """
369 369 Gets information about repository, and returns a dict for
370 370 usage in forms
371 371
372 372 :param repo_name:
373 373 """
374 374
375 375 repo_info = Repository.get_by_repo_name(repo_name)
376 376
377 377 if repo_info is None:
378 378 return None
379 379
380 380 defaults = repo_info.get_dict()
381 381 defaults['repo_name'] = repo_info.just_name
382 382
383 383 groups = repo_info.groups_with_parents
384 384 parent_group = groups[-1] if groups else None
385 385
386 386 # we use -1 as this is how in HTML, we mark an empty group
387 387 defaults['repo_group'] = getattr(parent_group, 'group_id', -1)
388 388
389 389 keys_to_process = (
390 390 {'k': 'repo_type', 'strip': False},
391 391 {'k': 'repo_enable_downloads', 'strip': True},
392 392 {'k': 'repo_description', 'strip': True},
393 393 {'k': 'repo_enable_locking', 'strip': True},
394 394 {'k': 'repo_landing_rev', 'strip': True},
395 395 {'k': 'clone_uri', 'strip': False},
396 396 {'k': 'push_uri', 'strip': False},
397 397 {'k': 'repo_private', 'strip': True},
398 398 {'k': 'repo_enable_statistics', 'strip': True}
399 399 )
400 400
401 401 for item in keys_to_process:
402 402 attr = item['k']
403 403 if item['strip']:
404 404 attr = remove_prefix(item['k'], 'repo_')
405 405
406 406 val = defaults[attr]
407 407 if item['k'] == 'repo_landing_rev':
408 408 val = ':'.join(defaults[attr])
409 409 defaults[item['k']] = val
410 410 if item['k'] == 'clone_uri':
411 411 defaults['clone_uri_hidden'] = repo_info.clone_uri_hidden
412 412 if item['k'] == 'push_uri':
413 413 defaults['push_uri_hidden'] = repo_info.push_uri_hidden
414 414
415 415 # fill owner
416 416 if repo_info.user:
417 417 defaults.update({'user': repo_info.user.username})
418 418 else:
419 419 replacement_user = User.get_first_super_admin().username
420 420 defaults.update({'user': replacement_user})
421 421
422 422 return defaults
423 423
424 424 def update(self, repo, **kwargs):
425 425 try:
426 426 cur_repo = self._get_repo(repo)
427 427 source_repo_name = cur_repo.repo_name
428 428
429 429 affected_user_ids = []
430 430 if 'user' in kwargs:
431 431 old_owner_id = cur_repo.user.user_id
432 432 new_owner = User.get_by_username(kwargs['user'])
433 433 cur_repo.user = new_owner
434 434
435 435 if old_owner_id != new_owner.user_id:
436 436 affected_user_ids = [new_owner.user_id, old_owner_id]
437 437
438 438 if 'repo_group' in kwargs:
439 439 cur_repo.group = RepoGroup.get(kwargs['repo_group'])
440 440 log.debug('Updating repo %s with params:%s', cur_repo, kwargs)
441 441
442 442 update_keys = [
443 443 (1, 'repo_description'),
444 444 (1, 'repo_landing_rev'),
445 445 (1, 'repo_private'),
446 446 (1, 'repo_enable_downloads'),
447 447 (1, 'repo_enable_locking'),
448 448 (1, 'repo_enable_statistics'),
449 449 (0, 'clone_uri'),
450 450 (0, 'push_uri'),
451 451 (0, 'fork_id')
452 452 ]
453 453 for strip, k in update_keys:
454 454 if k in kwargs:
455 455 val = kwargs[k]
456 456 if strip:
457 457 k = remove_prefix(k, 'repo_')
458 458
459 459 setattr(cur_repo, k, val)
460 460
461 461 new_name = cur_repo.get_new_name(kwargs['repo_name'])
462 462 cur_repo.repo_name = new_name
463 463
464 464 # if private flag is set, reset default permission to NONE
465 465 if kwargs.get('repo_private'):
466 466 EMPTY_PERM = 'repository.none'
467 467 RepoModel().grant_user_permission(
468 468 repo=cur_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM
469 469 )
470 470 if kwargs.get('repo_landing_rev'):
471 471 landing_rev_val = kwargs['repo_landing_rev']
472 472 RepoModel().set_landing_rev(cur_repo, landing_rev_val)
473 473
474 474 # handle extra fields
475 475 for field in filter(lambda k: k.startswith(RepositoryField.PREFIX), kwargs):
476 476 k = RepositoryField.un_prefix_key(field)
477 477 ex_field = RepositoryField.get_by_key_name(
478 478 key=k, repo=cur_repo)
479 479 if ex_field:
480 480 ex_field.field_value = kwargs[field]
481 481 self.sa.add(ex_field)
482 482
483 483 self.sa.add(cur_repo)
484 484
485 485 if source_repo_name != new_name:
486 486 # rename repository
487 487 self._rename_filesystem_repo(
488 488 old=source_repo_name, new=new_name)
489 489
490 490 if affected_user_ids:
491 491 PermissionModel().trigger_permission_flush(affected_user_ids)
492 492
493 493 return cur_repo
494 494 except Exception:
495 495 log.error(traceback.format_exc())
496 496 raise
497 497
498 498 def _create_repo(self, repo_name, repo_type, description, owner,
499 499 private=False, clone_uri=None, repo_group=None,
500 500 landing_rev=None, fork_of=None,
501 501 copy_fork_permissions=False, enable_statistics=False,
502 502 enable_locking=False, enable_downloads=False,
503 503 copy_group_permissions=False,
504 504 state=Repository.STATE_PENDING):
505 505 """
506 506 Create repository inside database with PENDING state, this should be
507 507 only executed by create() repo. With exception of importing existing
508 508 repos
509 509 """
510 510 from rhodecode.model.scm import ScmModel
511 511
512 512 owner = self._get_user(owner)
513 513 fork_of = self._get_repo(fork_of)
514 514 repo_group = self._get_repo_group(safe_int(repo_group))
515 515 default_landing_ref, _lbl = ScmModel.backend_landing_ref(repo_type)
516 516 landing_rev = landing_rev or default_landing_ref
517 517
518 518 try:
519 519 repo_name = safe_unicode(repo_name)
520 520 description = safe_unicode(description)
521 521 # repo name is just a name of repository
522 522 # while repo_name_full is a full qualified name that is combined
523 523 # with name and path of group
524 524 repo_name_full = repo_name
525 525 repo_name = repo_name.split(Repository.NAME_SEP)[-1]
526 526
527 527 new_repo = Repository()
528 528 new_repo.repo_state = state
529 529 new_repo.enable_statistics = False
530 530 new_repo.repo_name = repo_name_full
531 531 new_repo.repo_type = repo_type
532 532 new_repo.user = owner
533 533 new_repo.group = repo_group
534 534 new_repo.description = description or repo_name
535 535 new_repo.private = private
536 536 new_repo.archived = False
537 537 new_repo.clone_uri = clone_uri
538 538 new_repo.landing_rev = landing_rev
539 539
540 540 new_repo.enable_statistics = enable_statistics
541 541 new_repo.enable_locking = enable_locking
542 542 new_repo.enable_downloads = enable_downloads
543 543
544 544 if repo_group:
545 545 new_repo.enable_locking = repo_group.enable_locking
546 546
547 547 if fork_of:
548 548 parent_repo = fork_of
549 549 new_repo.fork = parent_repo
550 550
551 551 events.trigger(events.RepoPreCreateEvent(new_repo))
552 552
553 553 self.sa.add(new_repo)
554 554
555 555 EMPTY_PERM = 'repository.none'
556 556 if fork_of and copy_fork_permissions:
557 557 repo = fork_of
558 558 user_perms = UserRepoToPerm.query() \
559 559 .filter(UserRepoToPerm.repository == repo).all()
560 560 group_perms = UserGroupRepoToPerm.query() \
561 561 .filter(UserGroupRepoToPerm.repository == repo).all()
562 562
563 563 for perm in user_perms:
564 564 UserRepoToPerm.create(
565 565 perm.user, new_repo, perm.permission)
566 566
567 567 for perm in group_perms:
568 568 UserGroupRepoToPerm.create(
569 569 perm.users_group, new_repo, perm.permission)
570 570 # in case we copy permissions and also set this repo to private
571 571 # override the default user permission to make it a private repo
572 572 if private:
573 573 RepoModel(self.sa).grant_user_permission(
574 574 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
575 575
576 576 elif repo_group and copy_group_permissions:
577 577 user_perms = UserRepoGroupToPerm.query() \
578 578 .filter(UserRepoGroupToPerm.group == repo_group).all()
579 579
580 580 group_perms = UserGroupRepoGroupToPerm.query() \
581 581 .filter(UserGroupRepoGroupToPerm.group == repo_group).all()
582 582
583 583 for perm in user_perms:
584 584 perm_name = perm.permission.permission_name.replace(
585 585 'group.', 'repository.')
586 586 perm_obj = Permission.get_by_key(perm_name)
587 587 UserRepoToPerm.create(perm.user, new_repo, perm_obj)
588 588
589 589 for perm in group_perms:
590 590 perm_name = perm.permission.permission_name.replace(
591 591 'group.', 'repository.')
592 592 perm_obj = Permission.get_by_key(perm_name)
593 593 UserGroupRepoToPerm.create(perm.users_group, new_repo, perm_obj)
594 594
595 595 if private:
596 596 RepoModel(self.sa).grant_user_permission(
597 597 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
598 598
599 599 else:
600 600 perm_obj = self._create_default_perms(new_repo, private)
601 601 self.sa.add(perm_obj)
602 602
603 603 # now automatically start following this repository as owner
604 604 ScmModel(self.sa).toggle_following_repo(new_repo.repo_id, owner.user_id)
605 605
606 606 # we need to flush here, in order to check if database won't
607 607 # throw any exceptions, create filesystem dirs at the very end
608 608 self.sa.flush()
609 609 events.trigger(events.RepoCreateEvent(new_repo))
610 610 return new_repo
611 611
612 612 except Exception:
613 613 log.error(traceback.format_exc())
614 614 raise
615 615
616 616 def create(self, form_data, cur_user):
617 617 """
618 618 Create repository using celery tasks
619 619
620 620 :param form_data:
621 621 :param cur_user:
622 622 """
623 623 from rhodecode.lib.celerylib import tasks, run_task
624 624 return run_task(tasks.create_repo, form_data, cur_user)
625 625
626 626 def update_permissions(self, repo, perm_additions=None, perm_updates=None,
627 627 perm_deletions=None, check_perms=True,
628 628 cur_user=None):
629 629 if not perm_additions:
630 630 perm_additions = []
631 631 if not perm_updates:
632 632 perm_updates = []
633 633 if not perm_deletions:
634 634 perm_deletions = []
635 635
636 636 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
637 637
638 638 changes = {
639 639 'added': [],
640 640 'updated': [],
641 641 'deleted': [],
642 642 'default_user_changed': None
643 643 }
644 644
645 645 repo = self._get_repo(repo)
646 646
647 647 # update permissions
648 648 for member_id, perm, member_type in perm_updates:
649 649 member_id = int(member_id)
650 650 if member_type == 'user':
651 651 member_name = User.get(member_id).username
652 652 if member_name == User.DEFAULT_USER:
653 653 # NOTE(dan): detect if we changed permissions for default user
654 654 perm_obj = self.sa.query(UserRepoToPerm) \
655 655 .filter(UserRepoToPerm.user_id == member_id) \
656 656 .filter(UserRepoToPerm.repository == repo) \
657 657 .scalar()
658 658 if perm_obj and perm_obj.permission.permission_name != perm:
659 659 changes['default_user_changed'] = True
660 660
661 661 # this updates also current one if found
662 662 self.grant_user_permission(
663 663 repo=repo, user=member_id, perm=perm)
664 664 elif member_type == 'user_group':
665 665 # check if we have permissions to alter this usergroup
666 666 member_name = UserGroup.get(member_id).users_group_name
667 667 if not check_perms or HasUserGroupPermissionAny(
668 668 *req_perms)(member_name, user=cur_user):
669 669 self.grant_user_group_permission(
670 670 repo=repo, group_name=member_id, perm=perm)
671 671 else:
672 672 raise ValueError("member_type must be 'user' or 'user_group' "
673 673 "got {} instead".format(member_type))
674 674 changes['updated'].append({'type': member_type, 'id': member_id,
675 675 'name': member_name, 'new_perm': perm})
676 676
677 677 # set new permissions
678 678 for member_id, perm, member_type in perm_additions:
679 679 member_id = int(member_id)
680 680 if member_type == 'user':
681 681 member_name = User.get(member_id).username
682 682 self.grant_user_permission(
683 683 repo=repo, user=member_id, perm=perm)
684 684 elif member_type == 'user_group':
685 685 # check if we have permissions to alter this usergroup
686 686 member_name = UserGroup.get(member_id).users_group_name
687 687 if not check_perms or HasUserGroupPermissionAny(
688 688 *req_perms)(member_name, user=cur_user):
689 689 self.grant_user_group_permission(
690 690 repo=repo, group_name=member_id, perm=perm)
691 691 else:
692 692 raise ValueError("member_type must be 'user' or 'user_group' "
693 693 "got {} instead".format(member_type))
694 694
695 695 changes['added'].append({'type': member_type, 'id': member_id,
696 696 'name': member_name, 'new_perm': perm})
697 697 # delete permissions
698 698 for member_id, perm, member_type in perm_deletions:
699 699 member_id = int(member_id)
700 700 if member_type == 'user':
701 701 member_name = User.get(member_id).username
702 702 self.revoke_user_permission(repo=repo, user=member_id)
703 703 elif member_type == 'user_group':
704 704 # check if we have permissions to alter this usergroup
705 705 member_name = UserGroup.get(member_id).users_group_name
706 706 if not check_perms or HasUserGroupPermissionAny(
707 707 *req_perms)(member_name, user=cur_user):
708 708 self.revoke_user_group_permission(
709 709 repo=repo, group_name=member_id)
710 710 else:
711 711 raise ValueError("member_type must be 'user' or 'user_group' "
712 712 "got {} instead".format(member_type))
713 713
714 714 changes['deleted'].append({'type': member_type, 'id': member_id,
715 715 'name': member_name, 'new_perm': perm})
716 716 return changes
717 717
718 718 def create_fork(self, form_data, cur_user):
719 719 """
720 720 Simple wrapper into executing celery task for fork creation
721 721
722 722 :param form_data:
723 723 :param cur_user:
724 724 """
725 725 from rhodecode.lib.celerylib import tasks, run_task
726 726 return run_task(tasks.create_repo_fork, form_data, cur_user)
727 727
728 728 def archive(self, repo):
729 729 """
730 730 Archive given repository. Set archive flag.
731 731
732 732 :param repo:
733 733 """
734 734 repo = self._get_repo(repo)
735 735 if repo:
736 736
737 737 try:
738 738 repo.archived = True
739 739 self.sa.add(repo)
740 740 self.sa.commit()
741 741 except Exception:
742 742 log.error(traceback.format_exc())
743 743 raise
744 744
745 745 def delete(self, repo, forks=None, pull_requests=None, fs_remove=True, cur_user=None):
746 746 """
747 747 Delete given repository, forks parameter defines what do do with
748 748 attached forks. Throws AttachedForksError if deleted repo has attached
749 749 forks
750 750
751 751 :param repo:
752 752 :param forks: str 'delete' or 'detach'
753 753 :param pull_requests: str 'delete' or None
754 754 :param fs_remove: remove(archive) repo from filesystem
755 755 """
756 756 if not cur_user:
757 757 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
758 758 repo = self._get_repo(repo)
759 759 if repo:
760 760 if forks == 'detach':
761 761 for r in repo.forks:
762 762 r.fork = None
763 763 self.sa.add(r)
764 764 elif forks == 'delete':
765 765 for r in repo.forks:
766 766 self.delete(r, forks='delete')
767 767 elif [f for f in repo.forks]:
768 768 raise AttachedForksError()
769 769
770 770 # check for pull requests
771 771 pr_sources = repo.pull_requests_source
772 772 pr_targets = repo.pull_requests_target
773 773 if pull_requests != 'delete' and (pr_sources or pr_targets):
774 774 raise AttachedPullRequestsError()
775 775
776 776 old_repo_dict = repo.get_dict()
777 777 events.trigger(events.RepoPreDeleteEvent(repo))
778 778 try:
779 779 self.sa.delete(repo)
780 780 if fs_remove:
781 781 self._delete_filesystem_repo(repo)
782 782 else:
783 783 log.debug('skipping removal from filesystem')
784 784 old_repo_dict.update({
785 785 'deleted_by': cur_user,
786 786 'deleted_on': time.time(),
787 787 })
788 788 hooks_base.delete_repository(**old_repo_dict)
789 789 events.trigger(events.RepoDeleteEvent(repo))
790 790 except Exception:
791 791 log.error(traceback.format_exc())
792 792 raise
793 793
794 794 def grant_user_permission(self, repo, user, perm):
795 795 """
796 796 Grant permission for user on given repository, or update existing one
797 797 if found
798 798
799 799 :param repo: Instance of Repository, repository_id, or repository name
800 800 :param user: Instance of User, user_id or username
801 801 :param perm: Instance of Permission, or permission_name
802 802 """
803 803 user = self._get_user(user)
804 804 repo = self._get_repo(repo)
805 805 permission = self._get_perm(perm)
806 806
807 807 # check if we have that permission already
808 808 obj = self.sa.query(UserRepoToPerm) \
809 809 .filter(UserRepoToPerm.user == user) \
810 810 .filter(UserRepoToPerm.repository == repo) \
811 811 .scalar()
812 812 if obj is None:
813 813 # create new !
814 814 obj = UserRepoToPerm()
815 815 obj.repository = repo
816 816 obj.user = user
817 817 obj.permission = permission
818 818 self.sa.add(obj)
819 819 log.debug('Granted perm %s to %s on %s', perm, user, repo)
820 820 action_logger_generic(
821 821 'granted permission: {} to user: {} on repo: {}'.format(
822 822 perm, user, repo), namespace='security.repo')
823 823 return obj
824 824
825 825 def revoke_user_permission(self, repo, user):
826 826 """
827 827 Revoke permission for user on given repository
828 828
829 829 :param repo: Instance of Repository, repository_id, or repository name
830 830 :param user: Instance of User, user_id or username
831 831 """
832 832
833 833 user = self._get_user(user)
834 834 repo = self._get_repo(repo)
835 835
836 836 obj = self.sa.query(UserRepoToPerm) \
837 837 .filter(UserRepoToPerm.repository == repo) \
838 838 .filter(UserRepoToPerm.user == user) \
839 839 .scalar()
840 840 if obj:
841 841 self.sa.delete(obj)
842 842 log.debug('Revoked perm on %s on %s', repo, user)
843 843 action_logger_generic(
844 844 'revoked permission from user: {} on repo: {}'.format(
845 845 user, repo), namespace='security.repo')
846 846
847 847 def grant_user_group_permission(self, repo, group_name, perm):
848 848 """
849 849 Grant permission for user group on given repository, or update
850 850 existing one if found
851 851
852 852 :param repo: Instance of Repository, repository_id, or repository name
853 853 :param group_name: Instance of UserGroup, users_group_id,
854 854 or user group name
855 855 :param perm: Instance of Permission, or permission_name
856 856 """
857 857 repo = self._get_repo(repo)
858 858 group_name = self._get_user_group(group_name)
859 859 permission = self._get_perm(perm)
860 860
861 861 # check if we have that permission already
862 862 obj = self.sa.query(UserGroupRepoToPerm) \
863 863 .filter(UserGroupRepoToPerm.users_group == group_name) \
864 864 .filter(UserGroupRepoToPerm.repository == repo) \
865 865 .scalar()
866 866
867 867 if obj is None:
868 868 # create new
869 869 obj = UserGroupRepoToPerm()
870 870
871 871 obj.repository = repo
872 872 obj.users_group = group_name
873 873 obj.permission = permission
874 874 self.sa.add(obj)
875 875 log.debug('Granted perm %s to %s on %s', perm, group_name, repo)
876 876 action_logger_generic(
877 877 'granted permission: {} to usergroup: {} on repo: {}'.format(
878 878 perm, group_name, repo), namespace='security.repo')
879 879
880 880 return obj
881 881
882 882 def revoke_user_group_permission(self, repo, group_name):
883 883 """
884 884 Revoke permission for user group on given repository
885 885
886 886 :param repo: Instance of Repository, repository_id, or repository name
887 887 :param group_name: Instance of UserGroup, users_group_id,
888 888 or user group name
889 889 """
890 890 repo = self._get_repo(repo)
891 891 group_name = self._get_user_group(group_name)
892 892
893 893 obj = self.sa.query(UserGroupRepoToPerm) \
894 894 .filter(UserGroupRepoToPerm.repository == repo) \
895 895 .filter(UserGroupRepoToPerm.users_group == group_name) \
896 896 .scalar()
897 897 if obj:
898 898 self.sa.delete(obj)
899 899 log.debug('Revoked perm to %s on %s', repo, group_name)
900 900 action_logger_generic(
901 901 'revoked permission from usergroup: {} on repo: {}'.format(
902 902 group_name, repo), namespace='security.repo')
903 903
904 904 def delete_stats(self, repo_name):
905 905 """
906 906 removes stats for given repo
907 907
908 908 :param repo_name:
909 909 """
910 910 repo = self._get_repo(repo_name)
911 911 try:
912 912 obj = self.sa.query(Statistics) \
913 913 .filter(Statistics.repository == repo).scalar()
914 914 if obj:
915 915 self.sa.delete(obj)
916 916 except Exception:
917 917 log.error(traceback.format_exc())
918 918 raise
919 919
920 920 def add_repo_field(self, repo_name, field_key, field_label, field_value='',
921 921 field_type='str', field_desc=''):
922 922
923 923 repo = self._get_repo(repo_name)
924 924
925 925 new_field = RepositoryField()
926 926 new_field.repository = repo
927 927 new_field.field_key = field_key
928 928 new_field.field_type = field_type # python type
929 929 new_field.field_value = field_value
930 930 new_field.field_desc = field_desc
931 931 new_field.field_label = field_label
932 932 self.sa.add(new_field)
933 933 return new_field
934 934
935 935 def delete_repo_field(self, repo_name, field_key):
936 936 repo = self._get_repo(repo_name)
937 937 field = RepositoryField.get_by_key_name(field_key, repo)
938 938 if field:
939 939 self.sa.delete(field)
940 940
941 941 def set_landing_rev(self, repo, landing_rev_name):
942 942 if landing_rev_name.startswith('branch:'):
943 943 landing_rev_name = landing_rev_name.split('branch:')[-1]
944 944 scm_instance = repo.scm_instance()
945 945 if scm_instance:
946 946 return scm_instance._remote.set_head_ref(landing_rev_name)
947 947
948 948 def _create_filesystem_repo(self, repo_name, repo_type, repo_group,
949 949 clone_uri=None, repo_store_location=None,
950 950 use_global_config=False, install_hooks=True):
951 951 """
952 952 makes repository on filesystem. It's group aware means it'll create
953 953 a repository within a group, and alter the paths accordingly of
954 954 group location
955 955
956 956 :param repo_name:
957 957 :param alias:
958 958 :param parent:
959 959 :param clone_uri:
960 960 :param repo_store_location:
961 961 """
962 962 from rhodecode.lib.utils import is_valid_repo, is_valid_repo_group
963 963 from rhodecode.model.scm import ScmModel
964 964
965 965 if Repository.NAME_SEP in repo_name:
966 966 raise ValueError(
967 967 'repo_name must not contain groups got `%s`' % repo_name)
968 968
969 969 if isinstance(repo_group, RepoGroup):
970 970 new_parent_path = os.sep.join(repo_group.full_path_splitted)
971 971 else:
972 972 new_parent_path = repo_group or ''
973 973
974 974 if repo_store_location:
975 975 _paths = [repo_store_location]
976 976 else:
977 977 _paths = [self.repos_path, new_parent_path, repo_name]
978 978 # we need to make it str for mercurial
979 979 repo_path = os.path.join(*map(lambda x: safe_str(x), _paths))
980 980
981 981 # check if this path is not a repository
982 982 if is_valid_repo(repo_path, self.repos_path):
983 983 raise Exception('This path %s is a valid repository' % repo_path)
984 984
985 985 # check if this path is a group
986 986 if is_valid_repo_group(repo_path, self.repos_path):
987 987 raise Exception('This path %s is a valid group' % repo_path)
988 988
989 989 log.info('creating repo %s in %s from url: `%s`',
990 990 repo_name, safe_unicode(repo_path),
991 991 obfuscate_url_pw(clone_uri))
992 992
993 993 backend = get_backend(repo_type)
994 994
995 995 config_repo = None if use_global_config else repo_name
996 996 if config_repo and new_parent_path:
997 997 config_repo = Repository.NAME_SEP.join(
998 998 (new_parent_path, config_repo))
999 999 config = make_db_config(clear_session=False, repo=config_repo)
1000 1000 config.set('extensions', 'largefiles', '')
1001 1001
1002 1002 # patch and reset hooks section of UI config to not run any
1003 1003 # hooks on creating remote repo
1004 1004 config.clear_section('hooks')
1005 1005
1006 1006 # TODO: johbo: Unify this, hardcoded "bare=True" does not look nice
1007 1007 if repo_type == 'git':
1008 1008 repo = backend(
1009 1009 repo_path, config=config, create=True, src_url=clone_uri, bare=True,
1010 1010 with_wire={"cache": False})
1011 1011 else:
1012 1012 repo = backend(
1013 1013 repo_path, config=config, create=True, src_url=clone_uri,
1014 1014 with_wire={"cache": False})
1015 1015
1016 1016 if install_hooks:
1017 1017 repo.install_hooks()
1018 1018
1019 1019 log.debug('Created repo %s with %s backend',
1020 1020 safe_unicode(repo_name), safe_unicode(repo_type))
1021 1021 return repo
1022 1022
1023 1023 def _rename_filesystem_repo(self, old, new):
1024 1024 """
1025 1025 renames repository on filesystem
1026 1026
1027 1027 :param old: old name
1028 1028 :param new: new name
1029 1029 """
1030 1030 log.info('renaming repo from %s to %s', old, new)
1031 1031
1032 1032 old_path = os.path.join(self.repos_path, old)
1033 1033 new_path = os.path.join(self.repos_path, new)
1034 1034 if os.path.isdir(new_path):
1035 1035 raise Exception(
1036 1036 'Was trying to rename to already existing dir %s' % new_path
1037 1037 )
1038 1038 shutil.move(old_path, new_path)
1039 1039
1040 1040 def _delete_filesystem_repo(self, repo):
1041 1041 """
1042 1042 removes repo from filesystem, the removal is acctually made by
1043 1043 added rm__ prefix into dir, and rename internat .hg/.git dirs so this
1044 1044 repository is no longer valid for rhodecode, can be undeleted later on
1045 1045 by reverting the renames on this repository
1046 1046
1047 1047 :param repo: repo object
1048 1048 """
1049 1049 rm_path = os.path.join(self.repos_path, repo.repo_name)
1050 1050 repo_group = repo.group
1051 1051 log.info("Removing repository %s", rm_path)
1052 1052 # disable hg/git internal that it doesn't get detected as repo
1053 1053 alias = repo.repo_type
1054 1054
1055 1055 config = make_db_config(clear_session=False)
1056 1056 config.set('extensions', 'largefiles', '')
1057 1057 bare = getattr(repo.scm_instance(config=config), 'bare', False)
1058 1058
1059 1059 # skip this for bare git repos
1060 1060 if not bare:
1061 1061 # disable VCS repo
1062 1062 vcs_path = os.path.join(rm_path, '.%s' % alias)
1063 1063 if os.path.exists(vcs_path):
1064 1064 shutil.move(vcs_path, os.path.join(rm_path, 'rm__.%s' % alias))
1065 1065
1066 1066 _now = datetime.datetime.now()
1067 1067 _ms = str(_now.microsecond).rjust(6, '0')
1068 1068 _d = 'rm__%s__%s' % (_now.strftime('%Y%m%d_%H%M%S_' + _ms),
1069 1069 repo.just_name)
1070 1070 if repo_group:
1071 1071 # if repository is in group, prefix the removal path with the group
1072 1072 args = repo_group.full_path_splitted + [_d]
1073 1073 _d = os.path.join(*args)
1074 1074
1075 1075 if os.path.isdir(rm_path):
1076 1076 shutil.move(rm_path, os.path.join(self.repos_path, _d))
1077 1077
1078 1078 # finally cleanup diff-cache if it exists
1079 1079 cached_diffs_dir = repo.cached_diffs_dir
1080 1080 if os.path.isdir(cached_diffs_dir):
1081 1081 shutil.rmtree(cached_diffs_dir)
1082 1082
1083 1083
1084 1084 class ReadmeFinder:
1085 1085 """
1086 1086 Utility which knows how to find a readme for a specific commit.
1087 1087
1088 1088 The main idea is that this is a configurable algorithm. When creating an
1089 1089 instance you can define parameters, currently only the `default_renderer`.
1090 1090 Based on this configuration the method :meth:`search` behaves slightly
1091 1091 different.
1092 1092 """
1093 1093
1094 1094 readme_re = re.compile(r'^readme(\.[^\.]+)?$', re.IGNORECASE)
1095 1095 path_re = re.compile(r'^docs?', re.IGNORECASE)
1096 1096
1097 1097 default_priorities = {
1098 1098 None: 0,
1099 1099 '.text': 2,
1100 1100 '.txt': 3,
1101 1101 '.rst': 1,
1102 1102 '.rest': 2,
1103 1103 '.md': 1,
1104 1104 '.mkdn': 2,
1105 1105 '.mdown': 3,
1106 1106 '.markdown': 4,
1107 1107 }
1108 1108
1109 1109 path_priority = {
1110 1110 'doc': 0,
1111 1111 'docs': 1,
1112 1112 }
1113 1113
1114 1114 FALLBACK_PRIORITY = 99
1115 1115
1116 1116 RENDERER_TO_EXTENSION = {
1117 1117 'rst': ['.rst', '.rest'],
1118 1118 'markdown': ['.md', 'mkdn', '.mdown', '.markdown'],
1119 1119 }
1120 1120
1121 1121 def __init__(self, default_renderer=None):
1122 1122 self._default_renderer = default_renderer
1123 1123 self._renderer_extensions = self.RENDERER_TO_EXTENSION.get(
1124 1124 default_renderer, [])
1125 1125
1126 1126 def search(self, commit, path=u'/'):
1127 1127 """
1128 1128 Find a readme in the given `commit`.
1129 1129 """
1130 1130 nodes = commit.get_nodes(path)
1131 1131 matches = self._match_readmes(nodes)
1132 1132 matches = self._sort_according_to_priority(matches)
1133 1133 if matches:
1134 1134 return matches[0].node
1135 1135
1136 1136 paths = self._match_paths(nodes)
1137 1137 paths = self._sort_paths_according_to_priority(paths)
1138 1138 for path in paths:
1139 1139 match = self.search(commit, path=path)
1140 1140 if match:
1141 1141 return match
1142 1142
1143 1143 return None
1144 1144
1145 1145 def _match_readmes(self, nodes):
1146 1146 for node in nodes:
1147 1147 if not node.is_file():
1148 1148 continue
1149 1149 path = node.path.rsplit('/', 1)[-1]
1150 1150 match = self.readme_re.match(path)
1151 1151 if match:
1152 1152 extension = match.group(1)
1153 1153 yield ReadmeMatch(node, match, self._priority(extension))
1154 1154
1155 1155 def _match_paths(self, nodes):
1156 1156 for node in nodes:
1157 1157 if not node.is_dir():
1158 1158 continue
1159 1159 match = self.path_re.match(node.path)
1160 1160 if match:
1161 1161 yield node.path
1162 1162
1163 1163 def _priority(self, extension):
1164 1164 renderer_priority = (
1165 1165 0 if extension in self._renderer_extensions else 1)
1166 1166 extension_priority = self.default_priorities.get(
1167 1167 extension, self.FALLBACK_PRIORITY)
1168 1168 return (renderer_priority, extension_priority)
1169 1169
1170 1170 def _sort_according_to_priority(self, matches):
1171 1171
1172 1172 def priority_and_path(match):
1173 1173 return (match.priority, match.path)
1174 1174
1175 1175 return sorted(matches, key=priority_and_path)
1176 1176
1177 1177 def _sort_paths_according_to_priority(self, paths):
1178 1178
1179 1179 def priority_and_path(path):
1180 1180 return (self.path_priority.get(path, self.FALLBACK_PRIORITY), path)
1181 1181
1182 1182 return sorted(paths, key=priority_and_path)
1183 1183
1184 1184
1185 1185 class ReadmeMatch:
1186 1186
1187 1187 def __init__(self, node, match, priority):
1188 1188 self.node = node
1189 1189 self._match = match
1190 1190 self.priority = priority
1191 1191
1192 1192 @property
1193 1193 def path(self):
1194 1194 return self.node.path
1195 1195
1196 1196 def __repr__(self):
1197 1197 return '<ReadmeMatch {} priority={}'.format(self.path, self.priority)
@@ -1,897 +1,897 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 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 time
32 32 import traceback
33 33 import string
34 34
35 35 from zope.cachedescriptors.property import Lazy as LazyProperty
36 36
37 37 from rhodecode import events
38 38 from rhodecode.model import BaseModel
39 39 from rhodecode.model.db import (_hash_key, func, or_, in_filter_generator,
40 40 Session, RepoGroup, UserRepoGroupToPerm, User, Permission, UserGroupRepoGroupToPerm,
41 41 UserGroup, Repository)
42 42 from rhodecode.model.permission import PermissionModel
43 43 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
44 44 from rhodecode.lib.caching_query import FromCache
45 45 from rhodecode.lib.utils2 import action_logger_generic
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 class RepoGroupModel(BaseModel):
51 51
52 52 cls = RepoGroup
53 53 PERSONAL_GROUP_DESC = 'personal repo group of user `%(username)s`'
54 54 PERSONAL_GROUP_PATTERN = '${username}' # default
55 55
56 56 def _get_user_group(self, users_group):
57 57 return self._get_instance(UserGroup, users_group,
58 58 callback=UserGroup.get_by_group_name)
59 59
60 60 def _get_repo_group(self, repo_group):
61 61 return self._get_instance(RepoGroup, repo_group,
62 62 callback=RepoGroup.get_by_group_name)
63 63
64 64 def get_repo_group(self, repo_group):
65 65 return self._get_repo_group(repo_group)
66 66
67 67 @LazyProperty
68 68 def repos_path(self):
69 69 """
70 70 Gets the repositories root path from database
71 71 """
72 72
73 73 settings_model = VcsSettingsModel(sa=self.sa)
74 74 return settings_model.get_repos_location()
75 75
76 76 def get_by_group_name(self, repo_group_name, cache=None):
77 77 repo = self.sa.query(RepoGroup) \
78 78 .filter(RepoGroup.group_name == repo_group_name)
79 79
80 80 if cache:
81 81 name_key = _hash_key(repo_group_name)
82 82 repo = repo.options(
83 FromCache("sql_cache_short", "get_repo_group_%s" % name_key))
83 FromCache("sql_cache_short", f"get_repo_group_{name_key}"))
84 84 return repo.scalar()
85 85
86 86 def get_default_create_personal_repo_group(self):
87 87 value = SettingsModel().get_setting_by_name(
88 88 'create_personal_repo_group')
89 89 return value.app_settings_value if value else None or False
90 90
91 91 def get_personal_group_name_pattern(self):
92 92 value = SettingsModel().get_setting_by_name(
93 93 'personal_repo_group_pattern')
94 94 val = value.app_settings_value if value else None
95 95 group_template = val or self.PERSONAL_GROUP_PATTERN
96 96
97 97 group_template = group_template.lstrip('/')
98 98 return group_template
99 99
100 100 def get_personal_group_name(self, user):
101 101 template = self.get_personal_group_name_pattern()
102 102 return string.Template(template).safe_substitute(
103 103 username=user.username,
104 104 user_id=user.user_id,
105 105 first_name=user.first_name,
106 106 last_name=user.last_name,
107 107 )
108 108
109 109 def create_personal_repo_group(self, user, commit_early=True):
110 110 desc = self.PERSONAL_GROUP_DESC % {'username': user.username}
111 111 personal_repo_group_name = self.get_personal_group_name(user)
112 112
113 113 # create a new one
114 114 RepoGroupModel().create(
115 115 group_name=personal_repo_group_name,
116 116 group_description=desc,
117 117 owner=user.username,
118 118 personal=True,
119 119 commit_early=commit_early)
120 120
121 121 def _create_default_perms(self, new_group):
122 122 # create default permission
123 123 default_perm = 'group.read'
124 124 def_user = User.get_default_user()
125 125 for p in def_user.user_perms:
126 126 if p.permission.permission_name.startswith('group.'):
127 127 default_perm = p.permission.permission_name
128 128 break
129 129
130 130 repo_group_to_perm = UserRepoGroupToPerm()
131 131 repo_group_to_perm.permission = Permission.get_by_key(default_perm)
132 132
133 133 repo_group_to_perm.group = new_group
134 134 repo_group_to_perm.user_id = def_user.user_id
135 135 return repo_group_to_perm
136 136
137 137 def _get_group_name_and_parent(self, group_name_full, repo_in_path=False,
138 138 get_object=False):
139 139 """
140 140 Get's the group name and a parent group name from given group name.
141 141 If repo_in_path is set to truth, we asume the full path also includes
142 142 repo name, in such case we clean the last element.
143 143
144 144 :param group_name_full:
145 145 """
146 146 split_paths = 1
147 147 if repo_in_path:
148 148 split_paths = 2
149 149 _parts = group_name_full.rsplit(RepoGroup.url_sep(), split_paths)
150 150
151 151 if repo_in_path and len(_parts) > 1:
152 152 # such case last element is the repo_name
153 153 _parts.pop(-1)
154 154 group_name_cleaned = _parts[-1] # just the group name
155 155 parent_repo_group_name = None
156 156
157 157 if len(_parts) > 1:
158 158 parent_repo_group_name = _parts[0]
159 159
160 160 parent_group = None
161 161 if parent_repo_group_name:
162 162 parent_group = RepoGroup.get_by_group_name(parent_repo_group_name)
163 163
164 164 if get_object:
165 165 return group_name_cleaned, parent_repo_group_name, parent_group
166 166
167 167 return group_name_cleaned, parent_repo_group_name
168 168
169 169 def check_exist_filesystem(self, group_name, exc_on_failure=True):
170 170 create_path = os.path.join(self.repos_path, group_name)
171 171 log.debug('creating new group in %s', create_path)
172 172
173 173 if os.path.isdir(create_path):
174 174 if exc_on_failure:
175 175 abs_create_path = os.path.abspath(create_path)
176 176 raise Exception('Directory `{}` already exists !'.format(abs_create_path))
177 177 return False
178 178 return True
179 179
180 180 def _create_group(self, group_name):
181 181 """
182 182 makes repository group on filesystem
183 183
184 184 :param repo_name:
185 185 :param parent_id:
186 186 """
187 187
188 188 self.check_exist_filesystem(group_name)
189 189 create_path = os.path.join(self.repos_path, group_name)
190 190 log.debug('creating new group in %s', create_path)
191 191 os.makedirs(create_path, mode=0o755)
192 192 log.debug('created group in %s', create_path)
193 193
194 194 def _rename_group(self, old, new):
195 195 """
196 196 Renames a group on filesystem
197 197
198 198 :param group_name:
199 199 """
200 200
201 201 if old == new:
202 202 log.debug('skipping group rename')
203 203 return
204 204
205 205 log.debug('renaming repository group from %s to %s', old, new)
206 206
207 207 old_path = os.path.join(self.repos_path, old)
208 208 new_path = os.path.join(self.repos_path, new)
209 209
210 210 log.debug('renaming repos paths from %s to %s', old_path, new_path)
211 211
212 212 if os.path.isdir(new_path):
213 213 raise Exception('Was trying to rename to already '
214 214 'existing dir %s' % new_path)
215 215 shutil.move(old_path, new_path)
216 216
217 217 def _delete_filesystem_group(self, group, force_delete=False):
218 218 """
219 219 Deletes a group from a filesystem
220 220
221 221 :param group: instance of group from database
222 222 :param force_delete: use shutil rmtree to remove all objects
223 223 """
224 224 paths = group.full_path.split(RepoGroup.url_sep())
225 225 paths = os.sep.join(paths)
226 226
227 227 rm_path = os.path.join(self.repos_path, paths)
228 228 log.info("Removing group %s", rm_path)
229 229 # delete only if that path really exists
230 230 if os.path.isdir(rm_path):
231 231 if force_delete:
232 232 shutil.rmtree(rm_path)
233 233 else:
234 234 # archive that group`
235 235 _now = datetime.datetime.now()
236 236 _ms = str(_now.microsecond).rjust(6, '0')
237 237 _d = 'rm__%s_GROUP_%s' % (
238 238 _now.strftime('%Y%m%d_%H%M%S_' + _ms), group.name)
239 239 shutil.move(rm_path, os.path.join(self.repos_path, _d))
240 240
241 241 def create(self, group_name, group_description, owner, just_db=False,
242 242 copy_permissions=False, personal=None, commit_early=True):
243 243
244 244 (group_name_cleaned,
245 245 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(group_name)
246 246
247 247 parent_group = None
248 248 if parent_group_name:
249 249 parent_group = self._get_repo_group(parent_group_name)
250 250 if not parent_group:
251 251 # we tried to create a nested group, but the parent is not
252 252 # existing
253 253 raise ValueError(
254 254 'Parent group `%s` given in `%s` group name '
255 255 'is not yet existing.' % (parent_group_name, group_name))
256 256
257 257 # because we are doing a cleanup, we need to check if such directory
258 258 # already exists. If we don't do that we can accidentally delete
259 259 # existing directory via cleanup that can cause data issues, since
260 260 # delete does a folder rename to special syntax later cleanup
261 261 # functions can delete this
262 262 cleanup_group = self.check_exist_filesystem(group_name,
263 263 exc_on_failure=False)
264 264 user = self._get_user(owner)
265 265 if not user:
266 266 raise ValueError('Owner %s not found as rhodecode user', owner)
267 267
268 268 try:
269 269 new_repo_group = RepoGroup()
270 270 new_repo_group.user = user
271 271 new_repo_group.group_description = group_description or group_name
272 272 new_repo_group.parent_group = parent_group
273 273 new_repo_group.group_name = group_name
274 274 new_repo_group.personal = personal
275 275
276 276 self.sa.add(new_repo_group)
277 277
278 278 # create an ADMIN permission for owner except if we're super admin,
279 279 # later owner should go into the owner field of groups
280 280 if not user.is_admin:
281 281 self.grant_user_permission(repo_group=new_repo_group,
282 282 user=owner, perm='group.admin')
283 283
284 284 if parent_group and copy_permissions:
285 285 # copy permissions from parent
286 286 user_perms = UserRepoGroupToPerm.query() \
287 287 .filter(UserRepoGroupToPerm.group == parent_group).all()
288 288
289 289 group_perms = UserGroupRepoGroupToPerm.query() \
290 290 .filter(UserGroupRepoGroupToPerm.group == parent_group).all()
291 291
292 292 for perm in user_perms:
293 293 # don't copy over the permission for user who is creating
294 294 # this group, if he is not super admin he get's admin
295 295 # permission set above
296 296 if perm.user != user or user.is_admin:
297 297 UserRepoGroupToPerm.create(
298 298 perm.user, new_repo_group, perm.permission)
299 299
300 300 for perm in group_perms:
301 301 UserGroupRepoGroupToPerm.create(
302 302 perm.users_group, new_repo_group, perm.permission)
303 303 else:
304 304 perm_obj = self._create_default_perms(new_repo_group)
305 305 self.sa.add(perm_obj)
306 306
307 307 # now commit the changes, earlier so we are sure everything is in
308 308 # the database.
309 309 if commit_early:
310 310 self.sa.commit()
311 311 if not just_db:
312 312 self._create_group(new_repo_group.group_name)
313 313
314 314 # trigger the post hook
315 315 from rhodecode.lib import hooks_base
316 316 repo_group = RepoGroup.get_by_group_name(group_name)
317 317
318 318 # update repo group commit caches initially
319 319 repo_group.update_commit_cache()
320 320
321 321 hooks_base.create_repository_group(
322 322 created_by=user.username, **repo_group.get_dict())
323 323
324 324 # Trigger create event.
325 325 events.trigger(events.RepoGroupCreateEvent(repo_group))
326 326
327 327 return new_repo_group
328 328 except Exception:
329 329 self.sa.rollback()
330 330 log.exception('Exception occurred when creating repository group, '
331 331 'doing cleanup...')
332 332 # rollback things manually !
333 333 repo_group = RepoGroup.get_by_group_name(group_name)
334 334 if repo_group:
335 335 RepoGroup.delete(repo_group.group_id)
336 336 self.sa.commit()
337 337 if cleanup_group:
338 338 RepoGroupModel()._delete_filesystem_group(repo_group)
339 339 raise
340 340
341 341 def update_permissions(
342 342 self, repo_group, perm_additions=None, perm_updates=None,
343 343 perm_deletions=None, recursive=None, check_perms=True,
344 344 cur_user=None):
345 345 from rhodecode.model.repo import RepoModel
346 346 from rhodecode.lib.auth import HasUserGroupPermissionAny
347 347
348 348 if not perm_additions:
349 349 perm_additions = []
350 350 if not perm_updates:
351 351 perm_updates = []
352 352 if not perm_deletions:
353 353 perm_deletions = []
354 354
355 355 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
356 356
357 357 changes = {
358 358 'added': [],
359 359 'updated': [],
360 360 'deleted': [],
361 361 'default_user_changed': None
362 362 }
363 363
364 364 def _set_perm_user(obj, user, perm):
365 365 if isinstance(obj, RepoGroup):
366 366 self.grant_user_permission(
367 367 repo_group=obj, user=user, perm=perm)
368 368 elif isinstance(obj, Repository):
369 369 # private repos will not allow to change the default
370 370 # permissions using recursive mode
371 371 if obj.private and user == User.DEFAULT_USER:
372 372 return
373 373
374 374 # we set group permission but we have to switch to repo
375 375 # permission
376 376 perm = perm.replace('group.', 'repository.')
377 377 RepoModel().grant_user_permission(
378 378 repo=obj, user=user, perm=perm)
379 379
380 380 def _set_perm_group(obj, users_group, perm):
381 381 if isinstance(obj, RepoGroup):
382 382 self.grant_user_group_permission(
383 383 repo_group=obj, group_name=users_group, perm=perm)
384 384 elif isinstance(obj, Repository):
385 385 # we set group permission but we have to switch to repo
386 386 # permission
387 387 perm = perm.replace('group.', 'repository.')
388 388 RepoModel().grant_user_group_permission(
389 389 repo=obj, group_name=users_group, perm=perm)
390 390
391 391 def _revoke_perm_user(obj, user):
392 392 if isinstance(obj, RepoGroup):
393 393 self.revoke_user_permission(repo_group=obj, user=user)
394 394 elif isinstance(obj, Repository):
395 395 RepoModel().revoke_user_permission(repo=obj, user=user)
396 396
397 397 def _revoke_perm_group(obj, user_group):
398 398 if isinstance(obj, RepoGroup):
399 399 self.revoke_user_group_permission(
400 400 repo_group=obj, group_name=user_group)
401 401 elif isinstance(obj, Repository):
402 402 RepoModel().revoke_user_group_permission(
403 403 repo=obj, group_name=user_group)
404 404
405 405 # start updates
406 406 log.debug('Now updating permissions for %s in recursive mode:%s',
407 407 repo_group, recursive)
408 408
409 409 # initialize check function, we'll call that multiple times
410 410 has_group_perm = HasUserGroupPermissionAny(*req_perms)
411 411
412 412 for obj in repo_group.recursive_groups_and_repos():
413 413 # iterated obj is an instance of a repos group or repository in
414 414 # that group, recursive option can be: none, repos, groups, all
415 415 if recursive == 'all':
416 416 obj = obj
417 417 elif recursive == 'repos':
418 418 # skip groups, other than this one
419 419 if isinstance(obj, RepoGroup) and not obj == repo_group:
420 420 continue
421 421 elif recursive == 'groups':
422 422 # skip repos
423 423 if isinstance(obj, Repository):
424 424 continue
425 425 else: # recursive == 'none':
426 426 # DEFAULT option - don't apply to iterated objects
427 427 # also we do a break at the end of this loop. if we are not
428 428 # in recursive mode
429 429 obj = repo_group
430 430
431 431 change_obj = obj.get_api_data()
432 432
433 433 # update permissions
434 434 for member_id, perm, member_type in perm_updates:
435 435 member_id = int(member_id)
436 436 if member_type == 'user':
437 437 member_name = User.get(member_id).username
438 438 if isinstance(obj, RepoGroup) and obj == repo_group and member_name == User.DEFAULT_USER:
439 439 # NOTE(dan): detect if we changed permissions for default user
440 440 perm_obj = self.sa.query(UserRepoGroupToPerm) \
441 441 .filter(UserRepoGroupToPerm.user_id == member_id) \
442 442 .filter(UserRepoGroupToPerm.group == repo_group) \
443 443 .scalar()
444 444 if perm_obj and perm_obj.permission.permission_name != perm:
445 445 changes['default_user_changed'] = True
446 446
447 447 # this updates also current one if found
448 448 _set_perm_user(obj, user=member_id, perm=perm)
449 449 elif member_type == 'user_group':
450 450 member_name = UserGroup.get(member_id).users_group_name
451 451 if not check_perms or has_group_perm(member_name,
452 452 user=cur_user):
453 453 _set_perm_group(obj, users_group=member_id, perm=perm)
454 454 else:
455 455 raise ValueError("member_type must be 'user' or 'user_group' "
456 456 "got {} instead".format(member_type))
457 457
458 458 changes['updated'].append(
459 459 {'change_obj': change_obj, 'type': member_type,
460 460 'id': member_id, 'name': member_name, 'new_perm': perm})
461 461
462 462 # set new permissions
463 463 for member_id, perm, member_type in perm_additions:
464 464 member_id = int(member_id)
465 465 if member_type == 'user':
466 466 member_name = User.get(member_id).username
467 467 _set_perm_user(obj, user=member_id, perm=perm)
468 468 elif member_type == 'user_group':
469 469 # check if we have permissions to alter this usergroup
470 470 member_name = UserGroup.get(member_id).users_group_name
471 471 if not check_perms or has_group_perm(member_name,
472 472 user=cur_user):
473 473 _set_perm_group(obj, users_group=member_id, perm=perm)
474 474 else:
475 475 raise ValueError("member_type must be 'user' or 'user_group' "
476 476 "got {} instead".format(member_type))
477 477
478 478 changes['added'].append(
479 479 {'change_obj': change_obj, 'type': member_type,
480 480 'id': member_id, 'name': member_name, 'new_perm': perm})
481 481
482 482 # delete permissions
483 483 for member_id, perm, member_type in perm_deletions:
484 484 member_id = int(member_id)
485 485 if member_type == 'user':
486 486 member_name = User.get(member_id).username
487 487 _revoke_perm_user(obj, user=member_id)
488 488 elif member_type == 'user_group':
489 489 # check if we have permissions to alter this usergroup
490 490 member_name = UserGroup.get(member_id).users_group_name
491 491 if not check_perms or has_group_perm(member_name,
492 492 user=cur_user):
493 493 _revoke_perm_group(obj, user_group=member_id)
494 494 else:
495 495 raise ValueError("member_type must be 'user' or 'user_group' "
496 496 "got {} instead".format(member_type))
497 497
498 498 changes['deleted'].append(
499 499 {'change_obj': change_obj, 'type': member_type,
500 500 'id': member_id, 'name': member_name, 'new_perm': perm})
501 501
502 502 # if it's not recursive call for all,repos,groups
503 503 # break the loop and don't proceed with other changes
504 504 if recursive not in ['all', 'repos', 'groups']:
505 505 break
506 506
507 507 return changes
508 508
509 509 def update(self, repo_group, form_data):
510 510 try:
511 511 repo_group = self._get_repo_group(repo_group)
512 512 old_path = repo_group.full_path
513 513
514 514 # change properties
515 515 if 'group_description' in form_data:
516 516 repo_group.group_description = form_data['group_description']
517 517
518 518 if 'enable_locking' in form_data:
519 519 repo_group.enable_locking = form_data['enable_locking']
520 520
521 521 if 'group_parent_id' in form_data:
522 522 parent_group = (
523 523 self._get_repo_group(form_data['group_parent_id']))
524 524 repo_group.group_parent_id = (
525 525 parent_group.group_id if parent_group else None)
526 526 repo_group.parent_group = parent_group
527 527
528 528 # mikhail: to update the full_path, we have to explicitly
529 529 # update group_name
530 530 group_name = form_data.get('group_name', repo_group.name)
531 531 repo_group.group_name = repo_group.get_new_name(group_name)
532 532
533 533 new_path = repo_group.full_path
534 534
535 535 affected_user_ids = []
536 536 if 'user' in form_data:
537 537 old_owner_id = repo_group.user.user_id
538 538 new_owner = User.get_by_username(form_data['user'])
539 539 repo_group.user = new_owner
540 540
541 541 if old_owner_id != new_owner.user_id:
542 542 affected_user_ids = [new_owner.user_id, old_owner_id]
543 543
544 544 self.sa.add(repo_group)
545 545
546 546 # iterate over all members of this groups and do fixes
547 547 # set locking if given
548 548 # if obj is a repoGroup also fix the name of the group according
549 549 # to the parent
550 550 # if obj is a Repo fix it's name
551 551 # this can be potentially heavy operation
552 552 for obj in repo_group.recursive_groups_and_repos():
553 553 # set the value from it's parent
554 554 obj.enable_locking = repo_group.enable_locking
555 555 if isinstance(obj, RepoGroup):
556 556 new_name = obj.get_new_name(obj.name)
557 557 log.debug('Fixing group %s to new name %s',
558 558 obj.group_name, new_name)
559 559 obj.group_name = new_name
560 560
561 561 elif isinstance(obj, Repository):
562 562 # we need to get all repositories from this new group and
563 563 # rename them accordingly to new group path
564 564 new_name = obj.get_new_name(obj.just_name)
565 565 log.debug('Fixing repo %s to new name %s',
566 566 obj.repo_name, new_name)
567 567 obj.repo_name = new_name
568 568
569 569 self.sa.add(obj)
570 570
571 571 self._rename_group(old_path, new_path)
572 572
573 573 # Trigger update event.
574 574 events.trigger(events.RepoGroupUpdateEvent(repo_group))
575 575
576 576 if affected_user_ids:
577 577 PermissionModel().trigger_permission_flush(affected_user_ids)
578 578
579 579 return repo_group
580 580 except Exception:
581 581 log.error(traceback.format_exc())
582 582 raise
583 583
584 584 def delete(self, repo_group, force_delete=False, fs_remove=True):
585 585 repo_group = self._get_repo_group(repo_group)
586 586 if not repo_group:
587 587 return False
588 588 try:
589 589 self.sa.delete(repo_group)
590 590 if fs_remove:
591 591 self._delete_filesystem_group(repo_group, force_delete)
592 592 else:
593 593 log.debug('skipping removal from filesystem')
594 594
595 595 # Trigger delete event.
596 596 events.trigger(events.RepoGroupDeleteEvent(repo_group))
597 597 return True
598 598
599 599 except Exception:
600 600 log.error('Error removing repo_group %s', repo_group)
601 601 raise
602 602
603 603 def grant_user_permission(self, repo_group, user, perm):
604 604 """
605 605 Grant permission for user on given repository group, or update
606 606 existing one if found
607 607
608 608 :param repo_group: Instance of RepoGroup, repositories_group_id,
609 609 or repositories_group name
610 610 :param user: Instance of User, user_id or username
611 611 :param perm: Instance of Permission, or permission_name
612 612 """
613 613
614 614 repo_group = self._get_repo_group(repo_group)
615 615 user = self._get_user(user)
616 616 permission = self._get_perm(perm)
617 617
618 618 # check if we have that permission already
619 619 obj = self.sa.query(UserRepoGroupToPerm)\
620 620 .filter(UserRepoGroupToPerm.user == user)\
621 621 .filter(UserRepoGroupToPerm.group == repo_group)\
622 622 .scalar()
623 623 if obj is None:
624 624 # create new !
625 625 obj = UserRepoGroupToPerm()
626 626 obj.group = repo_group
627 627 obj.user = user
628 628 obj.permission = permission
629 629 self.sa.add(obj)
630 630 log.debug('Granted perm %s to %s on %s', perm, user, repo_group)
631 631 action_logger_generic(
632 632 'granted permission: {} to user: {} on repogroup: {}'.format(
633 633 perm, user, repo_group), namespace='security.repogroup')
634 634 return obj
635 635
636 636 def revoke_user_permission(self, repo_group, user):
637 637 """
638 638 Revoke permission for user on given repository group
639 639
640 640 :param repo_group: Instance of RepoGroup, repositories_group_id,
641 641 or repositories_group name
642 642 :param user: Instance of User, user_id or username
643 643 """
644 644
645 645 repo_group = self._get_repo_group(repo_group)
646 646 user = self._get_user(user)
647 647
648 648 obj = self.sa.query(UserRepoGroupToPerm)\
649 649 .filter(UserRepoGroupToPerm.user == user)\
650 650 .filter(UserRepoGroupToPerm.group == repo_group)\
651 651 .scalar()
652 652 if obj:
653 653 self.sa.delete(obj)
654 654 log.debug('Revoked perm on %s on %s', repo_group, user)
655 655 action_logger_generic(
656 656 'revoked permission from user: {} on repogroup: {}'.format(
657 657 user, repo_group), namespace='security.repogroup')
658 658
659 659 def grant_user_group_permission(self, repo_group, group_name, perm):
660 660 """
661 661 Grant permission for user group on given repository group, or update
662 662 existing one if found
663 663
664 664 :param repo_group: Instance of RepoGroup, repositories_group_id,
665 665 or repositories_group name
666 666 :param group_name: Instance of UserGroup, users_group_id,
667 667 or user group name
668 668 :param perm: Instance of Permission, or permission_name
669 669 """
670 670 repo_group = self._get_repo_group(repo_group)
671 671 group_name = self._get_user_group(group_name)
672 672 permission = self._get_perm(perm)
673 673
674 674 # check if we have that permission already
675 675 obj = self.sa.query(UserGroupRepoGroupToPerm)\
676 676 .filter(UserGroupRepoGroupToPerm.group == repo_group)\
677 677 .filter(UserGroupRepoGroupToPerm.users_group == group_name)\
678 678 .scalar()
679 679
680 680 if obj is None:
681 681 # create new
682 682 obj = UserGroupRepoGroupToPerm()
683 683
684 684 obj.group = repo_group
685 685 obj.users_group = group_name
686 686 obj.permission = permission
687 687 self.sa.add(obj)
688 688 log.debug('Granted perm %s to %s on %s', perm, group_name, repo_group)
689 689 action_logger_generic(
690 690 'granted permission: {} to usergroup: {} on repogroup: {}'.format(
691 691 perm, group_name, repo_group), namespace='security.repogroup')
692 692 return obj
693 693
694 694 def revoke_user_group_permission(self, repo_group, group_name):
695 695 """
696 696 Revoke permission for user group on given repository group
697 697
698 698 :param repo_group: Instance of RepoGroup, repositories_group_id,
699 699 or repositories_group name
700 700 :param group_name: Instance of UserGroup, users_group_id,
701 701 or user group name
702 702 """
703 703 repo_group = self._get_repo_group(repo_group)
704 704 group_name = self._get_user_group(group_name)
705 705
706 706 obj = self.sa.query(UserGroupRepoGroupToPerm)\
707 707 .filter(UserGroupRepoGroupToPerm.group == repo_group)\
708 708 .filter(UserGroupRepoGroupToPerm.users_group == group_name)\
709 709 .scalar()
710 710 if obj:
711 711 self.sa.delete(obj)
712 712 log.debug('Revoked perm to %s on %s', repo_group, group_name)
713 713 action_logger_generic(
714 714 'revoked permission from usergroup: {} on repogroup: {}'.format(
715 715 group_name, repo_group), namespace='security.repogroup')
716 716
717 717 @classmethod
718 718 def update_commit_cache(cls, repo_groups=None):
719 719 if not repo_groups:
720 720 repo_groups = RepoGroup.getAll()
721 721 for repo_group in repo_groups:
722 722 repo_group.update_commit_cache()
723 723
724 724 def get_repo_groups_as_dict(self, repo_group_list=None, admin=False,
725 725 super_user_actions=False):
726 726
727 727 from pyramid.threadlocal import get_current_request
728 728 _render = get_current_request().get_partial_renderer(
729 729 'rhodecode:templates/data_table/_dt_elements.mako')
730 730 c = _render.get_call_context()
731 731 h = _render.get_helpers()
732 732
733 733 def quick_menu(repo_group_name):
734 734 return _render('quick_repo_group_menu', repo_group_name)
735 735
736 736 def repo_group_lnk(repo_group_name):
737 737 return _render('repo_group_name', repo_group_name)
738 738
739 739 def last_change(last_change):
740 740 if admin and isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
741 741 ts = time.time()
742 742 utc_offset = (datetime.datetime.fromtimestamp(ts)
743 743 - datetime.datetime.utcfromtimestamp(ts)).total_seconds()
744 744 last_change = last_change + datetime.timedelta(seconds=utc_offset)
745 745 return _render("last_change", last_change)
746 746
747 747 def desc(desc, personal):
748 748 return _render(
749 749 'repo_group_desc', desc, personal, c.visual.stylify_metatags)
750 750
751 751 def repo_group_actions(repo_group_id, repo_group_name, gr_count):
752 752 return _render(
753 753 'repo_group_actions', repo_group_id, repo_group_name, gr_count)
754 754
755 755 def repo_group_name(repo_group_name, children_groups):
756 756 return _render("repo_group_name", repo_group_name, children_groups)
757 757
758 758 def user_profile(username):
759 759 return _render('user_profile', username)
760 760
761 761 repo_group_data = []
762 762 for group in repo_group_list:
763 763 # NOTE(marcink): because we use only raw column we need to load it like that
764 764 changeset_cache = RepoGroup._load_changeset_cache(
765 765 '', group._changeset_cache)
766 766 last_commit_change = RepoGroup._load_commit_change(changeset_cache)
767 767 row = {
768 768 "menu": quick_menu(group.group_name),
769 769 "name": repo_group_lnk(group.group_name),
770 770 "name_raw": group.group_name,
771 771
772 772 "last_change": last_change(last_commit_change),
773 773
774 774 "last_changeset": "",
775 775 "last_changeset_raw": "",
776 776
777 777 "desc": desc(h.escape(group.group_description), group.personal),
778 778 "top_level_repos": 0,
779 779 "owner": user_profile(group.User.username)
780 780 }
781 781 if admin:
782 782 repo_count = group.repositories.count()
783 783 children_groups = map(
784 784 h.safe_unicode,
785 785 itertools.chain((g.name for g in group.parents),
786 786 (x.name for x in [group])))
787 787 row.update({
788 788 "action": repo_group_actions(
789 789 group.group_id, group.group_name, repo_count),
790 790 "top_level_repos": repo_count,
791 791 "name": repo_group_name(group.group_name, children_groups),
792 792
793 793 })
794 794 repo_group_data.append(row)
795 795
796 796 return repo_group_data
797 797
798 798 def get_repo_groups_data_table(
799 799 self, draw, start, limit,
800 800 search_q, order_by, order_dir,
801 801 auth_user, repo_group_id):
802 802 from rhodecode.model.scm import RepoGroupList
803 803
804 804 _perms = ['group.read', 'group.write', 'group.admin']
805 805 repo_groups = RepoGroup.query() \
806 806 .filter(RepoGroup.group_parent_id == repo_group_id) \
807 807 .all()
808 808 auth_repo_group_list = RepoGroupList(
809 809 repo_groups, perm_set=_perms,
810 810 extra_kwargs=dict(user=auth_user))
811 811
812 812 allowed_ids = [-1]
813 813 for repo_group in auth_repo_group_list:
814 814 allowed_ids.append(repo_group.group_id)
815 815
816 816 repo_groups_data_total_count = RepoGroup.query() \
817 817 .filter(RepoGroup.group_parent_id == repo_group_id) \
818 818 .filter(or_(
819 819 # generate multiple IN to fix limitation problems
820 820 *in_filter_generator(RepoGroup.group_id, allowed_ids))
821 821 ) \
822 822 .count()
823 823
824 824 base_q = Session.query(
825 825 RepoGroup.group_name,
826 826 RepoGroup.group_name_hash,
827 827 RepoGroup.group_description,
828 828 RepoGroup.group_id,
829 829 RepoGroup.personal,
830 830 RepoGroup.updated_on,
831 831 RepoGroup._changeset_cache,
832 832 User,
833 833 ) \
834 834 .filter(RepoGroup.group_parent_id == repo_group_id) \
835 835 .filter(or_(
836 836 # generate multiple IN to fix limitation problems
837 837 *in_filter_generator(RepoGroup.group_id, allowed_ids))
838 838 ) \
839 839 .join(User, User.user_id == RepoGroup.user_id) \
840 840 .group_by(RepoGroup, User)
841 841
842 842 repo_groups_data_total_filtered_count = base_q.count()
843 843
844 844 sort_defined = False
845 845
846 846 if order_by == 'group_name':
847 847 sort_col = func.lower(RepoGroup.group_name)
848 848 sort_defined = True
849 849 elif order_by == 'user_username':
850 850 sort_col = User.username
851 851 else:
852 852 sort_col = getattr(RepoGroup, order_by, None)
853 853
854 854 if sort_defined or sort_col:
855 855 if order_dir == 'asc':
856 856 sort_col = sort_col.asc()
857 857 else:
858 858 sort_col = sort_col.desc()
859 859
860 860 base_q = base_q.order_by(sort_col)
861 861 base_q = base_q.offset(start).limit(limit)
862 862
863 863 repo_group_list = base_q.all()
864 864
865 865 repo_groups_data = RepoGroupModel().get_repo_groups_as_dict(
866 866 repo_group_list=repo_group_list, admin=False)
867 867
868 868 data = ({
869 869 'draw': draw,
870 870 'data': repo_groups_data,
871 871 'recordsTotal': repo_groups_data_total_count,
872 872 'recordsFiltered': repo_groups_data_total_filtered_count,
873 873 })
874 874 return data
875 875
876 876 def _get_defaults(self, repo_group_name):
877 877 repo_group = RepoGroup.get_by_group_name(repo_group_name)
878 878
879 879 if repo_group is None:
880 880 return None
881 881
882 882 defaults = repo_group.get_dict()
883 883 defaults['repo_group_name'] = repo_group.name
884 884 defaults['repo_group_description'] = repo_group.group_description
885 885 defaults['repo_group_enable_locking'] = repo_group.enable_locking
886 886
887 887 # we use -1 as this is how in HTML, we mark an empty group
888 888 defaults['repo_group'] = defaults['group_parent_id'] or -1
889 889
890 890 # fill owner
891 891 if repo_group.user:
892 892 defaults.update({'user': repo_group.user.username})
893 893 else:
894 894 replacement_user = User.get_first_super_admin().username
895 895 defaults.update({'user': replacement_user})
896 896
897 897 return defaults
@@ -1,1047 +1,1047 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 users model for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27 import datetime
28 28 import ipaddress
29 29
30 30 from pyramid.threadlocal import get_current_request
31 31 from sqlalchemy.exc import DatabaseError
32 32
33 33 from rhodecode import events
34 34 from rhodecode.lib.user_log_filter import user_log_filter
35 35 from rhodecode.lib.utils2 import (
36 36 safe_unicode, get_current_rhodecode_user, action_logger_generic,
37 37 AttributeDict, str2bool)
38 38 from rhodecode.lib.exceptions import (
39 39 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
40 40 UserOwnsUserGroupsException, NotAllowedToCreateUserError,
41 41 UserOwnsPullRequestsException, UserOwnsArtifactsException)
42 42 from rhodecode.lib.caching_query import FromCache
43 43 from rhodecode.model import BaseModel
44 44 from rhodecode.model.db import (
45 45 _hash_key, func, true, false, or_, joinedload, User, UserToPerm,
46 46 UserEmailMap, UserIpMap, UserLog)
47 47 from rhodecode.model.meta import Session
48 48 from rhodecode.model.auth_token import AuthTokenModel
49 49 from rhodecode.model.repo_group import RepoGroupModel
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53
54 54 class UserModel(BaseModel):
55 55 cls = User
56 56
57 57 def get(self, user_id, cache=False):
58 58 user = self.sa.query(User)
59 59 if cache:
60 60 user = user.options(
61 FromCache("sql_cache_short", "get_user_%s" % user_id))
61 FromCache("sql_cache_short", f"get_user_{user_id}"))
62 62 return user.get(user_id)
63 63
64 64 def get_user(self, user):
65 65 return self._get_user(user)
66 66
67 67 def _serialize_user(self, user):
68 68 import rhodecode.lib.helpers as h
69 69
70 70 return {
71 71 'id': user.user_id,
72 72 'first_name': user.first_name,
73 73 'last_name': user.last_name,
74 74 'username': user.username,
75 75 'email': user.email,
76 76 'icon_link': h.gravatar_url(user.email, 30),
77 77 'profile_link': h.link_to_user(user),
78 78 'value_display': h.escape(h.person(user)),
79 79 'value': user.username,
80 80 'value_type': 'user',
81 81 'active': user.active,
82 82 }
83 83
84 84 def get_users(self, name_contains=None, limit=20, only_active=True):
85 85
86 86 query = self.sa.query(User)
87 87 if only_active:
88 88 query = query.filter(User.active == true())
89 89
90 90 if name_contains:
91 91 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
92 92 query = query.filter(
93 93 or_(
94 94 User.name.ilike(ilike_expression),
95 95 User.lastname.ilike(ilike_expression),
96 96 User.username.ilike(ilike_expression)
97 97 )
98 98 )
99 99 # sort by len to have top most matches first
100 100 query = query.order_by(func.length(User.username))\
101 101 .order_by(User.username)
102 102 query = query.limit(limit)
103 103
104 104 users = query.all()
105 105
106 106 _users = [
107 107 self._serialize_user(user) for user in users
108 108 ]
109 109 return _users
110 110
111 111 def get_by_username(self, username, cache=False, case_insensitive=False):
112 112
113 113 if case_insensitive:
114 114 user = self.sa.query(User).filter(User.username.ilike(username))
115 115 else:
116 116 user = self.sa.query(User)\
117 117 .filter(User.username == username)
118 118 if cache:
119 119 name_key = _hash_key(username)
120 120 user = user.options(
121 FromCache("sql_cache_short", "get_user_%s" % name_key))
121 FromCache("sql_cache_short", f"get_user_{name_key}"))
122 122 return user.scalar()
123 123
124 124 def get_by_email(self, email, cache=False, case_insensitive=False):
125 125 return User.get_by_email(email, case_insensitive, cache)
126 126
127 127 def get_by_auth_token(self, auth_token, cache=False):
128 128 return User.get_by_auth_token(auth_token, cache)
129 129
130 130 def get_active_user_count(self, cache=False):
131 131 qry = User.query().filter(
132 132 User.active == true()).filter(
133 133 User.username != User.DEFAULT_USER)
134 134 if cache:
135 135 qry = qry.options(
136 136 FromCache("sql_cache_short", "get_active_users"))
137 137 return qry.count()
138 138
139 139 def create(self, form_data, cur_user=None):
140 140 if not cur_user:
141 141 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
142 142
143 143 user_data = {
144 144 'username': form_data['username'],
145 145 'password': form_data['password'],
146 146 'email': form_data['email'],
147 147 'firstname': form_data['firstname'],
148 148 'lastname': form_data['lastname'],
149 149 'active': form_data['active'],
150 150 'extern_type': form_data['extern_type'],
151 151 'extern_name': form_data['extern_name'],
152 152 'admin': False,
153 153 'cur_user': cur_user
154 154 }
155 155
156 156 if 'create_repo_group' in form_data:
157 157 user_data['create_repo_group'] = str2bool(
158 158 form_data.get('create_repo_group'))
159 159
160 160 try:
161 161 if form_data.get('password_change'):
162 162 user_data['force_password_change'] = True
163 163 return UserModel().create_or_update(**user_data)
164 164 except Exception:
165 165 log.error(traceback.format_exc())
166 166 raise
167 167
168 168 def update_user(self, user, skip_attrs=None, **kwargs):
169 169 from rhodecode.lib.auth import get_crypt_password
170 170
171 171 user = self._get_user(user)
172 172 if user.username == User.DEFAULT_USER:
173 173 raise DefaultUserException(
174 174 "You can't edit this user (`%(username)s`) since it's "
175 175 "crucial for entire application" % {
176 176 'username': user.username})
177 177
178 178 # first store only defaults
179 179 user_attrs = {
180 180 'updating_user_id': user.user_id,
181 181 'username': user.username,
182 182 'password': user.password,
183 183 'email': user.email,
184 184 'firstname': user.name,
185 185 'lastname': user.lastname,
186 186 'description': user.description,
187 187 'active': user.active,
188 188 'admin': user.admin,
189 189 'extern_name': user.extern_name,
190 190 'extern_type': user.extern_type,
191 191 'language': user.user_data.get('language')
192 192 }
193 193
194 194 # in case there's new_password, that comes from form, use it to
195 195 # store password
196 196 if kwargs.get('new_password'):
197 197 kwargs['password'] = kwargs['new_password']
198 198
199 199 # cleanups, my_account password change form
200 200 kwargs.pop('current_password', None)
201 201 kwargs.pop('new_password', None)
202 202
203 203 # cleanups, user edit password change form
204 204 kwargs.pop('password_confirmation', None)
205 205 kwargs.pop('password_change', None)
206 206
207 207 # create repo group on user creation
208 208 kwargs.pop('create_repo_group', None)
209 209
210 210 # legacy forms send name, which is the firstname
211 211 firstname = kwargs.pop('name', None)
212 212 if firstname:
213 213 kwargs['firstname'] = firstname
214 214
215 215 for k, v in kwargs.items():
216 216 # skip if we don't want to update this
217 217 if skip_attrs and k in skip_attrs:
218 218 continue
219 219
220 220 user_attrs[k] = v
221 221
222 222 try:
223 223 return self.create_or_update(**user_attrs)
224 224 except Exception:
225 225 log.error(traceback.format_exc())
226 226 raise
227 227
228 228 def create_or_update(
229 229 self, username, password, email, firstname='', lastname='',
230 230 active=True, admin=False, extern_type=None, extern_name=None,
231 231 cur_user=None, plugin=None, force_password_change=False,
232 232 allow_to_create_user=True, create_repo_group=None,
233 233 updating_user_id=None, language=None, description='',
234 234 strict_creation_check=True):
235 235 """
236 236 Creates a new instance if not found, or updates current one
237 237
238 238 :param username:
239 239 :param password:
240 240 :param email:
241 241 :param firstname:
242 242 :param lastname:
243 243 :param active:
244 244 :param admin:
245 245 :param extern_type:
246 246 :param extern_name:
247 247 :param cur_user:
248 248 :param plugin: optional plugin this method was called from
249 249 :param force_password_change: toggles new or existing user flag
250 250 for password change
251 251 :param allow_to_create_user: Defines if the method can actually create
252 252 new users
253 253 :param create_repo_group: Defines if the method should also
254 254 create an repo group with user name, and owner
255 255 :param updating_user_id: if we set it up this is the user we want to
256 256 update this allows to editing username.
257 257 :param language: language of user from interface.
258 258 :param description: user description
259 259 :param strict_creation_check: checks for allowed creation license wise etc.
260 260
261 261 :returns: new User object with injected `is_new_user` attribute.
262 262 """
263 263
264 264 if not cur_user:
265 265 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
266 266
267 267 from rhodecode.lib.auth import (
268 268 get_crypt_password, check_password)
269 269 from rhodecode.lib import hooks_base
270 270
271 271 def _password_change(new_user, password):
272 272 old_password = new_user.password or ''
273 273 # empty password
274 274 if not old_password:
275 275 return False
276 276
277 277 # password check is only needed for RhodeCode internal auth calls
278 278 # in case it's a plugin we don't care
279 279 if not plugin:
280 280
281 281 # first check if we gave crypted password back, and if it
282 282 # matches it's not password change
283 283 if new_user.password == password:
284 284 return False
285 285
286 286 password_match = check_password(password, old_password)
287 287 if not password_match:
288 288 return True
289 289
290 290 return False
291 291
292 292 # read settings on default personal repo group creation
293 293 if create_repo_group is None:
294 294 default_create_repo_group = RepoGroupModel()\
295 295 .get_default_create_personal_repo_group()
296 296 create_repo_group = default_create_repo_group
297 297
298 298 user_data = {
299 299 'username': username,
300 300 'password': password,
301 301 'email': email,
302 302 'firstname': firstname,
303 303 'lastname': lastname,
304 304 'active': active,
305 305 'admin': admin
306 306 }
307 307
308 308 if updating_user_id:
309 309 log.debug('Checking for existing account in RhodeCode '
310 310 'database with user_id `%s` ', updating_user_id)
311 311 user = User.get(updating_user_id)
312 312 else:
313 313 log.debug('Checking for existing account in RhodeCode '
314 314 'database with username `%s` ', username)
315 315 user = User.get_by_username(username, case_insensitive=True)
316 316
317 317 if user is None:
318 318 # we check internal flag if this method is actually allowed to
319 319 # create new user
320 320 if not allow_to_create_user:
321 321 msg = ('Method wants to create new user, but it is not '
322 322 'allowed to do so')
323 323 log.warning(msg)
324 324 raise NotAllowedToCreateUserError(msg)
325 325
326 326 log.debug('Creating new user %s', username)
327 327
328 328 # only if we create user that is active
329 329 new_active_user = active
330 330 if new_active_user and strict_creation_check:
331 331 # raises UserCreationError if it's not allowed for any reason to
332 332 # create new active user, this also executes pre-create hooks
333 333 hooks_base.check_allowed_create_user(user_data, cur_user, strict_check=True)
334 334 events.trigger(events.UserPreCreate(user_data))
335 335 new_user = User()
336 336 edit = False
337 337 else:
338 338 log.debug('updating user `%s`', username)
339 339 events.trigger(events.UserPreUpdate(user, user_data))
340 340 new_user = user
341 341 edit = True
342 342
343 343 # we're not allowed to edit default user
344 344 if user.username == User.DEFAULT_USER:
345 345 raise DefaultUserException(
346 346 "You can't edit this user (`%(username)s`) since it's "
347 347 "crucial for entire application"
348 348 % {'username': user.username})
349 349
350 350 # inject special attribute that will tell us if User is new or old
351 351 new_user.is_new_user = not edit
352 352 # for users that didn's specify auth type, we use RhodeCode built in
353 353 from rhodecode.authentication.plugins import auth_rhodecode
354 354 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.uid
355 355 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.uid
356 356
357 357 try:
358 358 new_user.username = username
359 359 new_user.admin = admin
360 360 new_user.email = email
361 361 new_user.active = active
362 362 new_user.extern_name = safe_unicode(extern_name)
363 363 new_user.extern_type = safe_unicode(extern_type)
364 364 new_user.name = firstname
365 365 new_user.lastname = lastname
366 366 new_user.description = description
367 367
368 368 # set password only if creating an user or password is changed
369 369 if not edit or _password_change(new_user, password):
370 370 reason = 'new password' if edit else 'new user'
371 371 log.debug('Updating password reason=>%s', reason)
372 372 new_user.password = get_crypt_password(password) if password else None
373 373
374 374 if force_password_change:
375 375 new_user.update_userdata(force_password_change=True)
376 376 if language:
377 377 new_user.update_userdata(language=language)
378 378 new_user.update_userdata(notification_status=True)
379 379
380 380 self.sa.add(new_user)
381 381
382 382 if not edit and create_repo_group:
383 383 RepoGroupModel().create_personal_repo_group(
384 384 new_user, commit_early=False)
385 385
386 386 if not edit:
387 387 # add the RSS token
388 388 self.add_auth_token(
389 389 user=username, lifetime_minutes=-1,
390 390 role=self.auth_token_role.ROLE_FEED,
391 391 description=u'Generated feed token')
392 392
393 393 kwargs = new_user.get_dict()
394 394 # backward compat, require api_keys present
395 395 kwargs['api_keys'] = kwargs['auth_tokens']
396 396 hooks_base.create_user(created_by=cur_user, **kwargs)
397 397 events.trigger(events.UserPostCreate(user_data))
398 398 return new_user
399 399 except (DatabaseError,):
400 400 log.error(traceback.format_exc())
401 401 raise
402 402
403 403 def create_registration(self, form_data,
404 404 extern_name='rhodecode', extern_type='rhodecode'):
405 405 from rhodecode.model.notification import NotificationModel
406 406 from rhodecode.model.notification import EmailNotificationModel
407 407
408 408 try:
409 409 form_data['admin'] = False
410 410 form_data['extern_name'] = extern_name
411 411 form_data['extern_type'] = extern_type
412 412 new_user = self.create(form_data)
413 413
414 414 self.sa.add(new_user)
415 415 self.sa.flush()
416 416
417 417 user_data = new_user.get_dict()
418 418 user_data.update({
419 419 'first_name': user_data.get('firstname'),
420 420 'last_name': user_data.get('lastname'),
421 421 })
422 422 kwargs = {
423 423 # use SQLALCHEMY safe dump of user data
424 424 'user': AttributeDict(user_data),
425 425 'date': datetime.datetime.now()
426 426 }
427 427 notification_type = EmailNotificationModel.TYPE_REGISTRATION
428 428
429 429 # create notification objects, and emails
430 430 NotificationModel().create(
431 431 created_by=new_user,
432 432 notification_subject='', # Filled in based on the notification_type
433 433 notification_body='', # Filled in based on the notification_type
434 434 notification_type=notification_type,
435 435 recipients=None, # all admins
436 436 email_kwargs=kwargs,
437 437 )
438 438
439 439 return new_user
440 440 except Exception:
441 441 log.error(traceback.format_exc())
442 442 raise
443 443
444 444 def _handle_user_repos(self, username, repositories, handle_user,
445 445 handle_mode=None):
446 446
447 447 left_overs = True
448 448
449 449 from rhodecode.model.repo import RepoModel
450 450
451 451 if handle_mode == 'detach':
452 452 for obj in repositories:
453 453 obj.user = handle_user
454 454 # set description we know why we super admin now owns
455 455 # additional repositories that were orphaned !
456 456 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
457 457 self.sa.add(obj)
458 458 left_overs = False
459 459 elif handle_mode == 'delete':
460 460 for obj in repositories:
461 461 RepoModel().delete(obj, forks='detach')
462 462 left_overs = False
463 463
464 464 # if nothing is done we have left overs left
465 465 return left_overs
466 466
467 467 def _handle_user_repo_groups(self, username, repository_groups, handle_user,
468 468 handle_mode=None):
469 469
470 470 left_overs = True
471 471
472 472 from rhodecode.model.repo_group import RepoGroupModel
473 473
474 474 if handle_mode == 'detach':
475 475 for r in repository_groups:
476 476 r.user = handle_user
477 477 # set description we know why we super admin now owns
478 478 # additional repositories that were orphaned !
479 479 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
480 480 r.personal = False
481 481 self.sa.add(r)
482 482 left_overs = False
483 483 elif handle_mode == 'delete':
484 484 for r in repository_groups:
485 485 RepoGroupModel().delete(r)
486 486 left_overs = False
487 487
488 488 # if nothing is done we have left overs left
489 489 return left_overs
490 490
491 491 def _handle_user_user_groups(self, username, user_groups, handle_user,
492 492 handle_mode=None):
493 493
494 494 left_overs = True
495 495
496 496 from rhodecode.model.user_group import UserGroupModel
497 497
498 498 if handle_mode == 'detach':
499 499 for r in user_groups:
500 500 for user_user_group_to_perm in r.user_user_group_to_perm:
501 501 if user_user_group_to_perm.user.username == username:
502 502 user_user_group_to_perm.user = handle_user
503 503 r.user = handle_user
504 504 # set description we know why we super admin now owns
505 505 # additional repositories that were orphaned !
506 506 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
507 507 self.sa.add(r)
508 508 left_overs = False
509 509 elif handle_mode == 'delete':
510 510 for r in user_groups:
511 511 UserGroupModel().delete(r)
512 512 left_overs = False
513 513
514 514 # if nothing is done we have left overs left
515 515 return left_overs
516 516
517 517 def _handle_user_pull_requests(self, username, pull_requests, handle_user,
518 518 handle_mode=None):
519 519 left_overs = True
520 520
521 521 from rhodecode.model.pull_request import PullRequestModel
522 522
523 523 if handle_mode == 'detach':
524 524 for pr in pull_requests:
525 525 pr.user_id = handle_user.user_id
526 526 # set description we know why we super admin now owns
527 527 # additional repositories that were orphaned !
528 528 pr.description += ' \n::detached pull requests from deleted user: %s' % (username,)
529 529 self.sa.add(pr)
530 530 left_overs = False
531 531 elif handle_mode == 'delete':
532 532 for pr in pull_requests:
533 533 PullRequestModel().delete(pr)
534 534
535 535 left_overs = False
536 536
537 537 # if nothing is done we have left overs left
538 538 return left_overs
539 539
540 540 def _handle_user_artifacts(self, username, artifacts, handle_user,
541 541 handle_mode=None):
542 542
543 543 left_overs = True
544 544
545 545 if handle_mode == 'detach':
546 546 for a in artifacts:
547 547 a.upload_user = handle_user
548 548 # set description we know why we super admin now owns
549 549 # additional artifacts that were orphaned !
550 550 a.file_description += ' \n::detached artifact from deleted user: %s' % (username,)
551 551 self.sa.add(a)
552 552 left_overs = False
553 553 elif handle_mode == 'delete':
554 554 from rhodecode.apps.file_store import utils as store_utils
555 555 request = get_current_request()
556 556 storage = store_utils.get_file_storage(request.registry.settings)
557 557 for a in artifacts:
558 558 file_uid = a.file_uid
559 559 storage.delete(file_uid)
560 560 self.sa.delete(a)
561 561
562 562 left_overs = False
563 563
564 564 # if nothing is done we have left overs left
565 565 return left_overs
566 566
567 567 def delete(self, user, cur_user=None, handle_repos=None,
568 568 handle_repo_groups=None, handle_user_groups=None,
569 569 handle_pull_requests=None, handle_artifacts=None, handle_new_owner=None):
570 570 from rhodecode.lib import hooks_base
571 571
572 572 if not cur_user:
573 573 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
574 574
575 575 user = self._get_user(user)
576 576
577 577 try:
578 578 if user.username == User.DEFAULT_USER:
579 579 raise DefaultUserException(
580 580 u"You can't remove this user since it's"
581 581 u" crucial for entire application")
582 582 handle_user = handle_new_owner or self.cls.get_first_super_admin()
583 583 log.debug('New detached objects owner %s', handle_user)
584 584
585 585 left_overs = self._handle_user_repos(
586 586 user.username, user.repositories, handle_user, handle_repos)
587 587 if left_overs and user.repositories:
588 588 repos = [x.repo_name for x in user.repositories]
589 589 raise UserOwnsReposException(
590 590 u'user "%(username)s" still owns %(len_repos)s repositories and cannot be '
591 591 u'removed. Switch owners or remove those repositories:%(list_repos)s'
592 592 % {'username': user.username, 'len_repos': len(repos),
593 593 'list_repos': ', '.join(repos)})
594 594
595 595 left_overs = self._handle_user_repo_groups(
596 596 user.username, user.repository_groups, handle_user, handle_repo_groups)
597 597 if left_overs and user.repository_groups:
598 598 repo_groups = [x.group_name for x in user.repository_groups]
599 599 raise UserOwnsRepoGroupsException(
600 600 u'user "%(username)s" still owns %(len_repo_groups)s repository groups and cannot be '
601 601 u'removed. Switch owners or remove those repository groups:%(list_repo_groups)s'
602 602 % {'username': user.username, 'len_repo_groups': len(repo_groups),
603 603 'list_repo_groups': ', '.join(repo_groups)})
604 604
605 605 left_overs = self._handle_user_user_groups(
606 606 user.username, user.user_groups, handle_user, handle_user_groups)
607 607 if left_overs and user.user_groups:
608 608 user_groups = [x.users_group_name for x in user.user_groups]
609 609 raise UserOwnsUserGroupsException(
610 610 u'user "%s" still owns %s user groups and cannot be '
611 611 u'removed. Switch owners or remove those user groups:%s'
612 612 % (user.username, len(user_groups), ', '.join(user_groups)))
613 613
614 614 left_overs = self._handle_user_pull_requests(
615 615 user.username, user.user_pull_requests, handle_user, handle_pull_requests)
616 616 if left_overs and user.user_pull_requests:
617 617 pull_requests = ['!{}'.format(x.pull_request_id) for x in user.user_pull_requests]
618 618 raise UserOwnsPullRequestsException(
619 619 u'user "%s" still owns %s pull requests and cannot be '
620 620 u'removed. Switch owners or remove those pull requests:%s'
621 621 % (user.username, len(pull_requests), ', '.join(pull_requests)))
622 622
623 623 left_overs = self._handle_user_artifacts(
624 624 user.username, user.artifacts, handle_user, handle_artifacts)
625 625 if left_overs and user.artifacts:
626 626 artifacts = [x.file_uid for x in user.artifacts]
627 627 raise UserOwnsArtifactsException(
628 628 u'user "%s" still owns %s artifacts and cannot be '
629 629 u'removed. Switch owners or remove those artifacts:%s'
630 630 % (user.username, len(artifacts), ', '.join(artifacts)))
631 631
632 632 user_data = user.get_dict() # fetch user data before expire
633 633
634 634 # we might change the user data with detach/delete, make sure
635 635 # the object is marked as expired before actually deleting !
636 636 self.sa.expire(user)
637 637 self.sa.delete(user)
638 638
639 639 hooks_base.delete_user(deleted_by=cur_user, **user_data)
640 640 except Exception:
641 641 log.error(traceback.format_exc())
642 642 raise
643 643
644 644 def reset_password_link(self, data, pwd_reset_url):
645 645 from rhodecode.lib.celerylib import tasks, run_task
646 646 from rhodecode.model.notification import EmailNotificationModel
647 647 user_email = data['email']
648 648 try:
649 649 user = User.get_by_email(user_email)
650 650 if user:
651 651 log.debug('password reset user found %s', user)
652 652
653 653 email_kwargs = {
654 654 'password_reset_url': pwd_reset_url,
655 655 'user': user,
656 656 'email': user_email,
657 657 'date': datetime.datetime.now(),
658 658 'first_admin_email': User.get_first_super_admin().email
659 659 }
660 660
661 661 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
662 662 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
663 663
664 664 recipients = [user_email]
665 665
666 666 action_logger_generic(
667 667 'sending password reset email to user: {}'.format(
668 668 user), namespace='security.password_reset')
669 669
670 670 run_task(tasks.send_email, recipients, subject,
671 671 email_body_plaintext, email_body)
672 672
673 673 else:
674 674 log.debug("password reset email %s not found", user_email)
675 675 except Exception:
676 676 log.error(traceback.format_exc())
677 677 return False
678 678
679 679 return True
680 680
681 681 def reset_password(self, data):
682 682 from rhodecode.lib.celerylib import tasks, run_task
683 683 from rhodecode.model.notification import EmailNotificationModel
684 684 from rhodecode.lib import auth
685 685 user_email = data['email']
686 686 pre_db = True
687 687 try:
688 688 user = User.get_by_email(user_email)
689 689 new_passwd = auth.PasswordGenerator().gen_password(
690 690 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
691 691 if user:
692 692 user.password = auth.get_crypt_password(new_passwd)
693 693 # also force this user to reset his password !
694 694 user.update_userdata(force_password_change=True)
695 695
696 696 Session().add(user)
697 697
698 698 # now delete the token in question
699 699 UserApiKeys = AuthTokenModel.cls
700 700 UserApiKeys().query().filter(
701 701 UserApiKeys.api_key == data['token']).delete()
702 702
703 703 Session().commit()
704 704 log.info('successfully reset password for `%s`', user_email)
705 705
706 706 if new_passwd is None:
707 707 raise Exception('unable to generate new password')
708 708
709 709 pre_db = False
710 710
711 711 email_kwargs = {
712 712 'new_password': new_passwd,
713 713 'user': user,
714 714 'email': user_email,
715 715 'date': datetime.datetime.now(),
716 716 'first_admin_email': User.get_first_super_admin().email
717 717 }
718 718
719 719 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
720 720 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
721 721 **email_kwargs)
722 722
723 723 recipients = [user_email]
724 724
725 725 action_logger_generic(
726 726 'sent new password to user: {} with email: {}'.format(
727 727 user, user_email), namespace='security.password_reset')
728 728
729 729 run_task(tasks.send_email, recipients, subject,
730 730 email_body_plaintext, email_body)
731 731
732 732 except Exception:
733 733 log.error('Failed to update user password')
734 734 log.error(traceback.format_exc())
735 735 if pre_db:
736 736 # we rollback only if local db stuff fails. If it goes into
737 737 # run_task, we're pass rollback state this wouldn't work then
738 738 Session().rollback()
739 739
740 740 return True
741 741
742 742 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
743 743 """
744 744 Fetches auth_user by user_id,or api_key if present.
745 745 Fills auth_user attributes with those taken from database.
746 746 Additionally set's is_authenitated if lookup fails
747 747 present in database
748 748
749 749 :param auth_user: instance of user to set attributes
750 750 :param user_id: user id to fetch by
751 751 :param api_key: api key to fetch by
752 752 :param username: username to fetch by
753 753 """
754 754 def token_obfuscate(token):
755 755 if token:
756 756 return token[:4] + "****"
757 757
758 758 if user_id is None and api_key is None and username is None:
759 759 raise Exception('You need to pass user_id, api_key or username')
760 760
761 761 log.debug(
762 762 'AuthUser: fill data execution based on: '
763 763 'user_id:%s api_key:%s username:%s', user_id, api_key, username)
764 764 try:
765 765 dbuser = None
766 766 if user_id:
767 767 dbuser = self.get(user_id)
768 768 elif api_key:
769 769 dbuser = self.get_by_auth_token(api_key)
770 770 elif username:
771 771 dbuser = self.get_by_username(username)
772 772
773 773 if not dbuser:
774 774 log.warning(
775 775 'Unable to lookup user by id:%s api_key:%s username:%s',
776 776 user_id, token_obfuscate(api_key), username)
777 777 return False
778 778 if not dbuser.active:
779 779 log.debug('User `%s:%s` is inactive, skipping fill data',
780 780 username, user_id)
781 781 return False
782 782
783 783 log.debug('AuthUser: filling found user:%s data', dbuser)
784 784
785 785 attrs = {
786 786 'user_id': dbuser.user_id,
787 787 'username': dbuser.username,
788 788 'name': dbuser.name,
789 789 'first_name': dbuser.first_name,
790 790 'firstname': dbuser.firstname,
791 791 'last_name': dbuser.last_name,
792 792 'lastname': dbuser.lastname,
793 793 'admin': dbuser.admin,
794 794 'active': dbuser.active,
795 795
796 796 'email': dbuser.email,
797 797 'emails': dbuser.emails_cached(),
798 798 'short_contact': dbuser.short_contact,
799 799 'full_contact': dbuser.full_contact,
800 800 'full_name': dbuser.full_name,
801 801 'full_name_or_username': dbuser.full_name_or_username,
802 802
803 803 '_api_key': dbuser._api_key,
804 804 '_user_data': dbuser._user_data,
805 805
806 806 'created_on': dbuser.created_on,
807 807 'extern_name': dbuser.extern_name,
808 808 'extern_type': dbuser.extern_type,
809 809
810 810 'inherit_default_permissions': dbuser.inherit_default_permissions,
811 811
812 812 'language': dbuser.language,
813 813 'last_activity': dbuser.last_activity,
814 814 'last_login': dbuser.last_login,
815 815 'password': dbuser.password,
816 816 }
817 817 auth_user.__dict__.update(attrs)
818 818 except Exception:
819 819 log.error(traceback.format_exc())
820 820 auth_user.is_authenticated = False
821 821 return False
822 822
823 823 return True
824 824
825 825 def has_perm(self, user, perm):
826 826 perm = self._get_perm(perm)
827 827 user = self._get_user(user)
828 828
829 829 return UserToPerm.query().filter(UserToPerm.user == user)\
830 830 .filter(UserToPerm.permission == perm).scalar() is not None
831 831
832 832 def grant_perm(self, user, perm):
833 833 """
834 834 Grant user global permissions
835 835
836 836 :param user:
837 837 :param perm:
838 838 """
839 839 user = self._get_user(user)
840 840 perm = self._get_perm(perm)
841 841 # if this permission is already granted skip it
842 842 _perm = UserToPerm.query()\
843 843 .filter(UserToPerm.user == user)\
844 844 .filter(UserToPerm.permission == perm)\
845 845 .scalar()
846 846 if _perm:
847 847 return
848 848 new = UserToPerm()
849 849 new.user = user
850 850 new.permission = perm
851 851 self.sa.add(new)
852 852 return new
853 853
854 854 def revoke_perm(self, user, perm):
855 855 """
856 856 Revoke users global permissions
857 857
858 858 :param user:
859 859 :param perm:
860 860 """
861 861 user = self._get_user(user)
862 862 perm = self._get_perm(perm)
863 863
864 864 obj = UserToPerm.query()\
865 865 .filter(UserToPerm.user == user)\
866 866 .filter(UserToPerm.permission == perm)\
867 867 .scalar()
868 868 if obj:
869 869 self.sa.delete(obj)
870 870
871 871 def add_extra_email(self, user, email):
872 872 """
873 873 Adds email address to UserEmailMap
874 874
875 875 :param user:
876 876 :param email:
877 877 """
878 878
879 879 user = self._get_user(user)
880 880
881 881 obj = UserEmailMap()
882 882 obj.user = user
883 883 obj.email = email
884 884 self.sa.add(obj)
885 885 return obj
886 886
887 887 def delete_extra_email(self, user, email_id):
888 888 """
889 889 Removes email address from UserEmailMap
890 890
891 891 :param user:
892 892 :param email_id:
893 893 """
894 894 user = self._get_user(user)
895 895 obj = UserEmailMap.query().get(email_id)
896 896 if obj and obj.user_id == user.user_id:
897 897 self.sa.delete(obj)
898 898
899 899 def parse_ip_range(self, ip_range):
900 900 ip_list = []
901 901
902 902 def make_unique(value):
903 903 seen = []
904 904 return [c for c in value if not (c in seen or seen.append(c))]
905 905
906 906 # firsts split by commas
907 907 for ip_range in ip_range.split(','):
908 908 if not ip_range:
909 909 continue
910 910 ip_range = ip_range.strip()
911 911 if '-' in ip_range:
912 912 start_ip, end_ip = ip_range.split('-', 1)
913 913 start_ip = ipaddress.ip_address(safe_unicode(start_ip.strip()))
914 914 end_ip = ipaddress.ip_address(safe_unicode(end_ip.strip()))
915 915 parsed_ip_range = []
916 916
917 917 for index in range(int(start_ip), int(end_ip) + 1):
918 918 new_ip = ipaddress.ip_address(index)
919 919 parsed_ip_range.append(str(new_ip))
920 920 ip_list.extend(parsed_ip_range)
921 921 else:
922 922 ip_list.append(ip_range)
923 923
924 924 return make_unique(ip_list)
925 925
926 926 def add_extra_ip(self, user, ip, description=None):
927 927 """
928 928 Adds ip address to UserIpMap
929 929
930 930 :param user:
931 931 :param ip:
932 932 """
933 933
934 934 user = self._get_user(user)
935 935 obj = UserIpMap()
936 936 obj.user = user
937 937 obj.ip_addr = ip
938 938 obj.description = description
939 939 self.sa.add(obj)
940 940 return obj
941 941
942 942 auth_token_role = AuthTokenModel.cls
943 943
944 944 def add_auth_token(self, user, lifetime_minutes, role, description=u'',
945 945 scope_callback=None):
946 946 """
947 947 Add AuthToken for user.
948 948
949 949 :param user: username/user_id
950 950 :param lifetime_minutes: in minutes the lifetime for token, -1 equals no limit
951 951 :param role: one of AuthTokenModel.cls.ROLE_*
952 952 :param description: optional string description
953 953 """
954 954
955 955 token = AuthTokenModel().create(
956 956 user, description, lifetime_minutes, role)
957 957 if scope_callback and callable(scope_callback):
958 958 # call the callback if we provide, used to attach scope for EE edition
959 959 scope_callback(token)
960 960 return token
961 961
962 962 def delete_extra_ip(self, user, ip_id):
963 963 """
964 964 Removes ip address from UserIpMap
965 965
966 966 :param user:
967 967 :param ip_id:
968 968 """
969 969 user = self._get_user(user)
970 970 obj = UserIpMap.query().get(ip_id)
971 971 if obj and obj.user_id == user.user_id:
972 972 self.sa.delete(obj)
973 973
974 974 def get_accounts_in_creation_order(self, current_user=None):
975 975 """
976 976 Get accounts in order of creation for deactivation for license limits
977 977
978 978 pick currently logged in user, and append to the list in position 0
979 979 pick all super-admins in order of creation date and add it to the list
980 980 pick all other accounts in order of creation and add it to the list.
981 981
982 982 Based on that list, the last accounts can be disabled as they are
983 983 created at the end and don't include any of the super admins as well
984 984 as the current user.
985 985
986 986 :param current_user: optionally current user running this operation
987 987 """
988 988
989 989 if not current_user:
990 990 current_user = get_current_rhodecode_user()
991 991 active_super_admins = [
992 992 x.user_id for x in User.query()
993 993 .filter(User.user_id != current_user.user_id)
994 994 .filter(User.active == true())
995 995 .filter(User.admin == true())
996 996 .order_by(User.created_on.asc())]
997 997
998 998 active_regular_users = [
999 999 x.user_id for x in User.query()
1000 1000 .filter(User.user_id != current_user.user_id)
1001 1001 .filter(User.active == true())
1002 1002 .filter(User.admin == false())
1003 1003 .order_by(User.created_on.asc())]
1004 1004
1005 1005 list_of_accounts = [current_user.user_id]
1006 1006 list_of_accounts += active_super_admins
1007 1007 list_of_accounts += active_regular_users
1008 1008
1009 1009 return list_of_accounts
1010 1010
1011 1011 def deactivate_last_users(self, expected_users, current_user=None):
1012 1012 """
1013 1013 Deactivate accounts that are over the license limits.
1014 1014 Algorithm of which accounts to disabled is based on the formula:
1015 1015
1016 1016 Get current user, then super admins in creation order, then regular
1017 1017 active users in creation order.
1018 1018
1019 1019 Using that list we mark all accounts from the end of it as inactive.
1020 1020 This way we block only latest created accounts.
1021 1021
1022 1022 :param expected_users: list of users in special order, we deactivate
1023 1023 the end N amount of users from that list
1024 1024 """
1025 1025
1026 1026 list_of_accounts = self.get_accounts_in_creation_order(
1027 1027 current_user=current_user)
1028 1028
1029 1029 for acc_id in list_of_accounts[expected_users + 1:]:
1030 1030 user = User.get(acc_id)
1031 1031 log.info('Deactivating account %s for license unlock', user)
1032 1032 user.active = False
1033 1033 Session().add(user)
1034 1034 Session().commit()
1035 1035
1036 1036 return
1037 1037
1038 1038 def get_user_log(self, user, filter_term):
1039 1039 user_log = UserLog.query()\
1040 1040 .filter(or_(UserLog.user_id == user.user_id,
1041 1041 UserLog.username == user.username))\
1042 1042 .options(joinedload(UserLog.user))\
1043 1043 .options(joinedload(UserLog.repository))\
1044 1044 .order_by(UserLog.action_date.desc())
1045 1045
1046 1046 user_log = user_log_filter(user_log, filter_term)
1047 1047 return user_log
General Comments 0
You need to be logged in to leave comments. Login now