changeset 3222:b4daef4cc26d beta

Group management delegation: this allows users to become an admin of groups a group admin can manage a group, change it's name or permissions. A group admin can create child group only within a group he is an admin off. Top-level group can be only created by super admins
author Marcin Kuzminski <marcin@python-works.com>
date Fri, 25 Jan 2013 01:40:09 +0100
parents dd0ee9119aa9
children 74e455c06881
files rhodecode/config/routing.py rhodecode/controllers/admin/repos_groups.py rhodecode/lib/auth.py rhodecode/lib/helpers.py rhodecode/model/db.py rhodecode/model/forms.py rhodecode/model/repos_group.py rhodecode/model/scm.py rhodecode/model/validators.py rhodecode/public/css/style.css rhodecode/templates/admin/permissions/permissions.html rhodecode/templates/admin/repos_groups/repos_groups_edit.html rhodecode/templates/admin/repos_groups/repos_groups_show.html rhodecode/templates/admin/users/user_edit.html rhodecode/templates/admin/users_groups/users_group_edit.html rhodecode/templates/index_base.html rhodecode/tests/functional/test_admin_repos.py rhodecode/tests/functional/test_repos_groups.py rhodecode/tests/models/common.py rhodecode/tests/models/test_repos_groups.py rhodecode/tests/test_validators.py
diffstat 21 files changed, 273 insertions(+), 133 deletions(-) [+]
line wrap: on
line diff
--- a/rhodecode/config/routing.py	Fri Jan 25 00:30:25 2013 +0100
+++ b/rhodecode/config/routing.py	Fri Jan 25 01:40:09 2013 +0100
@@ -54,7 +54,6 @@
         :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 @@
                   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))
 
--- a/rhodecode/controllers/admin/repos_groups.py	Fri Jan 25 00:30:25 2013 +0100
+++ b/rhodecode/controllers/admin/repos_groups.py	Fri Jan 25 01:40:09 2013 +0100
@@ -30,7 +30,7 @@
 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 @@
 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.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 @@
     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 @@
 
         :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 @@
 
         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 @@
             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:
         #    <input type="hidden" name="_method" value="PUT" />
         # 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 @@
             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:
         #    <input type="hidden" name="_method" value="DELETE" />
         # 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 @@
             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 @@
 
     @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 @@
         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 @@
         ## 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 @@
 
         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'),
--- a/rhodecode/lib/auth.py	Fri Jan 25 00:30:25 2013 +0100
+++ b/rhodecode/lib/auth.py	Fri Jan 25 01:40:09 2013 +0100
@@ -659,7 +659,6 @@
 
     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 @@
             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 @@
 
     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
--- a/rhodecode/lib/helpers.py	Fri Jan 25 00:30:25 2013 +0100
+++ b/rhodecode/lib/helpers.py	Fri Jan 25 01:40:09 2013 +0100
@@ -741,7 +741,8 @@
 # PERMS
 #==============================================================================
 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
-HasRepoPermissionAny, HasRepoPermissionAll
+HasRepoPermissionAny, HasRepoPermissionAll, HasReposGroupPermissionAll, \
+HasReposGroupPermissionAny
 
 
 #==============================================================================
--- a/rhodecode/model/db.py	Fri Jan 25 00:30:25 2013 +0100
+++ b/rhodecode/model/db.py	Fri Jan 25 01:40:09 2013 +0100
@@ -1172,15 +1172,18 @@
                                   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 = ' &raquo; '
         _name = lambda k: _literal(sep.join(k))
 
--- a/rhodecode/model/forms.py	Fri Jan 25 00:30:25 2013 +0100
+++ b/rhodecode/model/forms.py	Fri Jan 25 01:40:09 2013 +0100
@@ -115,7 +115,8 @@
     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 @@
         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),
--- a/rhodecode/model/repos_group.py	Fri Jan 25 00:30:25 2013 +0100
+++ b/rhodecode/model/repos_group.py	Fri Jan 25 01:40:09 2013 +0100
@@ -140,16 +140,21 @@
                                           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 @@
                 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 !
--- a/rhodecode/model/scm.py	Fri Jan 25 00:30:25 2013 +0100
+++ b/rhodecode/model/scm.py	Fri Jan 25 01:40:09 2013 +0100
@@ -77,11 +77,15 @@
     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 @@
             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 @@
         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 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 @@
         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
 
--- a/rhodecode/model/validators.py	Fri Jan 25 00:30:25 2013 +0100
+++ b/rhodecode/model/validators.py	Fri Jan 25 01:40:09 2013 +0100
@@ -475,11 +475,19 @@
                                    "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 @@
     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'
--- a/rhodecode/public/css/style.css	Fri Jan 25 00:30:25 2013 +0100
+++ b/rhodecode/public/css/style.css	Fri Jan 25 01:40:09 2013 +0100
@@ -4631,7 +4631,7 @@
 }
 
 #perms .perm_tag.write{
-  background-color: #B94A48;
+  background-color: #DB7525;
   color: #ffffff;    
 }
 
--- a/rhodecode/templates/admin/permissions/permissions.html	Fri Jan 25 00:30:25 2013 +0100
+++ b/rhodecode/templates/admin/permissions/permissions.html	Fri Jan 25 01:40:09 2013 +0100
@@ -148,7 +148,7 @@
                             %if section == 'repositories':
                                 <a href="${h.url('edit_repo',repo_name=k,anchor='permissions_manage')}">${_('edit')}</a>
                             %elif section == 'repositories_groups':
-                                <a href="${h.url('edit_repos_group',id=k,anchor='permissions_manage')}">${_('edit')}</a>
+                                <a href="${h.url('edit_repos_group',group_name=k,anchor='permissions_manage')}">${_('edit')}</a>
                             %else:
                                 --
                             %endif
--- a/rhodecode/templates/admin/repos_groups/repos_groups_edit.html	Fri Jan 25 00:30:25 2013 +0100
+++ b/rhodecode/templates/admin/repos_groups/repos_groups_edit.html	Fri Jan 25 01:40:09 2013 +0100
@@ -28,7 +28,7 @@
         </ul>
     </div>
     <!-- end box / title -->
-    ${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')}
     <div class="form">
         <!-- fields -->
         <div class="fields">
--- a/rhodecode/templates/admin/repos_groups/repos_groups_show.html	Fri Jan 25 00:30:25 2013 +0100
+++ b/rhodecode/templates/admin/repos_groups/repos_groups_show.html	Fri Jan 25 01:40:09 2013 +0100
@@ -51,12 +51,12 @@
                       <td>${gr.group_description}</td>
                       <td><b>${gr_cn}</b></td>
                       <td>
-                       <a href="${h.url('edit_repos_group',id=gr.group_id)}" title="${_('edit')}">
+                       <a href="${h.url('edit_repos_group',group_name=gr.group_name)}" title="${_('edit')}">
                          ${h.submit('edit_%s' % gr.group_name,_('edit'),class_="edit_icon action_button")}
                        </a>
                       </td>
                       <td>
-                       ${h.form(url('repos_group', id=gr.group_id),method='delete')}
+                       ${h.form(url('repos_group', group_name=gr.group_name),method='delete')}
                          ${h.submit('remove_%s' % gr.name,_('delete'),class_="delete_icon action_button",onclick="return confirm('"+ungettext('Confirm to delete this group: %s with %s repository','Confirm to delete this group: %s with %s repositories',gr_cn) % (gr.name,gr_cn)+"');")}
                        ${h.end_form()}
                       </td>
--- a/rhodecode/templates/admin/users/user_edit.html	Fri Jan 25 00:30:25 2013 +0100
+++ b/rhodecode/templates/admin/users/user_edit.html	Fri Jan 25 01:40:09 2013 +0100
@@ -231,7 +231,7 @@
                             %if section == 'repositories':
                                 <a href="${h.url('edit_repo',repo_name=k,anchor='permissions_manage')}">${_('edit')}</a>
                             %elif section == 'repositories_groups':
-                                <a href="${h.url('edit_repos_group',id=k,anchor='permissions_manage')}">${_('edit')}</a>
+                                <a href="${h.url('edit_repos_group',group_name=k,anchor='permissions_manage')}">${_('edit')}</a>
                             %else:
                                 --
                             %endif
--- a/rhodecode/templates/admin/users_groups/users_group_edit.html	Fri Jan 25 00:30:25 2013 +0100
+++ b/rhodecode/templates/admin/users_groups/users_group_edit.html	Fri Jan 25 01:40:09 2013 +0100
@@ -206,7 +206,7 @@
                         %if section == 'repositories':
                             <a href="${h.url('edit_repo',repo_name=k,anchor='permissions_manage')}">${_('edit')}</a>
                         %elif section == 'repositories_groups':
-                            <a href="${h.url('edit_repos_group',id=k,anchor='permissions_manage')}">${_('edit')}</a>
+                            <a href="${h.url('edit_repos_group',group_name=k,anchor='permissions_manage')}">${_('edit')}</a>
                         %else:
                             --
                         %endif
--- a/rhodecode/templates/index_base.html	Fri Jan 25 00:30:25 2013 +0100
+++ b/rhodecode/templates/index_base.html	Fri Jan 25 01:40:09 2013 +0100
@@ -6,17 +6,22 @@
             <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" value="${_('quick filter...')}"/> ${parent.breadcrumbs()} <span id="repo_count">0</span> ${_('repositories')}
             </h5>
             %if c.rhodecode_user.username != 'default':
+              <ul class="links">
                 %if h.HasPermissionAny('hg.admin','hg.create.repository')():
-                <ul class="links">
                   <li>
                   %if c.group:
                     <span>${h.link_to(_('Add repository'),h.url('admin_settings_create_repository',parent_group=c.group.group_id))}</span>
                   %else:
-                    <span>${h.link_to(_('Add repository'),h.url('admin_settings_create_repository'))}</span>
+                    <span>${h.link_to(_('Add repository'),h.url('admin_settings_create_repository'))}</span>                        
                   %endif
                   </li>
-                </ul>
                 %endif
+                %if c.group and h.HasReposGroupPermissionAny('group.admin')(c.group.group_name):
+                <li>
+                    <span>${h.link_to(_('Edit group'),h.url('edit_repos_group',group_name=c.group.group_name), title=_('You have admin right to this group, and can edit it'))}</span>
+                </li>
+                %endif
+              </ul>
             %endif
         </div>
         <!-- end box / title -->
--- a/rhodecode/tests/functional/test_admin_repos.py	Fri Jan 25 00:30:25 2013 +0100
+++ b/rhodecode/tests/functional/test_admin_repos.py	Fri Jan 25 01:40:09 2013 +0100
@@ -90,7 +90,8 @@
         ## create GROUP
         group_name = 'sometest'
         gr = ReposGroupModel().create(group_name=group_name,
-                                      group_description='test',)
+                                      group_description='test',
+                                      owner=TEST_USER_ADMIN_LOGIN)
         self.Session().commit()
 
         repo_name = 'ingroup'
--- a/rhodecode/tests/functional/test_repos_groups.py	Fri Jan 25 00:30:25 2013 +0100
+++ b/rhodecode/tests/functional/test_repos_groups.py	Fri Jan 25 01:40:09 2013 +0100
@@ -1,43 +1,51 @@
 from rhodecode.tests import *
 
+
 class TestReposGroupsController(TestController):
 
     def test_index(self):
+        self.log_user()
         response = self.app.get(url('repos_groups'))
-        # Test response...
+        response.mustcontain('There are no repositories groups yet')
 
-    def test_index_as_xml(self):
-        response = self.app.get(url('formatted_repos_groups', format='xml'))
-
-    def test_create(self):
-        response = self.app.post(url('repos_groups'))
+#    def test_index_as_xml(self):
+#        response = self.app.get(url('formatted_repos_groups', format='xml'))
+#
+#    def test_create(self):
+#        response = self.app.post(url('repos_groups'))
 
     def test_new(self):
+        self.log_user()
         response = self.app.get(url('new_repos_group'))
 
-    def test_new_as_xml(self):
-        response = self.app.get(url('formatted_new_repos_group', format='xml'))
-
-    def test_update(self):
-        response = self.app.put(url('repos_group', id=1))
-
-    def test_update_browser_fakeout(self):
-        response = self.app.post(url('repos_group', id=1), params=dict(_method='put'))
-
-    def test_delete(self):
-        response = self.app.delete(url('repos_group', id=1))
-
-    def test_delete_browser_fakeout(self):
-        response = self.app.post(url('repos_group', id=1), params=dict(_method='delete'))
-
-    def test_show(self):
-        response = self.app.get(url('repos_group', id=1))
-
-    def test_show_as_xml(self):
-        response = self.app.get(url('formatted_repos_group', id=1, format='xml'))
-
-    def test_edit(self):
-        response = self.app.get(url('edit_repos_group', id=1))
-
-    def test_edit_as_xml(self):
-        response = self.app.get(url('formatted_edit_repos_group', id=1, format='xml'))
+    def test_new_by_regular_user(self):
+        self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
+        response = self.app.get(url('new_repos_group'), status=403)
+#
+#    def test_new_as_xml(self):
+#        response = self.app.get(url('formatted_new_repos_group', format='xml'))
+#
+#    def test_update(self):
+#        response = self.app.put(url('repos_group', group_name=1))
+#
+#    def test_update_browser_fakeout(self):
+#        response = self.app.post(url('repos_group', group_name=1), params=dict(_method='put'))
+#
+#    def test_delete(self):
+#        self.log_user()
+#        response = self.app.delete(url('repos_group', group_name=1))
+#
+#    def test_delete_browser_fakeout(self):
+#        response = self.app.post(url('repos_group', group_name=1), params=dict(_method='delete'))
+#
+#    def test_show(self):
+#        response = self.app.get(url('repos_group', group_name=1))
+#
+#    def test_show_as_xml(self):
+#        response = self.app.get(url('formatted_repos_group', group_name=1, format='xml'))
+#
+#    def test_edit(self):
+#        response = self.app.get(url('edit_repos_group', group_name=1))
+#
+#    def test_edit_as_xml(self):
+#        response = self.app.get(url('formatted_edit_repos_group', group_name=1, format='xml'))
--- a/rhodecode/tests/models/common.py	Fri Jan 25 00:30:25 2013 +0100
+++ b/rhodecode/tests/models/common.py	Fri Jan 25 01:40:09 2013 +0100
@@ -21,7 +21,7 @@
         return gr
     if isinstance(parent_id, RepoGroup):
         parent_id = parent_id.group_id
-    gr = ReposGroupModel().create(path, desc, parent_id)
+    gr = ReposGroupModel().create(path, desc, TEST_USER_ADMIN_LOGIN, parent_id)
     return gr
 
 
--- a/rhodecode/tests/models/test_repos_groups.py	Fri Jan 25 00:30:25 2013 +0100
+++ b/rhodecode/tests/models/test_repos_groups.py	Fri Jan 25 01:40:09 2013 +0100
@@ -17,7 +17,7 @@
         return gr
     if isinstance(parent_id, RepoGroup):
         parent_id = parent_id.group_id
-    gr = ReposGroupModel().create(path, desc, parent_id)
+    gr = ReposGroupModel().create(path, desc, TEST_USER_ADMIN_LOGIN, parent_id)
     return gr
 
 
--- a/rhodecode/tests/test_validators.py	Fri Jan 25 00:30:25 2013 +0100
+++ b/rhodecode/tests/test_validators.py	Fri Jan 25 01:40:09 2013 +0100
@@ -79,7 +79,8 @@
                           {'group_name': HG_REPO, })
         gr = model.create(group_name='test_gr', group_description='desc',
                           parent=None,
-                          just_db=True)
+                          just_db=True,
+                          owner=TEST_USER_ADMIN_LOGIN)
         self.assertRaises(formencode.Invalid,
                           validator.to_python, {'group_name': gr.group_name, })
 
@@ -150,7 +151,8 @@
 
         gr = ReposGroupModel().create(group_name='group_test',
                                       group_description='desc',
-                                      parent=None,)
+                                      parent=None,
+                                      owner=TEST_USER_ADMIN_LOGIN)
         self.assertRaises(formencode.Invalid,
                           validator.to_python, {'repo_name': gr.group_name})