##// END OF EJS Templates
pull-reqests: workflow, change who can close a PR, it's only super-admin...
pull-reqests: workflow, change who can close a PR, it's only super-admin owner or person who can merge. - before it was every reviewer can close. Which really doesn't make sense.

File last commit:

r1282:90601d74 default
r1686:f0d2d511 default
Show More
repo_group.py
711 lines | 27.9 KiB | text/x-python | PythonLexer
project: added all source files and assets
r1 # -*- coding: utf-8 -*-
license: updated copyright year to 2017
r1271 # Copyright (C) 2011-2017 RhodeCode GmbH
project: added all source files and assets
r1 #
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
"""
repo group model for RhodeCode
"""
repo-groups: implemented default personal repo groups logic....
r1094 import os
project: added all source files and assets
r1 import datetime
import itertools
import logging
import shutil
import traceback
repo-groups: implemented default personal repo groups logic....
r1094 import string
project: added all source files and assets
r1
from zope.cachedescriptors.property import Lazy as LazyProperty
Martin Bornhold
events: Trigger repository group events (create/update/delete)
r557 from rhodecode import events
project: added all source files and assets
r1 from rhodecode.model import BaseModel
from rhodecode.model.db import (
RepoGroup, UserRepoGroupToPerm, User, Permission, UserGroupRepoGroupToPerm,
UserGroup, Repository)
repo-groups: implemented default personal repo groups logic....
r1094 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
project: added all source files and assets
r1 from rhodecode.lib.caching_query import FromCache
from rhodecode.lib.utils2 import action_logger_generic
log = logging.getLogger(__name__)
class RepoGroupModel(BaseModel):
cls = RepoGroup
meta-tags: fixes #4305, metatags are not taken into account when truncating output....
r1102 PERSONAL_GROUP_DESC = 'personal repo group of user `%(username)s`'
repo-groups: implemented default personal repo groups logic....
r1094 PERSONAL_GROUP_PATTERN = '${username}' # default
project: added all source files and assets
r1
def _get_user_group(self, users_group):
return self._get_instance(UserGroup, users_group,
callback=UserGroup.get_by_group_name)
def _get_repo_group(self, repo_group):
return self._get_instance(RepoGroup, repo_group,
callback=RepoGroup.get_by_group_name)
@LazyProperty
def repos_path(self):
"""
Gets the repositories root path from database
"""
settings_model = VcsSettingsModel(sa=self.sa)
return settings_model.get_repos_location()
def get_by_group_name(self, repo_group_name, cache=None):
repo = self.sa.query(RepoGroup) \
.filter(RepoGroup.group_name == repo_group_name)
if cache:
repo = repo.options(FromCache(
"sql_cache_short", "get_repo_group_%s" % repo_group_name))
return repo.scalar()
repo-groups: implemented default personal repo groups logic....
r1094 def get_default_create_personal_repo_group(self):
value = SettingsModel().get_setting_by_name(
'create_personal_repo_group')
return value.app_settings_value if value else None or False
def get_personal_group_name_pattern(self):
value = SettingsModel().get_setting_by_name(
'personal_repo_group_pattern')
val = value.app_settings_value if value else None
group_template = val or self.PERSONAL_GROUP_PATTERN
group_template = group_template.lstrip('/')
return group_template
def get_personal_group_name(self, user):
template = self.get_personal_group_name_pattern()
return string.Template(template).safe_substitute(
username=user.username,
user_id=user.user_id,
)
def create_personal_repo_group(self, user, commit_early=True):
desc = self.PERSONAL_GROUP_DESC % {'username': user.username}
personal_repo_group_name = self.get_personal_group_name(user)
# create a new one
RepoGroupModel().create(
group_name=personal_repo_group_name,
group_description=desc,
owner=user.username,
personal=True,
commit_early=commit_early)
project: added all source files and assets
r1 def _create_default_perms(self, new_group):
# create default permission
default_perm = 'group.read'
def_user = User.get_default_user()
for p in def_user.user_perms:
if p.permission.permission_name.startswith('group.'):
default_perm = p.permission.permission_name
break
repo_group_to_perm = UserRepoGroupToPerm()
repo_group_to_perm.permission = Permission.get_by_key(default_perm)
repo_group_to_perm.group = new_group
repo_group_to_perm.user_id = def_user.user_id
return repo_group_to_perm
repo-group-model: add flag to return proper object along the exported names.
r1151 def _get_group_name_and_parent(self, group_name_full, repo_in_path=False,
get_object=False):
project: added all source files and assets
r1 """
Get's the group name and a parent group name from given group name.
If repo_in_path is set to truth, we asume the full path also includes
repo name, in such case we clean the last element.
:param group_name_full:
"""
split_paths = 1
if repo_in_path:
split_paths = 2
_parts = group_name_full.rsplit(RepoGroup.url_sep(), split_paths)
if repo_in_path and len(_parts) > 1:
# such case last element is the repo_name
_parts.pop(-1)
group_name_cleaned = _parts[-1] # just the group name
parent_repo_group_name = None
if len(_parts) > 1:
parent_repo_group_name = _parts[0]
repo-group-model: add flag to return proper object along the exported names.
r1151 parent_group = None
project: added all source files and assets
r1 if parent_repo_group_name:
parent_group = RepoGroup.get_by_group_name(parent_repo_group_name)
repo-group-model: add flag to return proper object along the exported names.
r1151 if get_object:
return group_name_cleaned, parent_repo_group_name, parent_group
project: added all source files and assets
r1 return group_name_cleaned, parent_repo_group_name
def check_exist_filesystem(self, group_name, exc_on_failure=True):
create_path = os.path.join(self.repos_path, group_name)
log.debug('creating new group in %s', create_path)
if os.path.isdir(create_path):
if exc_on_failure:
repo-group: add path to exception when directory under which repo group should be created already exists.
r1152 abs_create_path = os.path.abspath(create_path)
raise Exception('Directory `{}` already exists !'.format(abs_create_path))
project: added all source files and assets
r1 return False
return True
def _create_group(self, group_name):
"""
makes repository group on filesystem
:param repo_name:
:param parent_id:
"""
self.check_exist_filesystem(group_name)
create_path = os.path.join(self.repos_path, group_name)
log.debug('creating new group in %s', create_path)
os.makedirs(create_path, mode=0755)
log.debug('created group in %s', create_path)
def _rename_group(self, old, new):
"""
Renames a group on filesystem
:param group_name:
"""
if old == new:
log.debug('skipping group rename')
return
log.debug('renaming repository group from %s to %s', old, new)
old_path = os.path.join(self.repos_path, old)
new_path = os.path.join(self.repos_path, new)
log.debug('renaming repos paths from %s to %s', old_path, new_path)
if os.path.isdir(new_path):
raise Exception('Was trying to rename to already '
'existing dir %s' % new_path)
shutil.move(old_path, new_path)
def _delete_filesystem_group(self, group, force_delete=False):
"""
Deletes a group from a filesystem
:param group: instance of group from database
:param force_delete: use shutil rmtree to remove all objects
"""
paths = group.full_path.split(RepoGroup.url_sep())
paths = os.sep.join(paths)
rm_path = os.path.join(self.repos_path, paths)
log.info("Removing group %s", rm_path)
# delete only if that path really exists
if os.path.isdir(rm_path):
if force_delete:
shutil.rmtree(rm_path)
else:
# archive that group`
_now = datetime.datetime.now()
_ms = str(_now.microsecond).rjust(6, '0')
_d = 'rm__%s_GROUP_%s' % (
_now.strftime('%Y%m%d_%H%M%S_' + _ms), group.name)
shutil.move(rm_path, os.path.join(self.repos_path, _d))
def create(self, group_name, group_description, owner, just_db=False,
repo-groups: implemented default personal repo groups logic....
r1094 copy_permissions=False, personal=None, commit_early=True):
project: added all source files and assets
r1
(group_name_cleaned,
parent_group_name) = RepoGroupModel()._get_group_name_and_parent(group_name)
parent_group = None
if parent_group_name:
parent_group = self._get_repo_group(parent_group_name)
repo-groups: implemented default personal repo groups logic....
r1094 if not parent_group:
# we tried to create a nested group, but the parent is not
# existing
raise ValueError(
'Parent group `%s` given in `%s` group name '
'is not yet existing.' % (parent_group_name, group_name))
project: added all source files and assets
r1
repo-groups: implemented default personal repo groups logic....
r1094 # because we are doing a cleanup, we need to check if such directory
# already exists. If we don't do that we can accidentally delete
# existing directory via cleanup that can cause data issues, since
# delete does a folder rename to special syntax later cleanup
# functions can delete this
project: added all source files and assets
r1 cleanup_group = self.check_exist_filesystem(group_name,
exc_on_failure=False)
try:
user = self._get_user(owner)
new_repo_group = RepoGroup()
new_repo_group.user = user
new_repo_group.group_description = group_description or group_name
new_repo_group.parent_group = parent_group
new_repo_group.group_name = group_name
repo-groups: implemented default personal repo groups logic....
r1094 new_repo_group.personal = personal
project: added all source files and assets
r1
self.sa.add(new_repo_group)
# create an ADMIN permission for owner except if we're super admin,
# later owner should go into the owner field of groups
if not user.is_admin:
self.grant_user_permission(repo_group=new_repo_group,
user=owner, perm='group.admin')
if parent_group and copy_permissions:
# copy permissions from parent
user_perms = UserRepoGroupToPerm.query() \
.filter(UserRepoGroupToPerm.group == parent_group).all()
group_perms = UserGroupRepoGroupToPerm.query() \
.filter(UserGroupRepoGroupToPerm.group == parent_group).all()
for perm in user_perms:
# don't copy over the permission for user who is creating
# this group, if he is not super admin he get's admin
# permission set above
if perm.user != user or user.is_admin:
UserRepoGroupToPerm.create(
perm.user, new_repo_group, perm.permission)
for perm in group_perms:
UserGroupRepoGroupToPerm.create(
perm.users_group, new_repo_group, perm.permission)
else:
perm_obj = self._create_default_perms(new_repo_group)
self.sa.add(perm_obj)
# now commit the changes, earlier so we are sure everything is in
# the database.
if commit_early:
self.sa.commit()
if not just_db:
self._create_group(new_repo_group.group_name)
# trigger the post hook
from rhodecode.lib.hooks_base import log_create_repository_group
repo_group = RepoGroup.get_by_group_name(group_name)
log_create_repository_group(
created_by=user.username, **repo_group.get_dict())
Martin Bornhold
events: Trigger repository group events (create/update/delete)
r557 # Trigger create event.
events.trigger(events.RepoGroupCreateEvent(repo_group))
project: added all source files and assets
r1 return new_repo_group
except Exception:
self.sa.rollback()
log.exception('Exception occurred when creating repository group, '
'doing cleanup...')
# rollback things manually !
repo_group = RepoGroup.get_by_group_name(group_name)
if repo_group:
RepoGroup.delete(repo_group.group_id)
self.sa.commit()
if cleanup_group:
RepoGroupModel()._delete_filesystem_group(repo_group)
raise
def update_permissions(
self, repo_group, perm_additions=None, perm_updates=None,
perm_deletions=None, recursive=None, check_perms=True,
cur_user=None):
from rhodecode.model.repo import RepoModel
from rhodecode.lib.auth import HasUserGroupPermissionAny
if not perm_additions:
perm_additions = []
if not perm_updates:
perm_updates = []
if not perm_deletions:
perm_deletions = []
req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
def _set_perm_user(obj, user, perm):
if isinstance(obj, RepoGroup):
self.grant_user_permission(
repo_group=obj, user=user, perm=perm)
elif isinstance(obj, Repository):
# private repos will not allow to change the default
# permissions using recursive mode
if obj.private and user == User.DEFAULT_USER:
return
# we set group permission but we have to switch to repo
# permission
perm = perm.replace('group.', 'repository.')
RepoModel().grant_user_permission(
repo=obj, user=user, perm=perm)
def _set_perm_group(obj, users_group, perm):
if isinstance(obj, RepoGroup):
self.grant_user_group_permission(
repo_group=obj, group_name=users_group, perm=perm)
elif isinstance(obj, Repository):
# we set group permission but we have to switch to repo
# permission
perm = perm.replace('group.', 'repository.')
RepoModel().grant_user_group_permission(
repo=obj, group_name=users_group, perm=perm)
def _revoke_perm_user(obj, user):
if isinstance(obj, RepoGroup):
self.revoke_user_permission(repo_group=obj, user=user)
elif isinstance(obj, Repository):
RepoModel().revoke_user_permission(repo=obj, user=user)
def _revoke_perm_group(obj, user_group):
if isinstance(obj, RepoGroup):
self.revoke_user_group_permission(
repo_group=obj, group_name=user_group)
elif isinstance(obj, Repository):
RepoModel().revoke_user_group_permission(
repo=obj, group_name=user_group)
# start updates
updates = []
log.debug('Now updating permissions for %s in recursive mode:%s',
repo_group, recursive)
# initialize check function, we'll call that multiple times
has_group_perm = HasUserGroupPermissionAny(*req_perms)
for obj in repo_group.recursive_groups_and_repos():
# iterated obj is an instance of a repos group or repository in
# that group, recursive option can be: none, repos, groups, all
if recursive == 'all':
obj = obj
elif recursive == 'repos':
# skip groups, other than this one
if isinstance(obj, RepoGroup) and not obj == repo_group:
continue
elif recursive == 'groups':
# skip repos
if isinstance(obj, Repository):
continue
else: # recursive == 'none':
# DEFAULT option - don't apply to iterated objects
# also we do a break at the end of this loop. if we are not
# in recursive mode
obj = repo_group
# update permissions
for member_id, perm, member_type in perm_updates:
member_id = int(member_id)
if member_type == 'user':
# this updates also current one if found
_set_perm_user(obj, user=member_id, perm=perm)
else: # set for user group
member_name = UserGroup.get(member_id).users_group_name
if not check_perms or has_group_perm(member_name,
user=cur_user):
_set_perm_group(obj, users_group=member_id, perm=perm)
# set new permissions
for member_id, perm, member_type in perm_additions:
member_id = int(member_id)
if member_type == 'user':
_set_perm_user(obj, user=member_id, perm=perm)
else: # set for user group
# check if we have permissions to alter this usergroup
member_name = UserGroup.get(member_id).users_group_name
if not check_perms or has_group_perm(member_name,
user=cur_user):
_set_perm_group(obj, users_group=member_id, perm=perm)
# delete permissions
for member_id, perm, member_type in perm_deletions:
member_id = int(member_id)
if member_type == 'user':
_revoke_perm_user(obj, user=member_id)
else: # set for user group
# check if we have permissions to alter this usergroup
member_name = UserGroup.get(member_id).users_group_name
if not check_perms or has_group_perm(member_name,
user=cur_user):
_revoke_perm_group(obj, user_group=member_id)
updates.append(obj)
# if it's not recursive call for all,repos,groups
# break the loop and don't proceed with other changes
if recursive not in ['all', 'repos', 'groups']:
break
return updates
def update(self, repo_group, form_data):
try:
repo_group = self._get_repo_group(repo_group)
old_path = repo_group.full_path
# change properties
if 'group_description' in form_data:
repo_group.group_description = form_data['group_description']
if 'enable_locking' in form_data:
repo_group.enable_locking = form_data['enable_locking']
if 'group_parent_id' in form_data:
parent_group = (
self._get_repo_group(form_data['group_parent_id']))
repo_group.group_parent_id = (
parent_group.group_id if parent_group else None)
repo_group.parent_group = parent_group
# mikhail: to update the full_path, we have to explicitly
# update group_name
group_name = form_data.get('group_name', repo_group.name)
repo_group.group_name = repo_group.get_new_name(group_name)
new_path = repo_group.full_path
if 'user' in form_data:
repo_group.user = User.get_by_username(form_data['user'])
self.sa.add(repo_group)
# iterate over all members of this groups and do fixes
# set locking if given
# if obj is a repoGroup also fix the name of the group according
# to the parent
# if obj is a Repo fix it's name
# this can be potentially heavy operation
for obj in repo_group.recursive_groups_and_repos():
# set the value from it's parent
obj.enable_locking = repo_group.enable_locking
if isinstance(obj, RepoGroup):
new_name = obj.get_new_name(obj.name)
log.debug('Fixing group %s to new name %s',
obj.group_name, new_name)
obj.group_name = new_name
elif isinstance(obj, Repository):
# we need to get all repositories from this new group and
# rename them accordingly to new group path
new_name = obj.get_new_name(obj.just_name)
log.debug('Fixing repo %s to new name %s',
obj.repo_name, new_name)
obj.repo_name = new_name
self.sa.add(obj)
self._rename_group(old_path, new_path)
Martin Bornhold
events: Trigger repository group events (create/update/delete)
r557 # Trigger update event.
events.trigger(events.RepoGroupUpdateEvent(repo_group))
project: added all source files and assets
r1 return repo_group
except Exception:
log.error(traceback.format_exc())
raise
def delete(self, repo_group, force_delete=False, fs_remove=True):
repo_group = self._get_repo_group(repo_group)
dan
integrations: refactor/cleanup + features, fixes #4181...
r731 if not repo_group:
return False
project: added all source files and assets
r1 try:
self.sa.delete(repo_group)
if fs_remove:
self._delete_filesystem_group(repo_group, force_delete)
else:
log.debug('skipping removal from filesystem')
Martin Bornhold
events: Trigger repository group events (create/update/delete)
r557 # Trigger delete event.
events.trigger(events.RepoGroupDeleteEvent(repo_group))
dan
integrations: refactor/cleanup + features, fixes #4181...
r731 return True
Martin Bornhold
events: Trigger repository group events (create/update/delete)
r557
project: added all source files and assets
r1 except Exception:
log.error('Error removing repo_group %s', repo_group)
raise
def grant_user_permission(self, repo_group, user, perm):
"""
Grant permission for user on given repository group, or update
existing one if found
:param repo_group: Instance of RepoGroup, repositories_group_id,
or repositories_group name
:param user: Instance of User, user_id or username
:param perm: Instance of Permission, or permission_name
"""
repo_group = self._get_repo_group(repo_group)
user = self._get_user(user)
permission = self._get_perm(perm)
# check if we have that permission already
obj = self.sa.query(UserRepoGroupToPerm)\
.filter(UserRepoGroupToPerm.user == user)\
.filter(UserRepoGroupToPerm.group == repo_group)\
.scalar()
if obj is None:
# create new !
obj = UserRepoGroupToPerm()
obj.group = repo_group
obj.user = user
obj.permission = permission
self.sa.add(obj)
log.debug('Granted perm %s to %s on %s', perm, user, repo_group)
action_logger_generic(
'granted permission: {} to user: {} on repogroup: {}'.format(
perm, user, repo_group), namespace='security.repogroup')
return obj
def revoke_user_permission(self, repo_group, user):
"""
Revoke permission for user on given repository group
:param repo_group: Instance of RepoGroup, repositories_group_id,
or repositories_group name
:param user: Instance of User, user_id or username
"""
repo_group = self._get_repo_group(repo_group)
user = self._get_user(user)
obj = self.sa.query(UserRepoGroupToPerm)\
.filter(UserRepoGroupToPerm.user == user)\
.filter(UserRepoGroupToPerm.group == repo_group)\
.scalar()
if obj:
self.sa.delete(obj)
log.debug('Revoked perm on %s on %s', repo_group, user)
action_logger_generic(
'revoked permission from user: {} on repogroup: {}'.format(
user, repo_group), namespace='security.repogroup')
def grant_user_group_permission(self, repo_group, group_name, perm):
"""
Grant permission for user group on given repository group, or update
existing one if found
:param repo_group: Instance of RepoGroup, repositories_group_id,
or repositories_group name
:param group_name: Instance of UserGroup, users_group_id,
or user group name
:param perm: Instance of Permission, or permission_name
"""
repo_group = self._get_repo_group(repo_group)
group_name = self._get_user_group(group_name)
permission = self._get_perm(perm)
# check if we have that permission already
obj = self.sa.query(UserGroupRepoGroupToPerm)\
.filter(UserGroupRepoGroupToPerm.group == repo_group)\
.filter(UserGroupRepoGroupToPerm.users_group == group_name)\
.scalar()
if obj is None:
# create new
obj = UserGroupRepoGroupToPerm()
obj.group = repo_group
obj.users_group = group_name
obj.permission = permission
self.sa.add(obj)
log.debug('Granted perm %s to %s on %s', perm, group_name, repo_group)
action_logger_generic(
'granted permission: {} to usergroup: {} on repogroup: {}'.format(
perm, group_name, repo_group), namespace='security.repogroup')
return obj
def revoke_user_group_permission(self, repo_group, group_name):
"""
Revoke permission for user group on given repository group
:param repo_group: Instance of RepoGroup, repositories_group_id,
or repositories_group name
:param group_name: Instance of UserGroup, users_group_id,
or user group name
"""
repo_group = self._get_repo_group(repo_group)
group_name = self._get_user_group(group_name)
obj = self.sa.query(UserGroupRepoGroupToPerm)\
.filter(UserGroupRepoGroupToPerm.group == repo_group)\
.filter(UserGroupRepoGroupToPerm.users_group == group_name)\
.scalar()
if obj:
self.sa.delete(obj)
log.debug('Revoked perm to %s on %s', repo_group, group_name)
action_logger_generic(
'revoked permission from usergroup: {} on repogroup: {}'.format(
group_name, repo_group), namespace='security.repogroup')
def get_repo_groups_as_dict(self, repo_group_list=None, admin=False,
super_user_actions=False):
from rhodecode.lib.utils import PartialRenderer
templating: use .mako as extensions for template files.
r1282 _render = PartialRenderer('data_table/_dt_elements.mako')
project: added all source files and assets
r1 c = _render.c
h = _render.h
def quick_menu(repo_group_name):
return _render('quick_repo_group_menu', repo_group_name)
def repo_group_lnk(repo_group_name):
return _render('repo_group_name', repo_group_name)
meta-tags: fixes #4305, metatags are not taken into account when truncating output....
r1102 def desc(desc, personal):
prefix = h.escaped_stylize(u'[personal] ') if personal else ''
project: added all source files and assets
r1 if c.visual.stylify_metatags:
meta-tags: fixes #4305, metatags are not taken into account when truncating output....
r1102 desc = h.urlify_text(prefix + h.escaped_stylize(desc))
project: added all source files and assets
r1 else:
meta-tags: fixes #4305, metatags are not taken into account when truncating output....
r1102 desc = h.urlify_text(prefix + h.html_escape(desc))
return _render('repo_group_desc', desc)
project: added all source files and assets
r1
def repo_group_actions(repo_group_id, repo_group_name, gr_count):
return _render(
'repo_group_actions', repo_group_id, repo_group_name, gr_count)
def repo_group_name(repo_group_name, children_groups):
return _render("repo_group_name", repo_group_name, children_groups)
def user_profile(username):
return _render('user_profile', username)
repo_group_data = []
for group in repo_group_list:
row = {
"menu": quick_menu(group.group_name),
"name": repo_group_lnk(group.group_name),
"name_raw": group.group_name,
meta-tags: fixes #4305, metatags are not taken into account when truncating output....
r1102 "desc": desc(group.group_description, group.personal),
project: added all source files and assets
r1 "top_level_repos": 0,
"owner": user_profile(group.user.username)
}
if admin:
repo_count = group.repositories.count()
children_groups = map(
h.safe_unicode,
itertools.chain((g.name for g in group.parents),
(x.name for x in [group])))
row.update({
"action": repo_group_actions(
group.group_id, group.group_name, repo_count),
"top_level_repos": repo_count,
"name": repo_group_name(group.group_name, children_groups),
})
repo_group_data.append(row)
return repo_group_data