diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py --- a/rhodecode/config/routing.py +++ b/rhodecode/config/routing.py @@ -54,7 +54,6 @@ def make_map(config): :param match_dict: """ repos_group_name = match_dict.get('group_name') - return is_valid_repos_group(repos_group_name, config['base_path']) def check_int(environ, match_dict): @@ -158,33 +157,33 @@ def make_map(config): action="new", conditions=dict(method=["GET"])) m.connect("formatted_new_repos_group", "/repos_groups/new.{format}", action="new", conditions=dict(method=["GET"])) - m.connect("update_repos_group", "/repos_groups/{id}", + m.connect("update_repos_group", "/repos_groups/{group_name:.*?}", action="update", conditions=dict(method=["PUT"], - function=check_int)) - m.connect("delete_repos_group", "/repos_groups/{id}", + function=check_group)) + m.connect("delete_repos_group", "/repos_groups/{group_name:.*?}", action="delete", conditions=dict(method=["DELETE"], - function=check_int)) - m.connect("edit_repos_group", "/repos_groups/{id:.*?}/edit", + function=check_group)) + m.connect("edit_repos_group", "/repos_groups/{group_name:.*?}/edit", action="edit", conditions=dict(method=["GET"],)) m.connect("formatted_edit_repos_group", - "/repos_groups/{id}.{format}/edit", + "/repos_groups/{group_name:.*?}.{format}/edit", action="edit", conditions=dict(method=["GET"], - function=check_int)) - m.connect("repos_group", "/repos_groups/{id}", + function=check_group)) + m.connect("repos_group", "/repos_groups/{group_name:.*?}", action="show", conditions=dict(method=["GET"], - function=check_int)) - m.connect("formatted_repos_group", "/repos_groups/{id}.{format}", + function=check_group)) + m.connect("formatted_repos_group", "/repos_groups/{group_name:.*?}.{format}", action="show", conditions=dict(method=["GET"], - function=check_int)) + function=check_group)) # ajax delete repos group perm user m.connect('delete_repos_group_user_perm', - "/delete_repos_group_user_perm/{group_name:.*}", + "/delete_repos_group_user_perm/{group_name:.*?}", action="delete_repos_group_user_perm", conditions=dict(method=["DELETE"], function=check_group)) # ajax delete repos group perm users_group m.connect('delete_repos_group_users_group_perm', - "/delete_repos_group_users_group_perm/{group_name:.*}", + "/delete_repos_group_users_group_perm/{group_name:.*?}", action="delete_repos_group_users_group_perm", conditions=dict(method=["DELETE"], function=check_group)) diff --git a/rhodecode/controllers/admin/repos_groups.py b/rhodecode/controllers/admin/repos_groups.py --- a/rhodecode/controllers/admin/repos_groups.py +++ b/rhodecode/controllers/admin/repos_groups.py @@ -30,7 +30,7 @@ import formencode from formencode import htmlfill from pylons import request, tmpl_context as c, url -from pylons.controllers.util import redirect +from pylons.controllers.util import abort, redirect from pylons.i18n.translation import _ from sqlalchemy.exc import IntegrityError @@ -39,7 +39,8 @@ import rhodecode from rhodecode.lib import helpers as h from rhodecode.lib.ext_json import json from rhodecode.lib.auth import LoginRequired, HasPermissionAnyDecorator,\ - HasReposGroupPermissionAnyDecorator + HasReposGroupPermissionAnyDecorator, HasReposGroupPermissionAll,\ + HasPermissionAll from rhodecode.lib.base import BaseController, render from rhodecode.model.db import RepoGroup, Repository from rhodecode.model.repos_group import ReposGroupModel @@ -47,8 +48,9 @@ from rhodecode.model.forms import ReposG from rhodecode.model.meta import Session from rhodecode.model.repo import RepoModel from webob.exc import HTTPInternalServerError, HTTPNotFound -from rhodecode.lib.utils2 import str2bool +from rhodecode.lib.utils2 import str2bool, safe_int from sqlalchemy.sql.expression import func +from rhodecode.model.scm import GroupList log = logging.getLogger(__name__) @@ -63,10 +65,21 @@ class ReposGroupsController(BaseControll def __before__(self): super(ReposGroupsController, self).__before__() - def __load_defaults(self): - c.repo_groups = RepoGroup.groups_choices() + def __load_defaults(self, allow_empty_group=False, exclude_group_ids=[]): + if HasPermissionAll('hg.admin')('group edit'): + #we're global admin, we're ok and we can create TOP level groups + allow_empty_group = True + + #override the choices for this form, we need to filter choices + #and display only those we have ADMIN right + groups_with_admin_rights = GroupList(RepoGroup.query().all(), + perm_set=['group.admin']) + c.repo_groups = RepoGroup.groups_choices(groups=groups_with_admin_rights, + show_empty_group=allow_empty_group) + # exclude filtered ids + c.repo_groups = filter(lambda x: x[0] not in exclude_group_ids, + c.repo_groups) c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups) - repo_model = RepoModel() c.users_array = repo_model.get_users_js() c.users_groups_array = repo_model.get_users_groups_js() @@ -77,7 +90,6 @@ class ReposGroupsController(BaseControll :param group_id: """ - self.__load_defaults() repo_group = RepoGroup.get_or_404(group_id) data = repo_group.get_dict() data['group_name'] = repo_group.name @@ -94,34 +106,37 @@ class ReposGroupsController(BaseControll return data - @HasPermissionAnyDecorator('hg.admin') def index(self, format='html'): """GET /repos_groups: All items in the collection""" # url('repos_groups') + group_iter = GroupList(RepoGroup.query().all(), perm_set=['group.admin']) sk = lambda g: g.parents[0].group_name if g.parents else g.group_name - c.groups = sorted(RepoGroup.query().all(), key=sk) + c.groups = sorted(group_iter, key=sk) return render('admin/repos_groups/repos_groups_show.html') - @HasPermissionAnyDecorator('hg.admin') def create(self): """POST /repos_groups: Create a new item""" # url('repos_groups') + self.__load_defaults() - repos_group_form = ReposGroupForm(available_groups = - c.repo_groups_choices)() + + # permissions for can create group based on parent_id are checked + # here in the Form + repos_group_form = ReposGroupForm(available_groups= + map(lambda k: unicode(k[0]), c.repo_groups))() try: form_result = repos_group_form.to_python(dict(request.POST)) ReposGroupModel().create( group_name=form_result['group_name'], group_description=form_result['group_description'], - parent=form_result['group_parent_id'] + parent=form_result['group_parent_id'], + owner=self.rhodecode_user.user_id ) Session().commit() h.flash(_('created repos group %s') \ % form_result['group_name'], category='success') #TODO: in futureaction_logger(, '', '', '', self.sa) except formencode.Invalid, errors: - return htmlfill.render( render('admin/repos_groups/repos_groups_add.html'), defaults=errors.value, @@ -132,40 +147,65 @@ class ReposGroupsController(BaseControll log.error(traceback.format_exc()) h.flash(_('error occurred during creation of repos group %s') \ % request.POST.get('group_name'), category='error') + parent_group_id = form_result['group_parent_id'] + #TODO: maybe we should get back to the main view, not the admin one + return redirect(url('repos_groups', parent_group=parent_group_id)) - return redirect(url('repos_groups')) - - @HasPermissionAnyDecorator('hg.admin') def new(self, format='html'): """GET /repos_groups/new: Form to create a new item""" # url('new_repos_group') + if HasPermissionAll('hg.admin')('group create'): + #we're global admin, we're ok and we can create TOP level groups + pass + else: + # we pass in parent group into creation form, thus we know + # what would be the group, we can check perms here ! + group_id = safe_int(request.GET.get('parent_group')) + group = RepoGroup.get(group_id) if group_id else None + group_name = group.group_name if group else None + if HasReposGroupPermissionAll('group.admin')(group_name, 'group create'): + pass + else: + return abort(403) + self.__load_defaults() return render('admin/repos_groups/repos_groups_add.html') - @HasPermissionAnyDecorator('hg.admin') - def update(self, id): - """PUT /repos_groups/id: Update an existing item""" + @HasReposGroupPermissionAnyDecorator('group.admin') + def update(self, group_name): + """PUT /repos_groups/group_name: Update an existing item""" # Forms posted to this method should contain a hidden field: # # Or using helpers: - # h.form(url('repos_group', id=ID), + # h.form(url('repos_group', group_name=GROUP_NAME), # method='put') - # url('repos_group', id=ID) + # url('repos_group', group_name=GROUP_NAME) - self.__load_defaults() - c.repos_group = RepoGroup.get(id) + c.repos_group = ReposGroupModel()._get_repos_group(group_name) + if HasPermissionAll('hg.admin')('group edit'): + #we're global admin, we're ok and we can create TOP level groups + allow_empty_group = True + elif not c.repos_group.parent_group: + allow_empty_group = True + else: + allow_empty_group = False + self.__load_defaults(allow_empty_group=allow_empty_group, + exclude_group_ids=[c.repos_group.group_id]) repos_group_form = ReposGroupForm( edit=True, old_data=c.repos_group.get_dict(), - available_groups=c.repo_groups_choices + available_groups=c.repo_groups_choices, + can_create_in_root=allow_empty_group, )() try: form_result = repos_group_form.to_python(dict(request.POST)) - ReposGroupModel().update(id, form_result) + new_gr = ReposGroupModel().update(group_name, form_result) Session().commit() h.flash(_('updated repos group %s') \ % form_result['group_name'], category='success') + # we now have new name ! + group_name = new_gr.group_name #TODO: in future action_logger(, '', '', '', self.sa) except formencode.Invalid, errors: @@ -180,19 +220,19 @@ class ReposGroupsController(BaseControll h.flash(_('error occurred during update of repos group %s') \ % request.POST.get('group_name'), category='error') - return redirect(url('edit_repos_group', id=id)) + return redirect(url('edit_repos_group', group_name=group_name)) - @HasPermissionAnyDecorator('hg.admin') - def delete(self, id): - """DELETE /repos_groups/id: Delete an existing item""" + @HasReposGroupPermissionAnyDecorator('group.admin') + def delete(self, group_name): + """DELETE /repos_groups/group_name: Delete an existing item""" # Forms posted to this method should contain a hidden field: # # Or using helpers: - # h.form(url('repos_group', id=ID), + # h.form(url('repos_group', group_name=GROUP_NAME), # method='delete') - # url('repos_group', id=ID) + # url('repos_group', group_name=GROUP_NAME) - gr = RepoGroup.get(id) + gr = c.repos_group = ReposGroupModel()._get_repos_group(group_name) repos = gr.repositories.all() if repos: h.flash(_('This group contains %s repositores and cannot be ' @@ -201,7 +241,7 @@ class ReposGroupsController(BaseControll return redirect(url('repos_groups')) try: - ReposGroupModel().delete(id) + ReposGroupModel().delete(group_name) Session().commit() h.flash(_('removed repos group %s') % gr.group_name, category='success') @@ -279,11 +319,11 @@ class ReposGroupsController(BaseControll @HasReposGroupPermissionAnyDecorator('group.read', 'group.write', 'group.admin') - def show(self, id, format='html'): - """GET /repos_groups/id: Show a specific item""" - # url('repos_group', id=ID) + def show(self, group_name, format='html'): + """GET /repos_groups/group_name: Show a specific item""" + # url('repos_group', group_name=GROUP_NAME) - c.group = RepoGroup.get_or_404(id) + c.group = c.repos_group = ReposGroupModel()._get_repos_group(group_name) c.group_repos = c.group.repositories.all() #overwrite our cached list with current filter @@ -291,7 +331,7 @@ class ReposGroupsController(BaseControll c.repo_cnt = 0 groups = RepoGroup.query().order_by(RepoGroup.group_name)\ - .filter(RepoGroup.group_parent_id == id).all() + .filter(RepoGroup.group_parent_id == c.group.group_id).all() c.groups = self.scm_model.get_repos_groups(groups) if c.visual.lightweight_dashboard is False: @@ -299,7 +339,7 @@ class ReposGroupsController(BaseControll ## lightweight version of dashboard else: c.repos_list = Repository.query()\ - .filter(Repository.group_id == id)\ + .filter(Repository.group_id == c.group.group_id)\ .order_by(func.lower(Repository.repo_name))\ .all() @@ -310,17 +350,25 @@ class ReposGroupsController(BaseControll return render('admin/repos_groups/repos_groups.html') - @HasPermissionAnyDecorator('hg.admin') - def edit(self, id, format='html'): - """GET /repos_groups/id/edit: Form to edit an existing item""" - # url('edit_repos_group', id=ID) + @HasReposGroupPermissionAnyDecorator('group.admin') + def edit(self, group_name, format='html'): + """GET /repos_groups/group_name/edit: Form to edit an existing item""" + # url('edit_repos_group', group_name=GROUP_NAME) - c.repos_group = ReposGroupModel()._get_repos_group(id) - defaults = self.__load_data(c.repos_group.group_id) + c.repos_group = ReposGroupModel()._get_repos_group(group_name) + #we can only allow moving empty group if it's already a top-level + #group, ie has no parents, or we're admin + if HasPermissionAll('hg.admin')('group edit'): + #we're global admin, we're ok and we can create TOP level groups + allow_empty_group = True + elif not c.repos_group.parent_group: + allow_empty_group = True + else: + allow_empty_group = False - # we need to exclude this group from the group list for editing - c.repo_groups = filter(lambda x: x[0] != c.repos_group.group_id, - c.repo_groups) + self.__load_defaults(allow_empty_group=allow_empty_group, + exclude_group_ids=[c.repos_group.group_id]) + defaults = self.__load_data(c.repos_group.group_id) return htmlfill.render( render('admin/repos_groups/repos_groups_edit.html'), diff --git a/rhodecode/lib/auth.py b/rhodecode/lib/auth.py --- a/rhodecode/lib/auth.py +++ b/rhodecode/lib/auth.py @@ -659,7 +659,6 @@ class HasRepoPermissionAnyDecorator(Perm def check_permissions(self): repo_name = get_repo_slug(request) - try: user_perms = set([self.user_perms['repositories'][repo_name]]) except KeyError: @@ -682,6 +681,7 @@ class HasReposGroupPermissionAllDecorato user_perms = set([self.user_perms['repositories_groups'][group_name]]) except KeyError: return False + if self.required_perms.issubset(user_perms): return True return False @@ -695,11 +695,11 @@ class HasReposGroupPermissionAnyDecorato def check_permissions(self): group_name = get_repos_group_slug(request) - try: user_perms = set([self.user_perms['repositories_groups'][group_name]]) except KeyError: return False + if self.required_perms.intersection(user_perms): return True return False diff --git a/rhodecode/lib/helpers.py b/rhodecode/lib/helpers.py --- a/rhodecode/lib/helpers.py +++ b/rhodecode/lib/helpers.py @@ -741,7 +741,8 @@ def action_parser(user_log, feed=False, # PERMS #============================================================================== from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \ -HasRepoPermissionAny, HasRepoPermissionAll +HasRepoPermissionAny, HasRepoPermissionAll, HasReposGroupPermissionAll, \ +HasReposGroupPermissionAny #============================================================================== diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -1172,15 +1172,18 @@ class RepoGroup(Base, BaseModel): self.group_name) @classmethod - def groups_choices(cls, check_perms=False): + def groups_choices(cls, groups=None, check_perms=False, show_empty_group=True): from webhelpers.html import literal as _literal from rhodecode.model.scm import ScmModel - groups = cls.query().all() + if not groups: + groups = cls.query().all() if check_perms: #filter group user have access to, it's done #magically inside ScmModel based on current user groups = ScmModel().get_repos_groups(groups) - repo_groups = [('', '')] + repo_groups = [] + if show_empty_group: + repo_groups = [('-1', '-- no parent --')] sep = ' » ' _name = lambda k: _literal(sep.join(k)) diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py --- a/rhodecode/model/forms.py +++ b/rhodecode/model/forms.py @@ -115,7 +115,8 @@ def UsersGroupForm(edit=False, old_data= return _UsersGroupForm -def ReposGroupForm(edit=False, old_data={}, available_groups=[]): +def ReposGroupForm(edit=False, old_data={}, available_groups=[], + can_create_in_root=False): class _ReposGroupForm(formencode.Schema): allow_extra_fields = True filter_extra_fields = False @@ -123,10 +124,15 @@ def ReposGroupForm(edit=False, old_data= group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True), v.SlugifyName()) group_description = v.UnicodeString(strip=True, min=1, - not_empty=True) - group_parent_id = v.OneOf(available_groups, hideList=False, - testValueList=True, - if_missing=None, not_empty=False) + not_empty=False) + if edit: + #FIXME: do a special check that we cannot move a group to one of + #it's children + pass + group_parent_id = All(v.CanCreateGroup(can_create_in_root), + v.OneOf(available_groups, hideList=False, + testValueList=True, + if_missing=None, not_empty=True)) enable_locking = v.StringBoolean(if_missing=False) recursive = v.StringBoolean(if_missing=False) chained_validators = [v.ValidReposGroup(edit, old_data), diff --git a/rhodecode/model/repos_group.py b/rhodecode/model/repos_group.py --- a/rhodecode/model/repos_group.py +++ b/rhodecode/model/repos_group.py @@ -140,16 +140,21 @@ class ReposGroupModel(BaseModel): group.name) shutil.move(rm_path, os.path.join(self.repos_path, _d)) - def create(self, group_name, group_description, parent=None, just_db=False): + def create(self, group_name, group_description, owner, parent=None, just_db=False): try: new_repos_group = RepoGroup() - new_repos_group.group_description = group_description + new_repos_group.group_description = group_description or group_name new_repos_group.parent_group = self._get_repos_group(parent) new_repos_group.group_name = new_repos_group.get_new_name(group_name) self.sa.add(new_repos_group) self._create_default_perms(new_repos_group) + #create an ADMIN permission for owner, later owner should go into + #the owner field of groups + self.grant_user_permission(repos_group=new_repos_group, + user=owner, perm='group.admin') + if not just_db: # we need to flush here, in order to check if database won't # throw any exceptions, create filesystem dirs at the very end @@ -229,10 +234,10 @@ class ReposGroupModel(BaseModel): break return updates - def update(self, repos_group_id, form_data): + def update(self, repos_group, form_data): try: - repos_group = RepoGroup.get(repos_group_id) + repos_group = self._get_repos_group(repos_group) recursive = form_data['recursive'] # iterate over all members(if in recursive mode) of this groups and # set the permissions ! diff --git a/rhodecode/model/scm.py b/rhodecode/model/scm.py --- a/rhodecode/model/scm.py +++ b/rhodecode/model/scm.py @@ -77,11 +77,15 @@ class CachedRepoList(object): super fast """ - def __init__(self, db_repo_list, repos_path, order_by=None): + def __init__(self, db_repo_list, repos_path, order_by=None, perm_set=None): self.db_repo_list = db_repo_list self.repos_path = repos_path self.order_by = order_by self.reversed = (order_by or '').startswith('-') + if not perm_set: + perm_set = ['repository.read', 'repository.write', + 'repository.admin'] + self.perm_set = perm_set def __len__(self): return len(self.db_repo_list) @@ -98,7 +102,7 @@ class CachedRepoList(object): scmr = dbr.scm_instance_cached(cache_map) # check permission at this level if not HasRepoPermissionAny( - 'repository.read', 'repository.write', 'repository.admin' + *self.perm_set )(dbr.repo_name, 'get repo check'): continue @@ -143,7 +147,7 @@ class SimpleCachedRepoList(CachedRepoLis for dbr in self.db_repo_list: # check permission at this level if not HasRepoPermissionAny( - 'repository.read', 'repository.write', 'repository.admin' + *self.perm_set )(dbr.repo_name, 'get repo check'): continue @@ -160,8 +164,18 @@ class SimpleCachedRepoList(CachedRepoLis class GroupList(object): - def __init__(self, db_repo_group_list): + def __init__(self, db_repo_group_list, perm_set=None): + """ + Creates iterator from given list of group objects, additionally + checking permission for them from perm_set var + + :param db_repo_group_list: + :param perm_set: list of permissons to check + """ self.db_repo_group_list = db_repo_group_list + if not perm_set: + perm_set = ['group.read', 'group.write', 'group.admin'] + self.perm_set = perm_set def __len__(self): return len(self.db_repo_group_list) @@ -173,7 +187,7 @@ class GroupList(object): for dbgr in self.db_repo_group_list: # check permission at this level if not HasReposGroupPermissionAny( - 'group.read', 'group.write', 'group.admin' + *self.perm_set )(dbgr.group_name, 'get group repo check'): continue diff --git a/rhodecode/model/validators.py b/rhodecode/model/validators.py --- a/rhodecode/model/validators.py +++ b/rhodecode/model/validators.py @@ -475,11 +475,19 @@ def CanWriteGroup(): "to create repository in this group") } + def to_python(self, value, state): + #root location + if value in [-1, "-1"]: + return None + return value + def validate_python(self, value, state): gr = RepoGroup.get(value) - if not HasReposGroupPermissionAny( - 'group.write', 'group.admin' - )(gr.group_name, 'get group of repo form'): + gr_name = gr.group_name if gr else None # None means ROOT location + val = HasReposGroupPermissionAny('group.write', 'group.admin') + forbidden = not val(gr_name, 'can write into group validator') + #parent group need to be existing + if gr and forbidden: msg = M(self, 'permission_denied', state) raise formencode.Invalid(msg, value, state, error_dict=dict(repo_type=msg) @@ -487,6 +495,46 @@ def CanWriteGroup(): return _validator +def CanCreateGroup(can_create_in_root=False): + class _validator(formencode.validators.FancyValidator): + messages = { + 'permission_denied': _(u"You don't have permissions " + "to create a group in this location") + } + + def to_python(self, value, state): + #root location + if value in [-1, "-1"]: + return None + return value + + def validate_python(self, value, state): + #TODO: REMOVE THIS !! + ################################ + import ipdb;ipdb.set_trace() + print 'setting ipdb debuggin for rhodecode.model.validators._validator.validate_python' + ################################ + + + gr = RepoGroup.get(value) + gr_name = gr.group_name if gr else None # None means ROOT location + + if can_create_in_root and gr is None: + #we can create in root, we're fine no validations required + return + + forbidden_in_root = gr is None and can_create_in_root is False + val = HasReposGroupPermissionAny('group.admin') + forbidden = not val(gr_name, 'can create group validator') + if forbidden_in_root or forbidden: + msg = M(self, 'permission_denied', state) + raise formencode.Invalid(msg, value, state, + error_dict=dict(group_parent_id=msg) + ) + + return _validator + + def ValidPerms(type_='repo'): if type_ == 'group': EMPTY_PERM = 'group.none' diff --git a/rhodecode/public/css/style.css b/rhodecode/public/css/style.css --- a/rhodecode/public/css/style.css +++ b/rhodecode/public/css/style.css @@ -4631,7 +4631,7 @@ PULL REQUESTS } #perms .perm_tag.write{ - background-color: #B94A48; + background-color: #DB7525; color: #ffffff; } diff --git a/rhodecode/templates/admin/permissions/permissions.html b/rhodecode/templates/admin/permissions/permissions.html --- a/rhodecode/templates/admin/permissions/permissions.html +++ b/rhodecode/templates/admin/permissions/permissions.html @@ -148,7 +148,7 @@ %if section == 'repositories': ${_('edit')} %elif section == 'repositories_groups': - ${_('edit')} + ${_('edit')} %else: -- %endif diff --git a/rhodecode/templates/admin/repos_groups/repos_groups_edit.html b/rhodecode/templates/admin/repos_groups/repos_groups_edit.html --- a/rhodecode/templates/admin/repos_groups/repos_groups_edit.html +++ b/rhodecode/templates/admin/repos_groups/repos_groups_edit.html @@ -28,7 +28,7 @@ - ${h.form(url('repos_group',id=c.repos_group.group_id),method='put')} + ${h.form(url('repos_group',group_name=c.repos_group.group_name),method='put')}